mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-12 17:19:28 +01:00
Headless: Boot a savestate without its game
Running a .ppst boots a placeholder program, with the disc ID and boot type peeked from the state, then loads the state over it. An ISO game gets a disc with nothing on it, which the mounts now load into by name. Meant for measuring performance with states that can be kept without the games; the app still refuses to boot a state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
09f65a860c
commit
8122a5ec70
8 files changed
+127
-1
No files matched your search
@@ -92,6 +92,8 @@ struct CoreParameter {
|
||||
bool startBreak = false;
|
||||
std::string *collectDebugOutput = nullptr;
|
||||
bool headLess = false; // Try to avoid messageboxes etc
|
||||
// Headless only: allows booting a savestate without its game, for measuring performance.
|
||||
bool bootFromState = false;
|
||||
// Headless leaves out compat settings when replaying frame dumps, unless this is set.
|
||||
bool dumpCompat = false;
|
||||
// Headless: GPU_USE_ feature bits to turn off, to test the fallbacks for GPUs without them.
|
||||
|
||||
@@ -2618,6 +2618,29 @@ bool __KernelLoadGEDump(MIPSState *mips, std::string_view base_filename, std::st
|
||||
return true;
|
||||
}
|
||||
|
||||
void __KernelLoadStateBootStub(MIPSState *mips) {
|
||||
__KernelLoadReset();
|
||||
|
||||
constexpr u32 codeStartAddr = PSP_GetUserMemoryBase();
|
||||
mips->pc = codeStartAddr;
|
||||
// b . (to itself), and the delay slot.
|
||||
Memory::WriteUnchecked_U32(MIPS_MAKE_B(-1), codeStartAddr);
|
||||
Memory::WriteUnchecked_U32(MIPS_MAKE_NOP(), codeStartAddr + 4);
|
||||
|
||||
PSPModule *module = new PSPModule();
|
||||
kernelObjects.Create(module);
|
||||
loadedModules.insert(module->GetUID());
|
||||
memset(&module->nm, 0, sizeof(module->nm));
|
||||
module->isFake = true;
|
||||
module->nm.entry_addr = codeStartAddr;
|
||||
module->nm.gp_value = -1;
|
||||
|
||||
SceUID threadID = __KernelSetupRootThread(module->GetUID(), 0, nullptr, 0x20, 0x1000, 0);
|
||||
__KernelSetThreadRA(threadID, NID_MODULERETURN);
|
||||
|
||||
__KernelStartIdleThreads(module->GetUID());
|
||||
}
|
||||
|
||||
int __KernelGPUReplay(MIPSState *mips) {
|
||||
// Special ABI: s0 and s1 are the "args". Not null terminated.
|
||||
const char *filenamep = Memory::GetCharPointer(mips->r[MIPS_REG_S1]);
|
||||
|
||||
@@ -241,6 +241,8 @@ class MIPSState;
|
||||
u32 __KernelGetModuleGP(SceUID module);
|
||||
bool KernelModuleIsKernelMode(SceUID module);
|
||||
bool __KernelLoadGEDump(MIPSState *mips, std::string_view base_filename, std::string *error_string);
|
||||
// A placeholder program that loops until a savestate is loaded over it.
|
||||
void __KernelLoadStateBootStub(MIPSState *mips);
|
||||
// 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<u8> *hostArgs = nullptr);
|
||||
// The VSH (the XMB) is what's running, not a game it launched.
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
#include "Core/ConfigValues.h"
|
||||
#include "Core/System.h"
|
||||
#include "Core/PSPLoaders.h"
|
||||
#include "Core/SaveState.h"
|
||||
#include "Core/HLE/sceKernelModule.h"
|
||||
#include "Core/HLE/PopsBoot.h"
|
||||
#include "Core/Util/PSARUnpack.h"
|
||||
@@ -699,6 +700,65 @@ bool Load_PSP_POPS(FileLoader *fileLoader, std::string *error_string) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// A disc with nothing on it. Every block reads as zeros, so the ISO filesystem finds no volume
|
||||
// descriptor and no files.
|
||||
class EmptyDiscBlockDevice : public BlockDevice {
|
||||
public:
|
||||
EmptyDiscBlockDevice() : BlockDevice(nullptr) {}
|
||||
bool ReadBlock(int blockNumber, u8 *outPtr, bool uncached) override {
|
||||
memset(outPtr, 0, GetBlockSize());
|
||||
return true;
|
||||
}
|
||||
u32 GetNumBlocks() const override { return 0; }
|
||||
bool IsDisc() const override { return true; }
|
||||
};
|
||||
|
||||
bool MountStateBoot(const Path &statePath, std::string *errorString) {
|
||||
SaveState::BootInfo info;
|
||||
if (!SaveState::PeekBootInfo(statePath, &info, errorString)) {
|
||||
return false;
|
||||
}
|
||||
if (info.discID.empty()) {
|
||||
*errorString = "The state doesn't say which game it's from";
|
||||
return false;
|
||||
}
|
||||
switch (info.bootType) {
|
||||
case SaveState::BootType::ISO:
|
||||
{
|
||||
// The game's open files on the disc are dropped as the state loads, and opening any
|
||||
// file fails, so the game only gets so far.
|
||||
auto iso = std::make_shared<ISOFileSystem>(&pspFileSystem, std::make_shared<EmptyDiscBlockDevice>());
|
||||
auto blockSystem = std::make_shared<ISOBlockSystem>(iso);
|
||||
pspFileSystem.Mount("umd0:", blockSystem);
|
||||
pspFileSystem.Mount("umd1:", blockSystem);
|
||||
pspFileSystem.Mount("umd:", blockSystem);
|
||||
pspFileSystem.Mount("disc0:", iso);
|
||||
break;
|
||||
}
|
||||
case SaveState::BootType::PBP:
|
||||
case SaveState::BootType::ELF:
|
||||
break;
|
||||
default:
|
||||
*errorString = "Can only boot states of games run from an ISO, PBP or ELF without the game";
|
||||
return false;
|
||||
}
|
||||
|
||||
g_paramSFO.SetValue("DISC_ID", info.discID, 16);
|
||||
g_paramSFO.SetValue("DISC_VERSION", info.discVersion, 8);
|
||||
g_paramSFO.SetValue("TITLE", SaveState::GetTitle(statePath), 128);
|
||||
INFO_LOG(Log::Loader, "Booting state %s of %s %s without the game", statePath.c_str(), info.discID.c_str(), info.discVersion.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Load_PSP_StateBoot(std::string *error_string) {
|
||||
if (PSP_CoreParameter().loadGameConfigs) {
|
||||
g_Config.LoadGameConfig(g_paramSFO.GetDiscID());
|
||||
System_PostUIMessage(UIMessage::CONFIG_LOADED);
|
||||
}
|
||||
__KernelLoadStateBootStub(currentMIPS);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Load_PSP_GE_Dump(FileLoader *fileLoader, std::string *error_string) {
|
||||
auto umd = std::make_shared<BlobFileSystem>(&pspFileSystem, fileLoader, "data.ppdmp");
|
||||
pspFileSystem.Mount("disc0:", umd);
|
||||
|
||||
@@ -26,6 +26,10 @@ class BlockDevice;
|
||||
bool Load_PSP_ISO(FileLoader *fileLoader, std::string *error_string);
|
||||
bool Load_PSP_ELF_PBP(FileLoader *fileLoader, std::string_view discId, bool loadGameConfigs, std::string *error_string);
|
||||
bool Load_PSP_GE_Dump(FileLoader *fileLoader, std::string *error_string);
|
||||
// Booting a savestate without its game: the first sets up the disc ID and a disc with nothing on it,
|
||||
// the second a placeholder program that runs until the state is loaded.
|
||||
bool MountStateBoot(const Path &statePath, std::string *errorString);
|
||||
bool Load_PSP_StateBoot(std::string *error_string);
|
||||
bool Load_PSP_POPS(FileLoader *fileLoader, std::string *error_string);
|
||||
|
||||
bool MountGameISO(FileLoader *fileLoader, std::string *errorString);
|
||||
|
||||
+16
-1
@@ -297,7 +297,7 @@ static void GetBootError(IdentifiedFileType type, std::string *errorString) {
|
||||
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::PPSSPP_SAVESTATE: *errorString = "This is a saved state, not a game."; break;
|
||||
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 = std::string("UMD Video plays in the XMB, which needs PSP firmware ") + UMD_VIDEO_MIN_FIRMWARE + " or later."; break;
|
||||
@@ -481,6 +481,15 @@ static bool CPU_Init(FileLoader *fileLoader, IdentifiedFileType type, std::strin
|
||||
// Title and disc ID; the PS1 image itself is read by the firmware's POPS manager.
|
||||
LoadParamSFOFromPBP(fileLoader);
|
||||
break;
|
||||
case IdentifiedFileType::PPSSPP_SAVESTATE:
|
||||
if (!g_CoreParameter.bootFromState) {
|
||||
GetBootError(type, errorString);
|
||||
return false;
|
||||
}
|
||||
if (!MountStateBoot(g_CoreParameter.fileToStart, errorString)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case IdentifiedFileType::PSP_UMD_VIDEO_ISO:
|
||||
{
|
||||
// The game list boots the XMB for these, which plays them.
|
||||
@@ -669,6 +678,12 @@ static bool CPU_Init(FileLoader *fileLoader, IdentifiedFileType type, std::strin
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case IdentifiedFileType::PPSSPP_SAVESTATE:
|
||||
// Headless loads the state itself, as with --state.
|
||||
if (!Load_PSP_StateBoot(errorString)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
GetBootError(type, errorString);
|
||||
|
||||
@@ -223,6 +223,14 @@ To check that a game keeps running after `--state=` loads, pass `--frame-stats`:
|
||||
the flips in each emulated second since the load. A stuck or crashed game shows as zeros or a short list, and a
|
||||
steady game as a flat 30 or 60. The state's load result is printed too, and a failed load exits 1.
|
||||
|
||||
Headless can also boot a savestate without its game: pass the `.ppst` as the file to run, without `--state=`.
|
||||
The state says which game it's from and whether that was booted from an ISO, a PBP or an ELF, so compat.ini and
|
||||
the game's config apply. An ISO game gets a disc with nothing on it: files it had open are dropped when the state
|
||||
loads, and opening any file fails. Most games keep running for a few seconds without the disc, enough to measure,
|
||||
but a state taken while the game streams music or video, or just before it loads a level, soon hits those errors,
|
||||
so save perf states in the middle of gameplay. Only states saved since the disc ID was added to them (`SaveStart`
|
||||
version 6) boot this way. The app doesn't: it refuses a `.ppst` as before.
|
||||
|
||||
To get past menus, `--press=SECONDS:MASK[:DURATION],...` holds buttons at emulated times (`MASK` is a sceCtrl
|
||||
mask, `0x8` START and `0x4000` CROSS, held 0.15 seconds by default), and `--screenshot-at=SECONDS:FILE,...` saves
|
||||
screenshots along the way.
|
||||
|
||||
@@ -1042,6 +1042,17 @@ int main(int argc, const char* argv[]) {
|
||||
testFilenames.end()
|
||||
);
|
||||
|
||||
// A savestate on its own boots without its game, and then loads.
|
||||
std::string stateBootPath;
|
||||
if (testFilenames.size() == 1 && endsWithNoCase(testFilenames[0], ".ppst")) {
|
||||
stateBootPath = testFilenames[0];
|
||||
if (stateToLoad) {
|
||||
fprintf(stderr, "--state can't be used when booting a state\n");
|
||||
return 1;
|
||||
}
|
||||
stateToLoad = stateBootPath.c_str();
|
||||
}
|
||||
|
||||
g_Config.bEnableLogging = (fullLog || outputDebugStringLog);
|
||||
g_logManager.Init(&g_Config.bEnableLogging, outputDebugStringLog);
|
||||
|
||||
@@ -1333,6 +1344,7 @@ int main(int argc, const char* argv[]) {
|
||||
coreParameter.mountRoot = mountRoot.empty() ? Path() : Path(mountRoot);
|
||||
coreParameter.startBreak = false;
|
||||
coreParameter.headLess = true;
|
||||
coreParameter.bootFromState = !stateBootPath.empty();
|
||||
coreParameter.dumpCompat = cmdLineOptions.dumpCompat.value_or(false);
|
||||
if (cmdLineOptions.disableGPUFeatures.has_value()) {
|
||||
std::vector<std::string> names;
|
||||
|
||||
Reference in new issue
Block a user