mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
e6ce5006b4
The repository root carried seven markdown files that were not onboarding
material, plus two that had been superseded by the code they described. This
sorts them: reader-facing docs to docs/, working notes to docs/internal/, and
the two superseded ones merged into the docs that replaced them.
Moved to docs/: WIDESCREEN.md, ENHANCEMENTS.md, TCP_COMMANDS.md
Moved to docs/internal/: PRINCIPLES.md, ISSUES.md, DEBUG.md, GTE_LM_FIX.md,
and from docs/: bios-hardening.md, overlay-status.md,
overlay-plan.md, overlay-recompilation-design.md,
overlay-discovery.md, TOMBA2_EXC_CONSULT_RESPONSE.md,
CASE_A_AOT_GAP.md, audit_inventory.md
Moved to docs/internal/upstream/: UPSTREAM_2026-08-20.md
The root keeps only what belongs there: README.md, CONTRIBUTING.md, LICENSE,
CLAUDE.md, THIRD_PARTY_ATTRIBUTION.md, RELEASE_NOTES.md, the dotfiles and
psxrecomp_cli.py.
MERGES
SPLITGEN_MIGRATION.md folded into docs/BUILDING.md as "Split generated C". Its
migration instructions were stale in a way that mattered: it told every game
repo to hand-edit its CMakeLists and add a file(GLOB ...) over
<serial>_full*.c, but runtime/runtime.cmake now does that itself. GEN_FULL_GLOB
is a multi-value argument of psxrecomp_add_game_runtime(); the loop at
runtime.cmake:1985 makes each pattern absolute against CMAKE_CURRENT_SOURCE_DIR,
expands it with file(GLOB), and concatenates the hits into
GAME_GENERATED_FULL_C. So a title names the pattern and gets however many shards
a regeneration produced. The rewritten section describes that, the two-step
fallback when the globs match nothing (GEN_FULL_FALLBACK, else <serial>_full.c
derived from the dispatch marker by substituting _dispatch.c), and the one
caveat the code actually has: that file(GLOB) is plain, not CONFIGURE_DEPENDS,
so a regeneration which changes the shard COUNT needs a re-configure to be seen.
psxrecomp-toml-readme.md folded into docs/GAME_PROJECT_SETUP.md as "EXE-only
autofill (psxrecomp-toml)", placed after "Disc autofill (probe_disc.py)" and
before "Project Studio" so the two autofill paths sit together. Its documented
flags were checked against the usage text in recompiler/src/main_toml.cpp:28-42
and against the argument parser at :150-165 -- all seven options match with no
drift, so the flag list is reproduced as-is. Added a pointer that probe_disc.py
is preferable when a cue exists, since that tool does the same EXE-header work
plus disc identity and netplay gates.
FUNCTIONAL FIXES
tools/gen_tcp_commands.py wrote to REPO/"TCP_COMMANDS.md". After the move that
path does not exist, so the generator would have created a new stray root file
and --check would have failed against it forever. Retargeted to
REPO/"docs"/"TCP_COMMANDS.md", along with the seven docstring and message
strings that name the path.
Running it surfaced two further problems, both fixed here:
* The doc was genuinely stale -- pre-existing drift, not introduced by this
commit (the moved file is byte-identical to origin/master, and no
debug-server command table is touched here). Four commands registered since
the last regeneration were missing from the index -- cdrom_timing_dump,
savestate_status, ws_ui_groups, xprobe_watch -- and scanline had gained a
backend. Regenerated: 307 -> 311 commands, and the change is confined to the
AUTOGEN block (the hand-written head and tail are byte-identical to master).
tools/gen_tcp_commands.py --check now exits 0.
* DOC.write_text() has no newline argument, so Python translated every newline
to os.linesep. On Windows that rewrote all 585 lines of the file to CRLF and
turned a four-line index update into a whole-file diff -- measured: 589 CRLF
/ 0 LF after a run, against 0 CRLF / 585 LF in the repo. Now written through
an explicit newline="" handle, verified to leave the file at 0 CRLF.
runtime/include/debug_server.h:9 and runtime/src/debug_server.c:8 cited "TCP.md
and DEBUG.md". TCP.md has never existed in this repo under that name; both now
name docs/TCP_COMMANDS.md and docs/internal/DEBUG.md.
LINK AND REFERENCE FIXES
Every inbound reference to a moved file was located by grepping each basename
across the whole tracked tree, not just the markdown. Markdown hrefs in
README.md, CONTRIBUTING.md, docs/ARCHITECTURE.md, docs/COMPILING_OVERLAYS.md,
docs/AOT_OVERLAY_PLAN.md, docs/RELOCATION_MANIFEST_FORMAT.md,
docs/config_schema.md, docs/OVERLAY_XREF.md, docs/TCP_COMMANDS.md and several
docs/internal/ files now point at the new locations; where two files became
siblings under docs/internal/ the path was shortened rather than lengthened
(bios-hardening.md's reference to overlay-discovery.md, for instance).
docs/COMPILING_OVERLAYS.md's "in this folder" phrasing was wrong once two of the
three files it listed moved, so it now names internal/ explicitly.
24 source-comment mentions of ENHANCEMENTS.md sections G1.x across 19 files
(recompiler, runtime, cmake) were retargeted to docs/ENHANCEMENTS.md with the
section numbers untouched. docs/SHADOW_ENHANCEMENTS.md is a DIFFERENT file and
was deliberately excluded.
20 rationale strings citing docs/CASE_A_AOT_GAP.md across the four
recompiler/seeds/*.json files were updated. Done as a byte-level substring
replacement rather than a JSON round-trip: two of those files are stored with
CRLF endings and two with LF, and re-serializing them reformatted 5,700 lines.
The byte-level version changes exactly 20 lines and each file still parses.
References that look similar but point elsewhere were deliberately left alone,
and are worth naming so a future pass does not "fix" them:
* "recomp-template PRINCIPLES.md" / "NES recomp template PRINCIPLES.md" in
CLAUDE.md:43, docs/SHADOW_ENHANCEMENTS.md:28,
docs/internal/HLE_SCHEDULER_CARVEOUT_PLAN.md:5,
runtime/src/debug_server.c:4934 and tools/parity_diff.py:8 are a different
repository's file. Only the five that mean this repo's PRINCIPLES.md were
updated.
* Every remaining bare ISSUES.md mention is a GAME repo's tracker
(MegaManX6Recomp 2/7/8/9, TombaRecomp 6), not this repo's.
* docs/disc-speed.md:4,54,56 and docs/internal/bios-hardening.md:5 name
overlay-discovery as a BRANCH, not a file, so no path applies.
INDEXES
README.md's "New here?" table gains rows for docs/WIDESCREEN.md,
docs/ENHANCEMENTS.md and docs/TCP_COMMANDS.md. docs/internal/README.md gains
sections indexing all fourteen documents that moved in, plus an "Upstream
contributor reviews" section describing upstream/ and the new sweep file, plus a
"Removed" bullet for CYCLE_TIMING_ARCH.md (deleted in the previous commit)
pointing at FAITHFUL_TIMING_PLAN.md, CYCLE_MODEL_BEETLE.md and
PRECISE_IRQ_SLICE.md -- following the SLJIT_PERSIST_CACHE.md precedent already
in that file.
docs/ENHANCEMENTS.md:7 claimed framework bugs live in "game repos' ISSUES.md
until a framework tracker exists". A framework tracker does exist; it now says
so and names the Beads epic.
VERIFICATION
A link checker was written to scan every tracked *.md for relative links,
resolving each against the tree being scanned. Baseline at origin/master: 129
files, 137 relative links, 3 broken. After both commits: 127 files, 159 relative
links, 3 broken -- the SAME three, all pre-existing and untouched here:
docs/DATA_SHARDS.md -> INSTANT_LOADS_PARKED.md (never existed)
docs/DATA_SHARDS.md -> INSTANT_LOADS_SPIKE2.md (never existed)
docs/NETPLAY.md -> ../lib/recomp-net/README.md (inside a submodule)
Zero new broken links. The checker's own coverage was checked against a raw grep
for markdown link openers: the 3-link difference is C++ lambdas inside code
blocks, correctly not treated as links.
Also verified: py_compile over all 113 tracked tools/*.py plus psxrecomp_cli.py;
all four seed JSONs still parse; the test-registration guard still reports 123
test files all registered; recompiler/tests/test_coverage_vault.py still passes;
and no file in this commit had its line endings flipped (checked mechanically
across all 50 modified files, after two files were caught and redone).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
818 lines
33 KiB
C++
818 lines
33 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 <cstdlib>
|
|
#include <cstring>
|
|
|
|
/* gte.cpp — position-cache fallback tier (ambiguity-gated, G1.4 exact table) */
|
|
extern "C" int gte_geometry_correction_lookup(uint32_t packed,
|
|
int32_t *x16, int32_t *y16);
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Shadow storage */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
enum {
|
|
PGXP_F_VX = 1u << 0, /* low half (screen X) tracked */
|
|
PGXP_F_VY = 1u << 1, /* high half (screen Y) tracked */
|
|
PGXP_F_VZ = 1u << 2, /* projected depth rode along intact */
|
|
PGXP_F_VXY = PGXP_F_VX | PGXP_F_VY,
|
|
};
|
|
|
|
struct PGXPValue {
|
|
int32_t x16, y16; /* sub-pixel screen coords, 16.16 */
|
|
uint16_t z; /* projected SZ depth (perspective source), 0 = none */
|
|
uint16_t flags;
|
|
uint32_t value; /* the guest word this shadow describes */
|
|
uint32_t gen; /* valid iff == s_gen (O(1) invalidate-all) */
|
|
};
|
|
|
|
#define PGXP_RAM_WORDS (0x200000u >> 2) /* 2 MB RAM */
|
|
#define PGXP_SCRATCH_WORDS (0x400u >> 2) /* 1 KB scratchpad */
|
|
#define PGXP_REG_HI 32
|
|
#define PGXP_REG_LO 33
|
|
|
|
static PGXPValue *s_ram = nullptr; /* lazily allocated, ~10 MB */
|
|
static PGXPValue s_scratch[PGXP_SCRATCH_WORDS];
|
|
static PGXPValue s_gpr[34]; /* 32 GPRs + HI + LO */
|
|
static PGXPValue s_gte[32]; /* GTE data registers */
|
|
|
|
static uint32_t s_gen = 1;
|
|
static int s_enabled = 0;
|
|
static int s_cpu_mode = 0;
|
|
static float s_tolerance = 0.5f; /* user-validated seam clamp (G1.10) */
|
|
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" void pgxp_set_cpu_mode(int enabled) { s_cpu_mode = enabled ? 1 : 0; }
|
|
extern "C" int pgxp_cpu_mode(void) { return s_cpu_mode; }
|
|
|
|
extern "C" void pgxp_set_tolerance(float pixels) { s_tolerance = pixels; }
|
|
extern "C" float pgxp_tolerance(void) { return s_tolerance; }
|
|
|
|
extern "C" void pgxp_suppress_begin(void) {
|
|
++s_suppress;
|
|
recompute_active();
|
|
}
|
|
|
|
extern "C" void pgxp_suppress_end(void) {
|
|
if (s_suppress != 0 && --s_suppress == 0) {
|
|
if (s_deferred_invalidate) {
|
|
s_deferred_invalidate = 0;
|
|
pgxp_invalidate_all();
|
|
}
|
|
recompute_active();
|
|
}
|
|
}
|
|
|
|
extern "C" void pgxp_get_stats(PGXPStats *out) {
|
|
if (out) *out = s_stats;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Address mapping + validation */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/* Guest address -> shadow slot, or NULL for BIOS/MMIO/KSEG2 (untrackable). */
|
|
static inline PGXPValue *pgxp_ptr(uint32_t addr) {
|
|
uint32_t m = addr & 0x1FFFFFFFu;
|
|
if (m < 0x00800000u) /* RAM + its mirrors */
|
|
return s_ram ? &s_ram[(m & 0x1FFFFCu) >> 2] : nullptr;
|
|
if ((m & 0xFFFFFC00u) == 0x1F800000u) /* scratchpad */
|
|
return &s_scratch[(m & 0x3FCu) >> 2];
|
|
return nullptr;
|
|
}
|
|
|
|
static inline int pv_live(const PGXPValue *pv) {
|
|
return pv && pv->gen == s_gen && (pv->flags & PGXP_F_VXY) != 0;
|
|
}
|
|
|
|
/* Drop whichever tracked halves no longer match the actual guest word; the
|
|
* depth belongs to the whole vertex, so any half going stale kills it too. */
|
|
static inline void pv_validate(PGXPValue *pv, uint32_t actual) {
|
|
if (pv->gen != s_gen) return;
|
|
uint32_t diff = pv->value ^ actual;
|
|
if ((pv->flags & PGXP_F_VX) && (diff & 0x0000FFFFu))
|
|
pv->flags &= (uint16_t)~(PGXP_F_VX | PGXP_F_VZ);
|
|
if ((pv->flags & PGXP_F_VY) && (diff & 0xFFFF0000u))
|
|
pv->flags &= (uint16_t)~(PGXP_F_VY | PGXP_F_VZ);
|
|
pv->value = actual;
|
|
}
|
|
|
|
/* Mark a slot as "known word, no precision" — keeps `value` current so later
|
|
* half-merges stay keyed correctly. */
|
|
static inline void pv_reset(PGXPValue *pv, uint32_t value) {
|
|
pv->x16 = 0; pv->y16 = 0; pv->z = 0;
|
|
pv->flags = 0;
|
|
pv->value = value;
|
|
pv->gen = s_gen;
|
|
}
|
|
|
|
static inline void pv_kill(PGXPValue *pv) { pv->gen = 0; }
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Instruction field helpers */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static inline uint32_t f_op(uint32_t i) { return i >> 26; }
|
|
static inline uint32_t f_rs(uint32_t i) { return (i >> 21) & 31u; }
|
|
static inline uint32_t f_rt(uint32_t i) { return (i >> 16) & 31u; }
|
|
static inline uint32_t f_rd(uint32_t i) { return (i >> 11) & 31u; }
|
|
static inline uint32_t f_shamt(uint32_t i) { return (i >> 6) & 31u; }
|
|
static inline uint32_t f_funct(uint32_t i) { return i & 63u; }
|
|
static inline int32_t f_simm(uint32_t i) { return (int32_t)(int16_t)(i & 0xFFFFu); }
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Memory-mode hooks: loads / stores */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
extern "C" void psx_pgxp_load(struct CPUState *cpu, uint32_t instr,
|
|
uint32_t addr, uint32_t value) {
|
|
(void)cpu;
|
|
if (!g_pgxp_active) return;
|
|
uint32_t rt = f_rt(instr);
|
|
if (rt == 0) return;
|
|
PGXPValue *dst = &s_gpr[rt];
|
|
|
|
switch (f_op(instr)) {
|
|
case 0x23: { /* LW */
|
|
PGXPValue *src = pgxp_ptr(addr);
|
|
if (src && src->gen == s_gen) {
|
|
pv_validate(src, value);
|
|
*dst = *src;
|
|
} else {
|
|
pv_reset(dst, value);
|
|
}
|
|
return;
|
|
}
|
|
case 0x21: /* LH */
|
|
case 0x25: { /* LHU */
|
|
PGXPValue *src = pgxp_ptr(addr);
|
|
int hi_half = (addr >> 1) & 1;
|
|
pv_reset(dst, value);
|
|
/* The high half of the extended GPR is a known-exact constant
|
|
* (0/-1 for LH, 0 for LHU): track it so re-packing via sll/or in
|
|
* cpu-mode keeps working. */
|
|
dst->y16 = ((int32_t)value >> 16) << 16;
|
|
dst->flags = PGXP_F_VY;
|
|
if (src && src->gen == s_gen) {
|
|
uint32_t actual_half = (value & 0xFFFFu) << (hi_half ? 16 : 0);
|
|
uint32_t mask = hi_half ? 0xFFFF0000u : 0x0000FFFFu;
|
|
uint16_t want = hi_half ? PGXP_F_VY : PGXP_F_VX;
|
|
if ((src->flags & want) && ((src->value ^ actual_half) & mask) == 0) {
|
|
dst->x16 = hi_half ? src->y16 : src->x16;
|
|
dst->flags |= PGXP_F_VX;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
default: /* LB/LBU/LWL/LWR: untrackable */
|
|
pv_reset(dst, value);
|
|
return;
|
|
}
|
|
}
|
|
|
|
extern "C" void psx_pgxp_store(struct CPUState *cpu, uint32_t instr,
|
|
uint32_t addr, uint32_t value) {
|
|
(void)cpu;
|
|
if (!g_pgxp_active) return;
|
|
PGXPValue *dst = pgxp_ptr(addr);
|
|
if (!dst) return;
|
|
uint32_t rt = f_rt(instr);
|
|
PGXPValue *src = (rt != 0) ? &s_gpr[rt] : nullptr;
|
|
|
|
switch (f_op(instr)) {
|
|
case 0x2B: { /* SW */
|
|
if (src && src->gen == s_gen) {
|
|
pv_validate(src, value);
|
|
*dst = *src;
|
|
} else {
|
|
pv_reset(dst, value);
|
|
}
|
|
return;
|
|
}
|
|
case 0x29: { /* SH */
|
|
int hi_half = (addr >> 1) & 1;
|
|
uint32_t half = value & 0xFFFFu;
|
|
if (dst->gen != s_gen) pv_reset(dst, half << (hi_half ? 16 : 0));
|
|
/* Patch the stored half into the tracked word; the other half's
|
|
* validity (if any) survives untouched. Depth never survives a
|
|
* half-write — the vertex it described no longer exists whole. */
|
|
if (hi_half) dst->value = (dst->value & 0x0000FFFFu) | (half << 16);
|
|
else dst->value = (dst->value & 0xFFFF0000u) | half;
|
|
dst->flags &= (uint16_t)~((hi_half ? PGXP_F_VY : PGXP_F_VX) | PGXP_F_VZ);
|
|
dst->z = 0;
|
|
if (src && src->gen == s_gen && (src->flags & PGXP_F_VX) &&
|
|
((src->value ^ value) & 0xFFFFu) == 0) {
|
|
if (hi_half) { dst->y16 = src->x16; dst->flags |= PGXP_F_VY; }
|
|
else { dst->x16 = src->x16; dst->flags |= PGXP_F_VX; }
|
|
}
|
|
return;
|
|
}
|
|
case 0x28: { /* SB */
|
|
if (dst->gen != s_gen) return; /* nothing tracked: stay dead */
|
|
uint32_t shift = (addr & 3u) * 8u;
|
|
dst->value = (dst->value & ~(0xFFu << shift)) |
|
|
((value & 0xFFu) << shift);
|
|
dst->flags &= (uint16_t)~(((addr & 2u) ? PGXP_F_VY : PGXP_F_VX) | PGXP_F_VZ);
|
|
dst->z = 0;
|
|
return;
|
|
}
|
|
default: /* SWL/SWR: forget the word */
|
|
pv_kill(dst);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Memory-mode hooks: COP2 transfers */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
extern "C" void psx_pgxp_cop2(struct CPUState *cpu, uint32_t instr,
|
|
uint32_t value, uint32_t addr) {
|
|
(void)cpu;
|
|
if (!g_pgxp_active) return;
|
|
|
|
switch (f_op(instr)) {
|
|
case 0x32: { /* LWC2: gte[rt] <- [addr] */
|
|
PGXPValue *src = pgxp_ptr(addr);
|
|
PGXPValue *dst = &s_gte[f_rt(instr)];
|
|
if (src && src->gen == s_gen) {
|
|
pv_validate(src, value);
|
|
*dst = *src;
|
|
} else {
|
|
pv_reset(dst, value);
|
|
}
|
|
return;
|
|
}
|
|
case 0x3A: { /* SWC2: [addr] <- gte[rt] */
|
|
PGXPValue *dst = pgxp_ptr(addr);
|
|
if (!dst) return;
|
|
PGXPValue *src = &s_gte[f_rt(instr)];
|
|
if (src->gen == s_gen) {
|
|
pv_validate(src, value);
|
|
*dst = *src;
|
|
} else {
|
|
pv_reset(dst, value);
|
|
}
|
|
return;
|
|
}
|
|
case 0x12: { /* COP2 register transfers */
|
|
switch (f_rs(instr)) {
|
|
case 0x00: { /* MFC2: gpr[rt] <- gte[rd] */
|
|
uint32_t rt = f_rt(instr);
|
|
if (rt == 0) return;
|
|
PGXPValue *src = &s_gte[f_rd(instr)];
|
|
if (src->gen == s_gen) {
|
|
pv_validate(src, value);
|
|
s_gpr[rt] = *src;
|
|
} else {
|
|
pv_reset(&s_gpr[rt], value);
|
|
}
|
|
return;
|
|
}
|
|
case 0x04: { /* MTC2: gte[rd] <- gpr[rt] */
|
|
uint32_t rt = f_rt(instr);
|
|
PGXPValue *dst = &s_gte[f_rd(instr)];
|
|
if (rt != 0 && s_gpr[rt].gen == s_gen) {
|
|
pv_validate(&s_gpr[rt], value);
|
|
*dst = s_gpr[rt];
|
|
} else {
|
|
pv_reset(dst, value);
|
|
}
|
|
/* An SXYP write (rd==15) shifts the hardware FIFO; we do not
|
|
* model it — regs 12..14 now describe words they no longer
|
|
* match, and validation drops them on next use. */
|
|
return;
|
|
}
|
|
case 0x02: { /* CFC2: control regs carry no
|
|
positions */
|
|
uint32_t rt = f_rt(instr);
|
|
if (rt != 0) pv_reset(&s_gpr[rt], value);
|
|
return;
|
|
}
|
|
default: /* CTC2: nothing shadowed */
|
|
return;
|
|
}
|
|
}
|
|
default:
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* CPU-mode hooks: arithmetic (tier 2, default off) */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/* One 16-bit operand component in 16.16: a tracked half contributes its
|
|
* sub-pixel value; an untracked half contributes its exact integer value
|
|
* (adding an exact offset to a tracked coordinate keeps the fraction). */
|
|
static inline int32_t comp16(const PGXPValue *pv, uint32_t value, int hi_half) {
|
|
if (pv && pv->gen == s_gen) {
|
|
if (!hi_half && (pv->flags & PGXP_F_VX) &&
|
|
((pv->value ^ value) & 0x0000FFFFu) == 0)
|
|
return pv->x16;
|
|
if (hi_half && (pv->flags & PGXP_F_VY) &&
|
|
((pv->value ^ value) & 0xFFFF0000u) == 0)
|
|
return pv->y16;
|
|
}
|
|
return ((int32_t)(int16_t)(hi_half ? (value >> 16) : value)) << 16;
|
|
}
|
|
|
|
static inline int comp_tracked(const PGXPValue *pv, uint32_t value, int hi_half) {
|
|
if (!pv || pv->gen != s_gen) return 0;
|
|
if (!hi_half)
|
|
return (pv->flags & PGXP_F_VX) &&
|
|
((pv->value ^ value) & 0x0000FFFFu) == 0;
|
|
return (pv->flags & PGXP_F_VY) &&
|
|
((pv->value ^ value) & 0xFFFF0000u) == 0;
|
|
}
|
|
|
|
/* Half-wise add/sub is only meaningful when the guest result shows no carry
|
|
* crossed the half boundary; otherwise the halves did not combine
|
|
* independently and the shadow must not pretend they did. */
|
|
static inline int halves_independent(uint32_t a, uint32_t b, uint32_t r, int sub) {
|
|
uint32_t lo = sub ? ((a & 0xFFFFu) - (b & 0xFFFFu))
|
|
: ((a & 0xFFFFu) + (b & 0xFFFFu));
|
|
return ((lo ^ r) & 0xFFFFu) == 0 &&
|
|
((((sub ? a - b : a + b)) ^ r) == 0) &&
|
|
((lo >> 16) == 0); /* no carry/borrow out of low */
|
|
}
|
|
|
|
extern "C" void psx_pgxp_alu(struct CPUState *cpu, uint32_t instr,
|
|
uint32_t result, uint32_t s1, uint32_t s2) {
|
|
(void)cpu;
|
|
if (!g_pgxp_active) return;
|
|
|
|
uint32_t op = f_op(instr);
|
|
uint32_t dst_reg;
|
|
PGXPValue *a = nullptr, *b = nullptr; /* source shadows (may be 0) */
|
|
|
|
if (op == 0) { /* SPECIAL */
|
|
dst_reg = f_rd(instr);
|
|
uint32_t rs = f_rs(instr), rt = f_rt(instr);
|
|
uint32_t funct = f_funct(instr);
|
|
if (funct == 0x11 || funct == 0x13) { /* MTHI / MTLO */
|
|
PGXPValue *hl = &s_gpr[funct == 0x11 ? PGXP_REG_HI : PGXP_REG_LO];
|
|
if (s_cpu_mode && rs != 0 && s_gpr[rs].gen == s_gen) {
|
|
pv_validate(&s_gpr[rs], result);
|
|
*hl = s_gpr[rs];
|
|
} else pv_reset(hl, result);
|
|
return;
|
|
}
|
|
if (dst_reg == 0) return;
|
|
PGXPValue *dst = &s_gpr[dst_reg];
|
|
|
|
switch (funct) {
|
|
case 0x10: /* MFHI */
|
|
if (s_cpu_mode && s_gpr[PGXP_REG_HI].gen == s_gen) {
|
|
pv_validate(&s_gpr[PGXP_REG_HI], result);
|
|
*dst = s_gpr[PGXP_REG_HI];
|
|
} else pv_reset(dst, result);
|
|
return;
|
|
case 0x12: /* MFLO */
|
|
if (s_cpu_mode && s_gpr[PGXP_REG_LO].gen == s_gen) {
|
|
pv_validate(&s_gpr[PGXP_REG_LO], result);
|
|
*dst = s_gpr[PGXP_REG_LO];
|
|
} else pv_reset(dst, result);
|
|
return;
|
|
case 0x00: case 0x02: case 0x03: /* SLL / SRL / SRA */
|
|
case 0x04: case 0x06: case 0x07: { /* SLLV / SRLV / SRAV */
|
|
uint32_t sh = (funct < 4) ? f_shamt(instr) : (s2 & 31u);
|
|
uint32_t src_reg = rt;
|
|
PGXPValue *src = (src_reg != 0) ? &s_gpr[src_reg] : nullptr;
|
|
int right = (funct & 3u) != 0;
|
|
if (!s_cpu_mode || sh != 16 || !src || src->gen != s_gen) {
|
|
pv_reset(dst, result);
|
|
return;
|
|
}
|
|
pv_validate(src, s1);
|
|
pv_reset(dst, result);
|
|
if (!right) { /* << 16: low comp -> Y */
|
|
if (src->flags & PGXP_F_VX) {
|
|
dst->y16 = src->x16;
|
|
dst->flags |= PGXP_F_VY;
|
|
}
|
|
dst->x16 = 0;
|
|
dst->flags |= PGXP_F_VX; /* low half exactly zero */
|
|
} else { /* >> 16: high comp -> X */
|
|
if (src->flags & PGXP_F_VY) {
|
|
dst->x16 = src->y16;
|
|
dst->flags |= PGXP_F_VX;
|
|
}
|
|
dst->y16 = ((int32_t)result >> 16) << 16; /* 0 or sign fill */
|
|
dst->flags |= PGXP_F_VY;
|
|
}
|
|
return;
|
|
}
|
|
case 0x20: case 0x21: /* ADD / ADDU */
|
|
case 0x22: case 0x23: /* SUB / SUBU */
|
|
case 0x25: { /* OR */
|
|
/* MOVE idioms first — they matter even without full cpu-mode
|
|
* arithmetic, and they are exact. */
|
|
int is_or = (funct == 0x25);
|
|
int is_sub = (funct == 0x22 || funct == 0x23);
|
|
if ((is_or || !is_sub) && rt == 0) { /* op rd, rs, $zero */
|
|
if (rs != 0 && s_gpr[rs].gen == s_gen) {
|
|
pv_validate(&s_gpr[rs], result);
|
|
*dst = s_gpr[rs];
|
|
} else pv_reset(dst, result);
|
|
return;
|
|
}
|
|
if ((is_or || !is_sub) && rs == 0) { /* op rd, $zero, rt */
|
|
if (rt != 0 && s_gpr[rt].gen == s_gen) {
|
|
pv_validate(&s_gpr[rt], result);
|
|
*dst = s_gpr[rt];
|
|
} else pv_reset(dst, result);
|
|
return;
|
|
}
|
|
if (!s_cpu_mode) { pv_reset(dst, result); return; }
|
|
a = (rs != 0) ? &s_gpr[rs] : nullptr;
|
|
b = (rt != 0) ? &s_gpr[rt] : nullptr;
|
|
if (is_or) {
|
|
/* Merge disjoint halves (the lhu/sll/or repack pattern). */
|
|
if ((s1 & s2) != 0) { pv_reset(dst, result); return; }
|
|
pv_reset(dst, result);
|
|
const PGXPValue *xsrc; uint32_t xval;
|
|
if ((s1 & 0xFFFFu) != 0) { xsrc = a; xval = s1; }
|
|
else { xsrc = b; xval = s2; }
|
|
const PGXPValue *ysrc; uint32_t yval;
|
|
if ((s1 & 0xFFFF0000u) != 0) { ysrc = a; yval = s1; }
|
|
else { ysrc = b; yval = s2; }
|
|
if (comp_tracked(xsrc, xval, 0)) {
|
|
dst->x16 = xsrc->x16; dst->flags |= PGXP_F_VX;
|
|
} else if ((result & 0xFFFFu) == 0) {
|
|
dst->x16 = 0; dst->flags |= PGXP_F_VX;
|
|
}
|
|
if (comp_tracked(ysrc, yval, 1)) {
|
|
dst->y16 = ysrc->y16; dst->flags |= PGXP_F_VY;
|
|
} else if ((result & 0xFFFF0000u) == 0) {
|
|
dst->y16 = 0; dst->flags |= PGXP_F_VY;
|
|
}
|
|
return;
|
|
}
|
|
/* add/sub: require at least one tracked component and halves
|
|
* that combined independently (no cross-half carry). */
|
|
if (!halves_independent(s1, s2, result, is_sub) ||
|
|
(!comp_tracked(a, s1, 0) && !comp_tracked(a, s1, 1) &&
|
|
!comp_tracked(b, s2, 0) && !comp_tracked(b, s2, 1))) {
|
|
pv_reset(dst, result);
|
|
return;
|
|
}
|
|
pv_reset(dst, result);
|
|
dst->x16 = is_sub ? comp16(a, s1, 0) - comp16(b, s2, 0)
|
|
: comp16(a, s1, 0) + comp16(b, s2, 0);
|
|
dst->y16 = is_sub ? comp16(a, s1, 1) - comp16(b, s2, 1)
|
|
: comp16(a, s1, 1) + comp16(b, s2, 1);
|
|
dst->flags = PGXP_F_VXY;
|
|
return;
|
|
}
|
|
default:
|
|
pv_reset(dst, result);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* immediates */
|
|
dst_reg = f_rt(instr);
|
|
if (dst_reg == 0) return;
|
|
PGXPValue *dst = &s_gpr[dst_reg];
|
|
uint32_t rs = f_rs(instr);
|
|
|
|
switch (op) {
|
|
case 0x0F: { /* LUI: both halves exact */
|
|
pv_reset(dst, result);
|
|
dst->x16 = 0;
|
|
dst->y16 = ((int32_t)result >> 16) << 16;
|
|
dst->flags = PGXP_F_VXY;
|
|
return;
|
|
}
|
|
case 0x08: case 0x09: { /* ADDI / ADDIU */
|
|
int32_t imm = f_simm(instr);
|
|
if (imm == 0) { /* MOVE idiom */
|
|
if (rs != 0 && s_gpr[rs].gen == s_gen) {
|
|
pv_validate(&s_gpr[rs], result);
|
|
*dst = s_gpr[rs];
|
|
} else pv_reset(dst, result);
|
|
return;
|
|
}
|
|
a = (rs != 0) ? &s_gpr[rs] : nullptr;
|
|
/* A negative immediate is a subtraction of its magnitude: checking
|
|
* carry on the sign-extended form would reject nearly every -N. */
|
|
{
|
|
int neg = imm < 0;
|
|
uint32_t mag = (uint32_t)(neg ? -imm : imm);
|
|
if (!s_cpu_mode ||
|
|
!halves_independent(s1, mag, result, neg) ||
|
|
(!comp_tracked(a, s1, 0) && !comp_tracked(a, s1, 1))) {
|
|
pv_reset(dst, result);
|
|
return;
|
|
}
|
|
int32_t dx = (int32_t)(mag & 0xFFFFu) << 16;
|
|
pv_reset(dst, result);
|
|
dst->x16 = comp16(a, s1, 0) + (neg ? -dx : dx);
|
|
dst->y16 = comp16(a, s1, 1); /* no carry crossed: Y untouched */
|
|
dst->flags = PGXP_F_VXY;
|
|
}
|
|
return;
|
|
}
|
|
case 0x0D: { /* ORI */
|
|
uint32_t imm = instr & 0xFFFFu;
|
|
if (imm == 0 || (s_cpu_mode && (s1 & 0xFFFFu) == 0)) {
|
|
a = (rs != 0) ? &s_gpr[rs] : nullptr;
|
|
pv_reset(dst, result);
|
|
if (comp_tracked(a, s1, 1)) {
|
|
dst->y16 = a->y16; dst->flags |= PGXP_F_VY;
|
|
}
|
|
if (imm == 0) {
|
|
if (comp_tracked(a, s1, 0)) {
|
|
dst->x16 = a->x16; dst->flags |= PGXP_F_VX;
|
|
}
|
|
} else { /* exact constant low half */
|
|
dst->x16 = ((int32_t)(int16_t)imm) << 16;
|
|
dst->flags |= PGXP_F_VX;
|
|
}
|
|
return;
|
|
}
|
|
pv_reset(dst, result);
|
|
return;
|
|
}
|
|
default: /* ANDI/XORI/SLTI/... */
|
|
pv_reset(dst, result);
|
|
return;
|
|
}
|
|
}
|
|
|
|
extern "C" void psx_pgxp_muldiv(struct CPUState *cpu, uint32_t instr,
|
|
uint32_t hi, uint32_t lo,
|
|
uint32_t s1, uint32_t s2) {
|
|
(void)cpu; (void)instr; (void)s1; (void)s2;
|
|
if (!g_pgxp_active) return;
|
|
/* Products/quotients of screen coordinates are not screen coordinates:
|
|
* record the results as known-but-imprecise so MFHI/MFLO stay honest. */
|
|
pv_reset(&s_gpr[PGXP_REG_HI], hi);
|
|
pv_reset(&s_gpr[PGXP_REG_LO], lo);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* GTE producer */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
extern "C" void pgxp_gte_push_sxy(int32_t x16, int32_t y16, uint16_t sz3,
|
|
uint32_t packed) {
|
|
if (!g_pgxp_active) return;
|
|
s_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;
|
|
}
|
|
|
|
extern "C" void pgxp_gte_reg_written(int reg, uint32_t value) {
|
|
/* Invalidation-class bookkeeping: runs even with the engine disarmed so
|
|
* seeded/leftover shadows can never outlive a guest register write. Only
|
|
* the suppression bracket skips it (the speculative pass rolls the
|
|
* machine state back, so its writes must not stick to the shadows). */
|
|
if (s_suppress != 0) return;
|
|
if (reg < 0 || reg > 31) return;
|
|
pv_kill(&s_gte[reg]);
|
|
pv_reset(&s_gte[reg], value);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* GPU consumer */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
extern "C" int pgxp_get_precise_vertex(uint32_t addr, uint32_t packet_word,
|
|
int32_t int_x, int32_t int_y,
|
|
int32_t *x16, int32_t *y16,
|
|
uint16_t *sz) {
|
|
s_stats.lookups++;
|
|
|
|
int32_t px = 0, py = 0;
|
|
uint16_t pz = 0;
|
|
int have = 0;
|
|
|
|
if (s_enabled && addr != 0xFFFFFFFFu) {
|
|
PGXPValue *pv = pgxp_ptr(addr);
|
|
if (pv && pv->gen == s_gen && (pv->flags & PGXP_F_VXY) != 0) {
|
|
if (pv->value == packet_word &&
|
|
(pv->flags & PGXP_F_VXY) == PGXP_F_VXY) {
|
|
px = pv->x16;
|
|
py = pv->y16;
|
|
pz = (pv->flags & PGXP_F_VZ) ? pv->z : 0;
|
|
have = PGXP_SRC_DATAFLOW;
|
|
} else {
|
|
s_stats.value_mismatch++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!have && gte_geometry_correction_lookup(packet_word, &px, &py)) {
|
|
pz = 0; /* fallback never carries depth */
|
|
have = PGXP_SRC_FALLBACK;
|
|
}
|
|
|
|
if (have) {
|
|
/* Truncation agreement: the GPU parsed 11-bit integers out of the
|
|
* packet; a precise position whose integer part disagrees (a
|
|
* wrapped/CPU-modified coordinate) must not be believed. */
|
|
if ((px >> 16) != int_x || (py >> 16) != int_y) {
|
|
s_stats.trunc_reject++;
|
|
have = 0;
|
|
} else if (s_tolerance >= 0.0f) {
|
|
float dx = (float)(px - (int_x << 16)) * (1.0f / 65536.0f);
|
|
float dy = (float)(py - (int_y << 16)) * (1.0f / 65536.0f);
|
|
if (dx > s_tolerance || dy > s_tolerance) {
|
|
s_stats.tolerance_reject++;
|
|
have = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!have) {
|
|
s_stats.native++;
|
|
*x16 = int_x << 16;
|
|
*y16 = int_y << 16;
|
|
*sz = 0;
|
|
return PGXP_SRC_NATIVE;
|
|
}
|
|
|
|
if (have == PGXP_SRC_DATAFLOW) s_stats.dataflow_hit++;
|
|
else s_stats.fallback_hit++;
|
|
if (pz != 0) s_stats.w_valid++;
|
|
*x16 = px;
|
|
*y16 = py;
|
|
*sz = pz;
|
|
return have;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Legacy v14 SWC2 tracker — kept as the base-flavour feed */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/* Emitted at every swc2 since ABI v14 (all flavours, all backends, overlay
|
|
* DLLs). In the pgxp flavour psx_pgxp_cop2 supersedes it for the same swc2 —
|
|
* the double write is idempotent (same source shadow, same destination). */
|
|
extern "C" void pgxp_store_gte_reg(uint32_t addr, uint8_t reg) {
|
|
if (!g_pgxp_active) return;
|
|
PGXPValue *dst = pgxp_ptr(addr);
|
|
if (!dst) return;
|
|
const PGXPValue *src = &s_gte[reg & 31u];
|
|
if (src->gen != s_gen) return;
|
|
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;
|
|
}
|