Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
c08b38f
Keep is_stmt on line rows, so a debugger stops where the compiler sai…
macabeus Sep 5, 2026
bca3657
Own execution stops in the machine, and make a restored snapshot repl…
macabeus Sep 5, 2026
616c93d
Answer the debugger's questions from the ELF: source to PC, ISA mode,…
macabeus Sep 5, 2026
3eca02b
Show a Thumb bl as one call, and name what a branch or literal points at
macabeus Sep 5, 2026
3da9e48
Read scopes, locations, values and call frames from the DWARF
macabeus Sep 5, 2026
c264fa6
Let a debugger observe interrupts, DMA, I/O writes and the display ti…
macabeus Sep 5, 2026
23bbbb9
Debug a GBA program from any IDE: sessions, breakpoints, gdb-style st…
macabeus Sep 5, 2026
f3855dc
Speak the Debug Adapter Protocol, so any editor can debug a ROM
macabeus Sep 5, 2026
0773578
Show what an editor cannot: the screen, PPU, I/O, trace, events and t…
macabeus Sep 5, 2026
275713e
Run the webapp's Debug page on debug-core and debug-ui, sharing the P…
macabeus Sep 5, 2026
fc5a8a3
Give editors without a screen one, and prove the adapter works as a p…
macabeus Sep 5, 2026
12c45af
Debug a GBA ROM in VS Code: the extension, its webviews, and an Exten…
macabeus Sep 5, 2026
cf14a53
Judge a condition where it is asked, and answer the machine's real state
macabeus Sep 5, 2026
20080c0
Answer a failed evaluation where it was asked, not as a notification
macabeus Sep 5, 2026
1b180cc
Stop a recording where it can be seen, and keep a panel's feeds to it…
macabeus Sep 5, 2026
8707465
Send a panel only what it is showing, and route recording to the Tool…
macabeus Sep 5, 2026
3f5259a
Keep one owner of the machine, and draw what is really mapped
macabeus Sep 5, 2026
56481f9
Record the dependencies the remediation added
macabeus Sep 5, 2026
cd2b38f
Write to the machine from the debug console
macabeus Sep 5, 2026
7e20207
Give the tests that run a console the time a console takes
macabeus Sep 5, 2026
f6271f1
Put the screen up when a session starts
macabeus Sep 6, 2026
c565642
Run the machine once a screen is watching it
macabeus Sep 6, 2026
204a097
List the recordings, with the screen each begins on
macabeus Sep 6, 2026
1a16ecc
Say what the code does, once, and only where the code cannot say it
macabeus Sep 6, 2026
b22b2b6
Fix what the comments were apologising for
macabeus Sep 6, 2026
b0c8fde
Play a recording back, instead of jumping through it
macabeus Sep 6, 2026
b413a40
Keep save states by sight, and let them be renamed or deleted
macabeus Sep 6, 2026
a9781a6
Read the machine while rendering, instead of copying it into state
macabeus Sep 6, 2026
52ec971
Use the editor's own icons, and keep a screen inside its card
macabeus Sep 6, 2026
31f7555
Keep a project's recordings, the way it keeps its save states
macabeus Sep 6, 2026
7029d84
Keep the machine a recording began on, so it replays there later
macabeus Sep 6, 2026
fab77ca
Say each of these things once, for save states and recordings alike
macabeus Sep 6, 2026
1f2d12e
Keep sweeping: one place for each thing the panels and the hosts repeat
macabeus Sep 6, 2026
2e0b154
Do not read a patched cartridge header as the wrong ELF
macabeus Sep 7, 2026
d917845
Ship the icon the manifest names
macabeus Sep 7, 2026
f884d7e
Let a launch configuration that says stopOnEntry mean it
macabeus Sep 7, 2026
4fd04df
Release the extension with the packages, and carry a smaller icon
macabeus Sep 7, 2026
0a13dba
Host the readme screenshot where anyone can see it
macabeus Sep 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [["@gba-kit/*"]],
"fixed": [["@gba-kit/*", "gba-kit-vscode"]],
"linked": [],
"access": "public",
"baseBranch": "main",
Expand Down
14 changes: 14 additions & 0 deletions .changeset/ide-debugger-debug-adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@gba-kit/debug-adapter': minor
'@gba-kit/debug-core': minor
---

