From ca34a14fd822f8b026101d98e462528964035806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 25 Jul 2026 14:08:39 +0200 Subject: [PATCH 1/5] Resolve unresolved-import calls to a module/NID in the "Unknown syscall" log A call through a still-pending import gets written as a generic "invalid syscall" opcode (WriteFuncMissingStub) that no longer carries the original module name or NID by the time it's actually invoked - but the address of the syscall instruction itself is exactly the stubAddr every pending FuncSymbolImport already records. Added KernelFindImportByStubAddr() to search loaded modules' importedFuncs for a match, and use it in GetSyscallFuncPointer's unknown-syscall path. Co-Authored-By: Claude Sonnet 5 --- Core/HLE/HLE.cpp | 12 +++++++++++- Core/HLE/sceKernelModule.cpp | 24 ++++++++++++++++++++++++ Core/HLE/sceKernelModule.h | 1 + 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/Core/HLE/HLE.cpp b/Core/HLE/HLE.cpp index 944098fab7..58a33476dc 100644 --- a/Core/HLE/HLE.cpp +++ b/Core/HLE/HLE.cpp @@ -39,6 +39,7 @@ #include "Core/HLE/ErrorCodes.h" #include "Core/HLE/sceKernelThread.h" #include "Core/HLE/sceKernelInterrupt.h" +#include "Core/HLE/sceKernelModule.h" #include "Core/HLE/HLE.h" enum { @@ -906,7 +907,16 @@ const HLEFunction *GetSyscallFuncPointer(MIPSOpcode op) { int modulenum = (callno & 0xFF000) >> 12; if (funcnum == 0xfff) { std::string_view modName = modulenum >= (int)moduleDB.size() ? "(unknown)" : moduleDB[modulenum].name; - ERROR_LOG(Log::HLE, "Unknown syscall: Module: '%.*s' (module: %d func: %d)", (int)modName.size(), modName.data(), modulenum, funcnum); + // This is what a still-unresolved import looks like once written as a syscall opcode - + // the original module name/NID aren't recoverable from the opcode itself (see + // WriteFuncMissingStub), but the calling address is a stub we may still be tracking. + std::string importModuleName, importingModuleName; + u32 nid = 0; + if (currentMIPS->pc >= 8 && KernelFindImportByStubAddr(currentMIPS->pc - 8, &importModuleName, &nid, &importingModuleName)) { + ERROR_LOG(Log::HLE, "Unknown syscall: unresolved import %s/%08x (%s), called from '%s'", importModuleName.c_str(), nid, GetHLEFuncName(importModuleName, nid), importingModuleName.c_str()); + } else { + ERROR_LOG(Log::HLE, "Unknown syscall: Module: '%.*s' (module: %d func: %d)", (int)modName.size(), modName.data(), modulenum, funcnum); + } return NULL; } if (modulenum >= (int)moduleDB.size()) { diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index 77484383df..86d9540ec8 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -766,6 +766,30 @@ void UnexportFuncSymbol(const FuncSymbolExport &func) { } } +// Used to add detail to the "Unknown syscall" log in HLE.cpp's GetSyscallFuncPointer - a call +// through a still-unresolved import ends up as a generic "invalid syscall" opcode that no +// longer carries the original module name/NID, but the (fixed, unique) address of the syscall +// instruction itself does - it's exactly the stubAddr every pending FuncSymbolImport recorded +// when it was written by WriteFuncMissingStub. +bool KernelFindImportByStubAddr(u32 stubAddr, std::string *importModuleName, u32 *nid, std::string *importingModuleName) { + u32 error; + for (SceUID moduleId : loadedModules) { + PSPModule *module = kernelObjects.Get(moduleId, error); + if (!module) { + continue; + } + for (const auto &func : module->importedFuncs) { + if (func.stubAddr == stubAddr) { + *importModuleName = func.moduleName; + *nid = func.nid; + *importingModuleName = module->GetName(); + return true; + } + } + } + return false; +} + void PSPModule::Cleanup() { MIPSAnalyst::ForgetFunctions(textStart, textEnd); diff --git a/Core/HLE/sceKernelModule.h b/Core/HLE/sceKernelModule.h index 13af5ef5ef..39274e0a59 100644 --- a/Core/HLE/sceKernelModule.h +++ b/Core/HLE/sceKernelModule.h @@ -228,6 +228,7 @@ u32 __KernelGetModuleGP(SceUID module); bool KernelModuleIsKernelMode(SceUID module); bool __KernelLoadGEDump(std::string_view base_filename, std::string *error_string); bool __KernelLoadExec(const char *filename, u32 paramPtr, std::string *error_string); +bool KernelFindImportByStubAddr(u32 stubAddr, std::string *importModuleName, u32 *nid, std::string *importingModuleName); int __KernelGPUReplay(); void __KernelReturnFromModuleFunc(); SceUID KernelLoadModule(const std::string &filename, std::string *error_string); From 8cb1732564e9aca190824bc99bfa28e09a8139e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 25 Jul 2026 10:44:33 +0200 Subject: [PATCH 2/5] Clean up the memory partition ID enum --- Core/HLE/sceKernelMemory.cpp | 12 ++++++------ Core/HLE/sceKernelMemory.h | 10 ++++++++++ Core/HLE/sceNetAdhocMatching.cpp | 4 +--- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/Core/HLE/sceKernelMemory.cpp b/Core/HLE/sceKernelMemory.cpp index 7aaf77cde5..939fbe0fe9 100644 --- a/Core/HLE/sceKernelMemory.cpp +++ b/Core/HLE/sceKernelMemory.cpp @@ -391,14 +391,14 @@ void __KernelMemoryShutdown() BlockAllocator *BlockAllocatorFromID(int id) { switch (id) { - case 1: + case KERNEL_PARTITION_ID: case 3: case 4: if (hleIsKernelMode()) return &kernelMemory; return nullptr; - case 2: + case USER_PARTITION_ID: case 6: return &userMemory; @@ -408,7 +408,7 @@ BlockAllocator *BlockAllocatorFromID(int id) { return &userMemory; return nullptr; - case 5: + case VSHELL_PARTITION_ID: return &volatileMemory; default: @@ -420,11 +420,11 @@ BlockAllocator *BlockAllocatorFromID(int id) { int BlockAllocatorToID(const BlockAllocator *alloc) { if (alloc == &kernelMemory) - return 1; + return KERNEL_PARTITION_ID; if (alloc == &userMemory) - return 2; + return USER_PARTITION_ID; if (alloc == &volatileMemory) - return 5; + return VSHELL_PARTITION_ID; return 0; } diff --git a/Core/HLE/sceKernelMemory.h b/Core/HLE/sceKernelMemory.h index 2dff1dfe38..a65db34604 100644 --- a/Core/HLE/sceKernelMemory.h +++ b/Core/HLE/sceKernelMemory.h @@ -32,6 +32,16 @@ enum MemblockType { PSP_SMEM_HighAligned = 4, }; +// Memory partition IDs, as passed to sceKernelAllocPartitionMemory() and friends. +// Per TyRaNiD: 1 = kernel, 2 = user, 3 = me, 4 = kernel mirror. 5 is the "volatile" +// partition also used by sceKernelVolatileMemLock() - the VSH uses this partition +// for its own private allocations while it's running. +enum { + KERNEL_PARTITION_ID = 1, + USER_PARTITION_ID = 2, + VSHELL_PARTITION_ID = 5, +}; + extern BlockAllocator userMemory; extern BlockAllocator kernelMemory; diff --git a/Core/HLE/sceNetAdhocMatching.cpp b/Core/HLE/sceNetAdhocMatching.cpp index b3e0eea6f2..14ee4c73fc 100644 --- a/Core/HLE/sceNetAdhocMatching.cpp +++ b/Core/HLE/sceNetAdhocMatching.cpp @@ -1907,10 +1907,8 @@ int NetAdhocMatching_Start(int matchingId, int evthPri, int evthPartitionId, int return hleLogDebug(Log::sceNet, 0); } -#define KERNEL_PARTITION_ID 1 -#define USER_PARTITION_ID 2 -#define VSHELL_PARTITION_ID 5 // This should be similar with sceNetAdhocMatchingStart2 but using USER_PARTITION_ID (2) for PartitionId params +// (KERNEL_PARTITION_ID/USER_PARTITION_ID/VSHELL_PARTITION_ID come from Core/HLE/sceKernelMemory.h) static int sceNetAdhocMatchingStart(int matchingId, int evthPri, int evthStack, int inthPri, int inthStack, int optLen, u32 optDataAddr) { WARN_LOG(Log::sceNet, "UNTESTED sceNetAdhocMatchingStart(%i, %i, %i, %i, %i, %i, %08x) at %08x", matchingId, evthPri, evthStack, inthPri, inthStack, optLen, optDataAddr, currentMIPS->pc); if (!g_Config.bEnableWlan) { From 7464269c0bcc98c29acda5b3f941ace4a42fca06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 25 Jul 2026 10:52:59 +0200 Subject: [PATCH 3/5] Updated AGENTS.md --- AGENTS.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 4c53e5d3fb..30ebcf3771 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,6 +45,30 @@ when making cross platform changes. New unit tests are added by listing them in availableTests in unittest.cpp. If they are large, put them in separate files in the unittest subdirectory. Remember to update both CMakeLists.txt and the visual studio project. +## Adding HLE modules + +HLE module implementations live in `Core/HLE/sce.cpp` / `.h` (e.g. `sceOpenPSID.cpp`, `scePauth.cpp` are good +small examples to copy from). A module is a `const HLEFunction []` table of +`{nid, &WrapX_YYY, "funcName", retChar, argString}` entries, registered via +`RegisterHLEModule("", ARRAY_SIZE(table), table)` inside a `Register_()` function declared in the header. + +- `FunctionWrappers.h` has generic `WrapX_YYY` templates for common signatures (return type X, args YYY) - check + there before writing a manual wrapper. +- Format string legend for the `retmask`/argmask chars: `x` = u32 (shown as hex), `i` = int/s32, `f` = float, `X` = u64, + `I` = s64, `v` = void. +- For functions of genuinely unknown purpose (only known by NID), name them `_` and stub them with + `return hleLogError(Log::HLE, 0, "UNIMPL");` - an established pattern (see `scePauth.cpp`, `sceOpenPSID.cpp`). +- **New modules must be registered at the very end** of the registration function in `Core/HLE/HLETables.cpp` (look for + the `// add new modules here.` comment near the end of that function) - not inserted alphabetically/logically among + the existing `Register_*()` calls. Module registration order affects numeric IDs used in savestates, so inserting a + new module earlier in that list would break save-state compatibility for saves made with older builds. +- Remember to add any new `.cpp`/`.c` file to **five** places: `Core/CMakeLists.txt`, `Core/Core.vcxproj`, + `Core/Core.vcxproj.filters`, `android/jni/Android.mk`, and `libretro/Makefile.common`. New `.h` files only need the + first three (`Android.mk`/`Makefile.common` are plain compiled-source lists, not project generators, so headers + don't go in them). Only the CMakeLists.txt change can be verified from a Linux/Mac build - the rest can't be + build-tested here, so double check them by hand against how an existing neighboring file (e.g. `sceVaudio.cpp`) is + listed in each. + ## Quick rebuild on Linux You don't need to do ./b.sh --debug to verify every single little change, instead use this shortcut: From 26ff5fdb002baa018c377606e861b96a5b31ee69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 25 Jul 2026 11:06:43 +0200 Subject: [PATCH 4/5] Robustness fix --- Core/HLE/sceKernelModule.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index 86d9540ec8..68dc4a2777 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -1135,10 +1135,17 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load _assert_msg_(temp != nullptr, "Failed to allocate gzip decompression buffer (decryptedSize: %d)", decryptedSize); memcpy(temp, ptr, decryptedSize); int outBytes = gzipDecompress((u8 *)ptr, maxElfSize, temp); - if (outBytes < 0) { - ERROR_LOG(Log::sceModule, "Module gzip decompression failed!"); - } free(temp); + if (outBytes < 0) { + // Not necessarily actually gzip - some kd/ system modules (and possibly VSH + // modules) use KL4E compression instead, which we don't support decompressing. + // Bail out cleanly here rather than falling through to parse whatever's left + // in the buffer (still compressed, not a valid ELF) as if it were real code. + *error_string = StringFromFormat("Module '%s' decompression failed", head->modname); + // TODO: Might be the wrong error code. + error = SCE_KERNEL_ERROR_FILEERR; + return nullptr; + } INFO_LOG(Log::sceModule, "gzip is enabled in '%s', decompressing (%d -> %d bytes, bufmax=%d).", head->modname, decryptedSize, outBytes, maxElfSize); } From a7ee0a7280a6d2966132521ba92295a6494e4e18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 25 Jul 2026 14:47:45 +0200 Subject: [PATCH 5/5] Include the code location in "ignored" bad memory access log lines --- Core/MemFault.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Core/MemFault.cpp b/Core/MemFault.cpp index 799b7ed104..4409a21652 100644 --- a/Core/MemFault.cpp +++ b/Core/MemFault.cpp @@ -296,7 +296,12 @@ bool HandleFault(uintptr_t hostAddress, void *ctx) { context->CTX_PC += info.instructionSize; g_numReportedBadAccesses++; if (g_numReportedBadAccesses < 100) { - ERROR_LOG(Log::MemMap, "Bad memory access detected and ignored: %08x (%p)", guestAddress, (void *)hostAddress); + std::string temp; + if (MIPSComp::jit && MIPSComp::jit->DescribeCodePtr(codePtr, temp)) { + ERROR_LOG(Log::MemMap, "Bad memory access detected and ignored: %08x (%p) at %s", guestAddress, (void *)hostAddress, temp.c_str()); + } else { + ERROR_LOG(Log::MemMap, "Bad memory access detected and ignored: %08x (%p)", guestAddress, (void *)hostAddress); + } } } else { std::string infoString = "";