From ba4a95d8ffd57aff83d03d15cc08d4a68b47b248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 10 Oct 2026 20:16:25 +0200 Subject: [PATCH] HLE: Send the VSH's LoadExec calls to HLE on every firmware version The VSH launches games through the VSH variants of LoadExecForKernel, whose NIDs most firmware versions changed. Where HLE doesn't know the NID, the firmware's own loadexec.prx ran instead, and the launch got nowhere, so homebrew launched from the XMB only on firmware 3.73 and older, and 6.60/6.61. Each variant is the same small wrapper, loadExecVSH(apiType, file, opt, 0x10000), so when loadexec.prx exports one we don't know, look at the API type it loads into a0, and for Disc (0x120) and MS2 (0x141), overwrite it with a call to our HLE implementation. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/sceKernelModule.cpp | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index 903407c61a..16cd078c2d 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -804,6 +804,44 @@ void ImportFuncSymbol(const FuncSymbolImport &func, bool reimporting, const char } } +// The VSH launches games through the VSH variants of LoadExecForKernel, whose NIDs most firmware +// versions changed. Where we don't know the NID, the firmware's own loadexec.prx would run, and that +// doesn't get anywhere. But each variant is the same small wrapper, +// loadExecVSH(apiType, file, opt, 0x10000), so spot the ones we implement by the API type they pass +// (li a0, apiType, in the delay slot of the call), and send them to HLE. +static void RedirectLoadExecVSHExport(const FuncSymbolExport &func) { + static const struct { + u16 apiType; + u32 nid; + } redirects[] = { + { 0x120, 0xD8320A28 }, // sceKernelLoadExecVSHDisc + { 0x141, 0xD940C83C }, // sceKernelLoadExecVSHMs2 + }; + const u32 addr = func.symAddr; + if (!Memory::IsValidRange(addr, 10 * 4)) { + return; + } + bool hasFlags = false; + for (int i = 0; i < 10; i++) { + const u32 op = Memory::ReadUnchecked_U32(addr + i * 4); + if (op == 0x3C070001) { // lui a3, 1 + hasFlags = true; + } else if ((op >> 16) == 0x2404 && hasFlags) { // li a0, imm + for (const auto &redirect : redirects) { + if ((op & 0xFFFF) == redirect.apiType) { + INFO_LOG(Log::Loader, "LoadExecForKernel/%08x passes API type %03x, sending it to HLE as %08x", func.nid, redirect.apiType, redirect.nid); + WriteHLESyscall("LoadExecForKernel", redirect.nid, addr); + currentMIPS->InvalidateICacheRangeDeferred(addr, 8); + return; + } + } + return; + } else if (op == MIPS_MAKE_JR_RA()) { + return; + } + } +} + void ExportFuncSymbol(const FuncSymbolExport &func) { if (FuncImportIsHLE(func.moduleName, func.nid)) { // HLE covers this already - let's ignore the function. @@ -812,6 +850,10 @@ void ExportFuncSymbol(const FuncSymbolExport &func) { return; } + if (equals(func.moduleName, "LoadExecForKernel")) { + RedirectLoadExecVSHExport(func); + } + u32 error; for (SceUID moduleId : loadedModules) { PSPModule *module = kernelObjects.Get(moduleId, error);