New package `@gba-kit/debug-adapter`: a Debug Adapter Protocol server for GBA programs. 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: breakpoints of every kind (line, function, instruction, conditional, hit count, logpoint, data breakpoints on reads and writes naming the code that touched the range, hardware events as exception filters), stepping by statement or instruction, a call stack with inlined frames, DWARF-typed variables with memory references and evaluate names, writable scalars and registers other than `cpsr`, hover/watch evaluation, disassembly with symbols and labels, memory read and write, loaded sources, restart (which reloads the ROM and ELF from disk, breakpoints carried over), and replay-exact `stepBack` / `reverseContinue`.

Emulator-only operations are `gba-kit/*` custom requests, typed in `@gba-kit/debug-adapter/protocol` (a re-export of `@gba-kit/debug-core/protocol`, where the vocabulary lives so browser clients need no Node package): buttons, frame and scanline steps, rewind by frames, save states (save, list, load, rename, delete), input recordings (kept under the project and listed again in the next session, replayable from where each was recorded, with delete), the palette / tiles / tilemap / sprite / background views, decoded I/O registers, trace and event logs, labels, memory search, and a frame/audio stream over a pipe the client owns. A `gba-kit/state` event reports every stop, resume and rewind, and every recording start or stop and tracing toggle; `gba-kit/lastRecording` hands out the recording last stopped, whoever stopped it.

