Commit Graph

1723 Commits

Author SHA1 Message Date
Alexandros Mandravillis eb12cb2411 Fix zero-BIOS MSVC registry build 2026-08-31 09:16:14 +03:00
Matt Stanley 6003e63fbc Merge pull request #282 from Alexbeav/pr/cdrom-seek-semantics
runtime: correct CD-ROM seek and read handoff
2026-08-30 20:53:55 -07:00
Matthew Stanley 94b64b0b4b Merge master into CD-ROM seek semantics PR 2026-08-30 20:53:43 -07:00
Matt Stanley 59b9658c21 Merge pull request #279 from Alexbeav/codex/cdrom-timed-reinsert-upstream
cdrom: model timed tray cycle for reinsert
2026-08-30 20:52:24 -07:00
Matt Stanley ac9be0b6c9 Merge pull request #277 from Alexbeav/pr/gpu-dma2-slicing
runtime: slice GPU linked-list DMA
2026-08-30 20:52:21 -07:00
Matt Stanley 0ed754a3ce Merge pull request #273 from kerokline/fix/present-skip-unpaced-vsync-cadence
runtime: pace frames the GL renderer elides a swap for
2026-08-30 20:52:18 -07:00
Matt Stanley 6411d16920 Merge pull request #272 from Alexbeav/pr/gte-read-classification
recompiler: sync GTE read-helper classification with runtime accessor semantics
2026-08-30 20:52:14 -07:00
Matt Stanley 0cbd435349 Merge pull request #271 from Alexbeav/pr/stale-static-guards
codegen+runtime: stop executing stale static code when a game overlays its own text
2026-08-30 20:52:11 -07:00
Matt Stanley 90b02420b2 Merge pull request #274 from Alexbeav/pr/psx-exe-boot-fallback
tools: support SYSTEM.CNF-less discs via the BIOS PSX.EXE boot path
2026-08-30 20:43:33 -07:00
Matt Stanley cab6143ac0 Merge pull request #276 from Alexbeav/pr/toolchain-unpack-modes
tools: restore Unix modes when unpacking the toolchain zip
2026-08-30 20:43:30 -07:00
Matt Stanley ff012589c7 Merge pull request #275 from Alexbeav/pr/python39-tools-compat
tools: keep prepare_disc working on Python 3.9
2026-08-30 20:43:26 -07:00
Matt Stanley 68a2b3819a Merge pull request #270 from Alexbeav/pr/cosim-icount-checkpoints
cosim: key checkpoints on retired-instruction count, not guest cycles
2026-08-30 20:43:23 -07:00
Alexandros Mandravillis d43442bfe7 Clarify CD-ROM test fixture usage 2026-08-31 02:20:05 +03:00
Alexandros Mandravillis 12a3eb7d11 Complete CD-ROM reset and snapshot guards 2026-08-31 02:20:05 +03:00
Alexandros Mandravillis 25e7624a0d Harden CD-ROM seek state coverage 2026-08-31 02:20:05 +03:00
Alexandros Mandravillis 3b837f4e2d Fix CD-ROM seek and read handoff 2026-08-31 02:20:04 +03:00
Alexandros Mandravillis fb02451aab cdrom: model timed tray cycle for reinsert 2026-08-30 23:24:47 +03:00
Alexandros Mandravillis 8af48ae986 runtime: slice GPU linked-list DMA 2026-08-30 23:17:24 +03:00
Alexandros Mandravillis d795fef42a tools: restore Unix modes when unpacking the toolchain zip
zipfile.extractall() ignores the mode stored in ZipInfo.external_attr, so
on POSIX hosts every file in cmake-clang-v1 is extracted 0644. The pack is
then unusable and ensure-toolchain fails with:

  Toolchain cmake probe failed (.../cmake-clang-v1/1.0.14/bin/cmake):
  [Errno 13] Permission denied

Restore the recorded mode after extraction. Zips built on Windows record
no Unix mode, in which case the extracted default already applies, and the
whole block is skipped on nt.

Verified against a zip recording 0755: extractall alone yields 0644 and
os.access(X_OK) False; with this change 0755 and True.
2026-08-30 22:28:28 +03:00
Alexandros Mandravillis da9094c2a3 tools: keep prepare_disc importable on Python 3.9
Path.write_text(newline=) is Python 3.10+. RHEL/Rocky 9, Debian 11 and
Ubuntu 20.04 all ship 3.9 as the system python3, where staging died with
'TypeError: write_text() got an unexpected keyword argument newline'
after writing the .bin but before the .cue.

