PGXP: precise culling as a mod option; the mod as a title's one switch

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>
This commit is contained in:
Shokunin
2026-10-01 06:02:52 -07:00
parent 5970e30ddf
commit 8630d3a6f8
18 changed files with 879 additions and 192 deletions
+69
View File
@@ -432,6 +432,75 @@ int main(void) {
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;