From 5a26494586044c8f8fd029eb69f52a171548fdb6 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Mon, 26 Oct 2026 21:59:00 -0500 Subject: [PATCH] tpm, mgs: memory cards, the games' own saves, on card image files The Phantom Menace and Metal Gear Solid save to a memory card in port 1: saves/tpm_card1.mcd and saves/mgs_card1.mcd, raw 128 KB card images as PC emulators use, made the first time the game looks. Unlike save states, which hold raw memory and load only into the build that wrote them, these survive firmware updates. - tpm-decomp fea7648..d51af21: libmcrd natively on the card image (tested on the desktop and the board: a save from one build loaded into another); recomp.py treats a jr to the function's own return address as a return (the save screen's bit packer returns through $a3) - psyz 3245d54: mcd, the card image and the BIOS's buXX: device - mgs_reversing 6d58e9f: the port's bu00: device and memcard.c on it (the game finds the card at boot) - Cart::get_memory_card_path names the image after the game's directory Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01SFCMzomCZ5wRHJN4sxwkvY --- README.md | 4 +++- components/mgs/README.md | 6 +++++- components/mgs/include/mgs.hpp | 4 ++++ components/mgs/mgs_reversing | 2 +- components/mgs/psyz | 2 +- components/mgs/src/mgs.cpp | 2 ++ components/tpm/CMakeLists.txt | 3 ++- components/tpm/README.md | 13 +++++++++---- components/tpm/include/tpm.hpp | 4 ++++ components/tpm/src/tpm.cpp | 2 +- components/tpm/src/tpm_engine.cpp | 10 +++++++++- components/tpm/src/tpm_engine.hpp | 6 ++++-- components/tpm/tpm-decomp | 2 +- main/cart.hpp | 10 ++++++++++ main/mgs_cart.hpp | 2 +- main/tpm_cart.hpp | 2 +- 16 files changed, 58 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 6950175..d23e71a 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,9 @@ docs/PLAN.md the Jedi Knight plan and history # (components/mgs/mgs_reversing/port/extract_disc.py) tpm/ # The Phantom Menace, USA Rev 1: the .bin of a bin/cue rip Star Wars - Episode I - The Phantom Menace (USA) (Rev 1).bin - saves/ # save states and their screenshots + saves/ # save states and their screenshots; the PlayStation + # games' memory cards (tpm_card1.mcd, mgs_card1.mcd: + # raw 128 KB images, as PC emulators use) ``` Example `metadata.csv` (one line per game: rom path, boxart, name; the diff --git a/components/mgs/README.md b/components/mgs/README.md index 14e11d9..466c50b 100644 --- a/components/mgs/README.md +++ b/components/mgs/README.md @@ -64,7 +64,11 @@ mgs/STAGE.DIR,boxart/mgs.jpg,Metal Gear Solid every thread's stack and context), keyed on the memory layout so a rebuild that leaves the game code in place keeps old states loadable. - Pause, reset, relaunch and quit in place. -- Not done: memory-card game saves (use states), full-motion video (MDEC, +- Memory card: the game's own saves (Mei Ling's codec frequency, 140.85; + the title's load) go to `/sdcard/saves/mgs_card1.mcd`, a raw 128 KB card + image as PC emulators use, made the first time the game looks. Unlike + save states they survive any rebuild. +- Not done: full-motion video (MDEC, movies are skipped). 56 of the 96 stages have decompiled actors upstream. ## Measuring diff --git a/components/mgs/include/mgs.hpp b/components/mgs/include/mgs.hpp index 366bb7f..13107c1 100644 --- a/components/mgs/include/mgs.hpp +++ b/components/mgs/include/mgs.hpp @@ -16,6 +16,10 @@ struct Config { /// Directory holding the disc files: STAGE.DIR, RADIO.DAT, FACE.DAT, ... /// (what port/extract_disc.py produces), e.g. "/sdcard/mgs". std::string data_dir; + /// The memory card in port 1, a 128 KB card image file (the format PC + /// emulators use): the game's own saves go there. Made, formatted, the + /// first time the game looks. Empty: no card. + std::string memory_card; }; /// Start the game. Returns false if the data is missing or the engine could diff --git a/components/mgs/mgs_reversing b/components/mgs/mgs_reversing index 0ac4d5d..6d58e9f 160000 --- a/components/mgs/mgs_reversing +++ b/components/mgs/mgs_reversing @@ -1 +1 @@ -Subproject commit 0ac4d5d167a0ff0b2013005844e08fd74d3428be +Subproject commit 6d58e9fe56d7fbdeafe93b59f7d17f62a9f3a90c diff --git a/components/mgs/psyz b/components/mgs/psyz index 0c2cfcb..3245d54 160000 --- a/components/mgs/psyz +++ b/components/mgs/psyz @@ -1 +1 @@ -Subproject commit 0c2cfcb60ab0e9638e8c7ab7f0e7d1abce766f65 +Subproject commit 3245d546f1a2becce2e17333d2e65e8dc9d8c612 diff --git a/components/mgs/src/mgs.cpp b/components/mgs/src/mgs.cpp index 2921674..61f30a9 100644 --- a/components/mgs/src/mgs.cpp +++ b/components/mgs/src/mgs.cpp @@ -15,6 +15,7 @@ #include #include "../mgs_reversing/port/mgs_snapshot.h" +#include #include "freertos/FreeRTOS.h" #include "freertos/task.h" @@ -323,6 +324,7 @@ bool init(const Config &config) { // the directory must be called MGS (FAT is case-insensitive about it). const auto root = std::filesystem::path(config.data_dir).parent_path().string(); Mgs_SetDataRoot(root.c_str()); + Mcd_SetPath(0, config.memory_card.c_str()); // the "bu00:" device (psyz/mcd.h) mgs_platform_init(config.data_dir.c_str()); // the frame buffers the scanout converts VRAM into (the S3 port's app_main // does this; here it is the cart's job) diff --git a/components/tpm/CMakeLists.txt b/components/tpm/CMakeLists.txt index 96148d2..bd8a5ae 100644 --- a/components/tpm/CMakeLists.txt +++ b/components/tpm/CMakeLists.txt @@ -119,7 +119,8 @@ if(TPM_ENGINE_READY) set(rt ${TPM_DECOMP}/recomp/runtime) file(GLOB rt_srcs ${rt}/*.c) file(GLOB gen_srcs ${TPM_GEN}/recomp_*.c) - set(psyz_srcs ${TPM_PSYZ}/psyz/src/psyz/psyz_spu.c) # (the GTE is the runtime's own: rt_gte.c) + # (the GTE is the runtime's own: rt_gte.c); psyz_mcd.c: the memory card's image file + set(psyz_srcs ${TPM_PSYZ}/psyz/src/psyz/psyz_spu.c ${TPM_PSYZ}/psyz/src/psyz/psyz_mcd.c) add_library(tpm_rt STATIC ${gen_srcs} ${rt_srcs} ${psyz_srcs}) idf_build_get_property(config_dir CONFIG_DIR) target_include_directories(tpm_rt PRIVATE diff --git a/components/tpm/README.md b/components/tpm/README.md index 0449290..a2e3e91 100644 --- a/components/tpm/README.md +++ b/components/tpm/README.md @@ -120,15 +120,20 @@ menu (3.6 MB each; save 1.5 s, load 0.6 s). - Pads: the HAL's merged gamepad, face buttons by position (the HAL's B is south): B = cross, A = circle, Y = square, X = triangle, L/R = L1/R1. - Movies play at full speed (15 fps, the game thread ~80% busy) with their - XA audio when the overlay was recompiled (`CONFIG_TPM_DISC_PATH`). There is - no memory card (the game says so and carries on). + XA audio when the overlay was recompiled (`CONFIG_TPM_DISC_PATH`). +- Memory card: the game's own saves (pause menu: Save Game; title: Load + Game) go to `/sdcard/saves/tpm_card1.mcd`, a raw 128 KB card image as PC + emulators use (DuckStation, ePSXe, ...), made the first time the game + looks. Unlike save states they load into any firmware build. - Frameskip (`CONFIG_TPM_FRAMESKIP`, default 2): when the game is ahead of the GPU, the worker leaves up to that many frames in a row undrawn, so the game keeps its pace instead of running in slow motion. - The pause menu pauses the game at its next loop back-edge. Save states hold guest RAM, VRAM, the SPU, the CPU / GTE / hardware state and the - runtime's live statics; without enough free PSRAM for the game's arena - (after another big cart) the game still runs, without save states. + runtime's live statics, and load only into the firmware build that wrote + them (the memory card is for keeping progress across updates); without + enough free PSRAM for the game's arena (after another big cart) the game + still runs, without save states. Where the time goes (in the level): both the game thread and the GPU worker are ~95% busy. The game thread is the recompiled code (with g_ram pinned in diff --git a/components/tpm/include/tpm.hpp b/components/tpm/include/tpm.hpp index 5c3b263..4809383 100644 --- a/components/tpm/include/tpm.hpp +++ b/components/tpm/include/tpm.hpp @@ -21,6 +21,10 @@ struct Config { /// Directory holding the disc's files: SLUS_008.84 and FILE.000 (FILE.001 /// .. FILE.003 later, for streams and movies), e.g. "/sdcard/tpm". std::string data_dir; + /// The memory card in port 1, a 128 KB card image file (the format PC + /// emulators use): the game's own saves go there. Made, formatted, the + /// first time the game looks. Empty: no card. + std::string memory_card; }; /// Open the data. Returns false if it is missing or not the USA Rev 1 disc. diff --git a/components/tpm/src/tpm.cpp b/components/tpm/src/tpm.cpp index ddf7f60..6b2c2e0 100644 --- a/components/tpm/src/tpm.cpp +++ b/components/tpm/src/tpm.cpp @@ -73,7 +73,7 @@ bool show(size_t index) { bool init(const Config &config) { config_ = config; #if defined(TPM_HAVE_ENGINE) - if (engine::init(config.data_dir)) { + if (engine::init(config.data_dir, config.memory_card)) { engine_ = true; running_ = true; return true; diff --git a/components/tpm/src/tpm_engine.cpp b/components/tpm/src/tpm_engine.cpp index 3518128..7713d93 100644 --- a/components/tpm/src/tpm_engine.cpp +++ b/components/tpm/src/tpm_engine.cpp @@ -63,6 +63,7 @@ constexpr uint32_t AUDIO_RATE = 44100; FILE *g_disc{nullptr}; uint32_t g_disc_sectors{0}; +std::string g_memory_card; // the card image in port 1 (plat_memcard_path) std::vector g_exe; TaskHandle_t g_game_task{nullptr}; @@ -810,7 +811,8 @@ std::string find_disc(const std::string &dir) { } // namespace -bool init(const std::string &data_dir) { +bool init(const std::string &data_dir, const std::string &memory_card) { + g_memory_card = memory_card; const auto disc = find_disc(data_dir); if (disc.empty()) { logger.error("no disc image (.bin, raw 2352-byte sectors) in {}", data_dir); @@ -1482,6 +1484,12 @@ static void __attribute__((noinline)) snap_saved_wait(void) { } } +// The game's own saves (libmcrd, tpm-decomp's rt_hle.c): a card image file. +const char *plat_memcard_path(int port) { + using namespace tpm::engine; + return port == 0 && !g_memory_card.empty() ? g_memory_card.c_str() : nullptr; +} + void plat_pause_wait(void) { using namespace tpm::engine; const int req = g_snap_req.load(); diff --git a/components/tpm/src/tpm_engine.hpp b/components/tpm/src/tpm_engine.hpp index f628e1a..f6f7f75 100644 --- a/components/tpm/src/tpm_engine.hpp +++ b/components/tpm/src/tpm_engine.hpp @@ -8,8 +8,10 @@ namespace tpm::engine { /// Open the disc image in data_dir (a .bin of raw 2352-byte sectors) and -/// start the game. Returns false if there is no usable image. -bool init(const std::string &data_dir); +/// start the game. Returns false if there is no usable image. memory_card: +/// the image file of the card in port 1 (made the first time the game looks; +/// empty: no card). +bool init(const std::string &data_dir, const std::string &memory_card); void deinit(); bool running(); void pause(); diff --git a/components/tpm/tpm-decomp b/components/tpm/tpm-decomp index 00fcb3b..d51af21 160000 --- a/components/tpm/tpm-decomp +++ b/components/tpm/tpm-decomp @@ -1 +1 @@ -Subproject commit 00fcb3bd8f23f925644eab30d1c1e54d3ef922fb +Subproject commit d51af21f74cccaf1a5d492c7ef39a48c3468bb98 diff --git a/main/cart.hpp b/main/cart.hpp index 7a39f5e..f15ef91 100644 --- a/main/cart.hpp +++ b/main/cart.hpp @@ -240,6 +240,16 @@ class Cart { return ""; } + /// The image file of a PlayStation game's memory card (the raw 128 KB + /// format PC emulators read), named after the game's data directory + /// (saves/tpm_card1.mcd): the game's own saves, which unlike save states + /// do not depend on the firmware build. + std::string get_memory_card_path(int card = 1) const { + namespace fs = std::filesystem; + const auto game = fs::path(get_rom_filename()).parent_path().filename().string(); + return savedir_ + "/" + game + fmt::format("_card{}.mcd", card); + } + std::string get_paused_image_path() const { return savedir_ + "/paused" + get_screenshot_extension(); } std::string get_screenshot_path(bool bypass_exist_check = false) const { diff --git a/main/mgs_cart.hpp b/main/mgs_cart.hpp index 623bbed..b7ed54d 100644 --- a/main/mgs_cart.hpp +++ b/main/mgs_cart.hpp @@ -18,7 +18,7 @@ class MgsCart : public Cart { handle_video_setting(); #if defined(ENABLE_MGS) const auto data_dir = std::filesystem::path(get_rom_filename()).parent_path().string(); - if (!mgs::init({.data_dir = data_dir})) { + if (!mgs::init({.data_dir = data_dir, .memory_card = get_memory_card_path()})) { logger_.error("could not start Metal Gear Solid"); running_ = false; } diff --git a/main/tpm_cart.hpp b/main/tpm_cart.hpp index ca917f2..19f3ef3 100644 --- a/main/tpm_cart.hpp +++ b/main/tpm_cart.hpp @@ -18,7 +18,7 @@ class TpmCart : public Cart { handle_video_setting(); #if defined(ENABLE_TPM) const auto data_dir = std::filesystem::path(get_rom_filename()).parent_path().string(); - if (!tpm::init({.data_dir = data_dir})) { + if (!tpm::init({.data_dir = data_dir, .memory_card = get_memory_card_path()})) { logger_.error("could not start The Phantom Menace"); running_ = false; }