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

+127 -104
View File
@@ -21,6 +21,7 @@
#include "Common/Data/Encoding/Utf8.h"
#include "Common/StringUtils.h"
#include "Core/Core.h"
#include "Core/Debugger/Breakpoints.h"
#include "Core/Debugger/DisassemblyManager.h"
#include "Core/Debugger/WebSocket/DisasmSubscriber.h"
@@ -51,7 +52,7 @@ protected:
};
DebuggerSubscriber *WebSocketDisasmInit(DebuggerEventHandlerMap &map) {
auto p = new WebSocketDisasmState();
WebSocketDisasmState *p = new WebSocketDisasmState();
map["memory.base"] = [p](DebuggerRequest &req) { p->Base(req); };
map["memory.disasm"] = [p](DebuggerRequest &req) { p->Disasm(req); };
map["memory.searchDisasm"] = [p](DebuggerRequest &req) { p->SearchDisasm(req); };
@@ -143,7 +144,7 @@ void WebSocketDisasmState::WriteDisasmLine(JsonWriter &json, const DisassemblyLi
json.pushDict("breakpoint");
json.writeBool("enabled", enabled);
json.writeUint("address", addr + breakpointOffset);
auto cond = g_breakpoints.GetBreakPointCondition(addr + breakpointOffset);
BreakPointCond *cond = g_breakpoints.GetBreakPointCondition(addr + breakpointOffset);
if (cond)
json.writeString("condition", cond->expressionString);
else
@@ -295,83 +296,88 @@ void WebSocketDisasmState::Base(DebuggerRequest &req) {
void WebSocketDisasmState::Disasm(DebuggerRequest &req) {
if (!currentDebugMIPS->isAlive() || !Memory::IsActive())
return req.Fail("CPU not started");
auto cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
// In case of client errors, we limit the range to something that won't make us crash.
static const uint32_t MAX_RANGE = 10000;
uint32_t start, end;
if (!req.ParamU32("address", &start))
return;
uint32_t count = 0;
if (!req.ParamU32("count", &count, false, DebuggerParamType::OPTIONAL))
return;
if (count != 0) {
count = std::min(count, MAX_RANGE);
// Let's assume everything is two instructions.
g_disassemblyManager.analyze(start - 4, count * 8 + 8);
start = g_disassemblyManager.getStartAddress(start);
if (start == -1)
req.ParamU32("address", &start);
end = g_disassemblyManager.getNthNextAddress(start, count);
} else if (req.ParamU32("end", &end)) {
end = std::max(start, end);
if (end - start > MAX_RANGE * 4)
end = start + MAX_RANGE * 4;
// Let's assume everything is two instructions at most.
g_disassemblyManager.analyze(start - 4, end - start + 8);
start = g_disassemblyManager.getStartAddress(start);
if (start == -1)
req.ParamU32("address", &start);
// Correct end and calculate count based on it.
// This accounts for macros as one line, although two instructions.
u32 stop = end;
u32 next = start;
count = 0;
if (stop < start) {
for (next = start; next > stop; next = g_disassemblyManager.getNthNextAddress(next, 1)) {
// Route the disassembly manager/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;
uint32_t start, end;
if (!req.ParamU32("address", &start))
return;
uint32_t count = 0;
if (!req.ParamU32("count", &count, false, DebuggerParamType::OPTIONAL))
return;
if (count != 0) {
count = std::min(count, MAX_RANGE);
// Let's assume everything is two instructions.
g_disassemblyManager.analyze(start - 4, count * 8 + 8);
start = g_disassemblyManager.getStartAddress(start);
if (start == -1)
req.ParamU32("address", &start);
end = g_disassemblyManager.getNthNextAddress(start, count);
} else if (req.ParamU32("end", &end)) {
end = std::max(start, end);
if (end - start > MAX_RANGE * 4)
end = start + MAX_RANGE * 4;
// Let's assume everything is two instructions at most.
g_disassemblyManager.analyze(start - 4, end - start + 8);
start = g_disassemblyManager.getStartAddress(start);
if (start == -1)
req.ParamU32("address", &start);
// Correct end and calculate count based on it.
// This accounts for macros as one line, although two instructions.
u32 stop = end;
u32 next = start;
count = 0;
if (stop < start) {
for (next = start; next > stop; next = g_disassemblyManager.getNthNextAddress(next, 1)) {
count++;
}
}
for (end = next; end < stop && end >= next; end = g_disassemblyManager.getNthNextAddress(end, 1)) {
count++;
}
} else {
// Error message already sent.
return;
}
for (end = next; end < stop && end >= next; end = g_disassemblyManager.getNthNextAddress(end, 1)) {
count++;
bool displaySymbols = true;
if (!req.ParamBool("displaySymbols", &displaySymbols, DebuggerParamType::OPTIONAL))
return;
JsonWriter &json = req.Respond();
json.pushDict("range");
json.writeUint("start", start);
json.writeUint("end", end);
json.pop();
json.pushArray("lines");
DisassemblyLineInfo line;
uint32_t addr = start;
for (uint32_t i = 0; i < count; ++i) {
g_disassemblyManager.getLine(addr, displaySymbols, line, cpuDebug);
WriteDisasmLine(json, line);
addr += line.totalSize;
// These are pretty long, so let's grease the wheels a bit.
if (i % 50 == 0)
req.Flush();
}
} else {
// Error message already sent.
return;
}
json.pop();
bool displaySymbols = true;
if (!req.ParamBool("displaySymbols", &displaySymbols, DebuggerParamType::OPTIONAL))
return;
JsonWriter &json = req.Respond();
json.pushDict("range");
json.writeUint("start", start);
json.writeUint("end", end);
json.pop();
json.pushArray("lines");
DisassemblyLineInfo line;
uint32_t addr = start;
for (uint32_t i = 0; i < count; ++i) {
g_disassemblyManager.getLine(addr, displaySymbols, line, cpuDebug);
WriteDisasmLine(json, line);
addr += line.totalSize;
// These are pretty long, so let's grease the wheels a bit.
if (i % 50 == 0)
req.Flush();
}
json.pop();
json.pushArray("branchGuides");
auto branchGuides = g_disassemblyManager.getBranchLines(start, end - start);
for (auto bl : branchGuides)
WriteBranchGuide(json, bl);
json.pop();
json.pushArray("branchGuides");
std::vector<BranchLine> branchGuides = g_disassemblyManager.getBranchLines(start, end - start);
for (const BranchLine &bl : branchGuides)
WriteBranchGuide(json, bl);
json.pop();
});
}
// Search disassembly for some text (memory.searchDisasm)
@@ -388,9 +394,6 @@ void WebSocketDisasmState::Disasm(DebuggerRequest &req) {
void WebSocketDisasmState::SearchDisasm(DebuggerRequest &req) {
if (!currentDebugMIPS->isAlive() || !Memory::IsActive())
return req.Fail("CPU not started");
auto cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
uint32_t start;
if (!req.ParamU32("address", &start))
@@ -419,41 +422,51 @@ void WebSocketDisasmState::SearchDisasm(DebuggerRequest &req) {
std::transform(match.begin(), match.end(), match.begin(), ::tolower);
DisassemblyLineInfo line;
bool found = false;
uint32_t addr = start;
do {
g_disassemblyManager.getLine(addr, displaySymbols, line, cpuDebug);
const std::string addressSymbol = g_symbolMap->GetLabelString(addr);
// Route the disassembly manager/symbol reads to the CPU thread instead of poking at them directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
// Note: for a very large [start, end) range, this scan itself can take a while - unlike memory.search
// there's no size cap here, so a huge range will block the CPU thread's own frame pump for its duration.
Core_RunOnCPUThread([&] {
DebugInterface *cpuDebug = CPUFromRequest(req);
if (!cpuDebug)
return;
std::string mergeForSearch;
// Address+space (9) + symbol + colon+space (2) + name + space(1) + params = 12 fixed size worst case.
mergeForSearch.resize(12 + addressSymbol.size() + line.name.size() + line.params.size());
DisassemblyLineInfo line;
bool found = false;
uint32_t addr = start;
do {
g_disassemblyManager.getLine(addr, displaySymbols, line, cpuDebug);
const std::string addressSymbol = g_symbolMap->GetLabelString(addr);
sprintf(&mergeForSearch[0], "%08x ", addr);
auto inserter = mergeForSearch.begin() + 9;
if (!addressSymbol.empty()) {
inserter = std::transform(addressSymbol.begin(), addressSymbol.end(), inserter, ::tolower);
*inserter++ = ':';
std::string mergeForSearch;
// Address+space (9) + symbol + colon+space (2) + name + space(1) + params = 12 fixed size worst case.
mergeForSearch.resize(12 + addressSymbol.size() + line.name.size() + line.params.size());
sprintf(&mergeForSearch[0], "%08x ", addr);
std::string::iterator inserter = mergeForSearch.begin() + 9;
if (!addressSymbol.empty()) {
inserter = std::transform(addressSymbol.begin(), addressSymbol.end(), inserter, ::tolower);
*inserter++ = ':';
*inserter++ = ' ';
}
inserter = std::transform(line.name.begin(), line.name.end(), inserter, ::tolower);
*inserter++ = ' ';
}
inserter = std::transform(line.name.begin(), line.name.end(), inserter, ::tolower);
*inserter++ = ' ';
inserter = std::transform(line.params.begin(), line.params.end(), inserter, ::tolower);
inserter = std::transform(line.params.begin(), line.params.end(), inserter, ::tolower);
if (mergeForSearch.find(match) != mergeForSearch.npos) {
found = true;
break;
}
if (mergeForSearch.find(match) != mergeForSearch.npos) {
found = true;
break;
}
addr = RoundMemAddressUp(addr + line.totalSize);
} while (addr != end);
addr = RoundMemAddressUp(addr + line.totalSize);
} while (addr != end);
JsonWriter &json = req.Respond();
if (found)
json.writeUint("address", addr);
else
json.writeNull("address");
JsonWriter &json = req.Respond();
if (found)
json.writeUint("address", addr);
else
json.writeNull("address");
});
}
// Assemble an instruction (memory.assemble)
@@ -476,12 +489,22 @@ void WebSocketDisasmState::Assemble(DebuggerRequest &req) {
if (!req.ParamString("code", &code))
return;
// Route the actual code assembly to the CPU thread instead of poking at it directly
// from this WebSocket handler thread - see Core_RunOnCPUThread() in Core.h.
std::string error;
if (!MipsAssembleOpcode(code, currentDebugMIPS, address, &error)) {
uint32_t encoding = 0;
bool ok = false;
Core_RunOnCPUThread([&] {
ok = MipsAssembleOpcode(code, currentDebugMIPS, address, &error);
if (ok)
encoding = Memory::Read_Instruction(address).encoding;
});
if (!ok) {
return req.Fail(StringFromFormat("Could not assemble: %s", error.c_str()));
}
JsonWriter &json = req.Respond();
Reporting::NotifyDebugger();
json.writeUint("encoding", Memory::Read_Instruction(address).encoding);
json.writeUint("encoding", encoding);
}