# YuGiOhForbiddenMemoriesRecomp Yu-Gi-Oh! Forbidden Memories (USA, SLUS-01411, NTSC-U) statically recompiled to a native PC executable with [PSXRecomp](https://github.com/mstan/psxrecomp). > This is a first bring-up pass, not a finished port, but it's playable: a > full duel — including a card fusion and a save — has been completed by a > human playing the MinGW build directly. See [Status](#status) and > [Screenshots](#screenshots). ## What This Is This repository contains the game-specific configuration, seeds, and build glue for running Yu-Gi-Oh! Forbidden Memories on the PSXRecomp framework. The game's MIPS code is machine-translated ("recompiled") ahead of time into native C, then compiled into a real Windows program that runs the game's own logic on a simulation of the PS1 hardware (GPU, SPU, GTE, CD-ROM, memory cards) plus the real, recompiled PS1 BIOS — no high-level emulation shims for the BIOS itself. It does **not** contain the game disc image, a retail PS1 BIOS, generated game code, or any decompiled/decompressed game assets. The framework bundles the MIT-licensed OpenBIOS (PCSX-Redux) and uses it by default, so no Sony BIOS is required. Game data comes from your own legally obtained disc. Important files: - `game.toml` — recompiler / runtime / video config (load address, entry PC, text size, seeds path, disc path, renderer, ...). - `seeds/funcs.txt` — JAL-target function-start seeds, auto-generated by the framework's `psxrecomp-toml` tool from the executable itself. Just addresses, not game code. - `tools/regen.ps1` — regenerates the recompiled C by invoking the framework recompiler against `game.toml`. - `psxrecomp/` — the [PSXRecomp](https://github.com/mstan/psxrecomp) framework, pulled in as a **git submodule** pinned to a known-good commit (`483a0d4`, the same commit `TombaRecomp` itself pins). - `docs/boot-log-mingw-relwithdebinfo.txt` — captured stdout from a real run (MinGW build), evidence for the boot/disc-read status below. - `docs/screenshots/` — a captured progression from the Konami splash screen through to a live duel (see [Screenshots](#screenshots)). - `docs/tcp-input-injection.md` + `tools/psx_dbg.py` — how the framework's TCP debug server was used to drive the game (press buttons, take screenshots) programmatically instead of simulating window input. ## Status | Area | State | |---|---| | Recompiler run (EXE → C) | Works — 3089 functions, ~3.4M lines of C, clean run | | Build (MinGW-w64) | Works — clean build, no source workarounds needed | | Build (MSVC) | Compiles, but the resulting binary crashes at startup — see [Known Issues](#known-issues) | | PS1 BIOS boot (recompiled, LLE) | Works (MinGW build) | | Disc mount / region detect | Works (MinGW build) — reads region + serial off the mounted image | | Title / main menu | Works | | Duel / gameplay | **Works** — a full duel, including a card fusion, completed by a human playing the MinGW build directly | | Memory cards, save/load | **Works** — save confirmed during manual play | | Renderers | `software`/`opengl` both work and are benchmarked in a real duel scene; `vulkan` exists in source but is not compiled into this repo's binary — see [Known Issues](#known-issues) | ## Session update — 2026-08-10 Several things below (Vulkan "not compiled", RelWithDebInfo builds, the software-default rationale) were true of the earlier pass but have since moved. Highlights of this session: - **Release build is ~2.2× faster — the single biggest win.** Switching the runtime from `-DCMAKE_BUILD_TYPE=RelWithDebInfo` (`-O2 -g`) to **`Release` (`-O3 -DNDEBUG`)** took the opening scene from **~0.27× → ~0.60×** real-time (≈16 → ≈36 fps, same scene, `opengl`). `-DNDEBUG` drops hot-path `assert()`s that were the dominant per-instruction cost. The recompiled emulation, not the renderer, is the framerate ceiling (`PSX_RUNTIME_PERF_DIAG=1` shows guest CPU ≈95% of wall-time; rendering ≈0). **Build `Release` for playing.** (Debug-tools builds — the TCP server on port 4470 — need `-DPSX_DEBUG_TOOLS=ON`, which `Release` disables for speed; use a separate build dir for RE work.) - **Vulkan is now built and runs** on real hardware (Intel), once the Vulkan SDK is installed so CMake finds `glslc` (auto-detected). It benchmarked the *slowest* of the three renderers here, though — kept as an option, not the default. - **Smooth 60 fps via frame interpolation.** The GL renderer interpolates presented frames up to the host refresh (guest timing untouched): `PSX_FRAME_INTERPOLATION=1 PSX_FRAME_INTERPOLATION_FPS=60` (opengl only). The game *logic* still ticks ~36 fps, but motion presents at a smooth 60. - **Modding works — a first `.psxmod` shipped.** The runtime's declarative mod system (`psxrecomp/docs/MOD_PACKAGES.md`: guarded `main_exe` byte-patches + disc overlays, in `/mods/`) was used to build **Infinite StarChips**: reverse-engineered the reward code (`wtrace` on the StarChip RAM address `0x801D07E0` → the writer at `0x80021EE4` → the game's own 999999 cap), then a one-instruction guarded patch (`nop` the cap-skip branch at `0x80021EFC`) makes every duel win max StarChips. No need to read the recompiled C — mods patch the original MIPS the recomp faithfully runs, guided by RE tools + community data (Data Crystal RAM map, GameShark codes). This is the model for community mods. - **A companion launcher** (arcane-themed LÖVE front-end: renderer picker, disc setup, save backups, a Smooth-60fps toggle, and a **Mods tab** that manages the `.psxmod` features above) lives at [YuGiOhRecompLauncher](https://github.com/eduardocalafell/YuGiOhRecompLauncher). The detailed prior-pass analysis (scene-dependent renderer benchmark, native/ interpreter split, widescreen scoping, path-to-60fps) is retained verbatim below for the record; read it with the Release-build / Vulkan-built updates above in mind. ## Screenshots Captured via the TCP debug server (see [`docs/tcp-input-injection.md`](docs/tcp-input-injection.md)), in order: | | | | |---|---|---| | ![Konami splash](docs/screenshots/01-konami-splash.png) Konami splash | ![Title screen](docs/screenshots/02-title-screen.png) Title screen | ![Name entry](docs/screenshots/03-name-entry.png) Name entry | | ![Intro cutscene](docs/screenshots/04-intro-cutscene.png) Intro cutscene | ![Intro dialogue](docs/screenshots/05-intro-dialogue.png) Intro dialogue | ![Town Plaza](docs/screenshots/06-town-plaza.png) Town Plaza hub | | ![Card screen](docs/screenshots/07-card-screen.png) Card screen | ![Duel start](docs/screenshots/08-duel-start.png) Duel start (LP 8000 vs 8000) | ![Card placed](docs/screenshots/09-duel-card-placed.png) Monster placed on the field | That automated pass got as far as placing a monster on the field; a full duel with a card fusion and a save was subsequently completed in a normal human play session on the same MinGW build. ## Requirements You need your **own legally obtained** dump of Yu-Gi-Oh! Forbidden Memories (SLUS-01411, NTSC-U). Nothing disc- or BIOS-related is included in this repository, and never will be — see [License / Legal](#license--legal). - A raw disc image with 2352-byte sectors (`.bin`/`.img` + `.cue`, or equivalent). A CloneCD dump (`.ccd`/`.img`/`.sub`) works as-is for the data track **as long as it's unscrambled** (check `DataTracksScrambled=0` in the `.ccd`) — the `.img` is then already a byte-for-byte standard raw `.bin` and just needs a minimal `.cue` pointing at it; no `ccd2iso`-style re-encoding (that would drop the Mode2/Form2 XA audio sectors). - The extracted main executable, `SLUS_014.11`, pulled out of the disc's ISO9660 filesystem. Use the framework's own extractor: ```sh python3 psxrecomp/tools/extract_psx_exe.py path/to/your.img SLUS_014.11 ygo/SLUS_014.11 ``` - A `disc/YUGIOH.cue` + your disc image, referenced by `game.toml`'s `disc = "disc/YUGIOH.cue"`. Minimal example (point `FILE` at your own image — an absolute path works and avoids copying the disc into the repo tree): ``` FILE "C:\path\to\your\YUGIOH.img" BINARY TRACK 01 MODE2/2352 INDEX 01 00:00:00 ``` ## Building From Source **Use MinGW-w64, not MSVC** — see [Known Issues](#known-issues) for why. Requirements: - CMake 3.20+ and Ninja. - MSYS2 MinGW-w64 toolchain (`mingw-w64-x86_64-toolchain`, `mingw-w64-x86_64-cmake`, `mingw-w64-x86_64-ninja`). MSYS2's own bundled `cmake`/`ninja` had a broken local CA-bundle (0-byte `ca-bundle.crt`) that breaks `FetchContent`'s HTTPS dependency downloads on a fresh install; if you hit `SSL Trust Anchors: error adding trust anchors`, either fix your MSYS2 `ca-certificates` install or configure once with a CMake/Ninja pair that has working TLS (any Windows-native CMake works), then build with MinGW gcc/g++ as the compiler from then on. - Python 3 (for `extract_psx_exe.py`). Clone with the framework submodule: ```sh git clone --recurse-submodules https://github.com//YuGiOhForbiddenMemoriesRecomp.git # or, in an existing clone: git submodule update --init --recursive ``` Then, **in order**: **Step 1 — Build the framework recompiler tool (one time):** ```sh cmake -S psxrecomp/recompiler -B psxrecomp/recompiler/build -G Ninja -DCMAKE_BUILD_TYPE=Release cmake --build psxrecomp/recompiler/build ``` **Step 2 — Extract your game's PS-X EXE** (see [Requirements](#requirements) above) to `ygo/SLUS_014.11`, and set up `disc/YUGIOH.cue`. **Step 3 — Generate the recompiled C** (`generated/SLUS_014.11_{full,dispatch}.c`): ```powershell pwsh tools/regen.ps1 # ...or invoke the recompiler directly on any platform: psxrecomp/recompiler/build/psxrecomp-game --config game.toml ``` **Step 4 — Configure and build the game runtime with MinGW:** ```sh cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DPSX_RECOMP_UI=OFF \ -DCMAKE_C_STANDARD=17 cmake --build build --target psx-runtime ``` (`PSX_RECOMP_UI=OFF` because this repo doesn't vendor the optional `recomp-ui` launcher submodule. `RelWithDebInfo` keeps symbols and turns on the framework's TCP debug server — see `psxrecomp/TCP_COMMANDS.md`.) **Step 5 — Run:** ```powershell .\build\Yu_Gi_Oh__Forbidden_Memories_Recompiled.exe --game game.toml ``` ## Known Issues ### MSVC build crashes at startup; MinGW-w64 does not Both compilers produce a working *build* (link succeeds), but only the MinGW-w64 (GCC 16.2.0) binary actually runs — it boots the recompiled BIOS, mounts the disc, and reaches the main menu. The MSVC (VS 2022 BuildTools 17.14, `cl` 19.44) binary crashes almost immediately with: ``` Exception code: 0xC0000409 (STATUS_STACK_BUFFER_OVERRUN / fail-fast path) Exception data: 0x0000000000000005 -> FAST_FAIL_FATAL_APP_EXIT Faulting module: ucrtbase.dll ``` `FAST_FAIL_FATAL_APP_EXIT` is what UCRT's `abort()` family raises on modern Windows — this is very likely a controlled `abort()`/contract-violation somewhere in the framework's runtime under MSVC's stricter C runtime, not a wild-pointer crash, but the root cause is **not yet identified**: getting further needs either a live debugger attached or an elevated session to enable WER `LocalDumps` for the process (both out of scope for this pass — the process aborts too fast for the diagnostic text, if any, to survive `stdout`'s buffering before the abrupt process teardown). Three real MSVC-specific portability gaps were found and worked around (build-flag-only, no source edits) to get MSVC to link at all — worth fixing upstream in the framework. Reported upstream with fresh, literal `cl.exe` output for all three: [mstan/psxrecomp#122](https://github.com/mstan/psxrecomp/issues/122). 1. **`runtime/src/main.cpp`**: `std::min`/`std::max` collide with the `` `min`/`max` macros (`C2589`/`C2059`). Workaround: `/DNOMINMAX`. 2. **`runtime/src/audio_trace.c`**: MSVC's C11 `` requires the opt-in `/experimental:c11atomics` flag (`C1189`) even in `/std:c17` mode. 3. **`generated/OpenBIOS_dispatch.c`** (BIOS-backend descriptor, framework code — not game-specific): a file-scope `static const uint32_t ... = 907u;` is used to initialize another `static` struct. GCC/Clang accept a `const`-qualified object as a static initializer as a long-standing extension; MSVC correctly rejects it per strict ISO C (`C2099: initializer is not a constant`). ### Renderer benchmark — now measured in a real duel, not just the hub Reported by a human playtester: the game runs noticeably slow / low-framerate with `game.toml`'s `[video] renderer = "software"` (the CPU rasterizer). This is a card-battle game with several animated 3D card/monster models per duel scene, exactly the kind of workload a CPU software rasterizer struggles with. `[video] renderer` accepts `"software"`, `"opengl"`, or `"vulkan"` (`recompiler/src/config_loader.cpp`). Switching is a **config-only change, no rebuild needed** (it's read at runtime, not baked in at compile time). **Methodology.** `PSX_FPS_TELEMETRY=1` in the environment makes the runtime print `[FPS] game: X fps (Yx) | frames: N` to `stderr` once a second (`Y` = ratio to the PS1's native 59.94 Hz), driven by `SDL_GetPerformanceCounter()` against the real simulated-vblank count — a measured wall-clock frame rate, not a proxy (`runtime/src/main.cpp`). A prior pass could only reach the Town Plaza hub before running out of session budget, blocked by a story-gated NPC. This pass used the **user's own real save** (`saves/card1.mcd`/`card2.mcd`, from a full manual duel + fusion + save session played directly on the MinGW build) as a shortcut, driven entirely over the TCP debug server ([`docs/tcp-input-injection.md`](docs/tcp-input-injection.md), `tools/psx_dbg.py`): title → `LOAD` → `CAMPAIGN` → `LEAVE SHOP` (Town Plaza hub) → rotate to the **Pharaoh's Palace** location → advance the "Prince Edu. You've returned..." dialogue into its `` prompt → confirm → place a card on the field. This lands in a real duel — LP 8000 vs 8000, full 3D perspective board, hand UI, a placed card — that a fresh campaign save cannot reach without fighting through the same story gate every time. Numbers are steady-state averages after a ≥40 s idle in that duel state (first samples after each scene transition discarded as warm-up): | Scene | `software` | `opengl` | |---|---|---| | Splash/title/menus (disc-read-bound, not renderer-bound) | 15.1 fps avg (11.4–20.9, n=30) | 15.3 fps avg (12.3–22.9, n=30) | | Town Plaza hub (free-camera 3D, steady state) | **47.9 fps avg** (43.7–52.7, n=60) | **41.4 fps avg** (37.5–45.9, n=60) | | **Real duel scene** (Prince Edu, card placed, steady state) | 35.8 fps avg (31.5–42.0, n=40) | **38.1 fps avg** (36.5–39.6, n=52) | Findings: 1. Renderer choice is **scene-dependent**, not a universal winner. `software` is ~16% faster in the low-complexity hub; `opengl` is ~6% faster in the real duel scene, which partially validates the original playtester report — but the duel-scene gap is modest (6%, and within `software`'s own run-to-run spread of 31.5–42.0 fps) rather than the dramatic difference "noticeably slow" suggested. Neither renderer gets remotely close to the PS1's native 60 fps (0.60–0.64x) in the duel scene. 2. `phase_profile` (TCP debug server, live 10 s sampling windows) explains most of the gap: under `software`, GPU emulation (the CPU doing the rasterization itself) consumes **~20% of sampled CPU time** (`gpu_share: 0.197`), vs. **~1%** under `opengl` (`gpu_share: 0.008–0.012`, rasterization offloaded to the real GPU). That ~19-point shift roughly tracks the ~6% FPS gap once the rest of the frame budget (game logic, BIOS, framework runtime — the `static_share` bucket, ~75% under `software` vs. ~93% under `opengl`) is held constant. 3. `game.toml` ships with `renderer = "software"` as the default: it wins the hub (the most-visited scene) and the duel-scene loss is modest. This is a closer call than the old hub-only data suggested, though — a future pass with heavier duel scenes (multiple monsters/effects on both sides, not just one placed card) might tip the balance toward `opengl`; re-benchmark before assuming this holds at higher duel complexity. ### Vulkan renderer — not compiled into this build `vulkan` is a real, fairly mature backend in the framework (`gpu_vk_renderer.c`, plus a dedicated test suite: command-buffer batching, present-timing, mailbox policy, a staging cache) — not the token stub the "experimental" label might suggest. But building it requires the **Vulkan SDK's `glslc`** shader compiler at CMake configure time (`psxrecomp/runtime/runtime.cmake`); this machine's `build-mingw` was configured without it (`GLSLC_EXE:FILEPATH=GLSLC_EXE-NOTFOUND` in `build-mingw/CMakeCache.txt`), so `gpu_vk_renderer.c` was compiled as an inert stub and no Vulkan symbols exist in the binary at all. Setting `[video] renderer = "vulkan"` and launching confirms this at runtime, verbatim from `stdout`: ``` psxrecomp: Vulkan renderer is not available in this build (PSX_ENABLE_VULKAN=OFF); using OpenGL instead. psxrecomp: renderer = opengl (opengl) ``` This is a clean, intentional fallback (`runtime/src/main.cpp`, `#ifndef PSX_HAVE_VULKAN` guard) — not a crash, not a silent no-op — but it means **Vulkan has never actually been exercised for this game**. To get a real measurement: install the Vulkan SDK (provides `glslc` + headers), make sure `VULKAN_SDK` is set or `glslc`/headers are on the default search paths, then reconfigure and rebuild (`cmake -S . -B build-mingw ...` per [Building From Source](#building-from-source) — CMake will pick up `PSX_HAVE_VULKAN` automatically once `glslc` is found) and re-run the same duel-scene `PSX_FPS_TELEMETRY=1` methodology above. Given the backend's apparent present-path maturity, it's a real candidate to beat `opengl`'s 38.1 fps — genuinely unknown until someone builds it. ### Native vs. interpreted code — measured, does not explain the FPS ceiling The working hypothesis going in was that a large fraction of duel-time CPU work might be falling through to the framework's dirty-RAM MIPS interpreter (the fallback for code installed/patched into RAM at runtime, which a static recompiler cannot see ahead of time — see `psxrecomp/CLAUDE.md` rule 18) instead of running as natively-compiled C, and that this might explain sub-60fps performance independent of renderer choice. The TCP debug server has the instrumentation to check this directly, live, during the duel scene (`dispatch_stats`, `dirty_ram_stats`, `phase_profile` — all queried in-session, not estimated): - **`dispatch_stats`**: `miss_total: 0`, `miss_unique: 0` in every session, millions of `static_hits`. Every function-level dispatch in the base EXE's own recompiled code resolves natively — **zero runtime dispatch misses**. The commonly-cited "~36% of functions have a confirmed clean prologue/epilogue" figure is a static-analysis quality metric on the recompiler's output, not a live dispatch-miss rate; it does not mean 64% of calls fall back to anything at runtime. - **`dirty_ram_stats`** *is* real and nonzero: `8.8M` interpreter blocks / `75M` instructions executed cumulatively over a session (boot through the duel). The hot addresses (e.g. one PC alone: 792,839 hits / 27.99M instructions) sit **inside** the main EXE's own static `text_size` window (`0x10000`–`0x1E0000`), meaning the game overlays/patches its own code at those addresses at runtime — invisible to the static recompiler by construction, so it genuinely has to be interpreted. This is a confirmed, real gap, not a guess. - **But `phase_profile`'s live sampling** (the metric that actually answers "where does duel-scene time go") shows the interpreter is a **minor** contributor in the steady-state duel scene: `interp_share` is consistently **~2.3–2.9%** of sampled CPU time (three separate samples, both renderers), while `static_share` (natively-compiled code) dominates at **~75–93%**. `overlay_loader_status`/`overlay_native_ring` confirm the framework's native-overlay compilation pipeline (used by TombaRecomp to convert exactly this class of runtime-loaded code to native C) is not set up for this game at all (`registered: 0`) — so the gap is real and a legitimate future optimization target, but it is **not** the dominant cost in the scene measured here. Whoever tackles it next should re-check `phase_profile` during a busier moment (simultaneous card-effect animations, both sides' fields populated) — this session's window was a relatively quiet single-card idle state, and interpreter share could be higher during heavier duel moments. - **Conclusion:** the native/interpreter ratio does **not** explain the ~38fps duel-scene ceiling under `opengl`. The honest correction to the original hypothesis: static/native code is already ~93% of sampled time under `opengl`; the remaining gap to 60fps is in what that native code itself costs (game logic, BIOS simulation, framework/MMIO overhead — see [Path to 60fps](#path-to-60fps) below), not in a missing-native-coverage problem. ### Widescreen — investigated, not implemented The framework has first-class, generic widescreen support (`psxrecomp/WIDESCREEN.md`, proven on TombaRecomp): a `[video] aspect_ratio = "W:H"` config plus an optional per-game `[widescreen]` block that tags the title's own render/cull call sites. `aspect_ratio` defaults to `"4:3"`, a mathematical identity — the framework's own docs are explicit that bringing up a **new** title is "a question of locating that title's own cull sites," not a drop-in toggle. **What was tried this session (config-only, no per-game work):** `[video] aspect_ratio = "16:9"` was added with **no** `[widescreen]` block. On boot, the runtime confirms it parsed the request (`psxrecomp: widescreen 16:9 (native-wide, present 1:1; engages at game entry)`) — but live `gpu_state` telemetry through the title screen, main menu, and the Town Plaza hub consistently reported `"ws":{"configured":0,"active":0,"present_native_43":1,...}`: the feature never actually engaged, and every screen rendered byte-for-byte 4:3. This empirically confirms the framework's own docs rather than contradicting them — for a title with zero per-game tagging, the generic config is inert by design, not broken. **Why this game specifically needs real per-title work, not a quick patch:** Forbidden Memories mixes two very different scene types visible in this session's own navigation: - **Town Plaza hub**: a genuine free/rotatable 3D camera (confirmed — rotating the view during navigation changed which building was labeled and its screen position). This needs the same object-visibility/cull-margin class of work `WIDESCREEN.md` still lists as its **top open item even for TombaRecomp** ("world-space culling pop-in — ROOT-CAUSED, patch set ready, NOT YET IMPLEMENTED" upstream) — a full per-function Ghidra pass to find the draw-classifier and despawn-margin immediates. - **Duel screens**: a fixed/scripted camera looking at a 3D-perspective game board, with card art and LP/UI as what are very likely screen-space 2D overlays (not free-camera billboards) — potentially *simpler* than Tomba's character-sprite-tagging problem, since there's no player camera to break the illusion. But this is a hypothesis, not a Ghidra-confirmed fact: it needs the same investigation to know whether the board's own GTE projection needs squash/cull treatment, and whether the newer `[widescreen.cull] auto_screen_x` auto-detection (shipped for Tomba's native-wide 16:9, pattern-matches the common `sltiu …,0x140`/`sltiu …,0xE0` screen-extent reject funnel with no manual address-hunting) covers the duel board's own reject logic for free. - **Menus** (deck building, card list, shop): almost certainly plain 2D, likely covered by the framework's existing generic `hud_sprt_squash`/ HUD-edge-anchoring with no per-game work — this is the one piece that's plausibly "free" here, matching how Tomba's menus/HUD are described. No Ghidra (or equivalent MIPS static-analysis) access was available in this session, so the actual per-title investigation — finding the duel board's render-funnel function, confirming whether card art needs `sprite_tag_funcs`, and finding the hub's draw-classifier/despawn-margin immediates (the same `FUN_80022E44`-style search `WIDESCREEN.md` documents doing for Tomba) — could not be attempted. This is a genuinely bigger project than this session's scope, matching the framework's own characterization; see the roadmap below rather than a half-finished patch. **Widescreen roadmap, in order:** 1. Attach Ghidra (or equivalent) to `ygo/SLUS_014.11` and find the duel board's screen-space reject/cull immediates — try `[widescreen.cull] auto_screen_x` first since it may need no manual address-hunting at all. 2. Determine whether duel-board card art is drawn via a shared per-prim GTE helper (needs `sprite_tag_funcs`, Tomba-style) or plain screen-space SPRTs (likely covered by existing `hud_sprt_squash` for free). 3. Separately tackle the Town Plaza hub's object-cull/despawn margins — the harder, still-open-upstream problem; consider shipping duel/menu widescreen first and gating the hub behind `4:3` if the cull work stalls. 4. Regenerate (`pwsh tools/regen.ps1`) after any `[widescreen]` config change, then verify visually via the TCP debug server's `wide_shot`/ `gpu_state` (`ws.configured`/`ws.active`) — not by eyeballing a `screenshot`, which captures the native 4:3 VRAM resolve and stays blind to the wide present surface (confirmed while testing this session: use `wide_shot`/`wide_full` for the actual wide frame). ### Path to 60fps Renderer choice alone will not reach 60fps: even `opengl`, the faster of the two in the duel scene, tops out at ~38 fps (0.64x). The native/interpreter ratio (this session's original suspect) is measured and **ruled out** as the dominant cause — see above. What's left, roughly prioritized: 1. **Profile inside the "static" bucket.** `phase_profile` currently only gives the coarse interp/native/static/gpu/other/exc split; it does not say what *inside* that ~75–93% static share is expensive. A hot-function breakdown of the live duel scene (`fn_stats`, `fntrace_arm` targeted at the duel-scene PC range) would distinguish "this is real PS1 game logic running as fast as compiled C can run it" from "framework/MMIO/timer simulation overhead worth trimming" — currently unknown either way. 2. **Look at `exc_share`** (~10–12% of sampled time in every duel-scene sample, both renderers) — likely COP0 exception/IRQ dispatch (VBlank, timers, SIO). A recompiled-BIOS exception path is a known place for recomp overhead relative to real silicon; worth measuring precisely before assuming it's irreducible. 3. **Wire up native-overlay compilation for this game's runtime-loaded code** (the confirmed-real `dirty_ram_stats` gap above). Currently a minor contributor in the single-card idle scene sampled here, but `overlay_loader_status` shows it's simply never been set up for this game (`registered: 0`) — re-measure under a busier duel moment before deciding whether it's worth the effort. 4. **Get a real Vulkan build measured** (see above) — the backend has nontrivial present-path optimization behind it that's never been exercised for this game. 5. **Accept that some gap may be structural.** The original PS1 hardware hit 60fps here with a purpose-built GPU/GTE ASIC; simulating that faithfully in software (whichever renderer) has a real floor. 60fps is a realistic target to *work toward* with the steps above, not a single-config fix — this session's contribution is ruling out one hypothesis (interpreter fallback) with live evidence and leaving a concrete, prioritized list for whoever continues it, rather than guessing at a fix. ### Not yet tested A real Vulkan build (this repo's binary was compiled without it — see above) and the widescreen per-title cull/sprite-tag work (scoped above, not implemented). Everything else through a full duel with fusion and save has been played manually — see [Status](#status). ## Credits Built on [PSXRecomp](https://github.com/mstan/psxrecomp) by [mstan](https://github.com/mstan) and contributors — the static MIPS-to-C recompiler and runtime framework that makes this possible. See that repository for the framework's own architecture docs, build instructions, and the wider ecosystem of recompiled PS1 games (TombaRecomp, MegaManX6Recomp, ApeEscapeRecomp, and others). ## License / Legal Yu-Gi-Oh! Forbidden Memories is copyright Konami. **This repository contains none of the game's original binaries, assets, disc data, or BIOS.** Nothing here is distributable game content: - `ygo/` and `disc/` (your extracted executable and disc image) are git-ignored and never committed. - `generated/` (the recompiled C, derivative of the copyrighted executable) is git-ignored, build output, and regenerated locally from your own disc. - No compiled binary (`.exe`) is published here or as a release artifact — a compiled binary embeds the game's logic natively, same as the generated C, and is withheld for the same reason. - `seeds/funcs.txt` contains only hex addresses (facts about where functions start in the executable), not code. This repository's own code (config, build glue, tooling) is provided as-is for documentation/research purposes. No license is granted for the game content it builds against; obtain and use your own legal copy.