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.
One pass per title: bump psxrecomp/recomp-ui to a ref that carries
docs/ci/templates/game-release.yml and the committed BIOS backends (refused
otherwise), un-ignore generated/, ensure [runtime] overlay_cache = true, emit
scripts/package_release.sh (retiring package_setup_release.sh) and
release.yml from the pinned template, rebuild emitters + regenerate the game
C from the title's disc, refresh framework_pins.txt, run check_boot_exe and
check_generated, commit; --push optional, --dry-run writes nothing.
The operation lives in project_studio/migrate_bundled.py; the tools/ script,
`git migrate-bundled` and `git bulk-migrate-bundled` (map_repos, per-repo
streaming) are its faces. Exercised end to end on a scratch copy of
Bomberman Party Edition (48 generated files committed, zip prefix kept,
generate_ci --check current).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Windows release job reports the committed backends as stale while
Linux and macOS accept the same tree; the configure error names no input.
check_bios_stamps.sh compares each stem's stamp with the local fingerprint
and, on mismatch, prints every hashed input's digest and the tool versions,
so the next run attributes the difference to a file.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
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>
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>
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.
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.
docs/SEGMENT_AWARE_CODE.md:
- §5.7 gains "Implemented in PR E": capture's per-segment bitmaps and schema
v3; compile_overlays.py's segment views (including dispatch-less records and
declared demands); the static fragment images; overlay_codegen_config() for
config code sites; the cache layout and the S record; the loader's
per-segment lookups and counters (unmapped segments included) and the
overlay_candidates fields; the full-VA compares; tests.
- §3.4, §5.3 and §5.4 point at it.
- §7.1 describes the overlay acceptance cases and the overlay-segment-shards
guard; the ledger stays at zero gaps.
- §7.2 records the probe's new overlay phase against Beetle (both BIOSes,
cold, warm and interpreter), the LLE boot after E, and R4's KUSEG and KSEG1
kernel shards with segment_alias_interp at 0 on a warm cache.
- §8 E is recorded.
AOT_OVERLAY_PLAN (the manifest contract and capture schema), AOT_SHARDING,
OVERLAY_CACHE_V2, COMPILING_OVERLAYS (the cache layout) and OVERLAY_XREF
(schema v3) follow the design, and MOD_PACKAGES notes that function-entry
hooks reach overlay shards of every segment. FAITHFUL_TIMING_PLAN and
ACCURACY_BURNDOWN get dated entries: overlay segments are closed, the last
masked return compares (the overlay shadow run's) are now exact, mod hooks
reach KUSEG/KSEG1 shards, and axis 5 records the probe's BIOS file read taking
about half Beetle's cycles (found here, not caused here).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/SEGMENT_AWARE_CODE.md: §5.4 and §5.5 gain "Implemented in PR D"
(variant planning and compilation, the BIOS variants and BIOS segment
misses, the multi-row lookup, the five exact return checks, seeds outside
the physical segments); §3.3, §5.2 and §5.6 point at them; §5.2 and §5.4
say that the jump-table rule applies to every game and overlay compile, not
only to variants; the §7.1 ledger is at zero gaps with the new
exact-return-contract guard; §7.2 records the probe's compiled column after
D, the LLE boot after D (SCPH-1001 first hits at 0, shell entry -445), with
#435 on top (0 through the shell entry on both BIOSes) and after the review
fixes (every hit unchanged, no BIOS segment miss); §8 D and §9's IsC note
are updated.
FAITHFUL_TIMING_PLAN and ACCURACY_BURNDOWN get dated entries: the SCPH-1001
KSEG1 kernel entry and the masked call-contract checks are closed, and BIOS
alias entries without a variant are recorded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/SEGMENT_AWARE_CODE.md:
- Status names PR C. §3.1, §3.2 and §3.3 describe the folded parser and
the physical lookup in the past tense and point at §5.3/§5.5.
- §5.2: the dispatch rows are now tested (B's two surviving mutants), and
the code segment's default is the link segment.
- §5.3 and §5.5 record what PR C implements: link-segment compile, the JAL
scans, seed and config-site checks, KSEG1 home images, the exact lookup,
the segment-miss ring and its TCP/exit-report surfaces, and why
psx_call_contract stays masked until D (the BIOS dispatch loop's four
return checks share the contract).
- §5.4: until D a variant seed is reported, not compiled.
- §7.1: eight ids closed and kept as regression guards; two open.
- §7.2: the probe's compiled column after C equals Beetle and the
interpreter; cycle watch, spin totals and the segment-miss ring.
- §5.3 also records the seeds directives, collect_game_misses, which
config-site kinds are refused (exact matches) and which are not
(physical matches), and two consequences until later PRs: a KUSEG title's
overlay code runs interpreted until E, and a KSEG0 title's alias entries
into its own text run interpreted until D.
- §8: C's validation and cross-title impact (codegen hash: overlay caches
recompile once, pre-C savestates are refused); D also makes the five
masked return checks exact; E returns KUSEG titles' overlays to shards.
FAITHFUL_TIMING_PLAN gets the dated log entry. ACCURACY_BURNDOWN records the
closed axis-6 item, the axis-4 progress, and the masked call-contract checks
as an open item for D.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
docs/SEGMENT_AWARE_CODE.md:
- §3.1 names emitter functions instead of line numbers, adds the
delay-slot jalr link and the VSync-query hook, and moves cosim_* out of
the identity keys.
- §5.2 records what PR B routes through runtime_pc(), what keeps the
compile address, the code segment's default (the image's own), and
what the tests cover (the dispatch rows wait for PR C).
- §5.4 notes that D's variants must tail-transfer jump-table cases in
another segment.
- §5.6 records that B routes emit_pre_icache's uncached test.
- §7.1 lists the two new regression guards; KNOWN_GAPS stays at 10.
- §7.2 adds the probe's interpreter and compiled columns next to
Beetle's.
- §8 records B's validation.
- §9 records the re-keyed store-PC keys and their SCPH-1001 gate, the
filters' Beetle divergence, and the confirmed game-syscall EPC gap
(its own change).
FAITHFUL_TIMING_PLAN and ACCURACY_BURNDOWN get the dated entries; the
burndown also records the RAM 0x0-0xF boot-scratch divergence from
Beetle. STRING_TRANSLATION's note that attributed GP0 quads to ROM
0xBFC38B1C now names the runtime key.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#429 charges psx_icache_fetch before every instruction whose runtime PC is
uncached, in both emitters, and makes the BIOS emitter's cache-line start
test use the runtime PC. The OpenBIOS check finds no uncharged KSEG1 site,
so segment_aware_codegen passes with 10 known gaps; bios-kseg1-fetch-charge
has not been in KNOWN_GAPS since the ledger's first commit in this series.
The check stays as a regression guard: an uncharged site reports the id as
a new gap.
docs/SEGMENT_AWARE_CODE.md:
- §5.6 describes the charge as landed, including the runtime-PC line-start
fix. The game emitter already has the predicate, but emit_pre_icache
tests the compile address: PR B must route both its psx_fetch_uncached()
argument and the fetch tag through runtime_pc(), and D adds the KSEG1
variants they reach.
- §7.1 lists ten open ids and records how A's id left the ledger.
- §8 marks A merged, with its Beetle parity (shell entry -126 on OpenBIOS,
-454 on SCPH-1001; the residuals are the IsC gap and SCPH-1001's KSEG1
kernel entry). D now only needs the KSEG1 variants; kseg1-fetch-charge
stays open until D. The macOS oracle recipe is docs/beetle-macos.md (#431).
- §1, §3.5 and the header say the BIOS undercharge is fixed. §10 is
unchanged.
- §3.1, §3.5 and §5.2 cite the emitter lines as they are after #429
(code_generator.cpp +6, full_function_emitter.cpp +10).
FAITHFUL_TIMING_PLAN §5 gets a log entry for the rebase.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Rebased onto master: #417 (the old base), #418 and #420 merged on
2026-09-29. The status line says so, and every cited source line is
refreshed for current master.
- PR A is #429. §7.2 records its passed LLE land gate against live Beetle
(shell entry -126 OpenBIOS, -454 SCPH-1001; ruler #1 64/64, ruler #2
14/14). The residue is the IsC gap (§9, now with per-flush numbers and the
prototype result) and SCPH-1001's KSEG1 kernel entry 0xA0000500 (§3.3, -9),
which PR D closes (§8).
- §7.2 records the synthetic EXE's first Beetle runs (macOS oracle recipe:
#431). Disc boot works on both BIOSes; sideloading does not, because
Beetle's loader forces a KSEG0 start. With the probe fixes, Beetle reads
56/56/82 cycles for KUSEG/KSEG0/KSEG1, a 26-cycle surcharge equal to the
fetch model's.
- Ledger values follow the new probe layout (probe_run 0x000100F0; 30 fetch
tags; leaf 22 vs Beetle 11). Still 10 ids.
- ACCURACY_BURNDOWN points the KSEG1 fetch item at #429; FAITHFUL_TIMING_PLAN
gets a dated log entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
memory.c:1292 also reads g_debug_last_store_pc in every build: a GP0 write
from 0xBFC38B1C gets a RAM source key for the widescreen prim matching. That
key lies in SCPH1001's Shell window (runtime 0x80050B1C), so it has to be
re-keyed with the other six when the BIOS stamp moves to runtime_pc().
List it in §3.1, §5.2, §9 and the FAITHFUL_TIMING_PLAN entry, and give each
relocated key its runtime PC.
§8: PR C closes eight ledger ids (§5.3's seven plus segment-miss), so
C and D close all ten; A (#429) closed the eleventh gap,
bios-kseg1-fetch-charge, on master. B's store-PC hole ships with the re-key of
all seven keys.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner settled §10's four questions on 2026-09-29, all as recommended:
- I-cache tags follow Beetle; the hardware bit-31 difference is recorded
in ACCURACY_BURNDOWN axis 4.
- Segment misses in static game code interpret loudly until the title is
regenerated.
- Extra segments are requested through segment-qualified seeds.
- The overlay cache uses per-segment subdirectories.
The doc called g_debug_last_store_pc a debug/identity hook. It is not:
memory.c's RAM 0x0-0xF store filters compare it with exact PCs in every
build, the interpreter stamps the full PC, and #420 (ABI v24) forwards
overlay stamps to the host. §3.1 now lists it as a baked PC, and §5.2
routes it through runtime_pc(). Consequences:
- Option A, re-measured with the 1,261 store-PC stamps in the three R4
shards: +3.8 % / +27.6 % __TEXT (was +3.2 % / +27.2 %).
- New ledger id store-pc-segment (10 KSEG0 stamps in the synthetic EXE's
home bodies), closed by §5.3. The ledger now has 10 known gaps.
- §9: several filter keys are ROM addresses of BIOS code that runs
relocated in SCPH1001. The BIOS emitter stamps ROM addresses and the
interpreter stamps runtime PCs, so the keys must be re-keyed when the
BIOS stamp moves to runtime_pc().
§8 notes that rollout PR A (fix/uncached-fetch-per-insn, based on master)
is being prepared. A has since landed as #429, so the ledger does not
list bios-kseg1-fetch-charge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compiled code bakes KSEG0 into link values, CPS/IRQ resume PCs and I-cache
tags. KUSEG-linked EXEs run KUSEG PCs through those bodies (and their KUSEG
seeds are dropped), and overlay code executed at KUSEG/KSEG1 stays
interpreted after #417. Until #429, both emitters also charged the uncached
KSEG1 fetch only at cache-line leaders, so compiled BIOS ROM code was 4
cycles short on 5,477 of OpenBIOS's 9,592 KSEG1 instruction sites (Beetle
and the interp charge every fetch).
docs/SEGMENT_AWARE_CODE.md recommends per-segment compiled variants (code
identity = full VA, emitter segment as a per-compile constant, exact-segment
dispatch, per-instruction fetch in uncached bodies, segment-aware capture
and cache keys) over segment-relative emission, which measures +3.2 % /
+27.2 % __TEXT on R4 shards.
tools/segment_testrom/gen_segment_exe.py emits a synthetic KUSEG-linked
PS-X EXE that also works as an oracle probe. The new ctest
segment_aware_codegen runs the real recompilers on it and on OpenBIOS and
checks emitted PCs, the emitted dispatch lookup and fetch cycles (through
runtime/src/psx_icache.c) against a Beetle ReadInstruction transcription.
It passes only while the observed gaps equal its ledger (9 today).
#429 (rollout A) fixed the BIOS undercharge on master before this landed,
so KNOWN_GAPS does not list bios-kseg1-fetch-charge: the OpenBIOS check
finds no uncharged KSEG1 site and stays as a regression guard that reports
the id as a new gap. The last commit of this series brings the doc up to
date with #429.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
FAITHFUL_TIMING_PLAN §5 (2026-09-29): with #429 on master, the LLE boot
now matches Beetle at every anchor to the shell on OpenBIOS (was -126)
and sits at -9 on SCPH-1001 (was -454), the KSEG1 kernel entry PR D
closes. Also measured without #429, the rulers (unchanged), R4's game
entry (+342 default boot, +18 LLE boot, 0 dispatch misses) and the
unchanged codegen hash.
ACCURACY_BURNDOWN axis 4: IsC item ticked; the SWL/SWR corner cases are
a new open item; the P2 scratchpad-under-IsC note points at Beetle's
rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The branch was rebased onto master 470f03b7, which now carries #418, so
the gate was re-checked on that tree without local merges. The
regenerated OpenBIOS and SCPH-1001 C is byte-identical to the gated
build's. Every boot anchor gives the same cycle (native - Beetle at the
shell entry: -126 on OpenBIOS, -454 on SCPH-1001). Ruler #1 matches Beetle
on all 64 passes, and ruler #2 on all 14 components on an HLE and an LLE
boot. uncached_fetch_charge and uncached_fetch_codegen_test pass. The
recompiler suite passes apart from the three environmental failures
master also shows on this Mac.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The land-gate entry said cyc_watch was armed from power-on on both
backends, but did not say how, and cited a local branch for the psx-beetle
build. The arming is PSX_CYC_WATCH / PSX_CYC_WATCH_N on psx-runtime and
psx-beetle (plus PSX_BEETLE_UNPACED=1 for a headless Beetle), documented in
docs/beetle-macos.md step 7 with the psx-beetle build; the entry now cites
both.
It also listed the shell entry as 0x80030000 on both sides. Both BIOS
profiles declare the shell window dispatch_key = "rom", so native keys it
at its ROM copy: the native anchor was 0xBFC0A500 (OpenBIOS) and
0xBFC18000 (SCPH-1001), and a native watch on 0x80030000 records nothing.
The numbers are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SEGMENT_AWARE_CODE.md §7.2 gates this change on an LLE boot with per-anchor
cycle parity against psx-beetle to the shell, plus ruler #1. Both ran on
macOS, each twice: with OpenBIOS (SHA-1 95419841b5104d552b14810b1ecbe6c1358bcdf1)
and with the owner's own SCPH-1001 dump (SHA-1
10155d8d6e6e832d6ea66db9bc098321fb5e8ebf), the same image on both sides. Native
master and this branch were built with #418 merged locally, so the mult/div
flush could not mask the result.
At the shell entry, native minus Beetle goes from -2,165,591 to -126 cycles on
OpenBIOS and from -7,049,462 to -454 on SCPH-1001. The ROM boot path is exact
on both. Two known gaps explain what is left:
- IsC stores do not reach the I-cache model, so a FlushCache leaves native
kernel lines valid that Beetle refills. A local prototype of Beetle's IsC
tag write takes OpenBIOS to 0 and SCPH-1001 to -9. It is recorded as an
axis-4 item here, not part of this change.
- SCPH-1001 enters its copied kernel through the KSEG1 alias 0xA0000500. The
four trampoline words run uncached in Beetle and cached natively (-9). That
is the KSEG1-alias gap in SEGMENT_AWARE_CODE.md §3.3.
Ruler #1 on SCPH-1001 matches Beetle on all 64 passes, on master and this
branch. Ruler #2 stays exact on all 15 loops. Beetle also confirms the memcpy
cost the branch now charges: 39 cycles per byte during boot, and 42 per byte,
860 and 3,884 cycles per call in R4.
The land gate item is ticked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 2026-09-29 log entry marked the live-Beetle validation OPEN inside a
done item. SEGMENT_AWARE_CODE.md §7.2 makes it a precondition for landing:
an LLE boot with per-anchor cycle parity against psx-beetle to the shell,
plus ruler #1. ACCURACY_BURNDOWN now carries it as its own unchecked gate
item, and the timing-plan entry says the PR stays draft until it runs on an
oracle machine.
The entry also framed the change as boot-only. OpenBIOS runs its A0/B0/C0
services in place, and 148 of its 192 A0 entries point into ROM, so the
per-instruction charge also applies during gameplay. Measured in an R4
race: memcpy's byte loop goes from 25 to 42 cycles per byte, and R4's two
memcpy calls per game frame go from 2,824 to 4,744 cycles (about 0.17% of
the frame). An LLE boot of R4 reaches the game entry 2,934,910 cycles
(1.10%) later, identical on two boots of each build, with 0 dispatch misses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cached line-leader rule charges a fetch at every 16-byte line start of
the runtime PC. The BIOS emitter tested bits[3:0] of the ROM address instead
and assumed every copy window preserves them. OpenBIOS copies its kernel from
ROM 0x1FC1E4D4 to RAM 0x500, which shifts them by 4. Each kernel line
crossing was charged one instruction early, where it is a hit, and the real
crossing went uncharged. 1,152 kernel instruction sites differed from the
interpreter.
The test now uses the runtime PC that relocate_ra already computes. In the
OpenBIOS kernel, 823 misplaced fetch sites go and 773 are added at runtime
line starts. Retail profiles are unaffected: SCPH-1001's copy windows are
16-byte aligned and SCPH-101/5552 declare none. The uncached_fetch_charge
ctest now also replays every cached OpenBIOS run from a cold cache against
the interpreter's fetch path and the Beetle transcription.
R4 (OpenBIOS, recompiled kernel, HLE boot) reaches the game entry 6,013
guest cycles later than with the uncached fix alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
The 2026-06-27 MMIO entry in FAITHFUL_TIMING_PLAN.md says the 13 prior
loops were unchanged when mmio_timer and mmio_spu were added. icache_miss
was not: the two new loops sit before it and move its loop top from
0x80010144 to 0x80010170, so each of its two misses costs one cycle more
and it reads +16 on both backends (re-measured against live psx-beetle; the
cycle_testrom README already explains the shift). The entry now carries a
dated correction, and the beetle-macos.md intro no longer points readers
at +14 as the current value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>