mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-01 14:58:14 +00:00
Merge pull request #22271 from hrydgard/firmware-screen
Add a Tools/PSP Firmware screen, improve compat with older firmwares
This commit is contained in:
21 files changed
+788
-37
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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".
|
||||
|
||||
@@ -99,6 +99,8 @@ list(APPEND UISource
|
||||
UploadScreen.cpp
|
||||
CwCheatScreen.h
|
||||
CwCheatScreen.cpp
|
||||
FirmwareScreen.h
|
||||
FirmwareScreen.cpp
|
||||
InstallUpdateScreen.h
|
||||
InstallUpdateScreen.cpp
|
||||
InstallZipScreen.h
|
||||
|
||||
@@ -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();
|
||||
}));
|
||||
}
|
||||
@@ -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_;
|
||||
};
|
||||
@@ -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")));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 -
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user