mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -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>
287 lines
15 KiB
C
287 lines
15 KiB
C
#ifndef PGXP_H
|
|
#define PGXP_H
|
|
|
|
/* PGXP value-propagation engine — runtime-internal API (pgxp.cpp).
|
|
*
|
|
* The engine shadows every 32-bit word of guest RAM/scratchpad, every GPR,
|
|
* and every GTE data register with the sub-pixel projection it carries
|
|
* (screen X/Y in 16.16 plus the projected SZ depth). The GTE fills shadows at
|
|
* RTPS/RTPT; the psx_pgxp_* hooks (pgxp_hooks.h) move them along with the
|
|
* data; the GPU asks for the precise position of each GP0 vertex word by the
|
|
* packet's RAM address, validated against the actual word — never guessed
|
|
* from the rounded position (the measured G1.4 dead end).
|
|
*
|
|
* Guest-visible state is NEVER touched: shadows are host-only, dropped on
|
|
* savestate/rewind, and suppressed during speculative validation passes.
|
|
*/
|
|
|
|
#include <stdint.h>
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
/* --- lifecycle / configuration ------------------------------------------ */
|
|
|
|
/* Master arm. Allocates the RAM shadow lazily; fails closed (stays disabled)
|
|
* if the allocation fails. Idempotent. */
|
|
void pgxp_set_enabled(int enabled);
|
|
int pgxp_enabled(void);
|
|
/* Armed and not inside a suppression bracket (speculative/replay pass):
|
|
* whether the hooks record anything right now. */
|
|
int pgxp_active(void);
|
|
|
|
/* Tier-2 propagation through CPU arithmetic (default off, like the reference
|
|
* implementations' default). Off is SAFE — value validation already stops
|
|
* stale shadows — it only bounds coverage. Live-tunable (TCP). */
|
|
void pgxp_set_cpu_mode(int enabled);
|
|
int pgxp_cpu_mode(void);
|
|
|
|
/* Reject a precise position whose sub-pixel offset from the native integer
|
|
* position exceeds this many pixels. < 0 disables the clamp (default). */
|
|
void pgxp_set_tolerance(float pixels);
|
|
float pgxp_tolerance(void);
|
|
|
|
/* Position-cache fallback tier (docs/ENHANCEMENTS.md G1.4/G1.11). On (the
|
|
* default, unchanged behaviour): a vertex with no validated dataflow shadow
|
|
* may take the sub-pixel fraction of the projection last cached at its integer
|
|
* screen position. Off ("dataflow only"): such a vertex draws native, and
|
|
* gte.cpp stops filling the cache. A title whose build carries the PGXP hooks
|
|
* reaches near-total dataflow coverage, so for it the cache only adds wrong
|
|
* fractions to CPU-built 2D polygons that happen to share a position with
|
|
* some unrelated 3D vertex. Live-tunable. */
|
|
void pgxp_set_position_fallback(int enabled);
|
|
int pgxp_position_fallback(void);
|
|
|
|
/* Preserve projection precision (docs/ENHANCEMENTS.md G1.11). Off (the
|
|
* default): RTPS/RTPT shadow the pre-truncation position the GTE itself
|
|
* computes from the integer IR1/IR2 and its UNR divide, so the shadow always
|
|
* truncates to the guest's SXY. On: they shadow the exact projection of the
|
|
* vertex, from the unshifted MACs (12 more fractional bits than IR/SZ3) and a
|
|
* true divide (pgxp_project_precise), which removes the residual wobble of
|
|
* near geometry. Guest-visible SXY, MAC0 and FLAG are unchanged either way.
|
|
* Because that position can then differ from the guest integer by more than
|
|
* the fraction, truncation agreement becomes a window (PGXP_PPP_AGREE_*) and
|
|
* a vertex at the GTE's saturation limit must still agree exactly. One
|
|
* consumer is not visual: [widescreen] precise_nclip takes its branch sign
|
|
* from these shadows, so in a title that sets both, that (already
|
|
* non-faithful) widescreen sign follows the exact projection too. */
|
|
void pgxp_set_preserve_projection(int enabled);
|
|
int pgxp_preserve_projection(void);
|
|
|
|
/* Truncation-agreement window under preserve-projection, in whole pixels:
|
|
* accept -BELOW < precise - native < ABOVE. For a value-validated shadow
|
|
* whose packet half is neither at the GTE saturation limit nor wrapped by the
|
|
* GPU's 11-bit parse (both of which need exact agreement), the guest
|
|
* integer is the IR path's floor of the same projection, so the difference is
|
|
* bounded by the IR path's own error: the dropped fraction (< 1 px), IR1/IR2
|
|
* truncation times H/SZ3 (< 2 px without divide overflow), and SZ3's
|
|
* truncation scaling the offset from the screen centre by up to
|
|
* |offset|/SZ3. The window is a sanity bound over that, not a filter of
|
|
* correct vertices: for R4 (H = 290) every on-screen vertex lies within it
|
|
* (measured range -1.9 .. 2.6 px), and what it rejects is an extreme near
|
|
* vertex far off-screen, which then draws where the hardware puts it. */
|
|
#define PGXP_PPP_AGREE_BELOW 4
|
|
#define PGXP_PPP_AGREE_ABOVE 5
|
|
|
|
/* The exact projection for preserve-projection mode, in the 16.16 screen
|
|
* space of the IR path. Inputs are RTPS's unshifted 44-bit MAC1..3, the shift
|
|
* (12 for sf=1), the IR1/IR2 the GTE stored, the SZ3 it pushed, H, OFX/OFY
|
|
* and the horizontal widescreen factor x_num/x_den the IR path applied (1/1
|
|
* when none). Returns 1 with the position in x16/y16 when the vertex
|
|
* qualifies (sf=1, IR1/IR2 not saturated, SZ3 not saturated and nonzero, and
|
|
* no divide overflow: H < 2*SZ3), else 0 and the caller keeps the IR path. */
|
|
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);
|
|
|
|
/* Whether an exact projection may be the shadow of the SXY word `packed`:
|
|
* the consumer's truncation agreement (the PGXP_PPP_AGREE_* window, exact at
|
|
* the saturation limits) on both halves. When it fails, RTPS keeps the IR
|
|
* path's shadow, which always agrees, so the vertex still draws precise
|
|
* instead of being rejected at the GPU. Counts ppp_produced on accept and
|
|
* ppp_window_fallback on reject. */
|
|
int pgxp_ppp_accept(int32_t x16, int32_t y16, uint32_t packed);
|
|
|
|
/* Precise culling (docs/ENHANCEMENTS.md G1.12). Off (the default): NCLIP is
|
|
* exactly the hardware's. On, while geometry correction is armed: when the
|
|
* three SXY FIFO shadows are live, describe the current register words and
|
|
* pass the same acceptance the GPU applies when it draws them, and the sign
|
|
* of their exact determinant differs from the integer one, NCLIP's MAC0
|
|
* takes the exact sign (magnitude: the exact doubled area rounded, at least
|
|
* 1). A game that culls on that sign then keeps the sub-pixel faces PGXP
|
|
* draws with positive area -- the far road rows a native zero-area test
|
|
* drops, which open gaps between their precisely placed neighbours -- and
|
|
* drops the slivers whose drawn winding is reversed. This CHANGES
|
|
* guest-visible MAC0 and so guest control flow: only the mod arms it
|
|
* (netplay clears the mod), and gte.cpp holds it off in every pass that is
|
|
* compared against another execution (overlay shadow diff, speculative and
|
|
* replay passes). Live-tunable over TCP. */
|
|
void pgxp_set_culling(int enabled);
|
|
int pgxp_culling(void);
|
|
|
|
/* NCLIP's exact determinant from the SXY0..2 register shadows, in 2^-32
|
|
* px^2 units (16.16 times 16.16). Returns 1 when all three shadows are live,
|
|
* carry the register words in sxy[] and pass the GPU consumer's truncation
|
|
* agreement and tolerance clamp; else 0. Counts nclip_precise. */
|
|
int pgxp_gte_nclip_precise(const uint32_t sxy[3], int64_t *cross);
|
|
|
|
/* Mod-owned request (the framework's psx.enhancement.pgxp package).
|
|
*
|
|
* The mod's activation runs at session start, before main.cpp's renderer
|
|
* setup, and that setup applies the [video] baseline (geometry_correction,
|
|
* perspective_texturing, pgxp_cpu_mode). An activation that armed the
|
|
* corrections directly was switched straight back off there. So the
|
|
* activation only records a request, and the session arming
|
|
* (psx_pgxp_session_arm, pgxp_session.h) takes it -- reads and clears it --
|
|
* and combines it with the baseline. A request therefore lives only from one
|
|
* session's activation to that session's arming: a later session whose plan
|
|
* is empty (netplay, or the mod disabled) cannot inherit it.
|
|
* reset_mod_owned_presentation() also clears it at every session start. */
|
|
void pgxp_mod_request(int enabled, int cpu_mode, int culling);
|
|
/* Returns nonzero when this session's plan asked for PGXP; *cpu_mode and
|
|
* *culling (either may be NULL) receive the mod's options. */
|
|
int pgxp_mod_requested(int *cpu_mode, int *culling);
|
|
/* pgxp_mod_requested, then clears the request. */
|
|
int pgxp_mod_request_take(int *cpu_mode, int *culling);
|
|
|
|
/* Drop all shadows (savestate load, raw RAM restore, timeline breaks).
|
|
* O(1) via generation bump. Deferred while suppressed. */
|
|
void pgxp_invalidate_all(void);
|
|
|
|
/* Counted suppression bracket for speculative native-validation passes and
|
|
* the GTE replay sandbox: hooks and producers no-op inside it. */
|
|
void pgxp_suppress_begin(void);
|
|
void pgxp_suppress_end(void);
|
|
|
|
/* --- GTE producer (gte.cpp) ---------------------------------------------- */
|
|
|
|
/* Called at the RTPS/RTPT projection with the pre-truncation 16.16 screen
|
|
* coordinates, the projected depth (SZ3), and the packed SXY word the guest
|
|
* sees. Shifts the shadow FIFO exactly like push_sxy (regs 12..15). */
|
|
void pgxp_gte_push_sxy(int32_t x16, int32_t y16, uint16_t sz3, uint32_t packed);
|
|
|
|
/* Current SXY FIFO shadow (index 0..3 selects GTE data regs 12..15).
|
|
* Returns nonzero when the shadow is live and carries X/Y precision. */
|
|
int pgxp_get_gte_sxy(uint32_t index, int32_t *x16, int32_t *y16);
|
|
/* Validate-on-read variant: check!=0 additionally requires the shadow's packed
|
|
* word to equal expect (the live guest SXY word). Sign consumers must use this
|
|
* form so stale FIFO shadows fail closed to native integer behavior. */
|
|
int pgxp_get_gte_sxy_checked(uint32_t index, uint32_t expect, int check,
|
|
int32_t *x16, int32_t *y16);
|
|
|
|
/* Guest write to a GTE data register outside gte_execute (MTC2/CTC2 handled
|
|
* by psx_pgxp_cop2; this is for direct gte_write_data paths): reg 15 performs
|
|
* the SXYP FIFO push on the shadows, others just invalidate/overwrite. */
|
|
void pgxp_gte_reg_written(int reg, uint32_t value);
|
|
|
|
/* --- GPU consumer (gpu.c) ------------------------------------------------ */
|
|
|
|
enum {
|
|
PGXP_SRC_NATIVE = 0, /* no shadow — caller uses the parsed integers */
|
|
PGXP_SRC_FALLBACK = 1, /* position-cache fallback (never carries depth)*/
|
|
PGXP_SRC_DATAFLOW = 2, /* address-keyed shadow, value-validated */
|
|
};
|
|
|
|
/* Precise position for one GP0 vertex word.
|
|
* addr guest address the word was DMA'd from, or 0xFFFFFFFF if
|
|
* unknown (immediate GP0 writes) — skips the dataflow tier.
|
|
* packet_word the actual word (validation key).
|
|
* int_x/int_y the natively parsed 11-bit positions (pre-draw-offset).
|
|
* On DATAFLOW/FALLBACK, *x16/*y16 hold the sub-pixel position (16.16, same
|
|
* coordinate space as the packet halves); *sz is the projected depth or 0.
|
|
* Safeguards applied here: the integer part must match the native parse
|
|
* (truncation agreement; a bounded window under preserve-projection) and the
|
|
* tolerance clamp. The fallback tier is skipped when position fallback is
|
|
* off. */
|
|
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);
|
|
|
|
/* Same decision as pgxp_get_precise_vertex, without counting it in the stats
|
|
* (the caller looks the vertex up again when it draws). */
|
|
int pgxp_probe_precise_vertex(uint32_t addr, uint32_t packet_word,
|
|
int32_t int_x, int32_t int_y);
|
|
|
|
/* --- observability -------------------------------------------------------- */
|
|
|
|
typedef struct PGXPStats {
|
|
uint64_t lookups; /* pgxp_get_precise_vertex calls */
|
|
uint64_t dataflow_hit;
|
|
uint64_t fallback_hit;
|
|
uint64_t native;
|
|
uint64_t value_mismatch; /* shadow present but wrong word (stale) */
|
|
uint64_t trunc_reject; /* integer part disagreed with native parse */
|
|
uint64_t tolerance_reject;
|
|
uint64_t w_valid; /* lookups that also carried a usable depth */
|
|
uint64_t produced; /* RTPS/RTPT projections pushed into shadows */
|
|
uint64_t swc2_stores; /* GTE reg shadows copied to RAM shadows */
|
|
uint64_t ppp_produced; /* of those, shadowed with the exact projection */
|
|
uint64_t ppp_window_fallback;/* exact projection outside the agreement
|
|
* window: shadowed with the IR path instead */
|
|
/* Geometry-corrected triangles by how many of their 3 vertices came out
|
|
* precise (gpu.c prepare_precise_triangle). `mixed` is the mesh-cracking
|
|
* exposure of docs/ENHANCEMENTS.md G1.1: an edge between a precise and a
|
|
* native vertex can disagree with its neighbour by the dropped fraction. */
|
|
uint64_t tri_precise; /* all three precise */
|
|
uint64_t tri_mixed; /* one or two precise */
|
|
uint64_t tri_native; /* none precise */
|
|
/* Textured quads that are axis-aligned rectangles in integer screen space
|
|
* (and in UV) whose four vertices are all dataflow-precise, so they were
|
|
* drawn as two precise triangles instead of the native 2D rectangle
|
|
* shortcut; rect_partial: such rectangles with 1..3 precise vertices,
|
|
* which keep the shortcut (drawing them as triangles would mix). */
|
|
uint64_t rect_bypass;
|
|
uint64_t rect_partial;
|
|
/* NCLIPs while geometry correction is armed (docs/ENHANCEMENTS.md G1.12):
|
|
* nclip_precise had an exact determinant; nclip_disagree: its sign (non-
|
|
* zero) differs from the integer MAC0's, so the game's cull decision on
|
|
* that face disagrees with the face PGXP draws -- the crack exposure that
|
|
* tri_* cannot see, because a culled face draws no triangle at all;
|
|
* nclip_corrected: of those, MAC0 replaced by precise culling. */
|
|
uint64_t nclip_precise;
|
|
uint64_t nclip_disagree;
|
|
uint64_t nclip_corrected;
|
|
} PGXPStats;
|
|
|
|
void pgxp_get_stats(PGXPStats *out);
|
|
/* Count one geometry-corrected triangle with `precise` (0..3) precise
|
|
* vertices. */
|
|
void pgxp_note_triangle(int precise);
|
|
/* One rectangle-shortcut quad sent down the triangle path (all_precise) or
|
|
* kept on the shortcut with only some precise vertices. */
|
|
void pgxp_note_rect_bypass(int all_precise);
|
|
/* One NCLIP whose exact sign disagreed with MAC0, and whether MAC0 was
|
|
* corrected. */
|
|
void pgxp_note_nclip(int disagree, int corrected);
|
|
|
|
/* --- gte.cpp forwarding surface (v14 ABI compat) -------------------------- */
|
|
|
|
/* SWC2 site: copy the GTE register shadow (regs 12..15) to the RAM shadow. */
|
|
void pgxp_store_gte_reg(uint32_t addr, uint8_t reg);
|
|
|
|
/* Address-keyed precise-word lookup (perspective texturing path). Returns
|
|
* nonzero when the tracked word matches `packed` AND carries a depth. */
|
|
int pgxp_load_precise_word(uint32_t addr, uint32_t packed,
|
|
int32_t *x16, int32_t *y16, uint16_t *z);
|
|
|
|
/* --- test accessors (always compiled; trivial) ---------------------------- */
|
|
|
|
/* index 0..3 selects the SXY0..SXYP register shadow (GTE data regs 12..15). */
|
|
void pgxp_test_seed_gte_sxy(uint32_t index, uint32_t packed,
|
|
int32_t x16, int32_t y16, uint16_t z, int valid);
|
|
void pgxp_test_get_gte_sxy(uint32_t index, uint32_t *packed,
|
|
int32_t *x16, int32_t *y16, uint16_t *z,
|
|
uint8_t *valid);
|
|
uint32_t pgxp_test_generation(void);
|
|
uint32_t pgxp_test_suppress_depth(void);
|
|
int pgxp_test_active(void);
|
|
void pgxp_test_set_generation(uint32_t gen);
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
#endif /* PGXP_H */
|