open(..., newline=) is behaviour-identical and available everywhere. No
other tool in the repo needs 3.10: the rest already rely on
'from __future__ import annotations'. Verified by running the real
prepare_disc under 3.9 end to end (digest check, PSX.EXE fallback,
staging, RESULT_CUE).
2026-08-30 22:28:28 +03:00
Alexandros Mandravillis 8268553aab tools: prepare_disc falls back to PSX.EXE boot when SYSTEM.CNF is absent
Mirrors the probe_disc.py fallback from 5ab7a053 into the staging step.
probe_disc.py accepts SYSTEM.CNF-less discs (King's Field, Dec 1994), but
prepare_disc.py still required the file, so 'generate' failed on any clean
worktree where disc/ was not already staged. Windows never hit this because
the staged boot EXE made psxrecomp_cli skip prepare_disc entirely.

Tools-only: runtime and recompiler sources are identical to 54fe30bf.
2026-08-30 22:28:22 +03:00
Alexandros Mandravillis cabe566d01 tools: probe falls back to PSX.EXE boot when SYSTEM.CNF is absent 2026-08-30 22:28:22 +03:00
Kevin a91884a4bb runtime: pace frames the GL renderer elides a swap for
The GL renderer may skip SDL_GL_SwapWindow when the displayed rect is
unchanged, so a double-buffered 30 Hz game does not swap an identical
front buffer on the intervening vblank. The frontend calls present once
per guest vblank, so eliding a swap also elides whatever that swap was
blocking on.

That is only safe while the wall-clock pacer owns cadence, because the
pacer runs per present regardless of what the renderer does. When driver
vsync owns cadence instead (~60 Hz panel, present_vsync_owns_cadence()),
the pacer is deliberately not run -- the two are XOR to avoid
double-blocking -- and the swap block becomes the only throttle. An
elided frame then blocks on nothing, and a game presenting every other
vblank advances two guest vblanks per block: exactly 2x real time.

Measured on BreathOfFire3Recomp (SLPS-00990, 60 Hz panel, OpenGL):
guest vblank counter sampled against wall clock ran 120.4 fps / 2.01x
before, 59.8 fps / 0.997x after. Swaps per second are unchanged at 60
either way -- the fix removes a free-running guest frame, not a swap --
and per-frame GPU cost is unchanged (present_gpu_ms_avg 12.2 -> 12.6,
scene_gpu_ms_avg 4.5 -> 4.1).

Gate both skip sites (VRAM and wide-FBO paths) on the cadence owner,
alongside the existing OSD and interpolation-cadence guards. Only the GL
renderer has this optimisation; the software and Vulkan paths always
present and were never affected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 14:20:09 -04:00
Alexandros Mandravillis 62dc9b9bb4 recompiler: sync GTE read-helper classification with runtime accessor semantics
The emitter's mfc2/cfc2 raw-vs-helper classification
(gte_register_classification.h) was out of sync with the runtime's
gte_read_data/gte_read_ctrl special cases: compiled code read OTZ(7)
and SZ0-3(16-19) unmasked and VZ/IR-class regs (1,3,5,8-11) without
sign extension, and cfc2 missed ctrl regs 4/12/20 — while the
interpreter always goes through the accessors. Nine data regs + three
ctrl regs of backend semantic divergence, locked in by a stale
static_assert mask.

data_read_needs_helper now covers every gte_read_data special case
(masks 0xB08F8FAA data / 0xEC101010 ctrl); regenerate titles to pick
it up.
2026-08-30 18:15:51 +03:00
Alexandros Mandravillis f07f801944 runtime: validate stale-static ranges in full — drop the exec_pc clip
dirty_ram_text_native_ok_ranges_from clipped validation to
[exec_pc, end), assuming a continuation never fetches bytes behind its
resume PC. False for any entry with a backward edge: a post-call
continuation re-enters mid-function and loops back into the clipped
region, executing stale static code. CMR2's overlay engine carries a
second copy of the vertex-transform kernel at 0x80016B34 that forked
exactly this way (r16 +0xA0 static vs +0x100 live, the same 0x60
delta as the first locus) after the inter-piece transfer guards
closed the fallthrough edge.

Validation now covers every emitted range of the entry regardless of
resume PC. A genuinely patched prologue blocks the whole entry to the
dirty-RAM interpreter -- correct, merely slower.
2026-08-30 18:15:23 +03:00
Alexandros Mandravillis 4d9060a54e codegen: stale-static guard on inter-piece host transfers
A game that overlays its own text at runtime (CMR2 streams transform-
loop variants with patched projection constants over the boot EXE)
kept executing the stale static translation through inter-piece host
transfers: dispatch validates an entry's emitted ranges, but split
function pieces chain via direct host calls (fallthrough to split
piece / next function, legacy non-CPS split branch/jump/continuation
transfers, non-CPS direct jal) with no revalidation. A clean entry
piece falls through natively into a stale sibling whose live bytes an
overlay replaced -- the CMR2 attract/gameplay wedge corruption
(first divergence: one ADDI immediate, screen-center 160 static vs
256 live, at 0x800199D4, retirement 3,203,596,289, frame ~11963).

Emit at every such transfer:
  if (!psx_game_text_native_ok(T)) { cpu->pc = T; return; }
publishing the PC and unwinding to the trampoline, whose dispatch
takes the sanctioned dirty-RAM-interpreter fallback over live bytes.
The non-CPS direct jal falls back to call_by_address instead.

dirty_ram_text_native_ok_ranges_from gains a page-clean fast path
(skip the memcmp when no touched page is guard-modified or runtime-
dirty) so the guards are near-free on pristine pages.

cosim.py: print full A/B cpu dumps in cpudiff mode (values, not just
diffs).
2026-08-30 18:14:32 +03:00
Alexandros Mandravillis 4aebb349da cosim: key checkpoints on retired-instruction count, not guest cycles
The cycle-keyed design hashed+parked at the first cosim_instr boundary
AFTER the stride cycle was crossed; the two backends can reach that
crossing at different retirement positions, so the oracle compared
different architectural states (observed as a +5-cycle park skew and
phantom CPU-register divergences mid-CPS).

Checkpoints are now taken inside cosim_instr, keyed on the retirement
count itself (a branch and its delay slot retire as one event in both
backends). Checkpoint k = state after k*stride retirements in BOTH
instances by construction. The guest-cycle clock stays inside the state
hash, so real cycle-accounting drift is still detected -- now pinned to
an exact instruction window instead of corrupting the alignment.

cosim.py: stride/--max/--start-cycle now in retirement units; the cycle
skew warning is now reported as a real timing divergence. launch() gives
instance B its own exe (the mods root is exe-dir-relative, so a shared
exe races on mods/state.toml publish regardless of cwd separation) and
refuses a stale COSIM_CWD_B exe.

Gates on CMR2 (SLUS-01222): compiled/compiled and interp/interp both
clean over 100M retirements with identical cross-pair chains; injected
1-bit RAM fault detected at the next checkpoint.
2026-08-30 18:14:24 +03:00
Matthew Stanley 47bda8172e runtime: fix savestates at dirty boundaries 2026-08-29 22:08:31 -07:00
Matt Stanley 09ec090d88 Merge pull request #259 from mstan/feat/multi-disc-launcher
Multi-disc: per-disc identity, launcher roster, and stop copying dumps into projects
2026-08-29 20:04:40 -07:00
Alex Vanderveen a44fb12575 runtime: tag savestates with the disc on a multi-image title
A savestate is whole-machine state. One taken on disc 2 and restored while
disc 1 is mounted resumes a guest that still believes it is reading disc 2,
and the slot list gives no hint: every disc of a set shares one entry_pc, and
entry_pc is the key the slot filenames already use. So the same slot number
meant a different machine on every disc, silently.

Savestates were already isolated per BIOS, as a directory. The disc joins that
as a FILENAME token inside it:

  <saves>/<bios>/state_<entry>_disc<N>_slot<NN>.pst

The BIOS stays a directory because a state is invalid across BIOS images
whatever disc it came from. The disc only has to not collide within one, and a
token does that without another level to browse, back up and migrate.

An empty token leaves the historical name byte-for-byte, so single-disc titles
keep every existing state -- the scope is set only when the roster has more
than one disc.

States saved before this predate the naming and are not listed. They are left
alone rather than migrated: nothing in a .pst says which disc it was taken on,
so placing them would be a guess, and guessing wrong reintroduces exactly the
mix-up this prevents. Startup names them and the directory so they can be
renamed deliberately.

Verified: slot 1 saved on disc 2 and on disc 3 produced disc2/disc3 files side
by side with no collision; booting disc 1 and loading slot 1 rejected with
"missing state_800110C0_disc1_slot01.pst" rather than restoring another disc's
machine; the pre-existing untagged slot 0 was untouched throughout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194TYJAqgyG9a9PJe9uUcgQ
2026-08-29 21:50:00 -04:00
Matt Stanley f24b7e5d19 Merge pull request #266 from TheRealBiggs/fix/pal-timing
Fix PAL timing
2026-08-29 18:40:33 -07:00
Alex Vanderveen 1102d42022 runtime: [disc] selected is authoritative only when it is present
Launching from an external frontend with disc 2 or 3 chosen opened the game
on disc 1 every time, and the launcher preselected disc 1 with it.

settings.toml carried the right answer:

  [disc]
  path = ".../Final Fantasy VII (USA) (Disc 2).cue"

with no `selected` key. The frontend writes the index only when it can work
out the roster position, and it compares its stored choice path to the disc
path exactly -- a .cue/.bin or normalisation difference misses, so it writes
`path` alone and leaves the index out.

The resolver then treated a MISSING key as selected = 1 and did what it was
designed to do for a stale path: overrode disc 2 back to disc 1. `path` alone
selected a disc correctly before the index existed, so this was a regression
introduced with the index, not a pre-existing gap. A new field may add a way
to choose a disc; it must not take away the old one.

Now the index decides only when it is present. Absent it, the path decides and
the index is derived from it (stem-compared, because everything downstream has
been through normalize_disc_path_for_launch, which swaps a .cue for its .bin
-- exactly the drift that made the frontend's own match miss). An off-roster
path is not evidence for any disc and leaves the default alone.

Verified all three ways against the real install's state: path=disc2 with no
index boots SCUS-94164 and the launcher preselects Disc 2; path=disc2 with
selected=3 still boots SCUS-94165, so an explicit index keeps overriding a
stale path; the roster-derived index keeps a later save writing a consistent
pair.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194TYJAqgyG9a9PJe9uUcgQ
2026-08-29 21:39:21 -04:00
Alex Vanderveen 72b17fd095 ci: carry the mod-catalog gate in the template, and detect stale workflows
Two gaps, one cause: a title installs the CI workflow once and then never
learns the template moved on.

- docs/ci/templates/setup-release.yml gains the release gate that asserts the
  zip actually ships a mod catalog, carries no per-machine state.toml, and
  carries no developer-channel package. It existed only in one title's
  workflow, so every project generated before now would publish an empty Mods
  page with a green build -- the exact failure the gate was written for.

- op_emit_ci_workflow stopped at "release.yml already filled" and reported OK.
  Filled is not the same as current. It now compares STEP NAMES against the
  template and reports which are missing, so Studio's migration tab surfaces
  the drift instead of silently blessing it.

Step names rather than a text diff, deliberately: a filled workflow always
differs from the template (tokens, and titles customise theirs), so a textual
comparison would report stale on every project forever and be ignored within a
week. A step the template defines and the installed file does not is
unambiguous. Names containing a token are skipped -- the installed copy has
them substituted, so they are evidence of nothing.

It reports rather than rewrites: re-emitting overwrites a customised workflow
wholesale, so that stays behind --force.

Checked for a version-stamp gap while here and found none: both the template
and the installed workflow already configure with
-DPSX_GAME_VERSION="${RELEASE_VERSION}" and verify the lobby pin against it,
and the CI cache covers only build-recompiler, never the runtime. The
mismatch that prompted the look was a stale LOCAL build directory, not CI.

Verified both directions: the current workflow reports "already filled and
current"; the same workflow with the gate stripped reports the missing step
by name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194TYJAqgyG9a9PJe9uUcgQ
2026-08-29 21:21:56 -04:00
Alex Vanderveen 86e9c40b34 package: stage assets/ so a rebuilt zip can configure and keeps its icon
The CMakeLists gate added in 6ec5e29b caught a real latent break: every
generated project passes APP_ICON "${CMAKE_CURRENT_SOURCE_DIR}/assets/
psxrecomp.ico" unguarded, but no wrapper staged assets/. The zip built fine
and would have died on the player's machine at cmake configure with "Cannot
find source file" -- exactly the failure that gate exists to move forward in
time. It fired on the FF7 release run.

Staging assets/ also keeps the rebuilt executable branded: runtime.cmake takes
the SDL window icon from the sibling psxrecomp.png in the same directory, so
without it a successful rebuild would still come out unbranded.

Conditional, matching the src/ and mods/ blocks above it, so it stays inert
for a project that has no assets/ of its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194TYJAqgyG9a9PJe9uUcgQ
2026-08-29 21:16:54 -04:00
Alex Vanderveen 29cba084fc package: keep src/ staging conditional after the merge
Both merged branches added src/ to the packaging template: feat/setup-host-
multi-disc as an unconditional --project-dir in the static argument list, and
fix/package-src-mods as a guarded EXTRA_PROJECT entry. git merged them
cleanly because they touch different lines, leaving src/ passed twice.

The duplicate copy is harmless, but the unconditional entry is not:
copy_proj() hard-fails on a missing path, so a scaffolded project without
src/ would stop packaging with "error: missing src". Every single-disc title
that has never added game-owned C is in that position.

Drops the static entry and keeps the guarded one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L1TpXRJve6WvwyZbSJAbCL
2026-08-29 21:09:27 -04:00
Alex Vanderveen 6ebb14e292 Merge feat/setup-host-multi-disc into feat/multi-disc-launcher 2026-08-29 21:08:16 -04:00
Alex Vanderveen 081bb79084 Merge fix/package-src-mods (#267) into feat/multi-disc-launcher
Brings the packaging gates and Project Studio audit for allowlist drift:

  - template stages src/, mods/ and the generated symbol headers
  - package_setup_host.sh refuses to zip a stage whose CMakeLists.txt names
    an unstaged path, or whose project sources include one
  - Studio Migrate gains packager_stages_sources / packager_stages_optional
    and op_sync_packager_project_dirs

Both branches touch tools/package_setup_host.sh and the packaging template,
so this is merged here rather than left to race on master.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L1TpXRJve6WvwyZbSJAbCL
2026-08-29 21:08:07 -04:00
Alex Vanderveen 8f3645c2bc Merge pull request #268 from mstan/master
upstream
2026-08-29 21:07:42 -04:00
Alex Vanderveen 61d23ba777 package: stage the symbol headers, and gate on what sources include
FF7 v0.1.3 shipped src/ff7_mods.c (the previous commit's fix) and then died
at the first object file:

  src/ff7_mods.c:20:10: fatal error: '../psx_symbols.h' file not found

psx_symbols.h is a tracked 82 KB header that game-owned C in src/ includes by
relative path. Nothing in CMakeLists.txt names it, so the CMakeLists gate
added in the previous commit could not see it — it is a compile input of the
shipped tree that only the source knows about.

Two changes:

1. The template stages psx_symbols.h and psx_symbols_overlays.h when present.

2. package_setup_host.sh gains a second pre-zip gate: every relative #include
   in the project's own staged sources must resolve inside the stage. Only
   project-owned paths (PROJECT_FILES / PROJECT_DIRS) are scanned — framework
   trees resolve their headers through -I and are not ours to police.

The include check accepts three resolutions: relative to the including file,
relative to the stage root, and finally any file of that basename anywhere in
the stage. That last one covers -I resolution out of the staged framework
trees (a bare "mod_plugins.h" comes from psxrecomp/runtime/include). It is
deliberately loose: a false pass only forfeits a warning, while a false
failure would block a good release.

Verified against the real v0.1.3 tree: the stage as shipped fails naming
exactly ../psx_symbols.h, and passes once the header is staged. mod_plugins.h
was a false positive in a minimal stage and is correctly cleared once the
framework tree is present.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L1TpXRJve6WvwyZbSJAbCL
2026-08-29 20:57:09 -04:00
Alex Vanderveen 0ffab10ddf studio: audit + fix packaging allowlist drift in the Migrate tab
The setup_host_packager check only asked whether scripts/package_setup_
release.sh exists. Final Fantasy VII had one, passed, and still shipped a
zip without src/ff7_mods.c — a file its own CMakeLists.txt compiles — so
the install died at cmake configure on the user's machine. op_emit_packager
could not have helped either: it early-returns when the file is present.

Two new checks in audit_project, both driven by the same rule the packager's
pre-zip gate uses, so the two can never disagree:

- packager_stages_sources (FAIL): a path CMakeLists.txt requires that the
  allowlist omits. This is the build-breaking case.
- packager_stages_optional (WARN): a path present in the repo and used under
  if(EXISTS ...). cmake skips the block, so the build passes and the feature
  ships dead — mods/preloaded/packages is exactly this: the activation
  plugin is compiled in and has no package to select.

op_sync_packager_project_dirs patches the existing wrapper rather than
regenerating it, because packagers get hand-extended with extra
--project-file lines that a template rewrite would drop.

The rule is general, not an src/mods special case: a fleet audit also
surfaced tests/ and tools/ on Crash-Team-Racing-Recomp and src/+mods/ on
EmeraldRecomp and Final-Fantasy-7.

The GUI iterates report.checks, so both appear in Migrate with no UI change.

Verified against Final-Fantasy-VII at origin/master: pristine audit reports
1 fail + 1 warn; one apply stages mods and src; re-audit is clean; a second
apply is a no-op; the result is valid shell. The idempotency check caught a
real parser bug — packager_staged_paths captured the closing paren of
EXTRA_PROJECT+=(--project-dir mods), so applied fixes read back as unstaged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L1TpXRJve6WvwyZbSJAbCL
2026-08-29 20:38:01 -04:00
Alex Vanderveen 6ec5e29b97 package: stage src/ and mods/, and gate on CMakeLists references
Final Fantasy VII shipped a release zip whose CMakeLists.txt compiles
src/ff7_mods.c, but the zip had no src/ at all. Installing it died at
configure with "Cannot find source file" and "No SOURCES given to target:
psx-runtime". The commit that added the widescreen mod plugin updated
CMakeLists.txt but not the packaging allowlist, and nothing checked.

The allowlist in the generated scripts/package_setup_release.sh is a closed
list that has to be kept in sync with CMakeLists.txt by hand, so this is a
class problem, not a one-off: of 23 game repos with a packaging script, all
23 carry mods/ and only 3 stage it; 7 carry src/ and only 2 stage it. Those
3 are hand-patched copies of this same fix, which is the tell.

Two changes:

1. templates/package_setup_release.sh.in stages src/ and mods/ when the
   project has them, using the existing EXTRA_PROJECT idiom (copy_proj hard
   -fails on a missing path, so they cannot be added unconditionally).
   src/ holds game-owned C that CMakeLists compiles in; mods/preloaded/
   packages holds the markers those plugins select themselves through, so
   without it a mod is built and never enabled.

2. package_setup_host.sh now refuses to zip a stage that cannot configure
   itself: every ${CMAKE_CURRENT_SOURCE_DIR}/<rel> the staged CMakeLists.txt
   names must exist in the stage. Paths used only inside if(EXISTS "...")
   are optional by construction and are skipped, as are globs and paths
   built from cmake variables. This turns a silent omission that surfaces on
   a user's machine into a release-time failure.

Verified against the real FF7 CMakeLists: the stage as shipped fails naming
exactly src/ff7_mods.c; with src/ staged it passes; the guarded
mods/preloaded/packages reference correctly does not fail a project without
mods/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L1TpXRJve6WvwyZbSJAbCL
2026-08-29 20:38:01 -04:00
Alex Vanderveen 60203cc160 Merge feat/host-pacing-and-hotkey-focus into feat/multi-disc-launcher
Folds the two runtime fixes that were also open as their own PR (#260,
fix/host-frame-pacing) into this branch:

  - runtime: don't fast-forward while the window lacks input focus
  - runtime: pace the frame even when driver vsync should own the cadence

Both branches already shared this branch's base (d14ca691, c481b6cd,
10f443eb), so the merge adds only those two. They are patch-identical to
#260's commits (verified by git patch-id), so #260 is superseded rather
than dropped and can be closed without losing work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L1TpXRJve6WvwyZbSJAbCL
2026-08-29 20:30:39 -04:00
Alex Vanderveen d6529481d4 packaging: a developer channel, kept out of published releases
Mods under active development had nowhere to live: a package was either in
the catalog and therefore in every release, or not built at all. Widescreen is
the immediate case -- it engages, but it is unvalidated as gameplay and FF7's
pre-rendered field backdrops still cull at the 4:3 bound, so it is not
something to put in front of players yet.

A package now declares its own maturity with channel = "developer". It still
ships with ordinary local builds and local exports, so it can be tested
exactly as a player would see it, and release packaging drops it.

The exclusion defaults from $CI -- on under any CI provider, off locally.
That is deliberate: a per-workflow opt-in is precisely the shape that left 21
of 23 ports shipping no mods at all, so this must not need 23 release
workflows edited to take effect. --exclude-dev-mods / --include-dev-mods and
EXCLUDE_DEV_MODS=0|1 override in either direction, and Studio's
`build export --exclude-dev-mods` reproduces what a release contains.

Applied in all three packagers (setup-host, Windows, macOS). Each prunes the
whole PACKAGE directory and then asserts no developer manifest survived --
including the SOURCE catalog under mods/preloaded, since the setup-host zip
rebuilds on the player's machine and would otherwise re-stage the package the
release just removed.

A manifest is never rewritten during packaging: editing a package while
packaging it would change content meant to be verifiable, which is why the
marker is package-level rather than per-feature.

Verified by packaging: local keeps 5 catalogs, CI=true drops to 4 with no
ff7.enhancements entry anywhere in the zip, and EXCLUDE_DEV_MODS=0 under
CI=true restores it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194TYJAqgyG9a9PJe9uUcgQ
2026-08-29 20:25:08 -04:00
Alex Vanderveen d2aeac70a5 packaging: a developer channel, kept out of published releases
Mods under active development had nowhere to live: a package was either in
the catalog and therefore in every release, or not built at all. Widescreen is
the immediate case -- it engages, but it is unvalidated as gameplay and FF7's
pre-rendered field backdrops still cull at the 4:3 bound, so it is not
something to put in front of players yet.

A package now declares its own maturity with channel = "developer". It still
ships with ordinary local builds and local exports, so it can be tested
exactly as a player would see it, and release packaging drops it.

The exclusion defaults from $CI -- on under any CI provider, off locally.
That is deliberate: a per-workflow opt-in is precisely the shape that left 21
of 23 ports shipping no mods at all, so this must not need 23 release
workflows edited to take effect. --exclude-dev-mods / --include-dev-mods and
EXCLUDE_DEV_MODS=0|1 override in either direction, and Studio's
`build export --exclude-dev-mods` reproduces what a release contains.

Applied in all three packagers (setup-host, Windows, macOS). Each prunes the
whole PACKAGE directory and then asserts no developer manifest survived --
including the SOURCE catalog under mods/preloaded, since the setup-host zip
rebuilds on the player's machine and would otherwise re-stage the package the
release just removed.

A manifest is never rewritten during packaging: editing a package while
packaging it would change content meant to be verifiable, which is why the
marker is package-level rather than per-feature.

Verified by packaging: local keeps 5 catalogs, CI=true drops to 4 with no
ff7.enhancements entry anywhere in the zip, and EXCLUDE_DEV_MODS=0 under
CI=true restores it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194TYJAqgyG9a9PJe9uUcgQ
2026-08-29 20:24:39 -04:00
Matthew Stanley 3d1cb5013f runtime: keep PAL cadence off 60 Hz host sync 2026-08-29 17:23:52 -07:00
Alex Vanderveen 647b233015 packaging: ship the mod catalog in every release, and let Studio bundle one
A built title stages its mod catalog next to the exe, but nothing put it in
the shipped package. Every release went out with an empty Mods page.

The packager already had --runtime-dir mods; it was opt-in and RUNTIME_DIRS
defaulted to empty, so each title had to pass it from its own generated
wrapper. An audit of the workspace found 21 of 23 ports never did, and the
two that did used the -optional variant that warns instead of failing. A
capability that exists, defaults off, and fails silently.

- package_setup_host.sh stages mods/ by DEFAULT and REQUIRES it. runtime.cmake
  guarantees <exe>/mods for every non-oracle target, so a missing one means
  broken build wiring and should fail the release rather than quietly ship an
  empty catalog. --no-mods opts out a title with none; an explicit
  --runtime-dir-optional mods still wins. It also ships the SOURCE catalog,
  because the setup-host zip rebuilds on the player's machine and that rebuild
  re-runs POST_BUILD staging from the source tree -- without it a rebuilt exe
  silently drops every game-owned mod and keeps only framework builtins.

- package_release.ps1 (Windows/MinGW) had no mod handling at all. It now
  stages the catalog before the stray-file scan, so the existing gate covers
  it, strips the build machine's mods/state.toml, and asserts at least one
  manifest survived -- matching the file's own assert-rather-than-trust idiom.

- package_release_macos.sh shipped ONE package where the player should have
  seen five. It took the catalog from mods/preloaded "never the build tree",
  but framework builtins (PGXP, fast loading, CD speed, bezel) live in
  psxrecomp/mods/builtin and only ever appear next to the exe. The real hazard
  was never the build tree, it was state.toml inside it -- so take the build
  tree and delete that one file. --mods-src still overrides.

- The wrapper template gains --project-dir src so future titles ship their
  game-owned trusted-plugin sources; CMakeLists.txt references them
  unconditionally, so the setup-host rebuild cannot configure without them.

Studio could build a title but never bundle one: the only route to a
distributable was install-CI, commit, push, wait. That is how an empty Mods
page survived unnoticed -- nobody produced a package locally to look inside.
buildops.export_release() runs the SAME wrapper CI runs, so a local zip and a
released one are the same artifact by construction; exposed as
`build export` on the CLI and an "Export release zip" button in the GUI.
Artifact tags default to the host and match the CI matrix.

Verified by packaging, not by reading: default produces 5 catalogs with no
state.toml; --no-mods reproduces the old behaviour at 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194TYJAqgyG9a9PJe9uUcgQ
2026-08-29 20:11:32 -04:00
Alex Vanderveen 82589d669a packaging: ship the mod catalog in every release, and let Studio bundle one
A built title stages its mod catalog next to the exe, but nothing put it in
the shipped package. Every release went out with an empty Mods page.

The packager already had --runtime-dir mods; it was opt-in and RUNTIME_DIRS
defaulted to empty, so each title had to pass it from its own generated
wrapper. An audit of the workspace found 21 of 23 ports never did, and the
two that did used the -optional variant that warns instead of failing. A
capability that exists, defaults off, and fails silently.

- package_setup_host.sh stages mods/ by DEFAULT and REQUIRES it. runtime.cmake
  guarantees <exe>/mods for every non-oracle target, so a missing one means
  broken build wiring and should fail the release rather than quietly ship an
  empty catalog. --no-mods opts out a title with none; an explicit
  --runtime-dir-optional mods still wins. It also ships the SOURCE catalog,
  because the setup-host zip rebuilds on the player's machine and that rebuild
  re-runs POST_BUILD staging from the source tree -- without it a rebuilt exe
  silently drops every game-owned mod and keeps only framework builtins.

- package_release.ps1 (Windows/MinGW) had no mod handling at all. It now
  stages the catalog before the stray-file scan, so the existing gate covers
  it, strips the build machine's mods/state.toml, and asserts at least one
  manifest survived -- matching the file's own assert-rather-than-trust idiom.

- package_release_macos.sh shipped ONE package where the player should have
  seen five. It took the catalog from mods/preloaded "never the build tree",
  but framework builtins (PGXP, fast loading, CD speed, bezel) live in
  psxrecomp/mods/builtin and only ever appear next to the exe. The real hazard
  was never the build tree, it was state.toml inside it -- so take the build
  tree and delete that one file. --mods-src still overrides.

- The wrapper template gains --project-dir src so future titles ship their
  game-owned trusted-plugin sources; CMakeLists.txt references them
  unconditionally, so the setup-host rebuild cannot configure without them.

Studio could build a title but never bundle one: the only route to a
distributable was install-CI, commit, push, wait. That is how an empty Mods
page survived unnoticed -- nobody produced a package locally to look inside.
buildops.export_release() runs the SAME wrapper CI runs, so a local zip and a
released one are the same artifact by construction; exposed as
`build export` on the CLI and an "Export release zip" button in the GUI.
Artifact tags default to the host and match the CI matrix.

Verified by packaging, not by reading: default produces 5 catalogs with no
state.toml; --no-mods reproduces the old behaviour at 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194TYJAqgyG9a9PJe9uUcgQ
2026-08-29 20:10:44 -04:00
Alex Vanderveen d0b9547365 setup host fix 2026-08-29 19:41:55 -04:00
Alex Vanderveen 022dbc640e setup: keep disc probe artifacts out of the project tree
update_disc_set.py wrote disc_probe*.json and disc_set.json beside
game.toml. Those are inputs to verify_disc_set, not project content --
and disc_probe*.json are COMMITTED provenance records of the original
scaffold, so every wizard Generate modified three tracked files in the
game repo. Caught by a dirty `git status` after a test run, which also
showed the rewrite downgrading a curated display_name back to the raw
file name.

They now go to a TemporaryDirectory and are discarded; --artifact-dir
keeps them when a caller actually wants them. Re-verified: still an exact
no-op against Final Fantasy VII's correct 3-disc game.toml, and it leaves
nothing behind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWiDNcWuNtDWHE7Pnhq4jp
2026-08-29 19:38:24 -04:00
Alex Vanderveen 617f7219e4 setup: keep disc probe artifacts out of the project tree
update_disc_set.py wrote disc_probe*.json and disc_set.json beside
game.toml. Those are inputs to verify_disc_set, not project content --
and disc_probe*.json are COMMITTED provenance records of the original
scaffold, so every wizard Generate modified three tracked files in the
game repo. Caught by a dirty `git status` after a test run, which also
showed the rewrite downgrading a curated display_name back to the raw
file name.

They now go to a TemporaryDirectory and are discarded; --artifact-dir
keeps them when a caller actually wants them. Re-verified: still an exact
no-op against Final Fantasy VII's correct 3-disc game.toml, and it leaves
nothing behind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWiDNcWuNtDWHE7Pnhq4jp
2026-08-29 19:38:13 -04:00