Legacy Win32 debugger: fix the painting problem with a frame-scoped mutex

Debugger windows (register list, disassembly view, memory view, breakpoint/
thread/module/stack lists, watch list) read CPU-thread-owned state directly
from the GUI thread's WM_PAINT/list-fill handlers, racing against the CPU
thread. Routing every read through Core_RunOnCPUThread would be too slow for
something invoked continuously on paint/list-refresh.

Add g_frameMutex (Core.h/Core.cpp), held by NativeFrame() only across the
span where it actually touches that state (running the CPU, processing
breakpoints, running the ImGui debugger) - not across input handling or the
present/frame-pacing waits. Debugger windows now hold the same mutex while
reading, giving synchronized reads without the round-trip cost of queuing
to the CPU thread.

CtrlRegisterList::onPaint() goes back to always reading live values (now
safe under the lock) and grays them out by color alone while the core is
running, rather than the earlier snapshot-caching approach.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hqm11k99viLfbJm2MkH4BH
This commit is contained in:
Henrik Rydgård
2026-08-08 17:22:12 +02:00
co-authored by Claude Sonnet 5
parent 03dcfd3931
commit f72709feb0
10 changed files with 129 additions and 38 deletions
+25 -17
View File
@@ -1257,29 +1257,37 @@ void NativeFrame(GraphicsContext *graphicsContext) {
g_requestManager.ProcessRequests();
g_breakpoints.Frame();
// Guards the span where we actually touch CPU-thread-owned debugger state (breakpoints,
// symbol map, registers, memory, etc.) against unsynchronized reads from other threads' paint
// handlers - see g_frameMutex in Core.h.
ScreenRenderFlags renderFlags = ScreenRenderFlags::NONE;
{
std::lock_guard<std::mutex> emuStateGuard(g_frameMutex);
// Apply the UIContext bounds as a 2D transformation matrix.
// NOTE: We compensate for the Y and Z conventions in the shaders, so we can use the same matrices in all backends.
Matrix4x4 ortho = ComputeOrthoMatrix(g_display.dp_xres, g_display.dp_yres, g_draw->GetDeviceCaps().coordConvention);
g_breakpoints.Frame();
// Can be overridden by sceDisplay which may pass true for the second argument.
g_frameTiming.ComputePresentMode(g_draw, false);
// Apply the UIContext bounds as a 2D transformation matrix.
// NOTE: We compensate for the Y and Z conventions in the shaders, so we can use the same matrices in all backends.
Matrix4x4 ortho = ComputeOrthoMatrix(g_display.dp_xres, g_display.dp_yres, g_draw->GetDeviceCaps().coordConvention);
ui_draw2d.PushDrawMatrix(ortho);
// Can be overridden by sceDisplay which may pass true for the second argument.
g_frameTiming.ComputePresentMode(g_draw, false);
g_screenManager->getUIContext()->SetTintSaturation(g_Config.fUITint, g_Config.fUISaturation);
ui_draw2d.PushDrawMatrix(ortho);
// All actual rendering (and also emulation) happens in this render() call.
ScreenRenderFlags renderFlags = g_screenManager->render();
if (g_screenManager->getUIContext()->Text()) {
g_screenManager->getUIContext()->Text()->OncePerFrame();
g_screenManager->getUIContext()->SetTintSaturation(g_Config.fUITint, g_Config.fUISaturation);
// All actual rendering (and also emulation) happens in this render() call.
renderFlags = g_screenManager->render();
if (g_screenManager->getUIContext()->Text()) {
g_screenManager->getUIContext()->Text()->OncePerFrame();
}
ui_draw2d.PopDrawMatrix();
runImDebugger(g_draw);
renderImDebugger(g_draw);
}
ui_draw2d.PopDrawMatrix();
runImDebugger(g_draw);
renderImDebugger(g_draw);
g_draw->EndFrame();
// This, between EndFrame and Present, is where we should actually wait to do present time management.