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; }