Debugger: Route BreakpointSubscriber, DisasmSubscriber, and most of

CPUCoreSubscriber through the CPU thread

Continues the pattern started with SteppingSubscriber: route breakpoint,
disassembly, symbol, and register access through Core_RunOnCPUThread()
instead of touching that state directly from the WebSocket handler thread.

Two intentional exceptions, matching the reasoning already used for
cpu.stepInto's "not currently stepping" branch:
- cpu.stepping's Core_Break() call stays unqueued - it's what makes the CPU
  thread start reaching the queue drain point in the first place.
- cpu.status stays unqueued - it's meant to be a cheap, frequently-pollable
  status check, and its "pc" field is already documented as inaccurate unless
  stepping. Matches how SteppingBroadcaster already reads the same state
  directly from the WebSocket thread.

Where a handler's response doesn't depend on anything the queued lambda
computed (plain add/remove endpoints), moved req.Respond() back out after
the Core_RunOnCPUThread() call for readability - the JSON building and
socket write happen later in Finish() regardless of where Respond() is
called, so there's no thread-safety difference either way, just clarity
about what actually needs to run on the CPU thread.

memory.searchDisasm carries the same caveat flagged for memory.search: its
scan range has no size cap, so if the CPU is stepping, a very large range
will now block the CPU thread's own frame pump for the scan's duration
rather than running unqueued on the WebSocket thread as before. Not fixed
here - noted in a comment at the call site.

Replaced remaining `auto` locals in these three files with concrete types.

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:56:32 +02:00
1 parent 29825c1e61
commit c32df3d931
3 files changed
+401 -307

No files matched your search

