Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .github/workflows/static_analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ jobs:
use_cmake: false

# (Optional) cppcheck args
cppcheck_args: -i$GITHUB_WORKSPACE/components/gbc/gnuboy -i$GITHUB_WORKSPACE/components/nes/nofrendo -i$GITHUB_WORKSPACE/components/msx/fmsx -i$GITHUB_WORKSPACE/components/doom/prboom -i$GITHUB_WORKSPACE/components/sms/smsplus -i$GITHUB_WORKSPACE/components/genesis/gwenesis -i$GITHUB_WORKSPACE/components/darkforces/tfe -i$GITHUB_WORKSPACE/components/gui/generated -i$GITHUB_WORKSPACE/components/menu/generated -i$GITHUB_WORKSPACE/components/jpegdec --check-level=exhaustive --force --enable=all --inline-suppr --inconclusive --platform=mips32 --std=c++17 --suppressions-list=$GITHUB_WORKSPACE/suppressions.txt
cppcheck_args: -i$GITHUB_WORKSPACE/components/gbc/gnuboy -i$GITHUB_WORKSPACE/components/nes/nofrendo -i$GITHUB_WORKSPACE/components/msx/fmsx -i$GITHUB_WORKSPACE/components/doom/prboom -i$GITHUB_WORKSPACE/components/sms/smsplus -i$GITHUB_WORKSPACE/components/genesis/gwenesis -i$GITHUB_WORKSPACE/components/darkforces/tfe -i$GITHUB_WORKSPACE/components/gui/generated -i$GITHUB_WORKSPACE/components/menu/generated -i$GITHUB_WORKSPACE/components/jpegdec -i$GITHUB_WORKSPACE/components/mgs/mgs -i$GITHUB_WORKSPACE/components/mgs/psyz -i$GITHUB_WORKSPACE/components/mgs/psyq --check-level=exhaustive --force --enable=all --inline-suppr --inconclusive --platform=mips32 --std=c++17 --suppressions-list=$GITHUB_WORKSPACE/suppressions.txt
11 changes: 11 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ set(DOOM_COMPONENTS "doom")
### DARK FORCES ###
set(DARKFORCES_COMPONENTS "darkforces")

### METAL GEAR SOLID ###
# Native port of the FoxdieTeam decompilation (components/mgs); needs the
# 16MB-PSRAM board (ESP32-S3-BOX-3).
set(MGS_COMPONENTS "mgs")

### SNES ###
# set(SNES_COMPONENTS "snes")

Expand Down Expand Up @@ -78,6 +83,11 @@ if(DARKFORCES_COMPONENTS)
add_compile_definitions(ENABLE_DARKFORCES)
endif()

# if MGS_COMPONENTS is set, add compile definitions for METAL GEAR SOLID
if(MGS_COMPONENTS)
add_compile_definitions(ENABLE_MGS)
endif()

