Skip to content

Machine code monitor debugger - #705

Draft
chrisgleissner wants to merge 92 commits into
GideonZ:test-mergefrom
chrisgleissner:feature/machine-code-monitor-debug
Draft

Machine code monitor debugger#705
chrisgleissner wants to merge 92 commits into
GideonZ:test-mergefrom
chrisgleissner:feature/machine-code-monitor-debug

Conversation

@chrisgleissner

@chrisgleissner chrisgleissner commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator

Overview

This PR adds a step and breakpoint debugger to the Machine Code Monitor's
Assembly view.

It provides:

  • Step Over, Step Into, Step Out, Continue, and Continue To Cursor.
  • Up to 10 breakpoints.
  • Live CPU state and prediction of the next execution target.
  • Debugging of RAM, RAM under ROM, and, on the Ultimate 64, visible BASIC and
    KERNAL ROM.
  • Telnet, UI Overlay, and UI Freeze modes.

On an Ultimate 64 Elite I all four monitor suites pass. On a U2+L in a C64
Ultimate the monitor and debugger suites pass and all fifteen matrix cells
pass; the matrix's closing 1000-opcode gate is open there, and one scope of the
regression suite does not pass and did not pass before this round either. Full
figures, the exact commands and what is still open are under Testing.

Documentation PR: GideonZ/1541u-documentation#31

Demo, showing a program cycling the background colour and then debugging
through KERNAL and BASIC code: https://youtu.be/ECsqq5HKPlE


Features

For full details see the
Debug Mode
chapter of the Machine Code Monitor documentation.

Execution control

D enters Debug mode from the Assembly view; C=+D or RUN/STOP leaves it.

Action Key Behaviour
Step Over D Executes the instruction at the program counter. For a JSR it 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.
Step Into T Executes exactly one instruction. A JSR lands on the first instruction of the callee.
Step Out U Runs to the caller of the current subroutine and stops there.
Continue G Runs until an enabled breakpoint is reached.
Continue To Cursor K Plants a temporary breakpoint at the Assembly cursor and runs until it is reached. Enabled breakpoints on the way still stop the run.

Inside Debug mode U is Step Out rather than the Assembly view's
undocumented-opcode toggle. O still 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 G or K.
Two sources describe that frame: the frames Step Into recorded, and the return
address on the live $0100 stack. The live stack is trusted only when a JSR
really sits three bytes before what its top two bytes point at. When neither
source yields an active frame, Step Out reports NOT IN SUBROUTINE.

Live CPU state

The debug footer shows:

  • Program counter.
  • Accumulator, X register, Y register, and stack pointer.
  • Processor flags.
  • IRQ and NMI vectors at $0314/$0315 and $0318/$0319.
  • Predicted jump, branch, call, and return targets.

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.

Action Key
Toggle a breakpoint on the cursor line P
Open the breakpoint list C=+P
Jump to a breakpoint slot 0 to 9
Change its label L
Set it to the cursor address S
Enable or disable it E
Delete it DEL

Breakpoints appear as [BRKx], where x is the slot number; a custom label
replaces this with [LABL]. Only enabled breakpoints stop execution, and G,
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.

  • Memory, ASCII, Screen Code, Binary and Assembly views remain available.
  • Memory can be inspected or edited without ending the debug session.
  • Edit mode and Debug mode can be active at the same time.
  • C=+X resets the C64 and returns the monitor to a clean state.

Screenshots

Debugger

Paused in the KERNAL SCNKEY routine. Dbg marks Debug mode active, [KEY] a
labelled breakpoint at $EA87, the CPU stopped at $EA98, and the highlighted
target $EAFB shows the branch will be taken.

Debugger paused in the KERNAL SCNKEY routine

Breakpoint list

The breakpoint popup follows the existing bookmark-list controls.

Debugger breakpoint list

Debug help

Shortcuts whose meaning changes while Debug mode is active are shown at the top
of the help screen.

