236 lines
8 KiB
Diff
236 lines
8 KiB
Diff
--- a/headless/Headless.cpp
|
|
+++ b/headless/Headless.cpp
|
|
@@ -10,6 +10,9 @@
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <limits>
|
|
+#include <sstream>
|
|
+#include <string>
|
|
+#include <vector>
|
|
#if PPSSPP_PLATFORM(ANDROID)
|
|
#include <jni.h>
|
|
#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<InputStep> *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<InputStep> *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<InputStep> inputSteps;
|
|
|
|
std::vector<std::string> testFilenames;
|
|
std::vector<std::string> 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;
|
|
|