mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
ff4be21c90
The v3.0 Japanese (SCPH-5500, 09/09/96) and US (SCPH-5501, 11/18/96) retail BIOS images join SCPH-1001, SCPH-101 and SCPH-5552, so every v3.0 region (J, A, E) has a profile. Both images match SCPH-1001 v2.2 byte for byte in the kernel, ROM [0x80, 0x18000). SCPH-5501's reset vector is identical too; SCPH-5500's differs in 24 bytes. The shell differs in both. So each profile is SCPH1001.toml with its own identity (name, id, rom, sha256 pin, stem): the copy windows and install slots are kernel properties and carry over unchanged. The shell runs through the dirty-RAM interpreter, as for SCPH5552. Each image gets its own seed list, derived by the same rule as phase2_ghidra_seeds_SCPH5552.json: the SCPH-1001 corpus filtered to seeds whose 64-byte ROM window is identical in both images (534 kept, 569 dropped), plus the two architectural vectors and 6 A0/B0/C0 kernel-table targets. The files carry only addresses, labels and rationales, plus the SHA-256 of both ROMs. - psx_bios_known_images.h: CRC-32 FF3EEB8C (SCPH-5500) and 8D8CB7E4 (SCPH-5501), both 512 KiB. A build that does not link these backends builds one from the player's own dump as a loadable module, like SCPH-5552, and the netplay lobby can name the image. - main_cli.cpp: map SCPH5500 / SCPH5501 dumps to their profiles. - build_cli.py: package both profiles with the CLI framework. - docs/BIOS_SELECTION.md: list them among the module-built images.
255 lines
13 KiB
Markdown
255 lines
13 KiB
Markdown
# Which BIOS a build uses
|
||
|
||
A PlayStation game needs a BIOS. PSXRecomp can supply one — **OpenBIOS**, an
|
||
MIT-licensed, from-scratch PS1 BIOS from the PCSX-Redux project that we are
|
||
allowed to redistribute — so a player can be handed a build and a disc image and
|
||
just play. A player who prefers their own dumped retail BIOS can use that
|
||
instead.
|
||
|
||
In a developer build both recompiled BIOS backends are linked. The OpenBIOS
|
||
image itself and its MIT notice are staged in `bios/` beside the executable;
|
||
the retail image is never shipped and comes from the player. Which backend runs
|
||
is decided when the game launches, not when it is built.
|
||
|
||
**Bundled releases link both backends.** Decision 2026-09-30: the recompiled
|
||
BIOS carries the same risk as the recompiled game, which titles already
|
||
commit, so the framework commits `generated/OpenBIOS_*.c` and
|
||
`generated/SCPH1001_*.c` (with their `.emitter.sha` stamps) and release CI
|
||
links both. The retail image itself is still never shipped: the runtime needs
|
||
its data at run time and only accepts the exact image the backend was built
|
||
from (size and CRC), so the player supplies their own dump as before. CI
|
||
configures with `PSXRECOMP_BIOS_STALE_FATAL=ON`, so a committed backend whose
|
||
stamp predates the emitter fails the release instead of linking (the
|
||
fingerprint excludes the ROM for this reason; the profile's SHA-256 pin stands
|
||
for it). Regenerate with `tools/regen_bios.sh --config bios/<stem>.toml` and
|
||
commit the output whenever the emitter, seeds or profile change.
|
||
|
||
**Other known images: the backend is built on the player's machine.** For a
|
||
retail image a build does not link (today SCPH-5500, SCPH-5501 and SCPH-5552),
|
||
the release ships
|
||
`overlay_toolchain/` and compiles game code from the player's own disc at
|
||
runtime; the same mechanism builds a BIOS backend from the player's own dump
|
||
(`runtime/include/psx_bios_module.h`, `runtime/src/psx_bios_module.c`,
|
||
`tools/bios_module_build.py`). When the player selects a dump whose size and
|
||
CRC match a shipped profile (`psx_bios_known_images.h`) but no linked backend,
|
||
the runtime:
|
||
|
||
1. looks for `<exe>/cache/bios/<os-arch>/bm<abi>_<codegen hash>_f<flavor>/<STEM>_<crc>.{so,dll}`
|
||
and loads it if present (ABI tag and codegen hash gated, like an overlay shard);
|
||
2. otherwise announces a one-time build, runs `bios_module_build.py` from the
|
||
toolchain (the bundled `psxrecomp-bios` recompiles the dump — the profile's
|
||
SHA-256 pin refuses any other image — and tcc or the system compiler turns
|
||
the C plus the module glue into a self-contained shared library), then
|
||
loads it;
|
||
3. registers the descriptor with `psx_bios_register()` and activates it. Every
|
||
existing selection path then sees it as one more linked backend: `bios.cfg`
|
||
remembers it, netplay settle sees it, and it is hot-swapped like OpenBIOS.
|
||
|
||
The module imports nothing from the executable. Everything it calls goes
|
||
through `OverlayCallbacks` plus a BIOS-specific `PsxBiosModuleCallbacks`
|
||
table, and the runtime globals the generated dispatcher reads are aliased by
|
||
pointer (the `#define`s in `psx_bios_module.h` rewrite the emitter's own
|
||
`extern` lines). The generated C is untouched apart from the emitter no longer
|
||
writing its CPS-marker constructor in a module build. A build takes about a
|
||
minute with gcc for the 32 MB SCPH-1001 output, seconds with tcc; it happens
|
||
once per dump and per framework codegen hash.
|
||
|
||
What the launcher shows in a bundled build: the BIOS row is present (a
|
||
linked retail backend makes a player choice meaningful). A SCPH-1001 dump
|
||
verifies as linked and hot-swaps; a dump of another known image verifies as
|
||
"prepare once" and the wizard's Prepare BIOS job builds its module with a
|
||
progress meter, then resumes; an image no profile covers is refused with the
|
||
accepted list. No build ever offers Generate & rebuild for a BIOS.
|
||
|
||
## The rule
|
||
|
||
> **No BIOS chosen → OpenBIOS. A BIOS the player explicitly chose → that BIOS.**
|
||
|
||
That is the whole user-facing contract. A player never has to find a BIOS file
|
||
to start playing, and never loses the ability to use their own.
|
||
|
||
A developer can switch this off for a title that is known not to work on
|
||
OpenBIOS; then a retail BIOS is required and the player is prompted for one.
|
||
|
||
## What counts as "chosen"
|
||
|
||
A deliberate act by the player:
|
||
|
||
- `--bios <path>` on the command line, or
|
||
- a BIOS selected in the launcher / settings, which is then remembered.
|
||
|
||
**Setup / Generate first-run discovery.** When there is no `bios.cfg` yet (and
|
||
no settings pick), install & build looks next to the executable / project for a
|
||
validated retail dump (`SCPH1001.BIN` and common filename variants; size + CRC
|
||
must match the compiled-in retail image). If found, that path is written to
|
||
`bios.cfg` and used for Generate & Play. If not found, the build stays on
|
||
**OpenBIOS** with no prompt.
|
||
|
||
Discovery does **not** run when `bios.cfg` already exists — including after the
|
||
player cleared the pick to OpenBIOS. Play-time resolution still never invents a
|
||
BIOS from random files on disk once a choice has been recorded.
|
||
|
||
A player who has chosen a BIOS can clear that choice and go back to OpenBIOS;
|
||
the choice is not permanent.
|
||
|
||
## "A retail BIOS" means one specific image
|
||
|
||
A build compiles in exactly one retail image (currently SCPH-1001) alongside
|
||
OpenBIOS. The compiled-in code is generated *from that exact image*, so a
|
||
different dump — SCPH-101, SCPH-5552, a bad rip — cannot be substituted: the
|
||
recompiled code would execute against data it does not match, and crash.
|
||
|
||
Every build therefore checks the identity of a chosen BIOS before using it, and
|
||
says which image it expects if the check fails. This is not DRM and not a
|
||
preference; it is the difference between running and wild-jumping.
|
||
|
||
The acceptance gate compares **file size and CRC32** against each linked
|
||
backend's recorded identity (`bios_backend_for_file` in `runtime/src/main.cpp`).
|
||
A SHA-256 is also recorded per image (`image_sha256` in
|
||
`runtime/include/psx_bios_image.h`) and is surfaced for provenance and bug
|
||
reports, but it is **not** part of the accept/reject decision.
|
||
|
||
If a chosen BIOS does not match:
|
||
|
||
- with OpenBIOS available, the player is told why and can continue on OpenBIOS;
|
||
- with OpenBIOS disabled for the title, the choice is rejected and re-prompted.
|
||
|
||
## Developer setting
|
||
|
||
One optional line in `game.toml`:
|
||
|
||
```toml
|
||
[runtime]
|
||
openbios = true # default. false = this title requires a retail BIOS.
|
||
```
|
||
|
||
Set `openbios = false` only for a title with a **verified** OpenBIOS
|
||
incompatibility. Per-title compatibility is not implied by the framework
|
||
supporting OpenBIOS — verify a title before shipping it that way.
|
||
|
||
This setting is deliberately **not** overridable by a player's `settings.toml`,
|
||
unlike most runtime options. It records a developer's compatibility finding, not
|
||
a preference. A player re-enabling OpenBIOS on a title known to break under it
|
||
produces confusing bug reports and no benefit.
|
||
|
||
There is no third mode. A title either allows OpenBIOS (and still honours an
|
||
explicit retail choice) or requires retail. "OpenBIOS only, retail forbidden"
|
||
would take away a working option from players for no reason.
|
||
|
||
## Boot behaviour does not depend on which BIOS runs
|
||
|
||
Whichever BIOS is active, `[runtime] bios_hle` (on by default) skips the BIOS
|
||
boot sequence and goes straight to the game. That is deliberate and enforced: the
|
||
skip is not a synthesized handoff, it just returns immediately from the BIOS's
|
||
call into its shell, so all it needs is the per-image address of that shell entry
|
||
— which every linked BIOS records. A player who switches BIOS gets the same
|
||
boot, and the flag means one thing everywhere.
|
||
|
||
The HLE tier's *other* half — servicing a few kernel calls (the B0 event family)
|
||
in the runtime instead of the recompiled BIOS — **is** per-image, because it needs
|
||
the kernel's own `DeliverEvent` return address to send a delivered callback back
|
||
to. Retail SCPH-1001 exports it; OpenBIOS deliberately does not until its event
|
||
semantics are validated, so OpenBIOS runs those calls on the recompiled kernel and
|
||
prints one line at startup saying so. That refusal is scoped to kernel calls and
|
||
never cancels the boot-skip.
|
||
|
||
Until 2026-07 it did: the boot-skip was derived from the already-refused
|
||
kernel-call decision, so choosing OpenBIOS silently sat the player in the boot
|
||
animation while the identical build on retail went straight to the game. The two
|
||
axes are now resolved in one pure, unit-tested function
|
||
(`psx_bios_hle_plan()`, `runtime/include/bios_hle_plan.h`), and the test asserts
|
||
the boot decision is byte-identical across the two images.
|
||
|
||
## Save data
|
||
|
||
**Memory cards are unaffected.** They are card images; they carry no BIOS state
|
||
and move freely between BIOSes and between builds.
|
||
|
||
**Savestates are BIOS-specific.** The kernel occupies RAM, and OpenBIOS and
|
||
retail lay that RAM out differently, so a savestate captured under one BIOS is
|
||
not valid under the other. Savestates therefore record which BIOS was active and
|
||
refuse to load under a different one. Without that check, switching BIOS would
|
||
silently corrupt a restored state — the only quiet failure mode in this design,
|
||
which is why it is enforced rather than warned about.
|
||
|
||
Slot files are also isolated on disk under the per-game memcard root:
|
||
|
||
- `<memcard_dir>/openbios/state_*_slot*.pst`
|
||
- `<memcard_dir>/scph1001/state_*_slot*.pst`
|
||
|
||
On first configure, any legacy loose `<memcard_dir>/state_*_slot*.pst` files are
|
||
moved once by `.pst` header `bios_checksum` (bundled OpenBIOS wordsum →
|
||
`openbios/`, otherwise → `scph1001/`). Memory cards stay in `<memcard_dir>/`;
|
||
netplay guest sandbox remains `<memcard_dir>/netplay/` (unscoped).
|
||
|
||
## Netplay lobby settle
|
||
|
||
Online and LAN lobbies advertise a per-peer BIOS offer and freeze a single
|
||
session BIOS at host Start (`openbios` or `scph1001`). The token `scph1001`
|
||
means "retail", not SCPH-1001: each offer also names the retail image the peer
|
||
would boot by its CRC-32 (`retail_crc`), and a retail session carries the CRC
|
||
every peer must boot (`session_bios_crc`). The rule lives in
|
||
`runtime/src/netplay_bios_settle.c`.
|
||
|
||
- **Online:** `bios_offer` (with `retail_crc`) on `set_ready` → host publishes
|
||
`match_caps.session_bios` and `match_caps.session_bios_crc`.
|
||
- **LAN:** peers append offer fields on `MOTK3 JOIN` (the CRC is line 6 of the
|
||
tail); host broadcasts the flags on `MOTK4 UPDATE` and puts the settled
|
||
token and CRC on `MOTK1 START` (lines 5 and 7).
|
||
|
||
Settle rule (same for both):
|
||
|
||
- **Retail** only when every seated peer offers a retail image and all the
|
||
CRCs are equal, and either the host prefers retail or nobody selected
|
||
OpenBIOS. A peer with no offer, no dump or no CRC (an older client) rules
|
||
retail out.
|
||
- Otherwise **OpenBIOS**, when every peer that sent an offer links it.
|
||
- Otherwise retail, if it is possible.
|
||
- Otherwise the host **refuses to start** and the launcher says which images
|
||
differ ("Players use different BIOS images (SCPH-5552 and SCPH-1001) …").
|
||
|
||
Every peer applies that session BIOS before boot, and for retail it boots only
|
||
a dump whose CRC is the settled one. Mixed BIOSes are invalid for rollback
|
||
(kernel RAM layout differs): before this rule an SCPH-5552 peer and an SCPH-1001
|
||
peer both offered `scph1001`, each booted its own image, and the boot digest
|
||
never matched. If the session settles to retail but a peer has no matching
|
||
dump, that peer **aborts the launch** rather than silently falling back
|
||
(which would desync immediately). A session from an older host carries no CRC;
|
||
peers then accept any retail dump, as before.
|
||
|
||
Session BIOS is **ephemeral**: it affects only that match’s runtime boot. It
|
||
does **not** rewrite `bios.cfg`, `settings.toml`, or the launcher Settings BIOS
|
||
row. Soft-return shows the player’s durable preference again; the next Start
|
||
re-settles from offers as usual.
|
||
|
||
## Why both are compiled in
|
||
|
||
The alternative — a build flavour per BIOS — was the original design, and it
|
||
required a game to state its BIOS in three places that nothing cross-checked
|
||
(`bios_config` in `game.toml`, plus two CMake variables). Setting one and
|
||
forgetting another produced a build that linked one BIOS's code against another
|
||
BIOS's generated game code, and it compiled cleanly.
|
||
|
||
Carrying both costs roughly 20 MB of binary and removes that entire class of
|
||
mistake: one build configuration, one source of truth, the choice made at
|
||
runtime where it can be validated.
|
||
|
||
It is affordable because the game's own recompiled code is **identical**
|
||
regardless of which BIOS it was generated against — verified byte-for-byte on
|
||
Tomba!, Tomba! 2 and Ape Escape. Only the BIOS code is duplicated, and the two
|
||
images collide on just 22 symbols, which are namespaced per BIOS.
|
||
|
||
## Attribution
|
||
|
||
OpenBIOS is MIT-licensed. Its notice is vendored at `bios/OpenBIOS.LICENSE`,
|
||
with the upstream source pin and build recipe in `bios/OpenBIOS.toml` and
|
||
attribution in `THIRD_PARTY_ATTRIBUTION.md`. Builds that ship it credit the
|
||
PCSX-Redux authors in the launcher, whether or not the licence compels it.
|
||
|
||
Native runtime builds automatically stage both `bios/openbios.bin` and
|
||
`bios/OpenBIOS.LICENSE`. Release packaging must copy that directory as a unit;
|
||
shipping the image without its notice violates the distribution contract.
|
||
|
||
Retail BIOS images are **not** redistributable and are never shipped. A player
|
||
using one supplies their own dump.
|