mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
689f3b348e
Co-authored-by: Yves <calibratedbeats@gmail.com>
149 lines
9.0 KiB
Markdown
149 lines
9.0 KiB
Markdown
# Experimental Win32/OpenGL OpenXR backend
|
|
|
|
Build with `-DPSX_OPENXR=ON` to opt in. The default is OFF: ordinary builds
|
|
do not fetch the SDK or require an active headset runtime. The enabled build
|
|
uses the official OpenXR-SDK source archive for annotated tag object
|
|
`b76b80adaf65ac3ad6cc1ce61974fb29a5d02352` (SHA256-pinned in `third_party/deps.manifest`;
|
|
same vendored/offline resolution as libchdr) and currently supports Win32/OpenGL. This is a PC headset backend; it does not
|
|
provide an Android/standalone Quest build.
|
|
|
|
## Opting in at run time: `PSX_OPENXR_ENABLE`
|
|
|
|
The CMake option `PSX_OPENXR` only compiles the backend. A compiled-in build
|
|
still creates the usual OpenGL 3.3 core context. Setting the environment variable
|
|
`PSX_OPENXR_ENABLE=1` makes an XR build request an OpenGL **4.6** core context
|
|
instead (the OpenXR OpenGL runtime requirements need it); the runtime logs one
|
|
line to stderr when this changes the context version. It has no effect in a build
|
|
without `-DPSX_OPENXR=ON`. The older spelling `PSX_OPENXR=1` (identical to
|
|
the CMake option name) still works but is deprecated, logs a note, and will be
|
|
removed. The variable only selects the context; the game plugin still enables
|
|
XR with `psx_mod_openxr_enable`.
|
|
|
|
## Threading
|
|
|
|
Every OpenXR entry point runs on the emulation/main thread that owns the GL
|
|
context: plugin hooks (`psx_mod_openxr_*`), the GL present path
|
|
(`gl_swap_with_osd` and the interpolation/hold presents it serves) and TCP
|
|
command handlers, which the debug server executes at its `debug_server_poll`
|
|
safe point on that same thread (its I/O thread only queues requests). There is
|
|
no second GL or XR thread, so the module has no locks. New code must keep it
|
|
that way or add marshalling.
|
|
|
|
## Session loss and recovery
|
|
|
|
`XR_ERROR_SESSION_LOST`/`XR_ERROR_INSTANCE_LOST` (from any call), session
|
|
`LOSS_PENDING` and instance loss tear the whole XR stack down at the end of
|
|
the failing call and fall back to the flat present. The backend stays enabled,
|
|
retries initialisation every few seconds, and counts `losses` in
|
|
`openxr_stats`. A session that reaches `EXITING` (the user closed the headset
|
|
app) is torn down and left disabled until `openxr_control enable=1` /
|
|
`psx_mod_openxr_enable(1)`. A swapchain image whose wait fails is treated as an
|
|
unrecoverable swapchain and takes the same path.
|
|
|
|
## Frame ownership and stereo submission
|
|
|
|
The game plugin enables the backend with `psx_mod_openxr_enable`, then calls
|
|
`psx_mod_openxr_begin(width,height,units_per_meter)` at a host frame boundary.
|
|
Begin waits/begins the XR frame and locates both eyes at its predicted time.
|
|
`psx_mod_openxr_view` returns the scoped rigid transform and asymmetric
|
|
projection for each eye. The plugin supplies its own draw-only callbacks to
|
|
the [atomic stereo renderer](STEREO_RENDERING.md).
|
|
|
|
`psx_mod_openxr_end(pair_rendered)` submits only the fresh complete pair from
|
|
that frame. A failed, refused or shed redraw submits zero layers, closing the
|
|
frame without recycling an old pair. Private swapchains receive eye textures;
|
|
guest execution, simulation time and guest TR registers are not owned by XR.
|
|
`psx_mod_openxr_recenter` establishes the shared pose origin. World scale,
|
|
native camera/body mapping, aiming and gameplay policy belong to the plugin.
|
|
An inverted relative head pose can be a deliberate sustained roll, so the
|
|
backend reports its up-dot and inversion streak through `openxr_stats` without
|
|
changing the origin. Explicit recenter and OpenXR reference-space change events
|
|
own origin changes.
|
|
|
|
## Offline actions and controller snapshots
|
|
|
|
Touch stick, trigger, squeeze and click actions are sampled at the normal
|
|
offline input boundary, never during eye replay. `psx_mod_openxr_input` exposes
|
|
activity/focus, sequence and synthetic metadata. Unfocused/inactive actions
|
|
are neutral; the game chooses how to map them through the separate trusted
|
|
controller-source API. Default flat input registers no XR source.
|
|
|
|
`psx_mod_openxr_hands` is read-only and safe during replay. It reports grip/aim
|
|
poses for both hands, activity, OpenXR validity/tracking flags, common origin,
|
|
predicted time, sequence and age. A cached pose is not a promise that tracking
|
|
is currently valid. Focus loss, disable and shutdown clear samples. Relative
|
|
controller transforms use the same metric pose math and origin as the eyes.
|
|
|
|
TCP `openxr_stats`, `openxr_views`, `openxr_input` and `openxr_hands` expose the
|
|
backend. `openxr_control` enables/recenters it. Synthetic `openxr_input_override`
|
|
and `openxr_hands_override` exist only with debug tools enabled and label their
|
|
samples. Their handlers and registrations are excluded by PSX_NO_DEBUG_TOOLS.
|
|
|
|
## Menus and native startup surfaces
|
|
|
|
`psx_mod_openxr_quad(distance_m,width_m,height_m)` chooses a frame-local menu
|
|
quad after pair begin. The request resets each frame; normal gameplay resumes
|
|
projection submission. Applications choose when their identical-eye menus
|
|
should become a comfortable quad. Distance must be 0.25..20m and dimensions
|
|
positive/bounded; values must be finite.
|
|
|
|
`psx_mod_openxr_native_surface(distance_m,width_m,units_per_meter)` permits
|
|
boot/menu/video presentation before any gameplay stereo hook. It persists
|
|
until disabled with distance zero. Width is positive and at most 10m, scale
|
|
1..65536, distance 0.25..20m; values must be finite. It refuses during replay,
|
|
an open XR frame, or compiled-out XR. Disable it before application pair begin.
|
|
|
|
Only a fresh native present arms submission. The letterboxed GL_BACK rectangle
|
|
is copied before host OSD, upright, to a VIEW-space quad preserving its aspect.
|
|
This path does not replay guest draws. Unchanged native frames still pump XR.
|
|
`openxr_stats.submitted_source` distinguishes stereo (1) and native (2).
|
|
Successful-submit counters and latched frame/cycle metadata describe the last
|
|
submission; they do not prove the layer currently visible. `video_info` exposes
|
|
the actual GL swap interval. The plugin can avoid a second desktop wait while
|
|
keeping the runtime's guest-speed deadline.
|
|
|
|
## Validation and limits
|
|
|
|
The original MoH alpha was accepted on Quest 3 via Virtual Desktop/VDXR for
|
|
startup/menu visibility, head tracking and mapped combat input. Menu quads and
|
|
native startup surfaces were exercised. This is historical acceptance of the
|
|
alpha checkpoint, not a new headset run of this rebased branch.
|
|
|
|
Fresh extraction checks cover action lifecycle/neutralization, pose math,
|
|
paired watchdog rollback, GTE identity/parallax/rigid/asymmetric projection,
|
|
and XR/debug-tool compile combinations. Controlled focus/reconnection tests,
|
|
other runtimes/headsets and fresh rebased headset acceptance remain pending.
|
|
Guest-frame replay cost does not guarantee headset-rate rendering. No claim
|
|
of independent headset-rate simulation, controller/barrel alignment or Android
|
|
support is made. The projection-scale prerequisite is runtime-only; no retail BIOS
|
|
regeneration is required. Bundled OpenBIOS is suitable for enabled builds.
|
|
|
|
The pin resolves to SDK commit `c15d38cb4bb10a5b7e075f74493ff13896e2597a`.
|
|
See [Third-Party Attribution](../THIRD_PARTY_ATTRIBUTION.md#openxr-sdk-loader---optional-pc-headset-backend)
|
|
for the Apache-2.0 SDK, MIT JsonCpp notices, cold-cache requirements and package
|
|
distribution. Consuming this feature requires a framework pin update and runtime
|
|
rebuild; it does not change guest code generation.
|
|
|
|
## Headset color candidate (2026-10-05)
|
|
|
|
`fix/vr-headset-color` corrects submission encoding. Native PSX/stereo textures
|
|
contain display-encoded RGB; the previous unconverted GL_RGBA8 swapchain made
|
|
the compositor interpret those values as linear. Prefer GL_SRGB8_ALPHA8, with
|
|
GL_FRAMEBUFFER_SRGB disabled during the copy and restored afterward. If only
|
|
GL_RGBA8 is available, the copy shader explicitly decodes sRGB instead.
|
|
|
|
Gameplay applies desktop post-gamma once before encoding/decoding. Native
|
|
GL_BACK already contains that adjustment and is copied without applying it
|
|
again. Neither path changes guest GPU lighting, textures or simulation.
|
|
`openxr_stats.swapchain_format` reports the selected GL enum (35907 sRGB,
|
|
32856 linear). The linear-only native fallback uses an intermediate copy.
|
|
|
|
`runtime/tests/run_openxr_color_gl.py` runs the actual private copy functions
|
|
and shader against source-owned RGB/grayscale ramps in hidden real GL. Eight
|
|
format/gamma/incoming-sRGB-state combinations pass for gameplay and native
|
|
copies, including vertical orientation, alpha and GL state restoration.
|
|
|
|
## Frame interpolation is off while a session runs
|
|
|
|
With frame interpolation on, each interpolated sub-present reaches gl_swap_with_osd and, with a native surface enabled, openxr_present_native -> psx_openxr_begin -> xrWaitFrame. XR would then run several times per guest frame (stacked waits, per-eye ghosting). So presentation-side interpolation is suspended whenever psx_openxr_session_active() is true (compiled, enabled, initialized and running; pure decision: psx_openxr_interp_gate). The compositor reprojects instead. History is reset on every transition and interpolation resumes when the session stops. In a default build, or when XR is not enabled or no session is running, the gate is constant false and interpolation is unchanged.
|