CTR Decrypted

PGXP (precision geometry)

The PS1 GTE rounds every projected vertex to a whole pixel, and the PS1 GPU maps textures affinely (linearly in screen space). That gives polygons that jitter as the camera moves ("vertex wobble") and textures that bend on large, close polygons ("texture warping"). PGXP (Parallel/Precision Geometry Transform Pipeline) keeps a floating-point copy of each projected vertex next to the integer one the game sees, and the renderer draws with the precise copy.

Game logic still runs on the retail integer values. Only what is drawn changes.

Source

Ported from High Octane commit 03014d84f (v1.5), which adapted it from CTR Turbocharged (revision 07d0c49f), crediting DuckStation (Stenzek) and the original PGXP work by iCatButler. The code is GPLv3; see THIRD_PARTY_NOTICES.md. High Octane has no later functional PGXP changes.

Option

OPTIONS → Video → PGXP (Anti-Wobble), saved as pgxp under [Video & QoL] in config.ini. The value is High Octane's mode number:

Menu pgxp= Effect
OFF 0 Retail rendering
TEXTURES 3 Perspective-correct texturing only. Vertices stay on whole pixels
GEOMETRY 1 Sub-pixel vertices (anti-wobble) only. Textures stay affine
FULL 2 Both. Default

The menu steps in that order. Changes apply at the next frame boundary, with no restart, because one set of shaders handles every mode (see Renderer below).

How it hooks in

Precise camera

Sub-pixel vertices only help if the camera is smooth, and the game's camera is integer: position in whole world units, angles in 4096ths of a turn, from ratan2 of integer vectors about 200 units long, so each one-unit change turns the view about 0.3°. While the camera-to-kart vector changes (accelerating, braking, turning) the whole screen steps. With GEOMETRY or FULL on, the renderer uses the same camera without the rounding (platform/native_pgxp_view.c). The game keeps its integer camera for everything it decides (culling, LOD, audio).

Level depth test

The PS1 has no depth buffer. It draws in ordering-table order, and the level renderer sorts coarsely: the 16 faces of a 4x4 quadblock share one slot, from the block's farthest corner. So water near the camera can paint over a nearer surface, in retail too (water over the N. Sanity Beach dock with the far camera). With TEXTURES or FULL, each level vertex has its view depth, and the level's water tests it:

Back-face culling

The level renderer culls back faces with the GTE's NCLIP (the projected triangle's signed area), and NCLIP uses the same precise vertices the GPU draws with whenever GEOMETRY or FULL is on (gNativePgxpIntegerNclipEnabled = 0; High Octane's equivalent pgxp_precise_nclip is off by default). A distant floor polygon seen almost edge-on is a fraction of a pixel tall; with integer vertices its area rounds to zero or flips sign and it is culled. In retail its neighbours round onto the same rows so nothing shows, but with precise vertices the strip opens and the sky shows through. Only rendering calls NCLIP (the level renderer and RenderBucket_QueueExecute.c), so gameplay is unaffected. To check: find_holes.py totals with CTR_PGXP_DEBUG=1 should match the retail baseline (CTR_PGXP_DEBUG=3).

Texture sampling

Debugging

CTR_PGXP_DEBUG (environment variable) tints polygons:

Value Tint
1 By how many vertices PGXP recovered: red for some, blue for none, untinted for all. The sky backdrop is blue, so holes show as blue
2 As 1, and recovered polygons switch between precise and retail positions every 2.5 seconds, tinted green in the retail phase
3 As 1, but always at retail positions with affine textures: the baseline to compare against
4 Level geometry by the overlay 226 bucket that emitted it: red 4x4, yellow dynamic, green 4x2, cyan 4x1 (low LOD), lilac water, orange full dynamic, white near-plane clipped

For reproducible captures, measurements and polygon dumps, use the tools in DEV-TOOLS.md.

Differences from High Octane

Known limitations