diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index 60df845931..6af14c266c 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -2026,6 +2026,17 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load return module; } +bool KernelUnloadModuleByID(SceUID moduleId) { + u32 error; + PSPModule *module = kernelObjects.Get(moduleId, error); + if (!module) { + return false; + } + module->Cleanup(); + kernelObjects.Destroy(moduleId); + return true; +} + 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 0ba6fc39a7..3bb43a0413 100644 --- a/Core/HLE/sceKernelModule.h +++ b/Core/HLE/sceKernelModule.h @@ -256,6 +256,12 @@ 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); + +// 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 +// expects that space back, and one 33KB module left sitting in the middle of it is enough to +// break a contiguous allocation. Returns false if the id is not a live module. +bool KernelUnloadModuleByID(SceUID moduleId); 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 fcce5fb477..cdc6e359cf 100644 --- a/Core/HLE/sceUtility.cpp +++ b/Core/HLE/sceUtility.cpp @@ -95,12 +95,17 @@ static const int mp4ModuleDeps[] = {0x0300, 0}; // 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. +// The firmware modules we swapped in for a library whose HLE is disabled, keyed by the utility +// module whose load brought them in. Remembered because the unload has to take out what we put +// in and nothing else: a game that ships its own copy loads it itself, and we must not free that. +static std::map> swappedFirmwareModules; + 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) { +static void LoadFirmwareModules(int utilityModule, 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", @@ -133,13 +138,33 @@ static void LoadFirmwareModules(const char *library, const FirmwareModule *modul ERROR_LOG(Log::sceUtility, "Failed to start %s (%08x)", modules[i].path, result); return; } + swappedFirmwareModules[utilityModule].push_back(id); INFO_LOG(Log::sceUtility, "Loaded the real %s", modules[i].path); } } +// The other half: give the memory back when the game says it is done with the library. Reverse +// order, since a later module may import from an earlier one. +static void UnloadFirmwareModules(int utilityModule) { + auto it = swappedFirmwareModules.find(utilityModule); + if (it == swappedFirmwareModules.end()) { + return; + } + for (auto id = it->second.rbegin(); id != it->second.rend(); ++id) { + if (KernelUnloadModuleByID(*id)) { + INFO_LOG(Log::sceUtility, "Unloaded the real module %d we had swapped in", *id); + } + } + swappedFirmwareModules.erase(it); +} + // mpeg.prx needs sceVideocodec, sceMpegbase and sceAudiocodec from us, all of which we implement, // so the module itself is the only thing that has to come from somewhere real. static void NotifyLoadStatusMpegBase(int state, u32 loadAddr, u32 totalSize) { + if (state == -1) { + UnloadFirmwareModules(0x303); + return; + } // 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. @@ -149,18 +174,22 @@ static void NotifyLoadStatusMpegBase(int state, u32 loadAddr, u32 totalSize) { static const FirmwareModule modules[] = { { "flash0:/kd/mpeg.prx", "sceMpeg_library" }, }; - LoadFirmwareModules("sceMpeg", modules, ARRAY_SIZE(modules)); + LoadFirmwareModules(0x303, "sceMpeg", modules, ARRAY_SIZE(modules)); } // libmp3.prx imports nothing but the kernel and sceAudiocodec, which we have. static void NotifyLoadStatusMp3(int state, u32 loadAddr, u32 totalSize) { + if (state == -1) { + UnloadFirmwareModules(0x304); + return; + } if (state != 1 || !(GetEffectiveDisableHLEFlags() & DisableHLEFlags::sceMp3)) { return; } static const FirmwareModule modules[] = { { "flash0:/kd/libmp3.prx", "sceMp3_Library" }, }; - LoadFirmwareModules("sceMp3", modules, ARRAY_SIZE(modules)); + LoadFirmwareModules(0x304, "sceMp3", modules, ARRAY_SIZE(modules)); } static void NotifyLoadStatusAvcodec(int state, u32 loadAddr, u32 totalSize) { @@ -171,6 +200,10 @@ static void NotifyLoadStatusAvcodec(int state, u32 loadAddr, u32 totalSize) { // functions from sceAudiocodec (Init and Decode) plus ordinary kernel calls, and mp4msv.prx - the // 41 functions libmp4 leans on - imports nothing at all. static void NotifyLoadStatusMp4(int state, u32 loadAddr, u32 totalSize) { + if (state == -1) { + UnloadFirmwareModules(0x308); + return; + } if (state != 1) { return; } @@ -194,10 +227,14 @@ static void NotifyLoadStatusMp4(int state, u32 loadAddr, u32 totalSize) { { "flash0:/kd/mp4msv.prx", "mp4msv_module" }, { "flash0:/kd/libmp4.prx", "sceMp4_library" }, }; - LoadFirmwareModules("sceMp4", modules, ARRAY_SIZE(modules)); + LoadFirmwareModules(0x308, "sceMp4", modules, ARRAY_SIZE(modules)); } static void NotifyLoadStatusAtrac(int state, u32 loadAddr, u32 totalSize) { + if (state == -1) { + UnloadFirmwareModules(0x302); + return; + } if (state == 1) { // The effective flags, for the same reason the loads above use them. if (GetEffectiveDisableHLEFlags() & DisableHLEFlags::sceAtrac) { @@ -208,7 +245,7 @@ static void NotifyLoadStatusAtrac(int state, u32 loadAddr, u32 totalSize) { static const FirmwareModule modules[] = { { "flash0:/kd/libatrac3plus.prx", "sceATRAC3plus_Library" }, }; - LoadFirmwareModules("sceAtrac", modules, ARRAY_SIZE(modules)); + LoadFirmwareModules(0x302, "sceAtrac", modules, ARRAY_SIZE(modules)); return; } @@ -449,13 +486,14 @@ void __UtilityInit() { DeactivateDialog(); SavedataParam::Init(); currentlyLoadedModules.clear(); + swappedFirmwareModules.clear(); volatileUnlockEvent = CoreTiming::RegisterEvent("UtilityVolatileUnlock", UtilityVolatileUnlock); ResetSecondsSinceLastGameSave(); } void __UtilityDoState(PointerWrap &p) { - auto s = p.Section("sceUtility", 1, 6); + auto s = p.Section("sceUtility", 1, 7); if (!s) { return; } @@ -479,6 +517,14 @@ void __UtilityDoState(PointerWrap &p) { } } + if (s >= 7) { + Do(p, swappedFirmwareModules); + } else if (p.mode == p.MODE_READ) { + // An older state has no record of what we swapped in, so the unload notification will + // leave those modules loaded rather than risk freeing something the game owns. + swappedFirmwareModules.clear(); + } + if (s >= 3) { Do(p, volatileUnlockEvent); } else {