mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
2158 lines
94 KiB
C++
2158 lines
94 KiB
C++
#include "execution_storage.h"
|
|
/* 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 safety invariant: a shadow is only ever BELIEVED after validation
|
|
* against the actual guest word it claims to describe, and only while no
|
|
* writer it did not see has replaced that word. A matching word alone does
|
|
* not prove provenance: two projections that round to the same integer X/Y
|
|
* carry different fractions and depths. So every CPU write of a GPR or
|
|
* memory word runs a hook that carries or resets its shadow, and DMA / host
|
|
* stores drop the shadow of each word they touch (pgxp_invalidate_word, from
|
|
* memory.c). Validation still catches any other writer that changes the
|
|
* word. We never model side effects — overwrite, invalidate, validate.
|
|
*
|
|
* 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 "psx_thread_local.h"
|
|
#include "pgxp_hooks.h"
|
|
#include "cpu_state.h"
|
|
#include "psx_memory.h"
|
|
#include "mod_memory.h"
|
|
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <new>
|
|
|
|
/* 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);
|
|
extern "C" int gte_geometry_correction_lookup_probe(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_PROJECTION = 1u << 3,
|
|
/* The tracked half was DERIVED by guest arithmetic that rounds (a CPU
|
|
* divide of a precise sum, see the scalar tier below), so the guest
|
|
* integer is that arithmetic's rounding of the value, not its floor: the
|
|
* precise value may sit up to one pixel below or two above it. */
|
|
PGXP_F_DX = 1u << 4,
|
|
PGXP_F_DY = 1u << 5,
|
|
PGXP_F_UNCLAMPED = 1u << 6, /* exact GTE projection; intact word only */
|
|
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) */
|
|
uint32_t identity; /* projected vertex identity (valid with PROJECTION) */
|
|
PGXPProjection projection;
|
|
};
|
|
|
|
/* 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. The same array
|
|
* continues with the trusted-mod guest regions: the GPU DMA aperture (an
|
|
* enhancement's enlarged primitive arena, e.g. Crash widescreen) and the
|
|
* Expansion 1 mod memory. Packets built there are tracked exactly like
|
|
* packets in RAM; without this every vertex an enlarged arena carried drew
|
|
* native and had no depth. */
|
|
#define PGXP_RAM_WORDS (PSX_MAIN_RAM_BACKING_BYTES >> 2)
|
|
#define PGXP_APERTURE_WORDS (PSX_MOD_GPU_DMA_APERTURE_SIZE >> 2)
|
|
#define PGXP_MODMEM_WORDS (PSX_MOD_MEMORY_SIZE >> 2)
|
|
#define PGXP_SHADOW_WORDS (PGXP_RAM_WORDS + PGXP_APERTURE_WORDS + PGXP_MODMEM_WORDS)
|
|
#define PGXP_SCRATCH_WORDS (0x400u >> 2) /* 1 KB scratchpad */
|
|
#define PGXP_REG_HI 32
|
|
#define PGXP_REG_LO 33
|
|
|
|
static PSX_THREAD_LOCAL PGXPValue *s_ram = nullptr; /* lazily allocated: RAM + mod regions */
|
|
enum { PGXP_EXEC_PAGE_WORDS = 256,
|
|
PGXP_EXEC_PAGES = (PGXP_SHADOW_WORDS + PGXP_EXEC_PAGE_WORDS - 1) / PGXP_EXEC_PAGE_WORDS };
|
|
/* Tracking starts only when a replay consumer requests a snapshot. Ordinary
|
|
* execution does not allocate version tables. A replay marks its modified
|
|
* snapshot pages invalid, so their next publication recopies even when the
|
|
* corresponding live page has not changed. */
|
|
static PSX_THREAD_LOCAL uint64_t *s_exec_versions;
|
|
static PSX_THREAD_LOCAL uint64_t s_exec_epoch = 1;
|
|
static PSX_THREAD_LOCAL uint64_t *s_bound_versions;
|
|
static inline PGXPValue *execution_word(PGXPValue *word) {
|
|
if (word && (s_exec_versions || s_bound_versions) &&
|
|
(uintptr_t)word - (uintptr_t)s_ram < PGXP_SHADOW_WORDS * sizeof(PGXPValue)) {
|
|
const size_t page = (size_t)(word - s_ram) / PGXP_EXEC_PAGE_WORDS;
|
|
if (s_bound_versions) s_bound_versions[page] = 0;
|
|
else if (s_exec_versions[page] != s_exec_epoch) s_exec_versions[page] = s_exec_epoch;
|
|
}
|
|
return word;
|
|
}
|
|
static PSX_THREAD_LOCAL PGXPValue s_scratch[PGXP_SCRATCH_WORDS];
|
|
static PSX_THREAD_LOCAL PGXPValue s_gpr[34]; /* 32 GPRs + HI + LO */
|
|
static PSX_THREAD_LOCAL PGXPValue s_gte[32]; /* GTE data registers */
|
|
static PSX_THREAD_LOCAL uint32_t s_model_origin[6]; /* only the six model input words */
|
|
static PSX_THREAD_LOCAL PGXPValue s_gtc[32]; /* GTE control registers (CTC2) */
|
|
|
|
/* Scalar tier (cpu-mode). Engines also use the GTE as a multiplier for
|
|
* vertex arithmetic: Ape Escape's terrain subdivider packs two projected X
|
|
* (or Y) halves into a light-matrix row with CTC2, weights them with MVMVA
|
|
* by their depths, reads MAC back with MFC2 and DIVs by the depth sum to get
|
|
* the screen position of an edge midpoint. A 16.16 half cannot hold those
|
|
* intermediate products, so registers carry an optional full-range precise
|
|
* value beside their half shadows: valid while `gen` is current and `value`
|
|
* still equals the register, killed by every hooked write that does not
|
|
* produce one. It turns back into a half shadow only when stored (SH / SW)
|
|
* as a 16-bit coordinate. */
|
|
struct PGXPScalar {
|
|
double v; /* precise value of the 32-bit register */
|
|
uint32_t value; /* the register value it describes */
|
|
uint32_t gen;
|
|
};
|
|
static PSX_THREAD_LOCAL PGXPScalar s_gpr_s[34]; /* GPRs + HI + LO */
|
|
static PSX_THREAD_LOCAL PGXPScalar s_gte_s[32]; /* GTE data registers (IR, MAC) */
|
|
static PSX_THREAD_LOCAL PGXPScalar s_gtc_s[32]; /* GTE control (TR / BK words) */
|
|
|
|
static PSX_THREAD_LOCAL uint32_t s_gen = 1;
|
|
static PSX_THREAD_LOCAL int s_enabled = 0;
|
|
static PSX_THREAD_LOCAL int s_cpu_mode = 0;
|
|
static PSX_THREAD_LOCAL int s_projection_tracking = 0;
|
|
static PSX_THREAD_LOCAL float s_tolerance = 0.5f; /* user-validated seam clamp (G1.10) */
|
|
static PSX_THREAD_LOCAL int s_position_fallback = 1; /* G1.4 cache tier (G1.11 switch) */
|
|
static PSX_THREAD_LOCAL int s_preserve_projection = 0; /* exact-projection shadows (G1.11) */
|
|
static PSX_THREAD_LOCAL int s_culling = 0; /* precise NCLIP sign (G1.12) */
|
|
static PSX_THREAD_LOCAL uint32_t s_suppress = 0;
|
|
static PSX_THREAD_LOCAL int s_deferred_invalidate = 0;
|
|
|
|
/* Single hot-path gate for every hook. */
|
|
static PSX_THREAD_LOCAL int g_pgxp_active = 0;
|
|
static inline void recompute_active(void) {
|
|
g_pgxp_active = (s_enabled && s_suppress == 0) ? 1 : 0;
|
|
}
|
|
|
|
static PSX_THREAD_LOCAL PGXPStats s_stats;
|
|
static PSX_THREAD_LOCAL PGXPStoreRecord *s_store_ring = nullptr; /* pgxp_store_ring; with s_ram */
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Lifecycle */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static void ck_wrapped(void);
|
|
|
|
extern "C" void pgxp_invalidate_all(void) {
|
|
if (s_suppress != 0) { s_deferred_invalidate = 1; return; }
|
|
std::memset(s_model_origin, 0, sizeof s_model_origin);
|
|
if (++s_gen == 0) {
|
|
ck_wrapped();
|
|
/* generation wrapped: physically clear so stale slots can't revive */
|
|
if (s_ram) {
|
|
std::memset(s_ram, 0, PGXP_SHADOW_WORDS * sizeof(PGXPValue));
|
|
for (size_t page = 0; page < PGXP_EXEC_PAGES; ++page) {
|
|
if (s_bound_versions) s_bound_versions[page] = 0;
|
|
else if (s_exec_versions) s_exec_versions[page] = s_exec_epoch;
|
|
}
|
|
}
|
|
std::memset(s_scratch, 0, sizeof(s_scratch));
|
|
std::memset(s_gpr, 0, sizeof(s_gpr));
|
|
std::memset(s_gte, 0, sizeof(s_gte));
|
|
std::memset(s_gtc, 0, sizeof(s_gtc));
|
|
std::memset(s_gpr_s, 0, sizeof(s_gpr_s));
|
|
std::memset(s_gte_s, 0, sizeof(s_gte_s));
|
|
std::memset(s_gtc_s, 0, sizeof(s_gtc_s));
|
|
s_gen = 1;
|
|
}
|
|
}
|
|
|
|
extern "C" void pgxp_set_enabled(int enabled) {
|
|
int resized = 0;
|
|
/* Writer/triangle/miss receipts are consumed only by debug commands.
|
|
* Production still propagates every precision shadow and keeps summary
|
|
* counters, but does not copy a diagnostic record for each guest store. */
|
|
#ifndef PSX_NO_DEBUG_TOOLS
|
|
if (enabled && !s_store_ring)
|
|
s_store_ring = (PGXPStoreRecord *)std::calloc(PGXP_STORE_RING_CAP,
|
|
sizeof(PGXPStoreRecord));
|
|
#endif
|
|
if (enabled && !s_ram) {
|
|
s_ram = (PGXPValue *)std::calloc(PGXP_SHADOW_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_projection_tracking(int enabled) {
|
|
if (s_projection_tracking == !!enabled) return;
|
|
s_projection_tracking = !!enabled;
|
|
pgxp_invalidate_all();
|
|
}
|
|
extern "C" int pgxp_projection_tracking(void) { return s_projection_tracking; }
|
|
|
|
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 PSX_THREAD_LOCAL int s_mod_request = 0;
|
|
static PSX_THREAD_LOCAL int s_mod_request_cpu = 0;
|
|
static PSX_THREAD_LOCAL 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++;
|
|
}
|
|
|
|
static PSX_THREAD_LOCAL PGXPTriRecord s_tri_ring[PGXP_TRI_RING_CAP];
|
|
static PSX_THREAD_LOCAL uint64_t s_tri_seq = 0;
|
|
|
|
static int ck_in_pass(void); /* render-pass checkpoint open */
|
|
|
|
extern "C" void pgxp_note_triangle_detail(const PGXPTriRecord *rec) {
|
|
#ifndef PSX_NO_DEBUG_TOOLS
|
|
PGXPTriRecord *r = &s_tri_ring[s_tri_seq % PGXP_TRI_RING_CAP];
|
|
*r = *rec;
|
|
r->seq = s_tri_seq++;
|
|
r->pass = ck_in_pass() ? 1u : 0u;
|
|
#else
|
|
(void)rec;
|
|
#endif
|
|
}
|
|
|
|
extern "C" uint64_t pgxp_tri_ring(const PGXPTriRecord **ring, uint32_t *cap) {
|
|
*ring = s_tri_ring;
|
|
*cap = PGXP_TRI_RING_CAP;
|
|
return s_tri_seq;
|
|
}
|
|
|
|
/* Store PC and frame counter belong to the host (debug server); it hands
|
|
* their addresses over once (pgxp_set_trace_sources) so the engine stays
|
|
* linkable on its own in the white-box tests. */
|
|
static PSX_THREAD_LOCAL const uint32_t *s_trace_pc = nullptr;
|
|
static PSX_THREAD_LOCAL const uint64_t *s_trace_frame = nullptr;
|
|
static PSX_THREAD_LOCAL uint64_t s_store_seq = 0;
|
|
|
|
extern "C" void pgxp_set_trace_sources(const uint32_t *store_pc,
|
|
const uint64_t *frame) {
|
|
s_trace_pc = store_pc;
|
|
s_trace_frame = frame;
|
|
}
|
|
|
|
static void note_store(uint32_t op, uint32_t reg, const PGXPValue *src,
|
|
uint32_t addr, uint32_t value, const PGXPValue *dst) {
|
|
#ifndef PSX_NO_DEBUG_TOOLS
|
|
if (!s_store_ring) return;
|
|
PGXPStoreRecord *r = &s_store_ring[s_store_seq % PGXP_STORE_RING_CAP];
|
|
r->seq = s_store_seq++;
|
|
r->frame = s_trace_frame ? (uint32_t)*s_trace_frame : 0u;
|
|
r->pc = s_trace_pc ? *s_trace_pc : 0u;
|
|
r->addr = addr;
|
|
r->value = value;
|
|
r->op = (uint8_t)op;
|
|
r->reg = (uint8_t)reg;
|
|
r->src_live = (src && src->gen == s_gen) ? 1u : 0u;
|
|
r->pad = 0;
|
|
r->src_flags = src ? src->flags : 0u;
|
|
r->dst_flags = (dst && dst->gen == s_gen) ? dst->flags : 0u;
|
|
r->dst_x16 = dst ? dst->x16 : 0;
|
|
r->dst_y16 = dst ? dst->y16 : 0;
|
|
#else
|
|
(void)op; (void)reg; (void)src; (void)addr; (void)value; (void)dst;
|
|
#endif
|
|
}
|
|
|
|
extern "C" uint64_t pgxp_store_ring(const PGXPStoreRecord **ring, uint32_t *cap) {
|
|
*ring = s_store_ring;
|
|
*cap = s_store_ring ? PGXP_STORE_RING_CAP : 0u;
|
|
return s_store_ring ? s_store_seq : 0u;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Address mapping + validation */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/* Guest address -> shadow slot, or NULL for BIOS/MMIO/KSEG2 (untrackable).
|
|
* Unallocated mod-region addresses map too: nothing stores there, and every
|
|
* consumer validates the shadow against the actual guest word. */
|
|
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 - PSX_MOD_GPU_DMA_APERTURE_BASE < PSX_MOD_GPU_DMA_APERTURE_SIZE)
|
|
return s_ram ? &s_ram[PGXP_RAM_WORDS +
|
|
((m - PSX_MOD_GPU_DMA_APERTURE_BASE) >> 2)] : nullptr;
|
|
if (m - PSX_MOD_MEMORY_BASE < PSX_MOD_MEMORY_SIZE)
|
|
return s_ram ? &s_ram[PGXP_RAM_WORDS + PGXP_APERTURE_WORDS +
|
|
((m - PSX_MOD_MEMORY_BASE) >> 2)] : nullptr;
|
|
if ((m & 0xFFFFFC00u) == 0x1F800000u) /* scratchpad */
|
|
return &s_scratch[(m & 0x3FCu) >> 2];
|
|
return nullptr;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Checkpoint (render passes) */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/* A render pass (render_pass.c) runs guest draw code on a sandboxed machine
|
|
* and puts RAM, scratchpad and registers back with raw copies afterwards. The
|
|
* shadows must roll back with them: otherwise they describe the words the
|
|
* pass wrote (interpolated vertices), and the live frame's packets - consumed
|
|
* after the passes - fail validation. Copying the shadow arrays whole per
|
|
* pass would cost tens of MB; instead every RAM / scratchpad slot a pass
|
|
* mutates is journaled on its first write, and register shadows (small) are
|
|
* copied whole. A journal that cannot grow, or a generation wrap during the
|
|
* pass, fails closed: rollback invalidates everything. */
|
|
struct PGXPJournalEntry {
|
|
PGXPValue *slot;
|
|
size_t index;
|
|
PGXPValue old;
|
|
};
|
|
|
|
static PSX_THREAD_LOCAL uint32_t s_ck_depth = 0;
|
|
static PSX_THREAD_LOCAL uint8_t *s_ck_bits = nullptr; /* one bit per shadow slot */
|
|
static PSX_THREAD_LOCAL PGXPValue *s_ck_ram = nullptr; /* s_ram the bits index */
|
|
static PSX_THREAD_LOCAL PGXPJournalEntry *s_ck_log = nullptr;
|
|
static PSX_THREAD_LOCAL size_t s_ck_n = 0, s_ck_cap = 0;
|
|
static PSX_THREAD_LOCAL int s_ck_lossy = 0;
|
|
static PSX_THREAD_LOCAL PGXPValue s_ck_gpr[34], s_ck_gte[32], s_ck_gtc[32];
|
|
static PSX_THREAD_LOCAL uint32_t s_ck_model_origin[6];
|
|
static PSX_THREAD_LOCAL PGXPScalar s_ck_gpr_s[34], s_ck_gte_s[32], s_ck_gtc_s[32];
|
|
static PSX_THREAD_LOCAL uint32_t s_ck_gen = 0, s_ck_suppress = 0;
|
|
static PSX_THREAD_LOCAL int s_ck_deferred = 0;
|
|
|
|
static inline size_t ck_index(const PGXPValue *pv) {
|
|
if (pv >= s_scratch && pv < s_scratch + PGXP_SCRATCH_WORDS)
|
|
return (size_t)PGXP_SHADOW_WORDS + (size_t)(pv - s_scratch);
|
|
return (size_t)(pv - s_ram);
|
|
}
|
|
|
|
/* Journal a RAM / scratchpad slot before its first mutation in a pass. */
|
|
static inline void ck_note(PGXPValue *pv, uint32_t addr) {
|
|
if (s_ck_depth == 0 || !pv) return;
|
|
if (s_ck_lossy || !s_ck_bits) { s_ck_lossy = 1; return; }
|
|
if (s_ram != s_ck_ram) {
|
|
s_ck_lossy = 1; /* shadow RAM appeared mid-pass */
|
|
return;
|
|
}
|
|
size_t i = ck_index(pv);
|
|
uint8_t bit = (uint8_t)(1u << (i & 7u));
|
|
if (s_ck_bits[i >> 3] & bit) return;
|
|
if (s_ck_n == s_ck_cap) {
|
|
size_t cap = s_ck_cap ? s_ck_cap * 2 : 65536;
|
|
PGXPJournalEntry *p = (PGXPJournalEntry *)std::realloc(
|
|
s_ck_log, cap * sizeof *p);
|
|
if (!p) { s_ck_lossy = 1; return; }
|
|
s_ck_log = p;
|
|
s_ck_cap = cap;
|
|
}
|
|
s_ck_bits[i >> 3] |= bit;
|
|
s_ck_log[s_ck_n].slot = pv;
|
|
s_ck_log[s_ck_n].index = i;
|
|
s_ck_log[s_ck_n].old = *pv;
|
|
s_ck_n++;
|
|
}
|
|
|
|
static void ck_wrapped(void) {
|
|
if (s_ck_depth != 0) s_ck_lossy = 1; /* shadows were cleared */
|
|
}
|
|
|
|
static int ck_in_pass(void) { return s_ck_depth != 0; }
|
|
|
|
static inline PGXPValue *pgxp_ptr_w(uint32_t addr) {
|
|
PGXPValue *pv = pgxp_ptr(addr);
|
|
ck_note(pv, addr);
|
|
return execution_word(pv);
|
|
}
|
|
|
|
static int packet_shadow_address(uint32_t addr, uint32_t *canonical) {
|
|
if ((addr & 3u) || addr >= 0xC0000000u) return 0;
|
|
const uint32_t physical = addr & 0x1FFFFFFFu;
|
|
if (physical < PSX_MAIN_RAM_WINDOW_BYTES) {
|
|
*canonical = psx_ram_canonical_offset(physical);
|
|
return 1;
|
|
}
|
|
if ((physical & 0xFFFFFC00u) == 0x1F800000u) {
|
|
*canonical = physical;
|
|
return 1;
|
|
}
|
|
if (psx_mod_gpu_dma_memory_contains(addr, 4u)) {
|
|
*canonical = physical;
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
extern "C" int pgxp_capture_word_shadow(uint32_t addr, uint32_t expected,
|
|
PGXPWordShadow *out) {
|
|
uint32_t canonical;
|
|
if (!out || !packet_shadow_address(addr, &canonical)) return 0;
|
|
PGXPWordShadow receipt{};
|
|
receipt.address = canonical;
|
|
receipt.value = expected;
|
|
receipt.source_generation = s_gen;
|
|
const PGXPValue *source = pgxp_ptr(addr);
|
|
if (s_enabled && source && source->gen == s_gen && source->value == expected) {
|
|
receipt.valid = 1;
|
|
receipt.x16 = source->x16;
|
|
receipt.y16 = source->y16;
|
|
receipt.z = source->z;
|
|
receipt.flags = source->flags;
|
|
receipt.projection = source->projection;
|
|
}
|
|
*out = receipt;
|
|
return 1;
|
|
}
|
|
|
|
static int restore_word_shadow(uint32_t addr, uint32_t expected,
|
|
const PGXPWordShadow *in, bool relocated) {
|
|
uint32_t canonical, source;
|
|
if (!in || !s_ck_depth || s_ck_lossy || !packet_shadow_address(addr, &canonical) ||
|
|
!packet_shadow_address(in->address, &source) || source != in->address ||
|
|
(!relocated && in->address != canonical) ||
|
|
in->value != expected || in->valid > 1u ||
|
|
in->source_generation != s_ck_gen) return 0;
|
|
if (in->valid && !s_enabled) return 0;
|
|
PGXPValue *destination = pgxp_ptr_w(addr);
|
|
// With PGXP disabled/unallocated, there is already no RAM shadow to clear.
|
|
if (!destination) return in->valid == 0u;
|
|
if (s_ck_lossy) return 0;
|
|
PGXPValue restored{};
|
|
if (in->valid) {
|
|
restored.value = in->value;
|
|
restored.gen = s_gen;
|
|
restored.x16 = in->x16;
|
|
restored.y16 = in->y16;
|
|
restored.z = in->z;
|
|
restored.flags = in->flags;
|
|
restored.projection = in->projection;
|
|
}
|
|
*destination = restored;
|
|
return 1;
|
|
}
|
|
|
|
extern "C" int pgxp_restore_word_shadow(uint32_t addr, uint32_t expected,
|
|
const PGXPWordShadow *in) {
|
|
return restore_word_shadow(addr, expected, in, false);
|
|
}
|
|
|
|
extern "C" int pgxp_restore_relocated_word_shadow(uint32_t destination,
|
|
uint32_t expected,
|
|
const PGXPWordShadow *in) {
|
|
return restore_word_shadow(destination, expected, in, true);
|
|
}
|
|
|
|
extern "C" void pgxp_checkpoint_begin(void) {
|
|
if (s_ck_depth++ != 0) return; /* the outermost pass journals */
|
|
if (!s_ck_bits) {
|
|
s_ck_bits = (uint8_t *)std::calloc(
|
|
((size_t)PGXP_SHADOW_WORDS + PGXP_SCRATCH_WORDS + 7u) / 8u, 1);
|
|
if (!s_ck_bits) s_ck_lossy = 1;
|
|
}
|
|
s_ck_ram = s_ram;
|
|
s_ck_n = 0;
|
|
std::memcpy(s_ck_gpr, s_gpr, sizeof s_gpr);
|
|
std::memcpy(s_ck_gte, s_gte, sizeof s_gte);
|
|
std::memcpy(s_ck_model_origin, s_model_origin, sizeof s_model_origin);
|
|
std::memcpy(s_ck_gtc, s_gtc, sizeof s_gtc);
|
|
std::memcpy(s_ck_gpr_s, s_gpr_s, sizeof s_gpr_s);
|
|
std::memcpy(s_ck_gte_s, s_gte_s, sizeof s_gte_s);
|
|
std::memcpy(s_ck_gtc_s, s_gtc_s, sizeof s_gtc_s);
|
|
s_ck_gen = s_gen;
|
|
s_ck_suppress = s_suppress;
|
|
s_ck_deferred = s_deferred_invalidate;
|
|
}
|
|
|
|
extern "C" void pgxp_checkpoint_rollback(void) {
|
|
if (s_ck_depth == 0) return;
|
|
if (--s_ck_depth != 0) return;
|
|
for (size_t i = s_ck_n; i-- > 0;) {
|
|
PGXPJournalEntry *e = &s_ck_log[i];
|
|
*e->slot = e->old;
|
|
size_t k = e->index;
|
|
s_ck_bits[k >> 3] &= (uint8_t)~(1u << (k & 7u));
|
|
}
|
|
s_ck_n = 0;
|
|
std::memcpy(s_gpr, s_ck_gpr, sizeof s_gpr);
|
|
std::memcpy(s_gte, s_ck_gte, sizeof s_gte);
|
|
std::memcpy(s_model_origin, s_ck_model_origin, sizeof s_model_origin);
|
|
std::memcpy(s_gtc, s_ck_gtc, sizeof s_gtc);
|
|
std::memcpy(s_gpr_s, s_ck_gpr_s, sizeof s_gpr_s);
|
|
std::memcpy(s_gte_s, s_ck_gte_s, sizeof s_gte_s);
|
|
std::memcpy(s_gtc_s, s_ck_gtc_s, sizeof s_gtc_s);
|
|
s_gen = s_ck_gen;
|
|
/* A watchdog abort can leave a suppress bracket open: the machine it
|
|
* interrupted is gone, so its bracket is too. */
|
|
s_suppress = s_ck_suppress;
|
|
s_deferred_invalidate = s_ck_deferred;
|
|
recompute_active();
|
|
if (s_ck_lossy || s_ram != s_ck_ram) {
|
|
s_ck_lossy = 0;
|
|
pgxp_invalidate_all();
|
|
}
|
|
}
|
|
|
|
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 (diff) pv->flags &= (uint16_t)~(PGXP_F_PROJECTION | PGXP_F_UNCLAMPED);
|
|
if ((pv->flags & PGXP_F_VX) && (diff & 0x0000FFFFu))
|
|
pv->flags &= (uint16_t)~(PGXP_F_VX | PGXP_F_DX | PGXP_F_VZ);
|
|
if ((pv->flags & PGXP_F_VY) && (diff & 0xFFFF0000u))
|
|
pv->flags &= (uint16_t)~(PGXP_F_VY | PGXP_F_DY | 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; }
|
|
|
|
extern "C" void pgxp_invalidate_word(uint32_t addr) {
|
|
if (!g_pgxp_active) return;
|
|
PGXPValue *pv = pgxp_ptr_w(addr); /* a render pass rolls it back */
|
|
if (pv) pv_kill(pv);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* 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); }
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Scalar tier helpers */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static inline void sc_kill(PGXPScalar *s) { s->gen = 0; }
|
|
static inline void sc_set(PGXPScalar *s, double v, uint32_t value) {
|
|
s->v = v; s->value = value; s->gen = s_gen;
|
|
}
|
|
static inline int sc_valid(const PGXPScalar *s, uint32_t value) {
|
|
return s->gen == s_gen && s->value == value;
|
|
}
|
|
|
|
/* A GPR write that produces no scalar ends whatever scalar the register had. */
|
|
static inline void gpr_written(uint32_t r) { if (r != 0) sc_kill(&s_gpr_s[r]); }
|
|
|
|
/* A register holding one 16-bit half, sign- or zero-extended (LH / LHU, or an
|
|
* MTC2 / CTC2 of such a register), whose half shadow is tracked: its precise
|
|
* value as a number. */
|
|
static inline int half_scalar(const PGXPValue *pv, uint32_t value, double *out) {
|
|
if (!pv || pv->gen != s_gen || pv->value != value) return 0;
|
|
if ((pv->flags & PGXP_F_VXY) != PGXP_F_VXY) return 0;
|
|
const int32_t sx = (int32_t)(int16_t)(value & 0xFFFFu);
|
|
const int32_t zx = (int32_t)(value & 0xFFFFu);
|
|
if ((int32_t)value != sx && (int32_t)value != zx) return 0;
|
|
if (pv->y16 != (((int32_t)value >> 16) << 16)) return 0; /* exact extension */
|
|
double v = pv->x16 / 65536.0; /* the half read as int16 */
|
|
if ((int32_t)value == zx && zx != sx) v += 65536.0; /* LHU of a negative half */
|
|
*out = v;
|
|
return 1;
|
|
}
|
|
|
|
/* The precise value of a register: its scalar, else its half shadow as a
|
|
* number, else the exact integer (*precise = 0). */
|
|
static inline double reg_scalar(const PGXPScalar *s, const PGXPValue *pv,
|
|
uint32_t value, int is_unsigned, int *precise) {
|
|
double v;
|
|
if (s && sc_valid(s, value)) { *precise = 1; return s->v; }
|
|
if (pv && half_scalar(pv, value, &v)) { *precise = 1; return v; }
|
|
*precise = 0;
|
|
return is_unsigned ? (double)value : (double)(int32_t)value;
|
|
}
|
|
|
|
static inline double gpr_scalar(uint32_t r, uint32_t value, int is_unsigned,
|
|
int *precise) {
|
|
if (r == 0) { *precise = 0; return 0.0; }
|
|
return reg_scalar(&s_gpr_s[r], &s_gpr[r], value, is_unsigned, precise);
|
|
}
|
|
|
|
/* A scalar that is about to become a 16-bit coordinate half: accept it when
|
|
* it describes `half` (the stored integer) within the derived window, and
|
|
* say whether the guest integer is its floor (plain) or a rounding of it
|
|
* (derived). Returns the 16.16 value, or 0 with *ok = 0. */
|
|
static inline int32_t scalar_to_half(double v, int32_t half, int *ok, int *derived) {
|
|
const double d = v - (double)half;
|
|
*ok = 0; *derived = 0;
|
|
if (!(d > -1.0 && d < 2.0)) return 0; /* also rejects NaN */
|
|
*ok = 1;
|
|
*derived = std::floor(v) != (double)half;
|
|
return (int32_t)std::floor(v * 65536.0);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Memory-mode hooks: loads / stores */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/* SXY2 and SXYP are one register seen at two addresses: whichever the hook
|
|
* just filled, the other describes the same word. */
|
|
static inline void gte_sxy2_mirror(uint32_t reg) {
|
|
if (reg == 14) s_gte[15] = s_gte[14];
|
|
else if (reg == 15) s_gte[14] = s_gte[15];
|
|
}
|
|
|
|
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];
|
|
gpr_written(rt); /* memory carries no scalars */
|
|
|
|
uint32_t lop = f_op(instr);
|
|
/* LWL at byte 3 and LWR at byte 0 of a word each load the WHOLE word
|
|
* (little-endian R3000A): the unaligned-copy idiom lwl/lwr on an aligned
|
|
* address is a plain word load, so it carries the word's shadow. */
|
|
if ((lop == 0x22 && (addr & 3u) == 3u) || (lop == 0x26 && (addr & 3u) == 0u))
|
|
lop = 0x23;
|
|
switch (lop) {
|
|
case 0x23: { /* LW */
|
|
PGXPValue *src = pgxp_ptr(addr); /* reads alone do not dirty a page */
|
|
if (src && src->gen == s_gen) {
|
|
if (src->value != value) { ck_note(src, addr); execution_word(src); }
|
|
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 && !(src->flags & PGXP_F_UNCLAMPED)) {
|
|
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;
|
|
if (src->flags & (hi_half ? PGXP_F_DY : PGXP_F_DX))
|
|
dst->flags |= PGXP_F_DX;
|
|
}
|
|
}
|
|
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_w(addr);
|
|
if (!dst) return;
|
|
uint32_t rt = f_rt(instr);
|
|
PGXPValue *src = (rt != 0) ? &s_gpr[rt] : nullptr;
|
|
|
|
uint32_t sop = f_op(instr);
|
|
/* SWL at byte 3 and SWR at byte 0 each store the WHOLE word (the
|
|
* unaligned-copy idiom on an aligned address): a plain word store. */
|
|
if ((sop == 0x2A && (addr & 3u) == 3u) || (sop == 0x2E && (addr & 3u) == 0u))
|
|
sop = 0x2B;
|
|
switch (sop) {
|
|
case 0x2B: { /* SW */
|
|
PGXPValue before;
|
|
std::memset(&before, 0, sizeof before);
|
|
if (src) before = *src;
|
|
if (src && src->gen == s_gen) {
|
|
pv_validate(src, value);
|
|
*dst = *src;
|
|
} else {
|
|
pv_reset(dst, value);
|
|
}
|
|
/* A register holding one coordinate as a full word (a scalar that
|
|
* fits a half, e.g. a CPU-divided position) stores as that half,
|
|
* with an exact sign extension above it. */
|
|
if (s_cpu_mode && rt != 0 && sc_valid(&s_gpr_s[rt], value) &&
|
|
(int32_t)value == (int32_t)(int16_t)(value & 0xFFFFu)) {
|
|
int ok, derived;
|
|
int32_t v16 = scalar_to_half(s_gpr_s[rt].v, (int16_t)(value & 0xFFFFu),
|
|
&ok, &derived);
|
|
if (ok) {
|
|
pv_reset(dst, value);
|
|
dst->x16 = v16;
|
|
dst->y16 = ((int32_t)value >> 16) << 16;
|
|
dst->flags = (uint16_t)(PGXP_F_VXY | (derived ? PGXP_F_DX : 0));
|
|
}
|
|
}
|
|
note_store(0x2B, rt, src ? &before : nullptr, addr, value, dst);
|
|
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_DY) : (PGXP_F_VX | PGXP_F_DX)) |
|
|
PGXP_F_VZ | PGXP_F_PROJECTION | PGXP_F_UNCLAMPED);
|
|
dst->z = 0;
|
|
int ok = 0, derived = 0;
|
|
int32_t v16 = 0;
|
|
/* Hooks pass either the full register or just the stored half (the
|
|
* interpreters), so the scalar is matched on the stored half. */
|
|
if (s_cpu_mode && rt != 0 && s_gpr_s[rt].gen == s_gen &&
|
|
(s_gpr_s[rt].value & 0xFFFFu) == half)
|
|
v16 = scalar_to_half(s_gpr_s[rt].v, (int16_t)half, &ok, &derived);
|
|
if (ok) {
|
|
/* A scalar (e.g. a midpoint computed by MVMVA + DIV) becomes a
|
|
* coordinate half here. */
|
|
if (hi_half) { dst->y16 = v16; dst->flags |= (uint16_t)(PGXP_F_VY | (derived ? PGXP_F_DY : 0)); }
|
|
else { dst->x16 = v16; dst->flags |= (uint16_t)(PGXP_F_VX | (derived ? PGXP_F_DX : 0)); }
|
|
} else if (src && src->gen == s_gen && !(src->flags & PGXP_F_UNCLAMPED) &&
|
|
(src->flags & PGXP_F_VX) &&
|
|
((src->value ^ value) & 0xFFFFu) == 0) {
|
|
const uint16_t d = (src->flags & PGXP_F_DX) ? (hi_half ? PGXP_F_DY : PGXP_F_DX) : 0;
|
|
if (hi_half) { dst->y16 = src->x16; dst->flags |= (uint16_t)(PGXP_F_VY | d); }
|
|
else { dst->x16 = src->x16; dst->flags |= (uint16_t)(PGXP_F_VX | d); }
|
|
}
|
|
note_store(0x29, rt, src, addr, half, dst);
|
|
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_DY) : (PGXP_F_VX | PGXP_F_DX)) |
|
|
PGXP_F_VZ | PGXP_F_PROJECTION | PGXP_F_UNCLAMPED);
|
|
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); /* reads alone do not dirty a page */
|
|
PGXPValue *dst = &s_gte[f_rt(instr)];
|
|
if (src && src->gen == s_gen) {
|
|
if (src->value != value) { ck_note(src, addr); execution_word(src); }
|
|
pv_validate(src, value);
|
|
*dst = *src;
|
|
} else {
|
|
pv_reset(dst, value);
|
|
}
|
|
if (s_projection_tracking && f_rt(instr) < 6u) s_model_origin[f_rt(instr)] = addr & 0x1FFFFFFFu;
|
|
sc_kill(&s_gte_s[f_rt(instr)]); /* memory carries no scalars */
|
|
gte_sxy2_mirror(f_rt(instr));
|
|
return;
|
|
}
|
|
case 0x3A: { /* SWC2: [addr] <- gte[rt] */
|
|
PGXPValue *dst = pgxp_ptr_w(addr);
|
|
if (!dst) return;
|
|
PGXPValue *src = &s_gte[f_rt(instr)];
|
|
const PGXPValue before = *src;
|
|
if (src->gen == s_gen) {
|
|
pv_validate(src, value);
|
|
*dst = *src;
|
|
} else {
|
|
pv_reset(dst, value);
|
|
}
|
|
note_store(0x3A, f_rt(instr), &before, addr, value, dst);
|
|
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);
|
|
}
|
|
/* MFC2 of MAC / IR carries an MVMVA result's scalar. */
|
|
if (s_cpu_mode && sc_valid(&s_gte_s[f_rd(instr)], value))
|
|
s_gpr_s[rt] = s_gte_s[f_rd(instr)];
|
|
else
|
|
gpr_written(rt);
|
|
return;
|
|
}
|
|
case 0x04: { /* MTC2: gte[rd] <- gpr[rt] */
|
|
uint32_t rt = f_rt(instr);
|
|
uint32_t rd = f_rd(instr);
|
|
if (rd < 6u) s_model_origin[rd] = 0;
|
|
PGXPValue *dst = &s_gte[rd];
|
|
if (rt != 0 && s_gpr[rt].gen == s_gen) {
|
|
pv_validate(&s_gpr[rt], value);
|
|
*dst = s_gpr[rt];
|
|
} else {
|
|
pv_reset(dst, value);
|
|
}
|
|
/* IR0..IR3 hold the low half sign-extended: a scalar that fits
|
|
* a half moves in as the register's precise value. */
|
|
sc_kill(&s_gte_s[rd]);
|
|
if (s_cpu_mode && rd >= 8 && rd <= 11 && rt != 0 &&
|
|
sc_valid(&s_gpr_s[rt], value) &&
|
|
(int32_t)value == (int32_t)(int16_t)(value & 0xFFFFu))
|
|
sc_set(&s_gte_s[rd], s_gpr_s[rt].v, value);
|
|
/* An SXYP write (rd==15) already shifted the FIFO shadows
|
|
* (pgxp_gte_reg_written, from the GTE register write). */
|
|
gte_sxy2_mirror(rd);
|
|
return;
|
|
}
|
|
case 0x02: { /* CFC2: gpr[rt] <- gtc[rd] */
|
|
uint32_t rt = f_rt(instr);
|
|
if (rt == 0) return;
|
|
uint32_t rd = f_rd(instr);
|
|
if (s_cpu_mode && s_gtc[rd].gen == s_gen) {
|
|
pv_validate(&s_gtc[rd], value);
|
|
s_gpr[rt] = s_gtc[rd];
|
|
} else {
|
|
pv_reset(&s_gpr[rt], value);
|
|
}
|
|
if (s_cpu_mode && sc_valid(&s_gtc_s[rd], value))
|
|
s_gpr_s[rt] = s_gtc_s[rd];
|
|
else
|
|
gpr_written(rt);
|
|
return;
|
|
}
|
|
case 0x06: { /* CTC2: gtc[rd] <- gpr[rt] */
|
|
/* Control registers are written only by CTC2, so their shadows
|
|
* stay exact until the next CTC2: matrix rows packed from two
|
|
* tracked halves (the GTE-as-multiplier idiom) and translation
|
|
* words carrying a scalar. */
|
|
uint32_t rt = f_rt(instr);
|
|
uint32_t rd = f_rd(instr);
|
|
if (s_cpu_mode && rt != 0 && s_gpr[rt].gen == s_gen) {
|
|
pv_validate(&s_gpr[rt], value);
|
|
s_gtc[rd] = s_gpr[rt];
|
|
} else {
|
|
pv_reset(&s_gtc[rd], value);
|
|
}
|
|
if (s_cpu_mode && rt != 0 && sc_valid(&s_gpr_s[rt], value))
|
|
s_gtc_s[rd] = s_gpr_s[rt];
|
|
else
|
|
sc_kill(&s_gtc_s[rd]);
|
|
return;
|
|
}
|
|
default:
|
|
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 && !(pv->flags & PGXP_F_UNCLAMPED)) {
|
|
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;
|
|
}
|
|
|
|
/* The derived-half marks (PGXP_F_DX / DY) an operand contributes through
|
|
* comp16: a tracked derived half keeps its rounding window in the sum. */
|
|
static inline uint16_t derived_bits(const PGXPValue *pv, uint32_t value) {
|
|
if (!pv || pv->gen != s_gen || (pv->flags & PGXP_F_UNCLAMPED)) return 0;
|
|
uint16_t d = 0;
|
|
if ((pv->flags & PGXP_F_DX) && (pv->flags & PGXP_F_VX) &&
|
|
((pv->value ^ value) & 0x0000FFFFu) == 0) d |= PGXP_F_DX;
|
|
if ((pv->flags & PGXP_F_DY) && (pv->flags & PGXP_F_VY) &&
|
|
((pv->value ^ value) & 0xFFFF0000u) == 0) d |= PGXP_F_DY;
|
|
return d;
|
|
}
|
|
|
|
static inline int comp_tracked(const PGXPValue *pv, uint32_t value, int hi_half) {
|
|
if (!pv || pv->gen != s_gen || (pv->flags & PGXP_F_UNCLAMPED)) 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 */
|
|
}
|
|
|
|
/* Bitwise ops (AND/OR/XOR/NOR and ANDI/ORI/XORI) move bits; they do not
|
|
* compute coordinates. GP0 decodes only the 11-bit field of each vertex half
|
|
* (bits 0..10 of X, 16..26 of Y), and engines pack clip / outcode bits into
|
|
* the rest of the word after projecting (Spider-Man ORs off-screen flags into
|
|
* bits 14/15 of each half of SXY2 before storing it). A result half whose GPU
|
|
* field equals the field of a tracked operand half therefore still describes
|
|
* that vertex. Carrying it is exact - no arithmetic, like a move - so it runs
|
|
* in both tiers. The carried value keeps the shadow invariant (integer part
|
|
* == the half read as int16): the fraction rides along, the integer becomes
|
|
* the result half. The GPU consumer rebases onto the field it decodes. */
|
|
static inline int half_exact(const PGXPValue *pv, uint32_t value, int hi_half) {
|
|
if (!comp_tracked(pv, value, hi_half)) return 0;
|
|
int32_t v16 = hi_half ? pv->y16 : pv->x16;
|
|
const int32_t half = (int32_t)(int16_t)(hi_half ? (value >> 16) : value);
|
|
/* A derived half keeps its rounding window around the integer. */
|
|
if (pv->flags & (hi_half ? PGXP_F_DY : PGXP_F_DX)) {
|
|
const int64_t d = (int64_t)v16 - (int64_t)half * 65536;
|
|
return d > -65536 && d < 2 * 65536;
|
|
}
|
|
/* A saturated projection shadow sits beyond the clamped word; it is not
|
|
* the value of that half and must not be carried as one. */
|
|
if (s_preserve_projection) {
|
|
const int32_t field = (int32_t)((uint32_t)half << 21) >> 21;
|
|
const int64_t d = (int64_t)v16 - (int64_t)half * 65536;
|
|
// Clip metadata uses bits 13..15. Bits 11/12 must still be the
|
|
// coordinate's sign fill; a real wrapped coordinate stays strict.
|
|
const uint16_t sign_fill = field < 0 ? 0x1800u : 0;
|
|
if (field > -0x400 && field < 0x3FF &&
|
|
(uint16_t(half) & 0x1800u) == sign_fill)
|
|
return d > -(int64_t)PGXP_PPP_AGREE_BELOW * 65536 &&
|
|
d < (int64_t)PGXP_PPP_AGREE_ABOVE * 65536;
|
|
}
|
|
return (v16 >> 16) == half;
|
|
}
|
|
|
|
static inline int carry_field(PGXPValue *dst, const PGXPValue *src,
|
|
uint32_t sval, uint32_t result, int hi_half) {
|
|
if (!half_exact(src, sval, hi_half)) return 0;
|
|
uint32_t sh = hi_half ? 16u : 0u;
|
|
if ((((sval ^ result) >> sh) & 0x7FFu) != 0) return 0;
|
|
/* Keep the precise offset from the operand's integer and rebase it on
|
|
* the result half's integer (equal field, flag bits may differ). */
|
|
const int32_t from = (int32_t)(int16_t)(sval >> sh);
|
|
const int32_t to = (int32_t)(int16_t)(result >> sh);
|
|
const int32_t v = (int32_t)((int64_t)(hi_half ? src->y16 : src->x16) +
|
|
((int64_t)to - from) * 65536);
|
|
const uint16_t d = src->flags & (hi_half ? PGXP_F_DY : PGXP_F_DX);
|
|
if (hi_half) { dst->y16 = v; dst->flags |= (uint16_t)(PGXP_F_VY | d); }
|
|
else { dst->x16 = v; dst->flags |= (uint16_t)(PGXP_F_VX | d); }
|
|
return 1;
|
|
}
|
|
|
|
static inline int has_depth(const PGXPValue *pv) {
|
|
return pv && pv->gen == s_gen && (pv->flags & PGXP_F_VZ) && pv->z != 0;
|
|
}
|
|
|
|
/* Depth belongs to a whole vertex: it survives an op only when both halves of
|
|
* the result came from one operand that carried it, and the other operand is
|
|
* not itself a vertex (a constant, a mask, an offset). */
|
|
static inline void keep_depth(PGXPValue *dst, const PGXPValue *p,
|
|
const PGXPValue *other) {
|
|
if ((dst->flags & PGXP_F_VXY) == PGXP_F_VXY && has_depth(p) &&
|
|
!has_depth(other)) {
|
|
dst->z = p->z;
|
|
dst->flags |= PGXP_F_VZ;
|
|
}
|
|
}
|
|
|
|
static void alu_bitwise(PGXPValue *dst, const PGXPValue *a, uint32_t s1,
|
|
const PGXPValue *b, uint32_t s2, uint32_t result) {
|
|
pv_reset(dst, result);
|
|
/* Prefer the operand that is a whole vertex, so a vertex ORed with flags
|
|
* keeps its depth even when the flag word happens to share a field. */
|
|
const PGXPValue *p = a; uint32_t pval = s1;
|
|
const PGXPValue *q = b; uint32_t qval = s2;
|
|
if (!has_depth(a) && has_depth(b)) { p = b; pval = s2; q = a; qval = s1; }
|
|
const PGXPValue *from[2] = { nullptr, nullptr };
|
|
for (int hi = 0; hi < 2; hi++) {
|
|
if (carry_field(dst, p, pval, result, hi)) from[hi] = p;
|
|
else if (carry_field(dst, q, qval, result, hi)) from[hi] = q;
|
|
else if (((hi ? result >> 16 : result) & 0xFFFFu) == 0) {
|
|
/* An exactly-zero half is an exact coordinate (the repack
|
|
* pattern ORs a shifted half into a zero half). */
|
|
if (hi) { dst->y16 = 0; dst->flags |= PGXP_F_VY; }
|
|
else { dst->x16 = 0; dst->flags |= PGXP_F_VX; }
|
|
}
|
|
}
|
|
if (from[0] && from[0] == from[1])
|
|
keep_depth(dst, from[0], from[0] == p ? q : p);
|
|
}
|
|
|
|
static const PGXPValue kNoShadow = { 0, 0, 0, 0, 0, 0 };
|
|
|
|
/* Snapshot a source GPR shadow: the destination may alias a source (in-place
|
|
* ops such as `or t2, t2, t9` or `addiu t0, t0, 4`), and it is reset before
|
|
* the sources are read. */
|
|
static inline const PGXPValue *gpr_shadow(uint32_t r, PGXPValue *copy) {
|
|
*copy = (r != 0) ? s_gpr[r] : kNoShadow;
|
|
return copy;
|
|
}
|
|
|
|
/* Scalar tier for one ALU op (cpu-mode): the destination register and the
|
|
* precise value of the result, when an operand carries a precise value and the
|
|
* op is exact arithmetic on it (no 32-bit wrap). Operands follow the hook
|
|
* convention: ADD-type s1 = rs, s2 = rt; immediate s1 = rs, s2 = imm; shifts
|
|
* s1 = rt, s2 = shamt (or rs for the variable forms); MF/MT HI/LO s1 = the
|
|
* moved value. Computed before the half tier resets the destination, which
|
|
* may alias a source. */
|
|
static int alu_scalar(uint32_t instr, uint32_t result, uint32_t s1, uint32_t s2,
|
|
int *dst_reg, double *out) {
|
|
const uint32_t op = f_op(instr);
|
|
int pa = 0, pb = 0;
|
|
double a, b;
|
|
*dst_reg = -1;
|
|
if (op == 0) {
|
|
const uint32_t funct = f_funct(instr);
|
|
const uint32_t rs = f_rs(instr), rt = f_rt(instr), rd = f_rd(instr);
|
|
switch (funct) {
|
|
case 0x10: case 0x12: { /* MFHI / MFLO */
|
|
const PGXPScalar *hl = &s_gpr_s[funct == 0x10 ? PGXP_REG_HI : PGXP_REG_LO];
|
|
*dst_reg = (int)rd;
|
|
if (!sc_valid(hl, result)) return 0;
|
|
*out = hl->v;
|
|
return 1;
|
|
}
|
|
case 0x11: case 0x13: /* MTHI / MTLO */
|
|
*dst_reg = funct == 0x11 ? PGXP_REG_HI : PGXP_REG_LO;
|
|
a = gpr_scalar(rs, result, 0, &pa);
|
|
if (!pa) return 0;
|
|
*out = a;
|
|
return 1;
|
|
case 0x00: case 0x02: case 0x03: /* SLL / SRL / SRA */
|
|
case 0x04: case 0x06: case 0x07: { /* SLLV / SRLV / SRAV */
|
|
*dst_reg = (int)rd;
|
|
const uint32_t sh = s2 & 31u; /* shamt, or rs for the V forms */
|
|
a = gpr_scalar(rt, s1, 0, &pa);
|
|
if (!pa) return 0;
|
|
const int left = (funct & 3u) == 0;
|
|
const int logical = (funct & 3u) == 2;
|
|
if (left) {
|
|
const int64_t exact = (int64_t)(int32_t)s1 * ((int64_t)1 << sh);
|
|
if (exact != (int64_t)(int32_t)result) return 0; /* wrapped */
|
|
*out = std::ldexp(a, (int)sh);
|
|
return 1;
|
|
}
|
|
if (logical && (int32_t)s1 < 0) return 0; /* not a division */
|
|
*out = std::ldexp(a, -(int)sh); /* floor(a/2^sh) is the guest */
|
|
return 1;
|
|
}
|
|
case 0x20: case 0x21: case 0x22: case 0x23: { /* ADD(U) / SUB(U) */
|
|
*dst_reg = (int)rd;
|
|
const int sub = funct >= 0x22;
|
|
a = gpr_scalar(rs, s1, 0, &pa);
|
|
b = gpr_scalar(rt, s2, 0, &pb);
|
|
if (!pa && !pb) return 0;
|
|
const int64_t exact = sub ? (int64_t)(int32_t)s1 - (int64_t)(int32_t)s2
|
|
: (int64_t)(int32_t)s1 + (int64_t)(int32_t)s2;
|
|
if (exact != (int64_t)(int32_t)result) return 0;
|
|
*out = sub ? a - b : a + b;
|
|
return 1;
|
|
}
|
|
case 0x25: /* OR: only the MOVE idiom */
|
|
*dst_reg = (int)rd;
|
|
if (rs != 0 && rt != 0) return 0;
|
|
a = gpr_scalar(rs != 0 ? rs : rt, result, 0, &pa);
|
|
if (!pa) return 0;
|
|
*out = a;
|
|
return 1;
|
|
default:
|
|
*dst_reg = (int)rd;
|
|
return 0;
|
|
}
|
|
}
|
|
*dst_reg = (int)f_rt(instr);
|
|
if (op == 0x08 || op == 0x09) { /* ADDI / ADDIU */
|
|
a = gpr_scalar(f_rs(instr), s1, 0, &pa);
|
|
if (!pa) return 0;
|
|
const int64_t exact = (int64_t)(int32_t)s1 + (int64_t)f_simm(instr);
|
|
if (exact != (int64_t)(int32_t)result) return 0;
|
|
*out = a + (double)f_simm(instr);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static void alu_halves(uint32_t instr, uint32_t result, uint32_t s1, uint32_t s2);
|
|
|
|
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;
|
|
int dst = -1;
|
|
double v = 0.0;
|
|
const int has = s_cpu_mode && alu_scalar(instr, result, s1, s2, &dst, &v);
|
|
alu_halves(instr, result, s1, s2);
|
|
if (dst > 0 && dst < 34) {
|
|
if (has) sc_set(&s_gpr_s[dst], v, result);
|
|
else sc_kill(&s_gpr_s[dst]);
|
|
}
|
|
}
|
|
|
|
static void alu_halves(uint32_t instr, uint32_t result, uint32_t s1, uint32_t s2) {
|
|
uint32_t op = f_op(instr);
|
|
PGXPValue ca, cb;
|
|
|
|
if (op == 0) { /* SPECIAL */
|
|
uint32_t 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 */
|
|
case 0x12: { /* MFLO */
|
|
PGXPValue *hl = &s_gpr[funct == 0x10 ? PGXP_REG_HI : PGXP_REG_LO];
|
|
if (s_cpu_mode && hl->gen == s_gen) {
|
|
pv_validate(hl, result);
|
|
*dst = *hl;
|
|
} 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);
|
|
const PGXPValue *src = gpr_shadow(rt, &ca);
|
|
int right = (funct & 3u) != 0;
|
|
pv_reset(dst, result);
|
|
if (!s_cpu_mode || sh != 16 || src->gen != s_gen)
|
|
return;
|
|
if (!right) { /* << 16: low comp -> Y */
|
|
if (comp_tracked(src, s1, 0)) {
|
|
dst->y16 = src->x16;
|
|
dst->flags |= PGXP_F_VY;
|
|
if (src->flags & PGXP_F_DX) dst->flags |= PGXP_F_DY;
|
|
}
|
|
dst->x16 = 0;
|
|
dst->flags |= PGXP_F_VX; /* low half exactly zero */
|
|
} else { /* >> 16: high comp -> X */
|
|
if (comp_tracked(src, s1, 1)) {
|
|
dst->x16 = src->y16;
|
|
dst->flags |= PGXP_F_VX;
|
|
if (src->flags & PGXP_F_DY) dst->flags |= PGXP_F_DX;
|
|
}
|
|
dst->y16 = ((int32_t)result >> 16) << 16; /* 0 or sign fill */
|
|
dst->flags |= PGXP_F_VY;
|
|
}
|
|
return;
|
|
}
|
|
case 0x24: case 0x25: case 0x26: case 0x27: { /* AND/OR/XOR/NOR */
|
|
const PGXPValue *a = gpr_shadow(rs, &ca);
|
|
const PGXPValue *b = gpr_shadow(rt, &cb);
|
|
/* OR with $zero is the MOVE idiom: the whole shadow, depth too. */
|
|
if (funct == 0x25 && (rs == 0 || rt == 0)) {
|
|
const PGXPValue *m = (rt == 0) ? a : b;
|
|
if (m->gen == s_gen) {
|
|
*dst = *m;
|
|
pv_validate(dst, result);
|
|
} else pv_reset(dst, result);
|
|
return;
|
|
}
|
|
alu_bitwise(dst, a, s1, b, s2, result);
|
|
return;
|
|
}
|
|
case 0x20: case 0x21: /* ADD / ADDU */
|
|
case 0x22: case 0x23: { /* SUB / SUBU */
|
|
const PGXPValue *a = gpr_shadow(rs, &ca);
|
|
const PGXPValue *b = gpr_shadow(rt, &cb);
|
|
int is_sub = (funct == 0x22 || funct == 0x23);
|
|
/* MOVE idioms first - exact, so they run in both tiers. */
|
|
if (rt == 0 || (!is_sub && rs == 0)) {
|
|
const PGXPValue *m = (rt == 0) ? a : b;
|
|
if (m->gen == s_gen) {
|
|
*dst = *m;
|
|
pv_validate(dst, result);
|
|
} else pv_reset(dst, result);
|
|
return;
|
|
}
|
|
if (!s_cpu_mode) { pv_reset(dst, result); 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 = (uint16_t)(PGXP_F_VXY | derived_bits(a, s1) | derived_bits(b, s2));
|
|
/* A vertex plus / minus an offset is still that vertex. */
|
|
if (comp_tracked(a, s1, 0) && comp_tracked(a, s1, 1))
|
|
keep_depth(dst, a, b);
|
|
else if (!is_sub && comp_tracked(b, s2, 0) && comp_tracked(b, s2, 1))
|
|
keep_depth(dst, b, a);
|
|
return;
|
|
}
|
|
default: /* SLT/SLTU/...: not a vertex */
|
|
pv_reset(dst, result);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* immediates */
|
|
uint32_t dst_reg = f_rt(instr);
|
|
if (dst_reg == 0) return;
|
|
PGXPValue *dst = &s_gpr[dst_reg];
|
|
const PGXPValue *a = gpr_shadow(f_rs(instr), &ca);
|
|
|
|
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 (a->gen == s_gen) {
|
|
*dst = *a;
|
|
pv_validate(dst, result);
|
|
} else pv_reset(dst, result);
|
|
return;
|
|
}
|
|
/* 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 = (uint16_t)(PGXP_F_VXY | derived_bits(a, s1));
|
|
if (comp_tracked(a, s1, 0) && comp_tracked(a, s1, 1))
|
|
keep_depth(dst, a, nullptr);
|
|
return;
|
|
}
|
|
case 0x0C: case 0x0D: case 0x0E: { /* ANDI / ORI / XORI */
|
|
uint32_t imm = instr & 0xFFFFu;
|
|
alu_bitwise(dst, a, s1, nullptr, imm, result);
|
|
/* ORI into a zero low half writes an exact constant there (the
|
|
* lui/ori and repack idioms). */
|
|
if (op == 0x0D && !(dst->flags & PGXP_F_VX) && (s1 & 0xFFFFu) == 0) {
|
|
dst->x16 = ((int32_t)(int16_t)imm) << 16;
|
|
dst->flags |= PGXP_F_VX;
|
|
}
|
|
return;
|
|
}
|
|
default: /* SLTI/SLTIU, MFC0/CFC0: reset */
|
|
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;
|
|
if (!g_pgxp_active) return;
|
|
/* Products/quotients of screen coordinates are not screen coordinates:
|
|
* the half shadows of HI/LO are known-but-imprecise. The scalar tier
|
|
* (cpu-mode) does carry a product or quotient when an operand is precise:
|
|
* a weighted sum divided by its weight total IS a screen coordinate (the
|
|
* perspective-correct midpoint of an edge), and it turns back into a
|
|
* half when stored. */
|
|
pv_reset(&s_gpr[PGXP_REG_HI], hi);
|
|
pv_reset(&s_gpr[PGXP_REG_LO], lo);
|
|
sc_kill(&s_gpr_s[PGXP_REG_HI]);
|
|
sc_kill(&s_gpr_s[PGXP_REG_LO]);
|
|
if (!s_cpu_mode) return;
|
|
const uint32_t funct = f_funct(instr);
|
|
const int is_unsigned = (funct & 1u) != 0;
|
|
if (is_unsigned && ((int32_t)s1 < 0 || (int32_t)s2 < 0))
|
|
return; /* stay within the signed range */
|
|
int pa = 0, pb = 0;
|
|
const double a = gpr_scalar(f_rs(instr), s1, 0, &pa);
|
|
const double b = gpr_scalar(f_rt(instr), s2, 0, &pb);
|
|
if (!pa && !pb) return;
|
|
if (funct == 0x18 || funct == 0x19) { /* MULT / MULTU */
|
|
const int64_t exact = (int64_t)(int32_t)s1 * (int64_t)(int32_t)s2;
|
|
if (exact != (int64_t)(int32_t)lo) return; /* HI is not a sign fill */
|
|
sc_set(&s_gpr_s[PGXP_REG_LO], a * b, lo);
|
|
return;
|
|
}
|
|
if (funct == 0x1A || funct == 0x1B) { /* DIV / DIVU */
|
|
if (s2 == 0 || b == 0.0) return; /* divide by zero: no quotient */
|
|
if (s1 == 0x80000000u && s2 == 0xFFFFFFFFu) return; /* overflow */
|
|
sc_set(&s_gpr_s[PGXP_REG_LO], a / b, lo);
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* GTE producer */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/* A tracked half as the precise value of the 16-bit integer the GTE used:
|
|
* the shadow must describe that integer (its half of `value`), and the
|
|
* precise value must floor to it, or lie in the derived window when the half
|
|
* came out of rounding guest arithmetic. */
|
|
static inline int half_precise(const PGXPValue *pv, int hi, int16_t used, double *out) {
|
|
if (!pv || pv->gen != s_gen) return 0;
|
|
if (!(pv->flags & (hi ? PGXP_F_VY : PGXP_F_VX))) return 0;
|
|
const uint16_t half = hi ? (uint16_t)(pv->value >> 16) : (uint16_t)(pv->value & 0xFFFFu);
|
|
if ((int16_t)half != used) return 0;
|
|
const int32_t v16 = hi ? pv->y16 : pv->x16;
|
|
const double v = v16 / 65536.0, d = v - (double)used;
|
|
if (pv->flags & (hi ? PGXP_F_DY : PGXP_F_DX)) {
|
|
if (!(d > -1.0 && d < 2.0)) return 0;
|
|
} else if ((v16 >> 16) != (int32_t)used) {
|
|
return 0;
|
|
}
|
|
*out = v;
|
|
return 1;
|
|
}
|
|
|
|
/* After a GTE command: every command except NCLIP / AVSZ3 / AVSZ4 rewrites
|
|
* IR0..3 / MAC1..3, so their scalars end there. MVMVA settles its own
|
|
* results (pgxp_gte_mvmva, before this runs). MAC0 never carries one. */
|
|
extern "C" void pgxp_gte_op_end(uint32_t func) {
|
|
sc_kill(&s_gte_s[24]);
|
|
if (func == 0x06 || func == 0x2D || func == 0x2E || func == 0x12) return;
|
|
for (int r = 8; r <= 11; r++) sc_kill(&s_gte_s[r]);
|
|
for (int r = 25; r <= 27; r++) sc_kill(&s_gte_s[r]);
|
|
}
|
|
|
|
static void mvmva_results_reset(const PGXPMvmva *op) {
|
|
/* The data-register shadows of the results describe the old values. */
|
|
for (int r = 0; r < 3; r++) {
|
|
pv_reset(&s_gte[25 + r], (uint32_t)op->mac[r]);
|
|
pv_reset(&s_gte[9 + r], (uint32_t)op->ir[r]);
|
|
sc_kill(&s_gte_s[25 + r]);
|
|
sc_kill(&s_gte_s[9 + r]);
|
|
}
|
|
}
|
|
|
|
extern "C" void pgxp_gte_mvmva(const PGXPMvmva *op) {
|
|
if (!g_pgxp_active) return;
|
|
if (!s_cpu_mode || op->mx > 2 || op->tv == 2) { /* reserved / FC bug */
|
|
mvmva_results_reset(op);
|
|
return;
|
|
}
|
|
static const int kBase[3] = { 0, 8, 16 }; /* RT, LLM, LCM */
|
|
double m[3][3], v[3], t[3];
|
|
int any = 0;
|
|
for (int k = 0; k < 9; k++) {
|
|
const int r = k / 3, c = k % 3;
|
|
const PGXPValue *pv = &s_gtc[kBase[op->mx] + (k >> 1)];
|
|
double p;
|
|
if (half_precise(pv, k & 1, op->m[r][c], &p)) { m[r][c] = p; any = 1; }
|
|
else m[r][c] = (double)op->m[r][c];
|
|
}
|
|
for (int c = 0; c < 3; c++) {
|
|
double p;
|
|
int pr = 0;
|
|
if (op->vv < 3) {
|
|
const PGXPValue *pv = &s_gte[2 * op->vv + (c == 2 ? 1 : 0)];
|
|
pr = half_precise(pv, c == 1, op->v[c], &p);
|
|
} else {
|
|
p = reg_scalar(&s_gte_s[9 + c], &s_gte[9 + c],
|
|
(uint32_t)(int32_t)op->v[c], 0, &pr);
|
|
}
|
|
v[c] = pr ? p : (double)op->v[c];
|
|
any |= pr;
|
|
}
|
|
for (int r = 0; r < 3; r++) {
|
|
t[r] = (double)op->t[r];
|
|
if (op->tv > 1) continue;
|
|
const int reg = (op->tv == 0 ? 5 : 13) + r; /* TR / BK words */
|
|
int pr = 0;
|
|
const double p = reg_scalar(&s_gtc_s[reg], &s_gtc[reg],
|
|
(uint32_t)(int32_t)(op->t[r] >> 12), 0, &pr);
|
|
if (pr) { t[r] = p * 4096.0; any = 1; }
|
|
}
|
|
/* Operands are read (the vector may be IR itself); now the results. */
|
|
mvmva_results_reset(op);
|
|
if (!any) return;
|
|
/* FLAG: MAC1/2/3 positive overflow bits 30/29/28, negative 27/26/25;
|
|
* IR1/2/3 saturation bits 24/23/22 (psx-spx GTE FLAG). */
|
|
static const uint32_t kMacOverflow[3] = { (1u << 30) | (1u << 27),
|
|
(1u << 29) | (1u << 26),
|
|
(1u << 28) | (1u << 25) };
|
|
static const uint32_t kIrSat[3] = { 1u << 24, 1u << 23, 1u << 22 };
|
|
for (int r = 0; r < 3; r++) {
|
|
if (op->flag & kMacOverflow[r]) continue;
|
|
/* The integers reported must reproduce the guest MAC: proves the
|
|
* operands handed over are the ones the GTE used. */
|
|
const int64_t exact = op->t[r] + (int64_t)op->m[r][0] * op->v[0] +
|
|
(int64_t)op->m[r][1] * op->v[1] +
|
|
(int64_t)op->m[r][2] * op->v[2];
|
|
if ((exact >> op->shift) != (int64_t)op->mac[r]) continue;
|
|
const double p = (t[r] + m[r][0] * v[0] + m[r][1] * v[1] + m[r][2] * v[2]) /
|
|
(op->shift ? 4096.0 : 1.0);
|
|
sc_set(&s_gte_s[25 + r], p, (uint32_t)op->mac[r]);
|
|
if (!(op->flag & kIrSat[r]) && op->ir[r] == op->mac[r])
|
|
sc_set(&s_gte_s[9 + r], p, (uint32_t)op->ir[r]);
|
|
}
|
|
}
|
|
|
|
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, verified IR1/IR2 conversion, 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;
|
|
const int64_t ix = mac1 >> 12, iy = mac2 >> 12;
|
|
const int64_t z_int = mac3 >> 12;
|
|
if (z_int <= 0) return 0;
|
|
const bool xy_clamped = ix != ir1 || iy != ir2;
|
|
const bool z_clamped = z_int > 0xFFFF;
|
|
const bool unclamped = xy_clamped || z_clamped;
|
|
if (unclamped) {
|
|
/* Only the opt-in exact projection may bypass IR saturation. Reject
|
|
* wrapped accumulators, lm=1 sign clamps and every other mismatch. */
|
|
const int64_t lo = -(INT64_C(1) << 43), hi = (INT64_C(1) << 43) - 1;
|
|
const int64_t cx = ix < -32768 ? -32768 : ix > 32767 ? 32767 : ix;
|
|
const int64_t cy = iy < -32768 ? -32768 : iy > 32767 ? 32767 : iy;
|
|
if (!s_preserve_projection || mac1 < lo || mac1 > hi ||
|
|
mac2 < lo || mac2 > hi || mac3 < lo || mac3 > hi ||
|
|
cx != ir1 || cy != ir2) return 0;
|
|
}
|
|
if ((uint32_t)(z_clamped ? 0xFFFF : 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 unclamped ? 2 : 1;
|
|
}
|
|
|
|
/* Defined below with the GPU consumer. */
|
|
static inline int pgxp_agrees(int32_t p16, int32_t native, int16_t half,
|
|
int derived);
|
|
|
|
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, 0) && pgxp_agrees(y16, hy, hy, 0)) {
|
|
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" void pgxp_gte_mark_unclamped(uint32_t depth) {
|
|
if (!g_pgxp_active) return;
|
|
s_gte[14].flags |= PGXP_F_UNCLAMPED;
|
|
s_gte[14].projection.z = (float)depth;
|
|
s_gte[15] = s_gte[14];
|
|
}
|
|
|
|
extern "C" float pgxp_get_unclamped_depth(uint32_t addr, uint32_t word) {
|
|
if (!g_pgxp_active || !s_preserve_projection) return 0.0f;
|
|
const PGXPValue *pv = pgxp_ptr(addr);
|
|
if (!pv || pv->gen != s_gen || pv->value != word ||
|
|
!(pv->flags & PGXP_F_UNCLAMPED) || !(pv->flags & PGXP_F_VZ)) return 0.0f;
|
|
return pv->projection.z;
|
|
}
|
|
|
|
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" void pgxp_gte_set_projection(const PGXPProjection *projection) {
|
|
if (!g_pgxp_active || !s_projection_tracking || !projection) return;
|
|
s_gte[14].projection = *projection;
|
|
s_gte[14].identity = 0; /* a projection has no model identity until its producer supplies one */
|
|
s_gte[14].flags |= PGXP_F_PROJECTION;
|
|
s_gte[15] = s_gte[14];
|
|
}
|
|
static int projection_read(const PGXPValue *pv, uint32_t packed, PGXPProjection *out) {
|
|
const uint16_t flags = PGXP_F_VXY | PGXP_F_PROJECTION;
|
|
if (!s_enabled || !s_projection_tracking || !pv || pv->gen != s_gen || pv->value != packed ||
|
|
(pv->flags & flags) != flags) return 0;
|
|
if (out) *out = pv->projection;
|
|
return 1;
|
|
}
|
|
extern "C" int pgxp_load_projection(uint32_t addr, uint32_t packed, PGXPProjection *out) {
|
|
return projection_read(pgxp_ptr(addr), packed, out);
|
|
}
|
|
extern "C" uint32_t pgxp_gte_input_identity(uint32_t vertex) {
|
|
return vertex < 3u ? s_model_origin[vertex * 2u] : 0;
|
|
}
|
|
extern "C" void pgxp_gte_projection_identity(uint32_t identity) {
|
|
s_gte[14].identity = identity;
|
|
s_gte[15].identity = identity;
|
|
}
|
|
extern "C" int pgxp_load_temporal_projection(uint32_t addr, uint32_t packed,
|
|
PGXPProjection *out, uint32_t *identity) {
|
|
const PGXPValue *v = pgxp_ptr(addr);
|
|
if (!projection_read(v, packed, out)) return 0;
|
|
*identity = v->identity;
|
|
return *identity != 0;
|
|
}
|
|
extern "C" int pgxp_get_gte_projection(uint32_t index, uint32_t packed, PGXPProjection *out) {
|
|
return index < 4 ? projection_read(&s_gte[12+index], packed, out) : 0;
|
|
}
|
|
|
|
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, uint16_t flags);
|
|
|
|
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], pv->flags) != 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;
|
|
if (reg < 6) s_model_origin[reg] = 0;
|
|
if (reg == 15) {
|
|
/* SXYP is a FIFO push: SXY1 moves to SXY0, SXY2 to SXY1, the value
|
|
* lands in SXY2 (psx-spx GTE "SXYP"). Engines draw quads from a
|
|
* projected-vertex table this way - load three corners, NCLIP, push
|
|
* the fourth through SXYP, NCLIP, store SXY0..2 into the packet - so
|
|
* the shadows shift with the registers. The MTC2 / LWC2 hook then
|
|
* copies the source shadow into the pushed slot. */
|
|
s_gte[12] = s_gte[13];
|
|
s_gte[13] = s_gte[14];
|
|
reg = 14;
|
|
}
|
|
pv_kill(&s_gte[reg]);
|
|
pv_reset(&s_gte[reg], value);
|
|
gte_sxy2_mirror(reg); /* SXY2 and SXYP: one register */
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* 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 decoded coordinate is saturated or the word is a genuinely
|
|
* wrapped coordinate. Known outcode bits 13..15 alone are not saturation;
|
|
* bits 11/12 must still match the decoded coordinate's sign fill. */
|
|
static inline int pgxp_agrees(int32_t p16, int32_t native, int16_t half,
|
|
int derived) {
|
|
const int64_t d = (int64_t)p16 - (int64_t)native * 65536;
|
|
/* A derived half (PGXP_F_DX/DY): the guest integer is the rounding its
|
|
* own arithmetic chose, so the value may sit one pixel either side of
|
|
* the truncation window - still bounded, still the same vertex. */
|
|
if (derived) return d > -65536 && d < 2 * 65536;
|
|
const uint16_t sign_fill = native < 0 ? 0x1800u : 0;
|
|
if (!s_preserve_projection || native <= -0x400 || native >= 0x3FF ||
|
|
(uint16_t(half) & 0x1800u) != sign_fill)
|
|
return (p16 >> 16) == native;
|
|
return d > -(int64_t)PGXP_PPP_AGREE_BELOW * 65536 &&
|
|
d < (int64_t)PGXP_PPP_AGREE_ABOVE * 65536;
|
|
}
|
|
|
|
/* Tolerance-clamp distance of one axis: how far the precise value moved the
|
|
* vertex from the guest integer. For a derived half the first pixel above
|
|
* the integer is the guest arithmetic's own rounding, not movement. */
|
|
static inline float pgxp_moved(int32_t p16, int32_t native, int derived) {
|
|
const float d = (float)((int64_t)p16 - (int64_t)native * 65536) * (1.0f / 65536.0f);
|
|
if (!derived) return std::fabs(d);
|
|
return d < 0.0f ? -d : (d > 1.0f ? d - 1.0f : 0.0f);
|
|
}
|
|
|
|
/* 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. `flags` are the shadow's (derived halves). */
|
|
static inline int pgxp_accept(int32_t px, int32_t py, int32_t int_x,
|
|
int32_t int_y, uint32_t word, uint16_t flags) {
|
|
const int dx_derived = (flags & PGXP_F_DX) != 0;
|
|
const int dy_derived = (flags & PGXP_F_DY) != 0;
|
|
/* This tag can only ride an intact, value-validated GTE word. Arithmetic
|
|
* constructs new flags and loses it; the position-cache tier never has it.
|
|
* Clamped IR is an integer hardware limit, not the enhanced visual vertex. */
|
|
if (!(s_preserve_projection && (flags & PGXP_F_UNCLAMPED)) &&
|
|
(!pgxp_agrees(px, int_x, (int16_t)(word & 0xFFFFu), dx_derived) ||
|
|
!pgxp_agrees(py, int_y, (int16_t)(word >> 16), dy_derived)))
|
|
return 1;
|
|
if (s_tolerance >= 0.0f) {
|
|
if (pgxp_moved(px, int_x, dx_derived) > s_tolerance ||
|
|
pgxp_moved(py, int_y, dy_derived) > s_tolerance)
|
|
return 2;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Rebase a shadow that describes the int16 half onto GP0's 11-bit field when
|
|
* the half carries flag bits ABOVE the field (Spider-Man packs clip flags in
|
|
* bits 14/15). A half whose bits 11-15 are a plain sign extension holds an
|
|
* ordinary number; if it overflowed the field (e.g. 0x0402 parses as -1022),
|
|
* that is CPU arithmetic the GPU wraps, which pgxp_agrees() only believes on
|
|
* exact integer agreement, so it is left to that check unchanged. */
|
|
static inline int32_t field_rebase(int32_t v16, uint32_t half, int32_t parsed) {
|
|
int32_t as16 = (int16_t)half;
|
|
int32_t as11 = ((int32_t)(half << 21)) >> 21;
|
|
if (parsed != as11 || parsed == as16) return v16;
|
|
const uint32_t above = (half >> 11) & 0x1Fu;
|
|
if (above == ((half & 0x8000u) ? 0x1Fu : 0u)) return v16;
|
|
return (int32_t)((int64_t)v16 + ((int64_t)as11 - as16) * 65536);
|
|
}
|
|
|
|
static int pgxp_get_precise_vertex_impl(uint32_t addr, uint32_t packet_word,
|
|
int32_t int_x, int32_t int_y,
|
|
int32_t *x16, int32_t *y16,
|
|
uint16_t *sz, int probe) {
|
|
/* A probe asks the same question without recording it (the rectangle
|
|
* shortcut runs one per corner before drawing): it skips every counter
|
|
* instead of copying the whole statistics record around the call. */
|
|
const int count = !probe;
|
|
if (count) s_stats.lookups++;
|
|
|
|
int32_t px = 0, py = 0;
|
|
uint16_t pz = 0;
|
|
uint16_t pflags = 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;
|
|
pflags = pv->flags;
|
|
have = PGXP_SRC_DATAFLOW;
|
|
} else if (count) {
|
|
s_stats.value_mismatch++;
|
|
}
|
|
}
|
|
}
|
|
|
|
const int fallback_hit = !have && s_position_fallback &&
|
|
(probe ? gte_geometry_correction_lookup_probe(packet_word, &px, &py)
|
|
: gte_geometry_correction_lookup(packet_word, &px, &py));
|
|
if (fallback_hit) {
|
|
pz = 0; /* fallback never carries depth */
|
|
have = PGXP_SRC_FALLBACK;
|
|
}
|
|
|
|
if (have) {
|
|
/* A shadow describes each half read as int16; GP0 decodes only its
|
|
* 11-bit field (engines keep clip / outcode bits above it). When the
|
|
* caller parsed the field, rebase the precise value onto it - the
|
|
* fraction is unchanged. Any other integer is caught below. */
|
|
px = field_rebase(px, packet_word & 0xFFFFu, int_x);
|
|
py = field_rebase(py, packet_word >> 16, int_y);
|
|
/* 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, pflags);
|
|
if (why == 1) {
|
|
if (count) s_stats.trunc_reject++;
|
|
have = 0;
|
|
pz = 0;
|
|
} else if (why == 2) {
|
|
/* The seam clamp (G1.10) keeps the native position, but the
|
|
* shadow still matched this exact word and its integers: its
|
|
* GTE depth describes this vertex. Depth testing needs only
|
|
* that, not the sub-pixel position. */
|
|
if (count) s_stats.tolerance_reject++;
|
|
if (have != PGXP_SRC_DATAFLOW) pz = 0;
|
|
have = 0;
|
|
}
|
|
} else {
|
|
pz = 0;
|
|
}
|
|
|
|
if (!have) {
|
|
if (count) s_stats.native++;
|
|
if (count && pz != 0) s_stats.depth_only++;
|
|
*x16 = int_x << 16;
|
|
*y16 = int_y << 16;
|
|
*sz = pz;
|
|
return PGXP_SRC_NATIVE;
|
|
}
|
|
|
|
if (count) {
|
|
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_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) {
|
|
return pgxp_get_precise_vertex_impl(addr, packet_word, int_x, int_y,
|
|
x16, y16, sz, 0);
|
|
}
|
|
|
|
extern "C" int pgxp_probe_precise_vertex(uint32_t addr, uint32_t packet_word,
|
|
int32_t int_x, int32_t int_y) {
|
|
int32_t x16, y16;
|
|
uint16_t sz;
|
|
return pgxp_get_precise_vertex_impl(addr, packet_word, int_x, int_y,
|
|
&x16, &y16, &sz, 1);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* 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_w(addr);
|
|
if (!dst) return;
|
|
const PGXPValue *src = &s_gte[reg & 31u];
|
|
if (src->gen != s_gen) return;
|
|
s_stats.swc2_stores++;
|
|
*dst = *src;
|
|
}
|
|
|
|
extern "C" int pgxp_store_flagged_gte_sxy(uint32_t addr, uint8_t reg,
|
|
uint32_t source_word, uint32_t stored_word) {
|
|
if (!g_pgxp_active || (addr & 3u) || reg < 12 || reg > 15 ||
|
|
((source_word ^ stored_word) & 0x07FF07FFu)) return 0;
|
|
const PGXPValue* src = &s_gte[reg];
|
|
if (src->gen != s_gen || src->value != source_word || !src->z ||
|
|
(src->flags & (PGXP_F_VXY | PGXP_F_VZ)) != (PGXP_F_VXY | PGXP_F_VZ)) return 0;
|
|
const int64_t x = (int64_t)src->x16 +
|
|
((int64_t)(int16_t)stored_word - (int16_t)source_word) * 65536;
|
|
const int64_t y = (int64_t)src->y16 +
|
|
((int64_t)(int16_t)(stored_word >> 16) - (int16_t)(source_word >> 16)) * 65536;
|
|
if (x < INT32_MIN || x > INT32_MAX || y < INT32_MIN || y > INT32_MAX) return 0;
|
|
PGXPValue* dst = pgxp_ptr_w(addr);
|
|
if (!dst) return 0;
|
|
*dst = *src;
|
|
dst->value = stored_word;
|
|
dst->x16 = (int32_t)x;
|
|
dst->y16 = (int32_t)y;
|
|
return 1;
|
|
}
|
|
|
|
extern "C" int pgxp_debug_shadow(int space, uint32_t key, int *live,
|
|
uint32_t *value, uint32_t *flags,
|
|
int32_t *x16, int32_t *y16, uint16_t *z) {
|
|
const PGXPValue *pv = nullptr;
|
|
if (space == 0) pv = pgxp_ptr(key);
|
|
else if (space == 1 && key < 34) pv = &s_gpr[key];
|
|
else if (space == 2 && key < 32) pv = &s_gte[key];
|
|
if (!pv) return 0;
|
|
*live = pv->gen == s_gen;
|
|
*value = pv->value;
|
|
*flags = pv->flags;
|
|
*x16 = pv->x16;
|
|
*y16 = pv->y16;
|
|
*z = pv->z;
|
|
return 1;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* 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. */
|
|
/* Always-on ring of refused precise-word lookups: which packet word, what
|
|
* the shadow at its address held, and why it was refused. Join it with
|
|
* wtrace_dump (writer PC per address) to find the guest code path that drops
|
|
* provenance. */
|
|
static PSX_THREAD_LOCAL PGXPWordMiss s_miss_ring[PGXP_MISS_RING_CAP];
|
|
static PSX_THREAD_LOCAL uint64_t s_miss_seq = 0;
|
|
|
|
static void note_word_miss(uint32_t addr, uint32_t packed, const PGXPValue *pv,
|
|
uint8_t reason) {
|
|
#ifndef PSX_NO_DEBUG_TOOLS
|
|
PGXPWordMiss *m = &s_miss_ring[s_miss_seq % PGXP_MISS_RING_CAP];
|
|
m->seq = s_miss_seq++;
|
|
m->addr = addr;
|
|
m->packet = packed;
|
|
m->shadow_value = pv ? pv->value : 0;
|
|
m->shadow_flags = pv ? (uint8_t)pv->flags : 0;
|
|
m->live = (pv && pv->gen == s_gen) ? 1 : 0;
|
|
m->reason = reason;
|
|
#else
|
|
(void)addr; (void)packed; (void)pv; (void)reason;
|
|
#endif
|
|
}
|
|
|
|
extern "C" uint64_t pgxp_word_miss_ring(const PGXPWordMiss **ring,
|
|
uint32_t *cap) {
|
|
*ring = s_miss_ring;
|
|
*cap = PGXP_MISS_RING_CAP;
|
|
return s_miss_seq;
|
|
}
|
|
|
|
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;
|
|
s_stats.word_lookups++;
|
|
PGXPValue *pv = pgxp_ptr(addr);
|
|
if (!pv || pv->gen != s_gen) {
|
|
s_stats.word_untracked++;
|
|
note_word_miss(addr, packed, pv, PGXP_MISS_UNTRACKED);
|
|
return 0;
|
|
}
|
|
if (pv->value != packed) {
|
|
s_stats.word_mismatch++;
|
|
note_word_miss(addr, packed, pv, PGXP_MISS_MISMATCH);
|
|
return 0;
|
|
}
|
|
if ((pv->flags & PGXP_F_VXY) != PGXP_F_VXY) {
|
|
s_stats.word_partial++;
|
|
note_word_miss(addr, packed, pv, PGXP_MISS_PARTIAL);
|
|
return 0;
|
|
}
|
|
if (x16) *x16 = pv->x16;
|
|
if (y16) *y16 = pv->y16;
|
|
if (z) *z = (pv->flags & PGXP_F_VZ) ? pv->z : 0;
|
|
if (!(pv->flags & PGXP_F_VZ) || pv->z == 0) {
|
|
s_stats.word_no_z++;
|
|
note_word_miss(addr, packed, pv, PGXP_MISS_NO_Z);
|
|
return 0;
|
|
}
|
|
s_stats.word_hit++;
|
|
return 1;
|
|
}
|
|
|
|
|
|
/* Only architectural precision state/configuration crosses the publication
|
|
* boundary. Journals, diagnostic rings and their allocations stay local to
|
|
* each executing thread. */
|
|
#define PGXP_EXEC_FIELDS(X) \
|
|
X(s_scratch) X(s_gpr) X(s_gte) X(s_gtc) X(s_model_origin) \
|
|
X(s_gpr_s) X(s_gte_s) X(s_gtc_s) X(s_gen) X(s_enabled) X(s_cpu_mode) \
|
|
X(s_projection_tracking) X(s_tolerance) X(s_position_fallback) \
|
|
X(s_preserve_projection) X(s_culling)
|
|
struct PGXPExecutionSnapshot {
|
|
#define EXEC_FIELD(name) decltype(::name) name{};
|
|
PGXP_EXEC_FIELDS(EXEC_FIELD)
|
|
#undef EXEC_FIELD
|
|
PGXPValue *words = nullptr;
|
|
uint64_t versions[PGXP_EXEC_PAGES]{};
|
|
bool ready = false;
|
|
};
|
|
extern "C" PGXPExecutionSnapshot *pgxp_execution_snapshot_create(void) {
|
|
auto *snapshot = new (std::nothrow) PGXPExecutionSnapshot;
|
|
if (!snapshot) return nullptr;
|
|
snapshot->words = (PGXPValue *)execution_storage_calloc(PGXP_SHADOW_WORDS * sizeof(PGXPValue));
|
|
if (!snapshot->words) { delete snapshot; return nullptr; }
|
|
return snapshot;
|
|
}
|
|
extern "C" void pgxp_execution_snapshot_destroy(PGXPExecutionSnapshot *snapshot) {
|
|
if (!snapshot) return;
|
|
if (s_bound_versions == snapshot->versions) std::abort();
|
|
std::free(snapshot->words);
|
|
delete snapshot;
|
|
}
|
|
extern "C" int pgxp_execution_snapshot_capture(PGXPExecutionSnapshot *snapshot) {
|
|
if (!snapshot || s_bound_versions || s_ck_depth || s_suppress) return 0;
|
|
snapshot->ready = false;
|
|
if (s_ram) {
|
|
if (!s_exec_versions) {
|
|
s_exec_versions = (uint64_t *)std::calloc(PGXP_EXEC_PAGES, sizeof(uint64_t));
|
|
if (!s_exec_versions) return 0;
|
|
for (size_t page = 0; page < PGXP_EXEC_PAGES; ++page) s_exec_versions[page] = 1;
|
|
}
|
|
for (size_t page = 0; page < PGXP_EXEC_PAGES; ++page) {
|
|
if (snapshot->versions[page] == s_exec_versions[page]) continue;
|
|
const size_t first = page * PGXP_EXEC_PAGE_WORDS;
|
|
const size_t count = PGXP_SHADOW_WORDS - first < PGXP_EXEC_PAGE_WORDS
|
|
? PGXP_SHADOW_WORDS - first : PGXP_EXEC_PAGE_WORDS;
|
|
std::memcpy(snapshot->words + first, s_ram + first, count * sizeof(PGXPValue));
|
|
snapshot->versions[page] = s_exec_versions[page];
|
|
}
|
|
}
|
|
#define EXEC_SAVE(name) std::memcpy(&snapshot->name, &name, sizeof name);
|
|
PGXP_EXEC_FIELDS(EXEC_SAVE)
|
|
#undef EXEC_SAVE
|
|
if (++s_exec_epoch == 0) std::abort();
|
|
snapshot->ready = true;
|
|
return 1;
|
|
}
|
|
extern "C" int pgxp_execution_snapshot_bind(PGXPExecutionSnapshot *snapshot) {
|
|
if (!snapshot || !snapshot->ready || s_ck_depth || s_suppress) return 0;
|
|
#define EXEC_LOAD(name) std::memcpy(&name, &snapshot->name, sizeof name);
|
|
PGXP_EXEC_FIELDS(EXEC_LOAD)
|
|
#undef EXEC_LOAD
|
|
s_ram = snapshot->words;
|
|
s_bound_versions = snapshot->versions;
|
|
s_deferred_invalidate = 0;
|
|
recompute_active();
|
|
return 1;
|
|
}
|
|
extern "C" void pgxp_execution_snapshot_unbind(void) {
|
|
if (!s_bound_versions) return;
|
|
if (s_ck_depth || s_suppress) std::abort();
|
|
s_ram = nullptr;
|
|
s_bound_versions = nullptr;
|
|
s_enabled = 0;
|
|
recompute_active();
|
|
}
|
|
#undef PGXP_EXEC_FIELDS
|