diff --git a/AGENTS.md b/AGENTS.md index e22fae9cf7..61fc752a2c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -383,11 +383,14 @@ Two more things found while doing this: `CDisasm::NotifyMapLoaded()`, which locks it internally) will deadlock. Keep such calls outside the queued lambda, same as the modal-dialog and `SendMessage()` rules above. -### Lock ordering: `g_frameMutex` before `Memory::Lock()`, always +### Lock ordering: `g_frameMutex` before `Core_LockAgainstShutdown()`, always -`Memory::Lock()` / `Memory::MemoryInitedLock` (a `recursive_mutex`, `g_shutdownLock` in -`Core/MemMap.cpp`) guards the memory system against being torn down or reinitialized under you. When -a function needs both it and `g_frameMutex`, **take `g_frameMutex` first**. +`Core_LockAgainstShutdown()` / `CoreShutdownLock` (a `recursive_mutex`, `g_shutdownLock` in +`Core/Core.cpp`) is held across `CPU_Shutdown()` and `Memory::Reinit()`, i.e. while the core is +going away. Take it on any thread other than the CPU thread before reading core state - emulated +memory, the symbol map, kernel objects - so none of it is freed mid-read. It was called +`Memory::Lock()` and only covered the memory map; the name misled people into thinking it was about +memory access. When a function needs both it and `g_frameMutex`, **take `g_frameMutex` first**. The CPU thread's order is structural and can't be changed: `NativeFrame()` wraps everything below it in `g_frameMutex`, and several things under there lock memory - `Core_ProcessCPUQueue()` running a @@ -395,7 +398,7 @@ queued WebSocket handler, and `runImDebugger()` -> `ImMemView` -> `DisassembleRa GUI-thread side is the one that has to match. (Getting it backwards deadlocked for real: a paint handler held the memory lock and waited for `g_frameMutex` while the CPU thread did the reverse.) -Also: **a `Core_RunOnCPUThread()` callback does not need `Memory::Lock()`** - teardown only happens +Also: **a `Core_RunOnCPUThread()` callback does not need `Core_LockAgainstShutdown()`** - teardown only happens on the CPU thread itself (`Memory::Shutdown()` via `CPU_Shutdown()` <- `PSP_Shutdown()`, all callers on that thread; `Memory::Reinit()` from `Memory::DoState()` on savestate load). Don't add one. @@ -419,12 +422,12 @@ just means acting on an answer that may already be stale. there, and drops it in a per-connection mailbox. Don't add a broadcaster that reads emulator state from the connection's own thread; produce the event on the CPU thread and push it instead. -The Win32 debugger's paint handlers *do* still need it, though, so don't "simplify" those away: +The Win32 debugger's GUI-thread readers *do* still need it, so don't "simplify" those away: teardown is not yet fully inside the `g_frameMutex` span. `EmuScreen::render()`'s `PSP_Shutdown()` is inside it, but the ones in `EmuScreen::sendMessage()` (`REQUEST_GAME_RESET`, loading a new game) run from `g_screenManager->sendMessage()` in `NativeFrame()`, which sits *above* where the guard is taken. Closing that hole - moving those shutdowns inside the span, or deferring them to render time - -is the prerequisite for dropping `Memory::Lock()` from the debugger entirely. +is the prerequisite for dropping the shutdown lock from the debugger entirely. Painting-problem design history, in case a similar tradeoff comes up elsewhere: routing every paint through `Core_RunOnCPUThread` was rejected as too slow for something invoked continuously. A diff --git a/Core/Core.cpp b/Core/Core.cpp index 4c686dd95b..1aa508c030 100644 --- a/Core/Core.cpp +++ b/Core/Core.cpp @@ -138,6 +138,21 @@ void Core_ProcessCPUQueue() { g_cpuQueueCond.notify_all(); } +// See Core.h. Recursive because Memory::Shutdown() nests inside CPU_Shutdown()'s acquire. +static std::recursive_mutex g_shutdownLock; + +CoreShutdownLock::CoreShutdownLock() { + g_shutdownLock.lock(); +} + +CoreShutdownLock::~CoreShutdownLock() { + g_shutdownLock.unlock(); +} + +CoreShutdownLock Core_LockAgainstShutdown() { + return CoreShutdownLock(); +} + // See Core.h for the rationale. Held by NativeFrame() (in NativeApp.cpp) around the span where it // actually touches CPU-thread-owned debugger state. std::mutex g_frameMutex; diff --git a/Core/Core.h b/Core/Core.h index 774bde4167..014aa14447 100644 --- a/Core/Core.h +++ b/Core/Core.h @@ -168,6 +168,20 @@ void Core_ReenterDispatcher(); // If you've done things that mess with caches, // even while it's fully running. void Core_RunOnCPUThread(std::function func); +// Held while the core is being torn down (CPU_Shutdown) or its memory map reinitialized. Take it on +// any thread other than the CPU thread before reading core state - emulated memory, the symbol map, +// kernel objects - so none of it can be freed mid-read. Recursive, so nesting is fine. +// +// It is not a lock on memory *access*: it doesn't stop the CPU thread mutating anything, only stop +// it going away. If you also need a stable snapshot, take g_frameMutex first - see the ordering +// rule in AGENTS.md. +class CoreShutdownLock { +public: + CoreShutdownLock(); + ~CoreShutdownLock(); +}; +CoreShutdownLock Core_LockAgainstShutdown(); + // Drains the queue Core_RunOnCPUThread() feeds. Normally called from the top of every // Core_RunLoopUntil() iteration, but that function is only reached while a game is actually // loaded/running (via EmuScreen) - so NativeFrame() (UI/NativeApp.cpp) also calls this directly, diff --git a/Core/Debugger/DisassemblyManager.cpp b/Core/Debugger/DisassemblyManager.cpp index 2bcd1da1b4..897a5f5360 100644 --- a/Core/Debugger/DisassemblyManager.cpp +++ b/Core/Debugger/DisassemblyManager.cpp @@ -28,6 +28,7 @@ #include "Common/Log.h" #include "Common/StringUtils.h" #include "Common/Math/math_util.h" +#include "Core/Core.h" #include "Core/MemMap.h" #include "Core/System.h" #include "Core/MIPS/MIPSDebugInterface.h" @@ -992,7 +993,7 @@ bool GetDisasmAddressText(u32 address, char *dest, size_t bufSize, bool abbrevia // Utilify function from the old debugger. std::string DisassembleRange(u32 start, u32 size, bool displaySymbols, MIPSDebugInterface *debugger) { - auto memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); std::string result; // gather all branch targets without labels diff --git a/Core/Debugger/WebSocket/MemorySubscriber.cpp b/Core/Debugger/WebSocket/MemorySubscriber.cpp index 7b260ca2a7..2f63cbf064 100644 --- a/Core/Debugger/WebSocket/MemorySubscriber.cpp +++ b/Core/Debugger/WebSocket/MemorySubscriber.cpp @@ -63,7 +63,7 @@ struct AutoDisabledReplacements { // by the time this is called, so nothing else can be concurrently executing MIPS code or touching the // JIT's emuhack ops on this thread while we hold onto them below. // -// Deliberately does NOT take a Memory::MemoryInitedLock: memory teardown only ever happens on the +// Deliberately does NOT take a CoreShutdownLock: memory teardown only ever happens on the // CPU thread too, so there's nothing to guard against, and taking it here deadlocked against the // Win32 debugger's paint handlers. See the lock ordering section in AGENTS.md. // diff --git a/Core/MemMap.cpp b/Core/MemMap.cpp index aecb6aeb8f..06d9596569 100644 --- a/Core/MemMap.cpp +++ b/Core/MemMap.cpp @@ -87,7 +87,6 @@ u32 g_PSPModel; static MemMapSetupFlags g_setupFlags; -std::recursive_mutex g_shutdownLock; // We don't declare the IO region in here since its handled by other means. static MemoryView views[] = { @@ -343,6 +342,9 @@ bool Init(MemMapSetupFlags flags) { void Reinit() { _assert_msg_(PSP_GetBootState() == BootState::Complete, "Cannot reinit during startup/shutdown"); Core_NotifyLifecycle(CoreLifecycle::MEMORY_REINITING); + // Held across both halves: between Shutdown() and Init() there is no memory map at all, and a + // reader that only saw Shutdown()'s own acquire could slip into that gap. + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); MemMapSetupFlags flags = g_setupFlags; Shutdown(); Init(flags); @@ -421,7 +423,7 @@ void DoState(PointerWrap &p) { } void Shutdown() { - std::lock_guard guard(g_shutdownLock); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); u32 flags = 0; MemoryMap_Shutdown(); base = nullptr; @@ -432,21 +434,6 @@ bool IsActive() { return base != nullptr; } -// Wanting to avoid include pollution, MemMap.h is included a lot. -MemoryInitedLock::MemoryInitedLock() -{ - g_shutdownLock.lock(); -} -MemoryInitedLock::~MemoryInitedLock() -{ - g_shutdownLock.unlock(); -} - -MemoryInitedLock Lock() -{ - return MemoryInitedLock(); -} - static Opcode Read_Instruction(u32 address, bool resolveReplacements, Opcode inst) { if (!MIPS_IS_EMUHACK(inst.encoding)) { return inst; diff --git a/Core/MemMap.h b/Core/MemMap.h index 2444f0c6e7..56d8201d0c 100644 --- a/Core/MemMap.h +++ b/Core/MemMap.h @@ -118,16 +118,6 @@ void DoState(PointerWrap &p); // False when shutdown has already been called. bool IsActive(); -class MemoryInitedLock { -public: - MemoryInitedLock(); - ~MemoryInitedLock(); -}; - -// This doesn't lock memory access or anything, it just makes sure memory isn't freed. -// Use it when accessing PSP memory from external threads. -MemoryInitedLock Lock(); - // used by JIT to read instructions. Does not resolve replacements. Opcode Read_Opcode_JIT(const u32 _Address); // used by JIT. Reads in the "Locked cache" mode diff --git a/Core/System.cpp b/Core/System.cpp index 3e1e452ca5..f8c8382f8d 100644 --- a/Core/System.cpp +++ b/Core/System.cpp @@ -571,8 +571,8 @@ void CPU_Shutdown(bool success) { // Held across the whole teardown, not just Memory::Shutdown() further down. Everything below // frees state the debugger UIs read from other threads - kernel objects, the symbol map, the // memory map - and this is the lock they take to be sure none of it goes away mid-read. See - // Memory::Lock(); it's recursive, so the nested acquire in Memory::Shutdown() is fine. - Memory::MemoryInitedLock coreLock = Memory::Lock(); + // Core_LockAgainstShutdown(); it's recursive, so the nested acquire in Memory::Shutdown() is fine. + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); UninstallExceptionHandler(); diff --git a/Windows/Debugger/CtrlDisAsmView.cpp b/Windows/Debugger/CtrlDisAsmView.cpp index ae5daf06b0..92bc260672 100644 --- a/Windows/Debugger/CtrlDisAsmView.cpp +++ b/Windows/Debugger/CtrlDisAsmView.cpp @@ -62,9 +62,9 @@ void CtrlDisAsmView::scanVisibleFunctions() { // Reads live memory/symbol state to detect function boundaries - hold g_frameMutex for the // duration, which NativeFrame() also holds while it's actually touching that state, and - // Memory::Lock() so the core can't be torn down mid-read. See g_frameMutex in Core.h. + // Core_LockAgainstShutdown() so the core can't be torn down mid-read. See g_frameMutex in Core.h. std::lock_guard frameGuard(g_frameMutex); - Memory::MemoryInitedLock memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); g_disassemblyManager.analyze(windowStart, g_disassemblyManager.getNthNextAddress(windowStart,visibleRows)-windowStart); } @@ -244,7 +244,7 @@ std::string trimString(std::string input) void CtrlDisAsmView::assembleOpcode(u32 address, const std::string &defaultText) { - Memory::MemoryInitedLock memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); if (!Core_IsStepping()) { MessageBox(wnd,L"Cannot change code while the core is running!",L"Error",MB_OK); return; @@ -462,9 +462,9 @@ void CtrlDisAsmView::onPaint(WPARAM wParam, LPARAM lParam) // with the CPU thread - hold g_frameMutex for the duration of the read, which NativeFrame() // also holds while it's actually touching that state. See g_frameMutex in Core.h. // - // g_frameMutex first, then Memory::Lock() - never the other way around. See CtrlMemView::onPaint. + // g_frameMutex first, then Core_LockAgainstShutdown() - never the other way around. See CtrlMemView::onPaint. std::lock_guard frameGuard(g_frameMutex); - Memory::MemoryInitedLock memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); if (!debugger->isAlive() || Achievements::HardcoreModeActive()) return; PAINTSTRUCT ps; @@ -1196,7 +1196,7 @@ void CtrlDisAsmView::onMouseMove(WPARAM wParam, LPARAM lParam, int button) void CtrlDisAsmView::updateStatusBarText() { - auto memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); if (!PSP_IsInited()) return; @@ -1230,7 +1230,7 @@ void CtrlDisAsmView::calculatePixelPositions() void CtrlDisAsmView::search(bool continueSearch) { - auto memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); u32 searchAddress; if (continueSearch == false || searchQuery[0] == 0) diff --git a/Windows/Debugger/CtrlMemView.cpp b/Windows/Debugger/CtrlMemView.cpp index 84cc271500..4d0a7807ff 100644 --- a/Windows/Debugger/CtrlMemView.cpp +++ b/Windows/Debugger/CtrlMemView.cpp @@ -185,10 +185,10 @@ void CtrlMemView::onPaint(WPARAM wParam, LPARAM lParam) { // thread - hold g_frameMutex for the duration of the read, which NativeFrame() also holds // while it's actually touching that state. See g_frameMutex in Core.h. // - // g_frameMutex first, then Memory::Lock() - never the other way around. The CPU thread has no + // g_frameMutex first, then Core_LockAgainstShutdown() - never the other way around. The CPU thread has no // choice but that order (NativeFrame wraps everything below it), so this side has to match. std::lock_guard frameGuard(g_frameMutex); - Memory::MemoryInitedLock memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); // draw to a bitmap for double buffering PAINTSTRUCT ps; @@ -424,7 +424,7 @@ void CtrlMemView::onKeyDown(WPARAM wParam, LPARAM lParam) { } void CtrlMemView::onChar(WPARAM wParam, LPARAM lParam) { - Memory::MemoryInitedLock memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); if (!PSP_IsInited()) return; @@ -526,7 +526,7 @@ void CtrlMemView::onMouseUp(WPARAM wParam, LPARAM lParam, int button) { case ID_MEMVIEW_COPYVALUE_8: { - auto memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); size_t tempSize = 3 * selectedSize + 1; char *temp = new char[tempSize]; memset(temp, 0, tempSize); @@ -556,7 +556,7 @@ void CtrlMemView::onMouseUp(WPARAM wParam, LPARAM lParam, int button) { case ID_MEMVIEW_COPYVALUE_16: { - auto memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); size_t tempSize = 5 * ((selectedSize + 1) / 2) + 1; char *temp = new char[tempSize]; memset(temp, 0, tempSize); @@ -577,7 +577,7 @@ void CtrlMemView::onMouseUp(WPARAM wParam, LPARAM lParam, int button) { case ID_MEMVIEW_COPYVALUE_32: { - auto memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); size_t tempSize = 9 * ((selectedSize + 3) / 4) + 1; char *temp = new char[tempSize]; memset(temp, 0, tempSize); @@ -598,7 +598,7 @@ void CtrlMemView::onMouseUp(WPARAM wParam, LPARAM lParam, int button) { case ID_MEMVIEW_COPYFLOAT_32: { - auto memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); std::ostringstream stream; stream << (Memory::IsValid4AlignedAddress(curAddress_) ? Memory::ReadUnchecked_Float(curAddress_) : NAN); auto temp_string = stream.str(); @@ -856,7 +856,7 @@ bool CtrlMemView::ParseSearchString(std::string_view query, bool asHex, std::vec std::vector CtrlMemView::searchString(std::string_view searchQuery) { std::vector searchResAddrs; - auto memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); if (!PSP_IsInited()) return searchResAddrs; @@ -893,7 +893,7 @@ std::vector CtrlMemView::searchString(std::string_view searchQuery) { }; void CtrlMemView::search(bool continueSearch) { - auto memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); if (!PSP_IsInited()) return; diff --git a/Windows/Debugger/CtrlRegisterList.cpp b/Windows/Debugger/CtrlRegisterList.cpp index e6c215c0b7..9ea4db02a4 100644 --- a/Windows/Debugger/CtrlRegisterList.cpp +++ b/Windows/Debugger/CtrlRegisterList.cpp @@ -203,7 +203,7 @@ void CtrlRegisterList::onPaint(WPARAM wParam, LPARAM lParam) // with the CPU thread - hold g_frameMutex for the duration of the read, which NativeFrame() // also holds while it's actually touching that state. See g_frameMutex in Core.h. std::lock_guard frameGuard(g_frameMutex); - Memory::MemoryInitedLock memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); // The values are a moving target while the core is running - gray them out rather than trying // to highlight "changes" that are really just noise at that point. bool running = !Core_IsStepping(); diff --git a/Windows/Debugger/Debugger_Disasm.cpp b/Windows/Debugger/Debugger_Disasm.cpp index 1633789731..63d54d16de 100644 --- a/Windows/Debugger/Debugger_Disasm.cpp +++ b/Windows/Debugger/Debugger_Disasm.cpp @@ -730,7 +730,7 @@ void CDisasm::Show(bool bShow, bool includeToTop) { // thread - hold g_frameMutex for the duration of the read, which NativeFrame() also // holds while it's actually touching that state. See g_frameMutex in Core.h. std::lock_guard frameGuard(g_frameMutex); - Memory::MemoryInitedLock memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); g_symbolMap->FillSymbolListBox(GetDlgItem(m_hDlg, IDC_FUNCTIONLIST), ST_FUNCTION); deferredSymbolFill_ = false; } @@ -741,7 +741,7 @@ void CDisasm::Show(bool bShow, bool includeToTop) { void CDisasm::NotifyMapLoaded() { if (m_bShowState != SW_HIDE && g_symbolMap) { std::lock_guard frameGuard(g_frameMutex); - Memory::MemoryInitedLock memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); g_symbolMap->FillSymbolListBox(GetDlgItem(m_hDlg, IDC_FUNCTIONLIST), ST_FUNCTION); } else { deferredSymbolFill_ = true; diff --git a/Windows/Debugger/Debugger_Lists.cpp b/Windows/Debugger/Debugger_Lists.cpp index 1e48c5eea8..dd93398f4f 100644 --- a/Windows/Debugger/Debugger_Lists.cpp +++ b/Windows/Debugger/Debugger_Lists.cpp @@ -261,7 +261,7 @@ void CtrlThreadList::reloadThreads() // while it's actually touching that state. See g_frameMutex in Core.h. { std::lock_guard frameGuard(g_frameMutex); - Memory::MemoryInitedLock memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); threads = GetThreadsInfo(); } Update(); @@ -337,7 +337,7 @@ void CtrlBreakpointList::reloadBreakpoints() // with the CPU thread - see g_frameMutex in Core.h. { std::lock_guard frameGuard(g_frameMutex); - Memory::MemoryInitedLock memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); displayedBreakPoints_ = g_breakpoints.GetBreakpoints(); displayedMemChecks_= g_breakpoints.GetMemChecks(); } @@ -750,9 +750,9 @@ void CtrlStackTraceView::loadStackTrace() { // the CPU thread - hold g_frameMutex for the duration of the read, which NativeFrame() also // holds while it's actually touching that state. See g_frameMutex in Core.h. // - // g_frameMutex first, then Memory::Lock() - never the other way around. See CtrlMemView::onPaint. + // g_frameMutex first, then Core_LockAgainstShutdown() - never the other way around. See CtrlMemView::onPaint. std::lock_guard frameGuard(g_frameMutex); - Memory::MemoryInitedLock memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); if (!PSP_IsInited()) return; @@ -848,7 +848,7 @@ void CtrlModuleList::loadModules() // actually touching that state. See g_frameMutex in Core.h. { std::lock_guard frameGuard(g_frameMutex); - Memory::MemoryInitedLock memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); if (g_symbolMap) { modules = g_symbolMap->getAllModules(); } else { @@ -874,7 +874,7 @@ void CtrlWatchList::RefreshValues() { // otherwise race with the CPU thread - hold g_frameMutex for the duration, which NativeFrame() // also holds while it's actually touching that state. See g_frameMutex in Core.h. std::lock_guard frameGuard(g_frameMutex); - Memory::MemoryInitedLock memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); int steppingCounter = Core_GetSteppingCounter(); int changes = false; diff --git a/Windows/Debugger/DumpMemoryWindow.cpp b/Windows/Debugger/DumpMemoryWindow.cpp index 7d03ddbc3d..495e837108 100644 --- a/Windows/Debugger/DumpMemoryWindow.cpp +++ b/Windows/Debugger/DumpMemoryWindow.cpp @@ -88,7 +88,7 @@ INT_PTR CALLBACK DumpMemoryWindow::dlgFunc(HWND hwnd, UINT iMsg, WPARAM wParam, // queued callback. enum class Outcome { NotInited, OpenFailed, Success } outcome = Outcome::NotInited; Core_RunOnCPUThread([&] { - Memory::MemoryInitedLock memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); if (!PSP_IsInited()) return; diff --git a/Windows/GEDebugger/GEDebugger.cpp b/Windows/GEDebugger/GEDebugger.cpp index d39bf1ed87..b446ffc55d 100644 --- a/Windows/GEDebugger/GEDebugger.cpp +++ b/Windows/GEDebugger/GEDebugger.cpp @@ -552,7 +552,7 @@ void CGEDebugger::PreviewToClipboard(const GPUDebugBuffer *dbgBuffer, bool saveA } void CGEDebugger::UpdatePreviews() { - auto memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); if (!PSP_IsInited()) { return; } diff --git a/Windows/GEDebugger/TabVertices.cpp b/Windows/GEDebugger/TabVertices.cpp index 3feaf56c21..cff41a1b11 100644 --- a/Windows/GEDebugger/TabVertices.cpp +++ b/Windows/GEDebugger/TabVertices.cpp @@ -186,7 +186,7 @@ void CtrlVertexList::GetColumnText(wchar_t *dest, size_t destSize, int row, int } int CtrlVertexList::GetRowCount() { - auto memLock = Memory::Lock(); + CoreShutdownLock coreLock = Core_LockAgainstShutdown(); if (!PSP_IsInited()) { return 0; }