Files
psxrecomp/docs/config_schema.md
T
Alex Vanderveen 816fb08fe2 bump + sio
2026-08-17 22:25:49 -04:00

510 lines
22 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.
# PSXRecomp v4 — config TOML schema
Consumed by:
- `psxrecomp-bios.exe` and `psxrecomp-game.exe` (via `--config <toml>`)
- `tools/audit_config.py` and the audit tooling that wraps it
- The runtime cmake macro (`runtime/runtime.cmake`)
Examples:
- `bios/SCPH1001.toml` — BIOS-only config (no game; psx-runtime targets this)
- `../TombaRecomp/game.toml` — game config (tomba-runtime targets this)
## How configs combine
A PSXRecomp v4 process **always** has a BIOS config. It optionally also has
a game config. Both are TOML files in this schema:
- `bios/SCPH1001.toml` (or another BIOS .toml in `bios/`) — describes the
BIOS. Always loaded.
- `<game>/game.toml` — describes a single game. Loaded ONLY when running
that game.
```
psxrecomp # BIOS-only regen (uses default bios.toml)
psxrecomp games/tomba/game.toml # BIOS + game regen
psx-runtime # boots BIOS discless
psx-runtime games/tomba/game.toml # boots BIOS, then loads game
```
How the two configs relate:
- **Scalar keys (`debug_port`, `window_title`, `memcard_dir`, ...)**: these come
from `game.toml` only. There is **no BIOS→game inheritance.** An earlier
version of this document described a shallow override where the game won and
otherwise inherited from `bios.toml`; that merge was never implemented.
`load_bios_config` (`recompiler/src/config_loader.cpp`) is called only from the
recompiler front-ends — `main_bios.cpp`, and `main_psx.cpp` purely to build the
`BiosAddressModel` — and never from `runtime/src/main.cpp`. Setting a
`[runtime]` scalar in a BIOS toml has no effect on a game run.
Runtime precedence is: environment > CLI > `settings.toml` > `game.toml` >
compiled-in default.
- **`[program]` (BIOS) and `[game]` blocks**: NOT merged — they describe
different programs. Both are visible to the loader.
- **Generated dispatch tables and C output**: ADDITIVE. BIOS contributes
`SCPH1001_*.c`; game contributes `<exe>_*.c`. No address overlap is
expected (BIOS lives at RAM 0x500-0x8500 + ROM 0xBFC..., game at
0x80010000+). The cmake macro `psxrecomp_v4_add_runtime_target` already
links both.
- **`[[audit.regions]]` and `[[audit.normalize.remap]]`**: additive — game
adds its regions on top of BIOS's.
## Top-level blocks
```toml
[program] # in bios.toml; describes the BIOS
[game] # in game.toml; describes the game
[prepare_disc] # optional; data-track digests for prepare/verify
[netplay] # optional; TOC / cue policy for online
[recompiler]
[runtime]
[audit]
```
## Netplay disc mount (`[netplay]`)
Optional. Online play needs the same CD geometry on every peer — data-track
CRC/SHA alone cannot distinguish a Track-01-only dump from a full Redump
multi-track cue. The runtime mounts the resolved path, fingerprints the TOC
(`disc_fp`), and gates Host/Join on `netplay_ok`. Peers also exchange
`disc_fp` through the lobby (`disc_mismatch` on join).
| Field | Default | Description |
|---|---|---|
| `require_cue` | `false` | Require a `.cue` mount (reject bare `.bin` / cue→bin fallback) |
| `required_tracks` | `0` | Exact `iso_track_count` when > 0 (e.g. MotK Redump = `17`) |
| `required_leadout_lba` | unset | Exact lead-out LBA when set |
| `required_disc_fp` | `""` | Exact lowercase hex SHA-256 TOC fingerprint when non-empty |
Offline Play may still launch with a TOC warning; first-run setup Finish and
online Create/Join require `netplay_ok` (and online also a clean verify +
non-empty `disc_fp`). Mirror `required_tracks` in the RetComM catalog as
`rom_identity.track_counts` so the hub library scan rejects Track-01-only dumps.
Wizard / RetComM / catalog submission accept Redump `.cue` + sibling `.bin`
tracks only — not `.iso`/`.chd` (cannot reliably expand to multi-track).
## Program / game block
- **bios.toml** has a `[program]` block describing the BIOS ROM.
- **game.toml** has a `[game]` block describing the game EXE / disc.
These are NOT alternatives; they're complementary. A runtime loading both
sees both blocks. The legacy single-file audit loader
(`tools/audit_config.py`) accepts either as the program-info source for
backwards compat, but going forward they are the canonical names for their
respective files.
### Fields
| Field | Required for | Description |
|---|---|---|
| `name` | both | display name, e.g. `"SCPH1001 BIOS"` |
| `id` | both | canonical id, e.g. `"SCPH-1001"` or `"SCUS-94236"` |
| `rom` | bios | path to raw flat binary, relative to project root |
| `exe` | game | path to PS-X EXE file, relative to project root |
| `load_address` | both | hex string, virtual address of first byte (`"0xBFC00000"` BIOS, `"0x80010000"` typical game) |
| `entry_pc` | both | hex string, first PC to execute |
| `text_size` | both | hex string, size in bytes of the static region. For games this also bounds main-EXE analysis and establishes the overlay floor. A smaller-than-header bound must be verified non-code and 4 KiB aligned. |
| `stack_base` | game | hex string, initial `$sp` value for the game |
| `disc` | game (single-disc) | path to .cue, relative to project root |
| `discs` | game (multi-disc) | array of .cue paths; `disc` is sugar for `discs = [disc]` |
## Recompiler block
```toml
[recompiler]
seeds = "recompiler/seeds/phase2_ghidra_seeds.json" # BIOS
seeds = "seeds/ghidra_funcs.txt" # game (note: game seeds aren't json today)
bios_thunks = "seeds/tomba_bios_thunks.txt" # game-only
bios_config = "psxrecomp/bios/SCPH1001.toml" # game-only: BIOS profile whose address
# model game codegen folds RAM aliases
# through (see "BIOS profiles" below);
# defaults to the SCPH1001 profile
out_dir = "generated" # both
strict = true # both — currently always true
discovery = "whole-image" # game-only: "whole-image" or "reachable"
out_stem = "SCPH1001" # optional; overrides the auto-derived stem
[[recompiler.patch]]
id = "descriptive-policy-name"
address = "0x80012340"
expected = "0x24020002"
replacement = "0x24020001"
note = "Why this game-owned instruction change is required" # optional
```
Output filenames: `<out_dir>/<out_stem>_full.c` and
`<out_dir>/<out_stem>_dispatch.c`. If `out_stem` is omitted, it's derived
from the `rom`/`exe` file basename with the trailing `.BIN` or `.EXE`
stripped (`Path.stem` is NOT used because it mishandles `SCUS_942.36`).
Game `discovery` defaults to `"whole-image"`, preserving the existing sweep and
pointer-table heuristics. Opt-in `"reachable"` starts at the executable entry
and evidence-backed seed roots, then follows callable direct `jal` targets. It
does not sweep arbitrary bytes for prologues or return-shaped words. Unresolved
`jalr`/indirect targets and unseen callbacks fail closed to runtime
interpretation; add an evidence-backed seed (or a `dispatch_root` seed for a
proven nonstandard boundary) when they should be compiled.
When `game.text_size` is smaller than the PS-X EXE header size, recompilation
uses it as a static-analysis bound. The value must be nonzero, instruction- and
4 KiB-aligned, retain `entry_pc`, and not extend past PS1 RAM. The original EXE
is still loaded from the user's disc. A generated config's canonical final-page
reservation may be slightly larger than the header; it does not widen analysis.
Each `[[recompiler.patch]]` replaces one exact 32-bit MIPS word before function
discovery, control-flow analysis, and normal translation. It is intended for
small, understood game-code changes whose addresses, opcodes, and policy remain
in the game repository. The framework does not contain title IDs or
title-specific addresses.
- `id`, `address`, `expected`, and `replacement` are required hex/string
fields; `note` is optional.
- IDs are case-sensitive and unique within one config.
- Addresses must be four-byte aligned and are unique by the PSX 29-bit physical
address. Thus `0x00012340`, `0x80012340`, and `0xA0012340` are aliases of one
site and cannot define separate patches.
- Main-EXE generation fails if the word at the target site is not `expected`.
This catches a wrong disc revision or stale patch instead of guessing.
- Captured overlays may place unrelated variants at one virtual address. In
overlay mode, a patch is applied only to a variant whose word is `expected`;
a nonmatching variant is translated unchanged.
- When `--config` and `--ws-config` supply the same byte-identical patch it is
deduplicated. Reusing an ID or physical address for different patch data is
an error.
Patches are build-time inputs, not runtime memory writes or live toggles.
Regenerate the affected main executable or captured overlays after changing
them.
### Guarded widescreen participation comparisons
Games may disable a proven object/model cull verdict in widened world views
without changing true 4:3 behavior:
```toml
[[widescreen.cull.keep]]
address = "0x8002B310"
expected = "0x28A21C01"
result = 1
```
- `expected` must encode `SLT`, `SLTU`, `SLTI`, or `SLTIU`.
- `result` must be 0 or 1.
- The site identity is the normalized physical address plus the complete
32-bit instruction. A nonmatching overlay variant at the same VA is left
unchanged.
- At true 4:3 the original comparison is evaluated. The configured result is
forced only when `psx_ws_x_margin() > 0`.
- Native generated code and the dirty-RAM interpreter implement the same
semantics.
- Regenerate main/overlay native code after changing the list.
Prefer an aspect-derived cone over `keep` when the original predicate is a
camera-frustum test. `keep` has no queue policy and is appropriate only for a
separately proven binary verdict.
### Aspect-aware terrain and model participation
Exact terrain-frustum angle loads can follow the live horizontal field:
```toml
[[widescreen.cull.angle]]
address = "0x8013F138"
expected = "0x24020155"
```
`expected` must be `ADDI`/`ADDIU rt,zero,imm`, with a positive 12-bit angular
half-extent below one quarter-turn. The helper widens `tan(angle)` by the live
per-side horizontal extent. It is exact at 4:3 and full-word guarded against
same-address overlay variants.
A model-list or per-child cosine rejection can use a horizontal-only envelope:
```toml
[widescreen.cull.aspect_cone]
forward_addr = "0x1F8000E8" # signed Q12 X/Z/Y halfwords
object_type_offset = 12
object_reg = 19
x_reg = 16
z_reg = 17
y_reg = 18
hysteresis_pixels = 24
queue_reserve = 4
queue_count_addrs = ["0x1F800144", "0x1F800150", "0x1F80015C"]
queue_capacities = [24, 40, 28]
queue_type_masks = ["0x00000204", "0x00000010", "0x00000020"]
[[widescreen.cull.aspect_cone.sites]]
address = "0x80077368"
expected = "0x28620358" # signed SLTI reject predicate
[[widescreen.cull.aspect_cone.sites]]
address = "0x8002B368"
expected = "0x0082202A" # signed SLT reject predicate
cosine_threshold = 856 # required for SLT; Q10
object_reg = 20 # optional per-site register overrides
x_reg = 19
z_reg = 18
y_reg = 17
queue_guard = false # this lower-level predicate appends to no fixed queue
```
- Sites must be signed `SLTI` or `SLT` reject predicates: zero is the keep
path and one is rejection.
- An `SLTI` site derives its Q10 cosine threshold from the immediate unless
`cosine_threshold` is given. An `SLT` site requires it explicitly.
- A vanilla keep is always preserved. Only a vanilla rejection is retested.
- Horizontal reach follows the current client aspect. Vertical reach,
near/far checks, type dispatch, and the game’s queue-capacity branches are
unchanged.
- `guard_pixels` is the activation guard outside the visible field.
`hysteresis_pixels` moves deactivation farther out.
- For `queue_guard = true`, non-visible guard/hysteresis candidates are
rejected at `capacity - queue_reserve`, preserving headroom for candidates
intersecting the visible wide field.
- Use `queue_guard = false` only after proving that the exact predicate does
not append to those queues.
- Site address, instruction, threshold, registers, queue policy, guard size,
and enclosing cone/queue metadata all contribute to overlay cache identity.
- Generated/native overlay code and the dirty-RAM interpreter use the same
live helper. Dynamic resizing therefore needs no recompilation.
The debug server’s `ws_aspect_cone_site` command accepts an `address` string
and reports exact-site identity/keep/reject counters.
Explicit `bias_sites` / `range_sites` may opt into an additional resident
object lead without widening terrain or render queues:
```toml
[widescreen.cull]
guard_pixels = 16
activation_guard_pixels = 256
bias_sites = ["0x80069BA8"]
range_sites = ["0x80069BB0"]
```
`activation_guard_pixels` is added only to the live margin emitted at those
two explicit site families, and only while widescreen reveals extra world.
At true 4:3 it is exactly zero. `guard_pixels` remains the shared
render/terrain participation guard; keep it small when terrain producers or
model queues have fixed capacity. Both values are restricted to `[0, 256]`
and contribute to native-overlay cache identity. Changing the activation
guard requires regenerating the game and overlay code.
## Runtime block
Consumed by the cmake macro `psxrecomp_v4_add_runtime_target` (eventually)
and by `runtime/src/main.cpp` as the source of compiled-in defaults.
```toml
[runtime]
debug_port = 4370 # TCP port for the debug server
window_title = "..." # SDL window title
controller = "digital" # "digital" or "dualshock"
memcard_dir = "." # memcard files location, relative to project root
```
### Opt-in warm CD routes
Per-game read acceleration is disabled unless the game explicitly declares
one or more strict routes. Each route arms on one `SetLoc`, then requires every
later file start to match `lbas` in order. A mismatch immediately restores the
normal configured disc timing. Only data-read cadence is accelerated; XA/CDDA,
seek, and motor timing remain authentic.
```toml
[[runtime.warm_cd_routes]]
arm_lba = 95947
lbas = [298, 299, 306]
instant_max_per_frame = 32
```
Up to 16 routes may be declared, with 1–64 LBAs each. The old singular
`[runtime.warm_cd_route]` table is deprecated and emits a warning when loaded;
it remains readable for compatibility. This enhancement is intentionally
opt-in and must not acquire a global default.
The other load-time accelerators are likewise opt-in:
```toml
[runtime]
idle_skip = true
turbo_audio_sink = true
```
### SIO1 serial link (`[runtime.link]`)
The SIO1 register file at `0x1F801050..0x105F` (link cable port) is always
live — every PlayStation has the registers whether or not a cable is plugged
in (`accuracy/axis4_sio1_serial.md`; env `PSX_SIO1_REGS=0` restores the legacy
reads-0 decode for A/B). This block configures the **peer** on the far side of
the cable, default none:
```toml
[runtime.link]
enabled = false # attach a link peer (default off = no cable)
backend = "null" # "null" | "loopback" | "crossover"
latency_cycles = 0 # deterministic extra wire delay per character
trace = false # reserved: SIO1 character trace ring
```
- `null` — no cable: DSR/CTS read low, TX shifts into the void.
- `loopback` — self-wired test cable (TX→own RX, DTR→own DSR): validates a
title's link path without a second console.
- `crossover` — reserved for the dual-console driver, which cross-wires two
in-process machines itself; selecting it without that driver behaves as
`null`.
All backends are deterministic (no wall clock, no sockets on the sim path)
and rollback/savestate-safe; wire state snapshots into `BS_SEC_SIO1`. Env
overrides for A/B: `PSX_SIO1_BACKEND`, `PSX_SIO1_LATENCY`.
### `turbo_loads` / `offer_turbo_loads` — deprecated and ignored
**Do not use these keys.** Load acceleration is owned by the Mods catalog:
`psx.enhancement.fast-loading` ("Fast Loading (host pacing)") and
`psx.enhancement.cd-speed`. Both target `game_id = "*"`, so they ship with every
title, both default to off, and both expose the multiplier and instant-scheduler
detail that a single opaque boolean never could. recomp-ui correspondingly draws
no generic Turbo loads row.
Both keys are still parsed so existing configs load without error, but neither is
honoured — the runtime logs a deprecation line naming the Fast Loading mod and
leaves acceleration off. Remove them from `game.toml`.
The same applies to `[video] turbo_loads` in a user's `settings.toml`: it is no
longer restored at startup, and it is no longer written back out, so the stale
row disappears on the first save after updating. This is deliberate. Because the
launcher stopped drawing a control for it, a persisted `true` was simultaneously
authoritative and unreachable: one run of a build whose `game.toml` said `true`
latched turbo on permanently, and no later config change could undo it. That
shipped to players in MegaManX6Recomp v1.0.4/v1.0.5 (MegaManX6Recomp#14). Never
restore this row without also restoring a UI control for it.
For development, the `turbo_loads` TCP debug command still toggles acceleration
at runtime.
`turbo_audio_sink` is meaningful only while load acceleration is active. It keeps
the guest SPU timeline advancing but discards accelerated samples before host
playback, then fades normal output back in.
## Audio Block
Game projects may choose the host playback cushion after validating their
audio production cadence:
```toml
[audio]
buffer_ms = 60
```
`buffer_ms` accepts 30–500 milliseconds and defaults to 180. Lower values
reduce audible input-to-sound delay, but leave less reserve for frames where a
game temporarily produces no audio and can therefore crackle on affected
titles. This is deliberately a per-game developer choice; it is not read from
the player's `settings.toml`.
## Video Block
Runtime video defaults live in `[video]`:
```toml
[video]
renderer = "opengl" # "software", "opengl", or "vulkan"
offer_vulkan = false # show Vulkan in the launcher only after game validation
auto_skip_fmv = false # legacy Settings/runtime default
offer_skip_fmv = true # false when the game exposes this through Mods
```
`renderer = "vulkan"` remains an experimental runtime choice and still requires
a build compiled with Vulkan support. `offer_vulkan` controls launcher
visibility only; it defaults to false so game projects must explicitly expose
Vulkan after validating their visuals and stability.
`offer_skip_fmv` defaults to true for compatibility with the shared PSX
Settings surface. A game migrating Skip FMVs into its built-in mod catalog sets
it to false. The runtime then hides the Settings row, ignores stale persisted
values, and leaves activation to the selected trusted plugin.
Reserved future fields:
- `default_disc_path` — game runtimes can pre-mount a disc
- `default_game_root` — for sibling-junction setups
## Audit block
See `docs/audit_inventory.md` for the audit pipeline. The schema here is
the input side: regions to walk, address-normalisation rules.
```toml
[audit]
function_starts = "generated/ghidra_function_starts.json" # optional
[[audit.regions]]
name = "..." # e.g. "Boot", "Kernel", "Shell", "Text"
rom_start = "0x..." # byte offset in rom/exe file
rom_end = "0x..." # exclusive
vaddr_base = "0x..." # virtual address corresponding to rom_start
[audit.normalize]
kseg_mask = "0x1FFFFFFF"
[[audit.normalize.remap]]
description = "..." # human-readable; not consumed by tooling
from_lo = "0x..." # inclusive
from_hi = "0x..." # exclusive
to_lo = "0x..." # target start offset (phys = phys - from_lo + to_lo)
```
## What's NOT in the schema yet (Phase B+)
These are noted here so future work knows where to slot them:
- Game `discs` field (Phase D). For now Tomba uses `disc = "..."`.
- `[runtime] disc_swap_command` — runtime-side disc swap (Phase D).
- `[recompiler] seeds` as an array of paths (currently single file;
Phase A might allow multiple).
- `[program] type` explicit discriminator (currently inferred from
`rom` vs `exe` field presence).
## BIOS profiles (`bios/<STEM>.toml`)
One profile per BIOS image; the profile is the single source of truth for the
image identity, the relocation windows, and the runtime anchors. Two ship:
`bios/SCPH1001.toml` (retail; user supplies the dump) and `bios/OpenBIOS.toml`
(MIT, redistributable, shipped with the build). Normal runtimes link both
generated backends (`PSXRECOMP_BIOS_STEMS=OpenBIOS;SCPH1001`) and select one at
launch. The recompiler's `[recompiler] bios_config` identifies the profile used
for game code generation; it does not choose the player's runtime BIOS.
```toml
[program.image] # identity; recompiler refuses a mismatched ROM
sha256 = "..." # pins the exact image (empty = unchecked)
redistributable = false # true: BIOS ships with the game; runtime hides
# any requirement to provide that image
[recompiler.address_model] # boot-time bulk code copies out of ROM
normalize_mask = "0x1FFFFFFF"
[[recompiler.address_model.copy]]
name = "Kernel Part 2" # comment label in the emitted C
rom_lo = "0x1FC10000" # [lo, hi) physical, hi EXCLUSIVE
rom_hi = "0x1FC18000"
ram_lo = "0x00000500" # physical RAM destination
runtime_base = "0x00000500" # vaddr the CPU executes the copy at
dispatch_key = "ram" # "ram": functions keyed by RAM address;
# "rom": RAM alias folds back to ROM
kernel_bless = true # runtime may byte-verify + run native
[[recompiler.install_slots]] # kernel-RAM PCs the BIOS patches at runtime
ram_addr = "0x00000CF0"
[recompiler.runtime_exports] # per-image HLE anchors (omit = unavailable)
shell_entry_phys = "0x00030000"
deliver_event_ret = "0x80001720"
```
Every `copy` entry is a claim that the boot copy is byte-verbatim; the
runtime kernel-bless memcmp enforces it. A BIOS with no copies (runs
entirely from ROM) is valid: normalization degenerates to the KSEG mask.
Semantic invariants (disjoint windows, no fold-output/input intersection,
single bless window) are enforced at load; violations refuse to build.