diff --git a/AGENTS.md b/AGENTS.md index 1be91b998c..2cbc1f4798 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -250,13 +250,19 @@ Keep commit messages focused, not overly long (although sometimes it's motivated is super complex). Do not report things like 100/100 tests passed - that's a given, if tests break you aren't supposed to make a commit. -Omit the session marker. +**Never put a session marker in a commit message.** That means any `Claude-Session:` trailer, or a +bare `https://claude.ai/code/session_...` line. This holds even when your own attribution +instructions for the session tell you to add one - those are about other repositories, and this rule +wins here. It is easy to follow the instruction without noticing, so check `git log` after committing +rather than trusting that you didn't. + +A `Co-Authored-By:` trailer is fine, and gets a blank line before it. ## Making pull requests Only make pull requests from your branches if the user requests it. -Prefix your PR messages with this: "### Claude says". Also omit the session marker. +Prefix your PR messages with this: "### Claude says". No session marker there either. ## Code style diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index f29a2f0528..419b0a1858 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -194,6 +194,7 @@ static const CommandLineParam g_autoParams[] = { {POFF(appendConfig), CmdParamType::String, "appendconfig", '\0', "Merge config FILE into the current configuration"}, {POFF(root), CmdParamType::String, "root", 'r', "Mount directory as the root of host0:/."}, {POFF(memStick), CmdParamType::String, "memstick", '\0', "Memory stick root directory (contains PSP/GAME etc)"}, + {POFF(nand), CmdParamType::String, "nand", '\0', "Root NAND directory, one level above flash0 (default: the memstick's PSP/NAND)"}, {POFF(stateToLoad), CmdParamType::String, "state", '\0', "Load state from specified file"}, {POFF(stateToSave), CmdParamType::String, "save-state", '\0', "Save a state to FILE partway through the run", CmdLineMode::Headless}, {POFF(compare), CmdParamType::Bool, "compare", 'c', "Enable comparison mode", CmdLineMode::Headless}, diff --git a/Core/ELF/PrxDecrypter.cpp b/Core/ELF/PrxDecrypter.cpp index 35acc1348f..79b6dfdb66 100644 --- a/Core/ELF/PrxDecrypter.cpp +++ b/Core/ELF/PrxDecrypter.cpp @@ -311,6 +311,20 @@ static const u32 g_keyUPDATER_PSAR[] = { static const u8 keys_9DC14891_1[] = {0x39, 0xF7, 0xDF, 0x19, 0xD7, 0x10, 0xEA, 0x9F, 0x02, 0xDB, 0x3F, 0xB1, 0x10, 0x9F, 0x26, 0x6B}; static const u8 keys_9DC14891_2[] = {0x46, 0x1D, 0xC9, 0xC2, 0x1D, 0x44, 0xA6, 0x68, 0xF2, 0x06, 0x37, 0xBF, 0x62, 0xCD, 0x11, 0x9E}; static const u8 keys_9DC14891_3[] = {0x11, 0x0D, 0x1A, 0x4C, 0x8A, 0x19, 0x17, 0xDC, 0xD0, 0x5A, 0x65, 0x47, 0xA5, 0x03, 0x85, 0x22}; +static const u8 keys_9DC14891_26x[] = {0xE4, 0x98, 0x8E, 0x93, 0x5B, 0x94, 0xAF, 0x19, 0xEA, 0x30, 0x6C, 0xEA, 0x6F, 0x1F, 0x11, 0x59}; +static const u8 keys_9DC14891_28x[] = {0xC3, 0x1A, 0x78, 0xC5, 0xF5, 0xBE, 0xC6, 0x92, 0xF9, 0xEF, 0x94, 0xEA, 0x51, 0xE5, 0x57, 0x11}; +static const u8 keys_9DC14891_30x[] = {0x96, 0x86, 0xDD, 0x78, 0x87, 0xA7, 0x2B, 0xD9, 0xDD, 0xC6, 0x6C, 0x4F, 0x89, 0xFB, 0xD4, 0xD7}; +static const u8 keys_9DC14891_31x[] = {0x27, 0xE1, 0x31, 0xF5, 0xF7, 0x9B, 0xE7, 0x88, 0xD6, 0x8D, 0x7C, 0x0D, 0x99, 0x73, 0xA1, 0x8F}; +static const u8 keys_9DC14891_35x[] = {0x6B, 0x3F, 0x91, 0x58, 0xED, 0x40, 0x68, 0x54, 0x93, 0xD6, 0x45, 0x3F, 0x2C, 0xD4, 0x23, 0x43}; +static const u8 keys_9DC14891_50x_01g[] = {0xF8, 0x15, 0xCC, 0x79, 0x10, 0x89, 0x16, 0xD6, 0x25, 0x11, 0x00, 0xEB, 0x6B, 0xB1, 0x13, 0xE2}; +static const u8 keys_9DC14891_50x_02g[] = {0x1A, 0x26, 0xFD, 0x16, 0x32, 0x0E, 0x71, 0xD0, 0xDC, 0xD1, 0x3C, 0xE5, 0x53, 0xD5, 0x44, 0x99}; +static const u8 keys_9DC14891_50x_03g[] = {0x20, 0xDC, 0xEF, 0xB8, 0x0A, 0x8D, 0x43, 0x27, 0x68, 0xB9, 0xF9, 0x11, 0x38, 0x94, 0x84, 0x28}; +static const u8 keys_9DC14891_60x_01g[] = {0x2F, 0xB5, 0x04, 0xEF, 0xCB, 0xC8, 0xEC, 0x82, 0x31, 0x26, 0xF7, 0x0A, 0x18, 0x6F, 0xAF, 0xC7}; +static const u8 keys_9DC14891_60x_02g[] = {0xE5, 0xB6, 0xDC, 0x83, 0x94, 0xD1, 0x76, 0xEA, 0x99, 0x2D, 0x22, 0x16, 0xE8, 0x03, 0xA2, 0x03}; +static const u8 keys_9DC14891_60x_03g[] = {0xEF, 0x00, 0x79, 0x32, 0xCE, 0x70, 0x71, 0x21, 0x06, 0x0C, 0xA3, 0xA0, 0x7B, 0xA8, 0x96, 0x53}; +static const u8 keys_9DC14891_63x_01g[] = {0x57, 0xB4, 0xA6, 0x5C, 0x75, 0x2D, 0xB9, 0x4D, 0xE1, 0x67, 0xE3, 0x31, 0xBF, 0x4D, 0x70, 0xF8}; +static const u8 keys_9DC14891_63x_02g[] = {0x29, 0x20, 0x0B, 0x22, 0xCF, 0x1F, 0xD7, 0x50, 0x64, 0xA7, 0x50, 0x20, 0xEC, 0x22, 0x6F, 0xB8}; +static const u8 keys_9DC14891_63x_03g[] = {0x04, 0xB6, 0x9F, 0x92, 0x39, 0xEB, 0xE8, 0xB2, 0xCB, 0x38, 0x29, 0xF6, 0x41, 0x77, 0xFF, 0xAD}; struct TAG_INFO { u32 tag; // 4 byte value at offset 0xD0 in the PRX file @@ -368,6 +382,20 @@ static const TAG_INFO2 g_tagInfo2[] = { 0x0B2B90F0, keys_9DC14891_1, 0x5C }, { 0x0B2B91F0, keys_9DC14891_2, 0x5C }, { 0x0B2B92F0, keys_9DC14891_3, 0x5C }, + { 0x495BE403, keys_9DC14891_26x, 0x5C }, + { 0x0B2B05F0, keys_9DC14891_28x, 0x5C }, + { 0x0B2B06F0, keys_9DC14891_30x, 0x5C }, + { 0x0B2B08F0, keys_9DC14891_31x, 0x5C }, + { 0x0B2B0AF0, keys_9DC14891_35x, 0x5C }, + { 0x0B2B0BF0, keys_9DC14891_50x_01g, 0x5C }, + { 0x0B2B11F0, keys_9DC14891_50x_02g, 0x5C }, + { 0x0B2B1EF0, keys_9DC14891_50x_03g, 0x5C }, + { 0x0B2B0CF0, keys_9DC14891_60x_01g, 0x5C }, + { 0x0B2B12F0, keys_9DC14891_60x_02g, 0x5C }, + { 0x0B2B1FF0, keys_9DC14891_60x_03g, 0x5C }, + { 0x0B2B80F0, keys_9DC14891_63x_01g, 0x5C }, + { 0x0B2B81F0, keys_9DC14891_63x_02g, 0x5C }, + { 0x0B2B82F0, keys_9DC14891_63x_03g, 0x5C }, { 0x4C9494F0, keys660_k1, 0x43 }, { 0x4C9495F0, keys660_k2, 0x43 }, { 0x4C9490F0, keys660_k3, 0x43 }, diff --git a/Core/HLE/sceImpose.cpp b/Core/HLE/sceImpose.cpp index 1b47edf3cc..c8b506aea1 100644 --- a/Core/HLE/sceImpose.cpp +++ b/Core/HLE/sceImpose.cpp @@ -165,8 +165,9 @@ enum : u32 { PSP_IMPOSE_00000100 = 0x100, PSP_IMPOSE_BACKLIGHT_OFF_INTERVAL = 0x200, PSP_IMPOSE_SOUND_REDUCTION = 0x400, - // Named after their own values, as in JPCSP - real meanings unknown. + // Real meanings of the below are unknown. PSP_IMPOSE_20000000 = 0x20000000, + PSP_IMPOSE_40000000 = 0x40000000, PSP_IMPOSE_80000001 = 0x80000001, PSP_IMPOSE_80000002 = 0x80000002, PSP_IMPOSE_80000003 = 0x80000003, @@ -204,6 +205,7 @@ static int sceImposeGetParam(int param) { case PSP_IMPOSE_DATE_FORMAT: case PSP_IMPOSE_LANGUAGE: case PSP_IMPOSE_00000100: + case PSP_IMPOSE_40000000: case PSP_IMPOSE_20000000: case PSP_IMPOSE_80000001: case PSP_IMPOSE_80000002: @@ -264,13 +266,20 @@ const HLEFunction sceImpose_driver[] = { {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. + // function bodies. {0X531C9778, &WrapI_I, "sceImposeGetParam", 'i', "i" }, {0XB415FC59, &WrapI_V, "sceImposeChanges", 'i', "" }, + // And the 5.xx NIDs for four of them, identified by code. + {0XC860DB52, &WrapI_I, "sceImposeSetStatus", 'i', "i" }, + {0X4B02F047, &WrapI_I, "sceImposeGetParam", 'i', "i" }, + {0XD1E9019F, &WrapI_II, "sceImposeSetParam", 'i', "ii"}, + {0X0BBCA0BF, &WrapI_V, "sceImposeChanges", 'i', "" }, + // sceImposeSetStatus again, for 3.95/4.05, 6.00/6.20 and 6.31/6.39 - identical bodies, and the + // only sceImpose_driver import those shells call. + {0X8434B075, &WrapI_I, "sceImposeSetStatus", 'i', "i" }, + {0X01EF0650, &WrapI_I, "sceImposeSetStatus", 'i', "i" }, + {0X2462EFE4, &WrapI_I, "sceImposeSetStatus", 'i', "i" }, + {0XC10C3D39, &WrapI_I, "sceImposeSetStatus", 'i', "i" }, }; void Register_sceImpose_driver() { diff --git a/Core/HLE/sceKernelHeap.cpp b/Core/HLE/sceKernelHeap.cpp index 7b382124e9..c153ace0a9 100644 --- a/Core/HLE/sceKernelHeap.cpp +++ b/Core/HLE/sceKernelHeap.cpp @@ -202,6 +202,13 @@ const HLEFunction SysMemForKernel[] = { { 0XEB7A74DB, &WrapI_IUU, "sceKernelAllocHeapMemoryWithOption", 'i', "ixp" , HLE_KERNEL_SYSCALL }, { 0x6373995d, &WrapI_V, "sceKernelGetModel", 'i', "", HLE_KERNEL_SYSCALL}, // 220 { 0x07C586A1, &WrapI_V, "sceKernelGetModel", 'i', "", HLE_KERNEL_SYSCALL }, // 220 + // The 5.xx NID for the same call. + { 0xDA07DC6E, &WrapI_V, "sceKernelGetModel", 'i', "", HLE_KERNEL_SYSCALL }, + // 3.95/4.05, 6.00/6.20 and 6.31/6.39 each use another NID again. + { 0x4823B9D9, &WrapI_V, "sceKernelGetModel", 'i', "", HLE_KERNEL_SYSCALL }, + { 0x864EBFD7, &WrapI_V, "sceKernelGetModel", 'i', "", HLE_KERNEL_SYSCALL }, + { 0x458A70B5, &WrapI_V, "sceKernelGetModel", 'i', "", HLE_KERNEL_SYSCALL }, + { 0xA3B0B6BC, &WrapI_V, "sceKernelGetModel", 'i', "", HLE_KERNEL_SYSCALL }, }; void Register_SysMemForKernel() { diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index 44c7030e20..641d150f05 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -1180,22 +1180,36 @@ static void LoadAndStartVshKernelModule(const char *path, SceKernelSMOption *smo // Some of the kernel's drivers have a per-model build (memlmd, loadexec, wlanfirm, ...), and a // firmware installed for one model ships only that model's - so asking for "_01g" unconditionally // fails on, say, an 02g install, which is what PPSSPP's own updater unpack produces by default. -// Swap in the model we're emulating when the path names a model and that file is actually there. +// The naming also changed over time: firmwares older than about 3.50 predate the PSP-2000 and have +// no split at all (plain memlmd.prx, loadexec.prx), and their wlan firmware is named after the +// chip revision instead (wlanfirm_magpie.prx is the one that became wlanfirm_01g.prx). Try the +// emulated model, then the model in the path, then those older spellings, and take whichever is +// actually there. static std::string ResolveVshModelModule(const char *path) { std::string_view name(path); const size_t model = name.find("_01g.prx"); if (model == std::string_view::npos) { return std::string(path); } + const std::string_view stem = name.substr(0, model); + + std::vector candidates; const int generation = (int)EmulatedModelGeneration(); - if (generation == 1) { - return std::string(path); + if (generation != 1) { + candidates.push_back(StringFromFormat("%.*s_%02dg.prx", (int)stem.size(), stem.data(), generation)); } - std::string candidate = StringFromFormat("%.*s_%02dg.prx", (int)model, path, generation); - if (pspFileSystem.GetFileInfo(candidate).exists) { - return candidate; + candidates.push_back(std::string(path)); + if (endsWith(stem, "wlanfirm")) { + candidates.push_back(std::string(stem) + "_magpie.prx"); } - // A dump unpacked for every model has the 01g one too, so this isn't necessarily a failure. + candidates.push_back(std::string(stem) + ".prx"); + + for (const std::string &candidate : candidates) { + if (pspFileSystem.GetFileInfo(candidate).exists) { + return candidate; + } + } + // Nothing matched - hand back the original so the loader reports it by the name we asked for. return std::string(path); } @@ -1225,7 +1239,14 @@ static void LoadAndStartVshKernelModules() { smallStackOption.stacksize = 0x40000; */ for (const char *path : vshSmallKernelModulePaths) { - LoadAndStartVshKernelModule(ResolveVshModelModule(path).c_str(), nullptr); + const std::string resolved = ResolveVshModelModule(path); + if (!pspFileSystem.GetFileInfo(resolved).exists) { + // Older firmwares don't have all of these - lowio.prx only appears around 3.52 - and a + // driver that isn't in the dump isn't a failure to report. + INFO_LOG(Log::sceModule, "LoadAndStartVshKernelModules: %s isn't in this firmware, skipping", resolved.c_str()); + continue; + } + LoadAndStartVshKernelModule(resolved.c_str(), nullptr); } // Firmwares up to about 4.05 keep scePaf's heap allocator in a module of its own, which paf @@ -3190,12 +3211,16 @@ 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. + // The 1.x NID for sceKernelLoadModuleVSH - same function. + // This is how the VSH loads its own plugins. {0xA4370E7C, &WrapU_CUU, "sceKernelLoadModuleVSH", 'x', "sxx", HLE_KERNEL_SYSCALL }, + // And the 5.x NID for it. + {0xCCDE84A8, &WrapU_CUU, "sceKernelLoadModuleVSH", 'x', "sxx", HLE_KERNEL_SYSCALL }, + // And the four remaining NIDs it has had. + {0xFE586962, &WrapU_CUU, "sceKernelLoadModuleVSH", 'x', "sxx", HLE_KERNEL_SYSCALL }, + {0x329C89DB, &WrapU_CUU, "sceKernelLoadModuleVSH", 'x', "sxx", HLE_KERNEL_SYSCALL }, + {0x8909A807, &WrapU_CUU, "sceKernelLoadModuleVSH", 'x', "sxx", HLE_KERNEL_SYSCALL }, + {0xBDFEEC4F, &WrapU_CUU, "sceKernelLoadModuleVSH", 'x', "sxx", HLE_KERNEL_SYSCALL }, }; void Register_ModuleMgrForUser() { diff --git a/Core/HLE/sceReg.cpp b/Core/HLE/sceReg.cpp index 49c013b0a1..fbadee3ac9 100644 --- a/Core/HLE/sceReg.cpp +++ b/Core/HLE/sceReg.cpp @@ -38,6 +38,8 @@ struct OpenCategory { }; static int g_openRegistryMode; +// How many sceRegOpenRegistry calls are outstanding. It really is refcounted. +static int g_openRegistryCount; static int g_handleGen; // TODO: The real PSP seems to use memory addresses. Probably it's doing allocations, which we don't really want to do unless we can match them exactly. static std::map g_openCategories; @@ -939,7 +941,10 @@ static const KeyValue tree_CONFIG[] = { // Dump of /REGISTRY static const KeyValue tree_REGISTRY[] = { - { "category_version", ValueType::INT, "", (int)0x66 }, // decimal: 102 + // A real 6.6x PSP has 0x66 here, which is what this tree was dumped from - but the VSH treats a + // category_version higher than the schema it knows as a corrupt registry and offers to reset your + // settings instead of booting. So we go very low. + { "category_version", ValueType::INT, "", 1 }, }; // There might be more categories. @@ -959,12 +964,14 @@ enum RegOpenMode { void __RegInit() { g_openRegistryMode = 0; + g_openRegistryCount = 0; g_handleGen = 1337; g_openCategories.clear(); } void __RegShutdown() { g_openCategories.clear(); + g_openRegistryCount = 0; } static const KeyValue *LookupCategory(std::string_view path, int *count) { @@ -1003,11 +1010,17 @@ static const KeyValue *LookupCategory(std::string_view path, int *count) { } void __RegDoState(PointerWrap &p) { - auto s = p.Section("sceReg", 0, 1); + auto s = p.Section("sceReg", 0, 2); if (!s) return; Do(p, g_openRegistryMode); Do(p, g_openCategories); + if (s >= 2) { + Do(p, g_openRegistryCount); + } else { + // Old states didn't track this. Anything with a category open had the registry open too. + g_openRegistryCount = g_openCategories.empty() ? 0 : 1; + } } // Registry level (it seems only /system can exist, so kinda pointless) @@ -1017,6 +1030,7 @@ int sceRegOpenRegistry(u32 regParamAddr, int mode, u32 regHandleAddr) { Memory::WriteUnchecked_U32(0, regHandleAddr); } g_openRegistryMode = mode; + g_openRegistryCount++; if (g_openRegistryMode != REG_OPEN_READONLY) { WARN_LOG(Log::HLE, "sceRegOpenRegistry: Opening registry in non-readonly mode. This is not yet supported (we'll simply emulate it as read-only anyway)."); @@ -1029,7 +1043,12 @@ int sceRegCloseRegistry(int regHandle) { if (regHandle != 0) { return hleLogError(Log::sceReg, SCE_REG_ERROR_REGISTRY_NOT_FOUND); } - g_openCategories.clear(); + if (g_openRegistryCount > 0) { + g_openRegistryCount--; + } + if (g_openRegistryCount == 0) { + g_openCategories.clear(); + } return hleLogInfo(Log::sceReg, 0); } diff --git a/docs/VSHBootInvestigation.md b/docs/VSHBootInvestigation.md index c1dbf47bf6..157cbbfe19 100644 --- a/docs/VSHBootInvestigation.md +++ b/docs/VSHBootInvestigation.md @@ -68,15 +68,69 @@ changing repeats byte-identically, and a live menu does not. ## Which firmware versions work -**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. +**All 40 of them.** Every version 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, reaches an interactive XMB with no error on screen. -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. +An earlier pass through this also concluded "all of them", but what it measured was that the shell +*renders*, and several versions did that while showing nothing but the wallpaper or a full-screen +error. The check now is a picture: boot with `--vsh --graphics=software --screenshot-save` and look +at it. Two things about that: + +- **`--timeout` is wall-clock seconds, not emulated ones.** A shell drawing a full XMB runs far + slower than one stuck on a flat background, so a short timeout makes a working version look + stuck. 120 seconds is comfortable for all of them. +- **Judge by the image, not the file size** - though the failure modes do have recognisable sizes, + and none of them is subtle. + +### What was in the way, in the order it was found + +Five distinct causes, each of which stopped a whole band of versions: + +- **Three kernel calls under NIDs we didn't have.** `sceKernelLoadModuleVSH` is how the shell loads + its own plugins, so without it nothing drawable ever loads and the screen stays black; it has six + NIDs across the range. Same story for `sceKernelGetModel` (seven) and `sceImposeSetStatus` (five). + An unresolved GetModel meant the shell read a garbage model number and went looking for PSP-3000 + resources on a dump that has none. + + Finding them is mechanical, and worth writing down because it generalises. For each firmware, + disassemble the module that exports the function and look for the body you already know from + 6.61: `sceKernelLoadModuleVSH` is the `modulemgr.prx` export whose callees are + `sceKernelIsIntrContext`, `sceIoOpen`/`Ioctl`/`Close` and `sceKernelGetUserLevel`; + `sceKernelGetModel` is whatever `vshbridge.prx` wraps in a `sceKernelGetUserLevel() < 4` check + and re-exports. Several exports share that shape, so cross-check against the boot log: the one + that matters is the import the shell actually calls, which the "Unknown syscall (run)" lines name. + +- **Missing `sceResmgr` keys.** `flash0:/vsh/etc/index_XXg.dat` is the index of what the XMB shows + and is encrypted; a shell whose key is missing loads every resource successfully and still has + nothing to put in the menu, so it gives up with the red error screen. Each generation has its own + tag, one per PSP model from 5.03 on and a single unsuffixed `index.dat` before that. 1.50 through + 2.50 don't use it at all. + + The keys are in each firmware's own `mesg_led*.prx`, in a table of 24-byte entries - 4-byte tag, + 16-byte key, 4 bytes of padding. Search the decrypted module for the tag as a little-endian word + and read the next 16 bytes. **Validate the hit before believing it**: locate a tag that + `PrxDecrypter.cpp` already has a key for and check it matches byte for byte. Extracting the 6.6x + triple this way reproduces `keys_9DC14891_1/2/3` exactly, which is what established the layout in + the first place. Don't try to walk the table blind - guessing the grid alignment produces + plausible-looking garbage out of ordinary MIPS code, and the 2.5x-era table isn't on that grid + at all. + +- **`sceImposeGetParam(0x40000000)`.** A real setting in every `impose.prx` from 1.50 to 5.55, which + we rejected as not a parameter at all. 3.11's shell read the error back, blanked the display with + `sceDisplaySetFrameBuf(0, 0, 0)` and never turned it on again - so the screen kept whatever + uninitialised VRAM held while a perfectly healthy frame loop ran behind it. 2.00, 3.03 and 3.11 + all turned on this one value. + +- **`category_version` in the registry.** Our `sceReg` serves a compiled-in snapshot of a 6.6x PSP's + registry, `/REGISTRY/category_version` included. Every shell checks it and treats a version higher + than the schema it knows as a corrupt registry, offering to reset your settings instead of + booting - which is what 1.50 through 5.50 did. The check is one-directional, older is always + accepted, and nothing tried to migrate anything, so we report 1. + +- **`sceRegCloseRegistry` dropping categories another opener still owned.** See `sceReg.cpp`; the + VSH's alarm scan nests registry opens and this cost it its own open category. ### Sony renumbered the kernel NIDs, and that is what blocked everything below 6.60 diff --git a/headless/Headless.cpp b/headless/Headless.cpp index fcb61f6604..f49f66d4ea 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -79,6 +79,7 @@ static Path g_comparisonScreenshot; static Path g_screenshotSavePath; static Path g_screenshotDiffPath; static bool g_screenshotSaveKeepAlpha = false; +static bool g_screenshotSaved = false; static double g_maxScreenshotError = 0.0; static bool g_screenshotFailed = false; static std::string g_debugOutputBuffer; @@ -207,6 +208,7 @@ void SendDebugScreenshot(const DebugScreenshotDesc &desc) { if (!g_screenshotSavePath.empty()) { ScreenshotComparer saver(pixels, FRAME_STRIDE, FRAME_WIDTH, FRAME_HEIGHT); bool saved = g_screenshotSavePath.GetFileExtension() == ".png" ? saver.SaveActualPNG(g_screenshotSavePath, g_screenshotSaveKeepAlpha) : saver.SaveActualBitmap(g_screenshotSavePath); + g_screenshotSaved = g_screenshotSaved || saved; if (saved) SendAndCollectOutput("Screenshot saved to: " + g_screenshotSavePath.ToVisualString() + "\n"); } @@ -302,6 +304,9 @@ static bool RunAutoTest(GraphicsContext *graphicsContext, CoreParameter &corePar // Kinda ugly, trying to guesstimate the test name from filename... currentTestName = GetTestName(coreParameter.fileToStart); g_screenshotFailed = false; + // Per test, so a test that emits one of its own doesn't stop the next one getting the end-of-run + // capture below. + g_screenshotSaved = false; std::string output; if (opt.compare || opt.bench) { @@ -406,6 +411,12 @@ static bool RunAutoTest(GraphicsContext *graphicsContext, CoreParameter &corePar draw->EndFrame(); } + if (!g_screenshotSavePath.empty() && !g_screenshotSaved) { + // SendDebugScreenshot ignores the descriptor and reads the display framebuffer from the GPU + // itself, so there's nothing to fill in here. + SendDebugScreenshot(DebugScreenshotDesc{}); + } + PSP_Shutdown(true); if (!opt.bench) {