Commit Graph

1431 Commits

Author SHA1 Message Date
Matthew Stanley 53521866b4 SDL3: separate Windows desktop borderless and exclusive fullscreen 2026-09-30 23:30:28 -07:00
Matthew Stanley 757790debe Keep large screen-space composites anchored as one group 2026-09-30 21:38:17 -07:00
Matthew Stanley 7c7133fbb4 Integrate MMX6 adaptive renderer with current framework master
Preserve current segmented dispatch, active-plan mod hooks and software scale limits. Extend function filters through the active plan, retain the last-store pointer with overlay ABI 25, and keep default config fingerprints stable. Refresh BIOS emitter stamps after byte-identical regeneration and reconcile regression fixtures.
2026-09-30 20:23:16 -07:00
Alex Vanderveen efcb48fbcb bios fingerprint: compute it with cmake -P, not a bash found on PATH
The windows-2022 release job accepted the committed backends in the step
shell (Git Bash) and refused them at configure: runtime.cmake ran the bash
fingerprint through whichever bash find_program() located, and that
environment hashed a byte-identical tree differently. The fingerprint is now
tools/bios_emitter_fingerprint.cmake, run with the configuring cmake itself
on every platform; the shell script is a thin wrapper around it. Same digest
shape as before, so existing stamps stay valid (verified on an LF tree and
an autocrlf clone).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-30 20:49:23 -04:00
Alex Vanderveen f180a32ec1 bios: commit the OpenBIOS and SCPH-1001 backends; releases link both
Decision (Alex, 2026-09-30): the recompiled BIOS carries the same risk as
the recompiled game, which titles already commit, so the retail backend is
compiled in rather than built on the player's machine. The retail image is
still never shipped: the runtime needs its data at run time and accepts only
the exact image the backend was built from, so the player supplies their
own dump as before.

- generated/{OpenBIOS,SCPH1001}_{full,dispatch}.c and their .emitter.sha
  stamps are tracked (.gitignore un-ignores exactly those); regenerated with
  the current emitter.
- bios_emitter_fingerprint.sh no longer hashes the ROM image: the profile's
  SHA-256 pin stands for it and the emitter refuses any other image, and CI
  has no dump, so hashing it made every CI configure read "stale".
- runtime.cmake: the staleness check covers every LINKED stem and
  -DPSXRECOMP_BIOS_STALE_FATAL=ON makes a stale stamp fail configure.
- game-release.yml: no CI-side OpenBIOS emission (generate_openbios.sh is
  gone); configure with BIOS_STALE_FATAL and assert both backends linked.
  check_generated.sh asserts the submodule carries both backends tracked
  and no dump is tracked anywhere.
- The player-side module build stays for a known image a build does not
  link (SCPH-5552).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-30 20:27:41 -04:00
Matthew Stanley ca14a118f0 fix(runtime): grow software present buffers for adaptive widths 2026-09-30 17:11:39 -07:00
Alex Vanderveen 170370a440 launcher: prepare a retail BIOS from the wizard with progress, not at Play
A buildable dump used to verify as usable and then block at Play behind a
modal while the module compiled. Now:

- ae_bios_verify reports a buildable, not-yet-cached dump as needs_regen,
  and the runtime wires recomp-ui's new bios_prepare_with_progress job
  (ae_bios_prepare) whenever the toolchain is present and no Generate host
  is: the launcher runs the build as a progress job from the wizard's
  "Prepare BIOS" button or the Play prompt, re-verifies, and resumes the
  wizard. Generate & rebuild is never offered for a BIOS in a bundled build.
- psx_bios_module.c gains a progress sink fed from the builder's output
  lines (dump / emit / compile / self-check / published map to stage
  percentages with readable messages); run_command streams lines on both
  platforms.
- The launch-time build stays as the fallback for --bios / --no-launcher.

Requires recomp-ui 9f86c49 (GameInfo.bios_prepare_with_progress).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-30 20:10:57 -04:00
Alex Vanderveen 60919f5a87 bios: build a retail backend from the player's own dump as a loadable module
Bundled releases link OpenBIOS only; retail-BIOS-derived C never ships.
"Bring your own BIOS" now works without it: when a dump matching a shipped
profile (psx_bios_known_images.h) is selected, the runtime builds that
backend on the player's machine with the overlay toolchain the release
already carries, caches it, and loads it as one more registered backend.

- runtime/include/psx_bios_module.h: the module contract. An overlay-style
  self-contained shared library (no imports from the executable) exporting
  psx_bios_module_{abi,codegen_hash,init,backend}; reaches the runtime only
  through OverlayCallbacks plus a new PsxBiosModuleCallbacks table (traps,
  dirty-RAM/kernel-bless probes, fntrace, log_probe) and pointer aliases of
  the globals the generated dispatcher touches. Its #defines rewrite the
  emitter's own extern lines, so the generated C compiles unchanged.
- runtime/include/psx_bios_module_glue.c.inc: module-side forwarders and the
  four exports, compiled after overlay_dispatch_preamble.c.inc.
- runtime/src/psx_bios_module.c: host side. Cache path
  <exe>/cache/bios/<os-arch>/bm<abi>_<codegen hash>_f<flavor>/<STEM>_<crc>,
  ABI + codegen-hash gated load, callback fill, and a blocking build via
  overlay_toolchain/bios_module_build.py (gcc via compiler.txt or PATH,
  else tcc), then psx_bios_register().
- psx_bios_backend.c: the registry is now a mutable table seeded from the
  CMake-generated psx_bios_builtin_registry by a constructor, with
  psx_bios_register(); every existing walker keeps working.
