Skip to content

0.7.0

Choose a tag to compare

@macabeus macabeus released this 07 Sep 02:38
· 7 commits to main since this release

The theme of this release: debug a GBA game where you write it.

gba-kit was an emulator and an ELF/DWARF reader with a scripting surface on top. Neither knew about the other at run time: the emulator ran instructions, the reader answered questions about a file. This release adds the layer between them — a debugging session that runs the machine and answers in source lines, symbols and typed values — then a Debug Adapter Protocol server so any editor can drive it, the panels an editor has no native view for, and a VS Code extension that puts all of it beside your code.

GBA Debugger for VS Code ships with this release.

All nine packages and the extension move to 0.7.0 together.

Three new packages

  • @gba-kit/debug-core — an IDE-agnostic debugging session, the layer an adapter, a browser page or a test drives the same way. It owns one machine and runs it frame by frame under a stop predicate, so every stop lands on an exact (frame, instruction) position and the hardware frame grid never drifts however often you interrupt it.
  • @gba-kit/debug-adapter — a Debug Adapter Protocol server. Any editor with a DAP client (VS Code, Neovim, Emacs, Zed, JetBrains) launches npx @gba-kit/debug-adapter and gets a source-level debugger for a ROM.
  • @gba-kit/debug-ui — the panels, as React components over a Transport seam, so one implementation serves a VS Code webview and a web page alike.

Breakpoints

  • Source lines, on the statement rows the compiler emitted, or on the entry of a call inlined at that line. A line with no code of its own slides forward to the next line that has some.
  • Functions, instruction addresses, conditions, hit counts (3, >= 3, % 4) and logpoints with {expressions}.
  • Data breakpoints on a typed variable path, a symbol's whole extent, a label or a hex address, for writes, reads or both. A hit names the code that touched the range, or the DMA channel and the instruction that started the transfer.
  • Hardware events as exception filters: VBlank, HBlank, IRQ request and entry, DMA, I/O writes, halts.

Stepping, and stepping back

Stepping works the way gdb steps: instruction, statement (over, into, out), and also by frame and by scanline. Frames are told apart by their CFA, so recursion and leaf functions step correctly. Inlined calls are hidden layers that a step-over walks past and a step-into reveals.

Rewind is replay-exact rather than approximate. Keyframes (XOR plus run-length deltas, a full snapshot every N) and a per-frame input log put the machine back at any earlier position by replaying it, so re-running from a rewound point reproduces the original run byte for byte. stepBack, reverseContinue to the previous breakpoint hit, and rewind-by-frames are all built on it.

Values

Call stacks are unwound through .debug_frame with a link-register fallback, with inlined frames in between. Locals and parameters are read at their location for this PC, including where the compiler kept an optimized-out value; when it kept none, the answer says so rather than showing a stale register.

Values unfold structs, unions, bitfields, both bitfield dialects, arrays, enums and pointers. A scalar that lives in memory is writable, as are the registers. Hover and watch take a Mesen-style expression grammar: C operators, [addr] / {addr} / u32(addr) reads, registers, frame / scanline / cycle, symbols, a.b[3].c paths, &symbol, and labels.

The machine beside the code

The screen runs in the editor, with keyboard and gamepad input and audio, and a transport bar for run, pause, frame step, rewind and record. Beside it: the palette, tiles at any character base, tilemaps rendered from the map and its tiles, sprites as a table of OAM with a painted preview of each, decoded I/O registers, an instruction trace, a hardware event log, memory search with narrowing, and labels for the addresses an ELF does not name — a decomp's gUnk_... — which persist per project, import and export as .sym, and appear in disassembly.

Save states and recordings, kept with the project

Both live under .gba-kit/ and are listed again the next time the project is opened.

A save state carries the screen it was saved on, so the drawer under the display shows them by sight rather than by name, and one can be renamed or deleted in place.

An input recording carries the screen it began on and, packed to about 24 KB, the machine as it was on its first frame. That last part is what lets "from where it was recorded" work in a session that never ran those frames. A replay plays back at the speed it was made, so it is watched rather than jumped through, and a breakpoint during one stops it where it hit.

In the emulator core

  • Gba.runFrame(shouldStop?) takes a stop predicate checked before every instruction and while the CPU is halted. A stop charges no cycle and the next call finishes the same hardware frame. Gba.runScanline(), Gba.frameCount and Gba.scanline are new.
  • Snapshot restore is bit-exact: scheduled events keep their fireCycle and only get their callbacks reattached, held buttons are restored, and frameCount is part of the snapshot, so running K frames from a restored snapshot reproduces the original run.
  • GbaSystemBus.peek / poke are side-effect-free debugger accesses: an EEPROM peek never clocks its protocol, and a poke stores the byte typed without notifying watchpoints.
  • GbaSystemBus.addReadWatchpoint gives reads the counterpart of the write watchpoints. The read paths pay one length check when none is set.
  • Gba.onHardwareEvent is one sink for interrupt requests and entries, DMA transfers with the instruction that started them, I/O writes, VBlank and HBlank, and halts. The hot paths pay nothing when nobody listens.
  • The HLE BIOS keeps no module-global state, so two Gba instances in one process cannot cross-talk.

In debug-info

DebugInfo.scopes answers the function and inlined calls containing a PC, the variables visible there, and where each lives at that PC, with location lists for DWARF 2 through 5, a DWARF expression evaluator and frame bases via the .debug_frame CFA. LineTable gained the queries stepping needs, SymbolIndex now keeps each symbol's binding and section so a file-static never satisfies a C extern, and checkRomIdentity compares an ELF's cartridge-window sections against a ROM and names the first mismatch.

Breaking change

  • CpuSnapshot.haltedBySWI is gone. Nothing ever set it, because a GBA halts through HALTCNT into the interrupt controller. A snapshot written with the field still loads and the field is ignored, but code that reads or constructs a CpuSnapshot must drop it.