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 emitter writes both helpers at a listed bgez_sites or
clip_edge_x_load_sites address whose instruction matches in overlay code
too, but overlay_dispatch_preamble.c.inc did not define them, so such a
shard failed to link (macOS, Windows) or to load (Linux dlopen RTLD_NOW)
and its overlay stayed interpreted.
The preamble now defines both over psx_ws_x_margin(), with the same pure
math gpu.c uses (ws_cull_edge.h, stdint-only, staged with the other
runtime headers). ws_cull_edge.h joins the shard compile surface in
codegen_hash_sources.cmake. overlay_shim_compile_contract links a shim
that calls both with the emitter's extern declarations, and
overlay_widescreen_callbacks runs them in an executable: identity with no
margin callback, widened at margin 120, interior clip edges unchanged.
Both fail without the definitions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
codegen_hash_sources.cmake is the canonical list of files folded into the
overlay cache tag, and its own header comment records why the list keeps
growing: something that changes emitted code but is not listed leaves the tag
put, existing DLLs keep loading, and new shards are stamped with the current
tag carrying different semantics. The comment names the 2026-07-03 instance
(full_function_emitter.cpp changed, only the .h files were listed).
The image VIEW was missing, one level below that. ps1_exe_parser.h now decides
what the emitter is allowed to see at all: it carries the READ bound
(end_address) and the ANALYSIS bound (analysis_end_address, which stops short
of an overlay capture's trailing delay-slot guard words), and
ps1_exe_parser.cpp decodes the producer's guard-byte declaration. A change
confined to either file moves every overlay shard's CFG -- measured in the
previous commit as a real change to Tomba 2's 0x8010A000 shard -- while leaving
the tag untouched. That is the same stale-but-same-tag hazard, reachable from a
one-line edit.
Adds ps1_exe_parser.h, ps1_exe_parser.cpp, and the headers of the walker and
analysis translation units already on the list (control_flow.h,
function_analysis.h, basic_block.h, code_generator.h) for the same reason: their
structs and bounds helpers are part of the emitter contract even when the .cpp
files do not move.
Verified the two independent computations still agree after the change --
runtime/include/overlay_codegen_hash.h says 0x73277fff and psxrecomp-game
--codegen-hash reports 73277fff -- so the stale-recompiler guard in
compile_overlays.py passes rather than being bypassed.
This bumps the tag, which is the intended effect: every existing overlay cache
is invalidated and resharded, because the analysis in the previous commit really
did change.
Bead beads-eio.3.100.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Emitter side of the value-propagation engine (ENHANCEMENTS.md G1.10):
- recompiler/src/pgxp_hook_emitter.{h,cpp} (new, in the codegen-hash
surface): ONE hook grammar shared by BOTH emitters, wrapping loads/
stores/COP2 transfers and precision-carrying ALU ops (moves, add/sub,
or-merge, shifts, LUI, HI/LO, muldiv) with PGXP_*() macros. Clobberable
sources are captured into locals before the statement. Precision-
destroying ops (AND/XOR/SLT/...) are deliberately unhooked - validate-
on-read drops their stale shadows for free.
- code_generator.cpp: central append in translate_instruction; LWC2/SWC2
emissions capture the RAW loaded/stored word (a masked GTE register
write must validate against the word as loaded).
- strict_translator.cpp: translate() wraps translate_impl()'s c_code with
the shared grammar; deferred load-delay variants hook the psx_ldd temp.
- BIOS regenerated, fingerprints verified CHANGED (SCPH1001 + OpenBIOS;
42.6k/12.2k macro sites). Base runtime + 33/33 tests green against it.
- Gating: macros expand only under -DPSX_PGXP=1. runtime.cmake grows a
PGXP option on psxrecomp_add_runtime_target (defines PSX_PGXP=1 +
PSX_OVERLAY_FLAVOR=2, _pgxp exe suffix) and PSX_PGXP_VARIANT to build
the sibling target from the same generated tree (framework runtime +
game wrapper). No ABI version bump needed: the appended PGXPHooks
table is NULL-tolerant and the flavor half of the ABI tag rejects any
host/DLL flavor mix.
- Shard caches: flavor folded into the cache tag (cg<V>_<hash>_gc<hash>
_f<flavor>) in overlay_loader.c + compile_overlays.py so base and pgxp
caches never share a directory; compile_overlays.py passes
-DPSX_PGXP=1 when flavor bit 2 is set; overlay preamble forwards
psx_pgxp_* through the table. PSX_OVERLAY_CODEGEN_VER 9 -> 10.
- Env overrides PSX_GEOMETRY_CORRECTION / PSX_PERSPECTIVE_TEXTURING /
PSX_PGXP_CPU_MODE (validation path: free-running headless boots arm
from the first vertex); census grew produced/swc2_stores and the pgxp
verb reports suppress/active gate state.
Validated: PSXRecomp_pgxp.exe boots the recompiled BIOS with hooks armed
(engine active, per-vertex lookups flowing; the BIOS shell has no RTPS
content so produced=0 there is correct - coverage measurement moves to a
3D title in phase 3).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Re-land of the shard-observability suite from 6da8c02 (stale branch), merged
against current master (keeps master's overlay_ext() platform suffixes,
recompiler_patch hash sources, and the unconditional v12 rfe forwarder).
- compile_overlays.py: ShardStats tallies every shard outcome — per-shard
"SHARD FAIL [class]" lines, a SHARD BUILD SUMMARY, a machine-readable
"PSX_SHARD_RESULT ok=N failed=M skipped=K" line, and a non-zero exit when
any shard that should have built failed (previously a run that built zero
shards exited 0, indistinguishable from a clean run).
- New --check preflight: build every shard into a throwaway dir and report
pass/fail — answers "would this game's shards compile against the current
recompiler + headers?" without a runtime.
- Retain audit-failed interior-fragment C (<phys>_<interior>_fragment_FAILED.c)
for triage, plus a persistent doomed-interior memo (interior_fail_memo.txt)
keyed (phys,interior,region_crc) so autocompile stops re-walking
data-as-code fragments every cycle (~90x off the V8 fragment pass).
- Extract DISPATCH_PREAMBLE to runtime/include/overlay_dispatch_preamble.c.inc
(single source of truth for the shard link contract, loaded by
load_dispatch_preamble(); missing file is a hard error).
- codegen_hash_sources.cmake: fold the shard COMPILE surface (overlay_api.h,
cpu_state.h, the .inc) into the cg tag so header/ABI/link-contract drift
reshards same-tree instead of silently breaking every new shard compile.
- autocompile.c: parse PSX_SHARD_RESULT from the child-output tail, expose
shard_ok/shard_fail/shard_skipped/shard_fail_total over autocompile_status;
ALWAYS rescan on completion so shards that did build load even when the
run also reported failures.
Live proof on the stale branch: Tomba's first autocompile surfaced 9 shard
link failures (undefined psx_rfe_mark_escape — since fixed by v12) and V8
surfaced 91 benign data-as-code fragment failures, all previously silent.
Codegen hash changes => every title reshards once on next build (expected;
loader rejects/regenerates stale shards).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The post-exception IRQ cooldown counted psx_check_interrupts CALLS. A call is a
backend-dependent unit: compiled checks at block edges (plus extra sites like a
`jr ra` return transition), the dirty-RAM interp checks per-instruction. So the
per-call countdown reached zero at a DIFFERENT guest cycle in each backend, the
next IRQ was delivered at a different cycle/EPC, and the two backends forked
(cosim first-divergence cp 3259 @ cyc ~213.5M). Convert the cooldown to a
guest-cycle DEADLINE (post_exception_cooldown_until): guest cycles advance
identically in both backends (shared, ruler-validated cost model), so the
delivery decision is now backend-independent. Magnitudes preserve prior
breathing room (claimed ~= old 8 blocks, unclaimed ~= old 500 blocks).
Also:
- gpu: GP1(00h) reset no longer clears VRAM. VRAM survives a GPU reset on real
hardware; only power-on gpu_init() clears our backing store (gpu_reset_state).
- codegen hash: include the emitter/translator/discovery .cpp TUs, not just
their .h. A change confined to full_function_emitter.cpp altered generated
dispatch+overlay code while leaving the hash unchanged -> stale overlay-cache
hazard (same class as the ws-emission-not-in-hash gap).
- debug: ring-buffer WINDOW-QUERY primitives -- frame_lo/frame_hi filters on
wtrace/mmio dumps + a capture-freeze frame so a causal window's tail is not
evicted by a post-window event storm before a probe reads it (query the
always-on ring for the window of interest, not arm-and-hope).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root cause of the Tomba pause-menu wedge (loaded saves only): autocompiled
overlay shards were emitted by a recompiler binary OLDER than the last emitter
change, yet stamped with the CURRENT cg tag (compile_overlays derives the tag
from --runtime-include; nothing tied it to the emitting binary). The stale
shards corrupted the display-list task queue during load-game. NOT the
IRQ-resume class the prior handoff claimed - disproven with the new ring.
- recompiler: bake the emitter-source hash (shared canonical list
runtime/codegen_hash_sources.cmake + hash_codegen.cmake) into psxrecomp-game;
new --codegen-hash flag prints it (machine-parseable, pre-banner).
- compile_overlays.py: HARD-FAIL when the recompiler binary's baked hash
differs from the cg tag hash it stamps (or the binary lacks the flag).
Applies to both cache and --static modes. Closes the read==write-tag /
stale-content drift class structurally.
- runtime.cmake: codegen-hash source list moved to the shared canonical file.
- interrupts.c: always-on exception-EXIT half added to irqctx_ring (take_pc,
real_epc, exit_pc, escape reason, same-thread/restore flags, v1/ra
saved-vs-exit, RestoreState redirect count) - proves per delivery which
escape path fired and whether the interrupted GPRs were restored. Kept the
same_thread_resume GPR-restore refinement (ring-verified equal-value in
practice; faithful: hardware restores all GPRs on every exception return).
- debug_server.c: irqctx_ring dump extended with the exit-half fields.
Validated live: Tomba menu opens on loaded save (Stormy Mountain) with the
cache fully enabled after regenerating shards with a rebuilt recompiler
(build-cosim + build-prod caches). Guard verified positive and negative.
MMX6 gate passed: boot -> title -> opening cutscene -> gameplay handoff OK.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>