Commit Graph

1404 Commits

Author SHA1 Message Date
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 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 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
Matt Stanley b2f72d7cce Merge pull request #422 from RetroPortingToolKit/feat/adaptive-widescreen
Add [widescreen.cull] bgez_sites and clip_edge_x_load_sites
2026-09-29 11:20:58 -07:00
Matt Stanley df4e905ee8 Merge pull request #428 from RetroPortingToolKit/fix/rematch-session-start
Run the full session start on the lobby rematch
2026-09-29 11:20:45 -07:00
Matt Stanley 4ea39cf942 Merge pull request #421 from RetroPortingToolKit/fix/mod-session-hygiene
Reset mod-owned presentation state at every session start
2026-09-29 11:20:12 -07:00
Matt Stanley c896ec5e31 Merge pull request #420 from RetroPortingToolKit/fix/fingerprint-guest-facts
frame_fingerprint: judge on guest-visible facts; overlay shards set the runtime's store PC (ABI v24)
2026-09-29 11:20:04 -07:00
Matt Stanley f9ad37d25f Merge pull request #418 from RetroPortingToolKit/fix/muldiv-deferred-deadlines
runtime: publish deferred CPU work before mult/div deadline checks
2026-09-29 11:19:57 -07:00
Matt Stanley 3a7ea5e890 Merge pull request #417 from RetroPortingToolKit/fix/overlay-segment-alias
overlay: run shards only for the segment they were compiled for
2026-09-29 11:19:48 -07:00
Matt Stanley d6dc76e149 Merge pull request #415 from RetroPortingToolKit/fix/portable-mod-catalog-staging-20260928
cmake: stage mod catalogs portably without relinking games
2026-09-29 11:19:41 -07:00
Matthew Stanley 16cec81f1e psx_ram_runtime_map_test: link doubles for the MMIO readers memory.c now keeps
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 11:15:54 -07:00
Shokunin d6fddd0676 psx-beetle: record cyc_watch hits instead of stubbing them
beetle_cyc_watch_* were link stubs that always answered hits=0: the
Beetle-side sampler lived in a local beetle-psx tree that never reached
docs/. tools/cycle_compare.py and tools/cycle_testrom/measure.py (ruler
#2) therefore had no oracle to compare against.

Implement the comparator here, driven by the new per-instruction PC hook
in docs/beetle_guest_cycles_hook.patch. Semantics match the native
cyc_watch in debug_server.c: an entry is taken before the anchor
instruction is fetched and carries the absolute guest cycle count of
everything before it; with an end anchor each entry is one A->B pass;
n is clamped to the native ring size (1024). The hook is installed only
while a watch is armed, so an idle oracle pays one untaken branch per
instruction.

This makes psx-beetle require the patched Beetle tree; the stubs hid
that, but the other oracle hooks (rtrace/irq/guest cycles/CDC peeks)
already needed hand-landed Beetle changes. exc_ring stays a stub: no
tool reads it and its hook was never published.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 10:58:25 -07:00
Shokunin 9ef516a7f1 psx-beetle: build on macOS and send large replies whole
Three portability fixes for the psx-beetle oracle target, none of which
changes behaviour on MinGW or Linux:

- beetle_cdc_decode_volume redeclared PSX_CDC at block scope inside an
  extern "C" function. GCC accepts that; clang gives the declaration C
  linkage and rejects it against psx.h's C++ declaration. psx.h already
  declares PSX_CDC, so drop the redeclaration.
- The debug server's client socket is non-blocking, and send_raw dropped
  the connection on the first EWOULDBLOCK. macOS starts a TCP send buffer
  at 128 KB, so a large reply (read_ram of the BIOS ROM or all of RAM)
  reached the client as a truncated JSON line. Wait for the socket to
  drain (select, 15 s budget, as debug_server.c's send_all_blocking does)
  and only drop the client on a real error.
- .gitignore had "beetle-psx/", which matches a directory but not the
  symlink docs recommend when the Beetle checkout lives outside the tree.
  "beetle-psx" matches both.

Also note the non-Windows make platforms in the target's comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 10:58:14 -07:00
Shokunin bd65c814f7 Run the full session start on the lobby rematch
The lobby rematch re-enters the emulator through goto session_reboot,
below the first-boot session block. After its commit (offline) or
netplay clear it reset mod-owned presentation state, but it never ran
mod_runtime_activate_plugins() or the controller, load-acceleration and
disc-speed resets. So an offline rematch with mods enabled applied the
plan's main-EXE and disc patches and ran its VBlank callbacks, but no
activation callback ran, and the function-entry hook table, which the
commit empties and only activation rebuilds, stayed empty. It also
mounted the stock disc even when the plan had built a derived disc
image.

start_mod_session() now holds the first-boot sequence, unchanged:
clear the controller-mode overrides and presentation policies, load
acceleration and disc speed; run reset_mod_owned_presentation();
activate; then apply the netplay local viewport, the controller-mode
overrides and load acceleration. The first boot and the rematch both
call it after their commit or netplay clear, and both then take the
mounted disc from session_disc_path(), which returns the plan's derived
image when there is one and otherwise the stock disc. Activation still
precedes renderer and window creation on the rematch, because those
happen after session_reboot on every session. The rematch re-applies
its 4:3 Settings clamp before the session start, so a plugin's
activation or the netplay local viewport can still set the aspect.

A netplay rematch clears the plan, so activating it runs nothing and
the match stays vanilla. The first boot's behaviour is unchanged.

Tests: test_mod_owned_display_controls.py now requires activation and
the mod-owned reset to happen only inside start_mod_session, in the
order reset, activate, apply. It requires both paths to call it after
their commit and before the disc mount, and the rematch 4:3 clamp to
come before it. mod_runtime_test adds the lobby rematch sequence:
activating after a netplay clear runs no callback, VBlank plugin or
hook; the offline rematch's commit and activation re-run the
activation callback and VBlank plugin and arm exactly the plan's hook.
test_mod_load_acceleration.py follows the reset block into the lambda body.
Two comments that said a soft return never re-applies the controller-mode
override are corrected: test_launcher_pad_mode_wiring.py and
test_launcher_pad_mode_resolve.cpp. MOD_PACKAGES.md now describes the
rematch as a full session start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 23:42:17 -07:00
Shokunin 4bb58c2a81 Render passes: measure a stale pass-cost estimate again
Only passes that run are measured. The first passes of a race can all run in
a transient (a busy host, code run for the first time): in earlier Grand
Prix runs the three warm-up passes measured 13-15 ms against a steady 3-7 ms
at the same setting. An estimate that high prices most plans out, no pass
runs to correct it, and a plugin that crossfades while passes are shed (R4
Frame Rate) stayed on the crossfade for most or all of the run: 4, 9 and 29
passes in 22 s at 300 Hz, 7 in a verify run at 120 Hz, against about 1400
when the first passes were fast.

render_pass_cost_note_plan() (render_pass_plan.c) counts the plans that
wanted passes since the last measured pass. After 30 (about a second of a
30 Hz game) the warm-up restarts, so plans ask for one pass until three have
been measured again. When the new median is more than 3/4 of the old
estimate, the old one was right (a size that is truly too expensive, or a
host at its limit) and the next wait doubles, up to 960 plans, so these
one-pass warm-ups stay rare; a re-measure that finds the estimate stale
keeps the wait at 30. render_pass_stats reports cost_rewarms.

render_pass_plan_test covers the restart after 30 plans, an estimate that
passes keep measuring never restarting, the minimum wait after a stale
estimate, and the doubling waits when re-measures confirm the estimate
(47 ms at 4K, 10 ms re-measured as 9); render_pass_guards pins the planner's
call and its one-pass re-plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:19:08 -07:00
Shokunin 314de7c03c ws_cull_edge_interp_test: capture stderr through a named file
tmpfile() on MSYS2 MinGW64 (msvcrt) creates its file in the root of the
current drive and fails for a normal account; the test then ran without a
capture and failed its overflow-log check. It now writes to a file named
by argv[1] (ctest passes one in the build directory), fails with a clear
message if that cannot be opened, and removes it afterwards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:05:03 -07:00