From a59637975abebabed6fbfb121370d6901c3bb9b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 20 Jul 2026 11:19:31 +0200 Subject: [PATCH] Call the new command line parser from all backends --- Common/System/NativeApp.h | 4 +- Core/CmdLine.cpp | 97 ++++++++++++++++++++++++++++++++++--- Core/CmdLine.h | 9 +++- Qt/QtMain.cpp | 18 ++++--- SDL/SDLMain.cpp | 73 ++++++---------------------- UI/NativeApp.cpp | 6 ++- Windows/main.cpp | 16 ++++-- android/jni/app-android.cpp | 18 ++++++- ios/SceneDelegate.mm | 14 ++++++ 9 files changed, 173 insertions(+), 82 deletions(-) diff --git a/Common/System/NativeApp.h b/Common/System/NativeApp.h index 9afaf5a301..513ab571df 100644 --- a/Common/System/NativeApp.h +++ b/Common/System/NativeApp.h @@ -40,10 +40,12 @@ void NativeGetAppInfo(std::string *app_dir_name, std::string *app_nice_name, boo // Otherwise, just return false. bool NativeIsAtTopLevel(); +struct CommandLineOptions; + // The very first function to be called after NativeGetAppInfo. Even NativeMix is not called // before this, although it may be called at any point in time afterwards (on any thread!) // This functions must NOT call OpenGL. Main thread. -void NativeInit(int argc, const char *argv[], const char *savegame_dir, const char *external_dir, const char *cache_dir); +void NativeInit(int argc, const char *argv[], const CommandLineOptions &cmdLineOptions, const char *savegame_dir, const char *external_dir, const char *cache_dir); void NativeSetAchievementsHostOverride(std::string_view host); void NativeClearAchievementsHostOverride(); diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index 532386f099..be0c12bee5 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -2,7 +2,59 @@ #include "Core/CmdLine.h" #include "Common/StringUtils.h" -void CommandLineOptions::Parse(int argc, const char *argv[]) { +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 + // case (usage printed under different circumstances, + // say in response to error during parsing commandline, + // may go to stderr). + FILE *dst = stdout; + + const char *progname = argc > 0 ? argv[0] : "ppsspp"; + // NOTE: wording largely taken from + // https://www.ppsspp.org/docs/reference/command-line/ + fprintf(dst, "PPSSPP - a PSP emulator (SDL build)\n"); + fprintf(dst, "Usage: %s [options] [FILE]\n\n", progname); + fprintf(dst, "Launches FILE (e.g. ISO image) if present.\n"); + fprintf(dst, "Options (some of these are specific to SDL backend):\n"); + fprintf(dst, " -h, --help show this message and exit\n"); + fprintf(dst, " --version show version information and exit\n"); + + fprintf(dst, " -d set the log level to debug\n"); + fprintf(dst, " -v set the log level to verbose\n"); + fprintf(dst, " --loglevel=INTEGER set the log level to specified value\n"); + fprintf(dst, " --log=FILE output log to FILE\n"); + fprintf(dst, " --state=FILE load state from FILE\n"); + + fprintf(dst, " -i use the interpreter\n"); + fprintf(dst, " -r use IR interpreter\n"); + fprintf(dst, " -j use JIT\n"); + fprintf(dst, " -J use IR JIT\n"); + + fprintf(dst, " --fullscreen force full screen mode, ignoring saved configuration\n"); + fprintf(dst, " --windowed force windowed mode, ignoring saved configuration\n"); + fprintf(dst, " --xres PIXELS set X resolution\n"); + fprintf(dst, " --yres PIXELS set Y resolution\n"); + fprintf(dst, " --dpi FACTOR set DPI\n"); + fprintf(dst, " --scale FACTOR set scale\n"); + fprintf(dst, " --ipad set resolution to 1024x768\n"); + fprintf(dst, " --portrait portrait mode\n"); + fprintf(dst, " --graphics=BACKEND use a different gpu backend\n"); + fprintf(dst, " options: gles, software, etc. (also opengl3.1, etc.)\n"); + + fprintf(dst, " --pause-menu-exit change \"Exit to menu\" in pause menu to \"Exit\"\n"); + fprintf(dst, " --escape-exit escape key exits the application\n"); + fprintf(dst, " --gamesettings go directly to settings\n"); + fprintf(dst, " --touchscreentest go directly to the touchscreentest screen\n"); + fprintf(dst, " --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 fprintf(stderr, ....). +// Actually might want to reconsider given Android... +CommandLineParseResult CommandLineOptions::Parse(int argc, const char *argv[]) { constexpr std::string_view gpuBackendStr = "--graphics="; constexpr std::string_view configOption = "--config="; constexpr std::string_view controlsOption = "--controlconfig="; @@ -12,10 +64,18 @@ void CommandLineOptions::Parse(int argc, const char *argv[]) { #endif // The rest is handled in NativeInit(). - for (size_t i = 1; i < argc; ++i) { + // NOTE: We don't increment i here, as we'll sometimes handle options that read the next argument. + for (size_t i = 1; i < argc; ) { const size_t len = strlen(argv[i]); - if (argv[i][0] != '-' || len < 2) { - continue; + + if (len > 0 && argv[i][0] != '-') { + // This is a filename to boot. + if (!bootFilename.has_value()) { + bootFilename = std::string(argv[i]); + } else { + // Already have a filename. + fprintf(stderr, "Warning: Ignoring extra boot filename '%s'.\n", argv[i]); + } } // single char commands, like -l, -s, -d @@ -31,19 +91,36 @@ void CommandLineOptions::Parse(int argc, const char *argv[]) { case 'd': debugLogLevel = true; break; + case 'h': + printUsage(argc, argv); + return CommandLineParseResult::Exit; + case 'v': + printf("%s\n", PPSSPP_GIT_VERSION); + return CommandLineParseResult::Exit; } } +#if defined(__APPLE__) + // On Apple system debugged executable may get -NSDocumentRevisionsDebugMode YES in argv. + if (equals(argv[i], "-NSDocumentRevisionsDebugMode")) { + // Ignore + } +#endif // Simple bool commands // NOTE: We need to parse --fullscreen early, before we create the window. - if (equals(argv[i], "--fullscreen")) { + 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 - if (startsWith(argv[i], gpuBackendStr)) { + } else if (startsWith(argv[i], gpuBackendStr)) { const std::string_view 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", @@ -66,8 +143,14 @@ void CommandLineOptions::Parse(int argc, const char *argv[]) { configFilename = std::string(argv[i] + configOption.size()); } else if (startsWith(argv[i], controlsOption)) { controlsConfigFilename = std::string(argv[i] + controlsOption.size()); + } else { + // Report unknown argument later once this is complete. } + + // To the next argument. + i++; } + return CommandLineParseResult::Continue; } void CommandLineOptions::ApplyToConfig() const { diff --git a/Core/CmdLine.h b/Core/CmdLine.h index cfd9c4977b..0197ee0311 100644 --- a/Core/CmdLine.h +++ b/Core/CmdLine.h @@ -3,6 +3,12 @@ #include #include "Core/ConfigValues.h" +enum class CommandLineParseResult { + Continue, + Exit, + Error, +}; + // We collect command line options in this struct, then we apply it to the config after it's been loaded. struct CommandLineOptions { std::optional fullscreen; @@ -23,6 +29,7 @@ struct CommandLineOptions { bool optionS = true; // a legacy option - void Parse(int argc, const char *argv[]); + // 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; }; diff --git a/Qt/QtMain.cpp b/Qt/QtMain.cpp index ce47575de0..9d2bb84e5c 100644 --- a/Qt/QtMain.cpp +++ b/Qt/QtMain.cpp @@ -49,6 +49,7 @@ #include "Common/TimeUtil.h" #include "Common/Log/LogManager.h" +#include "Core/CmdLine.h" #include "Core/Config.h" #include "Core/ConfigValues.h" #include "Core/HW/Camera.h" @@ -857,11 +858,16 @@ int main(int argc, char *argv[]) g_logManager.EnableOutput(LogOutput::Stdio); - for (int i = 1; i < argc; i++) { - if (!strcmp(argv[i], "--version")) { - printf("%s\n", PPSSPP_GIT_VERSION); - return 0; - } + CommandLineOptions cmdLineOptions; + CommandLineParseResult parseResult = cmdLineOptions.Parse(argc, argv); + switch (parseResult) { + case CommandLineParseResult::Exit: + return 0; + case CommandLineParseResult::Error: + return 1; + default: + // Continue with launch. + break; } // Ignore sigpipe. @@ -909,7 +915,7 @@ int main(int argc, char *argv[]) savegame_dir += "/"; external_dir += "/"; - NativeInit(argc, (const char **)argv, savegame_dir.c_str(), external_dir.c_str(), nullptr); + NativeInit(argc, (const char **)argv, cmdLineOptions, savegame_dir.c_str(), external_dir.c_str(), nullptr); g_mainWindow = new MainWindow(nullptr, g_Config.bFullScreen); g_mainWindow->show(); diff --git a/SDL/SDLMain.cpp b/SDL/SDLMain.cpp index ea99108edc..fa11359f8a 100644 --- a/SDL/SDLMain.cpp +++ b/SDL/SDLMain.cpp @@ -28,6 +28,7 @@ SDLJoystick *joystick = NULL; #include "Common/System/Request.h" #include "Common/System/NativeApp.h" #include "Common/Audio/AudioBackend.h" +#include "Core/CmdLine.h" #include "ext/glslang/glslang/Public/ShaderLang.h" #include "Common/Data/Format/PNGLoad.h" #include "Common/Net/Resolve.h" @@ -1382,70 +1383,24 @@ void UpdateSDLCursor() { #endif } -static int printUsage(const char *progname) -{ - // NOTE: by convention, --help outputs to stdout, - // not to stderr, since it is intended output in this - // case (usage printed under different circumstances, - // say in response to error during parsing commandline, - // may go to stderr). - FILE *dst = stdout; - - // NOTE: wording largely taken from - // https://www.ppsspp.org/docs/reference/command-line/ - fprintf(dst, "PPSSPP - a PSP emulator (SDL build)\n"); - fprintf(dst, "Usage: %s [options] [FILE]\n\n", progname); - fprintf(dst, "Launches FILE (e.g. ISO image) if present.\n"); - fprintf(dst, "Options (some of these are specific to SDL backend):\n"); - fprintf(dst, " -h, --help show this message and exit\n"); - fprintf(dst, " --version show version information and exit\n"); - - fprintf(dst, " -d set the log level to debug\n"); - fprintf(dst, " -v set the log level to verbose\n"); - fprintf(dst, " --loglevel=INTEGER set the log level to specified value\n"); - fprintf(dst, " --log=FILE output log to FILE\n"); - fprintf(dst, " --state=FILE load state from FILE\n"); - - fprintf(dst, " -i use the interpreter\n"); - fprintf(dst, " -r use IR interpreter\n"); - fprintf(dst, " -j use JIT\n"); - fprintf(dst, " -J use IR JIT\n"); - - fprintf(dst, " --fullscreen force full screen mode, ignoring saved configuration\n"); - fprintf(dst, " --windowed force windowed mode, ignoring saved configuration\n"); - fprintf(dst, " --xres PIXELS set X resolution\n"); - fprintf(dst, " --yres PIXELS set Y resolution\n"); - fprintf(dst, " --dpi FACTOR set DPI\n"); - fprintf(dst, " --scale FACTOR set scale\n"); - fprintf(dst, " --ipad set resolution to 1024x768\n"); - fprintf(dst, " --portrait portrait mode\n"); - fprintf(dst, " --graphics=BACKEND use a different gpu backend\n"); - fprintf(dst, " options: gles, software, etc. (also opengl3.1, etc.)\n"); - - fprintf(dst, " --pause-menu-exit change \"Exit to menu\" in pause menu to \"Exit\"\n"); - fprintf(dst, " --escape-exit escape key exits the application\n"); - fprintf(dst, " --gamesettings go directly to settings\n"); - fprintf(dst, " --touchscreentest go directly to the touchscreentest screen\n"); - fprintf(dst, " --appendconfig=FILE merge config FILE into the current configuration\n"); - - return 0; -} - #ifdef _WIN32 #undef main #endif int main(int argc, char *argv[]) { - for (int i = 1; i < argc; i++) { - if (!strcmp(argv[i], "--help") || !strcmp(argv[i], "-h")) - return printUsage(argv[0]); - else if (!strcmp(argv[i], "--version")) { - printf("%s\n", PPSSPP_GIT_VERSION); - return 0; - } - } - TimeInit(); + CommandLineOptions cmdLineOptions; + CommandLineParseResult parseResult = cmdLineOptions.Parse(argc, argv); + switch (parseResult) { + case CommandLineParseResult::Exit: + return 0; + case CommandLineParseResult::Error: + return 1; + default: + // Continue with launch. + break; + } + g_logManager.EnableOutput(LogOutput::Stdio); #ifdef HAVE_LIBNX @@ -1681,7 +1636,7 @@ int main(int argc, char *argv[]) { #else const char *external_dir = "/tmp"; #endif - NativeInit(remain_argc, (const char **)remain_argv, path, external_dir, nullptr); + NativeInit(remain_argc, (const char **)remain_argv, cmdLineOptions, path, external_dir, nullptr); // Use the setting from the config when initing the window. if (g_Config.bFullScreen) { diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index 158f979bed..723313f4d4 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -90,6 +90,7 @@ #include "Common/Thread/ThreadManager.h" #include "Common/Audio/AudioBackend.h" #include "Common/UI/PopupScreens.h" +#include "Core/CmdLine.h" #include "Core/ControlMapper.h" #include "Core/Config.h" #include "Core/ConfigValues.h" @@ -393,7 +394,7 @@ static void ClearFailedGPUBackends() { File::Delete(failedBackendsFile); } -void NativeInit(int argc, const char *argv[], const char *savegame_dir, const char *external_dir, const char *cache_dir) { +void NativeInit(int argc, const char *argv[], const CommandLineOptions &cmdLineOptions, const char *savegame_dir, const char *external_dir, const char *cache_dir) { net::Init(); // This needs to happen before we load the config. So on Windows we also run it in Main. It's fine to call multiple times. g_Config.Init(); @@ -582,6 +583,9 @@ void NativeInit(int argc, const char *argv[], const char *savegame_dir, const ch System_Notify(SystemNotification::CONFIG_LOADED); #endif + // Apply parsed command line options to config. + cmdLineOptions.ApplyToConfig(); + const char *fileToLog = nullptr; Path stateToLoad; diff --git a/Windows/main.cpp b/Windows/main.cpp index 0b299ab4d5..d0e5b6d2ed 100644 --- a/Windows/main.cpp +++ b/Windows/main.cpp @@ -1050,7 +1050,16 @@ int WINAPI WinMain(HINSTANCE _hInstance, HINSTANCE hPrevInstance, LPSTR szCmdLin // args provide argc and argv. CommandLineOptions cmdLineOptions; - cmdLineOptions.Parse((int)args.size(), args.data()); + CommandLineParseResult parseResult = cmdLineOptions.Parse((int)args.size(), args.data()); + switch (parseResult) { + case CommandLineParseResult::Exit: + return 0; + case CommandLineParseResult::Error: + return 1; + default: + // Continue with launch. + break; + } if (iCmdShow == SW_MAXIMIZE) { // Consider this to mean --fullscreen. I guess frontends might use it? @@ -1088,9 +1097,6 @@ int WINAPI WinMain(HINSTANCE _hInstance, HINSTANCE hPrevInstance, LPSTR szCmdLin g_Config.Load(cmdLineOptions.configFilename.c_str(), cmdLineOptions.controlsConfigFilename.c_str()); System_Notify(SystemNotification::CONFIG_LOADED); - // Apply parsed command line options to config. - cmdLineOptions.ApplyToConfig(); - #ifndef _DEBUG // See #11719 - too many Vulkan drivers crash on basic init. if (g_Config.IsBackendEnabled(GPUBackend::VULKAN)) { @@ -1098,7 +1104,7 @@ int WINAPI WinMain(HINSTANCE _hInstance, HINSTANCE hPrevInstance, LPSTR szCmdLin } #endif - NativeInit((int)args.size(), args.data(), "", "", nullptr); + NativeInit((int)args.size(), args.data(), cmdLineOptions, "", "", nullptr); // Consider at least the following cases before changing this code: // - By default in Release, the console should be hidden by default even if logging is enabled. diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index 3b8378446a..5d763f65c9 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -95,10 +95,10 @@ struct JNIEnv {}; #include "AndroidVulkanContext.h" #include "AndroidJavaGLContext.h" +#include "Core/CmdLine.h" #include "Core/Config.h" #include "Core/ConfigValues.h" #include "Core/Loaders.h" -#include "Core/FileLoaders/LocalFileLoader.h" #include "Core/KeyMap.h" #include "Core/System.h" #include "Core/HLE/sceUsbCam.h" @@ -826,10 +826,24 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeApp_init } } + CommandLineOptions cmdLineOptions; + CommandLineParseResult parseResult = cmdLineOptions.Parse((int)args.size(), args.data()); + switch (parseResult) { + case CommandLineParseResult::Exit: + EARLY_LOG("Command line parse said to exit - mobile, so ignoring."); + break; + case CommandLineParseResult::Error: + EARLY_LOG("Command line parse reported error - mobile, so ignoring."); + break; + default: + // Continue with launch. + break; + } + EARLY_LOG("Calling NativeInit with user_data_path %s, externalStorageDir %s, cacheDir %s", user_data_path.c_str(), externalStorageDir.c_str(), cacheDir.c_str()); // TODO: We should be able to do the Vulkan init in parallel with NativeInit. - NativeInit((int)args.size(), &args[0], user_data_path.c_str(), externalStorageDir.c_str(), cacheDir.c_str()); + NativeInit((int)args.size(), &args[0], cmdLineOptions, user_data_path.c_str(), externalStorageDir.c_str(), cacheDir.c_str()); bFirstResume = true; diff --git a/ios/SceneDelegate.mm b/ios/SceneDelegate.mm index e2b1cfb0e5..6bf3f62b5a 100644 --- a/ios/SceneDelegate.mm +++ b/ios/SceneDelegate.mm @@ -149,6 +149,20 @@ static NSString *ExtractGameInfoScheme(NSURL *url) { NSLog(@"SceneDelegate: startup path passed to argv: %s", gStartupArgStorage.c_str()); } + CommandLineOptions cmdLineOptions; + CommandLineParseResult parseResult = cmdLineOptions.Parse(argc, argv); + switch (parseResult) { + case CommandLineParseResult::Exit: + INFO_LOG(Log::System, "Command line parse said to exit - mobile, so ignoring."); + break; + case CommandLineParseResult::Error: + INFO_LOG(Log::System, "Command line parse reported error - mobile, so ignoring."); + break; + default: + // Continue with launch. + break; + } + NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; NSString *bundlePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingString:@"/assets/"]; NativeInit(argc, (const char**)argv, documentsPath.UTF8String, bundlePath.UTF8String, NULL);