Merge pull request #22281 from hrydgard/firmware-vsh-boot

Boot to XMB on all (probably) firmware versions
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2026-09-10 15:46:14 -06:00
commit 1d7c427eba
9 files changed
+192 -32

No files matched your search

+8 -2
View File
@@ -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
+1
View File
@@ -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},
+28
View File
@@ -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 },
+15 -6
View File
@@ -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>, "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>, "sceImposeGetParam", 'i', "i" },
{0XB415FC59, &WrapI_V<sceImposeChanges>, "sceImposeChanges", 'i', "" },
// And the 5.xx NIDs for four of them, identified by code.
{0XC860DB52, &WrapI_I<sceImposeSetStatus>, "sceImposeSetStatus", 'i', "i" },
{0X4B02F047, &WrapI_I<sceImposeGetParam>, "sceImposeGetParam", 'i', "i" },
{0XD1E9019F, &WrapI_II<sceImposeSetParam>, "sceImposeSetParam", 'i', "ii"},
{0X0BBCA0BF, &WrapI_V<sceImposeChanges>, "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>, "sceImposeSetStatus", 'i', "i" },
{0X01EF0650, &WrapI_I<sceImposeSetStatus>, "sceImposeSetStatus", 'i', "i" },
{0X2462EFE4, &WrapI_I<sceImposeSetStatus>, "sceImposeSetStatus", 'i', "i" },
{0XC10C3D39, &WrapI_I<sceImposeSetStatus>, "sceImposeSetStatus", 'i', "i" },
};
void Register_sceImpose_driver() {
+7
View File
@@ -202,6 +202,13 @@ const HLEFunction SysMemForKernel[] = {
{ 0XEB7A74DB, &WrapI_IUU<sceKernelAllocHeapMemoryWithOption>, "sceKernelAllocHeapMemoryWithOption", 'i', "ixp" , HLE_KERNEL_SYSCALL },
{ 0x6373995d, &WrapI_V<sceKernelGetModel>, "sceKernelGetModel", 'i', "", HLE_KERNEL_SYSCALL}, // 220
{ 0x07C586A1, &WrapI_V<sceKernelGetModel>, "sceKernelGetModel", 'i', "", HLE_KERNEL_SYSCALL }, // 220
// The 5.xx NID for the same call.
{ 0xDA07DC6E, &WrapI_V<sceKernelGetModel>, "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>, "sceKernelGetModel", 'i', "", HLE_KERNEL_SYSCALL },
{ 0x864EBFD7, &WrapI_V<sceKernelGetModel>, "sceKernelGetModel", 'i', "", HLE_KERNEL_SYSCALL },
{ 0x458A70B5, &WrapI_V<sceKernelGetModel>, "sceKernelGetModel", 'i', "", HLE_KERNEL_SYSCALL },
{ 0xA3B0B6BC, &WrapI_V<sceKernelGetModel>, "sceKernelGetModel", 'i', "", HLE_KERNEL_SYSCALL },
};
void Register_SysMemForKernel() {
+38 -13
View File
@@ -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<std::string> 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>, "sceKernelSelfStopUnloadModule", 'x', "xxx", HLE_KERNEL_SYSCALL },
{0xD5DDAB1F, &WrapU_CUU<sceKernelLoadModuleVSH>, "sceKernelLoadModuleVSH", 'x', "sxx", HLE_KERNEL_SYSCALL },
{0xD86DD11B, &WrapU_C<sceKernelSearchModuleByName>, "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>, "sceKernelLoadModuleVSH", 'x', "sxx", HLE_KERNEL_SYSCALL },
// And the 5.x NID for it.
{0xCCDE84A8, &WrapU_CUU<sceKernelLoadModuleVSH>, "sceKernelLoadModuleVSH", 'x', "sxx", HLE_KERNEL_SYSCALL },
// And the four remaining NIDs it has had.
{0xFE586962, &WrapU_CUU<sceKernelLoadModuleVSH>, "sceKernelLoadModuleVSH", 'x', "sxx", HLE_KERNEL_SYSCALL },
{0x329C89DB, &WrapU_CUU<sceKernelLoadModuleVSH>, "sceKernelLoadModuleVSH", 'x', "sxx", HLE_KERNEL_SYSCALL },
{0x8909A807, &WrapU_CUU<sceKernelLoadModuleVSH>, "sceKernelLoadModuleVSH", 'x', "sxx", HLE_KERNEL_SYSCALL },
{0xBDFEEC4F, &WrapU_CUU<sceKernelLoadModuleVSH>, "sceKernelLoadModuleVSH", 'x', "sxx", HLE_KERNEL_SYSCALL },
};
void Register_ModuleMgrForUser() {
+22 -3
View File
@@ -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<int, OpenCategory> 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);
}
+62 -8
View File
@@ -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
+11
View File
@@ -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) {