mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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`).
|
||||
|
||||
@@ -571,6 +571,13 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
rt.video_perspective_texturing =
|
||||
toml::find<bool>(video, "perspective_texturing");
|
||||
}
|
||||
if (video.contains("pgxp_cpu_mode")) {
|
||||
rt.video_pgxp_cpu_mode = toml::find<bool>(video, "pgxp_cpu_mode");
|
||||
}
|
||||
if (video.contains("pgxp_tolerance")) {
|
||||
rt.video_pgxp_tolerance =
|
||||
toml::find<double>(video, "pgxp_tolerance");
|
||||
}
|
||||
if (video.contains("crt_filter")) {
|
||||
const auto mode = toml::find<std::string>(video, "crt_filter");
|
||||
if (mode == "raw") rt.video_screen_kind = 0;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
* ----------------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
#include "cpu_state.h"
|
||||
#include "pgxp_hooks.h"
|
||||
#include <stdint.h>
|
||||
|
||||
/* 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
|
||||
|
||||
@@ -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 <stdint.h>
|
||||
|
||||
#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 */
|
||||
@@ -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 <stdint.h>
|
||||
|
||||
#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 */
|
||||
+19
-7
@@ -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 "$<TARGET_FILE_DIR:${target}>/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 "$<TARGET_FILE_DIR:${target}>/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
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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:
|
||||
|
||||
+51
-42
@@ -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],
|
||||
|
||||
+58
-117
@@ -1,6 +1,7 @@
|
||||
#include "gte.h"
|
||||
#include "cpu_state.h"
|
||||
#include "nd_intro_ot.h"
|
||||
#include "pgxp.h"
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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 <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
/* 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user