Debugger: Route cpu.stepInto's stepping state through the CPU thread

The WebSocket debugger's cpu.stepInto handler ran entirely on the WebSocket
handler thread, directly manipulating breakpoints and stepping state (via
Core_RequestCPUStep, g_breakpoints.SetSkipFirst, etc.) that's otherwise only
ever touched from the CPU thread (the one that calls Core_RunLoopUntil, and
thus indirectly NativeFrame).

Adds Core_RunOnCPUThread() - queues a function to run on the CPU thread and
blocks the caller until it's done. The queue is drained at the top of
Core_RunLoopUntil()'s loop, so it's reached continuously (in a tight spin)
while the CPU is stepping/paused, and at least once per call even while fully
running.

cpu.stepInto is the first consumer: once the CPU is already stepping, the
breakpoint/stepping manipulation is now routed through Core_RunOnCPUThread
instead of happening directly on the WebSocket thread. The "not currently
stepping" path still calls Core_Break() directly from the WebSocket thread,
since it's already documented free-threaded and is what makes the CPU thread
start reaching the queue-drain point in the first place.

More WebSocket debugger commands can be converted the same way going forward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hqm11k99viLfbJm2MkH4BH
This commit is contained in:
Henrik RydgårdandClaude Sonnet 5 committed 2026-08-08 11:21:46 +02:00
1 parent b1f0112cef
commit c6fccefa49
3 files changed
+108 -19

No files matched your search

+27 -19
View File
@@ -89,32 +89,40 @@ void WebSocketSteppingState::Into(DebuggerRequest &req) {
if (!currentDebugMIPS->isAlive())
return req.Fail("CPU not started");
if (!Core_IsStepping()) {
// Core_Break() is explicitly free-threaded (see Core.cpp), so no need to bounce this to the CPU
// thread - and we can't anyway, since Core_RunOnCPUThread() is only drained once the CPU actually
// *is* stepping, which this call is what triggers in the first place.
Core_Break(BreakReason::DebugStepInto, 0);
return;
}
uint32_t threadID;
auto cpuDebug = CPUFromRequest(req, &threadID);
if (!cpuDebug)
return;
// The CPU is already stepping/paused, so the CPU thread is spinning in Core_ProcessStepping() and will
// promptly drain this. Route the actual breakpoint/stepping manipulation there instead of poking at it
// directly from this WebSocket handler thread.
Core_RunOnCPUThread([&] {
uint32_t threadID;
auto cpuDebug = CPUFromRequest(req, &threadID);
if (!cpuDebug)
return;
if (cpuDebug == currentDebugMIPS) {
// If the current PC is on a breakpoint, the user doesn't want to do nothing.
g_breakpoints.SetSkipFirst(currentMIPS->pc);
if (cpuDebug == currentDebugMIPS) {
// If the current PC is on a breakpoint, the user doesn't want to do nothing.
g_breakpoints.SetSkipFirst(currentMIPS->pc);
Core_RequestCPUStep(CPUStepType::Into, 1);
} else {
uint32_t breakpointAddress = cpuDebug->GetPC();
PrepareResume();
// Could have advanced to the breakpoint already in PrepareResume().
// Note: we need to get cpuDebug again anyway (in case we ran some HLE above.)
cpuDebug = CPUFromRequest(req);
if (cpuDebug != currentDebugMIPS) {
g_breakpoints.AddBreakPoint(breakpointAddress, true);
AddThreadCondition(breakpointAddress, threadID);
Core_Resume();
Core_RequestCPUStep(CPUStepType::Into, 1);
} else {
uint32_t breakpointAddress = cpuDebug->GetPC();
PrepareResume();
// Could have advanced to the breakpoint already in PrepareResume().
// Note: we need to get cpuDebug again anyway (in case we ran some HLE above.)
cpuDebug = CPUFromRequest(req);
if (cpuDebug != currentDebugMIPS) {
g_breakpoints.AddBreakPoint(breakpointAddress, true);
AddThreadCondition(breakpointAddress, threadID);
Core_Resume();
}
}
}
});
}
// Step over the next instruction (cpu.stepOver)