Files
ppsspp/Core/System.cpp
T
Henrik RydgårdandClaude Opus 5 039ffc682d HLE: graduate the leaf libraries games carry on the disc
A survey of disc images found these shipped on a lot of them: LIBDEFLT and
LIBMT19937, LIBADLER, LIBMD5 on about 1/6, LIBSHA256 on 1/7, LIBHEAP on
1/12, LIBSFMT19937 on 1/24. None appears in any firmware dump and none is loadable
through sceUtility, so a game that imports one has to carry it - which makes
running the game's own module the safe default, the same argument psmfplayer
graduated on. Read off real discs with --re-module disc0:/..., all but sceHeap
import nothing at all, and sceHeap only Kernel_Library and ThreadManForUser.

sceFont joins them with a condition. Its module is on the disc like the others -
the most-shipped of all, on a third of the discs - but it reads its fonts from
flash0:/font and has nothing to fall back on, so it is only usable with a
firmware dump installed (our own fallback fonts are just not good enough anyway, it
chokes on them). CheckDisableHLEAvailability puts the HLE back when
those fonts are missing, since ours does have a fallback.

sceParseUri and sceParseHttp look like candidates and aren't: they are in the
firmware and sceUtility can load them, so a game may import them without
shipping one. Their headers say so.

The HLE implementations stay - old savestates index these tables by syscall
opcode - and are marked legacy the way scePsmf is.

Also: Move HLEInit after the filesystems are mounted

It ran before MountFileSystems, so the checks in it deciding whether a library
can run its real module instead of our HLE couldn't ask the PSP filesystem and
had to reach for host paths under the NAND directory. Nothing between
CoreTiming::Init and here touches HLE, and the kernel and the game's modules are
both loaded later, so it can simply move.

Two things fall out. The checks now spell their paths flash0:/... like the rest
of the emulator. And they run after AutoInstallFirmwareFromDisc, so a disc that
carries a firmware counts on the launch that installs it rather than the next
one - verified with an empty NAND and a disc carrying 3.11: the fonts land and
sceFont runs the game's own module the same boot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 21:09:33 -06:00

1000 lines
33 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/DisassemblyManager.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/Util/PSARUnpack.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::PSP_PKG: *errorString = "PKG game updates need to be installed, not booted."; 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();
// TODO: Put this somewhere better?
if (!g_CoreParameter.mountIso.empty()) {
g_CoreParameter.mountIsoLoader = ConstructFileLoader(g_CoreParameter.mountIso);
}
// Most game discs carry a firmware updater, so this is where a NAND with nothing (or only the
// fonts) in it gets filled in. Has to happen before the mount below: the install erases and
// rewrites the very directory flash0:/flash1: point at.
AutoInstallFirmwareFromDisc();
MountFileSystems();
// Init all the HLE modules. After the mount, and after the firmware install above, so that the
// checks in HLEInit deciding whether a library can run its real module instead of our HLE can
// ask the PSP filesystem the way everything else does - and see a firmware this very boot just
// installed off the disc. Nothing between CoreTiming::Init above and here touches HLE, and the
// kernel and the game's modules both come later, in the switch below.
HLEInit();
// 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.
// Everything the disassembly cache describes - emulated memory and the symbol map - is about
// to go away, so drop it here rather than leaving it to whichever debugger UI closes last.
ClearDisassemblyCache();
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);
});
}
}