diff --git a/Core/HLE/sceHprm.cpp b/Core/HLE/sceHprm.cpp index d6b9474152..20b66bfff4 100644 --- a/Core/HLE/sceHprm.cpp +++ b/Core/HLE/sceHprm.cpp @@ -100,6 +100,11 @@ const HLEFunction sceHprm_driver[] = {0X5FC5E53B, &WrapU_U, "sceHprmReadLatch", 'x', "x"}, // 6.00 - 6.20 {0X748FC3C8, &WrapU_V, "sceHprm_driver_DC895B2B", 'x', ""}, // 6.31 - 6.39 {0X605DEA7A, &WrapU_U, "sceHprmReadLatch", 'x', "x"}, // 5.03 - 5.55 + {0XA6E8D4F0, &WrapU_U, "sceHprmReadLatch", 'x', "x"}, // 3.95 - 4.05 + {0X8C728076, &WrapU_U, "sceHprmReadLatch", 'x', "x"}, // 3.72 - 3.90 + {0XF0AA1FB9, &WrapU_U, "sceHprmReadLatch", 'x', "x"}, // 3.71 + // Same story as sceRtc_driver: 3.51 and older export it to kernel mode under the user-mode NID. + {0X40D2F9F0, &WrapU_U, "sceHprmReadLatch", 'x', "x"}, // 1.50 - 3.51 }; void Register_sceHprm_driver() diff --git a/Core/HLE/sceImpose.cpp b/Core/HLE/sceImpose.cpp index 0cabcfa1d8..1b47edf3cc 100644 --- a/Core/HLE/sceImpose.cpp +++ b/Core/HLE/sceImpose.cpp @@ -262,6 +262,15 @@ const HLEFunction sceImpose_driver[] = { {0XBB12F974, &WrapI_I, "sceImposeSetStatus", 'i', "i" }, // The 6.60 name for the same call the user-mode module exports as 0x8C943191. {0X5557F4E2, &WrapU_UU, "sceImposeGetBatteryIconStatus", 'x', "xx"}, + // The 1.50 - 2.xx NIDs for the same two calls - impose.prx of that era exports them to kernel + // mode only, with no user-mode alias to match them against, so these were identified from the + // function bodies: GetParam is the same dispatch on a0 returning 0x8000xxxx for a bad index, + // and Changes is the same read-and-clear of one word in the impose context (at +0x84 there, + // +0xBC by 6.60). The VSH calls Changes once a frame, so unresolved they were most of the + // boot log on those versions. + // NOTE: new entries go at the end - the syscall opcode in a savestate is an index into this array. + {0X531C9778, &WrapI_I, "sceImposeGetParam", 'i', "i" }, + {0XB415FC59, &WrapI_V, "sceImposeChanges", 'i', "" }, }; void Register_sceImpose_driver() { diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index 8c182bb557..c6ed0bfeb5 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -1228,6 +1228,15 @@ static void LoadAndStartVshKernelModules() { LoadAndStartVshKernelModule(ResolveVshModelModule(path).c_str(), nullptr); } + // Firmwares up to about 4.05 keep scePaf's heap allocator in a module of its own, which paf + // imports as scePafHeaparea and can't allocate a single byte without. 5.01 and later compiled + // it into paf.prx and dropped the module, so this is absent (and unwanted) on those - hence + // the existence check rather than a warning from the loader. heaparea1 and heaparea2 are the + // same code with different compiled-in pool sizes; the first is the one the shell asks for. + if (pspFileSystem.GetFileInfo("flash0:/vsh/module/heaparea1.prx").exists) { + LoadAndStartVshKernelModule("flash0:/vsh/module/heaparea1.prx", nullptr); + } + static const char *const vshUiKernelModulePaths[] = { "flash0:/kd/vshbridge.prx", "flash0:/vsh/module/paf.prx", @@ -1536,8 +1545,11 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load module->nm.nsegment = reader.GetNumSegments(); module->nm.attribute = modinfo->moduleAttrs; - if ((module->nm.attribute & PSP_MODULE_VSH_MODE) != 0) { - // Used by the PSP's Visual Shell (VSH/XMB) and modules it loads, such as vshmain.prx. + // Used by the PSP's Visual Shell (VSH/XMB) and modules it loads, such as vshmain.prx. The + // name check is for firmware 1.50, whose vshmain.prx declares no attributes at all - Sony + // only started setting PSP_MODULE_VSH_MODE in 1.52. Without it the whole VSH bootstrap + // below was skipped and the shell ran with none of its support modules loaded. + if ((module->nm.attribute & PSP_MODULE_VSH_MODE) != 0 || equals(modinfo->name, "vsh_module")) { g_runningVSH = true; INFO_LOG(Log::sceModule, "VSH mode module detected: %s", modinfo->name); } @@ -1567,30 +1579,43 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load module->nm.gp_value = modinfo->gp; strncpy(module->nm.name, modinfo->name, ARRAY_SIZE(module->nm.name)); - if (equals(module->nm.name, "scePaf_Module")) { - // scePaf's own heap allocator expects a real memory-pool base address to already be - // patched into this BSS slot before any of its code runs. Real hardware's loader (or - // an early kernel init step) apparently does this - checked all 27 of scePaf's - // exported data vars, this address isn't one of them, so it's not the normal NID - // var-import linking path. Without this, offsets from scePaf's internal - // bump-allocator get used directly as absolute pointers, crashing almost immediately - // when a client module (e.g. vsh_module) makes its first heap allocation. - // See docs/VSHBootInvestigation.md for the full investigation. - // - // The slot is the second of the two pool pointers scePaf's own init fills in with - // sceKernelTryAllocateFpl. Its offset from the module base moves with every build, but - // it sits at a fixed distance below gp in all of them - checked against the paf.prx of - // 6.00, 6.20, 6.31, 6.37, 6.39, 6.60 and 6.61, where the base-relative offset ranges - // over 0x18CCD8..0x18D728 and gp - slot is 0x7E88 every time. - const u32 scePafHeapArenaGpOffset = 0x7E88; - u32 scePafHeapArenaSize = 0x00850000; // Matches scePaf's own compiled-in default heap size. + // scePaf's heap allocator expects a real memory-pool base address to already be in one of its + // BSS slots before any of its code runs. The module that owns the allocator fills that slot in + // itself, from its own module_start - but that start thread hasn't been scheduled yet when + // vshmain makes its first allocation, so the pointer is still null and the shell writes + // through it. Real hardware's kernel bootstrap starts these modules one at a time and waits; + // we can't, so pre-fill the slot with a real block instead. Without this the boot dies almost + // immediately, either on a null write inside scePaf (5.01+) or on vshmain storing the null the + // allocator handed back (up to 4.05). See docs/VSHBootInvestigation.md for the investigation. + // + // Which module owns it moved: up to about 4.05 the allocator is a separate heaparea1.prx, and + // from 5.01 it's compiled into paf.prx. Either way the slot is the second of the two pool + // pointers that module's init fills in with sceKernelTryAllocateFpl, and either way its offset + // from the module base moves with every build while its offset from gp does not - checked + // against paf.prx on 6.00, 6.20, 6.31, 6.37, 6.39, 6.60 and 6.61 (base-relative 0x18CCD8 to + // 0x18D728, gp - slot 0x7E88 every time) and heaparea1.prx on 3.95 and 4.05. + struct PafHeapOwner { + const char *moduleName; + u32 poolPointerGpOffset; + }; + static const PafHeapOwner pafHeapOwners[] = { + { "scePaf_Module", 0x7E88 }, + { "scePafHeaparea_Module", 0x7FCC }, + }; + for (const PafHeapOwner &owner : pafHeapOwners) { + if (!equals(module->nm.name, owner.moduleName)) { + continue; + } + // Matches the compiled-in default pool size in both modules. + u32 scePafHeapArenaSize = 0x00850000; u32 arenaAddr = userMemory.Alloc(scePafHeapArenaSize, false, "scePafHeapArena"); - u32 patchAddr = module->nm.gp_value - scePafHeapArenaGpOffset; + u32 patchAddr = module->nm.gp_value - owner.poolPointerGpOffset; if (arenaAddr != (u32)-1 && Memory::IsValid4AlignedAddress(patchAddr)) { Memory::WriteUnchecked_U32(arenaAddr, patchAddr); } else { - WARN_LOG(Log::sceModule, "Failed to patch scePaf heap arena pointer"); + WARN_LOG(Log::sceModule, "Failed to patch %s heap arena pointer", owner.moduleName); } + break; } if (equals(module->nm.name, "vsh_module")) { @@ -3163,6 +3188,12 @@ const HLEFunction ModuleMgrForKernel[] = { {0xD675EBB8, &WrapU_UUU, "sceKernelSelfStopUnloadModule", 'x', "xxx", HLE_KERNEL_SYSCALL }, {0xD5DDAB1F, &WrapU_CUU, "sceKernelLoadModuleVSH", 'x', "sxx", HLE_KERNEL_SYSCALL }, {0xD86DD11B, &WrapU_C, "sceKernelSearchModuleByName", 'x', "s", HLE_KERNEL_SYSCALL }, + // The 1.x NID for sceKernelLoadModuleVSH - same function, matched by its callee set in + // modulemgr.prx (sceKernelIsIntrContext, sceIoOpen/Ioctl/Close, sceKernelGetUserLevel). + // This is how the VSH loads its own plugins, so leaving it unresolved meant vshmain got + // module id 0 back and the sceKernelStartModule after it failed with UNKNOWN_MODULE. + // NOTE: new entries go at the end - the syscall opcode in a savestate is an index into this array. + {0xA4370E7C, &WrapU_CUU, "sceKernelLoadModuleVSH", 'x', "sxx", HLE_KERNEL_SYSCALL }, }; void Register_ModuleMgrForUser() { diff --git a/Core/HLE/sceRtc.cpp b/Core/HLE/sceRtc.cpp index b6e808b559..828575f834 100644 --- a/Core/HLE/sceRtc.cpp +++ b/Core/HLE/sceRtc.cpp @@ -946,6 +946,14 @@ static int sceRtcSetAlarmTick(u32 unknown1, u32 unknown2) { return hleLogError(Log::sceRtc, 0, "UNIMPL"); } +// "Has the RTC alarm fired?" - we don't model one and sceRtcSetAlarmTick above is a no-op, so the +// answer is always no. Same as JPCSP, which returns a bare 0. Left unimplemented this returned +// SCE_KERNEL_ERROR_LIBRARY_NOT_YET_LINKED, and the 3.0x-3.5x VSH took that as "ask the hardware +// instead" and blocked forever on a syscon reply. +static int sceRtcIsAlarmed() { + return hleLogDebug(Log::sceRtc, 0); +} + // Real signature per uofw (sceRtc_C2DDBEB5, src/kd/rtc/rtc.c): s32 sceRtcGetAlarmTick(u64 *tick). // PPSSPP doesn't track a real hardware RTC alarm, so there's nothing meaningful to report - // but leaving this fully unimplemented (nullptr in the function table) meant callers got back @@ -1143,7 +1151,7 @@ const HLEFunction sceRtc[] = {0X203CEB0D, &WrapI_U, "sceRtcGetLastReincarnatedTime", 'i', "x" }, {0X7D1FBED3, &WrapI_UU, "sceRtcSetAlarmTick", 'i', "xx" }, {0XF5FCC995, nullptr, "sceRtcGetCurrentNetworkTick", '?', "" }, - {0X81FCDA34, nullptr, "sceRtcIsAlarmed", '?', "" }, + {0X81FCDA34, &WrapI_V, "sceRtcIsAlarmed", 'i', "" }, {0XFB3B18CD, nullptr, "sceRtcRegisterCallback", '?', "" }, {0X6A676D2D, nullptr, "sceRtcUnregisterCallback", '?', "" }, {0XC2DDBEB5, &WrapI_U, "sceRtcGetAlarmTick", 'i', "x" }, @@ -1173,6 +1181,12 @@ const HLEFunction sceRtc_driver[] = { {0X54B9C589, &WrapI_UU, "sceRtcSetAlarmTick", 'i', "xx" }, // 6.31 - 6.39 {0X68AED59A, &WrapI_UU, "sceRtcSetAlarmTick", 'i', "xx" }, // 6.00 - 6.20 {0XADAF231F, &WrapI_UU, "sceRtcSetAlarmTick", 'i', "xx" }, // 5.03 - 5.55 + {0X55AC1C23, &WrapI_UU, "sceRtcSetAlarmTick", 'i', "xx" }, // 3.95 - 4.05 + {0X827BCB3F, &WrapI_UU, "sceRtcSetAlarmTick", 'i', "xx" }, // 3.72 - 3.90 + {0X329E8E3A, &WrapI_UU, "sceRtcSetAlarmTick", 'i', "xx" }, // 3.71 + // 3.51 and older didn't renumber it for kernel mode at all - the driver library exports it + // under the same NID as the user-mode one. + {0X7D1FBED3, &WrapI_UU, "sceRtcSetAlarmTick", 'i', "xx" }, // 2.71 - 3.51 }; void Register_sceRtc_driver() diff --git a/Core/Util/PSARUnpack.cpp b/Core/Util/PSARUnpack.cpp index 9b37c6e75b..67e7391624 100644 --- a/Core/Util/PSARUnpack.cpp +++ b/Core/Util/PSARUnpack.cpp @@ -1160,10 +1160,13 @@ bool EraseInstalledFirmware(const Path &nandRoot, std::string *error) { } bool FirmwareVersionSupportsVSH(std::string_view version) { - // Every firmware from 5.01 up boots to an interactive XMB, checked one release at a time - // against every version that ships on a disc (5.01, 5.02, 5.03, 5.50, 5.55, 6.00, 6.10, - // 6.20, 6.30, 6.31, 6.35, 6.37, 6.39, 6.60) plus the download-only 6.61. 4.05 and below - // still die on a null write inside vsh_module - a separate problem, not an offset to move. + // Every firmware boots to an interactive XMB, checked one release at a time against all 39 + // versions that ship on a disc - 1.50 through 6.60 - plus the download-only 6.61. So the + // only question left is whether this is a firmware at all: a fonts-only NAND has no version + // and nothing to boot. + // + // The lower bound is a sanity check rather than a real limit. 1.50 is the oldest firmware + // there is, so anything below it isn't a version string we wrote. // // "6.61" -> 661. Sony always writes the minor part with two digits, but don't rely on it: // a single-digit one is a tens value ("5.5" is 5.50, not 5.05). @@ -1186,7 +1189,7 @@ bool FirmwareVersionSupportsVSH(std::string_view version) { } else if (version.size() - dot != 3) { return false; } - return numeric >= 501; + return numeric >= 150; } std::string BundledUpdateInfo::Describe() const { diff --git a/docs/VSHBootInvestigation.md b/docs/VSHBootInvestigation.md index adc828bef4..c1dbf47bf6 100644 --- a/docs/VSHBootInvestigation.md +++ b/docs/VSHBootInvestigation.md @@ -68,71 +68,100 @@ changing repeats byte-identically, and a live menu does not. ## Which firmware versions work -**5.01 through 6.61**, all of them. `FirmwareVersionSupportsVSH()` -(`Core/Util/PSARUnpack.cpp`) is the gate the UI uses. +**All of them - 1.50 through 6.61.** `FirmwareVersionSupportsVSH()` (`Core/Util/PSARUnpack.cpp`) +is the gate the UI uses, and it now only asks whether a firmware is installed at all. -Checked one release at a time against every version that ships on a UMD - 5.01, 5.02, 5.03, 5.50, -5.55, 6.00, 6.10, 6.20, 6.30, 6.31, 6.35, 6.37, 6.39, 6.60 - plus the download-only 6.61. Each -reaches an interactive XMB; 6.00 was confirmed by rendering a GE dump off the running shell, which -is the same picture 6.60 gives. +Checked one release at a time against every one of the 39 versions that ships on a UMD - 1.50, +1.52, 2.00, 2.50, 2.60, 2.71, 2.80, 2.81, 2.82, 3.03, 3.11, 3.30, 3.40, 3.50, 3.51, 3.52, 3.71, +3.72, 3.73, 3.80, 3.90, 3.95, 3.96, 4.01, 4.05, 5.01, 5.02, 5.03, 5.50, 5.55, 6.00, 6.10, 6.20, +6.30, 6.31, 6.35, 6.37, 6.39, 6.60 - plus the download-only 6.61. 1.50 and 6.00 were also +confirmed by rendering a GE dump off the running shell; both give the same interactive XMB 6.60 +does. -**4.05 and below** still die on a `SIGSEGV at 00000000` inside `vsh_module` itself. Different -problem, not an offset to move. +### Sony renumbered the kernel NIDs, and that is what blocked everything below 6.60 -5.55 was briefly the odd one out: none of its `flash0:/kd` decrypted, because its modules are -tagged `0x4C941AF0`/`0x4C941BF0` and those two were the only entries of JPCSP's PRX tag table we -were missing. The shell came up anyway - vshmain and paf are user modules - but with not a single -driver behind it. Both keys are in `PrxDecrypter.cpp` now. +Not offsets - NIDs. A function PPSSPP HLEs under its 6.6x `*_driver` NID is unrecognized on an +older build, so the import resolves to the **real firmware module** instead, and the real module +goes places the emulator can't follow. -### What it took to get below 6.60 +| Function | 6.60/6.61 | 6.31-6.39 | 6.00-6.20 | 5.03-5.55 | 3.95-4.05 | 3.72-3.90 | 3.71 | 1.50-3.51 | +|---|---|---|---|---|---|---|---|---| +| `sceRtc_driver` `sceRtcSetAlarmTick` | `E09880CF` | `54B9C589` | `68AED59A` | `ADAF231F` | `55AC1C23` | `827BCB3F` | `329E8E3A` | `7D1FBED3` | +| `sceHprm_driver` `sceHprmReadLatch` | `E9B776BE` | `A3A87975` | `5FC5E53B` | `605DEA7A` | `A6E8D4F0` | `8C728076` | `F0AA1FB9` | `40D2F9F0` | -Two things, both the same shape: **Sony renumbered the kernel `*_driver` NIDs between firmware -versions**, so a function PPSSPP HLEs under the 6.6x NID is a stranger on an older build - and -the import then lands in the *real* firmware module instead, which is where the trouble starts. +The rtc one is the interesting failure. Without the HLE, the VSH's alarm call ran the real +`rtc.prx`, which called on into `syscon.prx` and blocked forever on a `SceSysconSync` semaphore. +The symptom was a boot where every thread was parked and `idle0` was running, and the tell was a +**fourth** `SceSysconSync` waiter that a healthy boot doesn't have (there are three, one each for +sceSYSCON_Driver, sceRTC_Service and SceWlanMac - that's their normal idle state). +`hle.backtrace thread=` named the whole chain: vsh_module -> vshbridge -> rtc -> +syscon -> wait. The hprm one is only a performance bug, but a loud one: the VSH reads the latch +once a frame, so an older firmware's 12-second boot logged ~20000 lines of the same import. -- **`sceRtc_driver` `sceRtcSetAlarmTick`** is `0xE09880CF` on 6.60/6.61, `0x54B9C589` on - 6.31-6.39, `0x68AED59A` on 6.00-6.20 and `0xADAF231F` on 5.03-5.55. Without the HLE the VSH's - alarm call ran the real `rtc.prx`, which called on into `syscon.prx` and blocked forever on a - `SceSysconSync` semaphore. That was the whole "stalls in sceVshBridge_Driver" symptom: every - thread parked, `idle0` running, and a fourth `SceSysconSync` waiter that a healthy boot doesn't - have. -- **`sceHprm_driver` `sceHprmReadLatch`** is `0xE9B776BE` on 6.60/6.61, `0xA3A87975` on 6.31-6.39, - `0x5FC5E53B` on 6.00-6.20 and `0x605DEA7A` on 5.03-5.55. The VSH calls it once a frame, so - before this an older firmware's boot log was ~20000 lines of one unresolved import. +Two more of the same shape, for 1.50-2.xx: `sceImpose_driver` exports `sceImposeGetParam` as +`0x531C9778` and `sceImposeChanges` as `0xB415FC59` there, with no user-mode alias. Changes runs +once a frame too. And `ModuleMgrForKernel` numbers `sceKernelLoadModuleVSH` `0xA4370E7C` on 1.x - +that's how the VSH loads its own plugins, so unresolved it got module id 0 back and the +`sceKernelStartModule` after it failed, exactly the way `0xD5DDAB1F` did on 6.61 before it was +implemented. **How to map a NID between versions.** Disassemble the same module from both firmwares with -`--re-module` and match by address: the builds move code around a little, but each of these -functions is also exported from the plain user-mode library under a NID that never changed -(`sceRtc/0x7D1FBED3`, `sceHprm/0x40D2F9F0`), so anchor on that export's address in each build and -read off the `*_driver` NID sitting at the same place. That is mechanical, and it is how all eight -NIDs above were found. +`--re-module` and match by address. Don't compare raw addresses - the builds move code - anchor on +the plain user-mode export whose NID never changed (`sceRtc/0x7D1FBED3` for SetAlarmTick, +`sceHprm/0x40D2F9F0` for ReadLatch) and read off the `*_driver` NID at the same address. Below +3.95 `sceRtcSetAlarmTick` isn't in the user-mode library at all, so anchor on a neighbour instead: +it is the driver export immediately below `sceRtcIsAlarmed`. Where even that fails, match the +function body - that is how the two impose calls and the 1.x LoadModuleVSH were identified. -Expect more of these as other paths get exercised - `sceVshBridge`, `sceDisplay_driver`, -`sceImpose_driver` and `sceCtrl_driver` all have version-specific NIDs that are currently -unresolved on every version, 6.61 included, and the boot survives them. +`sceRtcIsAlarmed` also had to be implemented (it returns 0, as in JPCSP). Left as a null entry it +returned `SCE_KERNEL_ERROR_LIBRARY_NOT_YET_LINKED`, and the 3.0x-3.5x VSH read that as "ask the +hardware instead" and went back to blocking on syscon. -### Making the two module patches version-safe +### The scePaf heap pool, and where it lives -Both patches used to be hardcoded offsets from the module base, applied to any module of the right -name. That is fine for the version they were derived from and quietly wrong for every other one. +scePaf's allocator wants a pool base already written into one of its BSS slots. The module that +owns the allocator does fill that slot in itself, from its own module_start - but that start +thread hasn't been scheduled yet when vshmain makes its first allocation, so the pointer is still +null and the shell writes through it. Real hardware's kernel bootstrap starts these modules one at +a time and waits; `LoadAndStartVshKernelModules()` can't, so it pre-fills the slot instead. -- **The scePaf heap arena slot is now found via gp**, not the module base. Its base-relative offset - moves with every build (0x18CCD8 on 6.00 through 0x18D728 on 6.60/6.61), but it sits at - `gp - 0x7E88` in all of them - checked against 6.00, 6.20, 6.31, 6.37, 6.39, 6.60 and 6.61. The - slot is the second of the two pool pointers scePaf's own init fills in with - `sceKernelTryAllocateFpl`, which is how to re-find it in a build not listed here: disassemble - `paf.prx`, find the one function that calls `sceKernelTotalMemSize`, and read the address handed - to the second `sceKernelTryAllocateFpl` as `a1`. -- **The vsh_module alarm-category patch now checks what it is overwriting.** That offset is in - rodata, so there is no gp anchor for it; instead the patch only fires when the word at - `+0x455C4` is the `0x3F666666` it was derived from. On 6.39 that word is a different float, and - on 6.20/6.00 it is ASCII string data (`5f746c75`, `776f6461`) - the old unconditional write was - corrupting a string table on those. Only 6.60/6.61 need the patch at all; every older version - reaches the XMB without it. +Which module owns it moved. **Up to 4.05 the allocator is a separate `flash0:/vsh/module/heaparea1.prx`**, +which paf imports as `scePafHeaparea` and cannot allocate a byte without; from 5.01 it is compiled +into paf.prx and heaparea1 is gone. Loading heaparea1 when it's present was the missing piece for +every 3.x and 4.x version - without it `scePafHeaparea_ACCE25B2` was an unresolved import, paf +built a heap out of an uninitialized stack pair, and vshmain died storing the null the allocator +handed back. -Getting the paf patch right on its own turned an immediate `SIGSEGV at 0000000c` inside -`scePaf_Module` (a null pool base plus a field offset) into a clean stall for every 6.0x-6.3x -version, which is what moved the blocker on to the rtc NID above. +Either way the slot is the second of the two pool pointers that module's init fills in with +`sceKernelTryAllocateFpl`, and either way its offset from the module base moves with every build +while its offset from gp does not: + +| Module | gp - slot | Checked against | +|---|---|---| +| `paf.prx` | `0x7E88` | 6.00, 6.20, 6.31, 6.37, 6.39, 6.60, 6.61 (base-relative 0x18CCD8..0x18D728) | +| `heaparea1.prx` | `0x7FCC` | 3.95, 4.05 | + +To re-find it in a build not listed: disassemble the module, find the one function that calls +`sceKernelTotalMemSize`, and read the address handed to the **second** of its two +`sceKernelTryAllocateFpl` calls as `a1`. The shape is identical in both modules - TotalMemSize, a +`> 0x2400000` test picking 0xA00000/0xC50000 pool sizes over the compiled-in defaults, then two +`sceKernelCreateFpl` + `sceKernelTryAllocateFpl` pairs writing to adjacent slots. + +### 1.50 declares no module attributes + +`g_runningVSH` was set from `PSP_MODULE_VSH_MODE` in the module's attribute word. 1.50's +vshmain.prx has attribute `0000` - Sony only started setting the flag in 1.52 - so the entire VSH +bootstrap was skipped and the shell ran with none of its support modules loaded. The check also +accepts the module *name* `vsh_module` now. + +### The vsh_module alarm-category patch only 6.6x needs + +That offset is in rodata, so there is no gp anchor for it; instead the patch only fires when the +word at `+0x455C4` is the `0x3F666666` it was derived from. On 6.39 that word is a different +float, and on 6.20/6.00 it is ASCII string data (`5f746c75`, `776f6461`) - the old unconditional +write was corrupting a string table on those. Every version below 6.60 reaches the XMB without the +patch, which is itself a hint that whatever precondition makes that scan safe on real hardware was +lost somewhere between 6.39 and 6.60. ### Kernel modules with per-model builds @@ -140,7 +169,14 @@ version, which is what moved the blocker on to the rtc NID above. `wlanfirm_01g.prx` by name. A firmware unpacked for one model ships only that model's build, and PPSSPP's own updater unpack defaults to 02g, so all three failed to load. `ResolveVshModelModule()` now substitutes the emulated model's suffix when that file exists, falling back to `_01g` for a -dump unpacked with model `any` (which has every model's). +dump unpacked with model `any` (which has every model's). Firmwares older than about 3.60 predate +the PSP-2000 and have no per-model split at all, so they are unaffected. + +### 5.55 needed two PRX keys + +Its `flash0:/kd` modules are tagged `0x4C941AF0`/`0x4C941BF0`, and those two were the only entries +of JPCSP's PRX tag table PPSSPP was missing. The shell came up anyway - vshmain and paf are user +modules - but with not a single driver behind it. ## The red error screen