Cross-build game products for macOS from a Linux host with osxcross,
next to the existing Linux-to-Windows MinGW toolchain file.
- PSX_DARWIN_ARCH selects x86_64 (default) or arm64.
- OSXCROSS_ROOT points at the osxcross target directory. It falls back to
$OSXCROSS_ROOT, then to the osxcross-conf on PATH.
- CMAKE_OSX_DEPLOYMENT_TARGET defaults to 11.0.
- ld64 is named with --ld-path, so the build does not depend on
osxcross/bin being on PATH.
- The host's pkg-config files are hidden from the Darwin build.
Documented in docs/BUILDING.md and docs/ci/README.md.
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>
A contributor bringing up a new title hit four dead-ends in a row, three of
which were the same missing step wearing different masks: `regen_bios.sh`
builds the BIOS emitter but never configures it, and nothing said so.
- "no recompiler build dir found (set PSXRECOMP_BIOS_BUILD)" pointed at an
env var when a fresh clone has no CMake tree to point it at. Both discovery
failure and an explicit override now name the actual missing step and print
the configure command.
- Setting PSXRECOMP_BIOS_BUILD skipped the CMakeCache.txt check that discovery
applies, so an unconfigured directory reached `cmake --build` and died with
cmake's bare "Error: could not load cache". Validate the override too.
- A CMake configure that FAILS still writes CMakeCache.txt — the cache lands
before the generate step, which never runs after a FATAL_ERROR. So "has a
cache" did not mean "is buildable": the directory left behind by his failed
SDL and BIOS-backend configures had a cache and no build.ninja, and the
build reported only `ninja: error: loading 'build.ninja': GetLastError() =
2`. Read the generator out of the cache, check for its build files, and say
what happened. Discovery skips such a directory and keeps looking rather
than selecting it and dying; an unrecognized generator falls through to
cmake instead of inventing a failure.
All errors now go to stderr (the old one went to stdout).
Docs: the README's framework build block configured the recompiler and jumped
straight to the runtime build, omitting the required BIOS regen between them —
exactly the hole he fell into. It is now the same numbered 1/2/3 sequence
docs/BUILDING.md already used. BUILDING.md gains troubleshooting entries for
the two CMake half-configured-directory errors, the fresh-clone "No recompiled
BIOS backend available", and PSXRECOMP_BIOS_BUILD being resolved relative to
the framework root rather than the shell's cwd.
Verified: both new failure paths and the half-configured-directory case
reproduce and now self-explain; discovery precedence (build-t2 over build),
explicit-override precedence, Ninja/Makefiles/unknown generators, and a CRLF
cache all behave; end-to-end `regen_bios.sh --config bios/OpenBIOS.toml`
still emits (Dispatch entries: 3911).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SDL_main.h is a single-header implementation in SDL3. Include it only from the translation unit that owns main so MinGW emits one WinMain instead of one per SDL-using source.
Extract the useful diagnostic from PR #13 into an opt-in, title-agnostic boot
write-range trace with TCP-queryable context.
Co-authored-by: NyperYuhgard <93950153+NyperYuhgard@users.noreply.github.com>
Both targets are defined unconditionally and every build produces them, but
neither had an add_test() entry, so neither could run and neither could fail.
This is the same gap the codegen tests below them had before they were wired
in, and docs/TESTING.md states the rule directly: "an unregistered test cannot
fail, and a test that cannot fail is not a test."
Both were run manually before being registered and both pass. They are
self-contained -- no BIOS profile, no generated C, no psxrecomp-game -- so
unlike the neighbouring tests they need no WORKING_DIRECTORY.
l2_structural_test -> L2 structural: 239/239 ok
reachable_discovery_test -> ALL PASS
ctest -N goes 33 -> 35, so the three places that state the count are updated
to match. Note they said 29, which was already stale before this change.
Correction to an earlier claim of mine: ctest was NOT absent. recompiler/ and
runtime/ already had 21 add_test() calls between them and `ctest` ran 8 tests
green in the recompiler tree. My original survey used a quoted git-grep
pathspec that the shell passed literally, matched nothing, and I read the empty
result as "no test wiring anywhere". The real problem was narrower: partial
registration and zero documentation.
Of 61 first-party test sources (excluding 3 fixtures/harnesses), 37 were
referenced by no CMakeLists at all. Each was run by hand; 21 pass and are now
registered, taking the recompiler tree from 8 to 29 tests -- still under five
seconds, and still requiring no BIOS dump, no disc image, and no generated
code.
The 15 runtime source-invariant guards are registered from
recompiler/CMakeLists.txt rather than runtime/CMakeLists.txt, following the
existing dirty_text_continuation_guards precedent. They only read runtime
source, and the runtime tree cannot configure until a BIOS has been generated,
so registering them there would have made them unreachable from the only tree a
newcomer can build.
Three tests are deliberately NOT registered because they fail today. Wiring a
known-red test would recreate exactly what took CI off pull requests. Detail is
in docs/TESTING.md; briefly:
test_interpreter_perf_guards.py asserts psx_devices_mmio_sync invalidates
the inline cycle limit. It does not. The function delegates either to
psx_devices_service_to_now() (which clears g_psx_cycle_fast_limit,
psx_cycles.c:161) or to psx_devices_recompute_deadline() (:153-157),
and that second branch never clears it. Needs a timing owner: the guard
may have found a real hole, or the invariant may have moved. The guard is
itself partly stale -- it still names s_next_service_cycle, since renamed
to psx_next_service_cycle.
test_runtime_perf_diag_guards.py asserts a substring no longer present in
the runtime source.
test_overlay_pair_dedup_runtime.py needs its companion harness built; not a
source-only guard.
New docs/TESTING.md covers running the suite, running one test, what each group
needs, the known-failing three, and why fixtures must not be registered. It
also records two traps that cost time here: parallel builds can crash cc1plus
on config_loader.cpp under memory pressure (retry with -j 1, it is not a code
error), and the codegen tests need an ABSOLUTE recompiler path -- a relative one
passes their os.path.isfile check and then fails inside subprocess.run on
Windows with WinError 2, which reads as a broken test.
Linked from CONTRIBUTING.md (above the existing manual regression checklist,
noting the suite does not tell you whether a game still runs) and from
docs/BUILDING.md.
Verified: clean configure + build, ctest 29/29 pass in 4.08s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
These are not stale-by-omission; each asserts something the code does not do,
and each was verified against the implementation before editing.
docs/BUILDING.md
- PSX_ENABLE_VULKAN was documented "off by default" in two places. It
defaults ON (runtime/runtime.cmake:778). Corrected both, and noted that
the build option being ON does not make Vulkan the runtime renderer.
- Removed the PSX_BUILD_COSIM row: no option() for it exists anywhere in
the tree. Replaced with PSX_NETPLAY, which is real (runtime.cmake:194).
- Step 2 (regenerate BIOS) was labelled "(Optional)". It is required before
the first runtime build: step 3 configures psx-runtime from
generated/OpenBIOS_full.c and fails at configure time without it
("Cannot find source file" / "No SOURCES given to target"). Regenerating
the *retail* backend remains genuinely optional; that distinction is now
explicit.
- "This repository has no required git submodules" contradicted .gitmodules
(recomp-ui, lib/recomp-net) and README's --recurse-submodules clone.
Replaced with a table of which submodule each build option needs.
docs/ARCHITECTURE.md
- Same Vulkan default correction.
- Removed the claim that BIOS and game configs merge with "game wins on
scalar keys". No such merge exists: load_bios_config is called only from
the recompiler front-ends (main_bios.cpp, and main_psx.cpp solely to build
the BiosAddressModel) and never from runtime/src/main.cpp.
docs/config_schema.md
- Same merge claim, stated in more detail. Replaced with the actual runtime
precedence: env > CLI > settings.toml > game.toml > compiled default.
docs/STRING_TRANSLATION.md
- Header said "SPEC (design only - no implementation yet)" above a shipped
feature: runtime/src/text_xlate.cpp (~970 lines), initialised from
main.cpp, configured via [localization]. Marked IMPLEMENTED and added the
public entry points.
docs/FEATURES.md
- Layer A (dynamic DLL overlay loading) was headed "SHELVED". It is the live
production dispatch path: overlay_loader.c is ~4,400 lines with 15
LoadLibrary sites, last modified 2026-07-26, and COMPILING_OVERLAYS.md
documents shipping the resulting .dll shards in releases. Added a status
banner and kept the original account as history, since the CRC-keying
dead ends it records are still worth reading.
docs/BIOS_SELECTION.md
- Claimed the identity gate checks "size, CRC32, SHA-256". It checks size
and CRC32 only (bios_backend_for_file, main.cpp:1088-1089); image_sha256
is recorded metadata and never enters the accept/reject decision.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The runtime links generated/<serial>_{full,dispatch}.c, produced by a separate
recompiler step run before the build. Those paths are marked GENERATED so cmake
configure succeeds before that step has run — which meant skipping generation
surfaced only as a raw "cc1: fatal error: <file>: No such file or directory"
deep in the build, with no hint that a step was skipped or what produces the file
(exactly the trap a from-source builder on a fresh checkout hits).
Add runtime/check_generated_sources.cmake and wire it into
psxrecomp_add_runtime_target as a build-time dependency of the runtime target:
a missing game source now aborts at build start with the files named and the
exact fix (build recompiler -> extract EXE -> generate -> rebuild), and configure
emits an early non-fatal warning. Framework-wide, so every game inherits it.
Also fix the docs/BUILDING.md "build and run a game" recompiler invocation (the
framework is a submodule at psxrecomp/, not ../psxrecomp/) and add the
recompiler-build step.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds PCSX-Redux's MIT-licensed OpenBIOS as a second BIOS the framework can
statically recompile, next to retail SCPH1001. Because OpenBIOS is
redistributable, a game verified against it ships its BIOS with the build —
players supply only a disc, and the launcher BIOS picker disappears.
The mechanism is general, not OpenBIOS-specific: every BIOS image gets a
bios/<STEM>.toml profile that is the single source of truth for its identity
and layout.
Recompiler
- bios/<STEM>.toml profiles: [program.image] (sha256 pin, redistributable
flag), [recompiler.address_model] (boot-time ROM->RAM copy windows,
kernel-bless window), install_slots, and [recompiler.runtime_exports]
HLE anchors (0 = feature structurally unavailable on that BIOS).
- BiosAddressModel (new): validated model built from the profile, replacing
~9 hardcoded SCPH1001 window constants that had drifted apart across the
emitter / discovery / game codegen. The emitted C normalize() derives
from the same table. from_config rejects overlapping/misaligned windows
and orderings where sequential folding != first-match. A CTest-registered
equivalence test sweeps every RAM/ROM address against verbatim copies of
the old helpers.
- Orphaned delay slots (the OpenBIOS-critical emitter fixes): jr/jalr
slots are inlined before the jump resolves — previously their side
effect was silently LOST (e.g. a case-return fragment's addiu $sp pop,
which unwound a shifted frame and walked the exception handler's IntRP
cursor into a wild dispatch). Orphaned branches resume at ds+4 on
not-taken instead of re-executing the slot (a read-modify-write slot
doubling twice broke OpenBIOS readPad). Jump targets latch pre-slot.
- MIPS-I load-delay modeling for dependent load pairs: the successor sees
the pre-load value (OpenBIOS's cardfasttrack.s relies on it on purpose —
"gotta break those bad emulators"). Unmodeled shapes warn at build time.
- Discovery hardening: branch-target dispatchability closure to fixpoint;
jump tables placed at jr+8 no longer walked as code; cross-function
continuations routed by an instruction->owner map.
- Signed MULT support (unreachable from the retail seed set; modern-GCC
OpenBIOS emits it freely), latency-modeled like MULTU.
- Seeds from ELF symbol tables + data-section code-pointer harvest
(tools/gen_bios_seeds_from_elf.py) — a source-built BIOS needs no
disassembly pass. 646 OpenBIOS seeds, incl. 5 empirical dispatch-miss
seeds.
- Stemmed outputs (<out_stem>_full.c / _dispatch.c) so two BIOSes coexist
in one out_dir. tools/regen_bios.sh is profile-driven (--config).
Runtime
- psx_bios_image: the generated dispatch now self-describes the BIOS it
was compiled from (identity: size/CRC32/SHA-256/wordsum; kernel-bless
window; HLE anchors; bundled flag). Runtime SCPH1001 hardcodes removed.
- Launch check is a hard identity gate (a mismatched image under
statically recompiled code is a guaranteed wild jump). Bundled builds
hide the BIOS picker and treat a missing/tampered bundled image as an
installation defect.
- Faithfulness fixes surfaced by OpenBIOS's tighter timing loops (all
global, argued in code comments, regression-tested against SCPH1001):
- SIO pad RX is cycle-paced like the memcard path (RX_RDY at
shift-complete, ACK +170); total ack-IRQ latency unchanged.
PSX_SIO_PAD_SYNC_RX=1 restores the legacy synchronous path for A/B.
- SPUSTAT derived from SPUCNT instead of permanently-asserted busy.
- GPUSTAT.13 (interlace field) alternates per vblank while interlaced,
pinned 0 in progressive.
- CD Init(0x0A) completes in ~3.9 ms instead of borrowing Beetle's
34 ms Reset figure (OpenBIOS's short bounded retry loop livelocked).
- Exception EPC guard accepts ROM-window block leaders, not just RAM.
- New TCP debug commands (documented in TCP_COMMANDS.md): bios_info,
s3_smear_watch, callret_watch; write-trace ring gains $s0-$s5.
bios/openbios.bin is vendored (MIT — notice in bios/OpenBIOS.LICENSE,
attribution in THIRD_PARTY_ATTRIBUTION.md, source pin + build recipe in
bios/OpenBIOS.toml). The emitter fingerprint digest is now content-only:
existing generated/*.emitter.sha will read STALE once; re-run
tools/regen_bios.sh.
Add a conservative source-checkout setup helper based on the Linux quick-start intent from PR #54.
The script builds the CLI/recompiler tools, refreshes BIOS C when a BIOS is present, and builds the BIOS-only runtime only when BIOS/generated sources are available. It deliberately leaves game project creation to the existing CLI generator.
Co-authored-by: Keeganator <keegan.pan@gmail.com>
Remove the in-tree launcher implementation and its submodules now that game projects use recomp-ui for launcher support.
Keep Vulkan hidden from the launcher unless a game explicitly sets [video] offer_vulkan = true, and clamp stale Vulkan settings back to OpenGL when it is not offered.
Co-authored-by: Martin Penkava <mpenkava1337@gmail.com>
Make the repo approachable for outside contributors and an IDE integrator:
- New contributor docs:
- docs/EXECUTION_MODEL.md — semi-technical tour of the static / native-overlay
/ interpreter execution model and how coverage gaps get plugged.
- docs/ARCHITECTURE.md — component-level view (recompiler, runtime, LLE+HLE
BIOS, overlay backend static->gcc->tcc, renderers, oracle model).
- docs/BUILDING.md — accurate dependency table, per-platform prereqs, build
steps, and the submodule + local-junction framework-linking setup.
- CONTRIBUTING.md — distilled core rules, verification expectations, PR flow.
- Declutter root: move ~14 internal dev-logs into docs/internal/ (history
preserved via rename) with an index README; repoint CLAUDE.md references.
- README: correct build recipe, LLE+HLE + overlay-backend summary, links to the
new docs.
- code_provider.c: update the stale backend comment to the real static->gcc->tcc
resolution (dead SLJIT.md reference removed).
- config_schema.md: drop the obsolete "once Phase A2/A3 ships --config" note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>