--- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -10,6 +10,9 @@ #include #include #include +#include +#include +#include #if PPSSPP_PLATFORM(ANDROID) #include #endif @@ -42,9 +45,12 @@ #include "Core/CoreTiming.h" #include "Core/System.h" #include "Core/WebServer.h" +#include "Core/HLE/sceCtrl.h" #include "Core/HLE/sceUtility.h" +#include "Core/Screenshot.h" #include "Core/SaveState.h" #include "GPU/GPUCommon.h" +#include "GPU/Common/GPUDebugInterface.h" #include "GPU/Common/FramebufferManagerCommon.h" #include "Common/Log.h" #include "Common/Log/LogManager.h" @@ -152,6 +158,14 @@ fprintf(stderr, " -j use jit (default)\n"); fprintf(stderr, " -c, --compare compare with output in file.expected\n"); fprintf(stderr, " --bench run multiple times and output speed\n"); + fprintf(stderr, " --strict-mem stop at an unaligned or invalid memory access, as a PSP\n"); + fprintf(stderr, " does, and say where (use with --jit-ir or --ir)\n"); + fprintf(stderr, " --snapshot=FILE save the screen as a PNG when the run ends\n"); + fprintf(stderr, " --dump-audio record what the program plays to the memory stick's\n"); + fprintf(stderr, " PSP/AUDIO folder as a WAV\n"); + fprintf(stderr, " --input=FILE play an input script: lines of MS buttons NAME...|none,\n"); + fprintf(stderr, " MS analog X Y (-1..1) or MS snapshot FILE, MS being\n"); + fprintf(stderr, " emulated milliseconds since boot\n"); fprintf(stderr, "\nSee headless.txt for details.\n"); return 1; @@ -171,14 +185,122 @@ } } +// One step of an --input script, from atUs (emulated time since boot) on. +enum class InputKind { + BUTTONS, + ANALOG, + SNAPSHOT, +}; + +struct InputStep { + u64 atUs; + InputKind kind; + u32 buttons; + float x; + float y; + std::string file; +}; + struct AutoTestOptions { double timeout; double maxScreenshotError; bool compare : 1; bool verbose : 1; bool bench : 1; + bool strictMem : 1; + const char *snapshot; + const std::vector *input; }; +// The PSP's screen as it is now, as a PNG (--snapshot). +static void SaveSnapshot(const char *filename) { + GPUDebugBuffer buffer; + u8 *converted = nullptr; + u32 w = (u32)-1; + u32 h = (u32)-1; + if (!gpuDebug || !gpuDebug->GetCurrentFramebuffer(buffer, GPU_DBG_FRAMEBUF_DISPLAY)) { + fprintf(stderr, "Snapshot: no display framebuffer\n"); + return; + } + const u8 *rgb = ConvertBufferToScreenshot(buffer, false, converted, w, h); + if (!rgb || !Save888RGBScreenshot(Path(std::string(filename)), ScreenshotFormat::PNG, rgb, w, h)) + fprintf(stderr, "Snapshot: could not write %s\n", filename); + delete [] converted; +} + +// Reads an --input script into steps, in the order given (times must not go +// back). False, saying why, for a line it cannot read. +static bool LoadInputScript(const char *filename, std::vector *steps) { + static const struct { + const char *name; + u32 bit; + } buttonNames[] = { + { "select", CTRL_SELECT }, { "start", CTRL_START }, { "up", CTRL_UP }, { "right", CTRL_RIGHT }, + { "down", CTRL_DOWN }, { "left", CTRL_LEFT }, { "ltrigger", CTRL_LTRIGGER }, { "rtrigger", CTRL_RTRIGGER }, + { "triangle", CTRL_TRIANGLE }, { "circle", CTRL_CIRCLE }, { "cross", CTRL_CROSS }, { "square", CTRL_SQUARE }, + }; + FILE *fp = File::OpenCFile(Path(std::string(filename)), "rt"); + if (!fp) { + fprintf(stderr, "Input script: cannot open %s\n", filename); + return false; + } + char line[1024]; + int lineNumber = 0; + bool ok = true; + while (ok && fgets(line, sizeof(line), fp)) { + lineNumber++; + std::istringstream words(line); + double ms = 0.0; + std::string kind; + if (line[0] == '#' || !(words >> ms >> kind)) + continue; + InputStep step{}; + step.atUs = (u64)(ms * 1000.0); + if (kind == "buttons") { + step.kind = InputKind::BUTTONS; + std::string name; + while (ok && words >> name) { + bool known = name == "none"; + for (const auto &b : buttonNames) { + if (name == b.name) { + step.buttons |= b.bit; + known = true; + } + } + ok = known; + } + } else if (kind == "analog") { + step.kind = InputKind::ANALOG; + ok = (bool)(words >> step.x >> step.y); + } else if (kind == "snapshot") { + step.kind = InputKind::SNAPSHOT; + ok = (bool)(words >> step.file); + } else { + ok = false; + } + if (ok) + steps->push_back(step); + else + fprintf(stderr, "Input script: cannot read line %d of %s\n", lineNumber, filename); + } + fclose(fp); + return ok; +} + +static void RunInputStep(const InputStep &step) { + switch (step.kind) { + case InputKind::BUTTONS: + __CtrlUpdateButtons(step.buttons, CTRL_MASK_USER & ~step.buttons); + break; + case InputKind::ANALOG: + __CtrlSetAnalogXY(CTRL_STICK_LEFT, step.x, step.y); + break; + case InputKind::SNAPSHOT: + SaveSnapshot(step.file.c_str()); + break; + } +} + bool RunAutoTest(HeadlessHost *headlessHost, CoreParameter &coreParameter, const AutoTestOptions &opt) { // Kinda ugly, trying to guesstimate the test name from filename... currentTestName = GetTestName(coreParameter.fileToStart); @@ -228,11 +350,14 @@ bool passed = true; double deadline = time_now_d() + opt.timeout; + size_t nextInput = 0; coreState = coreParameter.startBreak ? CORE_STEPPING_CPU : CORE_RUNNING_CPU; while (coreState == CORE_RUNNING_CPU || coreState == CORE_STEPPING_CPU) { int blockTicks = (int)usToCycles(1000000 / 10); PSP_RunLoopFor(blockTicks); + while (opt.input && nextInput < opt.input->size() && (*opt.input)[nextInput].atUs <= CoreTiming::GetGlobalTimeUs()) + RunInputStep((*opt.input)[nextInput++]); // If we were rendering, this might be a nice time to do something about it. if (coreState == CORE_NEXTFRAME) { @@ -261,6 +386,13 @@ Core_Stop(); } } + if (opt.strictMem && Core_GetExceptionInfo().type == MIPSExceptionType::MEMORY) { + const MIPSExceptionInfo &e = Core_GetExceptionInfo(); + printf("STRICT-MEM: %s exception at %08x (size %u), pc %08x\n%s\n", MemoryExceptionTypeAsString(e.memory_type), e.address, e.accessSize, e.pc, e.stackTrace.c_str()); + passed = false; + } + if (opt.snapshot) + SaveSnapshot(opt.snapshot); if (gpu) { gpu->EndHostFrame(); } @@ -370,6 +502,8 @@ int debuggerPort = -1; bool oldAtrac = false; bool outputDebugStringLog = false; + bool dumpAudio = false; + std::vector inputSteps; std::vector testFilenames; std::vector ignoredTests; @@ -411,6 +545,17 @@ testOptions.verbose = true; else if (!strcmp(argv[i], "--old-atrac")) oldAtrac = true; + else if (!strcmp(argv[i], "--strict-mem")) + testOptions.strictMem = true; + else if (!strcmp(argv[i], "--dump-audio")) + dumpAudio = true; + else if (!strncmp(argv[i], "--snapshot=", strlen("--snapshot=")) && strlen(argv[i]) > strlen("--snapshot=")) + testOptions.snapshot = argv[i] + strlen("--snapshot="); + else if (!strncmp(argv[i], "--input=", strlen("--input=")) && strlen(argv[i]) > strlen("--input=")) { + if (!LoadInputScript(argv[i] + strlen("--input="), &inputSteps)) + return printUsage(argv[0], "Unreadable --input script"); + testOptions.input = &inputSteps; + } else if (!strncmp(argv[i], "--graphics=", strlen("--graphics=")) && strlen(argv[i]) > strlen("--graphics=")) { const char *gpuName = argv[i] + strlen("--graphics="); @@ -558,6 +703,16 @@ g_Config.internalDataDirectory.clear(); g_Config.bUseOldAtrac = oldAtrac; g_Config.iForceEnableHLE = 0xFFFFFFFF; // Run all modules as HLE. We don't have anything to load in this context. + if (testOptions.strictMem) { + // Every access checked, and a bad one stops the run, as on a PSP. + g_Config.bFastMemory = false; + g_Config.bIgnoreBadMemAccess = false; + } + if (dumpAudio) { + // The mix is only made, and so recorded, with sound on. + g_Config.bEnableSound = true; + g_Config.bDumpAudio = true; + } // g_Config.bUseOldAtrac = true;