+162 -132
View File
@@ -72,6 +72,9 @@ void WebSocketCPUStepping(DebuggerRequest &req) {
return req.Fail("CPU not started");
}
if (!Core_IsStepping() && Core_IsActive()) {
// 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 queuing to it only makes sense once the CPU actually *is*
// stepping, which this call is what triggers in the first place.
Core_Break(BreakReason::DebugStep, 0);
}
}
@@ -89,11 +92,15 @@ void WebSocketCPUResume(DebuggerRequest &req) {
return req.Fail("CPU not stepping");
}
g_breakpoints.SetSkipFirst(currentMIPS->pc);
if (currentMIPS->inDelaySlot) {
Core_RequestCPUStep(CPUStepType::Into, 1);
}
Core_Resume();
// Route the actual breakpoint/stepping manipulation to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
g_breakpoints.SetSkipFirst(currentMIPS->pc);
if (currentMIPS->inDelaySlot) {
Core_RequestCPUStep(CPUStepType::Into, 1);
}
Core_Resume();
});
}
// Request the current CPU status (cpu.status)
@@ -105,6 +112,10 @@ void WebSocketCPUResume(DebuggerRequest &req) {
// - paused: boolean, CPU paused or not started yet.
// - pc: number value of PC register (inaccurate unless stepping.)
// - ticks: number of CPU cycles into emulation.
// Deliberately not routed through Core_RunOnCPUThread(): this is meant to be a cheap, frequently-pollable
// status check, and the "pc" field is already documented as inaccurate unless stepping - queuing it would
// add up to a frame of latency to every poll for no real accuracy benefit. Matches how SteppingBroadcaster
// already reads this same state directly from the WebSocket thread.
void WebSocketCPUStatus(DebuggerRequest &req) {
JsonWriter &json = req.Respond();
@@ -131,55 +142,59 @@ void WebSocketCPUStatus(DebuggerRequest &req) {
// - uintValues: array of unsigned integer values for the registers.
// - floatValues: array of strings showing float representation. May be "nan", "inf", or "-inf".
void WebSocketCPUGetAllRegs(DebuggerRequest &req) {
auto cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
// Route the actual register reads to the CPU thread instead of poking at them directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
DebugInterface *cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
JsonWriter &json = req.Respond();
JsonWriter &json = req.Respond();
json.pushArray("categories");
for (int c = 0; c < MIPSDebugInterface::GetNumCategories(); ++c) {
json.pushDict();
json.writeInt("id", c);
json.writeString("name", MIPSDebugInterface::GetCategoryName(c));
json.pushArray("categories");
for (int c = 0; c < MIPSDebugInterface::GetNumCategories(); ++c) {
json.pushDict();
json.writeInt("id", c);
json.writeString("name", MIPSDebugInterface::GetCategoryName(c));
int total = MIPSDebugInterface::GetNumRegsInCategory(c);
int total = MIPSDebugInterface::GetNumRegsInCategory(c);
json.pushArray("registerNames");
for (int r = 0; r < total; ++r)
json.writeString(MIPSDebugInterface::GetRegName(c, r));
if (c == 0) {
json.writeString("pc");
json.writeString("hi");
json.writeString("lo");
json.pushArray("registerNames");
for (int r = 0; r < total; ++r)
json.writeString(MIPSDebugInterface::GetRegName(c, r));
if (c == 0) {
json.writeString("pc");
json.writeString("hi");
json.writeString("lo");
}
json.pop();
json.pushArray("uintValues");
// Writing as floating point to avoid negatives. Actually double, so safe.
for (int r = 0; r < total; ++r)
json.writeUint(cpuDebug->GetRegValue(c, r));
if (c == 0) {
json.writeUint(cpuDebug->GetPC());
json.writeUint(cpuDebug->GetHi());
json.writeUint(cpuDebug->GetLo());
}
json.pop();
json.pushArray("floatValues");
// Note: String so it can have Infinity and NaN.
for (int r = 0; r < total; ++r)
json.writeString(RegValueAsFloat(cpuDebug->GetRegValue(c, r)));
if (c == 0) {
json.writeString(RegValueAsFloat(cpuDebug->GetPC()));
json.writeString(RegValueAsFloat(cpuDebug->GetHi()));
json.writeString(RegValueAsFloat(cpuDebug->GetLo()));
}
json.pop();
json.pop();
}
json.pop();
json.pushArray("uintValues");
// Writing as floating point to avoid negatives. Actually double, so safe.
for (int r = 0; r < total; ++r)
json.writeUint(cpuDebug->GetRegValue(c, r));
if (c == 0) {
json.writeUint(cpuDebug->GetPC());
json.writeUint(cpuDebug->GetHi());
json.writeUint(cpuDebug->GetLo());
}
json.pop();
json.pushArray("floatValues");
// Note: String so it can have Infinity and NaN.
for (int r = 0; r < total; ++r)
json.writeString(RegValueAsFloat(cpuDebug->GetRegValue(c, r)));
if (c == 0) {
json.writeString(RegValueAsFloat(cpuDebug->GetPC()));
json.writeString(RegValueAsFloat(cpuDebug->GetHi()));
json.writeString(RegValueAsFloat(cpuDebug->GetLo()));
}
json.pop();
json.pop();
}
json.pop();
});
}
enum class DebuggerRegType {
@@ -271,37 +286,41 @@ static DebuggerRegType ValidateCatReg(DebuggerRequest &req, int *cat, int *reg)
// - uintValue: value in register.
// - floatValue: string showing float representation. May be "nan", "inf", or "-inf".
void WebSocketCPUGetReg(DebuggerRequest &req) {
auto cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
// Route the actual register read to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
DebugInterface *cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
int cat, reg;
uint32_t val = 0;
switch (ValidateCatReg(req, &cat, &reg)) {
case DebuggerRegType::NORMAL:
val = cpuDebug->GetRegValue(cat, reg);
break;
int cat, reg;
uint32_t val = 0;
switch (ValidateCatReg(req, &cat, &reg)) {
case DebuggerRegType::NORMAL:
val = cpuDebug->GetRegValue(cat, reg);
break;
case DebuggerRegType::PC:
val = cpuDebug->GetPC();
break;
case DebuggerRegType::HI:
val = cpuDebug->GetHi();
break;
case DebuggerRegType::LO:
val = cpuDebug->GetLo();
break;
case DebuggerRegType::PC:
val = cpuDebug->GetPC();
break;
case DebuggerRegType::HI:
val = cpuDebug->GetHi();
break;
case DebuggerRegType::LO:
val = cpuDebug->GetLo();
break;
case DebuggerRegType::INVALID:
// Error response already sent.
return;
}
case DebuggerRegType::INVALID:
// Error response already sent.
return;
}
JsonWriter &json = req.Respond();
json.writeInt("category", cat);
json.writeInt("register", reg);
json.writeUint("uintValue", val);
json.writeString("floatValue", RegValueAsFloat(val));
JsonWriter &json = req.Respond();
json.writeInt("category", cat);
json.writeInt("register", reg);
json.writeUint("uintValue", val);
json.writeString("floatValue", RegValueAsFloat(val));
});
}
// Update the value of a single register (cpu.setReg)
@@ -334,50 +353,55 @@ void WebSocketCPUSetReg(DebuggerRequest &req) {
return req.Fail("CPU currently running (cpu.stepping first)");
}
auto cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
// Route the actual register write to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
DebugInterface *cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
uint32_t val;
if (!req.ParamU32("value", &val, true)) {
// Already sent error.
return;
}
int cat, reg;
switch (ValidateCatReg(req, &cat, &reg)) {
case DebuggerRegType::NORMAL:
if (cat == 0 && reg == 0 && val != 0) {
return req.Fail("Cannot change reg zero");
uint32_t val;
if (!req.ParamU32("value", &val, true)) {
// Already sent error.
return;
}
cpuDebug->SetRegValue(cat, reg, val);
// In case part of it was ignored (e.g. flags reg.)
val = cpuDebug->GetRegValue(cat, reg);
break;
case DebuggerRegType::PC:
cpuDebug->SetPC(val);
break;
case DebuggerRegType::HI:
cpuDebug->SetHi(val);
break;
case DebuggerRegType::LO:
cpuDebug->SetLo(val);
break;
int cat, reg;
switch (ValidateCatReg(req, &cat, &reg)) {
case DebuggerRegType::NORMAL:
if (cat == 0 && reg == 0 && val != 0) {
req.Fail("Cannot change reg zero");
return;
}
cpuDebug->SetRegValue(cat, reg, val);
// In case part of it was ignored (e.g. flags reg.)
val = cpuDebug->GetRegValue(cat, reg);
break;
case DebuggerRegType::INVALID:
// Error response already sent.
return;
}
case DebuggerRegType::PC:
cpuDebug->SetPC(val);
break;
case DebuggerRegType::HI:
cpuDebug->SetHi(val);
break;
case DebuggerRegType::LO:
cpuDebug->SetLo(val);
break;
Reporting::NotifyDebugger();
case DebuggerRegType::INVALID:
// Error response already sent.
return;
}
JsonWriter &json = req.Respond();
// Repeat it back just to avoid confusion on how it parsed.
json.writeInt("category", cat);
json.writeInt("register", reg);
json.writeUint("uintValue", val);
json.writeString("floatValue", RegValueAsFloat(val));
Reporting::NotifyDebugger();
JsonWriter &json = req.Respond();
// Repeat it back just to avoid confusion on how it parsed.
json.writeInt("category", cat);
json.writeInt("register", reg);
json.writeUint("uintValue", val);
json.writeString("floatValue", RegValueAsFloat(val));
});
}
// Evaluate an expression (cpu.evaluate)
@@ -394,26 +418,32 @@ void WebSocketCPUEvaluate(DebuggerRequest &req) {
return req.Fail("CPU not started");
}
auto cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
// Route the actual register/symbol reads to the CPU thread instead of poking at them directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
Core_RunOnCPUThread([&] {
DebugInterface *cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
std::string exp;
if (!req.ParamString("expression", &exp)) {
// Already sent error.
return;
}
std::string exp;
if (!req.ParamString("expression", &exp)) {
// Already sent error.
return;
}
u32 val;
PostfixExpression postfix;
if (!initExpression(cpuDebug, exp.c_str(), postfix)) {
return req.Fail(StringFromFormat("Could not parse expression syntax: %s", getExpressionError()));
}
if (!parseExpression(cpuDebug, postfix, val)) {
return req.Fail(StringFromFormat("Could not evaluate expression: %s", getExpressionError()));
}
u32 val;
PostfixExpression postfix;
if (!initExpression(cpuDebug, exp.c_str(), postfix)) {
req.Fail(StringFromFormat("Could not parse expression syntax: %s", getExpressionError()));
return;
}
if (!parseExpression(cpuDebug, postfix, val)) {
req.Fail(StringFromFormat("Could not evaluate expression: %s", getExpressionError()));
return;
}
JsonWriter &json = req.Respond();
json.writeUint("uintValue", val);
json.writeString("floatValue", RegValueAsFloat(val));
JsonWriter &json = req.Respond();
json.writeUint("uintValue", val);
json.writeString("floatValue", RegValueAsFloat(val));
});
}