From cef7da49b34f235627008294c545472eddf47aaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 20 Jul 2026 13:07:27 +0200 Subject: [PATCH 1/7] Add utility to parse parameters with params --- Core/CmdLine.cpp | 51 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index b8ac9fc6b4..d6c037c381 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -16,6 +16,51 @@ #endif +// Helper to parse parameters that take an argument. +// Supports: --param=value, --param value, -p value +// Returns true if argument was consumed, false otherwise. +// On success, 'value' contains the argument string and 'i' is incremented if needed. +static bool ParseParameterWithArg(int argc, const char *argv[], size_t &i, + const char *longName, char shortName, + const char **outValue) { + const char *arg = argv[i]; + const size_t len = strlen(arg); + + // Check for short form: -p value + if (shortName && len == 2 && arg[0] == '-' && arg[1] == shortName) { + if (i + 1 < argc) { + *outValue = argv[i + 1]; + i += 2; // Skip both -p and value + return true; + } + PRINT_STDERR("Error: -%c requires an argument.\n", shortName); + return false; + } + + // Check for long form with equals: --param=value + if (longName) { + std::string prefix = std::string("--") + longName + "="; + if (startsWith(arg, prefix)) { + *outValue = arg + prefix.size(); + i++; // Skip --param=value + return true; + } + + // Check for long form with space: --param value + if (equals(arg, std::string("--") + longName)) { + if (i + 1 < argc) { + *outValue = argv[i + 1]; + i += 2; // Skip both --param and value + return true; + } + PRINT_STDERR("Error: --%s requires an argument.\n", longName); + return false; + } + } + + return false; +} + static int printUsage(int argc, const char *argv[]) { // NOTE: by convention, --help outputs to stdout, // not to stderr, since it is intended output in this @@ -47,7 +92,7 @@ static int printUsage(int argc, const char *argv[]) { PRINT_STDOUT(" --windowed force windowed mode, ignoring saved configuration\n"); PRINT_STDOUT(" --xres PIXELS set X resolution\n"); PRINT_STDOUT(" --yres PIXELS set Y resolution\n"); - PRINT_STDOUT(" --dpi FACTOR set DPI\n"); + PRINT_STDOUT(" --dpi DPI set DPI\n"); PRINT_STDOUT(" --scale FACTOR set scale\n"); PRINT_STDOUT(" --ipad set resolution to 1024x768\n"); PRINT_STDOUT(" --portrait portrait mode\n"); @@ -90,7 +135,7 @@ CommandLineParseResult CommandLineOptions::Parse(int argc, const char *argv[]) { } } - // single char commands, like -l, -s, -d + // single char commands, like -l, -s if (len == 2) { switch (argv[i][1]) { case 'l': @@ -186,4 +231,6 @@ void CommandLineOptions::ApplyToConfig() const { g_Config.bAutoRun = false; g_Config.bSaveSettings = false; } + // Note: dpi is not applied here - it's platform-specific. + // Platforms should check cmdLineOptions.dpi.has_value() and handle accordingly. } From e6875fa33990ad14becc098a7837329d677f22eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 20 Jul 2026 14:57:59 +0200 Subject: [PATCH 2/7] More cmdline work. --- Core/CmdLine.cpp | 143 +++++++++++++++++++++++++++++++++--------- SDL/SDLMain.cpp | 30 +++------ Windows/EmuThread.cpp | 2 - Windows/main.cpp | 2 +- unittest/UnitTest.cpp | 16 +++++ 5 files changed, 139 insertions(+), 54 deletions(-) diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index d6c037c381..b4bfe8abf0 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -16,51 +16,121 @@ #endif +enum class CmdParamType { + Bool, + BoolInverse, + Int, + String, +}; + +struct CommandLineParam { + size_t offsetInStruct; // calculate with offsetof(CommandLineOptions, member). NOTE: Must point to an optional<> of the correct type! + CmdParamType type; + const char *longName; + char shortName; // can be 0 for no short name + const char *docString; +}; + +#define POFF(member) \ + {offsetof(CommandLineOptions, member)} + +enum class ParseParamResult { + Success, + NoMatch, + BadValue, +}; + +ParseParamResult SetValue(CommandLineOptions *options, const CommandLineParam ¶m, const std::string &value) { + switch (param.type) { + case CmdParamType::Bool: + case CmdParamType::BoolInverse: + { + bool v = true; + if (value == "true" || value == "1") { + v = true; + } else if (value == "false" || value == "0") { + v = false; + } else { + PRINT_STDERR("Error: Invalid value for boolean parameter --%s: '%s'. Expected 'true' or 'false'.\n", param.longName, value.c_str()); + return ParseParamResult::BadValue; + } + + if (param.type == CmdParamType::BoolInverse) { + v = !v; + } + *reinterpret_cast *>(reinterpret_cast(options) + param.offsetInStruct) = v; + break; + } + case CmdParamType::Int: + *reinterpret_cast *>(reinterpret_cast(options) + param.offsetInStruct) = std::stoi(value); + break; + case CmdParamType::String: + *reinterpret_cast *>(reinterpret_cast(options) + param.offsetInStruct) = value; + break; + default: + break; + } + return ParseParamResult::Success; +} + // Helper to parse parameters that take an argument. // Supports: --param=value, --param value, -p value // Returns true if argument was consumed, false otherwise. // On success, 'value' contains the argument string and 'i' is incremented if needed. -static bool ParseParameterWithArg(int argc, const char *argv[], size_t &i, - const char *longName, char shortName, - const char **outValue) { +static ParseParamResult ParseParameterStr(int argc, const char *argv[], size_t &i, const CommandLineParam ¶m, CommandLineOptions *options) { const char *arg = argv[i]; const size_t len = strlen(arg); // Check for short form: -p value - if (shortName && len == 2 && arg[0] == '-' && arg[1] == shortName) { + if (param.shortName && len == 2 && arg[0] == '-' && arg[1] == param.shortName) { if (i + 1 < argc) { - *outValue = argv[i + 1]; + ParseParamResult result = SetValue(options, param, argv[i + 1]); i += 2; // Skip both -p and value - return true; + return result; } - PRINT_STDERR("Error: -%c requires an argument.\n", shortName); - return false; + PRINT_STDERR("Error: -%c requires an argument.\n", param.shortName); + return ParseParamResult::BadValue; } // Check for long form with equals: --param=value - if (longName) { - std::string prefix = std::string("--") + longName + "="; + if (param.longName) { + std::string prefix = std::string("--") + param.longName + "="; if (startsWith(arg, prefix)) { - *outValue = arg + prefix.size(); + ParseParamResult result = SetValue(options, param, arg + prefix.size()); i++; // Skip --param=value - return true; + return result; + } + + // Check for boolean/inverse-boolean parameter - these don't take a separate param value. + if (param.type == CmdParamType::Bool && equals(arg, std::string("--") + param.longName)) { + ParseParamResult result = SetValue(options, param, "true"); + i++; // Skip --param + return result; + } else if (param.type == CmdParamType::BoolInverse && equals(arg, std::string("--") + param.longName)) { + ParseParamResult result = SetValue(options, param, "false"); + i++; // Skip --param + return result; } // Check for long form with space: --param value - if (equals(arg, std::string("--") + longName)) { + if (equals(arg, std::string("--") + param.longName)) { if (i + 1 < argc) { - *outValue = argv[i + 1]; + ParseParamResult result = SetValue(options, param, argv[i + 1]); i += 2; // Skip both --param and value - return true; + return result; } - PRINT_STDERR("Error: --%s requires an argument.\n", longName); - return false; + PRINT_STDERR("Error: --%s requires an argument.\n", param.longName); + return ParseParamResult::BadValue; } } - - return false; + 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"}, +}; + static int printUsage(int argc, const char *argv[]) { // NOTE: by convention, --help outputs to stdout, // not to stderr, since it is intended output in this @@ -88,13 +158,17 @@ static int printUsage(int argc, const char *argv[]) { PRINT_STDOUT(" -j use JIT\n"); PRINT_STDOUT(" -J use IR JIT\n"); - PRINT_STDOUT(" --fullscreen force full screen mode, ignoring saved configuration\n"); - PRINT_STDOUT(" --windowed force windowed mode, ignoring saved configuration\n"); + for (const auto ¶m : g_autoParams) { + 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"); - PRINT_STDOUT(" --ipad set resolution to 1024x768\n"); PRINT_STDOUT(" --portrait portrait mode\n"); PRINT_STDOUT(" --graphics=BACKEND use a different gpu backend\n"); PRINT_STDOUT(" options: gles, software, etc. (also opengl3.1, etc.)\n"); @@ -108,6 +182,7 @@ static int printUsage(int argc, const char *argv[]) { return 0; } + // Logging should be done with plain printf here. // Error reporting is done with PRINT_STDERR(....). // Actually might want to reconsider given Android... @@ -163,19 +238,28 @@ CommandLineParseResult CommandLineOptions::Parse(int argc, const char *argv[]) { // Ignore } #endif - // Simple bool commands - // NOTE: We need to parse --fullscreen early, before we create the window. - if (equals(argv[i], "--help")) { + bool parsedAutoParam = false; + // Loop through all the auto commands, call ParseParameterWithArg to handle them. + for (const auto ¶m : g_autoParams) { + ParseParamResult result = ParseParameterStr(argc, argv, i, param, this); + if (result == ParseParamResult::Success) { + parsedAutoParam = true; + break; + } else if (result == ParseParamResult::BadValue) { + return CommandLineParseResult::Exit; + } // else nomatch + } + + if (parsedAutoParam) { + // We already incremented i. + continue; + } else if (equals(argv[i], "--help")) { printUsage(argc, argv); return CommandLineParseResult::Exit; } else if (equals(argv[i], "--version")) { printf("%s\n", PPSSPP_GIT_VERSION); return CommandLineParseResult::Exit; - } else if (equals(argv[i], "--fullscreen")) { - fullscreen = true; - } else if (equals(argv[i], "--windowed")) { - fullscreen = false; // Commands with parameters. TODO: Should support both space and equals, like --config=foo.ini and --config foo.ini } else if (startsWith(argv[i], gpuBackendStr)) { const std::string_view restOfOption = argv[i] + gpuBackendStr.size(); @@ -203,7 +287,6 @@ CommandLineParseResult CommandLineOptions::Parse(int argc, const char *argv[]) { } else { // Report unknown argument later once this is complete. } - // To the next argument. i++; } diff --git a/SDL/SDLMain.cpp b/SDL/SDLMain.cpp index b7585f5095..9cf720dd5e 100644 --- a/SDL/SDLMain.cpp +++ b/SDL/SDLMain.cpp @@ -1441,8 +1441,6 @@ int main(int argc, char *argv[]) { const int linked = SDL_GetVersion(); int set_xres = -1; int set_yres = -1; - bool portrait = false; - bool set_ipad = false; float set_dpi = 0.0f; float set_scale = 1.0f; @@ -1461,10 +1459,7 @@ int main(int argc, char *argv[]) { Uint32 mode = 0; for (int i = 1; i < argc; i++) { - if (!strcmp(argv[i], "--fullscreen")) { - mode |= SDL_WINDOW_FULLSCREEN; - g_Config.DoNotSaveSetting(&g_Config.bFullScreen); - } else if (set_xres == -2) + if (set_xres == -2) set_xres = parseInt(argv[i]); else if (set_yres == -2) set_yres = parseInt(argv[i]); @@ -1480,10 +1475,6 @@ int main(int argc, char *argv[]) { set_dpi = -2; else if (!strcmp(argv[i], "--scale")) set_scale = -2; - else if (!strcmp(argv[i], "--ipad")) - set_ipad = true; - else if (!strcmp(argv[i], "--portrait")) - portrait = true; else if (!strncmp(argv[i], "--graphics=", strlen("--graphics="))) { const char *restOfOption = argv[i] + strlen("--graphics="); double val=-1.0; // Yes, floating point. @@ -1556,6 +1547,7 @@ int main(int argc, char *argv[]) { g_RefreshRate = displayMode->refresh_rate; SDL_free(displayIDs); + // TODO: Should only call this if we actually intend to use OpenGL. SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); @@ -1565,19 +1557,19 @@ int main(int argc, char *argv[]) { // Force fullscreen if the resolution is too low to run windowed. if (g_DesktopWidth < 480 * 2 && g_DesktopHeight < 272 * 2) { - mode |= SDL_WINDOW_FULLSCREEN; + cmdLineParams.fullscreen = true; } // If we're on mobile, don't try for windowed either. #if defined(MOBILE_DEVICE) && !PPSSPP_PLATFORM(SWITCH) - mode |= SDL_WINDOW_FULLSCREEN; + cmdLineParams.fullscreen = true; #elif defined(USING_FBDEV) || PPSSPP_PLATFORM(SWITCH) - mode |= SDL_WINDOW_FULLSCREEN; + cmdLineParams.fullscreen = true; #else mode |= SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY; #endif - if (mode & SDL_WINDOW_FULLSCREEN) { + if (cmdLineParams.fullscreen) { g_display.pixel_xres = g_DesktopWidth; g_display.pixel_yres = g_DesktopHeight; g_Config.bFullScreen = true; @@ -1585,16 +1577,9 @@ int main(int argc, char *argv[]) { // set a sensible default resolution (2x) g_display.pixel_xres = 480 * 2 * set_scale; g_display.pixel_yres = 272 * 2 * set_scale; - if (portrait) { - std::swap(g_display.pixel_xres, g_display.pixel_yres); - } g_Config.bFullScreen = false; } - if (set_ipad) { - g_display.pixel_xres = 1024; - g_display.pixel_yres = 768; - } if (!landscape) { std::swap(g_display.pixel_xres, g_display.pixel_yres); } @@ -1636,6 +1621,9 @@ int main(int argc, char *argv[]) { #else const char *external_dir = "/tmp"; #endif + + // After NativeInit, code should no longer look at cmdLineOptions, they should have been translated + // into g_Config settings. This is because NativeInit may modify g_Config settings based on the command line options. NativeInit(remain_argc, (const char **)remain_argv, cmdLineOptions, path, external_dir, nullptr); // Use the setting from the config when initing the window. diff --git a/Windows/EmuThread.cpp b/Windows/EmuThread.cpp index 407792bcf5..09460c0bba 100644 --- a/Windows/EmuThread.cpp +++ b/Windows/EmuThread.cpp @@ -43,8 +43,6 @@ static std::thread mainThread; static std::string g_error_message; static bool g_inLoop; -extern std::vector GetWideCmdLine(); - class GraphicsContext; static GraphicsContext *g_graphicsContext; diff --git a/Windows/main.cpp b/Windows/main.cpp index d0e5b6d2ed..1b46c21268 100644 --- a/Windows/main.cpp +++ b/Windows/main.cpp @@ -1050,7 +1050,7 @@ int WINAPI WinMain(HINSTANCE _hInstance, HINSTANCE hPrevInstance, LPSTR szCmdLin // args provide argc and argv. CommandLineOptions cmdLineOptions; - CommandLineParseResult parseResult = cmdLineOptions.Parse((int)args.size(), args.data()); + const CommandLineParseResult parseResult = cmdLineOptions.Parse((int)args.size(), args.data()); switch (parseResult) { case CommandLineParseResult::Exit: return 0; diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index 418c3c0e3a..4eb9788c2a 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -84,6 +84,7 @@ #include "Common/File/VFS/VFS.h" #include "Common/File/VFS/DirectoryReader.h" #include "Common/Math/fast/fast_matrix.h" +#include "Core/CmdLine.h" #include "Core/FileSystems/ISOFileSystem.h" #include "Core/MemMap.h" #include "Core/KeyMap.h" @@ -1310,6 +1311,20 @@ bool TestFriendlyPath() { return true; } +bool TestCmdLine() { + const char *argv[] = { + "ppsspp", + "--fullscreen", + "My_Game.iso" + }; + int argc = ARRAY_SIZE(argv); + CommandLineOptions options; + options.Parse(argc, argv); + EXPECT_TRUE(options.fullscreen.value_or(false)); + EXPECT_EQ_STR(options.bootFilename.value_or(""), std::string("My_Game.iso")); + return true; +} + // Check that RTTI is working. bool TestLang() { struct Base { virtual ~Base() = default; }; @@ -1393,6 +1408,7 @@ TestItem availableTests[] = { TEST_ITEM(FriendlyPath), TEST_ITEM(LinAlg), TEST_ITEM(Lang), + TEST_ITEM(CmdLine), }; int main(int argc, const char *argv[]) { From dca65d02adf905b376ea42cf7f27c54839b4f416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 20 Jul 2026 15:23:41 +0200 Subject: [PATCH 3/7] Move the force_gl_version command line hack to the new parser See #20687 --- Core/CmdLine.cpp | 17 +++++++++++++---- Core/CmdLine.h | 8 ++++++++ unittest/UnitTest.cpp | 36 ++++++++++++++++++++++++++---------- 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index b4bfe8abf0..52f76eac8c 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -262,23 +262,32 @@ CommandLineParseResult CommandLineOptions::Parse(int argc, const char *argv[]) { return CommandLineParseResult::Exit; // Commands with parameters. TODO: Should support both space and equals, like --config=foo.ini and --config foo.ini } else if (startsWith(argv[i], gpuBackendStr)) { - const std::string_view restOfOption = argv[i] + gpuBackendStr.size(); + const std::string restOfOption = argv[i] + gpuBackendStr.size(); // Force software rendering off, as picking gles implies HW acceleration. // We could add more options for software such as "software-gles", // "software-vulkan" and "software-d3d11", or something similar. // For now, software rendering force-activates OpenGL. + double glVersionTemp = 0.0f; if (restOfOption == "directx11" || restOfOption == "d3d11") { gpuBackend = GPUBackend::DIRECT3D11; softwareRendering = false; - } else if (restOfOption == "gles") { - gpuBackend = GPUBackend::OPENGL; - softwareRendering = false; } else if (restOfOption == "vulkan") { gpuBackend = GPUBackend::VULKAN; softwareRendering = false; } else if (restOfOption == "software") { gpuBackend = GPUBackend::OPENGL; softwareRendering = true; + } else if (sscanf(restOfOption.c_str(), "gles%lg", &glVersionTemp) == 1 || sscanf(restOfOption.c_str(), "opengl%lg", &glVersionTemp) == 1) { + g_Config.iGPUBackend = (int)GPUBackend::OPENGL; + g_Config.bSoftwareRendering = false; + force_gl_version = int(10.0 * glVersionTemp + 0.5); + } else if (restOfOption == "gles") { + gpuBackend = GPUBackend::OPENGL; + softwareRendering = false; + } else { + // Bad value, report error and exit. + PRINT_STDERR("Invalid value for --graphics: %s", restOfOption.c_str()); + return CommandLineParseResult::Exit; } } else if (startsWith(argv[i], configOption)) { configFilename = std::string(argv[i] + configOption.size()); diff --git a/Core/CmdLine.h b/Core/CmdLine.h index 049eac9746..aa35ef7a96 100644 --- a/Core/CmdLine.h +++ b/Core/CmdLine.h @@ -18,6 +18,14 @@ struct CommandLineOptions { std::optional bootFilename; + // SDL only: Option to force a specific OpenGL version (42="4.2", + // etc.; -1 means "try them all"). + // Implemented as a workaround for https://github.com/hrydgard/ppsspp/issues/20687 + // NOTE: this is currently not persistent (doesn't + // go to config), even though --graphics=openglX.Y + // also sets the GPU backend which does persist. + int force_gl_version = -1; + #ifndef _DEBUG bool showLogWindow = false; #else diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index 4eb9788c2a..40c6adfad0 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -1312,16 +1312,32 @@ bool TestFriendlyPath() { } bool TestCmdLine() { - const char *argv[] = { - "ppsspp", - "--fullscreen", - "My_Game.iso" - }; - int argc = ARRAY_SIZE(argv); - CommandLineOptions options; - options.Parse(argc, argv); - EXPECT_TRUE(options.fullscreen.value_or(false)); - EXPECT_EQ_STR(options.bootFilename.value_or(""), std::string("My_Game.iso")); + { + const char *argv[] = { + "ppsspp", + "--fullscreen", + "--graphics=d3d11", + "My_Game.iso" + }; + int argc = ARRAY_SIZE(argv); + CommandLineOptions options; + options.Parse(argc, argv); + EXPECT_TRUE(options.fullscreen.value_or(false)); + EXPECT_EQ_STR(options.bootFilename.value_or(""), std::string("My_Game.iso")); + EXPECT_TRUE(options.gpuBackend.has_value()); + EXPECT_EQ_INT((int)options.gpuBackend.value_or((GPUBackend)-1), (int)GPUBackend::DIRECT3D11); + } + // Test GL version override + { + const char *argv[] = { + "ppsspp", + "--graphics=gles3.3", + }; + int argc = ARRAY_SIZE(argv); + CommandLineOptions options; + options.Parse(argc, argv); + EXPECT_EQ_INT(options.force_gl_version, 33); + } return true; } From bd25962cbe0a1538c26cca5236b774c82e16e3c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 20 Jul 2026 16:29:29 +0200 Subject: [PATCH 4/7] Convert more options to the new command line parser --- Core/CmdLine.cpp | 51 ++++++++++---- Core/CmdLine.h | 11 ++- SDL/SDLMain.cpp | 34 +--------- UI/NativeApp.cpp | 152 ++++++++++++++++-------------------------- Windows/main.cpp | 10 --- unittest/UnitTest.cpp | 2 + 6 files changed, 109 insertions(+), 151 deletions(-) diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index 52f76eac8c..c3ea16f688 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -1,6 +1,7 @@ #include "Core/Config.h" #include "Core/CmdLine.h" #include "Common/StringUtils.h" +#include "Common/Log/LogManager.h" #ifdef HAVE_LIBRETRO_VFS @@ -129,6 +130,9 @@ static ParseParamResult ParseParameterStr(int argc, const char *argv[], size_t & 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\""}, }; static int printUsage(int argc, const char *argv[]) { @@ -169,20 +173,14 @@ static int printUsage(int argc, const char *argv[]) { PRINT_STDOUT(" --yres PIXELS set Y resolution\n"); PRINT_STDOUT(" --dpi DPI set DPI\n"); PRINT_STDOUT(" --scale FACTOR set scale\n"); - PRINT_STDOUT(" --portrait portrait mode\n"); PRINT_STDOUT(" --graphics=BACKEND use a different gpu backend\n"); PRINT_STDOUT(" options: gles, software, etc. (also opengl3.1, etc.)\n"); - PRINT_STDOUT(" --pause-menu-exit change \"Exit to menu\" in pause menu to \"Exit\"\n"); - PRINT_STDOUT(" --escape-exit escape key exits the application\n"); - PRINT_STDOUT(" --gamesettings go directly to settings\n"); - PRINT_STDOUT(" --touchscreentest go directly to the touchscreentest screen\n"); PRINT_STDOUT(" --appendconfig=FILE merge config FILE into the current configuration\n"); return 0; } - // Logging should be done with plain printf here. // Error reporting is done with PRINT_STDERR(....). // Actually might want to reconsider given Android... @@ -206,7 +204,8 @@ CommandLineParseResult CommandLineOptions::Parse(int argc, const char *argv[]) { bootFilename = std::string(argv[i]); } else { // Already have a filename. - PRINT_STDERR("Warning: Ignoring extra boot filename '%s'.\n", argv[i]); + PRINT_STDERR("Warning: Ignoring extra boot filename '%s' (can only pass in one).\n", argv[i]); + return CommandLineParseResult::Exit; } } @@ -221,21 +220,35 @@ CommandLineParseResult CommandLineOptions::Parse(int argc, const char *argv[]) { optionS = true; break; case 'd': - debugLogLevel = true; + logLevel = LogLevel::LDEBUG; + break; + case 'v': + logLevel = LogLevel::LVERBOSE; break; case 'h': printUsage(argc, argv); return CommandLineParseResult::Exit; - case 'v': - printf("%s\n", PPSSPP_GIT_VERSION); - return CommandLineParseResult::Exit; + case 'j': + cpuCore = CPUCore::JIT; + break; + case 'i': + cpuCore = CPUCore::INTERPRETER; + break; + case 'r': + cpuCore = CPUCore::IR_INTERPRETER; + break; + case 'J': + cpuCore = CPUCore::JIT_IR; + break; } } #if defined(__APPLE__) // On Apple system debugged executable may get -NSDocumentRevisionsDebugMode YES in argv. if (equals(argv[i], "-NSDocumentRevisionsDebugMode")) { - // Ignore + // Ignore the arg and the YES + i += 2; + continue; } #endif @@ -323,6 +336,20 @@ void CommandLineOptions::ApplyToConfig() const { g_Config.bAutoRun = false; g_Config.bSaveSettings = false; } + if (cpuCore.has_value()) { + g_Config.iCpuCore = (int)cpuCore.value(); + } + if (escapeExit.has_value()) { + g_Config.bPauseExitsEmulator = escapeExit.value(); + } + if (pauseMenuExit.has_value()) { + g_Config.bPauseMenuExitsEmulator = pauseMenuExit.value(); + } + + if (logLevel.has_value()) { + g_logManager.SetAllLogLevels(logLevel.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 aa35ef7a96..9495b5f970 100644 --- a/Core/CmdLine.h +++ b/Core/CmdLine.h @@ -1,6 +1,7 @@ #pragma once #include +#include "Common/Log.h" #include "Core/ConfigValues.h" enum class CommandLineParseResult { @@ -15,9 +16,16 @@ struct CommandLineOptions { std::optional gpuBackend; std::optional softwareRendering; std::optional enableLogging; - + std::optional logLevel; // Override log level with this. std::optional bootFilename; + std::optional cpuCore; + + std::optional startScreen; + + std::optional escapeExit; + std::optional pauseMenuExit; + // SDL only: Option to force a specific OpenGL version (42="4.2", // etc.; -1 means "try them all"). // Implemented as a workaround for https://github.com/hrydgard/ppsspp/issues/20687 @@ -31,7 +39,6 @@ struct CommandLineOptions { #else bool showLogWindow = true; #endif - bool debugLogLevel = false; std::string configFilename = ""; std::string controlsConfigFilename = ""; diff --git a/SDL/SDLMain.cpp b/SDL/SDLMain.cpp index 9cf720dd5e..afb49a188c 100644 --- a/SDL/SDLMain.cpp +++ b/SDL/SDLMain.cpp @@ -1449,14 +1449,6 @@ int main(int argc, char *argv[]) { const char *remain_argv[256] = { argv[0] }; constexpr int remain_argv_cap = (int)(sizeof(remain_argv) / sizeof(remain_argv[0])); - // Option to force a specific OpenGL version (42="4.2", - // etc.; -1 means "try them all"). - // Implemented as a workaround for https://github.com/hrydgard/ppsspp/issues/20687 - // NOTE: this is currently not persistent (doesn't - // go to config), even though --graphics=openglX.Y - // also sets the GPU backend which does persist. - int force_gl_version = -1; - Uint32 mode = 0; for (int i = 1; i < argc; i++) { if (set_xres == -2) @@ -1475,26 +1467,6 @@ int main(int argc, char *argv[]) { set_dpi = -2; else if (!strcmp(argv[i], "--scale")) set_scale = -2; - else if (!strncmp(argv[i], "--graphics=", strlen("--graphics="))) { - const char *restOfOption = argv[i] + strlen("--graphics="); - double val=-1.0; // Yes, floating point. - if (!strcmp(restOfOption, "vulkan")) { - g_Config.iGPUBackend = (int)GPUBackend::VULKAN; - g_Config.bSoftwareRendering = false; - } else if (!strcmp(restOfOption, "software")) { - // Same as on Windows, software presently implies OpenGL. - g_Config.iGPUBackend = (int)GPUBackend::OPENGL; - g_Config.bSoftwareRendering = true; - } else if (!strcmp(restOfOption, "gles") || !strcmp(restOfOption, "opengl")) { - // NOTE: OpenGL and GLES are treated the same for - // the purposes of option parsing. - g_Config.iGPUBackend = (int)GPUBackend::OPENGL; - g_Config.bSoftwareRendering = false; - } else if (sscanf(restOfOption, "gles%lg", &val) == 1 || sscanf(restOfOption, "opengl%lg", &val) == 1) { - g_Config.iGPUBackend = (int)GPUBackend::OPENGL; - g_Config.bSoftwareRendering = false; - force_gl_version = int(10.0 * val + 0.5); - } } else { if (remain_argc < remain_argv_cap - 1) { remain_argv[remain_argc++] = argv[i]; @@ -1660,7 +1632,7 @@ int main(int argc, char *argv[]) { std::string error_message; if (g_Config.iGPUBackend == (int)GPUBackend::OPENGL) { SDLGLGraphicsContext *glctx = new SDLGLGraphicsContext(); - if (glctx->Init(window, x, y, w, h, mode, &error_message, force_gl_version) != 0) { + if (glctx->Init(window, x, y, w, h, mode, &error_message, cmdLineOptions.force_gl_version) != 0) { // Let's try the fallback once per process run. fprintf(stderr, "GL init error '%s' - falling back to Vulkan\n", error_message.c_str()); g_Config.iGPUBackend = (int)GPUBackend::VULKAN; @@ -1690,7 +1662,7 @@ int main(int argc, char *argv[]) { // NOTE : This should match the three lines above in the OpenGL case. SDLGLGraphicsContext *glctx = new SDLGLGraphicsContext(); - if (glctx->Init(window, x, y, w, h, mode, &error_message, force_gl_version) != 0) { + if (glctx->Init(window, x, y, w, h, mode, &error_message, cmdLineOptions.force_gl_version) != 0) { fprintf(stderr, "GL fallback failed: %s\n", error_message.c_str()); return 1; } @@ -1866,7 +1838,7 @@ int main(int argc, char *argv[]) { if (g_Config.iGPUBackend == (int)GPUBackend::OPENGL) { SDLGLGraphicsContext *ctx = (SDLGLGraphicsContext *)graphicsContext; - if (!ctx->Init(window, x, y, w, h, mode, &error_message, force_gl_version)) { + if (!ctx->Init(window, x, y, w, h, mode, &error_message, cmdLineOptions.force_gl_version)) { fprintf(stderr, "Failed to reinit graphics.\n"); } } diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index 723313f4d4..ba019f3a53 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -543,7 +543,9 @@ void NativeInit(int argc, const char *argv[], const CommandLineOptions &cmdLineO #elif PPSSPP_PLATFORM(SWITCH) g_Config.memStickDirectory = g_Config.internalDataDirectory / "config/ppsspp"; g_Config.flash0Directory = g_Config.internalDataDirectory / "assets/flash0"; -#elif !PPSSPP_PLATFORM(WINDOWS) +#elif PPSSPP_PLATFORM(WINDOWS) + // ... +#else std::string config; if (getenv("XDG_CONFIG_HOME") != NULL) config = getenv("XDG_CONFIG_HOME"); @@ -574,14 +576,12 @@ void NativeInit(int argc, const char *argv[], const CommandLineOptions &cmdLineO g_logManager.Init(&g_Config.bEnableLogging); -#if !PPSSPP_PLATFORM(WINDOWS) g_Config.SetSearchPath(GetSysDirectory(DIRECTORY_SYSTEM)); // Note that if we don't have storage permission here, loading the config will // fail and it will be set to the default. Later, we load again when we get permission. - g_Config.Load(); + g_Config.Load(cmdLineOptions.configFilename.c_str(), cmdLineOptions.controlsConfigFilename.c_str()); System_Notify(SystemNotification::CONFIG_LOADED); -#endif // Apply parsed command line options to config. cmdLineOptions.ApplyToConfig(); @@ -603,7 +603,17 @@ void NativeInit(int argc, const char *argv[], const CommandLineOptions &cmdLineO forceLogLevel = true; }; - // TODO: Need a much better command line argument parser. + if (cmdLineOptions.logLevel.has_value()) { + setLogLevel(cmdLineOptions.logLevel.value()); + } + if (cmdLineOptions.startScreen.has_value()) { + if (equals(cmdLineOptions.startScreen.value(), "touchscreentest")) + gotoTouchScreenTest = true; + if (equals(cmdLineOptions.startScreen.value(), "gamesettings")) + gotoGameSettings = true; + if (equals(cmdLineOptions.startScreen.value(), "developertools")) + gotoDeveloperTools = true; + } for (int i = 1; i < argc; i++) { if (argv[i][0] == '-') { @@ -615,32 +625,6 @@ void NativeInit(int argc, const char *argv[], const CommandLineOptions &cmdLineO } #endif switch (argv[i][1]) { - case 'd': - // Enable debug logging - // Note that you must also change the max log level in Log.h. - setLogLevel(LogLevel::LDEBUG); - break; - case 'v': - // Enable verbose logging - // Note that you must also change the max log level in Log.h. - setLogLevel(LogLevel::LVERBOSE); - break; - case 'j': - g_Config.iCpuCore = (int)CPUCore::JIT; - g_Config.DoNotSaveSetting(&g_Config.iCpuCore); - break; - case 'i': - g_Config.iCpuCore = (int)CPUCore::INTERPRETER; - g_Config.DoNotSaveSetting(&g_Config.iCpuCore); - break; - case 'r': - g_Config.iCpuCore = (int)CPUCore::IR_INTERPRETER; - g_Config.DoNotSaveSetting(&g_Config.iCpuCore); - break; - case 'J': - g_Config.iCpuCore = (int)CPUCore::JIT_IR; - g_Config.DoNotSaveSetting(&g_Config.iCpuCore); - break; case '-': if (!strncmp(argv[i], "--loglevel=", strlen("--loglevel=")) && strlen(argv[i]) > strlen("--loglevel=")) setLogLevel(static_cast(std::atoi(argv[i] + strlen("--loglevel=")))); @@ -648,28 +632,10 @@ void NativeInit(int argc, const char *argv[], const CommandLineOptions &cmdLineO fileToLog = argv[i] + strlen("--log="); if (!strncmp(argv[i], "--state=", strlen("--state=")) && strlen(argv[i]) > strlen("--state=")) stateToLoad = Path(argv[i] + strlen("--state=")); - if (!strncmp(argv[i], "--escape-exit", strlen("--escape-exit"))) - g_Config.bPauseExitsEmulator = true; - if (!strncmp(argv[i], "--pause-menu-exit", strlen("--pause-menu-exit"))) - g_Config.bPauseMenuExitsEmulator = true; - if (!strcmp(argv[i], "--fullscreen")) { - g_Config.DoNotSaveSetting(&g_Config.bFullScreen); - g_Config.bFullScreen = true; - } if (!strncmp(argv[i], "--root=", strlen("--root=")) && strlen(argv[i]) > strlen("--root=")) { g_Config.DoNotSaveSetting(&g_Config.mountRoot); g_Config.mountRoot = Path(argv[i] + strlen("--root=")); } - if (!strcmp(argv[i], "--windowed")) { - g_Config.DoNotSaveSetting(&g_Config.bFullScreen); - g_Config.bFullScreen = false; - } - if (!strcmp(argv[i], "--touchscreentest")) - gotoTouchScreenTest = true; - if (!strcmp(argv[i], "--gamesettings")) - gotoGameSettings = true; - if (!strcmp(argv[i], "--developertools")) - gotoDeveloperTools = true; if (!strncmp(argv[i], "--appendconfig=", strlen("--appendconfig=")) && strlen(argv[i]) > strlen("--appendconfig=")) { g_Config.SetAppendedConfigIni(Path(argv[i] + strlen("--appendconfig="))); g_Config.LoadAppendedConfig(); @@ -677,57 +643,51 @@ void NativeInit(int argc, const char *argv[], const CommandLineOptions &cmdLineO break; } } else { - // This parameter should be a boot filename. Only accept it if we + // Ignore. Boot filename is extracted in the previous step. + } + } + + // This parameter should be a boot filename. Only accept it if we // don't already have one. - if (!gotBootFilename) { - gotBootFilename = true; - INFO_LOG(Log::System, "Boot filename found in args: '%s'", argv[i]); + if (cmdLineOptions.bootFilename.has_value()) { + std::string bootFilename = cmdLineOptions.bootFilename.value(); + gotBootFilename = true; + INFO_LOG(Log::System, "Boot filename found in args: '%s'", bootFilename.c_str()); - bool okToLoad = true; - bool okToCheck = true; - if (System_GetPropertyBool(SYSPROP_SUPPORTS_PERMISSIONS)) { - PermissionStatus status = System_GetPermissionStatus(SYSTEM_PERMISSION_STORAGE); - if (status == PERMISSION_STATUS_DENIED) { - ERROR_LOG(Log::IO, "Storage permission denied. Launching without argument."); - okToLoad = false; - okToCheck = false; - } else if (status != PERMISSION_STATUS_GRANTED) { - ERROR_LOG(Log::IO, "Storage permission not granted. Launching without argument check."); - okToCheck = false; - } else { - INFO_LOG(Log::IO, "Storage permission granted."); - } - } - if (okToLoad) { - std::string str = std::string(argv[i]); - // Handle file:/// URIs, since you get those when creating shortcuts on some Android systems. - if (startsWith(str, "file:///")) { - str = UriDecode(str.substr(7)); - INFO_LOG(Log::IO, "Decoding '%s' to '%s'", argv[i], str.c_str()); - } - - boot_filename = Path(str); - skipLogo = true; - } - // This is needed on iOS, to fixup the path to match the current app directory, if it's stored in it. - TryUpdateSavedPath(&boot_filename); - if (okToLoad && okToCheck) { - std::unique_ptr fileLoader(ConstructFileLoader(boot_filename)); - if (!fileLoader->Exists()) { - fprintf(stderr, "File not found: %s\n", boot_filename.c_str()); - -#if defined(_WIN32) || defined(__ANDROID__) || PPSSPP_PLATFORM(IOS) - boot_filename.clear(); -#else - // Bail. - exit(1); -#endif - } - } + bool okToLoad = true; + bool okToCheck = true; + if (System_GetPropertyBool(SYSPROP_SUPPORTS_PERMISSIONS)) { + PermissionStatus status = System_GetPermissionStatus(SYSTEM_PERMISSION_STORAGE); + if (status == PERMISSION_STATUS_DENIED) { + ERROR_LOG(Log::IO, "Storage permission denied. Launching without argument."); + okToLoad = false; + okToCheck = false; + } else if (status != PERMISSION_STATUS_GRANTED) { + ERROR_LOG(Log::IO, "Storage permission not granted. Launching without argument check."); + okToCheck = false; } else { - fprintf(stderr, "Syntax error: Can only boot one file.\nNote: Many command line args need a =, like --appendconfig=FILENAME.ini.\n"); + INFO_LOG(Log::IO, "Storage permission granted."); + } + } + if (okToLoad) { + // 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()); + } + + boot_filename = Path(bootFilename); + skipLogo = true; + } + // This is needed on iOS, to fixup the path to match the current app directory, if it's stored in it. + TryUpdateSavedPath(&boot_filename); + if (okToLoad && okToCheck) { + std::unique_ptr fileLoader(ConstructFileLoader(boot_filename)); + if (!fileLoader->Exists()) { + fprintf(stderr, "File not found: %s\n", boot_filename.c_str()); + #if defined(_WIN32) || defined(__ANDROID__) || PPSSPP_PLATFORM(IOS) - // Ignore and proceed. + boot_filename.clear(); #else // Bail. exit(1); diff --git a/Windows/main.cpp b/Windows/main.cpp index 1b46c21268..e92e4f5d51 100644 --- a/Windows/main.cpp +++ b/Windows/main.cpp @@ -1091,12 +1091,6 @@ int WINAPI WinMain(HINSTANCE _hInstance, HINSTANCE hPrevInstance, LPSTR szCmdLin InitMemstickDirectory(); CreateSysDirectories(); - // Load config up here, because those changes below would be overwritten - // if it's not loaded here first. - g_Config.SetSearchPath(GetSysDirectory(DIRECTORY_SYSTEM)); - g_Config.Load(cmdLineOptions.configFilename.c_str(), cmdLineOptions.controlsConfigFilename.c_str()); - System_Notify(SystemNotification::CONFIG_LOADED); - #ifndef _DEBUG // See #11719 - too many Vulkan drivers crash on basic init. if (g_Config.IsBackendEnabled(GPUBackend::VULKAN)) { @@ -1119,10 +1113,6 @@ int WINAPI WinMain(HINSTANCE _hInstance, HINSTANCE hPrevInstance, LPSTR szCmdLin SetWindowPos(console, NULL, g_Config.iConsoleWindowX, g_Config.iConsoleWindowY, 0, 0, SWP_NOSIZE | SWP_NOZORDER); } - if (cmdLineOptions.debugLogLevel) { - g_logManager.SetAllLogLevels(LogLevel::LDEBUG); - } - // Windows Media Foundation - used for camera and (currently, but not later) microphone support (we'll move to WASAPI for audio input later) if (!RegisterCMPTMFApis()) { ERROR_LOG(Log::System, "Failed to load Media Foundation functions"); diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index 40c6adfad0..5f710ea20f 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -1317,6 +1317,7 @@ bool TestCmdLine() { "ppsspp", "--fullscreen", "--graphics=d3d11", + "--pause-menu-exit", "My_Game.iso" }; int argc = ARRAY_SIZE(argv); @@ -1326,6 +1327,7 @@ bool TestCmdLine() { EXPECT_EQ_STR(options.bootFilename.value_or(""), 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_TRUE(options.pauseMenuExit.value_or(false)); } // Test GL version override { From 650a59f964f3603711f4fee8e5ede131b00e9faf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 20 Jul 2026 16:50:34 +0200 Subject: [PATCH 5/7] Migrate more config parsing to the new parser --- Core/CmdLine.cpp | 13 ++++++++++--- Core/CmdLine.h | 4 ++++ UI/NativeApp.cpp | 49 ++++++++++++++++++------------------------------ 3 files changed, 32 insertions(+), 34 deletions(-) diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index c3ea16f688..aa3c68a4b1 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -133,8 +133,14 @@ static const CommandLineParam g_autoParams[] = { {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(appendConfig), CmdParamType::String, "appendconfig", 0, "Merge config FILE into the current configuration"}, + {POFF(root), CmdParamType::String, "root", 0, "Mount root directory"}, + {POFF(stateToLoad), CmdParamType::String, "state", 0, "Load state from specified file"}, }; +// 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[]) { // NOTE: by convention, --help outputs to stdout, // not to stderr, since it is intended output in this @@ -175,9 +181,6 @@ static int printUsage(int argc, const char *argv[]) { 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"); - - PRINT_STDOUT(" --appendconfig=FILE merge config FILE into the current configuration\n"); - return 0; } @@ -350,6 +353,10 @@ void CommandLineOptions::ApplyToConfig() const { g_logManager.SetAllLogLevels(logLevel.value()); } + 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 9495b5f970..b4c203c2e7 100644 --- a/Core/CmdLine.h +++ b/Core/CmdLine.h @@ -26,6 +26,10 @@ struct CommandLineOptions { std::optional escapeExit; std::optional pauseMenuExit; + std::optional appendConfig; + std::optional root; // mount root, needs more explanation + std::optional stateToLoad; + // SDL only: Option to force a specific OpenGL version (42="4.2", // etc.; -1 means "try them all"). // Implemented as a workaround for https://github.com/hrydgard/ppsspp/issues/20687 diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index ba019f3a53..eeef028103 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -587,12 +587,8 @@ void NativeInit(int argc, const char *argv[], const CommandLineOptions &cmdLineO cmdLineOptions.ApplyToConfig(); const char *fileToLog = nullptr; - Path stateToLoad; bool gotBootFilename = false; - bool gotoGameSettings = false; - bool gotoTouchScreenTest = false; - bool gotoDeveloperTools = false; boot_filename.clear(); // Parse command line @@ -606,14 +602,6 @@ void NativeInit(int argc, const char *argv[], const CommandLineOptions &cmdLineO if (cmdLineOptions.logLevel.has_value()) { setLogLevel(cmdLineOptions.logLevel.value()); } - if (cmdLineOptions.startScreen.has_value()) { - if (equals(cmdLineOptions.startScreen.value(), "touchscreentest")) - gotoTouchScreenTest = true; - if (equals(cmdLineOptions.startScreen.value(), "gamesettings")) - gotoGameSettings = true; - if (equals(cmdLineOptions.startScreen.value(), "developertools")) - gotoDeveloperTools = true; - } for (int i = 1; i < argc; i++) { if (argv[i][0] == '-') { @@ -630,16 +618,6 @@ void NativeInit(int argc, const char *argv[], const CommandLineOptions &cmdLineO setLogLevel(static_cast(std::atoi(argv[i] + strlen("--loglevel=")))); if (!strncmp(argv[i], "--log=", strlen("--log=")) && strlen(argv[i]) > strlen("--log=")) fileToLog = argv[i] + strlen("--log="); - if (!strncmp(argv[i], "--state=", strlen("--state=")) && strlen(argv[i]) > strlen("--state=")) - stateToLoad = Path(argv[i] + strlen("--state=")); - if (!strncmp(argv[i], "--root=", strlen("--root=")) && strlen(argv[i]) > strlen("--root=")) { - g_Config.DoNotSaveSetting(&g_Config.mountRoot); - g_Config.mountRoot = Path(argv[i] + strlen("--root=")); - } - if (!strncmp(argv[i], "--appendconfig=", strlen("--appendconfig=")) && strlen(argv[i]) > strlen("--appendconfig=")) { - g_Config.SetAppendedConfigIni(Path(argv[i] + strlen("--appendconfig="))); - g_Config.LoadAppendedConfig(); - } break; } } else { @@ -647,6 +625,11 @@ void NativeInit(int argc, const char *argv[], const CommandLineOptions &cmdLineO } } + if (cmdLineOptions.appendConfig.has_value()) { + g_Config.SetAppendedConfigIni(Path(cmdLineOptions.appendConfig.value())); + g_Config.LoadAppendedConfig(); + } + // This parameter should be a boot filename. Only accept it if we // don't already have one. if (cmdLineOptions.bootFilename.has_value()) { @@ -730,8 +713,8 @@ void NativeInit(int argc, const char *argv[], const CommandLineOptions &cmdLineO g_BackgroundAudio.SFX().Init(); - if (!boot_filename.empty() && stateToLoad.Valid()) { - SaveState::Load(stateToLoad, -1, &ShowMessageAfterSaveStateAction); + if (!boot_filename.empty() && cmdLineOptions.stateToLoad.has_value()) { + SaveState::Load(Path(cmdLineOptions.stateToLoad.value()), -1, &ShowMessageAfterSaveStateAction); } if (g_Config.bAchievementsEnable) { @@ -751,14 +734,18 @@ void NativeInit(int argc, const char *argv[], const CommandLineOptions &cmdLineO if (g_Config.memStickDirectory.empty()) { INFO_LOG(Log::System, "No memstick directory! Asking for one to be configured."); g_screenManager->switchScreen(new LogoScreen(AfterLogoScreen::MEMSTICK_SCREEN_INITIAL_SETUP)); - } else if (gotoGameSettings) { - g_screenManager->switchScreen(new LogoScreen(AfterLogoScreen::TO_GAME_SETTINGS)); - } else if (gotoTouchScreenTest) { - g_screenManager->switchScreen(new MainScreen()); + } else if (cmdLineOptions.startScreen.has_value()) { + // Launch into specified start screen. This is useful for testing UI, more screens can be easily added here. + if (equals(cmdLineOptions.startScreen.value(), "touchscreentest")) { + g_screenManager->switchScreen(new MainScreen()); + } g_screenManager->push(new TouchTestScreen(Path())); - } else if (gotoDeveloperTools) { - g_screenManager->switchScreen(new MainScreen()); - g_screenManager->push(new DeveloperToolsScreen(Path())); + if (equals(cmdLineOptions.startScreen.value(), "gamesettings")) { + g_screenManager->switchScreen(new LogoScreen(AfterLogoScreen::TO_GAME_SETTINGS)); + } else if (equals(cmdLineOptions.startScreen.value(), "developertools")) { + g_screenManager->switchScreen(new MainScreen()); + g_screenManager->push(new DeveloperToolsScreen(Path())); + } } else if (skipLogo && !boot_filename.empty()) { INFO_LOG(Log::System, "Launching EmuScreen with boot filename '%s'", boot_filename.c_str()); g_screenManager->switchScreen(new EmuScreen(boot_filename)); From 4b24c187f281f3d73b3955c6b33b2cac4ddd635d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 20 Jul 2026 17:44:19 +0200 Subject: [PATCH 6/7] Move more command line parameters to the new parser --- Core/CmdLine.cpp | 98 ++++++++++++++++++++++++++---------- Core/CmdLine.h | 29 +++++++++-- UI/NativeApp.cpp | 8 +-- headless/Headless.cpp | 112 ++++++++++++++++-------------------------- unittest/UnitTest.cpp | 10 +++- 5 files changed, 153 insertions(+), 104 deletions(-) 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 From 9a2342b98e707b314fb816864a03d63d53c189ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 20 Jul 2026 18:11:47 +0200 Subject: [PATCH 7/7] SDL buildfix, headless logic fix --- SDL/SDLMain.cpp | 10 +++++----- headless/Headless.cpp | 13 +++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/SDL/SDLMain.cpp b/SDL/SDLMain.cpp index afb49a188c..b592ce7148 100644 --- a/SDL/SDLMain.cpp +++ b/SDL/SDLMain.cpp @@ -1467,7 +1467,7 @@ int main(int argc, char *argv[]) { set_dpi = -2; else if (!strcmp(argv[i], "--scale")) set_scale = -2; - } else { + else { if (remain_argc < remain_argv_cap - 1) { remain_argv[remain_argc++] = argv[i]; } else { @@ -1529,19 +1529,19 @@ int main(int argc, char *argv[]) { // Force fullscreen if the resolution is too low to run windowed. if (g_DesktopWidth < 480 * 2 && g_DesktopHeight < 272 * 2) { - cmdLineParams.fullscreen = true; + cmdLineOptions.fullscreen = true; } // If we're on mobile, don't try for windowed either. #if defined(MOBILE_DEVICE) && !PPSSPP_PLATFORM(SWITCH) - cmdLineParams.fullscreen = true; + cmdLineOptions.fullscreen = true; #elif defined(USING_FBDEV) || PPSSPP_PLATFORM(SWITCH) - cmdLineParams.fullscreen = true; + cmdLineOptions.fullscreen = true; #else mode |= SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY; #endif - if (cmdLineParams.fullscreen) { + if (cmdLineOptions.fullscreen) { g_display.pixel_xres = g_DesktopWidth; g_display.pixel_yres = g_DesktopHeight; g_Config.bFullScreen = true; diff --git a/headless/Headless.cpp b/headless/Headless.cpp index 247ef11c43..055b0e40bd 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -384,13 +384,16 @@ int main(int argc, const char* argv[]) { std::vector testFilenames; std::vector ignoredTests; - const char *mountIso = nullptr; - const char *mountRoot = nullptr; + std::string mountIso; + std::string mountRoot; if (cmdLineOptions.cpuCore.has_value()) { cpuCore = cmdLineOptions.cpuCore.value(); } + if (cmdLineOptions.root.has_value()) { + mountRoot = cmdLineOptions.root.value().c_str(); + } for (int i = 1; i < argc; i++) { if (!strcmp(argv[i], "-m") || !strcmp(argv[i], "--mount")) { @@ -470,8 +473,8 @@ int main(int argc, const char* argv[]) { coreParameter.gpuCore = glWorking ? gpuCore : GPUCORE_SOFTWARE; coreParameter.graphicsContext = graphicsContext; coreParameter.enableSound = false; - coreParameter.mountIso = mountIso ? Path(mountIso) : Path(); - coreParameter.mountRoot = mountRoot ? Path(mountRoot) : Path(); + coreParameter.mountIso = mountIso.empty() ? Path() : Path(mountIso); + coreParameter.mountRoot = mountRoot.empty() ? Path() : Path(mountRoot); coreParameter.startBreak = false; coreParameter.headLess = true; coreParameter.renderScaleFactor = 1; @@ -528,8 +531,6 @@ int main(int argc, const char* argv[]) { g_Config.bUseOldAtrac = oldAtrac; g_Config.iForceEnableHLE = 0xFFFFFFFF; // Run all modules as HLE. We don't have anything to load in this context. - // g_Config.bUseOldAtrac = true; - Path exePath = File::GetExeDirectory(); g_Config.flash0Directory = exePath / "assets/flash0";