Source-Level Debugging for the Atari Lynx

speckx1 pts0 comments

Source-Level Debugging for the Atari Lynx | Brian Peek

Skip to content

Source-Level Debugging for the Atari Lynx

August 7, 2026 &middot;<br>Atari Lynx,<br>GameDev,<br>Ganksoft,<br>Gaming

I work on several Atari Lynx projects, a handheld from 1989 with a 65C02 running at 4MHz and 64KB of RAM. One of the toolchains is cc65, which means you write C, it transpiles that to 6502 assembly, that gets compiled, and then… you debug it by staring at that assembly. Or you print things to the screen. Or other painful things. So I built a debugger on top of Gearlynx&rsquo;s debugger that allows for full C-source debugging rather than assembly only.

Gearlynx Debugger is a VSCode extension that gives you source-level debugging for Lynx games: breakpoints in your C file, real local variables, the call stack, the hardware state, and the game running live inside VSCode.

The Starting Point

Gearlynx is Nacho Sánchez Ginés&rsquo;s Lynx emulator, and it&rsquo;s excellent. It already ships a very capable debugger: a run-ahead disassembler, CPU and memory breakpoints, a memory editor, and hardware viewers for Mikey and Suzy.

But it is a 6502 debugger. Everything is an address. If you want to break on game_handle_input(), you go find its address in the map file, type it in, and then read disassembly. When you stop, you&rsquo;re looking at registers and raw memory, not at variables with names like x and y and gameState.

Meanwhile, VSCode already knows how to be a debugger UI. Breakpoints, call stacks, watch expressions, hover evaluation, the hex editor, and the disassembly view are all built in and driven by the Debug Adapter Protocol (DAP). cc65 already emits everything needed to map addresses back to source, if you pass the right flags. The missing piece was a way for those two to talk to each other.

Two Halves

The project ended up being two separate things:

A debug-monitor server inside Gearlynx : C++, a TCP server that exposes emulator state and execution control over a simple JSON protocol. This got upstreamed and ships in Gearlynx as of 1.2.15 .

The VSCode extension : TypeScript, a DAP adapter that speaks that protocol on one side and VSCode on the other, plus all the cc65 debug info parsing.

Splitting it this way means the emulator doesn&rsquo;t know anything about VSCode, cc65, or DAP. It answers questions about registers, memory, and breakpoints. Everything that requires understanding your source code lives in the extension.

The Gearlynx MCP Server Did the Hard Part

Gearlynx added Model Context Protocol support back in December 2025, so an AI agent could drive the emulator: set breakpoints, read memory, inspect Mikey and Suzy, step the CPU. Making that work meant giving the MCP server a programmatic handle on everything the ImGui debugger could already do, which is his DebugAdapter class: a plain C++ facade over the emulator core and the debugger&rsquo;s own state that hands back structs and JSON instead of drawing widgets. Execution control, breakpoints, memory areas, disassembly, hardware status, controller input, and trace logging.

That facade is exactly what a debug monitor needs, so I didn&rsquo;t write another one. The debug monitor is a second transport over Gearlynx&rsquo;s adapter, and almost every command in it is a thin translation:

m_debug_adapter = new DebugAdapter(core);<br>...<br>std::vectorDisasmLine> lines = m_debug_adapter->GetDisassembly(start, end, true);<br>json arr = json::array();<br>for (const DisasmLine& line : lines)<br>arr.push_back({<br>{"address", line.address},<br>{"name", line.name},<br>{"bytes", line.bytes},<br>{"size", line.size},<br>{"jump", line.jump},<br>{"jump_address", line.jump_address},<br>{"subroutine", line.subroutine}<br>});<br>return {{"lines", arr}};

It doesn&rsquo;t speak MCP directly since the DAP client wants different things. But the two servers sit side by side in emu.cpp, get initialized identically, and get pumped from the same place in the emulator loop:

emu_mcp_pump_commands();<br>emu_debug_monitor_pump_commands();

The interesting part is that the AI tooling work paid off in a completely non-AI way. Building the MCP server meant giving every debugger capability a name, a signature, and a return type that wasn&rsquo;t a UI widget. Once that boundary existed, hanging a second protocol off it was mostly plumbing, which is why the C++ side of this project is the small half.

VSCode Gearlynx<br>+-------------------+ TCP/JSON +---------------------+<br>| Gearlynx Debugger | | Debug Monitor |<br>| (DAP adapter) | port 6502 | Server |<br>+-------------------+ +---------------------+<br>| | TCP/binary | Framebuffer |<br>| Screen Viewer | | Server |<br>| (webview panel) | port 6503 | (60fps RGBA stream) |<br>+-------------------+ +---------------------+<br>| Emulator Core |<br>+---------------------+<br>The Wire Protocol

The debug monitor uses Content-Length: \r\n\r\n framing followed by UTF-8 JSON, which is the same framing DAP itself uses. Requests look like this:

"id": 12,<br>"cmd": "registers_get"

And responses echo the id back:

"id":...

debugger gearlynx rsquo debug line source

Related Articles