Skip to content

feat: add Metal Gear Solid (native decompilation via psyz) - #125

Open
finger563 wants to merge 10 commits into
mainfrom
feat/metal-gear-solid
Open

finger563 wants to merge 10 commits into
mainfrom
feat/metal-gear-solid

Conversation

@finger563

Copy link
Copy Markdown
Contributor

Summary

Adds Metal Gear Solid to esp-box-emu as a new core (components/mgs). This is not an emulator: it is the FoxdieTeam decompilation of the PlayStation game compiled natively for the ESP32-S3, following davidmonterocrespo24's port — the PSX SDK is replaced by psyz (C reimplementation with a software GPU rasterizer and the GTE) plus the PSY-Q library decompilation, and the PSX kernel's threads/vblank run on FreeRTOS. See components/mgs/VENDOR.md for the vendored trees, upstream commits, licenses and every change made.

What's in it

  • components/mgs/ — the game (419 translation units + the port layer), psyz, PSY-Q subset, shim headers, and the box glue:
    • src/mgs.cpp lifecycle: the game runs in its own task on core 0; run_mgs_rom() only maps the gamepad and paces. VRAM (1MB), main RAM (~1.4MB), face group (512KB) and two RGB565 present buffers are carved from the 4MB ROM block via the pool allocator.
    • Quit fully stops and deinitializes the core, no reboot: every static of libmgs.a is bracketed by linker SURROUND symbols (linker.lf: 128KB .bss → PSRAM, 41KB .data → DRAM); .data is snapshotted before the first launch and .bss/.data reset on deinit, after stopping the game at an mts-idle point and deleting its tasks. Verified on hardware: internal RAM returns to the byte across quit/relaunch cycles.
    • mgs_input.c PSX digital-pad frames (D-pad, A=○, B=✕, X=□, Y=△, START, SELECT; SELECT+A/B/X/Y = L1/R1/L2/R2); mgs_present.c BGR555→RGB565 presenter.
    • A hang detector (esp_timer watching the vblank tick and frame counter; dumps scheduler state and all task backtraces via esp_rom_printf).
  • Integration: main/mgs_cart.hpp, carts.hpp, rom_info (METAL_GEAR_SOLID, matched on STAGE.DIR), top-level CMake (MGS_COMPONENTS / ENABLE_MGS), README systems table + setup section.

Setup

Extract your own disc 1 image with components/mgs/tools/extract_disc.py into mgs/ on the card and add mgs/STAGE.DIR, boxart/mgs.jpg, Metal Gear Solid to metadata.csv. Boots straight into the dock (s00a); MGS_START_STAGE picks another stage.

Status (BOX-3, 16MB PSRAM)

  • Boots from the card, correct colors, input, emulator pause menu, quit → relaunch all verified; ~1 hour continuous play and 6 quit/relaunch cycles in one session.
  • Steady 8.9 fps at the dock — rasterizer-bound (~85 ms of each frame is per-texel PSRAM traffic), in line with the upstream port's 8–15 fps.
  • Image is 4.5MB of the 6MB app partition. Needs the 16MB-PSRAM board; the 8MB ESP-BOX is not expected to fit.
  • As upstream: no audio, codec calls, cutscenes/streaming or memory card; save states unsupported.
  • Known: a rare whole-system hang during the first stage load was seen twice (2 of ~9 launches), not reproduced since; the hang detector is in place to report it.

Test plan

  • Build (IDF 6.1), flash BOX-3
  • Launch from menu, play, colors/input correct
  • Pause menu → resume
  • Quit → relaunch (×6), memory returns to baseline (serial log)
  • 30-minute continuous runs ×2, no leaks, fps steady

🤖 Generated with Claude Code

https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1

Copilot AI balanced review requested due to automatic review settings October 5, 2026 02:19
@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

✅Static analysis result - no issues found! ✅

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The large native port and custom task lifecycle contain unresolved concurrency, memory-safety, and load-failure issues.

Review effort: Balanced
Findings: 3 High severity · 6 Medium severity · 1 Low severity

Open (10)
What changed in this PR

Adds Metal Gear Solid as a native ESP32-S3 core using the FoxdieTeam decompilation, psyz, and PSY-Q compatibility layers.

Changes:

  • Adds MGS lifecycle, rendering, input, task, and virtual-CD integration.
  • Vendors the game, psyz, and PSY-Q source trees.
  • Adds ROM detection, cart registration, extraction tools, and setup documentation.
File Description
CMakeLists.txt Enables the MGS component.
README.md Documents support and setup.
main/​carts.hpp Registers the MGS cart.
main/​mgs_cart.hpp Implements cart lifecycle integration.
components/​rom_info/​** Adds MGS identification and formatting.
components/​mgs/​CMakeLists.txt Configures the native game build.
components/​mgs/​linker.lf Places and brackets game statics.
components/​mgs/​VENDOR.md Records vendored sources and modifications.
components/​mgs/​include/​mgs.hpp Defines the core interface.
components/​mgs/​src/​** Implements lifecycle, input, and presentation.
components/​mgs/​tools/​** Adds disc extraction and stage tools.
components/​mgs/​shim/​** Supplies compatibility headers.
components/​mgs/​mgs/​** Vendors the game and ESP32 port layer.
components/​mgs/​psyz/​** Vendors the PSX SDK reimplementation.
components/​mgs/​psyq/​** Vendors the PSY-Q library subset.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread components/mgs/mgs/port/esp32_threads.c Outdated
Comment thread components/mgs/mgs/port/esp32_threads.c Outdated
Comment thread components/mgs/psyq/src/libsnd/vol.c
Comment thread components/mgs/mgs/port/esp32_threads.c
Comment on lines +147 to +150
work = GV_NewActor(EXEC_LEVEL, sizeof(Work));

