Machine code monitor debugger - #705
Draft
chrisgleissner wants to merge 20 commits into
Draft
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR introduces a debugger-capable Machine Code Monitor with breakpoint support across U64 and U2 targets, plus new automation tooling (repro scripts + soak test) and documentation updates to validate and explain the new debug behaviors.
Changes:
- Adds a Debug mode execution backend (BRK-based stepping, breakpoints, reset/re-entry orchestration) with target-specific implementations (U64/U2).
- Extends monitor UI/input handling for debug actions, global reset behavior, and updated status/banking display.
- Adds new deterministic repro scripts, soak testing, and updates docs/snapshots/build files to cover the new functionality.
Reviewed changes
Copilot reviewed 57 out of 61 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/developer/machine-code-monitor/snapshots/expected_snapshots.json | Updates expected CPU/view status line fragments to the new CxOy format. |
| tools/developer/machine-code-monitor/regression_repro.py | Adds deterministic REST-driven repro cases for monitor regressions. |
| tools/developer/machine-code-monitor/monitor_debug_soak.py | Adds a telnet-based debug soak test with a lightweight 6510 model comparison. |
| tools/developer/machine-code-monitor/issue_repro.py | Adds autonomous REST repro cases for current monitor blockers. |
| tools/developer/machine-code-monitor/README.md | Documents debug tests/soak usage and new environment variables. |
| target/u64ii/riscv/ultimate/Makefile | Builds new monitor debug/breakpoint sources for U64II RISC-V. |
| target/u64/riscv/ultimate/Makefile | Builds new monitor debug/breakpoint sources for U64 RISC-V. |
| target/u64/nios2/ultimate/Makefile | Builds new monitor debug/breakpoint sources for U64 Nios2. |
| target/u2plus_L/riscv/ultimate/Makefile | Builds new monitor debug/breakpoint sources for U2+L RISC-V. |
| target/u2plus/nios/ultimate/Makefile | Builds new monitor debug/breakpoint sources for U2+ Nios. |
| target/u2/riscv/ultimate/Makefile | Builds new monitor debug/breakpoint sources for U2 RISC-V. |
| target/pc/linux/machinemonitortest/Makefile | Adds PC-side machinemonitordebugtest suite and required sources. |
| software/userinterface/userinterface.h | Adds active monitor tracking and reset re-entry hook into HostClient. |
| software/userinterface/userinterface.cc | Implements global reset shortcut handling and wires it into keymapper. |
| software/userinterface/ui_elements.cc | Treats keymapper -2 as “global accelerator consumed” to exit popups. |
| software/u64/u64_machine.h | Adds raw/visible poke/peek variants and “preserving freeze restore” write. |
| software/u64/u64_machine.cc | Implements raw/visible memory access helpers and improves serve-control handling. |
| software/test/monitor/machine_monitor_test_support.h | Extends FakeKeyboard to allow pushing a key ahead of scripted input. |
| software/test/monitor/machine_monitor_test_support.cc | Implements FakeKeyboard push-head and updates UI string_edit stub signature. |
| software/test/monitor/machine_monitor_bookmarks_test.cc | Updates expected bookmark popup strings and key sequences for new flows. |
| software/monitor/u64_memory_backend.h | Adds reset/debug-session support and observed live CPU port tracking. |
| software/monitor/u64_memory_backend.cc | Updates U64 backend mapping semantics and creates U64 debug sessions. |
| software/monitor/u2_memory_backend.h | Adds reset/debug-session support for U2 backend. |
| software/monitor/u2_memory_backend.cc | Implements U2 reset and debug-session creation. |
| software/monitor/run_machine_monitor.cc | Reworks monitor lifecycle for reset re-entry and interface swap teardown. |
| software/monitor/monitor_init.h | Adds weak global-reset-cancel hook for monitor/debug cancellation. |
| software/monitor/monitor_file_io.h | Adds debug-context resume/staging APIs to safely hand off to execution. |
| software/monitor/monitor_file_io.cc | Implements U64 NMI trampoline helpers and staged NMI handoff paths. |
| software/monitor/monitor_debug_u64.h | Declares U64 debug session factory and helper for step CPU port. |
| software/monitor/monitor_debug_u64.cc | Implements U64-specific BRK debug session with volatile ROM patching support. |
| software/monitor/monitor_debug_u2.h | Declares U2 debug session factory. |
| software/monitor/monitor_debug_u2.cc | Implements U2-specific BRK debug session (no visible ROM patching). |
| software/monitor/monitor_debug_session.h | Introduces the DebugSession interface and result codes for debugger ops. |
| software/monitor/monitor_debug_predictor.h | Adds instruction classification for stepping prediction. |
| software/monitor/monitor_debug_predictor.cc | Implements predictor using fast opcode cases + disassembler length fallback. |
| software/monitor/monitor_debug_brk_session.h | Declares shared BRK-based debug session implementation and patch tracking. |
| software/monitor/monitor_debug.h | Defines DebugContext and MonitorDebug footer/help formatting API. |
| software/monitor/monitor_debug.cc | Implements debug footer layout + help text formatting. |
| software/monitor/monitor_breakpoints.h | Adds in-memory breakpoint table, labels, and popup formatting. |
| software/monitor/monitor_breakpoints.cc | Implements slot allocation, normalization, and popup row formatting. |
| software/monitor/memory_backend.h | Adds backing-store classification helpers and debug-session/reset hooks. |
| software/monitor/machine_monitor.h | Extends monitor state, disasm lane, debug/breakpoint UI plumbing and APIs. |
| software/monitor/disassembler_6502.h | Exposes operand_spec() for shared operand classification. |
| software/monitor/disassembler_6502.cc | Renames illegal mnemonics and refactors operand parsing to use operand_spec(). |
| software/monitor/assembler_6502.cc | Canonicalizes additional illegal mnemonic aliases during assembly lookup. |
| software/io/usb/tests/usb_keyboard_queue_test.cpp | Adds regression for Ctrl+R mapping distinct from cursor-down behavior. |
| software/io/usb/keyboard_usb.cc | Maps Ctrl+R to KEY_CTRL_R in control keymap. |
| software/io/stream/keyboard_vt100.cc | Adds Ctrl+R decoding from stream input (0x12 / ESC+r). |
| software/io/c64/keyboard_c64.cc | Maps matrix Ctrl+R to KEY_CTRL_R instead of PETSCII 0x12 collision. |
| software/io/c64/keyboard.h | Introduces KEY_CTRL_R and documents why 0x12 cannot be used. |
| software/io/c64/c64_subsys.cc | Cancels debug waits on reset and normalizes formatting/whitespace. |
| software/io/c64/c64.h | Adds begin/end stopped-session helpers and a refreeze() convenience. |
| software/io/c64/c64.cc | Adds pristine ROM snapshot/restore on reset + stopped-session helpers + refreeze(). |
| software/infra/host.h | Adds host callback to request reset re-entry after C64 reset. |
| doc/machine_code_monitor.md | Updates public documentation for modes, status line, edit/debug/breakpoints. |
Comments suppressed due to low confidence (4)
software/monitor/disassembler_6502.cc:1
- Branch opcode templates were changed to use an operand spec of
rel(e.g.\"BCC rel\",\"BNE rel\"), butoperand_length()/format_operand()no longer have the branch-special-case and also don’t recognizerel. This will cause branch instructions to disassemble with the wrong operand length and likely render an empty/incorrect operand/target, breaking both UI and any predictor logic that relies on disassembly output. Fix by handlingrelexplicitly (length=1 and formatting$%04Xtarget), or by reinstating a branch-specific path keyed offspec == \"rel\".
#include "disassembler_6502.h"
software/monitor/disassembler_6502.cc:147
- Branch opcode templates were changed to use an operand spec of
rel(e.g.\"BCC rel\",\"BNE rel\"), butoperand_length()/format_operand()no longer have the branch-special-case and also don’t recognizerel. This will cause branch instructions to disassemble with the wrong operand length and likely render an empty/incorrect operand/target, breaking both UI and any predictor logic that relies on disassembly output. Fix by handlingrelexplicitly (length=1 and formatting$%04Xtarget), or by reinstating a branch-specific path keyed offspec == \"rel\".
!strncmp(spec, "$nn", 3) || !strncmp(spec, "#", 1)) {
return 1;
}
return 0;
}
tools/developer/machine-code-monitor/issue_repro.py:1
- This line assigns
session.dump_ui_screen(...)intomdt.wait_stable_dump, overwriting the imported function/attribute on themonitor_direct_testmodule. That is almost certainly unintended and can break subsequent calls that rely onmdt.wait_stable_dump. Change this to only assign the frame (e.g.,frame = session.dump_ui_screen(...)) or call the real wait helper if you intended to use it.
tools/developer/machine-code-monitor/README.md:1 monitor_debug_soak.py(as added in this PR) does not define--copy-roms-to-ramor--yes-copy-romsarguments, so this example command is not runnable as documented. Either update the README to match the actual CLI flags, or add the missing argparse options and implement the described behavior.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
chrisgleissner
marked this pull request as draft
June 6, 2026 16:08
|
Just: WOW! Thank you! |
chrisgleissner
force-pushed
the
feature/machine-code-monitor-debug
branch
2 times, most recently
from
June 27, 2026 06:41
b5797b2 to
e75f5b0
Compare
chrisgleissner
force-pushed
the
feature/machine-code-monitor-debug
branch
2 times, most recently
from
July 21, 2026 21:55
8f6b8f2 to
84e7892
Compare
chrisgleissner
force-pushed
the
feature/machine-code-monitor-debug
branch
from
July 22, 2026 00:57
84e7892 to
5ebf0df
Compare
chrisgleissner
force-pushed
the
feature/machine-code-monitor-debug
branch
from
July 30, 2026 17:08
35b98fb to
3b23c5a
Compare
chrisgleissner
force-pushed
the
feature/machine-code-monitor-debug
branch
from
July 31, 2026 00:44
3b23c5a to
3105a60
Compare
chrisgleissner
force-pushed
the
feature/machine-code-monitor-debug
branch
from
July 31, 2026 06:09
3105a60 to
1df9591
Compare
chrisgleissner
force-pushed
the
feature/machine-code-monitor-debug
branch
from
July 31, 2026 07:03
1df9591 to
f6f649c
Compare
chrisgleissner
force-pushed
the
feature/machine-code-monitor-debug
branch
2 times, most recently
from
July 31, 2026 07:18
5018965 to
ab5c7ad
Compare
…-code-monitor-debug # Conflicts: # run-e2e-tests # tests/e2e/README.md
…ug' into feature/machine-code-monitor-debug # Conflicts: # run-e2e-tests
The hard BRK stub is installed in the KERNAL ROM image as well as in RAM under the KERNAL, but its forward vector at $03EE was seeded only from the RAM copy of $FFFE/$FFFF, which is $0000 on a normal machine. With a visible-ROM breakpoint armed, every jiffy IRQ of the running C64 entered the stub and was forwarded to $0000, so the CPU executed the 6510 port register as code and jammed before the launch NMI could be taken. Point the ROM copy's chain at the KERNAL entry it just saved. Remove the ROM fetch-coherency workaround built on the earlier misdiagnosis: the 150 ms mid-launch settle, the pre-launch BRK recommits, and DBG_ROM_ENTRY_UNCOHERENT with its E2E skip. The BRK is written once by install_brk_at, long before the CPU is released. U64 pulse_nmi_and_release now uses end_stopped_session_nmi like the U2 backend, so the request survives resume()'s un-stop. Contextless KERNAL entry: 1/10 before, 10/10 after. Full debug E2E run twice: 4 checks fixed, 0 regressions, 26 failures unchanged.
The Telnet remote session is a 60x24 VT100 screen (Screen_VT100:: get_size_x/get_size_y), not the physical 40x25 C64 display. The backend rendered it into a 40-column emulator, cutting off the columns where the monitor draws its Dbg/Edit/Undc flags. _ensure_no_debug() returns early when "Dbg" is absent from the header, so the suite silently never left Debug mode anywhere: the 6510 stayed parked in its spin loop and the machine looked dead. That is what the liveness, PC-not-reached and leftover-vector failures were. STATUS_LINE_RE matched only "CPU5 $A:...". format_status_line_impl also emits "C5O7 $A:..." when a view override is selected, so find_status_line could not locate the status row at all in the banked scenarios, which run in exactly that state. The breakpoint re-entry check armed $C300 and never removed it, which the post-suite hygiene check reports. Leaving Debug restores the patched byte but keeps the slot. Full debug suite on a U64 Elite: 57 passed / 5 skipped / 26 failed before, 87 passed / 0 skipped / 1 failed after. The remaining failure is an intermittent visible-ROM step, about 1 run in 5.
Firmware: guard the NULL parent window that release_host() leaves behind, rebuild UI objects on monitor reopen-after-reset, release the debug ownership token in the session destructor, and fully consume the reopen-debug one-shot. E2E: bind the matrix to the compat bridge, map newline for REST keys, classify Python errors as harness bugs, seed the oracle from live counter values, wait for BASIC ready before installing a fixture, assert breakpoint-slot hygiene per cell, add a 32-call straight-line Step Over run, and record every run in a cross-run ledger.
chrisgleissner
marked this pull request as ready for review
August 8, 2026 08:09
Step Out now picks between the frame Step Into recorded and the return address on the live 6510 stack, so it also works after arriving inside a subroutine with Go or Run to cursor. The live stack is used only when a JSR sits three bytes before what its top two bytes point at, and never when that address is the current PC. New E2E scenario proves it on hardware, plus three host cases covering the untraced frame, the JSR guard, and preferring the live stack over a stale traced one. Cleanup: drop the dead ROM_ENTRY_UNCOHERENT path from the matrix gate, two unused firmware helpers and seven unused test helpers, and tighten comments. Rewrite doc/machine_code_monitor.md against the code.
A subroutine that pushes after its JSR leaves bytes at the stack pointer that are not a return address, and those bytes can pass the JSR check by coincidence. The traced frame now wins that disagreement when the address its JSR pushed is still on the stack, so only a frame that has really returned loses to the live candidate. Also finish the ROM_ENTRY_UNCOHERENT removal in the run ledger, and rewrite the Debug chapter of the monitor manual for clarity.
chrisgleissner
marked this pull request as draft
August 10, 2026 13:01
Capture the 6510 port with an NMI stub so the monitor shows banking on entry; give the reading one lifetime, ended by unfreeze or reset. Fix the non-U64 build break, propagate dma_load failures, stop set_live_vic_bank clobbering CIA2 on a running machine, and make the G-repeat test start each iteration from a reset.
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.
Overview
This PR adds debugging support to the Machine Code Monitor's Assembly view.
It provides:
Current status
Ultimate 64 Elite I: all three monitor suites pass on firmware built from this branch and deployed over JTAG.
machine-code-monitor(Telnet, Overlay, Freeze)machine-code-monitor-debugmachine-code-monitor-matrixU2+L in a C64 Ultimate host: the lanes the cartridge supports pass.
--focus alerts--focus banking--focus entry-footermonitor_test.py --target u2Remaining validation:
monitor_debug_test.pygreen against the U2+L over Telnet. First measurement is 10 passed, 21 failed, 6 skipped; see "Outstanding work" below.Demo
The demo shows a small program cycling the background colour, followed by debugging through KERNAL and BASIC code:
https://youtu.be/ECsqq5HKPlE
Features
This chapter shows the main features of the debugger. For full details, please read the Debug Mode chapter of the Machine Code Monitor documentation.
Execution control
DJSR.TJSR.OGKDebugging supports:
Live CPU state
The debug footer shows:
$0314/$0315and$0318/$0319.Active flags and important values are highlighted. Branch targets are highlighted only when the branch will be taken.
The next instruction is marked in the Assembly view with
>...<, allowing the cursor to move elsewhere while the current execution position remains visible.Breakpoints
The debugger supports up to 10 non-persistent breakpoints.
RC=+R0to9LSEDELBreakpoints appear as
[BRKx], wherexis the slot number. A custom label replaces this with[LABL].RAM breakpoints work on both U64 and U2. On the U64, visible-ROM breakpoints temporarily modify the FPGA's writable copies of the BASIC and KERNAL ROM images. Persistent ROM storage is never changed.
Monitor integration
Debug mode extends the existing Assembly view without replacing the monitor's other functionality.
C=+Xresets the C64 and returns the monitor to a clean state.C=+DorRUN/STOP.Screenshots
Debugger
The debugger is paused in the KERNAL
SCNKEYroutine:Dbgindicates that Debug mode is active.[KEY]marks a labelled breakpoint at$EA87.$EA98.$EAFBshows that the current branch will be taken.Breakpoint list
The breakpoint popup follows the existing bookmark-list controls.
Debug help
Shortcuts whose meaning changes while Debug mode is active are shown at the top of the help screen.
Design
BRK-based debugging
The FPGA core does not provide hardware breakpoints or direct access to the 6510 registers for the application-hosted monitor. The debugger therefore stops execution by temporarily replacing instructions with
BRK.For each temporary breakpoint, it:
$00, the 6510BRKopcode.BRKis reached.Each modification records the address, original byte, and CPU-port state needed to restore it correctly. Debugger working memory and interrupt-vector locations cannot be used as breakpoint addresses.
The debugger temporarily uses the cassette buffer for its handler, resume code, NMI code, and working state. It also temporarily changes the RAM BRK vector at
$0316/$0317. All changes are restored when Debug mode ends.Platform interface
MemoryBackend::create_debug_session()separates the monitor UI from the U64- and U2-specific implementations.DebugSessioninterface.Stepping
The debugger has no hardware single-step support. Instead, it decodes the current instruction and calculates the addresses that may execute next.
Temporary
BRKinstructions are then placed at those addresses, the CPU resumes, and the debugger captures whichever breakpoint is reached.Step Out uses return addresses recorded when Step Into enters a
JSR, rather than relying solely on the current stack contents.When Continue starts on an existing breakpoint, the debugger first executes past it to avoid immediately stopping at the same address again.
ROM support
On the U64, BASIC and KERNAL breakpoints temporarily modify writable copies of the ROM images held by the FPGA.
The U2 reads the C64 ROM directly and has no equivalent writable copy, so visible-ROM breakpoints are unavailable on the U2. RAM breakpoints and register capture use the same shared debugger implementation on both devices.
Cleanup and mode handling
Temporary instructions, vectors, and working memory are restored on every exit path, including:
RUN/STOP.C=+O.C=+X.In Overlay mode, the debugger prepares the resume code before restoring modified program bytes. This prevents the running CPU from encountering partially restored code.
Freeze mode temporarily resumes the C64 while an instruction is executed, then freezes it again. Telnet and Overlay modes do not require this cycle.
Implementation
The main files are:
machine_monitor.ccmonitor_debug.{h,cc}monitor_breakpoints.{h,cc}monitor_debug_session.hmonitor_debug_brk_session.ccmonitor_debug_u64.ccmonitor_debug_u2.ccTesting
The monitor E2E tests live under
tests/e2e/monitor/and are registered with the repository-rootrun-testsrunner.monitor-harnessmonitor_harness_test.pymachine-code-monitormonitor_test.pymachine-code-monitor-debugmonitor_debug_test.pymachine-code-monitor-matrixmonitor_debug_matrix_test.pyUnit and host tests
All three host-side monitor suites under
target/pc/linux/machinemonitortestpass, covering the core monitor, bookmarks, and debugger.software/test/monitor/machine_monitor_debug_test.cccontains 174 cases covering instruction prediction, breakpoint handling, execution controls, Debug/Edit interaction, cleanup, timeout recovery, Freeze/Overlay behaviour, Step Out tracking, and U64 BASIC/KERNAL stepping.Debugger matrix
monitor_debug_matrix_test.pyis the main debugger release test. It exercises:The two traversal modes are important because normal debugging crosses memory-region boundaries rather than entering each region from a fresh bootstrap.
ram-rom-ramstarts in RAM, enters BASIC ROM at$BC0F, then returns to RAM.ram-rur-rom-ramtraverses RAM, RAM under ROM, visible ROM, and back again while switching$01between legs.A direct RAM-under-ROM to visible-ROM traversal is not practical because changing $01 while executing from the banked region immediately replaces the instruction stream being executed. The fixture therefore returns to ordinary RAM, changes the memory mapping there, and then enters ROM, matching how real 6510 code normally performs such transitions.
The traversal fixtures are also executed on the host through
mcm6502.py, which verifies the expected region sequence and catches broken fixtures before a hardware run begins.These traversal modes increase the matrix from 9 to 15 combinations per repetition. They are host-verified and now also pass on hardware.
Each matrix combination covers Step Over, Step Into, Step Out, Run to Cursor, breakpoint Continue, normal Continue, and Reset. Validation includes CPU state, memory effects, a 100-instruction comparison against both an independent 6510 interpreter and VICE, and a separate 1000-instruction live run.
Matrix results
Latest full run on an Ultimate 64 Elite I on 10 Aug 2026, firmware built from this branch after the
test-mergemerge and deployed over JTAG:The straight-line run complements the nesting chain: it repeats the same call from the same stack state, so a leaked breakpoint slot or a park and resume that drifts the stack shows up there and nowhere else.
Each run appends to a local ledger, by default under
doc/research/machine-code-monitor/matrix-runs/, which is not tracked by git. It records the commit, start and end times, per-cell status, and failure details in both Markdown and JSONL, so runs can be compared over time.Additional E2E coverage
The existing monitor regression suite remains available:
The broader debugger suite is run with:
It contains 89 checks covering stepping, breakpoints, memory-region entry, Continue behaviour, cleanup, and leaving Debug mode without affecting the monitor or C64.
The complete automatic E2E run is:
Known limitations
Outstanding work
Three items are open. None of them affects the U64 results above, and each is written up with its evidence in
doc/research/machine-monitor/debug/u2/handover-prompt-2026-08-10.md.1.
monitor_debug_test.pyagainst the U2+L over Telnet. First full measurement is 10 passed, 21 failed, 6 skipped. This lane has not been green before and is not part of a registered gate:run-testshas no--c64-host, so the split-host U2 setup is driven by scripts rather than the runner, and the matrix runs only a two-check subset of this suite as a preflight. That subset failing is what currently stops--focus matrixon the U2.The 21 failures are not one defect. They fall into three groups, and each needs to be resolved as a firmware defect, a stale test expectation, or a genuinely unsupported operation, rather than converted into a skip:
[BRKx][CPU]and[READ][CPU]on breakpoint rows. TheCPUtag beside each assembly row was deliberately removed, and with the banking now resolved on the U2 that tag readsRAM,BASorKRN. Checks 13 and 14 assert the earlier U64-shaped footer.CPU label row not found. The message is misleading: the code scans for the row holdingPC,SPandNV-BDIZC, so the actual symptom is that the debug register row never appeared.C=+Rbreakpoint popup, leaving Debug mode, and the combined Debug and Edit header.2. A stepping disagreement found by the stress oracle on the U2+L.
monitor_debug_stress.py --banking ram --focus stepsreportsscratch mismatch at $C8C7: oracle FE dev FD, on the same iteration at the same address on every run. It is reproducible rather than intermittent. Whether the fault is in the stepping engine or inmcm6502.pyis not yet established; running the same seed against the U64 distinguishes the two.3. The U64 volatile KERNAL image. The
banked-breakpointsgroup in the debugger suite leaves$E000reading$EEinstead of$85while reporting its own checks as passing, so the matrix has to start from a fresh firmware deployment. This is pre-existing rather than introduced here, proven by bisecting this branch's changes back out and observing it unchanged. All four firmware writers into the ROM window were instrumented and traced zero writes into$A000+during a reproducing run, which leaves the FPGA-level question of whetherU64_KERNAL_BASEand the address the 6510 fetches at$E000are the same copy.