Machine Code Monitor debug help

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 the BRK is 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 BRK
instructions 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
shared DebugSession interface.

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=+O and C=+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

File Responsibility
machine_monitor.cc UI integration, keyboard handling, Debug/Edit interaction
machine_monitor_debug_impl.inc Debug-mode key handling and popups
monitor_debug.{h,cc} Debug state, footer formatting, help text
monitor_breakpoints.{h,cc} Ten-slot non-persistent breakpoint table
monitor_debug_session.h Shared interface for U64, U2 and host tests
monitor_debug_brk_session.cc BRK handling, stepping, byte restoration, return addresses, cleanup
monitor_debug_u64.cc U64 hardware access and visible-ROM support
monitor_debug_u2.cc U2 hardware access

Testing

Host tests

The three host binaries under target/pc/linux/machinemonitortest all pass.
software/test/monitor/machine_monitor_debug_test.cc holds 189 cases covering
instruction prediction, breakpoint handling, execution controls, Debug/Edit
interaction, cleanup, timeout recovery, Freeze and Overlay behaviour, Step Out
tracking, and U64 BASIC/KERNAL stepping. Every firmware defect listed below has
a case there that fails without its fix.

These need no device and all pass:

make -C target/pc/linux/machinemonitortest
python3 tests/lib/lint_test.py
python3 tests/lib/registry_test.py
python3 tests/lib/observability_test.py
python3 tests/lib/stale_gates_test.py
python3 tests/e2e/monitor/monitor_harness_test.py

Hardware suites

Both machines run the firmware this branch builds, reporting git_commit_hash cfd27881, the merge commit. Nothing under software/ changes after that
commit, so every later commit is test harness only and the firmware under test
is this branch's firmware.

Every run below is a single command, given in full so it can be repeated.

Ultimate 64 Elite I: all green

./run-tests u64 -s machine-code-monitor -m all --attempts 1
./run-tests u64 -s machine-code-monitor-debug -s machine-code-monitor-matrix -m telnet --attempts 1
Suite Result Runtime
machine-code-monitor, Overlay 63 of 63 checks 468s
machine-code-monitor, Freeze 63 of 63 checks 467s
machine-code-monitor, Telnet 63 of 63 checks 2146s
machine-code-monitor-debug 96 passed, 0 skipped, 0 failed 2455s
machine-code-monitor-matrix 45 of 45 cells, opcode gate passed 5890s

U2+L in a C64 Ultimate: green except the opcode gate

./run-tests u2@c64u -s machine-code-monitor -m all --attempts 1
./run-tests u2@c64u -s machine-code-monitor-debug -m telnet --attempts 1
./run-tests u2@c64u -s machine-code-monitor-matrix -m telnet --attempts 1
Suite Result Runtime
machine-code-monitor, Overlay 63 of 63 checks 643s
machine-code-monitor, Freeze 63 of 63 checks 595s
machine-code-monitor, Telnet 63 of 63 checks 1831s
machine-code-monitor-debug 70 passed, 26 skipped, 0 failed 1489s
machine-code-monitor-matrix, the 15 cells 3 run, 12 skipped, 0 failed 1832s
machine-code-monitor-matrix, the closing 1000-opcode gate open, see below

The cartridge skips are not failures. They are described under Test harness
changes, in "What a cartridge cannot be asked to do".

Still to be settled: the 1000-opcode gate on the cartridge

The matrix's fifteen cells pass on the cartridge on every run. The gate that
follows them steps about 1440 instructions and compares the debugger's register
footer against an independent 6502 interpreter, and it requires zero
unrecovered failures. On the cartridge it produces about one per run.

The cause is measured, and it is the transport rather than the debugger. Every
keystroke and screen read for a cartridge target crosses the C64 Ultimate's
keyboard matrix and the cartridge bus. Driving the monitor's Jump prompt
directly, 2 characters were lost in 89 typed arguments, about 445 keys. The
gate's own counter agrees: a clean run records step_resends: 16 against 1440
steps, so the harness silently recovers about sixteen losses per run and
occasionally one gets past the resend budget.

