From b547e01c9de34d45ae5b8b5c244c7847d9286367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 23 Oct 2026 11:22:18 +0200 Subject: [PATCH] VSH: Boot games the XMB starts like any other game launch A disc game or memory stick EBOOT started from the XMB used to be swapped in under the XMB's session, so it ran without its PARAM.SFO, compat flags, game config, window title, savestates and so on. Much of that is decided once, at boot (the memory map, the GPU backend, the shader cache), so sceKernelLoadExec now asks the frontend for a reboot into the disc image or EBOOT (PSP_RequestBoot) and stops. The app and headless boot it like a game from the game list. The game keeps the XMB's launch args (CoreParameter::launchedFromVSH), so exiting boots the XMB again, with the disc that's in the drive and those args, and the XMB comes back where it was without auto-starting the disc. Homebrew keeps the disc that was in the drive. A disc whose executables are all zeroes (the EUR Video Demo Disc's "game") still loads in place and fails back to the XMB. In headless, the run carries on through the reboot, starting stopped under the debugger like the first boot, since the reboot loses a pending cpu.runUntilTime. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/CoreParameter.h | 12 +++++ Core/HLE/sceKernel.cpp | 4 ++ Core/HLE/sceKernelModule.cpp | 92 +++++++++++++++++++++++++++++++----- Core/HLE/sceKernelModule.h | 7 +-- Core/HLE/sceUmd.cpp | 28 +++++++++++ Core/HLE/sceUmd.h | 2 + Core/PSPLoaders.cpp | 4 +- Core/System.cpp | 41 ++++++++++++++++ Core/System.h | 17 +++++++ UI/EmuScreen.cpp | 25 ++++++++++ UI/EmuScreen.h | 4 ++ docs/VSHBootInvestigation.md | 9 ++++ headless/Headless.cpp | 50 +++++++++++++++++++- 13 files changed, 277 insertions(+), 18 deletions(-) diff --git a/Core/CoreParameter.h b/Core/CoreParameter.h index efbe80c0e8..419af33daf 100644 --- a/Core/CoreParameter.h +++ b/Core/CoreParameter.h @@ -19,6 +19,7 @@ #include #include +#include #include "Common/File/Path.h" #include "Core/Compatibility.h" @@ -33,6 +34,12 @@ enum GPUCore : int { GPUCORE_METAL = 6, }; +// The XMB session a game was started from. +struct VSHLaunch { + Path vshmain; // Host path of vshmain.prx. + std::vector vshmainArgs; // What the XMB passed when it launched the game. +}; + enum class FPSLimit { NORMAL = 0, CUSTOM1 = 1, @@ -72,6 +79,11 @@ struct CoreParameter { // Overrides the VSH's "UMD auto-start" setting (bVshUmdAutoboot) for this boot. std::optional umdAutoboot; + // Set when the XMB started this game, which then boots the XMB again when it exits. + std::optional launchedFromVSH; + // For a boot of vshmain.prx: what its module_start gets, which tells it it's coming back from a game. + std::vector vshmainArgs; + Path nandRoot; // If non-empty, use this directory as the root above flash0:-flash3: (which are hosted in subdirectories flash0-flash3 of it). std::string errorString; diff --git a/Core/HLE/sceKernel.cpp b/Core/HLE/sceKernel.cpp index 037b1b3b80..9fbbffac20 100644 --- a/Core/HLE/sceKernel.cpp +++ b/Core/HLE/sceKernel.cpp @@ -1060,6 +1060,10 @@ static int sceKernelExitVSH(u32 paramPtr) { } INFO_LOG(Log::sceKernel, "sceKernelExitVSH"); + // In a game, the same as sceKernelExitGame. + if (__KernelExitToVSH(currentMIPS)) { + return hleNoLog(0); + } __KernelSwitchOffThread("VSH exited"); Core_Stop(); diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index a75b5723f7..acb606db20 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -2579,7 +2579,7 @@ static bool __KernelLoadExecFromPtr(MIPSState * mips, const u8 *data, size_t siz return true; } -bool __KernelLoadExec(MIPSState *mips, const char *filename, u32 paramPtr, std::string *error_string) { +bool __KernelLoadExec(MIPSState *mips, const char *filename, u32 paramPtr, std::string *error_string, const std::vector *hostArgs) { std::vector fileData; if (pspFileSystem.ReadEntireFile(filename, fileData) < 0) { ERROR_LOG(Log::Loader, "Failed to load executable %s - file doesn't exist", filename); @@ -2588,7 +2588,7 @@ bool __KernelLoadExec(MIPSState *mips, const char *filename, u32 paramPtr, std:: return false; } - return __KernelLoadExecFromPtr(mips, fileData.data(), fileData.size(), filename, paramPtr, error_string); + return __KernelLoadExecFromPtr(mips, fileData.data(), fileData.size(), filename, paramPtr, error_string, hostArgs); } bool __KernelLoadExecFromBuffer(MIPSState *mips, const u8 *data, size_t size, u32 paramPtr, std::string *error_string) { @@ -2649,6 +2649,51 @@ int __KernelGPUReplay(MIPSState *mips) { return hleNoLog(result == GPURecord::ReplayResult::Break ? 1 : 0); } +// The XMB starting a disc game or something on the memory stick. Rather than swapping it in under +// the XMB's session, the frontend boots it like it boots anything, so it gets its PARAM.SFO, compat +// flags, game config, window title and so on. False for anything else, which loads in place. +static bool RebootFromVSHInto(const std::string &filename) { + // Some discs (the EUR Video Demo Disc) list a game whose executables are all zeroes. Loaded in + // place, that fails back to the XMB. + const auto isExecutable = [](const char *path) { + u8 head[4]{}; + const int fd = pspFileSystem.OpenFile(path, FILEACCESS_READ); + if (fd < 0) { + return false; + } + const size_t read = pspFileSystem.ReadFile(fd, head, sizeof(head)); + pspFileSystem.CloseFile(fd); + return read == sizeof(head) && (memcmp(head, "~PSP", 4) == 0 || memcmp(head, "\x7F" "ELF", 4) == 0); + }; + + const Path disc = __UmdGetPath(); + Path hostPath; + if (filename == "disc0:/PSP_GAME/SYSDIR/EBOOT.BIN" || filename == "disc0:/PSP_GAME/SYSDIR/BOOT.BIN") { + if (isExecutable("disc0:/PSP_GAME/SYSDIR/EBOOT.BIN") || isExecutable("disc0:/PSP_GAME/SYSDIR/BOOT.BIN")) { + hostPath = disc; + } + } else if (startsWith(filename, "ms0:/PSP/GAME") && endsWithNoCase(filename, "/EBOOT.PBP")) { + hostPath = GetSysDirectory(DIRECTORY_MEMSTICK_ROOT) / filename.substr(strlen("ms0:/")); + if (!File::Exists(hostPath)) { + WARN_LOG(Log::sceModule, "No %s for %s, starting it in place", hostPath.c_str(), filename.c_str()); + return false; + } + } + if (hostPath.empty()) { + return false; + } + + BootRequest request; + request.fileToStart = hostPath; + // Homebrew keeps the disc that's in the drive. + if (hostPath != disc) { + request.mountIso = disc; + } + request.launchedFromVSH = VSHLaunch{ PSP_CoreParameter().fileToStart, g_vshmainArgs }; + PSP_RequestBoot(request); + return true; +} + int sceKernelLoadExec(MIPSState *mips, const char *filename, u32 paramPtr) { std::string exec_filename = filename; PSPFileInfo info = pspFileSystem.GetFileInfo(exec_filename); @@ -2684,6 +2729,10 @@ int sceKernelLoadExec(MIPSState *mips, const char *filename, u32 paramPtr) { g_vshmainArgs.assign(args, args + vshParam->vshmainArgsSize); } INFO_LOG(Log::sceModule, "Keeping %d bytes of vshmain args", (int)g_vshmainArgs.size()); + if (RebootFromVSHInto(exec_filename)) { + __KernelSwitchOffThread("XMB started a game"); + return hleLogInfo(Log::sceModule, 0, "rebooting into it"); + } } std::string error_string; if (!__KernelLoadExec(mips, exec_filename.c_str(), paramPtr, &error_string)) { @@ -2700,9 +2749,34 @@ int sceKernelLoadExec(MIPSState *mips, const char *filename, u32 paramPtr) { return hleLogDebug(Log::sceModule, 0); } +// The XMB's args from when it launched a game tell it that it's coming back (and not to auto-start +// the disc in the drive, say). vshmain's module_start only takes them (copying them into its state) +// when they're 0x20 or 0x400 bytes and start with that size and then 0x20, which it doesn't write +// itself. +static std::vector VshmainReturnArgs(const std::vector &launchArgs) { + std::vector args = launchArgs; + if (args.size() >= 8) { + u32 header[2] = { (u32)args.size(), 0x20 }; + memcpy(args.data(), header, sizeof(header)); + } + return args; +} + bool __KernelExitToVSH(MIPSState *mips) { - // A session that started in the XMB, running a game it launched. The XMB itself exiting (power - // off) still ends the session. + // Normally the XMB launched this game by booting it, so it boots the XMB again. + const std::optional &launchedFromVSH = PSP_CoreParameter().launchedFromVSH; + if (launchedFromVSH) { + BootRequest request; + request.fileToStart = launchedFromVSH->vshmain; + request.mountIso = __UmdGetPath(); + request.vshmainArgs = VshmainReturnArgs(launchedFromVSH->vshmainArgs); + PSP_RequestBoot(request); + __KernelSwitchOffThread("back to the XMB"); + return true; + } + + // Otherwise it's something the XMB loaded in place, under its own session. The XMB itself + // exiting (power off) still ends the session. if (PSP_CoreParameter().fileToStart.GetFilename() != "vshmain.prx" || g_runningVSH) { return false; } @@ -2713,15 +2787,7 @@ bool __KernelExitToVSH(MIPSState *mips) { ERROR_LOG(Log::sceModule, "Couldn't return to the VSH: no %s", vshmain); return false; } - // Its args from when it launched the game tell it that it's coming back (and not to auto-start - // the disc in the drive, say). vshmain's module_start only takes them (copying them into its - // state) when they're 0x20 or 0x400 bytes and start with that size and then 0x20, which it - // doesn't write itself. - std::vector args = g_vshmainArgs; - if (args.size() >= 8) { - u32 header[2] = { (u32)args.size(), 0x20 }; - memcpy(args.data(), header, sizeof(header)); - } + std::vector args = VshmainReturnArgs(g_vshmainArgs); INFO_LOG(Log::sceModule, "Back to the VSH with %d bytes of vshmain args", (int)args.size()); if (!__KernelLoadExecFromPtr(mips, fileData.data(), fileData.size(), vshmain, 0, &error_string, args.empty() ? nullptr : &args)) { ERROR_LOG(Log::sceModule, "Couldn't return to the VSH: %s", error_string.c_str()); diff --git a/Core/HLE/sceKernelModule.h b/Core/HLE/sceKernelModule.h index 39c6a7547b..6ab8d38759 100644 --- a/Core/HLE/sceKernelModule.h +++ b/Core/HLE/sceKernelModule.h @@ -241,11 +241,12 @@ class MIPSState; u32 __KernelGetModuleGP(SceUID module); bool KernelModuleIsKernelMode(SceUID module); bool __KernelLoadGEDump(MIPSState *mips, std::string_view base_filename, std::string *error_string); -bool __KernelLoadExec(MIPSState *mips, const char *filename, u32 paramPtr, std::string *error_string); +// hostArgs, if set, are passed to the module instead of what paramPtr points to. +bool __KernelLoadExec(MIPSState *mips, const char *filename, u32 paramPtr, std::string *error_string, const std::vector *hostArgs = nullptr); // The VSH (the XMB) is what's running, not a game it launched. bool __KernelIsRunningVSH(); -// What sceKernelExitGame does in a game the XMB launched, as on a PSP: back to the XMB. False if this -// session didn't start in the XMB. +// What sceKernelExitGame does in a game the XMB launched, as on a PSP: back to the XMB, by booting it +// again. False if the XMB didn't launch this. bool __KernelExitToVSH(MIPSState *mips); // Exposed so sceVshBridge can reuse it directly for vshKernelLoadModuleBufferVSH, matching JPCSP. SceUID sceKernelLoadModuleBufferUsbWlan(u32 size, u32 bufPtr, u32 flags, u32 lmoptionPtr); diff --git a/Core/HLE/sceUmd.cpp b/Core/HLE/sceUmd.cpp index be80894793..f26a094d92 100644 --- a/Core/HLE/sceUmd.cpp +++ b/Core/HLE/sceUmd.cpp @@ -57,6 +57,8 @@ static std::map umdPausedWaits; bool g_UMDReplacePermit = false; bool UMDInserted = true; +// The image a disc swap or insert put in the drive. Empty if it's still the one we booted with. +static Path g_umdPath; struct PspUmdInfo { u32_le size; @@ -81,6 +83,7 @@ void __UmdInit() umdWaitingThreads.clear(); umdPausedWaits.clear(); g_UMDReplacePermit = false; + g_umdPath.clear(); __KernelRegisterWaitTypeFuncs(WAITTYPE_UMD, __UmdBeginCallback, __UmdEndCallback); } @@ -519,6 +522,7 @@ void __UmdReplace(const Path &filepath) { } Achievements::ChangeUMD(filepath, fileLoader); + g_umdPath = filepath; UMDInserted = false; // Wake any threads waiting for the disc to be removed. @@ -541,6 +545,7 @@ bool __UmdInsert(const Path &filepath, std::string *error) { return false; } Achievements::ChangeUMD(filepath, fileLoader); + g_umdPath = filepath; CoreTiming::UnscheduleEvent(umdInsertChangeEvent, 1); if (DriveCallbackPending()) { @@ -574,6 +579,29 @@ void __UmdEject() { __KernelNotifyCallback(driveCBId, PSP_UMD_NOT_PRESENT | PSP_UMD_CHANGED); } +Path __UmdGetPath() { + if (!UMDInserted) { + return Path(); + } + if (!g_umdPath.empty()) { + return g_umdPath; + } + const CoreParameter &coreParam = PSP_CoreParameter(); + if (!coreParam.mountIso.empty()) { + return coreParam.mountIso; + } + switch (coreParam.fileType) { + case IdentifiedFileType::PSP_ISO: + case IdentifiedFileType::PSP_ISO_NP: + case IdentifiedFileType::PSP_UMD_VIDEO_ISO: + case IdentifiedFileType::PSP_DISC_DIRECTORY: + return coreParam.fileToStart; + default: + // A homebrew or PS1 EBOOT, with no disc behind disc0:. + return Path(); + } +} + bool __UmdIsInserted() { // Counting a disc that's on its way in, which can't finish while emulation is paused. return UMDInserted || CoreTiming::IsScheduled(umdInsertChangeEvent); diff --git a/Core/HLE/sceUmd.h b/Core/HLE/sceUmd.h index 43681ec189..40d8c5f3ce 100644 --- a/Core/HLE/sceUmd.h +++ b/Core/HLE/sceUmd.h @@ -44,6 +44,8 @@ void __UmdReplace(const Path &filepath); bool __UmdInsert(const Path &filepath, std::string *error); void __UmdEject(); bool __UmdIsInserted(); +// The host path of the disc image in the drive, or empty if there's none. +Path __UmdGetPath(); // What's on the disc in the drive (PSP_UMD_TYPE_* bits), or 0 for an empty drive. u32 __UmdDiscType(); bool getUMDReplacePermit(); diff --git a/Core/PSPLoaders.cpp b/Core/PSPLoaders.cpp index 94d06b87b6..19a834f2e6 100644 --- a/Core/PSPLoaders.cpp +++ b/Core/PSPLoaders.cpp @@ -657,7 +657,9 @@ bool Load_PSP_ELF_PBP(FileLoader *fileLoader, std::string_view discId, bool load g_Config.LoadGameConfig(discID); } - return __KernelLoadExec(currentMIPS, finalName.c_str(), 0, error_string); + // The XMB coming back from a game it launched gets the args it launched it with. + const std::vector &vshmainArgs = PSP_CoreParameter().vshmainArgs; + return __KernelLoadExec(currentMIPS, finalName.c_str(), 0, error_string, vshmainArgs.empty() ? nullptr : &vshmainArgs); } // A PS1 EBOOT, run by the firmware's POPS manager, which loads its PS1 emulator for the game. diff --git a/Core/System.cpp b/Core/System.cpp index cdffa2f4e3..cdd645f989 100644 --- a/Core/System.cpp +++ b/Core/System.cpp @@ -92,6 +92,9 @@ enum CPUThreadState { MetaFileSystem pspFileSystem; ParamSFOData g_paramSFO; ParamSFOData g_paramSFORaw; + +static std::mutex g_bootRequestLock; +static std::optional g_bootRequest; static GlobalUIState globalUIState; CoreParameter g_CoreParameter; static FileLoader *g_loadedFile; @@ -791,6 +794,12 @@ bool PSP_InitStart(const CoreParameter &coreParam) { IncrementDebugCounter(DebugCounter::GAME_BOOT); + { + // One the frontend didn't act on (the user stopped the game instead) is stale now. + std::lock_guard guard(g_bootRequestLock); + g_bootRequest.reset(); + } + g_bootState = BootState::Booting; g_loaderDone = false; g_loaderFailed = false; @@ -933,6 +942,38 @@ BootState PollBootState() { return g_bootState; } +void BootRequest::ApplyTo(CoreParameter *coreParam) const { + coreParam->fileToStart = fileToStart; + coreParam->mountIso = mountIso; + coreParam->launchedFromVSH = launchedFromVSH; + coreParam->vshmainArgs = vshmainArgs; + coreParam->umdAutoboot.reset(); +} + +void PSP_RequestBoot(const BootRequest &request) { + INFO_LOG(Log::Loader, "Rebooting into %s (disc: %s)", request.fileToStart.c_str(), request.mountIso.c_str()); + { + std::lock_guard guard(g_bootRequestLock); + g_bootRequest = request; + } + Core_Stop(); +} + +bool PSP_HasBootRequest() { + std::lock_guard guard(g_bootRequestLock); + return g_bootRequest.has_value(); +} + +bool PSP_TakeBootRequest(BootRequest *request) { + std::lock_guard guard(g_bootRequestLock); + if (!g_bootRequest) { + return false; + } + *request = std::move(*g_bootRequest); + g_bootRequest.reset(); + return true; +} + void PSP_Shutdown(bool success) { // Do nothing if we never inited. if (g_bootState == BootState::Off) { diff --git a/Core/System.h b/Core/System.h index 883b4cb71f..a07377dc6e 100644 --- a/Core/System.h +++ b/Core/System.h @@ -79,6 +79,23 @@ BootState PollBootState(); void PSP_Shutdown(bool success); +// A boot the emulated system asks for itself: the XMB starting a game, or a game it started going +// back to it. The core stops, and the frontend boots this like anything else, so the game gets its +// PARAM.SFO, compat flags, game config and so on. +struct BootRequest { + Path fileToStart; + Path mountIso; + std::optional launchedFromVSH; + std::vector vshmainArgs; + + void ApplyTo(CoreParameter *coreParam) const; +}; +// Called on the CPU thread. Stops the core. +void PSP_RequestBoot(const BootRequest &request); +bool PSP_HasBootRequest(); +// For the frontend, once the core has stopped. Takes the pending request, if there is one. +bool PSP_TakeBootRequest(BootRequest *request); + FileLoader *PSP_LoadedFile(); void PSP_RunLoopWhileState(); diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index 5a0df93fe2..dbe7db9b33 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -323,6 +323,8 @@ void EmuScreen::ProcessGameBoot(const Path &filename) { coreParam.fileToStart = filename; coreParam.mountIso = mountIso_; coreParam.umdAutoboot = umdAutoboot_; + coreParam.launchedFromVSH = launchedFromVSH_; + coreParam.vshmainArgs = vshmainArgs_; coreParam.mountRoot = g_Config.mountRoot; coreParam.startBreak = !g_Config.bAutoRun; coreParam.headLess = false; @@ -622,6 +624,8 @@ void EmuScreen::sendMessage(UIMessage message, const char *value) { gamePath_ = newGamePath; mountIso_.clear(); umdAutoboot_.reset(); + launchedFromVSH_.reset(); + vshmainArgs_.clear(); } } else if (message == UIMessage::CONFIG_LOADED) { // In case we need to position touch controls differently. @@ -1418,6 +1422,23 @@ ViewLayoutMode EmuScreen::LayoutMode() const { } void EmuScreen::update() { + // The emulated system asking for a reboot: the XMB starting a game, or a game going back to it. + BootRequest request; + if (coreState == CORE_POWERDOWN && PSP_GetBootState() == BootState::Complete && !bootPending_ && PSP_TakeBootRequest(&request)) { + Achievements::UnloadGame(); + PSP_Shutdown(true); + screenManager()->cancelScreensAbove(this); + bootPending_ = true; + bootIsReset_ = false; + gamePath_ = request.fileToStart; + mountIso_ = request.mountIso; + // Coming back to the XMB mustn't auto-start the disc again. + umdAutoboot_.reset(); + launchedFromVSH_ = request.launchedFromVSH; + vshmainArgs_ = request.vshmainArgs; + RecreateViews(); + } + using namespace UI; // This is where views are recreated. @@ -1538,6 +1559,10 @@ bool EmuScreen::checkPowerDown() { // This is for handling things like sceKernelExitGame(). // Also for REQUEST_STOP. if (coreState == CORE_POWERDOWN && PSP_GetBootState() == BootState::Complete && !bootPending_) { + if (PSP_HasBootRequest()) { + // Not powering down: update() boots what it asks for. + return false; + } INFO_LOG(Log::System, "SELF-POWERDOWN!"); leaveGame(); return true; diff --git a/UI/EmuScreen.h b/UI/EmuScreen.h index 8aa1ed8be5..ca8f919bee 100644 --- a/UI/EmuScreen.h +++ b/UI/EmuScreen.h @@ -29,6 +29,7 @@ #include "Common/UI/Tween.h" #include "Core/KeyMap.h" #include "Core/ControlMapper.h" +#include "Core/CoreParameter.h" #include "UI/ImDebugger/ImCommand.h" struct AxisInput; @@ -112,6 +113,9 @@ private: Path gamePath_; Path mountIso_; std::optional umdAutoboot_; + // The rest of a BootRequest from the emulated system (see PSP_RequestBoot). + std::optional launchedFromVSH_; + std::vector vshmainArgs_; bool quit_ = false; std::string errorMessage_; diff --git a/docs/VSHBootInvestigation.md b/docs/VSHBootInvestigation.md index b6f61d8c85..fce5bea2c2 100644 --- a/docs/VSHBootInvestigation.md +++ b/docs/VSHBootInvestigation.md @@ -48,6 +48,15 @@ Two separate things: 6.61, to its title menu). In the app, the pause screen offers "Insert UMD" and "Eject UMD" while the XMB is running. + A disc game, or an EBOOT on the memory stick, that the XMB starts doesn't run inside the XMB's + session: `sceKernelLoadExec` asks the frontend to boot it (`PSP_RequestBoot`) and stops, and it + boots like a game picked from the game list, with its `PARAM.SFO`, compat flags, game config and + window title. Its `CoreParameter::launchedFromVSH` keeps the XMB's launch args, so exiting boots + the XMB again with whatever disc is in the drive, and the XMB comes back where it was without + auto-starting the disc. A disc whose executables are all zeroes (the EUR Video Demo Disc's + "game") still loads in place and fails back to the XMB. Under the debugger, the new boot starts + stopped, like the first one. + UMD Video discs work too: the disc type comes from what's on the disc (`PSP_GAME`, `UMD_VIDEO`, `UMD_AUDIO`), the shell lists a video disc in the Video column, and selecting it hands it to the firmware's own player (`video_plugin`, `video_main_plugin`, with `mpeg_vsh.prx`). Bourne Supremacy diff --git a/headless/Headless.cpp b/headless/Headless.cpp index b2bbca4c73..68bec897d3 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -559,7 +559,51 @@ static bool RunAutoTest(GraphicsContext *graphicsContext, CoreParameter &corePar int statsLastFlips = 0; std::vector> screenshotsAt = opt.screenshotsAt; coreState = coreParameter.startBreak ? CORE_STEPPING_CPU : CORE_RUNNING_CPU; - while (coreState == CORE_RUNNING_CPU || coreState == CORE_STEPPING_CPU) { + + // The XMB starting a game, or a game going back to it: boot that, the way the app does, and keep + // going on the same clocks. False if nothing asked for it, or the boot failed. + bool rebootFailed = false; + auto rebootIfRequested = [&]() -> bool { + BootRequest request; + if (coreState != CORE_POWERDOWN || !PSP_TakeBootRequest(&request)) { + return false; + } + if (gpu) { + gpu->EndHostFrame(); + } + if (draw) { + EndDrawFrame(draw); + } + PSP_Shutdown(true); + request.ApplyTo(&coreParameter); + std::string error; + if (PSP_InitStart(coreParameter)) { + while (PSP_InitUpdate(&error) == BootState::Booting) { + sleep_ms(1, "auto-test"); + } + } + if (!PSP_IsInited()) { + fprintf(stderr, "Failed to boot '%s': %s\n", coreParameter.fileToStart.c_str(), error.c_str()); + g_bootFailed = true; + rebootFailed = true; + return false; + } + System_Notify(SystemNotification::BOOT_DONE); + draw = coreParameter.graphicsContext ? coreParameter.graphicsContext->GetDrawContext() : nullptr; + if (draw) { + draw->BeginFrame(Draw::DebugFlags::NONE); + } + if (gpu) { + gpu->BeginHostFrame(g_Config.GetDisplayLayoutConfig(DeviceOrientation::Landscape)); + } + lastEmulatedTime = CoreTiming::GetGlobalTimeUs() / 1000000.0; + // Like the first boot. A debugger script's pending stop didn't survive the reboot, so it + // gets a fresh one here. + coreState = coreParameter.startBreak ? CORE_STEPPING_CPU : CORE_RUNNING_CPU; + return true; + }; + + while (coreState == CORE_RUNNING_CPU || coreState == CORE_STEPPING_CPU || rebootIfRequested()) { // Savestate loads/saves are queued and applied here, same as EmuScreen::render does in the // app. Without this, --state silently did nothing at all. SaveState::Process(); @@ -666,6 +710,10 @@ static bool RunAutoTest(GraphicsContext *graphicsContext, CoreParameter &corePar Core_Stop(); } } + + if (rebootFailed) { + return false; + } if (gpu) { gpu->EndHostFrame(); }