mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-08-31 09:45:24 +02:00
Module symbols are keyed by module+crc so they're shared by every game that
loads that module. But symbols the user attaches to addresses that aren't in
any module - the heap, the stack, scratchpad, a hardware register, typically
after a memory.search - describe one game's own memory layout and are worthless
to any other game. Those now go to PSP/SYSTEM/SYMBOLS/<gameID>_syms.ppsym.
They're module index 0 ("absolute"), which already round-trips through the
existing per-module code: GetModuleRelativeAddr/GetModuleAbsoluteAddr are
identity for it, so the file format is unchanged, just with absolute addresses.
SaveModuleSymbols only needed to stop requiring a ModuleEntry. Auto-load/save
hang off CPU_Init/CPU_Shutdown rather than module load/unload, gated on the same
bAutoSaveLoadSymbols setting - and deliberately not on SYSPROP_HAS_DEBUGGER,
which only the Windows port reports true for, so LoadSymbolsIfSupported next to
it does nothing at all on headless. hle.game.saveSymbols/loadSymbols expose it.
Four things found while doing it:
- Symbols outside any module were being dropped on the floor. AddFunction/
AddData/AddLabel take moduleIndex -1 as "work it out", pass it to
GetModuleIndex(), and store whatever comes back - but that returns -1 when no
module contains the address, and -1 is never an active module, so the symbol
never reached the active maps: invisible to every lookup and to any save.
hle.data.add had spotted this and normalized -1 to 0 locally; nothing else
did, so e.g. hle.func.add outside a module silently did nothing. Fixed
centrally in a new ResolveModuleIndex() the three of them share.
(This only became reachable with the GetModuleIndex() fix in 29a38af37e -
before that it returned a wrong-but-valid module index instead.)
- The saved files were almost entirely noise. Every function the analyzer finds
is named z_un_<addr> and every import stub zz_<name>, both regenerated from
scratch on each load. One real module wrote 13KB - 443 unnamed functions and
64 stub names - for the four names a human had actually chosen. Worse, on the
next run those were loaded back as authoritative and would beat the module's
own symbols to the address. Now only names that aren't regenerated get saved,
and a module with none writes no file at all (and removes a stale one, so
deleting a symbol sticks). That module's file went 13020 -> 81 bytes.
- LoadModuleSymbols trusted the addresses in the file. It's meant to be
hand-edited and can outlive the build it came from, so relative addresses past
the end of the module are now skipped with a warning instead of landing at
nonsense addresses.
- AddFunction and AddData both erased the map entry they were updating and then
read back through the now-dangling iterator to refresh the active copy. Only
latent: the refresh is guarded on the active copy's module matching the new
one, which is false exactly when the erase happens. Re-point the iterator at
the entry's new home instead, so it can't rot if that guard ever changes.
AddLabel already did the equivalent correctly, via a local copy.
Filename sanitizing goes through SanitizeString with a new FileName restriction
rather than being open-coded; unlike the existing restrictions it substitutes
'_' instead of dropping, so two module names can't collapse onto one file.
Verified end to end on headless with cpu_alu.prx: named a function inside the
module and data/functions in scratchpad and the heap, let it exit, checked both
files, rebooted and confirmed all of it came back at the right addresses.
Unit tests 51/51, pspautotests 314/314.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
570 lines
23 KiB
C++
570 lines
23 KiB
C++
#include <algorithm>
|
|
#include <cstdlib>
|
|
|
|
#include "Core/Config.h"
|
|
#include "Core/CmdLine.h"
|
|
#include "Core/WebServer.h"
|
|
#include "Common/StringUtils.h"
|
|
#include "Common/Log/LogManager.h"
|
|
|
|
#ifdef HAVE_LIBRETRO_VFS
|
|
|
|
// Actually, this probably shouldn't be built at all for libretro! But let's add the guard.
|
|
|
|
#define PRINT_STDOUT(...) printf(__VA_ARGS__)
|
|
#define PRINT_STDERR(...) printf(__VA_ARGS__)
|
|
|
|
#else
|
|
|
|
#define PRINT_STDOUT(...) printf(__VA_ARGS__)
|
|
#define PRINT_STDERR(...) fprintf(stderr, __VA_ARGS__)
|
|
|
|
#endif
|
|
|
|
enum class CmdParamType {
|
|
Bool,
|
|
BoolInverse,
|
|
Int,
|
|
Double,
|
|
String,
|
|
Enum,
|
|
};
|
|
|
|
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;
|
|
CmdLineMode mode;
|
|
const char **enumValues;
|
|
int enumCount;
|
|
};
|
|
|
|
enum class ParseParamResult {
|
|
Success,
|
|
NoMatch,
|
|
BadValue,
|
|
};
|
|
|
|
ParseParamResult SetValue(CommandLineOptions *options, const CommandLineParam ¶m, const std::string &value) {
|
|
uint8_t *optionsPtr = reinterpret_cast<uint8_t *>(options);
|
|
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<std::optional<bool> *>(optionsPtr + param.offsetInStruct) = v;
|
|
break;
|
|
}
|
|
case CmdParamType::Int:
|
|
*reinterpret_cast<std::optional<int> *>(optionsPtr + param.offsetInStruct) = std::stoi(value);
|
|
break;
|
|
case CmdParamType::Double:
|
|
*reinterpret_cast<std::optional<double> *>(optionsPtr + param.offsetInStruct) = std::stod(value);
|
|
break;
|
|
case CmdParamType::String:
|
|
*reinterpret_cast<std::optional<std::string> *>(optionsPtr + param.offsetInStruct) = value;
|
|
break;
|
|
case CmdParamType::Enum:
|
|
{
|
|
int enumIndex = -1;
|
|
for (int i = 0; i < param.enumCount; ++i) {
|
|
if (value == param.enumValues[i]) {
|
|
enumIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
if (enumIndex == -1) {
|
|
PRINT_STDERR("Error: Invalid value for parameter --%s: '%s'. Expected one of: ", param.longName, value.c_str());
|
|
for (int i = 0; i < param.enumCount; ++i) {
|
|
PRINT_STDERR("%s%s", param.enumValues[i], (i + 1 < param.enumCount) ? ", " : "\n");
|
|
}
|
|
return ParseParamResult::BadValue;
|
|
}
|
|
*reinterpret_cast<std::optional<int> *>(optionsPtr + param.offsetInStruct) = enumIndex;
|
|
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 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 (param.shortName && len == 2 && arg[0] == '-' && arg[1] == param.shortName) {
|
|
if (i + 1 < argc) {
|
|
ParseParamResult result = SetValue(options, param, argv[i + 1]);
|
|
i += 2; // Skip both -p and value
|
|
return result;
|
|
}
|
|
PRINT_STDERR("Error: -%c requires an argument.\n", param.shortName);
|
|
return ParseParamResult::BadValue;
|
|
}
|
|
|
|
// Check for long form with equals: --param=value
|
|
if (param.longName) {
|
|
std::string prefix = std::string("--") + param.longName + "=";
|
|
if (startsWith(arg, prefix)) {
|
|
ParseParamResult result = SetValue(options, param, arg + prefix.size());
|
|
i++; // Skip --param=value
|
|
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("--") + param.longName)) {
|
|
if (i + 1 < argc) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Else no match, continue.
|
|
}
|
|
return ParseParamResult::NoMatch;
|
|
}
|
|
|
|
// Enum ExceptionAction
|
|
const char *g_ExceptionActionValues[] = {
|
|
"default",
|
|
"ignore",
|
|
"log",
|
|
"break",
|
|
};
|
|
|
|
#define POFF(member) offsetof(CommandLineOptions, member)
|
|
static const CommandLineParam g_autoParams[] = {
|
|
{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", 'r', "Mount root directory"},
|
|
{POFF(stateToLoad), CmdParamType::String, "state", '\0', "Load state from specified file"},
|
|
{POFF(compare), CmdParamType::Bool, "compare", 'c', "Enable comparison mode", CmdLineMode::Headless},
|
|
{POFF(bench), CmdParamType::Bool, "bench", 'b', "Enable benchmark mode", CmdLineMode::Headless},
|
|
{POFF(oldAtrac), CmdParamType::Bool, "old-atrac", '\0', "Use old ATRAC decoder"},
|
|
{POFF(log), CmdParamType::String, "log", '\0', "Output log to FILE", CmdLineMode::Application},
|
|
{POFF(enableLogging), CmdParamType::Bool, "log", '\0', "Full log output, not just emulated printfs", CmdLineMode::Headless},
|
|
{POFF(screenshotFilename), CmdParamType::String, "screenshot", '\0', "Compare rendered output against a reference screenshot FILE", CmdLineMode::Headless},
|
|
{POFF(screenshotFilenameSave), CmdParamType::String, "screenshot-save", '\0', "Save rendered screenshot to specified path (PNG if the path ends in .png, BMP otherwise)", CmdLineMode::Headless},
|
|
{POFF(screenshotFilenameDiff), CmdParamType::String, "screenshot-diff", '\0', "Save a visual comparison image to FILE when comparing screenshots", CmdLineMode::Headless},
|
|
{POFF(screenshotSaveKeepAlpha), CmdParamType::Bool, "screenshot-keep-alpha", '\0', "Preserve the alpha channel when saving PNG screenshots (default: alpha is forced to 255)", CmdLineMode::Headless},
|
|
{POFF(timeout), CmdParamType::Double, "timeout", '\0', "Set the timeout value", CmdLineMode::Headless},
|
|
{POFF(maxScreenshotError), CmdParamType::Double, "max-mse", '\0', "Maximum allowed MSE error for screenshot comparison", CmdLineMode::Headless},
|
|
{POFF(mountIso), CmdParamType::String, "mount", 'm', "Mount ISO/CSO on umd1:", CmdLineMode::Headless},
|
|
{POFF(odsLog), CmdParamType::Bool, "odslog", 'o', "Also log through OutputDebugString (Windows)", CmdLineMode::Headless},
|
|
{POFF(generateInterpreterDispatch), CmdParamType::Bool, "generate-interpreter-dispatch", '\0', "Generate C++ interpreter dispatch code (ExecInstruction) to stdout and exit", CmdLineMode::Headless},
|
|
{POFF(resolutionScale), CmdParamType::Int, "resolution-scale", '\0', "Set the resolution scale factor"},
|
|
{POFF(debuggerPort), CmdParamType::Int, "debugger", '\0', "Enable the WebSocket debugger on this port (0 = pick automatically); see docs/WebSocketDebugger.md"},
|
|
{POFF(autoSaveLoadSymbols), CmdParamType::Bool, "auto-save-load-symbols", '\0', "Auto save/load per-module and per-game symbol files (see bAutoSaveLoadSymbols)", CmdLineMode::Both},
|
|
{POFF(bootVSH), CmdParamType::Bool, "vsh", '\0', "Boot the VSH (requires files dumped from a PSP in the flash0 directory)"},
|
|
{POFF(memReadAction), CmdParamType::Enum, "memread", '\0', "Set the action for memory read exceptions", CmdLineMode::Both, g_ExceptionActionValues, ARRAY_SIZE(g_ExceptionActionValues)},
|
|
{POFF(memWriteAction), CmdParamType::Enum, "memwrite", '\0', "Set the action for memory write exceptions", CmdLineMode::Both, g_ExceptionActionValues, ARRAY_SIZE(g_ExceptionActionValues)},
|
|
{POFF(breakAction), CmdParamType::Enum, "break", '\0', "Set the action for break exceptions", CmdLineMode::Both, g_ExceptionActionValues, ARRAY_SIZE(g_ExceptionActionValues)},
|
|
{POFF(logNativeCrashes), CmdParamType::Bool, "log-native-crashes", '\0', "Log a native stack trace (Windows only) on an otherwise-unhandled crash", CmdLineMode::Both},
|
|
{POFF(verbose), CmdParamType::Bool, "verbose", '\0', "Enable verbose output", CmdLineMode::Both},
|
|
{POFF(printEqualLines), CmdParamType::Bool, "print-equal-lines", '\0', "Print lines that are equal during comparison", CmdLineMode::Headless},
|
|
{POFF(xres), CmdParamType::Int, "xres", '\0', "Set X resolution", CmdLineMode::Application},
|
|
{POFF(yres), CmdParamType::Int, "yres", '\0', "Set Y resolution", CmdLineMode::Application},
|
|
{POFF(dpi), CmdParamType::Double, "dpi", '\0', "Set DPI", CmdLineMode::Application},
|
|
{POFF(scale), CmdParamType::Double, "scale", '\0', "Set scale", CmdLineMode::Application},
|
|
// TODO: At some point we should maybe simply expose all config settings to be set directly from the command line automatically?
|
|
};
|
|
|
|
// NOTE: On Windows this prints nothing unfortunately, since PPSSPP is not a "console app" (PPSSPPHeadless is though, there it works).
|
|
// 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.
|
|
int CommandLineOptions::PrintUsage(const char *progname, const char *situationText) const {
|
|
// 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).
|
|
// NOTE: wording largely taken from
|
|
// https://www.ppsspp.org/docs/reference/command-line/
|
|
if (situationText) {
|
|
PRINT_STDOUT("%s\n\n", situationText);
|
|
} else {
|
|
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");
|
|
}
|
|
}
|
|
|
|
if (mode == CmdLineMode::Application) {
|
|
PRINT_STDOUT("Usage: %s [options] [FILE]\n\n", progname);
|
|
} else {
|
|
PRINT_STDOUT("Usage: %s [options] [FILE1] [FILE2] [FILE3]...\n\n", progname);
|
|
}
|
|
|
|
PRINT_STDOUT("Launches FILE (for example, an ISO image) if present.\n");
|
|
PRINT_STDOUT("See headless/README.md for more details.\n\n");
|
|
PRINT_STDOUT("Options:\n");
|
|
PRINT_STDOUT(" -h, --help show this message and exit\n");
|
|
PRINT_STDOUT(" --version show version information and exit\n");
|
|
|
|
PRINT_STDOUT(" -d set the log level to debug\n");
|
|
PRINT_STDOUT(" -v set the log level to verbose\n");
|
|
PRINT_STDOUT(" --loglevel=INTEGER set the log level to specified value\n");
|
|
if (mode == CmdLineMode::Application) {
|
|
PRINT_STDOUT(" --log=FILE output log to FILE\n");
|
|
}
|
|
PRINT_STDOUT(" --state=FILE load state from FILE\n");
|
|
|
|
PRINT_STDOUT(" --cpu=CPU use the specified CPU core (interpreter, ir, jit, jit-ir)\n");
|
|
PRINT_STDOUT(" -i use the interpreter\n");
|
|
PRINT_STDOUT(" -r use IR interpreter\n");
|
|
PRINT_STDOUT(" -j use JIT\n");
|
|
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;
|
|
}
|
|
char key[64]{};
|
|
snprintf(key, ARRAY_SIZE(key), " --%s%s%s", param.longName,
|
|
param.shortName ? ", -" : "",
|
|
param.shortName ? std::string(1, param.shortName).c_str() : "");
|
|
// Fill key with spacing, keeping at least one space before the doc string
|
|
// even if the name itself ran past the normal column width.
|
|
size_t padTo = std::max(strlen(key) + 1, (size_t)24);
|
|
padTo = std::min(padTo, ARRAY_SIZE(key) - 1);
|
|
for (size_t j = strlen(key); j < padTo; ++j) {
|
|
key[j] = ' ';
|
|
}
|
|
if (param.type == CmdParamType::Enum) {
|
|
PRINT_STDOUT("%s%s\n", key, param.docString);
|
|
PRINT_STDOUT(" options: ");
|
|
for (int i = 0; i < param.enumCount; ++i) {
|
|
PRINT_STDOUT("%s%s", param.enumValues[i], i + 1 < param.enumCount ? ", " : "");
|
|
}
|
|
PRINT_STDOUT("\n");
|
|
} else {
|
|
PRINT_STDOUT("%s%s\n", key, param.docString);
|
|
}
|
|
}
|
|
|
|
if (mode == CmdLineMode::Headless) {
|
|
PRINT_STDOUT(" --ignore TESTNAME ignore the specified test (may be repeated)\n");
|
|
}
|
|
|
|
PRINT_STDOUT(" --graphics=BACKEND use a different gpu backend\n");
|
|
PRINT_STDOUT(" options: gles, software, etc. (also opengl3.1, etc.)\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...
|
|
CommandLineParseResult CommandLineOptions::Parse(int argc, const char *argv[], CmdLineMode mode) {
|
|
this->mode = mode;
|
|
constexpr std::string_view cpuBackendStr = "--cpu=";
|
|
constexpr std::string_view gpuBackendStr = "--graphics=";
|
|
constexpr std::string_view configOption = "--config=";
|
|
constexpr std::string_view controlsOption = "--controlconfig=";
|
|
constexpr std::string_view logLevelOption = "--loglevel=";
|
|
|
|
// The rest is handled in NativeInit().
|
|
// 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 (len > 0 && argv[i][0] != '-') {
|
|
// 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. Ignore.
|
|
PRINT_STDERR("Ignoring extra boot filename '%s' (can only pass in one).\n", argv[i]);
|
|
i++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// single char commands, like -l, -s
|
|
if (len == 2) {
|
|
switch (argv[i][1]) {
|
|
case 'l':
|
|
showLogWindow = true;
|
|
enableLogging = true;
|
|
break;
|
|
case 's':
|
|
optionS = true;
|
|
break;
|
|
case 'd':
|
|
logLevel = LogLevel::LDEBUG;
|
|
break;
|
|
case 'v':
|
|
logLevel = LogLevel::LVERBOSE;
|
|
break;
|
|
case 'h':
|
|
PrintUsage(argv[0], nullptr);
|
|
return CommandLineParseResult::Exit;
|
|
// Legacy cpucore options.
|
|
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 the arg and the YES
|
|
i += 2;
|
|
continue;
|
|
}
|
|
#endif
|
|
|
|
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;
|
|
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(argv[0], nullptr);
|
|
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
|
|
} 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.
|
|
// 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 == "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) {
|
|
gpuBackend = GPUBackend::OPENGL;
|
|
softwareRendering = false;
|
|
force_gl_version = int(10.0 * glVersionTemp + 0.5);
|
|
} else if (restOfOption == "gles" || restOfOption == "opengl") {
|
|
gpuBackend = GPUBackend::OPENGL;
|
|
softwareRendering = false;
|
|
} else {
|
|
// Bad value, report error and exit.
|
|
PRINT_STDERR("Invalid value for --graphics=: %s", restOfOption.c_str());
|
|
return CommandLineParseResult::Error;
|
|
}
|
|
} else if (startsWith(argv[i], cpuBackendStr)) {
|
|
const std::string restOfOption = argv[i] + cpuBackendStr.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 == "interpreter") {
|
|
cpuCore = CPUCore::INTERPRETER;
|
|
} else if (restOfOption == "jit") {
|
|
cpuCore = CPUCore::JIT;
|
|
} else if (restOfOption == "jit-ir") {
|
|
cpuCore = CPUCore::JIT_IR;
|
|
} else if (restOfOption == "ir") {
|
|
cpuCore = CPUCore::IR_INTERPRETER;
|
|
} else {
|
|
// Bad value, report error and exit.
|
|
PRINT_STDERR("Invalid value for --cpu=: %s", restOfOption.c_str());
|
|
return CommandLineParseResult::Error;
|
|
}
|
|
} else if (startsWith(argv[i], configOption)) {
|
|
configFilename = std::string(argv[i] + configOption.size());
|
|
} else if (startsWith(argv[i], controlsOption)) {
|
|
controlsConfigFilename = std::string(argv[i] + controlsOption.size());
|
|
} else if (startsWith(argv[i], logLevelOption)) {
|
|
logLevel = static_cast<LogLevel>(atoi(argv[i] + logLevelOption.size()));
|
|
} else if (equals(argv[i], "--ignore")) {
|
|
// Headless: skip a specific test. Can be repeated, so not a regular auto-param.
|
|
if (i + 1 < argc) {
|
|
ignoredTests.emplace_back(argv[i + 1]);
|
|
i += 2;
|
|
continue;
|
|
} else {
|
|
PRINT_STDERR("Error: --ignore requires an argument.\n");
|
|
return CommandLineParseResult::Error;
|
|
}
|
|
} else if (startsWith(argv[i], "--")) {
|
|
PRINT_STDERR("Error: Unknown parameter: %s\n", argv[i]);
|
|
return CommandLineParseResult::Error;
|
|
}
|
|
// To the next argument.
|
|
i++;
|
|
}
|
|
|
|
// Final adjustments to adjust for old inconsistent code
|
|
if (log.has_value()) {
|
|
enableLogging = true;
|
|
showLogWindow = true;
|
|
}
|
|
|
|
return CommandLineParseResult::Continue;
|
|
}
|
|
|
|
void CommandLineOptions::ApplyToConfig() const {
|
|
if (fullscreen.has_value()) {
|
|
g_Config.bFullScreen = fullscreen.value();
|
|
g_Config.DoNotSaveSetting(&g_Config.bFullScreen);
|
|
}
|
|
if (gpuBackend.has_value()) {
|
|
g_Config.iGPUBackend = (int)gpuBackend.value();
|
|
g_Config.DoNotSaveSetting(&g_Config.iGPUBackend);
|
|
}
|
|
if (cpuCore.has_value()) {
|
|
g_Config.iCpuCore = (int)cpuCore.value();
|
|
}
|
|
if (softwareRendering.has_value()) {
|
|
g_Config.bSoftwareRendering = softwareRendering.value();
|
|
g_Config.DoNotSaveSetting(&g_Config.bSoftwareRendering);
|
|
}
|
|
if (enableLogging.has_value()) {
|
|
g_Config.bEnableLogging = enableLogging.value();
|
|
g_Config.DoNotSaveSetting(&g_Config.bEnableLogging);
|
|
}
|
|
if (optionS) {
|
|
g_Config.bAutoRun = false;
|
|
g_Config.bSaveSettings = false;
|
|
}
|
|
if (escapeExit.has_value()) {
|
|
g_Config.bPauseExitsEmulator = escapeExit.value();
|
|
}
|
|
if (pauseMenuExit.has_value()) {
|
|
g_Config.bPauseMenuExitsEmulator = pauseMenuExit.value();
|
|
}
|
|
|
|
if (debuggerPort.has_value()) {
|
|
g_Config.iRemoteISOPort = debuggerPort.value();
|
|
g_Config.DoNotSaveSetting(&g_Config.iRemoteISOPort);
|
|
g_Config.bRemoteDebuggerOnStartup = true;
|
|
g_Config.DoNotSaveSetting(&g_Config.bRemoteDebuggerOnStartup);
|
|
// --debugger=0 still means "pick any free port", but a specific port was asked for by
|
|
// something that intends to connect to it, so don't quietly come up on a different one.
|
|
WebServerSetRequireExactPort(debuggerPort.value() != 0);
|
|
}
|
|
|
|
if (autoSaveLoadSymbols.has_value()) {
|
|
g_Config.bAutoSaveLoadSymbols = autoSaveLoadSymbols.value();
|
|
g_Config.DoNotSaveSetting(&g_Config.bAutoSaveLoadSymbols);
|
|
}
|
|
|
|
if (logLevel.has_value()) {
|
|
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());
|
|
}
|
|
|
|
if (resolutionScale.has_value()) {
|
|
g_Config.iInternalResolution = resolutionScale.value();
|
|
g_Config.DoNotSaveSetting(&g_Config.iInternalResolution);
|
|
}
|
|
|
|
if (memReadAction.has_value()) {
|
|
g_Config.iExceptionActionMemRead = memReadAction.value();
|
|
g_Config.DoNotSaveSetting(&g_Config.iExceptionActionMemRead);
|
|
}
|
|
|
|
if (memWriteAction.has_value()) {
|
|
g_Config.iExceptionActionMemWrite = memWriteAction.value();
|
|
g_Config.DoNotSaveSetting(&g_Config.iExceptionActionMemWrite);
|
|
}
|
|
|
|
if (breakAction.has_value()) {
|
|
g_Config.iExceptionActionBreak = breakAction.value();
|
|
g_Config.DoNotSaveSetting(&g_Config.iExceptionActionBreak);
|
|
}
|
|
|
|
if (logNativeCrashes.has_value()) {
|
|
g_Config.bLogNativeCrashStackTraces = logNativeCrashes.value();
|
|
g_Config.DoNotSaveSetting(&g_Config.bLogNativeCrashStackTraces);
|
|
}
|
|
|
|
// --vsh is applied by the caller (by setting their boot file name variable).
|
|
|
|
// Note: dpi is not applied here - it's platform-specific.
|
|
// Platforms should check cmdLineOptions.dpi.has_value() and handle accordingly.
|
|
}
|