Queue CPU step requests instead of rejecting all but the first

Only one step can be carried out per pass through Core_ProcessStepping(), so
roughly one per host frame. A second request arriving before that was rejected
outright - "Can't submit two steps in one host frame" - with no step performed,
which put the burden on every caller to notice and retry. A script firing five
cpu.stepInto in a row advanced one instruction and logged four errors.

They queue now, up to 8 deep; past that something is looping and it says so
rather than growing without bound. Five stepIntos advance five instructions.

The queue is deliberately *not* cleared by Core_Break(). That looks like the
obvious place for it - stopping for another reason should abandon a pending
plan, the way the temporary breakpoint and the runUntilTime deadline are
dropped there - but completing a step-over or step-out goes *through*
Core_Break(), since their temporary breakpoint is what stops us. Clearing there
would throw away everything after the first entry of any sequence. It's cleared
on CoreLifecycle::STARTING instead, so a step queued against the game that just
went away can't run against the new one.

g_cpuStepCommand keeps its existing double duty as both "the step in flight"
and "why we're stopped" (reason/relatedAddr, read by Core_GetSteppingReason),
so Core_Break()'s override check for an in-progress Over/Out is unchanged.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
This commit is contained in:
Henrik RydgårdandClaude Opus 5 committed 2026-08-18 09:32:04 +02:00
1 parent d72623c4a0
commit c0545658bc
2 files changed
+39 -11

No files matched your search

@@ -87,8 +87,8 @@ static DebugInterface *CPUFromRequest(DebuggerRequest &req, uint32_t *threadID =
//
// Response (same event name) with no extra data on success. A cpu.stepping event follows once
// the step completes.
// May fail (same-thread case only) if another step/run request is already pending this host
// frame - safe to retry shortly after.
// May fail (same-thread case only) if too many steps are already queued, which means a client is
// firing them faster than they can possibly be carried out.
//
// Note: any thread can wake the cpu when it hits the next instruction currently.
void WebSocketSteppingState::Into(DebuggerRequest &req) {
@@ -114,11 +114,10 @@ void WebSocketSteppingState::Into(DebuggerRequest &req) {
// If the current PC is on a breakpoint, the user doesn't want to do nothing.
g_breakpoints.SetSkipFirst(currentMIPS->pc);
// Core_RequestCPUStep() can fail (a step or run request is already queued this host
// frame - see its own "Can't submit two steps in one host frame" log). On failure no
// step ever happens and no cpu.stepping event ever fires, so a rejected step would
// otherwise be indistinguishable from one still in flight - the acknowledgement every
// request now gets doesn't tell those apart. Surface it as an error instead.
// Steps queue up now, so this only fails once the queue is full - a client stepping
// far faster than frames go by. No step happens and no cpu.stepping event fires in
// that case, which would otherwise be indistinguishable from one still in flight, so
// surface it as an error rather than leaving the caller waiting.
if (!Core_RequestCPUStep(CPUStepType::Into)) {
req.Fail("Could not step: a step or run request is already pending");
return;