From c0c715ac2e66dae1b894d9bc56a8c75d422b3202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 8 Sep 2026 11:43:49 -0600 Subject: [PATCH 1/2] Load injected firmware modules at the top of user memory, not the bottom The flash0 PRXes we swap in for our HLE took the lowest free block, which sits right where a game's own EBOOT wants to go. That pushes the game up, shifting every address in it - invalidating cheats and RetroAchievements - and for a game whose EBOOT has to load at a fixed low address it fails outright: Tekken 6 wants 0x08804018 and got "block taken", so it didn't boot at all. Give KernelLoadModule a fromTop flag and use it for the modules we inject. The game keeps its normal load address and the firmware sits out of the way at the top. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01GgACRqkQNpfJQ4fjwyoEup --- Core/HLE/sceKernelModule.cpp | 4 ++-- Core/HLE/sceKernelModule.h | 6 +++++- Core/HLE/sceUtility.cpp | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index 8abad4708b..e5979dbe57 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -1921,14 +1921,14 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load return module; } -SceUID KernelLoadModule(const std::string &filename, std::string *error_string) { +SceUID KernelLoadModule(const std::string &filename, std::string *error_string, bool fromTop) { std::vector buffer; if (pspFileSystem.ReadEntireFile(filename, buffer) < 0) return SCE_KERNEL_ERROR_NOFILE; u32 error = SCE_KERNEL_ERROR_ILLEGAL_OBJECT; u32 magic; - PSPModule *module = __KernelLoadELFFromPtr(&buffer[0], buffer.size(), 0, false, error_string, &magic, filename, error); + PSPModule *module = __KernelLoadELFFromPtr(&buffer[0], buffer.size(), 0, fromTop, error_string, &magic, filename, error); if (module == nullptr) return error; diff --git a/Core/HLE/sceKernelModule.h b/Core/HLE/sceKernelModule.h index 6fcbbcfd5b..c1f0c5d359 100644 --- a/Core/HLE/sceKernelModule.h +++ b/Core/HLE/sceKernelModule.h @@ -247,7 +247,11 @@ bool KernelFindImportByStubAddr(u32 stubAddr, std::string *importModuleName, u32 bool DescribeModuleAddress(u32 address, char *buffer, size_t bufferSize); int __KernelGPUReplay(); void __KernelReturnFromModuleFunc(); -SceUID KernelLoadModule(const std::string &filename, std::string *error_string); +// fromTop puts the module at the top of the user partition instead of the bottom. Use it for +// firmware modules we inject before the game loads - taking the bottom pushes the game's own ELF +// 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); 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 90102697cc..a6a9ba32fc 100644 --- a/Core/HLE/sceUtility.cpp +++ b/Core/HLE/sceUtility.cpp @@ -108,7 +108,7 @@ static void NotifyLoadStatusMp4(int state, u32 loadAddr, u32 totalSize) { continue; } std::string error; - SceUID id = KernelLoadModule(paths[i], &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()); From 5a7ca5a6a534126c1cf5224e875b67cb2160b3ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 8 Sep 2026 11:11:33 -0600 Subject: [PATCH 2/2] HLE: make the HLE boundary part of the machine state, so savestates survive it Which modules we HLE is decided when each module is loaded, and the syscall stubs written into memory then are what a savestate captures. But the setting was read live, so loading a state re-resolved its imports against whatever the config said now - and if that disagreed with how the state was made, every call into the module landed on an unresolved stub returning LIBRARY_NOT_YET_LINKED. Thrillville just retried sceMpegInit forever. Latch the flags on first use after boot, save them in the state, and restore them on load. Changing the setting now takes effect on the next boot, which is the only point it could have taken effect anyway. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01GgACRqkQNpfJQ4fjwyoEup --- Core/HLE/HLE.cpp | 41 +++++++++++++++++++++++++++++++++++++---- Core/HLE/HLE.h | 3 +++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/Core/HLE/HLE.cpp b/Core/HLE/HLE.cpp index 9804e517f3..88322d370f 100644 --- a/Core/HLE/HLE.cpp +++ b/Core/HLE/HLE.cpp @@ -203,14 +203,23 @@ DisableHLEFlags AlwaysDisableHLEFlags() { return DisableHLEFlags::scePsmf | DisableHLEFlags::scePsmfPlayer | DisableHLEFlags::sceCcc; } -// Process compat flags. +// Which modules we're HLE-ing is part of the machine's state, not a live setting: it's decided +// when each module is loaded, and the syscall stubs written into memory then are what a savestate +// captures. So latch it on the first use after boot, save it in the state, and restore it on load +// - otherwise a state made on one side of the boundary gets its imports re-resolved against the +// other, and every call into the module lands on an unresolved stub. Changing the setting takes +// effect on the next boot, which is the only point it could have taken effect anyway. +static DisableHLEFlags g_effectiveDisableHLE; +static bool g_disableHLELatched; + // Flags the user asked for that we can't honour this boot, because the firmware modules they -// need aren't in the NAND directory. Subtracted in GetDisableHLEFlags so that a missing dump +// need aren't in the NAND directory. Subtracted in ComputeDisableHLEFlags so that a missing dump // leaves the HLE in place rather than handing the game unresolved imports, which is much worse // than our stubs. Recomputed per boot, since the dump can appear between runs. static DisableHLEFlags g_unavailableDisableFlags = (DisableHLEFlags)0; -static DisableHLEFlags GetDisableHLEFlags() { +// Process compat flags. +static DisableHLEFlags ComputeDisableHLEFlags() { DisableHLEFlags flags = (DisableHLEFlags)g_Config.iDisableHLE | AlwaysDisableHLEFlags(); if (PSP_CoreParameter().compat.flags().DisableHLESceFont) { flags |= DisableHLEFlags::sceFont; @@ -225,6 +234,18 @@ static DisableHLEFlags GetDisableHLEFlags() { return flags; } +static DisableHLEFlags GetDisableHLEFlags() { + if (!g_disableHLELatched) { + g_effectiveDisableHLE = ComputeDisableHLEFlags(); + g_disableHLELatched = true; + } + return g_effectiveDisableHLE; +} + +DisableHLEFlags GetEffectiveDisableHLEFlags() { + return GetDisableHLEFlags(); +} + // Note: name is the modname from prx, not the export module name! bool ShouldHLEModule(std::string_view modname, bool *wasDisabledManually) { if (wasDisabledManually) { @@ -296,16 +317,28 @@ static void CheckDisableHLEAvailability() { void HLEInit() { CheckDisableHLEAvailability(); RegisterAllModules(); + // Latched lazily rather than here: the compat flags this depends on aren't loaded yet. + g_disableHLELatched = false; g_stackSize = 0; delayedResultEvent = CoreTiming::RegisterEvent("HLEDelayedResult", hleDelayResultFinish); g_idleOp = GetSyscallOp("FakeSysCalls", NID_IDLE); } void HLEDoState(PointerWrap &p) { - auto s = p.Section("HLE", 1, 2); + auto s = p.Section("HLE", 1, 3); if (!s) return; + if (s >= 3) { + int disableHLE = (int)GetDisableHLEFlags(); + Do(p, disableHLE); + if (p.mode == p.MODE_READ) { + // Whatever the config says now, this state's modules were loaded under these flags. + g_effectiveDisableHLE = (DisableHLEFlags)disableHLE; + g_disableHLELatched = true; + } + } + // Can't be inside a syscall when saving state, reset this so errors aren't misleading. if (g_stackSize) { ERROR_LOG(Log::HLE, "Can't save state while in a HLE syscall"); diff --git a/Core/HLE/HLE.h b/Core/HLE/HLE.h index a0c39650b3..b1fbc25772 100644 --- a/Core/HLE/HLE.h +++ b/Core/HLE/HLE.h @@ -127,6 +127,9 @@ void RegisterHLEModule(std::string_view name, int numFunctions, const HLEFunctio int GetNumRegisteredHLEModules(); const HLEModule *GetHLEModuleByIndex(int index); 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(); // Run the current thread's callbacks after the syscall finishes. void hleCheckCurrentCallbacks();