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(); }