From 2ba74f3b4b37adbeee797e8c2b50f764e9ba5e7e Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sat, 15 Aug 2026 18:34:47 -0700 Subject: [PATCH] pgxp: land clean-room value-propagation engine (phases 0+1, issue #92) The G1.2/G1.3 mechanism: per-word RAM/scratchpad + per-GPR + per-GTE-reg shadows carrying sub-pixel projections BY PROVENANCE, validated against the actual guest word on every read (never guessed from rounded position - the measured 93.1%-ambiguous G1.4 dead end). Clean-room from psx-spx + public design docs; vendored duckstation (CC BY-NC-ND) and beetle-psx (GPL) trees remain black-box oracles only - no code ported. - runtime/src/pgxp.cpp + pgxp.h: engine, per-vertex GetPreciseVertex (dataflow -> ambiguity-gated position cache -> native) with truncation agreement + tolerance safeguards; stats; test accessors. - pgxp_hooks.h: 5-entry hook funnel; PGXP_*() macros no-op unless -DPSX_PGXP=1 (codegen-time gate over ONE generated tree); appended PGXPHooks* table member in OverlayCallbacks (ABI bump ships with the Phase-2 emitter change). - Both interpreters wired: loads/stores/COP2 + precision-carrying arithmetic (moves, add/sub, or-merge, 16-bit shifts, LUI, HI/LO). - gte.cpp: RTPS feeds the GTE reg shadows; retired s_precise_sxy FIFO, hashed s_precision_store, and plain-store invalidation (redundant under validate-on-read; store hot path gets 3 calls back). - gpu.c: prepare_precise_triangle rewritten per-vertex; perspective UVs share the same provenance and compose per triangle. - [video] pgxp_cpu_mode / pgxp_tolerance keys; TCP pgxp verb live- toggles geometry/texture/cpu_mode/tolerance; geom_correction census grew the pgxp dataflow counters (G1.9 gate metric). - tests: new pgxp_test (registered); gte_register_access_test updated to precise invalidation contract. 33/33 enabled tests green. Also fixed while landing (both broke a fresh master tree): - runtime.cmake: once-only psx_game_version.txt stamp (multi-target file(GENERATE) collision broke every fresh single-config configure). - test_overlay_capture_retry: SDL3 rot (SDL_SetMainReady gone, inverted SDL_Init check) -> psx_sdl_init. Tracked as beads-eio.3.48; postmortem + this landing: ENHANCEMENTS.md G1-G1.10. Co-Authored-By: Claude Fable 5 --- ENHANCEMENTS.md | 71 ++ recompiler/src/config_loader.cpp | 7 + recompiler/src/config_loader.h | 14 + runtime/CMakeLists.txt | 11 + runtime/codegen_hash_sources.cmake | 1 + runtime/include/cpu_state.h | 6 + runtime/include/gpu.h | 1 + runtime/include/overlay_api.h | 10 + runtime/include/pgxp.h | 124 +++ runtime/include/pgxp_hooks.h | 93 +++ runtime/runtime.cmake | 26 +- runtime/src/debug_server.c | 61 +- runtime/src/dirty_ram_interp.c | 118 ++- runtime/src/gpu.c | 93 ++- runtime/src/gte.cpp | 175 ++-- runtime/src/main.cpp | 7 + runtime/src/memory.c | 7 +- runtime/src/pgxp.cpp | 788 +++++++++++++++++++ runtime/src/psx_interpreter.c | 115 ++- runtime/tests/test_gte_register_access.cpp | 10 +- runtime/tests/test_overlay_capture_retry.cpp | 9 +- runtime/tests/test_pgxp.cpp | 294 +++++++ 22 files changed, 1808 insertions(+), 233 deletions(-) create mode 100644 runtime/include/pgxp.h create mode 100644 runtime/include/pgxp_hooks.h create mode 100644 runtime/src/pgxp.cpp create mode 100644 runtime/tests/test_pgxp.cpp diff --git a/ENHANCEMENTS.md b/ENHANCEMENTS.md index 0b5c1507..c00dabda 100644 --- a/ENHANCEMENTS.md +++ b/ENHANCEMENTS.md @@ -766,3 +766,74 @@ defect, which is the whole finding of G1.1–G1.8. The withdrawn row, the full post-mortem, the mechanism that would fix it, and the re-test protocol are preserved on `park/pgxp-geometry-correction` (see its G1.9). + +### G1.10 — PGXP value-propagation engine LANDED, phases 0+1 (2026-08-15) + +The G1.2/G1.3 mechanism — precision travelling WITH the data — now exists +(`feat/pgxp-dataflow`, tracked as `beads-eio.3.48`). **Clean-room**: the +vendored `duckstation/` tree is CC BY-NC-ND (NoDerivatives) and `beetle-psx/` +is GPL, both incompatible with this project's PolyForm Noncommercial license, +so NO code was ported from either; they remain black-box behavioral oracles +only. The engine implements the publicly documented technique from psx-spx + +our own G1 analysis. + +**What exists now:** + +- `runtime/src/pgxp.cpp` + `runtime/include/pgxp.h`: per-word RAM+scratchpad + shadows (~10 MB, lazily allocated, fail-closed), per-GPR (+HI/LO) and + per-GTE-data-reg shadows. Each `PGXPValue` records the sub-pixel 16.16 + screen X/Y, the projected SZ depth, per-half validity flags, and the exact + guest word it describes. **The one safety invariant: a shadow is only + believed after validating that word against reality.** Overwrite-and- + validate only — no side-effect modelling, so DMA/memcpy/untracked writers + need no hooks at all (their stale shadows fail validation at use). The + plain-store `gte_precision_invalidate_word` calls in memory.c are removed + as redundant under this model (a small store-path win). +- Hook funnel (`runtime/include/pgxp_hooks.h`): 5 entry points + (`psx_pgxp_load/store/alu/muldiv/cop2`) taking the raw instruction word. + Generated code will call them via `PGXP_*()` macros that expand to nothing + unless compiled with `-DPSX_PGXP=1` — the G1.3 codegen-time gate, as a + preprocessor define over ONE generated tree (no flavour drift, base objects + byte-identical). `OverlayCallbacks` gained an appended-last `PGXPHooks*` + table (NULL-tolerant); the ABI bump arrives with the Phase-2 emitter change. +- Both interpreters (`dirty_ram_interp.c`, `psx_interpreter.c`) call the + hooks directly on loads/stores/COP2 transfers and the precision-carrying + arithmetic (moves, add/sub, or-merge, 16-bit shifts, LUI, HI/LO). Ops that + merely destroy precision are deliberately NOT hooked — validation already + drops their stale shadows. +- `pgxp_cpu_mode` (tier-2 arithmetic propagation, default off, matching the + references' default) and `pgxp_tolerance` (sub-pixel clamp, default off) + are new `[video]` keys and live-tunable via the TCP `pgxp` verb, which also + live-toggles `geometry`/`texture` for same-scene A/B (the G1.6 method rule + no longer needs a restart between arms). +- GPU consumption is now PER-VERTEX (`pgxp_get_precise_vertex`): dataflow + shadow first (validated against the actual packet word), the G1.4 exact + ambiguity-gated position cache second (never carries depth), native + integers last. Two safeguards on every precise vertex: truncation agreement + (integer part must equal the native 11-bit parse) and the tolerance clamp. + Mixed precise/native triangles are correct — a native vertex sits exactly + where the uncorrected pipeline put it, bounding any seam to < 1px of + sub-pixel fraction (the G1.1 cracks came from wrong-vertex substitution and + all-or-nothing triangles, both gone). Geometry and perspective UVs now + share one provenance source and compose per triangle. +- Census: `geom_correction` grew a `pgxp` object (dataflow_hit / + fallback_hit / native / value_mismatch / trunc_reject / tolerance_reject / + w_valid). The G1.9 gate now reads: dataflow-hit share must be dramatically + above the 5.2% position-cache ceiling before any launcher surface returns. +- Tests: `pgxp_test` (roundtrips, half-word semantics, validate-on-read, + DMA-overwrite rejection, repack arithmetic, suppression bracket, + safeguards) + `gte_register_access_test` updated to the precise + invalidation contract. 33/33 enabled runtime tests green. + +**Not yet (Phase 2+):** emitter macros at the ~47 sites, the `-DPSX_PGXP` +dual link targets + flavor-namespaced shard caches + ABI bump, BIOS regen, +and the Ape/Crash/Tomba2 census + `screenshot_hires` A/B campaign. Until the +emitter lands, compiled code feeds the engine only through the v14 swc2 +sites, so dataflow coverage on a running title comes from interpreted code +paths; the full-coverage census gate is a Phase-2/3 measurement. + +Incidental fixes while landing this: the per-target `psx_game_version.txt` +`file(GENERATE)` collision that broke every fresh single-config configure +(runtime.cmake once-only stamp), and `overlay_capture_retry_test` not +compiling under the SDL3 default (SDL_SetMainReady gone + inverted SDL_Init +check — it now uses `psx_sdl_init`). diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index cd38b9b9..cbc32a81 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -571,6 +571,13 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path& rt.video_perspective_texturing = toml::find(video, "perspective_texturing"); } + if (video.contains("pgxp_cpu_mode")) { + rt.video_pgxp_cpu_mode = toml::find(video, "pgxp_cpu_mode"); + } + if (video.contains("pgxp_tolerance")) { + rt.video_pgxp_tolerance = + toml::find(video, "pgxp_tolerance"); + } if (video.contains("crt_filter")) { const auto mode = toml::find(video, "crt_filter"); if (mode == "raw") rt.video_screen_kind = 0; diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index 1829e024..3663882d 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -387,6 +387,20 @@ struct RuntimeConfig { // [video] perspective_texturing = true. bool video_perspective_texturing = false; + // pgxp_cpu_mode: propagate sub-pixel precision through CPU arithmetic as + // well as memory moves (the PGXP engine's tier-2 hooks). Off by default — + // the same default as the reference implementations — because some games + // deliberately rely on integer truncation in their own math; value + // validation keeps it SAFE either way, this only trades coverage. + // Meaningful only in a pgxp-flavour build; live-tunable over TCP. + bool video_pgxp_cpu_mode = false; + + // pgxp_tolerance: reject a corrected vertex whose sub-pixel offset from + // the native integer position exceeds this many pixels (a last-ditch + // visual clamp; the truncation-agreement check already bounds offsets to + // < 1px). Negative disables the clamp (default). Live-tunable over TCP. + double video_pgxp_tolerance = -1.0; + // offer_vulkan: expose the experimental Vulkan renderer in the launcher. // Defaults false even for Vulkan-enabled builds; developers must opt in per // game once visuals are validated. diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index a5c185cf..f71dff85 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -83,6 +83,7 @@ if(BUILD_TESTING) add_executable(gte_register_access_test tests/test_gte_register_access.cpp src/gte.cpp + src/pgxp.cpp ) target_include_directories(gte_register_access_test PRIVATE include) target_compile_definitions(gte_register_access_test PRIVATE @@ -91,6 +92,16 @@ if(BUILD_TESTING) PSX_ENABLE_BLOCK_CYCLES=1) add_test(NAME gte_register_access_test COMMAND gte_register_access_test) + # PGXP value-propagation engine (ENHANCEMENTS.md G1.2/G1.3): shadow + # load/store/COP2 roundtrips, half-word semantics, validate-on-read + # (foreign overwrite + DMA-overwrite rejection), MOVE/repack arithmetic, + # suppression bracket, invalidate-all, and GetPreciseVertex safeguards + # (truncation agreement + tolerance clamp). White-box: includes src/pgxp.cpp + # and stubs the gte.cpp fallback-cache extern. + add_executable(pgxp_test tests/test_pgxp.cpp src/pgxp.cpp) + target_include_directories(pgxp_test PRIVATE include) + add_test(NAME pgxp_test COMMAND pgxp_test) + add_executable(psx_instr_cost_test tests/test_psx_instr_cost.cpp) target_include_directories(psx_instr_cost_test PRIVATE include) add_test(NAME psx_instr_cost_test COMMAND psx_instr_cost_test) diff --git a/runtime/codegen_hash_sources.cmake b/runtime/codegen_hash_sources.cmake index c6bbd938..3651f39e 100644 --- a/runtime/codegen_hash_sources.cmake +++ b/runtime/codegen_hash_sources.cmake @@ -44,6 +44,7 @@ set(PSXRECOMP_CODEGEN_HASH_SRCS # .inc is the shim every shard links against. ${PSXRECOMP_CODEGEN_HASH_ROOT}/runtime/include/overlay_api.h ${PSXRECOMP_CODEGEN_HASH_ROOT}/runtime/include/cpu_state.h + ${PSXRECOMP_CODEGEN_HASH_ROOT}/runtime/include/pgxp_hooks.h ${PSXRECOMP_CODEGEN_HASH_ROOT}/runtime/include/psx_cyc.h ${PSXRECOMP_CODEGEN_HASH_ROOT}/runtime/include/psx_cycles.h ${PSXRECOMP_CODEGEN_HASH_ROOT}/runtime/include/overlay_dispatch_preamble.c.inc) diff --git a/runtime/include/cpu_state.h b/runtime/include/cpu_state.h index 6de04789..4162db51 100644 --- a/runtime/include/cpu_state.h +++ b/runtime/include/cpu_state.h @@ -220,6 +220,12 @@ extern void gte_geometry_correction_stats(uint32_t *lookups, uint32_t *hits, uint32_t *miss_unrecorded, uint32_t *miss_ambiguous); +/* PGXP dataflow-shadowing hook macros (PGXP_LOAD/STORE/ALU/MULDIV/COP2). + * The emitter writes them unconditionally; they expand to real calls only + * under -DPSX_PGXP=1 (the pgxp build variant) and to ((void)0) otherwise. + * Pulled in here so every generated translation unit sees them. */ +#include "pgxp_hooks.h" + /* ============================================================================ * Dispatch call contract (Bug D / wild-return family fix) * ---------------------------------------------------------------------------- diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index c7f4ed56..153095de 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -310,6 +310,7 @@ int psx_ws_auto_cull_on(void); * tracking it needs, so a polygon only qualifies when every position word in * its DMA packet was written by a GTE projection store. Default off. */ void gpu_texture_correction_set(int enabled); +int gpu_texture_correction_enabled(void); /* Triangles drawn with perspective-correct UVs since startup. */ uint32_t gpu_texture_correction_hits(void); /* GTE-activity gameplay detector ([widescreen] gte_game_mode) for 3D titles diff --git a/runtime/include/overlay_api.h b/runtime/include/overlay_api.h index a0c3a7e8..ae4f8602 100644 --- a/runtime/include/overlay_api.h +++ b/runtime/include/overlay_api.h @@ -8,6 +8,7 @@ */ #include "cpu_state.h" +#include "pgxp_hooks.h" #include /* ABI version exported by every overlay DLL as `overlay_abi()`. The loader @@ -291,6 +292,15 @@ typedef struct { /* Aspect-scaled terrain-frustum angle helper (ABI v21). */ uint32_t (*ws_angle_widen)(uint32_t vanilla); + + /* PGXP dataflow-shadowing hook table (pgxp_hooks.h). Only pgxp-flavour + * shards (PSX_OVERLAY_FLAVOR pgxp bit, compiled with -DPSX_PGXP=1) emit + * calls to psx_pgxp_*; their preamble forwards through this table. Base- + * flavour shards never reference it. Appended LAST; NULL on an older host + * no-ops every hook — precision shadowing is visual-only, never + * load-bearing. The ABI version bump that arms this ships with the + * emitter change (Phase 2 of ENHANCEMENTS.md G1 value propagation). */ + const PGXPHooks *pgxp; } OverlayCallbacks; #ifdef __cplusplus diff --git a/runtime/include/pgxp.h b/runtime/include/pgxp.h new file mode 100644 index 00000000..d8bc2e7e --- /dev/null +++ b/runtime/include/pgxp.h @@ -0,0 +1,124 @@ +#ifndef PGXP_H +#define PGXP_H + +/* PGXP value-propagation engine — runtime-internal API (pgxp.cpp). + * + * The engine shadows every 32-bit word of guest RAM/scratchpad, every GPR, + * and every GTE data register with the sub-pixel projection it carries + * (screen X/Y in 16.16 plus the projected SZ depth). The GTE fills shadows at + * RTPS/RTPT; the psx_pgxp_* hooks (pgxp_hooks.h) move them along with the + * data; the GPU asks for the precise position of each GP0 vertex word by the + * packet's RAM address, validated against the actual word — never guessed + * from the rounded position (the measured G1.4 dead end). + * + * Guest-visible state is NEVER touched: shadows are host-only, dropped on + * savestate/rewind, and suppressed during speculative validation passes. + */ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* --- lifecycle / configuration ------------------------------------------ */ + +/* Master arm. Allocates the RAM shadow lazily; fails closed (stays disabled) + * if the allocation fails. Idempotent. */ +void pgxp_set_enabled(int enabled); +int pgxp_enabled(void); + +/* Tier-2 propagation through CPU arithmetic (default off, like the reference + * implementations' default). Off is SAFE — value validation already stops + * stale shadows — it only bounds coverage. Live-tunable (TCP). */ +void pgxp_set_cpu_mode(int enabled); +int pgxp_cpu_mode(void); + +/* Reject a precise position whose sub-pixel offset from the native integer + * position exceeds this many pixels. < 0 disables the clamp (default). */ +void pgxp_set_tolerance(float pixels); +float pgxp_tolerance(void); + +/* Drop all shadows (savestate load, raw RAM restore, timeline breaks). + * O(1) via generation bump. Deferred while suppressed. */ +void pgxp_invalidate_all(void); + +/* Counted suppression bracket for speculative native-validation passes and + * the GTE replay sandbox: hooks and producers no-op inside it. */ +void pgxp_suppress_begin(void); +void pgxp_suppress_end(void); + +/* --- GTE producer (gte.cpp) ---------------------------------------------- */ + +/* Called at the RTPS/RTPT projection with the pre-truncation 16.16 screen + * coordinates, the projected depth (SZ3), and the packed SXY word the guest + * sees. Shifts the shadow FIFO exactly like push_sxy (regs 12..15). */ +void pgxp_gte_push_sxy(int32_t x16, int32_t y16, uint16_t sz3, uint32_t packed); + +/* Guest write to a GTE data register outside gte_execute (MTC2/CTC2 handled + * by psx_pgxp_cop2; this is for direct gte_write_data paths): reg 15 performs + * the SXYP FIFO push on the shadows, others just invalidate/overwrite. */ +void pgxp_gte_reg_written(int reg, uint32_t value); + +/* --- GPU consumer (gpu.c) ------------------------------------------------ */ + +enum { + PGXP_SRC_NATIVE = 0, /* no shadow — caller uses the parsed integers */ + PGXP_SRC_FALLBACK = 1, /* position-cache fallback (never carries depth)*/ + PGXP_SRC_DATAFLOW = 2, /* address-keyed shadow, value-validated */ +}; + +/* Precise position for one GP0 vertex word. + * addr guest address the word was DMA'd from, or 0xFFFFFFFF if + * unknown (immediate GP0 writes) — skips the dataflow tier. + * packet_word the actual word (validation key). + * int_x/int_y the natively parsed 11-bit positions (pre-draw-offset). + * On DATAFLOW/FALLBACK, *x16/*y16 hold the sub-pixel position (16.16, same + * coordinate space as the packet halves); *sz is the projected depth or 0. + * Safeguards applied here: the integer part must match the native parse + * (truncation agreement) and the tolerance clamp. */ +int pgxp_get_precise_vertex(uint32_t addr, uint32_t packet_word, + int32_t int_x, int32_t int_y, + int32_t *x16, int32_t *y16, uint16_t *sz); + +/* --- observability -------------------------------------------------------- */ + +typedef struct PGXPStats { + uint64_t lookups; /* pgxp_get_precise_vertex calls */ + uint64_t dataflow_hit; + uint64_t fallback_hit; + uint64_t native; + uint64_t value_mismatch; /* shadow present but wrong word (stale) */ + uint64_t trunc_reject; /* integer part disagreed with native parse */ + uint64_t tolerance_reject; + uint64_t w_valid; /* lookups that also carried a usable depth */ +} PGXPStats; + +void pgxp_get_stats(PGXPStats *out); + +/* --- gte.cpp forwarding surface (v14 ABI compat) -------------------------- */ + +/* SWC2 site: copy the GTE register shadow (regs 12..15) to the RAM shadow. */ +void pgxp_store_gte_reg(uint32_t addr, uint8_t reg); + +/* Address-keyed precise-word lookup (perspective texturing path). Returns + * nonzero when the tracked word matches `packed` AND carries a depth. */ +int pgxp_load_precise_word(uint32_t addr, uint32_t packed, + int32_t *x16, int32_t *y16, uint16_t *z); + +/* --- test accessors (always compiled; trivial) ---------------------------- */ + +/* index 0..3 selects the SXY0..SXYP register shadow (GTE data regs 12..15). */ +void pgxp_test_seed_gte_sxy(uint32_t index, uint32_t packed, + int32_t x16, int32_t y16, uint16_t z, int valid); +void pgxp_test_get_gte_sxy(uint32_t index, uint32_t *packed, + int32_t *x16, int32_t *y16, uint16_t *z, + uint8_t *valid); +uint32_t pgxp_test_generation(void); +void pgxp_test_set_generation(uint32_t gen); + +#ifdef __cplusplus +} +#endif + +#endif /* PGXP_H */ diff --git a/runtime/include/pgxp_hooks.h b/runtime/include/pgxp_hooks.h new file mode 100644 index 00000000..257525f1 --- /dev/null +++ b/runtime/include/pgxp_hooks.h @@ -0,0 +1,93 @@ +#ifndef PGXP_HOOKS_H +#define PGXP_HOOKS_H + +/* PGXP dataflow-shadowing hook surface (ENHANCEMENTS.md G1.2/G1.3). + * + * The runtime keeps a host-only "shadow" of sub-pixel GTE projection results + * and follows them BY PROVENANCE as the game moves them through registers and + * RAM into a GP0 packet (pgxp.cpp). Generated code participates by calling + * these hooks after the guest operation completes. + * + * Gating model (the G1.3 decision): the emitter writes PGXP_*() macro + * invocations unconditionally; this header expands them to real calls only + * when the translation unit is compiled with -DPSX_PGXP=1 (the pgxp build + * variant). In the base variant the macros expand to ((void)0), so base + * objects compile to exactly the pre-feature code — the feature costs nothing + * unless a title links the pgxp target. + * + * RULES for emit sites: + * - Macro arguments MUST be side-effect free (they vanish in base builds). + * - Hooks run AFTER the guest architectural effect (value = what actually + * loaded/stored/resulted), so the shadow can validate against reality. + * - `cpu` must be in scope at every emit site (it always is in generated C). + * + * The interpreters (dirty_ram_interp.c, psx_interpreter.c) call the + * psx_pgxp_* functions directly — they are runtime code compiled once, and + * every hook early-outs on a single global when the feature is off. + * + * Overlay DLLs cannot link runtime symbols directly; they forward through the + * PGXPHooks table below (OverlayCallbacks.pgxp, overlay_api.h). A NULL table + * no-ops every hook — precision shadowing is a visual enhancement, never + * load-bearing. + * + * LICENSING: this is a clean-room implementation of the publicly documented + * PGXP technique (psx-spx, public design write-ups). Do NOT port code from + * the vendored duckstation/ (CC BY-NC-ND) or beetle-psx/ (GPL) trees; they + * are black-box behavioral oracles only. See ENHANCEMENTS.md G1. + */ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct CPUState; + +/* Funnel entry points (implemented in runtime/src/pgxp.cpp). The raw + * instruction word carries the op/rs/rt/rd/imm decode so the emitted call is + * uniform per class; `value` is the architectural result of the operation. + * + * load: rt <- [addr]; value = loaded word/half/byte (post-extension) + * store: [addr] <- rt; value = the stored word/half/byte + * alu: rd/rt <- op(s1, s2); result = the written value (also LUI, HILO + * moves and MOVE idioms — the body decodes the class from instr) + * muldiv: HI/LO <- op(s1, s2) + * cop2: MFC2/CFC2/MTC2/CTC2 (addr = 0) and LWC2/SWC2 (addr = guest addr); + * value = the transferred word + */ +void psx_pgxp_load (struct CPUState *cpu, uint32_t instr, uint32_t addr, uint32_t value); +void psx_pgxp_store (struct CPUState *cpu, uint32_t instr, uint32_t addr, uint32_t value); +void psx_pgxp_alu (struct CPUState *cpu, uint32_t instr, uint32_t result, uint32_t s1, uint32_t s2); +void psx_pgxp_muldiv(struct CPUState *cpu, uint32_t instr, uint32_t hi, uint32_t lo, uint32_t s1, uint32_t s2); +void psx_pgxp_cop2 (struct CPUState *cpu, uint32_t instr, uint32_t value, uint32_t addr); + +/* Forwarder table for overlay DLLs (OverlayCallbacks.pgxp). Appended-last + * member semantics apply: a NULL pointer (older host) means "no shadowing". */ +typedef struct PGXPHooks { + void (*load) (struct CPUState *cpu, uint32_t instr, uint32_t addr, uint32_t value); + void (*store) (struct CPUState *cpu, uint32_t instr, uint32_t addr, uint32_t value); + void (*alu) (struct CPUState *cpu, uint32_t instr, uint32_t result, uint32_t s1, uint32_t s2); + void (*muldiv)(struct CPUState *cpu, uint32_t instr, uint32_t hi, uint32_t lo, uint32_t s1, uint32_t s2); + void (*cop2) (struct CPUState *cpu, uint32_t instr, uint32_t value, uint32_t addr); +} PGXPHooks; + +#if defined(PSX_PGXP) && PSX_PGXP +#define PGXP_LOAD(instr, addr, val) psx_pgxp_load(cpu, (instr), (addr), (val)) +#define PGXP_STORE(instr, addr, val) psx_pgxp_store(cpu, (instr), (addr), (val)) +#define PGXP_ALU(instr, res, s1, s2) psx_pgxp_alu(cpu, (instr), (res), (s1), (s2)) +#define PGXP_MULDIV(instr, hi, lo, s1, s2) psx_pgxp_muldiv(cpu, (instr), (hi), (lo), (s1), (s2)) +#define PGXP_COP2(instr, val, addr) psx_pgxp_cop2(cpu, (instr), (val), (addr)) +#else +#define PGXP_LOAD(instr, addr, val) ((void)0) +#define PGXP_STORE(instr, addr, val) ((void)0) +#define PGXP_ALU(instr, res, s1, s2) ((void)0) +#define PGXP_MULDIV(instr, hi, lo, s1, s2) ((void)0) +#define PGXP_COP2(instr, val, addr) ((void)0) +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* PGXP_HOOKS_H */ diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index 8a49dda9..2c9ae0e4 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -299,6 +299,7 @@ set(PSXRECOMP_RUNTIME_SOURCES ${PSXRECOMP_ROOT}/runtime/src/crash_trace.c ${PSXRECOMP_ROOT}/runtime/src/freeze_heartbeat.c ${PSXRECOMP_ROOT}/runtime/src/gte.cpp + ${PSXRECOMP_ROOT}/runtime/src/pgxp.cpp ${PSXRECOMP_ROOT}/runtime/src/nd_intro_ot.c ${PSXRECOMP_ROOT}/runtime/src/crc32.c ${PSXRECOMP_ROOT}/runtime/src/psx_sha256.c @@ -1169,16 +1170,27 @@ function(psxrecomp_add_runtime_target target) # second GENERATE to the same path is rejected ("Files to be generated by # multiple different commands"). Multi-config (VS): exe lives in Release/, # so also drop a copy at the build root for packager lookup. - file(GENERATE - OUTPUT "$/psx_game_version.txt" - CONTENT "${PSXRT_GAME_VERSION}\n" - ) - get_property(_psxrt_ver_multi GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) - if(_psxrt_ver_multi) + # Once per build tree, not per target: with several runtime targets + # (psx-runtime + psx-oracle + psx-beetle) each TARGET_FILE_DIR genex is a + # DIFFERENT command producing the SAME path on single-config generators, + # which CMake rejects even when the content is identical (the ae9e6a4e + # hotfix fixed the same-target duplicate but not the multi-target one). + # All runtime targets in one tree share one PSXRT_GAME_VERSION, so a + # single stamp is the correct semantics anyway. + get_property(_psxrt_ver_stamped GLOBAL PROPERTY PSXRT_GAME_VERSION_STAMPED) + if(NOT _psxrt_ver_stamped) + set_property(GLOBAL PROPERTY PSXRT_GAME_VERSION_STAMPED 1) file(GENERATE - OUTPUT "${CMAKE_BINARY_DIR}/psx_game_version.txt" + OUTPUT "$/psx_game_version.txt" CONTENT "${PSXRT_GAME_VERSION}\n" ) + get_property(_psxrt_ver_multi GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) + if(_psxrt_ver_multi) + file(GENERATE + OUTPUT "${CMAKE_BINARY_DIR}/psx_game_version.txt" + CONTENT "${PSXRT_GAME_VERSION}\n" + ) + endif() endif() # OpenBIOS is part of the native runtime product, not a developer-machine diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 61cd2464..566e42d4 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -24,6 +24,7 @@ #include "code_provider.h" #include "overlay_backend.h" #include "cpu_state.h" +#include "pgxp.h" #include "dma.h" #include "gpu.h" #include "gpu_render.h" /* gr_scale + gr_render_display_hires (screenshot_hires) */ @@ -5196,16 +5197,71 @@ static void handle_geom_correction(int id, const char *json) * distinct vertices are landing on the same pixel. */ uint32_t lookups = 0, hits = 0, unrec = 0, ambig = 0; gte_geometry_correction_stats(&lookups, &hits, &unrec, &ambig); + /* PGXP dataflow census (per-vertex): the primary provenance source. + * dataflow_hit is the number that had to move — the G1.9 gate is a + * dataflow-hit share dramatically above the 5.2% the position table + * measured on its own. value_mismatch counts shadows that were present + * but described a different word (stale = provenance hole to hunt). */ + PGXPStats ps; + pgxp_get_stats(&ps); send_fmt("{\"id\":%d,\"ok\":true," "\"geometry_correction\":%d," "\"geometry_vertex_hits\":%u," "\"perspective_triangles\":%u," - "\"lookups\":%u,\"miss_unrecorded\":%u,\"miss_ambiguous\":%u}", + "\"lookups\":%u,\"miss_unrecorded\":%u,\"miss_ambiguous\":%u," + "\"pgxp\":{\"enabled\":%d,\"cpu_mode\":%d,\"tolerance\":%.3f," + "\"lookups\":%llu,\"dataflow_hit\":%llu,\"fallback_hit\":%llu," + "\"native\":%llu,\"value_mismatch\":%llu,\"trunc_reject\":%llu," + "\"tolerance_reject\":%llu,\"w_valid\":%llu}}", id, gte_geometry_correction_enabled(), (unsigned)hits, (unsigned)gpu_texture_correction_hits(), - (unsigned)lookups, (unsigned)unrec, (unsigned)ambig); + (unsigned)lookups, (unsigned)unrec, (unsigned)ambig, + pgxp_enabled(), pgxp_cpu_mode(), (double)pgxp_tolerance(), + (unsigned long long)ps.lookups, + (unsigned long long)ps.dataflow_hit, + (unsigned long long)ps.fallback_hit, + (unsigned long long)ps.native, + (unsigned long long)ps.value_mismatch, + (unsigned long long)ps.trunc_reject, + (unsigned long long)ps.tolerance_reject, + (unsigned long long)ps.w_valid); +} + +/* pgxp — live-tune the value-propagation engine for one-toggle isolation runs + * without a rebuild: {"cmd":"pgxp","cpu_mode":0|1,"tolerance":F}. Fields are + * optional; the reply echoes the resulting state (same shape as + * geom_correction's "pgxp" object, flattened). */ +static void handle_pgxp(int id, const char *json) +{ + /* Live toggles for the one-toggle-at-a-time A/B protocol (ENHANCEMENTS.md + * G1.6 method rule): same scene, flip one knob, screenshot_hires. */ + int geom = json_get_int(json, "geometry", -1); + if (geom >= 0) + gte_geometry_correction_set(geom != 0); + int tex = json_get_int(json, "texture", -1); + if (tex >= 0) + gpu_texture_correction_set(tex != 0); + if (geom >= 0 && tex < 0) { + /* keep the engine armed consistently with both flags */ + gpu_texture_correction_set(gpu_texture_correction_enabled()); + } + int cm = json_get_int(json, "cpu_mode", -1); + if (cm >= 0) + pgxp_set_cpu_mode(cm != 0); + /* tolerance is fractional (sub-pixel), so scan it directly — json_get_int + * would truncate 0.5 to 0. */ + const char *p = strstr(json, "\"tolerance\""); + if (p) { + p += 11; + while (*p == ' ' || *p == ':' || *p == '"') p++; + if (*p == '-' || (*p >= '0' && *p <= '9') || *p == '.') + pgxp_set_tolerance((float)strtod(p, NULL)); + } + send_fmt("{\"id\":%d,\"ok\":true,\"enabled\":%d,\"cpu_mode\":%d," + "\"tolerance\":%.3f}", + id, pgxp_enabled(), pgxp_cpu_mode(), (double)pgxp_tolerance()); } static void handle_gpu_state(int id, const char *json) @@ -13163,6 +13219,7 @@ static const CmdEntry s_commands[] = { { "write_ram", handle_write_ram }, { "gpu_state", handle_gpu_state }, { "geom_correction", handle_geom_correction }, + { "pgxp", handle_pgxp }, { "ws_aspect_cone_site", handle_ws_aspect_cone_site }, { "ws_margin", handle_ws_margin }, { "ws_hud_mode", handle_ws_hud_mode }, diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index fc3c5ad0..377f35aa 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -1512,30 +1512,48 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, switch (opc) { case 0x00: /* SPECIAL */ switch (fnt) { - case 0x00: /* SLL rd, rt, sh (also nop when all fields are 0) */ - cpu->gpr[rd] = cpu->gpr[rt] << sh; + case 0x00: { /* SLL rd, rt, sh (also nop when all fields are 0) */ + uint32_t a = cpu->gpr[rt]; + cpu->gpr[rd] = a << sh; + psx_pgxp_alu(cpu, insn, cpu->gpr[rd], a, sh); cpu->gpr[0] = 0; return 0; - case 0x02: /* SRL */ - cpu->gpr[rd] = cpu->gpr[rt] >> sh; + } + case 0x02: { /* SRL */ + uint32_t a = cpu->gpr[rt]; + cpu->gpr[rd] = a >> sh; + psx_pgxp_alu(cpu, insn, cpu->gpr[rd], a, sh); cpu->gpr[0] = 0; return 0; - case 0x03: /* SRA */ - cpu->gpr[rd] = (uint32_t)((int32_t)cpu->gpr[rt] >> sh); + } + case 0x03: { /* SRA */ + uint32_t a = cpu->gpr[rt]; + cpu->gpr[rd] = (uint32_t)((int32_t)a >> sh); + psx_pgxp_alu(cpu, insn, cpu->gpr[rd], a, sh); cpu->gpr[0] = 0; return 0; - case 0x04: /* SLLV */ - cpu->gpr[rd] = cpu->gpr[rt] << (cpu->gpr[rs] & 31); + } + case 0x04: { /* SLLV */ + uint32_t a = cpu->gpr[rt], b = cpu->gpr[rs]; + cpu->gpr[rd] = a << (b & 31); + psx_pgxp_alu(cpu, insn, cpu->gpr[rd], a, b); cpu->gpr[0] = 0; return 0; - case 0x06: /* SRLV */ - cpu->gpr[rd] = cpu->gpr[rt] >> (cpu->gpr[rs] & 31); + } + case 0x06: { /* SRLV */ + uint32_t a = cpu->gpr[rt], b = cpu->gpr[rs]; + cpu->gpr[rd] = a >> (b & 31); + psx_pgxp_alu(cpu, insn, cpu->gpr[rd], a, b); cpu->gpr[0] = 0; return 0; - case 0x07: /* SRAV */ - cpu->gpr[rd] = (uint32_t)((int32_t)cpu->gpr[rt] >> (cpu->gpr[rs] & 31)); + } + case 0x07: { /* SRAV */ + uint32_t a = cpu->gpr[rt], b = cpu->gpr[rs]; + cpu->gpr[rd] = (uint32_t)((int32_t)a >> (b & 31)); + psx_pgxp_alu(cpu, insn, cpu->gpr[rd], a, b); cpu->gpr[0] = 0; return 0; + } case 0x08: { /* JR rs */ uint32_t target = cpu->gpr[rs]; if (target & 3) return interp_exception(cpu, 4, target, pc); /* LoadAddressError */ @@ -1615,25 +1633,30 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, psx_muldiv_stall(cpu); /* stall to mult/div completion (faithful) */ #endif cpu->gpr[rd] = cpu->hi; + psx_pgxp_alu(cpu, insn, cpu->gpr[rd], cpu->hi, 0); cpu->gpr[0] = 0; return 0; case 0x11: /* MTHI */ cpu->hi = cpu->gpr[rs]; + psx_pgxp_alu(cpu, insn, cpu->hi, cpu->hi, 0); return 0; case 0x12: /* MFLO */ #ifdef PSX_ENABLE_BLOCK_CYCLES psx_muldiv_stall(cpu); /* stall to mult/div completion (faithful) */ #endif cpu->gpr[rd] = cpu->lo; + psx_pgxp_alu(cpu, insn, cpu->gpr[rd], cpu->lo, 0); cpu->gpr[0] = 0; return 0; case 0x13: /* MTLO */ cpu->lo = cpu->gpr[rs]; + psx_pgxp_alu(cpu, insn, cpu->lo, cpu->lo, 0); return 0; case 0x18: { /* MULT */ int64_t r = (int64_t)(int32_t)cpu->gpr[rs] * (int64_t)(int32_t)cpu->gpr[rt]; cpu->lo = (uint32_t)r; cpu->hi = (uint32_t)((uint64_t)r >> 32); + psx_pgxp_muldiv(cpu, insn, cpu->hi, cpu->lo, cpu->gpr[rs], cpu->gpr[rt]); #ifdef PSX_ENABLE_BLOCK_CYCLES psx_muldiv_set(cpu, psx_mult_latency_s(cpu->gpr[rs])); /* completion deadline */ #endif @@ -1643,6 +1666,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, uint64_t r = (uint64_t)cpu->gpr[rs] * (uint64_t)cpu->gpr[rt]; cpu->lo = (uint32_t)r; cpu->hi = (uint32_t)(r >> 32); + psx_pgxp_muldiv(cpu, insn, cpu->hi, cpu->lo, cpu->gpr[rs], cpu->gpr[rt]); #ifdef PSX_ENABLE_BLOCK_CYCLES psx_muldiv_set(cpu, psx_mult_latency_u(cpu->gpr[rs])); /* completion deadline */ #endif @@ -1661,6 +1685,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, cpu->lo = (uint32_t)(a / b); cpu->hi = (uint32_t)(a % b); } + psx_pgxp_muldiv(cpu, insn, cpu->hi, cpu->lo, cpu->gpr[rs], cpu->gpr[rt]); #ifdef PSX_ENABLE_BLOCK_CYCLES psx_muldiv_set(cpu, 37u); /* DIV completion deadline (fixed) */ #endif @@ -1674,31 +1699,41 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, cpu->lo = cpu->gpr[rs] / cpu->gpr[rt]; cpu->hi = cpu->gpr[rs] % cpu->gpr[rt]; } + psx_pgxp_muldiv(cpu, insn, cpu->hi, cpu->lo, cpu->gpr[rs], cpu->gpr[rt]); #ifdef PSX_ENABLE_BLOCK_CYCLES psx_muldiv_set(cpu, 37u); /* DIVU completion deadline (fixed) */ #endif return 0; case 0x20: /* ADD - overflow traps are delegated if they occur. */ - case 0x21: /* ADDU rd, rs, rt */ - cpu->gpr[rd] = cpu->gpr[rs] + cpu->gpr[rt]; + case 0x21: { /* ADDU rd, rs, rt */ + uint32_t a = cpu->gpr[rs], b = cpu->gpr[rt]; + cpu->gpr[rd] = a + b; + psx_pgxp_alu(cpu, insn, cpu->gpr[rd], a, b); cpu->gpr[0] = 0; return 0; + } case 0x22: /* SUB - overflow traps are delegated if they occur. */ - case 0x23: /* SUBU */ + case 0x23: { /* SUBU */ + uint32_t a = cpu->gpr[rs], b = cpu->gpr[rt]; if (rs == 0 && psx_ws_is_cull_negsub_site(pc)) - cpu->gpr[rd] = 0u - cpu->gpr[rt] - (uint32_t)psx_ws_x_margin(); + cpu->gpr[rd] = 0u - b - (uint32_t)psx_ws_x_margin(); else - cpu->gpr[rd] = cpu->gpr[rs] - cpu->gpr[rt]; + cpu->gpr[rd] = a - b; + psx_pgxp_alu(cpu, insn, cpu->gpr[rd], a, b); cpu->gpr[0] = 0; return 0; + } case 0x24: /* AND */ cpu->gpr[rd] = cpu->gpr[rs] & cpu->gpr[rt]; cpu->gpr[0] = 0; return 0; - case 0x25: /* OR */ - cpu->gpr[rd] = cpu->gpr[rs] | cpu->gpr[rt]; + case 0x25: { /* OR */ + uint32_t a = cpu->gpr[rs], b = cpu->gpr[rt]; + cpu->gpr[rd] = a | b; + psx_pgxp_alu(cpu, insn, cpu->gpr[rd], a, b); cpu->gpr[0] = 0; return 0; + } case 0x26: /* XOR */ cpu->gpr[rd] = cpu->gpr[rs] ^ cpu->gpr[rt]; cpu->gpr[0] = 0; @@ -1852,24 +1887,28 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, case 0x08: /* ADDI rt, rs, simm — same as ADDIU, sans overflow trap (we don't model traps here) */ { uint32_t widened = 0; + uint32_t a = cpu->gpr[rs]; if (psx_ws_angle_site(pc, insn, &widened)) cpu->gpr[rt] = widened; else - cpu->gpr[rt] = cpu->gpr[rs] + (uint32_t)simm + cpu->gpr[rt] = a + (uint32_t)simm + (psx_ws_is_cull_bias_site(pc) ? (uint32_t)psx_ws_activation_margin() : 0u); + psx_pgxp_alu(cpu, insn, cpu->gpr[rt], a, (uint32_t)simm); cpu->gpr[0] = 0; return 0; } case 0x09: /* ADDIU rt, rs, simm */ { uint32_t widened = 0; + uint32_t a = cpu->gpr[rs]; if (psx_ws_angle_site(pc, insn, &widened)) cpu->gpr[rt] = widened; else - cpu->gpr[rt] = cpu->gpr[rs] + (uint32_t)simm + cpu->gpr[rt] = a + (uint32_t)simm + (psx_ws_is_cull_bias_site(pc) ? (uint32_t)psx_ws_activation_margin() : 0u); + psx_pgxp_alu(cpu, insn, cpu->gpr[rt], a, (uint32_t)simm); cpu->gpr[0] = 0; return 0; } @@ -1930,10 +1969,13 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, cpu->gpr[rt] = cpu->gpr[rs] & imm; cpu->gpr[0] = 0; return 0; - case 0x0D: /* ORI */ - cpu->gpr[rt] = cpu->gpr[rs] | imm; + case 0x0D: { /* ORI */ + uint32_t a = cpu->gpr[rs]; + cpu->gpr[rt] = a | imm; + psx_pgxp_alu(cpu, insn, cpu->gpr[rt], a, imm); cpu->gpr[0] = 0; return 0; + } case 0x0E: /* XORI */ cpu->gpr[rt] = cpu->gpr[rs] ^ imm; cpu->gpr[0] = 0; @@ -1943,6 +1985,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, cpu->gpr[rt] = (uint32_t)psx_ws_player_x_bound((int32_t)(imm << 16)); else cpu->gpr[rt] = imm << 16; + psx_pgxp_alu(cpu, insn, cpu->gpr[rt], 0, 0); cpu->gpr[0] = 0; return 0; case 0x10: { /* COP0 */ @@ -2033,6 +2076,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, psx_gte_read(cpu, rt); #endif cpu->gpr[rt] = gte_read_data(cpu, (uint8_t)rd); + psx_pgxp_cop2(cpu, insn, cpu->gpr[rt], 0); cpu->gpr[0] = 0; return 0; } @@ -2041,6 +2085,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, psx_gte_read(cpu, rt); #endif cpu->gpr[rt] = gte_read_ctrl(cpu, (uint8_t)rd); + psx_pgxp_cop2(cpu, insn, cpu->gpr[rt], 0); cpu->gpr[0] = 0; return 0; } @@ -2049,6 +2094,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, psx_gte_stall(cpu); #endif gte_write_data(cpu, (uint8_t)rd, cpu->gpr[rt]); + psx_pgxp_cop2(cpu, insn, cpu->gpr[rt], 0); return 0; } if (cop_op == 0x06) { /* CTC2 */ @@ -2072,6 +2118,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, case 0x20: { /* LB rt, simm(rs) */ uint32_t addr = cpu->gpr[rs] + (uint32_t)simm; cpu->gpr[rt] = (uint32_t)(int32_t)(int8_t)psx_cyc_load_byte(cpu, addr, rt, 1u << rs); + psx_pgxp_load(cpu, insn, addr, cpu->gpr[rt]); cpu->gpr[0] = 0; return 0; } @@ -2079,6 +2126,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, uint32_t addr = cpu->gpr[rs] + (uint32_t)simm; if (addr & 1) return interp_exception(cpu, 4, addr, pc); /* LoadAddressError */ cpu->gpr[rt] = (uint32_t)(int32_t)(int16_t)psx_cyc_load_half(cpu, addr, rt, 1u << rs); + psx_pgxp_load(cpu, insn, addr, cpu->gpr[rt]); cpu->gpr[0] = 0; return 0; } @@ -2086,6 +2134,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, uint32_t addr = cpu->gpr[rs] + (uint32_t)simm; uint32_t word = psx_cyc_load_word(cpu, addr & ~3u, rt, 1u << rs); cpu->gpr[rt] = lwl_merge(addr, word, cpu->gpr[rt]); + psx_pgxp_load(cpu, insn, addr, cpu->gpr[rt]); cpu->gpr[0] = 0; return 0; } @@ -2101,12 +2150,14 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, cpu->gpr[rt] = psx_ws_xclip_bound(psx_cyc_load_word(cpu, addr, rt, 1u << rs)); else cpu->gpr[rt] = psx_cyc_load_word(cpu, addr, rt, 1u << rs); + psx_pgxp_load(cpu, insn, addr, cpu->gpr[rt]); cpu->gpr[0] = 0; return 0; } case 0x24: { /* LBU */ uint32_t addr = cpu->gpr[rs] + (uint32_t)simm; cpu->gpr[rt] = (uint32_t)psx_cyc_load_byte(cpu, addr, rt, 1u << rs); + psx_pgxp_load(cpu, insn, addr, cpu->gpr[rt]); cpu->gpr[0] = 0; return 0; } @@ -2114,6 +2165,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, uint32_t addr = cpu->gpr[rs] + (uint32_t)simm; if (addr & 1) return interp_exception(cpu, 4, addr, pc); /* LoadAddressError */ cpu->gpr[rt] = (uint32_t)psx_cyc_load_half(cpu, addr, rt, 1u << rs); + psx_pgxp_load(cpu, insn, addr, cpu->gpr[rt]); cpu->gpr[0] = 0; return 0; } @@ -2121,6 +2173,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, uint32_t addr = cpu->gpr[rs] + (uint32_t)simm; uint32_t word = psx_cyc_load_word(cpu, addr & ~3u, rt, 1u << rs); cpu->gpr[rt] = lwr_merge(addr, word, cpu->gpr[rt]); + psx_pgxp_load(cpu, insn, addr, cpu->gpr[rt]); cpu->gpr[0] = 0; return 0; } @@ -2128,6 +2181,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, uint32_t addr = cpu->gpr[rs] + (uint32_t)simm; g_debug_last_store_pc = pc; cpu->write_byte(addr, (uint8_t)cpu->gpr[rt]); + psx_pgxp_store(cpu, insn, addr, cpu->gpr[rt] & 0xFFu); return 0; } case 0x29: { /* SH */ @@ -2143,12 +2197,14 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, val = (uint16_t)psx_ws_backdrop_x((int16_t)val); g_debug_last_store_pc = pc; cpu->write_half(addr, val); + psx_pgxp_store(cpu, insn, addr, val); return 0; } case 0x2A: { /* SWL */ uint32_t addr = cpu->gpr[rs] + (uint32_t)simm; g_debug_last_store_pc = pc; interp_swl(cpu, addr, cpu->gpr[rt]); + psx_pgxp_store(cpu, insn, addr, cpu->gpr[rt]); return 0; } case 0x2B: { /* SW */ @@ -2156,12 +2212,14 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, if (addr & 3) return interp_exception(cpu, 5, addr, pc); /* StoreAddressError */ g_debug_last_store_pc = pc; cpu->write_word(addr, cpu->gpr[rt]); + psx_pgxp_store(cpu, insn, addr, cpu->gpr[rt]); return 0; } case 0x2E: { /* SWR */ uint32_t addr = cpu->gpr[rs] + (uint32_t)simm; g_debug_last_store_pc = pc; interp_swr(cpu, addr, cpu->gpr[rt]); + psx_pgxp_store(cpu, insn, addr, cpu->gpr[rt]); return 0; } case 0x32: { /* LWC2 — §1+DO_LDS charged by exec_one's top step (mask 0) */ @@ -2169,7 +2227,11 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, #ifdef PSX_ENABLE_BLOCK_CYCLES psx_gte_stall(cpu); /* COP2 reg write stalls to GTE completion */ #endif - gte_write_data(cpu, (uint8_t)rt, psx_cyc_lwc2_read(cpu, addr)); + { + uint32_t lw2 = psx_cyc_lwc2_read(cpu, addr); + gte_write_data(cpu, (uint8_t)rt, lw2); + psx_pgxp_cop2(cpu, insn, lw2, addr); + } return 0; } case 0x3A: { /* SWC2 */ @@ -2178,8 +2240,12 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn, psx_gte_stall(cpu); /* COP2 reg read stalls to GTE completion */ #endif g_debug_last_store_pc = pc; - cpu->write_word(addr, gte_read_data(cpu, (uint8_t)rt)); - gte_precision_store_word(addr, (uint8_t)rt); + { + uint32_t sw2 = gte_read_data(cpu, (uint8_t)rt); + cpu->write_word(addr, sw2); + gte_precision_store_word(addr, (uint8_t)rt); + psx_pgxp_cop2(cpu, insn, sw2, addr); + } return 0; } default: diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index 99f926f6..4bf9597b 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -11,6 +11,7 @@ */ #include "gpu.h" +#include "pgxp.h" #include "mod_memory.h" #include "gpu_primitive_reject.h" #include "gpu_sw_renderer.h" @@ -2956,48 +2957,64 @@ static void parse_vertex(uint32_t word, int32_t* x, int32_t* y) { *y = sign_extend((word >> 16) & 0x7FFu, 11); } -extern int gte_geometry_correction_lookup(uint32_t packed, - int32_t *x16, int32_t *y16); extern int gte_geometry_correction_enabled(void); static int s_texture_correction_enabled = 0; -extern void gte_precision_tracking_set(int enabled); extern int gte_precision_load_word(uint32_t addr, uint32_t packed, int32_t *x16, int32_t *y16, uint16_t *z); void gpu_texture_correction_set(int enabled) { s_texture_correction_enabled = enabled ? 1 : 0; - gte_precision_tracking_set(enabled); + /* The PGXP dataflow engine feeds BOTH corrections; arm it while either + * is on (geometry correction is toggled in gte.cpp, so re-derive here). */ + pgxp_set_enabled(s_texture_correction_enabled || + gte_geometry_correction_enabled()); +} + +int gpu_texture_correction_enabled(void) { + return s_texture_correction_enabled; } uint32_t gpu_texture_correction_hits(void) { return sw_perspective_triangle_count(); } -/* Match all three GP0 positions to recent GTE projections. Requiring a full - * triangle match prevents screen-space HUD/sprites that happen to share one - * coordinate from receiving world-geometry correction. The integer delta - * folds in draw offsets and any widescreen adjustment already applied. */ -static void prepare_precise_triangle(uint32_t w0, uint32_t w1, uint32_t w2, +/* Per-vertex precise positions (PGXP, ENHANCEMENTS.md G1). Each of the three + * packet words is resolved independently: the address-keyed dataflow shadow + * first (validated against the actual word — exact provenance, survives + * ordering-table reordering), the ambiguity-gated position cache second, the + * parsed integers last. Mixing precise and native vertices in one triangle + * is correct — a native vertex is exactly where the uncorrected pipeline put + * it, so shared edges between neighbouring triangles cannot disagree by more + * than the sub-pixel fraction. The integer delta folds in draw offsets and + * any widescreen adjustment already applied by the caller. */ +static void prepare_precise_triangle(int i0, int i1, int i2, const int32_t vx[3], const int32_t vy[3]) { - uint32_t words[3] = { w0, w1, w2 }; - int32_t fx[3], fy[3]; - const int geometry_enabled = gte_geometry_correction_enabled(); gr_set_perspective_triangle(0, 0.0f, 0.0f, 0.0f); - if (!geometry_enabled) { + if (!gte_geometry_correction_enabled()) { gr_set_precise_triangle(0, 0,0, 0,0, 0,0); return; } + const int idx[3] = { i0, i1, i2 }; + int32_t fx[3], fy[3]; + int any_precise = 0; for (int i = 0; i < 3; i++) { + uint32_t word = gp0_cmd_buf[idx[i]]; int32_t raw_x, raw_y; - parse_vertex(words[i], &raw_x, &raw_y); - if (!gte_geometry_correction_lookup(words[i], &fx[i], &fy[i])) { - gr_set_precise_triangle(0, 0,0, 0,0, 0,0); - return; - } - fx[i] = (int32_t)((int64_t)fx[i] + - (int64_t)(vx[i] - raw_x) * 65536); - fy[i] = (int32_t)((int64_t)fy[i] + - (int64_t)(vy[i] - raw_y) * 65536); + parse_vertex(word, &raw_x, &raw_y); + uint32_t addr = (gp0_cmd_source_addr == 0xFFFFFFFFu) + ? 0xFFFFFFFFu + : gp0_cmd_source_addr + (uint32_t)idx[i] * 4u; + int32_t px, py; + uint16_t sz; + if (pgxp_get_precise_vertex(addr, word, raw_x, raw_y, + &px, &py, &sz) != PGXP_SRC_NATIVE) + any_precise = 1; + fx[i] = (int32_t)((int64_t)px + (int64_t)(vx[i] - raw_x) * 65536); + fy[i] = (int32_t)((int64_t)py + (int64_t)(vy[i] - raw_y) * 65536); + } + if (!any_precise) { + gr_set_precise_triangle(0, 0,0, 0,0, 0,0); + return; } gr_set_precise_triangle(1, fx[0],fy[0], fx[1],fy[1], fx[2],fy[2]); } @@ -3209,7 +3226,7 @@ static void gp0_exec_mono_tri(void) { } if (draw_area_out_bbox(vx, vy, 3)) return; gr_set_semi_transparency(semi_trans, (int)semi_transparency); - prepare_precise_triangle(gp0_cmd_buf[1], gp0_cmd_buf[2], gp0_cmd_buf[3], + prepare_precise_triangle(1, 2, 3, vx, vy); gr_draw_flat_triangle(vx[0], vy[0], vx[1], vy[1], vx[2], vy[2], color); } @@ -3275,15 +3292,13 @@ static void gp0_exec_mono_quad(void) { if (!rej_a) { int32_t tx[3] = { vx[0], vx[1], vx[2] }; int32_t ty[3] = { vy[0], vy[1], vy[2] }; - prepare_precise_triangle(gp0_cmd_buf[1], gp0_cmd_buf[2], - gp0_cmd_buf[3], tx, ty); + prepare_precise_triangle(1, 2, 3, tx, ty); gr_draw_flat_triangle(vx[0], vy[0], vx[1], vy[1], vx[2], vy[2], color); } if (!rej_b) { int32_t tx[3] = { vx[2], vx[1], vx[3] }; int32_t ty[3] = { vy[2], vy[1], vy[3] }; - prepare_precise_triangle(gp0_cmd_buf[3], gp0_cmd_buf[2], - gp0_cmd_buf[4], tx, ty); + prepare_precise_triangle(3, 2, 4, tx, ty); gr_draw_flat_triangle(vx[2], vy[2], vx[1], vy[1], vx[3], vy[3], color); } } @@ -3306,7 +3321,7 @@ static void gp0_exec_shaded_tri(void) { } if (draw_area_out_bbox(vx, vy, 3)) return; gr_set_semi_transparency(semi_trans, (int)semi_transparency); - prepare_precise_triangle(gp0_cmd_buf[1], gp0_cmd_buf[3], gp0_cmd_buf[5], + prepare_precise_triangle(1, 3, 5, vx, vy); gr_draw_gouraud_triangle(vx[0], vy[0], c[0], vx[1], vy[1], c[1], @@ -3352,8 +3367,7 @@ static void gp0_exec_shaded_quad(void) { if (!rej_a) { int32_t tx[3] = { vx[0], vx[1], vx[2] }; int32_t ty[3] = { vy[0], vy[1], vy[2] }; - prepare_precise_triangle(gp0_cmd_buf[1], gp0_cmd_buf[3], - gp0_cmd_buf[5], tx, ty); + prepare_precise_triangle(1, 3, 5, tx, ty); gr_draw_gouraud_triangle(vx[0], vy[0], c[0], vx[1], vy[1], c[1], vx[2], vy[2], c[2]); @@ -3361,8 +3375,7 @@ static void gp0_exec_shaded_quad(void) { if (!rej_b) { int32_t tx[3] = { vx[2], vx[1], vx[3] }; int32_t ty[3] = { vy[2], vy[1], vy[3] }; - prepare_precise_triangle(gp0_cmd_buf[5], gp0_cmd_buf[3], - gp0_cmd_buf[7], tx, ty); + prepare_precise_triangle(5, 3, 7, tx, ty); gr_draw_gouraud_triangle(vx[2], vy[2], c[2], vx[1], vy[1], c[1], vx[3], vy[3], c[3]); @@ -3433,7 +3446,7 @@ static void gp0_exec_textured_tri(void) { if (draw_area_out_bbox(vx, vy, 3)) return; setup_textured_draw(color24, semi_trans, raw_texture); - prepare_precise_triangle(gp0_cmd_buf[1], gp0_cmd_buf[3], gp0_cmd_buf[5], + prepare_precise_triangle(1, 3, 5, vx, vy); prepare_texture_triangle(1, 3, 5); gr_draw_textured_triangle(vx[0], vy[0], u[0], v[0], @@ -3515,8 +3528,7 @@ static void gp0_exec_textured_quad(void) { if (!rej_a) { int32_t tx[3] = { vx[0], vx[1], vx[2] }; int32_t ty[3] = { vy[0], vy[1], vy[2] }; - prepare_precise_triangle(gp0_cmd_buf[1], gp0_cmd_buf[3], - gp0_cmd_buf[5], tx, ty); + prepare_precise_triangle(1, 3, 5, tx, ty); prepare_texture_triangle(1, 3, 5); gr_draw_textured_triangle(vx[0], vy[0], u[0], v[0], vx[1], vy[1], u[1], v[1], @@ -3526,8 +3538,7 @@ static void gp0_exec_textured_quad(void) { if (!rej_b) { int32_t tx[3] = { vx[2], vx[1], vx[3] }; int32_t ty[3] = { vy[2], vy[1], vy[3] }; - prepare_precise_triangle(gp0_cmd_buf[5], gp0_cmd_buf[3], - gp0_cmd_buf[7], tx, ty); + prepare_precise_triangle(5, 3, 7, tx, ty); prepare_texture_triangle(5, 3, 7); gr_draw_textured_triangle(vx[2], vy[2], u[2], v[2], vx[1], vy[1], u[1], v[1], @@ -3569,7 +3580,7 @@ static void gp0_exec_shaded_textured_tri(void) { if (draw_area_out_bbox(vx, vy, 3)) return; gr_set_semi_transparency(semi_trans, (int)semi_transparency); - prepare_precise_triangle(gp0_cmd_buf[1], gp0_cmd_buf[4], gp0_cmd_buf[7], + prepare_precise_triangle(1, 4, 7, vx, vy); prepare_texture_triangle(1, 4, 7); gr_draw_shaded_textured_triangle(vx[0], vy[0], u[0], v[0], c[0], @@ -3620,8 +3631,7 @@ static void gp0_exec_shaded_textured_quad(void) { if (!rej_a) { int32_t tx[3] = { vx[0], vx[1], vx[2] }; int32_t ty[3] = { vy[0], vy[1], vy[2] }; - prepare_precise_triangle(gp0_cmd_buf[1], gp0_cmd_buf[4], - gp0_cmd_buf[7], tx, ty); + prepare_precise_triangle(1, 4, 7, tx, ty); prepare_texture_triangle(1, 4, 7); gr_draw_shaded_textured_triangle(vx[0], vy[0], u[0], v[0], c[0], vx[1], vy[1], u[1], v[1], c[1], @@ -3631,8 +3641,7 @@ static void gp0_exec_shaded_textured_quad(void) { if (!rej_b) { int32_t tx[3] = { vx[2], vx[1], vx[3] }; int32_t ty[3] = { vy[2], vy[1], vy[3] }; - prepare_precise_triangle(gp0_cmd_buf[7], gp0_cmd_buf[4], - gp0_cmd_buf[10], tx, ty); + prepare_precise_triangle(7, 4, 10, tx, ty); prepare_texture_triangle(7, 4, 10); gr_draw_shaded_textured_triangle(vx[2], vy[2], u[2], v[2], c[2], vx[1], vy[1], u[1], v[1], c[1], diff --git a/runtime/src/gte.cpp b/runtime/src/gte.cpp index 7c8ffd94..311d6a16 100644 --- a/runtime/src/gte.cpp +++ b/runtime/src/gte.cpp @@ -1,6 +1,7 @@ #include "gte.h" #include "cpu_state.h" #include "nd_intro_ot.h" +#include "pgxp.h" #include #include #include @@ -222,38 +223,16 @@ static uint32_t s_geom_lookups = 0; /* lookups attempted * static uint32_t s_geom_miss_unrec = 0; /* nothing was ever recorded here */ static uint32_t s_geom_miss_ambig = 0; /* recorded, but not unambiguously */ -/* Exact GTE projection provenance for perspective texture correction. The - * recompiler/interpreters call gte_precision_store after SWC2 writes an SXY - * register into guest RAM. GPU DMA later supplies that exact RAM address, so - * depth lookup follows the packet rather than guessing from rounded X/Y. */ -struct PreciseProjection { - uint32_t packed; - int32_t x16, y16; - uint16_t z; - uint8_t valid; -}; -static PreciseProjection s_precise_sxy[4] = {}; - -#define PRECISION_STORE_SIZE 65536u -struct PrecisionStoreEntry { - uint32_t addr; - PreciseProjection projection; - uint32_t generation; -}; -static PrecisionStoreEntry s_precision_store[PRECISION_STORE_SIZE]; -static uint32_t s_precision_generation = 1; -static int s_precision_tracking = 0; -static PreciseProjection s_speculative_saved_sxy[4]; +/* Exact GTE projection provenance now lives in the PGXP value-propagation + * engine (pgxp.cpp): per-word RAM/scratchpad shadows plus per-GPR and per-GTE- + * register shadows, validated on read (ENHANCEMENTS.md G1.2/G1.3). The + * gte_precision_* entry points below are kept as the stable emit/ABI surface + * (v14 swc2 sites, tests) and forward into that engine. The hashed + * s_precision_store table and the s_precise_sxy FIFO it fed are retired — + * the GTE register shadows ARE the FIFO now. */ static uint32_t s_speculative_depth = 0; static int s_speculative_timeline_invalidated = 0; -static void gte_precision_generation_advance(void) { - if (++s_precision_generation == 0) { - std::memset(s_precision_store, 0, sizeof(s_precision_store)); - s_precision_generation = 1; - } -} - static void gte_geom_generation_advance(void) { if (++s_geom_generation == 0) { if (s_geom_cache) @@ -263,95 +242,63 @@ static void gte_geom_generation_advance(void) { } extern "C" void gte_precision_timeline_invalidate(void) { - for (int i = 0; i < 4; ++i) s_precise_sxy[i].valid = 0; /* Raw machine-state restore is authoritative even if host polling reached * it during a speculative validation pass. Defer the generation advance - * until the outer transaction ends so old provenance cannot be restored. */ + * until the outer transaction ends so old provenance cannot be restored. + * (pgxp_invalidate_all defers the same way behind its suppress bracket.) */ + pgxp_invalidate_all(); if (s_speculative_depth != 0) { s_speculative_timeline_invalidated = 1; return; } - gte_precision_generation_advance(); gte_geom_generation_advance(); } extern "C" void gte_precision_speculative_begin(void) { - if (s_speculative_depth++ == 0) { - std::memcpy(s_speculative_saved_sxy, s_precise_sxy, - sizeof(s_precise_sxy)); + if (s_speculative_depth++ == 0) s_speculative_timeline_invalidated = 0; - } + /* The speculative pass runs real GTE commands and hooks on state that is + * rolled back afterwards: suppress all shadow recording so the shadows + * still describe the restored timeline when the bracket closes. */ + pgxp_suppress_begin(); } extern "C" void gte_precision_speculative_end(void) { if (s_speculative_depth == 0) return; + pgxp_suppress_end(); if (--s_speculative_depth == 0) { if (s_speculative_timeline_invalidated) { - gte_precision_generation_advance(); gte_geom_generation_advance(); s_speculative_timeline_invalidated = 0; - } else { - std::memcpy(s_precise_sxy, s_speculative_saved_sxy, - sizeof(s_precise_sxy)); } } } -static inline uint32_t precision_hash(uint32_t addr) { - addr >>= 2; - addr ^= addr >> 11; - return (addr * 2654435761u) & (PRECISION_STORE_SIZE - 1u); -} - -static inline int precision_ram_address(uint32_t addr, uint32_t *physical) { - uint32_t mapped = addr & 0x1FFFFFFFu; - if (mapped >= 0x00800000u) return 0; - *physical = mapped & 0x1FFFFCu; - return 1; -} - extern "C" void gte_precision_tracking_set(int enabled) { - s_precision_tracking = enabled ? 1 : 0; - gte_precision_generation_advance(); + pgxp_set_enabled(enabled); } +/* v14 ABI emit surface: every swc2 site (all backends, overlay DLLs) calls + * this. The pgxp engine copies the GTE register shadow to the RAM shadow; + * the GPU-side lookup validates against the actual packet word, so a stale + * register shadow can never be believed. */ extern "C" void gte_precision_store_word(uint32_t addr, uint8_t reg) { - if (s_speculative_depth != 0 || s_gte_replay_sandbox || !s_precision_tracking || - reg < 12 || reg > 15) return; - int index = reg == 15 ? 2 : (int)reg - 12; - const PreciseProjection &projection = s_precise_sxy[index]; - if (!projection.valid) return; - uint32_t physical; - if (!precision_ram_address(addr, &physical)) return; - PrecisionStoreEntry &entry = s_precision_store[precision_hash(physical)]; - entry.addr = physical; - entry.projection = projection; - entry.generation = s_precision_generation; + if (s_gte_replay_sandbox || reg < 12 || reg > 15) return; + pgxp_store_gte_reg(addr, reg); } +/* Retired: plain-store invalidation is unnecessary under validate-on-read + * (the GPU compares the tracked word against the actual packet word). Kept + * as a no-op for link compatibility. */ extern "C" void gte_precision_invalidate_word(uint32_t addr) { - if (s_speculative_depth != 0 || s_gte_replay_sandbox || !s_precision_tracking) return; - uint32_t physical; - if (!precision_ram_address(addr, &physical)) return; - PrecisionStoreEntry &entry = s_precision_store[precision_hash(physical)]; - if (entry.generation == s_precision_generation && entry.addr == physical) - entry.generation = 0; + (void)addr; } extern "C" int gte_precision_load_word(uint32_t addr, uint32_t packed, int32_t *x16, int32_t *y16, uint16_t *z) { - if (s_speculative_depth != 0 || !s_precision_tracking) return 0; - uint32_t physical; - if (!precision_ram_address(addr, &physical)) return 0; - const PrecisionStoreEntry &entry = s_precision_store[precision_hash(physical)]; - if (entry.generation != s_precision_generation || entry.addr != physical || - !entry.projection.valid || entry.projection.packed != packed) - return 0; - if (x16) *x16 = entry.projection.x16; - if (y16) *y16 = entry.projection.y16; - if (z) *z = entry.projection.z; - return entry.projection.z != 0; + if (s_speculative_depth != 0) return 0; + return pgxp_load_precise_word(addr, packed, x16, y16, z); } /* Exact slot for a packed SXY pair, or -1 if it is outside the representable @@ -940,16 +887,9 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr) int64_t sx = sx16 >> 16; int64_t sy = sy16 >> 16; gte->push_sxy(sx, sy); - if (!s_gte_replay_sandbox) { - s_precise_sxy[0] = s_precise_sxy[1]; - s_precise_sxy[1] = s_precise_sxy[2]; - s_precise_sxy[2].packed = (uint32_t)gte->SXY[2]; - s_precise_sxy[2].x16 = (int32_t)sx16; - s_precise_sxy[2].y16 = (int32_t)sy16; - s_precise_sxy[2].z = gte->SZ[3]; - s_precise_sxy[2].valid = gte->SZ[3] != 0; - s_precise_sxy[3] = s_precise_sxy[2]; - } + if (!s_gte_replay_sandbox) + pgxp_gte_push_sxy((int32_t)sx16, (int32_t)sy16, gte->SZ[3], + (uint32_t)gte->SXY[2]); geom_note((uint32_t)gte->SXY[2], sx16, sy16); // Step 5: Depth cueing (MAC0/IR0) — only for last vertex of RTPT or RTPS @@ -2054,24 +1994,32 @@ extern "C" void gte_write_data(CPUState* cpu, uint8_t reg, uint32_t val) { cpu->gte_data[29] = packed; } break; + /* Guest writes to the SXY FIFO drop the affected register shadows: + * we never model FIFO side effects on shadows — the shifted regs 12/13 + * now describe words their shadows no longer match, and validation + * drops those on next use. */ case 12: case 13: cpu->gte_data[reg] = val; if (!PSXRecomp::GTE::s_gte_replay_sandbox) - for (int i = 0; i < 4; ++i) PSXRecomp::GTE::s_precise_sxy[i].valid = 0; + pgxp_gte_reg_written(reg, val); break; case 14: cpu->gte_data[14] = val; cpu->gte_data[15] = val; - if (!PSXRecomp::GTE::s_gte_replay_sandbox) - for (int i = 0; i < 4; ++i) PSXRecomp::GTE::s_precise_sxy[i].valid = 0; + if (!PSXRecomp::GTE::s_gte_replay_sandbox) { + pgxp_gte_reg_written(14, val); + pgxp_gte_reg_written(15, val); + } break; case 15: cpu->gte_data[12] = cpu->gte_data[13]; cpu->gte_data[13] = cpu->gte_data[14]; cpu->gte_data[14] = val; cpu->gte_data[15] = val; - if (!PSXRecomp::GTE::s_gte_replay_sandbox) - for (int i = 0; i < 4; ++i) PSXRecomp::GTE::s_precise_sxy[i].valid = 0; + if (!PSXRecomp::GTE::s_gte_replay_sandbox) { + pgxp_gte_reg_written(14, val); + pgxp_gte_reg_written(15, val); + } break; case 23: cpu->gte_data[23] = 0; @@ -2134,26 +2082,31 @@ extern "C" void gte_canonicalize_cpu_state(CPUState* cpu) { } #ifdef PSX_GTE_REGISTER_TEST +/* The precise-projection FIFO is the pgxp GTE register shadow (regs 12..15); + * these keep the historical test entry points working over it. */ extern "C" void gte_test_set_precise_valid_mask(uint32_t mask) { for (uint32_t i = 0; i < 4; ++i) - PSXRecomp::GTE::s_precise_sxy[i].valid = (mask >> i) & 1u; + pgxp_test_seed_gte_sxy(i, 0, 0, 0, 1, (mask >> i) & 1u); } extern "C" uint32_t gte_test_get_precise_valid_mask(void) { uint32_t mask = 0; - for (uint32_t i = 0; i < 4; ++i) - mask |= (PSXRecomp::GTE::s_precise_sxy[i].valid ? 1u : 0u) << i; + for (uint32_t i = 0; i < 4; ++i) { + uint8_t valid = 0; + pgxp_test_get_gte_sxy(i, nullptr, nullptr, nullptr, nullptr, &valid); + mask |= (valid ? 1u : 0u) << i; + } return mask; } extern "C" void gte_test_set_timeline_generations(uint32_t precision, uint32_t geometry) { - PSXRecomp::GTE::s_precision_generation = precision; + pgxp_test_set_generation(precision); PSXRecomp::GTE::s_geom_generation = geometry; } extern "C" uint32_t gte_test_get_precision_generation(void) { - return PSXRecomp::GTE::s_precision_generation; + return pgxp_test_generation(); } extern "C" uint32_t gte_test_get_geometry_generation(void) { @@ -2165,13 +2118,7 @@ extern "C" void gte_test_seed_precise_projection(uint32_t index, int32_t x16, int32_t y16, uint16_t z) { - if (index >= 4) return; - auto &p = PSXRecomp::GTE::s_precise_sxy[index]; - p.packed = packed; - p.x16 = x16; - p.y16 = y16; - p.z = z; - p.valid = 1; + pgxp_test_seed_gte_sxy(index, packed, x16, y16, z, 1); } extern "C" void gte_test_get_precise_projection(uint32_t index, @@ -2180,13 +2127,7 @@ extern "C" void gte_test_get_precise_projection(uint32_t index, int32_t* y16, uint16_t* z, uint8_t* valid) { - if (index >= 4) return; - const auto &p = PSXRecomp::GTE::s_precise_sxy[index]; - if (packed) *packed = p.packed; - if (x16) *x16 = p.x16; - if (y16) *y16 = p.y16; - if (z) *z = p.z; - if (valid) *valid = p.valid; + pgxp_test_get_gte_sxy(index, packed, x16, y16, z, valid); } extern "C" void gte_test_seed_geometry(uint32_t packed, int32_t x16, diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 460b184e..a0b0c554 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -32,6 +32,7 @@ extern "C" void psx_event_step_conservative_env_init(void); #include "overlay_backend.h" #include "gpu.h" +#include "pgxp.h" #include "interrupts.h" #include "present_ring.h" #include "load_transition_ring.h" @@ -1108,6 +1109,8 @@ static int g_video_texfilter = 0; /* 0=nearest, 1=bilinear */ * SXY readback are untouched. Default off = the faithful floor. */ static int g_video_geometry_correction = 0; static int g_video_perspective_texturing = 0; +static int g_video_pgxp_cpu_mode = 0; +static float g_video_pgxp_tolerance = -1.0f; static int g_video_renderer = PSXRecompV4::DEFAULT_VIDEO_RENDERER; static int g_fullscreen = 0; /* tri-state: 0 windowed, 1 borderless (desktop) * fullscreen, 2 exclusive fullscreen */ @@ -10384,6 +10387,8 @@ int main(int argc, char** argv) { gc.runtime.video_geometry_correction ? 1 : 0; g_video_perspective_texturing = gc.runtime.video_perspective_texturing ? 1 : 0; + g_video_pgxp_cpu_mode = gc.runtime.video_pgxp_cpu_mode ? 1 : 0; + g_video_pgxp_tolerance = (float)gc.runtime.video_pgxp_tolerance; g_video_renderer = gc.runtime.video_renderer; g_video_screen = gc.runtime.video_screen_kind; g_video_aspect_num = gc.runtime.video_aspect_num; @@ -12120,6 +12125,8 @@ session_reboot: * the draw path is the faithful integer one, unchanged. */ gte_geometry_correction_set(g_video_geometry_correction); gpu_texture_correction_set(g_video_perspective_texturing); + pgxp_set_cpu_mode(g_video_pgxp_cpu_mode); + pgxp_set_tolerance(g_video_pgxp_tolerance); if (g_video_geometry_correction || g_video_perspective_texturing) { std::fprintf(stdout, "psxrecomp: geometry correction %s, perspective texturing %s%s\n", diff --git a/runtime/src/memory.c b/runtime/src/memory.c index 74c4df0c..9c309083 100644 --- a/runtime/src/memory.c +++ b/runtime/src/memory.c @@ -1555,7 +1555,6 @@ static inline void d44_note(uint32_t phys, uint32_t old, uint32_t val) { } static void psx_write_word_raw(uint32_t addr, uint32_t val); -extern void gte_precision_invalidate_word(uint32_t addr); void psx_write_word(uint32_t addr, uint32_t val) { extern void (*g_overlay_flush_pending_cycles)(void); if (g_overlay_flush_pending_cycles) g_overlay_flush_pending_cycles(); @@ -1572,7 +1571,9 @@ void psx_write_word(uint32_t addr, uint32_t val) { } static void psx_write_word_raw(uint32_t addr, uint32_t val) { g_guest_store_count++; - gte_precision_invalidate_word(addr); + /* (pgxp) plain-store shadow invalidation retired: the PGXP engine + * validates tracked words against the actual packet word on read, so an + * overwritten word can never be believed (ENHANCEMENTS.md G1). */ /* KSEG2 cache control — before physical translation. */ if (addr == 0xFFFE0130u) { cache_ctrl = val; return; } /* KSEG2 guard — see psx_read_word_raw. */ @@ -1772,7 +1773,6 @@ void psx_write_half(uint32_t addr, uint16_t val) { } static void psx_write_half_raw(uint32_t addr, uint16_t val) { g_guest_store_count++; - gte_precision_invalidate_word(addr); if (sr_ptr && (*sr_ptr & 0x10000u)) return; /* KSEG2 guard — see psx_read_word_raw. */ @@ -2108,7 +2108,6 @@ void psx_write_byte(uint32_t addr, uint8_t val) { } static void psx_write_byte_raw(uint32_t addr, uint8_t val) { g_guest_store_count++; - gte_precision_invalidate_word(addr); if (sr_ptr && (*sr_ptr & 0x10000u)) return; /* KSEG2 guard — see psx_read_word_raw. */ diff --git a/runtime/src/pgxp.cpp b/runtime/src/pgxp.cpp new file mode 100644 index 00000000..87f19332 --- /dev/null +++ b/runtime/src/pgxp.cpp @@ -0,0 +1,788 @@ +/* pgxp.cpp — PGXP value-propagation engine (ENHANCEMENTS.md G1.2/G1.3). + * + * CLEAN-ROOM implementation of the publicly documented PGXP technique + * (psx-spx GTE docs + public design write-ups + our own G1 measurements). + * The vendored duckstation/ (CC BY-NC-ND) and beetle-psx/ (GPL) trees are + * black-box behavioral oracles only — no code from them appears here. + * + * Model + * ----- + * Every 32-bit word of guest RAM/scratchpad, every GPR (plus HI/LO), and + * every GTE data register owns a shadow slot recording the sub-pixel screen + * position that word carries (16.16 X/Y + projected SZ depth), the exact + * guest word it describes (`value`), and per-half validity flags. RTPS/RTPT + * fill the SXY shadow FIFO with the pre-truncation projection; the + * psx_pgxp_* hooks copy shadows along with the data (loads, stores, COP2 + * transfers, and — in cpu-mode — the arithmetic games use to repack vertex + * halves); the GPU asks for the precise position of a GP0 vertex word by the + * packet's RAM address. + * + * The single safety invariant: a shadow is only ever BELIEVED after + * validation against the actual guest word it claims to describe. Anything + * that writes guest state without a hook (DMA, memcpy loaders, un-hooked + * instructions) simply leaves a stale shadow behind, and the next validation + * drops it. We never model side effects — overwrite and validate only. The + * one accepted hole (shared with the reference implementations): an untracked + * writer storing the byte-identical word keeps the shadow alive, which is + * harmless because the position it describes is still that word. + * + * Everything here is host-only and visual-only: guest-visible state is never + * read back from shadows, shadows are dropped on savestate/rewind, and the + * speculative native-validation bracket suppresses all recording. + */ + +#include "pgxp.h" +#include "pgxp_hooks.h" +#include "cpu_state.h" + +#include +#include + +/* gte.cpp — position-cache fallback tier (ambiguity-gated, G1.4 exact table) */ +extern "C" int gte_geometry_correction_lookup(uint32_t packed, + int32_t *x16, int32_t *y16); + +/* ------------------------------------------------------------------------- */ +/* Shadow storage */ +/* ------------------------------------------------------------------------- */ + +enum { + PGXP_F_VX = 1u << 0, /* low half (screen X) tracked */ + PGXP_F_VY = 1u << 1, /* high half (screen Y) tracked */ + PGXP_F_VZ = 1u << 2, /* projected depth rode along intact */ + PGXP_F_VXY = PGXP_F_VX | PGXP_F_VY, +}; + +struct PGXPValue { + int32_t x16, y16; /* sub-pixel screen coords, 16.16 */ + uint16_t z; /* projected SZ depth (perspective source), 0 = none */ + uint16_t flags; + uint32_t value; /* the guest word this shadow describes */ + uint32_t gen; /* valid iff == s_gen (O(1) invalidate-all) */ +}; + +#define PGXP_RAM_WORDS (0x200000u >> 2) /* 2 MB RAM */ +#define PGXP_SCRATCH_WORDS (0x400u >> 2) /* 1 KB scratchpad */ +#define PGXP_REG_HI 32 +#define PGXP_REG_LO 33 + +static PGXPValue *s_ram = nullptr; /* lazily allocated, ~10 MB */ +static PGXPValue s_scratch[PGXP_SCRATCH_WORDS]; +static PGXPValue s_gpr[34]; /* 32 GPRs + HI + LO */ +static PGXPValue s_gte[32]; /* GTE data registers */ + +static uint32_t s_gen = 1; +static int s_enabled = 0; +static int s_cpu_mode = 0; +static float s_tolerance = -1.0f; +static uint32_t s_suppress = 0; +static int s_deferred_invalidate = 0; + +/* Single hot-path gate for every hook. */ +static int g_pgxp_active = 0; +static inline void recompute_active(void) { + g_pgxp_active = (s_enabled && s_suppress == 0) ? 1 : 0; +} + +static PGXPStats s_stats; + +/* ------------------------------------------------------------------------- */ +/* Lifecycle */ +/* ------------------------------------------------------------------------- */ + +extern "C" void pgxp_invalidate_all(void) { + if (s_suppress != 0) { s_deferred_invalidate = 1; return; } + if (++s_gen == 0) { + /* generation wrapped: physically clear so stale slots can't revive */ + if (s_ram) std::memset(s_ram, 0, PGXP_RAM_WORDS * sizeof(PGXPValue)); + std::memset(s_scratch, 0, sizeof(s_scratch)); + std::memset(s_gpr, 0, sizeof(s_gpr)); + std::memset(s_gte, 0, sizeof(s_gte)); + s_gen = 1; + } +} + +extern "C" void pgxp_set_enabled(int enabled) { + if (enabled && !s_ram) { + s_ram = (PGXPValue *)std::calloc(PGXP_RAM_WORDS, sizeof(PGXPValue)); + if (!s_ram) enabled = 0; /* fail closed: stay faithful */ + } + s_enabled = enabled ? 1 : 0; + pgxp_invalidate_all(); + recompute_active(); +} + +extern "C" int pgxp_enabled(void) { return s_enabled; } + +extern "C" void pgxp_set_cpu_mode(int enabled) { s_cpu_mode = enabled ? 1 : 0; } +extern "C" int pgxp_cpu_mode(void) { return s_cpu_mode; } + +extern "C" void pgxp_set_tolerance(float pixels) { s_tolerance = pixels; } +extern "C" float pgxp_tolerance(void) { return s_tolerance; } + +extern "C" void pgxp_suppress_begin(void) { + ++s_suppress; + recompute_active(); +} + +extern "C" void pgxp_suppress_end(void) { + if (s_suppress != 0 && --s_suppress == 0) { + if (s_deferred_invalidate) { + s_deferred_invalidate = 0; + pgxp_invalidate_all(); + } + recompute_active(); + } +} + +extern "C" void pgxp_get_stats(PGXPStats *out) { + if (out) *out = s_stats; +} + +/* ------------------------------------------------------------------------- */ +/* Address mapping + validation */ +/* ------------------------------------------------------------------------- */ + +/* Guest address -> shadow slot, or NULL for BIOS/MMIO/KSEG2 (untrackable). */ +static inline PGXPValue *pgxp_ptr(uint32_t addr) { + uint32_t m = addr & 0x1FFFFFFFu; + if (m < 0x00800000u) /* RAM + its mirrors */ + return s_ram ? &s_ram[(m & 0x1FFFFCu) >> 2] : nullptr; + if ((m & 0xFFFFFC00u) == 0x1F800000u) /* scratchpad */ + return &s_scratch[(m & 0x3FCu) >> 2]; + return nullptr; +} + +static inline int pv_live(const PGXPValue *pv) { + return pv && pv->gen == s_gen && (pv->flags & PGXP_F_VXY) != 0; +} + +/* Drop whichever tracked halves no longer match the actual guest word; the + * depth belongs to the whole vertex, so any half going stale kills it too. */ +static inline void pv_validate(PGXPValue *pv, uint32_t actual) { + if (pv->gen != s_gen) return; + uint32_t diff = pv->value ^ actual; + if ((pv->flags & PGXP_F_VX) && (diff & 0x0000FFFFu)) + pv->flags &= (uint16_t)~(PGXP_F_VX | PGXP_F_VZ); + if ((pv->flags & PGXP_F_VY) && (diff & 0xFFFF0000u)) + pv->flags &= (uint16_t)~(PGXP_F_VY | PGXP_F_VZ); + pv->value = actual; +} + +/* Mark a slot as "known word, no precision" — keeps `value` current so later + * half-merges stay keyed correctly. */ +static inline void pv_reset(PGXPValue *pv, uint32_t value) { + pv->x16 = 0; pv->y16 = 0; pv->z = 0; + pv->flags = 0; + pv->value = value; + pv->gen = s_gen; +} + +static inline void pv_kill(PGXPValue *pv) { pv->gen = 0; } + +/* ------------------------------------------------------------------------- */ +/* Instruction field helpers */ +/* ------------------------------------------------------------------------- */ + +static inline uint32_t f_op(uint32_t i) { return i >> 26; } +static inline uint32_t f_rs(uint32_t i) { return (i >> 21) & 31u; } +static inline uint32_t f_rt(uint32_t i) { return (i >> 16) & 31u; } +static inline uint32_t f_rd(uint32_t i) { return (i >> 11) & 31u; } +static inline uint32_t f_shamt(uint32_t i) { return (i >> 6) & 31u; } +static inline uint32_t f_funct(uint32_t i) { return i & 63u; } +static inline int32_t f_simm(uint32_t i) { return (int32_t)(int16_t)(i & 0xFFFFu); } + +/* ------------------------------------------------------------------------- */ +/* Memory-mode hooks: loads / stores */ +/* ------------------------------------------------------------------------- */ + +extern "C" void psx_pgxp_load(struct CPUState *cpu, uint32_t instr, + uint32_t addr, uint32_t value) { + (void)cpu; + if (!g_pgxp_active) return; + uint32_t rt = f_rt(instr); + if (rt == 0) return; + PGXPValue *dst = &s_gpr[rt]; + + switch (f_op(instr)) { + case 0x23: { /* LW */ + PGXPValue *src = pgxp_ptr(addr); + if (src && src->gen == s_gen) { + pv_validate(src, value); + *dst = *src; + } else { + pv_reset(dst, value); + } + return; + } + case 0x21: /* LH */ + case 0x25: { /* LHU */ + PGXPValue *src = pgxp_ptr(addr); + int hi_half = (addr >> 1) & 1; + pv_reset(dst, value); + /* The high half of the extended GPR is a known-exact constant + * (0/-1 for LH, 0 for LHU): track it so re-packing via sll/or in + * cpu-mode keeps working. */ + dst->y16 = ((int32_t)value >> 16) << 16; + dst->flags = PGXP_F_VY; + if (src && src->gen == s_gen) { + uint32_t actual_half = (value & 0xFFFFu) << (hi_half ? 16 : 0); + uint32_t mask = hi_half ? 0xFFFF0000u : 0x0000FFFFu; + uint16_t want = hi_half ? PGXP_F_VY : PGXP_F_VX; + if ((src->flags & want) && ((src->value ^ actual_half) & mask) == 0) { + dst->x16 = hi_half ? src->y16 : src->x16; + dst->flags |= PGXP_F_VX; + } + } + return; + } + default: /* LB/LBU/LWL/LWR: untrackable */ + pv_reset(dst, value); + return; + } +} + +extern "C" void psx_pgxp_store(struct CPUState *cpu, uint32_t instr, + uint32_t addr, uint32_t value) { + (void)cpu; + if (!g_pgxp_active) return; + PGXPValue *dst = pgxp_ptr(addr); + if (!dst) return; + uint32_t rt = f_rt(instr); + PGXPValue *src = (rt != 0) ? &s_gpr[rt] : nullptr; + + switch (f_op(instr)) { + case 0x2B: { /* SW */ + if (src && src->gen == s_gen) { + pv_validate(src, value); + *dst = *src; + } else { + pv_reset(dst, value); + } + return; + } + case 0x29: { /* SH */ + int hi_half = (addr >> 1) & 1; + uint32_t half = value & 0xFFFFu; + if (dst->gen != s_gen) pv_reset(dst, half << (hi_half ? 16 : 0)); + /* Patch the stored half into the tracked word; the other half's + * validity (if any) survives untouched. Depth never survives a + * half-write — the vertex it described no longer exists whole. */ + if (hi_half) dst->value = (dst->value & 0x0000FFFFu) | (half << 16); + else dst->value = (dst->value & 0xFFFF0000u) | half; + dst->flags &= (uint16_t)~((hi_half ? PGXP_F_VY : PGXP_F_VX) | PGXP_F_VZ); + dst->z = 0; + if (src && src->gen == s_gen && (src->flags & PGXP_F_VX) && + ((src->value ^ value) & 0xFFFFu) == 0) { + if (hi_half) { dst->y16 = src->x16; dst->flags |= PGXP_F_VY; } + else { dst->x16 = src->x16; dst->flags |= PGXP_F_VX; } + } + return; + } + case 0x28: { /* SB */ + if (dst->gen != s_gen) return; /* nothing tracked: stay dead */ + uint32_t shift = (addr & 3u) * 8u; + dst->value = (dst->value & ~(0xFFu << shift)) | + ((value & 0xFFu) << shift); + dst->flags &= (uint16_t)~(((addr & 2u) ? PGXP_F_VY : PGXP_F_VX) | PGXP_F_VZ); + dst->z = 0; + return; + } + default: /* SWL/SWR: forget the word */ + pv_kill(dst); + return; + } +} + +/* ------------------------------------------------------------------------- */ +/* Memory-mode hooks: COP2 transfers */ +/* ------------------------------------------------------------------------- */ + +extern "C" void psx_pgxp_cop2(struct CPUState *cpu, uint32_t instr, + uint32_t value, uint32_t addr) { + (void)cpu; + if (!g_pgxp_active) return; + + switch (f_op(instr)) { + case 0x32: { /* LWC2: gte[rt] <- [addr] */ + PGXPValue *src = pgxp_ptr(addr); + PGXPValue *dst = &s_gte[f_rt(instr)]; + if (src && src->gen == s_gen) { + pv_validate(src, value); + *dst = *src; + } else { + pv_reset(dst, value); + } + return; + } + case 0x3A: { /* SWC2: [addr] <- gte[rt] */ + PGXPValue *dst = pgxp_ptr(addr); + if (!dst) return; + PGXPValue *src = &s_gte[f_rt(instr)]; + if (src->gen == s_gen) { + pv_validate(src, value); + *dst = *src; + } else { + pv_reset(dst, value); + } + return; + } + case 0x12: { /* COP2 register transfers */ + switch (f_rs(instr)) { + case 0x00: { /* MFC2: gpr[rt] <- gte[rd] */ + uint32_t rt = f_rt(instr); + if (rt == 0) return; + PGXPValue *src = &s_gte[f_rd(instr)]; + if (src->gen == s_gen) { + pv_validate(src, value); + s_gpr[rt] = *src; + } else { + pv_reset(&s_gpr[rt], value); + } + return; + } + case 0x04: { /* MTC2: gte[rd] <- gpr[rt] */ + uint32_t rt = f_rt(instr); + PGXPValue *dst = &s_gte[f_rd(instr)]; + if (rt != 0 && s_gpr[rt].gen == s_gen) { + pv_validate(&s_gpr[rt], value); + *dst = s_gpr[rt]; + } else { + pv_reset(dst, value); + } + /* An SXYP write (rd==15) shifts the hardware FIFO; we do not + * model it — regs 12..14 now describe words they no longer + * match, and validation drops them on next use. */ + return; + } + case 0x02: { /* CFC2: control regs carry no + positions */ + uint32_t rt = f_rt(instr); + if (rt != 0) pv_reset(&s_gpr[rt], value); + return; + } + default: /* CTC2: nothing shadowed */ + return; + } + } + default: + return; + } +} + +/* ------------------------------------------------------------------------- */ +/* CPU-mode hooks: arithmetic (tier 2, default off) */ +/* ------------------------------------------------------------------------- */ + +/* One 16-bit operand component in 16.16: a tracked half contributes its + * sub-pixel value; an untracked half contributes its exact integer value + * (adding an exact offset to a tracked coordinate keeps the fraction). */ +static inline int32_t comp16(const PGXPValue *pv, uint32_t value, int hi_half) { + if (pv && pv->gen == s_gen) { + if (!hi_half && (pv->flags & PGXP_F_VX) && + ((pv->value ^ value) & 0x0000FFFFu) == 0) + return pv->x16; + if (hi_half && (pv->flags & PGXP_F_VY) && + ((pv->value ^ value) & 0xFFFF0000u) == 0) + return pv->y16; + } + return ((int32_t)(int16_t)(hi_half ? (value >> 16) : value)) << 16; +} + +static inline int comp_tracked(const PGXPValue *pv, uint32_t value, int hi_half) { + if (!pv || pv->gen != s_gen) return 0; + if (!hi_half) + return (pv->flags & PGXP_F_VX) && + ((pv->value ^ value) & 0x0000FFFFu) == 0; + return (pv->flags & PGXP_F_VY) && + ((pv->value ^ value) & 0xFFFF0000u) == 0; +} + +/* Half-wise add/sub is only meaningful when the guest result shows no carry + * crossed the half boundary; otherwise the halves did not combine + * independently and the shadow must not pretend they did. */ +static inline int halves_independent(uint32_t a, uint32_t b, uint32_t r, int sub) { + uint32_t lo = sub ? ((a & 0xFFFFu) - (b & 0xFFFFu)) + : ((a & 0xFFFFu) + (b & 0xFFFFu)); + return ((lo ^ r) & 0xFFFFu) == 0 && + ((((sub ? a - b : a + b)) ^ r) == 0) && + ((lo >> 16) == 0); /* no carry/borrow out of low */ +} + +extern "C" void psx_pgxp_alu(struct CPUState *cpu, uint32_t instr, + uint32_t result, uint32_t s1, uint32_t s2) { + (void)cpu; + if (!g_pgxp_active) return; + + uint32_t op = f_op(instr); + uint32_t dst_reg; + PGXPValue *a = nullptr, *b = nullptr; /* source shadows (may be 0) */ + + if (op == 0) { /* SPECIAL */ + dst_reg = f_rd(instr); + uint32_t rs = f_rs(instr), rt = f_rt(instr); + uint32_t funct = f_funct(instr); + if (funct == 0x11 || funct == 0x13) { /* MTHI / MTLO */ + PGXPValue *hl = &s_gpr[funct == 0x11 ? PGXP_REG_HI : PGXP_REG_LO]; + if (s_cpu_mode && rs != 0 && s_gpr[rs].gen == s_gen) { + pv_validate(&s_gpr[rs], result); + *hl = s_gpr[rs]; + } else pv_reset(hl, result); + return; + } + if (dst_reg == 0) return; + PGXPValue *dst = &s_gpr[dst_reg]; + + switch (funct) { + case 0x10: /* MFHI */ + if (s_cpu_mode && s_gpr[PGXP_REG_HI].gen == s_gen) { + pv_validate(&s_gpr[PGXP_REG_HI], result); + *dst = s_gpr[PGXP_REG_HI]; + } else pv_reset(dst, result); + return; + case 0x12: /* MFLO */ + if (s_cpu_mode && s_gpr[PGXP_REG_LO].gen == s_gen) { + pv_validate(&s_gpr[PGXP_REG_LO], result); + *dst = s_gpr[PGXP_REG_LO]; + } else pv_reset(dst, result); + return; + case 0x00: case 0x02: case 0x03: /* SLL / SRL / SRA */ + case 0x04: case 0x06: case 0x07: { /* SLLV / SRLV / SRAV */ + uint32_t sh = (funct < 4) ? f_shamt(instr) : (s2 & 31u); + uint32_t src_reg = rt; + PGXPValue *src = (src_reg != 0) ? &s_gpr[src_reg] : nullptr; + int right = (funct & 3u) != 0; + if (!s_cpu_mode || sh != 16 || !src || src->gen != s_gen) { + pv_reset(dst, result); + return; + } + pv_validate(src, s1); + pv_reset(dst, result); + if (!right) { /* << 16: low comp -> Y */ + if (src->flags & PGXP_F_VX) { + dst->y16 = src->x16; + dst->flags |= PGXP_F_VY; + } + dst->x16 = 0; + dst->flags |= PGXP_F_VX; /* low half exactly zero */ + } else { /* >> 16: high comp -> X */ + if (src->flags & PGXP_F_VY) { + dst->x16 = src->y16; + dst->flags |= PGXP_F_VX; + } + dst->y16 = ((int32_t)result >> 16) << 16; /* 0 or sign fill */ + dst->flags |= PGXP_F_VY; + } + return; + } + case 0x20: case 0x21: /* ADD / ADDU */ + case 0x22: case 0x23: /* SUB / SUBU */ + case 0x25: { /* OR */ + /* MOVE idioms first — they matter even without full cpu-mode + * arithmetic, and they are exact. */ + int is_or = (funct == 0x25); + int is_sub = (funct == 0x22 || funct == 0x23); + if ((is_or || !is_sub) && rt == 0) { /* op rd, rs, $zero */ + if (rs != 0 && s_gpr[rs].gen == s_gen) { + pv_validate(&s_gpr[rs], result); + *dst = s_gpr[rs]; + } else pv_reset(dst, result); + return; + } + if ((is_or || !is_sub) && rs == 0) { /* op rd, $zero, rt */ + if (rt != 0 && s_gpr[rt].gen == s_gen) { + pv_validate(&s_gpr[rt], result); + *dst = s_gpr[rt]; + } else pv_reset(dst, result); + return; + } + if (!s_cpu_mode) { pv_reset(dst, result); return; } + a = (rs != 0) ? &s_gpr[rs] : nullptr; + b = (rt != 0) ? &s_gpr[rt] : nullptr; + if (is_or) { + /* Merge disjoint halves (the lhu/sll/or repack pattern). */ + if ((s1 & s2) != 0) { pv_reset(dst, result); return; } + pv_reset(dst, result); + const PGXPValue *xsrc; uint32_t xval; + if ((s1 & 0xFFFFu) != 0) { xsrc = a; xval = s1; } + else { xsrc = b; xval = s2; } + const PGXPValue *ysrc; uint32_t yval; + if ((s1 & 0xFFFF0000u) != 0) { ysrc = a; yval = s1; } + else { ysrc = b; yval = s2; } + if (comp_tracked(xsrc, xval, 0)) { + dst->x16 = xsrc->x16; dst->flags |= PGXP_F_VX; + } else if ((result & 0xFFFFu) == 0) { + dst->x16 = 0; dst->flags |= PGXP_F_VX; + } + if (comp_tracked(ysrc, yval, 1)) { + dst->y16 = ysrc->y16; dst->flags |= PGXP_F_VY; + } else if ((result & 0xFFFF0000u) == 0) { + dst->y16 = 0; dst->flags |= PGXP_F_VY; + } + return; + } + /* add/sub: require at least one tracked component and halves + * that combined independently (no cross-half carry). */ + if (!halves_independent(s1, s2, result, is_sub) || + (!comp_tracked(a, s1, 0) && !comp_tracked(a, s1, 1) && + !comp_tracked(b, s2, 0) && !comp_tracked(b, s2, 1))) { + pv_reset(dst, result); + return; + } + pv_reset(dst, result); + dst->x16 = is_sub ? comp16(a, s1, 0) - comp16(b, s2, 0) + : comp16(a, s1, 0) + comp16(b, s2, 0); + dst->y16 = is_sub ? comp16(a, s1, 1) - comp16(b, s2, 1) + : comp16(a, s1, 1) + comp16(b, s2, 1); + dst->flags = PGXP_F_VXY; + return; + } + default: + pv_reset(dst, result); + return; + } + } + + /* immediates */ + dst_reg = f_rt(instr); + if (dst_reg == 0) return; + PGXPValue *dst = &s_gpr[dst_reg]; + uint32_t rs = f_rs(instr); + + switch (op) { + case 0x0F: { /* LUI: both halves exact */ + pv_reset(dst, result); + dst->x16 = 0; + dst->y16 = ((int32_t)result >> 16) << 16; + dst->flags = PGXP_F_VXY; + return; + } + case 0x08: case 0x09: { /* ADDI / ADDIU */ + int32_t imm = f_simm(instr); + if (imm == 0) { /* MOVE idiom */ + if (rs != 0 && s_gpr[rs].gen == s_gen) { + pv_validate(&s_gpr[rs], result); + *dst = s_gpr[rs]; + } else pv_reset(dst, result); + return; + } + a = (rs != 0) ? &s_gpr[rs] : nullptr; + /* A negative immediate is a subtraction of its magnitude: checking + * carry on the sign-extended form would reject nearly every -N. */ + { + int neg = imm < 0; + uint32_t mag = (uint32_t)(neg ? -imm : imm); + if (!s_cpu_mode || + !halves_independent(s1, mag, result, neg) || + (!comp_tracked(a, s1, 0) && !comp_tracked(a, s1, 1))) { + pv_reset(dst, result); + return; + } + int32_t dx = (int32_t)(mag & 0xFFFFu) << 16; + pv_reset(dst, result); + dst->x16 = comp16(a, s1, 0) + (neg ? -dx : dx); + dst->y16 = comp16(a, s1, 1); /* no carry crossed: Y untouched */ + dst->flags = PGXP_F_VXY; + } + return; + } + case 0x0D: { /* ORI */ + uint32_t imm = instr & 0xFFFFu; + if (imm == 0 || (s_cpu_mode && (s1 & 0xFFFFu) == 0)) { + a = (rs != 0) ? &s_gpr[rs] : nullptr; + pv_reset(dst, result); + if (comp_tracked(a, s1, 1)) { + dst->y16 = a->y16; dst->flags |= PGXP_F_VY; + } + if (imm == 0) { + if (comp_tracked(a, s1, 0)) { + dst->x16 = a->x16; dst->flags |= PGXP_F_VX; + } + } else { /* exact constant low half */ + dst->x16 = ((int32_t)(int16_t)imm) << 16; + dst->flags |= PGXP_F_VX; + } + return; + } + pv_reset(dst, result); + return; + } + default: /* ANDI/XORI/SLTI/... */ + pv_reset(dst, result); + return; + } +} + +extern "C" void psx_pgxp_muldiv(struct CPUState *cpu, uint32_t instr, + uint32_t hi, uint32_t lo, + uint32_t s1, uint32_t s2) { + (void)cpu; (void)instr; (void)s1; (void)s2; + if (!g_pgxp_active) return; + /* Products/quotients of screen coordinates are not screen coordinates: + * record the results as known-but-imprecise so MFHI/MFLO stay honest. */ + pv_reset(&s_gpr[PGXP_REG_HI], hi); + pv_reset(&s_gpr[PGXP_REG_LO], lo); +} + +/* ------------------------------------------------------------------------- */ +/* GTE producer */ +/* ------------------------------------------------------------------------- */ + +extern "C" void pgxp_gte_push_sxy(int32_t x16, int32_t y16, uint16_t sz3, + uint32_t packed) { + if (!g_pgxp_active) return; + s_gte[12] = s_gte[13]; + s_gte[13] = s_gte[14]; + PGXPValue *pv = &s_gte[14]; + pv->x16 = x16; + pv->y16 = y16; + pv->z = sz3; + pv->flags = (uint16_t)(PGXP_F_VXY | (sz3 != 0 ? PGXP_F_VZ : 0)); + pv->value = packed; + pv->gen = s_gen; + s_gte[15] = *pv; /* SXYP mirrors SXY2 */ +} + +extern "C" void pgxp_gte_reg_written(int reg, uint32_t value) { + /* Invalidation-class bookkeeping: runs even with the engine disarmed so + * seeded/leftover shadows can never outlive a guest register write. Only + * the suppression bracket skips it (the speculative pass rolls the + * machine state back, so its writes must not stick to the shadows). */ + if (s_suppress != 0) return; + if (reg < 0 || reg > 31) return; + pv_kill(&s_gte[reg]); + pv_reset(&s_gte[reg], value); +} + +/* ------------------------------------------------------------------------- */ +/* GPU consumer */ +/* ------------------------------------------------------------------------- */ + +extern "C" int pgxp_get_precise_vertex(uint32_t addr, uint32_t packet_word, + int32_t int_x, int32_t int_y, + int32_t *x16, int32_t *y16, + uint16_t *sz) { + s_stats.lookups++; + + int32_t px = 0, py = 0; + uint16_t pz = 0; + int have = 0; + + if (s_enabled && addr != 0xFFFFFFFFu) { + PGXPValue *pv = pgxp_ptr(addr); + if (pv && pv->gen == s_gen && (pv->flags & PGXP_F_VXY) != 0) { + if (pv->value == packet_word && + (pv->flags & PGXP_F_VXY) == PGXP_F_VXY) { + px = pv->x16; + py = pv->y16; + pz = (pv->flags & PGXP_F_VZ) ? pv->z : 0; + have = PGXP_SRC_DATAFLOW; + } else { + s_stats.value_mismatch++; + } + } + } + + if (!have && gte_geometry_correction_lookup(packet_word, &px, &py)) { + pz = 0; /* fallback never carries depth */ + have = PGXP_SRC_FALLBACK; + } + + if (have) { + /* Truncation agreement: the GPU parsed 11-bit integers out of the + * packet; a precise position whose integer part disagrees (a + * wrapped/CPU-modified coordinate) must not be believed. */ + if ((px >> 16) != int_x || (py >> 16) != int_y) { + s_stats.trunc_reject++; + have = 0; + } else if (s_tolerance >= 0.0f) { + float dx = (float)(px - (int_x << 16)) * (1.0f / 65536.0f); + float dy = (float)(py - (int_y << 16)) * (1.0f / 65536.0f); + if (dx > s_tolerance || dy > s_tolerance) { + s_stats.tolerance_reject++; + have = 0; + } + } + } + + if (!have) { + s_stats.native++; + *x16 = int_x << 16; + *y16 = int_y << 16; + *sz = 0; + return PGXP_SRC_NATIVE; + } + + if (have == PGXP_SRC_DATAFLOW) s_stats.dataflow_hit++; + else s_stats.fallback_hit++; + if (pz != 0) s_stats.w_valid++; + *x16 = px; + *y16 = py; + *sz = pz; + return have; +} + +/* ------------------------------------------------------------------------- */ +/* Legacy v14 SWC2 tracker — kept as the base-flavour feed */ +/* ------------------------------------------------------------------------- */ + +/* Emitted at every swc2 since ABI v14 (all flavours, all backends, overlay + * DLLs). In the pgxp flavour psx_pgxp_cop2 supersedes it for the same swc2 — + * the double write is idempotent (same source shadow, same destination). */ +extern "C" void pgxp_store_gte_reg(uint32_t addr, uint8_t reg) { + if (!g_pgxp_active) return; + PGXPValue *dst = pgxp_ptr(addr); + if (!dst) return; + const PGXPValue *src = &s_gte[reg & 31u]; + if (src->gen != s_gen) return; + *dst = *src; +} + +/* ------------------------------------------------------------------------- */ +/* Test accessors */ +/* ------------------------------------------------------------------------- */ + +extern "C" void pgxp_test_seed_gte_sxy(uint32_t index, uint32_t packed, + int32_t x16, int32_t y16, uint16_t z, + int valid) { + if (index >= 4) return; + PGXPValue *pv = &s_gte[12 + index]; + pv->x16 = x16; + pv->y16 = y16; + pv->z = z; + pv->value = packed; + pv->flags = (uint16_t)(PGXP_F_VXY | (z != 0 ? PGXP_F_VZ : 0)); + pv->gen = valid ? s_gen : 0; +} + +extern "C" void pgxp_test_get_gte_sxy(uint32_t index, uint32_t *packed, + int32_t *x16, int32_t *y16, uint16_t *z, + uint8_t *valid) { + if (index >= 4) return; + const PGXPValue *pv = &s_gte[12 + index]; + if (packed) *packed = pv->value; + if (x16) *x16 = pv->x16; + if (y16) *y16 = pv->y16; + if (z) *z = pv->z; + if (valid) *valid = pv_live(pv) ? 1 : 0; +} + +extern "C" uint32_t pgxp_test_generation(void) { return s_gen; } + +extern "C" void pgxp_test_set_generation(uint32_t gen) { s_gen = gen; } + +/* Address-keyed depth lookup for the shipped perspective-texturing path + * (gpu.c prepare_texture_triangle). Same contract as the retired hashed + * table: hit only when the tracked word matches the packet word exactly. */ +extern "C" int pgxp_load_precise_word(uint32_t addr, uint32_t packed, + int32_t *x16, int32_t *y16, uint16_t *z) { + if (!s_enabled) return 0; + PGXPValue *pv = pgxp_ptr(addr); + if (!pv || pv->gen != s_gen || pv->value != packed || + (pv->flags & PGXP_F_VXY) != PGXP_F_VXY) + return 0; + if (x16) *x16 = pv->x16; + if (y16) *y16 = pv->y16; + if (z) *z = (pv->flags & PGXP_F_VZ) ? pv->z : 0; + return (pv->flags & PGXP_F_VZ) && pv->z != 0; +} diff --git a/runtime/src/psx_interpreter.c b/runtime/src/psx_interpreter.c index 0ca57ba3..340305f0 100644 --- a/runtime/src/psx_interpreter.c +++ b/runtime/src/psx_interpreter.c @@ -211,12 +211,18 @@ static void exec_one(CPUState* cpu) { uint32_t rd = RD(insn); uint32_t sa = SA(insn); switch (func) { - case 0x00: set_reg(cpu, rd, rt_val << sa); break; /* SLL */ - case 0x02: set_reg(cpu, rd, rt_val >> sa); break; /* SRL */ - case 0x03: set_reg(cpu, rd, (uint32_t)((int32_t)rt_val >> sa)); break; /* SRA */ - case 0x04: set_reg(cpu, rd, rt_val << (rs_val & 31)); break; /* SLLV */ - case 0x06: set_reg(cpu, rd, rt_val >> (rs_val & 31)); break; /* SRLV */ - case 0x07: set_reg(cpu, rd, (uint32_t)((int32_t)rt_val >> (rs_val & 31))); break; /* SRAV */ + case 0x00: set_reg(cpu, rd, rt_val << sa); + psx_pgxp_alu(cpu, insn, cpu->gpr[rd], rt_val, sa); break; /* SLL */ + case 0x02: set_reg(cpu, rd, rt_val >> sa); + psx_pgxp_alu(cpu, insn, cpu->gpr[rd], rt_val, sa); break; /* SRL */ + case 0x03: set_reg(cpu, rd, (uint32_t)((int32_t)rt_val >> sa)); + psx_pgxp_alu(cpu, insn, cpu->gpr[rd], rt_val, sa); break; /* SRA */ + case 0x04: set_reg(cpu, rd, rt_val << (rs_val & 31)); + psx_pgxp_alu(cpu, insn, cpu->gpr[rd], rt_val, rs_val); break; /* SLLV */ + case 0x06: set_reg(cpu, rd, rt_val >> (rs_val & 31)); + psx_pgxp_alu(cpu, insn, cpu->gpr[rd], rt_val, rs_val); break; /* SRLV */ + case 0x07: set_reg(cpu, rd, (uint32_t)((int32_t)rt_val >> (rs_val & 31))); + psx_pgxp_alu(cpu, insn, cpu->gpr[rd], rt_val, rs_val); break; /* SRAV */ case 0x08: { /* JR */ uint32_t target = rs_val; exec_one(cpu); /* delay slot */ @@ -232,41 +238,54 @@ static void exec_one(CPUState* cpu) { } case 0x0C: raise_exception(cpu, 8, 0); return; /* SYSCALL */ case 0x0D: raise_exception(cpu, 9, 0); return; /* BREAK */ - case 0x10: set_reg(cpu, rd, cpu->hi); break; /* MFHI */ - case 0x11: cpu->hi = rs_val; break; /* MTHI */ - case 0x12: set_reg(cpu, rd, cpu->lo); break; /* MFLO */ - case 0x13: cpu->lo = rs_val; break; /* MTLO */ + case 0x10: set_reg(cpu, rd, cpu->hi); + psx_pgxp_alu(cpu, insn, cpu->gpr[rd], cpu->hi, 0); break; /* MFHI */ + case 0x11: cpu->hi = rs_val; + psx_pgxp_alu(cpu, insn, cpu->hi, rs_val, 0); break; /* MTHI */ + case 0x12: set_reg(cpu, rd, cpu->lo); + psx_pgxp_alu(cpu, insn, cpu->gpr[rd], cpu->lo, 0); break; /* MFLO */ + case 0x13: cpu->lo = rs_val; + psx_pgxp_alu(cpu, insn, cpu->lo, rs_val, 0); break; /* MTLO */ case 0x18: { /* MULT */ int64_t r = (int64_t)(int32_t)rs_val * (int64_t)(int32_t)rt_val; - cpu->lo = (uint32_t)r; cpu->hi = (uint32_t)(r >> 32); break; + cpu->lo = (uint32_t)r; cpu->hi = (uint32_t)(r >> 32); + psx_pgxp_muldiv(cpu, insn, cpu->hi, cpu->lo, rs_val, rt_val); break; } case 0x19: { /* MULTU */ uint64_t r = (uint64_t)rs_val * (uint64_t)rt_val; - cpu->lo = (uint32_t)r; cpu->hi = (uint32_t)(r >> 32); break; + cpu->lo = (uint32_t)r; cpu->hi = (uint32_t)(r >> 32); + psx_pgxp_muldiv(cpu, insn, cpu->hi, cpu->lo, rs_val, rt_val); break; } case 0x1A: /* DIV */ if (rt_val == 0) { cpu->lo = (rs_val & 0x80000000u) ? 1 : 0xFFFFFFFFu; cpu->hi = rs_val; } else if (rs_val == 0x80000000u && rt_val == 0xFFFFFFFFu) { cpu->lo = 0x80000000u; cpu->hi = 0; } else { cpu->lo = (uint32_t)((int32_t)rs_val / (int32_t)rt_val); cpu->hi = (uint32_t)((int32_t)rs_val % (int32_t)rt_val); } + psx_pgxp_muldiv(cpu, insn, cpu->hi, cpu->lo, rs_val, rt_val); break; case 0x1B: /* DIVU */ if (rt_val == 0) { cpu->lo = 0xFFFFFFFFu; cpu->hi = rs_val; } else { cpu->lo = rs_val / rt_val; cpu->hi = rs_val % rt_val; } + psx_pgxp_muldiv(cpu, insn, cpu->hi, cpu->lo, rs_val, rt_val); break; case 0x20: { /* ADD (overflow trap) */ int64_t r = (int64_t)(int32_t)rs_val + (int64_t)(int32_t)rt_val; if (r < (int64_t)INT32_MIN || r > (int64_t)INT32_MAX) { raise_exception(cpu, 12, 0); return; } - set_reg(cpu, rd, (uint32_t)(int32_t)r); break; + set_reg(cpu, rd, (uint32_t)(int32_t)r); + psx_pgxp_alu(cpu, insn, cpu->gpr[rd], rs_val, rt_val); break; } - case 0x21: set_reg(cpu, rd, rs_val + rt_val); break; /* ADDU */ + case 0x21: set_reg(cpu, rd, rs_val + rt_val); + psx_pgxp_alu(cpu, insn, cpu->gpr[rd], rs_val, rt_val); break; /* ADDU */ case 0x22: { /* SUB (overflow trap) */ int64_t r = (int64_t)(int32_t)rs_val - (int64_t)(int32_t)rt_val; if (r < (int64_t)INT32_MIN || r > (int64_t)INT32_MAX) { raise_exception(cpu, 12, 0); return; } - set_reg(cpu, rd, (uint32_t)(int32_t)r); break; + set_reg(cpu, rd, (uint32_t)(int32_t)r); + psx_pgxp_alu(cpu, insn, cpu->gpr[rd], rs_val, rt_val); break; } - case 0x23: set_reg(cpu, rd, rs_val - rt_val); break; /* SUBU */ + case 0x23: set_reg(cpu, rd, rs_val - rt_val); + psx_pgxp_alu(cpu, insn, cpu->gpr[rd], rs_val, rt_val); break; /* SUBU */ case 0x24: set_reg(cpu, rd, rs_val & rt_val); break; /* AND */ - case 0x25: set_reg(cpu, rd, rs_val | rt_val); break; /* OR */ + case 0x25: set_reg(cpu, rd, rs_val | rt_val); + psx_pgxp_alu(cpu, insn, cpu->gpr[rd], rs_val, rt_val); break; /* OR */ case 0x26: set_reg(cpu, rd, rs_val ^ rt_val); break; /* XOR */ case 0x27: set_reg(cpu, rd, ~(rs_val | rt_val)); break; /* NOR */ case 0x2A: set_reg(cpu, rd, (int32_t)rs_val < (int32_t)rt_val ? 1 : 0); break; /* SLT */ @@ -339,15 +358,19 @@ static void exec_one(CPUState* cpu) { case 0x08: { /* ADDI (overflow trap) */ int64_t r = (int64_t)(int32_t)rs_val + (int64_t)SIMM(insn); if (r < (int64_t)INT32_MIN || r > (int64_t)INT32_MAX) { raise_exception(cpu, 12, 0); return; } - set_reg(cpu, RT(insn), (uint32_t)(int32_t)r); break; + set_reg(cpu, RT(insn), (uint32_t)(int32_t)r); + psx_pgxp_alu(cpu, insn, cpu->gpr[RT(insn)], rs_val, (uint32_t)SIMM(insn)); break; } - case 0x09: set_reg(cpu, RT(insn), rs_val + (uint32_t)SIMM(insn)); break; /* ADDIU */ + case 0x09: set_reg(cpu, RT(insn), rs_val + (uint32_t)SIMM(insn)); + psx_pgxp_alu(cpu, insn, cpu->gpr[RT(insn)], rs_val, (uint32_t)SIMM(insn)); break; /* ADDIU */ case 0x0A: set_reg(cpu, RT(insn), (int32_t)rs_val < SIMM(insn) ? 1 : 0); break; /* SLTI */ case 0x0B: set_reg(cpu, RT(insn), rs_val < (uint32_t)SIMM(insn) ? 1 : 0); break; /* SLTIU */ case 0x0C: set_reg(cpu, RT(insn), rs_val & IMM16(insn)); break; /* ANDI */ - case 0x0D: set_reg(cpu, RT(insn), rs_val | IMM16(insn)); break; /* ORI */ + case 0x0D: set_reg(cpu, RT(insn), rs_val | IMM16(insn)); + psx_pgxp_alu(cpu, insn, cpu->gpr[RT(insn)], rs_val, IMM16(insn)); break; /* ORI */ case 0x0E: set_reg(cpu, RT(insn), rs_val ^ IMM16(insn)); break; /* XORI */ - case 0x0F: set_reg(cpu, RT(insn), (uint32_t)IMM16(insn) << 16); break; /* LUI */ + case 0x0F: set_reg(cpu, RT(insn), (uint32_t)IMM16(insn) << 16); + psx_pgxp_alu(cpu, insn, cpu->gpr[RT(insn)], 0, 0); break; /* LUI */ /* --- COP0 --- */ case 0x10: { @@ -376,14 +399,21 @@ static void exec_one(CPUState* cpu) { case 0x12: { uint32_t cop_op = RS(insn); switch (cop_op) { - case 0x00: /* MFC2 */ - set_load_delay(RT(insn), gte_read_data(cpu, RD(insn))); + case 0x00: { /* MFC2 */ + uint32_t v = gte_read_data(cpu, RD(insn)); + set_load_delay(RT(insn), v); + psx_pgxp_cop2(cpu, insn, v, 0); break; - case 0x02: /* CFC2 */ - set_load_delay(RT(insn), gte_read_ctrl(cpu, RD(insn))); + } + case 0x02: { /* CFC2 */ + uint32_t v = gte_read_ctrl(cpu, RD(insn)); + set_load_delay(RT(insn), v); + psx_pgxp_cop2(cpu, insn, v, 0); break; + } case 0x04: /* MTC2 */ gte_write_data(cpu, RD(insn), rt_val); + psx_pgxp_cop2(cpu, insn, rt_val, 0); break; case 0x06: /* CTC2 */ gte_write_ctrl(cpu, RD(insn), rt_val); @@ -403,12 +433,16 @@ static void exec_one(CPUState* cpu) { /* --- Loads --- */ case 0x20: { /* LB */ uint32_t addr = rs_val + (uint32_t)SIMM(insn); - set_load_delay(RT(insn), (uint32_t)(int32_t)(int8_t)cpu->read_byte(addr)); + uint32_t v = (uint32_t)(int32_t)(int8_t)cpu->read_byte(addr); + set_load_delay(RT(insn), v); + psx_pgxp_load(cpu, insn, addr, v); break; } case 0x21: { /* LH */ uint32_t addr = rs_val + (uint32_t)SIMM(insn); - set_load_delay(RT(insn), (uint32_t)(int32_t)(int16_t)cpu->read_half(addr)); + uint32_t v = (uint32_t)(int32_t)(int16_t)cpu->read_half(addr); + set_load_delay(RT(insn), v); + psx_pgxp_load(cpu, insn, addr, v); break; } case 0x22: { /* LWL */ @@ -423,21 +457,28 @@ static void exec_one(CPUState* cpu) { case 3: cur = word; break; } set_reg(cpu, RT(insn), cur); + psx_pgxp_load(cpu, insn, addr, cur); break; } case 0x23: { /* LW */ uint32_t addr = rs_val + (uint32_t)SIMM(insn); - set_load_delay(RT(insn), cpu->read_word(addr)); + uint32_t v = cpu->read_word(addr); + set_load_delay(RT(insn), v); + psx_pgxp_load(cpu, insn, addr, v); break; } case 0x24: { /* LBU */ uint32_t addr = rs_val + (uint32_t)SIMM(insn); - set_load_delay(RT(insn), (uint32_t)cpu->read_byte(addr)); + uint32_t v = (uint32_t)cpu->read_byte(addr); + set_load_delay(RT(insn), v); + psx_pgxp_load(cpu, insn, addr, v); break; } case 0x25: { /* LHU */ uint32_t addr = rs_val + (uint32_t)SIMM(insn); - set_load_delay(RT(insn), (uint32_t)cpu->read_half(addr)); + uint32_t v = (uint32_t)cpu->read_half(addr); + set_load_delay(RT(insn), v); + psx_pgxp_load(cpu, insn, addr, v); break; } case 0x26: { /* LWR */ @@ -452,6 +493,7 @@ static void exec_one(CPUState* cpu) { case 3: cur = (cur & 0xFFFFFF00u) | (word >> 24); break; } set_reg(cpu, RT(insn), cur); + psx_pgxp_load(cpu, insn, addr, cur); break; } @@ -459,11 +501,13 @@ static void exec_one(CPUState* cpu) { case 0x28: { /* SB */ uint32_t addr = rs_val + (uint32_t)SIMM(insn); cpu->write_byte(addr, (uint8_t)(rt_val & 0xFF)); + psx_pgxp_store(cpu, insn, addr, rt_val & 0xFFu); break; } case 0x29: { /* SH */ uint32_t addr = rs_val + (uint32_t)SIMM(insn); cpu->write_half(addr, (uint16_t)(rt_val & 0xFFFF)); + psx_pgxp_store(cpu, insn, addr, rt_val & 0xFFFFu); break; } case 0x2A: { /* SWL */ @@ -477,11 +521,13 @@ static void exec_one(CPUState* cpu) { case 3: cur = rt_val; break; } cpu->write_word(aligned, cur); + psx_pgxp_store(cpu, insn, addr, rt_val); break; } case 0x2B: { /* SW */ uint32_t addr = rs_val + (uint32_t)SIMM(insn); cpu->write_word(addr, rt_val); + psx_pgxp_store(cpu, insn, addr, rt_val); break; } case 0x2E: { /* SWR */ @@ -495,19 +541,24 @@ static void exec_one(CPUState* cpu) { case 3: cur = (cur & 0x00FFFFFFu) | (rt_val << 24); break; } cpu->write_word(aligned, cur); + psx_pgxp_store(cpu, insn, addr, rt_val); break; } /* --- COP2 load/store (LWC2/SWC2) --- */ case 0x32: { /* LWC2 */ uint32_t addr = rs_val + (uint32_t)SIMM(insn); - gte_write_data(cpu, RT(insn), cpu->read_word(addr)); + uint32_t v = cpu->read_word(addr); + gte_write_data(cpu, RT(insn), v); + psx_pgxp_cop2(cpu, insn, v, addr); break; } case 0x3A: { /* SWC2 */ uint32_t addr = rs_val + (uint32_t)SIMM(insn); - cpu->write_word(addr, gte_read_data(cpu, RT(insn))); + uint32_t v = gte_read_data(cpu, RT(insn)); + cpu->write_word(addr, v); gte_precision_store_word(addr, RT(insn)); + psx_pgxp_cop2(cpu, insn, v, addr); break; } diff --git a/runtime/tests/test_gte_register_access.cpp b/runtime/tests/test_gte_register_access.cpp index e00bf0a7..4de0b17a 100644 --- a/runtime/tests/test_gte_register_access.cpp +++ b/runtime/tests/test_gte_register_access.cpp @@ -488,7 +488,15 @@ int test_precise_sxy_invalidation() { for (uint8_t reg = 0; reg < 32; ++reg) { gte_test_set_precise_valid_mask(0xFu); gte_write_data(&cpu, reg, 0x12345678u); - const uint32_t expected = (reg >= 12 && reg <= 15) ? 0u : 0xFu; + /* The PGXP shadow drops exactly the register(s) the guest wrote: + * SXY0/SXY1 clear their own slot; SXY2 and SXYP clear both mirrors + * (regs 14+15). The untouched slots stay live — their register + * values did not change (SXYP's hardware FIFO shift makes 12/13 + * stale in VALUE, which validate-on-read handles at use; liveness + * alone is not a correctness claim). */ + uint32_t expected = 0xFu; + if (reg == 12 || reg == 13) expected &= ~(1u << (reg - 12)); + else if (reg == 14 || reg == 15) expected &= ~0xCu; const uint32_t actual = gte_test_get_precise_valid_mask(); if (actual != expected) return fail_value("precise SXY invalidation", 0, reg, diff --git a/runtime/tests/test_overlay_capture_retry.cpp b/runtime/tests/test_overlay_capture_retry.cpp index a8a8e0e0..fe126330 100644 --- a/runtime/tests/test_overlay_capture_retry.cpp +++ b/runtime/tests/test_overlay_capture_retry.cpp @@ -124,8 +124,13 @@ extern "C" uint32_t crc32_compute(const uint8_t *data, size_t size) { } int main() { - SDL_SetMainReady(); - if (SDL_Init(0) != 0) { + /* psx_sdl_init: 0 on success under BOTH backends. SDL3's SDL.h no longer + * pulls SDL_main.h, and its SDL_Init returns bool (true = success), so + * the previous SDL_SetMainReady() + `SDL_Init(0) != 0` rotted twice over + * when the default backend moved to SDL3: it did not compile, and the + * check was inverted. SDL_SetMainReady is unnecessary here — this test + * never uses SDL_main. */ + if (psx_sdl_init(0) != 0) { std::fprintf(stderr, "FAIL: SDL_Init: %s\n", SDL_GetError()); return 1; } diff --git a/runtime/tests/test_pgxp.cpp b/runtime/tests/test_pgxp.cpp new file mode 100644 index 00000000..59df32f3 --- /dev/null +++ b/runtime/tests/test_pgxp.cpp @@ -0,0 +1,294 @@ +/* test_pgxp.cpp — PGXP value-propagation engine unit tests (ENHANCEMENTS.md + * G1.2/G1.3). White-box over runtime/src/pgxp.cpp with the gte.cpp fallback + * cache stubbed, exercising exactly the properties the engine's safety rests + * on: provenance roundtrips, validate-on-read, half-word semantics, the + * repack arithmetic, the suppression bracket, and the GPU-side safeguards. */ + +#include "pgxp.h" +#include "pgxp_hooks.h" + +#include +#include + +static int g_failures = 0; +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, \ + #cond); \ + g_failures++; \ + } \ + } while (0) + +/* ---- gte.cpp fallback-cache stub ----------------------------------------- */ + +static uint32_t g_fb_packed = 0; +static int32_t g_fb_x16 = 0, g_fb_y16 = 0; +static int g_fb_valid = 0; + +extern "C" int gte_geometry_correction_lookup(uint32_t packed, + int32_t *x16, int32_t *y16) { + if (!g_fb_valid || packed != g_fb_packed) return 0; + if (x16) *x16 = g_fb_x16; + if (y16) *y16 = g_fb_y16; + return 1; +} + +/* ---- MIPS encodings ------------------------------------------------------ */ + +static uint32_t enc_i(uint32_t op, uint32_t rs, uint32_t rt, uint16_t imm) { + return (op << 26) | (rs << 21) | (rt << 16) | imm; +} +static uint32_t enc_r(uint32_t rs, uint32_t rt, uint32_t rd, uint32_t sh, + uint32_t funct) { + return (rs << 21) | (rt << 16) | (rd << 11) | (sh << 6) | funct; +} +static uint32_t enc_cop2(uint32_t sub, uint32_t rt, uint32_t rd) { + return (0x12u << 26) | (sub << 21) | (rt << 16) | (rd << 11); +} + +#define LW(rs, rt) enc_i(0x23, rs, rt, 0) +#define SW(rs, rt) enc_i(0x2B, rs, rt, 0) +#define LH(rs, rt) enc_i(0x21, rs, rt, 0) +#define LHU(rs, rt) enc_i(0x25, rs, rt, 0) +#define SH(rs, rt) enc_i(0x29, rs, rt, 0) +#define SB(rs, rt) enc_i(0x28, rs, rt, 0) +#define LWC2(rt) enc_i(0x32, 1, rt, 0) +#define SWC2(rt) enc_i(0x3A, 1, rt, 0) +#define MFC2(rt, rd) enc_cop2(0x00, rt, rd) +#define MTC2(rt, rd) enc_cop2(0x04, rt, rd) +#define ADDIU(rs, rt, imm) enc_i(0x09, rs, rt, (uint16_t)(imm)) +#define LUI(rt, imm) enc_i(0x0F, 0, rt, (uint16_t)(imm)) +#define SLL(rt, rd, sh) enc_r(0, rt, rd, sh, 0x00) +#define SRA(rt, rd, sh) enc_r(0, rt, rd, sh, 0x03) +#define OR(rs, rt, rd) enc_r(rs, rt, rd, 0, 0x25) +#define ADDU(rs, rt, rd) enc_r(rs, rt, rd, 0, 0x21) + +/* One projected vertex: x = 160.5, y = 80.25 -> packed integer word. */ +static const uint32_t PACKED = (80u << 16) | 160u; +static const int32_t X16 = (160 << 16) | 0x8000; /* 160.5 */ +static const int32_t Y16 = (80 << 16) | 0x4000; /* 80.25 */ +static const uint16_t SZ3 = 100; + +static const uint32_t ADDR_A = 0x80100000u; /* packet slot A (KSEG0) */ +static const uint32_t ADDR_B = 0x00100040u; /* packet slot B (KUSEG) */ + +static void produce_at(uint32_t addr) { + pgxp_gte_push_sxy(X16, Y16, SZ3, PACKED); + psx_pgxp_cop2(nullptr, SWC2(14), PACKED, addr); +} + +static int lookup(uint32_t addr, uint32_t word, int32_t ix, int32_t iy, + int32_t *x, int32_t *y, uint16_t *z) { + int32_t lx, ly; uint16_t lz; + int r = pgxp_get_precise_vertex(addr, word, ix, iy, &lx, &ly, &lz); + if (x) *x = lx; + if (y) *y = ly; + if (z) *z = lz; + return r; +} + +int main(void) { + pgxp_set_enabled(1); + pgxp_set_tolerance(-1.0f); + pgxp_set_cpu_mode(0); + + /* --- SWC2 produce -> GPU consume (the perspective-texturing spine) --- */ + produce_at(ADDR_A); + { + int32_t x, y; uint16_t z; + CHECK(lookup(ADDR_A, PACKED, 160, 80, &x, &y, &z) == PGXP_SRC_DATAFLOW); + CHECK(x == X16 && y == Y16 && z == SZ3); + /* mirrors resolve to the same shadow word */ + CHECK(lookup(0xA0100000u, PACKED, 160, 80, &x, &y, &z) == + PGXP_SRC_DATAFLOW); + } + + /* --- DMA/untracked overwrite: value validation rejects the shadow --- */ + { + int32_t x, y; uint16_t z; + uint32_t other = (81u << 16) | 161u; + CHECK(lookup(ADDR_A, other, 161, 81, &x, &y, &z) == PGXP_SRC_NATIVE); + CHECK(x == (161 << 16) && y == (81 << 16) && z == 0); + } + + /* --- LW/SW roundtrip: packet copied by the CPU keeps provenance --- */ + produce_at(ADDR_A); + psx_pgxp_load(nullptr, LW(1, 8), ADDR_A, PACKED); + psx_pgxp_store(nullptr, SW(1, 8), ADDR_B, PACKED); + { + int32_t x, y; uint16_t z; + CHECK(lookup(ADDR_B, PACKED, 160, 80, &x, &y, &z) == PGXP_SRC_DATAFLOW); + CHECK(x == X16 && y == Y16 && z == SZ3); + } + + /* --- stale GPR: register changed between load and store --- */ + psx_pgxp_load(nullptr, LW(1, 8), ADDR_A, PACKED); + psx_pgxp_store(nullptr, SW(1, 8), ADDR_B, 0xDEADBEEFu); /* r8 mutated */ + CHECK(lookup(ADDR_B, 0xDEADBEEFu, 0, 0, nullptr, nullptr, nullptr) == + PGXP_SRC_NATIVE); + + /* --- MOVE idiom (memory mode, no cpu_mode needed) --- */ + produce_at(ADDR_A); + psx_pgxp_load(nullptr, LW(1, 8), ADDR_A, PACKED); + psx_pgxp_alu(nullptr, ADDU(8, 0, 10), PACKED, PACKED, 0); + psx_pgxp_store(nullptr, SW(1, 10), ADDR_B, PACKED); + CHECK(lookup(ADDR_B, PACKED, 160, 80, nullptr, nullptr, nullptr) == + PGXP_SRC_DATAFLOW); + + /* --- MFC2 -> SW (register transfer path) --- */ + pgxp_gte_push_sxy(X16, Y16, SZ3, PACKED); + psx_pgxp_cop2(nullptr, MFC2(9, 14), PACKED, 0); + psx_pgxp_store(nullptr, SW(1, 9), ADDR_B, PACKED); + CHECK(lookup(ADDR_B, PACKED, 160, 80, nullptr, nullptr, nullptr) == + PGXP_SRC_DATAFLOW); + + /* --- LH/SH: halves travel independently, depth does not survive --- */ + produce_at(ADDR_A); + psx_pgxp_load(nullptr, LHU(1, 8), ADDR_A + 2u, PACKED >> 16); /* Y */ + psx_pgxp_store(nullptr, SH(1, 8), ADDR_B + 2u, PACKED >> 16); + psx_pgxp_load(nullptr, LHU(1, 8), ADDR_A, PACKED & 0xFFFFu); /* X */ + psx_pgxp_store(nullptr, SH(1, 8), ADDR_B, PACKED & 0xFFFFu); + { + int32_t x, y; uint16_t z; + CHECK(lookup(ADDR_B, PACKED, 160, 80, &x, &y, &z) == PGXP_SRC_DATAFLOW); + CHECK(x == X16 && y == Y16); + CHECK(z == 0); /* SH killed the vertex depth */ + } + + /* --- SB destroys the touched half only --- */ + produce_at(ADDR_B); + psx_pgxp_store(nullptr, SB(1, 8), ADDR_B, PACKED & 0xFFu); /* same byte */ + { + int32_t x, y; uint16_t z; + /* low half invalidated -> not a full XY hit anymore */ + CHECK(lookup(ADDR_B, PACKED, 160, 80, &x, &y, &z) != PGXP_SRC_DATAFLOW); + } + + /* --- cpu-mode repack: lhu / sll 16 / or (the classic vertex build) --- */ + pgxp_set_cpu_mode(1); + produce_at(ADDR_A); + psx_pgxp_load(nullptr, LHU(1, 8), ADDR_A + 2u, PACKED >> 16); /* Y */ + psx_pgxp_alu(nullptr, SLL(8, 9, 16), (PACKED >> 16) << 16, + PACKED >> 16, 16); + psx_pgxp_load(nullptr, LHU(1, 10), ADDR_A, PACKED & 0xFFFFu); /* X */ + psx_pgxp_alu(nullptr, OR(9, 10, 11), PACKED, + (PACKED >> 16) << 16, PACKED & 0xFFFFu); + psx_pgxp_store(nullptr, SW(1, 11), ADDR_B, PACKED); + { + int32_t x, y; + CHECK(lookup(ADDR_B, PACKED, 160, 80, &x, &y, nullptr) == + PGXP_SRC_DATAFLOW); + CHECK(x == X16 && y == Y16); + } + + /* --- cpu-mode addiu: fraction rides an integer offset (incl. -N) --- */ + psx_pgxp_load(nullptr, LW(1, 8), ADDR_A, PACKED); + psx_pgxp_alu(nullptr, ADDIU(8, 12, 4), PACKED + 4u, PACKED, 4u); + psx_pgxp_store(nullptr, SW(1, 12), ADDR_B, PACKED + 4u); + { + int32_t x, y; + CHECK(lookup(ADDR_B, PACKED + 4u, 164, 80, &x, &y, nullptr) == + PGXP_SRC_DATAFLOW); + CHECK(x == X16 + (4 << 16) && y == Y16); + } + psx_pgxp_load(nullptr, LW(1, 8), ADDR_A, PACKED); + psx_pgxp_alu(nullptr, ADDIU(8, 12, (uint16_t)-4), PACKED - 4u, PACKED, + (uint32_t)(int32_t)-4); + psx_pgxp_store(nullptr, SW(1, 12), ADDR_B, PACKED - 4u); + { + int32_t x; + CHECK(lookup(ADDR_B, PACKED - 4u, 156, 80, &x, nullptr, nullptr) == + PGXP_SRC_DATAFLOW); + CHECK(x == X16 - (4 << 16)); + } + pgxp_set_cpu_mode(0); + + /* --- cpu-mode OFF: the same repack must degrade to native, cleanly --- */ + produce_at(ADDR_A); + psx_pgxp_load(nullptr, LHU(1, 8), ADDR_A + 2u, PACKED >> 16); + psx_pgxp_alu(nullptr, SLL(8, 9, 16), (PACKED >> 16) << 16, + PACKED >> 16, 16); + psx_pgxp_store(nullptr, SW(1, 9), ADDR_B, (PACKED >> 16) << 16); + CHECK(lookup(ADDR_B, (PACKED >> 16) << 16, 0, 80, nullptr, nullptr, + nullptr) == PGXP_SRC_NATIVE); + + /* --- truncation agreement: integer part must match the native parse --- */ + produce_at(ADDR_A); + CHECK(lookup(ADDR_A, PACKED, 161, 80, nullptr, nullptr, nullptr) == + PGXP_SRC_NATIVE); + + /* --- tolerance clamp --- */ + produce_at(ADDR_A); + pgxp_set_tolerance(0.25f); /* fraction is 0.5 -> reject */ + CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) == + PGXP_SRC_NATIVE); + pgxp_set_tolerance(0.75f); /* 0.5 <= 0.75 -> accept */ + CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) == + PGXP_SRC_DATAFLOW); + pgxp_set_tolerance(-1.0f); + + /* --- fallback tier: no address -> position cache, never a depth --- */ + g_fb_valid = 1; g_fb_packed = PACKED; g_fb_x16 = X16; g_fb_y16 = Y16; + { + int32_t x, y; uint16_t z; + CHECK(lookup(0xFFFFFFFFu, PACKED, 160, 80, &x, &y, &z) == + PGXP_SRC_FALLBACK); + CHECK(x == X16 && y == Y16 && z == 0); + } + g_fb_valid = 0; + + /* --- suppression bracket: nothing records inside it --- */ + pgxp_invalidate_all(); + pgxp_suppress_begin(); + produce_at(ADDR_A); + pgxp_suppress_end(); + CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) == + PGXP_SRC_NATIVE); + + /* --- deferred invalidate inside the bracket --- */ + produce_at(ADDR_A); + pgxp_suppress_begin(); + pgxp_invalidate_all(); /* deferred */ + pgxp_suppress_end(); /* applies here */ + CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) == + PGXP_SRC_NATIVE); + + /* --- invalidate-all + generation wrap --- */ + produce_at(ADDR_A); + pgxp_invalidate_all(); + CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) == + PGXP_SRC_NATIVE); + pgxp_test_set_generation(0xFFFFFFFFu); + pgxp_invalidate_all(); + CHECK(pgxp_test_generation() == 1u); + + /* --- test accessors mirror the SXY FIFO shadows --- */ + pgxp_test_seed_gte_sxy(2, PACKED, X16, Y16, SZ3, 1); + { + uint32_t packed; int32_t x, y; uint16_t z; uint8_t valid; + pgxp_test_get_gte_sxy(2, &packed, &x, &y, &z, &valid); + CHECK(valid && packed == PACKED && x == X16 && y == Y16 && z == SZ3); + pgxp_test_seed_gte_sxy(2, 0, 0, 0, 0, 0); + pgxp_test_get_gte_sxy(2, &packed, &x, &y, &z, &valid); + CHECK(!valid); + } + + /* --- stats sanity: dataflow hits were counted --- */ + { + PGXPStats st; + pgxp_get_stats(&st); + CHECK(st.lookups > 0); + CHECK(st.dataflow_hit > 0); + CHECK(st.native > 0); + CHECK(st.fallback_hit > 0); + CHECK(st.value_mismatch > 0); + } + + if (g_failures) { + std::fprintf(stderr, "test_pgxp: %d FAILURES\n", g_failures); + return 1; + } + std::printf("test_pgxp: all checks passed\n"); + return 0; +}