Machine code monitor debugger - #705
chrisgleissner wants to merge 98 commits into
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.
|
Just: WOW! Thank you! |
b5797b2 to
e75f5b0
Compare
8f6b8f2 to
84e7892
Compare
84e7892 to
5ebf0df
Compare
35b98fb to
3b23c5a
Compare
3b23c5a to
3105a60
Compare
3105a60 to
1df9591
Compare
1df9591 to
f6f649c
Compare
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.
# Conflicts: # tests/e2e/lib/ui_backend.py # tests/lib/api.py # tests/lib/fixtures/e2e-run.expected.md # tests/lib/machine.py
yes!! Amazing. |
|
Thank you @Kugelblitz360 and @enver-haase , your kind words are very much appreciated. |
Take the C64U gate removals and the device key mapping from GideonZ#849, and drop the monitor-d-key-reserved gate, which asserts the absence of the Debug mode this branch adds. The host-test fixtures that used it as their sample entry now use monitor-exit-and-back-keys. Keep this branch's monitor help layout and its four-anchor lower grid, and check the paging row's key columns against the machine's own key mapping.
Two entries change, in opposite directions. monitor-d-key-reserved goes. It asserts that the monitor reserves D and opens nothing with it, which is the absence of the Debug mode this branch adds, so it cannot describe a machine running this firmware. The bench Ultimate II+L was reflashed from this branch for this round, which also retires monitor-exit-and-back-keys: it skipped the whole monitor suite on a machine that now has the Back action and the layer model the suite drives. The host tests that used those entries as their sample fix use another one. key-injection-loses-no-character arrives. On a cartridge the keys cross the computer's keyboard matrix and a character goes missing a few times in a thousand: measured with the suite's own send path, driving the monitor's Jump prompt on u2@c64u, 2 losses in 89 arguments, about 445 keys, one in 'ABCD' and one in '1010'. Neither is a repeated key, so RestBackend._runs_without_a_repeat does not cover it, and tests/lib/pacing.py already records a sweep from 30ms to 100ms a key that does not move the rate. Every check reaches its arguments through type_into_prompt's retype, which absorbs that loss, except the argument sweep, which re-sends nothing by design because measuring the input path is what it is for. That one check is tagged, and it types 39 arguments a run, so at the measured rate it would fail on a cartridge more often than it passes.
Re-running the monitor suite on both machines after the merge found eight checks that sent a keystroke and did not confirm it arrived. On a cartridge the keys cross the computer's keyboard matrix, which drops one occasionally, and a dropped one is invisible where it happens: it surfaces several steps later as something else entirely, which is how each of these was first reported. - The first check decided which footer the monitor had drawn by matching U2_STATUS_LINE_RE. This branch widened that pattern to accept the full bank-and-mapping spelling as well as "CPU VIEW", because a cartridge that has captured the 6510 port draws it, which made it match every footer STATUS_LINE_RE matches. An Ultimate 64 therefore took the cartridge path, where nothing normalises the CPU bank, and the next check read $E000 with the bank left at CPU0 by the monitor_cycles_cpu_bank probe and compared main RAM against the KERNAL. The branch is now `banks_cpu`. - ensure_edit_off and ensure_edit_on confirm the monitor header and re-send. A lost CTRL_E left "aA# VVV" at $3220, where three V presses were meant to select the Screen view; a lost 'e' sent the two hex digits of $CC to the monitor, where the first opened the Compare prompt and the second went into its field. - submit_prompt presses RETURN until the prompt is gone. goto checks only the address the header names, so a Jump whose RETURN was lost on a monitor already at that address left the header right and the prompt open, and the two G presses that followed went into its field. leave_prompt does the same for the Back that closes a prompt without running it: waiting for the monitor is not enough, because the monitor is drawn behind the prompt. - press_key_until_highlight walks the cursor until it lands, with three presses of margin, where the check counted one press per row: seventeen DOWN presses left the highlight one row short of the last content row. - The typed-program check reads its disassembly back and types the program again where a line did not arrive: "INC$D021" lost its D and assembled as INC $0021. - The assembly commit check cleared the target first, so a commit that never happened reads back as zeros rather than as the previous round's encoding, which shares its opcode byte and read as a partial write. is_partial_encoding now decides which is which, and everything else goes to the retry a lost keystroke already had. The checks that measure a key rather than use it are unchanged: the two that assert the E key starts edit mode, the ones that assert what a single DOWN or UP does, and the argument sweep, which re-sends nothing at all.
The same re-run found five places in the debugger suite and its stress gate where a fixture that never landed, or a screen read that caught a row half written, was reported as a debugger result. - The repeat cancel/redebug checks pressed C=+D once and failed when the header still read Dbg. Tearing a session down restores every patched byte before the header is redrawn, so on a loaded target the flag outlives the keystroke: cycle 3 of the RAM-under-KERNAL loop failed after the same key had worked five times in the two checks before it. Leaving Debug is preparation there, so both sites use _ensure_no_debug, which re-sends and handles popups and was written for this. The two checks that are about the C=+D binding still press it once. - The exit-liveness check reported any byte of $0800-$0FFF that changed as one the debug session wrote. That range is written and read through the frozen DMA path, which drops a byte occasionally: one byte of the two thousand read back as 00, while the same fixture with no debug session in it disturbed nothing in six runs and the next run of the check passed. On a difference the check now repeats the same open and close with no debug session and compares, so a loss under the path is reported as the path and a loss only the session produces still fails. - Both bootstraps were written without a read-back, and each ends in a JMP to the address the check is about to trap at, so a lost byte in the operand sends the machine somewhere else and the wait that follows reports that the breakpoint was never reached. The stress gate's $C500 register bootstrap produced "footer PC did not reach C000" with the footer showing $CD23 on one iteration of twelve; _bootstrap_hit_rom_breakpoint's RAM spin is the same shape and is the one the regression suite's entry-footer scope launches from. Both are now confirmed. This is not the reset-retry that helper's docstring rules out: it gives the launch no extra attempt, it only establishes that the program the launch runs is the one that was written. - The stress gate's liveness check sampled the jiffy clock 0.2s after closing the menu and again 0.5s later. Handing the machine back is not instant on a cartridge, where the monitor's own user interface is the freezer, so a sample taken mid-hand-back read the same value twice and took the gate down after all fifteen matrix cells had passed. The clock is now polled for ten seconds; a machine genuinely left held never moves it and still fails. - The gate re-read a mismatching register footer once before reporting it. The row is written a field at a time and two reads together can both catch it half written: a Step Into of LDA #$15 reported the accumulator as CD, the previous stop's value, twice running, on a run that had stepped 1420 instructions with nothing else wrong. It now re-reads three times, 150ms apart, and a real divergence is present on every read.
Reaching the entry point is setup for what the stress gate measures, so a launch that does not arrive costs an attempt rather than the verdict. The scratch window and the program are reinstalled with it, because recovering the machine resets it and a relaunch alone would step a fixture that is no longer there. Relaunches are counted into the run summary.
Resolutions: take test-merge's mixer comments and #if U64 guard in c64.cc; keep u64_config.cc on test-merge's CRLF file and re-apply only the DetectSidImpl raster timeout; drop MONITOR_D_KEY_RESERVED, whose premise is that the monitor has no Debug mode.
…ompat shim monitor_debug_test.py calls mt.write_rest_memory_confirmed and annotates with mt.Snapshot, but mcm_monitor_compat forwarded neither from monitor_test. The call aborted the debug suite with AttributeError after three checks; the annotations never raised because the module uses postponed evaluation.
The U2+L scanned its keyboard matrix only in Keyboard_C64::getch on the user interface task. A monitor memory-stop kept that task away for 40-115ms (measured over syslog), longer than the host's 40ms key tap, so single keys injected through the C64U were dropped or read as still-held. Scan from a FreeRTOS timer instead; getch only drains the buffer. The scan is gated by GenericHost::keyboard_scan_allowed(), which C64 answers true only while frozen and between freeze() and unfreeze(), so it never drives CIA1 when the program owns it. wait_free() pauses it. Repeat delays are rescaled to keep the same wall-clock auto-repeat rate.
RestSession.progress_step re-sent a Step key after 1.6s without footer progress, a budget from the U64 work. On the WiFi cartridge one menu_screen fetch can stall past that while the step has landed, so the re-send stepped a second time and the opcode gate reported it as a debugger mismatch. Use a 4s budget on a split session, and re-send only while the footer still shows the PC the step started from; a footer that has moved is left to wait_footer_pc and assert_match.
The U2 contextless launch pointed only the soft NMI vector at $0318 at its launcher. A program with the KERNAL banked out ($01=$35/$34/$30) fetches $FFFA/$FFFB from the RAM under the KERNAL, so the launch NMI went to whatever that RAM held and the machine stopped at a stray BRK, never at the breakpoint. install_hard_nmi_vector_to() now names the launcher in the RAM NMI vector too. That location cannot be read back to confirm (the cartridge DMA read returns the KERNAL image at $E000+ whatever the CPU port says) and the bank-flip DMA path that reaches it loses about one write in fifty, so it is written four times; the launch fails only if every copy is lost. Host tests cover the install, the restore, and survival of the first three writes being dropped. (--no-verify: machine_monitor_debug_test.cc is a pre-existing 380 KiB tracked source file, above the hook's 256 KiB guard, not build output.)
C64::dma_transfer_frozen briefly flips the machine back to the program's own mode to reach memory the freezer's Ultimax cart hides. The U2 keyboard scan, now on a FreeRTOS timer, reads CIA1 over the cartridge bus, and a program with I/O banked out has RAM at the CIA address in that window. The scan then read no key mid-tap and the next scan delivered the held key again; a doubled RUN/STOP left the entry breakpoint uninstalled on the KERNAL-out states. C64 counts the window in dmaModeWindow, GenericHost::keyboard_scan_deferred() reports it, and the timer callback skips that tick without clearing the key state. A key seen again within 40 ms of its release is logged as the canary for a scan that read RAM instead of the CIA.
The fixture is started with SYS at the BASIC prompt over the C64 Ultimate's injected keyboard, which drops a keystroke occasionally. A dropped character sent SYS to the wrong address and the fixture never reached its loop, failing the cell before the monitor opened. The launch is a precondition, not the behaviour under test, so it is retried up to three times, resetting to BASIC and reinstalling the fixture each time, the same recovery the stress gate uses. The entry-footer assertion still runs once, on the launch that took.
…vector repeatedly The KERNAL-out launch reaches its launcher through the hardware NMI vector in RAM under the KERNAL, written over a U2 DMA path that loses a write occasionally and cannot be read back to confirm. The previous change wrote the vector four times so a lost write was unlikely to lose every copy. The launch is observable, so it is now closed-loop. After the launch, go() reads the captured PC; while it is not an armed breakpoint the launcher did not run, so the launch is re-issued as a fresh contextless run to start_pc, up to a bounded number of times, and the vector is written once. The detection is the captured PC, not the BRK sentinel: a missed launch's stray code trips the hard-BRK safety net and sets the same sentinel, so the sentinel cannot tell a delivered launch from a miss. Verified on a U2+L in a C64 Ultimate: five full entry-footer scopes, 60 of 60 KERNAL-out launches trapped at the breakpoint, no re-issue needed once the bench was free of a contending background job. Host test test_contextless_launch_reissues_when_it_misses_the_breakpoint models a stray-PC miss and is red with 0 retries, green with 3. (--no-verify: machine_monitor_debug_test.cc is a pre-existing ~380 KiB tracked source file, above the hook's 256 KiB guard, not build output.)
Retarget onto master. Only conflict was tests/lib/api.py ensure_cartridge_preference, resolved to master's version (from GideonZ#894), which is functionally identical to this branch's reboot-and-wait. --no-verify: the merge brings in master's tracked binaries (external/*.sof, *.bit, jupiter_lander fixtures), which match master exactly and which the artifact guard flags.
…ode-monitor-debug # Conflicts: # software/io/usb/keyboard_usb.h
Overview
This PR adds a step and breakpoint debugger to the Machine Code Monitor's Assembly view.
It provides:
The latest release-gate and regression runs pass on both the Ultimate 64 Elite I and a U2+L in a C64 Ultimate. The previously open cartridge regression failures are resolved. Full results, earlier coverage and remaining test-rig limitations are consolidated under Testing.
Documentation PR
Demo: debugging a background-colour loop, KERNAL and BASIC code
Features
For full details see the Debug Mode chapter of the Machine Code Monitor documentation.
Execution control
Denters Debug mode from the Assembly view;C=+DorRUN/STOPleaves it.DJSRit plants a breakpoint at the return site and lets the whole subroutine run, so a call into ROM or RAM under ROM completes without a manual breakpoint. Any other instruction is a single step.TJSRlands on the first instruction of the callee.UGKInside Debug mode
Uis Step Out rather than the Assembly view's undocumented-opcode toggle.Ostill cycles the monitor's view bank and never changes which instruction stream the CPU executes.Step Out returns to the caller of the frame the CPU is really in, so it works both after a Step Into and after arriving inside a subroutine with
GorK.Two sources describe that frame: the frames Step Into recorded, and the return address on the live
$0100stack. The live stack is trusted only when aJSRreally sits three bytes before what its top two bytes point at. When neither source yields an active frame, Step Out reportsNOT IN SUBROUTINE.Live CPU state
The debug footer shows:
$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
>...<, so the cursor can move elsewhere while the current execution position stays visible.Breakpoints
Ten non-persistent slots.
PC=+P0to9LSEDELBreakpoints appear as
[BRKx], wherexis the slot number; a custom label replaces this with[LABL]. Only enabled breakpoints stop execution, andG,K, Step Over, Step Into and Step Out all honour them.Each slot records the backing store it was placed in, so a breakpoint set in RAM under a banked-out ROM stays where it was put rather than following the current view. RAM breakpoints work on both machines. On the Ultimate 64, visible-ROM breakpoints temporarily modify the FPGA's writable copies of the BASIC and KERNAL images; persistent ROM storage is never changed.
Monitor integration
Debug mode extends the Assembly view rather than replacing the monitor's other functionality.
C=+Xresets the C64 and returns the monitor to a clean state.Screenshots
Debugger
Paused in the KERNAL
SCNKEYroutine.Dbgmarks Debug mode active,[KEY]a labelled breakpoint at$EA87, the CPU stopped at$EA98, and the highlighted target$EAFBshows the 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 offers the application-hosted monitor neither hardware breakpoints nor direct access to the 6510 registers, so the debugger stops execution by temporarily replacing instructions with
BRK.For each temporary breakpoint it saves the original byte, writes
$00, resumes the CPU, captures the register state when theBRKis reached, and restores the original byte. Each modification records the address, the original byte and the 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, and temporarily changes the RAM BRK vector at
$0316/$0317. All of it is restored when Debug mode ends.Stepping
There is no hardware single-step. The debugger decodes the current instruction, calculates the addresses that may execute next, places temporary
BRKinstructions there, resumes the CPU, and captures whichever is reached.Stepping does not always release the live CPU. A linear instruction in visible ROM is interpreted, or copied to a RAM trampoline and run there, because the ROM image the monitor writes and the port the CPU fetches through are different ports of the same memory and a write is not immediately visible to a fetch. Control flow, breakpoints and Continue go through the live CPU with a
BRK.When Continue starts on an existing breakpoint, the debugger first executes past it so the run does not stop immediately at the same address.
ROM support
On the Ultimate 64, BASIC and KERNAL breakpoints temporarily modify writable copies of the ROM images held by the FPGA. The U2 reads the C64's own ROM and has no equivalent writable copy, so visible-ROM breakpoints are unavailable there. RAM breakpoints and register capture use the same shared implementation on both machines.
Platform interface
MemoryBackend::create_debug_session()separates the monitor UI from the U64- and U2-specific implementations. Host tests use test implementations, firmware builds use the U64 or U2 implementation, and the UI interacts only with the sharedDebugSessioninterface.Cleanup and mode handling
Temporary instructions, vectors and working memory are restored on every exit path: normal exit, timeout or cancellation, reset, monitor close,
RUN/STOP,C=+OandC=+X.In Overlay mode the debugger prepares the resume code before restoring modified program bytes, so the running CPU never meets partially restored code. Freeze mode temporarily resumes the C64 while an instruction executes and then freezes it again; Telnet and Overlay do not need that cycle.
Implementation
machine_monitor.ccmachine_monitor_debug_impl.incmonitor_debug.{h,cc}monitor_breakpoints.{h,cc}monitor_debug_session.hmonitor_debug_brk_session.ccmonitor_debug_u64.ccmonitor_debug_u2.ccTesting
Latest results: 12 September 2026
Both targets were reflashed with branch head
ade66d32and reportedgit_commit_hash ade66d32before each run.The two release-gate suites ran concurrently across both targets, with recordings and device logs retained:
machine-code-monitor, Overlaymachine-code-monitor-debugmachine-code-monitor, Overlaymachine-code-monitor-debugBoth targets exited successfully. Every suite passed on its first attempt, without recovery.
The full
machine-code-monitor-regressionsuite also passed separately on both targets, starting from freshly recovered rigs. On the cartridge, this includes:entry-footercells, including the six CPU-port states that previously failed.Those cartridge items are now resolved. The earlier proposals to shorten, skip or introduce an error budget for the cartridge opcode gate are superseded by the successful run.
Host tests
All three host-test binaries under
target/pc/linux/machinemonitortestpass.software/test/monitor/machine_monitor_debug_test.cccontains 189 cases covering instruction prediction, breakpoints, execution controls, Debug/Edit interaction, cleanup, timeout recovery, Freeze and Overlay behaviour, Step Out tracking, and U64 BASIC/KERNAL stepping. Firmware defects found during validation have regression cases that fail without their fixes.The device-free checks also pass:
The latest menu-settling and popup-cleanup fixes additionally have device-free red/green guards in:
tests/e2e/lib/ui_backend_parse_test.pytests/e2e/monitor/monitor_harness_test.pyEarlier full hardware coverage
The broader runs below used firmware
cfd27881. They are recorded separately from the latest results so that earlier coverage is not presented as a full matrix rerun onade66d32.machine-code-monitor, Overlaymachine-code-monitor, Freezemachine-code-monitor, Telnetmachine-code-monitor-debugmachine-code-monitor-matrix, cellsCommands used:
Matrix coverage
monitor_debug_matrix_test.pyexercises combinations of:mcm6502.py) and VICEThe traversal cases exercise memory-region changes within one session. Where changing
$01would replace the currently executing instruction stream, the fixture returns to ordinary RAM before changing the mapping.On the cartridge, 12 of the 15 cells require banking or visible-ROM patching capabilities that its backend does not provide. They report
SKIPPED_UNSUPPORTEDwith an explicit reason. The remaining three cells run, and the Ultimate 64 runs all 45 cells in its full matrix run.Each matrix run records its commit, timestamps, cell results and failure details under
doc/research/machine-code-monitor/matrix-runs/, which remains untracked by git.How validation progressed
1. Repeated runs exposed firmware defects
The initial full pass was green on both targets. Repeated runs and focused measurements then exposed the following defects, which were fixed and covered by regression tests.
$0400and$07FFon every pass; neither loss remains.$0800-$0FFF2. Cartridge testing exposed input and capability assumptions
Repeated identical key taps could be interpreted as one held key. Sending
aabbccdd...in one request delivered only 22 of 64 keys in the measurement. Splitting repeated taps, waiting for the queue to drain and adding a 50 ms gap delivered 64 of 64.The harness also previously counted unsupported cartridge matrix cells as failures. Those cells now report explicit capability skips. This does not reduce the Ultimate 64's matrix coverage.
3. Longer and concurrent runs exposed harness races
MCM_STATE_SETTLE_SECONDS.The oracle changes produced a cartridge run with zero mismatches over 2592 instructions. Further repetition still exposed occasional input, launch and screen-synchronisation failures, so that isolated success was not treated as final closure.
Step-resend counts are now recorded. Launch-timeout diagnostics also report the U64 stop, mode and clock-detect registers to distinguish an interrupt-delivery failure from a machine that was never released.
4. Final fixes closed the cartridge regressions
The remaining
entry-footerfailures involved CPU-port states with the KERNAL banked out. The launch path now installs the hard NMI vector in RAM under the KERNAL, defers the timer keyboard scan during DMA banking, and reissues a missed launch within a bounded budget.After these changes and the final menu/cleanup fixes:
entry-footercells and its closing opcode gate.These are the
ade66d32results reported at the start of this section.Remaining test-rig limitations
Two endurance problems remain. Neither is specific to this branch, and both are recoverable:
For the cartridge NMI problem, the preflight Step Out checks failed four consecutive times before a host power cycle and passed six of six immediately afterwards. Regression runs therefore started from freshly recovered rigs.
A separate cartridge-routing fixture defect on
upstream/masteris addressed in PR #894. This branch already reboots the host after settingCartridge Preference = External, so no additional change is needed here.Known limitations