Files
psxrecomp/docs/STEREO_RENDERING.md
T
Matthew Stanley dd0ce3d365 gte: render-view projection override replaces guest projection explicitly; fix abort test count after merge
Override skips guest-formula PGXP shadows; composition with fov_scale tested and documented.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 11:03:01 -07:00

5.2 KiB

Simultaneous stereo rendering

The paired API is an opt-in enhancement alongside temporal render passes. It uses the same frozen-time checkpoint, store policy, watchdog, VRAM journal and restore. It does not require interpolation, a FLIP source, a temporal plan or Q16 phase labels. Each eye callback receives LEFT=0 or RIGHT=1 explicitly.

Call psx_mod_render_stereo(cpu, frame, callback, user) from a game's measured draw boundary outside interrupts or active GPU DMA. The callback must rebuild and submit geometry, without waiting for VBlank or advancing game logic. The rect declares the capture region. Restore completes after each eye before the next one starts. Successful eye textures remain private until both transactions and capture/restore verification succeed. Failure retains the preceding complete pair. Host callback state is the plugin's responsibility, including selectors that must be cleared after watchdog rollback.

period_vblanks describes the game's draw cadence (1..8). An EMA of complete-pair host cost is compared with 80% of that cadence, using the runtime's live NTSC/PAL period. Expensive work is shed as a complete pair; every thirtieth request retries to recover a stale estimate. This is a conservative cadence budget, not a measured headset idle budget. Allocating or slow initial pairs may exceed it; completed guest work cannot be preempted by a host-time budget. Guest-cycle watchdogs remain the runaway bound.

OpenGL GPU-authoritative 15-bit rendering is required. CPU-authoritative dual raster, windowed high-resolution tiles, depth24 and native-wide rendering are initially unsupported. Session/resimulation/rewind/fast-forward and nesting gates remain. Faithful defaults do not call this API or enable stereo presentation.

psx_mod_set_stereo_presentation(1) opts into side-by-side output: one atomic pair per swap, LEFT on the left, RIGHT on the right, with each eye using the configured display aspect. Zero disables it. The first implementation uses ordinary game VBlank presentation cadence; it does not submit to OpenXR or create headset poses. Stereo and temporal interpolation should be configured separately; SBS takes precedence when a compatible complete pair is available. A pair is shown only while the plugin keeps submitting: it expires 24 VBlanks of guest time after its capture cycle (3x the slowest 8-VBlank cadence) and the presenter falls back to the normal flat image (menus, same-size FMV, a plugin that stopped submitting). Savestate/rollback loads and session resets drop the pair immediately.

Inside an eye callback, psx_mod_render_view_offset(x,y,z) replaces a scoped camera-space translation. RTPS and RTPT add it alongside TR before projection; the guest translation registers remain unchanged. Repeated transformations do not accumulate the offset. Normal returns and watchdog rollback restore the previous host value. Calls outside a render transaction are refused. The GTE test compares this seam against explicit pre-divide translation and measures 12 pixels at Z=800 versus 3 pixels at Z=3200 for X=24 and H=400.

psx_mod_render_view (full pose) is an explicit per-eye projection. With projection=1 it REPLACES the guest X/Y projection entirely: guest H, the widescreen/dome squash and the [video] fov_scale scaled-H do not feed it, and the guest-formula PGXP shadows are skipped for those vertices. The only coupling is projection_h_ref: when non-zero the focal lengths are multiplied by (fov-scaled guest H) / projection_h_ref, so a plugin can follow per-draw H changes. SZ, the depth cue and divide flags still come from the guest division. The pose exists only inside a render transaction and is restored with it, so nothing outside the sandbox sees it. test_render_pose covers the override together with fov_scale.

TCP inspection:

  • stereo_stats: pair outcomes, eye entry cycles/state hashes under VERIFY, sampled view offsets, latched failed attempt/eye/retained pair, and published pair metadata. render_pass_stats continues to expose shared transaction checks, aborts, leaks and dropped stores; its eye transactions also increment pass counters.
  • stereo_dump path=<absolute-directory> count=N: arm dumps of the next N complete published pairs, with left/right PNGs, a side-by-side composite sharing one pair ID. Pair/cycle/view metadata is inspected through TCP stereo_stats.captures, retained for the current capture request (maximum 100 records); the runtime writes no diagnostic JSON files. A capture client can use tools/stereo_capture.py::write_capture_manifests to write manifests after confirming all three PNGs for each returned ID. Arming another capture or resetting the session clears the metadata. It captures no scene on its own. Failed staging eyes never appear as published pairs.

Validation, source revisions and live limitations are recorded in the feature provenance record. The game's VR execution plan records historical alpha evidence. Projection scale and eye separation remain game-specific and must be calibrated independently. This API also works without a headset backend; it does not enable a headset mode by default.