mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 18:25:35 -04:00
8630d3a6f8
With every drawn vertex precise, a title can still show cracks where the game
culled a face on its integer winding. R4's road emitter runs NCLIP on each
far row and skips rows whose native height rounds to 0. Natively their
neighbours meet at the same integer y. With PGXP they sit at their exact y,
and the skipped row becomes a strip of sky across the road beyond the start
gantry, at 4K and in 2P. tri_mixed cannot see this: the missing face draws
no triangle.
Precise culling (DuckStation's "PGXP culling", minimal footprint):
- While geometry correction is armed, gte_nclip computes the exact
determinant of the three SXY register shadows (pgxp_gte_nclip_precise:
live, current words, the GPU's own truncation agreement and tolerance).
When its sign is nonzero and differs from the integer MAC0's, MAC0 takes
that sign, with the rounded exact doubled area (at least 1) as magnitude.
FLAG keeps the integer overflow bits.
- It changes guest-visible MAC0, so it is a `culling` option of
psx.enhancement.pgxp, false in the builtin, with no [video] key, so netplay
(which clears every mod) never runs it. It needs geometry correction.
It is held off in every pass compared against another execution: the
speculative and replay passes, and the whole overlay shadow diff
(psx_overlay_shadow_diff_active), interpreter pass included.
- Counters whenever geometry correction is armed: nclip_precise,
nclip_disagree (exact sign differs: the culled-face crack exposure),
nclip_corrected. TCP pgxp {"culling":0|1}; env PSX_PGXP_CULLING.
Session arming in one place (src/pgxp_session.cpp, psx_pgxp_session_arm):
main.cpp's renderer setup now calls the same code the test drives. The arming
takes the mod request (reads and clears it), so a session with an empty plan
cannot inherit an earlier session's request even if the reset were skipped.
pgxp_session_test now links the real mod runtime, the builtin plugin and the
builtin manifest, and covers:
- a default-on override;
- a netplay clear, with and without the reset;
- the off switch;
- the options;
- env overrides;
- pgxp_mod_only.
[video] pgxp_mod_only (game.toml, default false): for a title that ships
PGXP through the mod, the mod is the one switch. The [video]
geometry_correction / perspective_texturing / pgxp_cpu_mode values are not
applied, from game.toml or the player's settings.toml. The launcher also
hides its Perspective textures row (has_geometry_precision = 0). That row
showed "off" while a default-on mod corrected textures, and it could keep
texture correction on after the player switched the mod off.
geom_correction also reports texture_correction and the culling switch.
R4, Helter Skelter: 28.7% of NCLIPs disagree in sign. With culling, the
ladder is gone in same-frame 4K captures (1P straight and crest, 2P VS), and
the game emits about 24% more triangles. Its packet buffers peak at 60%.
fp_identity over 12000 frames: stock, PGXP off, and PGXP on with culling off
are IDENTICAL; with culling on, the RAM writes differ from the attract race
on, while cyc/mmio/mc agree throughout.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
998 lines
41 KiB
C++
998 lines
41 KiB
C++
/* pgxp.cpp — PGXP value-propagation engine (docs/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 "psx_memory.h"
|
|
|
|
#include <cmath>
|
|
#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) */
|
|
};
|
|
|
|
/* Shadow covers the host RAM backing so the opt-in 8 MB map tracks its high
|
|
* banks; retail sessions only ever touch the low 2 MiB of it. */
|
|
#define PGXP_RAM_WORDS (PSX_MAIN_RAM_BACKING_BYTES >> 2)
|
|
#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, ~40 MB VA */
|
|
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 = 0.5f; /* user-validated seam clamp (G1.10) */
|
|
static int s_position_fallback = 1; /* G1.4 cache tier (G1.11 switch) */
|
|
static int s_preserve_projection = 0; /* exact-projection shadows (G1.11) */
|
|
static int s_culling = 0; /* precise NCLIP sign (G1.12) */
|
|
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) {
|
|
int resized = 0;
|
|
if (enabled && !s_ram) {
|
|
s_ram = (PGXPValue *)std::calloc(PGXP_RAM_WORDS, sizeof(PGXPValue));
|
|
if (s_ram)
|
|
resized = 1;
|
|
else
|
|
enabled = 0; /* fail closed: stay faithful */
|
|
}
|
|
/* Re-applying configuration must not invalidate every live shadow. */
|
|
if (s_enabled == (enabled ? 1 : 0) && !resized)
|
|
return;
|
|
s_enabled = enabled ? 1 : 0;
|
|
pgxp_invalidate_all();
|
|
recompute_active();
|
|
}
|
|
|
|
extern "C" int pgxp_enabled(void) { return s_enabled; }
|
|
extern "C" int pgxp_active(void) { return g_pgxp_active; }
|
|
|
|
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_set_position_fallback(int enabled) {
|
|
s_position_fallback = enabled ? 1 : 0;
|
|
}
|
|
extern "C" int pgxp_position_fallback(void) { return s_position_fallback; }
|
|
|
|
extern "C" void pgxp_set_preserve_projection(int enabled) {
|
|
s_preserve_projection = enabled ? 1 : 0;
|
|
}
|
|
extern "C" int pgxp_preserve_projection(void) { return s_preserve_projection; }
|
|
|
|
extern "C" void pgxp_set_culling(int enabled) { s_culling = enabled ? 1 : 0; }
|
|
extern "C" int pgxp_culling(void) { return s_culling; }
|
|
|
|
static int s_mod_request = 0;
|
|
static int s_mod_request_cpu = 0;
|
|
static int s_mod_request_cull = 0;
|
|
|
|
extern "C" void pgxp_mod_request(int enabled, int cpu_mode, int culling) {
|
|
s_mod_request = enabled ? 1 : 0;
|
|
s_mod_request_cpu = (enabled && cpu_mode) ? 1 : 0;
|
|
s_mod_request_cull = (enabled && culling) ? 1 : 0;
|
|
}
|
|
|
|
extern "C" int pgxp_mod_requested(int *cpu_mode, int *culling) {
|
|
if (cpu_mode) *cpu_mode = s_mod_request_cpu;
|
|
if (culling) *culling = s_mod_request_cull;
|
|
return s_mod_request;
|
|
}
|
|
|
|
extern "C" int pgxp_mod_request_take(int *cpu_mode, int *culling) {
|
|
const int requested = pgxp_mod_requested(cpu_mode, culling);
|
|
pgxp_mod_request(0, 0, 0);
|
|
return requested;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
extern "C" void pgxp_note_rect_bypass(int all_precise) {
|
|
if (all_precise) s_stats.rect_bypass++;
|
|
else s_stats.rect_partial++;
|
|
}
|
|
|
|
extern "C" void pgxp_note_nclip(int disagree, int corrected) {
|
|
if (disagree) s_stats.nclip_disagree++;
|
|
if (corrected) s_stats.nclip_corrected++;
|
|
}
|
|
|
|
extern "C" void pgxp_note_triangle(int precise) {
|
|
if (precise >= 3) s_stats.tri_precise++;
|
|
else if (precise > 0) s_stats.tri_mixed++;
|
|
else s_stats.tri_native++;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* 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 < PSX_MAIN_RAM_WINDOW_BYTES) /* RAM + its mirrors (live map) */
|
|
return s_ram ? &s_ram[psx_ram_canonical_offset(m) >> 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" int pgxp_project_precise(int64_t mac1, int64_t mac2, int64_t mac3,
|
|
int shift, int32_t ir1, int32_t ir2,
|
|
uint32_t sz3, uint32_t h,
|
|
int32_t ofx, int32_t ofy,
|
|
int64_t x_num, int64_t x_den,
|
|
int32_t *x16, int32_t *y16) {
|
|
/* Qualify exactly the vertices whose guest projection is the plain
|
|
* IR * (H / SZ3) of the continuous one: sf=1, no IR1/IR2 clamp (the
|
|
* stored IR equals MAC >> 12), SZ3 unclamped and nonzero, and no UNR
|
|
* divide overflow (the GTE saturates the quotient when H >= 2*SZ3). */
|
|
if (shift != 12 || x_den <= 0) return 0;
|
|
if ((mac1 >> 12) != (int64_t)ir1 || (mac2 >> 12) != (int64_t)ir2) return 0;
|
|
const int64_t z_int = mac3 >> 12;
|
|
if (z_int <= 0 || z_int > 0xFFFF || (uint32_t)z_int != sz3) return 0;
|
|
if ((int64_t)h >= 2 * z_int) return 0;
|
|
/* (MAC/4096) * H / (MAC3/4096): the 4096s cancel. Double keeps ~52 bits,
|
|
* far below the 1/65536 px the shadow carries. Host-only and visual-only,
|
|
* so cross-platform bit equality of this value is not required. */
|
|
const double k = (double)h * 65536.0 / (double)mac3;
|
|
double fx = (double)ofx +
|
|
(double)mac1 * k * (double)x_num / (double)x_den;
|
|
double fy = (double)ofy + (double)mac2 * k;
|
|
/* Same transport bound as the IR path (gte.cpp): an extreme value must
|
|
* not wrap an int32 and masquerade as an on-screen shadow. */
|
|
const double lim = 4096.0 * 65536.0;
|
|
if (fx < -lim) fx = -lim;
|
|
if (fx > lim - 1.0) fx = lim - 1.0;
|
|
if (fy < -lim) fy = -lim;
|
|
if (fy > lim - 1.0) fy = lim - 1.0;
|
|
*x16 = (int32_t)std::floor(fx);
|
|
*y16 = (int32_t)std::floor(fy);
|
|
return 1;
|
|
}
|
|
|
|
/* Defined below with the GPU consumer. */
|
|
static inline int pgxp_agrees(int32_t p16, int32_t native, int16_t half);
|
|
|
|
extern "C" int pgxp_ppp_accept(int32_t x16, int32_t y16, uint32_t packed) {
|
|
const int16_t hx = (int16_t)(packed & 0xFFFFu);
|
|
const int16_t hy = (int16_t)(packed >> 16);
|
|
if (pgxp_agrees(x16, hx, hx) && pgxp_agrees(y16, hy, hy)) {
|
|
s_stats.ppp_produced++;
|
|
return 1;
|
|
}
|
|
s_stats.ppp_window_fallback++;
|
|
return 0;
|
|
}
|
|
|
|
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_stats.produced++;
|
|
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" int pgxp_get_gte_sxy(uint32_t index, int32_t *x16, int32_t *y16) {
|
|
return pgxp_get_gte_sxy_checked(index, 0u, 0, x16, y16);
|
|
}
|
|
|
|
extern "C" int pgxp_get_gte_sxy_checked(uint32_t index, uint32_t expect,
|
|
int check, int32_t *x16, int32_t *y16) {
|
|
if (index >= 4) return 0;
|
|
const PGXPValue *pv = &s_gte[12 + index];
|
|
if (!pv_live(pv) || (pv->flags & PGXP_F_VXY) != PGXP_F_VXY)
|
|
return 0;
|
|
if (check && pv->value != expect)
|
|
return 0;
|
|
if (x16) *x16 = pv->x16;
|
|
if (y16) *y16 = pv->y16;
|
|
return 1;
|
|
}
|
|
|
|
/* Defined below with the GPU consumer. */
|
|
static inline int pgxp_accept(int32_t px, int32_t py, int32_t int_x,
|
|
int32_t int_y, uint32_t word);
|
|
|
|
extern "C" int pgxp_gte_nclip_precise(const uint32_t sxy[3], int64_t *cross) {
|
|
if (!g_pgxp_active) return 0;
|
|
int64_t x[3], y[3];
|
|
for (int i = 0; i < 3; i++) {
|
|
const PGXPValue *pv = &s_gte[12 + i];
|
|
if (!pv_live(pv) || (pv->flags & PGXP_F_VXY) != PGXP_F_VXY ||
|
|
pv->value != sxy[i])
|
|
return 0;
|
|
/* The GPU parses the packet half as the same signed integer for every
|
|
* value the GTE can produce (its saturation limits are -0x400 and
|
|
* 0x3FF), so the register half stands in for the native parse. */
|
|
if (pgxp_accept(pv->x16, pv->y16, (int16_t)(sxy[i] & 0xFFFFu),
|
|
(int16_t)(sxy[i] >> 16), sxy[i]) != 0)
|
|
return 0;
|
|
x[i] = pv->x16;
|
|
y[i] = pv->y16;
|
|
}
|
|
/* NCLIP's determinant, sx0*(sy1-sy2) + sx1*(sy2-sy0) + sx2*(sy0-sy1),
|
|
* on 16.16 positions. Positions are bounded to +-2^28 (the transport
|
|
* clamp), so every product fits in 64 bits. */
|
|
*cross = (x[1] - x[0]) * (y[2] - y[0]) - (y[1] - y[0]) * (x[2] - x[0]);
|
|
s_stats.nclip_precise++;
|
|
return 1;
|
|
}
|
|
|
|
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 */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/* Truncation agreement for one axis. `native` is the GPU's parse of the
|
|
* packet half, `half` the raw 16-bit half in the word. Exact (integer part
|
|
* equals the native parse) unless preserve-projection is on; then the precise
|
|
* position may sit a bounded distance from it (PGXP_PPP_AGREE_*), except
|
|
* when the half is at or beyond the GTE saturation limits: there the guest
|
|
* integer is a clamp that says nothing about the vertex, or (beyond them) a
|
|
* CPU-modified word the GPU's 11-bit parse wraps. */
|
|
static inline int pgxp_agrees(int32_t p16, int32_t native, int16_t half) {
|
|
if (!s_preserve_projection || half <= -0x400 || half >= 0x3FF)
|
|
return (p16 >> 16) == native;
|
|
const int64_t d = (int64_t)p16 - (int64_t)native * 65536;
|
|
return d > -(int64_t)PGXP_PPP_AGREE_BELOW * 65536 &&
|
|
d < (int64_t)PGXP_PPP_AGREE_ABOVE * 65536;
|
|
}
|
|
|
|
/* The consumer-side safeguards on one candidate position: truncation
|
|
* agreement on both axes, then the tolerance clamp. Returns 0 when accepted,
|
|
* 1 for a truncation reject, 2 for a tolerance reject. Shared by the GPU
|
|
* lookup and the precise NCLIP so culling and drawing believe exactly the
|
|
* same vertices. */
|
|
static inline int pgxp_accept(int32_t px, int32_t py, int32_t int_x,
|
|
int32_t int_y, uint32_t word) {
|
|
if (!pgxp_agrees(px, int_x, (int16_t)(word & 0xFFFFu)) ||
|
|
!pgxp_agrees(py, int_y, (int16_t)(word >> 16)))
|
|
return 1;
|
|
if (s_tolerance >= 0.0f) {
|
|
float dx = (float)((int64_t)px - (int64_t)int_x * 65536) * (1.0f / 65536.0f);
|
|
float dy = (float)((int64_t)py - (int64_t)int_y * 65536) * (1.0f / 65536.0f);
|
|
if (std::fabs(dx) > s_tolerance || std::fabs(dy) > s_tolerance)
|
|
return 2;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
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 && s_position_fallback &&
|
|
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. Then the
|
|
* tolerance clamp. */
|
|
const int why = pgxp_accept(px, py, int_x, int_y, packet_word);
|
|
if (why == 1) {
|
|
s_stats.trunc_reject++;
|
|
have = 0;
|
|
} else if (why == 2) {
|
|
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;
|
|
}
|
|
|
|
extern "C" int pgxp_probe_precise_vertex(uint32_t addr, uint32_t packet_word,
|
|
int32_t int_x, int32_t int_y) {
|
|
const PGXPStats saved = s_stats;
|
|
int32_t x16, y16;
|
|
uint16_t sz;
|
|
const int src = pgxp_get_precise_vertex(addr, packet_word, int_x, int_y,
|
|
&x16, &y16, &sz);
|
|
s_stats = saved;
|
|
return src;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* 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;
|
|
s_stats.swc2_stores++;
|
|
*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" uint32_t pgxp_test_suppress_depth(void) { return s_suppress; }
|
|
extern "C" int pgxp_test_active(void) { return g_pgxp_active; }
|
|
|
|
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;
|
|
}
|