printf("LoadReq\n");
work->info = FS_LoadStageRequest(dir);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a8fcef1: NewLoader returns NULL when GV_NewActor fails.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To confirm the earlier fix (a8fcef1): NewLoader returns NULL when GV_NewActor fails.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Completed in 2796bd1: NewLoader already returned NULL on a failed GV_NewActor (a8fcef1); it now also sets GM_LoadComplete = -1 so the game, which cleared it before asking and waits for the loader's Die, does not wait forever on an actor that never existed. loader.c has a single GV_NewActor call.

Comment thread components/mgs/mgs/source/libfs/stageld.c
Comment thread components/mgs/mgs/source/libfs/stageld.c
Comment thread components/mgs/psyq/src/libsnd/vs_vtb.c Outdated
Comment thread components/mgs/psyq/src/libsnd/vs_vtbp.c Outdated
Comment thread README.md Outdated
@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

ESP-IDF Size Report for 'Esp Box Emu'

Metric Base PR Delta
FLASH 3,755,576 bytes (59.69%) 5,527,256 bytes (87.85%) 🔺 +1,771,680 bytes (+28.16%)
DRAM 145,612 bytes (42.61%) 192,700 bytes (56.38%) 🔺 +47,088 bytes (+13.77%)
IRAM 0 bytes 0 bytes 0 bytes
RAM (DRAM+IRAM) 145,612 bytes 192,700 bytes 🔺 +47,088 bytes (+13.77%)

FLASH uses app .bin size or json2 flash sum. RAM sums DRAM+IRAM via idf_size. Percentages shown when totals are available.
DRAM/IRAM usage does not include memory used by the heap allocator at runtime.
This report was generated by esp-idf-size-delta.

finger563 and others added 4 commits October 5, 2026 12:24
Vendors the FoxdieTeam decompilation (davidmonterocrespo24's esp32-port
branch), psyz and the PSY-Q decomp subset as components/mgs, with the
game's three big buffers hosted in the 4MB ROM block, every libmgs
static bracketed by linker symbols so the core can be fully stopped and
relaunched without a reboot, and a gamepad/present layer for the box.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
soft_render.c binds its cached VRAM pointer from a constructor, which ran
while g_RawVram was still NULL; the first ClearImage then faulted.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
… ChangeTh trace

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
An esp_timer watches the vblank tick and the frame counter and dumps the
scheduler state and every task's backtrace through esp_rom_printf when
they stall, so a wedged console still reports where it stopped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
@finger563
finger563 force-pushed the feat/metal-gear-solid branch from 6c57566 to c6275c7 Compare October 5, 2026 17:24
finger563 and others added 3 commits October 5, 2026 12:24
cppcheck flagged the glue's section-bounds arithmetic (the linker symbols
were declared as arrays, so subtracting them read as pointers to different
objects) and a C-style cast; the vendored game, psyz and PSY-Q trees are
excluded from the scan like the other cores' engines.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
pull_request_target runs the base branch's workflow, so the -i excludes
only apply once merged; suppressions.txt is read from the PR tree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
- esp32_threads.c: OpenTh fails instead of handing a task a PSRAM stack
  (which resets the chip on its first file read); a slot keeps its stack
  and TCB across CloseTh/OpenTh instead of leaking them per task exit;
  Mgs_ThreadsStopAll releases the storage only after FreeRTOS has retired
  the deleted tasks.
- libsnd: a zero-duration crescendo no longer divides by zero; VAB ids
  are checked against NUM_VAB with a strict bound.
- stageld.c / loader.c: a missing stage or a failed allocation aborts the
  load instead of reading a negative sector or dereferencing NULL.
- README: the extractor writes MGS/ under its output directory, so it is
  pointed at the card root and the metadata entry uses MGS/STAGE.DIR.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread components/mgs/mgs/source/game/loader.c Outdated
Comment thread components/mgs/psyz/include/rand.h
… (review)

loader.c marked the actor as reading even when FS_LoadStageRequest()
returned NULL, so its next Act handed NULL to FS_LoadStageSync(). PSY-Q's
rand() is 15-bit and the game scales results by >> 15; the hosted libc's
is 31-bit, so every file of the component now gets a narrowing wrapper
through the force-included gtemac.h.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The large native port has unresolved frame-ownership and lifecycle defects alongside a known intermittent system hang.

1 open finding
4 resolved since last review

🧠 Review effort: Balanced

…eview)

The game clears GM_LoadComplete before asking for a loader and waits for
the loader actor's Die to set it; when GV_NewActor fails that actor never
exists, so NewLoader now sets it itself instead of leaving the game
waiting forever.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The large native port still has unresolved lifecycle, buffer-ownership, startup-failure, path-safety, and tooling issues.

2 open findings

🧠 Review effort: Balanced

Comment on lines +128 to +133
i = (size_t)snprintf(out, n, "%s/", mgs_data_root);
for (; *p && i + 1 < n; p++) {
if (*p == ';') break;
out[i++] = (*p == '\\') ? '/' : *p;
}
out[i] = 0;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 150cd9d: when the data root alone does not fit, translate() terminates the buffer and returns instead of indexing with snprintf's wanted length.

…path buffer (review)

snprintf returns the length it wanted on truncation; translate() used it
as an index into the output buffer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AWhoGwibqcKDhcG17mjVT1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants