Files
ppsspp/Core/System.cpp
T
Henrik Rydgård e2ab84087e Fix five ways to end up stuck, crashed, or silently degraded
System.cpp stamped BootState::Complete unconditionally after InitGPU(), overwriting
the Failed that InitGPU sets when GPU_Init() fails - after it has already run
CPU_Shutdown(). PSP_InitUpdate then took the success path on a core that no longer
existed, down to a null Memory::base, and the first guest access dereferenced it.
InitGPU now reports failure and both callers honor it. (The libretro path had the
same problem from the other direction: it calls InitGPU after the Failed check.)

HandleAssert called g_assertCancelCallback directly on the IDCANCEL path, without
the null check its own BreakIntoPSPDebugger() helper does - and EmuScreen clears the
callback when a game is unloaded. So any assert after returning to the menu turned
"Cancel: skip and break into PPSSPP debugger" into a null jump, from the one button
whose entire purpose is surviving the assert.

__CheatDoState registered the cheat event type when the savestate had no CwCheat
section, but never scheduled it. CoreTiming::DoState has already swapped in the
state's event queue by then, which doesn't contain one either - so loading an old
savestate silently killed cheats, and the enable/disable polling with them, for the
rest of the session.

Achievements::ChangeUMD set g_isIdentifying and returned without clearing it when
hashing failed, leaving IsBlockingExecution() true forever - EmuScreen stops running
the CPU and the game is frozen until restart. Reachable from a disc swap on any ISO
whose PARAM.SFO or EBOOT.BIN can't be read.

x64Analyzer routed opcode 0x88 into the write path but had no case for it, so it hit
the default, logged from inside the crash handler, and failed. 0x88 is exactly what
the x64 JIT emits for a guest sb, so MemFault could never skip or ignore a bad byte
store the way it can a word one. Handle the 8-bit forms, and drop the 0x8a/0x8b cases
in the read path that the same 0xF0 mask made unreachable. Covered by a new
CheckAnalyze case, which fails without this change.

314 pspautotests pass, all unit tests pass.
2026-08-31 12:27:24 +02:00

984 lines
32 KiB
C++

