From f2eba8f7115892ed9d28d4606d58b50328e8b78f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 29 Sep 2026 10:17:46 -0600 Subject: [PATCH] sceKernelLoadModule: Charge for reading the file and loading it On hardware a load costs an open and a read of the file, plus about 1ms and 30us per KB of loader work (pspautotests threads/scheduling/callcosts), with the caller waiting throughout. It now charges sceIoOpen's and sceIoRead's estimates for the file plus that, instead of a flat 500us. Also notes why sceKernelLoadModuleByID fails from a game's own fd on ms0: or host0: on hardware, which we don't emulate. Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/HLE/sceIo.cpp | 33 ++++++++++++++++++--------------- Core/HLE/sceIo.h | 3 +++ Core/HLE/sceKernelModule.cpp | 12 ++++++++++-- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/Core/HLE/sceIo.cpp b/Core/HLE/sceIo.cpp index 86e878da39..10f02a4c03 100644 --- a/Core/HLE/sceIo.cpp +++ b/Core/HLE/sceIo.cpp @@ -1053,20 +1053,26 @@ static u32 npdrmRead(FileNode *f, u8 *data, int size) { return size; } +int __IoOpenDelayUs(const char *filename) { + // UMD: Speed varies from 1-6ms. + // Card: Path depth matters, but typically between 10-13ms on a standard Pro Duo. + return pspFileSystem.FlagsFromFilename(filename) & FileSystemFlags::UMD ? 4000 : 10000; +} + +int __IoReadDelayUs(int size) { + int us; + if (PSP_CoreParameter().compat.flags().ForceUMDReadSpeed || g_Config.iIOTimingMethod == IOTIMING_UMDSLOWREALISTIC) { + us = size / 4.2; + } else { + us = size / 100; + } + return std::max(us, 100); +} + static bool __IoRead(int &result, int id, u32 data_addr, int size, int &us) { PROFILE_THIS_SCOPE("io_rw"); // Low estimate, may be improved later from the ReadFile result. - - if (PSP_CoreParameter().compat.flags().ForceUMDReadSpeed || g_Config.iIOTimingMethod == IOTIMING_UMDSLOWREALISTIC) { - us = size / 4.2; - } - else { - us = size / 100; - } - - if (us < 100) { - us = 100; - } + us = __IoReadDelayUs(size); if (id == PSP_STDIN) { DEBUG_LOG(Log::sceIo, "sceIoRead STDIN"); @@ -1641,10 +1647,7 @@ static u32 sceIoOpen(const char *filename, int flags, int mode) { // These are fast to open, no delay or even rescheduling happens. return hleLogDebug(Log::sceIo, id); } - // UMD: Speed varies from 1-6ms. - // Card: Path depth matters, but typically between 10-13ms on a standard Pro Duo. - int delay = pspFileSystem.FlagsFromFilename(filename) & FileSystemFlags::UMD ? 4000 : 10000; - return hleDelayResult(hleLogDebug(Log::sceIo, id), "file opened", delay); + return hleDelayResult(hleLogDebug(Log::sceIo, id), "file opened", __IoOpenDelayUs(filename)); } } diff --git a/Core/HLE/sceIo.h b/Core/HLE/sceIo.h index ac6ccff262..422a0ea4fd 100644 --- a/Core/HLE/sceIo.h +++ b/Core/HLE/sceIo.h @@ -38,6 +38,9 @@ u32 sceIoIoctl(u32 id, u32 cmd, u32 indataPtr, u32 inlen, u32 outdataPtr, u32 ou int __IoIoctl(u32 id, u32 cmd, u32 indataPtr, u32 inlen, u32 outdataPtr, u32 outlen, int &usec); u32 __IoGetFileHandleFromId(u32 id, u32 &outError); +// What sceIoOpen and sceIoRead charge, for loaders that read whole files without going through them. +int __IoOpenDelayUs(const char *filename); +int __IoReadDelayUs(int size); void ConvertTmToPspDateTime(ScePspDateTime& date_out, const tm& date_in, int microSeconds); KernelObject *__KernelFileNodeObject(); diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index c98837bd79..6534ce8af1 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -2453,6 +2453,7 @@ u32 sceKernelLoadModule(const char *name, u32 flags, u32 optionAddr) { const u32 error = hleLogError(Log::Loader, SCE_KERNEL_ERROR_FILEERR, "module file size is 0"); return hleDelayResult(error, "module loaded", 500); } + const int fileSize = (int)fileData.size(); // A .sprx installed by a PKG game update comes wrapped in an NPDRM "\0PSPEDAT" container: a // 0x90-byte header naming the content ID, then the payload at the offset in its u16 at 0x0C. @@ -2544,8 +2545,11 @@ u32 sceKernelLoadModule(const char *name, u32 flags, u32 optionAddr) { INFO_LOG(Log::sceModule,"%i=sceKernelLoadModule(name=%s,flag=%08x,(...))", module->GetUID(), name, flags); } - // TODO: This is not the right timing and probably not the right wait type, just an approximation. - return hleDelayResult(hleNoLog(module->GetUID()), "module loaded", 500); + // Opening and reading the file, then about 1ms plus 30us per KB of loader work, the caller waiting + // throughout. The loader part is what a load took on hardware beyond an open and a read of the + // same file (pspautotests threads/scheduling/callcosts, from an SD card in a memory stick adapter). + const int loadUs = __IoOpenDelayUs(name) + __IoReadDelayUs(fileSize) + 1000 + fileSize / 34; + return hleDelayResult(hleNoLog(module->GetUID()), "module loaded", loadUs); } static u32 sceKernelLoadModuleNpDrm(const char *name, u32 flags, u32 optionAddr) { @@ -2984,6 +2988,10 @@ u32 sceKernelFindModuleByName(const char *name) } // The id in question here is a file handle. +// On hardware, from a game's own fd on ms0: or host0: this fails with +// SCE_KERNEL_ERROR_PROHIBIT_LOADMODULE_DEVICE: the file has to pass a kernel-only ioctl (0x00208001), +// which a user fd doesn't (it returns ILLEGAL_PERM). sceKernelLoadModule opens the file itself, so it +// passes. Whether a disc0:/umd0: fd passes is untested, so we let every device through. static u32 sceKernelLoadModuleByID(u32 id, u32 flags, u32 lmoptionPtr) { u32 error; u32 handle = __IoGetFileHandleFromId(id, error);