diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index aa3c68a4b1..65361d2ba2 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -30,6 +30,7 @@ struct CommandLineParam { const char *longName; char shortName; // can be 0 for no short name const char *docString; + CmdLineMode mode; }; #define POFF(member) \ @@ -119,29 +120,39 @@ static ParseParamResult ParseParameterStr(int argc, const char *argv[], size_t & ParseParamResult result = SetValue(options, param, argv[i + 1]); i += 2; // Skip both --param and value return result; + } else { + PRINT_STDERR("Error: --%s requires an argument.\n", param.longName); + return ParseParamResult::BadValue; } - PRINT_STDERR("Error: --%s requires an argument.\n", param.longName); - return ParseParamResult::BadValue; } + + // Else no match, continue. } return ParseParamResult::NoMatch; } static const CommandLineParam g_autoParams[] = { - {POFF(fullscreen), CmdParamType::Bool, "fullscreen", 0, "Force full screen mode"}, - {POFF(fullscreen), CmdParamType::BoolInverse, "windowed", 0, "Force windowed mode"}, - {POFF(startScreen), CmdParamType::String, "start-screen", 0, "Start on a specific screen (e.g. 'gamesettings', 'touchscreentest')"}, - {POFF(escapeExit), CmdParamType::Bool, "escape-exit", 0, "Escape key exits the application"}, - {POFF(pauseMenuExit), CmdParamType::Bool, "pause-menu-exit", 0, "Change \"Exit to menu\" in pause menu to \"Exit\""}, + {POFF(fullscreen), CmdParamType::Bool, "fullscreen", 0, "Force full screen mode", CmdLineMode::Application}, + {POFF(fullscreen), CmdParamType::BoolInverse, "windowed", 0, "Force windowed mode", CmdLineMode::Application}, + {POFF(startScreen), CmdParamType::String, "start-screen", 0, "Start on a specific screen (e.g. 'gamesettings', 'touchscreentest')", CmdLineMode::Application}, + {POFF(escapeExit), CmdParamType::Bool, "escape-exit", 0, "Escape key exits the application", CmdLineMode::Application}, + {POFF(pauseMenuExit), CmdParamType::Bool, "pause-menu-exit", 0, "Change \"Exit to menu\" in pause menu to \"Exit\"", CmdLineMode::Application}, {POFF(appendConfig), CmdParamType::String, "appendconfig", 0, "Merge config FILE into the current configuration"}, - {POFF(root), CmdParamType::String, "root", 0, "Mount root directory"}, + {POFF(root), CmdParamType::String, "root", 'r', "Mount root directory"}, {POFF(stateToLoad), CmdParamType::String, "state", 0, "Load state from specified file"}, + {POFF(compare), CmdParamType::Bool, "compare", 'c', "Enable comparison mode"}, + {POFF(bench), CmdParamType::Bool, "bench", 'b', "Enable benchmark mode"}, + {POFF(oldAtrac), CmdParamType::Bool, "old-atrac", 0, "Use old ATRAC decoder"}, + {POFF(log), CmdParamType::String, "log", 0, "Output log to FILE"}, + {POFF(screenshotFilename), CmdParamType::String, "screenshot", 0, "Take a screenshot and save to FILE"}, + {POFF(screenshotFilenameSave), CmdParamType::String, "screenshot-save", 0, "Save screenshot to specified path"}, + {POFF(timeout), CmdParamType::Int, "timeout", 0, "Set the timeout value"}, }; // NOTE: On Windows this prints nothing unfortunately, since PPSSPP is not a "console app". // A fun trick we could do is AttachConsole(ATTACH_PARENT_PROCESS) which works at least from git bash and powershell, if we also use WriteConsole. // However it's not exactly ideal. -static int printUsage(int argc, const char *argv[]) { +static int printUsage(int argc, const char *argv[], CmdLineMode mode) { // NOTE: by convention, --help outputs to stdout, // not to stderr, since it is intended output in this // case (usage printed under different circumstances, @@ -150,7 +161,13 @@ static int printUsage(int argc, const char *argv[]) { const char *progname = argc > 0 ? argv[0] : "ppsspp"; // NOTE: wording largely taken from // https://www.ppsspp.org/docs/reference/command-line/ - PRINT_STDOUT("PPSSPP - a PSP emulator (SDL build)\n"); + if (mode == CmdLineMode::Application) { + PRINT_STDOUT("PPSSPP - a PSP emulator\n"); + } else { + PRINT_STDOUT("PPSSPP Headless\n"); + PRINT_STDOUT("This is primarily meant as a non-interactive test tool.\n\n"); + } + PRINT_STDOUT("PPSSPP - a PSP emulator\n"); PRINT_STDOUT("Usage: %s [options] [FILE]\n\n", progname); PRINT_STDOUT("Launches FILE (e.g. ISO image) if present.\n"); PRINT_STDOUT("Options (some of these are specific to SDL backend):\n"); @@ -169,16 +186,23 @@ static int printUsage(int argc, const char *argv[]) { PRINT_STDOUT(" -J use IR JIT\n"); for (const auto ¶m : g_autoParams) { + if (param.mode != CmdLineMode::Both && param.mode != mode) { + // Skip mode-irrelevant parameters in help. + continue; + } PRINT_STDOUT(" --%s%s%s\n", param.longName, param.shortName ? ", -" : "", param.shortName ? std::string(1, param.shortName).c_str() : ""); PRINT_STDOUT(" %s\n", param.docString); } - PRINT_STDOUT(" --xres PIXELS set X resolution\n"); - PRINT_STDOUT(" --yres PIXELS set Y resolution\n"); - PRINT_STDOUT(" --dpi DPI set DPI\n"); - PRINT_STDOUT(" --scale FACTOR set scale\n"); + // These are only available in SDL. + if (mode == CmdLineMode::Application) { + PRINT_STDOUT(" --xres PIXELS set X resolution\n"); + PRINT_STDOUT(" --yres PIXELS set Y resolution\n"); + PRINT_STDOUT(" --dpi DPI set DPI\n"); + PRINT_STDOUT(" --scale FACTOR set scale\n"); + } PRINT_STDOUT(" --graphics=BACKEND use a different gpu backend\n"); PRINT_STDOUT(" options: gles, software, etc. (also opengl3.1, etc.)\n"); return 0; @@ -187,7 +211,7 @@ static int printUsage(int argc, const char *argv[]) { // Logging should be done with plain printf here. // Error reporting is done with PRINT_STDERR(....). // Actually might want to reconsider given Android... -CommandLineParseResult CommandLineOptions::Parse(int argc, const char *argv[]) { +CommandLineParseResult CommandLineOptions::Parse(int argc, const char *argv[], CmdLineMode mode) { constexpr std::string_view gpuBackendStr = "--graphics="; constexpr std::string_view configOption = "--config="; constexpr std::string_view controlsOption = "--controlconfig="; @@ -202,13 +226,14 @@ CommandLineParseResult CommandLineOptions::Parse(int argc, const char *argv[]) { const size_t len = strlen(argv[i]); if (len > 0 && argv[i][0] != '-') { - // This is a filename to boot. - if (!bootFilename.has_value()) { - bootFilename = std::string(argv[i]); + // This is a filename to boot. Headless supports multiple filenames. + if (bootFilenames.size() == 0 || mode == CmdLineMode::Headless) { + bootFilenames.emplace_back(argv[i]); } else { - // Already have a filename. - PRINT_STDERR("Warning: Ignoring extra boot filename '%s' (can only pass in one).\n", argv[i]); - return CommandLineParseResult::Exit; + // Already have a filename. Ignore. + PRINT_STDERR("Ignoring extra boot filename '%s' (can only pass in one).\n", argv[i]); + i++; + continue; } } @@ -229,8 +254,9 @@ CommandLineParseResult CommandLineOptions::Parse(int argc, const char *argv[]) { logLevel = LogLevel::LVERBOSE; break; case 'h': - printUsage(argc, argv); + printUsage(argc, argv, mode); return CommandLineParseResult::Exit; + // Legacy cpucore options. case 'j': cpuCore = CPUCore::JIT; break; @@ -258,6 +284,9 @@ CommandLineParseResult CommandLineOptions::Parse(int argc, const char *argv[]) { bool parsedAutoParam = false; // Loop through all the auto commands, call ParseParameterWithArg to handle them. for (const auto ¶m : g_autoParams) { + if (param.mode != CmdLineMode::Both && param.mode != mode) { + continue; + } ParseParamResult result = ParseParameterStr(argc, argv, i, param, this); if (result == ParseParamResult::Success) { parsedAutoParam = true; @@ -271,12 +300,18 @@ CommandLineParseResult CommandLineOptions::Parse(int argc, const char *argv[]) { // We already incremented i. continue; } else if (equals(argv[i], "--help")) { - printUsage(argc, argv); + printUsage(argc, argv, mode); return CommandLineParseResult::Exit; } else if (equals(argv[i], "--version")) { printf("%s\n", PPSSPP_GIT_VERSION); return CommandLineParseResult::Exit; - // Commands with parameters. TODO: Should support both space and equals, like --config=foo.ini and --config foo.ini + // Commands with parameters. TODO: Should support both space and equals, like --config=foo.ini and --config foo.ini + } else if (equals(argv[i], "--jit-ir")) { + // Headless legacy + cpuCore = CPUCore::JIT_IR; + } else if (equals(argv[i], "--ir")) { + // Headless legacy + cpuCore = CPUCore::IR_INTERPRETER; } else if (startsWith(argv[i], gpuBackendStr)) { const std::string restOfOption = argv[i] + gpuBackendStr.size(); // Force software rendering off, as picking gles implies HW acceleration. @@ -302,7 +337,7 @@ CommandLineParseResult CommandLineOptions::Parse(int argc, const char *argv[]) { softwareRendering = false; } else { // Bad value, report error and exit. - PRINT_STDERR("Invalid value for --graphics: %s", restOfOption.c_str()); + PRINT_STDERR("Invalid value for --graphics=: %s", restOfOption.c_str()); return CommandLineParseResult::Exit; } } else if (startsWith(argv[i], configOption)) { @@ -315,6 +350,13 @@ CommandLineParseResult CommandLineOptions::Parse(int argc, const char *argv[]) { // To the next argument. i++; } + + // Final adjustments to adjust for old inconsistent code + if (log.has_value()) { + enableLogging = true; + showLogWindow = true; + } + return CommandLineParseResult::Continue; } @@ -353,10 +395,16 @@ void CommandLineOptions::ApplyToConfig() const { g_logManager.SetAllLogLevels(logLevel.value()); } + if (oldAtrac.has_value()) { + g_Config.bUseOldAtrac = oldAtrac.value(); + g_Config.DoNotSaveSetting(&g_Config.bUseOldAtrac); + } + if (root.has_value()) { g_Config.DoNotSaveSetting(&g_Config.mountRoot); g_Config.mountRoot = Path(root.value()); } + // Note: dpi is not applied here - it's platform-specific. // Platforms should check cmdLineOptions.dpi.has_value() and handle accordingly. } diff --git a/Core/CmdLine.h b/Core/CmdLine.h index b4c203c2e7..5eb2b3ea09 100644 --- a/Core/CmdLine.h +++ b/Core/CmdLine.h @@ -10,14 +10,28 @@ enum class CommandLineParseResult { Error, }; +enum class CmdLineMode { + Both, + Application, + Headless, +}; + // We collect command line options in this struct, then we apply it to the config after it's been loaded. +// This parser is shared between regular PPSSPP and headless, so there are some options that are only useful +// in one of them. +// When adding new options, don't forget to update g_autoParams in CmdLine.cpp (or write manual parsing). struct CommandLineOptions { + // If returns CommandLineParseResult::Exit or ::Error, the program should exit immediately (with an error return code if Error). + CommandLineParseResult Parse(int argc, const char *argv[], CmdLineMode mode = CmdLineMode::Application); + void ApplyToConfig() const; + std::optional fullscreen; std::optional gpuBackend; std::optional softwareRendering; std::optional enableLogging; std::optional logLevel; // Override log level with this. - std::optional bootFilename; + std::optional log; + std::vector bootFilenames; std::optional cpuCore; @@ -48,7 +62,14 @@ struct CommandLineOptions { bool optionS = false; // a legacy option - // If returns CommandLineParseResult::Exit or ::Error, the program should exit immediately (with an error return code if Error). - CommandLineParseResult Parse(int argc, const char *argv[]); - void ApplyToConfig() const; + std::optional oldAtrac; + + // Headless options + std::optional compare; + std::optional bench; + std::optional verbose; + + std::optional screenshotFilename; + std::optional screenshotFilenameSave; + std::optional timeout; }; diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index eeef028103..f17d882845 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -631,9 +631,9 @@ void NativeInit(int argc, const char *argv[], const CommandLineOptions &cmdLineO } // This parameter should be a boot filename. Only accept it if we - // don't already have one. - if (cmdLineOptions.bootFilename.has_value()) { - std::string bootFilename = cmdLineOptions.bootFilename.value(); + // don't already have one. + if (!cmdLineOptions.bootFilenames.empty()) { + std::string bootFilename = cmdLineOptions.bootFilenames[0]; gotBootFilename = true; INFO_LOG(Log::System, "Boot filename found in args: '%s'", bootFilename.c_str()); @@ -656,7 +656,7 @@ void NativeInit(int argc, const char *argv[], const CommandLineOptions &cmdLineO // Handle file:/// URIs, since you get those when creating shortcuts on some Android systems. if (startsWith(bootFilename, "file:///")) { bootFilename = UriDecode(bootFilename.substr(7)); - INFO_LOG(Log::IO, "Decoding '%s' to '%s'", cmdLineOptions.bootFilename.value().c_str(), bootFilename.c_str()); + INFO_LOG(Log::IO, "Decoding '%s' to '%s'", cmdLineOptions.bootFilenames[0].c_str(), bootFilename.c_str()); } boot_filename = Path(bootFilename); diff --git a/headless/Headless.cpp b/headless/Headless.cpp index 500b116cbb..247ef11c43 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -36,6 +36,7 @@ #include "Common/TimeUtil.h" #include "Common/StringUtils.h" #include "Common/Thread/ThreadManager.h" +#include "Core/CmdLine.h" #include "Core/Config.h" #include "Core/ConfigValues.h" #include "Core/Core.h" @@ -198,8 +199,9 @@ bool RunAutoTest(HeadlessHost *headlessHost, CoreParameter &coreParameter, const return false; } - if (opt.compare) + if (opt.compare) { headlessHost->SetComparisonScreenshot(ExpectedScreenshotFromFilename(coreParameter.fileToStart), opt.maxScreenshotError); + } std::string error_string; while (PSP_InitUpdate(&error_string) == BootState::Booting) { @@ -226,8 +228,7 @@ bool RunAutoTest(HeadlessHost *headlessHost, CoreParameter &coreParameter, const bool passed = true; double deadline = time_now_d() + opt.timeout; coreState = coreParameter.startBreak ? CORE_STEPPING_CPU : CORE_RUNNING_CPU; - while (coreState == CORE_RUNNING_CPU || coreState == CORE_STEPPING_CPU) - { + while (coreState == CORE_RUNNING_CPU || coreState == CORE_STEPPING_CPU) { int blockTicks = (int)usToCycles(1000000 / 10); PSP_RunLoopFor(blockTicks); @@ -276,11 +277,13 @@ bool RunAutoTest(HeadlessHost *headlessHost, CoreParameter &coreParameter, const PSP_Shutdown(true); - if (!opt.bench) + if (!opt.bench) { headlessHost->FlushDebugOutput(); + } - if (opt.compare && passed) + if (opt.compare && passed) { passed = CompareOutput(coreParameter.fileToStart, output, opt.verbose); + } return passed; } @@ -324,7 +327,7 @@ static void AddRecursively(std::vector *tests, Path actualPath) { } } -static void AddTestsByPath(std::vector *tests, std::string_view path) { +static void AddToTestsByPath(std::vector *tests, std::string_view path) { if (endsWith(path, "/...")) { path = path.substr(0, path.size() - 4); // Recurse for tests @@ -337,8 +340,7 @@ static void AddTestsByPath(std::vector *tests, std::string_view pat } } -int main(int argc, const char* argv[]) -{ +int main(int argc, const char* argv[]) { PROFILE_INIT(); TimeInit(); #if PPSSPP_PLATFORM(WINDOWS) @@ -356,8 +358,22 @@ int main(int argc, const char* argv[]) _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); #endif + CommandLineOptions cmdLineOptions; + CommandLineParseResult parseResult = cmdLineOptions.Parse(argc, argv, CmdLineMode::Headless); + switch (parseResult) { + case CommandLineParseResult::Exit: + return 0; + case CommandLineParseResult::Error: + return 1; + case CommandLineParseResult::Continue: + break; + } + AutoTestOptions testOptions{}; - testOptions.timeout = std::numeric_limits::infinity(); + testOptions.compare = cmdLineOptions.compare.value_or(false); + testOptions.bench = cmdLineOptions.bench.value_or(false); + testOptions.timeout = cmdLineOptions.timeout.value_or(0); + bool fullLog = false; const char *stateToLoad = 0; GPUCore gpuCore = GPUCORE_SOFTWARE; @@ -370,71 +386,24 @@ int main(int argc, const char* argv[]) std::vector ignoredTests; const char *mountIso = nullptr; const char *mountRoot = nullptr; - const char *screenshotFilename = nullptr; - const char *screenshotSavePath = nullptr; - for (int i = 1; i < argc; i++) - { - if (!strcmp(argv[i], "-m") || !strcmp(argv[i], "--mount")) - { + if (cmdLineOptions.cpuCore.has_value()) { + cpuCore = cmdLineOptions.cpuCore.value(); + } + + + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "-m") || !strcmp(argv[i], "--mount")) { if (++i >= argc) return printUsage(argv[0], "Missing argument after -m"); mountIso = argv[i]; } - else if (!strcmp(argv[i], "-r") || !strcmp(argv[i], "--root")) - { - if (++i >= argc) - return printUsage(argv[0], "Missing argument after -r"); - mountRoot = argv[i]; - } else if (!strcmp(argv[i], "-l") || !strcmp(argv[i], "--log")) fullLog = true; else if (!strcmp(argv[i], "-o") || !strcmp(argv[i], "--odslog")) outputDebugStringLog = true; - else if (!strcmp(argv[i], "-i")) - cpuCore = CPUCore::INTERPRETER; - else if (!strcmp(argv[i], "-j")) - cpuCore = CPUCore::JIT; - else if (!strcmp(argv[i], "--jit-ir")) - cpuCore = CPUCore::JIT_IR; - else if (!strcmp(argv[i], "--ir")) - cpuCore = CPUCore::IR_INTERPRETER; - else if (!strcmp(argv[i], "-c") || !strcmp(argv[i], "--compare")) - testOptions.compare = true; - else if (!strcmp(argv[i], "--bench")) - testOptions.bench = true; else if (!strcmp(argv[i], "-v") || !strcmp(argv[i], "--verbose")) testOptions.verbose = true; - else if (!strcmp(argv[i], "--old-atrac")) - oldAtrac = true; - else if (!strncmp(argv[i], "--graphics=", strlen("--graphics=")) && strlen(argv[i]) > strlen("--graphics=")) - { - const char *gpuName = argv[i] + strlen("--graphics="); - if (!strcasecmp(gpuName, "gles")) - gpuCore = GPUCORE_GLES; - // There used to be a separate "null" rendering core - just use software. - else if (!strcasecmp(gpuName, "software") || !strcasecmp(gpuName, "null")) - gpuCore = GPUCORE_SOFTWARE; - else if (!strcasecmp(gpuName, "directx11") || !strcasecmp(gpuName, "d3d11")) - gpuCore = GPUCORE_DIRECTX11; - else if (!strcasecmp(gpuName, "vulkan")) - gpuCore = GPUCORE_VULKAN; - else - return printUsage(argv[0], "Unknown gpu backend specified after --graphics=. Allowed: software, directx9, directx11, vulkan, gles, null."); - } - // Default to GLES if no value selected. - else if (!strcmp(argv[i], "--graphics")) { -#if PPSSPP_API(ANY_GL) - gpuCore = GPUCORE_GLES; -#else - gpuCore = GPUCORE_DIRECTX11; -#endif - } else if (!strncmp(argv[i], "--screenshot=", strlen("--screenshot=")) && strlen(argv[i]) > strlen("--screenshot=")) - screenshotFilename = argv[i] + strlen("--screenshot="); - else if (!strncmp(argv[i], "--screenshot-save=", strlen("--screenshot-save=")) && strlen(argv[i]) > strlen("--screenshot-save=")) - screenshotSavePath = argv[i] + strlen("--screenshot-save="); - else if (!strncmp(argv[i], "--timeout=", strlen("--timeout=")) && strlen(argv[i]) > strlen("--timeout=")) - testOptions.timeout = strtod(argv[i] + strlen("--timeout="), nullptr); else if (!strncmp(argv[i], "--max-mse=", strlen("--max-mse=")) && strlen(argv[i]) > strlen("--max-mse=")) testOptions.maxScreenshotError = strtod(argv[i] + strlen("--max-mse="), nullptr); else if (!strncmp(argv[i], "--debugger=", strlen("--debugger=")) && strlen(argv[i]) > strlen("--debugger=")) @@ -447,11 +416,14 @@ int main(int argc, const char* argv[]) if (++i >= argc) return printUsage(argv[0], "Missing argument after --ignore"); ignoredTests.push_back(argv[i]); - } else { - AddTestsByPath(&testFilenames, argv[i]); } } + for (const std::string &filename : cmdLineOptions.bootFilenames) { + AddToTestsByPath(&testFilenames, filename); + } + + if (testFilenames.size() == 1 && testFilenames[0][0] == '@') testFilenames = ReadFromListFile(testFilenames[0].substr(1)); @@ -481,6 +453,8 @@ int main(int argc, const char* argv[]) g_logManager.EnableOutput(LogOutput::Printf); } + cmdLineOptions.ApplyToConfig(); + // Needs to be after log so we don't interfere with test output. g_threadManager.Init(cpu_info.num_cores, cpu_info.logical_cpu_count); @@ -584,10 +558,10 @@ int main(int argc, const char* argv[]) nextPath = nextPath.NavigateUp(); } - if (screenshotFilename) - headlessHost->SetComparisonScreenshot(Path(std::string(screenshotFilename)), testOptions.maxScreenshotError); - if (screenshotSavePath) - headlessHost->SetScreenshotSavePath(Path(std::string(screenshotSavePath))); + if (cmdLineOptions.screenshotFilename.has_value()) + headlessHost->SetComparisonScreenshot(Path(std::string(cmdLineOptions.screenshotFilename.value())), testOptions.maxScreenshotError); + if (cmdLineOptions.screenshotFilenameSave.has_value()) + headlessHost->SetScreenshotSavePath(Path(std::string(cmdLineOptions.screenshotFilenameSave.value()))); headlessHost->SetWriteFailureScreenshot(!getenv("GITHUB_ACTIONS") && !testOptions.bench); headlessHost->SetWriteDebugOutput(!testOptions.compare && !testOptions.bench); diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index 5f710ea20f..8daaeb0ed5 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -1318,15 +1318,21 @@ bool TestCmdLine() { "--fullscreen", "--graphics=d3d11", "--pause-menu-exit", + "--timeout=3", "My_Game.iso" }; int argc = ARRAY_SIZE(argv); CommandLineOptions options; - options.Parse(argc, argv); + options.Parse(argc, argv, CmdLineMode::Application); EXPECT_TRUE(options.fullscreen.value_or(false)); - EXPECT_EQ_STR(options.bootFilename.value_or(""), std::string("My_Game.iso")); + if (options.bootFilenames.empty()) { + EXPECT_TRUE(false); + return false; + } + EXPECT_EQ_STR(options.bootFilenames[0], std::string("My_Game.iso")); EXPECT_TRUE(options.gpuBackend.has_value()); EXPECT_EQ_INT((int)options.gpuBackend.value_or((GPUBackend)-1), (int)GPUBackend::DIRECT3D11); + EXPECT_EQ_INT(options.timeout.value_or(0), 3); EXPECT_TRUE(options.pauseMenuExit.value_or(false)); } // Test GL version override