mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
8630d3a6f8
With every drawn vertex precise, a title can still show cracks where the game
culled a face on its integer winding. R4's road emitter runs NCLIP on each
far row and skips rows whose native height rounds to 0. Natively their
neighbours meet at the same integer y. With PGXP they sit at their exact y,
and the skipped row becomes a strip of sky across the road beyond the start
gantry, at 4K and in 2P. tri_mixed cannot see this: the missing face draws
no triangle.
Precise culling (DuckStation's "PGXP culling", minimal footprint):
- While geometry correction is armed, gte_nclip computes the exact
determinant of the three SXY register shadows (pgxp_gte_nclip_precise:
live, current words, the GPU's own truncation agreement and tolerance).
When its sign is nonzero and differs from the integer MAC0's, MAC0 takes
that sign, with the rounded exact doubled area (at least 1) as magnitude.
FLAG keeps the integer overflow bits.
- It changes guest-visible MAC0, so it is a `culling` option of
psx.enhancement.pgxp, false in the builtin, with no [video] key, so netplay
(which clears every mod) never runs it. It needs geometry correction.
It is held off in every pass compared against another execution: the
speculative and replay passes, and the whole overlay shadow diff
(psx_overlay_shadow_diff_active), interpreter pass included.
- Counters whenever geometry correction is armed: nclip_precise,
nclip_disagree (exact sign differs: the culled-face crack exposure),
nclip_corrected. TCP pgxp {"culling":0|1}; env PSX_PGXP_CULLING.
Session arming in one place (src/pgxp_session.cpp, psx_pgxp_session_arm):
main.cpp's renderer setup now calls the same code the test drives. The arming
takes the mod request (reads and clears it), so a session with an empty plan
cannot inherit an earlier session's request even if the reset were skipped.
pgxp_session_test now links the real mod runtime, the builtin plugin and the
builtin manifest, and covers:
- a default-on override;
- a netplay clear, with and without the reset;
- the off switch;
- the options;
- env overrides;
- pgxp_mod_only.
[video] pgxp_mod_only (game.toml, default false): for a title that ships
PGXP through the mod, the mod is the one switch. The [video]
geometry_correction / perspective_texturing / pgxp_cpu_mode values are not
applied, from game.toml or the player's settings.toml. The launcher also
hides its Perspective textures row (has_geometry_precision = 0). That row
showed "off" while a default-on mod corrected textures, and it could keep
texture correction on after the player switched the mod off.
geom_correction also reports texture_correction and the culling switch.
R4, Helter Skelter: 28.7% of NCLIPs disagree in sign. With culling, the
ladder is gone in same-frame 4K captures (1P straight and crest, 2P VS), and
the game emits about 24% more triangles. Its packet buffers peak at 60%.
fp_identity over 12000 frames: stock, PGXP off, and PGXP on with culling off
are IDENTICAL; with culling on, the RAM writes differ from the attract race
on, while cyc/mmio/mc agree throughout.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
572 lines
25 KiB
C++
572 lines
25 KiB
C++
/* test_pgxp.cpp — PGXP value-propagation engine unit tests (docs/ENHANCEMENTS.md
|
|
* G1.2/G1.3). White-box over runtime/src/pgxp.cpp with the gte.cpp fallback
|
|
* cache stubbed, exercising exactly the properties the engine's safety rests
|
|
* on: provenance roundtrips, validate-on-read, half-word semantics, the
|
|
* repack arithmetic, the suppression bracket, and the GPU-side safeguards. */
|
|
|
|
#include "pgxp.h"
|
|
#include "pgxp_hooks.h"
|
|
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
|
|
static int g_failures = 0;
|
|
#define CHECK(cond) \
|
|
do { \
|
|
if (!(cond)) { \
|
|
std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, \
|
|
#cond); \
|
|
g_failures++; \
|
|
} \
|
|
} while (0)
|
|
|
|
/* ---- gte.cpp fallback-cache stub ----------------------------------------- */
|
|
|
|
static uint32_t g_fb_packed = 0;
|
|
static int32_t g_fb_x16 = 0, g_fb_y16 = 0;
|
|
static int g_fb_valid = 0;
|
|
|
|
extern "C" int gte_geometry_correction_lookup(uint32_t packed,
|
|
int32_t *x16, int32_t *y16) {
|
|
if (!g_fb_valid || packed != g_fb_packed) return 0;
|
|
if (x16) *x16 = g_fb_x16;
|
|
if (y16) *y16 = g_fb_y16;
|
|
return 1;
|
|
}
|
|
|
|
/* ---- MIPS encodings ------------------------------------------------------ */
|
|
|
|
static uint32_t enc_i(uint32_t op, uint32_t rs, uint32_t rt, uint16_t imm) {
|
|
return (op << 26) | (rs << 21) | (rt << 16) | imm;
|
|
}
|
|
static uint32_t enc_r(uint32_t rs, uint32_t rt, uint32_t rd, uint32_t sh,
|
|
uint32_t funct) {
|
|
return (rs << 21) | (rt << 16) | (rd << 11) | (sh << 6) | funct;
|
|
}
|
|
static uint32_t enc_cop2(uint32_t sub, uint32_t rt, uint32_t rd) {
|
|
return (0x12u << 26) | (sub << 21) | (rt << 16) | (rd << 11);
|
|
}
|
|
|
|
#define LW(rs, rt) enc_i(0x23, rs, rt, 0)
|
|
#define SW(rs, rt) enc_i(0x2B, rs, rt, 0)
|
|
#define LH(rs, rt) enc_i(0x21, rs, rt, 0)
|
|
#define LHU(rs, rt) enc_i(0x25, rs, rt, 0)
|
|
#define SH(rs, rt) enc_i(0x29, rs, rt, 0)
|
|
#define SB(rs, rt) enc_i(0x28, rs, rt, 0)
|
|
#define LWC2(rt) enc_i(0x32, 1, rt, 0)
|
|
#define SWC2(rt) enc_i(0x3A, 1, rt, 0)
|
|
#define MFC2(rt, rd) enc_cop2(0x00, rt, rd)
|
|
#define MTC2(rt, rd) enc_cop2(0x04, rt, rd)
|
|
#define ADDIU(rs, rt, imm) enc_i(0x09, rs, rt, (uint16_t)(imm))
|
|
#define LUI(rt, imm) enc_i(0x0F, 0, rt, (uint16_t)(imm))
|
|
#define SLL(rt, rd, sh) enc_r(0, rt, rd, sh, 0x00)
|
|
#define SRA(rt, rd, sh) enc_r(0, rt, rd, sh, 0x03)
|
|
#define OR(rs, rt, rd) enc_r(rs, rt, rd, 0, 0x25)
|
|
#define ADDU(rs, rt, rd) enc_r(rs, rt, rd, 0, 0x21)
|
|
|
|
/* One projected vertex: x = 160.5, y = 80.25 -> packed integer word. */
|
|
static const uint32_t PACKED = (80u << 16) | 160u;
|
|
static const int32_t X16 = (160 << 16) | 0x8000; /* 160.5 */
|
|
static const int32_t Y16 = (80 << 16) | 0x4000; /* 80.25 */
|
|
static const uint16_t SZ3 = 100;
|
|
|
|
static const uint32_t ADDR_A = 0x80100000u; /* packet slot A (KSEG0) */
|
|
static const uint32_t ADDR_B = 0x00100040u; /* packet slot B (KUSEG) */
|
|
|
|
static void produce_at(uint32_t addr) {
|
|
pgxp_gte_push_sxy(X16, Y16, SZ3, PACKED);
|
|
psx_pgxp_cop2(nullptr, SWC2(14), PACKED, addr);
|
|
}
|
|
|
|
static int lookup(uint32_t addr, uint32_t word, int32_t ix, int32_t iy,
|
|
int32_t *x, int32_t *y, uint16_t *z) {
|
|
int32_t lx, ly; uint16_t lz;
|
|
int r = pgxp_get_precise_vertex(addr, word, ix, iy, &lx, &ly, &lz);
|
|
if (x) *x = lx;
|
|
if (y) *y = ly;
|
|
if (z) *z = lz;
|
|
return r;
|
|
}
|
|
|
|
int main(void) {
|
|
pgxp_set_enabled(1);
|
|
pgxp_set_tolerance(-1.0f);
|
|
pgxp_set_cpu_mode(0);
|
|
|
|
/* --- SWC2 produce -> GPU consume (the perspective-texturing spine) --- */
|
|
produce_at(ADDR_A);
|
|
{
|
|
int32_t x, y; uint16_t z;
|
|
CHECK(lookup(ADDR_A, PACKED, 160, 80, &x, &y, &z) == PGXP_SRC_DATAFLOW);
|
|
CHECK(x == X16 && y == Y16 && z == SZ3);
|
|
/* mirrors resolve to the same shadow word */
|
|
CHECK(lookup(0xA0100000u, PACKED, 160, 80, &x, &y, &z) ==
|
|
PGXP_SRC_DATAFLOW);
|
|
}
|
|
|
|
/* --- DMA/untracked overwrite: value validation rejects the shadow --- */
|
|
{
|
|
int32_t x, y; uint16_t z;
|
|
uint32_t other = (81u << 16) | 161u;
|
|
CHECK(lookup(ADDR_A, other, 161, 81, &x, &y, &z) == PGXP_SRC_NATIVE);
|
|
CHECK(x == (161 << 16) && y == (81 << 16) && z == 0);
|
|
}
|
|
|
|
/* --- LW/SW roundtrip: packet copied by the CPU keeps provenance --- */
|
|
produce_at(ADDR_A);
|
|
psx_pgxp_load(nullptr, LW(1, 8), ADDR_A, PACKED);
|
|
psx_pgxp_store(nullptr, SW(1, 8), ADDR_B, PACKED);
|
|
{
|
|
int32_t x, y; uint16_t z;
|
|
CHECK(lookup(ADDR_B, PACKED, 160, 80, &x, &y, &z) == PGXP_SRC_DATAFLOW);
|
|
CHECK(x == X16 && y == Y16 && z == SZ3);
|
|
}
|
|
|
|
/* --- stale GPR: register changed between load and store --- */
|
|
psx_pgxp_load(nullptr, LW(1, 8), ADDR_A, PACKED);
|
|
psx_pgxp_store(nullptr, SW(1, 8), ADDR_B, 0xDEADBEEFu); /* r8 mutated */
|
|
CHECK(lookup(ADDR_B, 0xDEADBEEFu, 0, 0, nullptr, nullptr, nullptr) ==
|
|
PGXP_SRC_NATIVE);
|
|
|
|
/* --- MOVE idiom (memory mode, no cpu_mode needed) --- */
|
|
produce_at(ADDR_A);
|
|
psx_pgxp_load(nullptr, LW(1, 8), ADDR_A, PACKED);
|
|
psx_pgxp_alu(nullptr, ADDU(8, 0, 10), PACKED, PACKED, 0);
|
|
psx_pgxp_store(nullptr, SW(1, 10), ADDR_B, PACKED);
|
|
CHECK(lookup(ADDR_B, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
|
PGXP_SRC_DATAFLOW);
|
|
|
|
/* --- MFC2 -> SW (register transfer path) --- */
|
|
pgxp_gte_push_sxy(X16, Y16, SZ3, PACKED);
|
|
psx_pgxp_cop2(nullptr, MFC2(9, 14), PACKED, 0);
|
|
psx_pgxp_store(nullptr, SW(1, 9), ADDR_B, PACKED);
|
|
CHECK(lookup(ADDR_B, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
|
PGXP_SRC_DATAFLOW);
|
|
|
|
/* --- LH/SH: halves travel independently, depth does not survive --- */
|
|
produce_at(ADDR_A);
|
|
psx_pgxp_load(nullptr, LHU(1, 8), ADDR_A + 2u, PACKED >> 16); /* Y */
|
|
psx_pgxp_store(nullptr, SH(1, 8), ADDR_B + 2u, PACKED >> 16);
|
|
psx_pgxp_load(nullptr, LHU(1, 8), ADDR_A, PACKED & 0xFFFFu); /* X */
|
|
psx_pgxp_store(nullptr, SH(1, 8), ADDR_B, PACKED & 0xFFFFu);
|
|
{
|
|
int32_t x, y; uint16_t z;
|
|
CHECK(lookup(ADDR_B, PACKED, 160, 80, &x, &y, &z) == PGXP_SRC_DATAFLOW);
|
|
CHECK(x == X16 && y == Y16);
|
|
CHECK(z == 0); /* SH killed the vertex depth */
|
|
}
|
|
|
|
/* --- SB destroys the touched half only --- */
|
|
produce_at(ADDR_B);
|
|
psx_pgxp_store(nullptr, SB(1, 8), ADDR_B, PACKED & 0xFFu); /* same byte */
|
|
{
|
|
int32_t x, y; uint16_t z;
|
|
/* low half invalidated -> not a full XY hit anymore */
|
|
CHECK(lookup(ADDR_B, PACKED, 160, 80, &x, &y, &z) != PGXP_SRC_DATAFLOW);
|
|
}
|
|
|
|
/* --- cpu-mode repack: lhu / sll 16 / or (the classic vertex build) --- */
|
|
pgxp_set_cpu_mode(1);
|
|
produce_at(ADDR_A);
|
|
psx_pgxp_load(nullptr, LHU(1, 8), ADDR_A + 2u, PACKED >> 16); /* Y */
|
|
psx_pgxp_alu(nullptr, SLL(8, 9, 16), (PACKED >> 16) << 16,
|
|
PACKED >> 16, 16);
|
|
psx_pgxp_load(nullptr, LHU(1, 10), ADDR_A, PACKED & 0xFFFFu); /* X */
|
|
psx_pgxp_alu(nullptr, OR(9, 10, 11), PACKED,
|
|
(PACKED >> 16) << 16, PACKED & 0xFFFFu);
|
|
psx_pgxp_store(nullptr, SW(1, 11), ADDR_B, PACKED);
|
|
{
|
|
int32_t x, y;
|
|
CHECK(lookup(ADDR_B, PACKED, 160, 80, &x, &y, nullptr) ==
|
|
PGXP_SRC_DATAFLOW);
|
|
CHECK(x == X16 && y == Y16);
|
|
}
|
|
|
|
/* --- cpu-mode addiu: fraction rides an integer offset (incl. -N) --- */
|
|
psx_pgxp_load(nullptr, LW(1, 8), ADDR_A, PACKED);
|
|
psx_pgxp_alu(nullptr, ADDIU(8, 12, 4), PACKED + 4u, PACKED, 4u);
|
|
psx_pgxp_store(nullptr, SW(1, 12), ADDR_B, PACKED + 4u);
|
|
{
|
|
int32_t x, y;
|
|
CHECK(lookup(ADDR_B, PACKED + 4u, 164, 80, &x, &y, nullptr) ==
|
|
PGXP_SRC_DATAFLOW);
|
|
CHECK(x == X16 + (4 << 16) && y == Y16);
|
|
}
|
|
psx_pgxp_load(nullptr, LW(1, 8), ADDR_A, PACKED);
|
|
psx_pgxp_alu(nullptr, ADDIU(8, 12, (uint16_t)-4), PACKED - 4u, PACKED,
|
|
(uint32_t)(int32_t)-4);
|
|
psx_pgxp_store(nullptr, SW(1, 12), ADDR_B, PACKED - 4u);
|
|
{
|
|
int32_t x;
|
|
CHECK(lookup(ADDR_B, PACKED - 4u, 156, 80, &x, nullptr, nullptr) ==
|
|
PGXP_SRC_DATAFLOW);
|
|
CHECK(x == X16 - (4 << 16));
|
|
}
|
|
pgxp_set_cpu_mode(0);
|
|
|
|
/* --- cpu-mode OFF: the same repack must degrade to native, cleanly --- */
|
|
produce_at(ADDR_A);
|
|
psx_pgxp_load(nullptr, LHU(1, 8), ADDR_A + 2u, PACKED >> 16);
|
|
psx_pgxp_alu(nullptr, SLL(8, 9, 16), (PACKED >> 16) << 16,
|
|
PACKED >> 16, 16);
|
|
psx_pgxp_store(nullptr, SW(1, 9), ADDR_B, (PACKED >> 16) << 16);
|
|
CHECK(lookup(ADDR_B, (PACKED >> 16) << 16, 0, 80, nullptr, nullptr,
|
|
nullptr) == PGXP_SRC_NATIVE);
|
|
|
|
/* --- truncation agreement: integer part must match the native parse --- */
|
|
produce_at(ADDR_A);
|
|
CHECK(lookup(ADDR_A, PACKED, 161, 80, nullptr, nullptr, nullptr) ==
|
|
PGXP_SRC_NATIVE);
|
|
|
|
/* --- tolerance clamp --- */
|
|
produce_at(ADDR_A);
|
|
pgxp_set_tolerance(0.25f); /* fraction is 0.5 -> reject */
|
|
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
|
PGXP_SRC_NATIVE);
|
|
pgxp_set_tolerance(0.75f); /* 0.5 <= 0.75 -> accept */
|
|
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
|
PGXP_SRC_DATAFLOW);
|
|
pgxp_set_tolerance(-1.0f);
|
|
|
|
/* --- fallback tier: no address -> position cache, never a depth --- */
|
|
g_fb_valid = 1; g_fb_packed = PACKED; g_fb_x16 = X16; g_fb_y16 = Y16;
|
|
{
|
|
int32_t x, y; uint16_t z;
|
|
CHECK(lookup(0xFFFFFFFFu, PACKED, 160, 80, &x, &y, &z) ==
|
|
PGXP_SRC_FALLBACK);
|
|
CHECK(x == X16 && y == Y16 && z == 0);
|
|
}
|
|
g_fb_valid = 0;
|
|
|
|
/* --- dataflow only (G1.11): position fallback off skips the cache --- */
|
|
g_fb_valid = 1; g_fb_packed = PACKED; g_fb_x16 = X16; g_fb_y16 = Y16;
|
|
CHECK(pgxp_position_fallback() == 1); /* default: unchanged */
|
|
pgxp_set_position_fallback(0);
|
|
{
|
|
int32_t x, y; uint16_t z;
|
|
CHECK(lookup(0xFFFFFFFFu, PACKED, 160, 80, &x, &y, &z) ==
|
|
PGXP_SRC_NATIVE);
|
|
CHECK(x == (160 << 16) && y == (80 << 16) && z == 0);
|
|
/* a validated dataflow shadow is unaffected */
|
|
produce_at(ADDR_A);
|
|
CHECK(lookup(ADDR_A, PACKED, 160, 80, &x, &y, &z) == PGXP_SRC_DATAFLOW);
|
|
/* a stale shadow no longer falls through to the cache */
|
|
uint32_t other = (81u << 16) | 161u;
|
|
g_fb_packed = other;
|
|
g_fb_x16 = (161 << 16) | 0x8000;
|
|
g_fb_y16 = (81 << 16) | 0x4000;
|
|
CHECK(lookup(ADDR_A, other, 161, 81, &x, &y, &z) == PGXP_SRC_NATIVE);
|
|
pgxp_set_position_fallback(1);
|
|
CHECK(lookup(ADDR_A, other, 161, 81, &x, &y, &z) == PGXP_SRC_FALLBACK);
|
|
}
|
|
g_fb_valid = 0;
|
|
|
|
/* --- preserve projection (G1.11): the agreement window ---------------- */
|
|
{
|
|
CHECK(pgxp_preserve_projection() == 0); /* default: unchanged */
|
|
auto seed = [](int32_t x16, int32_t y16, uint32_t packed) {
|
|
pgxp_gte_push_sxy(x16, y16, SZ3, packed);
|
|
psx_pgxp_cop2(nullptr, SWC2(14), packed, ADDR_A);
|
|
};
|
|
const int32_t half = 1 << 15;
|
|
/* exact projection half a pixel BELOW the guest integer: the IR path
|
|
* can never produce that, so off rejects it and on accepts it */
|
|
seed((160 << 16) - half, Y16, PACKED);
|
|
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
|
PGXP_SRC_NATIVE);
|
|
pgxp_set_preserve_projection(1);
|
|
{
|
|
int32_t x, y;
|
|
CHECK(lookup(ADDR_A, PACKED, 160, 80, &x, &y, nullptr) ==
|
|
PGXP_SRC_DATAFLOW);
|
|
CHECK(x == (160 << 16) - half && y == Y16);
|
|
}
|
|
/* window edges: (-BELOW, +ABOVE) px exclusive */
|
|
const int32_t hi = PGXP_PPP_AGREE_ABOVE, lo = PGXP_PPP_AGREE_BELOW;
|
|
seed(((160 + hi) << 16) - 1, Y16, PACKED);
|
|
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
|
PGXP_SRC_DATAFLOW);
|
|
seed((160 + hi) << 16, Y16, PACKED);
|
|
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
|
PGXP_SRC_NATIVE);
|
|
seed(((160 - lo) << 16) + 1, Y16, PACKED);
|
|
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
|
PGXP_SRC_DATAFLOW);
|
|
seed((160 - lo) << 16, Y16, PACKED);
|
|
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
|
PGXP_SRC_NATIVE);
|
|
seed(X16, (80 + hi) << 16, PACKED); /* Y axis too */
|
|
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
|
PGXP_SRC_NATIVE);
|
|
/* a half beyond the GTE range, which the GPU's 11-bit parse wraps
|
|
* (a CPU-modified word), needs exact agreement: 0x0402 parses as
|
|
* -1022, a shadow at 1026.5 lies 2048 px away and one at -1021.5
|
|
* truncates to -1022 */
|
|
const uint32_t wrap = (80u << 16) | 0x0402u;
|
|
seed((1026 << 16) + half, Y16, wrap);
|
|
CHECK(lookup(ADDR_A, wrap, -1022, 80, nullptr, nullptr, nullptr) ==
|
|
PGXP_SRC_NATIVE);
|
|
seed(-(1022 << 16) + half, Y16, wrap);
|
|
CHECK(lookup(ADDR_A, wrap, -1022, 80, nullptr, nullptr, nullptr) ==
|
|
PGXP_SRC_DATAFLOW);
|
|
seed(-(1022 << 16) - half, Y16, wrap); /* in the window, not exact */
|
|
CHECK(lookup(ADDR_A, wrap, -1022, 80, nullptr, nullptr, nullptr) ==
|
|
PGXP_SRC_NATIVE);
|
|
/* an 11-bit wrapped coordinate is far outside any window */
|
|
seed(X16 + (2048 << 16), Y16, PACKED);
|
|
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
|
PGXP_SRC_NATIVE);
|
|
/* at the GTE saturation limit the guest integer is a clamp: the
|
|
* window does not apply, agreement must be exact */
|
|
const uint32_t sat = (80u << 16) | 0x3FFu; /* SX saturated at 1023 */
|
|
seed((1023 << 16) + half, Y16, sat);
|
|
CHECK(lookup(ADDR_A, sat, 1023, 80, nullptr, nullptr, nullptr) ==
|
|
PGXP_SRC_DATAFLOW); /* exact agreement ok */
|
|
seed((1024 << 16) + half, Y16, sat);
|
|
CHECK(lookup(ADDR_A, sat, 1023, 80, nullptr, nullptr, nullptr) ==
|
|
PGXP_SRC_NATIVE);
|
|
const uint32_t sat_lo = (0xFC00u << 16) | 160u; /* SY at -1024 */
|
|
seed(X16, -(1024 << 16) - half, sat_lo);
|
|
CHECK(lookup(ADDR_A, sat_lo, 160, -1024, nullptr, nullptr, nullptr) ==
|
|
PGXP_SRC_NATIVE);
|
|
/* the tolerance clamp measures the offset both ways */
|
|
seed((160 << 16) - half, Y16, PACKED);
|
|
pgxp_set_tolerance(0.25f);
|
|
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
|
PGXP_SRC_NATIVE);
|
|
pgxp_set_tolerance(0.75f);
|
|
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
|
PGXP_SRC_DATAFLOW);
|
|
pgxp_set_tolerance(-1.0f);
|
|
pgxp_set_preserve_projection(0);
|
|
}
|
|
|
|
/* --- pgxp_project_precise: the exact projection and its qualifiers --- */
|
|
{
|
|
/* V = (100, -50, 1000) through the identity: MAC = V * 4096 (sf=1),
|
|
* IR = V, SZ3 = 1000, H = 300, OFX/OFY = (160, 120) in 16.16. The
|
|
* exact screen position is (160 + 100 * 0.3, 120 - 50 * 0.3). */
|
|
const int64_t m1 = 100 * 4096 + 1234; /* fractional MAC bits */
|
|
const int64_t m2 = -50 * 4096 + 777;
|
|
const int64_t m3 = 1000 * 4096 + 2048;
|
|
const int32_t ofx = 160 << 16, ofy = 120 << 16;
|
|
int32_t x, y;
|
|
CHECK(pgxp_project_precise(m1, m2, m3, 12, (int32_t)(m1 >> 12),
|
|
(int32_t)(m2 >> 12), (uint32_t)(m3 >> 12),
|
|
300, ofx, ofy, 1, 1, &x, &y) == 1);
|
|
const double ex = 160.0 + (double)m1 * 300.0 / (double)m3;
|
|
const double ey = 120.0 + (double)m2 * 300.0 / (double)m3;
|
|
CHECK(x == (int32_t)std::floor(ex * 65536.0));
|
|
CHECK(y == (int32_t)std::floor(ey * 65536.0));
|
|
/* the horizontal widescreen factor is applied to X only */
|
|
int32_t xs, ys;
|
|
CHECK(pgxp_project_precise(m1, m2, m3, 12, (int32_t)(m1 >> 12),
|
|
(int32_t)(m2 >> 12), (uint32_t)(m3 >> 12),
|
|
300, ofx, ofy, 3, 4, &xs, &ys) == 1);
|
|
CHECK(ys == y);
|
|
CHECK(xs == (int32_t)std::floor((160.0 + (double)m1 * 300.0 /
|
|
(double)m3 * 0.75) * 65536.0));
|
|
/* disqualified: sf=0, a clamped IR, SZ3 0 or clamped, divide
|
|
* overflow (H >= 2*SZ3) -- the caller keeps the IR path */
|
|
CHECK(pgxp_project_precise(m1, m2, m3, 0, (int32_t)(m1 >> 12),
|
|
(int32_t)(m2 >> 12), 1000, 300, ofx, ofy,
|
|
1, 1, &x, &y) == 0);
|
|
CHECK(pgxp_project_precise(m1, m2, m3, 12, 0x7FFF, (int32_t)(m2 >> 12),
|
|
1000, 300, ofx, ofy, 1, 1, &x, &y) == 0);
|
|
CHECK(pgxp_project_precise(m1, m2, 2048, 12, (int32_t)(m1 >> 12),
|
|
(int32_t)(m2 >> 12), 0, 300, ofx, ofy, 1, 1,
|
|
&x, &y) == 0);
|
|
CHECK(pgxp_project_precise(m1, m2, (int64_t)0x12345 << 12, 12,
|
|
(int32_t)(m1 >> 12), (int32_t)(m2 >> 12),
|
|
0xFFFF, 300, ofx, ofy, 1, 1, &x, &y) == 0);
|
|
CHECK(pgxp_project_precise(m1, m2, 150 * 4096, 12, (int32_t)(m1 >> 12),
|
|
(int32_t)(m2 >> 12), 150, 300, ofx, ofy, 1, 1,
|
|
&x, &y) == 0);
|
|
CHECK(pgxp_project_precise(m1, m2, 151 * 4096, 12, (int32_t)(m1 >> 12),
|
|
(int32_t)(m2 >> 12), 151, 300, ofx, ofy, 1, 1,
|
|
&x, &y) == 1);
|
|
}
|
|
|
|
/* --- probe: the same decision, not counted --- */
|
|
{
|
|
produce_at(ADDR_A);
|
|
PGXPStats a, b;
|
|
pgxp_get_stats(&a);
|
|
CHECK(pgxp_probe_precise_vertex(ADDR_A, PACKED, 160, 80) ==
|
|
PGXP_SRC_DATAFLOW);
|
|
CHECK(pgxp_probe_precise_vertex(ADDR_A, PACKED, 161, 80) ==
|
|
PGXP_SRC_NATIVE);
|
|
pgxp_get_stats(&b);
|
|
CHECK(std::memcmp(&a, &b, sizeof a) == 0);
|
|
pgxp_note_rect_bypass(1);
|
|
pgxp_note_rect_bypass(0);
|
|
pgxp_note_rect_bypass(0);
|
|
pgxp_get_stats(&b);
|
|
CHECK(b.rect_bypass == a.rect_bypass + 1);
|
|
CHECK(b.rect_partial == a.rect_partial + 2);
|
|
}
|
|
|
|
/* --- preserve projection: the RTPS-side window check (G1.11) ---------- */
|
|
{
|
|
pgxp_set_preserve_projection(1);
|
|
PGXPStats a, b;
|
|
pgxp_get_stats(&a);
|
|
const int32_t half = 1 << 15;
|
|
/* inside the window: the exact projection is the shadow */
|
|
CHECK(pgxp_ppp_accept((160 << 16) - half, Y16, PACKED) == 1);
|
|
CHECK(pgxp_ppp_accept(((160 + PGXP_PPP_AGREE_ABOVE) << 16) - 1, Y16,
|
|
PACKED) == 1);
|
|
/* outside it (either axis): RTPS keeps the IR path's shadow */
|
|
CHECK(pgxp_ppp_accept((160 + PGXP_PPP_AGREE_ABOVE) << 16, Y16,
|
|
PACKED) == 0);
|
|
CHECK(pgxp_ppp_accept(X16, (80 - PGXP_PPP_AGREE_BELOW) << 16,
|
|
PACKED) == 0);
|
|
/* at the saturation limit agreement is exact */
|
|
const uint32_t sat = (80u << 16) | 0x3FFu;
|
|
CHECK(pgxp_ppp_accept((1023 << 16) + half, Y16, sat) == 1);
|
|
CHECK(pgxp_ppp_accept((1024 << 16) + half, Y16, sat) == 0);
|
|
pgxp_get_stats(&b);
|
|
CHECK(b.ppp_produced == a.ppp_produced + 3);
|
|
CHECK(b.ppp_window_fallback == a.ppp_window_fallback + 3);
|
|
pgxp_set_preserve_projection(0);
|
|
}
|
|
|
|
/* --- NCLIP's exact determinant (G1.12) -------------------------------- */
|
|
{
|
|
/* A far road row: native y 113, 113, 113 (zero area), exact y
|
|
* 112.64 / 113.12 / 113.83 -> positive area. */
|
|
const uint32_t w0 = (113u << 16) | 100u;
|
|
const uint32_t w1 = (113u << 16) | 220u;
|
|
const uint32_t w2 = (113u << 16) | 160u;
|
|
const int32_t y0 = (112 << 16) + (int32_t)(0.64 * 65536);
|
|
const int32_t y1 = (113 << 16) + (int32_t)(0.12 * 65536);
|
|
const int32_t y2 = (113 << 16) + (int32_t)(0.83 * 65536);
|
|
auto seed3 = [&](void) {
|
|
pgxp_test_seed_gte_sxy(0, w0, 100 << 16, y0, SZ3, 1);
|
|
pgxp_test_seed_gte_sxy(1, w1, 220 << 16, y1, SZ3, 1);
|
|
pgxp_test_seed_gte_sxy(2, w2, 160 << 16, y2, SZ3, 1);
|
|
};
|
|
const uint32_t words[3] = { w0, w1, w2 };
|
|
auto expect_cross = [&](void) {
|
|
return ((int64_t)(220 << 16) - (100 << 16)) * ((int64_t)y2 - y0) -
|
|
((int64_t)y1 - y0) * ((int64_t)(160 << 16) - (100 << 16));
|
|
};
|
|
PGXPStats a, b;
|
|
pgxp_get_stats(&a);
|
|
int64_t cross = 0;
|
|
/* IR path: y0 = 112.64 does not truncate to 113 -> not believed */
|
|
seed3();
|
|
CHECK(pgxp_gte_nclip_precise(words, &cross) == 0);
|
|
/* preserve-projection window: believed, exact determinant */
|
|
pgxp_set_preserve_projection(1);
|
|
seed3();
|
|
CHECK(pgxp_gte_nclip_precise(words, &cross) == 1);
|
|
CHECK(cross == expect_cross());
|
|
CHECK(cross > 0);
|
|
/* a stale shadow (word changed under it) fails closed */
|
|
const uint32_t stale[3] = { w0, w1, w2 ^ 1u };
|
|
CHECK(pgxp_gte_nclip_precise(stale, &cross) == 0);
|
|
/* the tolerance clamp applies as at the GPU */
|
|
pgxp_set_tolerance(0.25f);
|
|
CHECK(pgxp_gte_nclip_precise(words, &cross) == 0);
|
|
pgxp_set_tolerance(-1.0f);
|
|
/* nothing inside a suppression bracket */
|
|
pgxp_suppress_begin();
|
|
CHECK(pgxp_gte_nclip_precise(words, &cross) == 0);
|
|
pgxp_suppress_end();
|
|
pgxp_get_stats(&b);
|
|
CHECK(b.nclip_precise == a.nclip_precise + 1);
|
|
pgxp_note_nclip(1, 0);
|
|
pgxp_note_nclip(1, 1);
|
|
pgxp_get_stats(&b);
|
|
CHECK(b.nclip_disagree == a.nclip_disagree + 2);
|
|
CHECK(b.nclip_corrected == a.nclip_corrected + 1);
|
|
pgxp_set_preserve_projection(0);
|
|
}
|
|
|
|
/* --- the mod request lives until the session's arming takes it -------- */
|
|
{
|
|
CHECK(pgxp_culling() == 0); /* default off */
|
|
pgxp_set_culling(1);
|
|
CHECK(pgxp_culling() == 1);
|
|
pgxp_set_culling(0);
|
|
int cpu = -1, cull = -1;
|
|
pgxp_mod_request(1, 1, 1);
|
|
CHECK(pgxp_mod_requested(&cpu, &cull) == 1 && cpu == 1 && cull == 1);
|
|
CHECK(pgxp_mod_requested(nullptr, nullptr) == 1); /* peek keeps it */
|
|
CHECK(pgxp_mod_request_take(&cpu, &cull) == 1 && cpu == 1 && cull == 1);
|
|
CHECK(pgxp_mod_request_take(&cpu, &cull) == 0 && cpu == 0 && cull == 0);
|
|
pgxp_mod_request(0, 1, 1); /* options need the mod */
|
|
CHECK(pgxp_mod_requested(&cpu, &cull) == 0 && cpu == 0 && cull == 0);
|
|
}
|
|
|
|
/* --- triangle census (G1.1 crack exposure) --- */
|
|
{
|
|
PGXPStats a, b;
|
|
pgxp_get_stats(&a);
|
|
pgxp_note_triangle(3);
|
|
pgxp_note_triangle(2);
|
|
pgxp_note_triangle(1);
|
|
pgxp_note_triangle(0);
|
|
pgxp_get_stats(&b);
|
|
CHECK(b.tri_precise == a.tri_precise + 1);
|
|
CHECK(b.tri_mixed == a.tri_mixed + 2);
|
|
CHECK(b.tri_native == a.tri_native + 1);
|
|
}
|
|
|
|
/* --- suppression bracket: nothing records inside it --- */
|
|
pgxp_invalidate_all();
|
|
pgxp_suppress_begin();
|
|
produce_at(ADDR_A);
|
|
pgxp_suppress_end();
|
|
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
|
PGXP_SRC_NATIVE);
|
|
|
|
/* --- deferred invalidate inside the bracket --- */
|
|
produce_at(ADDR_A);
|
|
pgxp_suppress_begin();
|
|
pgxp_invalidate_all(); /* deferred */
|
|
pgxp_suppress_end(); /* applies here */
|
|
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
|
PGXP_SRC_NATIVE);
|
|
|
|
/* --- invalidate-all + generation wrap --- */
|
|
produce_at(ADDR_A);
|
|
pgxp_invalidate_all();
|
|
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
|
PGXP_SRC_NATIVE);
|
|
pgxp_test_set_generation(0xFFFFFFFFu);
|
|
pgxp_invalidate_all();
|
|
CHECK(pgxp_test_generation() == 1u);
|
|
|
|
/* --- test accessors mirror the SXY FIFO shadows --- */
|
|
pgxp_test_seed_gte_sxy(2, PACKED, X16, Y16, SZ3, 1);
|
|
{
|
|
uint32_t packed; int32_t x, y; uint16_t z; uint8_t valid;
|
|
pgxp_test_get_gte_sxy(2, &packed, &x, &y, &z, &valid);
|
|
CHECK(valid && packed == PACKED && x == X16 && y == Y16 && z == SZ3);
|
|
pgxp_test_seed_gte_sxy(2, 0, 0, 0, 0, 0);
|
|
pgxp_test_get_gte_sxy(2, &packed, &x, &y, &z, &valid);
|
|
CHECK(!valid);
|
|
}
|
|
|
|
/* --- stats sanity: dataflow hits were counted --- */
|
|
{
|
|
PGXPStats st;
|
|
pgxp_get_stats(&st);
|
|
CHECK(st.lookups > 0);
|
|
CHECK(st.dataflow_hit > 0);
|
|
CHECK(st.native > 0);
|
|
CHECK(st.fallback_hit > 0);
|
|
CHECK(st.value_mismatch > 0);
|
|
}
|
|
|
|
if (g_failures) {
|
|
std::fprintf(stderr, "test_pgxp: %d FAILURES\n", g_failures);
|
|
return 1;
|
|
}
|
|
std::printf("test_pgxp: all checks passed\n");
|
|
return 0;
|
|
}
|