// Copyright (C) 2012 PPSSPP Project
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include <atomic>
#include "ppsspp_config.h"
#ifdef _WIN32
#pragma warning(disable:4091)
#include "Common/CommonWindows.h"
#include <ShlObj.h>
#include <string>
#if !PPSSPP_PLATFORM(UWP)
#include "Windows/W32Util/ShellUtil.h"
#endif
#endif
#include <mutex>
#include "ext/lua/lapi.h"
#include "Common/System/System.h"
#include "Common/System/Request.h"
#include "Common/System/OSD.h"
#include "Common/Data/Text/I18n.h"
#include "Common/File/Path.h"
#include "Common/StringUtils.h"
#include "Common/File/FileUtil.h"
#include "Common/File/DirListing.h"
#include "Common/File/AndroidContentURI.h"
#include "Common/Log/LogManager.h"
#include "Common/TimeUtil.h"
#include "Common/Thread/ThreadUtil.h"
#include "Common/GPU/GraphicsContext.h"
#include "Core/MemFault.h"
#include "Core/HDRemaster.h"
#include "Core/MIPS/MIPS.h"
#include "Core/MIPS/MIPSAnalyst.h"
#include "Core/MIPS/MIPSVFPUUtils.h"
#include "Core/Debugger/LineInfo.h"
#include "Core/Debugger/SymbolMap.h"
#include "Core/System.h"
#include "Core/HLE/HLE.h"
#include "Core/HLE/Plugins.h"
#include "Core/HLE/ReplaceTables.h"
#include "Core/HLE/sceKernel.h"
#include "Core/HLE/sceUtility.h"
#include "Core/HW/Display.h"
#include "Core/Config.h"
#include "Core/Core.h"
#include "Core/Util/PathUtil.h"
#include "Core/CoreTiming.h"
#include "Core/CoreParameter.h"
#include "Core/FileLoaders/RamCachingFileLoader.h"
#include "Core/FileSystems/MetaFileSystem.h"
#include "Core/FileSystems/ISOFileSystem.h"
#include "Core/FileSystems/DirectoryFileSystem.h"
#include "Core/LuaContext.h"
#include "Core/Loaders.h"
#include "Core/PSPLoaders.h"
#include "Core/ELF/ParamSFO.h"
#include "Core/SaveState.h"
#include "Core/Util/RecentFiles.h"
#include "Common/ExceptionHandlerSetup.h"
#include "GPU/GPUCommon.h"
#include "GPU/Debugger/Playback.h"
#include "GPU/Debugger/RecordFormat.h"
#include "UI/DiscordIntegration.h"
enum CPUThreadState {
CPU_THREAD_NOT_RUNNING,
CPU_THREAD_RUNNING,
};
MetaFileSystem pspFileSystem;
ParamSFOData g_paramSFO;
ParamSFOData g_paramSFORaw;
static GlobalUIState globalUIState;
CoreParameter g_CoreParameter;
static FileLoader *g_loadedFile;
// For background loading thread.
static std::mutex loadingLock;
static std::thread g_loadingThread;
bool g_coreCollectDebugStats = false;
static int g_coreCollectDebugStatsCounter = 0;
static volatile CPUThreadState cpuThreadState = CPU_THREAD_NOT_RUNNING;
static GPUBackend gpuBackend;
static std::string gpuBackendDevice;
static bool g_fileLoggingWasEnabled;
// Atomic because it's read as a fast-fail from the WebSocket debugger's own thread while the
// CPU and loader threads move it along.
static std::atomic<BootState> g_bootState = BootState::Off;
BootState PSP_GetBootState() {
return g_bootState;
}
FileLoader *PSP_LoadedFile() {
return g_loadedFile;
}
void ResetUIState() {
globalUIState = UISTATE_MENU;
}
void UpdateUIState(GlobalUIState newState) {
// Never leave the EXIT state.
if (globalUIState != newState && globalUIState != UISTATE_EXIT) {
globalUIState = newState;
System_Notify(SystemNotification::DISASSEMBLY);
System_Notify(SystemNotification::UI_STATE_CHANGED);
System_SetKeepScreenBright(globalUIState == UISTATE_INGAME);
}
}
GlobalUIState GetUIState() {
return globalUIState;
}
void SetGPUBackend(GPUBackend type, std::string_view device) {
gpuBackend = type;
gpuBackendDevice = device;
}
GPUBackend GetGPUBackend() {
return gpuBackend;
}
std::string GetGPUBackendDevice() {
return gpuBackendDevice;
}
void CPU_Shutdown(bool success);
static Path SymbolMapFilename(const Path &currentFilename, const char *ext) {
File::FileInfo info{};
// can't fail, definitely exists if it gets this far
File::GetFileInfo(currentFilename, &info);
if (info.isDirectory) {
return currentFilename / (std::string(".ppsspp-symbols") + ext);
}
return currentFilename.WithReplacedExtension(ext);
};
static bool LoadSymbolsIfSupported() {
if (System_GetPropertyBool(SYSPROP_HAS_DEBUGGER)) {
if (!g_symbolMap)
return false;
if (PSP_CoreParameter().fileToStart.Type() == PathType::HTTP) {
// We don't support loading symbols over HTTP.
g_symbolMap->Clear();
return true;
}
bool result1 = g_symbolMap->LoadSymbolMap(SymbolMapFilename(PSP_CoreParameter().fileToStart, ".ppmap"));
// Load the old-style map file.
if (!result1)
result1 = g_symbolMap->LoadSymbolMap(SymbolMapFilename(PSP_CoreParameter().fileToStart, ".map"));
bool result2 = g_symbolMap->LoadNocashSym(SymbolMapFilename(PSP_CoreParameter().fileToStart, ".sym"));
return result1 || result2;
} else {
g_symbolMap->Clear();
return true;
}
}
static bool SaveSymbolMapIfSupported() {
if (g_symbolMap) {
return g_symbolMap->SaveSymbolMap(SymbolMapFilename(PSP_CoreParameter().fileToStart, ".ppmap"));
}
return false;
}
// The counterparts to the per-module symbol auto-load/save in Core/HLE/sceKernelModule.cpp, for
// the symbols that don't belong to any module - see SymbolMap::GetGameSymbolsPath. Gated on the
// same config setting as the module ones and, like them, deliberately not on SYSPROP_HAS_DEBUGGER
// (which only the Windows port reports true for, so LoadSymbolsIfSupported above does nothing at
// all on headless).
static void LoadGameSymbolsIfEnabled() {
if (!g_symbolMap || !g_Config.bAutoSaveLoadSymbols)
return;
g_symbolMap->LoadModuleSymbols(0, SymbolMap::GetGameSymbolsPath(g_paramSFO.GetDiscID()));
}
static void SaveGameSymbolsIfEnabled() {
if (!g_symbolMap || !g_Config.bAutoSaveLoadSymbols)
return;
// Writes nothing (and cleans up any previous file) if there are no such symbols.
g_symbolMap->SaveModuleSymbols(0, SymbolMap::GetGameSymbolsPath(g_paramSFO.GetDiscID()),
g_paramSFO.GetDiscID(), g_paramSFO.GetValueString("TITLE"));
}
bool DiscIDFromGEDumpPath(const Path &path, FileLoader *fileLoader, std::string *id) {
using namespace GPURecord;
// For newer files, it's stored in the dump.
Header header;
if (fileLoader->ReadAt(0, sizeof(header), &header) == sizeof(header)) {
const bool magicMatch = memcmp(header.magic, HEADER_MAGIC, sizeof(header.magic)) == 0;
if (magicMatch && header.version <= VERSION && header.version >= 4) {
size_t gameIDLength = strnlen(header.gameID, sizeof(header.gameID));
if (gameIDLength != 0) {
*id = std::string(header.gameID, gameIDLength);
return true;
}
}
}
// Fall back to using the filename.
std::string filename = path.GetFilename();
// Could be more discerning, but hey..
if (filename.size() > 10 && (filename[0] == 'U' || filename[0] == 'N') && filename[9] == '_') {
*id = filename.substr(0, 9);
return true;
} else {
return false;
}
}
static void GetBootError(IdentifiedFileType type, std::string *errorString) {
switch (type) {
case IdentifiedFileType::ARCHIVE_RAR:
#ifdef WIN32
*errorString = "RAR file detected (Require WINRAR)";
#else
*errorString = "RAR file detected (Require UnRAR)";
#endif
break;
case IdentifiedFileType::ARCHIVE_ZIP:
#ifdef WIN32
*errorString = "ZIP file detected (Require WINRAR)";
#else
*errorString = "ZIP file detected (Require UnRAR)";
#endif
break;
case IdentifiedFileType::ARCHIVE_7Z: *errorString = "7z file detected (Require 7-Zip)"; break;
case IdentifiedFileType::PSX_ISO: *errorString = "PSX game image detected."; break;
case IdentifiedFileType::PS2_ISO: *errorString = "PS2 game image detected."; break;
case IdentifiedFileType::PS3_ISO: *errorString = "PS2 game image detected."; break;
case IdentifiedFileType::NORMAL_DIRECTORY: *errorString = "Just a directory."; break;
case IdentifiedFileType::PPSSPP_SAVESTATE: *errorString = "This is a saved state, not a game."; break; // Actually, we could make it load it...
case IdentifiedFileType::PSP_SAVEDATA_DIRECTORY: *errorString = "This is save data, not a game."; break;
case IdentifiedFileType::PSP_PS1_PBP: *errorString = "PS1 EBOOTs are not supported by PPSSPP."; break;
case IdentifiedFileType::PSP_UMD_VIDEO_ISO: *errorString = "UMD Video ISOs are not supported by PPSSPP."; break;
case IdentifiedFileType::UNKNOWN_BIN:
case IdentifiedFileType::UNKNOWN_ELF:
case IdentifiedFileType::UNKNOWN_ISO:
case IdentifiedFileType::UNKNOWN: *errorString = "Unknown executable file type."; break;
case IdentifiedFileType::ERROR_IDENTIFYING: *errorString = "Error identifying file."; break;
default:
*errorString = StringFromFormat("Unhandled identified file type %d", (int)type);
break;
}
}
static void ShowCompatWarnings(const Compatibility &compat) {
// UI changes are best done after PSP_InitStart.
if (compat.flags().RequireBufferedRendering && g_Config.bSkipBufferEffects && !g_Config.bSoftwareRendering) {
auto gr = GetI18NCategory(I18NCat::GRAPHICS);
g_OSD.Show(OSDType::MESSAGE_WARNING, gr->T("BufferedRenderingRequired", "Warning: This game requires Rendering Mode to be set to Buffered."), 10.0f, "bufreq");
} else {
g_OSD.CancelById("bufreq");
}
if (compat.flags().RequireBlockTransfer && g_Config.iSkipGPUReadbackMode != (int)SkipGPUReadbackMode::NO_SKIP && !PSP_CoreParameter().compat.flags().ForceEnableGPUReadback) {
auto gr = GetI18NCategory(I18NCat::GRAPHICS);
g_OSD.Show(OSDType::MESSAGE_WARNING, gr->T("BlockTransferRequired", "Warning: This game requires Skip GPU Readbacks be set to No."), 10.0f, "blockxfer");
} else {
g_OSD.CancelById("blockxfer");
}
if (compat.flags().RequireDefaultCPUClock && g_Config.iLockedCPUSpeed != 0) {
auto gr = GetI18NCategory(I18NCat::GRAPHICS);
g_OSD.Show(OSDType::MESSAGE_WARNING, gr->T("DefaultCPUClockRequired", "Warning: This game requires the CPU clock to be set to default."), 10.0f, "defaultclock");
} else {
g_OSD.CancelById("defaultclock");
}
}
extern const std::string INDEX_FILENAME;
static void MountFileSystems() {
FileSystemFlags memstickFlags = FileSystemFlags::SIMULATE_FAT32 | FileSystemFlags::CARD;
Path pspDir = GetSysDirectory(DIRECTORY_PSP);
if (pspDir == g_Config.memStickDirectory) {
// Initially tried to do this with dual mounts, but failed due to save state compatibility issues.
INFO_LOG(Log::sceIo, "Enabling /PSP compatibility mode");
memstickFlags |= FileSystemFlags::STRIP_PSP;
}
auto memstickSystem = std::make_shared<DirectoryFileSystem>(&pspFileSystem, g_Config.memStickDirectory, memstickFlags);
pspFileSystem.Mount("ms0:", memstickSystem);
pspFileSystem.Mount("fatms0:", memstickSystem);
pspFileSystem.Mount("fatms:", memstickSystem);
pspFileSystem.Mount("pfat0:", memstickSystem);
// TODO: These should be made "lazy" mounts.
auto flash0System = std::make_shared<DirectoryFileSystem>(&pspFileSystem, g_Config.nandRootDirectory / "flash0", FileSystemFlags::FLASH);
auto flash1System = std::make_shared<DirectoryFileSystem>(&pspFileSystem, g_Config.nandRootDirectory / "flash1", FileSystemFlags::FLASH);
pspFileSystem.Mount("flash0:", flash0System);
pspFileSystem.Mount("flash1:", flash1System);
// NOTE: We don't handle the host0: mount here, it's in Load_PSP_ELF_PBP.
if (g_RemasterMode) {
const std::string gameId = g_paramSFO.GetDiscID();
const Path exdataPath = GetSysDirectory(DIRECTORY_EXDATA) / gameId;
if (File::Exists(exdataPath)) {
auto exdataSystem = std::make_shared<DirectoryFileSystem>(&pspFileSystem, exdataPath, FileSystemFlags::SIMULATE_FAT32 | FileSystemFlags::CARD);
pspFileSystem.Mount("exdata0:", exdataSystem);
INFO_LOG(Log::sceIo, "Mounted exdata/%s/ under memstick for exdata0:/", gameId.c_str());
} else {
INFO_LOG(Log::sceIo, "Did not find exdata/%s/ under memstick for exdata0:/", gameId.c_str());
}
}
}
// NOTE: The loader has already been fully resolved (ResolveFileLoaderTarget) and identified here.
static bool CPU_Init(FileLoader *fileLoader, IdentifiedFileType type, std::string *errorString) {
// Default memory settings
// Seems to be the safest place currently..
Memory::g_MemorySize = Memory::RAM_NORMAL_SIZE; // 32 MB of ram by default
g_RemasterMode = false;
g_DoubleTextureCoordinates = false;
Memory::g_PSPModel = g_Config.iPSPModel;
g_CoreParameter.fileType = type;
std::string geDumpDiscID;
std::string gameTitle = "Unidentified PSP title";
switch (type) {
case IdentifiedFileType::PSP_ISO:
case IdentifiedFileType::PSP_ISO_NP:
case IdentifiedFileType::PSP_DISC_DIRECTORY:
// Doesn't take ownership of fileLoader. We store it in g_loadedFile later.
if (!MountGameISO(fileLoader, errorString)) {
*errorString = "Failed to mount ISO file: " + *errorString;
return false;
}
if (LoadParamSFOFromDisc()) {
InitMemorySizeForGame();
}
if (type == IdentifiedFileType::PSP_ISO_NP && ((DumpFileType)g_Config.iDumpFileTypes & DumpFileType::PBP_ISO)) {
// We have to fetch the block device again, because we want to do this after loading PARAM.SFO.
std::string title = SanitizeString(g_paramSFO.GetValueString("TITLE"), StringRestriction::AlphaNumDashUnderscore, 0, 100);
std::string filename = title + "-" + g_paramSFO.GetDiscID() + ".iso";
Path path = GetSysDirectory(DIRECTORY_DUMP) / filename;
IFileSystem *fs = pspFileSystem.GetSystem("umd0:");
if (fs) {
std::shared_ptr<BlockDevice> bd = fs->GetBlockDevice();
if (bd) {
DumpBlockDeviceAsync(bd, path, title);
}
}
} else if (type == IdentifiedFileType::PSP_DISC_DIRECTORY) {
// Check for existence of ppsspp-index.lst - if it exists, the user likely knows what they're doing.
// TODO: Better would be to check that it was loaded successfully.
if (!File::Exists(g_CoreParameter.fileToStart / INDEX_FILENAME)) {
auto sc = GetI18NCategory(I18NCat::SCREEN);
g_OSD.Show(OSDType::MESSAGE_WARNING, sc->T("ExtractedIsoWarning", "Extracted ISOs often don't work.\nPlay the ISO file directly."), GetFriendlyPath(g_CoreParameter.fileToStart), 7.0f);
} else {
INFO_LOG(Log::Loader, "Extracted ISO loaded without warning - %s is present.", INDEX_FILENAME.c_str());
}
}
break;
case IdentifiedFileType::PSP_PBP:
case IdentifiedFileType::PSP_PBP_DIRECTORY:
// This is normal for homebrew.
// ERROR_LOG(Log::Loader, "PBP directory resolution failed.");
if (LoadParamSFOFromPBP(fileLoader)) {
InitMemorySizeForGame();
}
break;
case IdentifiedFileType::PSP_ELF:
if (Memory::g_PSPModel != PSP_MODEL_FAT) {
INFO_LOG(Log::Loader, "ELF, using full PSP-2000 memory access");
Memory::g_MemorySize = Memory::RAM_DOUBLE_SIZE;
}
break;
case IdentifiedFileType::PPSSPP_GE_DUMP:
// Try to grab the disc ID from the filename or GE dump.
if (DiscIDFromGEDumpPath(g_CoreParameter.fileToStart, fileLoader, &geDumpDiscID)) {
// Store in SFO, otherwise it'll generate a fake disc ID.
g_paramSFO.SetValue("DISC_ID", geDumpDiscID, 16);
gameTitle = g_CoreParameter.fileToStart.GetFilename();
}
break;
case IdentifiedFileType::PSP_UMD_VIDEO_ISO:
{
ERROR_LOG(Log::Loader, "PPSSPP doesn't support UMD Video.");
auto er = GetI18NCategory(I18NCat::ERRORS);
*errorString = er->T("PPSSPP doesn't support UMD Video.");
return false;
}
default:
{
// Trying to boot other things lands us here. We need to return a sensible error string.
ERROR_LOG(Log::Loader, "CPU_Init didn't recognize file: %s. %s", fileLoader->GetPath().c_str(), errorString->c_str());
Core_SendDebugOutput(LogLevel::LINFO, StringFromFormat("File not recognized: %s. %s", fileLoader->GetPath().c_str(), errorString->c_str()));
auto sy = GetI18NCategory(I18NCat::SYSTEM);
if (errorString->empty()) {
*errorString = sy->T("Not a PSP game");
}
return false;
}
}
// OK, pretty confident we have a PSP game.
if (g_paramSFO.IsValid()) {
gameTitle = SanitizeString(g_paramSFO.GetValueString("TITLE"), StringRestriction::NoLineBreaksOrSpecials);
}
const std::string id = g_paramSFO.GetValueString("DISC_ID");
const std::string windowTitle = id.empty() ? gameTitle : id + " : " + gameTitle;
INFO_LOG(Log::Loader, "%s", windowTitle.c_str());
System_SetWindowTitle(windowTitle);
auto sc = GetI18NCategory(I18NCat::SCREEN);
_dbg_assert_(!g_symbolMap);
g_symbolMap = new SymbolMap();
MIPSAnalyst::Reset();
Replacement_Init();
g_lua.Init();
// Here we have read the PARAM.SFO, let's see if we need any compatibility overrides.
// Homebrew get fake disc IDs assigned to the global paramSFO, so they shouldn't clash with real games.
std::string discId = g_paramSFO.GetDiscID();
g_CoreParameter.compat.Load(discId);
ShowCompatWarnings(g_CoreParameter.compat);
// This must be before Memory::Init because plugins can override the memory size.
if (type != IdentifiedFileType::PPSSPP_GE_DUMP) {
HLEPlugins::Init();
}
Memory::MemMapSetupFlags memMapFlags = Memory::MemMapSetupFlags::Default;
if (g_CoreParameter.compat.flags().NullPageValid) {
memMapFlags = Memory::MemMapSetupFlags::AllocNullPage;
}
// Initialize the memory map as early as possible (now that we've read the PARAM.SFO).
if (!Memory::Init(memMapFlags)) {
// We're screwed.
*errorString = "Memory init failed";
return false;
}
// If it was forced on the command line. We don't want to override that.
g_fileLoggingWasEnabled = g_logManager.GetOutputsEnabled() & LogOutput::File;
g_logManager.EnableOutput(LogOutput::File, g_Config.bEnableFileLogging || g_fileLoggingWasEnabled);
if ((g_logManager.GetOutputsEnabled() & LogOutput::File) && !g_logManager.GetLogFilePath().empty()) {
auto dev = GetI18NCategory(I18NCat::DEVELOPER);
Path logPath = g_logManager.GetLogFilePath();
g_OSD.Show(OSDType::MESSAGE_INFO, ApplySafeSubstitutions("%1: %2", dev->T("Log to file"), g_logManager.GetLogFilePath().ToVisualString()), 0.0f, "log_to_file");
if (System_GetPropertyBool(SYSPROP_CAN_SHOW_FILE)) {
g_OSD.SetClickCallback("log_to_file", [logPath]() {
System_ShowFileInFolder(logPath);
});
}
}
InitVFPU();
LoadSymbolsIfSupported();
LoadGameSymbolsIfEnabled();
mipsr4k.Reset();
CoreTiming::Init(&mipsr4k);
DisplayHWInit();
// Init all the HLE modules
HLEInit();
// TODO: Put this somewhere better?
if (!g_CoreParameter.mountIso.empty()) {
g_CoreParameter.mountIsoLoader = ConstructFileLoader(g_CoreParameter.mountIso);
}
MountFileSystems();
// Game-specific settings are load from for example Load_PSP_ISO (which calls g_Config.LoadGameConfig).
// We can't do things that depend on these before the below switch. So for example, the adjustment of the GPU core
// to software has now been moved below it.
// If they shut down early, we'll catch it when load completes.
// Note: this may return before init is complete, which is checked if CPU_IsReady().
g_loadedFile = fileLoader;
switch (type) {
case IdentifiedFileType::PSP_PBP_DIRECTORY:
{
std::string dir = fileLoader->GetPath().GetDirectory();
if (fileLoader->GetPath().Type() == PathType::CONTENT_URI) {
dir = AndroidContentURI(dir).FilePath();
}
size_t pos = dir.find("PSP/GAME/");
if (pos != std::string::npos) {
dir = ResolvePBPDirectory(Path(dir)).ToString();
pspFileSystem.SetStartingDirectory("ms0:/" + dir.substr(pos));
}
if (!Load_PSP_ELF_PBP(fileLoader, discId, g_CoreParameter.loadGameConfigs, errorString)) {
return false;
}
break;
}
// Looks like a wrong fall through but is not, both paths are handled above.
case IdentifiedFileType::PSP_PBP:
case IdentifiedFileType::PSP_ELF:
{
INFO_LOG(Log::Loader, "File is an ELF or loose PBP %s", fileLoader->GetPath().c_str());
if (!Load_PSP_ELF_PBP(fileLoader, discId, g_CoreParameter.loadGameConfigs, errorString)) {
ERROR_LOG(Log::Loader, "Failed to load ELF or loose PBP: %s", errorString->c_str());
return false;
}
break;
}
case IdentifiedFileType::PSP_ISO:
case IdentifiedFileType::PSP_ISO_NP:
case IdentifiedFileType::PSP_DISC_DIRECTORY: // behaves the same as the mounting is already done by now
pspFileSystem.SetStartingDirectory("disc0:/PSP_GAME/USRDIR");
if (!Load_PSP_ISO(fileLoader, errorString)) {
return false;
}
break;
case IdentifiedFileType::PPSSPP_GE_DUMP:
if (!Load_PSP_GE_Dump(fileLoader, errorString)) {
return false;
}
break;
default:
GetBootError(type, errorString);
g_CoreParameter.fileToStart.clear();
return false;
}
if (g_CoreParameter.updateRecent) {
g_recentFiles.Add(g_CoreParameter.fileToStart.ToString());
}
// Update things that depend on game-specific config here.
// Compat settings can override the software renderer, take care of that here.
if (g_Config.bSoftwareRendering || g_CoreParameter.compat.flags().ForceSoftwareRenderer) {
g_CoreParameter.gpuCore = GPUCORE_SOFTWARE;
}
InstallExceptionHandler(&Memory::HandleFault, g_Config.bLogNativeCrashStackTraces);
return true;
}
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
// Core_LockAgainstShutdown(); it's recursive, so the nested acquire in Memory::Shutdown() is fine.
CoreShutdownLock coreLock = Core_LockAgainstShutdown();
UninstallExceptionHandler();
GPURecord::Replay_Unload();
if (g_Config.bAutoSaveSymbolMap && success) {
SaveSymbolMapIfSupported();
}
if (success) {
SaveGameSymbolsIfEnabled();
}
Replacement_Shutdown();
CoreTiming::Shutdown();
__KernelShutdown();
HLEShutdown();
DisplayHWShutdown();
pspFileSystem.Shutdown(); // This unmounts all filesystems.
mipsr4k.Shutdown();
Memory::Shutdown();
HLEPlugins::Shutdown();
delete g_loadedFile;
g_loadedFile = nullptr;
delete g_CoreParameter.mountIsoLoader;
g_CoreParameter.mountIsoLoader = nullptr;
delete g_symbolMap;
g_symbolMap = nullptr;
// Line info outlives individual modules on purpose (see ~PSPModule), so the game going away is
// what ends it.
g_lineInfo.Clear();
g_lua.Shutdown();
g_logManager.EnableOutput(LogOutput::File, g_fileLoggingWasEnabled);
}
// Used for UMD switching only.
void UpdateLoadedFile(FileLoader *fileLoader) {
delete g_loadedFile;
g_loadedFile = fileLoader;
}
void PSP_UpdateDebugStats(bool collectStats) {
bool newState = collectStats || g_coreCollectDebugStatsCounter > 0;
if (g_coreCollectDebugStats != newState) {
g_coreCollectDebugStats = newState;
mipsr4k.ClearJitCacheDeferred();
}
if (!PSP_CoreParameter().frozen && !Core_IsStepping()) {
kernelStats.ResetFrame();
gpuStats.ResetFrame();
}
}
void PSP_ForceDebugStats(bool enable) {
if (enable) {
g_coreCollectDebugStatsCounter++;
} else {
g_coreCollectDebugStatsCounter--;
}
_assert_(g_coreCollectDebugStatsCounter >= 0);
}
// Returns false if the GPU couldn't be brought up - in which case it has already set
// BootState::Failed and torn the CPU back down, so the caller must not carry on.
static bool InitGPU(std::string *error_string) {
if (!gpu) { // should be!
INFO_LOG(Log::Loader, "Starting graphics...");
Draw::DrawContext *draw = g_CoreParameter.graphicsContext ? g_CoreParameter.graphicsContext->GetDrawContext() : nullptr;
// This set the `gpu` global.
GPUCore gpuCore = PSP_CoreParameter().gpuCore;
bool success = GPU_Init(gpuCore, g_CoreParameter.graphicsContext, draw);
if (!success) {
*error_string = "Unable to initialize rendering engine.";
CPU_Shutdown(false);
g_bootState = BootState::Failed;
return false;
}
}
return true;
}
bool PSP_InitStart(const CoreParameter &coreParam) {
if (g_bootState != BootState::Off) {
ERROR_LOG(Log::Loader, "Can't start loader thread - already on.");
return false;
}
IncrementDebugCounter(DebugCounter::GAME_BOOT);
g_bootState = BootState::Booting;
GraphicsContext *temp = g_CoreParameter.graphicsContext;
g_CoreParameter = coreParam;
if (g_CoreParameter.graphicsContext == nullptr) {
g_CoreParameter.graphicsContext = temp;
}
g_CoreParameter.errorString.clear();
std::string *errorString = &g_CoreParameter.errorString;
INFO_LOG(Log::Loader, "Starting loader thread...");
_assert_msg_(!g_loadingThread.joinable(), "%s", coreParam.fileToStart.c_str());
Core_NotifyLifecycle(CoreLifecycle::STARTING);
g_loadingThread = std::thread([errorString]() {
SetCurrentThreadName("ExecLoader");
AndroidJNIThreadContext jniContext;
NOTICE_LOG(Log::Boot, "PPSSPP %s", PPSSPP_GIT_VERSION);
Path filename = g_CoreParameter.fileToStart;
IdentifiedFileType fileType;
FileLoader *loadedFile = ResolveFileLoaderTarget(ConstructFileLoader(filename), &fileType, errorString);
if (System_GetPropertyBool(SYSPROP_ENOUGH_RAM_FOR_FULL_ISO)) {
if (g_Config.bCacheFullIsoInRam) {
switch (fileType) {
case IdentifiedFileType::PSP_ISO:
case IdentifiedFileType::PSP_ISO_NP:
loadedFile = new RamCachingFileLoader(loadedFile);
break;
default:
INFO_LOG(Log::Loader, "RAM caching is on, but file is not an ISO, so ignoring");
break;
}
}
}
// Use this to test exit-during-boot and other exceptional cases.
// sleep_ms(6000, "test");
g_CoreParameter.fileType = fileType;
// TODO: The reason we pass in g_CoreParameter.errorString here is that it's persistent -
// it gets written to from the loader thread that gets spawned.
if (!CPU_Init(loadedFile, fileType, &g_CoreParameter.errorString)) {
CPU_Shutdown(false);
g_CoreParameter.fileToStart.clear();
*errorString = g_CoreParameter.errorString;
if (errorString->empty()) {
*errorString = "Failed initializing CPU/Memory";
}
g_bootState = BootState::Failed;
return;
}
// Initialize the GPU as far as we can here (do things like load cache files).
_dbg_assert_(!gpu);
#ifndef __LIBRETRO__
// Must not stamp Complete over the Failed that InitGPU sets - it has already run
// CPU_Shutdown(), so PSP_InitUpdate would take the success path on a core that no longer
// exists, right down to a null Memory::base.
if (!InitGPU(errorString)) {
return;
}
#endif
g_bootState = BootState::Complete;
});
return true;
}
BootState PSP_InitUpdate(std::string *error_string) {
const BootState bootState = g_bootState;
if (bootState == BootState::Booting || bootState == BootState::Off) {
// Nothing to do right now.
_dbg_assert_(bootState == BootState::Booting || !g_loadingThread.joinable());
return bootState;
}
_dbg_assert_(bootState == BootState::Complete || bootState == BootState::Failed);
// Since we load on a background thread, wait for startup to complete.
_assert_msg_(g_loadingThread.joinable(), "bootstate: %d", (int)bootState);
g_loadingThread.join();
if (bootState == BootState::Failed) {
// Failed! (Note: PSP_Shutdown was already called on the loader thread).
Core_NotifyLifecycle(CoreLifecycle::START_COMPLETE);
*error_string = g_CoreParameter.errorString;
g_bootState = BootState::Off;
return BootState::Failed;
}
#ifdef __LIBRETRO__
if (!InitGPU(error_string)) {
// Same as the Failed branch above - the core is already gone.
Core_NotifyLifecycle(CoreLifecycle::START_COMPLETE);
*error_string = g_CoreParameter.errorString;
g_bootState = BootState::Off;
return BootState::Failed;
}
#endif
// Ok, async part of the boot completed, let's finish up things on the main thread.
if (gpu) {
gpu->FinishInitOnMainThread();
} else {
_dbg_assert_(gpu);
}
Core_NotifyLifecycle(CoreLifecycle::START_COMPLETE);
// The thread should have set it at this point.
_dbg_assert_(bootState == BootState::Complete);
return BootState::Complete;
}
// Most platforms should not use this one, they should call PSP_InitStart and then do their thing
// while repeatedly calling PSP_InitUpdate. This is basically just for libretro convenience.
BootState PSP_Init(const CoreParameter &coreParam, std::string *error_string) {
// InitStart doesn't really fail anymore.
if (!PSP_InitStart(coreParam)) {
g_bootState = BootState::Off;
return BootState::Failed;
}
while (true) {
BootState state = PSP_InitUpdate(error_string);
if (state != BootState::Booting) {
return state;
}
sleep_ms(5, "psp-init-poll");
}
}
BootState PollBootState() {
return g_bootState;
}
void PSP_Shutdown(bool success) {
// Do nothing if we never inited.
if (g_bootState == BootState::Off) {
ERROR_LOG(Log::Loader, "Unexpected PSP_Shutdown");
return;
}
_assert_(g_bootState != BootState::Failed);
Core_Stop();
if (g_Config.bFuncHashMap) {
MIPSAnalyst::StoreHashMap();
}
if (g_bootState == BootState::Booting) {
// This should only happen during failures.
Core_NotifyLifecycle(CoreLifecycle::START_COMPLETE);
}
Core_NotifyLifecycle(CoreLifecycle::STOPPING);
CPU_Shutdown(success);
GPU_Shutdown();
g_paramSFO.Clear();
g_paramSFORaw.Clear();
System_SetWindowTitle("");
currentMIPS = nullptr;
if (g_Config.IsGameSpecific()) {
g_Config.UnloadGameConfig();
}
Core_NotifyLifecycle(CoreLifecycle::STOPPED);
if (success) {
g_bootState = BootState::Off;
}
IncrementDebugCounter(DebugCounter::GAME_SHUTDOWN);
}
void PSP_RunLoopWhileState() {
// We just run the CPU until we get to vblank. This will quickly sync up pretty nicely.
// The actual number of cycles doesn't matter so much here as we will break due to CORE_NEXTFRAME, most of the time hopefully...
int blockTicks = usToCycles(1000000 / 10);
// Run until CORE_NEXTFRAME
PSP_RunLoopFor(blockTicks);
// TODO: Check for frame timeout?
}
void PSP_RunLoopFor(int cycles) {
Core_RunLoopUntil(CoreTiming::GetTicks(currentMIPS) + cycles);
}
const char *DumpFileTypeToString(DumpFileType type) {
switch (type) {
case DumpFileType::EBOOT: return "EBOOT";
case DumpFileType::PRX: return "PRX";
case DumpFileType::Atrac3: return "AT3";
default: return "N/A";
}
}
const char *DumpFileTypeToFileExtension(DumpFileType type) {
switch (type) {
case DumpFileType::EBOOT: return ".BIN";
case DumpFileType::PRX: return ".prx";
case DumpFileType::Atrac3: return ".at3";
default: return "N/A";
}
}
void DumpFileIfEnabled(const u8 *dataPtr, const u32 length, std::string_view name, DumpFileType type) {
if (!(g_Config.iDumpFileTypes & (int)type)) {
return;
}
if (!dataPtr) {
ERROR_LOG(Log::Loader, "Error dumping %s: invalid pointer", DumpFileTypeToString(DumpFileType::EBOOT));
return;
}
if (length == 0) {
ERROR_LOG(Log::Loader, "Error dumping %s: invalid length", DumpFileTypeToString(DumpFileType::EBOOT));
return;
}
const char *extension = DumpFileTypeToFileExtension(type);
std::string filenameToDumpTo = g_paramSFO.GetDiscID() + "_" + std::string(name);
if (!endsWithNoCase(filenameToDumpTo, extension)) {
filenameToDumpTo += extension;
}
const Path dumpDirectory = GetSysDirectory(DIRECTORY_DUMP);
const Path fullPath = dumpDirectory / filenameToDumpTo;
auto s = GetI18NCategory(I18NCat::SYSTEM);
std::string_view titleStr = "Dump Decrypted Eboot";
if (type != DumpFileType::EBOOT) {
titleStr = s->T(DumpFileTypeToString(type));
}
// If the file already exists, don't dump it again.
if (File::Exists(fullPath)) {
INFO_LOG(Log::sceModule, "%s already exists for this game, skipping dump.", filenameToDumpTo.c_str());
g_OSD.Show(OSDType::MESSAGE_INFO, titleStr, fullPath.ToVisualString(), 5.0f, "file_dumped");
if (System_GetPropertyBool(SYSPROP_CAN_SHOW_FILE)) {
g_OSD.SetClickCallback("file_dumped", [fullPath]() {
System_ShowFileInFolder(fullPath);
});
}
return;
}
// Make sure the dump directory exists before continuing.
if (!File::Exists(dumpDirectory)) {
if (!File::CreateDir(dumpDirectory)) {
ERROR_LOG(Log::sceModule, "Unable to create directory for EBOOT dumping, aborting.");
return;
}
}
FILE *file = File::OpenCFile(fullPath, "wb");
if (!file) {
ERROR_LOG(Log::sceModule, "Unable to write decrypted EBOOT.");
return;
}
const size_t lengthToWrite = length;
fwrite(dataPtr, sizeof(u8), lengthToWrite, file);
fclose(file);
INFO_LOG(Log::sceModule, "Successfully wrote %s to %s", DumpFileTypeToString(type), fullPath.ToVisualString().c_str());
// Re-using the translation string here.
g_OSD.Show(OSDType::MESSAGE_SUCCESS, titleStr, fullPath.ToVisualString(), 5.0f, "decr");
if (System_GetPropertyBool(SYSPROP_CAN_SHOW_FILE)) {
g_OSD.SetClickCallback("decr", [fullPath]() {
System_ShowFileInFolder(fullPath);
});
}
}