Skip to content

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 to step816 for VariantW65816 after the shared interrupt/halt boundary; step816 is a direct opcode switch (no table) that fetches at PBR:PC and operates on width-aware registers. The phase-1 Opcodes65816 table is retained only because bindTable still 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 Bus is 16-bit (Read(uint16)); the 65816 needs 16 MB.
  • Decision: a separate Bus24 interface (Read24/Write24 uint32), installed via SetBus24, that the 65816 core uses exclusively. For the runnable TUI, Bus24From16 mirrors the existing 16-bit MMIO/watchpoint bus into bank 0 (every bank aliases bank 0), so -cpu 65816 runs 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 CPU with the high halves (B, XH, YH, SPHi), the 16-bit D, the bank registers, and the E flag; mWide()/xWide() read M/X (P bits 5/4, native-only — locked set in emulation) and A16()/X16()/… compose the full registers. Emulation forces SPHi=$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],Y long 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 reforce SPHi=$01 at 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: blockMove moves the entire block in one Step (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 Disasm816 with 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). DisasmCPU dispatches to it for the 65816 variant.
  • Consequences: the disassembly length matches what step816 consumes; 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/RAM directly; 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/RAM access remains in render/nav. (2) control path (#471) — run/step/breakpoint/watchpoint enforcement is server-owned via a synchronous Server.RunBudget(maxSteps, step, stopAt) (chosen over an async continue so the local engine stays deterministic and re-entrant from the TUI's Update loop). 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/FrozenAddrs move the guard into MMIO.Write (a single length check that suppresses frozen addresses); Freeze writes the value through once then adds it to the set, so it works for peripheral- and RAM-mapped addresses alike. RAM.Freeze stays 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/dataBreakpointInfo expose memory watchpoints; setVariable writes 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/.hex onto the page wired to the existing loadUserFile path.
  • Consequences: a zero-install in-browser chippy at nkane.dev/chippy; .dbg symbol drag-drop deferred (the wasm load() API doesn't expose symbol loading yet).