Machine code monitor debugger - #705
Draft
chrisgleissner wants to merge 1 commit 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
2 times, most recently
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
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 to the Machine Code Monitor's Assembly view.
It provides:
Current status
The main debugger functionality is implemented and has been exercised across Telnet, UI Overlay, and UI Freeze modes.
One limitation is still open: a first-time jump into a BASIC or KERNAL breakpoint can miss the temporary
BRK. It is now detected, bounded and reported, but not removed, and it needs an FPGA-side change. RAM-under-ROM stepping is not affected by it. See "First-fetch handling for ROM breakpoints" and "Remaining work".Demo
The demo shows a small program cycling the background colour, followed by debugging through KERNAL and BASIC code:
https://youtu.be/ECsqq5HKPlE
Target branch
This PR builds on the Machine Code Monitor work already merged into
test-mergethrough PRs #358 and #704. Its target branch is thereforetest-merge, notmaster.Features
Execution control
DJSR.TJSR.OGKDebugging supports:
Live CPU state
The debug footer displays:
$0314/$0315and$0318/$0319.Active flags and important values are highlighted. A branch target is highlighted only when the branch will be taken.
The next instruction is marked in the Assembly view with
>...<. This allows 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.
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 that change meaning 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 writing
BRKinstructions.For each temporary breakpoint, the debugger:
$00, the 6510BRKopcode.BRKis reached.Each change records the address, original byte, and CPU-port state required 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.DebugSessionoperations.Stepping
The debugger does not have hardware single-step support. Instead, it decodes the current instruction and calculates the addresses that may execute next.
It then places temporary
BRKinstructions at those addresses, resumes the CPU, and captures the breakpoint that is reached.Step Out uses return addresses recorded when Step Into enters a
JSR. It does not depend only on reading the current stack.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. Visible-ROM breakpoints are therefore 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 process.
First-fetch handling for ROM breakpoints
Problem
A contextless visible-ROM entry means jumping from a small RAM program directly to a BASIC or KERNAL breakpoint at an address that has not already executed during the current debug session.
In this situation, the U64 can occasionally execute the original ROM byte instead of the newly written
BRKon the first fetch.The relevant C64 FPGA core is supplied to this repository as a prebuilt file, and its source is maintained elsewhere. The first-fetch behaviour therefore cannot be corrected directly in the FPGA code within this PR.
Previous test behaviour
Earlier versions of the tests reset the C64 and tried again when this happened.
A reset did not make the first fetch reliable. It only repeated the operation from a clean start until it happened to succeed.
The tests also waited for less time than the full firmware operation could require. Some slow but successful entries were therefore incorrectly counted as misses.
The reset-and-retry behaviour has been removed.
Current handling
The firmware now:
DBG_ROM_ENTRY_UNCOHERENTif the second attempt also misses.ROM BP ENTRY MISSED - RUN CODE FIRST.The small RAM launcher now writes
$2F/$37to$00/$01before entering ROM. This restores the expected C64 memory configuration before both the first attempt and any second attempt.The tests now wait for the complete
go()operation:ROM_ENTRY_UNCOHERENT.A host-side test also fails if reset-based retries, command replay, or session replay are added back to the main debugger tests.
Defects found in that handling
Testing the handling above on hardware showed it did not actually take effect. Three defects were found and fixed.
The miss was never reported.
wait_for_sentinel()counted its owndelay_ms(5)steps instead of measuring elapsed time. The same loop polls the cancel keyboard, and on a Telnet session that poll blocks for the socket's 200 ms receive timeout, so each nominal 5 ms iteration really took about 205 ms. A 900 ms budget therefore ran for about 37 seconds and a 5000 ms budget for over three minutes. A forced miss produced no output at all for 45 seconds. The loop now measures elapsed time withgetMsTimer(), and the same forced miss reportsROM BP ENTRY MISSEDafter 3.1 seconds. This path is only reached when the breakpoint is not hit, so successful operations are unaffected.The settle was skipped after the first debug session. The fetch-path settle and the
DBG_ROM_ENTRY_UNCOHERENTclassification were gated onlaunch_ctx == 0. In that branchlaunch_ctxis only relaunch metadata, andcleanup_to_context()leaves aresume_contextseed behind after every parked Debug exit, solaunch_ctxwas non-null for every ROM entry after the first Debug session of a monitor lifetime. Both the settle and the honest reporting were therefore lost exactly where they were needed. The gate is now the installed patch set. It requires all installed breakpoints to be in visible ROM, so a contextless step, which also installs its predicted next program counter in RAM and can legitimately stop there, is not misreported as a ROM first-fetch miss.The settle was too short. Measured on a U64 Elite as the hit rate over repeated first-time ROM entries: 33/40 at the original 50 ms, 39/40 at 150 ms, and 18/20 at 250 ms. The refresh saturates around 150 ms, which is now the value used. Pairing the visible-ROM launch with the raster-synced stop used for RAM-under-ROM was also measured and made the hit rate much worse (1/20), so that pairing was left as it was.
One test-side defect was fixed alongside them: a check skipped for a first-fetch miss left its entry breakpoint armed in ROM, and a later check then trapped on that leftover
BRKinstead of its own target. The skip path now clears the breakpoint it armed.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
Test suites
The monitor E2E tests are under
tests/e2e/monitor/and registered in the repository-rootrun-e2e-testsrunner.monitor-harnessmonitor_harness_test.pymachine-code-monitormonitor_test.pymachine-code-monitor-debugmonitor_debug_test.pymachine-code-monitor-matrixmonitor_debug_matrix_test.pyOther files in this directory are shared test code rather than standalone suites:
mcm6502.pymcm_rest.pymcm_localui.pymcm_split_rest.pyoverlay_lifecycle.pymonitor_debug_stress.pyfreeze_reentry_guard.pygen_interpreter_vectors.pyUnit tests
All three host-side monitor suites under
target/pc/linux/machinemonitortestpass:software/test/monitor/machine_monitor_debug_test.cccontains 174 cases covering:Hardware environment
Tested on:
The underlying Machine Code Monitor was also tested on an Ultimate II. Debugger-specific testing on U2 hardware remains outstanding.
Main debugger matrix
tests/e2e/monitor/monitor_debug_matrix_test.py, invoked through themachine-code-monitor-matrixselector, is the main debugger release test.It covers:
The last two are boundary-traversal modes, added because the first three each
enter their region cold from a bootstrap and then stay inside it. That is not
how the debugger is normally used. A developer debugs their own program in RAM
and reaches ROM by stepping into it, so these modes start with a live context in
a RAM fixture and cross a region boundary mid-trace:
ram-rom-ram$BC0F. Step Into crosses RAM to ROM, Step Out crosses ROM back to RAM.ram-rur-rom-ram$01between legs.A bank switch cannot execute from the window it is switching, so a direct
RAM-under-ROM to visible-ROM step is not expressible on a 6510: the
STA $01that maps KERNAL back in would change the bytes the CPU is fetching. The
traversal therefore returns to RAM between the two banked regions, which is what
real code does as well.
Each traversal step is checked against the same oracles as the rest of the
matrix, and the per-cell
boundary-walk-evidence.jsonrecords the programcounter and stack pointer at every crossing.
monitor_harness_test.pyalso executes both fixtures throughmcm6502.pyonthe host, asserting the region sequence, that every step of the walk is really
reached in order, and that the liveness loop keeps running. A broken fixture
therefore fails in under a second instead of part-way through an hour-long
hardware run.
These modes take the matrix from 9 to 15 combinations at one repetition. They
are implemented and verified on the host; they have not yet been run on
hardware.
Each combination executes:
The checks include:
An earlier run passed all 18 combinations, but its visible-ROM checks still used the now-removed reset retry. It remains useful evidence for the deterministic stepping behaviour, but not for first-time ROM entry.
Current testing without reset retries includes:
After the first-fetch defects were fixed, the full matrix was rerun without reset retries:
ROM_ENTRY_UNCOHERENT, so the visible-ROM entries genuinely completed rather than being tolerated.PRODUCTION READY.ROM_ENTRY_UNCOHERENT.Read that result with the same caution as the broad suite: the matrix verdict treats
ROM_ENTRY_UNCOHERENTas a non-failing terminal status, so a green matrix does not by itself prove that first-time ROM entry succeeded. It has to be read together with theROM_ENTRY_UNCOHERENTcount, which was zero here. One repetition per combination is also a single sample against a per-entry miss rate of roughly one in twenty.Core monitor regression suite
This runs
tests/e2e/monitor/monitor_test.py.Latest result:
It covers:
Gexecution.Broad debugger suite
This runs
tests/e2e/monitor/monitor_debug_test.py.The suite contains 86 checks covering:
Before the defects described under "First-fetch handling for ROM breakpoints" were fixed, long runs stopped at different first-time BASIC or KERNAL entry checks. The same behaviour occurred on the branch before the rebase, so the rebase did not introduce it: building and deploying the pre-rebase tip and running the same harness against it failed in the same way.
All 86 checks now complete. Three consecutive runs, each on a fresh JTAG deployment:
No run was free of skips, and a skipped check verifies nothing. Every skip is a first-time visible-ROM entry that missed the first fetch. The suite exits zero because the miss is the documented limitation rather than a debugger defect, so between 2 and 4 of the 86 checks did not exercise the behaviour they exist to cover. The affected checks vary between runs.
Across all completed runs the skips landed on these checks, most frequent first:
[52]BASIC ROM Step Over on visible JSR[51]KERNAL ROM Step Into on visible JSR[50]KERNAL ROM Step Over on visible JSR[55]KERNAL$E002G continues to BASIC$BC0F[49]KERNAL ROM Step Into from$E000[03],[54],[56]Every one is a visible-ROM entry. No RAM-under-ROM check was ever skipped or failed in any run. RAM-under-ROM breakpoints are written to RAM, which is coherent, so they do not take the settle path and a timeout there stays a plain
DBG_TIMEOUT; a host test enforces that distinction.Full automatic test run
All 11 automatic suites passed in 748 seconds.
Remaining work
Known limitations
BRK. Here, "first-time" means that the target ROM instruction has not already executed during the current debug session. Measured at roughly one entry in twenty on an otherwise idle device, and more often when the C64 is running code that rarely fetches from ROM, because the fetch-path settle depends on the CPU actually executing ROM while it runs. The debugger reports the miss and stays usable, but the operation the user asked for does not happen; in the E2E suites the affected checks are skipped, so they verify nothing on that run. RAM and RAM-under-ROM breakpoints are unaffected.VHDL Change
@GideonZ The final item under "Known Limitations" is the one outstanding issue that may require a VHDL-side fix.
The firmware writes the temporary
BRKinto the U64's writable ROM image before resuming the 6510. In rare cases, the first instruction fetch still receives the previous ROM byte instead of the newly writtenBRK.The firmware now detects this case, makes one bounded second attempt without resetting the C64, and reports
DBG_ROM_ENTRY_UNCOHERENTif the breakpoint is still missed.The application-side firmware has no direct way to force the 6510 instruction-fetch path to observe the updated ROM byte. It also has no hardware program-counter breakpoint facility to use instead. A complete fix may therefore require a change in the FPGA C64 core, most likely where ROM-image updates become visible to the 6510 fetch path.
The chapter "First-fetch handling for ROM breakpoints" in this PR description contains more details.