Yves
2026-10-04 05:56:08 +08:00
parent cbe828fcf5
commit ec82c05216
18 changed files with 1567 additions and 8 deletions
+20
View File
@@ -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.
+87
View File
@@ -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.
+2
View File
@@ -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).
+9 -2
View File
@@ -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.
+7
View File
@@ -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)
+1
View File
@@ -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
+60
View File
@@ -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);
+48
View File
@@ -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
+270
View File
@@ -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.
+21
View File
@@ -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()
+6
View File
@@ -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
View File
@@ -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 },
+137 -2
View File
@@ -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 &&
+8 -2
View File
@@ -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);
}
+573
View File
@@ -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=&current.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
}
+2 -1
View File
@@ -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,
+79
View File
@@ -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;
}