Six consecutive runs each produced one unrecovered event, and a different one
each time: a liveness sample taken mid hand-back, a register footer read while
the row was half written, twice, a launch that stopped somewhere other than the
entry point, twice, and a step that reported no progress. Each was fixed on its
own terms and none recurred. The Ultimate 64, which has no matrix in its input
path, passes the same gate twice with 45 of 45 cells.

So the remaining question is not a defect to find but a contract to choose for
this gate on a cartridge:

  1. Scope the gate to machines that serve machine:input themselves and report
    SKIPPED_UNSUPPORTED on a cartridge with the measured loss rate as the
    reason, which is how the matrix already treats the twelve cells a cartridge
    cannot run.
  2. Shorten the cartridge's --opcode-run so the expected unrecovered count
    sits well below one.
  3. Give the gate a stated error budget on a split target, tolerated and
    reported.

Nothing else in this PR is waiting on that decision.

Not re-run this round

machine-code-monitor-regression is a selection out of the other three suites.
All three were run in full instead, so it was not run again. Its entry-footer
scope is discussed under Outstanding work.

The Telnet monitor lane is markedly slower than Overlay or Freeze on both
machines because every screen read is a full 60-column redraw over the network.
On the cartridge every keystroke additionally crosses the C64 Ultimate's
keyboard matrix and the cartridge bus.

What the matrix covers

monitor_debug_matrix_test.py is the main release test. It exercises

{Telnet, UI Overlay, UI Freeze}
x
{RAM, RAM under ROM, visible ROM,
 RAM->ROM->RAM, RAM->RAM-under-ROM->ROM->RAM-under-ROM->RAM}

The two traversal modes matter because real debugging crosses memory-region
boundaries rather than entering each region from a fresh bootstrap.
ram-rom-ram starts in RAM, enters BASIC ROM at $BC0F, and returns to RAM.
ram-rur-rom-ram traverses RAM, RAM under ROM, visible ROM and back while
switching $01 between 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 returns
to ordinary RAM, changes the mapping there, and then enters ROM, which is how
real 6510 code makes that transition.

Each cell covers Step Over, Step Into, Step Out, Continue To Cursor, breakpoint
Continue, normal Continue and Reset, and validates CPU state and memory effects
against both an independent 6510 interpreter (mcm6502.py) and VICE. Each run
then executes a separate 1000-instruction live stress run. On the last Ultimate
64 run that gate stepped 2592 instructions with no mismatch; on the cartridge,
2592 instructions, also with none.

Each run appends to a ledger under
doc/research/machine-code-monitor/matrix-runs/, untracked by git, recording
the commit, start and end times, per-cell status and failure details.


Defects found and fixed

The first full pass on both machines reported green. Re-running it found the
following, each now fixed, with the measurement that showed it.

A monitor edit inside screen RAM did nothing while the freezer was up

C64::peek and C64::poke serve $0400-$07FF from the copy taken at freeze
time, because the freezer draws its own menu into screen RAM.
C64::dma_transfer_frozen did not: it served that range from live RAM while
already serving $0800-$0FFF and $D800-$DBFF from their copies. The monitor
reads through the first path and writes through the second, so an edit in screen
RAM went into the menu the freezer was displaying, never appeared in the view,
and was overwritten when the machine was released.

Measured on a U2+L before the fix: $0400 read 20, DF was written, the view
still read 20 and machine:readmem read DF. All three ranges now go through
one helper, frozen_backup_for, so a read and a write of one address reach the
same memory. The monitor suite's boundary sweep reported two write losses at
$0400 and $07FF on every pass before the change and none after.

A debug session corrupted about 100 bytes of RAM at $0800

