mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
openxr: add an experimental Win32/OpenGL backend
Adapted from the original feature commits, retaining their author credit: [22bdc9e9](https://github.com/FractalEngineer/psxrecomp/commit/22bdc9e90d14f7a04b79c83b77611c4eb980fef4) [5a1264b7](https://github.com/FractalEngineer/psxrecomp/commit/5a1264b79b69b5d75f7b8507e305b1e13a911a50) [ac6f84ba](https://github.com/FractalEngineer/psxrecomp/commit/ac6f84ba039b1d27bb1e93d3b6d05390dfa42ff9) [734977c9](https://github.com/FractalEngineer/psxrecomp/commit/734977c93e80789e9210a262daa35a88f41c40d4) [04714837](https://github.com/FractalEngineer/psxrecomp/commit/04714837245ebe68abbc462ac6822a3818eedf67) [5bafeebf](https://github.com/FractalEngineer/psxrecomp/commit/5bafeebf3f0c2eaaa38212f960b9f6f88351cb74) Source scope and current-master validation: docs/internal/upstream/experimental-openxr-win32-gl.md Co-authored-by: Yves <calibratedbeats@gmail.com>
This commit is contained in:
@@ -151,3 +151,23 @@ scaffolding a project and destructive on a live one.
|
||||
Keep the pin above accurate when re-syncing. These files are the reason a
|
||||
standalone setup-wizard install and a Retro build produce the same
|
||||
multi-disc `game.toml`; if the two drift, so do those two paths.
|
||||
|
||||
## OpenXR SDK loader - optional PC headset backend
|
||||
|
||||
[OpenXR-SDK](https://github.com/KhronosGroup/OpenXR-SDK) by The Khronos Group
|
||||
Inc. and contributors is licensed Apache-2.0. Its bundled JsonCpp sources are
|
||||
distributed under their MIT terms. Only `PSX_OPENXR=ON` links the static loader;
|
||||
the SDK provides the official host API/runtime discovery implementation needed
|
||||
to connect to the user's separately installed OpenXR runtime. The feature is
|
||||
currently Win32/OpenGL-only and defaults OFF, so ordinary builds do not fetch it.
|
||||
|
||||
The dependency is pinned by annotated tag object
|
||||
`b76b80adaf65ac3ad6cc1ce61974fb29a5d02352`, resolving to commit
|
||||
`c15d38cb4bb10a5b7e075f74493ff13896e2597a`. Enabled builds fetch and compile that
|
||||
source, including bundled JsonCpp; they need Git/network on a cold cache or a
|
||||
`FETCHCONTENT_SOURCE_DIR_PSX_OPENXR_SDK` override. No proprietary headset SDK,
|
||||
runtime binary, API layer or vendor driver is redistributed.
|
||||
|
||||
The complete SDK and JsonCpp notices are in
|
||||
`runtime/licenses/OpenXR-SDK-NOTICES.txt`; both existing release packagers copy
|
||||
that directory into player packages. The source dependency is unmodified.
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
# 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 official OpenXR-SDK annotated tag object `b76b80adaf65ac3ad6cc1ce61974fb29a5d02352`
|
||||
and currently supports Win32/OpenGL. This is a PC headset backend; it does not
|
||||
provide an Android/standalone Quest build.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
@@ -65,3 +65,5 @@ The game's [VR execution plan](https://github.com/FractalEngineer/Medal-Of-Honor
|
||||
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.
|
||||
|
||||
The optional headset backend is described in [OPENXR_RENDERING.md](OPENXR_RENDERING.md).
|
||||
|
||||
@@ -430,9 +430,9 @@ The TCP server is the canonical instrumentation surface. Rule 3 in `CLAUDE.md` i
|
||||
|
||||
## Complete command index (generated)
|
||||
|
||||
**325 commands registered** — 312 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
|
||||
**332 commands registered** — 319 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
|
||||
|
||||
62 of 325 have prose above; **263 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
|
||||
62 of 332 have prose above; **270 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
|
||||
|
||||
Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code.
|
||||
|
||||
@@ -603,6 +603,13 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
|
||||
| `mmio_clear` | ✓ | | ✓ |
|
||||
| `mmio_dump` | ✓ | | ✓ |
|
||||
| `mmx6_freshfix` | ✓ | | |
|
||||
| `openxr_control` | ✓ | | |
|
||||
| `openxr_hands` | ✓ | | |
|
||||
| `openxr_hands_override` | ✓ | | |
|
||||
| `openxr_input` | ✓ | | |
|
||||
| `openxr_input_override` | ✓ | | |
|
||||
| `openxr_stats` | ✓ | | |
|
||||
| `openxr_views` | ✓ | | |
|
||||
| `overlay_candidates` | ✓ | | |
|
||||
| `overlay_capture_dump` | ✓ | | |
|
||||
| `overlay_cps_probe` | ✓ | | |
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Experimental Win32/OpenGL OpenXR: source and validation
|
||||
|
||||
Reconstructed from [FractalEngineer/psxrecomp](https://github.com/FractalEngineer/psxrecomp), developed on
|
||||
`vr-dev` at `9976567eb8e95510ca7eeeeccd9af3a260885de5`.
|
||||
Exact source commits:
|
||||
|
||||
- [22bdc9e9](https://github.com/FractalEngineer/psxrecomp/commit/22bdc9e90d14f7a04b79c83b77611c4eb980fef4)
|
||||
- [5a1264b7](https://github.com/FractalEngineer/psxrecomp/commit/5a1264b79b69b5d75f7b8507e305b1e13a911a50)
|
||||
- [ac6f84ba](https://github.com/FractalEngineer/psxrecomp/commit/ac6f84ba039b1d27bb1e93d3b6d05390dfa42ff9)
|
||||
- [734977c9](https://github.com/FractalEngineer/psxrecomp/commit/734977c93e80789e9210a262daa35a88f41c40d4)
|
||||
- [04714837](https://github.com/FractalEngineer/psxrecomp/commit/04714837245ebe68abbc462ac6822a3818eedf67)
|
||||
- [5bafeebf](https://github.com/FractalEngineer/psxrecomp/commit/5bafeebf3f0c2eaaa38212f960b9f6f88351cb74)
|
||||
|
||||
Original author: Yves <calibratedbeats@gmail.com>. The feature commit retains
|
||||
that exact human author identity. The focused port consolidates the selected
|
||||
source hunks and corrections. Its commit also cites these sources and carries
|
||||
the original human contributor's `Co-authored-by: Yves <calibratedbeats@gmail.com>` trailer.
|
||||
|
||||
Adapted: Only SDK dependency/build integration, headset frame lifecycle, Touch actions and tracked hands, synthetic debug controls, menu quads and native startup presentation. Debug-less guards, complete dependency notices and focused docs were corrected during the port.
|
||||
|
||||
Deliberately excluded: Producer-PC filtering, title-specific transforms/hook PCs/input policy, private saves/discs, unrelated chronology, Android support and headset-rate simulation.
|
||||
|
||||
The feature was reconstructed on upstream master
|
||||
`db62eec15f81c88aef3d6dab51d4e566a7167d23`, retaining current upstream APIs,
|
||||
renderer/input policy and build integration. Validation after reconstruction:
|
||||
XR ON/OFF crossed with debug-tool ON/OFF C compilation, compiled-out action/hand lifecycle tests and the SDK loader native link. Historical Quest 3/Virtual Desktop VDXR acceptance is not fresh rebased headset evidence.
|
||||
|
||||
The motivating consumer is [Medal of Honor, SLUS-00974 (NTSC-U)](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/e323885958c2ae0b0df847bed6175f9e76524442).
|
||||
Game-specific integration remains in that repository. Consuming this change
|
||||
requires a deliberate framework pin update in the game repo and a rebuild.
|
||||
This is a host/runtime/tool change; it requires no BIOS or game C regeneration.
|
||||
|
||||
Historical alpha acceptance used the older framework checkpoint above. It is
|
||||
separate from current-master branch verification. Fresh game, renderer and
|
||||
headset checks, where applicable, are recorded in the PR validation notes;
|
||||
uncertain hardware/runtime behavior must not be inferred from a clean link.
|
||||
|
||||
## Current-master integration validation
|
||||
|
||||
Windows, GCC 13.2, Ninja, Release. Runtime test checks remain active with NDEBUG enabled.
|
||||
|
||||
Full Release runtime builds and nine focused CTest cases pass for XR ON/OFF crossed with debug tools ON/OFF. The rebuilt Medal of Honor game links with XR ON, launcher UI OFF, rewind ON and netplay OFF. OpenBIOS LLE reaches flat gameplay after a fresh save/load. A live initialization attempt found VirtualDesktopXR, then xrGetSystem returned XR_ERROR_FORM_FACTOR_UNAVAILABLE (-35); the game continued in flat mode with neutral XR input. No headset layers were submitted. Quest 3/VDXR alpha acceptance remains historical; fresh visual headset, controlled focus/reconnection and other-runtime tests remain pending.
|
||||
|
||||
Integration: [Medal of Honor, SLUS-00974 NTSC-U](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/e323885958c2ae0b0df847bed6175f9e76524442), built against the corrected framework stack with `PSXRECOMP_ROOT`. Bundled OpenBIOS LLE cold boot, menu navigation, mission gameplay, fresh savestate creation/loading and post-load input/frame advancement were checked with screenshots. An older alpha slot-5 savestate was refused; compatibility with historical states is not established. Memory-card round trips, attract-demo completion, other games and fresh headset visuals were not tested.
|
||||
|
||||
The registered recompiler suite reports 132 passes, four failures and three disabled tests. The four failures reproduce on unchanged upstream master in this Windows toolchain: `portable_settings_paths_test`, `optional_netplay_auth`, `compile_overlays_static_tail` and `aot_overlay_discovery`. The introduced BIOS-fingerprint and GL-guard failures are fixed.
|
||||
@@ -352,6 +352,13 @@ if(BUILD_TESTING)
|
||||
add_test(NAME sio_dualshock_rumble_test COMMAND sio_dualshock_rumble_test)
|
||||
set_tests_properties(sio_dualshock_rumble_test PROPERTIES DISABLED TRUE)
|
||||
|
||||
add_executable(openxr_input_test tests/test_openxr_input.c src/psx_openxr.c)
|
||||
target_include_directories(openxr_input_test PRIVATE include)
|
||||
if(NOT MSVC)
|
||||
target_link_libraries(openxr_input_test PRIVATE m)
|
||||
endif()
|
||||
add_test(NAME openxr_input_test COMMAND openxr_input_test)
|
||||
|
||||
add_executable(vr_pose_math_test tests/test_vr_pose_math.c src/vr_pose_math.c)
|
||||
target_include_directories(vr_pose_math_test PRIVATE include)
|
||||
if(NOT MSVC)
|
||||
|
||||
@@ -28,6 +28,7 @@ int gl_renderer_texture_banks_supported(void);
|
||||
/* Set the GL swap interval / vsync mode (1=vsync, 0=immediate, -1=adaptive).
|
||||
* Safe before or after context creation; applies live when a context exists. */
|
||||
void gl_renderer_set_swap_interval(int interval);
|
||||
int gl_renderer_get_swap_interval(void); /* Actual driver interval, -2 if unavailable. */
|
||||
|
||||
/* Presentation-only temporal blending. High-refresh sub-presents blend the two
|
||||
* most recent stable display images on the owning render thread/context; this
|
||||
|
||||
@@ -395,6 +395,66 @@ typedef struct PSXModRenderView {
|
||||
int32_t fx_q16, fy_q16, cx_delta_q16, cy_delta_q16;
|
||||
} PSXModRenderView;
|
||||
int psx_mod_render_view(const PSXModRenderView *view);
|
||||
/* Begin locates both views at one predicted time. End submits only a fresh
|
||||
* complete pair; failed/shed redraws submit zero layers. */
|
||||
int psx_mod_openxr_enable(int enabled);
|
||||
int psx_mod_openxr_begin(uint32_t width, uint32_t height, double units_per_meter);
|
||||
int psx_mod_openxr_view(uint32_t eye, PSXModRenderView *view);
|
||||
int psx_mod_openxr_end(int pair_rendered);
|
||||
/* Frame-local UI surface: the fresh pair's left image is shown to both eyes
|
||||
* on a head-relative quad. Call after begin, outside the draw transaction.
|
||||
* Dimensions and distance are meters; zero distance restores projection.
|
||||
* Requests reset at begin/end and never affect faithful guest rendering. */
|
||||
int psx_mod_openxr_quad(double distance_m, double width_m, double height_m);
|
||||
/* Opt-in native presentation surface for boot, videos and menus. Copies the
|
||||
* freshly drawn desktop content before host overlays, without guest replay.
|
||||
* Zero distance disables it. Applications disable it before scene begin and
|
||||
* re-enable when their scene renderer is inactive. Dimensions are meters;
|
||||
* height follows the presented content aspect. No retained stereo substitution. */
|
||||
int psx_mod_openxr_native_surface(double distance_m, double width_m,
|
||||
double units_per_meter);
|
||||
void psx_mod_openxr_recenter(void);
|
||||
/* Fresh action sample at the offline input boundary, never in an eye replay.
|
||||
* Positive Y is forward/up in XR. active[] refers only to thumbsticks;
|
||||
* other actions have independent activity. Unavailable/unfocused actions
|
||||
* return zero values. Click masks are active-high, unrelated to PSX pad bits. */
|
||||
#define PSX_MOD_XR_PRIMARY 1u /* left X / right A */
|
||||
#define PSX_MOD_XR_SECONDARY 2u /* left Y / right B */
|
||||
#define PSX_MOD_XR_MENU 4u /* Touch left Menu */
|
||||
#define PSX_MOD_XR_STICK 8u /* thumbstick click */
|
||||
#define PSX_MOD_XR_CLICKS 15u
|
||||
typedef struct PSXModOpenXRInput {
|
||||
uint32_t struct_size, focused, active[2], synthetic;
|
||||
float stick[2][2];
|
||||
uint64_t sequence;
|
||||
float trigger[2], squeeze[2]; /* [0,1] */
|
||||
uint32_t trigger_active[2], squeeze_active[2];
|
||||
uint32_t buttons[2], buttons_active[2]; /* PSX_MOD_XR_* masks */
|
||||
} PSXModOpenXRInput;
|
||||
int psx_mod_openxr_input(PSXModOpenXRInput *input);
|
||||
|
||||
/* Read-only controller snapshot from the latest located XR frame. Grip and aim
|
||||
* share the eye poses' predicted time and LOCAL space; no action sync or locate
|
||||
* occurs here, including during eye replay. Positions are meters; quaternions
|
||||
* are x,y,z,w. The origin matches the rendered view's recenter basis. Consumers
|
||||
* must check focus, activity, validity and age before using a cached pose. */
|
||||
enum { PSX_MOD_XR_GRIP_POSE = 0, PSX_MOD_XR_AIM_POSE = 1 };
|
||||
#define PSX_MOD_XR_ORIENTATION_VALID 1u
|
||||
#define PSX_MOD_XR_POSITION_VALID 2u
|
||||
#define PSX_MOD_XR_ORIENTATION_TRACKED 4u
|
||||
#define PSX_MOD_XR_POSITION_TRACKED 8u
|
||||
typedef struct PSXModTrackedPose {
|
||||
uint32_t active, flags;
|
||||
float position_m[3], orientation_xyzw[4];
|
||||
} PSXModTrackedPose;
|
||||
typedef struct PSXModOpenXRHands {
|
||||
uint32_t struct_size, focused, synthetic, origin_valid;
|
||||
uint64_t sequence, predicted_time;
|
||||
uint32_t age_ms; /* UINT32_MAX when no real frame has been located */
|
||||
double origin_position_m[3], origin_orientation_xyzw[4];
|
||||
PSXModTrackedPose pose[2][2]; /* [left/right][grip/aim] */
|
||||
} PSXModOpenXRHands;
|
||||
int psx_mod_openxr_hands(PSXModOpenXRHands *hands);
|
||||
|
||||
|
||||
int psx_mod_set_auto_skip_fmv(int enabled);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
#include "mod_plugins.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
typedef struct PSXOpenXRStats {
|
||||
uint32_t compiled, enabled, initialized, running, state, tracking, frame_open;
|
||||
int32_t result, last_failure_result;
|
||||
const char *last_failure;
|
||||
uint64_t view_flags;
|
||||
const char *stage;
|
||||
uint64_t waits, submitted, empty, failures, predicted_time;
|
||||
float ipd_m;
|
||||
double units_per_meter;
|
||||
uint64_t gl_version, min_gl_version, max_gl_version;
|
||||
char runtime[128];
|
||||
float pose[2][7], fov[2][4];
|
||||
PSXModRenderView view[2];
|
||||
uint64_t submitted_pair_id, submitted_guest_cycle;
|
||||
uint64_t submitted_predicted_time;
|
||||
uint32_t submitted_layer; /* 1 projection, 2 head-relative quad */
|
||||
uint64_t quad_submitted;
|
||||
double quad_distance_m, quad_width_m, quad_height_m; /* last submitted quad */
|
||||
uint32_t submitted_source; /* 1 fresh stereo pair, 2 native presentation */
|
||||
uint64_t native_submitted, submitted_native_frame;
|
||||
} PSXOpenXRStats;
|
||||
int psx_openxr_enable(int enabled);
|
||||
int psx_openxr_begin(int width,int height,double units);
|
||||
int psx_openxr_view(uint32_t eye,PSXModRenderView *out);
|
||||
/* copy callback: blit this pair's eye to runtime GL texture. */
|
||||
typedef int (*PSXOpenXRCopy)(uint32_t eye,uint32_t texture,int w,int h);
|
||||
int psx_openxr_end(int keep,PSXOpenXRCopy copy);
|
||||
int psx_openxr_quad(double distance_m,double width_m,double height_m);
|
||||
void psx_openxr_pair_metadata(uint64_t pair_id,uint64_t cycle);
|
||||
void psx_openxr_native_metadata(uint64_t frame);
|
||||
void psx_openxr_shutdown(void);
|
||||
void psx_openxr_recenter(void);
|
||||
void psx_openxr_stats(PSXOpenXRStats *out);
|
||||
int psx_openxr_input(PSXModOpenXRInput *out);
|
||||
void psx_openxr_input_snapshot(PSXModOpenXRInput *out);
|
||||
int psx_openxr_hands(PSXModOpenXRHands *out);
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
int psx_openxr_input_override(const PSXModOpenXRInput *input);
|
||||
int psx_openxr_hands_override(const PSXModOpenXRHands *hands);
|
||||
#endif
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,270 @@
|
||||
OpenXR SDK loader by The Khronos Group Inc. and contributors.
|
||||
Source: https://github.com/KhronosGroup/OpenXR-SDK
|
||||
Pinned tag object: b76b80adaf65ac3ad6cc1ce61974fb29a5d02352
|
||||
Resolved commit: c15d38cb4bb10a5b7e075f74493ff13896e2597a
|
||||
The unmodified loader is statically linked only with PSX_OPENXR=ON.
|
||||
License: Apache-2.0. Complete SDK license follows.
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
Bundled JsonCpp, compiled into the OpenXR loader.
|
||||
Source: OpenXR-SDK src/external/jsoncpp at the same revision.
|
||||
Distributed here under its MIT terms, with its complete upstream notice:
|
||||
|
||||
The JsonCpp library's source code, including accompanying documentation,
|
||||
tests and demonstration applications, are licensed under the following
|
||||
conditions...
|
||||
|
||||
Baptiste Lepilleur and The JsonCpp Authors explicitly disclaim copyright in all
|
||||
jurisdictions which recognize such a disclaimer. In such jurisdictions,
|
||||
this software is released into the Public Domain.
|
||||
|
||||
In jurisdictions which do not recognize Public Domain property (e.g. Germany as of
|
||||
2010), this software is Copyright (c) 2007-2010 by Baptiste Lepilleur and
|
||||
The JsonCpp Authors, and is released under the terms of the MIT License (see below).
|
||||
|
||||
In jurisdictions which recognize Public Domain property, the user of this
|
||||
software may choose to accept it either as 1) Public Domain, 2) under the
|
||||
conditions of the MIT License (see below), or 3) under the terms of dual
|
||||
Public Domain/MIT License conditions described here, as they choose.
|
||||
|
||||
The MIT License is about as close to Public Domain as a license can get, and is
|
||||
described in clear, concise terms at:
|
||||
|
||||
http://en.wikipedia.org/wiki/MIT_License
|
||||
|
||||
The full text of the MIT License follows:
|
||||
|
||||
========================================================================
|
||||
Copyright (c) 2007-2010 Baptiste Lepilleur and The JsonCpp Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use, copy,
|
||||
modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
========================================================================
|
||||
(END LICENSE TEXT)
|
||||
|
||||
The MIT license is compatible with both the GPL and commercial
|
||||
software, affording one all of the rights of Public Domain with the
|
||||
minor nuisance of being required to keep the above copyright notice
|
||||
and license text in the source code. Note also that by accepting the
|
||||
Public Domain "license" you can re-license your copy using whatever
|
||||
license you like.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Optional host headset backend. Faithful builds have no SDK dependency.
|
||||
option(PSX_OPENXR "Build experimental Win32/OpenGL OpenXR backend" OFF)
|
||||
if(PSX_OPENXR)
|
||||
if(NOT WIN32)
|
||||
message(FATAL_ERROR "PSX_OPENXR currently supports Win32/OpenGL")
|
||||
endif()
|
||||
include(FetchContent)
|
||||
set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_API_LAYERS OFF CACHE BOOL "" FORCE)
|
||||
set(BUILD_LOADER ON CACHE BOOL "" FORCE)
|
||||
set(DYNAMIC_LOADER OFF CACHE BOOL "" FORCE)
|
||||
FetchContent_Declare(psx_openxr_sdk
|
||||
GIT_REPOSITORY https://github.com/KhronosGroup/OpenXR-SDK.git
|
||||
GIT_TAG b76b80adaf65ac3ad6cc1ce61974fb29a5d02352)
|
||||
FetchContent_MakeAvailable(psx_openxr_sdk)
|
||||
if(MINGW)
|
||||
# Older MinGW desktop headers omit WINAPI_PARTITION_SYSTEM. The SDK
|
||||
# correctly treats it as zero; leave this vendor warning nonfatal.
|
||||
target_compile_options(openxr_loader PRIVATE -Wno-error=undef)
|
||||
endif()
|
||||
endif()
|
||||
@@ -18,6 +18,7 @@ include("${PSXRECOMP_ROOT}/cmake/psx_runtime_ipo.cmake")
|
||||
|
||||
include("${PSXRECOMP_ROOT}/cmake/psx_dependency_archive.cmake")
|
||||
include("${PSXRECOMP_ROOT}/runtime/chd_dependency.cmake")
|
||||
include("${PSXRECOMP_ROOT}/runtime/openxr_dependency.cmake")
|
||||
include("${PSXRECOMP_ROOT}/runtime/overlay_static_sources.cmake")
|
||||
include("${PSXRECOMP_ROOT}/runtime/netplay_dependency.cmake")
|
||||
|
||||
@@ -351,6 +352,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/gpu_vram_dirty.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/gpu_render.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/gpu_gl_renderer.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_openxr.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/vr_pose_math.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_controller_source.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/gpu_vk_renderer.c
|
||||
@@ -2144,6 +2146,10 @@ function(psxrecomp_add_runtime_target target)
|
||||
RECOMP_UI_PSX_HAS_REWIND=$<BOOL:${PSXRECOMP_HAS_RBENGINE_SNAP}>)
|
||||
endif()
|
||||
|
||||
if(PSX_OPENXR)
|
||||
target_compile_definitions(${target} PRIVATE PSX_OPENXR=1)
|
||||
target_link_libraries(${target} PRIVATE openxr_loader)
|
||||
endif()
|
||||
if(WIN32 OR MINGW)
|
||||
# opengl32: GL backend (gpu_gl_renderer.c). GL 1.x is exported directly
|
||||
# by opengl32; Phase 2b will load modern GL via SDL_GL_GetProcAddress.
|
||||
|
||||
+191
-1
@@ -1,3 +1,5 @@
|
||||
#include "psx_openxr.h"
|
||||
#include "mod_controller_source.h"
|
||||
/*
|
||||
* debug_server.c -- TCP debug server for PSX recomp v4
|
||||
*
|
||||
@@ -8089,6 +8091,182 @@ static void handle_render_pass_refuse(int id, const char *json)
|
||||
(unsigned)psx_mod_render_pass_status());
|
||||
}
|
||||
|
||||
static void handle_openxr_stats(int id, const char *json) {
|
||||
PSXOpenXRStats s; (void)json; psx_openxr_stats(&s);
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"compiled\":%u,\"enabled\":%u,"
|
||||
"\"initialized\":%u,\"running\":%u,\"state\":%u,\"tracking\":%u,"
|
||||
"\"frame_open\":%u,\"result\":%d,\"stage\":\"%s\",\"runtime\":\"%s\","
|
||||
"\"last_failure\":\"%s\",\"last_failure_result\":%d,\"view_flags\":%llu,"
|
||||
"\"waits\":%llu,\"submitted\":%llu,\"empty\":%llu,\"failures\":%llu,"
|
||||
"\"predicted_time\":%llu,\"ipd_m\":%.6f,\"units_per_meter\":%.6f,"
|
||||
"\"gl_version\":%llu,\"min_gl_version\":%llu,\"max_gl_version\":%llu,"
|
||||
"\"submitted_layer\":%u,\"quad_submitted\":%llu,"
|
||||
"\"submitted_source\":%u,\"native_submitted\":%llu,\"submitted_native_frame\":%llu,"
|
||||
"\"quad_distance_m\":%.6f,\"quad_width_m\":%.6f,\"quad_height_m\":%.6f}", id,s.compiled,s.enabled,
|
||||
s.initialized,s.running,s.state,s.tracking,s.frame_open,s.result,
|
||||
s.stage?s.stage:"off",s.runtime,s.last_failure?s.last_failure:"",s.last_failure_result,
|
||||
(unsigned long long)s.view_flags,(unsigned long long)s.waits,
|
||||
(unsigned long long)s.submitted,(unsigned long long)s.empty,
|
||||
(unsigned long long)s.failures,(unsigned long long)s.predicted_time,s.ipd_m,s.units_per_meter,
|
||||
(unsigned long long)s.gl_version,(unsigned long long)s.min_gl_version,
|
||||
(unsigned long long)s.max_gl_version,s.submitted_layer,
|
||||
(unsigned long long)s.quad_submitted,s.submitted_source,
|
||||
(unsigned long long)s.native_submitted,(unsigned long long)s.submitted_native_frame,
|
||||
s.quad_distance_m,s.quad_width_m,s.quad_height_m);
|
||||
}
|
||||
static void handle_openxr_views(int id,const char *json) {
|
||||
PSXOpenXRStats s; (void)json;psx_openxr_stats(&s);
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"predicted_time\":%llu,"
|
||||
"\"submitted_pair_id\":%llu,\"submitted_guest_cycle\":%llu,"
|
||||
"\"submitted_predicted_time\":%llu,\"eyes\":[",id,
|
||||
(unsigned long long)s.predicted_time,(unsigned long long)s.submitted_pair_id,
|
||||
(unsigned long long)s.submitted_guest_cycle,(unsigned long long)s.submitted_predicted_time);
|
||||
for(int e=0;e<2;e++) {
|
||||
PSXModRenderView *v=&s.view[e];
|
||||
send_fmt("%s{\"eye\":%d,\"position_m\":[%.6f,%.6f,%.6f],"
|
||||
"\"orientation_xyzw\":[%.6f,%.6f,%.6f,%.6f],"
|
||||
"\"fov_lrud\":[%.6f,%.6f,%.6f,%.6f],"
|
||||
"\"rotation_q12\":[%d,%d,%d,%d,%d,%d,%d,%d,%d],"
|
||||
"\"translation\":[%d,%d,%d],\"projection_q16\":[%d,%d,%d,%d]}",
|
||||
e?",":"",e,s.pose[e][0],s.pose[e][1],s.pose[e][2],s.pose[e][3],s.pose[e][4],s.pose[e][5],s.pose[e][6],
|
||||
s.fov[e][0],s.fov[e][1],s.fov[e][2],s.fov[e][3],
|
||||
v->rotation_q12[0],v->rotation_q12[1],v->rotation_q12[2],v->rotation_q12[3],v->rotation_q12[4],
|
||||
v->rotation_q12[5],v->rotation_q12[6],v->rotation_q12[7],v->rotation_q12[8],
|
||||
v->translation[0],v->translation[1],v->translation[2],
|
||||
v->fx_q16,v->fy_q16,v->cx_delta_q16,v->cy_delta_q16);
|
||||
}
|
||||
send_fmt("]}");
|
||||
}
|
||||
|
||||
static void handle_openxr_control(int id,const char *json) {
|
||||
if (json_get_int(json,"recenter",0)) psx_mod_openxr_recenter();
|
||||
int enable=json_get_int(json,"enable",-1);
|
||||
if (enable>=0 && !psx_mod_openxr_enable(enable!=0)) { send_err(id,"OpenXR enable refused (not compiled or rendering)");return; }
|
||||
handle_openxr_stats(id,json);
|
||||
}
|
||||
|
||||
/* Snapshot only: querying never syncs actions or consumes a controller sample. */
|
||||
static void handle_openxr_input(int id,const char *json) {
|
||||
(void)json;
|
||||
PSXModOpenXRInput s; psx_openxr_input_snapshot(&s);
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"sequence\":%llu,\"synthetic\":%u,"
|
||||
"\"focused\":%u,\"active\":[%u,%u],\"stick\":[[%.6f,%.6f],[%.6f,%.6f]],"
|
||||
"\"trigger\":[%.6f,%.6f],\"squeeze\":[%.6f,%.6f],"
|
||||
"\"trigger_active\":[%u,%u],\"squeeze_active\":[%u,%u],"
|
||||
"\"buttons\":[%u,%u],\"buttons_active\":[%u,%u],"
|
||||
"\"p1_source\":%d}",id,(unsigned long long)s.sequence,s.synthetic,s.focused,
|
||||
s.active[0],s.active[1],s.stick[0][0],s.stick[0][1],s.stick[1][0],s.stick[1][1],
|
||||
s.trigger[0],s.trigger[1],s.squeeze[0],s.squeeze[1],
|
||||
s.trigger_active[0],s.trigger_active[1],s.squeeze_active[0],s.squeeze_active[1],
|
||||
s.buttons[0],s.buttons[1],s.buttons_active[0],s.buttons_active[1],
|
||||
mod_controller_source_present(0));
|
||||
}
|
||||
/* Read-only controller poses from the eye frame's predicted time. */
|
||||
static void handle_openxr_hands(int id,const char *json) {
|
||||
(void)json;PSXModOpenXRHands s={0};s.struct_size=sizeof s;
|
||||
if(!psx_openxr_hands(&s)){send_err(id,"hand snapshot unavailable");return;}
|
||||
char buf[4096];size_t pos=0;
|
||||
pos+=(size_t)snprintf(buf+pos,sizeof buf-pos,
|
||||
"{\"id\":%d,\"ok\":true,\"sequence\":%llu,\"synthetic\":%u,\"focused\":%u,"
|
||||
"\"origin_valid\":%u,\"predicted_time\":%llu,\"age_ms\":%u,"
|
||||
"\"origin_position_m\":[%.9f,%.9f,%.9f],\"origin_orientation_xyzw\":[%.9f,%.9f,%.9f,%.9f],\"pose\":[",
|
||||
id,(unsigned long long)s.sequence,s.synthetic,s.focused,s.origin_valid,
|
||||
(unsigned long long)s.predicted_time,s.age_ms,
|
||||
s.origin_position_m[0],s.origin_position_m[1],s.origin_position_m[2],
|
||||
s.origin_orientation_xyzw[0],s.origin_orientation_xyzw[1],
|
||||
s.origin_orientation_xyzw[2],s.origin_orientation_xyzw[3]);
|
||||
for(int e=0;e<2;e++) {
|
||||
pos+=(size_t)snprintf(buf+pos,sizeof buf-pos,"%s[",e?",":"");
|
||||
for(int k=0;k<2;k++) {
|
||||
const PSXModTrackedPose *v=&s.pose[e][k];
|
||||
pos+=(size_t)snprintf(buf+pos,sizeof buf-pos,
|
||||
"%s{\"active\":%u,\"flags\":%u,\"position_m\":[%.9f,%.9f,%.9f],"
|
||||
"\"orientation_xyzw\":[%.9f,%.9f,%.9f,%.9f]}",k?",":"",v->active,v->flags,
|
||||
v->position_m[0],v->position_m[1],v->position_m[2],
|
||||
v->orientation_xyzw[0],v->orientation_xyzw[1],v->orientation_xyzw[2],v->orientation_xyzw[3]);
|
||||
}
|
||||
pos+=(size_t)snprintf(buf+pos,sizeof buf-pos,"]");
|
||||
}
|
||||
(void)snprintf(buf+pos,sizeof buf-pos,"]}");debug_server_send_line(buf);
|
||||
}
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
static void handle_openxr_hands_override(int id,const char *json) {
|
||||
if(json_get_int(json,"clear",0)){psx_openxr_hands_override(NULL);handle_openxr_hands(id,json);return;}
|
||||
char hand[16],pose[16];
|
||||
if(!json_get_str(json,"hand",hand,sizeof hand) || !json_get_str(json,"pose",pose,sizeof pose)) {
|
||||
send_err(id,"hand=left|right and pose=grip|aim required");return;
|
||||
}
|
||||
int e=!strcmp(hand,"left")?0:!strcmp(hand,"right")?1:-1;
|
||||
int k=!strcmp(pose,"grip")?0:!strcmp(pose,"aim")?1:-1;
|
||||
int active=json_get_int(json,"active",1),focused=json_get_int(json,"focused",1),flags=json_get_int(json,"flags",15);
|
||||
if(e<0 || k<0 || active<0 || active>1 || focused<0 || focused>1 || flags<0 || flags>15) {
|
||||
send_err(id,"invalid hand, pose, focus, activity or flags");return;
|
||||
}
|
||||
PSXModOpenXRHands s={0};s.struct_size=sizeof s;(void)psx_openxr_hands(&s);
|
||||
if(!s.synthetic){memset(&s,0,sizeof s);s.struct_size=sizeof s;s.origin_valid=1;s.origin_orientation_xyzw[3]=1;}
|
||||
s.focused=(uint32_t)focused;s.predicted_time=0;
|
||||
PSXModTrackedPose *v=&s.pose[e][k];memset(v,0,sizeof *v);
|
||||
v->active=(uint32_t)active;v->flags=(uint32_t)flags;
|
||||
const char *pn[3]={"px_mm","py_mm","pz_mm"},*qn[4]={"qx","qy","qz","qw"};
|
||||
for(int i=0;i<3;i++) {
|
||||
int value=json_get_int(json,pn[i],0);
|
||||
if(value < -100000 || value > 100000){send_err(id,"position must be -100000..100000 mm");return;}
|
||||
v->position_m[i]=(float)value/1000;
|
||||
}
|
||||
for(int i=0;i<4;i++) {
|
||||
int value=json_get_int(json,qn[i],i==3?1000000:0);
|
||||
if(value < -1000000 || value > 1000000){send_err(id,"quaternion components must be -1000000..1000000");return;}
|
||||
v->orientation_xyzw[i]=(float)value/1000000;
|
||||
}
|
||||
if(!psx_openxr_hands_override(&s)){send_err(id,"invalid synthetic pose numbers");return;}
|
||||
handle_openxr_hands(id,json);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Debug-only synthetic action sample; axes are signed thousandths [-1000,1000].
|
||||
* Explicitly tagged so desktop controls cannot be mistaken for device evidence. */
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
static void handle_openxr_input_override(int id,const char *json) {
|
||||
if(json_get_int(json,"clear",0)) psx_openxr_input_override(NULL);
|
||||
else {
|
||||
PSXModOpenXRInput s; memset(&s,0,sizeof s);s.struct_size=sizeof s;
|
||||
int focused=json_get_int(json,"focused",1);
|
||||
int left=json_get_int(json,"left_active",1),right=json_get_int(json,"right_active",1);
|
||||
if(focused<0 || focused>1 || left<0 || left>1 || right<0 || right>1) {
|
||||
send_err(id,"focused/active must be 0 or 1");return;
|
||||
}
|
||||
s.focused=(uint32_t)focused;s.active[0]=(uint32_t)left;s.active[1]=(uint32_t)right;
|
||||
int a[4]={json_get_int(json,"lx",0),json_get_int(json,"ly",0),
|
||||
json_get_int(json,"rx",0),json_get_int(json,"ry",0)};
|
||||
for(int i=0;i<4;i++) {
|
||||
if(a[i]<-1000 || a[i]>1000) { send_err(id,"axis must be -1000..1000");return; }
|
||||
s.stick[i/2][i%2]=(float)a[i]/1000;
|
||||
}
|
||||
const char *t[2]={"left_trigger","right_trigger"}, *g[2]={"left_squeeze","right_squeeze"};
|
||||
const char *ta[2]={"left_trigger_active","right_trigger_active"};
|
||||
const char *ga[2]={"left_squeeze_active","right_squeeze_active"};
|
||||
const char *b[2]={"left_buttons","right_buttons"};
|
||||
const char *ba[2]={"left_buttons_active","right_buttons_active"};
|
||||
for(int e=0;e<2;e++) {
|
||||
int trigger=json_get_int(json,t[e],0),squeeze=json_get_int(json,g[e],0);
|
||||
int trigger_active=json_get_int(json,ta[e],1),squeeze_active=json_get_int(json,ga[e],1);
|
||||
int buttons=json_get_int(json,b[e],0),buttons_active=json_get_int(json,ba[e],15);
|
||||
if(trigger<0 || trigger>1000 || squeeze<0 || squeeze>1000 ||
|
||||
trigger_active<0 || trigger_active>1 || squeeze_active<0 || squeeze_active>1 ||
|
||||
buttons<0 || buttons>15 || buttons_active<0 || buttons_active>15) {
|
||||
send_err(id,"trigger/squeeze must be 0..1000, activity 0|1, click masks 0..15");return;
|
||||
}
|
||||
s.trigger[e]=(float)trigger/1000;s.squeeze[e]=(float)squeeze/1000;
|
||||
s.trigger_active[e]=(uint32_t)trigger_active;s.squeeze_active[e]=(uint32_t)squeeze_active;
|
||||
s.buttons[e]=(uint32_t)buttons;s.buttons_active[e]=(uint32_t)buttons_active;
|
||||
}
|
||||
if(!psx_openxr_input_override(&s)) { send_err(id,"invalid synthetic input");return; }
|
||||
}
|
||||
handle_openxr_input(id,json);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
static void handle_stereo_stats(int id, const char *json) {
|
||||
RenderStereoStats s;
|
||||
GLRenderStereoDiag g;
|
||||
@@ -9235,6 +9413,7 @@ static void handle_video_info(int id, const char *json)
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"backend\":\"%s\",\"preset\":%d,"
|
||||
"\"reference_lines\":%d,\"requested_scale\":%d,\"effective_scale\":%d,"
|
||||
"\"internal_lines\":%u,\"gl\":%d,\"gl_max_dim\":%d,\"gl_max_scale\":%d,"
|
||||
"\"gl_swap_interval\":%d,"
|
||||
"\"gl_clamp_reason\":%d,\"gl_alloc_retries\":%d,\"gl_budget_mib\":%d,"
|
||||
"\"fbo_w\":%d,\"fbo_h\":%d,\"hidpi_window\":%d,\"window_w\":%d,"
|
||||
"\"window_h\":%d,\"drawable_w\":%d,\"drawable_h\":%d,"
|
||||
@@ -9246,7 +9425,7 @@ static void handle_video_info(int id, const char *json)
|
||||
id, gr_backend() == GR_BACKEND_OPENGL ? "opengl"
|
||||
: gr_backend() == GR_BACKEND_VULKAN ? "vulkan" : "software",
|
||||
preset, ref, req, eff, di.height * (unsigned)(eff > 0 ? eff : 1), gl,
|
||||
si.max_dim, si.max_scale, si.clamp_reason, si.alloc_retries, si.budget_mib,
|
||||
si.max_dim, si.max_scale, gl_renderer_get_swap_interval(), si.clamp_reason, si.alloc_retries, si.budget_mib,
|
||||
si.fbo_w, si.fbo_h, hidpi, ww, wh, pw, ph, di.display_x, di.display_y,
|
||||
di.width, di.height,
|
||||
si.hr_scale, si.windowed, si.window_x, si.window_w, si.window_fbo_w,
|
||||
@@ -14271,6 +14450,17 @@ static const CmdEntry s_commands[] = {
|
||||
{ "gl_ws_ablate", handle_gl_ws_ablate },
|
||||
{ "gl_interp", handle_gl_interp },
|
||||
{ "render_pass_stats", handle_render_pass_stats },
|
||||
{ "openxr_stats", handle_openxr_stats },
|
||||
{ "openxr_views", handle_openxr_views },
|
||||
{ "openxr_control", handle_openxr_control },
|
||||
{ "openxr_input", handle_openxr_input },
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
{ "openxr_input_override", handle_openxr_input_override },
|
||||
#endif
|
||||
{ "openxr_hands", handle_openxr_hands },
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
{ "openxr_hands_override", handle_openxr_hands_override },
|
||||
#endif
|
||||
{ "stereo_stats", handle_stereo_stats },
|
||||
{ "stereo_dump", handle_stereo_dump },
|
||||
{ "render_pass_dump", handle_render_pass_dump },
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
#include "latency_ring.h"
|
||||
#include "frame_pacing.h"
|
||||
#include "psx_rewind.h"
|
||||
#include "psx_openxr.h"
|
||||
|
||||
#include "psx_sdl.h"
|
||||
#if defined(PSX_SDL3)
|
||||
@@ -410,6 +411,10 @@ static GLuint s_osd_tex = 0;
|
||||
static int s_osd_tw = 0, s_osd_th = 0;
|
||||
static GLuint s_present_prog = 0, s_present_vao = 0;
|
||||
static void gl_swap_with_osd(void);
|
||||
static int s_native_surface_enabled, s_native_surface_pending;
|
||||
static int s_native_surface_rect[4]; /* Fresh native backbuffer content, GL coordinates. */
|
||||
static double s_native_surface_distance, s_native_surface_width, s_native_surface_units;
|
||||
static void openxr_present_native(void);
|
||||
static GLint s_present_uTex = -1, s_present_uUvRect = -1;
|
||||
static GLint s_present_uTexSize = -1, s_present_uSharpScale = -1;
|
||||
static GLint s_present_uSharp = -1;
|
||||
@@ -1015,6 +1020,10 @@ static uint64_t s_pres_seq = 0;
|
||||
|
||||
static void pres_record(int path, int dx, int dy, int w, int h,
|
||||
int lx, int ly, int lw, int lh) {
|
||||
s_native_surface_pending=s_native_surface_enabled &&
|
||||
(path==GL_PRES_VRAM || path==GL_PRES_WIDE || path==GL_PRES_CPU || path==GL_PRES_BLANK);
|
||||
s_native_surface_rect[0]=lx;s_native_surface_rect[1]=ly;
|
||||
s_native_surface_rect[2]=lw;s_native_surface_rect[3]=lh;
|
||||
/* The ring metadata stays always-on, but pixel probing must not: each
|
||||
* glReadPixels synchronously drains queued GPU work. Two probes per frame
|
||||
* were enough to make Tomba 2 miss its frame budget. */
|
||||
@@ -4626,10 +4635,20 @@ void gl_renderer_set_swap_interval(int interval) {
|
||||
}
|
||||
}
|
||||
}
|
||||
int gl_renderer_get_swap_interval(void) {
|
||||
if(!s_ctx)return -2;
|
||||
#if defined(PSX_SDL3)
|
||||
int interval=0;
|
||||
return SDL_GL_GetSwapInterval(&interval)?interval:-2;
|
||||
#else
|
||||
return SDL_GL_GetSwapInterval();
|
||||
#endif
|
||||
}
|
||||
|
||||
static void pass_resources_release(void);
|
||||
|
||||
void gl_renderer_shutdown(void) {
|
||||
s_native_surface_enabled=s_native_surface_pending=0;
|
||||
pass_resources_release();
|
||||
if (s_ctx) {
|
||||
for (unsigned i = 1; i < 65536u; ++i)
|
||||
@@ -6205,6 +6224,7 @@ static void pass_free_color_fbo(GLuint *tex, GLuint *rb, GLuint *fbo,
|
||||
/* Context teardown (gl_renderer_shutdown): free the pass images, backups
|
||||
* and journal, and forget their names so a new context makes fresh ones. */
|
||||
static void pass_resources_release(void) {
|
||||
psx_openxr_shutdown();
|
||||
stereo_resources_release();
|
||||
pass_gen_release(0);
|
||||
pass_gen_release(1);
|
||||
@@ -6584,6 +6604,119 @@ int gl_renderer_stereo_publish(uint64_t id, uint64_t cycle, const int32_t view[2
|
||||
if (s_stereo_dump_left > 0) stereo_dump(p);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static uint64_t s_xr_begin_pair;
|
||||
int psx_mod_openxr_enable(int enabled) {
|
||||
PSXOpenXRStats s; psx_openxr_stats(&s);
|
||||
if (s_pass_active || s.frame_open) return 0;
|
||||
return psx_openxr_enable(enabled);
|
||||
}
|
||||
void psx_mod_openxr_recenter(void) { psx_openxr_recenter(); }
|
||||
int psx_mod_openxr_quad(double distance,double width,double height) {
|
||||
if(s_pass_active)return 0;
|
||||
return psx_openxr_quad(distance,width,height);
|
||||
}
|
||||
int psx_mod_openxr_native_surface(double distance,double width,double units) {
|
||||
PSXOpenXRStats stats;psx_openxr_stats(&stats);
|
||||
if(s_pass_active || stats.frame_open || !stats.compiled ||
|
||||
!isfinite(distance) || !isfinite(width) || !isfinite(units))return 0;
|
||||
if(distance && (distance<.25 || distance>20 || width<=0 || width>10 || units<1 || units>65536))return 0;
|
||||
s_native_surface_enabled=distance!=0;s_native_surface_pending=0;
|
||||
s_native_surface_distance=distance;s_native_surface_width=width;s_native_surface_units=units;
|
||||
return 1;
|
||||
}
|
||||
int psx_mod_openxr_begin(uint32_t width, uint32_t height, double units) {
|
||||
if (s_native_surface_enabled || s_pass_active || gl_renderer_stereo_unavailable() != PSX_MOD_RENDER_PASS_READY ||
|
||||
!width || !height || width > VRAM_W || height > VRAM_H) return 0;
|
||||
s_xr_begin_pair = s_stereo_valid ? s_stereo_pair[s_stereo_current].id : 0;
|
||||
return psx_openxr_begin((int)width, (int)height, units);
|
||||
}
|
||||
int psx_mod_openxr_view(uint32_t eye, PSXModRenderView *view) {
|
||||
return psx_openxr_view(eye, view);
|
||||
}
|
||||
int psx_mod_openxr_input(PSXModOpenXRInput *input) {
|
||||
if (s_pass_active) return 0;
|
||||
return psx_openxr_input(input);
|
||||
}
|
||||
int psx_mod_openxr_hands(PSXModOpenXRHands *hands) {
|
||||
return psx_openxr_hands(hands); /* Snapshot only, also safe during replay. */
|
||||
}
|
||||
static int openxr_copy_eye(uint32_t eye, uint32_t texture, int w, int h) {
|
||||
StereoPair *p = &s_stereo_pair[s_stereo_current];
|
||||
GLint read_fbo, draw_fbo; GLuint target = 0;
|
||||
GLboolean scissor = glIsEnabled(GL_SCISSOR_TEST);
|
||||
glGetIntegerv(0x8CAA, &read_fbo);
|
||||
glGetIntegerv(0x8CA6, &draw_fbo);
|
||||
p_glGenFramebuffers(1, &target);
|
||||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, target);
|
||||
p_glFramebufferTexture2D(PSXGL_DRAW_FRAMEBUFFER, PSXGL_COLOR_ATTACHMENT0,GL_TEXTURE_2D, texture, 0);
|
||||
int ok = p_glCheckFramebufferStatus(PSXGL_DRAW_FRAMEBUFFER) == PSXGL_FRAMEBUFFER_COMPLETE;
|
||||
if (ok) {
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, p->fbo[eye]);
|
||||
/* Stereo capture texture row convention is opposite the XR layer.
|
||||
* User confirmed inverted headset output with the original direct blit.
|
||||
* Flip only submission; eye dumps and desktop presentation stay intact. */
|
||||
p_glBlitFramebuffer(0, p->th[eye], p->tw[eye], 0, 0, 0, w, h,
|
||||
GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||||
glFlush();
|
||||
}
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, (GLuint)read_fbo);
|
||||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, (GLuint)draw_fbo);
|
||||
if (scissor) glEnable(GL_SCISSOR_TEST);
|
||||
p_glDeleteFramebuffers(1, &target);return ok;
|
||||
}
|
||||
int psx_mod_openxr_end(int rendered) {
|
||||
int fresh = rendered && s_stereo_valid && !s_pass_active &&
|
||||
s_stereo_pair[s_stereo_current].id != s_xr_begin_pair;
|
||||
if (fresh) psx_openxr_pair_metadata(s_stereo_pair[s_stereo_current].id,
|
||||
s_stereo_pair[s_stereo_current].cycle);
|
||||
return psx_openxr_end(fresh, openxr_copy_eye);
|
||||
}
|
||||
|
||||
static int openxr_copy_native(uint32_t eye,uint32_t texture,int w,int h) {
|
||||
GLint read_fbo,draw_fbo,read_buffer;GLuint target=0;
|
||||
GLboolean scissor=glIsEnabled(GL_SCISSOR_TEST);
|
||||
(void)eye;
|
||||
glGetIntegerv(0x8CAA,&read_fbo);glGetIntegerv(0x8CA6,&draw_fbo);
|
||||
glGetIntegerv(GL_READ_BUFFER,&read_buffer);
|
||||
p_glGenFramebuffers(1,&target);p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER,target);
|
||||
p_glFramebufferTexture2D(PSXGL_DRAW_FRAMEBUFFER,PSXGL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,texture,0);
|
||||
int ok=p_glCheckFramebufferStatus(PSXGL_DRAW_FRAMEBUFFER)==PSXGL_FRAMEBUFFER_COMPLETE;
|
||||
if(ok) {
|
||||
int *r=s_native_surface_rect;
|
||||
glDisable(GL_SCISSOR_TEST);p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER,0);glReadBuffer(GL_BACK);
|
||||
/* Default framebuffer is already upright: unlike native VRAM/eye
|
||||
* textures, its bottom GL row is the displayed bottom row. */
|
||||
(void)pass_gl_errors();
|
||||
p_glBlitFramebuffer(r[0],r[1],r[0]+r[2],r[1]+r[3],0,0,w,h,GL_COLOR_BUFFER_BIT,GL_LINEAR);
|
||||
glFlush();ok=pass_gl_errors()==0;
|
||||
}
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER,(GLuint)read_fbo);glReadBuffer((GLenum)read_buffer);
|
||||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER,(GLuint)draw_fbo);
|
||||
if(scissor)glEnable(GL_SCISSOR_TEST);
|
||||
p_glDeleteFramebuffers(1,&target);return ok;
|
||||
}
|
||||
static void openxr_present_native(void) {
|
||||
int ww=0,wh=0,*r=s_native_surface_rect;
|
||||
if(!s_native_surface_enabled || !s_native_surface_pending || s_pass_active)return;
|
||||
s_native_surface_pending=0;
|
||||
SDL_GL_GetDrawableSize(s_win,&ww,&wh);
|
||||
if(r[0]<0 || r[1]<0 || r[2]<1 || r[3]<1 || r[0]+r[2]>ww || r[1]+r[3]>wh)return;
|
||||
/* No replay or retained pair: this exact native present is the source.
|
||||
* Direct lifecycle permits native 24-bit videos as well as 15-bit UI. */
|
||||
/* The shared locator also builds PSX projection matrices, whose domain
|
||||
* is native VRAM dimensions. Quad geometry does not use those matrices;
|
||||
* keep that domain bounded while copying the full drawable rectangle. */
|
||||
if(!psx_openxr_begin(512,240,s_native_surface_units))return;
|
||||
if(!psx_openxr_quad(s_native_surface_distance,s_native_surface_width,
|
||||
s_native_surface_width*(double)r[3]/r[2])) {
|
||||
(void)psx_openxr_end(0,NULL);return;
|
||||
}
|
||||
psx_openxr_native_metadata(s_frame_count);
|
||||
(void)psx_openxr_end(1,openxr_copy_native);
|
||||
}
|
||||
|
||||
static int stereo_present(int w, int h) {
|
||||
StereoPair *p = &s_stereo_pair[s_stereo_current];
|
||||
int ww, wh, lx, ly, lw, lh;
|
||||
@@ -6828,6 +6961,8 @@ static void gl_draw_osd_image(const uint32_t *px, int ow, int oh,
|
||||
|
||||
/* Composite host toast + volume bar into the default framebuffer, then swap. */
|
||||
static void gl_swap_with_osd(void) {
|
||||
openxr_present_native(); /* Copy guest content before host-only overlays. */
|
||||
s_native_surface_pending=0; /* Hold-last/resim cannot reuse a native source. */
|
||||
if (s_present_prog && s_ctx) {
|
||||
int ww = 0, wh = 0;
|
||||
SDL_GL_GetDrawableSize(s_win, &ww, &wh);
|
||||
@@ -7097,7 +7232,7 @@ void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear,
|
||||
flush_tex_batch();
|
||||
flush_cpu_upload();
|
||||
if (stereo_present(w, h)) return;
|
||||
if (s_force_present_remaining <= 0 &&
|
||||
if (!s_native_surface_enabled && s_force_present_remaining <= 0 &&
|
||||
s_last_present_path == GL_PRES_VRAM &&
|
||||
s_last_dx == disp_x && s_last_dy == disp_y &&
|
||||
s_last_dw == w && s_last_dh == h &&
|
||||
@@ -7260,7 +7395,7 @@ int gl_renderer_present_wide_fbo(int disp_x, int disp_y, int disp_h, int linear)
|
||||
flush_tex_batch();
|
||||
flush_cpu_upload();
|
||||
hiw_flush_queue(); /* windowed: queued wide mirrors */
|
||||
if (s_force_present_remaining <= 0 &&
|
||||
if (!s_native_surface_enabled && s_force_present_remaining <= 0 &&
|
||||
s_last_present_path == GL_PRES_WIDE &&
|
||||
s_last_dx == disp_x && s_last_dy == disp_y &&
|
||||
s_last_dw == g_wide_w && s_last_dh == disp_h &&
|
||||
|
||||
@@ -1997,8 +1997,14 @@ static void update_adaptive_widescreen() {
|
||||
* teardown resets them and macOS otherwise supplies a legacy 2.1 context. */
|
||||
static void configure_core_gl_context_attributes() {
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
|
||||
int major = 3, minor = 3;
|
||||
#if defined(PSX_OPENXR)
|
||||
/* Opt-in headset context. Ordinary GL launches keep the historic 3.3. */
|
||||
const char *xr = std::getenv("PSX_OPENXR");
|
||||
if (xr && xr[0] && xr[0] != '0') { major = 4; minor = 6; }
|
||||
#endif
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, major);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, minor);
|
||||
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
||||
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,573 @@
|
||||
/* Opt-in Win32 OpenXR lifecycle. No guest state, console diagnostics or timing
|
||||
* phases: located poses and projection submission belong to one host frame. */
|
||||
#include "psx_openxr.h"
|
||||
#include "vr_pose_math.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <math.h>
|
||||
static PSXOpenXRStats s_stats;
|
||||
static uint64_t s_pair_id,s_pair_cycle;
|
||||
static uint32_t s_source;
|
||||
static uint64_t s_native_frame;
|
||||
static double s_quad_distance,s_quad_width,s_quad_height;
|
||||
static PSXModOpenXRInput s_input;
|
||||
static PSXModOpenXRHands s_hands;
|
||||
static uint64_t s_hands_sequence, s_hands_sample_ms;
|
||||
static void hands_clear(void) {
|
||||
memset(&s_hands,0,sizeof s_hands);s_hands.struct_size=sizeof s_hands;
|
||||
s_hands.sequence=++s_hands_sequence;s_hands.age_ms=UINT32_MAX;
|
||||
s_hands_sample_ms=0;
|
||||
}
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
static int s_input_override;
|
||||
static PSXModOpenXRInput s_injected;
|
||||
static int s_hands_override;
|
||||
static PSXModOpenXRHands s_injected_hands;
|
||||
static int valid_pose_numbers(const double q[4],const double p[3]) {
|
||||
double n=0;
|
||||
for(int i=0;i<4;i++){if(!isfinite(q[i]))return 0;n+=q[i]*q[i];}
|
||||
for(int i=0;i<3;i++)if(!isfinite(p[i]) || fabs(p[i])>100)return 0;
|
||||
return n>=.5 && n<=1.5;
|
||||
}
|
||||
int psx_openxr_hands_override(const PSXModOpenXRHands *hands) {
|
||||
if(!hands){s_hands_override=0;hands_clear();return 1;}
|
||||
if(hands->struct_size!=sizeof *hands || hands->focused>1 || hands->origin_valid>1)return 0;
|
||||
for(int i=0;i<4;i++)if(!isfinite(hands->origin_orientation_xyzw[i]))return 0;
|
||||
for(int i=0;i<3;i++)if(!isfinite(hands->origin_position_m[i]) || fabs(hands->origin_position_m[i])>100)return 0;
|
||||
if(hands->origin_valid && !valid_pose_numbers(hands->origin_orientation_xyzw,hands->origin_position_m))return 0;
|
||||
for(int e=0;e<2;e++)for(int k=0;k<2;k++) {
|
||||
const PSXModTrackedPose *v=&hands->pose[e][k];
|
||||
if(v->active>1 || (v->flags & ~15u))return 0;
|
||||
double q[4],p[3];for(int i=0;i<4;i++)q[i]=v->orientation_xyzw[i];
|
||||
for(int i=0;i<3;i++)p[i]=v->position_m[i];
|
||||
for(int i=0;i<4;i++)if(!isfinite(q[i]))return 0;
|
||||
for(int i=0;i<3;i++)if(!isfinite(p[i]) || fabs(p[i])>100)return 0;
|
||||
if((v->flags & 3u) && !valid_pose_numbers(q,p))return 0;
|
||||
}
|
||||
s_injected_hands=*hands;s_injected_hands.synthetic=1;
|
||||
s_injected_hands.sequence=++s_hands_sequence;s_injected_hands.age_ms=0;
|
||||
for(int e=0;e<2;e++)for(int k=0;k<2;k++)
|
||||
if(!hands->focused || !hands->origin_valid || !hands->pose[e][k].active)
|
||||
memset(&s_injected_hands.pose[e][k],0,sizeof(PSXModTrackedPose));
|
||||
s_hands_override=1;return 1;
|
||||
}
|
||||
int psx_openxr_input_override(const PSXModOpenXRInput *input) {
|
||||
if (!input) { s_input_override=0; memset(&s_injected,0,sizeof s_injected); return 1; }
|
||||
if (input->struct_size!=sizeof *input || input->focused>1 ||
|
||||
input->active[0]>1 || input->active[1]>1) return 0;
|
||||
for(int e=0;e<2;e++) for(int a=0;a<2;a++)
|
||||
if(!isfinite(input->stick[e][a]) || fabsf(input->stick[e][a])>1) return 0;
|
||||
for(int e=0;e<2;e++) {
|
||||
if(input->trigger_active[e]>1 || input->squeeze_active[e]>1 ||
|
||||
(input->buttons[e] & ~PSX_MOD_XR_CLICKS) ||
|
||||
(input->buttons_active[e] & ~PSX_MOD_XR_CLICKS) ||
|
||||
!isfinite(input->trigger[e]) || input->trigger[e]<0 || input->trigger[e]>1 ||
|
||||
!isfinite(input->squeeze[e]) || input->squeeze[e]<0 || input->squeeze[e]>1)return 0;
|
||||
}
|
||||
s_injected=*input; s_input_override=1; return 1;
|
||||
}
|
||||
#endif
|
||||
void psx_openxr_input_snapshot(PSXModOpenXRInput *out) { if(out)*out=s_input; }
|
||||
void psx_openxr_pair_metadata(uint64_t id,uint64_t cycle){s_pair_id=id;s_pair_cycle=cycle;s_source=1;s_native_frame=0;}
|
||||
void psx_openxr_native_metadata(uint64_t frame){s_pair_id=s_pair_cycle=0;s_source=2;s_native_frame=frame;}
|
||||
#if defined(PSX_OPENXR)
|
||||
#include "host_time.h"
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <unknwn.h>
|
||||
#include <GL/gl.h>
|
||||
#define XR_USE_PLATFORM_WIN32
|
||||
#define XR_USE_GRAPHICS_API_OPENGL
|
||||
#include <openxr/openxr.h>
|
||||
#include <openxr/openxr_platform.h>
|
||||
static XrInstance s_instance;
|
||||
static XrSystemId s_system;
|
||||
static XrSession s_session;
|
||||
static XrSpace s_space;
|
||||
static XrSpace s_view_space;
|
||||
static XrActionSet s_actions;
|
||||
static XrAction s_stick;
|
||||
static XrAction s_trigger,s_squeeze,s_click[4];
|
||||
static XrAction s_pose_action[2];
|
||||
static XrSpace s_pose_space[2][2];
|
||||
static XrPath s_hand[2];
|
||||
static XrTime s_origin_reset_time;
|
||||
static XrSwapchain s_chain[2];
|
||||
static XrSwapchainImageOpenGLKHR *s_images[2];
|
||||
static uint32_t s_count[2];
|
||||
static int s_w[2],s_h[2],s_origin_valid;
|
||||
static XrView s_views[2];
|
||||
static PSXModRenderView s_render[2];
|
||||
static double s_oq[4],s_op[3];
|
||||
static XrTime s_time;
|
||||
static int check(XrResult r,const char *stage) {
|
||||
s_stats.result=r;
|
||||
if(XR_FAILED(r)) { s_stats.stage=s_stats.last_failure=stage;s_stats.last_failure_result=r;s_stats.failures++;return 0; }
|
||||
return 1;
|
||||
}
|
||||
static int input_initialize(void) {
|
||||
XrActionSetCreateInfo set={XR_TYPE_ACTION_SET_CREATE_INFO};
|
||||
strcpy(set.actionSetName,"gameplay"); strcpy(set.localizedActionSetName,"Gameplay");
|
||||
if(!check(xrCreateActionSet(s_instance,&set,&s_actions),"action_set"))return 0;
|
||||
if(!check(xrStringToPath(s_instance,"/user/hand/left",&s_hand[0]),"left_hand_path") ||
|
||||
!check(xrStringToPath(s_instance,"/user/hand/right",&s_hand[1]),"right_hand_path"))return 0;
|
||||
XrActionCreateInfo action={XR_TYPE_ACTION_CREATE_INFO};
|
||||
strcpy(action.actionName,"thumbstick"); strcpy(action.localizedActionName,"Thumbstick");
|
||||
action.actionType=XR_ACTION_TYPE_VECTOR2F_INPUT;
|
||||
action.countSubactionPaths=2; action.subactionPaths=s_hand;
|
||||
if(!check(xrCreateAction(s_actions,&action,&s_stick),"stick_action"))return 0;
|
||||
action.actionType=XR_ACTION_TYPE_FLOAT_INPUT;
|
||||
strcpy(action.actionName,"trigger");strcpy(action.localizedActionName,"Trigger");
|
||||
if(!check(xrCreateAction(s_actions,&action,&s_trigger),"trigger_action"))return 0;
|
||||
strcpy(action.actionName,"squeeze");strcpy(action.localizedActionName,"Grip squeeze");
|
||||
if(!check(xrCreateAction(s_actions,&action,&s_squeeze),"squeeze_action"))return 0;
|
||||
const char *names[4]={"primary","secondary","menu","stick_click"};
|
||||
const char *labels[4]={"Primary button","Secondary button","Menu","Stick click"};
|
||||
action.actionType=XR_ACTION_TYPE_BOOLEAN_INPUT;
|
||||
for(int i=0;i<4;i++) {
|
||||
strcpy(action.actionName,names[i]);strcpy(action.localizedActionName,labels[i]);
|
||||
if(!check(xrCreateAction(s_actions,&action,&s_click[i]),names[i]))return 0;
|
||||
}
|
||||
action.actionType=XR_ACTION_TYPE_POSE_INPUT;
|
||||
for(int k=0;k<2;k++) {
|
||||
strcpy(action.actionName,k?"aim_pose":"grip_pose");
|
||||
strcpy(action.localizedActionName,k?"Aim pose":"Grip pose");
|
||||
if(!check(xrCreateAction(s_actions,&action,&s_pose_action[k]),"pose_action"))return 0;
|
||||
for(int e=0;e<2;e++) {
|
||||
XrActionSpaceCreateInfo ci={XR_TYPE_ACTION_SPACE_CREATE_INFO};
|
||||
ci.action=s_pose_action[k];ci.subactionPath=s_hand[e];ci.poseInActionSpace.orientation.w=1;
|
||||
if(!check(xrCreateActionSpace(s_session,&ci,&s_pose_space[e][k]),"pose_space"))return 0;
|
||||
}
|
||||
}
|
||||
XrPath profile;
|
||||
if(!check(xrStringToPath(s_instance,"/interaction_profiles/oculus/touch_controller",&profile),"touch_profile"))return 0;
|
||||
const char *paths[2][7]={
|
||||
{"/user/hand/left/input/thumbstick","/user/hand/left/input/trigger/value",
|
||||
"/user/hand/left/input/squeeze/value","/user/hand/left/input/x/click",
|
||||
"/user/hand/left/input/y/click","/user/hand/left/input/menu/click",
|
||||
"/user/hand/left/input/thumbstick/click"},
|
||||
{"/user/hand/right/input/thumbstick","/user/hand/right/input/trigger/value",
|
||||
"/user/hand/right/input/squeeze/value","/user/hand/right/input/a/click",
|
||||
"/user/hand/right/input/b/click",NULL,"/user/hand/right/input/thumbstick/click"}};
|
||||
XrAction actions[7]={s_stick,s_trigger,s_squeeze,s_click[0],s_click[1],s_click[2],s_click[3]};
|
||||
XrActionSuggestedBinding bindings[17];uint32_t n=0;
|
||||
for(int e=0;e<2;e++)for(int a=0;a<7;a++)if(paths[e][a]) {
|
||||
bindings[n].action=actions[a];
|
||||
if(!check(xrStringToPath(s_instance,paths[e][a],&bindings[n].binding),"touch_input_path"))return 0;
|
||||
n++;
|
||||
}
|
||||
const char *pose_paths[2][2]={{"/user/hand/left/input/grip/pose","/user/hand/left/input/aim/pose"},
|
||||
{"/user/hand/right/input/grip/pose","/user/hand/right/input/aim/pose"}};
|
||||
for(int e=0;e<2;e++)for(int k=0;k<2;k++) {
|
||||
bindings[n].action=s_pose_action[k];
|
||||
if(!check(xrStringToPath(s_instance,pose_paths[e][k],&bindings[n].binding),"pose_binding"))return 0;
|
||||
n++;
|
||||
}
|
||||
XrInteractionProfileSuggestedBinding suggest={XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING};
|
||||
suggest.interactionProfile=profile; suggest.countSuggestedBindings=n; suggest.suggestedBindings=bindings;
|
||||
if(!check(xrSuggestInteractionProfileBindings(s_instance,&suggest),"touch_bindings"))return 0;
|
||||
XrSessionActionSetsAttachInfo attach={XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO};
|
||||
attach.countActionSets=1; attach.actionSets=&s_actions;
|
||||
return check(xrAttachSessionActionSets(s_session,&attach),"attach_actions");
|
||||
}
|
||||
static int initialize(void) {
|
||||
uint32_t count=0;
|
||||
XrExtensionProperties *ext=NULL;
|
||||
if(!check(xrEnumerateInstanceExtensionProperties(NULL,0,&count,NULL),"extensions"))return 0;
|
||||
ext=calloc(count,sizeof *ext); if(!ext){s_stats.stage="extensions_memory";return 0;}
|
||||
for(uint32_t i=0;i<count;i++)ext[i].type=XR_TYPE_EXTENSION_PROPERTIES;
|
||||
int supported=0;
|
||||
if(check(xrEnumerateInstanceExtensionProperties(NULL,count,&count,ext),"extensions"))
|
||||
for(uint32_t i=0;i<count;i++)
|
||||
if(!strcmp(ext[i].extensionName,XR_KHR_OPENGL_ENABLE_EXTENSION_NAME))supported=1;
|
||||
free(ext);
|
||||
if(!supported){s_stats.stage="opengl_extension_missing";return 0;}
|
||||
const char *extensions[]={XR_KHR_OPENGL_ENABLE_EXTENSION_NAME};
|
||||
XrInstanceCreateInfo ci={XR_TYPE_INSTANCE_CREATE_INFO};
|
||||
strcpy(ci.applicationInfo.applicationName,"PSXRecomp stereo");
|
||||
strcpy(ci.applicationInfo.engineName,"psxrecomp");
|
||||
ci.applicationInfo.apiVersion=XR_API_VERSION_1_0;
|
||||
ci.enabledExtensionCount=1;ci.enabledExtensionNames=extensions;
|
||||
if(!check(xrCreateInstance(&ci,&s_instance),"instance"))return 0;
|
||||
XrInstanceProperties ip={XR_TYPE_INSTANCE_PROPERTIES};
|
||||
if(check(xrGetInstanceProperties(s_instance,&ip),"instance_properties"))
|
||||
memcpy(s_stats.runtime,ip.runtimeName,sizeof s_stats.runtime);
|
||||
s_stats.runtime[sizeof s_stats.runtime-1]=0;
|
||||
XrSystemGetInfo si={XR_TYPE_SYSTEM_GET_INFO};si.formFactor=XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY;
|
||||
if(!check(xrGetSystem(s_instance,&si,&s_system),"system"))return 0;
|
||||
PFN_xrGetOpenGLGraphicsRequirementsKHR requirements=NULL;
|
||||
if(!check(xrGetInstanceProcAddr(s_instance,"xrGetOpenGLGraphicsRequirementsKHR",
|
||||
(PFN_xrVoidFunction*)&requirements),"graphics_requirements_proc"))return 0;
|
||||
XrGraphicsRequirementsOpenGLKHR req={XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_KHR};
|
||||
if(!requirements || !check(requirements(s_instance,s_system,&req),"graphics_requirements"))return 0;
|
||||
GLint major=0,minor=0;glGetIntegerv(0x821B,&major);glGetIntegerv(0x821C,&minor);
|
||||
XrVersion version=XR_MAKE_VERSION(major,minor,0);
|
||||
s_stats.gl_version=version;s_stats.min_gl_version=req.minApiVersionSupported;
|
||||
s_stats.max_gl_version=req.maxApiVersionSupported;
|
||||
if(version<req.minApiVersionSupported){s_stats.stage="opengl_version";s_stats.failures++;return 0;}
|
||||
XrGraphicsBindingOpenGLWin32KHR binding={XR_TYPE_GRAPHICS_BINDING_OPENGL_WIN32_KHR};
|
||||
binding.hDC=wglGetCurrentDC();binding.hGLRC=wglGetCurrentContext();
|
||||
if(!binding.hDC || !binding.hGLRC){s_stats.stage="opengl_context";return 0;}
|
||||
XrSessionCreateInfo sci={XR_TYPE_SESSION_CREATE_INFO};sci.next=&binding;sci.systemId=s_system;
|
||||
if(!check(xrCreateSession(s_instance,&sci,&s_session),"session"))return 0;
|
||||
if(!input_initialize())return 0;
|
||||
XrReferenceSpaceCreateInfo space={XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
|
||||
space.referenceSpaceType=XR_REFERENCE_SPACE_TYPE_LOCAL;space.poseInReferenceSpace.orientation.w=1;
|
||||
if(!check(xrCreateReferenceSpace(s_session,&space,&s_space),"local_space"))return 0;
|
||||
XrViewConfigurationView config[2]={{XR_TYPE_VIEW_CONFIGURATION_VIEW},{XR_TYPE_VIEW_CONFIGURATION_VIEW}};
|
||||
if(!check(xrEnumerateViewConfigurationViews(s_instance,s_system,XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO,
|
||||
2,&count,config),"view_config") || count!=2)return 0;
|
||||
uint32_t formats_n=0;
|
||||
if(!check(xrEnumerateSwapchainFormats(s_session,0,&formats_n,NULL),"formats"))return 0;
|
||||
int64_t *formats=calloc(formats_n,sizeof *formats);if(!formats){s_stats.stage="formats_memory";return 0;}
|
||||
supported=0;
|
||||
if(check(xrEnumerateSwapchainFormats(s_session,formats_n,&formats_n,formats),"formats"))
|
||||
for(uint32_t i=0;i<formats_n;i++)if(formats[i]==GL_RGBA8)supported=1;
|
||||
free(formats);
|
||||
if(!supported){s_stats.stage="rgba8_format_missing";return 0;}
|
||||
for(int eye=0;eye<2;eye++) {
|
||||
s_w[eye]=config[eye].recommendedImageRectWidth;s_h[eye]=config[eye].recommendedImageRectHeight;
|
||||
XrSwapchainCreateInfo sc={XR_TYPE_SWAPCHAIN_CREATE_INFO};
|
||||
sc.usageFlags=XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT;sc.format=GL_RGBA8;
|
||||
sc.sampleCount=1;sc.width=s_w[eye];sc.height=s_h[eye];sc.faceCount=1;sc.arraySize=1;sc.mipCount=1;
|
||||
if(!check(xrCreateSwapchain(s_session,&sc,&s_chain[eye]),"swapchain"))return 0;
|
||||
if(!check(xrEnumerateSwapchainImages(s_chain[eye],0,&s_count[eye],NULL),"images"))return 0;
|
||||
s_images[eye]=calloc(s_count[eye],sizeof *s_images[eye]);
|
||||
if(!s_images[eye]){s_stats.stage="images_memory";return 0;}
|
||||
for(uint32_t i=0;i<s_count[eye];i++)s_images[eye][i].type=XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_KHR;
|
||||
if(!check(xrEnumerateSwapchainImages(s_chain[eye],s_count[eye],&s_count[eye],
|
||||
(XrSwapchainImageBaseHeader*)s_images[eye]),"images"))return 0;
|
||||
}
|
||||
s_stats.initialized=1;s_stats.stage="waiting_session";return 1;
|
||||
}
|
||||
static int events(void) {
|
||||
for(;;) {
|
||||
XrEventDataBuffer event={XR_TYPE_EVENT_DATA_BUFFER};
|
||||
XrResult r=xrPollEvent(s_instance,&event);
|
||||
if(r==XR_EVENT_UNAVAILABLE)break;
|
||||
if(!check(r,"events"))return 0;
|
||||
if(event.type==XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED) {
|
||||
XrEventDataSessionStateChanged *e=(void*)&event;
|
||||
s_stats.state=e->state;
|
||||
if(e->state==XR_SESSION_STATE_READY && !s_stats.running) {
|
||||
XrSessionBeginInfo begin={XR_TYPE_SESSION_BEGIN_INFO};
|
||||
begin.primaryViewConfigurationType=XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
|
||||
if(!check(xrBeginSession(s_session,&begin),"begin_session"))return 0;
|
||||
s_stats.running=1;
|
||||
} else if(e->state==XR_SESSION_STATE_STOPPING) {
|
||||
if(!check(xrEndSession(s_session),"end_session"))return 0;
|
||||
s_stats.running=0;
|
||||
} else if(e->state==XR_SESSION_STATE_LOSS_PENDING || e->state==XR_SESSION_STATE_EXITING) {
|
||||
s_stats.enabled=0;s_stats.running=0;s_stats.stage="session_exit";return 0;
|
||||
}
|
||||
} else if(event.type==XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING) {
|
||||
/* Recenter from the next valid located pair. */
|
||||
const XrEventDataReferenceSpaceChangePending *e=(const void*)&event;
|
||||
if(e->referenceSpaceType==XR_REFERENCE_SPACE_TYPE_LOCAL) s_origin_reset_time=e->changeTime;
|
||||
} else if(event.type==XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING) {
|
||||
s_stats.enabled=0;s_stats.running=0;s_stats.stage="instance_loss";return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
static void hands_locate(void) {
|
||||
PSXModOpenXRHands current=s_hands;
|
||||
current.predicted_time=(uint64_t)s_time;
|
||||
current.focused=s_stats.state==XR_SESSION_STATE_FOCUSED && s_input.focused;
|
||||
current.origin_valid=s_origin_valid;
|
||||
memcpy(current.origin_position_m,s_op,sizeof s_op);
|
||||
memcpy(current.origin_orientation_xyzw,s_oq,sizeof s_oq);
|
||||
if(current.focused && current.origin_valid)for(int e=0;e<2;e++)for(int k=0;k<2;k++) {
|
||||
XrActionStateGetInfo get={XR_TYPE_ACTION_STATE_GET_INFO};
|
||||
get.action=s_pose_action[k];get.subactionPath=s_hand[e];
|
||||
XrActionStatePose state={XR_TYPE_ACTION_STATE_POSE};
|
||||
if(!check(xrGetActionStatePose(s_session,&get,&state),"pose_state"))continue;
|
||||
PSXModTrackedPose *v=¤t.pose[e][k];v->active=state.isActive;
|
||||
if(!state.isActive)continue;
|
||||
XrSpaceLocation loc={XR_TYPE_SPACE_LOCATION};
|
||||
XrResult r=xrLocateSpace(s_pose_space[e][k],s_space,s_time,&loc);
|
||||
if(!check(r,"locate_hand") || r!=XR_SUCCESS)continue;
|
||||
v->flags=(uint32_t)loc.locationFlags & 15u;
|
||||
if((v->flags & 3u)!=3u)continue; /* Values remain zero for incomplete poses. */
|
||||
double norm=0;float q[4]={loc.pose.orientation.x,loc.pose.orientation.y,
|
||||
loc.pose.orientation.z,loc.pose.orientation.w};
|
||||
float p[3]={loc.pose.position.x,loc.pose.position.y,loc.pose.position.z};
|
||||
int valid=1;
|
||||
for(int i=0;i<4;i++){if(!isfinite(q[i]))valid=0;norm+=(double)q[i]*q[i];}
|
||||
for(int i=0;i<3;i++)if(!isfinite(p[i]) || fabsf(p[i])>100)valid=0;
|
||||
if(!valid || norm<.5 || norm>1.5){v->flags=0;continue;}
|
||||
memcpy(v->orientation_xyzw,q,sizeof q);memcpy(v->position_m,p,sizeof p);
|
||||
}
|
||||
s_hands=current;s_hands_sample_ms=psx_host_mono_ms();
|
||||
}
|
||||
#endif
|
||||
int psx_openxr_hands(PSXModOpenXRHands *out) {
|
||||
if(!out || out->struct_size!=sizeof *out)return 0;
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
if(s_hands_override){*out=s_injected_hands;return 1;}
|
||||
#endif
|
||||
*out=s_hands;out->struct_size=sizeof *out;out->age_ms=UINT32_MAX;
|
||||
#if defined(PSX_OPENXR)
|
||||
if(s_hands_sample_ms) {
|
||||
uint64_t now=psx_host_mono_ms();
|
||||
uint64_t age=now>=s_hands_sample_ms?now-s_hands_sample_ms:UINT32_MAX;
|
||||
out->age_ms=age<UINT32_MAX?(uint32_t)age:UINT32_MAX;
|
||||
}
|
||||
#endif
|
||||
return 1;
|
||||
}
|
||||
void psx_openxr_stats(PSXOpenXRStats *out) {
|
||||
if(out){*out=s_stats;
|
||||
#if defined(PSX_OPENXR)
|
||||
out->compiled=1;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
void psx_openxr_recenter(void) {
|
||||
hands_clear();
|
||||
#if defined(PSX_OPENXR)
|
||||
s_origin_valid=0;
|
||||
#endif
|
||||
}
|
||||
void psx_openxr_shutdown(void) {
|
||||
#if defined(PSX_OPENXR)
|
||||
if(s_stats.frame_open)psx_openxr_end(0,NULL);
|
||||
for(int eye=0;eye<2;eye++) {
|
||||
for(int k=0;k<2;k++) {
|
||||
if(s_pose_space[eye][k])xrDestroySpace(s_pose_space[eye][k]);
|
||||
s_pose_space[eye][k]=XR_NULL_HANDLE;
|
||||
}
|
||||
if(s_chain[eye])xrDestroySwapchain(s_chain[eye]);
|
||||
s_chain[eye]=XR_NULL_HANDLE;free(s_images[eye]);s_images[eye]=NULL;s_count[eye]=0;
|
||||
}
|
||||
if(s_space)xrDestroySpace(s_space);s_space=XR_NULL_HANDLE;
|
||||
if(s_view_space)xrDestroySpace(s_view_space);s_view_space=XR_NULL_HANDLE;
|
||||
if(s_session)xrDestroySession(s_session);s_session=XR_NULL_HANDLE;
|
||||
if(s_actions)xrDestroyActionSet(s_actions);s_actions=XR_NULL_HANDLE;s_stick=XR_NULL_HANDLE;
|
||||
s_trigger=s_squeeze=XR_NULL_HANDLE;memset(s_click,0,sizeof s_click);
|
||||
memset(s_pose_action,0,sizeof s_pose_action);
|
||||
if(s_instance)xrDestroyInstance(s_instance);s_instance=XR_NULL_HANDLE;
|
||||
s_origin_valid=0;s_origin_reset_time=0;s_stats.initialized=s_stats.running=s_stats.tracking=s_stats.frame_open=0;
|
||||
#endif
|
||||
s_stats.enabled=0;
|
||||
s_quad_distance=s_quad_width=s_quad_height=0;
|
||||
memset(&s_input,0,sizeof s_input);
|
||||
hands_clear();
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
s_input_override=0;
|
||||
s_hands_override=0;memset(&s_injected_hands,0,sizeof s_injected_hands);
|
||||
#endif
|
||||
}
|
||||
int psx_openxr_enable(int enabled) {
|
||||
psx_openxr_shutdown();memset(&s_stats,0,sizeof s_stats);
|
||||
#if defined(PSX_OPENXR)
|
||||
s_stats.compiled=1;s_stats.enabled=enabled!=0;s_stats.stage=enabled?"requested":"off";return 1;
|
||||
#else
|
||||
s_stats.stage="not_compiled";return !enabled;
|
||||
#endif
|
||||
}
|
||||
int psx_openxr_view(uint32_t eye,PSXModRenderView *out) {
|
||||
#if defined(PSX_OPENXR)
|
||||
if(s_stats.frame_open && s_stats.tracking && out && eye<2){*out=s_render[eye];return 1;}
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
int psx_openxr_input(PSXModOpenXRInput *out) {
|
||||
if(!out || out->struct_size!=sizeof *out)return 0;
|
||||
uint64_t seq=s_input.sequence+1;
|
||||
memset(&s_input,0,sizeof s_input);s_input.struct_size=sizeof s_input;s_input.sequence=seq;
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
if(s_input_override) {
|
||||
s_input=s_injected;s_input.sequence=seq;s_input.synthetic=1;
|
||||
for(int e=0;e<2;e++) if(!s_input.focused || !s_input.active[e])
|
||||
s_input.stick[e][0]=s_input.stick[e][1]=0;
|
||||
for(int e=0;e<2;e++) {
|
||||
if(!s_input.focused || !s_input.trigger_active[e])s_input.trigger[e]=0;
|
||||
if(!s_input.focused || !s_input.squeeze_active[e])s_input.squeeze[e]=0;
|
||||
s_input.buttons[e] &= s_input.focused ? s_input.buttons_active[e] : 0;
|
||||
}
|
||||
if(!s_input.focused)hands_clear();
|
||||
*out=s_input;return 1;
|
||||
}
|
||||
#endif
|
||||
#if defined(PSX_OPENXR)
|
||||
if(s_stats.enabled && s_stats.initialized && !s_stats.frame_open && events() && s_stats.running) {
|
||||
XrActiveActionSet active={s_actions,XR_NULL_PATH};
|
||||
XrActionsSyncInfo sync={XR_TYPE_ACTIONS_SYNC_INFO};sync.countActiveActionSets=1;sync.activeActionSets=&active;
|
||||
XrResult r=xrSyncActions(s_session,&sync);
|
||||
if(check(r,"sync_actions") && r==XR_SUCCESS && s_stats.state==XR_SESSION_STATE_FOCUSED) {
|
||||
PSXModOpenXRInput current=s_input;current.focused=1;int valid=1;
|
||||
for(int e=0;e<2;e++) {
|
||||
XrActionStateGetInfo get={XR_TYPE_ACTION_STATE_GET_INFO};get.action=s_stick;get.subactionPath=s_hand[e];
|
||||
XrActionStateVector2f state={XR_TYPE_ACTION_STATE_VECTOR2F};
|
||||
if(!check(xrGetActionStateVector2f(s_session,&get,&state),"stick_state")) { valid=0; break; }
|
||||
current.active[e]=state.isActive;
|
||||
if(state.isActive) {
|
||||
current.stick[e][0]=state.currentState.x;current.stick[e][1]=state.currentState.y;
|
||||
for(int a=0;a<2;a++) if(!isfinite(current.stick[e][a]) || fabsf(current.stick[e][a])>1) valid=0;
|
||||
}
|
||||
XrAction analog[2]={s_trigger,s_squeeze};
|
||||
for(int a=0;a<2;a++) {
|
||||
get.action=analog[a];
|
||||
XrActionStateFloat value={XR_TYPE_ACTION_STATE_FLOAT};
|
||||
if(!check(xrGetActionStateFloat(s_session,&get,&value),"analog_action_state")) { valid=0;break; }
|
||||
float v=value.isActive?value.currentState:0;
|
||||
if(!isfinite(v) || v<0 || v>1) { valid=0;break; }
|
||||
if(a==0){current.trigger_active[e]=value.isActive;current.trigger[e]=v;}
|
||||
else {current.squeeze_active[e]=value.isActive;current.squeeze[e]=v;}
|
||||
}
|
||||
for(int a=0;a<4;a++) {
|
||||
get.action=s_click[a];
|
||||
XrActionStateBoolean value={XR_TYPE_ACTION_STATE_BOOLEAN};
|
||||
if(!check(xrGetActionStateBoolean(s_session,&get,&value),"click_action_state")) { valid=0;break; }
|
||||
if(value.isActive) {
|
||||
current.buttons_active[e] |= 1u<<a;
|
||||
if(value.currentState)current.buttons[e] |= 1u<<a;
|
||||
}
|
||||
}
|
||||
if(!valid)break;
|
||||
}
|
||||
if(valid)s_input=current;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if(!s_input.focused)hands_clear();
|
||||
*out=s_input;return 1; /* Always a fresh neutral sample when unavailable. */
|
||||
}
|
||||
int psx_openxr_begin(int width,int height,double units) {
|
||||
#if defined(PSX_OPENXR)
|
||||
if(!s_stats.enabled || s_stats.frame_open)return 0;
|
||||
s_quad_distance=s_quad_width=s_quad_height=0;
|
||||
s_pair_id=s_pair_cycle=s_native_frame=0;s_source=0;
|
||||
hands_clear();
|
||||
s_stats.units_per_meter=units;
|
||||
if(!s_stats.initialized && !initialize()) {
|
||||
/* Retain the failed producer/result. Avoid repeated costly startup until
|
||||
* explicit re-enable; a disconnected headset is an inspectable state. */
|
||||
psx_openxr_shutdown();return 0;
|
||||
}
|
||||
if(!events() || !s_stats.running)return 0;
|
||||
XrFrameWaitInfo wait={XR_TYPE_FRAME_WAIT_INFO};XrFrameState fs={XR_TYPE_FRAME_STATE};
|
||||
if(!check(xrWaitFrame(s_session,&wait,&fs),"wait_frame"))return 0;
|
||||
s_stats.waits++;s_time=fs.predictedDisplayTime;s_stats.predicted_time=(uint64_t)s_time;
|
||||
XrFrameBeginInfo begin={XR_TYPE_FRAME_BEGIN_INFO};
|
||||
if(!check(xrBeginFrame(s_session,&begin),"begin_frame"))return 0;
|
||||
s_stats.frame_open=1;s_stats.tracking=0;
|
||||
if(!fs.shouldRender){psx_openxr_end(0,NULL);return 0;}
|
||||
XrViewLocateInfo loc={XR_TYPE_VIEW_LOCATE_INFO};loc.viewConfigurationType=XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
|
||||
loc.displayTime=s_time;loc.space=s_space;
|
||||
XrViewState vs={XR_TYPE_VIEW_STATE};uint32_t count=0;
|
||||
memset(s_views,0,sizeof s_views);s_views[0].type=s_views[1].type=XR_TYPE_VIEW;
|
||||
int located=check(xrLocateViews(s_session,&loc,&vs,2,&count,s_views),"locate_views");
|
||||
s_stats.view_flags=(uint64_t)vs.viewStateFlags;
|
||||
if(!located || count!=2 ||
|
||||
(vs.viewStateFlags&(XR_VIEW_STATE_ORIENTATION_VALID_BIT|XR_VIEW_STATE_POSITION_VALID_BIT))!=
|
||||
(XR_VIEW_STATE_ORIENTATION_VALID_BIT|XR_VIEW_STATE_POSITION_VALID_BIT)) {
|
||||
s_stats.stage="tracking_invalid";psx_openxr_end(0,NULL);return 0;
|
||||
}
|
||||
double q[2][4],p[2][3];
|
||||
for(int i=0;i<2;i++) {
|
||||
XrPosef *v=&s_views[i].pose;
|
||||
q[i][0]=v->orientation.x;q[i][1]=v->orientation.y;q[i][2]=v->orientation.z;q[i][3]=v->orientation.w;
|
||||
p[i][0]=v->position.x;p[i][1]=v->position.y;p[i][2]=v->position.z;
|
||||
}
|
||||
if(s_origin_reset_time && s_time>=s_origin_reset_time) {
|
||||
s_origin_valid=0;s_origin_reset_time=0;
|
||||
}
|
||||
if(!s_origin_valid) {
|
||||
memcpy(s_oq,q[0],sizeof s_oq);
|
||||
for(int j=0;j<3;j++)s_op[j]=(p[0][j]+p[1][j])*.5;
|
||||
s_origin_valid=1;
|
||||
}
|
||||
double separation=0;
|
||||
for(int j=0;j<3;j++)separation+=(p[1][j]-p[0][j])*(p[1][j]-p[0][j]);
|
||||
s_stats.ipd_m=(float)sqrt(separation);
|
||||
for(int i=0;i<2;i++) {
|
||||
XrFovf *v=&s_views[i].fov;
|
||||
double f[4]={v->angleLeft,v->angleRight,v->angleUp,v->angleDown};
|
||||
if(!vr_pose_to_view(q[i],p[i],s_oq,s_op,f,units,width,height,&s_render[i])) {
|
||||
s_stats.stage="view_math";psx_openxr_end(0,NULL);return 0;
|
||||
}
|
||||
}
|
||||
for(int i=0;i<2;i++) {
|
||||
for(int j=0;j<3;j++)s_stats.pose[i][j]=(float)p[i][j];
|
||||
for(int j=0;j<4;j++)s_stats.pose[i][j+3]=(float)q[i][j];
|
||||
s_stats.fov[i][0]=s_views[i].fov.angleLeft;s_stats.fov[i][1]=s_views[i].fov.angleRight;
|
||||
s_stats.fov[i][2]=s_views[i].fov.angleUp;s_stats.fov[i][3]=s_views[i].fov.angleDown;
|
||||
s_stats.view[i]=s_render[i];
|
||||
}
|
||||
hands_locate();
|
||||
s_stats.tracking=1;s_stats.stage="located";return 1;
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
int psx_openxr_quad(double distance,double width,double height) {
|
||||
if(!isfinite(distance) || !isfinite(width) || !isfinite(height))return 0;
|
||||
if(distance!=0 && (distance<.25 || distance>20 || width<=0 || width>20 || height<=0 || height>20))return 0;
|
||||
#if defined(PSX_OPENXR)
|
||||
if(!s_stats.frame_open || !s_stats.tracking)return 0;
|
||||
if(distance && !s_view_space) {
|
||||
XrReferenceSpaceCreateInfo space={XR_TYPE_REFERENCE_SPACE_CREATE_INFO};
|
||||
space.referenceSpaceType=XR_REFERENCE_SPACE_TYPE_VIEW;space.poseInReferenceSpace.orientation.w=1;
|
||||
if(!check(xrCreateReferenceSpace(s_session,&space,&s_view_space),"quad_view_space"))return 0;
|
||||
}
|
||||
s_quad_distance=distance;s_quad_width=distance?width:0;s_quad_height=distance?height:0;return 1;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
int psx_openxr_end(int keep,PSXOpenXRCopy copy) {
|
||||
#if defined(PSX_OPENXR)
|
||||
if(!s_stats.frame_open)return 0;
|
||||
XrCompositionLayerProjectionView pv[2]={{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW},{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW}};
|
||||
int complete=keep && copy && s_stats.tracking;
|
||||
int quad=s_quad_distance>0;
|
||||
for(int eye=0;eye<(quad?1:2) && complete;eye++) {
|
||||
uint32_t index=0;
|
||||
XrSwapchainImageAcquireInfo ai={XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
|
||||
if(!check(xrAcquireSwapchainImage(s_chain[eye],&ai,&index),"acquire")){complete=0;break;}
|
||||
XrSwapchainImageWaitInfo wi={XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO};wi.timeout=1000000000;
|
||||
XrResult wr=xrWaitSwapchainImage(s_chain[eye],&wi);
|
||||
/* A timeout leaves ownership acquired. Wait before releasing; never
|
||||
* release an image the runtime has not made writable. */
|
||||
if(wr==XR_TIMEOUT_EXPIRED){wi.timeout=XR_INFINITE_DURATION;wr=xrWaitSwapchainImage(s_chain[eye],&wi);}
|
||||
int writable=check(wr,"wait_image");
|
||||
if(writable) {
|
||||
if(index>=s_count[eye] || !copy(eye,s_images[eye][index].image,s_w[eye],s_h[eye])) {
|
||||
s_stats.stage="copy_image";s_stats.failures++;complete=0;
|
||||
}
|
||||
XrSwapchainImageReleaseInfo ri={XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
|
||||
if(!check(xrReleaseSwapchainImage(s_chain[eye],&ri),"release"))complete=0;
|
||||
} else complete=0;
|
||||
pv[eye].pose=s_views[eye].pose;pv[eye].fov=s_views[eye].fov;
|
||||
pv[eye].subImage.swapchain=s_chain[eye];pv[eye].subImage.imageRect.extent.width=s_w[eye];
|
||||
pv[eye].subImage.imageRect.extent.height=s_h[eye];
|
||||
}
|
||||
XrCompositionLayerProjection layer={XR_TYPE_COMPOSITION_LAYER_PROJECTION};
|
||||
layer.space=s_space;layer.viewCount=2;layer.views=pv;
|
||||
XrCompositionLayerQuad surface={XR_TYPE_COMPOSITION_LAYER_QUAD};
|
||||
surface.space=s_view_space;surface.eyeVisibility=XR_EYE_VISIBILITY_BOTH;
|
||||
surface.subImage=pv[0].subImage;surface.pose.orientation.w=1;
|
||||
surface.pose.position.z=(float)-s_quad_distance;
|
||||
surface.size.width=(float)s_quad_width;surface.size.height=(float)s_quad_height;
|
||||
const XrCompositionLayerBaseHeader *layers[]={quad?(void*)&surface:(void*)&layer};
|
||||
XrFrameEndInfo end={XR_TYPE_FRAME_END_INFO};end.displayTime=s_time;
|
||||
end.environmentBlendMode=XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
|
||||
end.layerCount=complete?1:0;end.layers=complete?layers:NULL;
|
||||
int ok=check(xrEndFrame(s_session,&end),"end_frame");s_stats.frame_open=0;
|
||||
if(ok && complete) {
|
||||
s_stats.submitted++;s_stats.submitted_pair_id=s_pair_id;
|
||||
s_stats.submitted_guest_cycle=s_pair_cycle;s_stats.submitted_predicted_time=(uint64_t)s_time;
|
||||
s_stats.submitted_layer=quad?2:1;
|
||||
s_stats.submitted_source=s_source;s_stats.submitted_native_frame=s_native_frame;
|
||||
if(s_source==2)s_stats.native_submitted++;
|
||||
if(quad){s_stats.quad_submitted++;s_stats.quad_distance_m=s_quad_distance;
|
||||
s_stats.quad_width_m=s_quad_width;s_stats.quad_height_m=s_quad_height;}
|
||||
} else s_stats.empty++;
|
||||
if(ok && complete)s_stats.stage="submitted";
|
||||
s_quad_distance=s_quad_width=s_quad_height=0;
|
||||
return ok && complete;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
@@ -69,7 +69,8 @@ def main():
|
||||
# The renderer's render-pass paths call the journal and VRAM policy in
|
||||
# render_pass_plan.c (self-contained: no other runtime symbols).
|
||||
sources = [("probe", fixture), ("sw", framework / "runtime/src/gpu_sw_renderer.c"),
|
||||
("rp", framework / "runtime/src/render_pass_plan.c")]
|
||||
("rp", framework / "runtime/src/render_pass_plan.c"),
|
||||
("xr", framework / "runtime/src/psx_openxr.c")]
|
||||
for name, source in sources:
|
||||
if run([compiler / "gcc.exe", "-std=c11", "-O2", "-flto", "-DPSX_SDL3=1",
|
||||
"-DPSX_NO_DEBUG_TOOLS=1", *includes, "-c", source,
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
#include "psx_openxr.h"
|
||||
#include <stdio.h>
|
||||
#include <math.h>
|
||||
/* Evaluate side effects and failures even when NDEBUG is defined. */
|
||||
#define CHECK(expr) do { \
|
||||
if (!(expr)) { \
|
||||
fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #expr); \
|
||||
return 1; \
|
||||
} \
|
||||
} while (0)
|
||||
int main(void) {
|
||||
/* UI requests require a live tracked frame. Off/unavailable requests
|
||||
* must leave submission state neutral, including invalid dimensions. */
|
||||
CHECK(!psx_openxr_quad(2,2,1.5));
|
||||
CHECK(!psx_openxr_quad(NAN,2,1.5));
|
||||
CHECK(!psx_openxr_quad(.1,2,1.5));
|
||||
CHECK(!psx_openxr_quad(2,0,1.5));
|
||||
CHECK(!psx_openxr_quad(0,0,0));
|
||||
PSXOpenXRStats stats;psx_openxr_stats(&stats);
|
||||
CHECK(!stats.submitted_layer && !stats.quad_submitted);
|
||||
CHECK(!stats.submitted_source && !stats.native_submitted && !stats.submitted_native_frame);
|
||||
PSXModOpenXRInput p={0},s={0};
|
||||
p.struct_size=sizeof p;
|
||||
CHECK(psx_openxr_input(&p) && !p.focused && p.stick[0][0]==0 && !p.synthetic);
|
||||
s.struct_size=sizeof s;s.focused=1;s.active[0]=s.active[1]=1;
|
||||
s.stick[0][0]=-.75f;s.stick[0][1]=1;s.stick[1][0]=.6f;
|
||||
s.trigger_active[1]=1;s.trigger[1]=.75f;
|
||||
s.squeeze_active[0]=1;s.squeeze[0]=.6f;
|
||||
s.buttons_active[0]=PSX_MOD_XR_PRIMARY|PSX_MOD_XR_MENU;
|
||||
s.buttons[0]=PSX_MOD_XR_PRIMARY|PSX_MOD_XR_SECONDARY|PSX_MOD_XR_MENU;
|
||||
CHECK(psx_openxr_input_override(&s));
|
||||
CHECK(psx_openxr_input(&p) && p.synthetic && p.stick[0][0]==-.75f && p.stick[1][0]==.6f);
|
||||
CHECK(p.trigger[1]==.75f && p.squeeze[0]==.6f);
|
||||
CHECK(p.buttons[0]==(PSX_MOD_XR_PRIMARY|PSX_MOD_XR_MENU));
|
||||
uint64_t seq=p.sequence;psx_openxr_input_snapshot(&p);CHECK(p.sequence==seq);
|
||||
s.focused=0;CHECK(psx_openxr_input_override(&s));
|
||||
CHECK(psx_openxr_input(&p) && p.synthetic && !p.focused && p.stick[0][1]==0 && p.stick[1][0]==0);
|
||||
CHECK(!p.trigger[1] && !p.squeeze[0] && !p.buttons[0]);
|
||||
s.focused=1;s.active[0]=0;CHECK(psx_openxr_input_override(&s));
|
||||
CHECK(psx_openxr_input(&p) && p.stick[0][0]==0 && p.stick[1][0]==.6f);
|
||||
CHECK(p.squeeze[0]==.6f && p.buttons[0]); /* independent of stick activity */
|
||||
s.trigger_active[1]=0;s.squeeze_active[0]=0;s.buttons_active[0]=0;
|
||||
CHECK(psx_openxr_input_override(&s));
|
||||
CHECK(psx_openxr_input(&p) && !p.trigger[1] && !p.squeeze[0] && !p.buttons[0]);
|
||||
s.trigger[0]=NAN;CHECK(!psx_openxr_input_override(&s));s.trigger[0]=0;
|
||||
s.squeeze[0]=1.1f;CHECK(!psx_openxr_input_override(&s));s.squeeze[0]=0;
|
||||
s.buttons_active[0]=16;CHECK(!psx_openxr_input_override(&s));s.buttons_active[0]=0;
|
||||
s.stick[0][0]=NAN;CHECK(!psx_openxr_input_override(&s));
|
||||
s.stick[0][0]=1.1f;CHECK(!psx_openxr_input_override(&s));
|
||||
CHECK(psx_openxr_input_override(0));
|
||||
CHECK(psx_openxr_input(&p) && !p.synthetic && p.stick[1][0]==0);
|
||||
s.stick[0][0]=1;CHECK(psx_openxr_input_override(&s));
|
||||
psx_openxr_shutdown();CHECK(psx_openxr_input(&p) && !p.synthetic && !p.focused);
|
||||
PSXModOpenXRHands h={0},got={0};got.struct_size=sizeof got;
|
||||
CHECK(psx_openxr_hands(&got) && !got.focused && got.age_ms==UINT32_MAX);
|
||||
h.struct_size=sizeof h;h.focused=1;h.origin_valid=1;h.origin_orientation_xyzw[3]=1;
|
||||
h.pose[1][1].active=1;h.pose[1][1].flags=15;
|
||||
h.pose[1][1].orientation_xyzw[3]=1;h.pose[1][1].position_m[0]=.3f;
|
||||
CHECK(psx_openxr_hands_override(&h));
|
||||
CHECK(psx_openxr_hands(&got) && got.synthetic && got.age_ms==0 && got.pose[1][1].position_m[0]==.3f);
|
||||
uint64_t hands_seq=got.sequence;
|
||||
CHECK(psx_openxr_hands(&got) && got.sequence==hands_seq); /* Read only. */
|
||||
h.pose[1][1].orientation_xyzw[3]=0;CHECK(!psx_openxr_hands_override(&h));
|
||||
CHECK(psx_openxr_hands(&got) && got.sequence==hands_seq); /* Reject before mutation. */
|
||||
h.pose[1][1].orientation_xyzw[3]=1;h.pose[1][1].flags=16;CHECK(!psx_openxr_hands_override(&h));
|
||||
h.pose[1][1].flags=1;CHECK(psx_openxr_hands_override(&h)); /* Partial flags retained. */
|
||||
CHECK(psx_openxr_hands(&got) && got.pose[1][1].flags==1);
|
||||
h.pose[1][1].position_m[0]=NAN;CHECK(!psx_openxr_hands_override(&h));h.pose[1][1].position_m[0]=.3f;
|
||||
h.focused=0;CHECK(psx_openxr_hands_override(&h));
|
||||
CHECK(psx_openxr_hands(&got) && !got.focused && !got.pose[1][1].flags && !got.pose[1][1].active);
|
||||
h.focused=1;h.pose[1][1].active=0;CHECK(psx_openxr_hands_override(&h));
|
||||
CHECK(psx_openxr_hands(&got) && !got.pose[1][1].flags);
|
||||
CHECK(psx_openxr_hands_override(NULL));
|
||||
CHECK(psx_openxr_hands(&got) && !got.synthetic && !got.pose[1][1].flags);
|
||||
got.struct_size=0;CHECK(!psx_openxr_hands(&got));got.struct_size=sizeof got;
|
||||
h.pose[1][1].active=1;CHECK(psx_openxr_hands_override(&h));
|
||||
psx_openxr_shutdown();CHECK(psx_openxr_hands(&got) && !got.synthetic && !got.origin_valid);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user