diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index 9f5a388265..1e77dd9bcd 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -195,6 +195,7 @@ static const CommandLineParam g_autoParams[] = { {POFF(root), CmdParamType::String, "root", 'r', "Mount directory as the root of host0:/."}, {POFF(memStick), CmdParamType::String, "memstick", '\0', "Memory stick root directory (contains PSP/GAME etc)"}, {POFF(stateToLoad), CmdParamType::String, "state", '\0', "Load state from specified file"}, + {POFF(stateToSave), CmdParamType::String, "save-state", '\0', "Save a state to FILE partway through the run", CmdLineMode::Headless}, {POFF(compare), CmdParamType::Bool, "compare", 'c', "Enable comparison mode", CmdLineMode::Headless}, {POFF(bench), CmdParamType::Bool, "bench", 'b', "Enable benchmark mode", CmdLineMode::Headless}, {POFF(oldAtrac), CmdParamType::Bool, "old-atrac", '\0', "Use old ATRAC decoder"}, diff --git a/Core/CmdLine.h b/Core/CmdLine.h index 817262b804..01331301a4 100644 --- a/Core/CmdLine.h +++ b/Core/CmdLine.h @@ -66,6 +66,7 @@ struct CommandLineOptions { // meant copying it in. Points at the same layout the app uses, so the two can share one. std::optional memStick; std::optional stateToLoad; + std::optional stateToSave; // Headless: unpack the firmware inside an official updater EBOOT.PBP (given as the boot // filename) into this directory, then exit without booting anything. diff --git a/headless/Headless.cpp b/headless/Headless.cpp index 3a78043746..30d38546cf 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -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) {