U64Machine::poke_visible_preserving_freeze_restore also wrote
ram_backup[address] for addresses below $0400. ram_backup holds
$0800-$0FFF, so restore_io() copied those bytes into the user's RAM when the
machine was released. The debugger's handler, trampolines and vectors live in
$0300-$03FF, and every parked resume writes $0000 and $0001 through this
path, so $0800 and $0801, the first two bytes of a BASIC program, took the
CPU port values.

Measured on an Ultimate 64: a pattern was written to $0800-$0FFF with the
machine running, one debug session was opened and closed, and 101 bytes had
changed, starting $0800 A5 -> 2F and $0801 A4 -> 37. The helper is removed
and its callers use poke_visible, which needs no special case because
restore_io() restores nothing below $0400. The same measurement after the
change reports 0 bytes changed, and
Exit-liveness: a debug session leaves $0800-$0FFF alone now asserts it.

A visible-ROM step behaved differently over Telnet

The interpreted and trampolined route for a linear step in visible ROM was
selected by !debug_owner.remote, so Overlay and Freeze took it and Telnet did
not. Over Telnet the same step wrote a BRK into the KERNAL image and released
the live CPU onto it. Which user interface owns the session does not change
which CPU runs the step or which port serves its fetch, so the transport test is
removed and every transport takes the same route.
test_remote_visible_rom_linear_step_keeps_brk_out_of_rom_image fails with the
transport test present.

Leaving the monitor on a cartridge left the 6510 held

run_machine_monitor.cc released ownership after a Go only under
#if defined(U64). Every other target called release_host(), which takes down
the UI objects but does not unfreeze: only
release_ownership() -> C64::unfreeze() -> C64::resume() writes C64_STOP back
to 0. On a cartridge the monitor's UI is the freezer, so leaving Debug with a G
dropped the interface while the machine stayed held, and a host reset could not
reach the cartridge's own stop. The guard now matches the deferred-Go block a
hundred lines above, which already handed back on every target.

A reset left the session believing its handler was still installed

A reset rebuilds the KERNAL's soft vector table, which puts $0316/$0317 back
on the KERNAL's own BRK handler at $FE66. The session went on recording that
it had installed its own handler there, and save_and_install_handler()
returned early whenever handler_installed was set. The next launch therefore
armed a BRK that trapped into the KERNAL, nothing set the sentinel, and the
launch sat until the five-second watchdog and reported DEBUG TIMEOUT.

Measured on an Ultimate 64: $0316/$0317 were written with 5D 03, the
debugger's handler address, a machine:reset was issued, and they read back
66 FE.

A reset reaches the session by two routes, and both are fixed.
request_reset_cancel() handles the reset the monitor performs itself; when the
session is parked it deliberately writes nothing, because those addresses hold
the code the CPU is executing, but it also left the installed-flags set, and that
branch now calls forget_installed_state(). The reset button, a host reset and
SYS 64738 never reach request_reset_cancel() at all, so
save_and_install_handler() now reads $0316/$0317 and reinstalls the handler,
its trampoline and the vector when they no longer name the handler, instead of
trusting the flag. test_reset_while_parked_makes_the_next_launch_reinstall and
test_reset_behind_the_session_makes_the_next_launch_reinstall cover the two
routes and each fails without its change.

The breakpoint list moved the debug context

Picking a slot with a digit key called apply_go_local with an address the
cursor was not on, which sets debug_cursor_override, which makes the next
Continue start at that address instead of resuming the captured stop. Pressing
RETURN on the same slot did not, because it set the cursor first. Reading the
breakpoint list must not cost the session its context, so both keys now place the
cursor before the jump. Two host tests pin the two paths to the same behaviour.

The header ran Poll and Dbg together

Poll is drawn at width-9 and is four characters; Dbg is drawn at
width-8. With both active the header read PDbg. P is the breakpoint key
once Debug owns the keyboard, so poll mode could not be turned off from there
either. Entering Debug now clears poll mode, which also stops a parked machine
being redrawn on a timer.

