headless: add --save-state, the counterpart to --state

--state could load a savestate but nothing could produce one without a
GUI, so savestate bugs couldn't be reproduced or regression-tested from
a script. This saves one partway through the run, once the game is
actually up.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JvJR8oJNSCimCM9KXVLjfq
This commit is contained in:
Henrik RydgårdandClaude Opus 5 committed 2026-09-04 18:14:41 -06:00
1 parent 5a84f287c4
commit 2389b8ce96
3 files changed
+21 -1

No files matched your search

+19 -1
View File
@@ -84,6 +84,9 @@ static bool g_writeFailureScreenshot = true;
static bool g_writeDebugOutput = true;
// Set from the savestate callback on the emu thread, read after it has been joined.
static bool g_stateLoadFailed = false;
// Set by --save-state. Saving needs the game to actually be running, so it happens from the run
// loop rather than up front like a load does.
static std::string g_stateToSave;
#if PPSSPP_PLATFORM(ANDROID)
JNIEnv *getEnv() {
@@ -338,13 +341,24 @@ static bool RunAutoTest(GraphicsContext *graphicsContext, CoreParameter &corePar
}
bool passed = true;
double deadline = time_now_d() + opt.timeout;
const double startTime = time_now_d();
double deadline = startTime + opt.timeout;
// Late enough that the game is past booting, early enough to leave the run some time after.
double saveStateAt = startTime + opt.timeout * 0.7;
coreState = coreParameter.startBreak ? CORE_STEPPING_CPU : CORE_RUNNING_CPU;
while (coreState == CORE_RUNNING_CPU || coreState == CORE_STEPPING_CPU) {
// 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();
if (!g_stateToSave.empty() && time_now_d() > saveStateAt) {
const std::string filename = g_stateToSave;
g_stateToSave.clear();
SaveState::Save(Path(filename), -1, [](SaveState::Status status, std::string_view message, std::string_view) {
fprintf(stderr, "%.*s\n", (int)message.size(), message.data());
});
}
int blockTicks = (int)usToCycles(1000000 / 10);
PSP_RunLoopFor(blockTicks);
@@ -878,6 +892,10 @@ int main(int argc, const char* argv[]) {
}
}
if (cmdLineOptions.stateToSave.has_value()) {
g_stateToSave = cmdLineOptions.stateToSave.value();
}
if (stateToLoad) {
// Queued now, actually applied by SaveState::Process() once the game is up and running.
SaveState::Load(Path(stateToLoad), -1, [](SaveState::Status status, std::string_view message, std::string_view) {