mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
3e262c128d
The store policy and the CPU side of the out-of-rect VRAM journal move into render_pass_plan.c as pure functions (render_pass_store_to, render_pass_vram_policy, render_pass_journal_add/_rollback); memory.c and gpu_gl_renderer.c only wire them to their arrays and GL copies. Behaviour is unchanged. render_pass_sandbox_test covers what the plan's U5a list asked for: - every device class a pass may not reach (SPU key-on and control, CD, timer, other DMA channels, GP1, SIO, memory control, MDEC, expansion, KSEG2) is dropped under its class and never delivered, while GP0, GP1 DMA mode/info, I_STAT/I_MASK, GPU/OTC DMA and DPCR reach the device; - a real pass (render_pass.c, psx_cycles.c, timers.c) that makes those stores, acknowledges and masks every interrupt, runs 10^6 cycles while timer 2 is armed to interrupt every 1000, and changes a timer behind the store policy's back: drops counted per class, no device advanced, no interrupt raised, RAM, scratchpad, I_STAT/I_MASK, timers and clock restored, and the timer interrupts again once time is live; - fast-forward and backend status refuse the pass without running it; - VRAM writes inside the rect are allowed, outside ones journaled once (covered writes not twice, clipped to VRAM) and rolled back exactly, newest first; a full journal or a mode that cannot journal refuses. Mutations it catches: SPU or CD mis-classed, device service not frozen, rollback order or entry skipped, I_STAT, timer, RAM or scratchpad restore removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
131 lines
6.5 KiB
Python
131 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Pin the render-pass sandbox (docs/RENDER_PASSES.md) at its choke points.
|
|
|
|
A render pass runs guest draw code in frozen time and restores the machine
|
|
afterwards. That only holds if every path that could let time pass, deliver an
|
|
interrupt, touch a device the restore does not cover, or record the pass into
|
|
the live timeline checks g_psx_render_pass_active first. These are the choke
|
|
points; each guard below names the hole it closes.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
import re
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
SRC = ROOT / "runtime" / "src"
|
|
|
|
|
|
def body(text, signature):
|
|
"""Return the first ~40 lines of the function whose definition starts with
|
|
`signature` (enough to see its early-return gates)."""
|
|
i = text.index(signature)
|
|
return "\n".join(text[i:].splitlines()[:40])
|
|
|
|
|
|
cycles = (SRC / "psx_cycles.c").read_text(encoding="utf-8")
|
|
b = body(cycles, "void psx_devices_service_to_now(void) {")
|
|
assert re.search(r"if \(g_psx_render_pass_active\)[^\n]*return;", b), (
|
|
"device servicing must stop during a pass (time would advance)")
|
|
b = body(cycles, "void psx_devices_mmio_sync(void) {")
|
|
assert "g_psx_render_pass_active" in b, (
|
|
"MMIO sync must not catch devices up during a pass")
|
|
b = body(cycles, "void psx_advance_cycles_slow(uint32_t cycles) {")
|
|
assert "g_psx_render_pass_active" in b, (
|
|
"the slow/conservative cycle path must not advance devices during a pass")
|
|
|
|
irq = (SRC / "interrupts.c").read_text(encoding="utf-8")
|
|
for sig in ("int psx_interrupt_delivery_needed(const CPUState* cpu) {",
|
|
"void psx_check_interrupts(CPUState* cpu) {"):
|
|
lines = body(irq, sig).splitlines()[:6]
|
|
assert any("g_psx_render_pass_active" in l and "return" in l for l in lines), (
|
|
"no interrupt may be delivered inside a pass: " + sig)
|
|
|
|
dma = (SRC / "dma.c").read_text(encoding="utf-8")
|
|
assert "while (g_psx_render_pass_active && gpu_linked_list.active)" in dma, (
|
|
"GPU linked-list DMA must complete synchronously in a pass")
|
|
b = body(dma, "static void schedule_delayed_complete(int ch, uint32_t total_words,")
|
|
assert "g_psx_render_pass_active" in b, (
|
|
"delayed DMA completion would never arrive in frozen time")
|
|
|
|
mem = (SRC / "memory.c").read_text(encoding="utf-8")
|
|
for fn in ("void psx_write_word(uint32_t addr, uint32_t val) {",
|
|
"void psx_write_half(uint32_t addr, uint16_t val) {",
|
|
"void psx_write_byte(uint32_t addr, uint8_t val) {"):
|
|
lines = body(mem, fn).splitlines()[:6]
|
|
assert any("render_pass_store" in l for l in lines), (
|
|
"pass stores must bypass live-timeline observers: " + fn)
|
|
b = body(mem, "static void render_pass_store(")
|
|
assert "render_pass_store_to(&t, addr, val, width)" in b and \
|
|
"g_render_pass_dropped_writes[cls]++" in b, (
|
|
"pass stores must go through the tested store policy "
|
|
"(render_pass_store_to, render_pass_sandbox_test) and count drops")
|
|
plan = (SRC / "render_pass_plan.c").read_text(encoding="utf-8")
|
|
assert "render_pass_mmio_class(phys, val, width)" in body(
|
|
plan, "int render_pass_store_to("), (
|
|
"pass MMIO stores must go through the MMIO allow-list")
|
|
gl = (SRC / "gpu_gl_renderer.c").read_text(encoding="utf-8")
|
|
assert "render_pass_vram_policy(&s_pj_cpu" in body(
|
|
gl, "static int pass_refuse_write("), (
|
|
"out-of-rect VRAM writes must go through the tested journal policy")
|
|
assert "render_pass_journal_rollback(&s_pj_cpu" in body(
|
|
gl, "static void pass_journal_rollback("), (
|
|
"the journal rollback must restore the CPU VRAM rows")
|
|
cls = body(plan, "int render_pass_mmio_class(")
|
|
for dev in ("RENDER_PASS_DROP_SPU", "RENDER_PASS_DROP_CD",
|
|
"RENDER_PASS_DROP_TIMER"):
|
|
assert dev in cls, "a pass must never reach " + dev
|
|
|
|
dbg = (SRC / "debug_server.c").read_text(encoding="utf-8")
|
|
for fn in ("void debug_server_trace_write_check(",
|
|
"void debug_server_trace_mmio_write("):
|
|
assert "g_psx_render_pass_active" in body(dbg, fn), (
|
|
"rolled-back pass writes must stay out of the frame fingerprints: " + fn)
|
|
|
|
rp = (SRC / "render_pass.c").read_text(encoding="utf-8")
|
|
for gate in ("psx_netplay_active()", "psx_rewind_is_open()",
|
|
"psx_selfcheck_resim_active()", "psx_get_in_exception()",
|
|
"dma_gpu_linked_list_active()",
|
|
"psx_presentation_fast_forward()"):
|
|
assert gate in rp, "passes must refuse to run when " + gate
|
|
main = (SRC / "main.cpp").read_text(encoding="utf-8")
|
|
ff = main[main.index("const int present_every ="):]
|
|
ff = ff[:ff.index("turbo_skip = (turbo_skip + 1) % present_every;")]
|
|
assert "s_presentation_fast_forward = 1;" in ff, (
|
|
"manual fast-forward must refuse render passes (it presents every 2nd "
|
|
"VBlank, so the plan would otherwise still see presents)")
|
|
|
|
b = body(rp, "static void checkpoint_restore(CPUState *cpu) {")
|
|
assert "nesting_restore(&s_ck.nest);" in b, (
|
|
"a watchdog abort longjmps past the exits of the frames it leaves: the "
|
|
"restore must put the host nesting back (render_pass_abort_test)")
|
|
|
|
gl = (SRC / "gpu_gl_renderer.c").read_text(encoding="utf-8")
|
|
b = body(gl, "static int pass_gen_reserve(int gi, uint32_t need, int w, int h) {")
|
|
assert "need > pass_slot_cap(w, h)" in b, (
|
|
"pass image textures must stay inside the slot cap's memory budget")
|
|
assert "pass_gen_reserve(gi, g->n + 1u, g->tex_w, g->tex_h)" in gl, (
|
|
"pass image textures are made as slots fill, not all PASS_SLOTS up front")
|
|
assert "for (uint32_t i = 0; i < PASS_SLOTS; i++) {\n if (!s_pgen_tex" not in gl, (
|
|
"no eager allocation of every slot texture")
|
|
assert "pass_resources_release();" in body(gl, "void gl_renderer_shutdown(void) {"), (
|
|
"pass textures die with the context: forget them at shutdown")
|
|
|
|
# Default off means no cost for other titles: the headers folded into the
|
|
# overlay codegen hash (runtime/codegen_hash_sources.cmake) must not carry the
|
|
# render-pass API, or every title's overlay cache and savestates would be
|
|
# invalidated by a feature they never use. The freeze lives in the
|
|
# runtime-only psx_cycle_freeze.h.
|
|
INC = ROOT / "runtime" / "include"
|
|
hash_list = (ROOT / "runtime" / "codegen_hash_sources.cmake").read_text(
|
|
encoding="utf-8")
|
|
for header in re.findall(r"runtime/include/([\w.]+)", hash_list):
|
|
text = (INC / header).read_text(encoding="utf-8")
|
|
for name in ("g_psx_render_pass_active", "PsxCycleFreeze",
|
|
"psx_cycle_freeze", "render_pass"):
|
|
assert name not in text, (
|
|
f"{header} is in the codegen hash and must not mention {name}")
|
|
assert "psx_cycle_freeze.h" not in hash_list, (
|
|
"psx_cycle_freeze.h is runtime-only; keep it out of the codegen hash")
|
|
|
|
print("render-pass sandbox guards passed")
|