# make the components list for the emulators we want
set(EMULATOR_COMPONENTS
${NES_COMPONENTS}
Expand All @@ -88,6 +98,7 @@ set(EMULATOR_COMPONENTS
${GENESIS_COMPONENTS}
${DOOM_COMPONENTS}
${DARKFORCES_COMPONENTS}
${MGS_COMPONENTS}
)

set(
Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ that removes the frame-rate cap for maximum speed.
| MSX 1 / 2 | [fmsx](https://fms.komkon.org/fMSX/) | — | D-Pad / A / B / Start / Select |
| Doom | [prboom](https://prboom.sourceforge.net/) | — | Full speed with audio **and haptic feedback** |
| Dark Forces | [The Force Engine](https://theforceengine.github.io/) (via [BSzili's Amiga port](https://github.com/BSzili/TheForceEngine/tree/amiga)) | — | Software renderer, iMuse sound effects and OPL3 music; see [Dark Forces setup](#dark-forces-setup) |
| Metal Gear Solid | [mgs_reversing](https://github.com/FoxdieTeam/mgs_reversing) decompilation, native via [psyz](https://github.com/Xeeynamo/sotn-decomp/tree/master/tools/psyz) (after [davidmonterocrespo24's ESP32-S3 port](https://velxio.dev/blog/posts/metal-gear-solid-on-esp32-s3/)) | — | Experimental, BOX-3 only; software-rasterized 3D at ~10 fps, no sound or codec yet; see [Metal Gear Solid setup](#metal-gear-solid-setup) |

> 🧬 **Full-speed Genesis on the S3.** The Genesis core ships with a custom
> **dual-core** modification of gwenesis that spreads the workload across both of
Expand Down Expand Up @@ -252,6 +253,42 @@ darkforces/DARK.GOB, boxart/darkforces.jpg, Star Wars: Dark Forces

Settings (`settings.ini`) and agent / save files are written to the same folder.

### Metal Gear Solid setup

Metal Gear Solid is a native build of the decompiled game (not an emulator) and
needs the files from your own disc image (US disc 1, `SLUS-00594`). Extract
them with the script in `components/mgs/tools/`:

```bash
python components/mgs/tools/extract_disc.py "Metal Gear Solid (USA) (Disc 1).bin" /Volumes/SDCARD
```

The extractor creates an `MGS/` folder under the directory it is given, so
pointing it at the card root writes the game's files to `MGS/` on the card:

```
MGS/STAGE.DIR (71 MB: every stage)
MGS/RADIO.DAT
MGS/FACE.DAT
MGS/VOX.DAT
MGS/DEMO.DAT
MGS/BRF.DAT
MGS/ZMOVIE.STR (optional, full-motion video; not played)
```

Then reference `STAGE.DIR` from `metadata.csv`:

```csv
MGS/STAGE.DIR, boxart/mgs.jpg, Metal Gear Solid
```

The game boots straight into the dock (`s00a`, where the game begins);
`MGS_START_STAGE` in `components/mgs/CMakeLists.txt` picks another stage. It
needs the 16MB-PSRAM board (ESP32-S3-BOX-3). Controls: d-pad, A = circle,
B = cross, X = square, Y = triangle, START, SELECT; SELECT + A/B/X/Y = L1/R1/L2/R2.
Save states are not supported (the port has no memory card yet); see
`components/mgs/VENDOR.md` for what works and what does not.

### metadata.csv format

Each line maps a ROM to its boxart and display name:
Expand Down
119 changes: 119 additions & 0 deletions components/mgs/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Metal Gear Solid for esp-box-emu.
#
# A native build of the FoxdieTeam decompilation (mgs_reversing), using the
# ESP32-S3 port by davidmonterocrespo24: the PlayStation SDK is replaced by psyz
# (a C reimplementation with a software GPU rasterizer and the GTE) plus the
# PSY-Q library decompilation, and the PSX kernel's threads and vblank run on
# FreeRTOS. See VENDOR.md for what is vendored and what was changed.
#
# Only one stage can be linked statically (every file in source/stage/ defines
# its own _StageCharacterEntries[]); port/stage_union.c is the merged table
# the ESP32 port generates for all the stages that can be packed.
# COMPONENT_DIR, not CMAKE_CURRENT_SOURCE_DIR: IDF also evaluates this file in
# its early requirements pass, from the build directory
set(MGS_ROOT ${COMPONENT_DIR}/mgs)
set(PSYZ_ROOT ${COMPONENT_DIR}/psyz)
set(PSYQ_ROOT ${COMPONENT_DIR}/psyq)

# Where the game drops you when it starts (any stage of port/stage_union.c
# works, given the full STAGE.DIR). s00a is the dock, where the game begins;
# "title" is the game's own boot chain, which the upstream port cannot run yet
# (memory card and CD streaming are missing).
set(MGS_START_STAGE "s00a" CACHE STRING "stage Metal Gear Solid boots into")

# ---- the game (overlays and stage tables are curated by the vendoring) ----
file(GLOB_RECURSE SRC_GAME ${MGS_ROOT}/source/*.c)
list(APPEND SRC_GAME ${MGS_ROOT}/port/stage_union.c)

# ---- the port layer: PSX kernel / SDK pieces on FreeRTOS -------------------
set(SRC_PORT
${MGS_ROOT}/port/psyz_port.c
${MGS_ROOT}/port/psyq_compat.c
${MGS_ROOT}/port/soft_render.c
${MGS_ROOT}/port/platform_headless.c
${MGS_ROOT}/port/stage_stubs.c
${MGS_ROOT}/port/esp32_threads.c
${MGS_ROOT}/port/esp32_vblank.c
${MGS_ROOT}/port/virtual_cd.c)

# ---- psyz (minus the pieces MGS supplies itself) ---------------------------
file(GLOB SRC_PSYZ ${PSYZ_ROOT}/src/psyz/*.c)

# ---- the PSY-Q decompilation subset (psyq/SOURCES.txt) ----------------------
file(STRINGS ${PSYQ_ROOT}/SOURCES.txt PSYQ_FILES)
set(SRC_PSYQ "")
foreach(f ${PSYQ_FILES})
list(APPEND SRC_PSYQ ${PSYQ_ROOT}/${f})
endforeach()

# ---- esp-box-emu glue -------------------------------------------------------
set(SRC_GLUE
src/mgs.cpp
src/platform/mgs_input.c
src/platform/mgs_present.c)

idf_component_register(
INCLUDE_DIRS "include"
SRCS ${SRC_GLUE} ${SRC_GAME} ${SRC_PORT} ${SRC_PSYZ} ${SRC_PSYQ}
PRIV_INCLUDE_DIRS
"src/platform"
"${MGS_ROOT}/source" # MUST precede psyz: both have a common.h
"${MGS_ROOT}/source/include"
"shim"
"${PSYZ_ROOT}/include"
"${PSYZ_ROOT}/src"
"${PSYQ_ROOT}/src" # psyz's libspu/libspu_private.h
"${MGS_ROOT}/port/include"
REQUIRES box-emu statistics pool_allocator esp_timer heap esp_system
LDFRAGMENTS "linker.lf"
)

# the game's build configuration: C only (INTEGRAL clashes with a template
# parameter in libfmt, which the C++ glue uses)
target_compile_definitions(${COMPONENT_LIB} PRIVATE
$<$<COMPILE_LANGUAGE:C>:INTEGRAL>
$<$<COMPILE_LANGUAGE:C>:__psyz>
$<$<COMPILE_LANGUAGE:C>:VERSION_PC>
$<$<COMPILE_LANGUAGE:C>:NON_MATCHING>
$<$<COMPILE_LANGUAGE:C>:PERMUTER>
$<$<COMPILE_LANGUAGE:C>:HARD_LINK>
$<$<COMPILE_LANGUAGE:C>:_internal_version_us>
MGS_ESPBOX=1
MGS_START_STAGE="${MGS_START_STAGE}")

# 1998 PSY-Q C: gnu17 (GCC 14's gnu23 default rejects it), int/long/pointer
# mixing that was harmless on MIPS, and type punning everywhere (strict aliasing
# removed the stores of CompMatrix and detached every limb from every model).
# -O2: the rasterizer and the GTE are the frame budget.
target_compile_options(${COMPONENT_LIB} PRIVATE
-O2
-w
-Wno-error
-fno-strict-aliasing
$<$<COMPILE_LANGUAGE:C>:-std=gnu17>
$<$<COMPILE_LANGUAGE:C>:-Wno-incompatible-pointer-types>
$<$<COMPILE_LANGUAGE:C>:-Wno-int-conversion>
$<$<COMPILE_LANGUAGE:C>:-Wno-implicit-function-declaration>
$<$<COMPILE_LANGUAGE:C>:-Wno-builtin-declaration-mismatch>
$<$<COMPILE_LANGUAGE:C>:-Wno-return-mismatch>
# SHELL: keeps CMake from merging the repeated -include flags
"$<$<COMPILE_LANGUAGE:C>:SHELL:-include string.h>"
"$<$<COMPILE_LANGUAGE:C>:SHELL:-include gtemac.h>"
# MGS_WHERE breadcrumbs (the printf redirection inside is for another board)
"$<$<COMPILE_LANGUAGE:C>:SHELL:-include ${MGS_ROOT}/port/mgs_printf.h>")

# Global names the game shares with other emulator cores in this firmware
# (gnuboy's sound_off, fMSX's ClearImage); rename them throughout libmgs.a.
target_compile_definitions(${COMPONENT_LIB} PRIVATE
$<$<COMPILE_LANGUAGE:C>:sound_off=mgs_sound_off>
$<$<COMPILE_LANGUAGE:C>:ClearImage=mgs_ClearImage>)

# The PSY-Q decompilation's <common.h> is psyz's (INCLUDE_ASM compiles to
# nothing there); the game's own common.h shadows it on the include path, so
# force psyz's in first for these files.
set_source_files_properties(${SRC_PSYQ} PROPERTIES
COMPILE_OPTIONS "-include;${PSYZ_ROOT}/include/common.h")

# the game's main() is started from the emulator (src/mgs.cpp)
set_source_files_properties(${MGS_ROOT}/source/main/main.c PROPERTIES
COMPILE_DEFINITIONS "main=mgs_main")
80 changes: 80 additions & 0 deletions components/mgs/VENDOR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Vendored sources

Metal Gear Solid here is not emulated: it is the FoxdieTeam decompilation of
the PlayStation game compiled natively for the ESP32-S3, following
davidmonterocrespo24's port (see
https://velxio.dev/blog/posts/metal-gear-solid-on-esp32-s3/). Three trees are
vendored; the emulator-side glue lives in `src/` and `include/`.

## `mgs/` — the game and the ESP32 port layer

- Upstream: https://github.com/FoxdieTeam/mgs_reversing (no license file; the
decompiled game code is Konami's)
- Branch used: https://github.com/davidmonterocrespo24/mgs_reversing/tree/esp32-port
- Commit: `5a2c7ee2be477bdde465654b5bf52de1275e1096`

`mgs/source/` is the subset the ESP32 port links (`esp32/main/CMakeLists.txt`
upstream): everything except `contrib/`, `stagevr/`, `snake_vr/`, the per-stage
`stage/*.c` tables (replaced by `port/stage_union.c`, the merged table for the
stages that are 100% C) and the overlays other than `s07a s11e s11i d18ar s08br
s19br`. `mgs/port/` is the port layer (software GPU, FreeRTOS threads and
vblank, virtual CD over files, headless platform), `tools/` the disc extraction
scripts, `mgs/ESP32_README.md` and `mgs/PORT_STATUS.md` the upstream notes on
what works.

## `psyz/` — the PlayStation SDK reimplementation

- Upstream: https://github.com/Xeeynamo/sotn-decomp/tree/master/tools/psyz (MPL-2.0)
- Fork used: https://github.com/davidmonterocrespo24/psyz/tree/esp32
- Commit: `97b064c28a28d79d80655e120b336e70097328ae`

`psyz/include` (the SDK headers; unlicensed upstream), `psyz/src/psyz/*.c`
(minus `libgte_hw_sqrt.c` and `libsn.c`, which MGS supplies itself; MPL-2.0),
`psyz/src/{internal,draw}.h` and the software rasterizer
`psyz/src/platform/soft_raster.inc.c` (MPL-2.0, `psyz/src/platform/LICENSE`).

## `psyq/` — the PSY-Q library decompilation

- Upstream: the `decomp/` tree of the same psyz repository (MIT, `psyq/LICENSE`)
- Commit: `97b064c28a28d79d80655e120b336e70097328ae`

Only the translation units listed in `psyq/SOURCES.txt` (the set the SOTN ESP32
port proved to compile plus the libspu units MGS needs; `libpress/dec.c` is not
in the upstream tree) and the private headers next to them.

## Changes made for esp-box-emu

All guarded by `MGS_ESPBOX` where they sit in vendored code:

- `shim/`: the PSY-Q headers the port expected from a private directory
(`libpad.h` constants, `mgrex.h`, `game.h`).
- The three big static buffers are pointers filled from the emulator's 4MB ROM
block: `mgs_main_ram` (`libgv/libgv.h`, `libgv/resident.c`, `game/strctrl.c`,
`sound/sd_main.c`, `port/psyz_port.c`), `mgs_face_group` (`port/psyz_port.c`,
`menu/radiomes.c`) and `g_RawVram` (`port/soft_render.c`, `libdg/display.c`,
`port/esp32_vblank.c`, `psyz/src/platform/soft_raster.inc.c`).
`GV_ResidentAreaBottom` is computed at init instead of being a static
initializer of the array's address.
- `port/esp32_threads.c`: per-slot stack sizes from the measured high-water
marks; `Mgs_ThreadsPause/Resume/StopAll()`.
- `port/esp32_vblank.c`: `Mgs_PauseVblank/ResumeVblank/StopVblank()`.
- `port/virtual_cd.c`: files are read from the data root directly;
`Mgs_CdDeinit()`.
- `port/platform_headless.c`: `Mgs_GetDataRoot()`.
- `port/psyz_port.c`: `psyz_CompMatrix()` (the name `port/include/gtemac.h`
routes `gte_CompMatrix` to).
- `source/libsio/dummy.c`: the mts idle task's console poll yields a tick
(`Mgs_IdleYield()`), so the emulator's own tasks on core 0 keep running.
- `psyz/src/psyz/{libcd,libsnd,libspu,psyz_spu}.c`: `libspu_private.h` is found
through the include path instead of a `../../decomp/` relative path.
- `linker.lf`: every static of `libmgs.a` goes to PSRAM (.bss) or is bracketed
by `_mgs_{bss,common,data}_start/_end`, which `src/mgs.cpp` uses to reset the
game's state between launches (the PSX booted from scratch each time).
- `sound_off` and `ClearImage` are renamed with `-D` to avoid the other cores'
globals.

## Status

What the upstream port reports (`mgs/PORT_STATUS.md`): boots into any packed
stage, Snake and the guards work, 3D with correct skeletons; no audio, no codec
calls, no cutscenes/streaming, no memory card, 8-15 fps.
30 changes: 30 additions & 0 deletions components/mgs/include/mgs.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#pragma once

#include <cstdint>
#include <span>
#include <string>
#include <string_view>

// Metal Gear Solid (The FoxdieTeam decompilation, built natively through psyz)
// for esp-box-emu. The game runs in its own FreeRTOS tasks: run_mgs_rom() only
// feeds it the gamepad and paces the emulator loop.
//
// `rom_filename` is the path of STAGE.DIR; every other game file (RADIO.DAT,
// FACE.DAT, ...) is expected in the same directory.
void init_mgs(const std::string& rom_filename, uint8_t* romdata, size_t rom_data_size);
void reset_mgs();
void run_mgs_rom();
void deinit_mgs();

void pause_mgs_tasks();
void resume_mgs_tasks();

// true once the game has ended (or failed to start); the cart then returns to
// the emulator menu
bool mgs_quit_requested();

// Save states are not supported (the port has no memory card yet).
void load_mgs(std::string_view save_path, int save_slot);
void save_mgs(std::string_view save_path, int save_slot);

std::span<uint8_t> get_mgs_video_buffer();
24 changes: 24 additions & 0 deletions components/mgs/linker.lf
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Metal Gear Solid: the game's statics.
#
# Everything the decompiled game, psyz and the PSY-Q libraries keep in .bss
# lives in PSRAM (requires CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY). The
# SURROUND symbols (_mgs_bss_start/_end, _mgs_common_start/_end,
# _mgs_data_start/_end) let the emulator reset the game's static state between
# launches: the PSX booted from scratch every time, the decompiled code assumes
# zeroed .bss and fresh .data, and this core must be fully stoppable.
[scheme:mgs_statics]
entries:
bss -> extern_ram
common -> extern_ram
data -> dram0_data

[mapping:mgs]
archive: libmgs.a
entries:
* (mgs_statics);
bss -> extern_ram SURROUND(mgs_bss),
common -> extern_ram SURROUND(mgs_common),
data -> dram0_data SURROUND(mgs_data)
# the emulator-side glue owns the reset itself (the .data snapshot, the
# lifecycle flags) and must survive it: keep it in the default sections
mgs (default)
Loading
Loading