Follow-ups to the marker-skew hardening, closing the remaining legs of
the generate+build loop class:
- Loop breaker: host_prepare_generate stamps a successful generate into
.psxrecomp_last_generate at the project root. When apply() is about to
reopen the wizard because sources are missing and the stamp is recent,
the wizard's prepare note becomes a diagnostic naming exactly which
gate is unmet (the expected *_dispatch.c and/or the BIOS backends) and
what generated/ actually contains, instead of silently asking for
another round. The stamp is dropped once sources are visible so a much
later manual wipe of generated/ reads as a fresh first run.
- Preflight: generate now refuses up front when the project root sits
under the Windows temp directory (Explorer's run-from-inside-the-ZIP
extraction, which evaporates) or is not writable, with instructions,
instead of failing downstream in ways that present as the same loop.
- CI lint: tools/ci/check_boot_exe.sh cross-checks the three baked
copies of the boot-EXE name (game.toml boot_exe/exe, CMakeLists
GEN_MARKER, codegen_setup.c gen_marker_relpath) and the setup-release
template gains a fail-fast "Lint boot-EXE name consistency" step, so a
skew fails the maintainer's release instead of the player's install.
op_emit_ci_workflow's step-name drift detection flags older titles
automatically. Script is bash-3.2-clean for macOS runners; verified
against fixture repos (consistent / skewed / boot_exe override /
single-source) and real ports - where it immediately caught
Tomba2Recomp's placeholder gen_marker_relpath ("SLUS_01234" vs
SCUS_944.54), a live instance of the reported loop.
Both gitignore templates learn the new sidecar names.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfPHUBEYiSCwaGiwRc8ea7
A boot-exe name skew between game.toml, codegen_setup.c's
gen_marker_relpath, and CMakeLists' GEN_MARKER used to loop the setup
wizard forever with every stage green: generate accepted any
*_dispatch.c and reported ok, the rebuild silently produced another
setup host (REQUIRE_GAME_C defaults OFF and nothing set it), and the
Windows deferred helper relaunched it straight back into first-run
setup. Reported by self-build users on ports whose three baked
@BOOT_EXE@ copies disagree.
Make each stage assert what the next one gates on:
- cmd_rebuild now configures with -DPSXRECOMP_REQUIRE_GAME_C=ON and the
canonical -DPSXRECOMP_FORCE_SETUP_HOST=OFF (the legacy BPE_ alias only
maps ON). Defaults precede --cmake-extra so a deliberate override
still wins via cmake's last-one-wins.
- generate errors when the produced dispatch name differs from the
expected marker, naming both; the setup host passes
--gen-marker <basename of gen_marker_relpath> so generate checks the
exact name the launcher and CMake will later demand.
- The Windows deferred helper re-resolves the exe from
psxrecomp_exe_name-<target>.txt after the build (the pre-build guess
goes stale on a title rename) and verifies marker + exe exist before
relaunching, pausing with a report-to-maintainer message otherwise.
Relaunch/heal and both failure gates exercised under Wine cmd.
Multi-disc titles keep a single boot EXE (update_disc_set.py only
maintains discs/disc_serials), so the strict name check is safe there.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfPHUBEYiSCwaGiwRc8ea7
Records the wizard's disc picks where the runtime actually looks, so a
standalone-wizard install reaches the same end state as a RetComM build.
The gap this closes. The runtime's hot-swap roster is built from
game.toml [game] discs (game_discs = gc.discs). disc.cfg is the
mounted-image cache and read_cached_path() takes only its first line via
std::getline. So the previous commit -- which taught the wizard to ASK
for every disc and write them all to disc.cfg -- recorded discs 2..N
where nothing reads them for the roster. The player located three images
and still got a one-disc roster.
Nothing in the standalone path wrote [game] discs. The tooling that does
lives in retcomm-studio, which is why the RetComM path produced a working
3-disc config and the wizard could not.
tools/new_project_layout/probe_disc.py synced forward
tools/new_project_layout/verify_disc_set.py vendored (new here)
Both from retcomm-studio @ a2b71ed9 (MIT, (c) Alex Vanderveen; psxrecomp
is PolyForm-NC, so this is permissive-into-stricter and the notice is
recorded in THIRD_PARTY_ATTRIBUTION.md). psxrecomp's probe_disc.py was a
stale fork predating multi-disc support: 90 lines behind, with its only
8 unique lines being older revisions of the same functions, so the sync
drops nothing psxrecomp-specific. A single-disc project still emits
`disc = "..."` byte-identically to before.
update_disc_set.py is new, and is the piece neither repo had. probe_disc
--write-game-toml renders a COMPLETE game.toml, which is right when
scaffolding and destructive afterwards -- it would discard a live
project's [video], [controller], [widescreen], [netplay] and its
comments. This probes each located image, verifies them as one set, and
then edits ONLY disc / discs / disc_serials / required_disc_fps. It skips
any key whose value it already agrees with, so it does not reflow a
hand-formatted array and a re-run is a no-op. It refuses to write at all
when verify_disc_set rejects the set: images that are not one program are
a mistake to surface, not a config to record.
Generate runs it before invoking the CLI (the CLI reads game.toml).
Failure is reported and non-fatal -- Generate needs only the boot image,
and refusing to build because disc 3 could not be probed is worse than
building with a single-disc roster. Absent script (older SDK checkout) or
fewer than two located discs are both early returns, so single-disc
titles are untouched.
Verified against Final Fantasy VII's three real images:
- from a single-disc game.toml, produces discs + disc_serials
(SCUS-94163/64/65) + required_disc_fps identical to the values the
RetComM path had already produced in that repo;
- all 114 remaining lines of config and comments byte-for-byte intact;
- a second run changes nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWiDNcWuNtDWHE7Pnhq4jp
The runtime launcher has built a multi-disc roster from game.toml
[game] discs for a while. The SETUP host never did -- it did not read
that key at all -- so on a first run the wizard could not know a title
was a multi-disc set and asked for a single image. The other discs were
discovered missing much later, mid-game, at the first swap.
Read [game] discs in psxrecomp_codegen_host_apply and publish it as
gi.discs / gi.num_discs, matching the runtime's rule that a roster only
appears when there is more than one image. A single-disc title publishes
nothing and its wizard is byte-for-byte the page it was before.
The reader is ~60 lines rather than a TOML dependency: this host shells
out to Python for every real parse, and one array of strings does not
justify linking a parser. Only [game] discs is recognised; anything else
is skipped rather than guessed at. Relative entries are absolutised
against the project root, because the launcher existence-checks what we
publish and its cwd is not necessarily that root -- a relative path would
otherwise render as "missing" on a perfectly good install.
Implements persist_setup_discs (recomp-ui, appended additively to
RecompLauncherCGameInfo). disc.cfg becomes one path per line in disc
order. That is additive in the direction that matters: a reader taking
only the first line still gets disc 1, exactly what it got before. A slot
the player has not located is written as an EMPTY line rather than
skipped, so line N is always disc N and filling one in later cannot
renumber the others. Nothing located at all leaves any existing disc.cfg
alone instead of replacing it with blank lines.
The BIOS half delegates to host_persist_setup with rom_path NULL, which
runs its bios.cfg logic and skips its single-path disc write, so the two
entry points cannot drift on BIOS handling.
Verified against Final Fantasy VII's real 3-disc game.toml by driving
psxrecomp_codegen_host_apply directly: num_discs = 3, persist_setup_discs
set, all three absolute paths in disc order.
Requires the matching recomp-ui change to be visible; without it the
roster is published to a launcher that ignores the field, which is inert
rather than broken.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWiDNcWuNtDWHE7Pnhq4jp
The runtime's filename was derived three times from three copies of the same
window title. runtime.cmake sets OUTPUT_NAME from
MAKE_C_IDENTIFIER(WINDOW_TITLE); psxrecomp_cli.py rebuilt it from --exe-name;
and the in-runtime self-compiler rebuilt it again from
codegen_setup.exe_basename. The rule is identical in all three, so they agree
right up until a game is renamed and only one copy is refreshed. After that the
build links <new>.exe while both consumers look for <old>.exe, and the failure
is reported as
build succeeded but binary missing: ...\<old>.exe
Build failed. Fix the errors above, then rebuild manually.
on a build that had no errors at all, with the executable sitting in the build
directory under the name CMake chose. Seen on Revelations: Persona, where CMake
emitted Revelations__Persona_Recompiled.exe and the setup host wanted
Revelations_Persona__Recompiled.exe — same algorithm, colon in a different
place in each side's copy of the title.
runtime.cmake now writes the OUTPUT_NAME it actually set to
<build-dir>/psxrecomp_exe_name-<target>.txt, and both consumers prefer that
over their own derivation. Written at configure time with file(WRITE) rather
than file(GENERATE): _psxrt_exe_name is a plain variable carrying no generator
expression and does not vary per configuration, so one fixed filename is
correct on multi-config generators too. The marker holds the name only, never a
path — the consumers already know their build directory and only ever got the
basename wrong.
A build tree without the marker still falls back to exe_basename, so older
trees and foreign build directories behave exactly as before.
The error messages were misleading in both directions and are now specific: a
name disagreement says the build succeeded and names both sides, while a
genuinely absent binary lists the executables that are present. The PGO
training path resolved the binary the same guessing way and now shares the
resolver.
Adds test_runtime_exe_name.py, which pins all four behaviours against the real
Revelations: Persona names, and relaxes the GAME_PROJECT_SETUP.md note that
previously just warned people to keep the two values in sync by hand.
Verified: the resolver against a real Ninja build tree whose OUTPUT_NAME and
marker were produced by the same two lines runtime.cmake uses, including the
exact stale-name case that failed in the wild; test_cli_project_packaging still
passes; the self-compiler change compiles clean. The runtime.cmake edit itself
was exercised in an isolated reproduction rather than a full runtime configure
— this checkout has no BIOS dump or generated game C to build one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017AwqKPQFnK3x7xQbK4PQKj
The cache was accepted forever once any cmake ran: default min version was
empty, so a Deck (or any host) that cached a pre-sysroot pack (<=1.0.13)
never re-downloaded and kept compiling against host headers — absent on
stock SteamOS — with objects that mis-link after a pack upgrade
(__isoc23_strtoul).
Both resolvers now default the floor to 1.0.14, the first pack with the
Linux build sysroot. This is a framework floor, not a per-title pin:
RETCOMM_TOOLCHAIN_MIN_VERSION still overrides in either direction, and
"0" / "off" / "none" disables it (pre-floor behavior).
- tools/toolchain_pack.py: DEFAULT_MIN_VERSION in default_min_version();
the ensure_toolchain stale-probe passes min_version="0" so its log line
still sees below-floor packs.
- host: toolchain_min_version() mirrors the same default and disable
tokens; below-floor caches now fail host_portable_cmake_ready and the
wizard downloads /releases/latest.
Verified: Python — floor/override/disable semantics, real v1.0.10 pack
rejected and v1.0.14 accepted, an isolated cache holding only v1.0.10 no
longer resolves (visible again with floor disabled), full cache resolves
to v1.0.14. C — same matrix via a unit harness incl. manifest-less dirs;
both platform branches syntax-checked.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vc7zPEnfUFMhMtJzUEruyb
(cherry picked from commit 00f0a9a9bb)
Stock SteamOS (Steam Deck) has no /usr/include and no GL dev symlinks, so
the wizard rebuild died at recomp-ui's find_package(OpenGL REQUIRED) with
"Could NOT find OpenGL (missing: OPENGL_INCLUDE_DIR)" — reported to the
user only as "psxrecomp rebuild failed (exit 1)" (recomp-ui issue #34).
The cmake-clang-v1 >=1.0.14 pack already bundles a jammy sysroot with GL
headers and a legacy libGL.so stub, but only clang.cfg used it; CMake's
find_* still searched the host.
- psxrecomp_cli: when the active pack carries sysroot/usr/include (Linux),
pass -DCMAKE_SYSROOT=<pack>/sysroot -DOpenGL_GL_PREFERENCE=LEGACY to both
the game and the emitters configure. LEGACY is required: the sysroot has
legacy libGL.so + GL/gl.h but no GLVND dev files (glx.h/libOpenGL.so/
libGLX.so), so the GLVND path cannot complete and OpenGL::GL would never
be created. Skipped when the caller already sets CMAKE_SYSROOT.
- codegen host: run_cli_posix/run_cli_win now route the child's stderr
into the progress pipe and keep a last-lines capture (CliTail); on
failure err_msg carries the most recent error-looking line — JSON
progress rows contribute their message, raw rows (compiler output,
Python tracebacks) contribute as-is. Exit-3 disc-verify text unchanged.
Verified against a stock-SteamOS simulation (bwrap: tmpfs over
/usr/include, GL dev symlinks removed) using the shipped v1.0.14 pack:
configure previously failed exactly as on the Deck; with the sysroot args
FindOpenGL resolves from the pack, the build links, and the binary runs.
CliTail paths exercised with real compact-JSON rows and raw stderr.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vc7zPEnfUFMhMtJzUEruyb
(cherry picked from commit 0bb45442f8)
Pass generated shard paths via a SOURCES_FILE so cmake -P is not killed by
CreateProcess's ~8191-char limit, and keep has_bios on for OpenBIOS-only
setup SDKs so the first-run wizard still offers SCPH1001.
Co-authored-by: Cursor <cursoragent@cursor.com>
Probe cmake --version; if the cache or latest pointer is unusable, clear
the bad pointer and stamp, then re-download (or install from zip) so
Generate & rebuild can recover without manual cleanup.
Co-authored-by: Cursor <cursoragent@cursor.com>
Install into versioned <tag>/ with latest as a pointer (not extract-into-
latest), honor RETCOMM_DATA_HOME/TOOLCHAIN_CACHE, and search both XDG and
~/.local/share so install.sh packs resolve during ensure/rebuild.
Co-authored-by: Cursor <cursoragent@cursor.com>
Codegen host hands g_wizard_bios (or OpenBIOS clear) to the CLI with
--disc/--bios. CLI skips SCPH1001 emit when the backend already exists.
Co-authored-by: Cursor <cursoragent@cursor.com>
Relative bios.cfg / settings picks like bios/SCPH1001.BIN broke after
rebuild when cwd was build/; launcher verify also ignored exe-anchored
resolve. Absolutize sidecars, hydrate from bios.cfg, and resolve in
ae_bios_verify.
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep wizard BIOS memory + sidecar writes to project root/build, and restore
NULL bios_path leave-untouched persist semantics from the sidecar PR.
Co-authored-by: Cursor <cursoragent@cursor.com>
Floor ensure-toolchain at 1.0.4 (libxml2 + clang.cfg -fuse-ld=lld) and
prepend pack lib/ to LD_LIBRARY_PATH when activating the toolchain.
Co-authored-by: Cursor <cursoragent@cursor.com>
Setup-host cwd and the rebuilt binary directory often differ on Windows;
resolve --bios from those sidecars and chdir to the project root before
exec on POSIX so relaunch sees the same picks.
Co-authored-by: Cursor <cursoragent@cursor.com>