ADR 0010 — v1.7.0: TUI-via-DAP flip, freeze-beyond-RAM, and the full 65816 core¶
- Status: Accepted
- Release: v1.7.0 (2026-06-25; epic #458)
- Theme: Finish the TUI-via-DAP migration, extend the debugger facilities (data breakpoints, freeze beyond RAM), ship the WASM playground, and land the headline feature — a complete, Tom Harte-validated WDC 65C816 core (#456).
D1–D7 cover the 65816 core (the headline, and the most load-bearing decisions); D8–D11 cover the debugger/TUI and packaging work that rounds out the release.
D1 — A second execution core (step816), not an opcode-table variant¶
- Context: the existing variant seam (
c.opcodes *[256]Instr, dispatched per-CPU) carried NMOS/CMOS/NES because they share an 8-bit datapath, 16-bit addressing, and a fixed cycle-per-mode shape. The 65816 breaks all three: 16-bit-capable A/X/Y/SP/D, a 24-bit address space, bank registers (DBR/PBR), and width-dependent (M/X) operand sizes and cycle counts. - Decision: give the 65816 its own interpreter.
Step()branches tostep816forVariantW65816after the shared interrupt/halt boundary;step816is a direct opcode switch (no table) that fetches at PBR:PC and operates on width-aware registers. The phase-1Opcodes65816table is retained only becausebindTablestill binds it, but it is dead for execution. - Consequences: zero hot-path cost for the 8-bit cores (the branch is one variant compare). The 65816 logic is fully isolated in
cpu/{cpu816,addr816,arith816,ctrl816,exec816,disasm816}.go. The trade-off — opcode behavior isn't shared with the 6502 cores — is acceptable because almost nothing is genuinely shared once widths and 24-bit addressing enter.
D2 — A distinct 24-bit bus (Bus24), bridged to the 16-bit bus for the TUI¶
- Context: chippy's
Busis 16-bit (Read(uint16)); the 65816 needs 16 MB. - Decision: a separate
Bus24interface (Read24/Write24 uint32), installed viaSetBus24, that the 65816 core uses exclusively. For the runnable TUI,Bus24From16mirrors the existing 16-bit MMIO/watchpoint bus into bank 0 (every bank aliases bank 0), so-cpu 65816runs bank-0 programs through the same RAM the panels render and the reset vector resolves normally. - Consequences: the core stays bank-correct and is validated against the full 24-bit Harte memory; the TUI runs emulation-mode and bank-0 native programs today. Cross-bank accesses alias to bank 0 — a documented limitation until a bank-aware bus and bank-aware panels land (future work, not part of the core).
D3 — Width model on the existing CPU struct¶
- Context: 16-bit registers must coexist with the 8-bit fields the other cores use.
- Decision: extend
CPUwith the high halves (B,XH,YH,SPHi), the 16-bitD, the bank registers, and theEflag;mWide()/xWide()read M/X (P bits 5/4, native-only — locked set in emulation) andA16()/X16()/…compose the full registers. Emulation forcesSPHi=$01. - Consequences: the 8-bit cores ignore the new fields (zero/unused); the width helpers make every opcode kernel a single width-branch.
D4 — Validate final state + cycle count against Tom Harte (not per-cycle bus)¶
- Context: the 65816 Harte corpus (pin
dff67125) is 512 files (256 opcodes × emulation/native) with 16-bit state and a 24-bit-address/pin-flag cycle list. - Decision: the harness (
TestHarte65816,//go:build harte) validates the final register/memory state plus the cycle count for all 256 opcodes in both modes — not the per-cycle bus trace (a later accuracy pass, mirroring how the 6502/65C02 bus traces followed their state suites). The exact cycle model (width penalties, DL≠0, indexed page-cross, indexed-write/RMW extra cycle, no decimal-mode penalty) was reverse-engineered from the corpus. - Consequences: 254 of 256 opcodes are state-and-count exact in both modes (MVN/MVP excepted — D6). CI downloads only the implemented-opcode subset.
D5 — Emulation-mode direct-page and stack quirks, pinned empirically¶
- Context: the 65816's emulation mode has irregular wrap behavior that documentation states inconsistently; the corpus is ground truth.
- Decision: encode exactly what Harte shows: (1) the DL=0 page-wrap applies to the direct-page base offset, after which pointer bytes increment flat 16-bit — except the
[dp],Ylong pointer, whose three bytes page-wrap (readDPLongWrap), while plain[dp]stays flat; (2) the new 16-bit stack instructions (PEA/PEI/PER/PHD/PLD/JSL/RTL) use a full 16-bit SP mid-instruction and reforceSPHi=$01at the end, while the legacy ops page-wrap within page 1; (3)JMP/JSR (abs,X)read the pointer high byte wrapping within the program bank; (4)PEI's direct-page word read page-wraps where the(dp)addressing modes do not. - Consequences: these are the kind of details no spec captures correctly; pinning them against the corpus is the only reliable path, and each is documented at its call site.
D6 — MVN/MVP as a whole-block move; excluded from the Harte harness¶
- Context: the Harte corpus caps each block-move test at ~100 cycles, leaving the instruction partway through (a generator artifact, not silicon behavior).
- Decision:
blockMovemoves the entire block in oneStep(7 cycles/byte) for debugger sanity, rather than re-running the opcode per byte as silicon does. MVN/MVP are validated by a dedicated unit test, not the harness. - Consequences: the 256-opcode core is functionally complete and debugger-friendly; the one place chippy deliberately diverges from the corpus is documented and unit-tested.
D7 — Width-dependent disassembly¶
- Context: the 65816 immediate length depends on the live M/X state (LDA # is 2 or 3 bytes).
- Decision: a dedicated
Disasm816with its own 256-entry mnemonic/mode table reads the CPU's current M/X/E to size immediates and renders the new syntaxes (long$123456,[dp],sr,S,MVN src,dst).DisasmCPUdispatches to it for the 65816 variant. - Consequences: the disassembly length matches what
step816consumes; the TUI disassembly panel renders 65816 programs correctly in bank 0.
D8 — Complete the TUI-via-DAP flip: panels render and control flow through DAP (#461, #471)¶
- Context: the TUI historically read
cpu.CPU/RAMdirectly; the migration to source every panel from DAP snapshots (so the local and remote debug paths share one rendering/control surface) was the last v1.7 architecture item. - Decision: (1) render path (#461) — all five panels (registers, flags, stack, memory, disassembly) plus navigation read DAP-sourced snapshots; zero direct
cpu/RAMaccess remains in render/nav. (2) control path (#471) — run/step/breakpoint/watchpoint enforcement is server-owned via a synchronousServer.RunBudget(maxSteps, step, stopAt)(chosen over an async continue so the local engine stays deterministic and re-entrant from the TUI'sUpdateloop). The rich TUI rewind is kept as the documented local-engine exception; single-step and memory-edit stay direct (low value to route through DAP). - Consequences: the local TUI and a remote DAP client render and drive execution through the same protocol surface; the flip is
internal/-only, so the public Go API stays additive and v1.7.0 remains a minor bump.
D9 — Freeze beyond RAM: bus-level write-suppress (#463)¶
- Context: the debugger freeze facility (
RAM.Freeze, #422) was RAM-only, but a CPU write to a peripheral/cart-mapped address never reaches RAM (MMIO intercepts it), so RAM-level freeze couldn't hold those values. - Decision:
MMIO.Freeze/Unfreeze/Frozen/FrozenAddrsmove the guard intoMMIO.Write(a single length check that suppresses frozen addresses);Freezewrites the value through once then adds it to the set, so it works for peripheral- and RAM-mapped addresses alike.RAM.Freezestays for direct-RAM contexts. - Consequences: hosts (nessy) can freeze peripheral/cart values; zero hot-path cost when nothing is frozen (perfgate green).
D10 — DAP data breakpoints + setVariable parity (#453, #454)¶
- Context: the DAP surface lacked watchpoint and write-back parity with the editor protocol.
- Decision:
setDataBreakpoints/dataBreakpointInfoexpose memory watchpoints;setVariablewrites Globals scalars and array children. - Consequences: editors get full watchpoint + variable-edit support against the same engine the TUI uses.
D11 — Hosted WASM playground (#457)¶
- Context: chippy already built a WASM target and a
web/playground deployed to GitHub Pages. - Decision: finish the last UX item — drag-and-drop a
.bin/.prg/.hexonto the page wired to the existingloadUserFilepath. - Consequences: a zero-install in-browser chippy at nkane.dev/chippy;
.dbgsymbol drag-drop deferred (the wasmload()API doesn't expose symbol loading yet).