Merge pull request #22271 from hrydgard/firmware-screen

Add a Tools/PSP Firmware screen, improve compat with older firmwares
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2026-09-09 16:50:13 -06:00
commit 9356a3acbc
21 files changed
+788 -37

No files matched your search

+55
View File
@@ -61,6 +61,8 @@ static const u8 keys500_c[] = {0xA3, 0x5D, 0x51, 0xE6, 0x56, 0xC8, 0x01, 0xCA, 0
static const u8 keys505_a[] = {0x7B, 0x94, 0x72, 0x27, 0x4C, 0xCC, 0x54, 0x3B, 0xAE, 0xDF, 0x46, 0x37, 0xAC, 0x01, 0x4D, 0x87};
static const u8 keys505_0[] = {0x2E, 0x8E, 0x97, 0xA2, 0x85, 0x42, 0x70, 0x73, 0x18, 0xDA, 0xA0, 0x8A, 0xF8, 0x62, 0xA2, 0xB0};
static const u8 keys505_1[] = {0x58, 0x2A, 0x4C, 0x69, 0x19, 0x7B, 0x83, 0x3D, 0xD2, 0x61, 0x61, 0xFE, 0x14, 0xEE, 0xAA, 0x11};
static const u8 keys555_k1[] = {0x9F, 0xFD, 0x4C, 0x28, 0x20, 0xB1, 0x3E, 0x76, 0x36, 0x4A, 0xAB, 0x1C, 0x54, 0xBC, 0x3B, 0xDC};
static const u8 keys555_k2[] = {0xAB, 0x1A, 0x74, 0x43, 0xF7, 0x4F, 0xE5, 0xFF, 0x04, 0xA5, 0xFC, 0x3B, 0xEC, 0xD4, 0xF8, 0xF0};
static const u8 keys570_5k[] = {0x6D, 0x72, 0xA4, 0xBA, 0x7F, 0xBF, 0xD1, 0xF1, 0xA9, 0xF3, 0xBB, 0x07, 0x1B, 0xC0, 0xB3, 0x66};
static const u8 keys600_1[] = {0xE3, 0x52, 0x39, 0x97, 0x3B, 0x84, 0x41, 0x1C, 0xC3, 0x23, 0xF1, 0xB8, 0xA9, 0x09, 0x4B, 0xF0};
static const u8 keys600_2[] = {0xE1, 0x45, 0x93, 0x2C, 0x53, 0xE2, 0xAB, 0x06, 0x6F, 0xB6, 0x8F, 0x0B, 0x66, 0x91, 0xE7, 0x1E};
@@ -404,6 +406,8 @@ static const TAG_INFO2 g_tagInfo2[] =
{ 0x4C9422F0, keys600_2, 0x43 },
{ 0x4C941EF0, keys600_1, 0x43 },
{ 0x4C9429F0, keys570_5k, 0x43 },
{ 0x4C941BF0, keys555_k2, 0x43 },
{ 0x4C941AF0, keys555_k1, 0x43 },
{ 0x457B0BF0, keys505_a, 0x5B },
{ 0x4C9419F0, keys505_1, 0x43 },
{ 0x4C9418F0, keys505_0, 0x43 },
@@ -780,6 +784,27 @@ struct PRXType9
};
static_assert(sizeof(PRXType9) == 0x150, "inconsistent size of PRX Type 9");
// KIRK CMD1 writes the header plus data_offset plus data_size (rounded up to 16) bytes into
// outbuf, and all three of those come out of the header we just decrypted rather than from the
// caller. The SHA1 check above doesn't bound them - it only covers the header, so it passes just
// as happily for a block that's been cut short as for a whole one. A caller that has to guess how
// long a block is (the PSAR walker does: an updater doesn't record the length of its second
// block, so it tries the sizes real updaters use) then hands us a size that's too small and KIRK
// runs off the end of the buffer. That showed up as an intermittent crash unpacking any official
// updater, since whether the overrun lands on an unmapped page depends on the heap layout.
static bool KirkOutputFits(u32 headerSize, u32 dataOffset, u32 dataSize, u32 size) {
const u64 alignedDataSize = ((u64)dataSize + 15) & ~(u64)15;
return (u64)headerSize + (u64)dataOffset + alignedDataSize <= (u64)size;
}
static bool KirkOutputFits(const KIRK_CMD1_HEADER *header, u32 size) {
return KirkOutputFits(sizeof(KIRK_CMD1_HEADER), header->data_offset, header->data_size, size);
}
static bool KirkOutputFits(const KIRK_CMD1_ECDSA_HEADER *header, u32 size) {
return KirkOutputFits(sizeof(KIRK_CMD1_ECDSA_HEADER), header->data_offset, header->data_size, size);
}
static int pspDecryptType0(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 size) {
DEBUG_LOG(Log::Loader, "Decrypting tag %02X", (u32)*(u32_le *)&inbuf[0xD0]);
const auto decryptSize = *(s32_le*)&inbuf[0xB0];
@@ -830,6 +855,11 @@ static int pspDecryptType0(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 siz
memcpy(reinterpret_cast<u8*>(header)+sizeof(KIRK_CMD1_HEADER), type0.prxHeader, sizeof(type0.prxHeader));
decryptKirkHeaderType0(reinterpret_cast<u8*>(header), type0.kirkBlock, xorbuf, pti->code);
if (!KirkOutputFits(header, size))
{
return -5;
}
if (kirk_sceUtilsBufferCopyWithRange(kirk, outbuf, size, reinterpret_cast<u8*>(header), size - offset, KIRK_CMD_DECRYPT_PRIVATE) != 0)
{
return -4;
@@ -885,6 +915,11 @@ static int pspDecryptType1(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 siz
memcpy(reinterpret_cast<u8*>(header)+sizeof(KIRK_CMD1_HEADER), type1.prxHeader, sizeof(type1.prxHeader));
decryptKirkHeaderType0(reinterpret_cast<u8*>(header), type1.kirkBlock, xorbuf, pti->code);
if (!KirkOutputFits(header, size))
{
return -5;
}
if (kirk_sceUtilsBufferCopyWithRange(kirk, outbuf, size, reinterpret_cast<u8*>(header), size - offset, KIRK_CMD_DECRYPT_PRIVATE) != 0)
{
return -4;
@@ -949,6 +984,11 @@ static int pspDecryptType2(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 siz
decryptKirkHeader(reinterpret_cast<u8*>(header), type2.kirkHeader, xorbuf.cbegin()+0x10, pti->code);
header->mode = 1;
if (!KirkOutputFits(header, size))
{
return -5;
}
if (kirk_sceUtilsBufferCopyWithRange(kirk, outbuf, size, reinterpret_cast<u8*>(header), size - offset, KIRK_CMD_DECRYPT_PRIVATE) != 0)
{
return -4;
@@ -1017,6 +1057,11 @@ static int pspDecryptType5(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 siz
decryptKirkHeader(reinterpret_cast<u8*>(header), type5.kirkHeader, xorbuf.cbegin()+0x10, pti->code);
header->mode = 1;
if (!KirkOutputFits(header, size))
{
return -5;
}
if (kirk_sceUtilsBufferCopyWithRange(kirk, outbuf, size, reinterpret_cast<u8*>(header), size - offset, KIRK_CMD_DECRYPT_PRIVATE) != 0)
{
return -4;
@@ -1084,6 +1129,11 @@ static int pspDecryptType6(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 siz
header->mode = 1;
header->ecdsa_hash = 1;
if (!KirkOutputFits(header, size))
{
return -5;
}
if (kirk_sceUtilsBufferCopyWithRange(kirk, outbuf, size, reinterpret_cast<u8*>(header), size - offset, KIRK_CMD_DECRYPT_PRIVATE) != 0)
{
return -4;
@@ -1156,6 +1206,11 @@ static int pspDecryptType9(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 siz
// branch only writes the mode word and zeroes the rest of that region.
header->ecdsa_hash = 0;
if (!KirkOutputFits(header, size))
{
return -5;
}
if (kirk_sceUtilsBufferCopyWithRange(kirk, outbuf, size, reinterpret_cast<u8*>(header), size - offset, KIRK_CMD_DECRYPT_PRIVATE) != 0)
{
return -4;
+14
View File
@@ -91,6 +91,20 @@ const HLEFunction sceHprm_driver[] =
// Purpose unknown - JPCSP names it after its NID too, and returns 0. Present so the VSH's
// one startup call resolves instead of trapping.
{0XDC895B2B, &WrapU_V<sceHprm_driver_DC895B2B>, "sceHprm_driver_DC895B2B", 'x', ""},
// Older firmwares number these two differently. Same functions - matched by address against
// the same module's user-mode sceHprm exports (sceHprmReadLatch is sceHprm/0x40D2F9F0 in
// every build). The VSH calls ReadLatch once a frame, so without these an older firmware's
// XMB spends the whole boot trapping on an unresolved import.
// NOTE: new entries go at the end - the syscall opcode in a savestate is an index into this array.
{0XA3A87975, &WrapU_U<sceHprmReadLatch>, "sceHprmReadLatch", 'x', "x"}, // 6.31 - 6.39
{0X5FC5E53B, &WrapU_U<sceHprmReadLatch>, "sceHprmReadLatch", 'x', "x"}, // 6.00 - 6.20
{0X748FC3C8, &WrapU_V<sceHprm_driver_DC895B2B>, "sceHprm_driver_DC895B2B", 'x', ""}, // 6.31 - 6.39
{0X605DEA7A, &WrapU_U<sceHprmReadLatch>, "sceHprmReadLatch", 'x', "x"}, // 5.03 - 5.55
{0XA6E8D4F0, &WrapU_U<sceHprmReadLatch>, "sceHprmReadLatch", 'x', "x"}, // 3.95 - 4.05
{0X8C728076, &WrapU_U<sceHprmReadLatch>, "sceHprmReadLatch", 'x', "x"}, // 3.72 - 3.90
{0XF0AA1FB9, &WrapU_U<sceHprmReadLatch>, "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>, "sceHprmReadLatch", 'x', "x"}, // 1.50 - 3.51
};
void Register_sceHprm_driver()
+9
View File
@@ -262,6 +262,15 @@ const HLEFunction sceImpose_driver[] = {
{0XBB12F974, &WrapI_I<sceImposeSetStatus>, "sceImposeSetStatus", 'i', "i" },
// The 6.60 name for the same call the user-mode module exports as 0x8C943191.
{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.
{0X531C9778, &WrapI_I<sceImposeGetParam>, "sceImposeGetParam", 'i', "i" },
{0XB415FC59, &WrapI_V<sceImposeChanges>, "sceImposeChanges", 'i', "" },
};
void Register_sceImpose_driver() {
+90 -22
View File
@@ -50,6 +50,7 @@
#include "Core/ELF/PrxDecrypter.h"
#include "Core/HLE/scePspNpDrm_user.h"
#include "Core/Util/KL4E.h"
#include "Core/Util/PSARUnpack.h"
#include "Core/FileSystems/FileSystem.h"
#include "Core/FileSystems/MetaFileSystem.h"
#include "Core/Util/BlockAllocator.h"
@@ -1176,6 +1177,28 @@ 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.
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 int generation = (int)EmulatedModelGeneration();
if (generation == 1) {
return std::string(path);
}
std::string candidate = StringFromFormat("%.*s_%02dg.prx", (int)model, path, generation);
if (pspFileSystem.GetFileInfo(candidate).exists) {
return candidate;
}
// A dump unpacked for every model has the 01g one too, so this isn't necessarily a failure.
return std::string(path);
}
static void LoadAndStartVshKernelModules() {
// These 11 are small, simple kernel drivers (a few KB to ~100KB of code each) that don't
// declare their own smaller module_start_thread_stacksize, so __KernelStartModule's
@@ -1202,7 +1225,16 @@ static void LoadAndStartVshKernelModules() {
smallStackOption.stacksize = 0x40000;
*/
for (const char *path : vshSmallKernelModulePaths) {
LoadAndStartVshKernelModule(path, nullptr);
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[] = {
@@ -1513,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);
}
@@ -1544,27 +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")) {
// NOTE: This hackery is likely firmware-version-specific, so will need tweaking for
// other firmware versions than 6.61.
//
// 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.
const u32 scePafHeapArenaOffset = 0x18D728; // Offset from module base to the BSS pointer slot.
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->memoryBlockAddr + scePafHeapArenaOffset;
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")) {
@@ -1587,12 +1638,23 @@ static PSPModule *__KernelLoadELFFromPtr(const u8 *ptr, size_t elfSize, u32 load
// would look like (the scan's own code already handles count<=0 as "nothing to do"
// for category 0 the same way). This is a narrow, targeted patch of one 4-byte value
// vsh_module itself never properly initializes, not a general vsh_module patch.
//
// Unlike the scePaf patch above, this offset is into rodata rather than at a fixed
// distance from gp, and it moves with the build - so check that what's there is the
// value we identified before overwriting it, rather than writing blind into a firmware
// we haven't looked at. 6.60 and 6.61 ship byte-identical builds of vshmain.prx and
// both have the same float here; anything else is a version this patch wasn't derived
// from, and those don't reach an XMB for other reasons anyway.
const u32 vshAlarmCategory1CountOffset = 0x455C4; // Offset from module base.
const u32 vshAlarmCategory1CountExpected = 0x3F666666; // Leftover 0.9f from a float array.
u32 patchAddr = module->memoryBlockAddr + vshAlarmCategory1CountOffset;
if (Memory::IsValid4AlignedAddress(patchAddr)) {
Memory::WriteUnchecked_U32(0, patchAddr);
} else {
if (!Memory::IsValid4AlignedAddress(patchAddr)) {
WARN_LOG(Log::sceModule, "Failed to patch vsh_module alarm category 1 count");
} else if (Memory::ReadUnchecked_U32(patchAddr) != vshAlarmCategory1CountExpected) {
WARN_LOG(Log::sceModule, "vsh_module isn't the build the alarm-category patch was derived from (%08x at +%x), leaving it alone",
Memory::ReadUnchecked_U32(patchAddr), vshAlarmCategory1CountOffset);
} else {
Memory::WriteUnchecked_U32(0, patchAddr);
}
}
@@ -3126,6 +3188,12 @@ 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.
{0xA4370E7C, &WrapU_CUU<sceKernelLoadModuleVSH>, "sceKernelLoadModuleVSH", 'x', "sxx", HLE_KERNEL_SYSCALL },
};
void Register_ModuleMgrForUser() {
+27 -2
View File
@@ -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>, "sceRtcGetLastReincarnatedTime", 'i', "x" },
{0X7D1FBED3, &WrapI_UU<sceRtcSetAlarmTick>, "sceRtcSetAlarmTick", 'i', "xx" },
{0XF5FCC995, nullptr, "sceRtcGetCurrentNetworkTick", '?', "" },
{0X81FCDA34, nullptr, "sceRtcIsAlarmed", '?', "" },
{0X81FCDA34, &WrapI_V<sceRtcIsAlarmed>, "sceRtcIsAlarmed", 'i', "" },
{0XFB3B18CD, nullptr, "sceRtcRegisterCallback", '?', "" },
{0X6A676D2D, nullptr, "sceRtcUnregisterCallback", '?', "" },
{0XC2DDBEB5, &WrapI_U<sceRtcGetAlarmTick>, "sceRtcGetAlarmTick", 'i', "x" },
@@ -1154,14 +1162,31 @@ void Register_sceRtc()
RegisterHLEModule("sceRtc", ARRAY_SIZE(sceRtc), sceRtc);
}
// sceRtc_driver is the kernel-only alias some firmware-660+ modules (e.g. the VSH's
// sceRtc_driver is the kernel-only alias some firmware modules (e.g. the VSH's
// sceVshBridge_Driver) import from instead of plain sceRtc - same underlying functions,
// just also exported under a second, kernel-suffixed module name. Confirmed by cross-
// checking jpcsp's sceRtc.java, which registers 0xE09880CF as an alternate NID on the exact
// same sceRtcSetAlarmTick() method (JPCSP doesn't distinguish import module names the way
// PPSSPP's HLE dispatch does, but the NID->function mapping is the same either way).
//
// Sony renumbered the kernel NIDs across firmware versions, so the same function has three of
// them. All three are the export at rtc.prx+0xB28, which every one of these builds also
// exports as the user-mode sceRtc/0x7D1FBED3 (sceRtcSetAlarmTick) - that's how they line up.
// Covering the older two matters for the VSH: without the HLE, sceVshBridge_Driver's alarm
// call lands in the real rtc.prx, which goes on into syscon.prx and blocks forever on a
// SceSysconSync semaphore that never gets signalled.
const HLEFunction sceRtc_driver[] = {
{0XE09880CF, &WrapI_UU<sceRtcSetAlarmTick>, "sceRtcSetAlarmTick", 'i', "xx" },
// NOTE: new entries go at the end - the syscall opcode in a savestate is an index into this array.
{0X54B9C589, &WrapI_UU<sceRtcSetAlarmTick>, "sceRtcSetAlarmTick", 'i', "xx" }, // 6.31 - 6.39
{0X68AED59A, &WrapI_UU<sceRtcSetAlarmTick>, "sceRtcSetAlarmTick", 'i', "xx" }, // 6.00 - 6.20
{0XADAF231F, &WrapI_UU<sceRtcSetAlarmTick>, "sceRtcSetAlarmTick", 'i', "xx" }, // 5.03 - 5.55
{0X55AC1C23, &WrapI_UU<sceRtcSetAlarmTick>, "sceRtcSetAlarmTick", 'i', "xx" }, // 3.95 - 4.05
{0X827BCB3F, &WrapI_UU<sceRtcSetAlarmTick>, "sceRtcSetAlarmTick", 'i', "xx" }, // 3.72 - 3.90
{0X329E8E3A, &WrapI_UU<sceRtcSetAlarmTick>, "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>, "sceRtcSetAlarmTick", 'i', "xx" }, // 2.71 - 3.51
};
void Register_sceRtc_driver()
+143
View File
@@ -22,6 +22,7 @@
#include "zlib.h"
#include "Common/File/DirListing.h"
#include "Common/File/FileUtil.h"
#include "Common/File/Path.h"
#include "Common/Log.h"
@@ -1049,6 +1050,148 @@ static bool ReadUpdaterPSAR(const Path &filename, std::vector<u8> *psar, std::st
return false;
}
// Adds up everything under a directory. Returns false if the directory isn't there at all, which
// is how the caller tells "no firmware" from "an empty one".
static bool ScanDirRecursive(const Path &dir, int *fileCount, u64 *totalSize) {
std::vector<File::FileInfo> files;
if (!File::GetFilesInDir(dir, &files)) {
return false;
}
for (const File::FileInfo &file : files) {
if (file.isDirectory) {
ScanDirRecursive(file.fullName, fileCount, totalSize);
} else {
(*fileCount)++;
*totalSize += file.size;
}
}
return true;
}
static int CountFilesInDir(const Path &dir) {
std::vector<File::FileInfo> files;
if (!File::GetFilesInDir(dir, &files)) {
return 0;
}
int count = 0;
for (const File::FileInfo &file : files) {
if (!file.isDirectory) {
count++;
}
}
return count;
}
// flash0:/vsh/etc/version.txt, as the firmware itself writes it:
// release:6.60:
// build:5455,0,3,1,0:builder@vsh-build6
// system:57716@release_660,0x06060010:
// vsh:p6616@release_660,v58533@release_660,20110727:
// target:1:WorldWide
// The date at the end of the vsh line is the only date in there, and the target line's last
// field is the region the firmware was built for.
static void ParseVersionTxt(std::string_view contents, InstalledFirmwareInfo *info) {
std::vector<std::string_view> lines;
SplitString(contents, '\n', lines);
for (std::string_view line : lines) {
line = StripSpaces(line);
if (startsWith(line, "release:")) {
std::string_view rest = line.substr(strlen("release:"));
const size_t colon = rest.find(':');
info->version = std::string(colon == std::string_view::npos ? rest : rest.substr(0, colon));
} else if (startsWith(line, "vsh:")) {
// The build date is the last comma-separated field, e.g. "...,20110727:".
std::string_view rest = line.substr(strlen("vsh:"));
if (!rest.empty() && rest.back() == ':') {
rest = rest.substr(0, rest.size() - 1);
}
const size_t comma = rest.rfind(',');
if (comma != std::string_view::npos) {
const std::string_view date = rest.substr(comma + 1);
if (date.size() == 8 && std::all_of(date.begin(), date.end(), [](char c) { return c >= '0' && c <= '9'; })) {
info->buildDate = StringFromFormat("%.*s-%.*s-%.*s",
4, date.data(), 2, date.data() + 4, 2, date.data() + 6);
}
}
} else if (startsWith(line, "target:")) {
const size_t colon = line.rfind(':');
if (colon != std::string_view::npos && colon + 1 < line.size()) {
info->target = std::string(line.substr(colon + 1));
}
}
}
}
void ReadInstalledFirmwareInfo(const Path &nandRoot, InstalledFirmwareInfo *info) {
*info = InstalledFirmwareInfo{};
const Path flash0 = nandRoot / "flash0";
for (const char *dir : { "flash0", "flash1", "ipl" }) {
ScanDirRecursive(nandRoot / dir, &info->fileCount, &info->totalSize);
}
info->anythingInstalled = info->fileCount > 0;
if (!info->anythingInstalled) {
return;
}
info->fontCount = CountFilesInDir(flash0 / "font");
info->kernelModuleCount = CountFilesInDir(flash0 / "kd");
info->hasVsh = File::Exists(flash0 / "vsh/module/vshmain.prx");
std::string versionTxt;
if (File::ReadTextFileToString(flash0 / "vsh/etc/version.txt", &versionTxt)) {
ParseVersionTxt(versionTxt, info);
}
}
bool EraseInstalledFirmware(const Path &nandRoot, std::string *error) {
bool success = true;
for (const char *dir : { "flash0", "flash1", "ipl" }) {
const Path path = nandRoot / dir;
if (File::Exists(path) && !File::DeleteDirRecursively(path)) {
ERROR_LOG(Log::Loader, "Failed to erase %s", path.c_str());
if (error) {
*error = "Couldn't erase " + path.ToString();
}
success = false;
}
}
return success;
}
bool FirmwareVersionSupportsVSH(std::string_view version) {
// 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).
const size_t dot = version.find('.');
if (dot == std::string_view::npos || dot == 0 || dot + 1 >= version.size()) {
return false;
}
int numeric = 0;
for (size_t i = 0; i < version.size(); i++) {
if (i == dot) {
continue;
}
if (version[i] < '0' || version[i] > '9') {
return false;
}
numeric = numeric * 10 + (version[i] - '0');
}
if (version.size() - dot == 2) { // One digit after the dot.
numeric *= 10;
} else if (version.size() - dot != 3) {
return false;
}
return numeric >= 150;
}
std::string BundledUpdateInfo::Describe() const {
if (!present) {
return std::string();
+29
View File
@@ -141,6 +141,35 @@ bool ReadBundledUpdateInfo(IFileSystem *fs, std::string_view pathPrefix, Bundled
// decryption needed, so it's cheap enough to check every disc with. Empty if there's no updater.
std::string ReadUpdaterVersion(const Path &filename);
// What's actually in the NAND directory right now. That can be anything from a handful of fonts
// we pulled off a game disc to a full firmware unpacked from an updater, so this reports what's
// there rather than assuming one or the other.
struct InstalledFirmwareInfo {
bool anythingInstalled = false; // flash0/flash1 exist and hold at least one file.
// From flash0:/vsh/etc/version.txt, which only a full firmware install has. Empty if all
// that's there is a partial install like the fonts.
std::string version; // "6.60"
std::string buildDate; // "2011-07-27". Empty if the file doesn't spell one out.
std::string target; // "WorldWide"
bool hasVsh = false; // flash0:/vsh/module/vshmain.prx - what launching the XMB needs.
int fontCount = 0; // Files in flash0:/font, which is all sceFont wants.
int kernelModuleCount = 0; // Files in flash0:/kd, which is what --disable-hle wants.
int fileCount = 0;
u64 totalSize = 0;
};
// Walks the NAND directory (the one holding flash0/flash1). A full firmware is only a few
// hundred files, so this is cheap, but it does read the whole tree.
void ReadInstalledFirmwareInfo(const Path &nandRoot, InstalledFirmwareInfo *info);
// Wipes what's in the NAND directory: flash0, flash1 and ipl. Two firmwares can't be merged -
// files a newer one dropped would linger and still get loaded - so an install starts from empty.
bool EraseInstalledFirmware(const Path &nandRoot, std::string *error);
// 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);
// The same three, for the disc mounted as disc0: - i.e. the game that's running. These read
// through the mounted filesystem instead of opening the image a second time, which also means
// they work for the shapes that aren't an image at all, like a folder-based "disc".
+2
View File
@@ -99,6 +99,8 @@ list(APPEND UISource
UploadScreen.cpp
CwCheatScreen.h
CwCheatScreen.cpp
FirmwareScreen.h
FirmwareScreen.cpp
InstallUpdateScreen.h
InstallUpdateScreen.cpp
InstallZipScreen.h
+191
View File
@@ -0,0 +1,191 @@
// Copyright (c) 2026- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include "Common/Data/Text/I18n.h"
#include "Common/Data/Text/Parsers.h"
#include "Common/Log.h"
#include "Common/StringUtils.h"
#include "Common/System/Request.h"
#include "Common/System/System.h"
#include "Common/UI/Notice.h"
#include "Common/UI/PopupScreens.h"
#include "Common/UI/ScreenManager.h"
#include "Common/UI/View.h"
#include "Common/UI/ViewGroup.h"
#include "Core/Config.h"
#include "Core/System.h"
#include "Core/Util/PathUtil.h"
#include "UI/FirmwareScreen.h"
#include "UI/InstallUpdateScreen.h"
#include "UI/MiscViews.h"
std::string_view FirmwareScreen::GetTitle() const {
auto sy = GetI18NCategory(I18NCat::SYSTEM);
return sy->T("PSP Firmware");
}
void FirmwareScreen::BeforeCreateViews() {
nandRoot_ = GetSysDirectory(DIRECTORY_NAND);
ReadInstalledFirmwareInfo(nandRoot_, &info_);
}
void FirmwareScreen::CreateContentViews(UI::ViewGroup *parent) {
using namespace UI;
auto sy = GetI18NCategory(I18NCat::SYSTEM);
auto iz = GetI18NCategory(I18NCat::INSTALLZIP);
auto di = GetI18NCategory(I18NCat::DIALOG);
auto st = GetI18NCategory(I18NCat::STORE); // Borrow "Size" from here, like GameScreen does.
LinearLayout *content = parent->Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)));
if (!info_.anythingInstalled) {
content->Add(new NoticeView(NoticeLevel::INFO, sy->T("No firmware installed"),
sy->T("FirmwareSources", "PPSSPP has its own replacements for the PSP's system files, so a firmware is optional. Installing one gets you the real fonts, and lets you launch the XMB. Most game discs carry a firmware updater - open a game's info screen to install from it.")));
content->Add(new TextWithImage(ImageID("I_FOLDER"), GetFriendlyPath(nandRoot_)));
return;
}
if (!info_.version.empty()) {
std::string versionLine = std::string(sy->T("Firmware version")) + ": " + info_.version;
content->Add(new TextView(versionLine, ALIGN_LEFT, false))->SetBig(true);
if (!info_.buildDate.empty() || !info_.target.empty()) {
std::string details = info_.buildDate;
if (!info_.target.empty()) {
if (!details.empty()) {
details += " - ";
}
details += info_.target;
}
content->Add(new TextView(details, ALIGN_LEFT, false));
}
} else {
// No flash0:/vsh/etc/version.txt, so this isn't a full firmware - most likely just the
// fonts, which is what installing from a running game's disc leaves behind.
content->Add(new NoticeView(NoticeLevel::INFO, sy->T("Partial firmware installed"),
sy->T("Some system files are present, but not a complete firmware")));
}
// Without any kernel modules there's no firmware here to speak of - it's the fonts we pulled
// off a game's disc, and rows reading "Kernel modules: 0 / XMB: No" say nothing useful.
const bool fullFirmware = info_.kernelModuleCount > 0;
content->Add(new ItemHeader(sy->T("Contents")));
content->Add(new InfoItem(sy->T("Fonts"), info_.fontCount));
if (fullFirmware) {
content->Add(new InfoItem(sy->T("Kernel modules"), info_.kernelModuleCount));
}
if (fullFirmware || info_.hasVsh) {
content->Add(new InfoItem(sy->T("XMB (VSH)"), info_.hasVsh ? di->T("Yes") : di->T("No")));
}
if (fullFirmware) {
content->Add(new InfoItem(sy->T("Files"), info_.fileCount));
}
content->Add(new InfoItem(st->T("Size"), NiceSizeFormat(info_.totalSize)));
content->Add(new ItemHeader(iz->T("Install into folder")));
content->Add(new TextView(GetFriendlyPath(nandRoot_), ALIGN_LEFT | FLAG_WRAP_TEXT, false));
}
void FirmwareScreen::CreateSettingsViews(UI::ViewGroup *parent) {
using namespace UI;
auto sy = GetI18NCategory(I18NCat::SYSTEM);
auto iz = GetI18NCategory(I18NCat::INSTALLZIP);
// Booting anything replaces the running game, and erasing the NAND out from under a game
// that has flash0 mounted is worse - so while a game is running, this screen is read-only.
const bool gameRunning = PSP_IsInited();
Choice *launch = parent->Add(new Choice(sy->T("Launch XMB"), ImageID("I_PLAY")));
launch->OnClick.Add([this](UI::EventParams &) {
LaunchVSH();
});
launch->SetEnabled(!gameRunning && info_.hasVsh && FirmwareVersionSupportsVSH(info_.version));
if (info_.hasVsh && !FirmwareVersionSupportsVSH(info_.version)) {
parent->Add(new NoticeView(NoticeLevel::WARN, sy->T("XMB unsupported"),
sy->T("XMBUnsupportedVersion", "Only firmware 6.61 can boot the XMB so far")));
}
if (System_GetPropertyBool(SYSPROP_HAS_FILE_BROWSER)) {
Choice *install = parent->Add(new Choice(iz->T("Install PSP firmware update"), ImageID("I_FOLDER_UPLOAD")));
install->OnClick.Add([this](UI::EventParams &) {
BrowseForUpdater();
});
install->SetEnabled(!gameRunning);
}
if (info_.anythingInstalled) {
Choice *erase = parent->Add(new Choice(sy->T("Erase firmware"), ImageID("I_TRASHCAN")));
erase->OnClick.Add([this](UI::EventParams &) {
AskToErase();
});
erase->SetEnabled(!gameRunning);
}
if (System_GetPropertyBool(SYSPROP_HAS_OPEN_DIRECTORY)) {
parent->Add(new Choice(sy->T("Show firmware folder")))->OnClick.Add([this](UI::EventParams &) {
System_LaunchUrl(LaunchUrlType::LOCAL_FOLDER, nandRoot_.ToString());
});
}
if (gameRunning) {
parent->Add(new NoticeView(NoticeLevel::INFO,
sy->T("FirmwareNeedsNoGame", "Stop the game to change the installed firmware"), ""));
}
}
void FirmwareScreen::dialogFinished(const Screen *dialog, DialogResult result) {
// Coming back from the installer (or the erase confirmation), the NAND may well look
// different than it did when we scanned it. Rescanning is cheap, so just always redo it -
// BeforeCreateViews does the scan.
RecreateViews();
UITwoPaneBaseDialogScreen::dialogFinished(dialog, result);
}
void FirmwareScreen::LaunchVSH() {
System_PostUIMessage(UIMessage::REQUEST_GAME_BOOT, (nandRoot_ / "flash0/vsh/module/vshmain.prx").ToString());
}
void FirmwareScreen::BrowseForUpdater() {
auto iz = GetI18NCategory(I18NCat::INSTALLZIP);
System_BrowseForFile(GetRequesterToken(), iz->T("Install PSP firmware update"), BrowseFileType::BOOTABLE,
[this](std::string_view value, int) {
screenManager()->push(new InstallUpdateScreen(Path(value), "", false));
});
}
void FirmwareScreen::AskToErase() {
auto sy = GetI18NCategory(I18NCat::SYSTEM);
auto di = GetI18NCategory(I18NCat::DIALOG);
std::string question(sy->T("EraseFirmwareConfirm", "This deletes everything in the NAND folder, including the fonts."));
screenManager()->push(new UI::MessagePopupScreen(sy->T("Erase firmware"), question, di->T("Delete"), di->T("Cancel"),
[this](bool erase) {
if (!erase) {
return;
}
std::string error;
if (!EraseInstalledFirmware(nandRoot_, &error)) {
ERROR_LOG(Log::Loader, "Failed to erase the firmware: %s", error.c_str());
}
RecreateViews();
}));
}
+52
View File
@@ -0,0 +1,52 @@
// Copyright (c) 2026- PPSSPP Project.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License 2.0 for more details.
// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#pragma once
#include "Common/File/Path.h"
#include "Common/UI/UIScreen.h"
#include "Common/UI/ViewGroup.h"
#include "Core/Util/PSARUnpack.h"
#include "UI/SimpleDialogScreen.h"
// What's in PSP/NAND - which can be nothing at all, just the fonts we pulled off some game's
// disc, or a full firmware unpacked from an updater. Also the place to launch the XMB from,
// since that's what a full firmware buys you.
class FirmwareScreen : public UITwoPaneBaseDialogScreen {
public:
FirmwareScreen(const Path &gamePath)
: UITwoPaneBaseDialogScreen(gamePath, TwoPaneFlags::SettingsToTheRight | TwoPaneFlags::ContentsCanScroll) {}
const char *tag() const override { return "Firmware"; }
protected:
std::string_view GetTitle() const override;
void BeforeCreateViews() override;
void CreateSettingsViews(UI::ViewGroup *parent) override;
void CreateContentViews(UI::ViewGroup *parent) override;
void dialogFinished(const Screen *dialog, DialogResult result) override;
private:
void LaunchVSH();
void BrowseForUpdater();
void AskToErase();
Path nandRoot_;
InstalledFirmwareInfo info_;
};
+12
View File
@@ -51,6 +51,7 @@
#include "UI/GameScreen.h"
#include "UI/GameSettingsScreen.h"
#include "UI/GameInfoCache.h"
#include "UI/InstallUpdateScreen.h"
#include "UI/BaseScreens.h"
#include "UI/MiscScreens.h"
#include "UI/MainScreen.h"
@@ -658,6 +659,17 @@ void GameScreen::CreateContextMenu(UI::ViewGroup *parent) {
btnDeleteUpdate->OnClick.Handle(this, &GameScreen::OnDeleteGameUpdate);
}
// Most discs carry a firmware updater, and the firmware inside it is what our flash0 wants.
// Not while a game is running, though - installing wipes the NAND the running game has mounted.
if (!inGame_ && (knownFlags_ & GameInfoFlags::BUNDLED_UPDATE_INFO) && info_->bundledUpdate.present) {
auto iz = GetI18NCategory(I18NCat::INSTALLZIP);
Choice *btnInstallFirmware = parent->Add(new Choice(iz->T("Install PSP firmware update"), ImageID("I_FOLDER_UPLOAD")));
const BundledUpdateInfo update = info_->bundledUpdate;
btnInstallFirmware->OnClick.Add([this, update](UI::EventParams &e) {
screenManager()->push(new InstallUpdateScreen(gamePath_, update.title, false, update.archiveSize));
});
}
// Don't want to be able to delete the game while it's running.
if (!inGame_) {
Choice *deleteChoice = parent->Add(new Choice(ga->T("Delete Game"), ImageID("I_WARNING")));
+4
View File
@@ -50,6 +50,7 @@
#include "UI/DevScreens.h"
#include "UI/DeveloperToolsScreen.h"
#include "UI/DisplayLayoutScreen.h"
#include "UI/FirmwareScreen.h"
#include "UI/RemoteISOScreen.h"
#include "UI/SavedataScreen.h"
#include "UI/SystemInfoScreen.h"
@@ -1138,6 +1139,9 @@ void GameSettingsScreen::CreateToolsSettings(UI::ViewGroup *tools) {
tools->Add(new Choice(ri->T("Remote disc streaming")))->OnClick.Add([=](UI::EventParams &) {
screenManager()->push(new RemoteISOScreen(gamePath_));
});
tools->Add(new Choice(sy->T("PSP Firmware")))->OnClick.Add([=](UI::EventParams &) {
screenManager()->push(new FirmwareScreen(gamePath_));
});
}
void GameSettingsScreen::CreateSystemSettings(UI::ViewGroup *systemSettings) {
+19 -8
View File
@@ -36,12 +36,13 @@
#include "UI/MiscViews.h"
#include "UI/EmuScreen.h"
InstallUpdateScreen::InstallUpdateScreen(const Path &path, std::string_view title)
: UISimpleBaseDialogScreen(Path(), SimpleDialogFlags::ContentsCanScroll), path_(path), title_(title) {
InstallUpdateScreen::InstallUpdateScreen(const Path &path, std::string_view title, bool allowRun, u64 archiveSize)
: UISimpleBaseDialogScreen(Path(), SimpleDialogFlags::ContentsCanScroll), path_(path), title_(title), allowRun_(allowRun) {
destination_ = GetSysDirectory(DIRECTORY_NAND);
fileSize_ = archiveSize;
File::FileInfo fileInfo;
if (File::GetFileInfo(path_, &fileInfo)) {
if (fileSize_ == 0 && File::GetFileInfo(path_, &fileInfo)) {
fileSize_ = fileInfo.size;
}
// There's no practical way to merge two firmwares, so an install replaces whatever is there.
@@ -80,7 +81,8 @@ void InstallUpdateScreen::CreateDialogViews(UI::ViewGroup *parent) {
container->Add(new TextView(GetFriendlyPath(destination_)))->SetAlign(FLAG_WRAP_TEXT);
if (overwrites_) {
container->Add(new NoticeView(NoticeLevel::WARN, di->T("Confirm Overwrite"), ""));
container->Add(new NoticeView(NoticeLevel::WARN, di->T("Confirm Overwrite"),
iz->T("The firmware already installed will be erased first")));
}
container->Add(new Spacer(12.0f));
@@ -90,10 +92,12 @@ void InstallUpdateScreen::CreateDialogViews(UI::ViewGroup *parent) {
StartInstall();
});
Choice *runChoice = container->Add(new Choice(dev->T("Run"), ImageID("I_PLAY")));
runChoice->OnClick.Add([this](UI::EventParams &e) {
screenManager()->switchScreen(new EmuScreen(path_));
});
if (allowRun_) {
Choice *runChoice = container->Add(new Choice(dev->T("Run"), ImageID("I_PLAY")));
runChoice->OnClick.Add([this](UI::EventParams &e) {
screenManager()->switchScreen(new EmuScreen(path_));
});
}
progressBar_ = container->Add(new ProgressBar());
progressBar_->SetVisibility(V_GONE);
@@ -125,6 +129,13 @@ void InstallUpdateScreen::StartInstall() {
options.progress = [state](float progress) {
state->progress = progress;
};
// Two firmwares can't be merged - a file the new one doesn't have would linger and still
// get loaded - so start from an empty NAND.
if (!EraseInstalledFirmware(destination, &state->error)) {
state->success = false;
state->done = true;
return;
}
state->success = UnpackUpdater(path, destination, options, &state->stats, &state->error);
if (state->success && state->stats.written == 0) {
// Nothing came out, so the archive had no file list for the model we asked for -
+11 -4
View File
@@ -33,13 +33,19 @@
#include "UI/BaseScreens.h"
#include "UI/SimpleDialogScreen.h"
// An official PSP firmware updater (a PSP/GAME/UPDATE/EBOOT.PBP, or the folder holding one).
// Running it isn't going to get anyone anywhere, but the firmware inside it is exactly what the
// emulated flash0/flash1 want, so offer to unpack it into the NAND directory instead.
// An official PSP firmware updater: a PSP/GAME/UPDATE/EBOOT.PBP (or the folder holding one), or
// a game disc, most of which carry one at PSP_GAME/SYSDIR/UPDATE/DATA.BIN. Running an updater
// isn't going to get anyone anywhere, but the firmware inside it is exactly what the emulated
// flash0/flash1 want, so offer to unpack it into the NAND directory instead.
class InstallUpdateScreen : public UISimpleBaseDialogScreen {
public:
// title is the updater's SFO title, which already carries the version ("PSP Update ver 6.61").
InstallUpdateScreen(const Path &path, std::string_view title);
// allowRun offers to boot the updater instead of unpacking it - which makes sense when the
// user picked the updater to launch it, but not when they came here to install a firmware.
// archiveSize is how big the firmware itself is; pass it for a disc, where the size of the
// file we were handed is the game's and says nothing about what's being installed. Zero means
// "the file is the updater", which is the PBP case.
InstallUpdateScreen(const Path &path, std::string_view title, bool allowRun, u64 archiveSize = 0);
void CreateDialogViews(UI::ViewGroup *parent) override;
void update() override;
@@ -70,6 +76,7 @@ private:
std::string title_;
u64 fileSize_ = 0;
bool overwrites_ = false;
bool allowRun_ = false;
std::shared_ptr<InstallState> state_;
bool reportedDone_ = false;
+1 -1
View File
@@ -101,7 +101,7 @@ static void LaunchFile(ScreenManager *screenManager, Screen *currentScreen, cons
std::string title = info->GetTitle(); // includes the version.
// The unpacker wants the PBP itself, not the folder it happens to sit in.
const Path pbpPath = info->fileType == IdentifiedFileType::PSP_PBP ? path : path / "EBOOT.PBP";
screenManager->push(new InstallUpdateScreen(pbpPath, title));
screenManager->push(new InstallUpdateScreen(pbpPath, title, true));
return;
}
break;
+2
View File
@@ -80,6 +80,7 @@
<ClCompile Include="TouchControlLayoutScreen.cpp" />
<ClCompile Include="TouchControlVisibilityScreen.cpp" />
<ClCompile Include="InstallPkgScreen.cpp" />
<ClCompile Include="FirmwareScreen.cpp" />
<ClCompile Include="InstallUpdateScreen.cpp" />
<ClCompile Include="InstallZipScreen.cpp" />
<ClCompile Include="Theme.cpp" />
@@ -139,6 +140,7 @@
<ClInclude Include="TouchControlLayoutScreen.h" />
<ClInclude Include="TouchControlVisibilityScreen.h" />
<ClInclude Include="InstallPkgScreen.h" />
<ClInclude Include="FirmwareScreen.h" />
<ClInclude Include="InstallUpdateScreen.h" />
<ClInclude Include="InstallZipScreen.h" />
<ClInclude Include="Theme.h" />
+6
View File
@@ -43,6 +43,9 @@
<ClCompile Include="InstallPkgScreen.cpp">
<Filter>Screens</Filter>
</ClCompile>
<ClCompile Include="FirmwareScreen.cpp">
<Filter>Screens</Filter>
</ClCompile>
<ClCompile Include="InstallUpdateScreen.cpp">
<Filter>Screens</Filter>
</ClCompile>
@@ -200,6 +203,9 @@
<ClInclude Include="InstallPkgScreen.h">
<Filter>Screens</Filter>
</ClInclude>
<ClInclude Include="FirmwareScreen.h">
<Filter>Screens</Filter>
</ClInclude>
<ClInclude Include="InstallUpdateScreen.h">
<Filter>Screens</Filter>
</ClInclude>
+2
View File
@@ -115,6 +115,7 @@
<ClInclude Include="..\..\UI\ImDebugger\ImMemView.h" />
<ClInclude Include="..\..\UI\ImDebugger\ImStructViewer.h" />
<ClInclude Include="..\..\UI\InstallPkgScreen.h" />
<ClInclude Include="..\..\UI\FirmwareScreen.h" />
<ClInclude Include="..\..\UI\InstallUpdateScreen.h" />
<ClInclude Include="..\..\UI\InstallZipScreen.h" />
<ClInclude Include="..\..\UI\JitCompareScreen.h" />
@@ -175,6 +176,7 @@
<ClCompile Include="..\..\UI\ImDebugger\ImMemView.cpp" />
<ClCompile Include="..\..\UI\ImDebugger\ImStructViewer.cpp" />
<ClCompile Include="..\..\UI\InstallPkgScreen.cpp" />
<ClCompile Include="..\..\UI\FirmwareScreen.cpp" />
<ClCompile Include="..\..\UI\InstallUpdateScreen.cpp" />
<ClCompile Include="..\..\UI\InstallZipScreen.cpp" />
<ClCompile Include="..\..\UI\JitCompareScreen.cpp" />
+6
View File
@@ -76,6 +76,9 @@
<ClCompile Include="..\..\UI\InstallPkgScreen.cpp">
<Filter>Screens</Filter>
</ClCompile>
<ClCompile Include="..\..\UI\FirmwareScreen.cpp">
<Filter>Screens</Filter>
</ClCompile>
<ClCompile Include="..\..\UI\InstallUpdateScreen.cpp">
<Filter>Screens</Filter>
</ClCompile>
@@ -225,6 +228,9 @@
<ClInclude Include="..\..\UI\InstallPkgScreen.h">
<Filter>Screens</Filter>
</ClInclude>
<ClInclude Include="..\..\UI\FirmwareScreen.h">
<Filter>Screens</Filter>
</ClInclude>
<ClInclude Include="..\..\UI\InstallUpdateScreen.h">
<Filter>Screens</Filter>
</ClInclude>
+1
View File
@@ -989,6 +989,7 @@ LOCAL_SRC_FILES := \
$(SRC)/UI/Background.cpp \
$(SRC)/UI/CwCheatScreen.cpp \
$(SRC)/UI/InstallPkgScreen.cpp \
$(SRC)/UI/FirmwareScreen.cpp \
$(SRC)/UI/InstallUpdateScreen.cpp \
$(SRC)/UI/InstallZipScreen.cpp \
$(SRC)/UI/JitCompareScreen.cpp \
+112
View File
@@ -66,6 +66,118 @@ checking what is actually on screen means running the app build. From headless,
the per-frame display list signature (see the red error screen section): a screen that is finished
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.
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.
### Sony renumbered the kernel NIDs, and that is what blocked everything below 6.60
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.
| 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` |
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=<SCE_VSH_GRAPHICS>` 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.
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. 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.
`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.
### The scePaf heap pool, and where it lives
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.
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.
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
`LoadAndStartVshKernelModules()` asked for `memlmd_01g.prx`, `loadexec_01g.prx` and
`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). 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
What is known, all measured from a 40-emulated-second `--vsh` boot: