From 9d79b0d69381f7598d2fa2a7ea135f19f0a9ed08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 3 Nov 2026 01:36:47 +0100 Subject: [PATCH] HLE: Fall back to our HLE when a game asks for a firmware module we haven't got sceMpeg runs the real mpeg.prx by default since #22309, and boot only puts the HLE back when neither the firmware nor the disc has one. MX vs. ATV Untamed carries an mpeg.prx it never loads, so without a firmware it got the real sceMpeg, then asked sceUtility for the firmware's module, which wasn't there. Every sceMpeg import stayed a missing stub, and the game crashed at boot writing through what the calls returned. Now that load turns the library over to our HLE and resolves the waiting imports against it. The latched HLE flags change with it, so a savestate made afterwards restores the HLE too. Atrac goes on to set up its HLE memory the same way. For a library the user disabled themselves the warning still shows; for one that's real by default, like the boot-time check, it's only logged. MX now plays its intro video without a firmware, and still loads the real module with one. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/HLE.cpp | 14 +++++++++++++ Core/HLE/HLE.h | 3 +++ Core/HLE/sceKernelModule.cpp | 15 ++++++++++++++ Core/HLE/sceKernelModule.h | 2 ++ Core/HLE/sceUtility.cpp | 38 ++++++++++++++++++++++++------------ 5 files changed, 59 insertions(+), 13 deletions(-) diff --git a/Core/HLE/HLE.cpp b/Core/HLE/HLE.cpp index 84ceab9e57..7c4d31e3ab 100644 --- a/Core/HLE/HLE.cpp +++ b/Core/HLE/HLE.cpp @@ -286,6 +286,20 @@ DisableHLEFlags GetEffectiveDisableHLEFlags() { return GetDisableHLEFlags(); } +bool HLEFallBackToHLE(DisableHLEFlags flag) { + if (!(GetDisableHLEFlags() & flag)) { + return false; + } + // The latched flags are what a savestate records, so a state made after this restores the HLE too. + g_effectiveDisableHLE &= ~flag; + for (size_t i = 0; i < ARRAY_SIZE(g_moduleMeta); i++) { + if (g_moduleMeta[i].disableFlag == flag && g_moduleMeta[i].importName) { + KernelReimportLibrary(g_moduleMeta[i].importName); + } + } + return true; +} + // Note: name is the modname from prx, not the export module name! // See SetForceRealModuleLoads. static bool g_forceRealModuleLoads = false; diff --git a/Core/HLE/HLE.h b/Core/HLE/HLE.h index 75cd9caf8c..36a60cf9a7 100644 --- a/Core/HLE/HLE.h +++ b/Core/HLE/HLE.h @@ -136,6 +136,9 @@ DisableHLEFlags AlwaysDisableHLEFlags(); // The flags actually in effect for this boot - latched at the first module load and restored from // savestates, so it can differ from what g_Config says if the setting changed since. DisableHLEFlags GetEffectiveDisableHLEFlags(); +// For when the real module for a library turns out not to be available after all: runs the library on +// our HLE from now on, and resolves the imports already waiting for it. False if it already was. +bool HLEFallBackToHLE(DisableHLEFlags flag); // Run the current thread's callbacks after the syscall finishes. void hleCheckCurrentCallbacks(); diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index 6b21faed0d..7768ce3965 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -2317,6 +2317,21 @@ bool KernelUnloadModuleByID(SceUID moduleId) { return true; } +void KernelReimportLibrary(std::string_view importName) { + u32 error; + for (SceUID moduleId : loadedModules) { + PSPModule *module = kernelObjects.Get(moduleId, error); + if (!module) { + continue; + } + for (const FuncSymbolImport &func : module->importedFuncs) { + if (equals(importName, func.moduleName)) { + ImportFuncSymbol(func, true, module->nm.name); + } + } + } +} + bool KernelModuleIsLoaded(std::string_view name) { u32 error; for (SceUID moduleId : loadedModules) { diff --git a/Core/HLE/sceKernelModule.h b/Core/HLE/sceKernelModule.h index 32541b885c..1c2d30c04f 100644 --- a/Core/HLE/sceKernelModule.h +++ b/Core/HLE/sceKernelModule.h @@ -276,6 +276,8 @@ SceUID KernelLoadModule(const std::string &filename, std::string *error_string, // Whether a real (non-HLE-stub) module calling itself this is loaded. Lets a caller tell whether a // library is already provided before bringing in another copy of it. bool KernelModuleIsLoaded(std::string_view name); +// Resolves every loaded module's imports from importName again, for when the side it's on has changed. +void KernelReimportLibrary(std::string_view importName); // Take back out a module we brought in ourselves (the firmware swap in sceUtility). Frees the // memory block, which is the point: a game that unloads its video libraries before a level load diff --git a/Core/HLE/sceUtility.cpp b/Core/HLE/sceUtility.cpp index 9a080eaf64..b5d4786a52 100644 --- a/Core/HLE/sceUtility.cpp +++ b/Core/HLE/sceUtility.cpp @@ -106,7 +106,8 @@ struct FirmwareModule { const char *moduleName; // what the module calls itself once loaded }; -static void LoadFirmwareModules(int utilityModule, const char *library, const FirmwareModule *modules, size_t count) { +// Returns false if the library ended up on our HLE after all, for want of the firmware's module. +static bool LoadFirmwareModules(int utilityModule, const char *library, DisableHLEFlags flag, const FirmwareModule *modules, size_t count) { for (size_t i = 0; i < count; i++) { if (KernelModuleIsLoaded(modules[i].moduleName)) { DEBUG_LOG(Log::sceUtility, "%s is already loaded - not loading %s on top of it", @@ -114,10 +115,18 @@ static void LoadFirmwareModules(int utilityModule, const char *library, const Fi continue; } if (!pspFileSystem.GetFileInfo(modules[i].path).exists) { - // Nothing to fall back to: the game's imports were resolved against the real module - // when it loaded, so putting our HLE back now is not an option. - ERROR_LOG(Log::sceUtility, "%s HLE is disabled, but %s isn't in the firmware and the " - "game didn't bring its own - it will get unresolved imports", library, modules[i].path); + // The game's imports were left waiting for the real module when it loaded, so they can + // still go to our HLE instead. Without that, every call into the library is a missing + // stub: MX vs. ATV Untamed, which carries an mpeg.prx it never loads (so boot picked the + // real sceMpeg), asks for this one and crashed when the calls returned nothing. + HLEFallBackToHLE(flag); + INFO_LOG(Log::sceUtility, "%s isn't in the firmware and the game didn't bring its own - using the HLE %s", + modules[i].path, library); + if (AlwaysDisableHLEFlags() & flag) { + // On by default, so not something the user chose: like the boot-time check, nothing on + // screen for landing on the HLE. + return false; + } auto sy = GetI18NCategory(I18NCat::SYSTEM); // Keyed per library, so a game that asks for the module again replaces the message // rather than stacking another copy of it, and two missing libraries still both show. @@ -126,22 +135,23 @@ static void LoadFirmwareModules(int utilityModule, const char *library, const Fi g_OSD.Show(OSDType::MESSAGE_WARNING, ApplySafeSubstitutions( sy->T("%1 needs a firmware installed to run without HLE. Install one, or re-enable HLE for it."), library), 6.0f, osdId); - return; + return false; } std::string error; SceUID id = KernelLoadModule(modules[i].path, &error, false); if (id < 0) { ERROR_LOG(Log::sceUtility, "Couldn't load %s: %s", modules[i].path, error.c_str()); - return; + return true; } const int result = __KernelStartModule(id, 0, 0, 0, nullptr, nullptr); if (result < 0) { ERROR_LOG(Log::sceUtility, "Failed to start %s (%08x)", modules[i].path, result); - return; + return true; } swappedFirmwareModules[utilityModule].push_back(id); INFO_LOG(Log::sceUtility, "Loaded the real %s", modules[i].path); } + return true; } // The other half: give the memory back when the game says it is done with the library. Reverse @@ -175,7 +185,7 @@ static void NotifyLoadStatusMpegBase(int state, u32 loadAddr, u32 totalSize) { static const FirmwareModule modules[] = { { "flash0:/kd/mpeg.prx", "sceMpeg_library" }, }; - LoadFirmwareModules(0x303, "sceMpeg", modules, ARRAY_SIZE(modules)); + LoadFirmwareModules(0x303, "sceMpeg", DisableHLEFlags::sceMpeg, modules, ARRAY_SIZE(modules)); } // libmp3.prx imports nothing but the kernel and sceAudiocodec, which we have. @@ -190,7 +200,7 @@ static void NotifyLoadStatusMp3(int state, u32 loadAddr, u32 totalSize) { static const FirmwareModule modules[] = { { "flash0:/kd/libmp3.prx", "sceMp3_Library" }, }; - LoadFirmwareModules(0x304, "sceMp3", modules, ARRAY_SIZE(modules)); + LoadFirmwareModules(0x304, "sceMp3", DisableHLEFlags::sceMp3, modules, ARRAY_SIZE(modules)); } static void NotifyLoadStatusAvcodec(int state, u32 loadAddr, u32 totalSize) { @@ -228,7 +238,7 @@ static void NotifyLoadStatusMp4(int state, u32 loadAddr, u32 totalSize) { { "flash0:/kd/mp4msv.prx", "mp4msv_module" }, { "flash0:/kd/libmp4.prx", "sceMp4_library" }, }; - LoadFirmwareModules(0x308, "sceMp4", modules, ARRAY_SIZE(modules)); + LoadFirmwareModules(0x308, "sceMp4", DisableHLEFlags::sceMp4, modules, ARRAY_SIZE(modules)); } static void NotifyLoadStatusAtrac(int state, u32 loadAddr, u32 totalSize) { @@ -248,8 +258,10 @@ static void NotifyLoadStatusAtrac(int state, u32 loadAddr, u32 totalSize) { static const FirmwareModule modules[] = { { "flash0:/kd/libatrac3plus.prx", "sceATRAC3plus_Library" }, }; - LoadFirmwareModules(0x302, "sceAtrac", modules, ARRAY_SIZE(modules)); - return; + if (LoadFirmwareModules(0x302, "sceAtrac", DisableHLEFlags::sceAtrac, modules, ARRAY_SIZE(modules))) { + return; + } + // Back on our HLE, which needs its memory like any other load. } // We try to imitate a recent version of the prx.