Merge BIOS setup-host PR #343 for combined validation

This commit is contained in:
Matthew Stanley
2026-09-11 22:53:05 -07:00
7 changed files with 524 additions and 78 deletions
+185 -20
View File
@@ -2,6 +2,8 @@
#include "psxrecomp_codegen_host.h"
#include "psx_bios_known_images.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
@@ -1135,15 +1137,74 @@ static int resolve_build_paths(void) {
return join_path(g_exe_path, sizeof(g_exe_path), g_build_dir, exe_name);
}
/* Pair a <stem>_dispatch.c with its <stem>_full.c, the same pairing
* runtime.cmake requires before it will link a BIOS backend. */
static int dispatch_has_full(const char* dir, const char* name) {
static const char suffix[] = "_dispatch.c";
char stem[512], full[600], path[1200];
size_t len = strlen(name);
size_t slen = sizeof(suffix) - 1;
if (len <= slen || strcmp(name + len - slen, suffix) != 0)
return 0;
if (len - slen >= sizeof(stem))
return 0;
memcpy(stem, name, len - slen);
stem[len - slen] = '\0';
if ((size_t)snprintf(full, sizeof(full), "%s_full.c", stem) >= sizeof(full))
return 0;
if (!join_path(path, sizeof(path), dir, full))
return 0;
return path_is_file(path);
}
/* Does psxrecomp/generated/ hold ANY recompiled BIOS backend?
*
* This used to probe two hardcoded names, OpenBIOS_dispatch.c and
* SCPH1001_dispatch.c. A port that pins a different image — via
* PSXRECOMP_BIOS_STEMS / game.toml recompiler.bios_config, as every wave-3
* kit does with SCPH5552 — emits its backend under that other stem, so the
* probe was permanently unsatisfied: Generate kept succeeding, the wizard
* kept reopening, and first-run setup could never complete. Accept any stem
* that has both halves instead of naming images here. */
static int generated_has_bios_backend(const char* dir) {
#if defined(_WIN32)
char pat[1200];
WIN32_FIND_DATAA fd;
HANDLE h;
int found = 0;
if (!join_path(pat, sizeof(pat), dir, "*_dispatch.c"))
return 0;
h = FindFirstFileA(pat, &fd);
if (h == INVALID_HANDLE_VALUE)
return 0;
do {
if (dispatch_has_full(dir, fd.cFileName)) {
found = 1;
break;
}
} while (FindNextFileA(h, &fd));
FindClose(h);
return found;
#else
DIR* d = opendir(dir);
struct dirent* e;
int found = 0;
if (!d)
return 0;
while (!found && (e = readdir(d)) != NULL) {
if (dispatch_has_full(dir, e->d_name))
found = 1;
}
closedir(d);
return found;
#endif
}
static int bios_backends_missing(void) {
char openbios[1100], scph[1100];
if (!join_path(openbios, sizeof(openbios), g_project_root,
"psxrecomp/generated/OpenBIOS_dispatch.c"))
char gen[1100];
if (!join_path(gen, sizeof(gen), g_project_root, "psxrecomp/generated"))
return 1;
if (!join_path(scph, sizeof(scph), g_project_root,
"psxrecomp/generated/SCPH1001_dispatch.c"))
return 1;
return !(path_is_file(openbios) || path_is_file(scph));
return !generated_has_bios_backend(gen);
}
int psxrecomp_codegen_host_sources_missing(
@@ -1209,7 +1270,7 @@ static void write_sidecar_near_exe(const char* near_exe, const char* name,
write_line_file(path, value ? value : "");
}
/* IEEE CRC-32 (zlib / Ethernet) — SCPH-1001 identity for setup discovery. */
/* IEEE CRC-32 (zlib / Ethernet) — retail BIOS identity for setup discovery. */
static uint32_t host_crc32(const unsigned char* data, size_t len) {
uint32_t crc = 0xFFFFFFFFu;
size_t i, j;
@@ -1221,17 +1282,23 @@ static uint32_t host_crc32(const unsigned char* data, size_t len) {
return ~crc;
}
/* Does this file match the retail image THIS build pins? A setup host has no
* linked backend to ask, so it consults psx_bios_known_images.h rather than
* assuming SCPH-1001 — which made every non-SCPH1001 kit reject a perfectly
* good dump. An unknown pinned stem adopts nothing and the player is asked. */
static int retail_bios_file_ok_c(const char* path) {
const PsxKnownBiosImage* want = psx_expected_bios();
FILE* f;
long size;
unsigned char* buf;
uint32_t crc;
if (!want) return 0;
if (!path || !path[0] || !path_is_file(path)) return 0;
f = fopen(path, "rb");
if (!f) return 0;
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return 0; }
size = ftell(f);
if (size != 512 * 1024) { fclose(f); return 0; }
if (size != (long)want->size) { fclose(f); return 0; }
if (fseek(f, 0, SEEK_SET) != 0) { fclose(f); return 0; }
buf = (unsigned char*)malloc((size_t)size);
if (!buf) { fclose(f); return 0; }
@@ -1243,22 +1310,22 @@ static int retail_bios_file_ok_c(const char* path) {
fclose(f);
crc = host_crc32(buf, (size_t)size);
free(buf);
return crc == 0x37157331u; /* SCPH-1001 */
return crc == want->crc32;
}
/* Prefer a player-supplied SCPH1001 next to the project/exe for Generate.
* Missing → leave empty (OpenBIOS). Does not override an explicit OpenBIOS. */
/* Prefer a player-supplied dump of the pinned retail image next to the
* project/exe for Generate. Missing → leave empty (OpenBIOS). Does not
* override an explicit OpenBIOS. */
static int discover_retail_bios_c(char* out, size_t cap) {
static const char* names[] = {
"SCPH1001.BIN", "scph1001.bin", "SCPH-1001.BIN", "scph-1001.bin",
"SCPH1001.bin", "scph1001.BIN",
};
char names[8][32];
int nnames = psx_known_bios_filenames(psx_expected_bios(), names, 8);
static const char* subs[] = {
"bios", "", "system", "firmware", "psxrecomp/bios", "psxrecomp-v4/bios",
};
char roots[3][1100];
int nroots = 0;
int r, s, n;
if (nnames <= 0) { out[0] = 0; return 0; }
if (g_project_root[0]) {
snprintf(roots[nroots], sizeof(roots[0]), "%s", g_project_root);
++nroots;
@@ -1282,7 +1349,7 @@ static int discover_retail_bios_c(char* out, size_t cap) {
} else {
snprintf(dir, sizeof(dir), "%s", walk);
}
for (n = 0; n < (int)(sizeof(names) / sizeof(names[0])); ++n) {
for (n = 0; n < nnames; ++n) {
char cand[1300];
if (!join_path(cand, sizeof(cand), dir, names[n])) continue;
if (!retail_bios_file_ok_c(cand)) continue;
@@ -3667,6 +3734,7 @@ static const char* host_loop_breaker_note(void) {
char sidecar[1200], line[64], found[512], marker_abs[1200];
long long then, now;
const char* marker_rel;
const char* cause;
int game_missing, bios_missing;
if (!join_path(sidecar, sizeof(sidecar), g_project_root,
HOST_LAST_GENERATE_SIDECAR))
@@ -3687,16 +3755,30 @@ static const char* host_loop_breaker_note(void) {
if (!game_missing && !bios_missing)
return NULL;
list_generated_dispatch(found, sizeof(found));
/* Each branch has its own cause, so do not assert a single one. A missing
* game dispatch really does point at disagreeing boot-EXE names. Missing
* BIOS backends do not: they mean Generate never emitted them, normally
* because no retail BIOS was available to emit them from. Blaming
* boot-EXE names for that sent players after the wrong thing. */
if (game_missing)
cause = "please report this to the port maintainer: the project's "
"boot-EXE names disagree";
else
cause = "Generate produced the game code but no BIOS backend, which "
"normally means it had no retail BIOS to work from. Select "
"the PlayStation BIOS dump this port requires (named in the "
"README; it must be exactly 512 KB) in the launcher, then "
"run Generate again";
snprintf(g_loop_breaker_note, sizeof(g_loop_breaker_note),
"A Generate completed here recently, yet the launcher still "
"cannot find %s%s%s. generated/ contains: %s. Running Generate "
"again will very likely loop — please report this to the port "
"maintainer: the project's boot-EXE names disagree.",
"again will very likely loop — %s.",
game_missing ? marker_rel : "",
(game_missing && bios_missing) ? " and " : "",
bios_missing ? "the BIOS backends under psxrecomp/generated/"
: "",
found[0] ? found : "no *_dispatch.c at all");
found[0] ? found : "no *_dispatch.c at all",
cause);
fprintf(stderr, "psxrecomp-codegen: %s\n", g_loop_breaker_note);
return g_loop_breaker_note;
}
@@ -4200,9 +4282,92 @@ static int host_paths_same_file(const char* a, const char* b) {
#endif
}
/* --setup-selfcheck: report what the setup host believes about this tree, as
* JSON on stdout, then exit. 0 = generated sources complete, 2 = the wizard
* would reopen, 1 = could not tell (no project root).
*
* This exists because the decision layer -- "are the generated sources
* present?" -- had no headless entry point. Disc selection, BIOS selection and
* generation were already scriptable via psxrecomp_cli.py; the verdict on
* whether setup is DONE was reachable only by clicking through the wizard, so
* nothing in CI could assert it. A stem mismatch there shipped a first-run
* loop on 26 titles before anyone noticed.
*
* Deliberately ahead of the PSX_HAS_GAME_DISPATCH early return below, so a
* product build answers too. Every title already calls this function with
* argc/argv, so no per-title change is needed to gain the flag. */
static void host_json_str(const char* s) {
putchar('"');
for (; s && *s; ++s) {
if (*s == '\\' || *s == '"')
putchar('\\');
putchar(*s);
}
putchar('"');
}
static void host_selfcheck_or_return(const PsxrecompCodegenHostConfig* cfg,
int argc, char** argv) {
const PsxKnownBiosImage* want;
const char* marker_rel;
char marker_abs[1200];
int i, missing, game_ok, bios_ok;
for (i = 1; i < argc; ++i)
if (argv[i] && strcmp(argv[i], "--setup-selfcheck") == 0)
break;
if (i >= argc)
return;
if (!cfg || !cfg->cmake_target || !cfg->exe_basename) {
printf("{\"error\": \"no codegen host config linked\"}\n");
exit(1);
}
/* Sets g_cfg and g_project_root as a side effect. */
missing = psxrecomp_codegen_host_sources_missing(cfg);
if (!g_project_root[0]) {
printf("{\"error\": \"project root not found\"}\n");
exit(1);
}
marker_rel = cfg_or(cfg->gen_marker_relpath,
"generated/SLUS_011.89_dispatch.c");
game_ok = join_path(marker_abs, sizeof(marker_abs), g_project_root,
marker_rel) && path_is_file(marker_abs);
bios_ok = !bios_backends_missing();
want = psx_expected_bios();
printf("{\n");
printf(" \"display_name\": ");
host_json_str(cfg_or(cfg->display_name, "Game"));
printf(",\n \"project_root\": ");
host_json_str(g_project_root);
printf(",\n \"expected_bios_stem\": ");
host_json_str(PSX_EXPECTED_BIOS_STEM);
printf(",\n \"expected_bios_id\": ");
host_json_str(want ? want->id : "");
printf(",\n \"expected_bios_crc32\": ");
if (want) {
char crcbuf[16];
snprintf(crcbuf, sizeof(crcbuf), "0x%08X", want->crc32);
host_json_str(crcbuf);
} else {
printf("null");
}
printf(",\n \"game_dispatch\": ");
host_json_str(marker_rel);
printf(",\n \"game_dispatch_present\": %s", game_ok ? "true" : "false");
printf(",\n \"bios_backends_present\": %s", bios_ok ? "true" : "false");
printf(",\n \"sources_missing\": %s", missing ? "true" : "false");
printf("\n}\n");
fflush(stdout);
exit(missing ? 2 : 0);
}
/* Setup-host zip-root exe → build-release product (bios/mods/assets/settings). */
void psxrecomp_codegen_host_forward_if_built(
const PsxrecompCodegenHostConfig* cfg, int argc, char** argv) {
host_selfcheck_or_return(cfg, argc, argv); /* exits when requested */
#if defined(PSX_HAS_GAME_DISPATCH)
/* Full game binary — already the product tree. */
(void)cfg;
+9 -9
View File
@@ -688,7 +688,7 @@ def _build_recompiler_targets(
progress.log(" ".join(cmake_args))
proc = subprocess.run(
cmake_args, cwd=str(project_root), capture_output=True, text=True
cmake_args, cwd=str(project_root), capture_output=True, text=True, encoding="utf-8", errors="replace"
)
for stream in (proc.stdout, proc.stderr):
if stream:
@@ -705,7 +705,7 @@ def _build_recompiler_targets(
for target in targets:
build_cmd += ["--target", target]
progress.log(" ".join(build_cmd))
proc = subprocess.run(build_cmd, capture_output=True, text=True)
proc = subprocess.run(build_cmd, capture_output=True, text=True, encoding="utf-8", errors="replace")
for stream in (proc.stdout, proc.stderr):
if stream:
for line in stream.splitlines():
@@ -818,7 +818,7 @@ def regen_bios_profile(
[str(bios_tool), "--config", profile_rel],
cwd=str(fw),
capture_output=True,
text=True,
text=True, encoding="utf-8", errors="replace",
)
for stream in (proc.stdout, proc.stderr):
if not stream:
@@ -938,7 +938,7 @@ def run_prepare_disc(
str(project_root),
str(source),
]
proc = subprocess.run(cmd, cwd=str(project_root), capture_output=True, text=True)
proc = subprocess.run(cmd, cwd=str(project_root), capture_output=True, text=True, encoding="utf-8", errors="replace")
out = (proc.stdout or "") + (proc.stderr or "")
for line in out.splitlines():
if line.strip():
@@ -1141,7 +1141,7 @@ def cmd_generate(args: argparse.Namespace, progress: ProgressReporter) -> int:
cmd,
cwd=str(project_root),
capture_output=True,
text=True,
text=True, encoding="utf-8", errors="replace",
)
ri_warn = 0
for stream in (proc.stdout, proc.stderr):
@@ -1314,7 +1314,7 @@ def _cmake_configure(
*extra,
]
progress.log(" ".join(cmd))
proc = subprocess.run(cmd, cwd=str(project_root), capture_output=True, text=True)
proc = subprocess.run(cmd, cwd=str(project_root), capture_output=True, text=True, encoding="utf-8", errors="replace")
for stream in (proc.stdout, proc.stderr):
if stream:
for line in stream.splitlines():
@@ -1397,7 +1397,7 @@ def _cmake_build(
target,
]
progress.log(" ".join(cmd))
proc = subprocess.run(cmd, capture_output=True, text=True)
proc = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="replace")
for stream in (proc.stdout, proc.stderr):
if stream:
for line in stream.splitlines():
@@ -1583,7 +1583,7 @@ def run_pgo_train(
r = subprocess.run(
["xcrun", "--find", "llvm-profdata"],
capture_output=True,
text=True,
text=True, encoding="utf-8", errors="replace",
check=False,
)
if r.returncode == 0 and r.stdout.strip():
@@ -1964,7 +1964,7 @@ def cmd_analyze(args: argparse.Namespace, progress: ProgressReporter) -> int:
progress.phase("analyze", pct=0.3, message=f"Analyzing {exe_path.name}…")
progress.log(" ".join(cmd))
proc = subprocess.run(cmd, cwd=str(project_root), capture_output=True, text=True)
proc = subprocess.run(cmd, cwd=str(project_root), capture_output=True, text=True, encoding="utf-8", errors="replace")
for stream in (proc.stdout, proc.stderr):
if stream:
for line in stream.splitlines():
+2
View File
@@ -649,6 +649,8 @@ if(BUILD_TESTING)
foreach(_t IN ITEMS
bios_selection_guards
capture_history
cli_retail_bios_profile
codegen_host_bios_stems
default_renderer_guards
mod_owned_display_controls
fmv_quiet_guards
+106
View File
@@ -0,0 +1,106 @@
/* psx_bios_known_images.h — identities of the retail BIOS images this
* framework ships a build profile for.
*
* A *built* host asks its linked backends (psx_bios_registry) which image it
* accepts. A *setup* host has no linked backend — that is what makes it a
* setup host — so first-run discovery, bios.cfg seeding and picker copy had
* nowhere to look and hardcoded SCPH-1001. Any kit pinning another image via
* PSXRECOMP_BIOS_STEM / recompiler.bios_config (every wave-3 kit pins
* SCPH5552) then refused to auto-discover a perfectly good dump and told the
* player their BIOS was the wrong one.
*
* Keep in sync with bios/<stem>.toml. A stem absent from this table still
* builds and runs; it only loses first-run auto-discovery and falls back to
* asking the player, which is safe.
*/
#ifndef PSX_BIOS_KNOWN_IMAGES_H
#define PSX_BIOS_KNOWN_IMAGES_H
#include <stdint.h>
#include <string.h>
/* Set by runtime.cmake from PSXRECOMP_BIOS_STEM. */
#ifndef PSX_EXPECTED_BIOS_STEM
#define PSX_EXPECTED_BIOS_STEM "SCPH1001"
#endif
typedef struct PsxKnownBiosImage {
const char* stem; /* generated/<stem>_dispatch.c, bios/<stem>.toml */
const char* id; /* profile id, e.g. "SCPH-5552" */
uint32_t crc32; /* IEEE CRC-32 (zlib/Ethernet) over the whole image */
uint32_t size; /* bytes */
} PsxKnownBiosImage;
/* Each entry verified two ways: against the dump itself, and against the
* identity the recompiler emits into generated/<stem>_dispatch.c. */
static const PsxKnownBiosImage psx_known_bios_images[] = {
{ "SCPH1001", "SCPH-1001", 0x37157331u, 524288u },
{ "SCPH5552", "SCPH-5552", 0xD786F0B9u, 524288u },
};
static inline const PsxKnownBiosImage* psx_known_bios_by_stem(const char* stem) {
size_t i;
if (!stem || !stem[0])
return 0;
for (i = 0; i < sizeof(psx_known_bios_images) / sizeof(psx_known_bios_images[0]); ++i) {
if (strcmp(psx_known_bios_images[i].stem, stem) == 0)
return &psx_known_bios_images[i];
}
return 0;
}
/* The retail image THIS build was configured for, or NULL when the pinned
* stem is not in the table. Callers must handle NULL by asking the player
* rather than assuming SCPH-1001. */
static inline const PsxKnownBiosImage* psx_expected_bios(void) {
return psx_known_bios_by_stem(PSX_EXPECTED_BIOS_STEM);
}
/* A short label for the pinned image, for player-facing copy: "SCPH5552.BIN".
* Falls back to the raw stem when the identity is not in the table. */
static inline const char* psx_expected_bios_label(void) {
static char label[40];
if (!label[0]) {
const PsxKnownBiosImage* e = psx_expected_bios();
const char* stem = e ? e->stem : PSX_EXPECTED_BIOS_STEM;
size_t n = strlen(stem);
if (n > sizeof(label) - 5)
n = sizeof(label) - 5;
memcpy(label, stem, n);
memcpy(label + n, ".BIN", 5);
}
return label;
}
/* Filename spellings worth probing for <img> during first-run discovery,
* written into out[0..n). Covers the stem and the dashed profile id, each in
* upper and lower case: SCPH5552.BIN, scph5552.bin, SCPH-5552.BIN, ... */
static inline int psx_known_bios_filenames(const PsxKnownBiosImage* img,
char out[][32], int cap) {
const char* bases[2];
int n = 0, b, lower;
if (!img)
return 0;
bases[0] = img->stem;
bases[1] = img->id;
for (b = 0; b < 2; ++b) {
for (lower = 0; lower < 2; ++lower) {
const char* ext = lower ? "bin" : "BIN";
size_t i, len = strlen(bases[b]);
char* dst;
if (n >= cap || len + 5 > 32)
continue;
dst = out[n++];
for (i = 0; i < len; ++i) {
char c = bases[b][i];
dst[i] = (char)(lower && c >= 'A' && c <= 'Z' ? c - 'A' + 'a' : c);
}
dst[len] = '.';
memcpy(dst + len + 1, ext, 3);
dst[len + 4] = 0;
}
}
return n;
}
#endif /* PSX_BIOS_KNOWN_IMAGES_H */
+32 -10
View File
@@ -515,7 +515,7 @@ else()
if(PSXRECOMP_HAS_RECOMP_NET)
message(FATAL_ERROR
"psxrecomp: PSX_NETPLAY needs retcomm-rbengine.\n"
" git submodule update --init lib/retcomm-rbengine\n"
" git submodule update --init --recursive lib/retcomm-rbengine\n"
" or -DRECOMP_RBENGINE_ROOT=/path/to/retcomm-rbengine")
endif()
endif()
@@ -544,7 +544,13 @@ if(PSX_REWIND)
message(FATAL_ERROR
"psxrecomp: PSX_REWIND=ON exposes the Rewind launcher controls "
"but no retcomm-rbengine snap-ring backend was found.\n"
" git submodule update --init lib/retcomm-rbengine\n"
"A source ZIP downloaded from GitHub never contains submodule "
"contents and cannot build. Clone instead:\n"
" git clone --recurse-submodules <repo-url>\n"
"In an existing clone, run this from the GAME repo root. Note "
"--recursive: rbengine is a submodule of psxrecomp, not of the "
"game, so a non-recursive init leaves it empty.\n"
" git submodule update --init --recursive\n"
" or -DRECOMP_RBENGINE_ROOT=/path/to/retcomm-rbengine\n"
" or configure with -DPSX_REWIND=OFF to hide Rewind.")
endif()
@@ -1248,15 +1254,18 @@ function(psxrecomp_add_runtime_target target)
# where releases are validated. Dev checkouts still resolve the relative
# default without prompting via the exe-dir upward search, which also tries
# <ancestor>/psxrecomp-v4/<relative> for game-project layouts.
# Follow the stem this build actually pins; assuming SCPH1001 here handed
# every non-SCPH1001 kit a default path that could never resolve.
set(_psxrt_stem_bios "bios/${PSXRECOMP_BIOS_STEM}.BIN")
if(NOT PSXRT_DEFAULT_BIOS_PATH)
set(PSXRT_DEFAULT_BIOS_PATH "bios/SCPH1001.BIN")
set(PSXRT_DEFAULT_BIOS_PATH "${_psxrt_stem_bios}")
elseif(IS_ABSOLUTE "${PSXRT_DEFAULT_BIOS_PATH}")
message(WARNING
"DEFAULT_BIOS_PATH '${PSXRT_DEFAULT_BIOS_PATH}' is absolute; refusing to "
"bake a build-machine path into the binary (release exes must prompt on "
"user machines). Using relative 'bios/SCPH1001.BIN' instead — drop the "
"user machines). Using relative '${_psxrt_stem_bios}' instead — drop the "
"DEFAULT_BIOS_PATH argument from this game's CMakeLists.")
set(PSXRT_DEFAULT_BIOS_PATH "bios/SCPH1001.BIN")
set(PSXRT_DEFAULT_BIOS_PATH "${_psxrt_stem_bios}")
endif()
if(NOT DEFINED PSXRT_DEFAULT_GAME_CONFIG_PATH)
set(PSXRT_DEFAULT_GAME_CONFIG_PATH "")
@@ -1715,6 +1724,10 @@ function(psxrecomp_add_runtime_target target)
target_compile_definitions(${target} PRIVATE
DEFAULT_DEBUG_PORT=${PSXRT_DEBUG_PORT}
PSX_DEFAULT_BIOS_PATH="${PSXRT_DEFAULT_BIOS_PATH}"
# The retail stem this build pins. A setup host has no linked
# backend to ask, so this is how it knows which image to look for
# and name (psx_bios_known_images.h) instead of assuming SCPH-1001.
PSX_EXPECTED_BIOS_STEM="${PSXRECOMP_BIOS_STEM}"
# Where the shipped redistributable image lives, relative to the exe.
# This is what a player gets when they choose no BIOS.
PSX_BUNDLED_BIOS_PATH="${PSXRECOMP_BUNDLED_BIOS_PATH}"
@@ -1909,11 +1922,20 @@ function(psxrecomp_add_runtime_target target)
if(PSX_RECOMP_UI AND NOT PSXRT_ORACLE)
if(NOT RECOMP_UI_ROOT OR NOT EXISTS "${RECOMP_UI_ROOT}/recomp_ui.cmake")
message(FATAL_ERROR
"PSX_RECOMP_UI=ON but recomp-ui is missing.\n"
"Add at the game repo root:\n"
" git submodule add -b master "
"https://github.com/RetroPortingToolKit/recomp-ui.git recomp-ui\n"
"Or set -DRECOMP_UI_ROOT=/path/to/recomp-ui")
"PSX_RECOMP_UI=ON but recomp-ui is missing from the game "
"repo root.\n"
"A source ZIP downloaded from GitHub never contains "
"submodule contents and cannot build. Clone instead:\n"
" git clone --recurse-submodules <repo-url>\n"
"In a clone that already declares recomp-ui, fetch the "
"pinned commit:\n"
" git submodule update --init --recursive\n"
"Only if this repo does not declare recomp-ui yet, add it "
"(use the fork this project pins, not necessarily "
"upstream):\n"
" git submodule add <recomp-ui-url> recomp-ui\n"
"Or point at an existing checkout: "
"-DRECOMP_UI_ROOT=/path/to/recomp-ui")
endif()
# recomp-ui gates its Mods view behind RECOMP_UI_ENABLE_MODS, which
# defaults OFF there -- correct for a cross-console launcher, since a
+63 -39
View File
@@ -16,6 +16,7 @@
#include "boot_state.h"
#include "bios_hle.h"
#include "bios_hle_plan.h"
#include "psx_bios_known_images.h"
#include "psx_bios_backend.h"
#include "psx_cycles.h"
#include "starvation_ring.h"
@@ -2514,11 +2515,32 @@ static std::filesystem::path resolve_bios_for_runtime(const char* requested,
const bool bundled_only =
openbios_allowed && bundled && !player_bios_selectable;
/* 0. Setup host (CI zip root): no BIOS backends are linked yet, so there
* is nothing an image could be validated against — bios_backend_for_file()
* rejects every file, including the correct one, and a title with
* openbios = false has no fallback to land on. This MUST precede the
* explicit-choice branch below: a remembered bios.cfg pick reached
* validate_bios_for_launch() first and deadlocked first-run setup — the
* player was told their good SCPH-1001 was "not an image this build was
* compiled from", and could never supply the BIOS that Generate needs in
* order to emit the backend. Play belongs to the product binary under
* build-release/ after Generate & rebuild. */
if (psx_bios_registry_count == 0) {
launcher_warning("Setup host — finish Generate & rebuild",
"This executable is the first-run setup host (no game/BIOS code "
"linked).\n\n"
"Use Generate & rebuild in the launcher. After that succeeds, open "
"this same shortcut again — it starts the game from build-release/ "
"(where bios/, mods/, and settings live).\n\n"
"Or run build-release/<game>.exe directly.");
return {};
}
/* 1. An explicit choice: --bios, else a remembered pick. A product build
* with only its bundled backend has no meaningful player choice: ignore
* stale settings/bios.cfg paths instead of validating an image the hidden
* launcher row cannot clear. Setup hosts (registry_count == 0) retain their
* picker/generation flow. */
* launcher row cannot clear. Setup hosts (registry_count == 0) already
* returned above. */
std::filesystem::path chosen;
if (!bundled_only) {
if (requested_is_explicit && requested && requested[0]) {
@@ -2551,19 +2573,6 @@ static std::filesystem::path resolve_bios_for_runtime(const char* requested,
return {};
}
/* Setup host (CI zip root): no BIOS backends linked yet. Play belongs to
* the product binary under build-release/ after Generate & rebuild. */
if (psx_bios_registry_count == 0) {
launcher_warning("Setup host — finish Generate & rebuild",
"This executable is the first-run setup host (no game/BIOS code "
"linked).\n\n"
"Use Generate & rebuild in the launcher. After that succeeds, open "
"this same shortcut again — it starts the game from build-release/ "
"(where bios/, mods/, and settings live).\n\n"
"Or run build-release/<game>.exe directly.");
return {};
}
/* 3. This title requires a retail BIOS: ask for one. */
const std::string accepted = bios_accepted_images();
launcher_info((s_picker_game_name + " — PlayStation BIOS needed").c_str(),
@@ -2571,7 +2580,8 @@ static std::filesystem::path resolve_bios_for_runtime(const char* requested,
"Step 1 of 2 — PlayStation BIOS\n\n"
"In the next window, select your PlayStation BIOS dump. This build "
"requires the exact image it was compiled from: " + accepted + ". "
"Usually named SCPH1001.BIN and exactly 512 KB. Dump from your own "
"Usually named " + std::string(psx_expected_bios_label()) +
" and exactly 512 KB. Dump from your own "
"console or otherwise legally obtain it.\n\n"
"(This is NOT the game disc — that is asked for next.)");
std::string bios_title =
@@ -2704,24 +2714,27 @@ static bool retail_bios_file_ok(const std::filesystem::path& path) {
const PsxBiosBackend* b = bios_backend_for_file(path, nullptr, nullptr);
return b && b->image && !b->image->image_bundled;
}
/* Setup host (no backends linked yet): accept validated SCPH-1001 only. */
constexpr uint64_t kSize = 512u * 1024u;
constexpr uint32_t kScph1001Crc = 0x37157331u;
/* Setup host (no backends linked yet): accept the retail image THIS build
* pins, from psx_bios_known_images.h. This used to hardcode SCPH-1001, so
* a kit pinning anything else refused to seed from a correct dump. An
* unknown pinned stem seeds nothing and the player is asked instead. */
const PsxKnownBiosImage* want = psx_expected_bios();
if (!want) return false;
std::ifstream f(path, std::ios::binary | std::ios::ate);
if (!f.is_open()) return false;
const auto size = static_cast<uint64_t>(f.tellg());
if (size != kSize) return false;
if (size != static_cast<uint64_t>(want->size)) return false;
std::vector<uint8_t> data(static_cast<size_t>(size));
if (!read_at(f, 0, data.data(), data.size())) return false;
return crc32_compute(data.data(), data.size()) == kScph1001Crc;
return crc32_compute(data.data(), data.size()) == want->crc32;
}
static std::filesystem::path discover_retail_bios_near(const char* argv0) {
namespace fs = std::filesystem;
static const char* kNames[] = {
"SCPH1001.BIN", "scph1001.bin", "SCPH-1001.BIN", "scph-1001.bin",
"SCPH1001.bin", "scph1001.BIN",
};
char name_buf[8][32];
const int name_count =
psx_known_bios_filenames(psx_expected_bios(), name_buf, 8);
if (name_count <= 0) return {};
static const char* kSubdirs[] = {
"bios", "", "system", "firmware", "psxrecomp/bios", "psxrecomp-v4/bios",
};
@@ -2730,8 +2743,8 @@ static std::filesystem::path discover_retail_bios_near(const char* argv0) {
for (fs::path root = exe_dir; !root.empty(); root = root.parent_path()) {
for (const char* sub : kSubdirs) {
const fs::path dir = (sub && sub[0]) ? (root / sub) : root;
for (const char* name : kNames) {
const fs::path cand = dir / name;
for (int ni = 0; ni < name_count; ++ni) {
const fs::path cand = dir / name_buf[ni];
if (retail_bios_file_ok(cand)) {
auto abs = fs::weakly_canonical(cand, ec);
if (ec) abs = fs::absolute(cand, ec);
@@ -7666,12 +7679,14 @@ namespace {
out->ok = 1;
std::snprintf(out->detail, sizeof(out->detail),
"OpenBIOS will be emitted on Generate & rebuild "
"(optional: pick SCPH1001). Play uses "
"build-release/ after rebuild.");
"(optional: pick %s). Play uses "
"build-release/ after rebuild.",
psx_expected_bios_label());
return 1;
}
std::snprintf(out->detail, sizeof(out->detail),
"PlayStation BIOS required (SCPH1001.BIN).");
"PlayStation BIOS required (%s).",
psx_expected_bios_label());
return 1;
}
/* Match runtime resolve: relative picks like bios/SCPH1001.BIN must not
@@ -7689,12 +7704,16 @@ namespace {
"BIOS file not found.");
return 1;
}
const PsxKnownBiosImage* want = psx_expected_bios();
const std::streamoff want_size =
want ? (std::streamoff)want->size : (std::streamoff)(512 * 1024);
const std::streamoff size = f.tellg();
if (size != 512 * 1024) {
if (size != want_size) {
std::snprintf(out->detail, sizeof(out->detail),
"BIOS must be exactly 512 KiB (got %lld). Use "
"SCPH1001.BIN.",
(long long)size);
"BIOS must be exactly %lld bytes (got %lld). Use "
"%s.",
(long long)want_size, (long long)size,
psx_expected_bios_label());
return 1;
}
std::vector<uint8_t> data((size_t)size);
@@ -7704,15 +7723,20 @@ namespace {
return 1;
}
const uint32_t crc = crc32_compute(data.data(), data.size());
if (crc != 0x37157331u) {
if (want && crc != want->crc32) {
out->warn = 1;
std::snprintf(out->detail, sizeof(out->detail),
"CRC32 %08X (validated dump is SCPH1001 CRC32 "
"37157331).",
crc);
"CRC32 %08X (this build expects %s, CRC32 %08X).",
crc, want->id, want->crc32);
} else if (!want) {
out->warn = 1;
std::snprintf(out->detail, sizeof(out->detail),
"CRC32 %08X (this build pins %s, whose identity "
"is not recorded here).",
crc, PSX_EXPECTED_BIOS_STEM);
} else {
std::snprintf(out->detail, sizeof(out->detail),
"SCPH1001.BIN (CRC OK).");
"%s (CRC OK).", psx_expected_bios_label());
}
/* Setup host (no backends yet): file is fine for first Generate. */
if (psx_bios_registry_count == 0) {
@@ -0,0 +1,127 @@
#!/usr/bin/env python3
"""Regression: the codegen host must accept ANY recompiled BIOS stem.
bios_backends_missing() in host/psxrecomp_codegen_host.c used to probe two
hardcoded filenames, psxrecomp/generated/OpenBIOS_dispatch.c and
SCPH1001_dispatch.c. Ports that pin another image via recompiler.bios_config /
PSXRECOMP_BIOS_STEMS emit <stem>_dispatch.c + <stem>_full.c instead, so the
probe could never be satisfied: Generate succeeded, the setup wizard reopened,
and first-run setup looped forever. Every wave-3 kit pins SCPH5552 and every
one of them shipped with that loop.
test_cli_retail_bios_profile.py already covers the psxrecomp_cli.py half of
this feature. This is the C host half, which had no coverage.
"""
from pathlib import Path
import os
import shutil
import subprocess
import sys
import tempfile
ROOT = Path(__file__).resolve().parents[2]
HOST_C = ROOT / "host" / "psxrecomp_codegen_host.c"
PROBE = """
#include <string.h>
#include <stdio.h>
#include "psxrecomp_codegen_host.h"
/* recomp-ui symbol the host references but sources_missing() never reaches. */
int recomp_launcher_relaunch_exe(char* o, size_t c) { (void)o; (void)c; return 0; }
int main(void) {
PsxrecompCodegenHostConfig cfg;
memset(&cfg, 0, sizeof(cfg));
cfg.cmake_target = "psx-runtime";
cfg.exe_basename = "Probe";
cfg.gen_marker_relpath = "generated/SCUS_943.51_dispatch.c";
printf("%d", psxrecomp_codegen_host_sources_missing(&cfg));
return 0;
}
"""
def find_recomp_ui() -> Path | None:
env = os.environ.get("RECOMP_UI_ROOT")
candidates = ([Path(env)] if env else []) + [
ROOT.parent / "recomp-ui",
ROOT / "recomp-ui",
]
for c in candidates:
if (c / "src" / "recomp_launcher.h").is_file():
return c
return None
def make_project(root: Path, bios_files: list[str]) -> None:
"""A project tree the host recognises, with the given BIOS artefacts."""
(root / "generated").mkdir(parents=True, exist_ok=True)
(root / "generated" / "SCUS_943.51_dispatch.c").write_text("", encoding="utf-8")
fw_gen = root / "psxrecomp" / "generated"
fw_gen.mkdir(parents=True, exist_ok=True)
(root / "game.toml").write_text("[game]\n", encoding="utf-8")
(root / "psxrecomp" / "psxrecomp_cli.py").write_text("", encoding="utf-8")
for name in bios_files:
(fw_gen / name).write_text("", encoding="utf-8")
def main() -> int:
ui = find_recomp_ui()
if ui is None:
print("SKIP: recomp-ui not found (set RECOMP_UI_ROOT)")
return 0
cc = shutil.which("cc") or shutil.which("gcc") or shutil.which("clang")
if cc is None:
print("SKIP: no C compiler on PATH")
return 0
with tempfile.TemporaryDirectory() as tmp:
tmp = Path(tmp)
probe_c = tmp / "probe.c"
probe_c.write_text(PROBE, encoding="utf-8")
exe = tmp / ("probe.exe" if os.name == "nt" else "probe")
build = subprocess.run(
[cc, "-std=c11", "-o", str(exe), str(probe_c), str(HOST_C),
"-I", str(ROOT / "host"),
"-I", str(ROOT / "runtime" / "include"),
"-I", str(ui / "src"), "-I", str(ui / "src" / "common")],
capture_output=True, text=True)
if build.returncode != 0:
print("FAIL: could not build probe\n" + build.stderr[-2000:])
return 1
cases = [
# (BIOS artefacts present, expected sources_missing, why)
(["SCPH5552_dispatch.c", "SCPH5552_full.c"], 0,
"a pinned non-SCPH1001 stem must satisfy the host"),
(["OpenBIOS_dispatch.c", "OpenBIOS_full.c"], 0,
"the bundled stem must still satisfy the host"),
(["SCPH5552_dispatch.c"], 1,
"a dispatch with no _full.c is not a linkable backend"),
([], 1,
"no BIOS backend at all means setup is genuinely incomplete"),
]
failures = 0
for artefacts, expected, why in cases:
project = tmp / ("case%d" % len(artefacts + [why]))
shutil.rmtree(project, ignore_errors=True)
make_project(project, artefacts)
env = dict(os.environ, PSXRECOMP_PROJECT_ROOT=str(project))
run = subprocess.run([str(exe)], capture_output=True, text=True, env=env)
got = run.stdout.strip()
if got != str(expected):
print("FAIL: %s\n artefacts=%s expected=%s got=%r"
% (why, artefacts or "(none)", expected, got))
failures += 1
else:
print("ok: %s" % why)
if failures:
return 1
print("PASS: codegen host accepts any paired BIOS stem")
return 0
if __name__ == "__main__":
sys.exit(main())