`gba-kit-screen` (`npx -p @gba-kit/debug-adapter gba-kit-screen`) is a browser page with the display and a keyboard gamepad for editors that have none, fed by the adapter over the same pipe (which is two-way: the page's button presses come back). `newPipePath()` names a fresh pipe for a client to listen on.

Launch diagnostics refuse an ELF whose loadable bytes differ from the ROM (naming the first mismatching section) unless `allowElfMismatch` is set, and say when no source file was found under `cwd`. Responses always precede the `stopped` they cause, and variable references are dropped whenever the machine moves, so a client expands again at the new stop; one held across a restart is refused as stale.

`@gba-kit/debug-core`: `Program.hasCodeAt` (for `breakpointLocations`), and `SourceMapper.localFiles` keeps the file system's spelling on case-insensitive systems.
19 changes: 19 additions & 0 deletions .changeset/ide-debugger-debug-core.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@gba-kit/debug-core': minor
---

New package: an IDE-agnostic debugging session for GBA programs, the layer a Debug
Adapter Protocol server, a browser page or a test drives the same way.

- **A `Session` owns one machine** and answers in addresses, frames, symbols and typed values. It runs frame by frame under a stop predicate, so every stop lands on an exact (frame, instruction) position and the hardware frame grid never drifts.
- **Breakpoints of every kind**: source lines (statement rows, or the entry of a call inlined at that line; a line without code slides forward), instruction addresses, function names, 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, naming the code that touched it (or the DMA channel and the instruction that started it); event breakpoints on VBlank, HBlank, IRQ request/entry, DMA, I/O writes and halts.
- **Stepping the way gdb steps**: instruction, statement (over, into, out), frame and scanline. Frames are told apart by their CFA, so recursion and leaf functions step correctly; inlined calls are hidden layers a step-over walks past and a step-into reveals, and a stop at the entry of an inlined call shows the call site until stepped into.
- **Call stacks, scopes and values from the DWARF**: physical frames unwound through `.debug_frame` with a link-register fallback, inlined frames in between, locals and parameters with their location at this PC (or where the compiler did keep an optimized-out value), globals, registers and machine state; values unfold structs, unions, bitfields, arrays, enums and pointers, and a scalar that lives in memory is writable, as are the registers of the Registers scope.
- **A Mesen-style expression grammar** for conditions, logpoints and the watch view: C operators, `[addr]` / `{addr}` / `u32(addr)` reads, registers, `frame` / `scanline` / `cycle`, symbols and `a.b[3].c` paths, `&symbol`, labels.
- **Replay-exact rewind**: keyframes (XOR + run-length deltas, a full snapshot every N) plus a per-frame input log put the machine back at any earlier (frame, instruction) by replaying it; `stepBack`, `reverseContinue` (to the previous breakpoint hit) and `rewindFrames` are built on it, and re-running from a rewound point reproduces the original run byte for byte.
- **Tracing and events**: an instruction trace ring and a hardware event log with frame, scanline and cycle stamps.
- **Labels** for addresses the ELF does not name (a decomp's `gUnk_...`), persisted per project and importable from `.sym` files, usable in expressions and shown in disassembly; a `labels` session event says when they change.
- **Input recording and replay** (`recording` and `tracing` session events say when one starts or stops, `recordingStart` and `lastRecording` say where it began and what it produced; a finished take carries the screen and the machine it began on, packed, and reads and writes as a file, so a project keeps its recordings and replays one from where it was recorded in a session that never ran those frames), save states bound to the ROM's hash (each keeping the screen it was saved on, so a view can list them by sight), memory search with narrowing, and the emulator views: palette, tiles, tilemaps, sprites, backgrounds and decoded I/O registers.
- **`@gba-kit/debug-core/protocol`**: the `gba-kit/*` request and event vocabulary a debug adapter answers and every client speaks, with the argument helpers (entry counts, rewind frames, tile counts), the body builders for a saved state and a take, and the audio sample rate both hosts share, so the two implementations of the protocol agree without either restating it.
- **Shares a machine with a player**: a session can wrap an existing `Gba` (`SessionOptions.machine`) and `resync()` after someone else drove it (a play mode, a state loaded outside), so a page plays a ROM and debugs it in turns.
- Tested against one small C program built three ways (Thumb -O0, Thumb -O2, ARM -O0), whose ROM/ELF pairs are committed under `test-fixtures/` and rebuilt on CI.
14 changes: 14 additions & 0 deletions .changeset/ide-debugger-debug-info.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@gba-kit/debug-info': minor
---

The queries an IDE debugger needs on top of the parser:

- `LineTable.sourceToPcs(file, line)`, `nearestLineWithCode`, `rowAt(address)` (statement-aware, for stepping) and `files`; paths are matched normalized. A line's locations are the starts of its statement runs: one per piece of code the compiler emitted for it (a loop condition, a hoisted load), not one per row, and rows without `is_stmt` are not places to stop.
- `DwarfScopes.inlineCallSitesAt(file, line)` and `entryPc(inlined)`: where a call inlined at a source line is entered (`DW_AT_entry_pc`, else the lowest range). Such a line has no rows of its own, so it is where a breakpoint on it goes.
- `SymbolIndex` keeps each symbol's binding and section; `globalSymbol(name)` / `DebugInfo.globalSymbolAddress` answer only with a defined global (a file-static of the same spelling never satisfies a C `extern`, and two globals at different addresses are refused as ambiguous). Linker globals placed inside a section (`gFoo = .;`, as a decomp's ldscript does) resolve, not only `SHN_ABS` ones; undefined/common symbols and absolute FUNC placeholders are dropped.
- `modeAt(address)` reports the instruction set from GNU `$a` / `$t` / `$d` mapping symbols.
- `checkRomIdentity(rom)` compares the ELF's cartridge-window sections with a ROM and names the first mismatch; `isLinked` distinguishes an image from an object file (`ElfFile.type`).
- Line rows for code the linker discarded (addresses below every loadable section) are dropped, so a PC in the BIOS stub does not resolve into them.
- `readDwarfEntries(elf)` exports the DIE trees with attribute forms and unit versions, for scope- and location-level readers.
- `DebugInfo.scopes` (`DwarfScopes`): the function and inlined calls containing a PC, the variables visible there and where they live at that PC (location lists for DWARF 2–5, a DWARF expression evaluator, frame bases via `.debug_frame` CFA), typed value trees for any DWARF type (structs, both bitfield dialects, arrays, enums, pointers), call-frame unwinding, and "optimized out" answers that say where the compiler did keep the value.
11 changes: 11 additions & 0 deletions .changeset/ide-debugger-debug-ui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@gba-kit/debug-ui': minor
---

New package: the debugger panels an editor has no native view for, as React components over a `Transport` seam, so one implementation serves a VS Code webview and a web page alike.

- **Screen** with keyboard and gamepad input (sent as one button mask so the two never fight), audio through an `AudioWorklet` fed from a queue of sample chunks, and a transport bar: run/pause, frame step, rewind, record (a stopped recording opens the Recording tab through the transport's `showPanel`).
- **Palette**, **Tiles** (any character base, 4/8 bpp, palette bank), **Tilemap** (rendered from the map and its tiles, with per-entry inspection), **Sprites** (a table of OAM with a painted preview of each, 1D and 2D mapping), **I/O registers** (decoded fields, filterable), **Trace** and **Events** (the instruction trace and the hardware event log), **Memory search** (search and narrow), **Labels** (edit, import `.sym`, export), **Save states** (each shown as the screen it was saved on, to load, rename or delete) and **Recording** (record, replay, open as a script, delete; recordings the project kept are listed with the ones made now). The Screen panel carries the same save states as a drawer beneath the display. Actions are drawn with VS Code's own icons (the codicon font), so the panels use the same glyph for the same idea as the editor around them.
- `DebugPanels` puts them behind tabs for a host with one slot.
- `createMessageTransport` / `serveTransport` speak `postMessage` between a webview and its host (a feed is unsubscribed once its last listener leaves, so frames and audio stop crossing to a panel that no longer shows them); `createSessionTransport` answers the same requests from an in-process `@gba-kit/debug-core` session. `@gba-kit/debug-ui/transport` exports the transport alone, for a host that bundles no React.
- Styled through `--gk-*` variables (`@gba-kit/debug-ui/styles.css`), so a host paints the panels in its own theme.
18 changes: 18 additions & 0 deletions .changeset/ide-debugger-execution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@gba-kit/gba-emulator': minor
'@gba-kit/arm-emulator': minor
'@gba-kit/gba-browser': patch
---

Debugger-grade execution and inspection 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, so frames stay on the hardware grid however often a debugger interrupts them. `Gba.runScanline()`, `Gba.frameCount` and `Gba.scanline` are new; `runFrame` returns a `RunOutcome`.
- A CPU debug hook that refuses an instruction costs no scheduler cycle (`ArmCpu.halted` distinguishes a halted CPU from a refused instruction).
- `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, the field being ignored, but code that reads or constructs a `CpuSnapshot` must drop it.
- Snapshot restore is bit-exact: scheduled events keep their `fireCycle` and only get their callbacks reattached (`Scheduler.reattach`, `TimerController.reattachEvents`, `DmaController.reattachEvents`), held buttons are restored, and `frameCount` is part of the snapshot, so running K frames from a restored snapshot reproduces the original run.
- The HLE BIOS keeps no module-global state: `handleSwi` takes a per-machine `BiosEnv`, so two `Gba` instances in one process cannot cross-talk.
- `GbaSystemBus.peek` / `poke`: side-effect-free debugger reads (an EEPROM peek never clocks its protocol) and writes that store the byte typed (no OAM drop / VRAM duplication) without notifying data watchpoints.
- `GbaSystemBus.addReadWatchpoint`: read data breakpoints, the counterpart of the write watchpoints. A load overlapping the range reports the value it returned and which DMA channel, if any, performed it; the read paths pay one length check when none is set.
- `EmulatorBridge.loadState` releases the buttons a snapshot restores, so a loaded state does not arrive with buttons held. `EmulatorBridge.refreshFrame()` repaints the canvas from the PPU after another driver of the same `Gba` (a debug session) moved it, `saveState` draws its thumbnail from the screen as it is now rather than the last frame the bridge rendered, and `run()` clears only the CPU debug hooks the bridge itself installed.
- `disassembleThumbAt` / `disassembleArmAt`: a Thumb `bl` prefix/suffix pair is one 4-byte instruction with its target, and branch / literal-pool targets can be symbolized.
- `Gba.onHardwareEvent`: one sink for interrupt requests and entries, DMA transfers (with the instruction that started them), I/O writes, VBlank/HBlank and halts — the feed for an event log; the hot paths pay nothing when nobody listens.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@ Thumbs.db
# Cache
.cache/


# VS Code downloaded for the Extension Development Host tests
apps/vscode-extension/.vscode-test/
apps/vscode-extension/*.vsix
32 changes: 20 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,28 @@
- **TypeScript-native** — Emulator built entirely in TypeScript, designed for the JS/TS ecosystem
- **Modular npm packages** — Use just the ARM CPU core, the GBA emulator, or the Node.js, browser, and React runtimes
- **First-class scripting API** — Run headless emulation from Node.js scripts for automated testing, TAS, ROM research, and tooling
- **Built-in debugger** — Run the disassemblier, set breakpoints, open the memory viewer, inspect registers, and more
- **Built-in debugger** — Run the disassembler, set breakpoints, open the memory viewer, inspect registers, and more

## Packages

| Package | Description |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------- |
| [`@gba-kit/arm-emulator`](packages/arm-emulator) | ARM7TDMI CPU emulator (Thumb + ARM instruction sets) |
| [`@gba-kit/gba-emulator`](packages/gba-emulator) | Full GBA hardware emulation (PPU, APU, DMA, timers, interrupts, system bus) |
| [`@gba-kit/gba-node`](packages/gba-node) | Headless Node.js runtime for scripted GBA emulation |
| [`@gba-kit/gba-browser`](packages/gba-browser) | Browser runtime for GBA emulation (Canvas rendering, keyboard input, IndexedDB save states) |
| [`@gba-kit/gba-react`](packages/gba-react) | React hooks for GBA emulation (`useEmulator`, `useEmulatorCanvas`, `useEmulatorKeyboard`) |
| [`@gba-kit/debug-info`](packages/debug-info) | Parse ELF symbols + DWARF line tables (PC→source) for source-level debugging |
| Package | Description |
| -------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| [`@gba-kit/arm-emulator`](packages/arm-emulator) | ARM7TDMI CPU emulator (Thumb + ARM instruction sets) |
| [`@gba-kit/gba-emulator`](packages/gba-emulator) | Full GBA hardware emulation (PPU, APU, DMA, timers, interrupts, system bus) |
| [`@gba-kit/gba-node`](packages/gba-node) | Headless Node.js runtime for scripted GBA emulation |
| [`@gba-kit/gba-browser`](packages/gba-browser) | Browser runtime for GBA emulation (Canvas rendering, keyboard input, IndexedDB save states) |
| [`@gba-kit/gba-react`](packages/gba-react) | React hooks for GBA emulation (`useEmulator`, `useEmulatorCanvas`, `useEmulatorKeyboard`) |
| [`@gba-kit/debug-info`](packages/debug-info) | Parse ELF symbols + DWARF line tables (PC→source) for source-level debugging |
| [`@gba-kit/debug-core`](packages/debug-core) | IDE-agnostic debugging session: breakpoints, gdb-style stepping, DWARF values, exact rewind |
| [`@gba-kit/debug-adapter`](packages/debug-adapter) | Debug Adapter Protocol server: debug a ROM from VS Code, Neovim, Emacs, Zed or JetBrains |
| [`@gba-kit/debug-ui`](packages/debug-ui) | React panels for the screen, PPU, I/O, trace, events, labels; hosted by editors and the web |

## Apps

| App | Description |
| -------------------------------- | ---------------------------------------------------------------------------------------- |
| [`@gba-kit/webapp`](apps/webapp) | Browser-based GBA debugger with disassembly, breakpoints, memory viewer, and save states |
| App | Description |
| ----------------------------------------- | ---------------------------------------------------------------------------------------- |
| [`@gba-kit/webapp`](apps/webapp) | Browser-based GBA debugger with disassembly, breakpoints, memory viewer, and save states |
| [`gba-kit-vscode`](apps/vscode-extension) | VS Code extension: debug a GBA ROM in C with the screen, PPU and I/O views beside it |

## Scripting

Expand Down Expand Up @@ -117,8 +121,12 @@ gba-kit/
gba-browser/ # Browser runtime (Canvas, keyboard, IndexedDB)
gba-react/ # React hooks (wraps gba-browser)
debug-info/ # ELF/DWARF parser (PC→source)
debug-core/ # IDE-agnostic debug session (breakpoints, stepping, rewind)
debug-adapter/ # Debug Adapter Protocol server
debug-ui/ # React debugger panels (screen, PPU, I/O, trace, labels)
apps/
webapp/ # Browser debugger UI + dev server
vscode-extension/ # VS Code debugger extension
```

### Using with npm link
Expand Down
10 changes: 10 additions & 0 deletions apps/vscode-extension/.vscodeignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
**
!dist/extension.js
!dist/adapter.js
!dist/webview.js
!dist/webview.css
!dist/codicon.ttf
!media/logo.png
!README.md
!LICENSE
!package.json
Loading
Loading