From 4b74eccc38ecdc7d246ac262c2e9b38554b39ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 2 Nov 2026 23:36:06 +0100 Subject: [PATCH] sceReg: Set category_version to the installed firmware's schema version The shell writes the default of every registry key added after the stored category_version, and some of those (umd_autoboot) are now mapped to our config. Each firmware's version is the default in its registry schema table, in vshmain.prx (common_util.prx before 2.00). Installing a firmware now stores it, and headless sets it from the NAND's firmware. Also builds the config setting lookup in headless and libretro: without g_Config.Init(), sceReg served its static dump instead of the mapped config values. Co-Authored-By: Claude Opus 5.5 --- Core/Util/PSARUnpack.cpp | 32 ++++++++++++++++++++++++++++++++ Core/Util/PSARUnpack.h | 3 +++ headless/Headless.cpp | 10 ++++++++++ libretro/libretro.cpp | 2 ++ 4 files changed, 47 insertions(+) diff --git a/Core/Util/PSARUnpack.cpp b/Core/Util/PSARUnpack.cpp index 6df7a209af..320bcc4cf3 100644 --- a/Core/Util/PSARUnpack.cpp +++ b/Core/Util/PSARUnpack.cpp @@ -1343,6 +1343,33 @@ bool EraseInstalledFirmware(const Path &nandRoot, std::string *error) { return success; } +int SceRegCategoryVersionForFirmware(std::string_view version) { + // The default of /REGISTRY/category_version in each firmware's registry schema, a table of + // { name, type, size, default } entries in vsh/module/vshmain.prx (common_util.prx before 2.00). + // When the stored version is below this, the shell writes the default of every key added since + // that version (from 1, that includes umd_autoboot). One above it means a corrupt registry. + static const struct { + int firmware; // The first firmware with this version, as FirmwareVersionToInt() gives it. + int categoryVersion; + } versions[] = { + { 100, 0x00 }, { 150, 0x01 }, { 200, 0x06 }, { 250, 0x13 }, { 260, 0x14 }, { 270, 0x1b }, + { 280, 0x20 }, { 300, 0x26 }, { 310, 0x2c }, { 330, 0x31 }, { 350, 0x35 }, { 360, 0x36 }, + { 370, 0x3b }, { 380, 0x3e }, { 395, 0x40 }, { 405, 0x43 }, { 420, 0x46 }, { 500, 0x4c }, + { 550, 0x58 }, { 570, 0x61 }, { 600, 0x63 }, { 620, 0x64 }, { 630, 0x66 }, + }; + const int fw = FirmwareVersionToInt(version); + if (fw < versions[0].firmware) { + return 1; + } + int categoryVersion = versions[0].categoryVersion; + for (const auto &v : versions) { + if (fw >= v.firmware) { + categoryVersion = v.categoryVersion; + } + } + return categoryVersion; +} + int FirmwareVersionToInt(std::string_view version) { const size_t dot = version.find('.'); if (dot == std::string_view::npos || dot == 0 || dot + 1 >= version.size()) { @@ -1674,6 +1701,11 @@ bool InstallFirmware(const Path &updater, const Path &nandRoot, const PSARUnpack if (ok) { ok = SwapStagingIntoPlace(nandRoot, staging, error); } + if (ok) { + // Match the registry to the new firmware's schema, so its shell doesn't reset the + // settings (which sceReg maps to our config) to its defaults at first boot. + g_Config.iSceRegCategoryVersion = SceRegCategoryVersionForFirmware(stats->firmwareVersion); + } // Whether it worked or not, don't leave the staging copy behind. if (File::Exists(staging) && !File::DeleteDirRecursively(staging)) { diff --git a/Core/Util/PSARUnpack.h b/Core/Util/PSARUnpack.h index 4663fb71b8..ab9abe471d 100644 --- a/Core/Util/PSARUnpack.h +++ b/Core/Util/PSARUnpack.h @@ -181,6 +181,9 @@ bool EraseInstalledFirmware(const Path &nandRoot, std::string *error); // makes the result safe to compare with: an unknown version is older than every real one. int FirmwareVersionToInt(std::string_view version); +// The /REGISTRY/category_version the shell of this firmware expects. 1 for an unknown version. +int SceRegCategoryVersionForFirmware(std::string_view version); + // The firmware versions we can actually boot the VSH (XMB) on. Every other version loads, but // the module patches it needs are version-specific, so it won't get anywhere. bool FirmwareVersionSupportsVSH(std::string_view version); diff --git a/headless/Headless.cpp b/headless/Headless.cpp index f645780e76..7847602ca9 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -941,6 +941,8 @@ int main(int argc, const char* argv[]) { #endif PROFILE_INIT(); TimeInit(); + // Builds the pointer to setting lookup, which sceReg's registry mapping uses. + g_Config.Init(); #if PPSSPP_PLATFORM(WINDOWS) if (!IsDebuggerPresent()) { SetCleanExitOnAssert(); @@ -1445,6 +1447,14 @@ int main(int argc, const char* argv[]) { coreParameter.nandRoot = nand; } + // Headless doesn't keep a config, so set the registry version that installing this firmware + // in the app would have stored. + { + InstalledFirmwareInfo firmwareInfo; + ReadInstalledFirmwareInfo(g_Config.nandRootDirectory, &firmwareInfo, false); + g_Config.iSceRegCategoryVersion = SceRegCategoryVersionForFirmware(firmwareInfo.version); + } + // Placed here rather than with the other early-exit subcommands above, because resolving a // "flash0:/kd/foo.prx" module path needs nandRootDirectory, which is only settled just above. if (cmdLineOptions.reDecrypt.has_value()) { diff --git a/libretro/libretro.cpp b/libretro/libretro.cpp index b6f3fa5c76..95052bd89f 100644 --- a/libretro/libretro.cpp +++ b/libretro/libretro.cpp @@ -1225,6 +1225,8 @@ void retro_init(void) if (environ_cb(RETRO_ENVIRONMENT_GET_INPUT_BITMASKS, NULL)) libretro_supports_bitmasks = true; + // Builds the pointer to setting lookup, which sceReg's registry mapping uses. + g_Config.Init(); g_Config.Load("", ""); g_Config.iInternalResolution = 0;