Commit Graph

357 Commits

Author SHA1 Message Date
Matt Stanley 1d414fc7b1 Merge pull request #314 from mstan/chore/repo-cleanup
chore: repo hygiene — purge dead scratch tooling, relocate root docs/fixtures/BIOS profiles
2026-09-03 11:30:32 -07:00
Matt Stanley 71f3c8c360 Merge pull request #304 from Alexbeav/pr/gpu-dma2-correctness-ff7
dma: preserve live linked-list reads with word-cost timing
2026-09-03 11:24:02 -07:00
Matthew Stanley e6ce5006b4 docs: move root and stale docs into docs/ and docs/internal/
The repository root carried seven markdown files that were not onboarding
material, plus two that had been superseded by the code they described. This
sorts them: reader-facing docs to docs/, working notes to docs/internal/, and
the two superseded ones merged into the docs that replaced them.

Moved to docs/:            WIDESCREEN.md, ENHANCEMENTS.md, TCP_COMMANDS.md
Moved to docs/internal/:   PRINCIPLES.md, ISSUES.md, DEBUG.md, GTE_LM_FIX.md,
                           and from docs/: bios-hardening.md, overlay-status.md,
                           overlay-plan.md, overlay-recompilation-design.md,
                           overlay-discovery.md, TOMBA2_EXC_CONSULT_RESPONSE.md,
                           CASE_A_AOT_GAP.md, audit_inventory.md
Moved to docs/internal/upstream/: UPSTREAM_2026-08-20.md

The root keeps only what belongs there: README.md, CONTRIBUTING.md, LICENSE,
CLAUDE.md, THIRD_PARTY_ATTRIBUTION.md, RELEASE_NOTES.md, the dotfiles and
psxrecomp_cli.py.

MERGES

SPLITGEN_MIGRATION.md folded into docs/BUILDING.md as "Split generated C". Its
migration instructions were stale in a way that mattered: it told every game
repo to hand-edit its CMakeLists and add a file(GLOB ...) over
<serial>_full*.c, but runtime/runtime.cmake now does that itself. GEN_FULL_GLOB
is a multi-value argument of psxrecomp_add_game_runtime(); the loop at
runtime.cmake:1985 makes each pattern absolute against CMAKE_CURRENT_SOURCE_DIR,
expands it with file(GLOB), and concatenates the hits into
GAME_GENERATED_FULL_C. So a title names the pattern and gets however many shards
a regeneration produced. The rewritten section describes that, the two-step
fallback when the globs match nothing (GEN_FULL_FALLBACK, else <serial>_full.c
derived from the dispatch marker by substituting _dispatch.c), and the one
caveat the code actually has: that file(GLOB) is plain, not CONFIGURE_DEPENDS,
so a regeneration which changes the shard COUNT needs a re-configure to be seen.

psxrecomp-toml-readme.md folded into docs/GAME_PROJECT_SETUP.md as "EXE-only
autofill (psxrecomp-toml)", placed after "Disc autofill (probe_disc.py)" and
before "Project Studio" so the two autofill paths sit together. Its documented
flags were checked against the usage text in recompiler/src/main_toml.cpp:28-42
and against the argument parser at :150-165 -- all seven options match with no
drift, so the flag list is reproduced as-is. Added a pointer that probe_disc.py
is preferable when a cue exists, since that tool does the same EXE-header work
plus disc identity and netplay gates.

FUNCTIONAL FIXES

tools/gen_tcp_commands.py wrote to REPO/"TCP_COMMANDS.md". After the move that
path does not exist, so the generator would have created a new stray root file
and --check would have failed against it forever. Retargeted to
REPO/"docs"/"TCP_COMMANDS.md", along with the seven docstring and message
strings that name the path.

Running it surfaced two further problems, both fixed here:

  * The doc was genuinely stale -- pre-existing drift, not introduced by this
    commit (the moved file is byte-identical to origin/master, and no
    debug-server command table is touched here). Four commands registered since
    the last regeneration were missing from the index -- cdrom_timing_dump,
    savestate_status, ws_ui_groups, xprobe_watch -- and scanline had gained a
    backend. Regenerated: 307 -> 311 commands, and the change is confined to the
    AUTOGEN block (the hand-written head and tail are byte-identical to master).
    tools/gen_tcp_commands.py --check now exits 0.
  * DOC.write_text() has no newline argument, so Python translated every newline
    to os.linesep. On Windows that rewrote all 585 lines of the file to CRLF and
    turned a four-line index update into a whole-file diff -- measured: 589 CRLF
    / 0 LF after a run, against 0 CRLF / 585 LF in the repo. Now written through
    an explicit newline="" handle, verified to leave the file at 0 CRLF.

runtime/include/debug_server.h:9 and runtime/src/debug_server.c:8 cited "TCP.md
and DEBUG.md". TCP.md has never existed in this repo under that name; both now
name docs/TCP_COMMANDS.md and docs/internal/DEBUG.md.

LINK AND REFERENCE FIXES

