Files
psxrecomp/docs/GAME_PROJECT_SETUP.md
T
Shokunin ff570512ed docs: ENHANCEMENTS G1.11/G1.12, PGXP as a title's default, with R4 numbers
G1.11: the hook-flavor runtime, the mod armed at session start, dataflow
only, exact projection, and the rectangle shortcut. It covers what tri_mixed
can and cannot see, and the R4 census after every change:
- 1P: 99.91% dataflow, 0 of 389k drawn triangles mixed;
- menus, attract and 2P VS: every truncation reject is a vertex at the
  saturation limit.
The wobble table drops the preserve-projection row, which is 0 by
construction.

G1.12: precise culling (the culled-row crack, the rule, why it is mod-only,
packet headroom, identity, cost under R4's default stack) and
[video] pgxp_mod_only.

TCP_COMMANDS: the new geom_correction fields and the pgxp verb's culling
knob. MOD_PACKAGES: the session arming takes the request; how a title
ships PGXP on. GAME_PROJECT_SETUP: pointer to pgxp_mod_only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 06:08:55 -07:00

639 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Bringing up a game project on PSXRecomp
This guide is for **title developers**: you keep game code and config in your
own repository, pin this framework and `recomp-ui` as **root-level submodules**,
commit the recompiled game C under `generated/`, and ship a **bundled** zip:
the compiled game, built by CI from that committed C. Players bring their own
disc and play; nothing is generated or built on their machine
(`docs/ci/BUNDLED_RELEASES.md`).
Related docs:
| Doc | When |
| ---------------------------------------------- | ------------------------------------------------------------------------ |
| `[BUILDING.md](BUILDING.md)` | Local build of the framework / BIOS runtime |
| `[LOCAL_CODEGEN_SDK.md](LOCAL_CODEGEN_SDK.md)` | CLI contract for Generate & rebuild |
| `[NETPLAY.md](NETPLAY.md)` | **Read more about netplay** (rollback, SFU/ICE, dual-raster, disc gates) |
| `[ci/README.md](ci/README.md)` | Shared CI scripts and composite actions |
| `[CONTRIBUTING.md](../CONTRIBUTING.md)` | Framework contribution rules |
---
## New Project Layout (preview)
The preferred title-repo shape is **root-level** `psxrecomp/` + `recomp-ui/`
submodules. Scaffolding scripts create that layout, pin submodule SHAs, fill
CI + packager stubs, and (with `--disc`) autofill identity, TOC fingerprint,
and a first-pass seed list from the boot EXE.
| Platform | Script |
| ----------------------- | --------------------------------------------------------------------------------------------- |
| Linux / macOS | `[tools/new_project_layout/setup_project.sh](../tools/new_project_layout/setup_project.sh)` |
| Windows | `[tools/new_project_layout/setup_project.ps1](../tools/new_project_layout/setup_project.ps1)` |
| Disc probe (standalone) | `[tools/new_project_layout/probe_disc.py](../tools/new_project_layout/probe_disc.py)` |
| Boxart fetch | `[tools/new_project_layout/fetch_boxart.py](../tools/new_project_layout/fetch_boxart.py)` |
| Token fill helper | `[tools/new_project_layout/fill_tokens.py](../tools/new_project_layout/fill_tokens.py)` |
### Scaffold flow (paths on CLI, rest prompted)
**Required path flag** (use your shell’s tab-completion): `--disc` / `-Disc`
pointing at a legal Redump `.cue`. Missing `--disc` exits with a nag — no
prompt for that path.
```bash
# Interactive (TTY): prompts for name, players, zip prefix, recomp-ui, …
sh tools/new_project_layout/setup_project.sh \
--disc /path/to/legal/game.cue \
--dir ~/src \
--bios /path/to/SCPH1001.BIN # optional; only used if you answer Y to Generate
```
```powershell
powershell -File tools\new_project_layout\setup_project.ps1 `
-Disc C:\dumps\game.cue -Dir C:\src
```
Non-interactive / CI (`--yes` / `-Yes` or `PSXRECOMP_SETUP_YES=1`): requires
`--name` + `--disc`; Y/N options default **off** unless you pass enable flags.
| Path / CLI | Role |
| ------------------ | ------------------------------------------------------------- |
| `--disc` / `-Disc` | **Required.** Stage cue+bins, probe identity / seeds / TOC fp |
| `--dir` / `-Dir` | Parent directory for the new repo (default `.`) |
| `--bios` / `-Bios` | Optional `SCPH1001.BIN` when Generate runs |
| `--boot-exe` | Optional until probe overwrites from `SYSTEM.CNF` |
| Prompted (or flag override) | Notes |
| --------------------------- | ----- |
| Project name | Required; `--name` / `-Name` skips prompt |
| Players | Default **2**, max **8** |
| Zip / CI prefix | Default acronym from name (e.g. MotK → `motk`) |
| Description / publisher / year / region | Optional marketing → `catalog_identity.json` + README |
| Include **recomp-ui**? | **N** → no `recomp-ui` submodule, `PSX_RECOMP_UI=OFF`, skip wizard/netplay |
| Setup wizard? / Netplay? | Only asked if recomp-ui = Y; **netplay auto-off (no prompts) when players = 1** |
| Netplay lobby URL? | Only if netplay = Y (players ≥ 2); default host `netplay.retcomm.net` → `ws://…:8765` |
| GitHub Actions release workflow? | Y → `.github/workflows/release.yml` (logs submodule SHAs via `record_pins.sh`) |
| Fetch libretro boxart? | Needs network |
| Run Generate now? | Emitters + OpenBIOS + game C; `generated/` is **committed** (release CI builds from it) |
| Configure & build after Generate? | Only if Generate = Y |
| Create GitHub repo (`gh`)? | Opt-in; needs `gh` auth |
Flag overrides (non-interactive): `--enable-recomp-ui` / `--no-recomp-ui`,
`--enable-wizard` / `--no-wizard`, `--enable-netplay` / `--no-netplay`,
`--lobby-url`, `--description` / `--publisher` / `--year` / `--region`,
`--enable-ci` / `--no-ci`, `--fetch-boxart` / `--no-fetch-boxart`,
`--generate` / `--no-generate`, `--enable-build` / `--no-build`,
`--create-github` / `--no-github` / `--github-visibility`, … (PowerShell:
`-EnableRecompUi`, `-LobbyUrl`, `-EnableBuild`, `-CreateGithub`, …).
What runs after answers:
1. Create repo stubs (`CMakeLists.txt`, `game.toml`, codegen, rich `.gitignore`,
`symbols.toml`, `README`, empty `mods/preloaded/packages/`, …) + copy
`tools/sync_symbols.py`
2. Submodule `psxrecomp` (+ `recomp-ui` only if accepted); detach at fetched
SHAs; optional snapshot → `framework_pins.txt`
3. Packager stub; optional CI workflow (submodule gitlinks are the pin;
`record_pins.sh` logs SHAs)
4. Stage disc → `probe_disc.py` (marketing + seeds) → `psx_symbols.h` →
optional boxart → initial commit → optional `gh repo create` (no push) →
optional Generate → optional cmake/ninja build → single `git push`. If
Actions has not indexed `release.yml` (common after a private first push),
the scaffold force-publishes when visibility is public and nudges with a
tiny follow-up commit that retouches the workflow file.
Standalone helpers: `probe_disc.py`, `fetch_boxart.py`, `fill_tokens.py`,
`sync_symbols.py`.
### Disc autofill (`probe_disc.py`)
With a Redump-style `**.cue**`:
| Output | Contents |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `game.toml` | Serial / boot EXE / EXE header; `[prepare_disc]` Track 01 digests; `[netplay] require_cue`, `required_tracks`, `required_disc_fp` (`psxrecomp-toc-v1`) |
| `seeds/ghidra_funcs.txt` | Entry PC + direct **JAL targets** inside the boot EXE text (first-pass; MotK-class ~850 addrs) |
| `catalog_identity.json` | Digests + `track_counts` + `disc_fp` + optional `marketing` (description / publisher / year / region / players) |
| `disc_probe.json` | Full probe dump |
| `.recomp.json` | Community metadata ([recomp.fyi spec](https://recomp.fyi/spec)): game, system, type, toolchain, `status: exploring`, and `original` region + serial. Written with or without `--disc`; region comes from the serial prefix when known. `write_recomp_json.py` only fills missing fields, so re-running it never overwrites your edits |
| `symbols.toml` + `psx_symbols.h` | Boot entry stub + `PSX_FN_*` header (`tools/sync_symbols.py`; see [`SYMBOLS.md`](SYMBOLS.md)) |
Re-run without scaffolding:
```bash
python3 tools/new_project_layout/probe_disc.py disc/game.cue \
--write-game-toml game.toml \
--write-catalog catalog_identity.json \
--write-seeds seeds/ghidra_funcs.txt \
--disc-rel disc/game.cue --out-dir disc --players 2
```
Prefer a **full multi-track** Redump cue. A single-TRACK dump will warn and will
fail online multi-track gates (see `[NETPLAY.md](NETPLAY.md)`).
### EXE-only autofill (`psxrecomp-toml`)
When you have a PS-X EXE but no cue — or you just want a `game.toml` skeleton
without running the full disc probe — `psxrecomp-toml` (built alongside the
other CLI tools from `recompiler/`, source `recompiler/src/main_toml.cpp`)
reads the EXE header and writes the config for you. It saves opening a hex
editor or Ghidra just to recover the entry point, load address and text size.
What it produces:
- **`game.toml`** — `load_address`, `entry_pc` and `text_size` taken from the
actual PS-X EXE header, plus the game name and ID.
- **Seeds** — the destinations of every direct jump-and-link (`JAL`) it finds
in the code segment, optionally widened with the addresses immediately
following each `jr $ra`.
```
Usage: psxrecomp-toml <PS1-EXE> [options]
Options:
--output <path> Write game.toml to <path> (default: stdout)
--seeds <path> Also write JAL-target seed file to <path>
--name <str> Game name in TOML (default: derived from EXE)
--id <str> Game ID (default: auto-detect or empty)
--stdout Force output to stdout even with --output
--include-after-return
Add addresses after jr $ra to seeds (more coverage,
may include some data addresses)
-h, --help Show this help
```
Example:
```bash
psxrecomp-toml ./isos/SCES_028.34 \
--output ./projects/CrashBash/game.toml \
--seeds ./projects/CrashBash/recompiler/seeds/seeds.txt \
--name "Crash-Bash-EUR"
```
It also prints the EXE, game ID, entry PC, load address, text size, stack base
and seed count to stderr, so you can sanity-check the header it read.
`--include-after-return` trades precision for coverage: an address just past a
`jr $ra` is usually the next function, but sometimes it is data, so those seeds
can introduce junk entries.
**This is a first pass, not full discovery.** Static `JAL` scanning cannot
resolve dynamic function tables or indirect register dispatches. When the
recompiler later reports discovery gaps or unknown dispatches at runtime, those
newly-found addresses still have to be fed back into the seeds file. Prefer
`probe_disc.py` above when you have a cue — it does the same header work plus
disc identity, digests and netplay gates.
### Project Studio — migrate / update existing titles
Older titles (e.g. `psxrecomp-v4` submodule, `psxrecomp_add_runtime_target`,
`packaging/` prebuilt zips) can be audited and migrated onto this layout with
**Project Studio**. Releases are **bundled**: the tool un-ignores and expects
committed `generated/`, and emits the packager + CI that ship the compiled game.
| Entry | Role |
| ----- | ---- |
| `tools/new_project_layout/migrate_project.py` | CLI: `audit` / `plan` / `apply` / `gui` / `ops` |
| `tools/new_project_layout/project_studio_gui.py` | Tkinter GUI |
| `tools/new_project_layout/project_studio/` | Shared detect / plan / ops library |
| `tools/new_project_layout/README.md` | Short usage |
```bash
# From a psxrecomp checkout (or any title that vendors this toolkit path):
python3 tools/new_project_layout/migrate_project.py audit --root ~/src/ApeEscapeRecomp
python3 tools/new_project_layout/migrate_project.py apply --root ~/src/ApeEscapeRecomp --dry-run
python3 tools/new_project_layout/migrate_project.py gui
```
Typical apply order: rename `psxrecomp-v4` → emit `codegen_setup` → rewrite
CMake (`psxrecomp_add_game_runtime` + wizard) → bundled packager + CI →
optional `probe_disc` / pins. CMake rewrites keep
`CMakeLists.txt.pre_migrate.bak` plus `CMakeLists.migrate_extras.txt` when the
old file had tests / `EXTRAS_SOURCES` / mod `POST_BUILD` hooks.
`apply` always enables **recomp-ui + setup wizard** (the shipped game's
first-run disc picker). Netplay stays opt-in (`--enable-netplay` / GUI).
### After scaffold (still not automatic)
If you answered Y to boxart + Generate (+ optional build), you get a local
playable tree (OpenBIOS / optional retail BIOS C). You still must:
1. **Boot / soak** — fix missing seeds, overlays, FMV/runtime quirks in `game.toml`
2. **Netplay QA** — LAN then lobby; confirm digests + TOC fp; pin `VERSION`
3. **Polish** — more symbols in `symbols.toml`, boxart name mismatches
4. **Ship** — commit `generated/` (the scaffold does this after a successful
Generate), push, enable Actions, tag `vX.Y.Z`. CI builds the committed C and
attaches the compiled game for every platform.
**Legacy layout** (CLI `psxrecomp build`, nested UI under older trees,
`tools/setup_dev.sh` for framework-only boots) remains supported. Prefer the
New Project Layout for new titles until the project fully adopts it.
---
## Opt-in launcher features (build flags)
Both surfaces are **OFF by default**. The New Project Layout can enable them
when you accept **recomp-ui** and answer Y to wizard/netplay (or pass
`--enable-wizard` / `--enable-netplay`). Declining recomp-ui sets
`PSX_RECOMP_UI=OFF` and skips those prompts. You can also set the flags in
`CMakeLists.txt` before `include(runtime.cmake)`, or pass `-D…=ON`. Do not
enable them until tested — other recomp-ui consumers stay dark when `GameInfo`
is zero-init.
| CMake option | Default | Effect |
| ------------------ | ------- | --------------------------------------------------------------------------------------------- |
| `PSX_NETPLAY` | OFF | Link `recomp-net` + lobby client; advertise **full netplay UI** when the title has 2+ players. Also defaults `RNET_ENABLE_ICE=ON` (libjuice via URL FetchContent, or `third_party/libjuice` / `RNET_LIBJUICE_ROOT`) unless `-DRNET_ENABLE_ICE=OFF`. |
| `PSX_SETUP_WIZARD` | OFF | Advertise **first-run setup wizard + Generate & rebuild** (`GameInfo.setup_wizard_supported`) |
`psxrecomp_add_game_runtime` helpers:
- `ENABLE_NETPLAY_IF_PRESENT` — *supplemental* only; still set `PSX_NETPLAY ON`
**before** `include(runtime.cmake)` when `lib/recomp-net` exists (otherwise
`option(PSX_NETPLAY)` stays OFF and `psx_netplay_sched.c` cannot find
`recomp_net/session.h`)
- `NETPLAY_LOBBY_URL "ws://host:port"` — compile-time default lobby URL
(`PSX_NET_LOBBY_DEFAULT_URL`; env `PSX_NET_LOBBY_URL` still wins at runtime)
- `ENABLE_SETUP_WIZARD` — force `PSX_SETUP_WIZARD=ON` for that target
- `PGXP` — build the title's one runtime as the PGXP hook flavor
(`-DPSX_PGXP=1`, overlay cache flavor `_f2`, no `_pgxp` suffix). The generated
C already carries the hook sites, so `generated/` does not change. PGXP stays
off at runtime until `psx.enhancement.pgxp` or the `[video]` keys arm it; with
it off the guest runs exactly as in the base flavor. Savestates of the two
flavors are not interchangeable. A title that ships PGXP on by default does so
through a default-on override of `psx.enhancement.pgxp` plus
`[video] pgxp_mod_only = true`. See `ENHANCEMENTS.md` G1.11/G1.12.
Minimal opt-in example:
```cmake
set(PSXRECOMP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/psxrecomp")
if(EXISTS "${PSXRECOMP_ROOT}/lib/recomp-net/CMakeLists.txt")
set(PSX_NETPLAY ON CACHE BOOL "…" FORCE)
endif()
set(PSX_SETUP_WIZARD ON CACHE BOOL "…" FORCE)
include("${PSXRECOMP_ROOT}/runtime/runtime.cmake")
psxrecomp_add_game_runtime(psx-runtime
ENABLE_NETPLAY_IF_PRESENT
NETPLAY_LOBBY_URL "ws://netplay.retcomm.net:8765"
ENABLE_SETUP_WIZARD
…
)
```
Without `PSX_SETUP_WIZARD`, recomp-ui never opens the first-run modal and never
shows Generate & rebuild — even if prepare callbacks are present. Without
`PSX_NETPLAY`, the NETPLAY button stays hidden.
**Read more about netplay:** `[NETPLAY.md](NETPLAY.md)` — rollback, hybrid
OpenGL+1× SW authority, ICE/TURN, SFU online vs LAN star, multitap, and
multi-track `.cue`/`.bin` requirements.
---
## Recommended repository layout
Everything game-specific lives at the **root** of *your* title repo. Framework
and UI are submodules next to that code — not nested under each other.
The Retro / Generate & rebuild CLI (`psxrecomp_cli.py`, `tools/prepare_disc.py`,
pack helpers) ships **inside** the `psxrecomp` submodule. There is no separate
`psxrecomp-sdk/` tree.
```text
YourGameRecomp/ # your git repo
├── .gitmodules
├── CMakeLists.txt # thin: psxrecomp_add_game_runtime(...)
├── game.toml # probe autofills identity / netplay gates
├── catalog_identity.json # Retro / catalog digests + track_counts + disc_fp
├── .recomp.json # community metadata (recomp.fyi); keep `status` current
├── framework_pins.txt # optional scaffold snapshot (gitlinks are authoritative)
├── README.md # scaffold stub (legal + quick start)
├── VERSION # release pin (e.g. 0.1.0)
├── assets/
│ ├── psxrecomp.ico # default Windows app icon (Retro-themed pad)
│ ├── psxrecomp.png # staged beside exe / packaging
│ └── psxrecomp.svg # source mark (see psxrecomp/assets/)
├── seeds/ghidra_funcs.txt # probe: boot-EXE JAL seeds (grow over time)
├── codegen_setup.c / .h # title PsxrecompCodegenHostConfig (+ apply hooks)
├── launcher_assets/img/
│ ├── boxart.tga # optional: --fetch-boxart (libretro Named_Boxarts)
│ └── BOXART_SOURCE.txt # attribution URL
├── mods/preloaded/ # scaffold: empty catalog for shipped .psxmod packages
│ ├── README.md # staged to <exe>/mods/README.md
│ └── packages/ # packages/<id>/<version>/manifest.toml …
│ # declared to the framework as
│ # PRELOADED_MODS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/mods/preloaded"
│ # which stages it into <exe>/mods/bundled beside
│ # the framework's builtins. NEVER copy it with
│ # your own POST_BUILD command — see
│ # docs/MOD_PACKAGES.md ("How bundled/ gets staged").
├── scripts/
│ └── package_release.sh # scaffold fills from package_release.sh.in
├── .github/workflows/
│ └── release.yml # scaffold fills from docs/ci/templates/game-release.yml
├── psxrecomp/ # submodule → framework + CLI + codegen host + CI tools
│ ├── host/psxrecomp_codegen_host.*
│ └── lib/recomp-net/ # nested submodule (netplay)
├── recomp-ui/ # submodule → launcher UI
├── generated/ # COMMITTED recompiled game C — release CI builds it
└── disc/ # local disc working tree — gitignore
```
Minimal `CMakeLists.txt` shape. The wizard is the shipped game's first-run disc
picker; `GEN_MARKER` / `GEN_FULL_GLOB` name the committed `generated/` C, which
release CI requires (`-DPSXRECOMP_REQUIRE_GAME_C=ON`).
```cmake
set(PSXRECOMP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/psxrecomp")
# set(PSX_NETPLAY ON CACHE BOOL "…" FORCE) # multiplayer titles only
set(PSX_SETUP_WIZARD ON CACHE BOOL
"Advertise first-run setup wizard + Generate & rebuild in recomp-ui" FORCE)
include("${PSXRECOMP_ROOT}/runtime/runtime.cmake")
psxrecomp_add_game_runtime(psx-runtime
# ENABLE_NETPLAY_IF_PRESENT
ENABLE_SETUP_WIZARD
WINDOW_TITLE "My Game Recompiled"
GEN_MARKER "generated/SLUS_01234_dispatch.c"
GEN_FULL_GLOB "generated/SLUS_01234_full_*.c"
CODEGEN_SETUP_SOURCES codegen_setup.c
DEFAULT_GAME_CONFIG_PATH "game.toml"
LAUNCHER_BOXART "${CMAKE_CURRENT_SOURCE_DIR}/launcher_assets/img/boxart.tga"
APP_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/psxrecomp.ico"
)
```
Or use the scaffolding scripts under **New Project Layout (preview)** above.
### Add the submodules
```bash
cd YourGameRecomp
git submodule add -b master https://github.com/mstan/psxrecomp.git psxrecomp
git submodule add -b master https://github.com/RetroPortingToolKit/recomp-ui.git recomp-ui
git submodule update --init --recursive
```
The New Project Layout scaffold **pins** via detached submodule gitlinks
(optional `framework_pins.txt` snapshot for humans/logs). Do not float on
`main`/`master` in release CI — bump submodules deliberately when you want a
newer framework or UI.
Clone for contributors:
```bash
git clone --recurse-submodules https://github.com/you/YourGameRecomp.git
```
---
## Local development loop
After scaffold (or a manual clone):
1. **Legal disc** — under `disc/` or the path in `game.toml` (never commit dumps
or retail BIOS).
2. **Seeds** — scaffold/`probe_disc.py` writes a first-pass `seeds/ghidra_funcs.txt`
from the boot EXE. Grow it as overlays / runtime paths miss.
3. **Build emitters** (once per machine / after recompiler changes):
```bash
./psxrecomp/tools/ci/build_emitters.sh
```
4. **Generate game (+ OpenBIOS) C** — setup wizard (if enabled) or:
```bash
python3 psxrecomp/psxrecomp_cli.py generate \
--config game.toml --project-root . --disc disc/game.cue
```
5. **Commit the generated C** — it is the release's input:
```bash
git add generated && git commit -m "Regenerate game C"
```
6. **Build the playable runtime**:
```bash
cmake -S . -B build-release -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build-release --target psx-runtime -j"$(nproc)"
```
7. **Soak** — offline boot → LAN netplay → lobby. Tune `[runtime]` quirks only
after you have evidence (see MotK `game.toml` comments for the pattern).
Details: `[LOCAL_CODEGEN_SDK.md](LOCAL_CODEGEN_SDK.md)`, `[BUILDING.md](BUILDING.md)`,
`[NETPLAY.md](NETPLAY.md)`.
---
## Bundled releases (compiled game zip)
CI builds the committed `generated/` C and ships a zip that is the game:
- The game executable (signed on Windows when a certificate is configured)
- `assets/`, `mods/bundled/` (catalog verified against the build's manifest),
`game.toml` / `game_options.toml`
- `bios/openbios.bin` + its MIT notice — the release runs on OpenBIOS
- `overlay_toolchain/` so overlays outside the compiled static shard can be
turned native from the player's disc at runtime
- `licenses/` (third-party notices), `README.txt`
It contains **no** sources, emitters at the root, CLI, `psxrecomp/` or
`recomp-ui/` tree, generated C, disc, or BIOS dump. Players (or
[Retro](https://github.com/RetroPortingToolKit/Retro-Launcher)) extract it,
pick their legal disc on first run, and play; an update is a raw zip extract
over the previous one. Details and the exact CI steps:
[`ci/BUNDLED_RELEASES.md`](ci/BUNDLED_RELEASES.md). CI tip: leave the
`psxrecomp` gitlink pinned and use `reuse_cached_emitters` so release jobs skip
rebuilding emitters when only host sources moved.
Do **not** set `PSX_PGO` in CI. PGO stays user-local when `[pgo] enabled = true`.
---
## Shared tools (use these; do not reimplement)
| Tool | Role |
| ------------------------------- | -------------------------------------------------------- |
| `tools/ci/normalize_version.sh` | `vX.Y.Z` → `VERSION` / `TAG` |
| `tools/ci/check_generated.sh` | Committed game C + framework BIOS backends present |
| `tools/ci/record_pins.sh` | Log submodule SHAs |
| `tools/ci/build_emitters.sh` | Build `psxrecomp-game` + `psxrecomp-bios` |
| `tools/fetch_toolchain.sh` | Windows emitter build on the CI machine (never shipped) |
| `tools/bundle_mingw_dlls.sh` | Windows runtime DLL copy |
| `tools/package_game_release.sh` | Bundled zip: compiled game + runtime data (title args) |
| `tools/release_stage.py` | Mod catalog / overlay toolchain / overlay cache staging |
Composite actions (from the game repo after checkout):
```yaml
- uses: ./psxrecomp/.github/actions/build-emitters
# Optional — only when packaging with --embed-toolchain:
# - uses: ./psxrecomp/.github/actions/fetch-toolchain
# with:
# artifact: ${{ matrix.artifact }}
- uses: ./psxrecomp/.github/actions/stage-setup-sdk
with:
stage: dist/stage-setup-${{ matrix.artifact }}
recompiler-build: build-recompiler
allow-no-toolchain: 'true'
```
---
## CI workflow template
For an existing project, `tools/generate_ci` writes the filled workflow and
nothing else (no commit, no push, no migration plan):
```sh
sh psxrecomp/tools/generate_ci.sh # in the project; --check, --force, --dry-run
```
```powershell
powershell -File psxrecomp\tools\generate_ci.ps1
```
It is the single-purpose face of Project Studio's `emit_ci_workflow`: the
template comes from the project's own psxrecomp submodule, the zip prefix from
`scripts/package_release.sh` so CI uploads the zips the packager built,
and an installed workflow is compared by step name -- `--check` exits 1 when
it is missing or stale, and only `--force` overwrites one.
The New Project Layout scaffold **copies and fills** this for you after
submodules land:
- `.github/workflows/release.yml` ← `docs/ci/templates/game-release.yml`
(Linux x64, Windows x64, macOS arm64 + Intel)
- `scripts/package_release.sh` ← zip prefix / exe / display name
`--exe-name` / `codegen_setup.exe_basename` should match CMake `OUTPUT_NAME`
(`MAKE_C_IDENTIFIER(WINDOW_TITLE)`, e.g. `TwistedMetal4_Recompiled`), not the
bare project folder name (`TwistedMetal4Recomp`). Scaffolding derives
`EXE_BASENAME` from `WINDOW_TITLE` for that reason.
It is no longer fatal when they disagree. CMake is the single source of truth:
`runtime.cmake` writes the `OUTPUT_NAME` it chose to
`<build-dir>/psxrecomp_exe_name-<target>.txt`, and both the build CLI and the
in-runtime self-compiler read that in preference to their own copy. A build
tree predating the marker still falls back to `exe_basename`.
This matters because the two values are separate copies of the same window
title, and renaming a game updates one of them. Before the marker existed, that
drift produced `build succeeded but binary missing` on a build with no errors
at all — the executable was sitting in the build directory under the name CMake
had chosen. To pin the name explicitly instead of deriving it, pass `EXE_NAME`
to `psxrecomp_add_game_runtime()`.
Zip prefix defaults to a short acronym from `--name` (e.g. `MastersOfTerasKasiRecomp`
→ `motk`); override with `--zip-prefix` / `-ZipPrefix`.
Manual install (existing repos):
```bash
mkdir -p .github/workflows scripts
cp psxrecomp/docs/ci/templates/game-release.yml .github/workflows/release.yml
# replace YOUR_ZIP_PREFIX / YOUR_GAME_TITLE / yourgame-release
# add scripts/package_release.sh (fill tools/new_project_layout/templates/package_release.sh.in)
```
Or re-run token fill against the submodule template:
```bash
python3 psxrecomp/tools/new_project_layout/fill_tokens.py \
psxrecomp/docs/ci/templates/game-release.yml \
.github/workflows/release.yml \
--ci-placeholders \
--set ZIP_PREFIX=mygame \
--set GAME_TITLE='My Game Recompiled'
```
Full action reference: `[ci/README.md](ci/README.md)`.
---
## Bundled release checklist
Use this before tagging a release.
### Repository
- [ ] `psxrecomp/` and `recomp-ui/` are root-level submodules on pinned commits
- [ ] CLI lives in the submodule (`psxrecomp/psxrecomp_cli.py`) — no sibling sdk
- [ ] `game.toml` has disc identity (`[prepare_disc]` hashes/sizes) and boot EXE
```
(scaffold `--disc` / `probe_disc.py` autofills these)
```
- [ ] Multi-track titles that ship netplay: `[netplay] require_cue` +
```
`required_tracks` + `required_disc_fp` so Track-01-only dumps cannot go
online against full Redump cues
```
- [ ] Optional: commit `catalog_identity.json` (+ `framework_pins.txt` snapshot)
- [ ] `.recomp.json` `status` matches reality (`exploring` → `in-progress` →
`playable` → `released`, or `paused`) — community lists read it daily, and a
stale status is worse than none
- [ ] `seeds/ghidra_funcs.txt` covers the boot path (probe JAL pass + discoveries);
```
`VERSION` matches the release you will tag
```
- [ ] Disc images, BIOS, logs, and runtime dumps are gitignored; `generated/`
is **not** (scaffold writes a rich `.gitignore`; see also
`docs/ci/templates/game.gitignore`)
- [ ] `generated/<boot>_dispatch.c` + `_full_*.c` (+ `overlays_static*.c` when
the AOT profile declares it) are committed and current for the pinned
framework (`tools/ci/check_generated.sh` passes locally)
- [ ] The pinned `psxrecomp` commits its BIOS backends (`psxrecomp/generated/
{OpenBIOS,SCPH1001}_*.c`) with stamps current for its emitter
- [ ] Submodule gitlinks (`psxrecomp` / `recomp-ui` / nested `recomp-net`) are
```
the SHAs you intend to ship (CI builds those; `record_pins.sh` only logs)
```
- [ ] Full build configures from the committed C: CI passes
`-DPSXRECOMP_REQUIRE_GAME_C=ON -DPSX_SETUP_WIZARD=ON` and asserts the
configure log says `linking generated game C (full runtime)` and
`BIOS backends linked: OpenBIOS;SCPH1001`
- [ ] `[runtime] overlay_cache = true` in `game.toml` (the packager refuses
otherwise)
- [ ] Thin `codegen_setup.c` + `psxrecomp_add_game_runtime` (codegen host is in
`psxrecomp/host/`). Must export `psx_game_codegen_forward_if_built` (see
`codegen_setup.c.in`) — `main.cpp` always calls it; in a full build it is a
no-op
### Packaging (shared helpers)
- [ ] CI uses `./psxrecomp/.github/actions/build-emitters` (OpenBIOS emission
and the shipped overlay toolchain need them)
- [ ] `scripts/package_release.sh` calls `psxrecomp/tools/package_game_release.sh`
- [ ] Staged tree has the game exe, `psx_game_version.txt`, `assets/`,
`mods/bundled/`, `game.toml`, `bios/openbios.bin` + `OpenBIOS.LICENSE`,
`overlay_toolchain/`, `licenses/` — and **no** `psxrecomp/`, `recomp-ui/`,
CLI, root-level emitters, sources, generated C, retail `.BIN`
- [ ] Windows zip has the runtime DLLs the exe imports beside it
(`bundle_mingw_dlls.sh`), signed when a certificate is configured
### Zip contents smoke test
- [ ] Linux / macOS / Windows artifacts named consistently
(`YOUR_PREFIX-<ver>-<linux-x64|windows-x64|macos-arm64|macos-x64>.zip`)
- [ ] Extract → run → pick a legal disc → the game boots; saves/settings land
beside the exe
- [ ] Retro install/update (if you publish a catalog entry) uses this **same**
zip as a prebuilt Play binary (raw zip extract; no build step)
### Publish
- [ ] Tag `vX.Y.Z`; GitHub Release attaches all four platform zips
- [ ] Release notes say the zip is the compiled game: bring your own disc,
no BIOS, no build
- [ ] Catalog / Retro entry points at the release assets when ready