The original Ultimate II paid for a monitor it does not build

C64::freeze() called capture_cpu_port_via_nmi() on every non-U64 target.
That reading exists for the monitor's banking display, and target/u2 builds no
monitor sources. The five targets that do build the monitor now define
MACHINE_MONITOR=1 and the call is compiled out elsewhere. The U2 image drops
from 810,872 to 809,304 bytes against a 811,008-byte partition, so this branch
costs that target 120 bytes rather than 1,688, and its freezer no longer pays a
stop, NMI and restore round trip it cannot use.


Test harness changes

A cartridge loses one of two identical key taps

Keys for a cartridge target are queued as taps on the keyboard matrix of the
computer it is plugged into, and the cartridge reads that matrix once per call to
Keyboard_C64::getch(). Between two taps of one key inside a single request the
matrix is empty for one 20 ms tick. A scan that does not land in that gap sees
the same matrix position on both sides of it, reads one key held down, and
suppresses the second tap as auto-repeat. Two different keys are never confused,
which is why the existing key-injection instrument never saw this: it walks an
alphabet.

Measured on u2@c64u, typing into the monitor's ASCII edit page and reading the
bytes back, 16 keys a run:

text how it was sent arrived
aabbccdd... one request 22 of 64
aabbccdd... split at each repeat, previous queue waited out, 150 ms idle 64 of 64
aabbccdd... the same, 50 ms idle 64 of 64
aabbccdd... the same, 20 ms idle 64 of 64
abcdefgh... one request 64 of 64

RestBackend._post_events splits a batch wherever an event repeats the one
before it, waits for the queue the previous run left behind, then waits
pacing.SPLIT_REPEATED_KEY_GAP_SECONDS before posting the next. Waiting for the
drain is what carries this: splitting alone, with the next request posted
immediately, still lost 15 of 64. The gap is set to 50 ms, since the sweep shows
nothing is bought by waiting longer. A device target has no matrix in the path
and sends the events as one request.

What a cartridge cannot be asked to do

Twelve of the matrix's fifteen cells cannot run on a U2+L, and the suite used to
report them as failures. U2MemoryBackend::supports_cpu_banking() returns false,
because a DMA read of $0001 returns a mirror refreshed only at reset, so the
monitor has no bank view from which to place a RAM-under-ROM breakpoint.
supports_visible_rom_patching() is overridden to true only in the U64 backend,
so a BRK cannot be written into a visible ROM image. The firmware refuses both,
with BRK $E000 IN ROM BLOCKS DEBUG, and that refusal is correct.

Four memory modes depend on one of those capabilities: ram-under-rom and rom
place their entry breakpoint where the cartridge cannot patch, and ram-rom-ram
and ram-rur-rom-ram step into the JSR $BC0F in BASIC, which sets a breakpoint
on a call target inside the ROM image and is answered with DEBUG NOT SUPPORTED.
Those cells now finish as SKIPPED_UNSUPPORTED with the reason printed, and are
excluded from the failure tables rather than counted as findings. The decision is
keyed on the split-session flag, so a single-host run skips nothing: the Ultimate
64 still runs all 45 cells.

Two runs at once

Running the two machines' suites concurrently used to fail. run-tests numbers
the runs it starts itself in E2E_PORT_SLOT, but numbers them from zero per
invocation, so a separately started Ultimate 64 run and U2+L run were both
offered slot 0 and their VICE oracles fought over 127.0.0.1:6518. The loser
reported VICE oracle setup failed: timed out, which reads as a device fault and
is not one.

The slot is now claimed rather than assumed. E2E_PORT_SLOT is the preferred
starting point, so a lone run keeps the layout it was given, and a run that finds
that slot taken moves to the next free one. The claim is an exclusive lock the
kernel releases if the run is killed, and the ports are probed as well, because a
VICE left behind by a killed run holds its port without holding a lock. Verified
directly: four concurrent claims take four distinct ports, and a deliberate port
squatter is stepped over.