Every inbound reference to a moved file was located by grepping each basename
across the whole tracked tree, not just the markdown. Markdown hrefs in
README.md, CONTRIBUTING.md, docs/ARCHITECTURE.md, docs/COMPILING_OVERLAYS.md,
docs/AOT_OVERLAY_PLAN.md, docs/RELOCATION_MANIFEST_FORMAT.md,
docs/config_schema.md, docs/OVERLAY_XREF.md, docs/TCP_COMMANDS.md and several
docs/internal/ files now point at the new locations; where two files became
siblings under docs/internal/ the path was shortened rather than lengthened
(bios-hardening.md's reference to overlay-discovery.md, for instance).
docs/COMPILING_OVERLAYS.md's "in this folder" phrasing was wrong once two of the
three files it listed moved, so it now names internal/ explicitly.

24 source-comment mentions of ENHANCEMENTS.md sections G1.x across 19 files
(recompiler, runtime, cmake) were retargeted to docs/ENHANCEMENTS.md with the
section numbers untouched. docs/SHADOW_ENHANCEMENTS.md is a DIFFERENT file and
was deliberately excluded.

20 rationale strings citing docs/CASE_A_AOT_GAP.md across the four
recompiler/seeds/*.json files were updated. Done as a byte-level substring
replacement rather than a JSON round-trip: two of those files are stored with
CRLF endings and two with LF, and re-serializing them reformatted 5,700 lines.
The byte-level version changes exactly 20 lines and each file still parses.

References that look similar but point elsewhere were deliberately left alone,
and are worth naming so a future pass does not "fix" them:

  * "recomp-template PRINCIPLES.md" / "NES recomp template PRINCIPLES.md" in
    CLAUDE.md:43, docs/SHADOW_ENHANCEMENTS.md:28,
    docs/internal/HLE_SCHEDULER_CARVEOUT_PLAN.md:5,
    runtime/src/debug_server.c:4934 and tools/parity_diff.py:8 are a different
    repository's file. Only the five that mean this repo's PRINCIPLES.md were
    updated.
  * Every remaining bare ISSUES.md mention is a GAME repo's tracker
    (MegaManX6Recomp 2/7/8/9, TombaRecomp 6), not this repo's.
  * docs/disc-speed.md:4,54,56 and docs/internal/bios-hardening.md:5 name
    overlay-discovery as a BRANCH, not a file, so no path applies.

INDEXES

README.md's "New here?" table gains rows for docs/WIDESCREEN.md,
docs/ENHANCEMENTS.md and docs/TCP_COMMANDS.md. docs/internal/README.md gains
sections indexing all fourteen documents that moved in, plus an "Upstream
contributor reviews" section describing upstream/ and the new sweep file, plus a
"Removed" bullet for CYCLE_TIMING_ARCH.md (deleted in the previous commit)
pointing at FAITHFUL_TIMING_PLAN.md, CYCLE_MODEL_BEETLE.md and
PRECISE_IRQ_SLICE.md -- following the SLJIT_PERSIST_CACHE.md precedent already
in that file.

docs/ENHANCEMENTS.md:7 claimed framework bugs live in "game repos' ISSUES.md
until a framework tracker exists". A framework tracker does exist; it now says
so and names the Beads epic.

VERIFICATION

A link checker was written to scan every tracked *.md for relative links,
resolving each against the tree being scanned. Baseline at origin/master: 129
files, 137 relative links, 3 broken. After both commits: 127 files, 159 relative
links, 3 broken -- the SAME three, all pre-existing and untouched here:

    docs/DATA_SHARDS.md -> INSTANT_LOADS_PARKED.md   (never existed)
    docs/DATA_SHARDS.md -> INSTANT_LOADS_SPIKE2.md   (never existed)
    docs/NETPLAY.md     -> ../lib/recomp-net/README.md  (inside a submodule)

Zero new broken links. The checker's own coverage was checked against a raw grep
for markdown link openers: the 3-link difference is C++ lambdas inside code
blocks, correctly not treated as links.

Also verified: py_compile over all 113 tracked tools/*.py plus psxrecomp_cli.py;
all four seed JSONs still parse; the test-registration guard still reports 123
test files all registered; recompiler/tests/test_coverage_vault.py still passes;
and no file in this commit had its line endings flipped (checked mechanically
across all 50 modified files, after two files were caught and redone).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 07:39:07 -07:00
Matthew Stanley f55c731c5f chore: remove dead scratch tooling and stale root files
Removes 204 tools/ scripts (203 .py/.sh plus tools/_missing_funcs.json) that
no tracked file references. Verified mechanically, not by eye: for each of the
204 stems, a word-boundary grep across every tracked text file in the repo,
excluding files that are themselves being deleted, returns zero hits. So
nothing kept imports, execs, or documents any of them. Most are the
underscore-prefixed one-shot probes the .gitignore convention already treats
as scratch (tools/_*.py is ignored; these predate that rule and were force-
added), plus a long tail of session-specific dump/decode/poll scripts with
hardcoded local paths and addresses.

Also removed at the root:

  * card2.mcd.bak.before_beetle_copy -- a stray backup of a memcard.
  * .mcp.json -- local MCP server wiring, not repo configuration.
  * SCPH1001.toml -- a root duplicate of bios/SCPH1001.toml, still on the
    pre-refactor [[recompiler.bios_vectors]] schema while the bios/ copy has
    moved to [recompiler.address_model]. Every reference in the tree already
    names bios/SCPH1001.toml, so the root copy was a stale trap: editing it
    would have had no effect on any build.
  * docs/CYCLE_TIMING_ARCH.md -- superseded by
    docs/internal/FAITHFUL_TIMING_PLAN.md, CYCLE_MODEL_BEETLE.md and
    PRECISE_IRQ_SLICE.md; nothing linked to it.

Memcard fixtures move out of the repo root into
runtime/tests/fixtures/memcards/ with a README recording what each of the
four images actually contains (directory entries read out of the images, not
guessed). Nothing in the build read them from a fixed path -- the four
hardcoded consumers (tools/check_card.py, inspect_card.py, peek_cards.py,
verify_card_buffer.py) are all in the deletion above.

.gitignore is corrected to match: runtime memcards a session creates stay
ignored (*.mcr anywhere, /*.mcd at the root, which is where the runtime drops
card1.mcd/card2.mcd when no path is configured), while the tracked fixtures
are re-included. The old !dummy.*.mcr negation existed only to keep the two
root oracle cards tracked and is replaced by
!runtime/tests/fixtures/memcards/*. Verified with git check-ignore --no-index
on each moved file, on the pre-existing runtime/tests/test_card_synth.mcd and
runtime/tests/test_dir/card1.mcd fixtures (all not-ignored), and on
hypothetical root card1.mcd / card2.mcd / saves/card1.mcr (all ignored).

beetle_libretro.cpp's warn-only memcard sanity check keeps its behaviour; its
WARNING now names where populated fixtures can be copied from. The cards are
opened relative to the process's working directory because the libretro
GET_SAVE_DIRECTORY environment callback answers "." (same file, line 419), so
the message says to copy them into the CWD rather than implying a repo path.

BIOS profiles SCPH101.toml and SCPH5552.toml move from the root into bios/,
joining OpenBIOS.toml and SCPH1001.toml so that bios/*.toml is the whole set.
Behaviour-preserving: config paths inside a profile resolve against
find_project_root() (config_loader.cpp:821), which walks up from the profile's
directory for .gitignore/.git/CMakeLists.txt -- bios/ has none of those, so it
lands on the same repo root as before, and bios/SCPH1001.toml already relied
on exactly that. tools/build_cli.py's staging map is updated to the new
sources; it already copied them to framework/bios/, so the packaged layout is
unchanged. recompiler/src/main_cli.cpp's bios/-first lookup keeps its root
fallback (harmless, and it is what makes older layouts still resolve).

Fixes a real latent break while in bios/SCPH5552.toml: seeds pointed at
"seeds/phase2_ghidra_seeds_SCPH5552.json", but there is no seeds/ directory at
the repo root -- the file lives at
recompiler/seeds/phase2_ghidra_seeds_SCPH5552.json, which is what SCPH101.toml
already named. Porting these two profiles to the address_model schema is
deliberately NOT done here; it is a separate tracked follow-up.

Test files that no build system knew about:

  * tools/test_coverage_vault.py PASSES (8/8). Moved to
    recompiler/tests/test_coverage_vault.py and registered as the ctest
    coverage_vault, which puts it inside runtime/check_test_registration.cmake's
    glob so it can never silently rot again. Its ROOT is adjusted from
    parents[1] to parents[2] for the new depth, matching every other python
    test in that directory. Re-run from the new location and from an unrelated
    CWD: 8/8. The registration guard was run standalone before and after, and
    with the add_test() deliberately broken as a negative control -- it does
    name recompiler/tests/test_coverage_vault.py as an orphan when
    unregistered, so the registration is load-bearing rather than decorative.
  * tools/test_compile_overlays_additive.py FAILS, 10 of 14 tests erroring
    against an API that has moved on, so it is removed rather than wired up.
    It calls four module attributes that no longer exist in
    tools/compile_overlays.py -- load_additive_captures,
    validated_cached_bundle_ids, cached_bundle_pairs, compile_and_publish_dll
    (zero occurrences each) -- and two signatures have changed under it:
    parse_overlay_func_ids() no longer takes require_stored_crc, and
    load_region_coverage() now takes 2-3 positional arguments where the test
    passes 5. Reviving it means rewriting it against the current additive
    capture API, which is a change to overlay-cache test coverage and needs its
    own commit and its own evidence, not a rename.

Verification: py_compile over all 113 remaining tracked tools/*.py plus
psxrecomp_cli.py passes; bash -n over every remaining tracked tools/*.sh
passes; importing sdk_progress, pad_delivery and cursor_writers from tools/
succeeds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 07:39:06 -07:00
Matt Stanley d08d84a3df Merge pull request #296 from kerokline/perf/static-overlay-parallel
overlays: parallel static compile, linear resume-wrapper pass, split output
2026-09-03 07:30:43 -07:00
Matthew Stanley 57648745a3 mods: the framework stages the title's catalog too (5 titles could not package)
4cc04be3 ("rebump") renamed the staged mod catalog from <exe>/mods/packages to
<exe>/mods/bundled so that a rebuild could wipe build output without deleting
the player's own installed mods. runtime.cmake followed the rename. The shared
release module followed it (release_overlay_stage.ps1's Add-ModCatalog reads
mods/bundled). docs/MOD_PACKAGES.md already described the destination as "the
framework's mods/builtin/packages PLUS the title's mods/preloaded/packages".

No title CMakeLists followed, because the second half of that sentence was
implemented five times, once per repository, as a hand-written

    add_custom_command(TARGET psx-runtime POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy_directory
            "${<TITLE>_PRELOADED_MODS}" "$<TARGET_FILE_DIR:psx-runtime>/mods")

in ApeEscapeRecomp, Tomba2Recomp and MegaManX4/5/6Recomp. That names the
destination as a STRING. There is no symbol, no header, no link dependency --
so renaming a staged output directory breaks nothing a configure, compile or
link step can see, and the diffstat of the commit that does it looks cosmetic.
The five titles went on staging into a directory neither the launcher nor any
packager reads, and discovery was deferred to whichever title next ran a
release packager, in a different repository, on a later day.

MEASURED, on real build output. Framework origin/master 22fbbfca plus
Tomba2Recomp origin/master cdf00af, both unmodified, both built:

  build-dir mods/
    bundled/psx.enhancement.cd-speed          <- framework, correct
    bundled/psx.enhancement.fast-loading
    bundled/psx.enhancement.pgxp
    bundled/psx.presentation.bezel
    packages/tomba2.debug.debug-menu          <- title, stranded
    packages/tomba2.enhancement.skip-fmvs
    packages/tomba2.enhancement.widescreen
    packages/tomba2.experimental.interpolated-frame-rate

and Add-ModCatalog run against that tree, from this commit's parent:

  Mod catalog is missing package(s) the sources define:
  tomba2.debug.debug-menu, tomba2.enhancement.skip-fmvs,
  tomba2.enhancement.widescreen, tomba2.experimental.interpolated-frame-rate.
  They exist in the source tree but did not reach ...\mods\bundled, so the
  release would ship a Mods page the dev build does not have.

Shipped releases are NOT affected. Tomba 2 v0.0.9 pins 04d9184b, which is an
ancestor of 4cc04be3 and contains zero occurrences of migrate_legacy_root and
bundled_root; its artifact is a self-consistent mods/packages layout. This is a
forward break only.

THE FIX PUTS THE LAYOUT IN ONE PLACE.

psxrecomp_add_runtime_target() gains a one-value keyword PRELOADED_MODS_DIR.
A title declares WHERE its packages live and never HOW they are laid out:

    set(APE_PRELOADED_MODS "${CMAKE_CURRENT_SOURCE_DIR}/mods/preloaded")
    psxrecomp_add_runtime_target(psx-runtime
        ...
        PRELOADED_MODS_DIR "${APE_PRELOADED_MODS}")

A keyword argument rather than a positional one or a variable read out of the
caller's scope, for three reasons. It is what the other fourteen one-value
arguments already are, so it needs no new convention and reads the same as the
LAUNCHER_BOXART line next to it. A pre-set variable would be invisible at the
call site and would silently do nothing if set after the call -- the same
"fails quietly in the caller's repository" shape as the bug being fixed. And it
is PER TARGET, which a global variable cannot be: Tomba 2 has two runtime
targets needing two different catalogs (mods/preloaded, mods/preloaded_ita).
psxrecomp_v4_add_runtime_target and psxrecomp_add_game_runtime both forward
their remaining arguments, so it reaches through both wrappers unchanged.

_psxrt_stage_mod_catalog() now emits, in one POST_BUILD command sequence per
runtime target: wipe mods/bundled; remove exactly the ids it is about to stage
from any pre-existing mods/packages; copy the framework builtins; copy the
title's packages; copy <dir>/README.md to mods/README.md; verify. Consequences
worth stating:

  * mods/README.md previously reached mods/ only as a side effect of copying
    the whole mods/preloaded tree. Now that only packages/ is staged it is
    copied on purpose, or it would silently disappear. Checked per title: all
    five carry mods/preloaded/README.md and none carries a root mods/README.md,
    so mods/preloaded/README.md is the only source there ever was.
  * a title may still OVERRIDE a framework builtin at the same id and version,
    because the framework copy runs first and the title copy overwrites it.
    Tomba 2's Italian catalog relies on exactly this, shipping localized psx.*
    manifests. The id LIST is deduplicated for the assertion and the reported
    count while the copies keep both, or the build claims to stage 11 packages
    for a catalog that holds 7.
  * COSIM targets stage nothing. They have no launcher and no Mods page, and
    they land in the same output directory as the real runtime, so letting one
    stage had it wipe and re-stage the runtime's catalog with only the
    framework half whenever -DPSX_BUILD_COSIM=ON built it last.

TWO GUARDS, because the failure mode being closed is silence.

Configure time: a project with packages under mods/preloaded/packages that does
not declare PRELOADED_MODS_DIR is a FATAL_ERROR naming the packages and the
argument to add. That is exactly the missed-title state and it is detectable
before any build runs. An empty packages/ is fine (the scaffold creates one),
and PRELOADED_MODS_DIR NONE is the explicit opt-out.

Build time: runtime/psx_check_mod_catalog.cmake fails the build if a declared
package did not reach mods/bundled, or if any package this build stages turned
up under mods/packages -- which after this change can only mean a CMakeLists
still stages its own catalog by hand. It runs as the LAST POST_BUILD step, via
cmake_language(DEFER), because POST_BUILD commands execute in registration
order and the framework block registers BEFORE anything a title adds after its
psxrecomp_add_runtime_target() call. Verified by inspecting build.ninja: the
deferred command lands after a title-registered POST_BUILD, not before it.

The deferred pass takes NO arguments and reads its work list out of global
properties. cmake_language(DEFER CALL <fn> "${var}") does not carry a
function-local variable through -- measured with cmake 4.2.2, the callee
receives an empty argument and an add_custom_command built from it fails at
directory end with "CMakeLists.txt:DEFERRED". Recorded so the next reader does
not re-derive it.

Ids the build does NOT stage are deliberately left alone in mods/packages. On a
self-compiling setup release that directory can hold packages the player
installed under the pre-split layout, and mod_packages.cpp's
migrate_legacy_root() relocates those into mods/installed on the next launch.
Removing only the ids this build stages is also what lets a build directory
made before the split be migrated silently, so the guard's "an id turned up in
mods/packages" report has no false positives.

The same script is registered as the ctest psx_staged_mod_catalog_test in every
project that stages a catalog, with REQUIRE_STAGED=0 so a tree where the
runtime was never built reports a skip rather than a spurious failure. Where
two runtime targets share an output directory it accepts either target's
complete catalog, since after a full build mods/bundled holds whichever linked
last -- which is correct for the packager, as it builds exactly one target per
variant, and each target's own POST_BUILD guard already verified its catalog at
stage time.

TESTS. runtime/tests/test_mod_catalog_layout.py exercises the guard against
on-disk fixtures: a reproduction of the broken state as it actually appeared
(bundled/ holding the four psx.* ids, packages/ holding the four game ids), a
declared package simply absent, a package directory with no manifest.toml, a
FOREIGN legacy package that must NOT fail the build, the require-staged split
between the two callers, and the shared-output-directory sibling case. It also
pins two source invariants in runtime.cmake so the layout cannot drift back
into per-title copies. Registered in runtime/CMakeLists.txt; the
test-registration guard reports 123 test file(s), all registered.

Framework ctest: 59/59 pass, 2 pre-existing DISABLED.

Also updated so the next title does not repeat this: the project scaffold
template now passes PRELOADED_MODS_DIR (it staged no catalog at all before, so
a scaffolded project with mods/preloaded/packages shipped none), and
docs/MOD_PACKAGES.md gains a "How bundled/ gets staged" section written at
titles.

Bead beads-eio.3.101. The five title-side patches must land AFTER this commit:
they reference PRELOADED_MODS_DIR, which does not exist before it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 16:37:19 -07:00
Alexandros Mandravillis 9dfb4174ad runtime: version GPU DMA snapshot format 2026-09-03 01:55:30 +03:00
Alexandros Mandravillis 99f775c839 runtime: close GPU DMA review gaps 2026-09-03 01:20:43 +03:00
Alexandros Mandravillis f424a8144d runtime: preserve live GPU DMA payload timing 2026-09-03 01:20:41 +03:00
Alex Vanderveen 4cc04be3e6 rebump 2026-09-02 13:26:06 -04:00
Kevin 7ab698caa5 overlays: parallel static compile, linear resume-wrapper pass, split output
Static mode (--static) was serial end to end and produced one translation
unit, so a whole title compiled on one core twice: once in Python, once in
gcc. Measured on BoF3 (339 captures, 79,688 identities) in a build-dbg loop:
phase "compile all overlay bands" ~12 min, runtime build ~4 min at -O0.

Three changes, each independently verified byte-identical:

1. static_capture_job(): the per-capture static work (recompiler subprocess,
   C audit, range audit, namespacing) is now a module-level, picklable job
   that returns its part + captured log. --jobs runs it in a
   ProcessPoolExecutor; results merge in capture order, so static_parts and
   entry sources are order-identical to the sequential loop. --jobs 1 and
   --jobs 3 on three bands: diff -r of the outputs is empty.

2. add_cps_resume_cases(): the universal CPS resume-wrapper pass called
   add_cps_resume_case() once per entry, and each call re-scanned and
   rebuilt the WHOLE part source. cProfile on three bands: 7,386 entries ->
   22,159 re.search calls, 366 s of 424 s total. The batch form locates
   every signature once, edits each host's segment in isolation, rebuilds
   once. Same three bands: 418 s -> 5 s, outputs byte-identical (diff -r
   against the per-entry output). The per-entry function is kept; a unit
   test asserts the two agree on fresh-switch, existing-switch, missing
   label and missing host cases.

3. write_static_outputs(): overlays_static.c now holds the dispatcher plus an
   extern prototype per compiled entry, and every part becomes its own unit
   overlays_static_NNNN.c (each already carries its #includes and a private
   namespace). runtime.cmake globs the siblings with CONFIGURE_DEPENDS, so a
   changed part count needs no reconfigure. Unchanged units are not
   rewritten (mtime preserved -> not recompiled); stale units are removed.
   --static-single-file restores the monolithic file.

Result, same BoF3 loop on a 12-core box: phase 5a 12 min -> 20 s, runtime
build 4 min -> 69 s (358 units in parallel), whole loop ~16 min -> 90 s.
Shard summary unchanged (332 ok / 7 benign audit fails), identity count
unchanged. Headless boot of the rebuilt runtime: 485,650 static checks,
485,244 hits (99.92%), miss_total 0, 60 emu fps through the LOGO overlay.

Note: tools/test_compile_overlays_additive.py already fails on master (it
references removed functions: compile_and_publish_dll, load_additive_captures,
validated_cached_bundle_ids); untouched here.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 09:20:32 -04:00
Alex Vanderveen 209970cfe5 self-build: loop breaker, install-folder preflight, boot-EXE name lint
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
2026-09-01 11:00:29 -04:00
Matthew Stanley 9abb83de8f Merge PR 283 after PR 282 revert for regression validation
# Conflicts:
#	recompiler/CMakeLists.txt
2026-08-31 13:50:04 -07:00
Matthew Stanley a4721966bc Revert "Merge pull request #282 from Alexbeav/pr/cdrom-seek-semantics"
This reverts commit 6003e63fbc, reversing
changes made to 59b9658c21.
2026-08-31 13:43:27 -07:00
Alexandros Mandravillis a851e26d48 Preserve visible depth24 scanout rows 2026-08-31 09:15:39 +03:00
Alexandros Mandravillis 3faec8e53e Document depth24 scanout positioning 2026-08-31 09:15:38 +03:00
Alexandros Mandravillis 3b837f4e2d Fix CD-ROM seek and read handoff 2026-08-31 02:20:04 +03: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 8f3645c2bc Merge pull request #268 from mstan/master
upstream
2026-08-29 21:07:42 -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 7edfe486d6 Merge pull request #264 from mstan/master
upstream
2026-08-29 16:45:55 -04:00
Alex Vanderveen 2ac6d181d9 Merge pull request #262 from mstan/master
upstream
2026-08-29 16:44:57 -04:00
Alex Vanderveen d14ca6917c bump 2026-08-29 01:50:12 -04:00
Alex Vanderveen cc448e20a1 build: let CMake own the runtime executable name
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
2026-08-28 23:48:56 -04:00
Alex Vanderveen 77095c589c runtime: make local rewind opt-in, and give it a settings key
Rewind was on for everyone and could only be turned off with PSX_REWIND=0 in
the environment. It now defaults off and is enabled from settings.toml
([video] rewind) or the launcher's Display card.

The cost it was charging silently: each snapshot is a whole machine — 2 MB main
RAM + 1 MB VRAM + 512 KB SPU RAM — serialized uncompressed into a buffer
malloc'd at a fixed 5 MiB cap that is never shrunk to the actual length, and one
is captured every rewind_interval frames (15 by default, tightening toward 4
while an FMV runs), each with a 128x96 thumbnail built pixel by pixel. At the
default depth of 50 a full ring is roughly 250 MB resident; at the maximum 200
it approaches 1 GB. That is a lot to hand a low-end host for a feature a session
may never open.

Turning it off genuinely avoids all of it: psx_rewind_configure() returns before
any allocation when rewind is not wanted, and both capture paths are guarded by
psx_rewind_enabled(). The only residue is a 450 KB BSS overlay panel whose pages
stay untouched.

The toggle applies live rather than at next launch — freeing the ring is the
whole point, so waiting for a restart would miss it. psx_rewind_shutdown()
already resets the overlay and drops a pending load, so tearing down mid-session
is safe.

PSX_REWIND still outranks the setting in both directions and now accepts 1 as
well as 0. Netplay rollback is a separate subsystem with its own ring and is
unaffected; rewind was already suppressed during netplay.

Because off is now the common case, psx_rewind_toggle() says so on the OSD
instead of leaving F8 / Select+R3 looking broken. It stays silent only in a
-DPSX_REWIND=OFF build, where there is nothing to enable.

Adds rewind_settings_test (default off, explicit off distinguishable from
absent, save/load round trip, tuning keys independent of the enable) and the
first documentation the feature has had, in config_schema.md.

Verified: recompiler tests build and pass; main.cpp type-checks against the new
recomp-ui ABI field; psx_rewind.c compiles both with and without
PSX_HAS_RBENGINE_SNAP; the existing test_rewind_toggle_combo.py still passes.
Not run end to end — this checkout has no BIOS dump to boot.

Needs recomp-ui a31ecbc for Settings.rewind_enabled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017AwqKPQFnK3x7xQbK4PQKj
2026-08-28 17:33:52 -04:00
Alexandros Mandravillis d596b0ec46 runtime: [runtime] overlay_region_floor config key; bundled toolchain drives gcc when available
Two shipping gaps left Gran Turismo's gameplay and FMV code on the
interpreter (0.55-0.64x real time) even though the recompiled boot EXE was
fine:

1. The overlay region floor defaults to the boot EXE text end. GT's secondary
   EXEs (GTOS/GTMENU/GTMAIN/GTEND) all load at 0x80010000, inside that range,
   so they were never overlay-cache eligible. The only way to lower the floor
   was the PSX_OVERLAY_REGION_FLOOR env var, which a shipped package cannot
   carry. Add `[runtime] overlay_region_floor` (hex string or integer), applied
   before the env override with the same kernel-window clamp.

2. The bundled overlay_toolchain/ was only used to build the tcc command.
   A dev/production box with gcc on PATH but no game.toml autocompile command
   got no compiler at all. Treat the bundle as a gcc toolchain provider too:
   gcc counts as available when the bundle is present and a gcc/cc/clang is
   on PATH, and the same bundle then drives compile_overlays.py with
   --compiler gcc. One package yields gcc shards on a dev box and tcc shards
   on a toolchain-less player box.

Measured on GT1 (SCUS-94194), headless boot -> intro FMV -> attract, with the
floor at 0x10000 and gcc autocompile: 1.27-1.38x real time vs 0.55-0.64x;
SPU production 27 kHz -> 59 kHz (the FMV audio distortion was starvation).

(cherry picked from commit 0d226e5069bdf1395c819110beec40367a44a699)
2026-08-26 21:43:30 +03:00
Matt Stanley f294f7397a Merge pull request #226 from mstan/hotfix/linux-selfcompile
Self-compile hotfixes: offline dependency vendoring, GLVND GL, Windows/MinGW C++17, and a green ctest
2026-08-25 11:59:06 -07:00
Alex Vanderveen e6ad1bc4a6 bump
(cherry picked from commit 422583fc97)
2026-08-25 11:17:44 -04:00
Alex Vanderveen db2c03e109 hotfix for offline build deps
(cherry picked from commit 986de03fcf)
2026-08-25 11:17:44 -04:00
Matthew Stanley 5c876bd385 recompiler: add standalone static analyzer 2026-08-25 00:41:00 -07:00
Matthew Stanley d849485ab2 tools: add DuckStation oracle and GPU frame tools 2026-08-24 22:52:33 -07:00
Matthew Stanley 84a901289d mods: make bezel artwork user-selected 2026-08-24 17:03:52 -07:00
Matthew Stanley 9ba703e5ed Merge remote-tracking branch 'origin/master' into split/pr169-presentation-bezel
# Conflicts:
#	runtime/runtime.cmake
2026-08-24 15:16:40 -07:00
Matthew Stanley c8e3dac678 presentation: expose bezel artwork as a mod 2026-08-24 15:15:15 -07:00
Matthew Stanley 7a872bc018 docs: add host optimization and upstream audit notes 2026-08-23 13:09:19 -07:00
Matthew Stanley 55cec09acd docs: retire stale sljit references, attribute TinyCC
The sljit Tier-2 in-process JIT was replaced by the tcc tier in c2a3f6ad
(2026-06-25) and removed outright in 5b7e69b4 (2026-07-15). That removal
reworded the C-level comments but touched no markdown, so the prose still
described sljit as a live, default-on backend.

Backends today are static -> gcc -> tcc: gcc is the development default when
it is on PATH, tcc is the toolchain-free path for players. Both are external
compilers invoked as a subprocess that produce a DLL; neither is an in-process
JIT. docs/ARCHITECTURE.md, docs/BUILDING.md, docs/EXECUTION_MODEL.md,
docs/COMPILING_OVERLAYS.md and README.md already said this correctly and are
unchanged here.

- THIRD_PARTY_ATTRIBUTION.md: the sljit entry claimed code vendored at
  lib/sljit/ and compiled into the runtime. That directory does not exist.
  Replaced with a TinyCC entry, which was missing entirely despite tcc being
  the compiler intended to reach end users.
- docs/internal/SLJIT_PERSIST_CACHE.md: deleted. It documented the persisted
  shard cache of a subsystem that no longer exists, as "implemented" and
  "default-on". docs/internal/README.md updated to point at the current model
  instead of a dead link.
- docs/ASYNC_OVERLAY_COMPILE.md: superseded-in-part header. Deliberately NOT
  rewritten sljit->tcc: the passages about serializing .sljit blobs,
  overlay_sljit_deserialize and an in-memory JIT registry describe a mechanism
  tcc never used, so renaming it would trade one false claim for another. The
  async worker/file-handoff design and the CPS work remain accurate.
- docs/OVERLAY_CACHE_V2.md: sljit dropped from the dispatch priority chain.
- ISSUES.md: Issue #7 closed as superseded — the backend it reported against
  was deleted rather than fixed.
- docs/internal/COSIM_ORACLE.md: sljit_helpers left CPUState with the tier.
- Dated session logs (RECURSION_BUG.md 25.5, BACKDROP_PRELOAD.md,
  ecosystem-watch.md) keep their narrative and get a pointer to the removal.

One thing this does not fix: docs assert the tcc toolchain is "downloaded at
release-packaging time and bundled beside the game exe". runtime/src/main.cpp
does look for <exe_dir>/overlay_toolchain/, but no script in this repository
creates it. The attribution entry now states only what the code does and notes
that if packaging supplies the bundle, it happens outside this repo and must
carry the TinyCC notice. Flagged rather than papered over.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 18:16:34 -07:00
Alex Vanderveen d4ea4ade1f fix crash path from bad splash screen bug, and optimize cross platform pacing + rollback 2026-08-14 02:25:59 -04:00
Alex Vanderveen 4274a76a19 fmv hardening 2026-08-13 01:28:56 -04:00
Alex Vanderveen d5b5a029fe hotfix ci template 2026-08-12 21:53:26 -04:00
Alex Vanderveen c4f412be17 project studio, harden prefetch sdl3 2026-08-12 16:49:03 -04:00
Alex Vanderveen b1c2317c44 Merge pull request #138 from mstan/master
upstream merge
2026-08-12 02:11:00 -04:00
Matthew Stanley 0bd6ddf730 Merge master into fix/issue72-overlay-project-root
Conflict: recompiler/CMakeLists.txt. Master gained #134's test registrations
in the same block where #133 added stall_report. Resolved as the union of both.

Also RE-ENABLES aot_overlay_discovery, which is not part of the conflict region
and so would otherwise have survived the merge untouched. #134 registered it
DISABLED with this reason:

  "fails in check_real_batched_fragment_publication: that check reaches
   MOD.compile_fragment_batch, which spawns psxrecomp-game with cwd set to its
   own TemporaryDirectory, so BIOS-profile resolution finds nothing ... it
   needs an owner, and DISABLED keeps it counted until then."

This branch is that owner -- resolving BIOS-profile lookup against
--project-root instead of the CWD is exactly that fix. Landing it while leaving
the test disabled would ship the fix with its own proof switched off, which is
the silent-rot class #134 exists to prevent.

Measured on this tree, msys64 toolchain driven from PowerShell:

  - pristine origin/master: test_aot_overlay_discovery.py exits 1 at
    check_real_batched_fragment_publication, "no BIOS profile found"
  - this merge: ALL PASS, and ctest -R aot_overlay_discovery passes in 7.64s
    with the DISABLED property removed
  - baked codegen hash 0xfbb44d49u on BOTH arms -- the cache tag does not move,
    so existing overlay caches keep loading
  - full ctest failure set identical to pristine master, no new failures

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 22:27:33 -07:00
Alex Vanderveen 2207cd9a09 auto versioning CI releases 2026-08-11 22:05:20 -04:00
Alex Vanderveen 581d4e927c Merge origin/master into feat/legendofmanaetc
Resolve main.cpp by keeping ae_fill/rematch netplay bring-up and taking
master's mod-safe turbo_loads/skip_fmv apply (no pre-activation clobber).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 19:28:20 -04:00
Alex Vanderveen 599d326dd0 default multitap and multitap hack, and savestate optimizations 2026-08-11 14:20:26 -04:00
Alex Vanderveen 4e8900e6fe bios netplay patch 2026-08-10 23:20:30 -04:00
Alex Vanderveen 91d683d87d bios hotfix 2026-08-10 22:53:17 -04:00
Alex Vanderveen 71adcfa2c0 BIOS pathing 2026-08-10 22:17:30 -04:00
Matthew Stanley f0cfa92afa config: retire the legacy generic turbo_loads switch — load acceleration is mod-only
Load acceleration moved to the Mods catalog in 125d84d
(psx.enhancement.fast-loading "Fast Loading (host pacing)" and
psx.enhancement.cd-speed, both `game_id = "*"` so they ship with every
title, both default-off), and recomp-ui deliberately stopped drawing a
generic Turbo loads row. But the legacy switch was left live here with
`offer_turbo_loads` defaulting TRUE, so any title that had not explicitly
migrated still got turbo forced on — with no UI to turn it back off.

That shipped to players. MegaManX6Recomp v1.0.4 and v1.0.5 both carry
`[runtime] turbo_loads = true` in the game.toml inside the release zip
(verified by extracting the artifacts). Two paths forced it on:

  * game.toml [runtime] turbo_loads set g_turbo_loads_enabled directly;
  * a stale [video] turbo_loads in settings.toml was restored, and the
    seed write persisted it — so one run latched the value permanently
    and no later game.toml change could undo it.

Reported as MegaManX6Recomp#14: the WARNING screen self-advances and load
spinners run fast, with Turbo confirmed off in Mods. Tomba 2 hit the same
defect (fixed per-title), and MMX5 is also unmigrated. Third occurrence of
one config default, so fix the class instead of the titles.

Both keys are still PARSED, so existing configs load without error, but
neither is honoured:

  * offer_turbo_loads defaults false and turbo_loads_offered becomes a
    constexpr false, matching how ws_offered / skip_fmv_offered /
    frame_interpolation_offered already model mod-owned features. No title
    can offer the generic switch, so forgetting to migrate is no longer
    punished.
  * game.toml turbo_loads logs a deprecation line naming the Fast Loading
    mod and enables nothing.
  * settings.toml [video] turbo_loads is neither restored nor written back
    out, so the stale row is dropped on the first save — same write-only
    latch reasoning already documented for fast_boot, which bit harder here
    because there was no UI control at all.

Also fixes a related hole found while auditing: the in-game Settings apply
path wrote both g_turbo_loads_enabled and g_auto_skip_fmv from a launcher
snapshot taken BEFORE mod_runtime_activate_plugins() ran, so the first
in-game Apply silently turned a player's enabled Fast Loading or Tweaks FMV
feature back off. Both are now gated on their offered flags.

The supported surfaces are unchanged: the Fast Loading / CD Speed mod
plugins still enable acceleration (verified: "mod selected 8x load
acceleration"), and the `turbo_loads` TCP debug command still toggles it
for development.

Verified against MMX6 built on this framework: with game.toml
`turbo_loads = true` AND `offer_turbo_loads = true` AND a stale
settings.toml `turbo_loads = true`, both deprecation lines fire and the
debug server reports enabled=0, turbo_frames=0. With the keys removed and
no mods enabled, it boots silently with enabled=0.

test_mod_load_acceleration.py is updated to pin the new contract; it also
had a stale assertion on the old 2..16 multiplier range, which is now
pinned to the symbolic PSX_MOD_LOAD_ACCEL_MAX bound. The guard-test suite
goes from 9 pre-existing failures to 8 (no new failures; this one fixed).

Refs: beads-eio.3.26, MegaManX6Recomp#14

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 18:52:16 -07:00
Alex Vanderveen 2c1942b569 project migration tools 2026-08-10 20:44:55 -04:00