Skip to content

ADR 0015 — v1.12.0: 65816 tail — bank navigation + interrupt-timing validation

  • Status: Accepted
  • Release: v1.12.0 (2026-07-10)
  • Theme: Draining the 65816 follow-up epic (#517) — bank-navigation UX in the playground and the TUI, plus per-cycle validation of hardware-interrupt timing. Per-release decisions fold in here.

D1 — Playground bank selector (#521/#523)

  • Context: the WASM playground exposed the 65816 (ADR 0014 D3) but its memory/disassembly panes were bank-0 only — no way to inspect banks 1-255 of a running 65816 program in the browser (the TUI already had :bank).

  • Decision: widen jsReadMem/jsDisasm (cmd/chippy-wasm) to 24-bit addresses via a readByte24 helper (banks 1-255 through Banked24, bank 0 through RAM) and a bankBus (a 16-bit cpu.Bus over readByte24) for cpu.DisasmCPUAt; jsState exposes pbr. The web UI (web/) gains a bank input, shown only for the 65816, feeding the memory pane; disassembly anchors at PBR:PC from state; both panes render $BB:XXXX.

  • Consequences: the browser panes reach the same bank-aware inspection the TUI has. The Go plumbing reuses the DAP-tested DisasmCPUAt + bank-view path, so there's no new disassembly logic — only the JS/wasm glue.

D2 — Pinned cross-bank disassembly (#520/#524)

  • Context: cross-bank disassembly (ADR 0013 D2) followed the live program bank PBR, so a pinned view (disasm-follow off) still used the live PBR — you couldn't hold or scroll the disassembly in a bank ≠ the one executing.

  • Decision: add DisasmAnchorBank (persisted) and a disasmBank() helper — live PBR when following, the pinned bank otherwise — threaded through syncDisasm/disasmView/disasmScroll (the first pin captures the live PBR). A new :da command ($XXXX / $BB:XXXX / symbol, via parseDisasmTarget) pins the view to any bank:offset.

  • Consequences: the pinned disassembly is a full 24-bit view; the header and row addresses show the pinned bank. 8-bit cores are unaffected (disasmBank() returns 0). Cross-bank scrolling within the snapshot still clamps at bank edges (the DAP window is per-bank) — :da is the cross-bank jump.

D3 — Hardware-interrupt per-cycle bus trace (#519/#525)

  • Context: the 65816 core is Tom Harte-validated for all 256 opcodes (state
  • full pin bus trace), but the corpus is opcodes-only — it never exercises hardware IRQ/NMI. serviceInterrupt816 (ADR 0014 D1) was cycle-count correct but emitted no bus cycles for the two leading internal cycles of interrupt entry, so nothing validated the per-cycle sequence.

  • Decision: emit the two io816() internal cycles (dummy reads at PBR:PC, pinNone) before the stack writes, per the WDC sequence. The 7/8-cycle count is added abstractly, so this surfaces only on the bus trace. A new non-Harte cpu/interrupt816_test.go (irqTraceBus recorder) pins the full sequence against a hand-authored WDC reference: 2 internal → stack writes (PCH/PCL/P, native +PBR, pinData) → 2 vector reads (pinVector = VDA+VPB), for emulation IRQ and native NMI.

  • Consequences: interrupt entry is now bus-exact, not just count-exact. serviceInterrupt816 is unreachable from brk/cop, so the Harte state/count/bus-trace harnesses are unaffected. The remaining epic item, #518 ABORT, stays parked — no abort source exists in-tree to drive it.