Two further faults surfaced only under the load of a second suite, and both were
the harness misreading a screen it had failed to fetch rather than a debugger
defect. select_monitor_bank() pressed its bank key even when the status read
came back empty, so it cycled the view while blind and walked away from the bank
it had been asked for; an unreadable screen is now retried as a read, and only a
parsed status spends a keypress. STATE_SETTLE_TIMEOUT_SECONDS was a fixed 12
seconds, after which _await_snapshot() returns the stale snapshot and the
caller reports a missing footer; it can now be raised for a loaded run with
MCM_STATE_SETTLE_SECONDS.

Keeping the oracle in step with the device

The opcode-volume gate compared the debugger's register footer against the
independent 6510 oracle after waiting for the footer's PC to reach the oracle's
PC. A JSR nest passes through every address twice, once descending and once as
the RTS chain unwinds, and the two visits differ by the frame the JSR pushed, so
matching on PC alone could accept the wrong visit and then report the other
one's registers as a divergence. Measured on the cartridge: one mismatch in each
of five consecutive runs at about 2500 instructions a run, against none in 1440
steps of the same seed on an Ultimate 64. The wait now requires the stack pointer
the step must land on as well as the PC, and a mismatch is confirmed by one
further read before it is reported, since a real divergence survives a second
read and a row caught mid-update does not. The gate then reported 0 errors over
2592 instructions.

Two explanations were tested and discarded first. A duplicated keystroke was
ruled out by counting re-sends, which the summary now records: every mismatch
happened with step_resends: 0. A guard that confirmed every footer read, rather
than only a mismatching one, produced more failures than it prevented and was
reverted.

Diagnostics

wait_for_sentinel calls log_launch_timeout_state() before returning
DBG_TIMEOUT. The U64 backend prints C64_STOP, C64_MODE and
C64_CLOCK_DETECT, which separate a launch whose interrupt request never
produced an edge from one whose machine was never released. The three registers
are read without side effects, on a path that has already failed.


Known limitations

  • Conditional breakpoints, watchpoints and CPU execution history are not
    supported.
  • Breakpoints stop only between instructions, and each debugging operation has a
    fixed maximum wait time.
  • Visible-ROM breakpoints are supported only on the Ultimate 64, because the U2
    has no writable copy of the C64 ROM for the debugger to modify temporarily.
  • On the U2+L the CPU port is read by running a short stub on the 6510 through
    the NMI vector. While the machine is frozen that reading cannot go stale,
    because the CPU is halted; on a machine left running it is the port as sampled
    when the monitor opened.

Outstanding work

One suite failure is open, on the cartridge only. The
machine-code-monitor-regression suite runs a split-session-only scope called
entry-footer, which asserts the debug footer on the monitor's first frame for
six CPU-port states across four VIC banks. On the U2+L, 12 of its 24 cells fail:
the three port states that leave the KERNAL banked out ($30, $34, $35)
fail on all four VIC banks, and the three that leave it mapped ($33, $37,
and $37 with a DDR variant) pass on all four. The failure is
entry breakpoint: footer PC did not reach C000, so the entry breakpoint does
not trap when the program runs with the KERNAL banked out.

This is not a regression from this round. The same scope failed 4 of 24 cells on
an earlier run in this round, and 12 of 24 now. The change is in the harness
robustness described above, not in the firmware, and the scope has not been
green on the cartridge at any point in this round. It is reported here rather
than skipped, because unlike the twelve matrix cells this scope is not blocked
by a missing hardware capability: $FFFE with the KERNAL banked out is RAM, and
a cartridge can patch RAM. Whether the fault is in the hard-vector install or in
the U2's sampled CPU-port reading has not been established, and no claim is made
here about which.

The Ultimate 64 does not run this scope: it exists specifically because the
matrix skips monitor bank selection on a cartridge.

