Repository navigation
0.6.0
The theme of this release: the analysis surface refuses or flags what it cannot answer, instead of returning a plausible value.
A GBA bus answers every address. It rounds an unaligned load down, reads undecoded space as open bus, and discards a store to ROM — all correct for the CPU, and all indistinguishable from the value you actually asked for. That is fine for emulation and wrong for a debugger. The bus is unchanged; the scripting and debug-info surfaces on top of it now refuse.
All six packages move to 0.6.0 together.
Breaking changes
read16/read32throw on a misaligned address, naming the one the hardware would have read, and on space the bus decodes to nothing. They previously answered. UsereadBytes(address, size)for 1–4 bytes at any alignment.read32is now unsigned. It previously returned a signed 32-bit value, so a high bit came back negative.wait({ pc })is nowwait({ execution }).wait()also throws on an unrecognised condition, where it previously returned immediately — so a stale{ pc }in an untyped script was a silent no-op.
Reads that refuse
readBytes(address, size)— 1–4 bytes at any alignment, assembled byte by byte.readMember/writeMember— read or write a DWARFMemberLocationat a base address, for an instance no symbol names: one reached through a pointer, an array element, anything placed at run time.addressToSymbolreportsexact— whetherst_sizecovered the address, or whether containment was merely inferred from the next symbol's start. In a decomp ELF most symbols come from hand-written asm and carry no size, so a lookup landing kilobytes past a function still resolved to it with nothing in the answer to say so.
Writes, and bounds the DWARF states
The scripting surface had no write API at all, so scripts reached for the raw bus — which rounds a misaligned store down and silently discards a store to ROM.
write8/write16/write32/writeBytes(address, size, value)— new, carrying the same guards as the reads.writeVariable(path, value)— the write counterpart toreadVariable, merging a bitfield into its container without disturbing its neighbours.- Variable paths take subscripts, bounds-checked against the DWARF extent:
readVariable('gLayers[2].width'),writeVariable('gGrid[1][3]', 0). An index past the end throws instead of resolving into whatever the linker placed next. A dimension the DWARF leaves unstated (extern T x[][4]) is not checked. symbolExtent(name)— an object's byte size, and whether it came fromst_sizeor the DWARF type. A write starting inside a known extent and running past its end is refused, naming what it would have hit.addressToSymbolresolves linker-placed globals (SHN_ABS/NOTYPE), which it previously skipped entirely. In a decomp that is most of them.
Execution, observed rather than sampled
wait({ pc }) compared the PC once per frame, so code that runs constantly read as never reached.
ArmCpu.addExecWatchpoint(address, cb)— fires from the CPU's instruction step and returns a disposer. Composable, and independent ofsetDebugHooks, which is a single slot.watchExecution(target, options?)— the execution counterpart towatchMemory, reportingcount(every execution, always exact —0means it did not run),hits,droppedandstop(). Takes an address or a symbol name.wait({ execution })— wait for an instruction to execute, by address or symbol.watchMemoryreportsdropped, so a cappedhitsarray is not read as the whole story.
Each hit carries lr and its source location. Note that lr names a caller only for an address a bl reached — one fallen into from the instruction above, branched to, or entered by a tail call carries whatever the last unrelated call left behind.
Symbols wherever an address goes
- A numeric code address has bit 0 cleared. A Thumb function pointer carries it set, so
watchExecution(ptr)counted0where the same function by name counted420. watchMemorytakes a symbol name, and throws on an unknown one instead of coercing it to address0and watching nothing.- A symbol watch defaults to the object's whole extent rather than
st_size— which is null for a linker-placed global, sowatchSymbol('gLayers')watched 1 byte of 112.
ARM7TDMI
- THUMB empty-Rlist quirk for
LDMIA/STMIA. THUMB.15 encodes the register list in 8 bits, soRlist == 0is representable, and the ARM7TDMI does not treat it as a no-op: it transfers R15 and advances the base by0x40(GBATEK, THUMB.15). The per-register loop did not run and the base was left alone, sostmia r1!, {}stored nothing and moved nothing.
Install
npm install @gba-kit/gba-node@0.6.0 # Node
npm install @gba-kit/gba-browser@0.6.0 # browser
npm install @gba-kit/gba-react@0.6.0 # ReactAlso published at 0.6.0: @gba-kit/gba-emulator, @gba-kit/arm-emulator, @gba-kit/debug-info.
Per-package detail is in each package's CHANGELOG.md. Scripting API reference: docs/scripting.md.
Full diff: https://github.com/macabeus/gba-kit/compare/@gba-kit/gba-emulator@0.5.0...v0.6.0