mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
67bdf98eaf
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>
369 lines
22 KiB
Python
369 lines
22 KiB
Python
#!/usr/bin/env python3
|
|
"""Structural regression checks for interpreter fallback hot-path guards."""
|
|
|
|
from pathlib import Path
|
|
import re
|
|
import sys
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
|
def body(source, name):
|
|
match = re.search(rf"\b(?:static\s+)?(?:inline\s+)?(?:int|void|uint8_t|uint16_t|uint32_t|uint64_t|DWORD|OverlayPreparedImage|PublishItem)\s*\*?\s*(?:WINAPI\s+)?{name}\s*\([^;]*?\)\s*\{{",
|
|
source, re.S)
|
|
if not match:
|
|
raise AssertionError(f"missing function {name}")
|
|
start, depth = match.end(), 1
|
|
for pos in range(start, len(source)):
|
|
depth += source[pos] == "{"
|
|
depth -= source[pos] == "}"
|
|
if depth == 0:
|
|
return source[start:pos]
|
|
raise AssertionError(f"unterminated function {name}")
|
|
|
|
|
|
def main():
|
|
interp = (ROOT / "runtime/src/dirty_ram_interp.c").read_text(encoding="utf-8")
|
|
capture = (ROOT / "runtime/src/overlay_capture.c").read_text(encoding="utf-8")
|
|
loader = (ROOT / "runtime/src/overlay_loader.c").read_text(encoding="utf-8")
|
|
autocompile = (ROOT / "runtime/src/autocompile.c").read_text(encoding="utf-8")
|
|
memory = (ROOT / "runtime/src/memory.c").read_text(encoding="utf-8")
|
|
gte = (ROOT / "runtime/src/gte.cpp").read_text(encoding="utf-8")
|
|
traps = (ROOT / "runtime/src/traps.c").read_text(encoding="utf-8")
|
|
interrupts = (ROOT / "runtime/src/interrupts.c").read_text(encoding="utf-8")
|
|
cycles = (ROOT / "runtime/src/psx_cycles.c").read_text(encoding="utf-8")
|
|
cyc_header = (ROOT / "runtime/include/psx_cyc.h").read_text(encoding="utf-8")
|
|
instr_cost = (ROOT / "runtime/include/psx_instr_cost.h").read_text(encoding="utf-8")
|
|
starvation = (ROOT / "runtime/include/starvation_ring.h").read_text(encoding="utf-8")
|
|
|
|
record = body(interp, "exec_pc_table_record")
|
|
if "uint32_t *slot = &g_dirty_ram_exec_pc_bitmap[word >> 5]" not in record:
|
|
raise AssertionError("executed-PC recording is not a direct bitmap OR")
|
|
if "pc_table_get_or_insert" in record:
|
|
raise AssertionError("executed-PC recording regressed to hash probing")
|
|
if "sizeof(g_dirty_ram_exec_pc_bitmap)" not in capture:
|
|
raise AssertionError("autocapture does not snapshot the 64 KiB bitmap")
|
|
write_window = body(capture, "write_json_window")
|
|
if ("size += 4u" not in write_window or
|
|
"phys + size <= ram_size - 4u" not in write_window or
|
|
"window_hi" in write_window):
|
|
raise AssertionError(
|
|
"capture delay-slot guard is missing or stops at an artificial window boundary")
|
|
if "g_dirty_ram_exec_pc_table" in capture:
|
|
raise AssertionError("capture still references the removed execution table")
|
|
before_dma = body(capture, "overlay_capture_before_dma")
|
|
commit = before_dma.find("preserve_snapshot_async(evidence_lo, evidence_hi)")
|
|
clear_exec = before_dma.find("memset(&g_dirty_ram_exec_pc_bitmap", commit)
|
|
clear_dispatch = before_dma.find(
|
|
"dirty_ram_dispatch_evidence_clear(", commit)
|
|
if min(commit, clear_exec, clear_dispatch) < 0 or not commit < clear_exec < clear_dispatch:
|
|
raise AssertionError("outgoing variant is not queued before evidence reset")
|
|
if "first_page = lo >> page_shift" not in before_dma:
|
|
raise AssertionError("pre-DMA capture is not scoped to touched whole pages")
|
|
for guard in ("first_page - 1u", "last_page + 1u", "exec_pc_bitmap"):
|
|
if guard not in capture:
|
|
raise AssertionError(f"pre-DMA capture lost executed-page halo: {guard}")
|
|
autocap_tick = body(capture, "overlay_autocapture_tick")
|
|
failed_write = autocap_tick.find("if (!sig && job)")
|
|
clear_owner = autocap_tick.find("s_autocap_write_job = NULL", failed_write)
|
|
retry_pending = autocap_tick.find("if (s_autocap_write_job)", clear_owner)
|
|
enabled_gate = autocap_tick.find("if (!s_autocap_enabled || !s_active)")
|
|
if min(failed_write, clear_owner, retry_pending, enabled_gate) < 0 or not (
|
|
failed_write < clear_owner < retry_pending < enabled_gate):
|
|
raise AssertionError("failed periodic capture is not retained and retried before new epochs")
|
|
failed_branch = autocap_tick[failed_write:clear_owner]
|
|
for required in ("job->attempts++", "job->retry_frame", "return;"):
|
|
if required not in failed_branch:
|
|
raise AssertionError("periodic capture failure can discard its immutable evidence epoch")
|
|
retry_branch = autocap_tick[retry_pending:enabled_gate]
|
|
if ("SDL_AtomicGet(&s_autocap_write_state) == 0" not in retry_branch or
|
|
"autocap_write_launch(job)" not in retry_branch or
|
|
"autocap_write_job_free" in retry_branch or "memset(" in retry_branch):
|
|
raise AssertionError("periodic capture retry mutates or frees newer live evidence")
|
|
provider_try = body(capture, "autocap_provider_request_try")
|
|
accepted = provider_try.find("if (cp->request && cp->request())")
|
|
accepted_sig = provider_try.find("s_autocap_sig_at_req =", accepted)
|
|
retry_sig = provider_try.find("s_autocap_provider_sig_pending = 0", accepted_sig)
|
|
retry_delay = provider_try.find("s_autocap_provider_retry_frame = frame +", retry_sig)
|
|
if min(accepted, accepted_sig, retry_sig, retry_delay) < 0 or not (
|
|
accepted < accepted_sig < retry_sig < retry_delay):
|
|
raise AssertionError("compiler spawn failure can mark durable evidence as requested")
|
|
if "s_autocap_provider_sig_pending = sig" not in autocap_tick:
|
|
raise AssertionError("durable capture is not retained until provider request acceptance")
|
|
wait_pending = body(capture, "overlay_capture_wait_pending")
|
|
if "preserve_snapshot_enqueue_owned(job)" not in wait_pending:
|
|
raise AssertionError("shutdown drops a failed periodic capture instead of draining it")
|
|
dispatch_inner = body(interp, "dirty_ram_dispatch_inner")
|
|
if "(!current_page_dirty || next_page != current_page)" not in dispatch_inner:
|
|
raise AssertionError("page fast path does not preserve clean-miss behavior")
|
|
exec_one = body(interp, "exec_one_fetched")
|
|
for scope_guard in (
|
|
"pc_phys >= 0x1FC00000u && pc_phys < 0x1FC80000u",
|
|
"pc_phys < 0x00010000u",
|
|
"(in_bios_rom || in_bios_kernel_ram)"):
|
|
if scope_guard not in exec_one:
|
|
raise AssertionError(
|
|
f"load-delay value semantics lost BIOS ownership guard: {scope_guard}")
|
|
# RFE backend-contract parity: the recompiled rfe and every overlay shard
|
|
# (ABI v12) call psx_rfe_mark_escape after popping SR, and the generated
|
|
# trampoline runs psx_rfe_escape_check after each return. The interpreter
|
|
# must do BOTH — mark at its RFE case and take the escape at the committed
|
|
# transfer — or an interpreted handler whose EPC lands in dirty RAM keeps
|
|
# interpreting inside psx_check_interrupts' window and in_exception never
|
|
# clears (vblank vetoed forever: the dbg-build boot livelock).
|
|
if "psx_rfe_mark_escape();" not in interp:
|
|
raise AssertionError("interpreter RFE does not honor the rfe escape contract")
|
|
rfe_take = dispatch_inner.find("psx_rfe_escape_check(cpu);")
|
|
local_flow = dispatch_inner.find("allow_local_dirty_flow && target != 0")
|
|
if rfe_take < 0 or local_flow < 0 or rfe_take > local_flow:
|
|
raise AssertionError(
|
|
"interpreter does not take a pending rfe escape before local dirty flow")
|
|
|
|
# Keyed by the full PC and matched by segment since §5.7
|
|
# (docs/SEGMENT_AWARE_CODE.md); the body lives in the per-segment half.
|
|
dispatch = body(loader, "overlay_loader_dispatch_seg")
|
|
cached = dispatch.find("lazy_miss_cached(addr)")
|
|
lookup = dispatch.find("idx_head_seg(phys, seg)")
|
|
record_miss = dispatch.rfind("lazy_miss_record(addr)")
|
|
if min(cached, lookup, record_miss) < 0 or not cached < lookup < record_miss:
|
|
raise AssertionError("negative miss cache is not guarding lookup/recorded at final miss")
|
|
if "lazy_miss_invalidate_loader();" not in body(loader, "overlay_loader_rescan"):
|
|
raise AssertionError("cache rescan does not invalidate negative misses")
|
|
if "lazy_miss_invalidate_loader();" not in body(loader, "load_one_dll_prepared"):
|
|
raise AssertionError("DLL publication does not invalidate negative misses")
|
|
lazy_match = body(loader, "lazy_man_matches")
|
|
if "man_delay_slots_hashed(&lm->fn)" not in lazy_match:
|
|
raise AssertionError("legacy manifests can expose unhashed delay slots")
|
|
rebuild = body(loader, "rebuild_lazy_manifest_index")
|
|
if "overlay_watch_set_range(lo, lm->fn.len[r])" not in rebuild:
|
|
raise AssertionError("unloaded manifest pages are not generation-watched")
|
|
if "manifest_ok = 1" not in rebuild:
|
|
raise AssertionError("strictly parsed manifests are not marked usable")
|
|
if "cache_idx_has_basename" in loader:
|
|
raise AssertionError("immutable repair artifacts are still deduped by logical name")
|
|
scan_one = body(loader, "scan_one_cache_dir")
|
|
for guard in ("cache_name_is_immutable(fd.cFileName)",
|
|
"cache_idx_has_path(full)", "e->logical_crc = crc",
|
|
"e->tier = (uint8_t)tier"):
|
|
if guard not in scan_one:
|
|
raise AssertionError(f"artifact-aware cache indexing lost: {guard}")
|
|
has_crc = body(loader, "overlay_loader_has_cached_crc")
|
|
for guard in ("manifest_ok", "!s_cache_idx[i].load_failed",
|
|
"logical_crc == crc", "lazy_man_crc(",
|
|
"man_delay_slots_hashed("):
|
|
if guard not in has_crc:
|
|
raise AssertionError(f"cached-CRC query accepts unusable artifacts: {guard}")
|
|
try_load = body(loader, "try_load_region")
|
|
if "lazy_is_loadable(li, region_start, seg, phys, 0)" not in try_load:
|
|
raise AssertionError("exact manifest entries are still gated by dirty-run base")
|
|
if "lazy_is_loadable(li, region_start, seg, phys, 1)" not in try_load:
|
|
raise AssertionError("non-exact range fallback lost region narrowing")
|
|
if "lazy_candidate_preferred" not in try_load:
|
|
raise AssertionError("lazy artifact selection is still directory-order dependent")
|
|
for guard in ("load_failed", "goto retry_artifact"):
|
|
if guard not in try_load and guard not in body(loader, "lazy_is_loadable"):
|
|
raise AssertionError(f"failed artifact can suppress a valid fallback: {guard}")
|
|
range_lookup = body(loader, "overlay_find_by_range")
|
|
for guard in ("s_range_candidate_generation", "range_candidate_preferred"):
|
|
if guard not in range_lookup:
|
|
raise AssertionError(f"loaded range selection lost tier/new-artifact ordering: {guard}")
|
|
ac_poll = body(autocompile, "autocompile_poll_main")
|
|
if "one idempotent batch-end" not in ac_poll or "overlay_loader_rescan();" not in ac_poll:
|
|
raise AssertionError("direct shard handoff is not reconciled into the additive index")
|
|
if ("overlay_loader_commit_published(published->image)" not in ac_poll or
|
|
"overlay_loader_load_published(" in ac_poll):
|
|
raise AssertionError("emulation thread regressed to first-mapping published DLLs")
|
|
watcher = body(autocompile, "watch_thread")
|
|
preparer = body(autocompile, "publish_prepare_thread_main")
|
|
if "out_append(buf, (int)got);" not in watcher:
|
|
raise AssertionError("compile watcher no longer drains child output")
|
|
if ("overlay_loader_prepare_published(item->path)" in watcher or
|
|
"overlay_loader_prepare_published(item->path)" not in preparer):
|
|
raise AssertionError("pipe draining and DLL preparation are not separated")
|
|
if ("s_publish_commit_active" not in preparer or
|
|
"s_publish_commit_active++" not in body(autocompile,
|
|
"publish_ready_pop") or
|
|
"publish_commit_finished();" not in ac_poll):
|
|
raise AssertionError("worker mapping can overlap main-thread loader commit")
|
|
if "AC_PUBLISH_CAP" in autocompile:
|
|
raise AssertionError("live publication queue can silently overflow a fixed path ring")
|
|
prepare_image = body(loader, "overlay_loader_prepare_published")
|
|
commit_image = body(loader, "overlay_loader_commit_published")
|
|
validated = prepare_image.find(
|
|
"published_path_valid(dll_path, canon, sizeof(canon))")
|
|
guard_open = prepare_image.find(
|
|
"CreateFileA(canon, GENERIC_READ, FILE_SHARE_READ")
|
|
first_map = prepare_image.find("LoadLibraryA(canon)")
|
|
if min(validated, guard_open, first_map) < 0 or not (
|
|
validated < guard_open < first_map):
|
|
raise AssertionError(
|
|
"Windows published image must be canonical-validated and "
|
|
"guard-held before its worker first-map")
|
|
if "LoadLibraryA(dll_path)" in prepare_image:
|
|
raise AssertionError(
|
|
"worker first-map regressed to mapping raw (non-canonical) pipe text")
|
|
if ("load_one_dll_prepared(canon, handle)" not in commit_image or
|
|
"overlay_library_close(handle)" not in commit_image):
|
|
raise AssertionError("prepared image commit does not consume every speculative reference")
|
|
watched_write = body(memory, "overlay_watch_note_write")
|
|
if "g_dirty_ram_exec_page_bitmap" not in watched_write:
|
|
raise AssertionError("RAM writes do not clear stale per-page capture evidence")
|
|
if "overlay_capture_before_dma" in watched_write:
|
|
raise AssertionError("universal RAM-write hook regressed to snapshot I/O")
|
|
if "overlay_loader_note_code_write()" not in watched_write:
|
|
raise AssertionError("manifested code writes do not invalidate negative misses")
|
|
if "g_dirty_ram_code_gen++" in watched_write:
|
|
raise AssertionError("watched writes churn the interpreter's global code epoch")
|
|
candidate = body(loader, "overlay_loader_is_candidate")
|
|
if "exact_entry_has(phys)" not in candidate or "idx_head" in candidate:
|
|
raise AssertionError("candidate presence regressed from bitmap to hash probes")
|
|
if "cache_path_equal(s_loaded_paths[i], path)" not in body(
|
|
loader, "dll_already_loaded"):
|
|
raise AssertionError("published/scanned path separator aliases can double-load DLLs")
|
|
scheduler = body(traps, "psx_scheduler_run")
|
|
shadow_sched_fixup = scheduler.find(
|
|
"overlay_loader_shadow_scheduler_escape_fixup();")
|
|
scheduler_reset = scheduler.find("g_psx_dispatch_depth = 0")
|
|
if min(shadow_sched_fixup, scheduler_reset) < 0 or shadow_sched_fixup > scheduler_reset:
|
|
raise AssertionError("scheduler longjmp can bypass shadow cleanup")
|
|
shadow_cleanup = body(loader, "shadow_escape_cleanup")
|
|
for guard in ("gte_precision_speculative_end();",
|
|
"g_shadow_mmio_watch = s_shadow_saved_mmio_watch;",
|
|
"s_native_exec = s_shadow_saved_native_exec;",
|
|
"s_suppress_irq = s_shadow_saved_supp;",
|
|
"g_exec_phase = s_shadow_saved_exec_phase;"):
|
|
if guard not in shadow_cleanup:
|
|
raise AssertionError(f"shadow nonlocal cleanup lost: {guard}")
|
|
for switch_fn, mutation in (
|
|
("psx_request_thread_switch", "psx_save_context_to_tcb"),
|
|
("psx_change_thread_fiber", "psx_bind_current_host_thread")):
|
|
switch_body = body(traps, switch_fn)
|
|
bail = switch_body.find(
|
|
"overlay_loader_shadow_native_thread_switch_bail()")
|
|
first_mutation = switch_body.find(mutation)
|
|
if min(bail, first_mutation) < 0 or bail > first_mutation:
|
|
raise AssertionError(
|
|
f"{switch_fn} can mutate scheduler state before shadow bail")
|
|
syscall = body(traps, "psx_syscall")
|
|
for guard in ("switch_result < 0", "return 1;"):
|
|
if guard not in syscall:
|
|
raise AssertionError(f"syscall lost native shadow bail: {guard}")
|
|
shadow_switch_bail = body(
|
|
loader, "overlay_loader_shadow_native_thread_switch_bail")
|
|
for guard in ("s_shadow_scheduler_bail = 1;", "g_psx_call_bail = 1;"):
|
|
if guard not in shadow_switch_bail:
|
|
raise AssertionError(f"native scheduler divergence can escape: {guard}")
|
|
interrupt_poll = body(interrupts, "psx_check_interrupts")
|
|
scheduler_jumps = [m.start() for m in re.finditer(
|
|
r"longjmp\(g_scheduler_jmpbuf", interrupt_poll)]
|
|
if len(scheduler_jumps) != 2:
|
|
raise AssertionError("unexpected direct interrupt scheduler escape count")
|
|
previous_jump = -1
|
|
for jump in scheduler_jumps:
|
|
guard = interrupt_poll.rfind(
|
|
"overlay_loader_shadow_native_thread_switch_bail()", 0, jump)
|
|
if guard <= previous_jump:
|
|
raise AssertionError(
|
|
"direct interrupt scheduler escape bypasses native shadow bail")
|
|
previous_jump = jump
|
|
posix_sweep = body(loader, "posix_abi_sweep_file")
|
|
if not (posix_sweep.find("cache_name_is_immutable(file->name)") <
|
|
posix_sweep.find("psx_overlay_posix_library_open")):
|
|
raise AssertionError("ABI preflight can execute a legacy POSIX constructor")
|
|
abi_sweep = body(loader, "abi_preflight_sweep")
|
|
if not (abi_sweep.find("cache_name_is_immutable(fd.cFileName)") <
|
|
abi_sweep.find("LoadLibraryA(full)")):
|
|
raise AssertionError("ABI preflight can execute a legacy Windows DllMain")
|
|
|
|
interp_step = body(interp, "interp_cyc_step")
|
|
if "g_psx_cycle_fast_limit" not in interp_step or "next <= g_psx_cycle_fast_limit" not in interp_step:
|
|
raise AssertionError("interpreter lost its exact pre-deadline one-cycle fast path")
|
|
if "g_ls_replay_active" not in interp_step or "g_event_step_conservative" not in interp_step:
|
|
raise AssertionError("interpreter cycle fast path does not guard exact diagnostic modes")
|
|
if "g_psx_cycle_fast_limit" in cyc_header:
|
|
raise AssertionError("runtime cycle fast path leaked into the overlay-DLL shared header")
|
|
if "PSX_NO_DEBUG_TOOLS" not in starvation or "STARVATION_RING_ENABLED 0" not in starvation:
|
|
raise AssertionError("production still enables the diagnostic starvation ring")
|
|
dep_mask = body(instr_cost, "psx_cyc_dep_res_mask")
|
|
if ("op_roles[64]" not in dep_mask or "special_roles[64]" not in dep_mask or
|
|
"switch (op)" in dep_mask):
|
|
raise AssertionError("load-delay dependency masks regressed to a second instruction decoder")
|
|
gte_execute = body(gte, "gte_execute")
|
|
direct_import = gte_execute.find("gte_import_cpu_state(>e, cpu);")
|
|
command = gte_execute.find("gte_run_command(>e, cmd);")
|
|
direct_export = gte_execute.find("gte_export_cpu_state(cpu, >e);")
|
|
deadline = gte_execute.find("psx_gte_set(cpu, psx_gte_cmd_latency(cmd));")
|
|
if min(direct_import, command, direct_export, deadline) < 0 or not (
|
|
direct_import < command < direct_export < deadline):
|
|
raise AssertionError("GTE direct marshaling or command deadline ordering regressed")
|
|
gte_marshaling = (gte_execute + body(gte, "gte_import_cpu_state") +
|
|
body(gte, "gte_export_cpu_state"))
|
|
for old_selector in ("gte_mtc2(>e", "gte_ctc2(>e",
|
|
"gte_mfc2(>e", "gte_cfc2(>e"):
|
|
if old_selector in gte_marshaling:
|
|
raise AssertionError("GTE command bridge regressed to selector dispatch loops")
|
|
for fn in ("psx_devices_mmio_sync", "psx_advance_cycles_exact", "psx_cycles_resync_after_restore"):
|
|
if "g_psx_cycle_fast_limit = 0" not in body(cycles, fn):
|
|
raise AssertionError(f"{fn} does not invalidate the inline cycle limit")
|
|
mmio_sync = body(cycles, "psx_devices_mmio_sync")
|
|
if not (mmio_sync.find("psx_devices_service_to_now()") <
|
|
mmio_sync.find("s_next_service_cycle = 0") <
|
|
mmio_sync.find("g_psx_cycle_fast_limit = 0")):
|
|
raise AssertionError("MMIO catch-up can republish a stale inline cycle limit")
|
|
service = body(cycles, "psx_devices_service_to_now")
|
|
if not (service.find("g_psx_cycle_fast_limit = 0") <
|
|
service.find("s_in_device_service = 1") <
|
|
service.find("psx_devices_recompute_deadline()") <
|
|
service.find("s_in_device_service = 0")):
|
|
raise AssertionError("device-service reentrancy can observe a nonzero inline cycle limit")
|
|
load_charge = body(memory, "psx_load_charge_cycles")
|
|
if "next <= g_psx_cycle_fast_limit" not in load_charge or "psx_advance_cycles(cycles)" not in load_charge:
|
|
raise AssertionError("runtime load timing lost its exact deadline fast/fallback paths")
|
|
for guard in ("g_ls_replay_active", "g_event_step_conservative", "next >= psx_cycle_count"):
|
|
if guard not in load_charge:
|
|
raise AssertionError(f"runtime load timing lost guard: {guard}")
|
|
if "!defined(PSX_COSIM) && !STARVATION_RING_ENABLED" not in memory:
|
|
raise AssertionError("runtime load fast path leaks into COSIM/diagnostic builds")
|
|
load_timing = body(memory, "psx_cyc_load_timing")
|
|
if "psx_load_charge_cycles(1u)" not in load_timing or "psx_cyc_base(cpu)" in load_timing:
|
|
raise AssertionError("runtime loads still use the out-of-line one-cycle base charge")
|
|
ram_fast = body(memory, "psx_cyc_main_ram_fast_addr")
|
|
for guard in ("g_ls_mode != 0", "g_ls_replay_active", "g_ds_recording",
|
|
"g_ram_read_watch_active",
|
|
"g_dma_exec_depth > 0", "addr >= 0xC0000000u",
|
|
"phys >= 0x00800000u", "RAM_SIZE - width"):
|
|
if guard not in ram_fast:
|
|
raise AssertionError(f"main-RAM value fast path lost guard: {guard}")
|
|
for fn in ("psx_cyc_load_word", "psx_cyc_load_half", "psx_cyc_load_byte",
|
|
"psx_cyc_lwc2_read"):
|
|
load_body = body(memory, fn)
|
|
timing = load_body.find("psx_cyc_")
|
|
fast = load_body.find("psx_cyc_main_ram_fast_addr")
|
|
fallback = load_body.rfind("psx_read_")
|
|
if min(timing, fast, fallback) < 0 or not timing < fast < fallback:
|
|
raise AssertionError(f"{fn} does not order timing, RAM fast path, fallback")
|
|
if "defined(PSX_NO_DEBUG_TOOLS) && !defined(PSX_COSIM)" not in memory:
|
|
raise AssertionError("main-RAM value fast path leaks into diagnostic/COSIM builds")
|
|
|
|
gte_exec = body(gte, "gte_execute")
|
|
for probe in ("s_gte_exec_count++", "s_gte_caller_ra =",
|
|
"int16_t intpl_pre_ir[4]", "gte_rtp_record(>e, cmd)",
|
|
"gte_intpl_record(>e, intpl_pre_ir, intpl_pre_fc)"):
|
|
pos = gte_exec.find(probe)
|
|
begin = gte_exec.rfind("#ifndef PSX_NO_DEBUG_TOOLS", 0, pos)
|
|
end = gte_exec.find("#endif", pos)
|
|
if pos < 0 or begin < 0 or end < 0:
|
|
raise AssertionError(f"production GTE path still executes diagnostic probe: {probe}")
|
|
|
|
print("PASS: capture/cache and interpreter cycle hot-path guards intact")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
sys.exit(main())
|
|
except AssertionError as exc:
|
|
print(f"FAIL: {exc}")
|
|
sys.exit(1)
|