Two rig endurance problems also remain, neither specific to this branch and both
recoverable:

  1. The Ultimate 64 stops answering on the network after roughly four to eight
    back-to-back matrix runs and needs a JTAG redeploy.
  2. The C64 Ultimate stops delivering the cartridge's NMI after sustained
    testing, which surfaces as unrelated-looking failures across the debug
    checks, and needs a host power cycle. Measured: the two Step Out checks the
    matrix preflight runs failed four times in a row before one, and passed six
    of six immediately after.

The runs reported above were each started on a freshly recovered rig for that
reason.

Copilot AI review requested due to automatic review settings June 6, 2026 16:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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\"), but operand_length()/format_operand() no longer have the branch-special-case and also don’t recognize rel. 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 handling rel explicitly (length=1 and formatting $%04X target), or by reinstating a branch-specific path keyed off spec == \"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\"), but operand_length()/format_operand() no longer have the branch-special-case and also don’t recognize rel. 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 handling rel explicitly (length=1 and formatting $%04X target), or by reinstating a branch-specific path keyed off spec == \"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(...) into mdt.wait_stable_dump, overwriting the imported function/attribute on the monitor_direct_test module. That is almost certainly unintended and can break subsequent calls that rely on mdt.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-ram or --yes-copy-roms arguments, 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.

Comment thread doc/machine_code_monitor.md Outdated
Comment thread doc/machine_code_monitor.md
@chrisgleissner
chrisgleissner marked this pull request as draft June 6, 2026 16:08
@chrisgleissner chrisgleissner changed the title Add debugger to machine code monitor Machine code monitor debugger Jun 6, 2026
@Kugelblitz360

Copy link
Copy Markdown

Just: WOW! Thank you!

@chrisgleissner
chrisgleissner force-pushed the feature/machine-code-monitor-debug branch 2 times, most recently from b5797b2 to e75f5b0 Compare June 27, 2026 06:41
@chrisgleissner
chrisgleissner force-pushed the feature/machine-code-monitor-debug branch 2 times, most recently from 8f6b8f2 to 84e7892 Compare July 21, 2026 21:55
@chrisgleissner
chrisgleissner force-pushed the feature/machine-code-monitor-debug branch from 84e7892 to 5ebf0df Compare July 22, 2026 00:57
@chrisgleissner
chrisgleissner force-pushed the feature/machine-code-monitor-debug branch from 35b98fb to 3b23c5a Compare July 30, 2026 17:08
@chrisgleissner
chrisgleissner force-pushed the feature/machine-code-monitor-debug branch from 3b23c5a to 3105a60 Compare July 31, 2026 00:44
@chrisgleissner
chrisgleissner force-pushed the feature/machine-code-monitor-debug branch from 3105a60 to 1df9591 Compare July 31, 2026 06:09
@chrisgleissner
chrisgleissner force-pushed the feature/machine-code-monitor-debug branch from 1df9591 to f6f649c Compare July 31, 2026 07:03
@chrisgleissner
chrisgleissner force-pushed the feature/machine-code-monitor-debug branch 2 times, most recently from 5018965 to ab5c7ad Compare July 31, 2026 07:18
chrisgleissner and others added 7 commits July 31, 2026 12:12
…-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.
@chrisgleissner
chrisgleissner marked this pull request as ready for review September 1, 2026 09:22
@chrisgleissner
chrisgleissner marked this pull request as draft September 1, 2026 09:27
@chrisgleissner
chrisgleissner marked this pull request as ready for review September 1, 2026 17:55
@chrisgleissner
chrisgleissner marked this pull request as draft September 3, 2026 07:51
@enver-haase

Copy link
Copy Markdown
Contributor

Just: WOW! Thank you!

yes!! Amazing.

@chrisgleissner

chrisgleissner commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator Author

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.
@chrisgleissner chrisgleissner added 3.16 Targets 3.16 release enhancement labels Sep 9, 2026
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.)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3.16 Targets 3.16 release enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants