From a7dc184613f88786a8cf3d285a93486607043d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 8 Jul 2026 19:45:42 +0200 Subject: [PATCH 1/2] Reapply "Implement exit callbacks. Attempt to solve #21828" It was a mistake to revert c4d3cc8 , need to take savestates into account This reverts commit d5cec869339b31dbbe1b135632b2f01f887d64b2. --- Core/HLE/sceKernelThread.cpp | 82 +++++++++++++++++++++++++++++++++++- Core/HLE/sceKernelThread.h | 9 ++++ UI/EmuScreen.cpp | 41 +++++++++++++++++- 3 files changed, 128 insertions(+), 4 deletions(-) diff --git a/Core/HLE/sceKernelThread.cpp b/Core/HLE/sceKernelThread.cpp index 6dcd483909..52f33bd724 100644 --- a/Core/HLE/sceKernelThread.cpp +++ b/Core/HLE/sceKernelThread.cpp @@ -300,6 +300,31 @@ public: SceUID cbId; }; +// Set when an exit callback dispatch is in flight, cleared by the action below when it returns. +static bool g_exitCallbackPending = false; + +class ActionAfterExitCallback : public PSPAction { +public: + ActionAfterExitCallback() {} + void run(MipsCall &call) override { + INFO_LOG(Log::sceKernel, "Exit callback returned (v0 = %08x). Powering down.", currentMIPS->r[MIPS_REG_V0]); + g_exitCallbackPending = false; + // The callback may have already powered down by calling sceKernelExitGame - that's fine, + // Core_Stop is idempotent. Either way, the host will see CORE_POWERDOWN and tear the game down. + Core_Stop(); + } + + static PSPAction *Create() { + return new ActionAfterExitCallback; + } + + void DoState(PointerWrap &p) override { + auto s = p.Section("ActionAfterExitCallback", 1); + if (!s) + return; + } +}; + u32 PSPThread::GetMissingErrorCode() { return SCE_KERNEL_ERROR_UNKNOWN_THID; } @@ -521,6 +546,7 @@ static bool dispatchEnabled = true; static MipsCallManager mipsCalls; static int actionAfterCallback; static int actionAfterMipsCall; +static int actionAfterExitCallback; // When inside a callback, delays are "paused", and rechecked after the callback returns. static std::map pausedDelays; @@ -808,6 +834,8 @@ void __KernelThreadingInit() eventThreadEndTimeout = CoreTiming::RegisterEvent("ThreadEndTimeout", &hleThreadEndTimeout); actionAfterMipsCall = __KernelRegisterActionType(ActionAfterMipsCall::Create); actionAfterCallback = __KernelRegisterActionType(ActionAfterCallback::Create); + actionAfterExitCallback = __KernelRegisterActionType(ActionAfterExitCallback::Create); + g_exitCallbackPending = false; // Create the two idle threads, as well. With the absolute minimal possible priority. // 4096 stack size - don't know what the right value is. Hm, if callbacks are ever to run on these threads... @@ -825,7 +853,7 @@ void __KernelThreadingInit() void __KernelThreadingDoState(PointerWrap &p) { - auto s = p.Section("sceKernelThread", 1, 4); + auto s = p.Section("sceKernelThread", 1, 5); if (!s) return; @@ -861,6 +889,10 @@ void __KernelThreadingDoState(PointerWrap &p) __KernelRestoreActionType(actionAfterMipsCall, ActionAfterMipsCall::Create); Do(p, actionAfterCallback); __KernelRestoreActionType(actionAfterCallback, ActionAfterCallback::Create); + if (s >= 5) { + Do(p, actionAfterExitCallback); + __KernelRestoreActionType(actionAfterExitCallback, ActionAfterExitCallback::Create); + } Do(p, pausedDelays); @@ -1050,6 +1082,7 @@ void __KernelThreadingShutdown() { pausedDelays.clear(); threadEventHandlers.clear(); pendingDeleteThreads.clear(); + g_exitCallbackPending = false; } std::string __KernelThreadingSummary() { @@ -2689,7 +2722,7 @@ SceUID sceKernelCreateCallback(const char *name, u32 entrypoint, u32 signalArg) if (thread) thread->callbacks.push_back(id); - return hleLogDebug(Log::sceKernel, id); + return hleLogInfo(Log::sceKernel, id); } int sceKernelDeleteCallback(SceUID cbId) @@ -3490,6 +3523,51 @@ int sceKernelRegisterExitCallback(SceUID cbId) return hleLogDebug(Log::sceKernel, 0); } +// Dispatch the exit callback registered via sceKernelRegisterExitCallback on the thread that registered +// it, so the game has a chance to run its cleanup before we shut down. Returns false if no callback can +// be dispatched (none registered, registering thread dead, etc.) - the caller should then power down +// immediately. While true is returned, __KernelIsExitCallbackPending() will report true until the +// callback returns (via the chained ActionAfterExitCallback). +bool __KernelInvokeRegisteredExitCallback() { + if (g_exitCallbackPending) { + WARN_LOG(Log::sceKernel, "__KernelInvokeRegisteredExitCallback: already in progress"); + return true; + } + + u32 error; + PSPCallback *cb = kernelObjects.Get(registeredExitCbId, error); + if (!cb) { + INFO_LOG(Log::sceKernel, "__KernelInvokeRegisteredExitCallback: no exit callback registered"); + return false; + } + + PSPThread *thread = kernelObjects.Get(cb->nc.threadId, error); + if (!thread) { + WARN_LOG(Log::sceKernel, "__KernelInvokeRegisteredExitCallback: registering thread %08x is gone", cb->nc.threadId); + return false; + } + if (thread->isStopped()) { + WARN_LOG(Log::sceKernel, "__KernelInvokeRegisteredExitCallback: registering thread %s is stopped", thread->GetName()); + return false; + } + + ActionAfterExitCallback *action = (ActionAfterExitCallback *)__KernelCreateAction(actionAfterExitCallback); + + // Exit callbacks on real PSP receive (arg1, arg2, common) where arg1/arg2 are zero for HOME-button exit. + const u32 args[] = { 0, 0, cb->nc.commonArgument }; + + INFO_LOG(Log::sceKernel, "__KernelInvokeRegisteredExitCallback: dispatching '%s' (cb %08x) on thread '%s'", + cb->nc.name, registeredExitCbId, thread->GetName()); + + g_exitCallbackPending = true; + __KernelCallAddress(thread, cb->nc.entrypoint, action, args, 3, true, registeredExitCbId); + return true; +} + +bool __KernelIsExitCallbackPending() { + return g_exitCallbackPending; +} + // Update the exit callback status? int LoadExecForUser_362A956B() { diff --git a/Core/HLE/sceKernelThread.h b/Core/HLE/sceKernelThread.h index aff67f617d..41e664fbed 100644 --- a/Core/HLE/sceKernelThread.h +++ b/Core/HLE/sceKernelThread.h @@ -451,6 +451,15 @@ void __KernelChangeThreadState(SceUID threadId, ThreadStatus newStatus); int LoadExecForUser_362A956B(); int sceKernelRegisterExitCallback(SceUID cbId); +// Dispatch the exit callback registered via sceKernelRegisterExitCallback on its registering thread, +// so the game has a chance to clean up before we shut down. Returns false if no callback can be +// dispatched (none registered, the thread is gone, etc.) - in that case, the host should proceed +// to power down immediately. +bool __KernelInvokeRegisteredExitCallback(); +// True while a previously-invoked exit callback hasn't yet returned. The host should keep running +// emulation while this is true, and only tear down after it goes false (or after a timeout). +bool __KernelIsExitCallbackPending(); + KernelObject *__KernelThreadEventHandlerObject(); SceUID sceKernelRegisterThreadEventHandler(const char *name, SceUID threadID, u32 mask, u32 handlerPtr, u32 commonArg); int sceKernelReleaseThreadEventHandler(SceUID uid); diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index 4b635d73c9..bf49fc0be1 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -65,6 +65,7 @@ using namespace std::placeholders; #include "GPU/GPUCommon.h" #include "Core/MIPS/MIPS.h" #include "Core/HLE/sceCtrl.h" +#include "Core/HLE/sceKernelThread.h" #include "Core/HLE/sceSas.h" #include "Core/HLE/sceNet.h" #include "Core/HLE/sceDisplay.h" @@ -471,6 +472,33 @@ EmuScreen::~EmuScreen() { bootPending_ = false; } + // Catch-all for shutdown paths that didn't go through REQUEST_GAME_STOP (e.g. user navigated + // away with the back button, opened a different game, etc.). If an exit callback is registered + // and the core is still running, drive the CPU synchronously here so the callback gets a chance + // to run - there are no more host frames after this point. Capped tightly so a misbehaving + // callback can't stall shutdown. + if (!bootPending_ && PSP_IsInited() && coreState != CORE_POWERDOWN) { + const bool inflight = __KernelIsExitCallbackPending(); + if (inflight || __KernelInvokeRegisteredExitCallback()) { + INFO_LOG(Log::Loader, "EmuScreen dtor: %s exit callback synchronously before shutdown", + inflight ? "finishing in-flight" : "running"); + if (coreState == CORE_NEXTFRAME) + coreState = CORE_RUNNING_CPU; + const s64 chunkCycles = usToCycles(100000); // ~100ms of emulated time per chunk + const int maxChunks = 20; // hard cap: ~2s of emulated time total + for (int i = 0; i < maxChunks && __KernelIsExitCallbackPending(); i++) { + if (coreState != CORE_RUNNING_CPU && coreState != CORE_NEXTFRAME) + break; + PSP_RunLoopFor((int)chunkCycles); + if (coreState == CORE_NEXTFRAME) + coreState = CORE_RUNNING_CPU; + } + if (__KernelIsExitCallbackPending()) { + WARN_LOG(Log::Loader, "Exit callback didn't return in time; powering down anyway."); + } + } + } + Achievements::UnloadGame(); PSP_Shutdown(true); @@ -563,8 +591,17 @@ void EmuScreen::sendMessage(UIMessage message, const char *value) { WARN_LOG(Log::Loader, "Can't stop during a pending boot"); return; } - // The destructor will take care of shutting down. - screenManager()->switchScreen(new MainScreen()); + // Give the game a chance to run its registered exit callback before tearing things down. + // If the dispatch took, we keep running emulation; ActionAfterExitCallback (or a direct + // sceKernelExitGame from inside the callback) flips coreState to CORE_POWERDOWN, and + // checkPowerDown() then switches us to MainScreen as usual. Pressing stop again while + // the callback is in flight, or stopping a non-running core, falls through to immediate shutdown. + if (coreState == CORE_RUNNING_CPU && !__KernelIsExitCallbackPending() && __KernelInvokeRegisteredExitCallback()) { + INFO_LOG(Log::Loader, "REQUEST_GAME_STOP: running exit callback before shutdown."); + } else { + // No callback (or already in flight, or core not running) - the destructor will take care of shutting down. + screenManager()->switchScreen(new MainScreen()); + } } else if (message == UIMessage::REQUEST_GAME_RESET) { if (bootPending_) { WARN_LOG(Log::Loader, "Can't reset during a pending boot"); From b0018ab8831b6b6fc54d32654bcf6a33b287577d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 8 Jul 2026 19:47:33 +0200 Subject: [PATCH 2/2] Remove broken exit callback execution from EmuScreen destructor --- UI/EmuScreen.cpp | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index bf49fc0be1..91836f7a59 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -472,32 +472,7 @@ EmuScreen::~EmuScreen() { bootPending_ = false; } - // Catch-all for shutdown paths that didn't go through REQUEST_GAME_STOP (e.g. user navigated - // away with the back button, opened a different game, etc.). If an exit callback is registered - // and the core is still running, drive the CPU synchronously here so the callback gets a chance - // to run - there are no more host frames after this point. Capped tightly so a misbehaving - // callback can't stall shutdown. - if (!bootPending_ && PSP_IsInited() && coreState != CORE_POWERDOWN) { - const bool inflight = __KernelIsExitCallbackPending(); - if (inflight || __KernelInvokeRegisteredExitCallback()) { - INFO_LOG(Log::Loader, "EmuScreen dtor: %s exit callback synchronously before shutdown", - inflight ? "finishing in-flight" : "running"); - if (coreState == CORE_NEXTFRAME) - coreState = CORE_RUNNING_CPU; - const s64 chunkCycles = usToCycles(100000); // ~100ms of emulated time per chunk - const int maxChunks = 20; // hard cap: ~2s of emulated time total - for (int i = 0; i < maxChunks && __KernelIsExitCallbackPending(); i++) { - if (coreState != CORE_RUNNING_CPU && coreState != CORE_NEXTFRAME) - break; - PSP_RunLoopFor((int)chunkCycles); - if (coreState == CORE_NEXTFRAME) - coreState = CORE_RUNNING_CPU; - } - if (__KernelIsExitCallbackPending()) { - WARN_LOG(Log::Loader, "Exit callback didn't return in time; powering down anyway."); - } - } - } + // TODO: We need to somehow handle exit callbacks here, too. Achievements::UnloadGame(); PSP_Shutdown(true);