Files
ppsspp/Core/HLE/PopsBoot.cpp
T
Henrik RydgårdandClaude Opus 5.5 31fd8e8a1c POPS: Multi-disc on every firmware from 5.01, by leaving the descrambling to the emulator
With the converted header's key of zeros, the emulator's descrambling of its disc switching strings only
reverses them, so ours go in backwards and its code is left alone. The tables are found by the code that
indexes them, which is the same from 3.95 on with only the registers differing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-23 19:36:02 +02:00

853 lines
35 KiB
C++

// Copyright (c) 2012- 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/.
// PS1 EBOOTs, run by the firmware's own POPS manager (popsman.prx) and emulator (pops_0Xg.prx).
// This stands in for the parts of the POPS boot PPSSPP doesn't run: the kernel's init, and the
// Media Engine the manager's ME loop runs on. See docs/POPS.md.
#include <cstring>
#include <functional>
#include <memory>
#include <string>
#include "zlib.h"
#include "ext/libkirk/amctrl.h"
#include "Common/Serialize/Serializer.h"
#include "Common/Data/Encoding/Utf8.h"
#include "Common/Data/Text/I18n.h"
#include "Common/Serialize/SerializeFuncs.h"
#include "Core/CoreTiming.h"
#include "Core/Debugger/MemBlockInfo.h"
#include "Core/ELF/PBPReader.h"
#include "Core/ELF/PrxDecrypter.h"
#include "Core/FileSystems/MetaFileSystem.h"
#include "Core/Font/PGF.h"
#include "Core/HLE/FunctionWrappers.h"
#include "Core/HLE/HLE.h"
#include "Core/HLE/HLEHelperThread.h"
#include "Core/HLE/PopsBoot.h"
#include "Core/HLE/sceChnnlsv.h"
#include "Core/HLE/sceKernelMemory.h"
#include "Core/HLE/sceKernelModule.h"
#include "Core/HLE/sceKernelThread.h"
#include "Core/HW/PopsMMIO.h"
#include "Core/Loaders.h"
#include "Core/MIPS/MIPS.h"
#include "Core/MIPS/MIPSCodeUtils.h"
#include "Core/MIPS/MIPSTables.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;
static u32 g_popsInitPath = 0;
// The places in the POPS manager used here, from its base: scePopsManLoadModule, and the checks
// replaced for store and converted games. Its ME loop (6.60's +2F88) runs on the emulated ME.
struct PopsManLayout {
u32 loadModule;
u32 loadModuleFrame; // Its first instruction, addiu sp, sp, -frame.
// The license check (path, keyOut, fingerprintOut). Its first success latches the version key and
// the content ID fingerprint (6.60's +0124). 5.x has none: the kernel hands the key to the manager.
u32 keyProbe;
u32 versionKey;
u32 keyLatched;
u32 keyFingerprint;
// Whether data matches its MAC: its own, and the one it exports to the emulator (6.60's
// sceMeAudio_F6637A72).
u32 checkMac;
u32 checkMacExport;
// 5.x: the license lookup in the export the emulator opens the game with (5.55's
// sceMeAudio_A1457825 calls scePspNpDrm_driver_4478C033 on the content ID).
u32 licenseCall;
// What scePopsManLoadModule's second argument has to be for it to load the emulator: 5.50 and later
// skip it on 2, 5.0x on 0.
u32 loadArg = 0;
};
// The managers have the same code at these places, and older ones import kernel functions by older
// NIDs (see GetHLENidAlias()).
static const PopsManLayout POPSMAN_LAYOUTS[] = {
{ 0x1E20, 0x120, 0x266C, 0x49E0, 0x4A18, 0x4A1C, 0x0A28, 0x0AB8, 0 }, // 6.35-6.61
{ 0x1E48, 0x120, 0x2694, 0x4AE0, 0x4B18, 0x4B1C, 0x0A28, 0x0AB8, 0 }, // 6.30-6.31
{ 0x1E48, 0x130, 0x26F8, 0x4B30, 0x4B68, 0x4B6C, 0x0A28, 0x0AB8, 0 }, // 6.20
{ 0x19E8, 0x130, 0x2270, 0x46D0, 0x4704, 0x4708, 0x07B8, 0x0848, 0 }, // 6.00-6.10
{ 0x155C, 0x130, 0, 0x3550, 0x3570, 0x3578, 0x0500, 0x058C, 0x0144 }, // 5.55
{ 0x1538, 0x130, 0, 0x34D0, 0x34F0, 0x34F8, 0x0500, 0x058C, 0x0144 }, // 5.50
{ 0x1538, 0x020, 0, 0x33D0, 0x33F0, 0x33F8, 0x0500, 0x058C, 0x0144, 1 }, // 5.00-5.05
{ 0x150C, 0x030, 0, 0x3510, 0x3530, 0x3538, 0x0538, 0x05C4, 0x0138, 1 }, // 4.01-4.05
{ 0x1404, 0x030, 0, 0x3390, 0x33B0, 0x33B8, 0x0538, 0x05C4, 0x0138, 1 }, // 3.95-3.96
};
static int g_popsManLayout = 0;
static const u32 SCE_POPSMAN_LOAD_MODULE_NID = 0x29B3FB24;
void __PopsBootInit() {
g_popsBoot = true;
}
void __PopsBootShutdown() {
g_popsBoot = false;
g_popsConvertedPBP = false;
g_popsIcon0Usable = true;
g_popsManLayout = 0;
g_popsPBPPath.clear();
// The kernel objects and memory are already gone with the kernel.
if (g_popsInitThread) {
g_popsInitThread->Forget();
}
g_popsInitThread.reset();
g_popsInitPath = 0;
}
bool PopsBootActive() {
return g_popsBoot;
}
static void DoHelperThread(PointerWrap &p, std::unique_ptr<HLEHelperThread> &thread) {
bool has = thread != nullptr;
Do(p, has);
if (p.mode == PointerWrap::MODE_READ) {
// The kernel objects come back with the kernel's own state.
if (thread) {
thread->Forget();
}
thread.reset(has ? new HLEHelperThread() : nullptr);
}
if (thread) {
thread->DoState(p);
}
}
void __PopsBootDoState(PointerWrap &p) {
auto s = p.Section("PopsBoot", 0, 2);
if (!s) {
if (p.mode == PointerWrap::MODE_READ) {
__PopsBootShutdown();
}
return;
}
Do(p, g_popsBoot);
Do(p, g_popsConvertedPBP);
Do(p, g_popsInitPath);
DoHelperThread(p, g_popsInitThread);
if (s >= 2) {
Do(p, g_popsManLayout);
} else {
g_popsManLayout = 0;
}
}
static const PopsManLayout &PopsMan() {
return POPSMAN_LAYOUTS[g_popsManLayout];
}
static u32 PopsManBase() {
const u32 loadModule = KernelFindModuleExport("scePopsMan", SCE_POPSMAN_LOAD_MODULE_NID);
return loadModule ? loadModule - PopsMan().loadModule : 0;
}
// Picks the layout of the loaded manager, by the code at its places. False if none matches.
static bool FindPopsManLayout(bool converted) {
const u32 loadModule = KernelFindModuleExport("scePopsMan", SCE_POPSMAN_LOAD_MODULE_NID);
if (!loadModule) {
return false;
}
for (int i = 0; i < (int)ARRAY_SIZE(POPSMAN_LAYOUTS); i++) {
const PopsManLayout &layout = POPSMAN_LAYOUTS[i];
const u32 base = loadModule - layout.loadModule;
const struct {
u32 offset;
u32 original;
} code[] = {
{ layout.loadModule, 0x27BD0000 | ((0x10000 - layout.loadModuleFrame) & 0xFFFF) },
{ layout.checkMac, 0x27BDFFC0 }, // addiu sp, sp, -0x40
{ layout.checkMacExport, 0x27BDFFB0 },
};
bool match = true;
for (const auto &c : code) {
match = match && Memory::ReadUnchecked_U32(base + c.offset) == c.original;
}
if (layout.keyProbe != 0) {
match = match && Memory::ReadUnchecked_U32(base + layout.keyProbe) == 0x27BDFEF0;
match = match && Memory::ReadUnchecked_U32(base + layout.keyProbe + 4) == 0x2CA70001;
} else {
// Without the probe, only converted PBPs: a store game's key would come from the kernel.
match = match && converted && (Memory::ReadUnchecked_U32(base + layout.licenseCall) >> 26) == 3;
}
if (match) {
g_popsManLayout = i;
return true;
}
}
return false;
}
// A converted PBP's disc block, raw DEFLATE. Returns the output capacity - 1, as the emulator's
// own decoder does.
static int __PopsDeflateDecompress(u32 destSize, u32 src, u32 dest) {
if (!Memory::IsValidRange(dest, destSize) || !Memory::IsValidAddress(src)) {
return hleLogError(Log::sceKernel, -1, "bad buffers");
}
z_stream stream{};
stream.next_in = (Bytef *)Memory::GetPointerUnchecked(src);
stream.avail_in = (uInt)Memory::ClampValidSizeAt(src, destSize * 2);
stream.next_out = (Bytef *)Memory::GetPointerWriteUnchecked(dest);
stream.avail_out = (uInt)destSize;
if (inflateInit2(&stream, -MAX_WBITS) != Z_OK) {
return hleLogError(Log::sceKernel, -1, "inflateInit2 failed");
}
const int err = inflate(&stream, Z_FINISH);
inflateEnd(&stream);
if (err != Z_STREAM_END) {
return hleLogError(Log::sceKernel, -1, "inflate failed %d", err);
}
NotifyMemInfo(MemBlockFlags::WRITE, dest, (u32)stream.total_out, "PopsDeflate");
return hleLogDebug(Log::sceKernel, (int)(destSize - 1));
}
// A PS1 PBP's disc image starts after the PSAR's header: 0x400 in, or 0x200 for a multi-disc one.
static bool ReadDiscImage(const PBPReader &pbp, size_t size, u8 *out) {
char psar[8];
if (!pbp.ReadSubFile(PBP_UNKNOWN_PSAR, 0, sizeof(psar), psar)) {
return false;
}
return pbp.ReadSubFile(PBP_UNKNOWN_PSAR, memcmp(psar, "PSTITLE", 7) == 0 ? 0x200 : 0x400, size, out);
}
// The content ID (DATA.PSP + 0x560, as in the license) and the disc image's PGD header.
static bool ReadPBPDrmHeaders(u8 *pgdHeader, u8 *contentId) {
// A converted one has no PGD, and one made from a disc image isn't at its path.
if (g_popsConvertedPBP) {
return false;
}
std::unique_ptr<FileLoader> loader(ConstructFileLoader(g_popsPBPPath));
PBPReader pbp(loader.get());
return pbp.IsValid() && pbp.ReadSubFile(PBP_EXECUTABLE_PSP, 0x560, 0x30, contentId) &&
ReadDiscImage(pbp, 0x90, pgdHeader) && memcmp(pgdHeader, "\0PGD", 4) == 0;
}
// Replaces the manager's license check, which needs the game's license and activation. The
// version key comes from the disc image's PGD header instead (its MAC_70 is the key's MAC), as
// CFW's popcorn does. Returns what the check does on failure.
static int __PopsGetVersionKey(u32 pathAddr, u32 keyOut, u32 fingerprintOut) {
const char *path = Memory::GetCharPointer(pathAddr);
if (!path || !Memory::IsValidRange(keyOut, 16)) {
return hleLogError(Log::sceKernel, 0x80020148, "bad arguments");
}
u8 pgdHeader[0x90];
u8 contentId[0x30]{};
if (!ReadPBPDrmHeaders(pgdHeader, contentId)) {
return hleLogWarning(Log::sceKernel, 0x80020148, "%s has no PGD disc image", path);
}
PGD_DESC *pgd = pgd_open(__ChnnlsvKirkState(), pgdHeader, 2, nullptr);
if (!pgd) {
return hleLogError(Log::sceKernel, 0x80020148, "%s: bad PGD header", path);
}
u8 versionKey[16];
memcpy(versionKey, pgd->vkey, 16);
pgd_close(pgd);
Memory::Memcpy(keyOut, versionKey, 16, "PopsVersionKey");
if (Memory::IsValidRange(fingerprintOut, sizeof(contentId))) {
Memory::Memcpy(fingerprintOut, contentId, sizeof(contentId), "PopsVersionKey");
}
const u32 base = PopsManBase();
if (Memory::ReadUnchecked_U32(base + PopsMan().keyLatched) == 0) {
Memory::WriteUnchecked_U32(1, base + PopsMan().keyLatched);
Memory::Memcpy(base + PopsMan().versionKey, versionKey, 16, "PopsVersionKey");
Memory::Memcpy(base + PopsMan().keyFingerprint, contentId, sizeof(contentId), "PopsVersionKey");
}
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() {
RegisterHLEModule("PopsBootCalls", ARRAY_SIZE(PopsBootCalls), PopsBootCalls);
}
static void PatchCall(u32 addr, const char *func) {
Memory::WriteUnchecked_U32(MIPS_MAKE_SYSCALL("PopsBootCalls", func), addr);
}
// The user modules the firmware's POPS boot configuration (flash0:/kd/pspbtcnf.bin) starts after the
// manager: those in its POPS mode under /vsh/module/. They differ by version (4.05 adds heaparea2.prx).
static std::vector<std::string> PopsUserModules() {
std::vector<u8> file;
if (pspFileSystem.ReadEntireFile("flash0:/kd/pspbtcnf.bin", file, true) < 0 || file.size() < 0x150) {
return {};
}
std::vector<u8> plain(file.size());
const int size = pspDecryptPRX(file.data(), plain.data(), (u32)file.size());
auto word = [&](u32 offset) -> u32 {
return offset + 4 <= (u32)size ? plain[offset] | (plain[offset + 1] << 8) | (plain[offset + 2] << 16) | ((u32)plain[offset + 3] << 24) : 0;
};
if (size <= 0 || word(0) != 0x0F803001) {
return {};
}
// Header: modules at 0x20 (offset, count), names at 0x30. A module is 32 bytes: name offset, then
// at +8 its mode flags (POPS is 8).
const u32 modules = word(0x20), count = word(0x24), names = word(0x30);
std::vector<std::string> paths;
bool afterManager = false;
for (u32 i = 0; i < count && modules + i * 32 + 12 <= (u32)size; i++) {
const u32 nameOffset = names + word(modules + i * 32);
if (nameOffset >= (u32)size || !(word(modules + i * 32 + 8) & 8)) {
continue;
}
const std::string name((const char *)&plain[nameOffset], strnlen((const char *)&plain[nameOffset], size - nameOffset));
if (name == "/kd/popsman.prx") {
afterManager = true;
} else if (afterManager && startsWith(name, "/vsh/module/")) {
paths.push_back("flash0:" + name);
}
}
return paths;
}
void __PopsBootLoadModules() {
std::vector<std::string> paths = PopsUserModules();
if (paths.empty()) {
WARN_LOG(Log::sceKernel, "POPS: couldn't read the boot configuration, starting 6.60's modules");
paths = {
"flash0:/vsh/module/libfont_hv.prx",
"flash0:/vsh/module/pafmini.prx",
"flash0:/vsh/module/common_util.prx",
"flash0:/vsh/module/libpspvmc.prx",
};
}
for (const std::string &path : paths) {
std::string error;
const SceUID moduleId = KernelLoadModule(path, &error);
if (moduleId < 0) {
WARN_LOG(Log::sceKernel, "POPS: failed to load %s: %s", path.c_str(), error.c_str());
continue;
}
if (__KernelStartModule(moduleId, 0, 0, 0, nullptr, nullptr) < 0) {
WARN_LOG(Log::sceKernel, "POPS: failed to start %s", path.c_str());
}
}
}
// A store PBP's disc image is a PGD, a converted one's isn't.
static bool IsConvertedPBP(FileLoader *loader) {
PBPReader pbp(loader);
u8 magic[4];
return pbp.IsValid() && ReadDiscImage(pbp, sizeof(magic), magic) && memcmp(magic, "\0PGD", 4) != 0;
}
// Sends code at addr to a syscall through a trampoline, then on to resume. The emulator takes
// syscalls in its own text for import stubs, so they can't go in place.
static void PatchTrampoline(u32 addr, const char *func, u32 delaySlotOp, u32 resume) {
u32 size = 16;
const u32 trampoline = kernelMemory.Alloc(size, false, func);
PatchCall(trampoline, func);
Memory::WriteUnchecked_U32(delaySlotOp, trampoline + 4);
Memory::WriteUnchecked_U32(MIPS_MAKE_J(resume), trampoline + 8);
Memory::WriteUnchecked_U32(MIPS_MAKE_NOP(), trampoline + 12);
Memory::WriteUnchecked_U32(MIPS_MAKE_J(trampoline), addr);
currentMIPS->InvalidateICacheRangeDeferred(addr, 8);
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;
}
// Whether "li a0, 0x9300" comes up to five instructions before addr, with no jump or branch in between.
static bool SetsBlockSizeBefore(u32 addr, u32 textAddr) {
for (int i = 1; i <= 5 && addr - 4 * i >= textAddr; i++) {
const u32 op = Memory::ReadUnchecked_U32(addr - 4 * i);
if (op == 0x34049300) {
return true;
}
const MIPSInfo info = MIPSGetInfo(MIPSOpcode(op));
if (info & (IS_JUMP | IS_CONDBRANCH)) {
return false;
}
}
return false;
}
// The emulator's multi-disc strings, 14 for each of its 12 languages, as {length, UTF-16 pointer}: the HOME
// menu's item, then three dialogs of a line each. A store game's PSTITLEIMG header has the key they're
// descrambled with when a game has several discs (pops +29xxx in 6.60). A converted one's is zeros, which
// only reverses them, and they're ours, from our translations. Each dialog's lines are left-aligned and
// centred as a block. Its disc number is drawn over a gap in a line, at a position (one each for the
// switch dialog, the disc picker's label and the confirmation) from a table of its own.
enum {
DISC_MENU_ITEM = 0,
DISC_SWITCH = 1, // The current disc, then 2-3.
DISC_LABEL = 4,
DISC_CONFIRM = 5, // The disc it switches to, then 6-9.
DISC_RESET = 10, // 10-13, the current disc on 11.
DISC_STRING_COUNT = 14,
POPS_LANGUAGES = 12,
};
// The dialog text is ltn0.pgf at this scale, and the label's at the second. The number positions are for
// the firmware's English layout (the most lines each dialog has, the number on the first line of the switch
// dialog and the confirmation and the second of the reset dialog), measured against the firmware's strings
// and positions in each language that ltn0 has the glyphs for, and screenshots. The reset dialog's number is
// the label's, moved by a table of its own (per language, two words).
static const float DIALOG_TEXT_SCALE = 0.685f;
static const float LABEL_TEXT_SCALE = 0.61f;
static const float DIALOG_CENTER_X = 240.0f;
static const float NUMBER_OFFSET_X = 4.0f;
static const float RESET_NUMBER_OFFSET_X = 5.0f;
static const float LABEL_CENTER_X = 246.5f;
static const float MAX_LINE_WIDTH = 420.0f;
static const int SWITCH_LINES = 2, SWITCH_NUMBER_Y = 95;
static const int CONFIRM_LINES = 4, CONFIRM_NUMBER_Y = 88;
static const int RESET_LINES = 4, RESET_NUMBER_Y = 95;
static const int LABEL_NUMBER_Y = 160;
// The width of text in ltn0.pgf's units, or -1 if a glyph is missing.
static float TextWidth(const PGF &font, const std::u16string &text) {
float width = 0.0f;
for (char16_t c : text) {
PGFCharInfo info{};
if (!font.GetCharInfo(c, &info, -1)) {
return -1.0f;
}
width += info.sfp26AdvanceH / 64.0f;
}
return width;
}
// Words into lines no wider than maxWidth pixels, or none if a word is wider or a glyph is missing.
static std::vector<std::u16string> WrapText(const PGF &font, const std::u16string &text, float maxWidth) {
std::vector<std::u16string> lines;
std::u16string line;
size_t pos = 0;
while (pos < text.size()) {
size_t end = text.find(u' ', pos);
if (end == std::u16string::npos) {
end = text.size();
}
const std::u16string word = text.substr(pos, end - pos);
const std::u16string candidate = line.empty() ? word : line + u" " + word;
const float width = TextWidth(font, candidate);
if (width < 0.0f) {
return {};
}
if (width * DIALOG_TEXT_SCALE <= maxWidth) {
line = candidate;
} else if (line.empty()) {
return {};
} else {
lines.push_back(line);
line = word;
}
pos = end + 1;
}
if (!line.empty()) {
lines.push_back(line);
}
return lines;
}
// Words into exactly count lines that fit the dialog, narrowing them until they split that way. The
// dialogs are centred vertically by their number of lines, which the number's position is for.
static std::vector<std::u16string> WrapToLines(const PGF &font, const std::u16string &text, int count) {
for (float width = MAX_LINE_WIDTH; width >= 40.0f; width -= 4.0f) {
std::vector<std::u16string> lines = WrapText(font, text, width);
if ((int)lines.size() == count) {
return lines;
}
if (lines.empty() || (int)lines.size() > count) {
break;
}
}
return {};
}
// A line with the disc number: %1 is a gap for it, as wide as the firmware's. Its text before the gap
// goes in prefix. False if it has no %1, or glyphs the font hasn't.
static bool NumberLine(const PGF &font, std::string_view text, std::u16string *line, std::u16string *prefix) {
const size_t gap = text.find("%1");
if (gap == std::string_view::npos) {
return false;
}
std::string_view before = text.substr(0, gap);
while (!before.empty() && before.back() == ' ') {
before.remove_suffix(1);
}
*prefix = ConvertUTF8ToUCS2(before);
*line = *prefix + u" " + ConvertUTF8ToUCS2(text.substr(gap + 2));
return TextWidth(font, *line) >= 0.0f && TextWidth(font, *line) * DIALOG_TEXT_SCALE <= MAX_LINE_WIDTH;
}
struct DiscSwitchText {
std::u16string strings[DISC_STRING_COUNT];
// The switch dialog's, the label's, the confirmation's and the reset dialog's.
s16 positions[4][2];
};
// The strings and number positions for one wording of them.
static bool LayOutDiscSwitchText(const PGF &font, const std::function<std::string_view(const char *key)> &T, DiscSwitchText *out) {
std::u16string *s = out->strings;
s[DISC_MENU_ITEM] = ConvertUTF8ToUCS2(T("Switch Discs"));
if (TextWidth(font, s[DISC_MENU_ITEM]) < 0.0f) {
return false;
}
// Where the number goes in a dialog's line with the gap, in pixels.
auto gapX = [&](const std::u16string *lines, int count, const std::u16string &prefix) {
float widest = 0.0f;
for (int i = 0; i < count; i++) {
widest = std::max(widest, TextWidth(font, lines[i]));
}
return DIALOG_CENTER_X - DIALOG_TEXT_SCALE * widest / 2 + DIALOG_TEXT_SCALE * TextWidth(font, prefix) + NUMBER_OFFSET_X;
};
// A dialog: optionally a line, then the line with the gap, then text wrapped to the rest of its lines.
auto dialog = [&](int first, const char *lineBefore, const char *numberText, const char *restText, int lines, std::u16string *prefix) {
std::u16string line;
if (!NumberLine(font, T(numberText), &line, prefix)) {
return false;
}
std::vector<std::u16string> rest = WrapToLines(font, ConvertUTF8ToUCS2(T(restText)), lines - (lineBefore ? 2 : 1));
if (rest.empty()) {
return false;
}
int i = first;
if (lineBefore) {
s[i++] = ConvertUTF8ToUCS2(T(lineBefore));
}
s[i++] = line;
for (const std::u16string &r : rest) {
s[i++] = r;
}
return TextWidth(font, s[first]) >= 0.0f;
};
std::u16string prefix;
if (!dialog(DISC_SWITCH, nullptr, "The current disc is Disc %1.", "Select a disc to switch to.", SWITCH_LINES, &prefix)) {
return false;
}
out->positions[0][0] = (s16)gapX(&s[DISC_SWITCH], SWITCH_LINES, prefix);
out->positions[0][1] = SWITCH_NUMBER_Y;
if (!NumberLine(font, T("Disc %1"), &s[DISC_LABEL], &prefix)) {
return false;
}
out->positions[1][0] = (s16)(LABEL_CENTER_X + LABEL_TEXT_SCALE * (TextWidth(font, prefix) - TextWidth(font, s[DISC_LABEL]) / 2));
out->positions[1][1] = LABEL_NUMBER_Y;
if (!dialog(DISC_CONFIRM, nullptr, "This will switch to Disc %1.", "If the game didn't ask for another disc, it might not be able to continue. Switch discs anyway?", CONFIRM_LINES, &prefix)) {
return false;
}
out->positions[2][0] = (s16)gapX(&s[DISC_CONFIRM], CONFIRM_LINES, prefix);
out->positions[2][1] = CONFIRM_NUMBER_Y;
if (!dialog(DISC_RESET, "The game will be reset.", "The current disc is Disc %1.", "Select the disc to start after the reset.", RESET_LINES, &prefix)) {
return false;
}
out->positions[3][0] = (s16)(gapX(&s[DISC_RESET], RESET_LINES, prefix) - NUMBER_OFFSET_X + RESET_NUMBER_OFFSET_X);
out->positions[3][1] = RESET_NUMBER_Y;
return true;
}
// Puts our strings and their number positions in the emulator's tables, in every language's row.
static bool InstallDiscSwitchText(u32 stringTable, u32 positionTable, u32 resetTable) {
std::vector<u8> fontData;
PGF font;
if (pspFileSystem.ReadEntireFile("flash0:/font/ltn0.pgf", fontData, true) < 0 || !font.ReadPtr(fontData.data(), fontData.size())) {
WARN_LOG(Log::sceKernel, "POPS: no flash0:/font/ltn0.pgf to lay out the disc switching text with");
return false;
}
auto po = GetI18NCategory(I18NCat::POPS);
DiscSwitchText text;
if (!LayOutDiscSwitchText(font, [&](const char *key) { return po->T(key); }, &text)) {
// The translation doesn't fit, or needs glyphs ltn0 hasn't: the English text does.
if (!LayOutDiscSwitchText(font, [](const char *key) { return std::string_view(key); }, &text)) {
return false;
}
}
u32 size = 0;
for (const std::u16string &s : text.strings) {
size += ((u32)s.size() * 2 + 3) & ~3;
}
u32 alloc = std::max(size, 4U);
const u32 strings = kernelMemory.Alloc(alloc, false, "PopsDiscText");
if (strings == (u32)-1) {
return false;
}
u32 ptr = strings;
u32 entries[DISC_STRING_COUNT][2];
for (int i = 0; i < DISC_STRING_COUNT; i++) {
// Backwards: the descrambling reverses them, and with our header's key of zeros does nothing else.
const std::u16string &s = text.strings[i];
for (size_t c = 0; c < s.size(); c++) {
Memory::WriteUnchecked_U16(s[s.size() - 1 - c], ptr + (u32)c * 2);
}
entries[i][0] = (u32)s.size();
entries[i][1] = s.empty() ? 0 : ptr;
ptr += ((u32)s.size() * 2 + 3) & ~3;
}
for (int lang = 0; lang < POPS_LANGUAGES; lang++) {
for (int i = 0; i < DISC_STRING_COUNT; i++) {
Memory::WriteUnchecked_U32(entries[i][0], stringTable + (lang * DISC_STRING_COUNT + i) * 8);
Memory::WriteUnchecked_U32(entries[i][1], stringTable + (lang * DISC_STRING_COUNT + i) * 8 + 4);
}
for (int i = 0; i < 3; i++) {
Memory::WriteUnchecked_U16(text.positions[i][0], positionTable + (lang * 3 + i) * 4);
Memory::WriteUnchecked_U16(text.positions[i][1], positionTable + (lang * 3 + i) * 4 + 2);
}
Memory::WriteUnchecked_U32(text.positions[3][0] - text.positions[1][0], resetTable + lang * 8);
Memory::WriteUnchecked_U32(text.positions[3][1] - text.positions[1][1], resetTable + lang * 8 + 4);
}
NotifyMemInfo(MemBlockFlags::WRITE, strings, size, "PopsDiscText");
INFO_LOG(Log::sceKernel, "POPS: disc numbers at %d,%d %d,%d %d,%d %d,%d", text.positions[0][0], text.positions[0][1], text.positions[1][0], text.positions[1][1], text.positions[2][0], text.positions[2][1], text.positions[3][0], text.positions[3][1]);
return true;
}
// The address a lui at addr and an addiu after it make.
static u32 LuiAddiuAddress(u32 addr) {
return (Memory::ReadUnchecked_U32(addr) << 16) + (s16)(Memory::ReadUnchecked_U32(addr + 4) & 0xFFFF);
}
// Finds the multi-disc strings' tables in the emulator, and puts ours there.
static void PatchDiscSwitchText(u32 textAddr, u32 textSize) {
u32 stringTable = 0, positionTable = 0, resetTable = 0;
for (u32 addr = textAddr; addr + 0x20 < textAddr + textSize; addr += 4) {
auto at = [&](u32 offset) { return Memory::ReadUnchecked_U32(addr + offset); };
// The language's row: lang * 0x70 + table (into s7 in 6.x, k0 in 5.x, the stack in 3.x).
if (!stringTable && at(0) == 0x000219C0 && at(4) == 0x00021100 && at(8) == 0x00621823 && (at(12) >> 16) == 0x3C02 && (at(16) >> 16) == 0x2442 && (at(20) & 0xFFFF07FF) == 0x00620021) {
stringTable = LuiAddiuAddress(addr + 12);
}
// The number position, (lang * 3 + i) * 4 + table.
if (at(0) == 0x00031040 && at(4) == 0x00431021 && at(8) == 0x00501021 && (at(12) >> 16) == 0x3C03 && (at(16) >> 16) == 0x2463 && at(20) == 0x00021080) {
positionTable = LuiAddiuAddress(addr + 12);
}
// After it, the reset dialog's number's move from the label's: lang * 8 + table.
if (positionTable && !resetTable && (at(0) >> 16) == 0x3C02 && (at(4) >> 16) == 0x2442 && at(8) == 0x000318C0 && at(12) == 0x00621821) {
resetTable = LuiAddiuAddress(addr);
}
}
if (!stringTable || !positionTable || !resetTable) {
WARN_LOG(Log::sceKernel, "POPS: didn't find the disc switching text (%08x %08x %08x)", stringTable, positionTable, resetTable);
return;
}
if (InstallDiscSwitchText(stringTable, positionTable, resetTable)) {
INFO_LOG(Log::sceKernel, "POPS: disc switching text at %08x, numbers at %08x", stringTable, positionTable);
}
}
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;
}
PatchDiscSwitchText(textAddr, textSize);
if (!decodeStub) {
ERROR_LOG(Log::sceKernel, "POPS: the emulator doesn't import scePopsMan_0090B2C8, the disc won't read");
return;
}
// A converted PBP's disc blocks are raw DEFLATE. As CFW's popcorn does, send the disc read's
// decoder call to the scePopsMan_0090B2C8 import, which inflates. That's the jal that gets
// "li a0, 0x9300" (a block of 16 raw sectors): in its delay slot (6.x), or shortly before it with
// no other jump or branch in between (5.x, 3.x).
int sites = 0;
int trackBranches = 0;
for (u32 addr = textAddr + 4; addr + 8 < textAddr + textSize; addr += 4) {
const u32 op = Memory::ReadUnchecked_U32(addr);
const u32 next = Memory::ReadUnchecked_U32(addr + 4);
if ((op >> 26) == 3 && (next == 0x34049300 || SetsBlockSizeBefore(addr, textAddr))) {
Memory::WriteUnchecked_U32(MIPS_MAKE_JAL(decodeStub), addr);
sites++;
} else if ((op == 0x14C00014 && next == 0x24E2FFFF) || (op == 0x14A00014 && next == 0x24C2FFFF)) {
// Also popcorn's: the track index's length doesn't count the CD audio tracks a converted
// image adds, so always take the full-length branch (b +0x14; addiu v0, aN, 0).
Memory::WriteUnchecked_U32(0x10000014, addr);
Memory::WriteUnchecked_U32(next & 0xFFFF0000, addr + 4);
trackBranches++;
}
}
Memory::WriteUnchecked_U32(MIPS_MAKE_JR_RA(), decodeStub);
PatchCall(decodeStub + 4, "__PopsDeflateDecompress");
currentMIPS->InvalidateICacheRangeDeferred(textAddr, textSize);
currentMIPS->InvalidateICacheRangeDeferred(decodeStub, 8);
INFO_LOG(Log::sceKernel, "POPS: converted PBP, %d disc decoder calls now inflate, %d track index branches taken", sites, trackBranches);
}
// After the game path, a word for the version key function the manager registers.
static u32 KeyFuncSlot(const std::string &gamePath) {
return g_popsInitPath + (((u32)gamePath.size() + 1 + 3) & ~3);
}
void __PopsBootSetVersionKeyFunc(u32 func) {
if (g_popsInitPath) {
Memory::WriteUnchecked_U32(func, KeyFuncSlot(Memory::GetCharPointer(g_popsInitPath)));
}
}
static void PatchReturn(u32 addr, u32 delaySlotOp) {
Memory::WriteUnchecked_U32(MIPS_MAKE_JR_RA(), addr);
Memory::WriteUnchecked_U32(delaySlotOp, addr + 4);
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;
}
bool __PopsBootStartInit(FileLoader *pbp, const std::string &gamePath, std::string *error) {
if (!FindPopsManLayout(IsConvertedPBP(pbp))) {
auto er = GetI18NCategory(I18NCat::ERRORS);
*error = er->T("PS1FirmwareVersion", "So far, PS1 games only run with firmware 6.00 to 6.61 (converted ones also with 3.95 to 5.55), and a different version is installed. Install one of those from an official PSP update file in Settings > Tools > PSP Firmware.");
return false;
}
const u32 base = PopsManBase();
g_popsPBPPath = pbp->GetPath();
g_popsConvertedPBP = IsConvertedPBP(pbp);
g_popsIcon0Usable = IsUsableIcon0(pbp);
// 4.05's manager allocates from a pool with the kernel's sceKernelAllocateFpl, which isn't resolved for
// kernel modules (see ThreadManForKernel): only its import gets the user one.
if (u32 stub = KernelFindModuleImport("scePops_Manager", "ThreadManForKernel", 0xD979E9BF)) {
WriteHLESyscall("ThreadManForUser", 0xD979E9BF, stub);
currentMIPS->InvalidateICacheRangeDeferred(stub, 8);
}
const u32 loadModule = base + PopsMan().loadModule;
if (PopsMan().keyProbe != 0) {
PatchReturn(base + PopsMan().keyProbe, MIPS_MAKE_SYSCALL("PopsBootCalls", "__PopsGetVersionKey"));
}
if (g_popsConvertedPBP) {
// A converted PBP has no MACs. As popcorn does, take its data as authentic.
PatchReturn(base + PopsMan().checkMac, 0x24020001); // li v0, 1
PatchReturn(base + PopsMan().checkMacExport, 0x24020001);
// With several discs, the disc header check (6.60's sceMeAudio_14447BA0, whose NID differs between
// versions) also compares the header with a MAC in the PSTITLEIMG header, by calling
// sceDrmBBMacFinal2 itself. Every MAC the manager compares passes.
const u32 macFinal2 = KernelFindModuleImport("scePops_Manager", "sceAmctrl_driver", 0xF5186D8E);
int macCalls = 0;
for (const auto &range : __KernelModuleCodeRanges()) {
if (!macFinal2 || base < range.first || base >= range.first + range.second) {
continue;
}
for (u32 addr = range.first; addr + 4 <= range.first + range.second; addr += 4) {
if (Memory::ReadUnchecked_U32(addr) == MIPS_MAKE_JAL(macFinal2)) {
Memory::WriteUnchecked_U32(0x24020000, addr); // li v0, 0
currentMIPS->InvalidateICacheRangeDeferred(addr, 4);
macCalls++;
}
}
}
INFO_LOG(Log::sceKernel, "POPS: %d MAC comparisons pass", macCalls);
if (PopsMan().licenseCall != 0) {
// It has no license, so the lookup can only fail.
Memory::WriteUnchecked_U32(0x24020000, base + PopsMan().licenseCall); // li v0, 0
currentMIPS->InvalidateICacheRangeDeferred(base + PopsMan().licenseCall, 4);
}
}
const u32 pathSize = (u32)gamePath.size() + 1;
u32 allocSize = ((pathSize + 3) & ~3) + 4;
g_popsInitPath = kernelMemory.Alloc(allocSize, false, "PopsInitPath");
Memory::Memcpy(g_popsInitPath, gamePath.c_str(), pathSize, "PopsInitPath");
const u32 keyFuncSlot = KeyFuncSlot(gamePath);
Memory::WriteUnchecked_U32(0, keyFuncSlot);
// The kernel's init: get the version key through the registered function, if any, then
// scePopsManLoadModule(path, loadArg). Starts with a1 = its copy of the path, below the manager's
// priority so it runs once module_start is done.
const u32 code[] = {
0x00A08021, // move s0, a1
0x03E08821, // move s1, ra
0x3C190000 | (((keyFuncSlot + 0x8000) >> 16) & 0xFFFF), // lui t9, slot
0x8F390000 | (keyFuncSlot & 0xFFFF), // lw t9, slot(t9)
0x27BDFFF0, // addiu sp, sp, -16
0x13200003, // beqz t9, +3
0x02002021, // move a0, s0
0x0320F809, // jalr t9: func(path, key)
0x03A02821, // move a1, sp
0x02002021, // move a0, s0
0x24050000 | (PopsMan().loadArg & 0xFFFF), // li a1, loadArg
0x0220F821, // move ra, s1
MIPS_MAKE_J(loadModule), // returns to the thread's end
0x27BD0010, // addiu sp, sp, 16
};
g_popsInitThread.reset(new HLEHelperThread("ScePopsInit", code, (u32)ARRAY_SIZE(code), 0x40, 0x4000));
g_popsInitThread->Start(pathSize, g_popsInitPath);
INFO_LOG(Log::sceKernel, "POPS: init will call scePopsManLoadModule(%s, %d)", gamePath.c_str(), PopsMan().loadArg);
return true;
}