From 1c7f393e2bae5fde58c8796ea2302d7364f4e6cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 26 Aug 2026 12:15:17 +0200 Subject: [PATCH] Misc shutdown fixes * Destroy the ImGui debugger explicitly, not at static destruction time * Windows: destroy the debugger windows before NativeShutdown() * Clear the disassembly cache where it goes stale, not in view destructors (behind a no-op stub for libretro, which doesn't build DisassemblyManager.cpp) Co-Authored-By: Claude Opus 5 (1M context) --- Core/Debugger/DisassemblyManager.cpp | 4 ++++ Core/Debugger/DisassemblyManager.h | 9 +++++++++ Core/Debugger/WebSocket/DisasmSubscriber.cpp | 3 --- Core/Debugger/WebSocket/SteppingSubscriber.cpp | 3 --- Core/System.cpp | 5 +++++ UI/ImDebugger/ImDisasmView.cpp | 4 ---- UI/ImDebugger/ImDisasmView.h | 1 - UI/NativeApp.cpp | 6 ++++++ Windows/Debugger/CtrlDisAsmView.cpp | 1 - Windows/main.cpp | 8 +++++--- 10 files changed, 29 insertions(+), 15 deletions(-) diff --git a/Core/Debugger/DisassemblyManager.cpp b/Core/Debugger/DisassemblyManager.cpp index 897a5f5360..f73c35552d 100644 --- a/Core/Debugger/DisassemblyManager.cpp +++ b/Core/Debugger/DisassemblyManager.cpp @@ -40,6 +40,10 @@ DisassemblyManager g_disassemblyManager; +void ClearDisassemblyCache() { + g_disassemblyManager.clear(); +} + bool isInInterval(u32 start, u32 size, u32 value) { return start <= value && value <= (start+size-1); } diff --git a/Core/Debugger/DisassemblyManager.h b/Core/Debugger/DisassemblyManager.h index 35d25411ed..54acf2c87b 100644 --- a/Core/Debugger/DisassemblyManager.h +++ b/Core/Debugger/DisassemblyManager.h @@ -204,6 +204,15 @@ private: extern DisassemblyManager g_disassemblyManager; +// Drops the cached disassembly. Call this when what it describes - emulated memory and the symbol +// map - goes away. DisassemblyManager.cpp isn't built for libretro (see libretro/Makefile.common), +// so there it's a no-op, the same way the WebSocket debugger entry points are. +#ifdef __LIBRETRO__ +inline void ClearDisassemblyCache() {} +#else +void ClearDisassemblyCache(); +#endif + bool isInInterval(u32 start, u32 size, u32 value); bool IsLikelyStringAt(uint32_t addr); diff --git a/Core/Debugger/WebSocket/DisasmSubscriber.cpp b/Core/Debugger/WebSocket/DisasmSubscriber.cpp index 9ba7714085..f343b69252 100644 --- a/Core/Debugger/WebSocket/DisasmSubscriber.cpp +++ b/Core/Debugger/WebSocket/DisasmSubscriber.cpp @@ -37,9 +37,6 @@ public: WebSocketDisasmState() { g_disassemblyManager.setCpu(currentDebugMIPS); } - ~WebSocketDisasmState() { - g_disassemblyManager.clear(); - } void Base(DebuggerRequest &req); void Disasm(DebuggerRequest &req); diff --git a/Core/Debugger/WebSocket/SteppingSubscriber.cpp b/Core/Debugger/WebSocket/SteppingSubscriber.cpp index d1ca048c16..e9f5493e99 100644 --- a/Core/Debugger/WebSocket/SteppingSubscriber.cpp +++ b/Core/Debugger/WebSocket/SteppingSubscriber.cpp @@ -33,9 +33,6 @@ struct WebSocketSteppingState : public DebuggerSubscriber { WebSocketSteppingState() { g_disassemblyManager.setCpu(currentDebugMIPS); } - ~WebSocketSteppingState() { - g_disassemblyManager.clear(); - } void Into(DebuggerRequest &req); void Over(DebuggerRequest &req); diff --git a/Core/System.cpp b/Core/System.cpp index 36d64ddb0d..e13d417678 100644 --- a/Core/System.cpp +++ b/Core/System.cpp @@ -50,6 +50,7 @@ #include "Core/MIPS/MIPS.h" #include "Core/MIPS/MIPSAnalyst.h" #include "Core/MIPS/MIPSVFPUUtils.h" +#include "Core/Debugger/DisassemblyManager.h" #include "Core/Debugger/LineInfo.h" #include "Core/Debugger/SymbolMap.h" #include "Core/System.h" @@ -616,6 +617,10 @@ void CPU_Shutdown(bool success) { pspFileSystem.Shutdown(); // This unmounts all filesystems. + // Everything the disassembly cache describes - emulated memory and the symbol map - is about + // to go away, so drop it here rather than leaving it to whichever debugger UI closes last. + ClearDisassemblyCache(); + mipsr4k.Shutdown(); Memory::Shutdown(); HLEPlugins::Shutdown(); diff --git a/UI/ImDebugger/ImDisasmView.cpp b/UI/ImDebugger/ImDisasmView.cpp index db60cb2757..28bd7ec1ac 100644 --- a/UI/ImDebugger/ImDisasmView.cpp +++ b/UI/ImDebugger/ImDisasmView.cpp @@ -38,10 +38,6 @@ ImDisasmView::ImDisasmView() { displaySymbols_ = true; } -ImDisasmView::~ImDisasmView() { - g_disassemblyManager.clear(); -} - void ImDisasmView::ScanVisibleFunctions() { g_disassemblyManager.analyze(windowStart_, g_disassemblyManager.getNthNextAddress(windowStart_, visibleRows_) - windowStart_); } diff --git a/UI/ImDebugger/ImDisasmView.h b/UI/ImDebugger/ImDisasmView.h index a11b2ea4da..61429968b5 100644 --- a/UI/ImDebugger/ImDisasmView.h +++ b/UI/ImDebugger/ImDisasmView.h @@ -23,7 +23,6 @@ class MIPSState; class ImDisasmView { public: ImDisasmView(); - ~ImDisasmView(); // Public variables bounds to imgui checkboxes bool followPC_ = true; diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index 1435c92907..999d9aec71 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -1046,7 +1046,13 @@ void NativeShutdownGraphics(GraphicsContext *graphicsContext) { } ImGui_ImplThin3d_DestroyDeviceObjects(); ImGui_ImplThin3d_Shutdown(); + // Destroy the debugger here, while the things it refers to are still alive. If left to + // static destruction, ~ImDisasmView runs after Core's globals are gone and its + // g_disassemblyManager.clear() walks a destroyed map (and locks a destroyed mutex). + imDebugger_.reset(); ImGui::DestroyContext(ctx_); + ctx_ = nullptr; + imguiInited_ = false; } #if PPSSPP_PLATFORM(WINDOWS) && !PPSSPP_PLATFORM(UWP) diff --git a/Windows/Debugger/CtrlDisAsmView.cpp b/Windows/Debugger/CtrlDisAsmView.cpp index ff03709ad3..a229a572b9 100644 --- a/Windows/Debugger/CtrlDisAsmView.cpp +++ b/Windows/Debugger/CtrlDisAsmView.cpp @@ -209,7 +209,6 @@ CtrlDisAsmView::~CtrlDisAsmView() { DeleteObject(font); DeleteObject(boldfont); - g_disassemblyManager.clear(); } static COLORREF scaleColor(COLORREF color, float factor) diff --git a/Windows/main.cpp b/Windows/main.cpp index 2bb6bf5a31..1de6919fd8 100644 --- a/Windows/main.cpp +++ b/Windows/main.cpp @@ -1312,6 +1312,11 @@ int WINAPI WinMain(HINSTANCE _hInstance, HINSTANCE hPrevInstance, LPSTR szCmdLin _dbg_assert_(mainThread.joinable()); mainThread.join(); + // The debugger windows reach into core state as they go away (CtrlDisAsmView touches + // g_disassemblyManager, for example), so tear them down while the core is still around. + MainWindow::DestroyDebugWindows(); + DialogManager::DestroyAll(); + // It's safe to call NativeShutdown, we've joined the main thread. NativeShutdown(); @@ -1319,9 +1324,6 @@ int WINAPI WinMain(HINSTANCE _hInstance, HINSTANCE hPrevInstance, LPSTR szCmdLin // g_InputManager.StopPolling() is called in WM_DESTROY - MainWindow::DestroyDebugWindows(); - DialogManager::DestroyAll(); - TimeShutdown(); UnRegisterCMPTMFApis();