POPS: Pass the manual's name check, and a blank ICON0 for an unusable one

Both as CFW's popcorn does, for any EBOOT. The emulator only shows the
manual when a name the manager hands it matches one it keeps, and popcorn
makes that check pass.

It only takes an 80x80 PNG as ICON0. A converted EBOOT without one hangs,
and one with a 144x80 ICON0 (the size of a PSP game's) crashes it. Such an
ICON0 now reads as a blank 80x80 PNG: its size and its read are replaced.

Both are found by their code, which is the same in pops_01g and pops_02g.
This commit is contained in:
Francisco José García García authored and Henrik Rydgård committed 2026-10-21 13:15:30 +02:00
1 parent 478a1e6aa2
commit b3aa456cdd
1 file changed
+65
+65
View File
@@ -47,10 +47,12 @@
#include "Core/MIPS/MIPSCodeUtils.h"
#include "Core/MemMap.h"
#include "Core/MemMapHelpers.h"
#include "Core/Screenshot.h"
#include "Core/System.h"
static bool g_popsBoot = false;
static bool g_popsConvertedPBP = false;
static bool g_popsIcon0Usable = true;
// The game's PBP on the host.
static Path g_popsPBPPath;
static std::unique_ptr<HLEHelperThread> g_popsInitThread;
@@ -92,6 +94,7 @@ void __PopsBootInit() {
void __PopsBootShutdown() {
g_popsBoot = false;
g_popsConvertedPBP = false;
g_popsIcon0Usable = true;
g_popsPBPPath.clear();
// The kernel objects and memory are already gone with the kernel.
if (g_popsInitThread) {
@@ -213,9 +216,30 @@ static int __PopsGetVersionKey(u32 pathAddr, u32 keyOut, u32 fingerprintOut) {
return hleLogInfo(Log::sceKernel, 0, "%.36s", (const char *)contentId);
}
// An 80x80 PNG with nothing in it.
static const std::vector<u8> &BlankIcon0() {
static std::vector<u8> png;
if (png.empty()) {
const std::vector<u8> pixels(80 * 80 * 4, 0);
Save8888RGBAScreenshot(png, pixels.data(), 80, 80);
}
return png;
}
// The emulator's read of an ICON0 it can't use (see __PopsBootPatchEmulator): a blank one instead.
static int __PopsReadIcon0(u32 file, u32 buf, u32 offset, u32 size) {
const std::vector<u8> &png = BlankIcon0();
if (size != png.size() || !Memory::IsValidRange(buf, size)) {
return hleLogError(Log::sceKernel, -1, "unexpected read");
}
Memory::Memcpy(buf, png.data(), size, "PopsIcon0");
return hleLogDebug(Log::sceKernel, (int)size);
}
const HLEFunction PopsBootCalls[] = {
{ 0x13370006, &WrapI_UUU<__PopsDeflateDecompress>, "__PopsDeflateDecompress", 'i', "xxx" },
{ 0x13370007, &WrapI_UUU<__PopsGetVersionKey>, "__PopsGetVersionKey", 'i', "xxx" },
{ 0x13370008, &WrapI_UUUU<__PopsReadIcon0>, "__PopsReadIcon0", 'i', "xxxx" },
};
void Register_PopsBootCalls() {
@@ -267,7 +291,34 @@ static void PatchTrampoline(u32 addr, const char *func, u32 delaySlotOp, u32 res
currentMIPS->InvalidateICacheRangeDeferred(trampoline, size);
}
// A function that makes a syscall (jr ra; syscall), for the emulator to call in place of one of its own.
static u32 MakeCallStub(const char *func) {
u32 size = 8;
const u32 stub = kernelMemory.Alloc(size, false, func);
Memory::WriteUnchecked_U32(MIPS_MAKE_JR_RA(), stub);
PatchCall(stub + 4, func);
currentMIPS->InvalidateICacheRangeDeferred(stub, size);
return stub;
}
void __PopsBootPatchEmulator(u32 textAddr, u32 textSize, u32 decodeStub) {
// As CFW's popcorn does: the manual's name check always passes, and an ICON0 the emulator can't
// use reads as a blank one.
for (u32 addr = textAddr + 0x30; addr + 0x30 < textAddr + textSize; addr += 4) {
const u32 op = Memory::ReadUnchecked_U32(addr);
if (op == 0x24050080 && Memory::ReadUnchecked_U32(addr + 24) == 0x24030001) {
// li a1, 0x80 (the size of the name it gets), then the check's call becomes li v0, 1.
Memory::WriteUnchecked_U32(0x24020001, addr + 8);
} else if (!g_popsIcon0Usable && op == 0x00432823 && Memory::ReadUnchecked_U32(addr - 8) == 0x8FA20010 &&
Memory::ReadUnchecked_U32(addr - 4) == 0x8FA3000C && Memory::ReadUnchecked_U32(addr + 0x2C) == Memory::ReadUnchecked_U32(addr - 0x30)) {
// subu a1, v0, v1: the ICON0's size, from the offsets in the PBP header it read 0x30 earlier
// with the same call it then reads the ICON0 with.
Memory::WriteUnchecked_U32(0x24050000 | (u32)BlankIcon0().size(), addr);
Memory::WriteUnchecked_U32(MIPS_MAKE_JAL(MakeCallStub("__PopsReadIcon0")), addr + 0x2C);
}
}
currentMIPS->InvalidateICacheRangeDeferred(textAddr, textSize);
if (!g_popsConvertedPBP) {
return;
}
@@ -316,6 +367,19 @@ static void PatchReturn(u32 addr, u32 delaySlotOp) {
currentMIPS->InvalidateICacheRangeDeferred(addr, 8);
}
// The emulator only takes an 80x80 PNG as ICON0, and goes wrong on others. A 144x80 one (the size of
// a PSP game's) crashes it.
static bool IsUsableIcon0(FileLoader *loader) {
PBPReader pbp(loader);
u8 header[24];
if (!pbp.IsValid() || !pbp.ReadSubFile(PBP_ICON0_PNG, 0, sizeof(header), header)) {
return false;
}
static const u8 pngStart[16] = { 0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n', 0, 0, 0, 13, 'I', 'H', 'D', 'R' };
static const u8 size80x80[8] = { 0, 0, 0, 80, 0, 0, 0, 80 };
return memcmp(header, pngStart, 16) == 0 && memcmp(header + 16, size80x80, 8) == 0;
}
// The installed firmware's version, from flash0:/vsh/etc/version.txt ("release:6.61:"). Empty if there's
// none, as in a partial install.
static std::string FirmwareVersion() {
@@ -349,6 +413,7 @@ bool __PopsBootStartInit(FileLoader *pbp, const std::string &gamePath, std::stri
}
g_popsPBPPath = pbp->GetPath();
g_popsConvertedPBP = IsConvertedPBP(pbp);
g_popsIcon0Usable = IsUsableIcon0(pbp);
const u32 loadModule = base + POPSMAN_LOAD_MODULE;
PatchReturn(base + POPSMAN_KEY_PROBE, MIPS_MAKE_SYSCALL("PopsBootCalls", "__PopsGetVersionKey"));
if (g_popsConvertedPBP) {