- main.cpp: validate_bios_for_launch acquires a module (announcing a first
  build) before declaring a mismatch; discovery accepts a buildable dump;
  the launcher verify reports it usable ("ready" / "compiled on first
  launch") with no Generate & rebuild; the BIOS row shows whenever the
  toolchain is present; bundled_only respects module support.
- emitter: the CPS-marker constructor is not emitted in a module build.
- overlay_api.h: OverlayCallbacks gains a struct tag;
  overlay_loader_callbacks() exposes the host table before loader init.
- tools/bios_module_build.py (ships in the toolchain): identity + profile
  SHA-256 gate, staged emitter tree, emit, three-TU compile, ctypes
  self-check, atomic publish. release_stage.py stages psxrecomp-bios, every
  bios/*.toml, the seeds and the builder into overlay_toolchain/.
- Tests: runtime/tests/test_bios_module_host.c against three fake modules
  (good / wrong ABI / wrong hash) on real dlopen; recompiler/tests/
  test_bios_module_build.py builds the bundled OpenBIOS end to end and reads
  the descriptor back; guards extended.

Verified on Twisted Metal 4 (bundled build + staged toolchain, headless):
first launch with --bios SCPH1001.BIN builds and loads the module and boots
on image=SCPH-1001; the second launch loads it from cache.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-30 19:26:29 -04:00
Matthew Stanley 9fc5b50d19 Merge remote-tracking branch 'origin/master' into test/netplay-bios-wire-20260930 2026-09-30 15:21:24 -07:00
Matthew Stanley e63a23b74c Merge latest master for segment E; preserve completed IsC accuracy notes 2026-09-30 15:06:59 -07:00
Matthew Stanley 4717aacd63 Merge latest master for segment D; retain both landed accuracy fixes 2026-09-30 15:05:40 -07:00
Matthew Stanley 913822f358 Merge latest master for segment C; retain IsC notes and bundled-release test 2026-09-30 15:04:16 -07:00
Matt Stanley 1b99be0b8e Merge pull request #436 from RetroPortingToolKit/refactor/emitter-runtime-pc
Segment-aware code B: route every baked guest PC through runtime_pc()
2026-09-30 15:02:46 -07:00
Matt Stanley fcc685ce1d Merge pull request #435 from RetroPortingToolKit/fix/isc-icache-invalidate
Apply cache-isolated (IsC) stores to the caches, as Beetle does
2026-09-30 15:00:19 -07:00
Matt Stanley befdfea2db Merge pull request #442 from RetroPortingToolKit/fix/windows-framework-validation-20260930
fix: unblock Windows framework regression builds and tests
2026-09-30 14:58:38 -07:00
Alex Vanderveen 0b6baa49c4 launcher: never offer "Generate & rebuild" for a BIOS in a bundled build
A shipped bundled build links OpenBIOS only and carries no CLI, sources
or toolchain, yet the codegen host forced the BIOS row on and a retail
pick answered with needs_regen ("Generate & rebuild to switch"), a step
that cannot run there.

- The BIOS row is shown only when a choice can mean something: a retail
  backend is linked, nothing is linked yet (setup host), or the codegen
  host actually wired Generate (prepare_with_progress). main.cpp decides
  after host_apply instead of the host forcing has_bios.
- ae_bios_verify short-circuits a bundled-only build with a plain
  "runs its bundled OpenBIOS only" message before the identity checks,
  whose "this build expects SCPH-1001" wording names the pinned stem,
  not a linked backend, and sets needs_regen only when Generate exists.
- test_bios_selection_guards pins both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-30 17:54:53 -04:00
Matthew Stanley c472e4ee3b tests: exercise lobby BIOS CRC wire roundtrip and fresh local offers 2026-09-30 14:30:38 -07:00
Matthew Stanley 962d949f1e Merge remote-tracking branch 'origin/master' into fix/framework-tests-followup-20260930
# Conflicts:
#	runtime/tests/test_packagers_never_format_cache_tag.py
2026-09-30 14:27:29 -07:00
Alex Vanderveen 874df9b5bb release: ship bundled, compiled games from committed generated/ C
Retire the setup-host release model (CI wiped generated/ and shipped
emitters + sources + a wizard so every player regenerated and rebuilt the
title). Titles now commit generated/ and CI ships the compiled game.

- docs/ci/templates/game-release.yml replaces setup-release.yml: gate on
  committed game C (tools/ci/check_generated.sh), emit the OpenBIOS backend
  in CI (tools/ci/generate_openbios.sh), configure with
  PSXRECOMP_REQUIRE_GAME_C=ON and assert the full path was taken, package
  with tools/package_game_release.sh, verify the zip is a game not a kit.
- tools/package_game_release.sh: exe, assets, bundled OpenBIOS, configs, mod
  catalog + overlay toolchain via release_stage.py, notices; refuses sources,
  emitters, generated C, BIOS dumps. clear_generated.sh removed;
  package_setup_host.sh marked retired.
- Scaffold: gitignore templates track generated/, package_release.sh.in
  wrapper, setup_project.{sh,ps1} commit generated/ after Generate.
- Project Studio: audit flags ignored/untracked generated/, retail BIOS C
  and setup-host workflows; merge_gitignore un-ignores generated/; ops emit
  the bundled packager + workflow; generate_ci uses the new template.
- Docs: docs/ci/BUNDLED_RELEASES.md replaces HOST_ONLY_RELEASES.md.
- Tests: tools/tests/test_bundled_release_layout.py; fix pre-existing
  false positives in the packager-surface and overlay-static tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-30 17:11:14 -04:00
Alex Vanderveen 2b3f8c66c5 Merge pull request #445 from Alexbeav/up/netplay-exit-reason
netplay: say why a match ended, and end a boot mismatch in 3 s
2026-09-30 16:58:02 -04:00
Alex Vanderveen d8b644bf6d Merge pull request #444 from Alexbeav/up/netplay-bios-identity
netplay: settle the lobby BIOS by image, not by "any retail"
2026-09-30 16:52:27 -04:00
Matthew Stanley 7450dee7dd fix: unblock Windows runtime builds and current framework tests 2026-09-30 12:55:56 -07:00
Matt Stanley c604cea4d5 Merge pull request #432 from Cyanoxide/feat/configurable-depth24-trailing-margin
Make FMV depth24 trailing margin configurable per title
2026-09-30 11:09:04 -07:00
Alex Vanderveen f8b4773cd4 Merge pull request #440 from Alexbeav/up/netplay-libjuice-offline
build: vendor libjuice so netplay builds configure offline
2026-09-30 09:59:16 -04:00
Alexandros Mandravillis c2d96c9903 tests: pass mid and account to chat_push in the lobby chat fixture
0f1fd021a gave chat_push message-id and account parameters. The fixture
still called the four-argument form, so lobby_chat_ring_test stopped
compiling in any tree with the netplay submodules checked out. Pass NULL
for both; the ring behavior under test does not depend on them.
2026-09-30 16:10:59 +03:00
Alexandros Mandravillis 5c7a37146c netplay: say why a match ended, and end a boot mismatch in 3 s
A netplay match that ends early returns both players to the lobby. The
runtime knew why (netplay_soft_exit's origin) but only printed it to
stdout, so players saw the lobby again with no reason. A boot-digest
mismatch (peers booted different BIOS images or boot settings) first
held a black window for the full 20 s admit-stall watchdog.

netplay_soft_exit now records a sentence for its origin, and the soft
return to the lobby puts it on the launcher's status line through
last_error (the room and browser views already show it). The dig0 gate
timestamps a mismatch; once the same pair of digests has held for 3 s,
the watchdog ends the match with its own reason. A peer that gets the
other side's BYE while it sees the same mismatch reports the mismatch,
not "the other player left". A CLI match prints the sentence to stderr.

The origin-to-text map and the grace decision live in
netplay_exit_reason.c, covered by netplay_exit_reason_test. Builds on
the previous commit's psx_lobby_set_last_error.
2026-09-30 15:59:58 +03:00
Alexandros Mandravillis 5fbd11921a netplay: settle the lobby BIOS by image, not by "any retail"
The lobby's BIOS identity was a two-value token, openbios or scph1001,
and scph1001 meant "some retail dump". An SCPH-5552 peer and an
SCPH-1001 peer both offered it, the host settled it, each peer booted
its own image, and the rollback boot digest never matched: about 20 s
of black window, then both players back in the lobby with no reason
(reported in #243).

Each offer now carries the CRC-32 of the retail image that peer would
boot (online bios_offer.retail_crc; LAN JOIN line 6). The host settles
retail only when every seat offers the same CRC, else OpenBIOS when
every seat links it, else refuses to start and puts the reason on the
launcher's status line ("Players use different BIOS images (SCPH-5552
and SCPH-1001) ..."). The settled CRC ships in match_caps
(session_bios_crc) and on MOTK1 START line 7, and each peer boots only a
dump with that CRC. Rooms where every peer has the same image keep
working, including SCPH-5552-only builds with no OpenBIOS. Old peers
send no CRC, so a mixed old/new room falls back to OpenBIOS or refuses;
a session from an old host still accepts any retail dump. The lobby
server relays bios_offer and match_caps as opaque objects, so it needs
no change.

The rule lives in netplay_bios_settle.c, shared by the online and LAN
paths and covered by netplay_bios_settle_test. last_error grows to fit
one sentence, with psx_lobby_set_last_error to set it.
2026-09-30 15:58:52 +03:00
Alexandros Mandravillis 328d879512 build: vendor libjuice so netplay builds configure offline
With PSX_NETPLAY=ON, recomp-net FetchContents libjuice v1.7.2 from GitHub
at configure time. That download is outside third_party/deps.manifest, so
PSX_DEPS_OFFLINE does not cover it, and a build with no network (a sealed
CI job, a player behind a firewall) fails inside the FetchContent subbuild
with a curl error.

Commit the same archive recomp-net pins (MPL-2.0, 108 KB) and declare it
before add_subdirectory(recomp-net). FetchContent keeps the first
declaration of a name, so recomp-net's own FetchContent_MakeAvailable
builds the vendored copy with its own options. The configure stops if
recomp-net ever pins a different libjuice than the manifest.

Ship the MPL-2.0 notice and source location in runtime/licenses/ and
THIRD_PARTY_ATTRIBUTION.md. Two ctest cases configure a stand-in
recomp-net with the network sealed: the matching pin must configure,
a different pin must stop with the mismatch message.
2026-09-30 15:57:32 +03:00
Shokunin 67bdf98eaf runtime: run each overlay shard only for its own segment's PCs
docs/SEGMENT_AWARE_CODE.md §5.7 (decision 4). #417 made every KUSEG and
KSEG1 overlay dispatch fall to the interpreter, because every shard was
compiled for KSEG0. The loader now keys shards by segment, so a shard
compiled for KUSEG or KSEG1 runs for PCs of that segment.

Cache layout. KSEG0 shards keep their directory and {phys}_{crc} names, so
no existing cache is invalidated. KUSEG and KSEG1 shards live in the cache
tag's seg-kuseg/ and seg-kseg1/ subdirectories, under the same names; both
tiers scan them, and a shard's segment is its directory's
(cache_seg_from_path).

Manifest. A KUSEG or KSEG1 manifest starts with `S <segment>` (00000000 or
A0000000) before its first F. F entries are full VAs in that segment; the
parser no longer forces them to KSEG0. R records stay KSEG0-spelled byte
extents. Without S a manifest is KSEG0, as before. Refused, when indexing,
when loading and through live publication alike:
- a manifest whose segment differs from its directory's;
- an F entry outside its segment;
- S after an F, S twice, or S that is not a RAM segment base.

Dispatch. Each candidate records its segment. The physical indexes stay (the
bytes are one identity), and every lookup matches the PC's segment:
- the entry chain (idx_head_seg);
- CPS continuation ownership;
- the lazy manifest index and try_load_region;
- the interpreter's native-call contract.

The lazy-miss and range-owner memos are keyed by the full PC. A PC in a
segment that maps no RAM never runs a shard. Exports are resolved by their
full VA.

Counters. segment_alias_interp (from #417) now counts KUSEG/KSEG1 dispatches
the loader leaves to the interpreter because no shard of their segment is
cached or valid yet. The new segment_native counts those a shard of their
own segment runs. Both are in the TCP overlay_loader_status reply. A warm
cache takes the first to 0.

Full-VA compares:
- the overlay idle note's return test compares the resume PC with $ra
  exactly, and counts it as internal only for a running shard of its segment;
- the shadow diff's two "returned to the caller" tests compare the exact PC;
- the diff mode's kernel-window exclusion tests the physical address, so
  KSEG0 and KUSEG kernel candidates are treated alike. Before, it tested the
  full PC, so only a KUSEG PC ever matched, and #417's gate never let one reach
  it. This is opt-in (PSX_OVERLAY_DIFF).

Tests (loader harness, overlay_segment_gate):
- a KUSEG shard runs natively for its KUSEG PCs, entry and continuation,
  while KSEG0/KSEG1 aliases are interpreted;
- one shard per segment for the same word, preloaded and lazy;
- a stale own-segment shard is interpreted rather than handed to another
  segment's;
- a KUSEG continuation where KSEG0 has an entry at that word, and a lazy one
  whose competing KSEG0 bundle is newer;
- refused manifests, including through live publication;
- the native-call contract;
- a source check that the interpreter records the segment from the full PC,
  and that no site clears dispatch evidence without its segment bits.

The harness gains a segment-run scenario that drives real compiled shards
through a CPS dispatch loop. overlay_init_guard and overlay_retry_c11 follow
the per-segment lookup, and so do the disabled interpreter_perf_guards'
negative-miss and try_load_region checks (it still stops where it did on D).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 02:43:46 -07:00
Shokunin 752f8ff2e7 runtime: record the segment of each interpreted dispatch (capture schema v3)
docs/SEGMENT_AWARE_CODE.md §5.7. Overlay bytes are segment-free, but a PC
is not: a KUSEG, KSEG0 or KSEG1 entry of the same word links, traps, fetches
and stamps store PCs in its own segment, so each needs a shard compiled for
that segment. Capture kept only the physical word, so every shard was
compiled for KSEG0 and #417's gate interpreted the other segments forever.

dirty_ram_interp.h adds one sibling bitmap per segment that maps RAM
(g_dirty_ram_dispatch_seg_bitmap[3]; 768 KiB for the 8 MiB backing).
dirty_ram_dispatch_inner sets the bit for the full PC next to the existing
any-segment bit, once per interpreted dispatch. A PC in 0x20000000-0x7FFFFFFF
or KSEG2 addresses no RAM in Beetle and gets no bit.
dirty_ram_dispatch_evidence_clear() drops both together at every site that
dropped dispatch evidence: boot reset, a store into an executed page, DMA
preservation and the autocapture epoch. Snapshot jobs copy the siblings.

Capture schema v3 adds, per region,
  "dispatch_entry_segments": {"kuseg": [...], "kseg0": [...], "kseg1": [...]}
with each PC spelled in its segment. dispatch_entry_pcs keeps its v2 meaning
(dispatched in any segment) and KSEG0 spelling, so existing readers are
unaffected. A record without the field is KSEG0 only.

Guest behaviour is unchanged: the bitmaps are host-only capture evidence and
are not saved in savestates. overlay_capture_retry_test checks the v3 record
through both writers (the background epoch and the synchronous final
capture), including that a cleared epoch's segment does not outlive it.
psx_ram_runtime_map_stubs.c defines the new bitmap, which memory.c's write
paths now reference, so psx_ram_runtime_map_test still links on GNU
toolchains. The disabled interpreter_perf_guards check follows the evidence
clear (it would otherwise contradict overlay_segment_gate's wiring check).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 02:43:45 -07:00
Shokunin a0ae0c4f6a runtime, recompiler: compare call-contract return PCs exactly
psx_call_contract and the BIOS dispatch loop compared a return PC with the
call's link physically ((a ^ b) & 0x1FFFFFFF). A PC carries a segment, and
the link a call writes is in the segment of the body that made it
(docs/SEGMENT_AWARE_CODE.md §5.5). A guest that returns to another
segment's alias of the call site did not return to that body, so letting
the suspended C continuation run there would execute it with the wrong
segment's PCs. §5.5 deferred these checks to this PR so one call contract
has one rule. All five are exact now:
- psx_call_contract (cpu_state.h): the bail-resolve PC and the $ra check;
- the emitted BIOS dispatch loop: the return boundary, the bail resolve, the
  wild-return test and the exception-stack straddle.

Since B routes every baked link through runtime_pc() and C/D compile each
body for the segment it runs in, a legitimate return always matches in
full. The ledger gains the exact-return-contract guard: it compiles
psx_call_contract and checks that another segment's alias starts a bail and
resolves nothing, and that the emitted OpenBIOS dispatch has the four exact
checks and no masked one.

OpenBIOS and SCPH-1001 dispatch C change in those four lines; game C does
not change. cpu_state.h is in the codegen hash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:24:07 -07:00
Shokunin 94be8f6386 recompiler, runtime: compile BIOS segment variants; SCPH-1001 enters its kernel at 0xA0000500
SCPH-1001's reset code enters its relocated kernel through the uncached
alias (lui $a0,0xA000; addiu $a0,$a0,0x500; jr $a0 at ROM 0xBFC0044C).
Beetle charges those four instructions as uncached fetches, 20 cycles; the
BIOS dispatch normalized 0xA0000500 to the kernel body compiled for runtime
0x00000500, which charges cached fetches, 11. That is the -9 cycles left at
SCPH-1001's kernel entry since #429 (docs/SEGMENT_AWARE_CODE.md §3.3).

A BIOS seed that is a runtime PC inside a relocated copy window, in another
segment than the window runs in, now asks for a segment variant (§5.4): the
function at those ROM bytes and its direct-edge closure (branch and J/JAL
targets that leave it, the call return and fall-through past its end) are
emitted again with the seed's segment. Every PC a variant computes from its
own PCs is in that segment (links, fetch tags and the uncached test, IRQ
resume, branch, J and fallthrough targets, store and exception PCs), a KSEG1
variant charges a fetch per instruction, and a jump-table case in another
segment is dispatched instead of continuing locally. Variants and their
continuations get exact-PC rows in a segment_variant_table. A variant seed
that names no emitted function entry stops the build, and so does one in a
segment that does not map physical memory (0x20000000-0x7FFFFFFF, KSEG2).

The dispatch still finds the word by its normalized key (and applies the
kernel-bless guard); a new psx_bios_hit_body() then picks the body for the
exact PC. A variant compiled for that PC runs. Any other PC in another
segment than the one the word's home body was compiled for still runs the
home body, as before, but is now recorded as a BIOS segment miss in the
segment-miss ring (kind "bios", with the home PC), so a new alias entry is
loud instead of silent (§5.5). psx_bios_key_home_pc(), emitted from the
address model, gives each key's home runtime PC: its copy window's runtime
address, or the ROM run in place. The ring, TCP segment_misses and the exit
report carry each entry's kind; tools/collect_game_misses.py lists a BIOS
segment miss for the BIOS seeds instead of writing it into the game's.

SCPH-1001's seeds gain 0xA0000500 (kernel_entry_kseg1): a 4-instruction
KSEG1 variant of kernel_trampoline_0. Every BIOS's dispatch C gains the two
helpers and calls psx_bios_hit_body() where it called the home body. A BIOS
without variant seeds (OpenBIOS, SCPH-101, SCPH-5552) keeps its full C;
SCPH-1001's full C gains the variant body and its dispatch C the one-row
segment_variant_table.

New bios_segment_variants test: OpenBIOS with KSEG1 variants of kernel
0x12BC (closure 0x6F8 by jal and 0x70C by fall-through) and 0x13F4 (0x1424
by a conditional branch only) and a KSEG0 variant of kernel memset 0x540 (a
KUSEG jump table), plus a static check of SCPH-1001's seed. It compiles the
emitted lookup, with and without variants, and queries every variant row,
every home key's home PC (checked against the PC each function body was
compiled for) and every other segment's alias of it. Seeds in a segment
that does not map physical memory stop the build, and a runtime PC in its
window's own segment stays a discovery seed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:24:07 -07:00
Shokunin d67f0f9396 recompiler: compile a KUSEG-linked EXE for its link segment; exact dispatch
A PS-X EXE header may carry KUSEG addresses (Alien Resurrection SLUS-00633,
Kula World SCES-01000); the BIOS then runs the program at KUSEG PCs, so its
links, EPCs and I-cache tags are KUSEG. The parser folded such a header to
KSEG0, so the whole program compiled for a segment it never runs in, and
the dispatch lookup matched any segment by physical address
(docs/SEGMENT_AWARE_CODE.md §3.2, §5.3, §5.5).

- EXE parser: keeps the header's addresses. link_segment() is the load
  address's segment; the load address and entry must share it and lie in
  the 8 MiB RAM window of KUSEG, KSEG0 or KSEG1 (KSEG1 compiles as an
  uncached home segment). phys_load_address()/contains_phys() do byte
  addressing; the BSS overlap check and the text_size bound are physical.
- function_analysis: the three JAL scans take the target's top bits from
  the delay slot's PC instead of assuming KSEG0 (unchanged for KSEG0).
- Seeds are range-checked by physical address. A seed in the link segment
  is an entry (0x000100E4 was silently dropped before). A seed for the same
  bytes in another segment asks for a segment variant (§5.4): it is listed
  in a WARNING, never folded into the home body. Seeds outside the image are
  counted. The producer_range, cross_call_allow and hosted_interior
  directives have no variant form: a foreign spelling is refused with the
  link-segment one instead of a bare "invalid" error.
- Config code sites that are matched exactly (widescreen and cull sites,
  mod hooks, hot/data-shard funcs, the VSync-query function and its
  event-horizon return PCs, persisted options) and name the image's bytes
  in another segment are refused with the spelling that matches, since they
  would now match nothing. Kinds matched by physical address (byte patches,
  the full-word-guarded cull keep/angle/aspect-cone and signed-X-bound
  sites, dome call sites) reach the same bytes in any spelling and are not
  refused. Overlay compiles are exempt until PR E.
- tools/collect_game_misses.py wrote every dirty-RAM miss as a KSEG0 seed,
  which a KUSEG-linked image now reports instead of compiling. It writes
  them in the link segment (--segment, --game-toml, or a segmented text
  range; it refuses to guess from a physical one) and adds the
  segment_misses rows at their full PC.
- Dispatch source moves to game_dispatch_emitter.cpp. The lookup indexes
  the physical word, then requires row.addr == addr; a second row for one
  word is an error until variants share the table. psx_game_address_in_text
  stays physical, so a segment miss takes the clean-text-miss path (the
  previous commit records it).

For a KSEG0 title the game C is byte-identical; its dispatch file changes
only in psx_game_find_entry (the exact-PC compare) and its comment.

Tests:
- segment_aware_codegen closes link-segment, fetch-tag-segment,
  irq-resume-segment, resume-pc-segment, store-pc-segment,
  home-seed-accepted, alias-fetch-coherence and segment-miss; they stay as
  regression guards, and resume-pc-segment now also checks the dispatch
  rows' keys and resume PCs.
- kuseg_dispatch_lookup: KUSEG rows, names and baked PCs; aliases miss while
  text stays physical; variant seeds reported; a function reached only by
  jal and a SaveState-style continuation are discovered in a KUSEG image
  (kills the two discovery scans reverted to KSEG0; the third, the
  invalid-JAL data heuristic, reads the same bits either way); mixed-segment
  headers rejected; a KSEG1 image charged per instruction; foreign config
  sites refused; the RAM-mirror contract as before.
- kuseg_dispatch_lookup also compiles the lookup's binary-search form (a
  table spanning 2 MiB or more) and queries every segment's alias of each
  row; refuses a KSEG0 event-horizon site and KSEG0 seeds directives with
  their KUSEG spelling; accepts a KSEG0 signed_x_bound site and applies it.
- emitter_runtime_pc_test: the dispatch rows follow runtime_pc() and keep
  the compile identity in func_ names (kills B's two surviving mutants: 76
  and 12 failing checks); two rows on one physical word are an error.
- collect_game_misses (new, tools/tests): link-segment seeds from each
  source, segment misses at their full PC, no guessing, no duplicates,
  against a fake server using the runtime's own summary row format.
- reachable_discovery_test: the parser keeps KUSEG, accepts KSEG1, rejects
  mixed segments and non-RAM load addresses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:57:34 -07:00
Shokunin 3fb8b4c773 runtime: record segment misses in static game code
A PC carries a segment, and the game dispatch table is about to be keyed by
the full PC (docs/SEGMENT_AWARE_CODE.md §5.5). A PC whose physical word has a
compiled row only in another segment then has no body of its own: dispatch
interprets it through the existing clean-text-miss path and never runs the
other segment's body. Decision 2 in §10 is to interpret such a miss loudly
until the title is regenerated, so record it:

- psx_segment_miss.{c,h}: classification (the other segment's row at the
  same word, through psx_game_is_function_entry), an always-on ring with the
  full PC, the row that exists, $ra, $sp and the frame, and a per-PC count.
- dirty_ram_dispatch_inner hands every clean-text miss to
  psx_segment_miss_note(), which records it when the PC has no row of its
  own while another segment's alias does.
- TCP: new `segment_misses` (summary, or {"tail":N}); `dispatch_stats`
  gains segment_miss_total/_unique; the emu-thread ping reports the total.
- psx_last_run_report.json gains a `segment_misses` section (top 50 PCs).
- psx_segment_miss_test covers classification, the note helper, the ring,
  wrap-around and the summary order. segment_miss_wiring_test (a source
  guard) pins the helper's call in dirty_ram_dispatch_inner, the TCP
  command and its summary row shape, the dispatch_stats and ping fields and
  the exit-report section, which no unit test can reach.

Nothing is recorded with a dispatcher generated before the exact lookup: its
physical lookup finds a row for every alias, so the check never fires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:57:34 -07:00
Shokunin 76e6e33235 runtime: PSX_FORCE_INTERP routes clean game text to the interpreter again
PSX_FORCE_INTERP=1 (b0391bc7) is the tooling switch that sends every
dispatch into game text through the dirty-RAM interpreter, so the
interpreter's cycle model can be measured against Beetle. It works by
reporting the pages dirty. The native-safety checks later learned to
decide flagged pages by the actual bytes (dirty_ram_text_native_ok and
the exact-range dirty_ram_text_native_ok_ranges_from), and an untouched
page matches its reference image, so the compiled body ran anyway: the
switch had no effect on clean text.

Both checks now report no game text as native-safe while the switch is
on. Nothing changes without the environment variable.

Found while taking the interpreter column of the segment-aware probe
(docs/SEGMENT_AWARE_CODE.md §7.2): with the switch set, the probe's
links still came back KSEG0 and its cycles matched the compiled build
exactly. With this change the interpreter column matches Beetle on
OpenBIOS and SCPH-1001.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:10:45 -07:00
Shokunin 980274a405 recompiler: route every baked guest PC through runtime_pc()
Segment-aware code, rollout PR B (docs/SEGMENT_AWARE_CODE.md §5.2).

Game/overlay emitter: CodeGenerator gains a code segment and
runtime_pc(addr) = code_seg | phys. Links (including a jalr in a branch
delay slot), fetch tags and the uncached-fetch test in emit_pre_icache,
interrupt resume PCs, CPS exit PCs and continuation keys, store-PC
stamps, the reserved-instruction EPC, the slice resume PC,
call_by_address and stale-guard targets, the call contract's return PC,
cosim PCs, the VSync-query hook's base PC, the data stubs'
unknown-dispatch PC and the dispatch rows' addr/resume_pc all go through
it. func_/block_ names, .ranges and entry-hook ids keep the compile
address. The VSync-query hook is not emitted for an uncached code
segment: its hand-timed body charges cached fetches only. The segment
defaults to the image's own, so runtime_pc(addr) == addr for every
existing input and KSEG0 titles regenerate byte for byte.

BIOS emitter: StrictTranslator::translate(d, runtime_pc) stamps stores,
the syscall EPC and the break and unaligned-access PCs with the PC the
code runs at; the psx_ldd_ temporaries keep the ROM address. The
full-function emitter passes relocate_ra() to every translate() call and
publishes the runtime PC on a fallthrough. Relocated kernel and shell
code therefore stamps its RAM address, as the interpreter does.

memory.c re-keys, in the same change, the seven SCPH-1001 store-PC keys
that named ROM addresses of relocated code: RAM-0 filters 0x00000F00
(ROM 0xBFC10A00), 0x80056EB4, 0x800585E4, 0x80058788, 0x80059C50 (ROM
0xBFC3EEB4/405E4/40788/41C50), the opt-in Tomba EvCB key 0x00001CE4 (ROM
0xBFC117E4) and the GP0 source key 0x80050B1C (ROM 0xBFC38B1C). Without
the re-key the new stamps would silently disable all seven. A RAM PC is
ambiguous where the ROM key was not, so each key goes through
scph1001_relocated_store(pc, rom): it matches only while the active
image is SCPH-1001 and the word at pc is still the ROM word copied
there. Other BIOSes and game code at those addresses never match, as
before.

Tests: emitter_runtime_pc_test (new, CTest, run with PSX_CPS unset and
set to 0) compiles a small program through generate_all_functions with
the code segment left default, KSEG0, KUSEG and KSEG1, in CPS,
CPS-overlay and legacy mode. It requires every emission path to be
reached and checks every guest-visible PC class, the identity keys,
physical-only differences and KSEG1 per-instruction fetch charging, and
the translator's runtime PCs in every PC-bearing form. The ledger gains
two regression guards: bios-runtime-pc (OpenBIOS output, plus every
translate() call site) and store-pc-keys-runtime (memory.c keys against
SCPH1001.toml's windows). KNOWN_GAPS is unchanged: B closes no ledger id
on its own (§5.2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:10:45 -07:00
Shokunin 825a89a9c5 runtime: keep host stores out of IsC, as DMA is
Only CPU stores are cache-isolated. Mods, the FMV skip, debug pokes, the
bg2d ring fill and the exception sentinel write guest memory from the host;
they now go through psx_host_write_*, which bypasses isc_store the way DMA
does, so a host store inside a FlushCache window reaches memory instead of
rewriting an I-cache tag. The render-pass checkpoint restores the new depth
counter like the DMA depth. isc_store_test covers it (a lane-0 host store in
tag-test mode would otherwise mark a cold line valid).

Docs: the SCPH-1001 residual starts at kernel init 0x598, and without #429
the fix removes 113 of the 126 OpenBIOS flush cycles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:55:26 -07:00
Shokunin 621a92d49b runtime: apply cache-isolated (IsC) stores to the caches, as Beetle does
memory.c dropped every store made while SR.IsC was set. Beetle's
PS_CPU::WriteMemory sends them to the caches instead
(mednafen/psx/cpu.cpp:482-512), before any address decode:

- I-cache enabled (BIU bit 11) with a tag-test, invalidate or lock mode
  bit: the store rewrites the tag and valid bits of its line. This is how
  FlushCache (A 44h) and the boot cache init invalidate the I-cache.
- D-cache enabled with lock mode clear ((BIU & 0x81) == 0x80): the store
  lands in the scratchpad at addr & 0x3FF.
- Nothing reaches the bus, the BIU register at 0xFFFE0130 included.

psx_icache_isc_store transcribes the tag write; memory.c's three store
chokepoints call it (and the scratchpad write) first, so compiled code,
overlay shards and both interpreters share it. DMA writes are not CPU
stores and now bypass IsC, as in Beetle. A render pass applies the same
effect (its checkpoint restores tags and scratchpad), and the overlay
shadow record fails closed on an isolated store, which its replay
cannot redo.

isc_store_test drives the real memory.c and psx_icache.c: 15 of its
checks fail on master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:12:55 -07:00
Matt Stanley 0ffab1ceb9 Merge pull request #429 from RetroPortingToolKit/fix/uncached-fetch-per-insn
Charge the uncached (KSEG1) instruction fetch on every instruction
2026-09-29 16:31:20 -07:00
Matt Stanley 9a8737d579 Merge pull request #431 from RetroPortingToolKit/docs/beetle-macos
docs: build the psx-beetle oracle on macOS (plus the missing Beetle hook patches)
2026-09-29 16:31:07 -07:00
Shokunin 2dc4e2d591 Test: pass interp_capture's origin and redrawn arguments in the GL fixture
The merge of #427 (58bdf987) kept the fixture's 8-argument
interp_capture(...) call, but #424 had already grown the function to 11
parameters (origin_x, origin_y, redrawn). The fixture is compiled when the
test runs, so CMake configures and builds cleanly and
gl_scale_invariance_test then fails in about 0.2 s with "too few arguments
to function call, expected 11, have 8".

Pass 0, 0, 1 (origin at the capture rectangle, frame redrawn), as the
pre-merge integration of #424 and #427 did. With it the test passes on
macOS arm64 (Metal GL 4.1): lines, masks, captures and render passes at
1x-18x, the window mode and both clamps, 0 failures.

Test-only; no runtime change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:04:53 -07:00
Shokunin 7342e6bcd1 emitters: charge an uncached (KSEG1) fetch before every instruction
Beetle's ReadInstruction never fills an I-cache line for a fetch at
0xA0000000 or above: every such fetch costs +4 and clears the pending load
give-back. The dirty-RAM interpreter charges that by fetching at every PC.
Both static emitters emitted psx_icache_fetch only at cache-line leaders,
which is exact for cached code only. In OpenBIOS, 5,477 of the 9,592
in-place ROM (KSEG1) instruction sites were uncharged: 21,908 cycles short
per pass through that code, each also missing its give-back clear.

Both emitters now charge a fetch before every instruction whose runtime PC
is uncached. The predicate is psx_fetch_uncached in psx_instr_cost.h, which
psx_icache.c now uses too. The A0/B0/C0 call-vector native stubs charge one
fetch per executed word, so a KSEG1 call pays like the interpreter. Cached
code keeps the leader rule. R4's regenerated game C is byte-identical, and
the OpenBIOS diff is insertions only. psx_instr_cost.h joins the codegen
hash list, since the emitters fold it into emitted code.

Tests:
- ctest uncached_fetch_charge runs psxrecomp-bios on OpenBIOS and compares
  per-instruction fetch charges three ways: the compiled fetches, the
  interpreter's fetch path (psx_icache_fetch_interp) at every PC, and a
  Beetle transcription.
- uncached_fetch_codegen_test checks the game emitter at KSEG1 and KSEG0.

Both tests fail on master. This closes bios-kseg1-fetch-charge in the
segment_aware_codegen ledger (#419).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 12:27:57 -07:00
Shokunin 8915131ced psx-beetle: say 4382 in the port comments
beetle_main.cpp has defaulted to port 4382 since 4380 began colliding
with other oracles, but its header and the psx-beetle block in
runtime/CMakeLists.txt still said 4380. beetle_debug_server.c now says
4380 is only its fallback for a port <= 0. Comments only.

A correction to 9ef516a7 ("psx-beetle: build on macOS and send large
replies whole"): its message says none of its changes affect MinGW or
Linux. The send_raw change does. On EWOULDBLOCK every platform now waits
for the socket (up to 15 s) instead of dropping the client mid-reply.
That only turns a truncated reply into a complete one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 12:26:45 -07:00
Shokunin 216eb31ef2 cyc_watch: arm boot anchors from power-on on both backends
Boot anchors (reset, the kernel copy, the first A0/B0/C0 calls, the shell)
pass within the first frames, before a TCP cyc_watch arm can land, so a
native-vs-Beetle boot comparison could not be run from the tree.

psx-runtime (debug_server.c) and psx-beetle (beetle_main.cpp) now read
PSX_CYC_WATCH ("<pc>" or "<pc>-<end>" for region mode) and PSX_CYC_WATCH_N
(default 16) at start-up and arm the watch before the first guest
instruction. The native arm shares the TCP handler's code path
(cyc_watch_arm); psx-beetle calls beetle_cyc_watch_arm. With neither
variable set nothing changes. tools/cycle_compare.py --no-arm then reads
both rings.

PSX_BEETLE_UNPACED=1 skips psx-beetle's 59.94 Hz wall-clock pacing for
headless oracle runs. Guest timing is unaffected.

docs/beetle-macos.md step 7 documents the variables and the anchor-key
difference for BIOS copy windows declared dispatch_key = "rom": the shell
is keyed natively at its ROM address (OpenBIOS 0xBFC0A500, SCPH-1001
0xBFC18000) while Beetle sees 0x80030000. Checked on macOS: the documented
example records the OpenBIOS shell entry at 5,015,670 in Beetle, the value
of the #429 land gate, and the native watch arms from boot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 12:26:16 -07:00
Cyanoxide 4aa72128fa Make the depth24 trailing margin configurable per title 2026-09-29 20:00:35 +01:00
Matthew Stanley 58bdf987d9 Merge master into feat/internal-resolution: combine with #422/#423/#424
Keep #422's 1x GL_LINES batch (gated s_hr_scale == 1 && !s_hiw) beside the >1x quad batch; FLIP capture uses hiw_capture_size; render passes use s_hr_scale and are refused under s_hiw.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 11:23:28 -07:00
Matthew Stanley 2bf4739f44 Merge master into feat/internal-resolution-settings: regenerate TCP_COMMANDS.md
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 11:22:53 -07:00
Matt Stanley f8a6f4f185 Merge pull request #425 from RetroPortingToolKit/feat/internal-resolution-gl
Raise the GL internal-resolution ceiling to the driver's limits
2026-09-29 11:22:28 -07:00
Matthew Stanley 914d8cb305 Merge master into feat/frame-rate: reset_on_load onto #420's fingerprint struct
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 11:22:02 -07:00
Matt Stanley d5e41d7b7d Merge pull request #423 from RetroPortingToolKit/feat/frame-rate-blend
Add a flip-aware frame-interpolation source (opt-in)
2026-09-29 11:21:11 -07:00