From 4df01fd990de1ffc3af9d415dc6b04c14514f8b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 12 Sep 2026 09:58:26 -0600 Subject: [PATCH] sceUtility: load the firmware's module for a disabled HLE library Doing this was written once, for sceMp4, remembering the two UIDs it had loaded in a static. That static was the only thing stopping a second copy being loaded on top of the first, and it didn't survive a savestate: resuming in a fresh process left it at zero while the restored module list already had the modules in it. Ask the module list instead, which needs no state of its own and is right after a savestate load and across games. With the mechanism shared, sceMp3 and sceAtrac get it too. libmp3.prx and libatrac3plus.prx each import nothing but the kernel and sceAudiocodec, so both run against what we already have. The sceAtrac checkbox previously led to an error log and a debug assert saying it wouldn't work, with a note that we could go and find the file - which is what this does. This is only ever the path for a game that ships no copy of the library. One that does loads it directly and never asks sceUtility for it. Also: - a module we're really loading no longer reserves its stand-in block as well. That block only stands in for memory we aren't otherwise taking, so reserving both charges the game twice, at the top of user memory where thread stacks come from. - the module loads at the bottom of the user partition. fromTop is for a module injected before the game's executable is placed; by the time a game calls sceUtility nothing moves either way, and the firmware's utility.prx allocates AV module memory as PSP_SMEM_Low. - NotifyLoadStatusAtrac read the raw setting rather than the effective flags, so it could fire for a flag that wasn't in effect. - the missing-firmware case now says so on screen, naming the library and pointing at installing a firmware, rather than only logging. Co-Authored-By: Claude Opus 5 (1M context) --- Core/HLE/HLE.cpp | 12 ++- Core/HLE/sceKernelModule.cpp | 15 ++++ Core/HLE/sceKernelModule.h | 4 + Core/HLE/sceUtility.cpp | 160 ++++++++++++++++++++++++----------- 4 files changed, 140 insertions(+), 51 deletions(-) diff --git a/Core/HLE/HLE.cpp b/Core/HLE/HLE.cpp index 4633f3ddcd..cdc7d40ed8 100644 --- a/Core/HLE/HLE.cpp +++ b/Core/HLE/HLE.cpp @@ -337,7 +337,17 @@ static void hleDelayResultFinish(u64 userdata, int cycleslate) { WARN_LOG(Log::HLE, "Someone else woke up HLE-blocked thread %d?", threadID); } -// Which firmware files a flag needs before it can be honoured. +// Which files need to be present for a disable-hle-flag to be honoured. +// +// Two shapes end up here. sceMp4 because libmp4.prx and mp4msv.prx are firmware libraries no game +// ships, so without a dump there is nothing to run at all. sceFont because the module is on the +// disc like any other but reads its fonts from flash0:/font with nothing to fall back on. Either +// way the HLE is the only thing that can serve, so the flag comes off. +// +// sceMpeg, sceMp3 and sceAtrac are deliberately not here: plenty of discs carry their own copy +// (Death Jr. has MPEG.PRX and LIBATRAC3PLUS.PRX under PSP_GAME/USRDIR/MODULES), and dropping the +// flag for want of a dump would replace a perfectly good disc module with our HLE. Those check for +// a real module at the point they would load one, and warn there if neither source has it. static void CheckDisableHLEAvailability() { g_unavailableDisableFlags = (DisableHLEFlags)0; diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index 641d150f05..35d4a9fe2f 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -2026,6 +2026,21 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load return module; } +bool KernelModuleIsLoaded(std::string_view name) { + u32 error; + for (SceUID moduleId : loadedModules) { + PSPModule *module = kernelObjects.Get(moduleId, error); + // A fake module is our own HLE stand-in, which isn't the real library being asked about. + if (!module || module->isFake) { + continue; + } + if (equals(name, module->nm.name)) { + return true; + } + } + return false; +} + SceUID KernelLoadModule(const std::string &filename, std::string *error_string, bool fromTop) { std::vector buffer; if (pspFileSystem.ReadEntireFile(filename, buffer) < 0) diff --git a/Core/HLE/sceKernelModule.h b/Core/HLE/sceKernelModule.h index c1f0c5d359..0ba6fc39a7 100644 --- a/Core/HLE/sceKernelModule.h +++ b/Core/HLE/sceKernelModule.h @@ -252,6 +252,10 @@ void __KernelReturnFromModuleFunc(); // up, which shifts every address in it and invalidates cheats and achievements, and outright // fails for a game like Tekken 6 whose EBOOT must load at a fixed low address. SceUID KernelLoadModule(const std::string &filename, std::string *error_string, bool fromTop = false); + +// 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); int __KernelStartModule(SceUID moduleId, u32 argsize, u32 argAddr, u32 returnValueAddr, SceKernelSMOption *smoption, bool *needsWait); u32 __KernelStopUnloadSelfModuleWithOrWithoutStatus(u32 exitCode, u32 argSize, u32 argp, u32 statusAddr, u32 optionAddr, bool WithStatus); u32 sceKernelFindModuleByUID(u32 uid); diff --git a/Core/HLE/sceUtility.cpp b/Core/HLE/sceUtility.cpp index 2eb935507d..3f16d40a3d 100644 --- a/Core/HLE/sceUtility.cpp +++ b/Core/HLE/sceUtility.cpp @@ -25,6 +25,10 @@ #include "Common/Serialize/SerializeMap.h" #include "Common/Serialize/SerializeSet.h" #include "Common/File/VFS/VFS.h" +#include "Common/Data/Text/I18n.h" +#include "Common/StringUtils.h" +#include "Common/System/OSD.h" +#include "Core/FileSystems/MetaFileSystem.h" #include "Core/Config.h" #include "Core/CoreTiming.h" #include "Core/HLE/HLE.h" @@ -81,62 +85,105 @@ static const int atrac3PlusModuleDeps[] = {0x0300, 0}; static const int mpegBaseModuleDeps[] = {0x0300, 0}; static const int mp4ModuleDeps[] = {0x0300, 0}; +// Loading the firmware's module for a library we've been told not to HLE. +// +// This is the path for a game that brings no copy of its own. One that does loads it directly and +// never comes here: a third of the checked games ship an AV library, and of the twelve seen doing both, +// not one asked sceUtility for a library it had already loaded - only for the ones it hadn't +// brought. Toca Race Driver ships LIBMP3.PRX and asks for 0x300 to 0x303. +// +// So the module-list check below is a guard rather than the normal path, and it costs nothing: +// asking the list rather than remembering what we loaded means this needs no state of its own. It +// is right after a savestate load, across games, and if a game unloads a library and asks again. +struct FirmwareModule { + const char *path; // in the firmware + const char *moduleName; // what the module calls itself once loaded +}; + +static void LoadFirmwareModules(const char *library, 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", + modules[i].moduleName, modules[i].path); + 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); + 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. + char osdId[64]; + snprintf(osdId, sizeof(osdId), "hle_no_module_%s", library); + 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; + } + 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; + } + 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; + } + INFO_LOG(Log::sceUtility, "Loaded the real %s", modules[i].path); + } +} + +// libmp3.prx imports nothing but the kernel and sceAudiocodec, which we have. +static void NotifyLoadStatusMp3(int state, u32 loadAddr, u32 totalSize) { + // The effective flags, not the raw setting: those also account for the compat flags, for a + // firmware dump that isn't there, and for the boundary a savestate restored - resolving + // imports one way and loading modules the other is how a game ends up with neither. + if (state != 1 || !(GetEffectiveDisableHLEFlags() & DisableHLEFlags::sceMp3)) { + return; + } + static const FirmwareModule modules[] = { + { "flash0:/kd/libmp3.prx", "sceMp3_Library" }, + }; + LoadFirmwareModules("sceMp3", modules, ARRAY_SIZE(modules)); +} + static void NotifyLoadStatusAvcodec(int state, u32 loadAddr, u32 totalSize) { JpegNotifyLoadStatus(state); } // The MP4 libraries are a good candidate for running the real thing: libmp4.prx needs only two // functions from sceAudiocodec (Init and Decode) plus ordinary kernel calls, and mp4msv.prx - the -// 41 functions libmp4 leans on - imports nothing at all. So with a firmware dump present, the pair -// can be loaded for real and left to decode through our sceAudiocodec HLE. -static SceUID g_mp4RealModules[2] = { 0, 0 }; - +// 41 functions libmp4 leans on - imports nothing at all. static void NotifyLoadStatusMp4(int state, u32 loadAddr, u32 totalSize) { - // The effective flags, not the raw setting: those also account for a firmware dump that isn't - // there or is too old to have sceMp4 (which is the whole point of CheckDisableHLEAvailability), - // for the compat flags. - if (!(GetEffectiveDisableHLEFlags() & DisableHLEFlags::sceMp4)) { + if (state != 1 || !(GetEffectiveDisableHLEFlags() & DisableHLEFlags::sceMp4)) { return; } - - if (state == 1) { - // mp4msv first - libmp4 imports from it, and an import can only resolve to a module that - // is already loaded. - static const char *const paths[2] = { - "flash0:/kd/mp4msv.prx", - "flash0:/kd/libmp4.prx", - }; - for (int i = 0; i < 2; i++) { - if (g_mp4RealModules[i]) { - continue; - } - std::string error; - SceUID id = KernelLoadModule(paths[i], &error, true); - if (id < 0) { - ERROR_LOG(Log::sceUtility, "sceMp4 HLE is disabled, but %s wouldn't load (%s) - " - "the game will get unresolved imports", paths[i], error.c_str()); - return; - } - int result = __KernelStartModule(id, 0, 0, 0, nullptr, nullptr); - if (result < 0) { - ERROR_LOG(Log::sceUtility, "Failed to start %s (%08x)", paths[i], result); - return; - } - g_mp4RealModules[i] = id; - INFO_LOG(Log::sceUtility, "Loaded the real %s", paths[i]); - } - } + // mp4msv first - libmp4 imports from it, and an import can only resolve to a module that is + // already loaded. + static const FirmwareModule modules[] = { + { "flash0:/kd/mp4msv.prx", "mp4msv_module" }, + { "flash0:/kd/libmp4.prx", "sceMp4_library" }, + }; + LoadFirmwareModules("sceMp4", modules, ARRAY_SIZE(modules)); } static void NotifyLoadStatusAtrac(int state, u32 loadAddr, u32 totalSize) { if (state == 1) { - // If HLE of sceAtrac is disabled, things will break! - // For now we do angry logging and a debug assert. - if ((DisableHLEFlags)g_Config.iDisableHLE & DisableHLEFlags::sceAtrac) { - ERROR_LOG(Log::ME, "sceAtrac HLE is disabled, and the game tries to load sceAtrac from firmware - this won't work!"); - _dbg_assert_(false); - - // Actually, if the user has an F0 (psardumper) dump, we could go look for the file there. + // The effective flags, for the same reason the loads above use them. + if (GetEffectiveDisableHLEFlags() & DisableHLEFlags::sceAtrac) { + // libatrac3plus.prx imports only Kernel_Library and sceAudiocodec, both of which we + // have, so the real module runs against our HLE the same way libmp3.prx does. Nothing + // below applies once it does: the atrac contexts then live in that module's own bss, + // not in the block we hand out here, and the game's calls go to it rather than to us. + static const FirmwareModule modules[] = { + { "flash0:/kd/libatrac3plus.prx", "sceATRAC3plus_Library" }, + }; + LoadFirmwareModules("sceAtrac", modules, ARRAY_SIZE(modules)); + return; } // We try to imitate a recent version of the prx. @@ -146,11 +193,22 @@ static void NotifyLoadStatusAtrac(int state, u32 loadAddr, u32 totalSize) { _dbg_assert_(bssSize <= totalSize); __AtracNotifyLoadModule(version, 0, loadAddr, bssSize); } else if (state == -1) { - // Unload. + // Unload. Harmless when the firmware's module took over - there was no load to undo. __AtracNotifyUnloadModule(); } } +// Which library each AV utility module provides, for the ones a real module can take over when +// HLE is disabled for it. See LoadModuleInternal. +static DisableHLEFlags UtilityModuleLibraryFlag(u32 module) { + switch (module) { + case 0x302: return DisableHLEFlags::sceAtrac; // av_atrac3plus + case 0x304: return DisableHLEFlags::sceMp3; // av_mp3 + case 0x308: return DisableHLEFlags::sceMp4; // av_mp4 + default: return (DisableHLEFlags)0; + } +} + ModuleLoadInfo::ModuleLoadInfo(int m, u32 s, const char *_name, ModuleLoadCallback n) : name(_name), mod(m), size(s), dependencies(noDeps), notify(n) {} ModuleLoadInfo::ModuleLoadInfo(int m, u32 s, const char *_name, const int *d, ModuleLoadCallback n) @@ -178,7 +236,7 @@ static const ModuleLoadInfo moduleLoadInfo[] = { // Changing this breaks some bad cheats though.. ModuleLoadInfo(0x302, 0x00008000, "av_atrac3plus", atrac3PlusModuleDeps, &NotifyLoadStatusAtrac), ModuleLoadInfo(0x303, 0x0000c000, "av_mpegbase", mpegBaseModuleDeps), - ModuleLoadInfo(0x304, 0x00004000, "av_mp3"), + ModuleLoadInfo(0x304, 0x00004000, "av_mp3", &NotifyLoadStatusMp3), ModuleLoadInfo(0x305, 0x0000a300, "av_vaudio"), ModuleLoadInfo(0x306, 0x00004000, "av_aac"), ModuleLoadInfo(0x307, 0x00000000, "av_g729"), @@ -364,9 +422,6 @@ void __UtilityInit() { DeactivateDialog(); SavedataParam::Init(); currentlyLoadedModules.clear(); - // Vital to reset these between games, otherwise we might think they're already loaded. - g_mp4RealModules[0] = 0; - g_mp4RealModules[1] = 0; volatileUnlockEvent = CoreTiming::RegisterEvent("UtilityVolatileUnlock", UtilityVolatileUnlock); ResetSecondsSinceLastGameSave(); @@ -719,10 +774,15 @@ static int LoadModuleInternal(u32 module, bool av) { } u32 allocSize = info->size; + const DisableHLEFlags libraryFlag = UtilityModuleLibraryFlag(module); + if (libraryFlag != (DisableHLEFlags)0 && (GetEffectiveDisableHLEFlags() & libraryFlag)) { + allocSize = 0; + } + u32 address = 0; - char name[128]; - snprintf(name, sizeof(name), "UtilityModule/%3x_%s", module, info->name); if (allocSize != 0) { + char name[128]; + snprintf(name, sizeof(name), "UtilityModule/%3x_%s", module, info->name); address = userMemory.Alloc(allocSize, false, name); } currentlyLoadedModules[module] = address;