bios: add the SCPH-5500 and SCPH-5501 profiles

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.
This commit is contained in:
Alexandros Mandravillis
2026-10-01 00:01:56 +03:00
parent fd019bbc84
commit ff4be21c90
9 changed files with 5681 additions and 4 deletions
+142
View File
@@ -0,0 +1,142 @@
# bios/SCPH5500.toml — BIOS build profile for the retail Sony SCPH-5500 image
# (Japan, v3.0 09/09/96).
#
# Compared byte for byte with the SCPH-1001 v2.2 image (bios/SCPH1001.toml):
# ROM [0x00000,0x00080) reset vector 24 bytes differ
# ROM [0x00080,0x18000) kernel identical
# ROM [0x18000,0x80000) shell differs
# So every kernel property below (copy windows, install slots) carries over
# unchanged. Because the reset vector differs, this image has its own seed
# list, recompiler/seeds/phase2_ghidra_seeds_SCPH5500.json: the SCPH-1001
# corpus filtered to seeds whose 64-byte window is identical in both ROMs,
# plus the architectural vectors and this ROM's own A0/B0/C0 table targets
# (the same rule as phase2_ghidra_seeds_SCPH5552.json). The shell has no seeds
# of its own; it runs through the dirty-RAM interpreter (build with
# -DPSX_SHELLWIN_INTERP=ON), as for SCPH5552.
#
# One profile per BIOS image. Consumed by `psxrecomp-bios --config`, by
# tools/regen_bios.sh and by tools/bios_emitter_fingerprint.sh, as for
# SCPH1001.toml. The .BIN image itself stays untracked (bios/* gitignore rule).
[program]
name = "Sony SCPH-5500 BIOS (Japan, v3.0 09/09/96)"
id = "SCPH-5500"
rom = "bios/SCPH5500.BIN"
load_address = "0xBFC00000"
entry_pc = "0xBFC00000"
text_size = "0x80000"
# Declared identity. The emitter refuses to generate from any other image
# (main_bios.cpp's declared-identity gate). SCPH-5500 v3.0: CRC32 FF3EEB8C,
# 512 KiB.
[program.image]
sha256 = "9c0421858e217805f4abe18698afea8d5aa36ff0727eb8484944e00eb5e7eadb"
license = "proprietary"
redistributable = false
[recompiler]
seeds = "recompiler/seeds/phase2_ghidra_seeds_SCPH5500.json"
out_dir = "generated"
out_stem = "SCPH5500"
strict = true
# The BIOS's boot-time bulk copies of CODE out of ROM into RAM. Single source
# of truth for every relocation window in the recompiler AND in the C it
# emits (the generated dispatch's normalize(), the kernel-bless guard, the
# game/shell-overlap tests). Each entry is a CLAIM that the copy is
# byte-verbatim; the runtime's kernel-bless memcmp enforces it for the
# bless window. All bounds are [lo, hi) with hi EXCLUSIVE.
#
# dispatch_key = "ram": functions in the window are keyed by RAM address
# (normalize folds ROM -> RAM).
# dispatch_key = "rom": functions keep their ROM key (normalize folds the
# RAM alias back to ROM).
[recompiler.address_model]
normalize_mask = "0x1FFFFFFF"
[[recompiler.address_model.copy]]
name = "Kernel Part 2"
rom_lo = "0x1FC10000"
rom_hi = "0x1FC18000"
ram_lo = "0x00000500"
runtime_base = "0x00000500" # KUSEG — the kernel executes uncached-region code cached at 0x500
dispatch_key = "ram"
kernel_bless = true
[[recompiler.address_model.copy]]
name = "Shell"
rom_lo = "0x1FC18000"
rom_hi = "0x1FC43000"
ram_lo = "0x00030000"
runtime_base = "0x80030000" # KSEG0 — LoadRunShell jumps to 0x80030000
dispatch_key = "rom"
kernel_bless = false
# NOTE (do not "fix" without evidence): runtime/src/bios_hle.c documents the
# actual LoadRunShell copy as 0x67FF0 bytes (ROM 0xBFC18000 -> RAM 0x30000..
# 0x97FF0). Every pre-profile window constant covered only 0x2B000 of that,
# and dirty_ram_interp.c tracks live RAM in [0x5B000,0x8F000) as an open
# class-B bug. Widening this window is a REAL BEHAVIOR CHANGE requiring its
# own evidence-backed commit — it is not a parameterization cleanup.
# Kernel-RAM RANGES the BIOS and the game's Psy-Q libapi patchers overwrite at
# runtime (CLAUDE.md rule 18; docs/dynamic_handler_install.md). The emitter
# plants a compare-against-ROM hook at each range start so the guest's patched
# instructions execute; the runtime excludes the range from the kernel-bless
# memcmp and resumes native at the range end, so only the patched words
# interpret.
#
# `resume` says how the compiled body picks up again: "jalr" for the classic
# 4-word lui/addiu/jalr/nop stub (the call returns to ram_addr+0x10),
# "fallthrough" when the patched words ARE the function, "none" when the patch
# jumps out and never comes back.
#
# Measured on a live boot of Breath of Fire III (SLPS-00990, Psy-Q SDK 3.70)
# against the ROM source, 30 s in, identical at t+8 s — the patchers are
# _patch_gte / _patch_card / _patch_card2 / _patch_pad. Every SDK title runs
# these, so the ranges are a property of the BIOS revision and libapi release,
# not of one game. Re-measure per SDK version before trusting the numbers:
# the schema tolerates declaring the union.
[[recompiler.install_slots]]
ram_addr = "0x00000C88" # exception handler: register-save prologue
len = "0x30" # shifted two words + an inserted mfc0 v0,Cause
resume = "fallthrough" # the patched words are the function itself
[[recompiler.install_slots]]
ram_addr = "0x00000CF0" # SIO data-byte handler dispatch slot
len = "0x10" # lui/addiu/jalr/nop over ROM zeros
resume = "jalr" # the stub's call returns to 0x0D00
[[recompiler.install_slots]]
ram_addr = "0x00004964" # pad driver clear routine, 11 words NOP'd
len = "0x2C" # (RemovePatchPad / ChgclrPAD family)
resume = "fallthrough"
[[recompiler.install_slots]]
ram_addr = "0x00004D98" # _patch_card2 payload: jalr into boot EXE
len = "0x14" # 0x8017EB6C; the ra landing word is NOP'd too,
resume = "fallthrough" # so the body resumes past it, not at +0x10
[[recompiler.install_slots]]
ram_addr = "0x00006444" # _patch_card payload: the kernel's card
len = "0x10" # handler replaced by jr into boot EXE
resume = "none" # 0x8017EAA0 — a jr never returns here
# NOT declared: RAM 0x0500, one word of kernel_trampoline_0 cleared to zero
# (ROM `lui t2, 0`). It differs on every boot, but what writes it is not yet
# identified, and a range declared on a guess would tell the bless verifier to
# stop checking a word it should be checking. Rule 14: unknown is acceptable,
# guessing is not. Undeclared means that body keeps interpreting, exactly as
# it does today.
# Per-image anchors couriered into the generated C (psx_bios_image) for the
# runtime HLE tier. Values are FACTS about this image, not tunables: the
# shell entry is where LoadRunShell (ROM 0xBFC06FF0) jumps after its copy;
# deliver_event_ret is the instruction after the kernel DeliverEvent loop's
# `jalr v0` (kernel RAM 0x1718), the $ra a delivered event callback returns
# through. Omit a key on a BIOS that has no such anchor — the runtime then
# treats that HLE feature as structurally unavailable instead of firing on a
# wrong address.
[recompiler.runtime_exports]
shell_entry_phys = "0x00030000"
deliver_event_ret = "0x80001720"
+141
View File
@@ -0,0 +1,141 @@
# bios/SCPH5501.toml — BIOS build profile for the retail Sony SCPH-5501 image
# (USA, v3.0 11/18/96).
#
# Compared byte for byte with the SCPH-1001 v2.2 image (bios/SCPH1001.toml):
# ROM [0x00000,0x00080) reset vector identical
# ROM [0x00080,0x18000) kernel identical
# ROM [0x18000,0x80000) shell differs
# So every kernel property below (copy windows, install slots) carries over
# unchanged. The seed list, recompiler/seeds/phase2_ghidra_seeds_SCPH5501.json,
# is the SCPH-1001 corpus filtered to seeds whose 64-byte window is identical
# in both ROMs, plus the architectural vectors and this ROM's own A0/B0/C0
# table targets (the same rule as phase2_ghidra_seeds_SCPH5552.json). The shell
# has no seeds of its own; it runs through the dirty-RAM interpreter (build
# with -DPSX_SHELLWIN_INTERP=ON), as for SCPH5552.
#
# One profile per BIOS image. Consumed by `psxrecomp-bios --config`, by
# tools/regen_bios.sh and by tools/bios_emitter_fingerprint.sh, as for
# SCPH1001.toml. The .BIN image itself stays untracked (bios/* gitignore rule).
[program]
name = "Sony SCPH-5501 BIOS (USA, v3.0 11/18/96)"
id = "SCPH-5501"
rom = "bios/SCPH5501.BIN"
load_address = "0xBFC00000"
entry_pc = "0xBFC00000"
text_size = "0x80000"
# Declared identity. The emitter refuses to generate from any other image
# (main_bios.cpp's declared-identity gate). SCPH-5501 v3.0: CRC32 8D8CB7E4,
# 512 KiB.
[program.image]
sha256 = "11052b6499e466bbf0a709b1f9cb6834a9418e66680387912451e971cf8a1fef"
license = "proprietary"
redistributable = false
[recompiler]
seeds = "recompiler/seeds/phase2_ghidra_seeds_SCPH5501.json"
out_dir = "generated"
out_stem = "SCPH5501"
strict = true
# The BIOS's boot-time bulk copies of CODE out of ROM into RAM. Single source
# of truth for every relocation window in the recompiler AND in the C it
# emits (the generated dispatch's normalize(), the kernel-bless guard, the
# game/shell-overlap tests). Each entry is a CLAIM that the copy is
# byte-verbatim; the runtime's kernel-bless memcmp enforces it for the
# bless window. All bounds are [lo, hi) with hi EXCLUSIVE.
#
# dispatch_key = "ram": functions in the window are keyed by RAM address
# (normalize folds ROM -> RAM).
# dispatch_key = "rom": functions keep their ROM key (normalize folds the
# RAM alias back to ROM).
[recompiler.address_model]
normalize_mask = "0x1FFFFFFF"
[[recompiler.address_model.copy]]
name = "Kernel Part 2"
rom_lo = "0x1FC10000"
rom_hi = "0x1FC18000"
ram_lo = "0x00000500"
runtime_base = "0x00000500" # KUSEG — the kernel executes uncached-region code cached at 0x500
dispatch_key = "ram"
kernel_bless = true
[[recompiler.address_model.copy]]
name = "Shell"
rom_lo = "0x1FC18000"
rom_hi = "0x1FC43000"
ram_lo = "0x00030000"
runtime_base = "0x80030000" # KSEG0 — LoadRunShell jumps to 0x80030000
dispatch_key = "rom"
kernel_bless = false
# NOTE (do not "fix" without evidence): runtime/src/bios_hle.c documents the
# actual LoadRunShell copy as 0x67FF0 bytes (ROM 0xBFC18000 -> RAM 0x30000..
# 0x97FF0). Every pre-profile window constant covered only 0x2B000 of that,
# and dirty_ram_interp.c tracks live RAM in [0x5B000,0x8F000) as an open
# class-B bug. Widening this window is a REAL BEHAVIOR CHANGE requiring its
# own evidence-backed commit — it is not a parameterization cleanup.
# Kernel-RAM RANGES the BIOS and the game's Psy-Q libapi patchers overwrite at
# runtime (CLAUDE.md rule 18; docs/dynamic_handler_install.md). The emitter
# plants a compare-against-ROM hook at each range start so the guest's patched
# instructions execute; the runtime excludes the range from the kernel-bless
# memcmp and resumes native at the range end, so only the patched words
# interpret.
#
# `resume` says how the compiled body picks up again: "jalr" for the classic
# 4-word lui/addiu/jalr/nop stub (the call returns to ram_addr+0x10),
# "fallthrough" when the patched words ARE the function, "none" when the patch
# jumps out and never comes back.
#
# Measured on a live boot of Breath of Fire III (SLPS-00990, Psy-Q SDK 3.70)
# against the ROM source, 30 s in, identical at t+8 s — the patchers are
# _patch_gte / _patch_card / _patch_card2 / _patch_pad. Every SDK title runs
# these, so the ranges are a property of the BIOS revision and libapi release,
# not of one game. Re-measure per SDK version before trusting the numbers:
# the schema tolerates declaring the union.
[[recompiler.install_slots]]
ram_addr = "0x00000C88" # exception handler: register-save prologue
len = "0x30" # shifted two words + an inserted mfc0 v0,Cause
resume = "fallthrough" # the patched words are the function itself
[[recompiler.install_slots]]
ram_addr = "0x00000CF0" # SIO data-byte handler dispatch slot
len = "0x10" # lui/addiu/jalr/nop over ROM zeros
resume = "jalr" # the stub's call returns to 0x0D00
[[recompiler.install_slots]]
ram_addr = "0x00004964" # pad driver clear routine, 11 words NOP'd
len = "0x2C" # (RemovePatchPad / ChgclrPAD family)
resume = "fallthrough"
[[recompiler.install_slots]]
ram_addr = "0x00004D98" # _patch_card2 payload: jalr into boot EXE
len = "0x14" # 0x8017EB6C; the ra landing word is NOP'd too,
resume = "fallthrough" # so the body resumes past it, not at +0x10
[[recompiler.install_slots]]
ram_addr = "0x00006444" # _patch_card payload: the kernel's card
len = "0x10" # handler replaced by jr into boot EXE
resume = "none" # 0x8017EAA0 — a jr never returns here
# NOT declared: RAM 0x0500, one word of kernel_trampoline_0 cleared to zero
# (ROM `lui t2, 0`). It differs on every boot, but what writes it is not yet
# identified, and a range declared on a guess would tell the bless verifier to
# stop checking a word it should be checking. Rule 14: unknown is acceptable,
# guessing is not. Undeclared means that body keeps interpreting, exactly as
# it does today.
# Per-image anchors couriered into the generated C (psx_bios_image) for the
# runtime HLE tier. Values are FACTS about this image, not tunables: the
# shell entry is where LoadRunShell (ROM 0xBFC06FF0) jumps after its copy;
# deliver_event_ret is the instruction after the kernel DeliverEvent loop's
# `jalr v0` (kernel RAM 0x1718), the $ra a delivered event callback returns
# through. Omit a key on a BIOS that has no such anchor — the runtime then
# treats that HLE feature as structurally unavailable instead of firing on a
# wrong address.
[recompiler.runtime_exports]
shell_entry_phys = "0x00030000"
deliver_event_ret = "0x80001720"
+2 -1
View File
@@ -25,7 +25,8 @@ 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-5552), the release ships
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`,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+6 -2
View File
@@ -49,7 +49,8 @@ void usage(const char* program) {
"--output <directory> [--name <title>]\n\n"
"The output contains generated game/BIOS C, game.toml, CMakeLists.txt,\n"
"and build scripts. No compiler toolchain is bundled.\n"
"Supported BIOS: OpenBIOS, SCPH1001, SCPH101, SCPH5552 (any 512 KiB PS1 BIOS dump).\n",
"Supported BIOS: OpenBIOS, SCPH1001, SCPH101, SCPH5500, SCPH5501, SCPH5552\n"
"(any 512 KiB PS1 BIOS dump).\n",
program);
}
@@ -155,6 +156,8 @@ const BiosProfile& select_bios_profile(const fs::path& bios_path) {
{"OPENBIOS", {"OpenBIOS.toml", "OpenBIOS"}},
{"SCPH1001", {"SCPH1001.toml", "SCPH1001"}},
{"SCPH101", {"SCPH101.toml", "SCPH101"}},
{"SCPH5500", {"SCPH5500.toml", "SCPH5500"}},
{"SCPH5501", {"SCPH5501.toml", "SCPH5501"}},
{"SCPH5552", {"SCPH5552.toml", "SCPH5552"}},
};
const std::string token = PSXRecompV4::bios_model_token(bios_path);
@@ -163,7 +166,8 @@ const BiosProfile& select_bios_profile(const fs::path& bios_path) {
}
throw std::runtime_error(
"unsupported BIOS filename '" + bios_path.filename().string() +
"' (expected openbios.bin, SCPH1001.BIN, SCPH101.BIN, or SCPH5552.BIN)");
"' (expected openbios.bin, SCPH1001.BIN, SCPH101.BIN, SCPH5500.BIN, "
"SCPH5501.BIN, or SCPH5552.BIN)");
}
fs::path find_bios_profile(const fs::path& framework,
@@ -62,7 +62,8 @@ def main() -> int:
for profile in (
"OpenBIOS.toml", "SCPH1001.toml",
"SCPH101.toml", "SCPH5552.toml"):
"SCPH101.toml", "SCPH5500.toml", "SCPH5501.toml",
"SCPH5552.toml"):
require(package, f'"{profile}"',
f"CLI package omits BIOS profile {profile}")
require(package, 'shutil.copy2(ROOT / ".gitignore", framework)',
+2
View File
@@ -35,6 +35,8 @@ typedef struct PsxKnownBiosImage {
* identity the recompiler emits into generated/<stem>_dispatch.c. */
static const PsxKnownBiosImage psx_known_bios_images[] = {
{ "SCPH1001", "SCPH-1001", 0x37157331u, 524288u },
{ "SCPH5500", "SCPH-5500", 0xFF3EEB8Cu, 524288u },
{ "SCPH5501", "SCPH-5501", 0x8D8CB7E4u, 524288u },
{ "SCPH5552", "SCPH-5552", 0xD786F0B9u, 524288u },
};
+2
View File
@@ -117,6 +117,8 @@ def main() -> int:
"OpenBIOS.toml": ROOT / "bios" / "OpenBIOS.toml",
"SCPH1001.toml": ROOT / "bios" / "SCPH1001.toml",
"SCPH101.toml": ROOT / "bios" / "SCPH101.toml",
"SCPH5500.toml": ROOT / "bios" / "SCPH5500.toml",
"SCPH5501.toml": ROOT / "bios" / "SCPH5501.toml",
"SCPH5552.toml": ROOT / "bios" / "SCPH5552.toml",
}
(framework / "bios").mkdir()