Debug GBA programs from an IDE: debug-core, a Debug Adapter, the panels, and a VS Code extension - #16
Open
macabeus wants to merge 33 commits into
Open
Debug GBA programs from an IDE: debug-core, a Debug Adapter, the panels, and a VS Code extension#16macabeus wants to merge 33 commits into
macabeus wants to merge 33 commits into
Conversation
… safe globals, ROM identity
…epping, DWARF values and exact rewind `@gba-kit/debug-core` is the IDE-agnostic layer a Debug Adapter, a browser page or a test drives the same way. A `Session` owns one machine and answers in source lines, call frames, typed values, frames and scanlines. - Execution under a stop predicate: every stop is an exact (frame, instruction) position, and frames stay on the hardware grid. - Breakpoints: source lines resolve to the starts of a line's statement runs (one per piece of code the compiler emitted for it, `is_stmt` rows only), or to the entry of a call inlined at that line, which has no rows of its own; instruction and function breakpoints; conditions, hit counts and logpoints in a Mesen-style expression grammar; data breakpoints for writes, reads or both (read watchpoints are new in the bus), naming the writer or the DMA that did it; event breakpoints on VBlank, HBlank, IRQs, DMA, I/O and halts. - Stepping like gdb: statement rows, CFA frame identity, inlined calls as hidden layers. A stop at the entry of an inlined call shows the call site until the user steps in, whether it came from a breakpoint, a step-out, a pause or a rewind (gdb's rule for inline frames). - Call stacks, scopes and values from the DWARF, with writable scalars. - Replay-exact rewind from keyframes and an input log: step back, reverse continue to the previous breakpoint hit, rewind N frames; re-running from a rewound point reproduces the original run byte for byte. - Trace ring, hardware event log, labels persisted per project, input recording and replay, save states bound to the ROM hash, memory search, and the palette / tile / tilemap / sprite / I/O views. Tested on one C program built as Thumb -O0, Thumb -O2 and ARM -O0; the ROM/ELF pairs are committed and CI rebuilds them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KvrRmTvh3Qhu7CCeM65xFJ
`@gba-kit/debug-adapter` puts a `@gba-kit/debug-core` session behind DAP: breakpoints of every kind (lines, functions, instructions, conditions, hit counts, logpoints, read/write data breakpoints with the writer named, hardware events as exception filters), stepping by statement or instruction with the response always ahead of the stop it causes, a call stack with inlined frames, DWARF-typed variables with memory references and evaluate names, writable scalars and registers, hover/watch evaluation, disassembly with symbols and labels, memory, loaded sources, restart, and replay-exact stepBack and reverseContinue. Variable references from before the machine last moved are refused as stale. What only an emulator has is a `gba-kit/*` custom request, typed in `@gba-kit/debug-adapter/protocol`: buttons, frame and scanline steps, rewind by frames, save states, input recordings, the PPU and I/O views, trace and event logs, labels, memory search, and a frame/audio stream over a pipe the client owns, so frames never queue behind the DAP connection. Launch diagnostics refuse an ELF that is not the ROM's build unless told otherwise, and say when no source file was found under cwd. `dist/cli.js` runs it over stdio for any editor; the class also runs in-process. The tests drive it with real DAP messages over in-memory streams against the debug-core fixtures. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KvrRmTvh3Qhu7CCeM65xFJ
…ools as React panels `@gba-kit/debug-ui` is the set of debugger panels an editor has no native view for, over a `Transport` seam so one implementation serves a VS Code webview (postMessage to the extension host) and the webapp (direct calls into a debug-core session) alike: - Screen with keyboard and gamepad input as one button mask, audio through an AudioWorklet fed from a ring buffer, and run/pause, frame step, rewind and record. - Palette, tiles (any character base, 4/8 bpp, palette bank), tilemap rendered from its tiles, sprites as an OAM table with each one painted (1D and 2D mapping), I/O registers with decoded fields, the instruction trace, the hardware event log, memory search with narrowing, labels with .sym import and export, save states, and input recordings. - `DebugPanels` puts them behind tabs; `--gk-*` variables paint them in the host's theme. The pixel helpers, transports, key maps and views are tested in Node; the in-process transport against a real session on the debug-core fixtures. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KvrRmTvh3Qhu7CCeM65xFJ
…lay page's machine The Debug page now drives a `@gba-kit/debug-core` session instead of the browser bridge's ad-hoc breakpoints and steps: statement stepping, step into/out/back, frame steps, rewind, the session's disassembly with labels and source lines, and its register view (`lr`/`pc` symbolized, `cpsr` decoded). The emulator views come from `@gba-kit/debug-ui`: the screen, I/O registers, palette, tiles, tilemap, sprites, trace, events, memory search and labels, replacing the page's own screen and I/O views. The dev server's sidecar ELF loads once at the app level and the Source view's picker hands a chosen ELF up, so the session is rebuilt with it. Play and Debug take turns driving one `Gba`: a session can wrap an existing machine (`SessionOptions.machine`) and `resync()` when someone else drove it (returning from Play, loading a save state), forgetting the history that no longer describes the machine and re-arming its hooks. History is anchored at the frame a session starts or resyncs on, so step-back works right away. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KvrRmTvh3Qhu7CCeM65xFJ
…lain process - `gba-kit-screen`: a browser page with the GBA display and a keyboard gamepad, served by a dependency-free HTTP + WebSocket server that owns the pipe the adapter is told about with `gba-kit/stream`. The pipe is now two-way: a `type 3` message carries a button mask back, so a screen that is not a DAP client still plays. - A second client: the adapter spawned as a child process on stdio, driven with hand-framed DAP messages through a launch, a function breakpoint, a stop, a stack and a disconnect that exits the process. - Neovim, Emacs, Zed and JetBrains recipes in the adapter's README; the root README lists the new packages and the extension. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KvrRmTvh3Qhu7CCeM65xFJ
…sion Development Host suite `apps/vscode-extension` is the thin editor layer over the DAP seam: it registers the `gba-kit` debugger (the bundled adapter as its own Node process, inline in the extension host when `node` is not on the PATH or by setting `gba-kit.adapter`), owns the Screen and Tools webviews that host `@gba-kit/debug-ui`, and exposes commands that are one custom request each: frame and scanline steps, rewind, input recording, save and load states, label import and export. Frames and audio reach the webviews over a pipe the extension owns; the panels talk to the extension host in messages that a tested bridge routes to the debug session. esbuild produces `dist/extension.js`, `dist/adapter.js` and the browser bundle `dist/webview.js` + `webview.css`; `vsce package --no-dependencies` makes a 355 KB .vsix from them. `test:vscode` runs a suite inside an Extension Development Host on the debug-core fixtures: a breakpoint set before launch is sent during configuration and verified, hits in `main` on the right line, scopes and a DWARF value read through VS Code, a frame step through the extension's command, both panels open, the session ends. The DAP traffic is recorded through VS Code's tracker API for the assertions and the failure report. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KvrRmTvh3Qhu7CCeM65xFJ
Remediation of the audit findings in the emulator, the DWARF reader and the session core. - Reverse continue judged every condition in one expression environment per frame, so a breakpoint's condition was evaluated in the scope of whichever breakpoint the frame reached first. Its locals were missing there, and a condition that cannot be evaluated counts as a hit, so the scan reported breakpoints the forward run never reached. The environment is now built per address, and reverse replay evaluates conditions, reconstructs hit counts and consults data and event breakpoints as its documentation always claimed. - Enum constants resolve: `DwarfScopes.enumeratorByName` indexes every enumeration in the ELF, and the inspector consults it after locals, globals and linker declarations, so a variable still shadows a constant. Hovering an enumerator now answers with its value and type instead of "unknown symbol". - A halted CPU no longer counts polls as instructions, so a position taken while halted stays exact across replay. - A throwing `output` listener no longer recurses into `#emit` until the stack overflows; `restart` and `resync` route through `#stop`, so a state change is always announced. - Hit counters reset with the machine, unnamed constants got names, duplicated bitfield extraction and the replay-mode ritual were folded into one place, and several doc comments that described code other than the one below them were corrected. The new reverse-continue test fails on all three fixture variants without the scoping fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Remediation of the audit findings in the debug adapter. The adapter reported every failure with `sendErrorResponse(response, <number>, <message>)`, whose default destination is the user, so VS Code turned an error response into a modal notification. Its own hover and watch send `evaluate` with an empty expression on a stop, which the core rightly refuses, so a breakpoint in a decomp raised an "empty expression" notification each time the machine stopped. Inspection, execution and custom-request failures now answer through `#fail`, which reports in the request's own response; only a launch that cannot start still asks to be shown, because nothing else would surface it. `evaluateRequest` also checks that an expression is a string before trimming it, instead of crashing on a request without one. Also here: variable references re-expand after a write instead of going stale, the frame stream drops a slow consumer's backlog rather than growing it, `ScreenServer` honours the host and port it is given, and the timer test states what it actually pins, so it no longer fails when an unrelated timer ends during a launch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…self Remediation of the audit findings in the panels. "Stop recording" on the Screen panel opened an editor holding the script, which is not where a recording is reviewed or replayed. The transport gained `showPanel`, so stopping now reveals the Tools panel on its Recording tab; a host without one still gets the editor. The Recording panel shows the session's last recording whoever stopped it, and its passive refresh no longer writes to the error line, which belongs to what the user just did, nor asks anything before a session exists. Also here: a panel subscribes to a feed on its first listener and unsubscribes on its last, so a hidden panel is not sent 150 KB frames; the state a panel renders comes from one place; and the pixel helpers handle a short palette, an odd sprite tile and a truncated tilemap rather than reading past their arrays. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s panel Remediation of the audit findings in the VS Code extension. The host bridge now holds a `showPanel` request until the webview subscribes, so the Tools panel opens on its Recording tab even when the panel is created by that same request, and the recording command reveals it instead of opening an editor. Frames are withheld from a hidden panel and the newest is sent once it is shown again, while audio keeps flowing, because a user may listen while reading code. Feeds are dropped when their last listener goes. The frame server moved out of `extension.ts` into its own module with tests, and the webview shell states the content policy the panels actually need. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Remediation of the audit findings in the webapp and the browser bridge. Instruction breakpoints live in the session rather than in the Debug page's state, so switching to Play and back shows the breakpoints that are actually set. The memory viewer honours what the bus reports as readable and draws unmapped bytes as such instead of as zeros, and the register view reads the session's own naming. `EmulatorBridge.refreshFrame` re-reads the PPU into the canvas, so the Play screen and a save state's thumbnail show the machine as the debugger left it rather than the last frame Play itself drew. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`pnpm-lock.yaml` for the dev dependencies the audit's new tests and checks introduced across the workspace. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A console line of the shape `place = value` stores, where until now the console could only read. The place is resolved exactly as any other expression is, so `gUnk_03005220.dreamStones = 10` and `gEntityInfo[3].id = 2` name a member and an element the same way a watch or a hover names them, and the value is an expression too (`g_samples[2] = g_samples[1] + 1`). The write itself is the one the variables view already performs, so a bitfield, an enum and a narrow int are range-checked and refused by name rather than silently truncated. `=` is told apart from `==`, `!=`, `<=` and `>=`, so a comparison typed in the console stays a comparison; a compound operator is refused by name instead of failing as the unreadable target `x +`. Only the console writes: a hover over `a = b` in the source reads, because an editor sends whatever is under the mouse and must never store it. A client that asked for `invalidated` is told the variables it holds went stale, so the views re-read after a console write. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four packages ran on vitest's 5s default while their tests boot a GBA and run hundreds of frames, which is seconds of honest work. On a loaded machine or a busy CI runner they crossed the line and the build read as broken: the same commit passed on one runner and failed on another, on the emulator's predicate-stop test and the webapp's thumbnail test. They now allow 20s, as the debug adapter's tests already did. No assertion changed; only the clock they are judged against. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The screen is what a debug session is for, so it now comes up with one instead of waiting for the command. A panel that is already open is left exactly where it is rather than revealed, so a session starting never pulls a tab out from under whoever is reading it, and the panel is created before the frame stream is attached, so the first frame reaches it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A screen exists to be watched, so a session with one up runs instead of sitting at its entry stop waiting for Run. It happens once per session, and only from the entry stop: opening the screen while stopped at a breakpoint leaves the machine exactly where it stopped. Opening the screen later, at a session that is already sitting at its entry, runs it then. `gba-kit/state` now carries the reason the machine is stopped, which is what tells an entry stop from a breakpoint; any client can use it to say why it is paused. This does mean `stopOnEntry: true` runs on as soon as its screen appears. Set it to false to skip the stop altogether, or close the screen to sit at it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Recording panel showed only the last recording, as a script and a row of buttons. It now lists every recording of the session as a grid of Thumbnail, Script and Actions. - The session keeps its finished recordings, each with the screen it began on (halved in each axis, so a row costs kilobytes rather than the 150 KB a frame does), and drops the oldest past twenty so a long session is bounded. - A recording is replayed either from where it was recorded, which puts the machine back there and reproduces it, or from here, which presses the same buttons wherever the machine is now — a recorded move used somewhere else. - The script's own open button floats in its corner instead of sitting in a row, and it is offered only when the host has an editor to open it in. - The log button is gone: the log is what the script says, in a form nobody reads. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A pass over every comment and every line of prose this branch wrote, against the code beneath it. - Corrected what was untrue. Among them: a full keyframe is written every `fullEvery` + 1, not every `fullEvery`; `stepFrame` finishes the current frame rather than advancing a whole one; `romHash` falls back to a length when `crypto.subtle` is absent; the tile cap is 2048 4bpp tiles, not 8bpp; `epoch` moves on a restart and a resync; `decodeDelta` never writes in place; the I/O table is a chosen list, not every register the emulator models; address-class DWARF forms are read LSB-first, so a big-endian target's DIE addresses are wrong; a register-resident local is shown, not written; a variable reference is dropped when the machine moves, and refused only across a restart. - Deleted what the code already said, and what narrated the change rather than describing the code. - Matched each file's own voice, and cut the rest for length. No executable line moved; the suite is unchanged and green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comment pass found sixteen places where a comment described a defect instead of the code being right. Each is fixed at the source, with a test that fails without it. - The DWARF reader honours the payload's byte order everywhere. `readBytes` assembled address forms LSB-first, so `DW_AT_low_pc` was misread on a big-endian target and no function resolved at all; the `.debug_addr`, `rnglistx` and `loclistx` index tables read the same way round. The order is now a field of `DwarfSections`, so a reader cannot forget to ask. - Every 32-bit bus read returns an unsigned word. A word with bit 31 set was negative, which is why `disassembleFunction` never recognised `bx lr` and a 32-bit `searchMemory` never matched such a value. - A line whose run begins with a non-statement row keeps its address, instead of losing the statement row behind it. - `loadState` starts a new epoch, so a client's handles are stale rather than silently pointing at another machine. - `EntryIndex.unit` names the offset it cannot find instead of answering with another unit's bases. - A write from the console tells the session's listeners, so the webapp's memory, register and disassembly views refresh rather than showing stale bytes until the next stop. - The event log names the register a write landed in, as the stop does. - Loading a state trims the name the way saving it did, so a state saved as ` hi ` can be loaded again. - `#memberFacts` exposes the array rank it already returned; `CpuSnapshot` loses a field nothing ever set (a snapshot carrying it still loads, and the changeset says so); `dataSpecs` and an unreachable name fallback are gone. - The timer round-trip program enables a timer, which it never did, so the test exercises the overflow drift it was written for. Three test names now describe what their bodies do, and a DWARF 5 case reaches the path it claims. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replaying a recording ran its frames in one blocking loop inside the request, so the machine arrived at the end before the caller heard anything. On a game that spends its frame asleep in `VBlankIntrWait`, both replay actions landed back on that same line with nothing on screen to say they had run. A replay now drives the same paced loop a resume does: one recorded mask a tick, over any button the user is still holding, so the playback is watched at the speed it was recorded and a breakpoint, a stall or a pause ends it where it hit. `Session.replaying` and the state body's `replaying` say one is under way, and the Recording panel says so too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A save state is the whole machine at a moment; the game's own battery save is something else, so the term stays "save state" everywhere a user reads it. The tool tab said only "States", which also collided with the running/stopped state the panels report. It now says what it holds. A state keeps the screen it was saved on, as a 120×80 thumbnail written before the snapshot so a listing still reads it from the file's head. The GBA Screen panel gained a drawer of this ROM's states, each shown as that screen: click one to load it, or rename and delete it in place. The tool panel lists the same ones as a table with the same three actions. `gba-kit/renameState` and `gba-kit/deleteState` carry that over the protocol, answered by the adapter and by the in-process transport alike. `HostFiles` grew an optional `remove`, and a host without one refuses to delete rather than pretending the file went away. The webapp's own drawer now names a new save for the frame it holds, as the debugger does, instead of counting saves this session. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six of this package's Effects were doing work React has a better answer for. The debugger's state was subscribed to and copied into component state, so a panel's first render always showed "connecting" and the real state arrived a render later. The transport is the store, so the state now comes from `useSyncExternalStore`: it is read while rendering. Both transports keep one state object per change, and the in-process one follows the session itself rather than leaving that to whoever subscribed, so a panel rendering before it subscribes still sees the machine as it is. Painting a canvas was written out four times. `Screenshot`, the recording thumbnail and the sprite preview now all go through `usePixels`, the one place that touches a canvas, and the sprite's pixels are worked out while rendering instead of inside the Effect that paints them. Three panels each fetched a list their own way, none of them guarding against a slow response landing after a newer one. They share `useFetched` now, which drops a response that a newer read or an unmounted view has overtaken, and `useAtStop` is written in terms of it. Two paths that both refreshed a list — the panel's own action and the event the session raises — are one path: the event, which a recording stopped elsewhere or a label named in the editor already travels. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The panels drew their actions with whatever glyph came to hand: emoji for sound and delete, arrows and geometric shapes elsewhere. VS Code ships the icons it uses for these ideas, so the panels use those: the codicon font, named as the editor names them, through one `Icon` component whose set is closed. A run button is `debug-continue`, a rewind is `debug-step-back`, a delete is `trash`. The font ships with the extension beside the stylesheet that loads it, and the webapp gets the same set from the same import. A destructive action no longer shouts before it is reached. Delete is the editor's toolbar button — the glyph alone, its frame drawn under the pointer, and only then coloured as a warning — instead of a red-bordered button sitting in a row of cards. A save state's screen was drawn at the size it was stored at, which overflowed the card holding it. A `Screenshot` without a `scale` now takes the width it is given and keeps its shape, so a card's screen is the card's width whatever the stored size, and the recording table draws its screen at the size it was stored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A recording lived only as long as the session that made it. It now goes under the project's `.gba-kit/recordings/`, one file per take carrying the input, the screen it began on and when it was made, and a session opened later lists and replays what earlier ones recorded. The file is named for the moment it was taken and the frame it starts on, so two takes of the same frame are two files. Nothing is deleted to make room: a launch reads newest first and stops at the twenty a session lists, leaving older files where they are, and `gba-kit/deleteRecording` removes one when the user asks. A file that is not one of ours, or belongs to another ROM, is passed over. The take is answered from the session and written to disk after, so this session reads its own recordings from memory and the file is for the next one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AJKogeQcw1deu97DMTNs42
"From where recorded" rewinds to the frame the recording starts at, which a session opened later cannot reach: it never ran those frames, so there is no history to rewind through, and the action failed with "no longer in history". A recording now carries the machine as it was on its first frame. Not as a save state, which would be near a megabyte a take, but run-length encoded against nothing: most of a GBA's RAM is zero, so a ~470 KB snapshot packs to ~24 KB and a recording file goes from 51 KB to 86 KB. Rewinding is still tried first, being exact and cheap; the stored state is the way back when it cannot reach. `gba-kit/replay` takes the `id` of the take a recording came from, so the session can use the state it kept for it. A recording replayed from somewhere else, or one written before this, replays from here as it did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AJKogeQcw1deu97DMTNs42
The two grew up side by side and had drifted into saying the same things twice,
sometimes three times.
In debug-core, a `Screen` is now one type with one pair of converters, used by
both file formats instead of each spelling out `{width, height, rgba}` and its
base64. A session names its own `.gba-kit/` paths, so the directory is written in
one place rather than in the adapter twice and the core once, and takes the
screen it keeps beside a state or a recording through one method.
The protocol builds the bodies for a saved state and a take, so the debug adapter
and the in-process transport agree on their shape without either restating it.
That also retires the transport's own base64, which was a second implementation
of one the core already exported, and the adapter's use of `Buffer` for the same.
In debug-ui, the save states panel and the recording panel had a copy each of
"run this, show that it is running, show why it failed"; they share `useAction`
now. Renaming in place had a copy in the panel and another in the drawer, both
carrying a draft, a commit and the two keys; `EditableName` is that once, and an
uncontrolled field, so Escape is putting the name back rather than a second path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Following the same idea outward from save states and recordings. The two implementations of the protocol were each building the PPU views and the screen body by hand, and had already drifted: one read a client's numbers as numbers, the other trusted them, and each had its own base64. Both come from the protocol now, which is where the shape they must agree on belongs. The session announced a run in four places and started a paced one in two; `#beginRun` and `#runPaced` say it once. Pressing a button and setting the whole mask both spelled out when input reaches the machine and when it waits for the frame boundary; `#holdButtons` is that rule, in one place, with the reason. The adapter reported a breakpoint three times over and asked for its session twice; both are one function now. Two panels drew the same "when did this happen" columns of a log table, two views of a save state drew the same rename and delete, and a renderer wrote a pixel two ways. Each is one thing now, and the save-state pieces both views share live in a module of their own rather than inside one of them. The panels also had seven copies of one magic padding and three of another; those are two named classes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Debug a GBA ROM where the code is written: breakpoints in C, DWARF-typed variables, data breakpoints, replay-exact rewind, and the screen, PPU and I/O views beside the source. Built as four layers so a second editor costs only the thin top one.
What this adds
@gba-kit/debug-core@gba-kit/debug-adaptergba-kit/*custom requests, a frame/audio/input pipe, andgba-kit-screenfor editors with no display@gba-kit/debug-uiTransportseam, hosted by both a VS Code webview and the webappapps/vscode-extensionThe emulator, the DWARF reader and the disassembler gained what a debugger needs underneath: execution that stops on a predicate, bit-exact snapshot restore, a hardware event sink, read and write watchpoints, DWARF scopes, locations, values and call-frame unwinding, and statement-aware line queries.
The webapp's Debug page now runs on the same core and panels, sharing one machine with the Play page.
Notable design decisions
supportsStepBackis advertised only because step-back really is replay-exact; set-next-statement is not claimed.Verification
Audit
The branch was then reviewed by an adversarial multi-agent audit: 173 findings raised, 151 confirmed, 149 fixed across the last six commits, each fix independently re-checked. It found and fixed, among others, three reported defects: an "empty expression" notification on every breakpoint stop, enum constants unresolved in hover, and stop-recording opening an editor instead of the Recording tab.
Two defects survived that audit's own validation and were caught by re-running the suite by hand: reverse continue cached one expression environment per frame, so a condition was judged in another breakpoint's scope and a false hit stopped the machine; and a test compared a process-wide timer count for equality, so it failed under load.
Known gaps
session.breakpoints.replaceDatarecords a data breakpoint without installing a watchpoint. No shipped caller reaches it.blob:toworker-srcrather thanscript-srcfor the audio worklet.🤖 Generated with Claude Code