From 0b2c6812e1bfa4bd8b88c3fd4bd63d6215e470ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 19 Sep 2026 10:32:56 -0600 Subject: [PATCH] Headless: refuse a run that can't honour an explicit --disable-hle When the firmware module a --disable-hle bit asks for is neither installed nor on the disc, that library silently runs our HLE instead. For the emulator that is the right thing; for a test tool it means the run measures something other than what was asked for and says so only as one INFO line, which is easy to grep past and easy to never see. It cost a round of wrong results here, where the memory stick headless defaults to (beside the exe, not the app's) had no firmware, so a comparison against the real mpeg.prx was quietly a comparison against the HLE it was supposed to be measured against. g_unavailableDisableFlags already records exactly which flags fell back, so it just needed an accessor. Headless now names each one, prints the flash0:/kd and memory stick it looked in, and fails the run. Only an explicit --disable-hle binds. sceMpeg and sceMp4 are LLE by default, and falling back is the correct and expected behaviour wherever no firmware is installed - making that fatal would fail every run on such a machine. Co-Authored-By: Claude Opus 5 (1M context) --- Core/HLE/HLE.cpp | 4 ++++ Core/HLE/HLE.h | 4 ++++ docs/debugging.md | 7 +++++++ headless/Headless.cpp | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+) diff --git a/Core/HLE/HLE.cpp b/Core/HLE/HLE.cpp index 7b23506309..19a18a96d4 100644 --- a/Core/HLE/HLE.cpp +++ b/Core/HLE/HLE.cpp @@ -446,6 +446,10 @@ void HLECheckModuleAvailability() { } } +DisableHLEFlags HLEGetUnavailableDisableFlags() { + return g_unavailableDisableFlags; +} + void HLEInit() { RegisterAllModules(); // Latched lazily rather than here: the compat flags this depends on aren't loaded yet. diff --git a/Core/HLE/HLE.h b/Core/HLE/HLE.h index 09933620c2..fb65bbffd0 100644 --- a/Core/HLE/HLE.h +++ b/Core/HLE/HLE.h @@ -188,6 +188,10 @@ inline s64 hleDelayResult(s64 result, const char *reason, int usec) { void HLEInit(); void HLECheckModuleAvailability(); +// The flags that were asked for but could not be honoured, because the firmware module they need +// is neither installed nor on the disc - those libraries ran our HLE instead. HLECheckModuleAvailability() +// fills this in during boot, so it only means anything after that. +DisableHLEFlags HLEGetUnavailableDisableFlags(); void HLEDoState(PointerWrap &p); void HLEShutdown(); const HLEFunction *HLEGetFunctionBeingCalled(); diff --git a/docs/debugging.md b/docs/debugging.md index 0d286ded64..5593578ffc 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -171,6 +171,13 @@ produced a round of bogus results here: The last three compound: the fix is to treat the run's exit code and a positive "we got here" counter as preconditions, and only then believe the error counts. +For the silent-fallback half of this, headless refuses the run rather than substituting: an explicit +`--disable-hle=` whose firmware module isn't there names the module, prints the `flash0:/kd` and memory stick +it looked in (usually enough to spot that it's the one beside the exe), and exits 1. Only an *explicit* +`--disable-hle` binds - sceMpeg and sceMp4 are LLE by default and still fall back quietly, or every run on a +machine with no firmware would fail. So when a measurement depends on the real module, pass the flag +explicitly even though it's on by default, and the run will tell you if it didn't get it. + ## Debugging and breakpoint considerations It might be worth trying the interpreter - all types of breakpoints are the most reliable with this CPU backend. diff --git a/headless/Headless.cpp b/headless/Headless.cpp index 0bdd823435..cfc73f1095 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -55,6 +55,7 @@ #include "Core/Core.h" #include "Core/CoreTiming.h" #include "Core/EmuThread.h" +#include "Core/HLE/HLE.h" #include "Core/MIPS/MIPSTables.h" #include "Core/System.h" #include "Core/Util/PSARUnpack.h" @@ -83,6 +84,9 @@ static bool g_screenshotSaved = false; static double g_maxScreenshotError = 0.0; static bool g_screenshotFailed = false; static std::string g_debugOutputBuffer; +// Set when a run was asked for a configuration that couldn't be honoured. That isn't a test result, +// so it fails the process whether or not this run was comparing anything. +static bool g_configRefused = false; static bool g_writeFailureScreenshot = true; static bool g_writeDebugOutput = true; // Whether the emulated program's stdout/stderr are forwarded to ours. On by default - just running @@ -325,6 +329,10 @@ struct AutoTestOptions { bool verbose; bool bench; bool printEqualLines; + // What --disable-hle asked for, or 0 if it was not passed. Only an explicit request binds: + // sceMpeg and sceMp4 run the firmware module by default now and fall back to the HLE wherever + // none is installed, which must not fail every run on such a machine. + int requiredDisableHLE; }; static bool RunAutoTest(GraphicsContext *graphicsContext, CoreParameter &coreParameter, const AutoTestOptions &opt) { @@ -364,6 +372,29 @@ static bool RunAutoTest(GraphicsContext *graphicsContext, CoreParameter &corePar return false; } + // Running a different configuration than the one asked for measures the wrong thing without + // saying so, which is worse in a test tool than not running at all. + const int missingHLE = (int)HLEGetUnavailableDisableFlags() & opt.requiredDisableHLE; + if (missingHLE) { + for (int i = 0; i < (int)DisableHLEFlags::Count; i++) { + if (!(missingHLE & (1 << i))) { + continue; + } + const HLEModuleMeta *meta = GetHLEModuleMetaByFlag((DisableHLEFlags)(1 << i)); + fprintf(stderr, "--disable-hle asked for %s, but no firmware module for it is installed " + "or on the disc - our HLE would run instead.\n", meta ? meta->modname : "an unknown module"); + } + // Nearly always because headless defaulted the memory stick to one beside the exe rather than + // the app's, so the firmware installed through the app isn't the firmware it looked at. + fprintf(stderr, "Looked in %s (memory stick %s).\n", + (g_Config.nandRootDirectory / "flash0" / "kd").c_str(), g_Config.memStickDirectory.c_str()); + GitHubActionsPrint("error", "Requested --disable-hle unavailable for %s", currentTestName.c_str()); + g_configRefused = true; + // Booted, so it has to come down the same way a finished run does. + PSP_Shutdown(true); + return false; + } + System_Notify(SystemNotification::BOOT_DONE); PSP_UpdateDebugStats((DebugOverlay)g_Config.iDebugOverlay == DebugOverlay::DEBUG_STATS || g_Config.bLogFrameDrops); @@ -607,6 +638,10 @@ int RunTests(GraphicsContext *graphicsContext, CoreParameter &coreParameter, con } } + if (g_configRefused) { + return 1; + } + return 0; } @@ -668,6 +703,7 @@ int main(int argc, const char* argv[]) { testOptions.verbose = cmdLineOptions.verbose.value_or(false); testOptions.printEqualLines = cmdLineOptions.printEqualLines.value_or(false); testOptions.maxScreenshotError = cmdLineOptions.maxScreenshotError.value_or(0.0); + testOptions.requiredDisableHLE = cmdLineOptions.disableHLE.value_or(0); bool fullLog = cmdLineOptions.enableLogging.value_or(false); const char *stateToLoad = cmdLineOptions.stateToLoad.has_value() ? cmdLineOptions.stateToLoad.value().c_str() : nullptr;