Initial public release: OVRPlugin→OpenXR interoperability shim

An independent reimplementation of Meta's libOVRPlugin ABI on top of OpenXR, so
VrApi-era Meta Quest VR titles can run on non-Meta OpenXR runtimes (Monado,
Steam Frame) instead of being locked to Meta hardware. Original code only — no
Meta/Epic/Capcom binaries, headers, or assets. Includes a desktop harness that
drives the shim against Monado headless.

Scope/legal: interoperability; entitlement handling is out of scope. See README
for the legal/scope section and docs/ for the research trail and design notes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6sFYGXZPsq3v7xtcDES6g
This commit is contained in:
Daniel LynchandClaude Opus 4.8 committed 2026-06-29 00:48:48 -04:00
commit a72a79ad29
57 files changed
+9304

No files matched your search

+36
View File
@@ -0,0 +1,36 @@
# Local working directories — third-party / personal material, never committed.
# dump/ = the user's own dumped game files (see README: patch-only, dump-your-own)
# ue_src/ = third-party sources consulted under their own licenses, not redistributable
# backup/ = user save data
dump/
ue_src/
backup/
ghidra_proj/
# Raw reverse-engineering output (derivative of Meta's proprietary binary).
# Keep only the plain symbol-NAME lists (the public OVRPlugin API surface).
analysis/*
!analysis/all_exports.txt
!analysis/shim_surface.txt
# Large third-party toolchains (fetched locally, not redistributed)
# (re-include our own desktop OpenXR harness source)
tools/*
!tools/desktop-harness/
# Vendored third-party headers (fetched by scripts/fetch_deps.sh)
shim/third_party/
# Build artifacts / secrets
shim/build/
build/
packaging/out/
packaging/work/
packaging/libs/
*.so
*.apk
*.o
*.keystore
*.zip
*.tar.gz
save_backup/
+51
View File
@@ -0,0 +1,51 @@
# Target host platform — Steam Frame
Researched 2026-06-23. Answers "can the dumped APK run on Steam Frame, and do we
need Android given SteamOS is Linux?"
## Do we need the Android side? YES.
The game is an **Android binary**, not a Linux one — ARM64==ARM64 does NOT bridge:
- `libUE4.so` links **bionic** libc (ABI-incompatible with SteamOS glibc).
- Depends on Android system libs: liblog, libandroid (ANativeActivity,
AAssetManager, input), libOpenSLES; boots via a Java/JNI NativeActivity.
- Reads OBB assets via Android AssetManager/storage paths.
=> Cannot run the .so bare on SteamOS. Must run inside an Android runtime.
Only Android-free path = full native source recompile (no source -> not viable).
## The host pieces all exist (and are open)
- **Lepton** = Valve's official Android-on-Linux layer; a **Waydroid/AOSP fork
built specifically to run Quest APKs on Steam Frame**, with sideloading. APKs
run **native ARM64, no emulation** (the "Waydroid needs x86" caveat is about
Waydroid on x86 PCs; Frame is ARM so it doesn't apply). Walkabout Mini Golf
(Quest title) already cited running on it.
- **Monado** = open OpenXR runtime, runs on **Linux AND Android**, Vulkan
compositor using VK_KHR_external_memory_fd / external_semaphore_fd (matches our
Vulkan-renderer finding).
## Architecture
```
Steam Frame (SteamOS / Arch Linux, ARM64)
└─ Lepton (AOSP/Waydroid container, native ARM64)
└─ RE4 VR APK (unmodified bionic Android binary)
├─ libUE4.so → [SHIM libOVRPlugin] → OpenXR → Monado → Frame compositor (Vulkan)
└─ ovr_* Platform SDK → out of scope (no entitlement code ships in this repo —
a valid entitlement is the user's responsibility; see README "Legal / scope")
```
## Why the shim IS the project
Meta ended VrApi support 2022-08-31; OpenXR is the only supported Quest API and
Valve's whole stack is OpenXR (Monado). So:
- OpenXR Quest games -> Lepton+Monado likely run them with little/no work.
- VrApi games (RE4 VR) -> won't: Lepton/AOSP will never ship Meta's proprietary
libvrapi.so, so the unmodified game finds no VR runtime. The OVRPlugin->OpenXR
shim is exactly what bridges a dead-API VrApi game to Frame's OpenXR stack.
## Remaining real unknowns (gated on Frame shipping ~summer 2026)
1. Does Lepton expose an OpenXR loader+runtime to apps INSIDE the container?
(Almost certainly yes for the OpenXR-Quest-game use case; ride on it.)
2. Can a SIDELOADED app reach the runtime + compositor (perms across the Waydroid
boundary)?
3. **Likely the real technical crux:** sharing Vulkan swapchain images from inside
the Lepton container out to the host Monado/Frame compositor
(VK_KHR_external_memory_fd across the container GPU boundary). May be moot if
Monado's compositor runs inside the container.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Daniel Lynch
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.
+75
View File
@@ -0,0 +1,75 @@
# ovrplugin-openxr-shim
A from-scratch reimplementation of Meta's `libOVRPlugin.so` on top of **OpenXR**,
so legacy VrApi/OVRPlugin-based Meta Quest titles can run on standard OpenXR
runtimes (Monado, and eventually Valve's **Steam Frame** under Lepton).
**Status:** *Resident Evil 4 VR* boots and is playable on a Quest 2 through this
shim — stereo rendering, head + controller tracking, buttons, grips, haptics, and
save loading all work. (Developed as a preservation / interoperability experiment.)
## What it is
Quest's `libOVRPlugin.so` is the C shim Unreal/Unity games call to talk to Meta's
VR runtime. Meta deprecated the underlying VrApi in 2022 and the whole modern stack
(incl. Steam Frame's Monado) is OpenXR-only, so VrApi-era titles have no runtime on
non-Meta OpenXR platforms. This project re-exports the `ovrp_*` C API backed by
OpenXR instead, as a **drop-in replacement** `libOVRPlugin.so`:
```
game (libUE4.so) ──ovrp_* C API──> [THIS SHIM] ──OpenXR──> runtime (Monado / Meta / …)
```
It implements the OpenXR instance/session lifecycle, the Vulkan graphics binding,
the frame loop + swapchains, layer compositing, and action-based input — mapping all
of it to the `ovrp_*` ABI the game expects.
## Legal / scope
- This repo contains **only original code**. It does **not** include or redistribute
any game, the Meta runtime, Meta's headers, or Epic's UnrealEngine source. You
must build the shim yourself and apply it to a copy of a game **you legally own and
dump yourself** (patch-only, dump-your-own — like ROM-hack patches).
- Reimplementing an API for interoperability is the goal here; no proprietary binaries
or decompiled source are published.
- Entitlement/ownership checks are **out of scope**: this project ships no circumvention code
and circumvents nothing. On Quest the platform's real entitlement check runs unchanged (you
own the title). Running on hardware with no Meta backend requires a valid entitlement by
other means — that is the user's responsibility and not provided here.
- Not affiliated with or endorsed by Meta, Capcom, Epic Games, or Valve. All
trademarks belong to their owners.
- Provided as-is, no warranty. You are responsible for compliance with applicable law and
the terms of any software you use it with, in your jurisdiction.
## Build
```sh
scripts/fetch_deps.sh # OpenXR + Vulkan headers (Apache-2.0), Android NDK
shim/build_android.sh # -> shim/build/arm64/libOVRPlugin.so
```
Needs: Android NDK (r27c), a JDK, and the OpenXR/Vulkan headers (the fetch script
gets them). A host x86-64 build is also supported for compile-validation.
## Use (with your own dumped game)
```sh
packaging/repack.sh /path/to/your/base.apk # swap the shim in, re-sign
adb install -r packaging/out/<game>-shim.apk
# push your own dumped OBB, then launch on a dev-mode Quest
```
See `packaging/README.md` and `TESTING.md` for the full flow.
## Layout
- `shim/src/` — the implementation: `xr_runtime` (session/frame loop/swapchains),
`vk_session` (Vulkan binding + ext), `layers`, `xr_input` (action sets), `core`
(the ovrp_* entry points), `android_init`, generated `stubs`.
- `shim/include/ovrplugin_shim.h` — the `ovrp_*` C ABI (clean-room from observed ABI).
- `packaging/` — repack/sign tooling.
- `docs/` — research notes + session handoffs (`docs/README.md` narrates how the
frame-pacing "ghost" was solved); `TESTING.md`/`HOST.md` at root.
## Acknowledgements
Built against the [OpenXR](https://www.khronos.org/openxr/) and
[Vulkan](https://www.vulkan.org/) specs and the Khronos OpenXR loader.
+115
View File
@@ -0,0 +1,115 @@
# Testing the shim — strategy & plan
We do NOT need Steam Frame to validate the hard part. The shim's whole job is
OVRPlugin -> OpenXR, and **the Quest 2 already runs an OpenXR runtime** (Meta's
Horizon OS runtime — the one that replaced VrApi). So the shim can be tested on
hardware we own, today.
## The three paths
| Path | Tests what | Available | Effort |
|------|-----------|-----------|--------|
| **A. Quest 2 + shim swap** | the real shim, real HW, real game, on Meta's OpenXR runtime | now | NDK arm64 build + Android instance handshake + manifest (real entitlement — you own it) |
| **B. Monado-sim harness** | the shim's OpenXR call logic, fast iteration | now | small C harness + Linux arm64 (VM on the Apple-Silicon MacBook) |
| **C. Steam Frame + Lepton** | the actual target (Monado under Lepton) | ~summer 2026 | everything |
Recommended order: **B for fast logic iteration, then A for the real proof.**
A is the thesis-validator; if RE4 renders on the Quest through our OpenXR shim
instead of libvrapi.so, the project is essentially proven.
---
## Path A — Quest 2 (the real test)
Idea: build the shim as an Android arm64 `.so`, drop it into the RE4 APK in place
of the real `libOVRPlugin.so`, sideload, run. Our shim calls the Quest's own
`libopenxr_loader` -> Meta's OpenXR runtime.
Prereqs to do first (these are the currently-open work items):
1. **NDK arm64 build** of the shim — DONE. `shim/build_android.sh` -> NDK r27c ->
build/arm64/libOVRPlugin.so (aarch64, 438/438 drop-in, NEEDED libopenxr_loader).
2. **Instance handshake** — DONE. src/android_init.c: JNI_OnLoad captures the JavaVM;
xrr_pre_init calls xrInitializeLoaderKHR + enables XR_KHR_android_create_instance
+ chains XrInstanceCreateInfoAndroidKHR. Activity from Initialize5 arg4 with an
Application-context reflection fallback. [VERIFY-ON-HW] whether Meta's runtime
accepts the Application context vs requiring the real Activity, and the
PreInitialize3-creates-instance-before-activity ordering.
3. **Manifest** — add the OpenXR usage declarations Meta's runtime expects
(`<uses-feature android:name="android.hardware.vr.headtracking">` already there;
add OpenXR `<meta-data>`/intent bits per Meta's OpenXR mobile docs).
4. **Entitlement** — on Quest you OWN RE4 and the Quest has the real Meta Horizon
platform service, so leave the ORIGINAL libovrplatformloader.so untouched and only
swap libOVRPlugin.so. Logged into the owning account, the real ovr_Entitlement check
passes legitimately ("you own it"). **CONFIRMED on device 2026-06-29:** a
debug-re-signed, legit-mode build (original libovrplatformloader.so, no stub) launches
into the game on a Quest 2 — re-signing does **not** break the entitlement check.
Entitlement handling on hardware with no Meta backend (e.g. Steam Frame) is out of
scope for this repo and is the user's responsibility.
Then:
```
# repack (you own the copy; patch-only distribution)
unzip base.apk -d apk/
cp shim/build/arm64/libOVRPlugin.so apk/lib/arm64-v8a/libOVRPlugin.so
# rebuild + zipalign + sign with your own debug key, then:
adb install -r re4vr-shim.apk # or push OBB + sideload
adb logcat | grep -iE 'xrr|OVRPlugin|openxr' # watch the [xrr] logs
```
Expected first-run signal: instance/session create succeed in logcat; if the frame
loop spins and the projection layer submits, you get an image (even if poses/input
are rough). Known rough edges on first run: depth (Unsupported), input (controllers
return NotYetImplemented), the [VERIFY-ON-HW] swapchain index lockstep.
## Path B — Monado simulated (fast iteration, works on the Mac)
Monado has a simulated/headless HMD driver — no real headset. Run it in a Linux
arm64 VM (UTM/QEMU on Apple Silicon), point an OpenXR loader at it, and run the
harness (tests/harness.c) which drives the ovrp_* sequence:
PreInitialize3 -> Initialize5 -> SetupLayer -> [WaitToBeginFrame -> BeginFrame4 ->
GetNodePoseState3 -> EndFrame4] xN -> Shutdown2
and asserts each returns ovrpSuccess. This exercises the real OpenXR calls without
RE4 or hardware. Build:
```
# inside the Linux arm64 env, with Monado + openxr loader installed:
cc -std=c11 -Iinclude -Ithird_party/openxr tests/harness.c \
src/*.c -lopenxr_loader -lvulkan -o harness # needs a Vulkan device/headless
XR_RUNTIME_JSON=/path/to/monado/openxr_monado-dev.json ./harness
```
Note: Initialize5 needs real Vulkan handles; for a pure-logic smoke test the harness
can pass a headless VkInstance/Device (or we add a "no-gfx" build flag that skips
xrCreateSession to test the non-rendering calls first).
## Path C — Steam Frame (the target)
Same arm64 shim `.so`, but the APK runs under **Lepton** (Valve's Waydroid/AOSP) and
the OpenXR runtime is **Monado**. Once Frame ships: NDK build -> repack -> sideload
into Lepton -> the open question is whether Lepton exposes the OpenXR loader +
Vulkan swapchain sharing across the container (see HOST.md unknowns). Path A having
worked makes this mostly a packaging/runtime-plumbing exercise.
---
## Current state (what's ready to test vs not)
WIRED & building (host x86-64, validation only):
- Session lifecycle: instance/system/session create, event-driven state machine.
- Frame loop: xrWaitFrame/Begin/EndFrame, predicted display time.
- Poses: xrLocateViews (eyes) + xrLocateSpace (head).
- Swapchains: xrCreateSwapchain from ovrpLayerDesc, enumerate VkImages, per-frame
acquire/wait/release, real XrCompositionLayerProjection submit.
- 20 OpenXR functions; 239/239 ovrp_ symbols.
NOT yet (the to-do list before a meaningful Quest run):
- arm64/Android NDK build (host build only so far).
- [ANDROID-TODO] JavaVM/activity -> XrInstanceCreateInfoAndroidKHR + xrInitializeLoaderKHR.
- Input: controller/hand action sets (GetControllerState4 returns NotYetImplemented).
- Depth layer (SetupLayerDepth -> Unsupported).
- Map ovrp_GetInstance/DeviceExtensionsVk -> xrGetVulkan*ExtensionsKHR (so the app
creates its VkInstance/Device with the runtime's required extensions).
- [VERIFY-ON-HW] swapchain index lockstep assumption.
## Pick-up-tomorrow shortlist
1. Install Android NDK; cross-build the shim to arm64 (proves it builds for target).
2. Stand up the Monado-sim Linux arm64 VM on the Mac + run tests/harness.c (Path B
smoke test of the non-gfx calls).
3. Then start the [ANDROID-TODO] instance handshake (gates the real Quest run).
+438
View File
@@ -0,0 +1,438 @@
ovrp_AddCustomMetadata
ovrp_AutoThreadScheduling
ovrp_BeginFrame
ovrp_BeginFrame2
ovrp_BeginFrame3
ovrp_BeginFrame4
ovrp_CalculateEyeLayerDesc
ovrp_CalculateEyeLayerDesc2
ovrp_CalculateEyeLayerDesc3
ovrp_CalculateEyePreviewRect
ovrp_CalculateEyeViewportRect
ovrp_CalculateLayerDesc
ovrp_CloseCameraDevice
ovrp_ConvertPoseToCameraSpace
ovrp_DestroyDistortionWindow
ovrp_DestroyDistortionWindow2
ovrp_DestroyEyeTexture
ovrp_DestroyLayer
ovrp_DestroyMirrorTexture
ovrp_DestroyMirrorTexture2
ovrp_DismissHSW
ovrp_DoesCameraDeviceSupportDepth
ovrp_EndEye
ovrp_EndEye2
ovrp_EndFrame
ovrp_EndFrame2
ovrp_EndFrame3
ovrp_EndFrame4
ovrp_EnqueueDestroyLayer
ovrp_EnqueueSetupLayer
ovrp_EnqueueSetupLayer2
ovrp_EnqueueSubmitLayer
ovrp_EnqueueSubmitLayer2
ovrp_EnumerateAllCameraDevices
ovrp_EnumerateAvailableCameraDevices
ovrp_GetActiveController
ovrp_GetActiveController2
ovrp_GetAdapterId
ovrp_GetAdaptiveGpuPerformanceScale
ovrp_GetAdaptiveGpuPerformanceScale2
ovrp_GetAppAsymmetricFov
ovrp_GetAppChromaticCorrection
ovrp_GetAppCPUPriority
ovrp_GetAppCPUPriority2
ovrp_GetAppCpuStartToGpuEndTime
ovrp_GetAppCpuStartToGpuEndTime2
ovrp_GetAppFramerate
ovrp_GetAppFramerate2
ovrp_GetAppHasInputFocus
ovrp_GetAppHasSystemOverlayPresent
ovrp_GetAppHasVrFocus
ovrp_GetAppHasVrFocus2
ovrp_GetAppLatencyTimings
ovrp_GetAppLatencyTimings2
ovrp_GetAppMonoscopic
ovrp_GetAppPerfStats
ovrp_GetAppPerfStats2
ovrp_GetAppShouldQuit
ovrp_GetAppShouldQuit2
ovrp_GetAppShouldRecenter
ovrp_GetAppShouldRecenter2
ovrp_GetAppShouldRecreateDistortionWindow
ovrp_GetAppShouldRecreateDistortionWindow2
ovrp_GetAppSRGB
ovrp_GetASWAdaptiveMode
ovrp_GetASWDepthScale
ovrp_GetASWEnable
ovrp_GetASWVelocityScale
ovrp_GetAudioInDeviceId
ovrp_GetAudioInDeviceId2
ovrp_GetAudioInId
ovrp_GetAudioInId2
ovrp_GetAudioOutDeviceId
ovrp_GetAudioOutDeviceId2
ovrp_GetAudioOutId
ovrp_GetAudioOutId2
ovrp_GetBatteryStatus
ovrp_GetBoundaryConfigured
ovrp_GetBoundaryConfigured2
ovrp_GetBoundaryDimensions
ovrp_GetBoundaryDimensions2
ovrp_GetBoundaryGeometry
ovrp_GetBoundaryGeometry2
ovrp_GetBoundaryGeometry3
ovrp_GetBoundaryVisible
ovrp_GetBoundaryVisible2
ovrp_GetBufferCount
ovrp_GetCameraDeviceColorFrameBgraPixels
ovrp_GetCameraDeviceColorFrameSize
ovrp_GetCameraDeviceDepthConfidencePixels
ovrp_GetCameraDeviceDepthFramePixels
ovrp_GetCameraDeviceDepthFrameSize
ovrp_GetCameraDeviceDepthSensingMode
ovrp_GetCameraDeviceIntrinsicsParameters
ovrp_GetCameraDevicePreferredDepthQuality
ovrp_GetCaps
ovrp_GetCaps2
ovrp_GetConnectedControllers
ovrp_GetConnectedControllers2
ovrp_GetControllerHapticsDesc
ovrp_GetControllerHapticsDesc2
ovrp_GetControllerHapticsState
ovrp_GetControllerHapticsState2
ovrp_GetControllerState
ovrp_GetControllerState2
ovrp_GetControllerState3
ovrp_GetControllerState4
ovrp_GetCurrentTrackingTransformPose
ovrp_GetDepthCompositingSupported
ovrp_GetDesiredEyeTextureFormat
ovrp_GetDeviceExtensionsVk
ovrp_GetDisplayAdapterId
ovrp_GetDisplayAdapterId2
ovrp_GetDominantHand
ovrp_GetExternalCameraCalibrationRawPose
ovrp_GetExternalCameraCount
ovrp_GetExternalCameraExtrinsics
ovrp_GetExternalCameraIntrinsics
ovrp_GetExternalCameraName
ovrp_GetExternalCameraPose
ovrp_GetEyeAcceleration
ovrp_GetEyeFovLayerId
ovrp_GetEyeFrustum
ovrp_GetEyeOcclusionMesh
ovrp_GetEyeOcclusionMeshEnabled
ovrp_GetEyePixelsPerTanAngleAtCenter
ovrp_GetEyePixelsPerTanAngleAtCenter2
ovrp_GetEyePose
ovrp_GetEyePreviewRect
ovrp_GetEyeRecommendedResolutionScale
ovrp_GetEyeTextureArrayEnabled
ovrp_GetEyeTextureArraySupported
ovrp_GetEyeTextureArraySupported2
ovrp_GetEyeTextureFlippedY
ovrp_GetEyeTextureScale
ovrp_GetEyeTextureShared
ovrp_GetEyeTextureSize
ovrp_GetEyeTextureStageCount
ovrp_GetEyeVelocity
ovrp_GetEyeViewportScale
ovrp_GetFloat
ovrp_GetGPUFrameTime
ovrp_GetGPUUtilLevel
ovrp_GetGPUUtilSupported
ovrp_GetHandNodePoseStateLatency
ovrp_GetHandState
ovrp_GetHandState2
ovrp_GetHandTrackingEnabled
ovrp_GetHeadphonesPresent
ovrp_GetHeadPoseModifier
ovrp_GetHmdColorDesc
ovrp_GetHmdToEyeOffset
ovrp_GetHmdToEyeOffset2
ovrp_GetInitialized
ovrp_GetInputState
ovrp_GetInstanceExtensionsVk
ovrp_GetLayerAndroidSurfaceObject
ovrp_GetLayerOcclusionMesh
ovrp_GetLayerTexture
ovrp_GetLayerTexture2
ovrp_GetLayerTextureFoveation
ovrp_GetLayerTexturePtr
ovrp_GetLayerTextureSpaceWarp
ovrp_GetLayerTextureStageCount
ovrp_GetLocalTrackingSpaceRecenterCount
ovrp_GetMesh
ovrp_GetMixedRealityInitialized
ovrp_GetNativePointer
ovrp_GetNativeSDKPointer
ovrp_GetNativeSDKPointer2
ovrp_GetNativeSDKVersion
ovrp_GetNativeSDKVersion2
ovrp_GetNodeAcceleration
ovrp_GetNodeAcceleration2
ovrp_GetNodeFrustum
ovrp_GetNodeFrustum2
ovrp_GetNodeOrientationTracked
ovrp_GetNodeOrientationTracked2
ovrp_GetNodeOrientationValid
ovrp_GetNodePose
ovrp_GetNodePose2
ovrp_GetNodePoseState
ovrp_GetNodePoseState2
ovrp_GetNodePoseState3
ovrp_GetNodePoseStateRaw
ovrp_GetNodePositionTracked
ovrp_GetNodePositionTracked2
ovrp_GetNodePositionValid
ovrp_GetNodePresent
ovrp_GetNodePresent2
ovrp_GetNodeVelocity
ovrp_GetNodeVelocity2
ovrp_GetPerfMetricsFloat
ovrp_GetPerfMetricsInt
ovrp_GetPredictedDisplayTime
ovrp_GetReorientHMDOnControllerRecenter
ovrp_GetSkeleton
ovrp_GetStatus
ovrp_GetStatus2
ovrp_GetString
ovrp_GetSystemBatteryLevel
ovrp_GetSystemBatteryLevel2
ovrp_GetSystemBatteryStatus
ovrp_GetSystemBatteryStatus2
ovrp_GetSystemBatteryTemperature
ovrp_GetSystemBatteryTemperature2
ovrp_GetSystemCpuLevel
ovrp_GetSystemCpuLevel2
ovrp_GetSystemDisplayAvailableFrequencies
ovrp_GetSystemDisplayFrequency
ovrp_GetSystemDisplayFrequency2
ovrp_GetSystemGpuLevel
ovrp_GetSystemGpuLevel2
ovrp_GetSystemHeadphonesPresent
ovrp_GetSystemHeadphonesPresent2
ovrp_GetSystemHeadsetType
ovrp_GetSystemHeadsetType2
ovrp_GetSystemHmd3DofModeEnabled
ovrp_GetSystemMultiViewSupported
ovrp_GetSystemMultiViewSupported2
ovrp_GetSystemPowerSavingMode
ovrp_GetSystemPowerSavingMode2
ovrp_GetSystemProductName
ovrp_GetSystemProductName2
ovrp_GetSystemRecommendedMSAALevel
ovrp_GetSystemRecommendedMSAALevel2
ovrp_GetSystemRegion
ovrp_GetSystemRegion2
ovrp_GetSystemVolume
ovrp_GetSystemVolume2
ovrp_GetSystemVSyncCount
ovrp_GetSystemVSyncCount2
ovrp_GetTiledMultiResDynamic
ovrp_GetTiledMultiResLevel
ovrp_GetTiledMultiResSupported
ovrp_GetTimeInSeconds
ovrp_GetTrackerFrustum
ovrp_GetTrackerPose
ovrp_GetTrackingCalibratedOrigin
ovrp_GetTrackingCalibratedOrigin2
ovrp_GetTrackingIPDEnabled
ovrp_GetTrackingIPDEnabled2
ovrp_GetTrackingOrientationEnabled
ovrp_GetTrackingOrientationEnabled2
ovrp_GetTrackingOrientationSupported
ovrp_GetTrackingOrientationSupported2
ovrp_GetTrackingOriginType
ovrp_GetTrackingOriginType2
ovrp_GetTrackingPositionEnabled
ovrp_GetTrackingPositionEnabled2
ovrp_GetTrackingPositionSupported
ovrp_GetTrackingPositionSupported2
ovrp_GetTrackingTransformRawPose
ovrp_GetTrackingTransformRelativePose
ovrp_GetUseOverriddenExternalCameraFov
ovrp_GetUseOverriddenExternalCameraStaticPose
ovrp_GetUserEyeDepth
ovrp_GetUserEyeHeight
ovrp_GetUserEyeHeight2
ovrp_GetUserIPD
ovrp_GetUserIPD2
ovrp_GetUserNeckEyeDistance
ovrp_GetUserNeckEyeDistance2
ovrp_GetUserPresent
ovrp_GetUserPresent2
ovrp_GetVersion
ovrp_GetVersion2
ovrp_GetViewportStencil
ovrp_GetVrApiPropertyInt
ovrp_HasCameraDeviceOpened2
ovrp_Initialize
ovrp_Initialize2
ovrp_Initialize3
ovrp_Initialize4
ovrp_Initialize5
ovrp_InitializeMixedReality
ovrp_IsCameraDeviceAvailable2
ovrp_IsCameraDeviceColorFrameAvailable2
ovrp_IsCameraDeviceDepthFrameAvailable
ovrp_IsPerfMetricsSupported
ovrp_IsRequestingASWData
ovrp_Media_CreateCustomCameraAnchor
ovrp_Media_DestroyCustomCameraAnchor
ovrp_Media_EncodeMrcFrame
ovrp_Media_EncodeMrcFrameDualTexturesWithPoseTime
ovrp_Media_EncodeMrcFrameWithDualTextures
ovrp_Media_EncodeMrcFrameWithPoseTime
ovrp_Media_EnumerateCameraAnchorHandles
ovrp_Media_GetCameraAnchorHandle
ovrp_Media_GetCameraAnchorName
ovrp_Media_GetCameraAnchorType
ovrp_Media_GetCameraMinMaxDistance
ovrp_Media_GetCurrentCameraAnchorHandle
ovrp_Media_GetCustomCameraAnchorPose
ovrp_Media_GetInitialized
ovrp_Media_GetMrcActivationMode
ovrp_Media_GetMrcAudioSampleRate
ovrp_Media_GetMrcFrameImageFlipped
ovrp_Media_GetMrcFrameInverseAlpha
ovrp_Media_GetMrcFrameSize
ovrp_Media_GetMrcInputVideoBufferType
ovrp_Media_Initialize
ovrp_Media_IsMrcActivated
ovrp_Media_IsMrcEnabled
ovrp_Media_SetAvailableQueueIndexVulkan
ovrp_Media_SetCameraMinMaxDistance
ovrp_Media_SetCustomCameraAnchorPose
ovrp_Media_SetHeadsetControllerPose
ovrp_Media_SetMrcActivationMode
ovrp_Media_SetMrcAudioSampleRate
ovrp_Media_SetMrcFrameImageFlipped
ovrp_Media_SetMrcFrameInverseAlpha
ovrp_Media_SetMrcFrameSize
ovrp_Media_SetMrcInputVideoBufferType
ovrp_Media_Shutdown
ovrp_Media_SyncMrcFrame
ovrp_Media_Update
ovrp_Media_UseMrcDebugCamera
ovrp_OpenCameraDevice
ovrp_OverrideExternalCameraFov
ovrp_OverrideExternalCameraStaticPose
ovrp_PreInitialize
ovrp_PreInitialize2
ovrp_PreInitialize3
ovrp_RecenterPose
ovrp_RecenterTrackingOrigin
ovrp_RecenterTrackingOrigin2
ovrp_RecreateEyeTexture
ovrp_ReleaseEyeTexture
ovrp_ResetAppPerfStats
ovrp_ResetAppPerfStats2
ovrp_ResetDefaultExternalCamera
ovrp_SendEvent
ovrp_SendEvent2
ovrp_SetAppAsymmetricFov
ovrp_SetAppChromaticCorrection
ovrp_SetAppCPUPriority
ovrp_SetAppCPUPriority2
ovrp_SetAppEngineInfo
ovrp_SetAppEngineInfo2
ovrp_SetAppIgnoreVrFocus
ovrp_SetAppMonoscopic
ovrp_SetAppSRGB
ovrp_SetASWAdaptiveMode
ovrp_SetASWEnable
ovrp_SetBoundaryVisible
ovrp_SetBoundaryVisible2
ovrp_SetCameraDeviceDepthSensingMode
ovrp_SetCameraDevicePreferredColorFrameSize
ovrp_SetCameraDevicePreferredDepthQuality
ovrp_SetCaps
ovrp_SetClientColorDesc
ovrp_SetColorScaleAndOffset
ovrp_SetControllerHaptics
ovrp_SetControllerHaptics2
ovrp_SetControllerVibration
ovrp_SetControllerVibration2
ovrp_SetDefaultExternalCamera
ovrp_SetDepthCompositingInfo
ovrp_SetDepthProjInfo
ovrp_SetDesiredEyeTextureFormat
ovrp_SetDeveloperMode
ovrp_SetExternalCameraProperties
ovrp_SetEyeOcclusionMeshEnabled
ovrp_SetEyeTexture
ovrp_SetEyeTextureArrayEnabled
ovrp_SetEyeTextureFlippedY
ovrp_SetEyeTextureScale
ovrp_SetEyeTextureShared
ovrp_SetEyeViewportScale
ovrp_SetFloat
ovrp_SetHandNodePoseStateLatency
ovrp_SetHeadPoseModifier
ovrp_SetInhibitSystemUX
ovrp_SetInhibitSystemUX2
ovrp_SetNodePositionTracked
ovrp_SetNodePositionTracked2
ovrp_SetOctilinearInfo
ovrp_SetOverlayQuad
ovrp_SetOverlayQuad2
ovrp_SetOverlayQuad3
ovrp_SetRemoteHandedness
ovrp_SetReorientHMDOnControllerRecenter
ovrp_SetSystemCpuLevel
ovrp_SetSystemCpuLevel2
ovrp_SetSystemDisplayFrequency
ovrp_SetSystemGpuLevel
ovrp_SetSystemGpuLevel2
ovrp_SetSystemVSyncCount
ovrp_SetSystemVSyncCount2
ovrp_SetThreadPerformance
ovrp_SetTiledMultiResDynamic
ovrp_SetTiledMultiResLevel
ovrp_SetTrackingCalibratedOrigin2
ovrp_SetTrackingIPDEnabled
ovrp_SetTrackingIPDEnabled2
ovrp_SetTrackingOrientationEnabled
ovrp_SetTrackingOrientationEnabled2
ovrp_SetTrackingOriginType
ovrp_SetTrackingOriginType2
ovrp_SetTrackingPositionEnabled
ovrp_SetTrackingPositionEnabled2
ovrp_SetupDisplayObjects
ovrp_SetupDisplayObjects2
ovrp_SetupDistortionWindow
ovrp_SetupDistortionWindow2
ovrp_SetupDistortionWindow3
ovrp_SetupEyeTexture
ovrp_SetupEyeTexture2
ovrp_SetupLayer
ovrp_SetupLayerDepth
ovrp_SetupMirrorTexture
ovrp_SetupMirrorTexture2
ovrp_SetUserEyeDepth
ovrp_SetUserEyeHeight
ovrp_SetUserEyeHeight2
ovrp_SetUserIPD
ovrp_SetUserIPD2
ovrp_SetUserNeckEyeDistance
ovrp_SetUserNeckEyeDistance2
ovrp_SetVrApiPropertyFloat
ovrp_SetVrApiPropertyInt
ovrp_ShowSystemUI
ovrp_ShowSystemUI2
ovrp_ShowUI
ovrp_Shutdown
ovrp_Shutdown2
ovrp_ShutdownMixedReality
ovrp_TestBoundaryNode
ovrp_TestBoundaryNode2
ovrp_TestBoundaryPoint
ovrp_TestBoundaryPoint2
ovrp_Update
ovrp_Update2
ovrp_Update3
ovrp_UpdateCameraDevices
ovrp_UpdateExternalCamera
ovrp_WaitToBeginFrame
+239
View File
@@ -0,0 +1,239 @@
ovrp_AddCustomMetadata
ovrp_AutoThreadScheduling
ovrp_BeginFrame4
ovrp_CalculateEyeLayerDesc2
ovrp_CalculateEyePreviewRect
ovrp_CalculateEyeViewportRect
ovrp_CalculateLayerDesc
ovrp_CloseCameraDevice
ovrp_ConvertPoseToCameraSpace
ovrp_DestroyDistortionWindow2
ovrp_DestroyLayer
ovrp_DestroyMirrorTexture2
ovrp_DoesCameraDeviceSupportDepth
ovrp_EndFrame4
ovrp_EnumerateAllCameraDevices
ovrp_EnumerateAvailableCameraDevices
ovrp_GetActiveController2
ovrp_GetAdaptiveGpuPerformanceScale2
ovrp_GetAppCPUPriority2
ovrp_GetAppCpuStartToGpuEndTime2
ovrp_GetAppFramerate2
ovrp_GetAppHasInputFocus
ovrp_GetAppHasSystemOverlayPresent
ovrp_GetAppHasVrFocus2
ovrp_GetAppLatencyTimings2
ovrp_GetAppPerfStats2
ovrp_GetAppShouldQuit2
ovrp_GetAppShouldRecenter2
ovrp_GetAppShouldRecreateDistortionWindow2
ovrp_GetASWAdaptiveMode
ovrp_GetASWDepthScale
ovrp_GetASWVelocityScale
ovrp_GetAudioInDeviceId2
ovrp_GetAudioInId2
ovrp_GetAudioOutDeviceId2
ovrp_GetAudioOutId2
ovrp_GetBoundaryConfigured2
ovrp_GetBoundaryDimensions2
ovrp_GetBoundaryGeometry3
ovrp_GetBoundaryVisible2
ovrp_GetCameraDeviceColorFrameBgraPixels
ovrp_GetCameraDeviceColorFrameSize
ovrp_GetCameraDeviceDepthConfidencePixels
ovrp_GetCameraDeviceDepthFramePixels
ovrp_GetCameraDeviceDepthFrameSize
ovrp_GetCameraDeviceDepthSensingMode
ovrp_GetCameraDeviceIntrinsicsParameters
ovrp_GetCameraDevicePreferredDepthQuality
ovrp_GetConnectedControllers2
ovrp_GetControllerHapticsDesc2
ovrp_GetControllerHapticsState2
ovrp_GetControllerState4
ovrp_GetCurrentTrackingTransformPose
ovrp_GetDepthCompositingSupported
ovrp_GetDeviceExtensionsVk
ovrp_GetDisplayAdapterId2
ovrp_GetDominantHand
ovrp_GetExternalCameraCalibrationRawPose
ovrp_GetExternalCameraCount
ovrp_GetExternalCameraExtrinsics
ovrp_GetExternalCameraIntrinsics
ovrp_GetExternalCameraName
ovrp_GetExternalCameraPose
ovrp_GetEyeFovLayerId
ovrp_GetEyePixelsPerTanAngleAtCenter2
ovrp_GetEyeTextureArraySupported2
ovrp_GetGPUFrameTime
ovrp_GetGPUUtilLevel
ovrp_GetGPUUtilSupported
ovrp_GetHandNodePoseStateLatency
ovrp_GetHandState
ovrp_GetHandState2
ovrp_GetHandTrackingEnabled
ovrp_GetHeadPoseModifier
ovrp_GetHmdColorDesc
ovrp_GetHmdToEyeOffset2
ovrp_GetInitialized
ovrp_GetInstanceExtensionsVk
ovrp_GetLayerAndroidSurfaceObject
ovrp_GetLayerOcclusionMesh
ovrp_GetLayerTexture2
ovrp_GetLayerTextureFoveation
ovrp_GetLayerTextureStageCount
ovrp_GetLocalTrackingSpaceRecenterCount
ovrp_GetMesh
ovrp_GetMixedRealityInitialized
ovrp_GetNativeSDKPointer2
ovrp_GetNativeSDKVersion2
ovrp_GetNodeFrustum2
ovrp_GetNodeOrientationTracked2
ovrp_GetNodeOrientationValid
ovrp_GetNodePoseState3
ovrp_GetNodePoseStateRaw
ovrp_GetNodePositionTracked2
ovrp_GetNodePositionValid
ovrp_GetNodePresent2
ovrp_GetPerfMetricsFloat
ovrp_GetPerfMetricsInt
ovrp_GetPredictedDisplayTime
ovrp_GetReorientHMDOnControllerRecenter
ovrp_GetSkeleton
ovrp_GetSystemCpuLevel2
ovrp_GetSystemDisplayAvailableFrequencies
ovrp_GetSystemDisplayFrequency2
ovrp_GetSystemGpuLevel2
ovrp_GetSystemHeadsetType2
ovrp_GetSystemHmd3DofModeEnabled
ovrp_GetSystemMultiViewSupported2
ovrp_GetSystemPowerSavingMode2
ovrp_GetSystemProductName2
ovrp_GetSystemRecommendedMSAALevel2
ovrp_GetSystemRegion2
ovrp_GetSystemVSyncCount2
ovrp_GetTiledMultiResDynamic
ovrp_GetTiledMultiResLevel
ovrp_GetTiledMultiResSupported
ovrp_GetTimeInSeconds
ovrp_GetTrackingCalibratedOrigin2
ovrp_GetTrackingIPDEnabled2
ovrp_GetTrackingOrientationEnabled2
ovrp_GetTrackingOrientationSupported2
ovrp_GetTrackingOriginType2
ovrp_GetTrackingPositionEnabled2
ovrp_GetTrackingPositionSupported2
ovrp_GetTrackingTransformRawPose
ovrp_GetTrackingTransformRelativePose
ovrp_GetUseOverriddenExternalCameraFov
ovrp_GetUseOverriddenExternalCameraStaticPose
ovrp_GetUserEyeHeight2
ovrp_GetUserIPD2
ovrp_GetUserNeckEyeDistance2
ovrp_GetUserPresent2
ovrp_GetVersion2
ovrp_GetViewportStencil
ovrp_GetVrApiPropertyInt
ovrp_HasCameraDeviceOpened2
ovrp_Initialize5
ovrp_InitializeMixedReality
ovrp_IsCameraDeviceAvailable2
ovrp_IsCameraDeviceColorFrameAvailable2
ovrp_IsCameraDeviceDepthFrameAvailable
ovrp_IsPerfMetricsSupported
ovrp_IsRequestingASWData
ovrp_Media_CreateCustomCameraAnchor
ovrp_Media_DestroyCustomCameraAnchor
ovrp_Media_EncodeMrcFrame
ovrp_Media_EncodeMrcFrameDualTexturesWithPoseTime
ovrp_Media_EncodeMrcFrameWithDualTextures
ovrp_Media_EncodeMrcFrameWithPoseTime
ovrp_Media_EnumerateCameraAnchorHandles
ovrp_Media_GetCameraAnchorHandle
ovrp_Media_GetCameraAnchorName
ovrp_Media_GetCameraAnchorType
ovrp_Media_GetCameraMinMaxDistance
ovrp_Media_GetCurrentCameraAnchorHandle
ovrp_Media_GetCustomCameraAnchorPose
ovrp_Media_GetInitialized
ovrp_Media_GetMrcActivationMode
ovrp_Media_GetMrcAudioSampleRate
ovrp_Media_GetMrcFrameImageFlipped
ovrp_Media_GetMrcFrameInverseAlpha
ovrp_Media_GetMrcFrameSize
ovrp_Media_GetMrcInputVideoBufferType
ovrp_Media_Initialize
ovrp_Media_IsMrcActivated
ovrp_Media_IsMrcEnabled
ovrp_Media_SetAvailableQueueIndexVulkan
ovrp_Media_SetCameraMinMaxDistance
ovrp_Media_SetCustomCameraAnchorPose
ovrp_Media_SetHeadsetControllerPose
ovrp_Media_SetMrcActivationMode
ovrp_Media_SetMrcAudioSampleRate
ovrp_Media_SetMrcFrameImageFlipped
ovrp_Media_SetMrcFrameInverseAlpha
ovrp_Media_SetMrcFrameSize
ovrp_Media_SetMrcInputVideoBufferType
ovrp_Media_Shutdown
ovrp_Media_SyncMrcFrame
ovrp_Media_Update
ovrp_Media_UseMrcDebugCamera
ovrp_OpenCameraDevice
ovrp_OverrideExternalCameraFov
ovrp_OverrideExternalCameraStaticPose
ovrp_PreInitialize3
ovrp_RecenterTrackingOrigin2
ovrp_ResetAppPerfStats2
ovrp_ResetDefaultExternalCamera
ovrp_SendEvent
ovrp_SendEvent2
ovrp_SetAppCPUPriority2
ovrp_SetAppEngineInfo2
ovrp_SetASWAdaptiveMode
ovrp_SetBoundaryVisible2
ovrp_SetCameraDeviceDepthSensingMode
ovrp_SetCameraDevicePreferredColorFrameSize
ovrp_SetCameraDevicePreferredDepthQuality
ovrp_SetClientColorDesc
ovrp_SetColorScaleAndOffset
ovrp_SetControllerHaptics2
ovrp_SetControllerVibration2
ovrp_SetDefaultExternalCamera
ovrp_SetDeveloperMode
ovrp_SetHandNodePoseStateLatency
ovrp_SetHeadPoseModifier
ovrp_SetInhibitSystemUX2
ovrp_SetNodePositionTracked2
ovrp_SetRemoteHandedness
ovrp_SetReorientHMDOnControllerRecenter
ovrp_SetSystemCpuLevel2
ovrp_SetSystemDisplayFrequency
ovrp_SetSystemGpuLevel2
ovrp_SetSystemVSyncCount2
ovrp_SetThreadPerformance
ovrp_SetTiledMultiResDynamic
ovrp_SetTiledMultiResLevel
ovrp_SetTrackingCalibratedOrigin2
ovrp_SetTrackingIPDEnabled2
ovrp_SetTrackingOrientationEnabled2
ovrp_SetTrackingOriginType2
ovrp_SetTrackingPositionEnabled2
ovrp_SetupDisplayObjects2
ovrp_SetupDistortionWindow3
ovrp_SetupLayer
ovrp_SetupLayerDepth
ovrp_SetupMirrorTexture2
ovrp_SetUserEyeHeight2
ovrp_SetUserIPD2
ovrp_SetUserNeckEyeDistance2
ovrp_SetVrApiPropertyFloat
ovrp_SetVrApiPropertyInt
ovrp_ShowSystemUI2
ovrp_Shutdown2
ovrp_ShutdownMixedReality
ovrp_TestBoundaryNode2
ovrp_TestBoundaryPoint2
ovrp_Update3
ovrp_UpdateCameraDevices
ovrp_UpdateExternalCamera
ovrp_WaitToBeginFrame
+43
View File
@@ -0,0 +1,43 @@
# RE4 VR shim — docs
Research trail and session handoffs for the OVRPlugin→OpenXR shim that runs RE4 VR
on non-Meta OpenXR runtimes (Quest 2 today; Steam Frame the goal).
- **`handoffs/`** — chronological session handoffs (the day-by-day journey).
- **`research/`** — reverse-engineering notes, design docs, the ghost-fix writeup, and
`related-work.md` (how Overport/others run Quest games elsewhere; why this shim is novel).
- `../analysis/` — raw RE dumps (`*.txt`, mostly gitignored; `all_exports.txt` +
`shim_surface.txt` feed `shim/gen_stubs.sh`).
- Operational docs live at repo root: `README.md`, `TESTING.md`, `HOST.md`.
## The "ghost" — how it was actually solved (the short version)
For weeks the headline bug was a **VR "ghost"**: doubled/tripled hands and watches,
seated-mode deform, standing-mode black flashes. It survived every theory it *looked*
like — stereo geometry, FOV/IPD, depth reprojection, render-submit ordering, GPU load.
The break came from building **P4 passthru** (`research/ghost-fix-2026-06-27.md`):
running the real Meta `libOVRPlugin` *inside* our app proved native is flawless, so the
bug was in **our path**. Then always-on anomaly logging caught the real signature —
**dropped frames** — and a recording + frame-blending showed it was **whole-frame
temporal judder**, not a stereo/geometry artifact. Localizing the stall showed UE's
render thread blocking ~85–150ms during motion at only ~14ms of GPU work.
Root cause: the shim left `ovrp_WaitToBeginFrame` a **no-op** and ran `xrWaitFrame` on
the **render** thread, so the game thread was unpaced and UE's pipelined renderer
desynced → render-thread stalls → dropped frames → the compositor's timewarp filled the
gaps → judder. **Fix: pace the game thread like vrapi** (`xrWaitFrame` in
`xrr_wait_frame`, frameState handed to the render thread via a 1:1 FIFO ring). Result:
render stall 150→3ms, drops 0.79% (~native 0.34%), steady 72Hz — ghost gone.
Commits `79476c3` (fix) + `c0ecff3` (XR_FRAME_DISCARDED guard).
Secondary wins kept as defaults: game-driven FFR (apply the game's foveation), and the
per-frame luma readback off.
## Reading order if you're new
1. `research/ghost-fix-2026-06-27.md` — the full diagnosis chain (start here).
2. `handoffs/` newest → oldest — the journey, including the dead ends (reproject,
submit-ordering, depth) that were ruled out.
3. `research/RECON.md`, `RE-NOTES.md`, `SHIM-SCOPE.md` — how the shim was reverse-engineered.
4. `research/render-submit-sync-design.md` — the render/submit sync design.
+74
View File
@@ -0,0 +1,74 @@
# RE4 VR Shim — Handoff (2026-06-24 session)
Entry point for the next session. Detailed investigation log: `NEXT-SESSION-LATENCY.md`.
## TL;DR
- The head-motion **ghosting is frame drops** caused by the synchronous tile-memory
**flush-wait serializing CPU↔GPU** (proven: removing it on the title dropped frame
time 13–36ms → ~3ms and killed the ghosting).
- **Shipped fix (always on): perf levels.** We were no-op'ing the game's CPU/GPU level
requests; now forwarded via `XR_EXT_performance_settings`. Confirmed on device: GPU
clock 305→587MHz, CPU steady 2419MHz, framerate up. The default build is playable
(synchronous path + perf fix).
- **Copy-ring (the real serialization fix) is built but blocked** on an MSAA-resolve
interaction in gameplay — see below. Default OFF (`debug.re4vr.copyring`).
- **Caveat — the title still ghosts INITIALLY in every build we tried.** Default build:
title ghosting reduced by the perf fix but not gone. Copy-ring on: title ghosts at
first, then "looks good after a while" (settles, not clean from frame 1). Likely
cause of the *initial* ghosting in both: the title's **asset streaming / load hitches**
(the ~49ms `CPU&GPU` spikes in VrApi, Stale frames), which NONE of the fixes address.
So: perf fix + copy-ring attack the steady-state flush-wait drops; the initial
load-driven hitching is a separate, still-open cause.
## Build / deploy
```
./shim/build_android.sh && ./packaging/repack.sh && adb install -r packaging/out/re4vr-shim.apk
adb logcat -d -s xrr # shim logs
```
Device = Quest 2 (USB serial <redacted-serial>, or wireless <redacted-ip>:5555).
Package `com.Armature.VR4`, activity `com.epicgames.ue4.GameActivity`.
## Runtime debug toggles (system props; relaunch unless noted)
- `debug.re4vr.copyring 1` — shim copy-ring (eye-only; gameplay black, see below)
- `debug.re4vr.sscap 1` — cap supersample 1.2x→1.0x (free GPU/bandwidth)
- `debug.re4vr.diag 1|2|3` — 1=projection only(drop quads), 2=force mono, 3=2x zoom (live)
- `debug.re4vr.dump N` — one-shot eye-texture readback to /sdcard/Android/data/com.Armature.VR4/files/
- `debug.re4vr.depth 1` — DEAD END (UE passes None; Meta ignores plain KHR depth)
- `debug.re4vr.pipeline 1` — DEAD END (render-ahead by holding OpenXR images; breaks UE stage lockstep)
- `debug.re4vr.noflushwait 1` — perf probe: skip flush-wait (renders black; FPS recovers → confirms serialization)
## The open problem: copy-ring gameplay = MSAA resolve into our shim
RenderDoc (offline replay over USB) showed:
- UE renders the eye with **2× MSAA** into its own target, render pass **resolve
attachment = our copy-ring shim** (the image we copy to OpenXR). No stage mismatch.
- On the **title** that resolve lands → copy-ring works. In **gameplay** the resolved
shim isn't the scene at our copy (white/garbage) → black/flashing.
- So: UE's MSAA resolve into our **externally-created** shim VkImage works on the title
but not in gameplay. Next suspects: the shim's `MUTABLE_FORMAT`/sRGB-vs-UNORM resolve
view, create flags vs a valid resolve-dst, or Adreno tile-MSAA specifics. Single-shim
made it worse (UE needs distinct per-stage images for TAA/buffering).
- The fundamental tension: removing the flush-wait needs pipelining, which breaks UE's
1:1 stage↔acquire lockstep (the synchronous path relies on it). Copy-ring decouples
via shim images but inherits the MSAA-resolve issue.
## RenderDoc offline-replay pipeline (set up this session — no headset needed to analyze)
- App must be **debuggable**: `tools/apktool.jar` decode → add `android:debuggable="true"`
→ rebuild → sign (debug.keystore) → `packaging/out/re4vr-dbg.apk`. (RE-uses the shim.)
- RenderDoc server installed (`org.renderdoc.renderdoccmd.arm64`). Capture over **USB**
(wireless adb drops the replay; local desktop replay of an Android capture fails).
- Headless analysis: `qrenderdoc --python script.py` with
`adb://<usb-serial>` → CreateRemoteServerConnection → CopyCaptureToRemote →
remote.OpenCapture. Scripts + captures in `~/renderdoc-captures/` (rd_*.py,
RE4/black.rdc = gameplay, RE4/title.rdc). Use `action.outputs` for RT attribution and
SaveTexture (NOT GetMinMax/Typeless — it lies) for content.
## Recommended next steps
1. Chase the MSAA-resolve-into-shim failure (RenderDoc pipeline is ready): compare the
title resolve (works) vs gameplay (fails) render-pass setup; try shim WITHOUT
MUTABLE_FORMAT, or matching UE's exact resolve-target image create info.
2. If copy-ring stays blocked, the perf fix is the shipped win; consider lighter levers
(sscap) and accept residual menu-screen ghosting.
## Notes
- Diagnostic logging is still in the shim (rate-limited; strip before public release).
- Depth format mapping was corrected (None=10; D16=6/D24_S8=7/D32_FP=8/D32_S824=9).
+123
View File
@@ -0,0 +1,123 @@
# RE4 VR Shim — Handoff (2026-06-25 session)
Entry point for next session. Prior handoff: `HANDOFF-2026-06-24.md`.
Detailed per-finding log lives in the auto-memory (`MEMORY.md` index).
## TL;DR
- **Root cause of the black flashes / 3×-hand / stutter is one thing: a GPU-load-gated
render-submit race.** Under load UE 4.25's RHI thread `vkQueueSubmit`s the eye render
*after* our `xrEndFrame`, so we present a stale/empty (black) swapchain image. PROVEN by
texture dump (gameplay eye image `min=max=0`, pure black) and confirmed load-gated
(in-game **Standing** comfort setting flashes black; **Sitting** doesn't — higher camera =
more visible geometry = blows frame budget).
- **Fixes tried this session:**
- `vkQueueWaitIdle` before resolve (`debug.re4vr.qwait`) — **no effect** (UE hadn't
submitted yet; can't drain unsubmitted work).
- **Deferred-flush pipeline** (`debug.re4vr.pipeline=1`, rewrote the render-ahead path to
hold frame N and flush+present at N+1 after UE submits) — reduced *pure* black but
**crashes more + adds latency**, marginal. **SHELVED.** Pipeline path has been the
unstable one all along.
- **FFR** (`debug.re4vr.ffr`, NEW this session) — implemented via `XR_FB_foveation`
(dynamic). Validated (game uses foveation natively) and helps headroom/smoothness, but
**not enough** to bring Standing under budget alone.
- **RE of the original libs confirmed:** `ovrp_EndFrame4` does **zero GPU sync** — VrApi's
system-owned textures carry it. Nothing to copy; our explicit-barrier approach is right,
we just have a timing/threading mismatch. OpenXR gives the same free sync IF we
submit-before-release (portable; the Quest-specific path would NOT port to Steam Frame).
- **NEXT (the real lever):** re-enable the **game's native dynamic-performance subsystem**
that our stubs disable (see below). AppSW turned out to be a weaker lead — RE4 doesn't
drive it.
## PRIMARY next task — re-enable the game's dynamic-perf machinery
RE4 was built to scale its own GPU load under pressure, but we stub the whole subsystem as
unsupported (logcat, all fire ~8×/session):
```
ovrp_GetGPUFrameTime -> Unsupported (game polls GPU frame time to decide)
ovrp_GetTiledMultiResLevel -> Unsupported (TiledMultiRes = Oculus name for FFR)
ovrp_SetTiledMultiResLevel -> Unsupported
ovrp_SetTiledMultiResDynamic -> Unsupported (dynamic FFR driven by GPU time)
ovrp_IsPerfMetricsSupported -> Unsupported
ovrp_GetSystemRecommendedMSAALevel2 -> NotYetImplemented
ovrp_GetLayerTextureFoveation -> NotYetImplemented (still stubbed even though we apply FFR at OpenXR)
```
Plan:
1. **Report TiledMultiRes supported + honor `Set*` calls** by mapping the game's requested
FFR level/dynamic onto our OpenXR `apply_foveation()` (xr_runtime.c). Today FFR is a
static prop (`debug.re4vr.ffr`); instead let the GAME drive the level (it already wants
dynamic, GPU-time-based). Stub fns in `shim/src/stubs.c`; real handlers go in
xr_runtime.c next to `apply_foveation`.
2. **Implement `ovrp_GetGPUFrameTime`** so the game's dynamic loop has its input. Source:
OpenXR has no portable GPU-time query; options = `XR_FB_... ` perf ext, or feed our own
measured frame dt (we already compute it for the `FRAME`/`HITCH` trace). Even an
approximate value lets the game's scaler engage.
3. Re-check Standing after: if the game drops its own resolution/foveation under load and
the black stops, this is the fix. It's the game's *intended* GPU-bound mitigation.
## SECONDARY — Application SpaceWarp (verify first, likely not viable as-is)
- `ovrp_GetLayerTextureSpaceWarp` is **stubbed but RE4 does NOT call it** this session; UE
passes a **108-byte EyeFov desc, flags=0** (no motion-vector variant). So UE isn't
rendering motion vectors → AppSW can't be fed.
- The **system** compositor IS running SpaceWarp/ASW (logcat tag `SpaceWarp`/`SpaceWarpCore`,
PID 2438) — that's compositor-side reprojection (no app MV needed). It may already be
reprojecting; our black frames defeat it (can't extrapolate a black frame).
- IF you pursue AppSW: needs (a) game to render velocity (UE 4.25 may not support it without
the Meta fork), (b) provide MV+depth swapchains via `ovrp_GetLayerTextureSpaceWarp`,
(c) submit `XrCompositionLayerSpaceWarpInfoFB` (chained on the projection view) via
`XR_FB_space_warp` (extension + types already in bundled headers, openxr.h:6021+;
`XrSystemSpaceWarpPropertiesFB` gives recommended MV size). **Verify (a) before investing.**
## Current build / stable config
Build/deploy:
```
./shim/build_android.sh && ./packaging/repack.sh
adb -s <redacted-serial> install -r packaging/out/re4vr-shim.apk
adb -s <redacted-serial> logcat | grep -i xrr
```
Device Quest (USB serial `<redacted-serial>`, or `<redacted-ip>:5555`).
Pkg `com.Armature.VR4`, activity `com.epicgames.ue4.GameActivity`.
**STABLE config left set:** `pipeline=0` (deferred-flush OFF), `ffr=2` (medium), `sscap=0`.
Consider compiling the deferred-flush path out / default FFR medium for a clean baseline.
## Runtime debug toggles (system props; relaunch unless noted "live")
- `debug.re4vr.ffr 0|1|2|3` — NEW. Fixed Foveated Rendering off/low/med/high (XR_FB_foveation, dynamic). Default 2.
- `debug.re4vr.pipeline 1` — deferred-flush render-ahead (live). UNSTABLE/crashes; shelved.
- `debug.re4vr.qwait 1` — diag: vkQueueWaitIdle before resolve (live). No effect; keep for reference.
- `debug.re4vr.trace 1|2` — NEW. 1=per-frame FRAME line (dt+HITCH) + starvation logs; 2=per-call WAIT/BEGIN/END + per-layer dump (live).
- `debug.re4vr.sscap 1` — cap supersample 1.2x→1.0x.
- `debug.re4vr.copyring 1` — copy-ring (eye-only; gameplay black). From prior session.
- `debug.re4vr.diag 1|2|3|4` — visual A/B: 1=proj only, 2=mono, 3=zoom, 4=head-lock quad (live).
- `debug.re4vr.dump N` — eye-texture readback to /sdcard/Android/data/com.Armature.VR4/files/*.ppm. **UNRELIABLE probe — see below.**
- `debug.re4vr.depth 1`, `debug.re4vr.noflushwait 1` — prior-session probes.
## What's RULED OUT for the black (don't re-investigate)
Quad placement/presence (diag=1/4), ViewportRect (UE always submits full), empty-frame
submission (`SUBMIT-BLACK=0` over 6730 frames), head-vs-eye pose mismatch (poses match
exactly), stereo layout/IPD (array, correct), pure frame hitching (pipeline cut hitches
804→39 but black persisted). It is specifically the **render-submit timing race**, GPU-load-gated.
## Diagnostic recipes (USE THESE — earlier probes misled us)
- **Perception is ground truth.** The `dump` probe is UNRELIABLE: it reads stale swapchain
buffers AND its fence-wait *forces* the render (observer effect — "dumping forced the load").
- **Recordings: limited-range.** `YAVG=16` IS black (not grey). 30fps mono UNDERSAMPLES 72Hz
flashes (lower bound). Separate sustained black (menu/scene transitions) from brief isolated
flashes. ffmpeg: `-vf "signalstats,metadata=print:key=lavfi.signalstats.YAVG:file=y.txt"`;
blend with `tmix=frames=N` to reveal ghosting; search the WHOLE clip, not the start.
- **Frame timing (reliable, not undersampled):** `debug.re4vr.trace 1` → grep `HITCH` / `FRAME`.
- **RE pipeline:** `JAVA_HOME=tools/jdk-21.0.11+10 tools/ghidra_12.1.2_PUBLIC/support/analyzeHeadless
ghidra_proj NAME -import dump/apk_libs/lib/arm64-v8a/libOVRPlugin.so -scriptPath ghidra_scripts
-postScript DumpEndFrame.java -deleteProject`. Original libs extracted in `dump/apk_libs/`.
## Key code locations (xr_runtime.c unless noted)
- Eye-fov submit / build_composition (~560), the resolve barrier path is in `vk_session.c`
(`xrr_vk_flush_submit_ex`, barrier COLOR_ATTACHMENT_WRITE→MEMORY_READ; NO QueueWaitIdle anywhere).
- `submit_pending` (~740): where a black frame reaches the compositor (`SUBMIT-BLACK` log).
- Pipeline (deferred-flush) branch in `xrr_end_frame` (~900); sync branch (~960).
- `apply_foveation` / `ffr_level` (just before `xrr_setup_layer`, ~1148) — FFR; extend here
to let the game drive the level (TiledMultiRes handlers).
- Capability stubs to implement: `shim/src/stubs.c` (GetGPUFrameTime, TiledMultiRes*, etc.).
- Texture handoff: `xrr_get_layer_texture` (~1230) — UE renders into `colorImages[stage]`.
## Open side-bug (parked)
One-time Seated-mode height bug: spawned very high at title, height oscillated high/normal
in-game; not replicable. Tracking-origin/recenter race at init (`ovrp_SetTrackingOriginType2`,
`ovrp_GetLocalTrackingSpaceRecenterCount` stubbed). Note `ReorientHMDOnControllerRecenter` is stubbed.
@@ -0,0 +1,104 @@
# RE4 VR Shim — Handoff (2026-06-26, Lever 2 reproject session)
Continuation of `HANDOFF-2026-06-26-skipblack.md`. Branch `latency-perffix-copyring`.
This session **built and shipped the working in-game black fix (Lever 2 reproject)** and hit
the tuning wall that points to a render-ahead pivot. Detail in auto-memory (MEMORY.md index):
`luma-gate-works-bad-vs-benign-black`, `reproject-works-playable`,
`reproject-tuning-and-renderahead-pivot`, `submit-count-cannot-detect-black`,
`skipblack-blacks-are-sustained-onset-only`.
## TL;DR — the in-game black is FIXED (reproject), "1000x better than black, playable"
The whole project's black-hunt is functionally solved. On a truncation-black frame we blit the
held last-good eye image back over it and present with the SAVED pose; the compositor timewarps
it to the current head pose instead of flashing black. Committed through `3522e5c`.
## What was built this session (commits a45362c..3522e5c)
1. **Post-hitch detector + barcode validation tool** (a45362c, c2a6c29): instrument-only black
flag; on-screen frameIndex barcode that turns red on a flagged frame for frame-exact video
correlation (`debug.re4vr.barcode`).
2. **Submit-count ruled out** (508f55a): UE issues a flat 3 vkQueueSubmits/frame on black AND
normal frames — truncation is fewer DRAWS, not fewer submits. Dead end, don't retry.
3. **Per-frame luma black gate** (571aa00): `xrr_vk_frame_luma` reads max luma of a few rows of
the resolved eye image; covers the whole sustained-black TAIL (post-hitch only caught the
onset). `debug.re4vr.lumagate`, `lumathr` (default 12). **Bad black = luma<thr AND
appLayers==1** (appLayers>=2 = a menu's black eye-fov behind a UI quad — benign).
4. **Reproject** (7c0882b): `debug.re4vr.reproject=1`. Copy-ring hold (`L->holdImage`, no
cross-frame swapchain hold so no pipeline=1 instability). Save acquired->hold on good frames
(with pose), restore hold->acquired on black frames, submit with saved pose
(`g_lastGoodViews`, used in `build_composition`).
5. **Tuning** (e3bbe85, 3522e5c): `savethr` (default 40, only hold clearly-lit frames — fixes
dim reprojected frames); `abruptdrop` (default 0 = reproject all; raise to skip gradual
fades); `holdevery` (default 4, throttle the save copy for perf).
## State of play (device-tested)
- **Black flashes GONE, playable.** Stable, no crashes over multiple soaks.
- **Perf ~17ms (~59fps).** Breakdown measured: resolve-wait ~7ms + luma ~3.5ms + save ~0.6ms.
Folding luma into the resolve cmd buffer = WASH (luma's cost is a forced Adreno tile-resolve
from the TRANSFER_SRC transition, not shareable wait overhead — tried + reverted). 72Hz needs
attacking the 7ms resolve-wait = render-ahead.
- **Remaining artifacts (all inherent to single-frame reprojection):** (a) reproject OVERRIDES
the game's intentional fades (comfort/teleport/scene/load) — they're black too; abruptdrop
helps but can't cleanly separate fade from shallow truncation. (b) long ~1s holds during
loads are jarring. (c) head-rotation during a hold shows timewarp edge-black; depth-hold (not
done) would add positional reprojection.
## NEXT — revisit RENDER-AHEAD (user's strategic call), but verify the premise first
Why now: render-ahead adds 1 frame latency -> UE gets more GPU budget/frame -> FEWER/shorter
truncations -> reproject (and its artifacts/long-holds) needed far less; and reproject+luma are
now a SAFETY NET for render-ahead's old instability (detect+hide any black instead of flashing).
- **GATE (do this first):** resolve the contradiction — memory `black-is-not-submit-timing`
says UE renders empty regardless of timing, but the user observed black DROPPED when the luma
readback (a per-frame GPU sync) turned on. Experiment: use the luma gate to COUNT
appLayers==1 truncation-black frames per minute at different added-latency/sync levels (e.g.
baseline vs lumagate-on vs an added sync). If latency reduces truncation frequency ->
render-ahead is justified; if not -> stay with reproject + tuning + maybe depth-hold.
- If justified: build render-ahead informed by the luma signal (present held frame only when
needed, not blindly like the old `pipeline=1`), with reproject as the fallback. See
`deferred-flush-unstable-abandoned` for the old failure modes (acquire/release imbalance).
## Game perf RE (done this session) — one new lever, several myths busted
Static RE of the GAME binary `dump/apk_libs/lib/arm64-v8a/libUE4.so` via capstone. Full report:
`analysis/game-perf-RE.md`; memory `game-perf-RE-findings`. This REPLACES the behavioral
inferences in `game-doesnt-drive-dynamic-perf-from-feed` with code proof:
- **No game-side dynamic-FFR loop exists** — `SetTiledMultiRes{Level,Dynamic}` args are config
bytes (`SetTiledMultiResDynamic(Settings.byte[0x11d])`), never compared to GPU time/metrics. So
shim-side dynamic FFR is the ONLY path (nothing we return makes the game loop). Boot `Dynamic(0)`
is just that config value.
- **Perf-metrics gate hypothesis REFUTED** — game never gates FFR on `IsPerfMetricsSupported`;
perf-metrics feed only a stats/HUD collector. Commit 68a7093 unlocked no behavior.
- **AppSW dead** (`GetLayerTextureSpaceWarp` not even a string in the binary).
- **`GetSystemRecommendedMSAALevel2` = hazard** (over-report -> more MSAA -> more load). Keep at 1.
### NEW game-driven lever — implement `ovrp_GetAdaptiveGpuPerformanceScale2` (do next session)
At its call site the game does `resolution = baseDensity * sqrt(scale)` where `scale` comes from
this API (table slot g+0x130). We return Unsupported -> scale=1.0 -> "viewport always full under
load". **If the shim returns `scale < 1.0` under GPU pressure (derive from `g_gpuFrameMs` vs the
13.9ms budget), the game self-downscales resolution — NO game patch.** Gated on `Settings.byte[0x19]
bit4` (adaptive-res enabled) — VERIFY on device. CAVEAT: this is a GPU-LOAD (pixel) lever; the
truncation is DRAW-bound (quarter-res still blacks), so it's a throughput/comfort/headroom win that
*might* indirectly slacken the render thread — NOT a guaranteed truncation fix. Easy + game-native;
worth stacking, measure truncation effect with the luma counter. (Find the API's stub in
`shim/src/stubs.c`; we already own `g_gpuFrameMs` + apply-foveation path.)
## Secondary follow-ups (the user "likes them all")
- **Depth-hold** (artifact reduction): enable depth (works on device, gameplay renders), hold
+restore depth alongside color, chain it (`build_composition` already chains depth) -> proper
positional reprojection. Helps the head-rotation swim/edge-black.
- **Verify the ghosting-fixed-by-readback** observation independently.
## Build / deploy / config
```
./shim/build_android.sh && ./packaging/repack.sh # repack bundles LAST build — check timestamp
adb -s <redacted-serial> install -r packaging/out/re4vr-shim.apk
adb -s <redacted-serial> logcat | grep -iE "xrr|REPROJECT"
```
Clean device state set this session: `reproject=1 holdevery=4 savethr=40 abruptdrop=0 depth=0
trace=0 barcode=0`. Launch blocked by controllers-asleep dialog — wake controllers first.
## Key reproject toggles (system props, re-read each frame unless noted)
- `debug.re4vr.reproject 1` — the fix (implies luma readback).
- `debug.re4vr.lumathr N` (12) — black threshold; `savethr N` (40) — min luma to HOLD a frame.
- `debug.re4vr.abruptdrop N` (0=reproject all; ~25 skips fades but misses shallow truncations).
- `debug.re4vr.holdevery N` (4) — save last-good every Nth frame (perf throttle).
- `debug.re4vr.barcode 1` — on-screen frameIndex barcode (red = reprojected frame), validation.
- `debug.re4vr.depth 1` — experimental depth swapchain (renders OK; not yet held for reproject).
@@ -0,0 +1,81 @@
# RE4 VR Shim — Handoff (2026-06-26, skipblack instrument session)
Continuation of `HANDOFF-2026-06-26.md` (morning). Branch `latency-perffix-copyring`.
This session built + ran **Lever 2 step 1 (instrument-only black-frame detector)** and
validated the signal is real but coarse. Detailed finding in auto-memory
`skipblack-posthitch-signal-validated`. Design: `analysis/lever2-detect-skip-black-handoff.md`.
## TL;DR — the post-hitch black signal is REAL but needs a tighter gate + frame-accurate proof
Added `debug.re4vr.skipblack 1` (instrument-only): after a HITCH (dt>20ms), flag the next
`skipblackn` frames as LIKELY-TRUNCATED and log them. NO behavior change at level 1. Ran on
device (standing, walk off bridge into house). 47 HITCHes / 89 flagged frames. Result:
- **The truncated/black frame has a signature: a SHORT (<12ms) frame right after a big hitch**
— it finishes fast because UE rendered almost nothing into it (matches RenderDoc 28-vs-198
draws → pure black).
- **Big hitches (≥40ms): 25, of which 52% have that <12ms truncated recovery frame.**
- **Mild hitches (20–40ms): 22, only 5%** — these recover to a full ~14ms frame = a 1-frame
judder, NOT black. The current heuristic OVER-flags these.
- HITCH magnitudes span 20ms → 3.8s. The multi-hundred-ms / multi-second ones are
**load/streaming stalls** (level transitions) — a different event from per-frame gameplay
black drops. Must be separated from the gameplay black.
- Ground truth is still COARSE: user confirmed "black" happened, but not frame-accurate; the
short-dt=black link rests on the single RenderDoc capture, not per-frame verified.
Raw frame/flag sequence persisted: `analysis/skipblack-2026-06-26/frame-sequence.log`.
## NEXT TASK — do BOTH (user decision this session):
### (3) Tighten the detector, then re-instrument
- **Gate the arm on hitch ≥40ms** (drops the 22 harmless mild hitches; ~halves false flags).
- **Add short-dt confirmation:** only treat a post-hitch frame as truncated if its own
dt < ~12ms. Combines the two corroborating signals; cuts the ~48% of big hitches whose
recovery wasn't short (those may be a different failure mode — investigate separately).
- **Handle sustained-overload clusters** (e.g. f=3162–3173: ~10 consecutive hitches). The
fixed "next 2 frames" model is wrong there — the whole burst is bad. Consider: stay armed
while consecutive frames keep hitching, not a fixed count.
- Re-run instrument-only and re-inspect (reuse the awk recipes from this session's analysis).
- Code is all in `shim/src/xr_runtime.c`: `skipblack_level()`/`skipblack_count()` (~205) and
the flag block inside the FRAME/HITCH trace (~1186, look for `SKIPBLACK: LIKELY-TRUNCATED`).
### (2) Frame-accurate ground truth via Meta Cast (do alongside / before reproject)
- In-game recording is broken (empty mp4); display 0 is FLAG_SECURE so scrcpy/screenrecord
can't see VR. Use **Meta Cast → desktop screen-record** while walking off the bridge.
- Run with `skipblack=1 skipblackn=2 trace=1`, capture logcat with timestamps in parallel.
- Align the video's visible black flashes to the `SKIPBLACK: LIKELY-TRUNCATED` log
timestamps to PROVE (or refute) flag == perceived black, per frame. This is the
`verify-before-theory` discipline — don't build the reproject on the coarse signal alone.
### THEN — Lever 2 step 2 (reproject, `debug.re4vr.skipblack 2`)
Only after (2)+(3). Re-present the **last-good** eye image with the current pose so the
OpenXR compositor timewarps it instead of showing black.
- **Use the COPY-ring, NOT a cross-frame swapchain hold** — holding across frames is what
sank `pipeline=1` (see memory `deferred-flush-unstable-abandoned`). Copy last-good eye →
shim VkImage (`xrr_vk_alloc_images`/`shimImages[]`, copy path in end_frame ~922), blit it
into the acquired image on a flagged frame, submit the composition with the CURRENT pose.
- Low harm on false positives: reprojecting a stale frame ≈ what the compositor already does
during the hitch, so over-flagging mild hitches is tolerable but wasteful — the ≥40ms gate
keeps it clean.
- Success = black flashes replaced by (at worst) brief reprojection judder, not black.
## Build / deploy / test (unchanged)
```
./shim/build_android.sh && ./packaging/repack.sh # repack bundles LAST build — check
# shim/build/arm64/libOVRPlugin.so timestamp
adb -s <redacted-serial> install -r packaging/out/re4vr-shim.apk
adb -s <redacted-serial> logcat | grep -iE "xrr|SKIPBLACK"
```
Launch is BLOCKED by the controllers-asleep system dialog
(`app_launch_blocked_controller_required`) — wake controllers + headset on before
`am start -n com.Armature.VR4/com.epicgames.ue4.GameActivity`.
Logcat is dominated by PerfMetrics polling — filter to `FRAME f=|HITCH|SKIPBLACK` for pacing.
## Props
- `debug.re4vr.skipblack 0|1|2` — 0 off / 1 instrument-only (built) / 2 reproject (TODO).
- `debug.re4vr.skipblackn N` — post-hitch frames to flag (default 2). Will change with the
cluster-aware rewrite in (3).
- Reset to baseline now: skipblack=0, skipblackn=0, trace=0, ffr=2, resscale=100.
## State
- Code committed? **NO — `skipblack` detector is UNCOMMITTED** in the working tree
(`shim/src/xr_runtime.c`). Commit it (instrument-only, gated off, safe) before further work.
- Branch `latency-perffix-copyring`, clean except the skipblack edit + the new analysis dir.
+74
View File
@@ -0,0 +1,74 @@
# RE4 VR Shim — Handoff (2026-06-26 session)
Entry point for next session. Prior: `HANDOFF-2026-06-25.md`. Detailed findings in the
auto-memory (`MEMORY.md` index). Commits this session: `68a7093`, `5fa59f4` (branch
`latency-perffix-copyring`).
## TL;DR — the in-game black is a UE frame-drop, and we proved it
After a long, thorough investigation, the in-game black is **conclusively a UE-internal
frame-drop under load**, NOT a shim bug:
- RenderDoc (`~/renderdoc-captures/RE4/work_frame.rdc`): under load UE renders only
**~28 draws into its eye target vs ~198 in a normal frame** (a truncated frame), and the
**resolved/presented eye = pure black** (MEAN [0,0,0]).
- **It's draw / render-thread-time bound, NOT pixel-bound:** even quarter-res
(`resscale=50`) + max FFR (`ffr=3`) still blacks. Resolution doesn't change draw count.
- **Ruled out, decisively:** submit-timing (built a vkQueueSubmit hook + present-on-submit;
even a full one-frame defer still blacks), image-targeting (LAYER MISMATCH = 0,
stage==acquiredIndex), empty-frame submission (SUBMIT-BLACK/COMPOSE-EMPTY = 0),
load-reduction (FFR + quarter-res).
- Load-gated: bridge (sparse) never blacks; house/dense geometry blacks; worse while casting.
## NEXT TASK — Lever 2: detect the bad frame + reproject
Full design in `analysis/lever2-detect-skip-black-handoff.md`. Idea: when UE hands us a
truncated/black frame, DON'T present it — re-present the last-good eye image with the
current pose so the OpenXR compositor reprojects (timewarp) instead of showing black.
- **Bad-frame signal:** start with the **post-hitch heuristic** (the truncated frame is the
recovery frame after a `dt>20ms` hitch we already detect). The vkQueueSubmit hook's
submit-count probably WON'T distinguish bad frames (it's draw-bound; UE may batch).
- **Re-present:** reuse the copy-ring infra (`xrr_vk_alloc_images`, `shimImages[]`, the
copy path in end_frame) — hold a copy of the last-good eye image, blit it into the
acquired image on a bad frame. Avoids the cross-frame-hold instability that sank
`pipeline=1`.
- **Discipline:** implement the detector as **instrument-only first** (log "likely
truncated frame" after each hitch), validate the signal correlates with perceived black,
THEN build the reproject. Gate behind `debug.re4vr.skipblack` (default 0).
## What was built this session (all committed, gated off by default)
- **Dynamic-perf bridge** (`68a7093`): `ovrp_*TiledMultiRes*` (FFR) + `GetGPUFrameTime` +
`IsPerfMetricsSupported`/`GetPerfMetrics{Float,Int}` forwarded to OpenXR. Re-enables RE4's
perf APIs (were Unsupported). The game accepts them but does NOT self-scale from the feed.
Fixed a NULL-foveation-profile runtime crash (use `XR_FOVEATION_LEVEL_NONE_FB`).
- **UE vkQueueSubmit hook** (`5fa59f4`): dlsym-patch UE's exported global
`VulkanDynamicAPI::vkQueueSubmit` -> trampoline -> `xrr_on_ue_submit`. Robust + reusable
(e.g. for Lever 2 instrumentation). Gated: `debug.re4vr.submithook` 0/1/2.
- **resscale** (`5fa59f4`): `debug.re4vr.resscale` = % of eye size. Doesn't fix the black;
keep as a load knob.
## Settled side-findings (don't re-investigate)
- **Title ghost = cold-load reprojection judder** (cold-launch only; self-resolves warm;
single eye-fov layer, no quad). See memory `title-ghost-is-coldload-reprojection-judder`.
- **Bridge "vignette" = the GAME's comfort vignette** (user toggled it off; not a shim bug).
- **An in-game "crash" was an OOM SIGKILL** (lmkd), worsened by in-game recording (which is
broken — empty mp4). Capture via Meta Cast -> desktop record instead (scrcpy/screenrecord
can't see VR: display 0 is FLAG_SECURE). RenderDoc recipe: memory `renderdoc-remote-replay`.
## Build / deploy / config
```
./shim/build_android.sh && ./packaging/repack.sh
adb -s <redacted-serial> install -r packaging/out/re4vr-shim.apk
adb -s <redacted-serial> logcat | grep -i xrr
```
WARNING: `repack.sh` silently bundles the LAST successful build — confirm the build had no
errors + check `shim/build/arm64/libOVRPlugin.so` timestamp before repack.
Device Quest 2, USB serial `<redacted-serial>`. Pkg `com.Armature.VR4`, activity
`com.epicgames.ue4.GameActivity` (launching needs controllers awake, else a system dialog
blocks `am start`).
Props reset to baseline: ffr=2, resscale=100, sscap=0, submithook=0, defern=3, trace=0
(none fix the black — all cosmetic/diagnostic now).
## Key runtime toggles (system props; relaunch unless noted)
- `debug.re4vr.submithook 0|1|2` — UE submit hook: 0 off, 1 instrument, 2 present-on-submit.
- `debug.re4vr.defern N` — present-on-submit: present on Nth render submit (default 3).
- `debug.re4vr.resscale N` — eye buffer = N% of native (default 100).
- `debug.re4vr.ffr 0..3`, `debug.re4vr.sscap 1`, `debug.re4vr.trace 1|2` — FFR / supersample
cap / frame+hitch trace. `debug.re4vr.pipeline 1` (deferred-flush, unstable, shelved).
@@ -0,0 +1,97 @@
# RE4 VR Shim — Handoff (2026-06-27, native-parity / P4 passthru)
Branch `latency-perffix-copyring`. Goal this arc: **get the shim to native (stock VrApi) parity.**
Stock RE4 VR is flawless on this Quest 2; our OpenXR shim had black logos + eye-layer ghosting.
Detailed findings in auto-memory (MEMORY.md): `native-parity-diagnosis`, `logo-black-is-layer-zorder`,
`eye-ghost-submission-path-clean-need-p4`, `three-perf-levers-built`.
## SHIPPED + VERIFIED this session
**Logo black FIXED — it was a layer z-order bug.** RE'd it: the 3 intro logos are Oculus splash
layers (`OculusHMD::FSplash`); the game submits `[logo-quad, eye-fov]` in that order, and OpenXR
composites strictly in array order (last = on top), so our opaque eye-fov drew ON TOP of the logo →
black. VrApi treats eye-fov as the base regardless of order. Fix: `build_composition`
(`shim/src/xr_runtime.c`) stable-partitions so projection layers go bottom, quads on top. Gated
`debug.re4vr.eyebottom` (default ON). **Device-verified: logos now render.** A/B: `eyebottom 0`.
## STILL OPEN: eye-layer ghosting (hand deforms / "three hands", close objects, SEATED mode)
Standing mode = black+reproject instead (load-gated, see `standing-vs-sitting-blackflash`); seated =
clean ghost repro (no reproject confound). **Every shim submission metric is CLEAN** (all
device-verified, see `eye-ghost-submission-path-clean-need-p4`): depth on didn't fix it; submit is
18ms EARLY not late; VrApi `Stale=0`; pose render==submit (`g_xr.views`); FOV render==submit; ffr
ruled out; mono cast shows a normal hand in sharp frames → it's a per-eye/stereo artifact we can't
see from our side. **Hence P4.**
## NEXT TASK — build the full P4 passthru forwarding (DE-RISKED, viable)
Run the REAL OVRPlugin inside our app and LOG native's actual per-eye poses/FOV/layer submission, to
diff against our clean-but-ghosting values. Feasibility PROVEN: `PASSTHRU-PROBE: dlopen OK` — real
lib + libvrapi load in our unofficial app, entry points resolve (probe in `core.c` `passthru_probe()`
at PreInitialize3, gated `debug.re4vr.passthru_probe`).
**Already in place:** SONAME-patched real lib at `packaging/libs/arm64/libOVRPlugin_real.so`
(`patchelf --set-soname libOVRPlugin_real.so`); `repack.sh` auto-bundles it when present (verified in
the APK). `libvrapi.so` already rides along from the original APK.
**The build (all-or-nothing — real lib must own the whole session to run EndFrame4):**
1. New `shim/src/passthru.c`: `dlopen("libOVRPlugin_real.so", RTLD_NOW|RTLD_LOCAL)` once; build a
table of real fn pointers (dlsym each `ovrp_*` the game uses); expose `pt_active()` +
`pt_<fn>()` accessors (or a single dispatch). Gate: `debug.re4vr.passthru` (default 0).
2. In EVERY game-called export (the ~30 in `core.c`/`layers.c` + the ~12 stubs the game hits — see
below), add at the top: `if (pt_active()) { ...optional log...; return real_ovrp_X(args); }`.
Partial forwarding CRASHES mid-frame, so forward the COMPLETE called set. The game-called set =
everything currently implemented in core.c/layers.c PLUS these stubs it invokes (from device log):
`DestroyDistortionWindow2, SetupDisplayObjects2, SetReorientHMDOnControllerRecenter,
SetClientColorDesc, SetAppEngineInfo2, SetAppCPUPriority2, InitializeMixedReality,
GetViewportStencil, GetSystemRecommendedMSAALevel2, GetLocalTrackingSpaceRecenterCount,
GetLayerTextureFoveation, GetControllerHapticsDesc2`. (Confirm the live set by grepping a passthru
boot for any of OUR `stub ovrp`/impl logs that still fire — those are unforwarded calls.)
3. LOG the ghost-relevant ones: `EndFrame4` (per-layer pose+fov+swapchain+flags),
`GetNodePoseState3`(Eye L/R return), `GetNodeFrustum2`(return), `CalculateEyeLayerDesc2`,
`WaitToBeginFrame`/`EndFrame4` timing. Forward these to real, log args/returns.
4. Boot test: with `passthru 1`, does the game run NATIVE through our shim (looks like stock, no
ghost)? If yes → capture native EndFrame4 per-eye poses/fov, **diff against our values** (ours are
in the STEREO/HEADvsEYE logs). The delta is the ghost cause. If the game won't init native (Init5
entitlement/session), fall back to static RE of the projection path.
CAVEAT: in passthru our shim must NOT also init OpenXR — make `xrr_init`/frame-loop no-op when
pt_active (real lib owns the session). Watch for double-init of VrApi/OpenXR.
## The 3 perf levers (built earlier this session — all DEAD ENDS, gated off)
- **Adaptive-res** (`ovrp_GetAdaptiveGpuPerformanceScale2`, core.c): INERT — game's `Settings.byte
[0x19] bit4` clear, never downscales. `debug.re4vr.adaptivescale`.
- **Render-ahead** (`debug.re4vr.renderahead`): stable now (old pipeline=1 instability fixed) but
does NOT reduce black (still 66-76% under load — black is draw/streaming-bound, not latency).
BLACKCOUNT counter (`debug.re4vr.blackcount`) is the measurement tool.
- **Depth-hold** (`debug.re4vr.depthhold`, needs `depth 1`): built, untested, ADDS memory pressure.
- **Reproject remains the actual in-game black fix.** Corridor black = engine streaming/memory stall
(lmkd thrash, Graphics 1.69GB), not GPU — confirmed; GPU knobs don't help. Streaming-throttle CVar
is a documented-but-not-built future lever (config-injection DEAD — Shipping ignores external
UE4CommandLine.txt; would need CVar memory-patch via `IConsoleManager`).
## Code state (UNCOMMITTED on branch) — RECOMMEND COMMIT before further work
`git status`: M `core.c stubs.c vk_session.c xr_runtime.c xr_runtime.h packaging/repack.sh`; new
`HANDOFF-2026-06-27*.md`, `save_backup/`, `packaging/libs/arm64/libOVRPlugin_real.so` (986KB binary —
needed for passthru; confirm it's committed or staged). +383/-88 in shim. Contains: eyebottom fix
(keep — verified), 3 perf levers (gated off), QUADLUMA + per-layer luma diag, passthru probe,
depth-aware vk helpers (`xrr_vk_alloc_images_ex`/`xrr_vk_copy_submit_ex`).
## Build / deploy / device workflow
```
./shim/build_android.sh && ./packaging/repack.sh # repack auto-bundles libOVRPlugin_real.so
adb -s <redacted-serial> install -r packaging/out/re4vr-shim.apk # -r over SAME-signed shim PRESERVES data (no OBB/save dance)
adb -s <redacted-serial> logcat -G 16M # big buffer so brief windows don't rotate
adb -s <redacted-serial> logcat | grep -iE "xrr|PASSTHRU"
```
- **Every cold relaunch needs a manual dismiss** of the UnOfficialApp dialog (debug-signed) and the
ControllerRequired dialog if controllers asleep → autonomous cold-launch capture is blocked; ask
the user to dismiss + boot. Logos play on cold start only.
- **SAVE DISCIPLINE:** `adb pull /sdcard/Android/data/com.Armature.VR4/files/savegame00.sav` BEFORE
any uninstall (uninstall wipes app data; cloud doesn't restore unofficial-app saves — lost one this
session). Only stock<->shim swaps need uninstall; shim->shim is `install -r` (preserves data). OBB
preserve trick: `adb shell mv /sdcard/Android/obb/com.Armature.VR4 /sdcard/obb_bak` before uninstall.
- Stock original APK (pristine, real 907KB OVRPlugin) for A/B: `dump/obb/VR4-Android-Shipping-arm64.apk`.
- Current device props: `eyebottom 1 reproject 1 depth 1 depthhold 0 renderahead 0 blackcount 1
adaptivescale 1 passthru_probe 1` (set `passthru 1` for the new path once built).
## RE tooling (works)
capstone 5.0.7 in python (NOT pyelftools — absent; parse ELF phdrs manually, see scratchpad
disasm.py pattern). `nm -DC` for libUE4 dynsym. PluginWrapper offset→name mapping was UNRELIABLE;
trust call-site offsets + the dynamic `stub ovrp` log signal instead.
+105
View File
@@ -0,0 +1,105 @@
# RE4 VR Shim — Handoff (2026-06-27, 3-levers + streaming-bound reframe)
Branch `latency-perffix-copyring`. Prior: `HANDOFF-2026-06-26-reproject.md`. Auto-memory:
`three-perf-levers-built`, plus the prior reproject/black chain. This session built the 3
forward perf levers, soak-tested them, and **reframed the in-game black as a level-streaming /
memory-bandwidth stall (NOT GPU-bound)** — which retires the GPU levers and points at a UE
async-streaming CVar as the only remaining cause-fix.
## What was built (uncommitted on the branch; compiles clean, gated, device-tested)
1. **Lever A — dynamic resolution** `ovrp_GetAdaptiveGpuPerformanceScale2` (real impl in
`core.c`, helper `xrr_adaptive_gpu_scale()` in `xr_runtime.c`, stub removed from `stubs.c`).
Props `debug.re4vr.adaptivescale` (1), `adaptivefloor` (60). **DEVICE RESULT: inert** — zero
`VIEWPORT SUB` under extreme load ⇒ game's `Settings.byte[0x19] bit4` is clear, game ignores
our scale (the handoff caveat held).
2. **Lever B — render-ahead + truncation counter.** Shared `reproject_resolved_eye()` now feeds
both present paths; render-ahead via `debug.re4vr.renderahead 1` (pose-patches `g_pending` to
last-good on a black held frame; clean engage/disengage drain + presentIndex guard).
`BLACKCOUNT` counter (`debug.re4vr.blackcount`, default 1). **DEVICE RESULT: render-ahead is
STABLE (old pipeline=1 instability fixed) but NOT a black cure** — still spiked to 66–76%
black with `pipelined=1`. Confirms black is draw/CPU-bound, not latency-bound. Counter works
perfectly and was the instrument for everything below.
3. **Lever C — depth-hold** (`debug.re4vr.depthhold 1`, default 0; needs `depth 1`; sync path
only). Depth-aware `xrr_vk_alloc_images_ex` / `xrr_vk_copy_submit_ex` (isDepth). **No crash
from the depth barriers, but it ADDS graphics memory in the exact pressured zone — net
negative for the streaming-stall black; left OFF.**
## THE REFRAME (this session's real finding) — black is a STREAMING/MEMORY stall
User observation: the long black/reproject episode was **in a corridor next to a load-zone
door** — which has *fewer* draws than the open areas that render fine. That contradicts
"draw-bound." The trace agrees:
- `dumpsys meminfo`: **Graphics 1.69 GB**, TOTAL RSS 2.87 GB (Quest 2). During the black window
(02:06:41–45) the system logged `RescueParty: lmkd_native` + **`ActivityManager: Lost
connection to lmkd`** — the low-memory killer daemon fell over. Severe memory thrash.
- Black frames were at **near-normal 72Hz cadence** (dt ~21–24ms), only ~8 true hitches in 25s
but 240–275/360 frames black ⇒ GPU keeping cadence while UE hands us **empty** frames ⇒ the
**game thread is stalled (streaming the next zone in), not the GPU**.
Conclusion: near a load door UE pre-streams the adjacent zone's textures → memory spike → page
reclaim/lmkd thrash → game-thread stall → empty (black) eye frames. **GPU knobs (resolution,
FFR, render-ahead) cannot help; they target a bottleneck that isn't the limiter.**
### Mitigation that DID help (memory, not pixels)
Config `renderahead=0 reproject=1 depth=0 depthhold=0 sscap=1`: peak black **76% → ~16%**, most
windows 0%. Note **`sscap` did nothing** (eye buffer stayed 1728×1900 — already the recommended
size, no supersample to cap); the win came from **`depth=0` freeing graphics memory**. Cleanest
possible proof it's memory-bound. Reproject hides the residual ⇒ playable.
**Recommended shipping config:** `renderahead=0 reproject=1 holdevery=4 savethr=40 abruptdrop=0
depth=0 depthhold=0 blackcount=1 adaptivescale=1 adaptivefloor=60` (adaptivescale harmless even
if inert).
## NEXT TASK — throttle UE async streaming via a CVar (the only remaining cause-fix)
User's instinct: deprioritize the background stream-ahead, accept a longer load screen. Maps to
UE4 CVars: **`s.AsyncLoadingTimeLimit`**, `s.LevelStreamingActorsUpdateTimeLimit`,
`s.PriorityAsyncLoadingExtraTime`, `s.AsyncLoadingUseFullTimeLimit`, and memory-side
`r.Streaming.PoolSize` / `r.Streaming.LimitPoolSizeToVRAM` / prefetch distance. Lowering the
async time limits spreads streaming over more frames (less per-frame game-thread theft → smoother
active scene, slower stream). Lowering the streaming pool caps the pre-spike.
### CHEAP path PROBED & DEAD (don't retry the same way)
External `UE4CommandLine.txt` override is NOT read by this Shipping build. Tested
`-execcmds="t.MaxFPS 20"` at both `/sdcard/UE4Game/VR4/UE4CommandLine.txt` and the app-scoped
`/sdcard/Android/data/com.Armature.VR4/files/UE4Game/VR4/UE4CommandLine.txt`; **end_frame
heartbeat stayed at 71 fps** (decisive — heartbeat = game render rate; VrApi FPS=72 is just the
reprojecting compositor, ignore it). The engine only uses the baked `assets/UE4CommandLine.txt`
(`../../../VR4/VR4.uproject`). Minor caveat: `t.MaxFPS` *could* be VR-overridden, but two paths
+ heartbeat make "file not read" the strong read.
- **Secondary cheap probe worth ONE try before memory-patching:** `GameUserSettings.ini`
scalability — `GGameUserSettingsIni` is read from a *writable* app-scoped Saved/Config path.
Write `[ScalabilityGroups] sg.TextureQuality=0 / sg.ViewDistanceQuality=1` and see if the
texture-streaming pool shrinks (watch `dumpsys meminfo` Graphics + BLACKCOUNT in the corridor).
Uncertain (game may overwrite settings via its own UI), but declarative if it sticks.
### ROBUST path — set the CVar from the shim (the actual handoff task)
The console-variable machinery is present and several helpers are **exported (`T`)** in
`dump/apk_libs/lib/arm64-v8a/libUE4.so`:
- `_Z22CreateConsoleVariablesv`, `_Z27ForEachCVarInSectionFromIni...`, and (seen via strings)
`LoadConsoleVariablesFromINI`, plus `IConsoleVariable` / `IConsoleManager` infra and
`OnCVarChange`. Kismet `GetConsoleVariable{Int,Float,Bool}Value` exist too.
- **Plan:** after UE init (we already dlsym-patch UE's exported globals for the submit hook — same
technique), resolve `IConsoleManager::Get()` then `FindConsoleVariable("s.AsyncLoadingTimeLimit")`
and call `->Set("2", ECVF_SetByCode)`. RE steps: (a) find `IConsoleManager::Get` (likely a local
symbol — use the full symbol table / capstone like the game-perf RE; `CreateConsoleVariablesv` and
the `RegisterConsoleVariable` callsites reference the manager singleton). (b) confirm the
`IConsoleVariable::Set(const TCHAR*, EConsoleVariableFlags)` vtable slot ABI. (c) call it for the
3–4 streaming CVars at a safe point (first frame). Gate behind `debug.re4vr.streamthrottle`.
- **Verify:** corridor soak, `BLACKCOUNT` delta + `dumpsys meminfo` Graphics, and confirm load
transitions still complete (just slower). The counter is already the measurement tool.
## IS THIS NECESSARY OR OVER-ENGINEERING? (honest call)
**Leaning over-engineering for the value.** The streaming-stall black is already well-mitigated
(reproject + depth-off: 76%→16% peak, mostly 0%, residual hidden as a brief hold). The CVar
memory-patch is invasive RE (console-manager ABI, fragile across game updates) for a marginal win
on a transient that's already hidden. **Recommend: stop at the memory-light + reproject config,
document the CVar route as a known lever, and only build it if the corridor reproject-holds are
perceptually annoying enough to the user to justify it.** Try the GameUserSettings probe first
(cheap) if pursuing further.
## Build / deploy / config
```
./shim/build_android.sh && ./packaging/repack.sh # check libOVRPlugin.so timestamp before repack
adb -s <redacted-serial> install -r packaging/out/re4vr-shim.apk
adb -s <redacted-serial> logcat | grep -iE "xrr|REPROJECT|BLACKCOUNT|VIEWPORT SUB"
```
Files changed this session: `core.c`, `stubs.c`, `vk_session.c`, `xr_runtime.c/.h` (+316/−89).
Not yet committed.
+381
View File
@@ -0,0 +1,381 @@
# RE4 VR Shim — Next Session Handoff: Motion Reprojection / Latency
**Date locked:** 2026-06-23. **State:** RE4 VR is **playable** on Quest 2 via the
OVRPlugin→OpenXR shim. One known issue remains: **ghosting/duplication during head
movement** (and the title/menu "duping" — same root cause). This doc is the plan to
fix it.
Deploy any change with: `./shim/build_android.sh && ./packaging/repack.sh && adb install -r packaging/out/re4vr-shim.apk`
Logs: `adb logcat -d -s xrr`. Device = Quest 2 over wireless adb (`adb connect <redacted-ip>:5555`).
---
## What works (do not regress)
- Rendering is correct & stable when the head is still; stereo/IPD/FOV/poses all verified.
- **Tile-memory flush barrier** (`src/vk_session.c xrr_vk_flush_image`) — THE fix for the
black/tearing. Quest's Adreno is a tiler; UE's eye render must be flushed to main
memory + made visible before the compositor reads. We submit a `VkImageMemoryBarrier`
on UE's queue in `xrr_end_frame` before `xrReleaseSwapchainImage`.
- **Floor tracking** = `XR_REFERENCE_SPACE_TYPE_LOCAL_FLOOR` when the game requests
FloorLevel (gun sits on hip). Eye-level sinks you into the floor; STAGE reverses movement.
- Menus placed at the game's real submitted pose/size (respect `ovrpLayerSubmitFlag_HeadLocked`).
- `DestroyLayer` implemented (+ slot reuse); finite swapchain-wait timeout (no hangs).
## The remaining problem (precisely)
- Symptom: scene/menu **renders correctly but duplicates in two places during head
movement**; offset tracks head-motion direction; absent when still. Also shows in-game
as **black flashes on movement**. Same root cause.
- **Proven NOT the cause** (ruled out by logging + a pulled headset video whose mono
recorded frames are CLEAN): content rendering, stereo/IPD (65-68mm), FOV, swapchain
stage-vs-acquired index (always matched), session cycling (gone, ~72fps steady),
eye poses (flags=0xf), pixel count (capping supersample 1.2x→1.0 did NOT help).
- ~~Root cause = submit latency from the mandatory flush-wait.~~ **REFUTED 2026-06-24
by on-device measurement — see below.**
## UPDATE 2026-06-24: latency theory DISPROVEN; render-ahead abandoned
Measured on device (synchronous build, instrumented `xrr_end_frame`):
- **`submit-vs-predictedDisplayTime = −17 to −18 ms`** every frame (in menu AND gameplay).
We hand `xrEndFrame` to the compositor ~1.5 frames (at 90 Hz) **BEFORE** the intended
display moment. We are NOT late — there is healthy headroom. The whole "xrEndFrame
lands after predictedDisplayTime" premise is false.
- flush-wait blocks the render thread ~1 ms in menus, up to ~8–9 ms in gameplay, but
never enough to miss the deadline (still −17 ms).
- **Title screen dupes too, where flush-wait is only ~1 ms** → a bug that persists with
near-zero latency cannot be a latency bug.
- App runs **90/90 fps → ASW/motion-smoothing is NOT engaging** (would show 45/90).
- The pulled-video mono frames are clean → the doubling is introduced **at display time,
in stereo** (a mono capture can't show stereo divergence).
**Conclusion: the dupe is a stereo/compositor presentation issue, NOT latency.** The
render-ahead pipeline (below) was therefore the wrong fix AND broke rendering (see
"Why render-ahead failed"). It is now OFF by default and gated behind
`debug.re4vr.pipeline` purely for reference. Do not pursue it.
### Ruled out on 2026-06-24 (with the exact data)
- Layer layout: `layout=3` = `ovrpLayout_Array`, `arraySize=2`, we submit
`imageArrayIndex=eye` — correct, not a side-by-side/double-wide mismatch.
- Per-eye FOV: properly asymmetric & mirrored (L eye `right=+0.785`, R eye
`left=−0.785`, toed toward the nose), `up/down` symmetric. Correct.
- IPD ~0.068 m. Eye positions sane (`L≈(-0.037,1.088,0.005) R≈(0.031,1.090,0.007)`).
### Head-vs-eye camera mismatch — ALSO RULED OUT (2026-06-24)
`HEADvsEYE:` capture: `head.pos == eyeMid` exactly and `head.q == e0.q == e1.q`
exactly on every frame. UE's Head-derived cameras and our submitted `xrLocateViews`
eye poses are geometrically identical (parallel rig, midpoint = head). Not it.
### Visual A/B probes (debug.re4vr.diag) — localized the dupe INTO the per-eye image
Added a no-rebuild toggle: `adb shell setprop debug.re4vr.diag N; relaunch`.
- `diag=1` projection only (drop quads), `diag=2` force mono (both eyes sample
array layer 0). Applied in `build_composition`.
- **diag=2 (force mono): dupe STILL present** — a fixed offset down-and-right,
visible with the head still, small gap. → NOT stereo divergence (both eyes get
the identical image yet still doubled) and NOT a motion/reprojection ghost (those
vanish when still).
- **diag=1 (projection only): dupe STILL present** → NOT the quad/menu overlapping.
- `SUBMITLIST:` capture confirms only ONE eye-fov layer (LayerId=1) is submitted
(most frames `nLayers=1`); occasionally + one quad (LayerId=0). Eye layer flags
`0x4` (InverseAlpha-ish) / `0x14`; we don't act on them (compositing hints, not
layout — unlikely to cause a positional dupe).
### Where it stands: the duplicate is INSIDE one eye's image
With both eyes fed the identical array-layer-0 image and still doubled, each per-eye
image itself contains two offset copies. So the doubling is introduced either (a) in
UE's render INTO the eye texture, or (b) in the compositor's per-eye display path —
NOT in our stereo/layer pairing, poses, FOV, layout, or timing (all verified correct).
### ROOT CAUSE FOUND (2026-06-24): the dupe is a QUAD overlapping the projection
Texture readback + visual A/B settled it:
- Dumped both eye array layers + the quad to PPM (`xrr_vk_dump_image`, gated by
`debug.re4vr.dump`). **Both eye textures are CLEAN single images** (the eye
projection is a clean RE4 castle scene; pre-title eye is pure black; the quad is a
clean "armature" studio logo). So nothing we render is doubled.
- **Closing one eye: still doubled** → the second copy is within each eye's image
(NOT stereo divergence — earlier "converges at a head pose" was the world-locked
quad parallaxing against the projection).
- **`diag=1` (drop quad layers): the dupe DISAPPEARS** (logo becomes single).
→ **The duplicate IS the quad.** The game's UI/logo is present in the eye projection
AND re-submitted as a separate world-locked quad layer; the compositor shows both,
offset, so they parallax with head motion. Worst on title/menus (heavy UI quads),
mild in-game (few quads), flashes (quad submitted only on some frames per SUBMITLIST),
persists one-eyed (two real copies). NOT latency, stereo, FOV, layout, or timing.
### REFINED 2026-06-24 (via Quest recordings + frame blends): TWO artifacts
Pulled Quest spectator recordings (`/sdcard/Oculus/VideoShots/`) and blended head-
motion frame pairs (`ffmpeg fps=30` + ImageMagick `-average`). Single spectator frames
are always clean; blending two frames ~0.1-0.2s apart during a head turn exposes
differential motion. Findings:
- **Artifact A — logo duplication (diag=0):** the blend shows TWO "resident evil 4"
logos offset vertically while the background is nearly aligned → a second logo copy
that OVER-moves vs the scene. With `diag=1` (quads dropped) the over-moving copy is
gone. BUT the logo is STILL present in `diag=1` → the logo also lives in the eye
projection (the game renders it into the eye buffer) AND is re-submitted as a quad.
The quad copy is the visible dupe. Fix = handle/suppress the duplicate quad.
- **Artifact B — scene ghosting on head motion (persists with diag=1):** user still
sees heavy ghosting of the whole scene during head turns with quads dropped, yet
single spectator frames are clean → it's PER-EYE reprojection ghosting at display
(mono spectator can't show it). Textbook symptom of a **projection layer submitted
WITHOUT depth** → compositor can't do positional reprojection → head translation
uncompensated → ghosting. (`xrr_setup_layer_depth` currently returns Unsupported.)
### Depth layer (XR_KHR_composition_layer_depth) — IMPLEMENTED + TESTED → NEGATIVE
Implemented behind `debug.re4vr.depth` (default off): enable the ext in pre_init,
create a D32_FLOAT depth swapchain per eye layer (`setup_layer`), hand UE the depth
images via GetLayerTexture2, acquire/wait/flush(depth-aspect barrier)/release in
lockstep with color, chain `XrCompositionLayerDepthInfoKHR` on each projection view.
Tunable: `debug.re4vr.depth_nearz_mm`, `debug.re4vr.depth_revz`. Confined to the
synchronous path (`!g_pipelineActive`).
On device: depth swapchain created cleanly (fmt=126, 3 images, matches color), NO
xrEndFrame/validation errors, both reverse-Z and standard-Z tried. **Result: zero
improvement to the ghosting**, and the Quest spectator recording went **fully black**
with depth on (headset still rendered). → Meta's runtime accepts but does NOT use
plain KHR depth for reprojection (it uses Application SpaceWarp / motion vectors).
**Depth is not the fix here.** Left gated behind the prop (off); do not enable.
Recommendation (original, now amended): depth did NOT fix B.
### ROOT CAUSE OF ARTIFACT B FOUND (2026-06-24): frame drops from flush-wait serialization
Meta `VrApi` perf log during head motion (depth OFF, normal build):
- Calm: `FPS=72/72` steady. During motion: `FPS=30-64/72` (and `30-55/90`) with
`Stale=40-70` → the app misses display rate, compositor shows stale/reprojected
frames → the ghosting. Settles when still (app catches up).
- `App` GPU time is only 2-6ms (rendering is cheap!) but `CPU&GPU` total is 13-36ms
→ CPU↔GPU are SERIALIZED, inflating frame time. The synchronous flush-wait
(`xrr_vk_flush_wait` in end_frame, ~8ms/frame measured) blocks the render thread on
UE's GPU work each frame, killing CPU/GPU pipelining. Depth=1 doubled it (color +
depth flush-wait) → `30/90, 31ms`, which is why depth made ghosting WORSE.
**Artifact B = frame drops caused by the flush-wait serializing CPU and GPU.** Not
latency-submit, not depth, not stereo. The fix is to get the flush-wait off the
critical path so CPU/GPU pipeline again.
### Perf-level fix — IMPLEMENTED + CONFIRMED WIN (2026-06-24)
We were no-op'ing `ovrp_SetSystemCpuLevel2/GpuLevel2`. The game requests CPU=2
(SUSTAINED_HIGH) and GPU=3 (BOOST); now forwarded via XR_EXT_performance_settings
(`xrr_set_perf_level`, always on). On device: `perf level: CPU ovrp=2->xr=50 rc=0`,
`GPU ovrp=3->xr=75 rc=0`. Clocks ramped GPU 305-490MHz -> **525-587MHz**, CPU steady
2419MHz. FPS improved to mostly 72/72 + some 90/90 (user confirmed "fps higher, logo
better"). Remaining: motion drops to 45-68/72 (flush-wait, see copy ring) + occasional
severe 49ms hitches (likely title asset streaming, not our code). Keep this fix.
### UE-source facts (ue_src/, real OVR_Plugin_Types.h) that pin the copy-ring design
- UE wraps OUR VkImage as its RHI render target (`RHICreateTexture2D[Array]FromResource`
in CustomPresent_Vulkan) — so handing UE shim images via GetLayerTexture2 works.
- Present flow (`FinishRHIFrame_RHIThread`): for each layer UpdateLayer_RHIThread builds
the ovrpLayerSubmit (TextureStage = the stage UE rendered into THIS frame) -> EndFrame4
(our xrr_end_frame) -> on success, `IncrementSwapChainIndex_RHIThread` advances UE's
stage for next frame. So `submit->TextureStage` reliably names the image UE just drew.
- depthFormat 10 = ovrpTextureFormat_None (D16=6,D24_S8=7,D32_FP=8,D32_S824=9) -> UE does
NOT render depth. Depth path is moot (mapping corrected).
- ovrpLayerSubmit_EyeFov tail carries ViewportRect[2], DepthNear/Far, Fov[2] (in the
un-reversed union pad) if ever needed.
THE FIX for the motion drops (validated target): shim-owned copy ring.
Design (eye layers only, behind debug.re4vr.copyring): allocate shim VkImages per eye
layer (color, COLOR_ATTACHMENT|SAMPLED|TRANSFER_SRC); GetLayerTexture2 returns shim
images so UE renders into them on its own stage cadence. OpenXR swapchain gets
+TRANSFER_DST usage. Each end_frame: copy shimImages[submit->TextureStage] ->
openxr[acquiredIndex] (4 barriers: shim COLOR->TRANSFER_SRC, openxr UNDEFINED->
TRANSFER_DST, vkCmdCopyImage all array layers, openxr TRANSFER_DST->COLOR_ATTACHMENT,
shim TRANSFER_SRC->COLOR_ATTACHMENT) submitted NO-wait; present the PREVIOUS frame's
openxr image (wait its copy token = already done -> no CPU stall) with the stored
composition; hold this frame's openxr. The copy both resolves tile memory AND is
pipelined, so the CPU never blocks on the current frame's GPU = breaks the serialization.
UE's stage is decoupled (copy uses TextureStage explicitly), so no stage-coupling break.
### COPY RING — IMPLEMENTED (untested), behind debug.re4vr.copyring (default off)
Code: `xrr_vk_alloc_images`/`xrr_vk_free_images`/`xrr_vk_copy_submit` (vk_session.c),
shim fields on XrLayer, setup_layer allocates shim images + adds TRANSFER_DST to the
eye swapchain, GetLayerTexture2 hands UE the shim images, end_frame has a copy-ring
branch (copy shim[TextureStage]->openxr[acquiredIndex] no-wait, present previous frame,
hold current). Builds clean. **VALIDATION CUT: eye layers only — quads are DROPPED in
copy-ring mode, so MENUS/UI ARE HIDDEN.** It's for measuring whether removing the eye
flush-wait serialization restores framerate; if confirmed, next step is to handle quads
(composite current-frame quads on top of the pipelined eye, or give quads shim+copy too).
### TEST PLAN (run together when ready; perf-level fix is already always-on)
All live except where noted. Relaunch after setting copyring/sscap (decided at setup).
- Baseline (perf fix only): props all 0. Move head, `adb logcat -d | grep VrApi` — note
the FPS/Stale distribution (expect 72/72 mostly + drops to 45-68/72 on motion).
- Copy ring: `adb shell setprop debug.re4vr.copyring 1` + relaunch. Expect: scene visible
but NO menus; check VrApi FPS holds 72/72 (or 90/90) on motion with fewer Stale, and
ghosting reduced. Log: `copy-ring: ENABLED`, `copyring: allocated N shim images`,
`end_frame copy-ring`. If black/garbage -> a barrier/layout bug in xrr_vk_copy_submit.
- Supersample cap (stackable): `adb shell setprop debug.re4vr.sscap 1` + relaunch
(eye 1728x1900 -> 1440x1584; frees GPU + shrinks the copy). Log: `supersample cap: 1.0x`.
- Revert: set all back to 0, relaunch.
The 49ms hitches (title asset streaming) are NOT addressed by any of these.
### COPY-RING TEST RESULT (2026-06-24): works on title, BLACK in-game
On-device with copyring=1:
- Engaged cleanly (`copy-ring: ENABLED`, `allocated 3 shim images` for 1440 & 1728 eye
layers), frames flowed (`end_frame copy-ring #20161+`), one transient `xrEndFrame
FAILED rc=-23` (XR_ERROR_LAYER_INVALID) at the 1440->1728 layer swap.
- **TITLE: after a moment, ghosting RESOLVED — "everything looks good."** Frame pacing
hugely improved: long stretches of `FPS=72/72 Stale=0 CPU&GPU=2.9-3.2ms` (vs baseline
13-36ms) = the CPU/GPU serialization is broken, exactly as intended. **This proves the
flush-wait serialization is the ghosting root cause.**
- **IN-GAME: mostly BLACK.** From the one in-game snapshot: copy-ring still running,
app stuck at stage=2 (note: title also runs stage=2 and works, so stage isn't it),
a `WaitSwapchainImage TIMEOUT layer=1` near startup, no error flood.
- Leading hypothesis: the 1-frame pipeline holds 2 of the 3 OpenXR swapchain images;
under heavier in-game load the compositor can't return the 3rd in time -> begin_frame
xrWaitSwapchainImage TIMES OUT -> the present-pending chain breaks and does NOT
self-recover -> persistent black. (Title is light enough to never time out.)
- NEXT (tractable, not fundamental): make the copy-ring chain robust to a timeout —
on a missed acquire, present empty (valid) that frame and cleanly re-establish the
chain next good frame (never submit an invalid/stale layer; keep swapchain
acquire/release perfectly balanced across the timeout path). Then re-test in-game.
Also worth: confirm the timeout frequency in-game (capture was flaky — headset must
be worn for the whole window). Copy-ring left OFF by default; perf fix is the
shippable win so far.
### COPY-RING RETEST + PIXEL DUMP (2026-06-24): shim redirection breaks gameplay render
Robustness fix (present-empty on broken chain) added; retested in-game = still black.
Live buffer showed copy-ring running fine in-game: ~4000 frames (`copy-ring #2161..6241`),
**0 TIMEOUT, ~2 chain-broken, no FAILED** — so NOT timeouts/chain-break/errors.
Added a pixel dump to the copy-ring path (`debug.re4vr.dump` -> cr_shim_a0.ppm /
cr_xr_a0.ppm). Dumped the shim (UE's render target) and the post-copy openxr image:
- **Both are pure black (mean=0, std=0 — every pixel 0) in gameplay** (`shim stage=0`).
The copy is faithful (openxr == shim); the SHIM ITSELF is black = UE rendered nothing
into the shim image in gameplay.
- On the TITLE the copy-ring showed content (castle) -> shim had content there.
Conclusion: **UE renders into our shim image on the title but NOT in gameplay** — its
heavier gameplay render path apparently doesn't write to the resource-wrapped shim
(RHICreateTexture2DArrayFromResource) image we hand it via GetLayerTexture2. Likely a
UE render-target/MSAA/resolve detail specific to the 3D scene path. Needs UE-internals
+ RenderDoc to chase; not resolvable via remote logcat/dump loop.
## RENDERDOC DEEP-DIVE (2026-06-24) — copy-ring gameplay = MSAA resolve interaction
Set up offline RenderDoc replay (huge: no headset needed for analysis):
- App made debuggable via apktool (packaging/work/dbg flow -> re4vr-dbg.apk); RenderDoc
Android server installed; capture over USB; replay headless with
`qrenderdoc --python` over `adb://<usb-serial>` + CreateRemoteServerConnection +
CopyCaptureToRemote + remote.OpenCapture (local replay of an Android capture fails;
wireless adb drops the replay connection — USB is required). Scripts in
~/renderdoc-captures/rd_*.py ; captures RE4/black.rdc (gameplay), RE4/title.rdc.
- FINDING: UE renders the eye with **2x MSAA** into its OWN target (RenderDoc res 11965,
ms=2), and the render pass **resolve attachment is our copy-ring shim** (ms=1) — i.e.
UE resolves the MSAA scene INTO the shim we copy. So there is NO stage mismatch; the
shim IS the resolve target. BUT in the gameplay capture the resolved shim is not the
scene at our copy point (reads white/garbage), while on the title it lands fine.
- So the copy-ring gameplay black is an **MSAA-resolve-into-our-shim interaction**: our
externally-created VkImage (MUTABLE_FORMAT + COLOR|SAMPLED|TRANSFER_SRC|DST|INPUT_ATT,
TILING_OPTIMAL) isn't receiving UE's MSAA resolve correctly in the gameplay path.
Suspects to chase next: the MUTABLE_FORMAT/sRGB-vs-UNORM view used as resolve target,
the image's create flags vs what a valid resolve dst needs, or tile-memory MSAA
specifics on Adreno. (Single-shim made it worse — UE needs distinct per-stage images.)
- Tooling note: GetMinMax(...,CompType.Typeless) gives misleading values; trust
SaveTexture/visual instead.
## NET STATE (end of 2026-06-24 session)
- SHIPPABLE WIN: perf-level fix (always on) — clocks boost, framerate up, confirmed.
- ROOT CAUSE PROVEN: motion ghosting = frame drops from the flush-wait serializing
CPU/GPU (copy-ring eliminated it on the title -> CPU&GPU 13-36ms dropped to ~3ms).
- COPY-RING: built + behind debug.re4vr.copyring (default off). Works on title, but the
shim redirection leaves gameplay black (UE not rendering into the shim in-game).
Parked pending UE-render-path investigation.
- DEAD ENDS (with evidence): latency-submit theory (we submit early), depth layer (UE
passes None; Meta ignores plain KHR depth anyway), render-ahead-by-holding-OpenXR-
images (breaks UE's texture-stage coupling), stereo/FOV/layout (all correct).
- Levers available: debug.re4vr.sscap (supersample cap), and the perf fix is permanent.
- Diagnostic toolkit retained: debug.re4vr.{copyring,depth,pipeline,noflushwait,diag,
dump,sscap} + the perf/flush-wait probes. UE renders into shim-allocated
VkImages on its own stage cadence (decoupled from the OpenXR swapchain, so no
stage-coupling break like the render-ahead attempt). Each frame: copy the shim image
into a freshly-acquired OpenXR image and pipeline the flush (wait the PREVIOUS frame's
copy, which is already done) → CPU never blocks on the current frame's GPU. Releases
the OpenXR image normally each frame (no holding → stage cadence intact). Cost: one
full-res image copy/frame (~10% bandwidth) + extra VRAM; big but well-scoped. This is
the original "option 2" and is now backed by the VrApi frame-drop data.
Tooling that worked: `debug.re4vr.diag` visual A/B + `debug.re4vr.dump` texture readback
+ Quest recording → frame-blend (the only way to make the artifact objectively visible).
### (Artifact A) figure out the correct quad handling
Open question — why does the same content appear in BOTH the projection and a quad
(real OVRPlugin presumably shows it once)? Avenues:
1. Confirm the overlap: dump eye + quad on the SAME title frame and check the logo is
in both (double-render) vs the quad being the only intended copy.
2. UI routing: the game may render UI into the eye buffer only because some ovrp_* call
we stub makes it think it's NOT in a layer-composited VR mode. Audit stubs that gate
UE's "render UI to a separate layer vs into the eye buffer" decision.
3. Quad placement: we world-lock the quad at the app's submitted pose in appSpace
(LOCAL_FLOOR). If the app's pose assumes a different space/convention, our quad is
offset from where the projection shows the same content; correct placement (or
head-locking) could make them coincide.
Workaround that proves the cause (not a fix): `diag=1` drops quads → dupe gone but
menus/overlays vanish and title head-tracking feels less smooth.
### (superseded) read back the eye texture — DONE, textures are clean
Add a one-shot GPU readback of eye-swapchain array layer 0 (copy VkImage→host buffer,
dump PPM, `adb pull`) gated behind a prop. If the dumped texture is doubled → it's
UE's render (game/UE-side; investigate multiview / the RE4 VR mod's render setup). If
the dumped texture is CLEAN/single → the compositor introduces it at display (per-eye
distortion/reprojection path; pursue Meta-specific settings / frame capture).
Everything cheaper than this has been exhausted. Device left on the clean synchronous
(playable) path; `debug.re4vr.diag`/`debug.re4vr.pipeline` both 0.
## (Dead end, kept for reference) The render-ahead pipeline
## The fix: take the flush-wait OUT of the critical path (pipeline it) — IMPLEMENTED 2026-06-24
Render-ahead by one frame so we never block the submit. **Status: built, compiles
clean, NOT yet tested on device.** Deploy + test per the commands at top.
How it works now (`xr_runtime.c xrr_end_frame`, gated by `g_pipelineActive`):
1. `xrr_begin_frame`: acquires image `A_N` for each layer as before (unchanged).
2. `xrr_end_frame` **(A)**: `xrr_vk_flush_submit(A_N)` — submits the barrier, returns a
ring token, does **NOT** wait.
3. **(B)**: waits the *previous* frame's flush token (already done → ~free), releases
`A_{N-1}` (FIFO → releases the older, present-pending image), and `xrEndFrame`s the
**stored** composition `g_pending` (frame N-1's views + predictedDisplayTime).
4. **(C)** builds frame N's composition into `g_pending`; **(D)** promotes `A_N` to
`presentPending` (held one more frame; `begin_frame` re-acquires fresh).
- **Why the stage↔acquire invariant survives:** still exactly one acquire + one release
per frame, just offset by one → the OpenXR FIFO and UE's `TextureStage` stay in
lockstep (the `LAYER MISMATCH` log will fire if this ever breaks — watch it).
- Net: +1 frame latency (~13ms, absorbed by normal reprojection); CPU never blocks on
the flush → `xrEndFrame` lands on schedule → ghosting should clear.
- **Engage gate:** pipeline turns on once `xrr_vk_flush_ready()` AND every active layer
has `imageCount >= 2` (Quest gives 3). Until then it runs the **synchronous fallback**
(old flush+wait+release path, retained) — so worst case = today's behaviour, not a
regression. Look for `render-ahead pipeline engaged` in logcat to confirm it switched.
- New split flush API in `vk_session.c`: `xrr_vk_flush_submit`/`_wait`/`_ready`; ring
grown to `XRR_MAX_LAYERS*2` so a token stays valid a full frame.
### On-device validation checklist
- Confirm `render-ahead pipeline engaged` appears once, early.
- Watch for `LAYER MISMATCH` (should NOT appear) and `xrEndFrame FAILED` (should NOT).
- Heartbeat now prints `pipelined=1`. Framerate should stay ~72fps.
- **First tuning knob if ghosting persists:** `submit_pending()` uses the STORED
`g_pending.displayTime` (frame N-1's predicted time). If motion judders/over-reprojects,
try using the *current* `g_xr.frameState.predictedDisplayTime` instead while keeping
the stored views — one-line change, documented inline. (Views must stay stored.)
- Session teardown: `pipeline_reset()` releases held images + clears `g_pending` on
STOPPING so a restart doesn't present a stale composition over destroyed swapchains.
- Risk: medium-high (acquire/release pairing, holding an image across frames). If it
misbehaves, set `g_pipelineActive` permanently 0 to fall back to the synchronous path.
## Other levers to try (cheaper, possibly complementary)
- **Cap supersample**: `src/layers.c ovrp_CalculateEyeLayerDesc2` — `if (textureScale>1) textureScale=1;`
Tried, didn't fix ghosting alone, but frees GPU headroom; may help combined with pipelining.
- **Submit a depth layer** (`XR_KHR_composition_layer_depth`): better positional reprojection.
`xrr_setup_layer_depth` currently returns Unsupported; would need a depth swapchain +
`GetLayerTexture2` depth handles + `XrCompositionLayerDepthInfoKHR` on the projection.
- **Adjust predicted display time**: account for the flush-wait latency when filling
`xrEndFrame.displayTime` so the compositor reprojects less. Hacky; secondary.
## Key files / functions
- `src/vk_session.c` — `xrr_vk_flush_image` (the flush+wait; pipelining changes the wait),
`detect_ue_queue` (queue is family0/idx0), `xrr_vk_set_handles`.
- `src/xr_runtime.c` — `xrr_begin_frame`, `xrr_end_frame` (acquire/flush/release/compose),
`make_app_space` (LOCAL_FLOOR), heartbeat/diag logs.
- `src/layers.c` — `ovrp_CalculateEyeLayerDesc2` (resolution/FOV), quad placement is in
`xr_runtime.c` end_frame.
- Diagnostic logging still in (rate-limited, harmless): begin/end heartbeats, view poses,
layer stage-vs-acquired mismatch, quad pose/size/flags, GetNodePose nodes. Strip before
any public release.
## Verified facts to trust
- App's rendered frames are CLEAN (pulled headset video confirms) — the bug is display-time.
- Frame loop runs ~72fps with the flush-wait; the issue is per-frame reprojection from
pose/time latency, not dropped framerate.
- Removing the flush-wait → black (runtime won't sync for us). The wait is mandatory in
its current synchronous form; pipelining is how to keep it without the latency.
+60
View File
@@ -0,0 +1,60 @@
# Reverse-engineering notes — libOVRPlugin.so
Pinned version: OVRPlugin **1.51** / pkg `ovrplugin-android-universal:19.0.0.449.531`.
Cross-reference header: public **OVRPlugin.cs @ v1.51** (Unity Oculus Integration,
many GitHub mirrors) — gives ovrp_ signatures + [StructLayout] struct layouts.
## RE pipeline (working)
Headless Ghidra 12.1.2, driven by `ghidra_scripts/DumpOvrp.java`:
```
export JAVA_HOME=~/dev/re4vr-port/tools/jdk-21.0.11+10 # portable Temurin JDK
tools/ghidra_12.1.2_PUBLIC/support/analyzeHeadless ghidra_proj re4vr \
-import dump/apk_libs/lib/arm64-v8a/libOVRPlugin.so \
-scriptPath ghidra_scripts -postScript DumpOvrp.java -deleteProject
```
Gotchas solved: Ghidra needs a JDK (JREs rejected) -> portable Temurin in tools/.
Ghidra 12 dropped bundled Jython -> scripts must be Java, not .py.
Output: `analysis/ovrp_decomp.txt` (537 ovrp_ sigs, 27 core bodies).
## Key structural finding
Exported ovrp_* are THIN THUNKS: `(*PTR_ovrp_X)()` jumping to the real impl,
which calls internal C++ `OVR::Util::Compositor::*` (partially symbolized).
=> The game (Java System.loadLibrary + dlsym) calls the EXPORTED symbols. So the
SHIM just re-exports the same ovrp_ symbol names backed by OpenXR and replaces
libOVRPlugin.so wholesale. We do NOT reverse the internal Compositor C++.
Decompilation = reference for semantics + struct sizes only.
## ABI confirmed (triangulation: Ghidra shape + v1.51 header types = MATCH)
ovrp_GetNodePoseState3(ovrpStep, int frameIndex, ovrpNode, ovrpPoseStatef* out):
- null out -> returns 0xfffffc17 = -1001 (ovrpFailure_InvalidParameter)
- not init -> returns 0xfffffc16 = -1002 (ovrpFailure_NotInitialized)
- success -> memcpy(out, ..., 0x58) then return 0
- **ovrpPoseStatef = 0x58 = 88 bytes** == public layout:
ovrpPosef(28) + 4x ovrpVector3f(48) + double Time(8) -> pad 88. EXACT MATCH.
=> Public OVRPlugin.cs v1.51 struct layouts are TRUSTWORTHY for this binary.
ovrpResult error-code convention confirmed (-1001 invalid, -1002 not-init).
## Layer structs reversed (2026-06-23, analysis/struct_layouts.txt)
Ghidra recovered C++ type NAMES from demangled symbols but empty layouts (no DWARF);
real offsets came from decompiling OVR::Util::Compositor methods.
- Two compositor backends: CompositorVRAPI_OpenGL + CompositorVRAPI_Vulkan (RE4=Vulkan).
- **ovrpLayerDesc = 0x7c (124B)** [VERIFIED]: ImportLayerDesc memset's 0x7c; switch on
+0x00 = ovrpShape (cases 0,1,2,4,5 overlays; case 3 = EyeFov projection). Three
historical copy sizes 0x68/0x6c/0x7c = base / +DepthFormat / +MotionVector. Full
field layout written to header, offsets confirmed (Fov@0x20, VisibleRect@0x40,
DepthFormat@0x68).
- **ovrpLayerSubmit = 0x130 (304B)** [VERIFIED]: EndFrame4 allocs count*0x130. Header
fields (LayerId, TextureStage, ViewportRect[2], Pose@0x28, Flags) = public layout;
per-shape union tail still [TODO] (reserved bytes for now). Stored internally as
ovrpLayerSubmitUnion in a std::map<int,pair<union,int>>.
Header now has _Static_asserts sizeof(ovrpLayerDesc)==124 && ovrpLayerSubmit==304;
both pass, shim rebuilds (239 symbols).
## Next RE passes (when continuing)
- Dedup the export-thunk vs impl entries in the dump (script polish).
- For each CORE fn: pair Ghidra arg-shape with v1.51 C# sig -> finalized C header
for the shim (ovrp types + struct layouts).
- Reverse outliers NOT in the public header (e.g. SpaceWarp/ASW property paths the
binary references, any custom Armature behavior).
- Entitlement (libovrplatformloader's ovr_* surface) is out of scope for this repo:
no replacement ships and nothing is circumvented (see README "Legal / scope").
+204
View File
@@ -0,0 +1,204 @@
# RE4 VR (Quest 2) — Dump & Recon Checklist
Goal of this phase: **non-destructively dump your own legally-owned copy of RE4 VR
off the Quest 2 and answer the one question that decides the whole project** —
is the VR runtime OpenXR (shimmable) or VrApi (proprietary, must reimplement)?
Target app: `com.Armature.VR4` (Armature Studio / Capcom / Oculus, UE 4.25.3)
Legal posture: dump-your-own only. We extract from hardware *you own* running a
copy *you own*. **Nothing here gets redistributed** — only patches/shims you
author, applied by people who dump their own copy. Same model as ReXGlue.
---
## 0. Prereqs (do while the Quest charges)
- [ ] Install Android platform-tools (adb): `sudo apt install android-tools-adb`
or grab Google's platform-tools zip.
- [ ] Verify: `adb version`
- [ ] Install analysis tooling:
- [ ] Ghidra (native .so disassembly/patching)
- [ ] `patchelf`, `binutils` (`readelf`, `nm`, `objdump`), `file`, `unzip`
- [ ] Python 3 + `pip install lief` (scripted ELF inspection/patching)
- [ ] FModel **or** umodel/UModel (UE 4.25 .pak browsing) — optional this phase
- [ ] FluffyQuack's UnrealPak tools — optional this phase
- [ ] Enable **Developer Mode** on the Quest (Meta Quest mobile app →
Devices → Developer Mode → on; requires a registered dev org — free).
- [ ] Plug Quest into PC via USB-C, put on headset, **Allow USB debugging**
when prompted (and check "always allow from this computer").
---
## 1. Confirm the device + app are visible
```bash
adb devices # should list one device, state "device" not "unauthorized"
adb shell pm list packages | grep -i armature # expect: com.Armature.VR4
adb shell dumpsys package com.Armature.VR4 | grep -i versionName
```
- [ ] Device shows as `device`
- [ ] `com.Armature.VR4` present
- [ ] Note the versionName here: `____________`
---
## 2. Locate and pull the APK(s)
Split APKs are common, so grab every path.
```bash
adb shell pm path com.Armature.VR4 # prints one or more base/split apk paths
mkdir -p ~/dev/re4vr-port/dump && cd ~/dev/re4vr-port/dump
# pull each path the command above printed, e.g.:
adb pull /data/app/~~xxxx/com.Armature.VR4-yyyy/base.apk .
# repeat for any split_*.apk lines
```
- [ ] `base.apk` pulled
- [ ] Any `split_*.apk` pulled
- [ ] Record sizes: `ls -lh *.apk`
---
## 3. Pull the OBB (game data / .pak files)
```bash
adb shell ls -la /sdcard/Android/obb/com.Armature.VR4/
adb pull /sdcard/Android/obb/com.Armature.VR4/ .
```
- [ ] OBB pulled (e.g. `main.NNN.com.Armature.VR4.obb`)
- [ ] Note the version number NNN in the OBB filename: `______`
(you'll need it if you ever repack)
---
## 4. ⭐ THE DECISIVE CHECK — OpenXR vs VrApi
This is the whole reason we're here. Inspect the native libs in the APK.
```bash
cd ~/dev/re4vr-port/dump
unzip -l base.apk | grep -iE 'lib/arm64-v8a/.*\.so' # list native libs
# the money grep:
unzip -l base.apk | grep -iE 'arm64.*(vrapi|openxr|ovrplatform|oculus|UE4)'
```
Interpret the result:
| Lib found in `lib/arm64-v8a/` | Meaning | Difficulty |
|-----------------------------------|-----------------------------------------------------|------------|
| `libopenxr_loader.so` | ✅ Standard OpenXR — Steam Frame provides a runtime; you translate vendor extensions. | Tractable |
| `libvrapi.so` | ⚠️ Proprietary Meta VrApi — must reimplement/shim the runtime. | Hard |
| `libovrplatformloader.so` | Present either way — this is the **entitlement check** to NOP/stub. | Required patch |
| `libUE4.so` (or split into modules)| The Unreal runtime itself — the host you'll be hooking. | n/a |
- [x] **RESULT — runtime is:** ☑ **VrApi** (legacy OVRPlugin path). Confirmed
2026-06-23 on app v2.3 (versionCode 203). `libvrapi.so` present,
`libopenxr_loader.so` ABSENT. `libOVRPlugin.so` NEEDs `libvrapi.so` and
imports 114 `vrapi_*` symbols; 0 OpenXR symbols anywhere.
- [x] `libovrplatformloader.so` present? ☑ yes (hard-NEEDED by libUE4.so)
**REFINED WIRING (the seam that matters):**
```
libUE4.so --(ovrp_* C API, 239 refs)--> libOVRPlugin.so --(114 vrapi_)--> libvrapi.so --> Horizon OS
libUE4.so --(NEEDED + ovr_* Platform SDK, 132 refs)--> libovrplatformloader.so (entitlement)
```
- libUE4.so has **0 vrapi_ refs** — game speaks **OVRPlugin's ovrp_* C API**, not
VrApi directly. libvrapi is just OVRPlugin's backend.
- => **PORT SEAM = reimplement libOVRPlugin.so (ovrp_* on OpenXR), drop libvrapi.**
ovrp_* is the documented Unity-shared API (OVR_Plugin.h); modern Meta OVRPlugin
has an OpenXR backend = reference impl / prior art.
- => **Stub the ovr_* Platform SDK** (libovrplatformloader.so) — entitlement/account,
just return OK; don't reimplement.
> *Editor's note (2026-06): the entitlement-stub approach described here and elsewhere in this
> doc (the §4 table, the wiring summary above, the decision tree below) was **not** carried
> into the project. Entitlement handling is out of scope and the shim ships no circumvention
> code — see the README's scope section. These passages are kept as a record of the original
> recon, not as instructions.*
> If you want to double-check beyond filename presence, extract and inspect
> imports of `libUE4.so` (it may dynamically link the runtime):
> ```bash
> unzip base.apk 'lib/arm64-v8a/*' -d apk_libs
> readelf -d apk_libs/lib/arm64-v8a/libUE4.so | grep -i NEEDED
> nm -D --defined-only apk_libs/lib/arm64-v8a/libopenxr_loader.so 2>/dev/null | grep -i xr | head
> nm -D apk_libs/lib/arm64-v8a/libUE4.so | grep -iE 'xrCreate|vrapi_' | head
> ```
> `xrCreate*`/`xr*` symbols → OpenXR codepath. `vrapi_*` symbols → VrApi codepath.
> A binary can ship both libs but only *call* one — the symbol check tells you
> which is actually wired up.
---
## 5. Manifest & build recon
```bash
# Needs apktool (sudo apt install apktool) OR aapt from build-tools
aapt dump badging base.apk | grep -iE 'sdkVersion|native-code|package'
apktool d -s base.apk -o apk_decoded # -s = don't decode .dex, faster
grep -iE 'oculus|vr|xr|entitlement|permission' apk_decoded/AndroidManifest.xml
```
- [ ] minSdk / targetSdk noted: `______`
- [ ] `native-code` ABI (expect `arm64-v8a`): `______`
- [ ] Any Oculus/entitlement metadata flags in manifest noted below.
---
## 6. Confirm .pak accessibility (asset layer)
The community reports these are unencrypted — verify so you know the asset
layer is open if you ever need it.
```bash
# unzip the OBB (it's a zip), find Content/Paks/*.pak, then:
# try opening in FModel/umodel as UE 4.25.3, no AES key
```
- [x] .pak opens with no AES key (confirms community finding) ☑ yes
Verified 2026-06-23: pakchunk9 footer bEncryptedIndex=0, EncryptionKeyGuid
all-zero, pak version 9 (FrozenIndex / UE4.25-26). Plaintext index — 53
readable asset paths incl. /Game/Levels/BIO4/... Asset layer fully open.
- OBB layout: store (uncompressed) zip. main.203 (4.0GB) + patch.203 (4.0GB,
the v2.3 update layer) + a stashed VR4-Android-Shipping-arm64.apk (== base.apk).
Paks at VR4/Content/Paks/pakchunk{0..9}[optional]-Android_ETC2.pak (ETC2 =
Android texture compression; 'optional' = hi-res texture chunks).
Bink cutscenes at VR4/Content/Movies/*.bk2.
---
## 7. Record findings → decide path
Fill this in, then we branch:
```
versionName: 2.3 (versionCode 203, minSdk 25, targetSdk 29)
runtime: VrApi (libvrapi via libOVRPlugin; NO openxr) [CONFIRMED 2026-06-23]
ovrplatform loader: present (hard-NEEDED by libUE4.so)
UE version: 4.25.3 (engine = libUE4.so, stripped, arm64)
ABIs: arm64-v8a (single base.apk, no splits)
paks encrypted: NO — unencrypted, no AES key (CONFIRMED 2026-06-23, pak v9)
device: Quest 2 serial <redacted-serial> (codename hollywood)
```
**Decision tree:**
- **OpenXR** → next phase: map which Meta OpenXR vendor extensions the binary
requests, plan the OpenXR→Steam-Frame-runtime shim + entitlement stub.
This is the "weekend-of-shimming" branch.
> *Editor's note (2026-06): the "entitlement stub" floated here was later removed from the
> project. Entitlement handling is out of scope and the shim ships no circumvention code —
> see the README's scope section. This line is left as a record of the original plan.*
- **VrApi** → next phase: scope a VrApi reimplementation/translation shim
(much larger). Reassess whether the project is worth it vs. waiting/UEVR.
- **Either way** → `libovrplatformloader.so` entitlement bypass is a required
Ghidra patch (legal for your own copy).
---
## Notes / scratch
(paste command output, symbol dumps, and decisions here as you go)
+60
View File
@@ -0,0 +1,60 @@
# OVRPlugin -> OpenXR shim — scope
Derived 2026-06-23 from RE4 VR v2.3 (`com.Armature.VR4`). Method: extracted the
distinct `ovrp_*` names `libUE4.so` references (dlsym targets) and intersected
with `libOVRPlugin.so`'s exports. Raw lists in `analysis/`.
## Headline numbers
- OVRPlugin exports **438** `ovrp_*` entry points.
- The game actually references **239** of them. That's the shim surface.
- But **~150 of the 239 are stub-to-constant / no-op** for a basic port.
Realistically **~85-90 functions need real implementation**, of which the
genuinely hard core is **~40** (frame loop + layer/swapchain + the display
interop).
## Render API: VULKAN (confirmed)
`libUE4.so` has VulkanRHI compiled in and calls `ovrp_Get{Instance,Device}ExtensionsVk`.
libGLESv2/EGL are NEEDED but that's the standard Android baseline; the active
renderer is Vulkan. => swapchain path is the standard **OpenXR `XR_KHR_vulkan_enable2`**
(import the app's VkImages into `XrSwapchain`). Well-trodden, not exotic.
## STUB to no-op/constant (~131 counted, +misc ≈ 150)
| Group | count | how to stub |
|---|---|---|
| Mixed Reality Capture (`ovrp_Media_*`) | 37 | return not-initialized / no-op |
| Camera device (passthrough/depth) | 21 | report unavailable |
| External camera (MRC) | 13 | report 0 cameras |
| Perf/GPU/CPU/ASW/foveation tuning | 31 | accept+ignore, return safe defaults |
| Boundary / Guardian | 7 | "not configured" (or map to XR play bounds later) |
| Hand tracking | 7 | disabled (RE4 VR is controller-only) |
| System-info getters | 15 | return plausible constants (headset type, region…) |
Stubbing these = ~150 functions for near-free. None affect core gameplay.
## MUST implement (~88 counted; the project's real work)
| Group | count | notes |
|---|---|---|
| Tracking / poses | 32 | mostly mechanical: map OpenXR `xrLocateSpace`/views to `ovrp` pose structs |
| Display / layer / swapchain | 20 | THE HARD PART — projection layers, eye FOV, foveation, swapchain stages |
| Input / controllers | 11 | map Touch controller -> OpenXR action set; mechanical but fiddly |
| Eye / user params | 10 | IPD, eye height, pixels-per-tan-angle -> from XR view config |
| Init / shutdown | 8 | session create/begin/end, instance+system setup |
| Frame loop | 6 | `xrWaitFrame`/`xrBeginFrame`/`xrEndFrame` <-> `ovrp_*Frame4`, predicted display time |
## Verdict
Single-developer-feasible, meaty. The "239 functions" headline collapses to
**~40 hard + ~45 mechanical + ~150 stubs.** The hard 40 are the standard guts of
any OpenXR app (frame loop, projection layers, Vulkan swapchain, pose/input
mapping). **Modern Meta OVRPlugin already ships an OpenXR backend** — that's the
reference for exact `ovrp_*` semantics and struct layouts, which removes most of
the guesswork.
Biggest unknowns / next probes:
1. Exact `ovrp_*` struct layouts/ABI (need OVR_Plugin.h matching this OVRPlugin
version, or RE them in Ghidra). ABI mismatch = crashes.
2. How the Java side loads libOVRPlugin (System.loadLibrary) — confirms the
replacement mechanism (drop-in shim .so with same SONAME).
3. Entitlement: out of scope for this project — no `ovr_*` replacement ships here;
the platform's real check runs unchanged (see README "Legal / scope").
4. Whether Steam Frame exposes an Android-app OpenXR runtime at all (the OTHER
big external unknown — the shim is moot if the APK can't load there).
+195
View File
@@ -0,0 +1,195 @@
# P4 passthru — NATIVE ground-truth (2026-06-27, title screen, seated)
Captured via `debug.re4vr.passthru=1` (real libOVRPlugin owns the session through our shim).
Boots clean to the title screen, no ghost — so these are the CORRECT values to match.
## Eye layer desc (CalculateEyeLayerDesc2)
- size: **1440 x 1584** per eye
- FovL: U 1.111 D 1.192 L 0.933 R 1.000 (tangents)
- FovR: U 1.111 D 1.192 L 1.000 R 0.933
- => asymmetric (U≠D) AND canted stereo (per-eye L/R mirrored: inner edge = 1.000, outer = 0.933)
## EndFrame4 submit
- 1 layer, id=1 (first 3 frames) then id=2, **flags=0x4**, pose = all-zero (0,0,0)/(0,0,0,0)
- flags 0x4 = bit2 (NOT HeadLocked=0x1)
## Eye poses (GetNodePoseState3, head tilted on table)
- eye0 (L): pos=(-0.0934, 1.2170, 0.2354) quat=(0.0314, 0.1109, -0.0207, -0.9931)
- eye1 (R): pos=(-0.0271, 1.2203, 0.2503) quat=(same as L)
- both eyes share orientation; offset L->R ≈ (+0.066, +0.003, +0.015) m in world (IPD ~66mm)
## OUR shim values (captured passthru=0, title screen) — DIFF RESULT
- eye size: **1440x1584** — MATCHES native (the 1728x1900 was an old supersample config).
- FOV: ours via OpenXR xrLocateViews, logged as angles (rad). Steady-state L.fov(l,r,u,d)=
(-0.750,0.785,0.838,-0.873) -> tangents U1.110 D1.190 L0.932 R0.991 — MATCHES native
(U1.111 D1.192 L0.933 R1.000). The game does NOT call GetNodeFrustum2; FOV comes from
CalculateEyeLayerDesc2 in BOTH paths.
- IPD: 0.065-0.068 — MATCHES native ~0.066.
- eye orientation: e0.q==e1.q==head.q in both — MATCHES.
- submit flags: eye-fov LayerId flags=0x4 — MATCHES native (0x14 later = menu-quad layer
present, same game logic).
## CONCLUSION (title screen)
Static eye geometry (size/fov/ipd/orientation/flags) is IDENTICAL native vs shim at the title
screen. The seated in-game hand-deform ghost is therefore NOT a static-geometry mismatch.
ONE anomaly: our shim's FIRST ~5s report a WIDER outer FOV (L outer angle -0.855, tan 1.149)
that settles to native's -0.750/0.933 — a cold-start transient (matches title-ghost-is-coldload
-reprojection-judder; self-resolves warm). Not the persistent in-game ghost.
## CONFIRMED: passthru (native) has ZERO ghost — hands + title (user, 2026-06-27)
=> the ghost is in OUR shim's path, not the game/headset.
## *** ACTUAL ROOT CAUSE: DROPPED FRAMES -> compositor reprojection multiples (2026-06-27 PM) ***
The submit-ordering theory below was WRONG (submithook=2 present-on-submit AND submithook=3
+full device-wait-idle completion BOTH still ghost). Added always-on ANOMALY logging of the
OpenXR runtime RESPONSES (not our inputs, which all check out): locateViews validity,
WaitSwapchainImage timeout, xrEndFrame errors, frame-pacing. In-game ghost capture result:
- frame-pacing: 42 MISS / 3986 frames (~1%), gaps 21-137ms vs 13.9ms period
- locateViews invalid: 0 WaitSwapchain timeout: 0 xrEndFrame err: 0
=> the ONLY fault is DROPPED FRAMES. We miss the display deadline; the compositor reprojects the
held frame to fill the gap; during motion that = the flashing multiples. Scales exactly with user's
report: 21ms gap (1 drop)=mild, 137ms (~10 drops)=violent; load/position-dependent; non-deterministic;
clean dumped eye textures (pixels fine, just late). User CONFIRMED dump "no ghost" was the dump's
crawling fps freezing reprojection, not a fix — consistent.
KEY OPEN Q: are the drops OURS (shim latency, e.g. per-frame xrr_vk_flush_wait) or the game's load
(native hits them too but its compositor rides them out cleanly)? Added ENDFRAME-PACE log at
ovrp_EndFrame4 ENTRY (core.c) that runs in BOTH modes -> compare NATIVE vs shim gap rate.
- if NATIVE also hits 137ms gaps clean -> fix = match native compositor frame-timing/handling
(ExtraLatencyMode / phase sync / how late frames are submitted), NOT eliminate hitches.
- if NATIVE smooth -> our shim adds the latency -> reduce it (flush-wait is prime suspect).
NOTE for Steam Frame: passthru is Meta-only (vrapi); the OpenXR shim is the only cross-platform
path, so this drop/repro fix is what matters for portability.
## *** SUCCESS: game-thread pacing FIXED the ghost (2026-06-27, user-confirmed) ***
User: title ghost GONE, seated ghost GONE, standing black-flash GONE, "way better... feels like a
great success." Logs: BC (render-thread stall) 148ms->~3ms (FIXED); frame-pacing drops 0.79%
(180/22862, was ~1%+10%-in-motion, native 0.34%); WAITPACE steady 13.92ms. The ROOT CAUSE was: our
shim left the game thread UNPACED (WaitToBeginFrame no-op) and paced the render thread instead, which
desynced UE's internal pipeline and stalled the render thread during motion -> dropped frames ->
compositor judder = the "ghost". Fix = pace the game thread (xrWaitFrame in xrr_wait_frame) + FIFO
frameState handoff to render thread. Keep ffr=-1 (game FFR) + blackcount=0. RESIDUAL (tunable):
occasional "BeginFrame too many times" -> XR_FRAME_DISCARDED = brief hiccup; from the ring DROPPING
oldest frameState when render falls behind (desyncs wait/begin 1:1). FIX: block the game thread for
ring space (back-pressure) instead of dropping. THEN: strip diagnostic scaffolding (FLOOP/STALL/
JUDDER/WAITPACE/burst-dump/submithook leftovers), make ffr=-1+blackcount=0 defaults, commit.
## (attempt that became the fix) game-thread pacing rework
FLOOP trace proved the loop: WAIT(N) on GAME thread (tid A) runs 1 frame ahead of BEGIN/END(N-1)
on RENDER thread (tid B); steady BEGIN->END ~1ms. Old shim: WaitToBeginFrame=no-op, xrWaitFrame on
RENDER thread inside ovrp_BeginFrame4 (a workaround for "BeginFrame too many times"). REWORK (matches
vrapi + OpenXR's recommended pipelined model): xrWaitFrame now runs in xrr_wait_frame on the GAME
thread (blocks=paces it), NOT under g_xrlock; the frameState is handed to the render thread via a 1:1
FIFO ring (g_fsRing/g_fsHead/g_fsTail + g_fsCond). xrr_begin_frame pops the frameState (cond_timedwait
20ms) instead of calling xrWaitFrame; xrBeginFrame/xrEndFrame stay on the render thread. Theory: the
game thread was unpaced (no-op wait) so UE's internal pipeline desynced and the render thread stalled
~85ms during motion -> dropped frames -> judder. Pacing the game thread should fix it. RISK: wait/
begin must stay 1:1 to the runtime; if a begin is rejected (inFrame) after a wait was pushed, could
desync -> watch for xrWaitFrame/xrBeginFrame xrfail. Keep ffr=-1 + blackcount=0 (both help). Testing.
## *** REFINED: BC (UE render thread) BLOCKS ~85ms at only ~14ms GPU = architectural (2026-06-27 latest) ***
After FFR=-1 (applied ffr=1, GPU fed~13.7ms) AND blackcount/lumagate off (luma readback gone):
ghost PERSISTS. STALL split now: BC (begin_frame->end_frame = UE render) = 47-101ms while GPU does
only ~14ms of work => UE's RENDER THREAD is BLOCKING ~85ms, not computing. A(end->begin)~0.3-1.8ms,
our end-frame phases <25ms. FFR helped (BC was 5ms one run) but BC hitch is non-deterministic and
returns. Native at the SAME ~14ms GPU + same FFR = smooth (0.34% 1-frame drops). So the cause is
NOT GPU load, NOT our per-frame overhead, NOT submit-timing — it's UE's render thread intermittently
stalling under our OpenXR frame-loop. RULED OUT for the stall: xrWaitFrame (<25ms, 1 hit),
lock-wait, flush, xrEndFrame, WaitSwapchainImage (all <25ms). The block is in UE's own
render-recording window (begin_frame return -> EndFrame4 call).
HYPOTHESIS (user's "vrapi lazy / openxr eager"): our frame loop paces the RENDER thread (xrWaitFrame
in ovrp_BeginFrame4) and makes the GAME thread's ovrp_WaitToBeginFrame a NO-OP — opposite of vrapi,
which paces the GAME thread. So the game thread runs unpaced/eager and the render-thread back-pressure
(xrWaitFrame + frames-in-flight + compositor holding our 3 swapchain images during reproj) makes UE's
render thread block intermittently => drops => judder. Comment at xrr_wait_frame says game-thread
pacing was tried and caused "BeginFrame too many times" (XR_ERROR_CALL_ORDER_INVALID) -> they
worked around by render-thread pacing. The REAL fix is likely an architectural frame-loop rework:
pace the GAME thread (like vrapi) with correct xrWaitFrame/Begin/End 1:1:1 ordering across the two
threads. Non-trivial, real risk. Other cheap-ish probes: more swapchain images (UE frames-in-flight
stall if compositor holds our 3); check UE's own RHI frame-pacing/dynamic-res CVars.
## (helped, secondary) full-res render (FFR forced OFF) raised GPU load -> drop judder
Stall localization (per-phase + gap-split probes in xr_runtime.c) showed the 105-250ms stalls are
NOT in any of our blocking calls (xrWaitFrame/lock/flush/xrEndFrame/WaitSwapchainImage all <25ms,
A=end->begin ~0.3-1.4ms) — they're in BC = begin_frame->end_frame = UE's own render. Matched-motion:
native = 0.34% drops (all 1-frame); shim = more drops + 137-264ms stalls. So OUR shim makes UE's
render hitch. WHY: GPU-TIME log shows `fed=14ms gameLevel=1 dynamic=0 -> applied ffr=0` — the GAME
requests foveation (TiledMultiRes level 1, which native honors) but our shim had debug.re4vr.ffr=0
FORCING foveation OFF -> UE renders FULL RES -> GPU pinned at ~14ms (right at the 13.9ms/72Hz budget,
zero headroom) -> any head-motion load spike pushes GPU over budget -> render thread stalls on GPU ->
dropped frame -> compositor timewarp judder = the ghost. Native applies the game's FFR -> headroom ->
smooth. FIX (quality-neutral, matches native): debug.re4vr.ffr=-1 (game-driven) so we apply the
game's requested foveation level. Testing now. If confirmed, make ffr=-1 (game-driven) the default
in code (not 0). FFR maps game TiledMultiRes -> XR_FB_foveation (xr_runtime.c apply_foveation /
foveation_entrypoints, ~L1766+).
## *** VISUALLY CONFIRMED: whole-frame TEMPORAL JUDDER (2026-06-27 late) ***
Pulled the user's on-device recordings (/sdcard/Oculus/VideoShots/*.mp4, 30fps mono). Blending 3
consecutive frames (ImageMagick -evaluate-sequence mean) of the title screen shows the "Resident
Evil" banner + candle flames DOUBLED — two sharp offset copies (diagonal shift), WHOLE frame, not
just close objects. = real temporal judder (two distinct positions), not motion blur, not stereo.
Matches user: "blend frames shows it / not just close objects / mild-violent / random."
Tooling that works: adb pull the VideoShots mp4 (adb screenrecord gives 0 bytes — Quest blocks the
VR surface); ffmpeg extract frames; `compare -metric MAE` to find motion spikes; `convert
-evaluate-sequence mean` to blend & reveal judder; Read the PNG to view it.
Key: frame-pacing shows we DO present ~72fps (not half-rate), yet consecutive frames land at TWO
positions => the predicted-display-time or submitted pose ALTERNATES/jitters frame-to-frame and the
compositor timewarp snaps between spots. Added JUDDER probe: per-frame predictedDisplayTime delta in
xrr_begin_frame (after xrWaitFrame) — steady ~13.9ms = pose source; alternating/jittery = timing.
Mechanism candidate: our frame loop splits ovrp_WaitToBeginFrame(game thread, no-op) from
xrWaitFrame+LocateViews+Begin (render thread, in xrr_begin_frame) — this nonstandard pacing can give
the compositor jittery predicted times => judder. Native (vrapi) is phase-locked => smooth.
## (SUPERSEDED) render/composite SUBMIT-ORDERING race theory
Chain of elimination, all by in-MOTION data (static title was a red herring — geometry matches
statically; ghost only shows in motion):
- FOV: render (CalculateEyeLayerDesc2) == composite (g_xr.views) == native, per-frame, stable.
(Earlier "inner-fov mismatch" was MY arithmetic error: tan(0.785 rad)=1.000, not 0.991.)
- IPD ~0.065, eye orientation == head, head == eye-mid, no LAYER MISMATCH (stage==acquired),
full viewport, 3-image swapchain. ALL geometry/composition intrinsics correct.
- DECISIVE: a 60-frame eye-texture burst dump (debug.re4vr.dump=N) showed EVERY frame CLEAN
(single hand) AND the user saw NO ghost while the dump ran. The dump adds a per-frame GPU
fence-wait (synchronous readback) that stalls the game thread enough that UE's eye render
(on its own RHI-thread queue, NOT our s_queue — qwait was a no-op, confirming separate queue)
lands before we release+xrEndFrame. => the ghost is: WE RELEASE THE SWAPCHAIN / xrEndFrame
BEFORE UE SUBMITS ITS EYE RENDER. OpenXR then syncs the compositor against incomplete/previous
content => flashing per-eye double on fast/close motion. NOT geometry, NOT depth, NOT reproject.
- NEXT: test the built submit-hook (debug.re4vr.submithook=2 = present/release-on-submit; patches
UE's global vkQueueSubmit PFN) — it orders our release AFTER UE's eye submit. Was refuted for
BLACK but the ghost is a different artifact. If it fixes the ghost, refine to minimize latency.
If not, try a completion fence injected at the hooked submit, or device-wait before release.
## Full call census (PTC, native returns) — 41 PT_FWD'd fns, all match our shim's returns
All getters return success/same values in native and shim. Only diff: GetMixedRealityInitialized
native=1 vs ours=0 (MR irrelevant to eye render). GetSystemDisplayFrequency2/PerfMetrics have
pre-init transient failures (-1002/-1008) then succeed — same as ours. CONCLUSION: the game makes
the same calls and gets the same answers in both modes => the ghost is NOT a getter-return diff;
it's in COMPOSITION (our OpenXR layer submit vs native vrapi compositor), which is not a game call.
## *** KEY DIFFERENCE: native submits eye-fov with ReverseZ depth reprojection ***
Native EndFrame4 eye-fov layer flags=**0x4 = ovrpLayerSubmitFlag_ReverseZ** (1<<2). NOT NoDepth(0x8).
=> native composites with DEPTH-BASED POSITIONAL TIMEWARP, reverse-Z convention. And the game DOES
request a depth buffer: our CalculateEyeLayerDesc2 logged depthFormat=10. Depth-aware reprojection
is exactly what corrects close-object parallax under head translation — its absence = "deform/swim
on close objects" = the seated hand ghost. PRIME SUSPECT.
Our shim CAN chain XrCompositionLayerDepthInfoKHR (xr_runtime.c build_composition L896, reverse-z via
g_depthRevZ; depth swapchain created L2012 gated on `depth_wanted()` + game depthFormat). But memory
says "depth on didn't fix it" — so VERIFY whether the game actually RENDERS valid depth into OUR
depth swapchain (UE only renders depth if GetLayerTexture2 returns a depth handle AND its RHI targets
it). If our depth image is empty/garbage, positional timewarp is a no-op (or worse) => ghost persists
even with depth=1. That's the next probe.
## NEXT probe: verify our depth reprojection is functionally live (passthru=0, depth=1)
1. "setup_layer: DEPTH swapchain ..." present? (swapchain created)
2. does GetLayerTexture2 return a depth handle to the game (outDepthTex non-null path)?
3. is depth chained each frame in build_composition (diag==0 && !pipeline && depthSwapchain)?
4. is the depth IMAGE actually written by UE (dump min/max; all-1.0 or all-0 = not rendered)?
If depth never reaches our swapchain -> that's the fix (wire UE's depth -> our depth image), and it
would explain native(ReverseZ)=clean vs ours=ghost.
## (superseded) NEXT: in-game passthru (the actual repro)
Title doesn't exercise the seated hand-deform. Run passthru=1, load save, play SEATED:
(a) does native eliminate the hand-deform? If yes -> ghost is in our submission/render path,
not geometry (since geometry matches). Capture in-game native EndFrame4/pose/fov, diff vs
our in-game STEREO/views/HEADvsEYE logs at the same moment.
(b) if native ALSO deforms -> not our shim's fault (game/headset).
@@ -0,0 +1,86 @@
# Lever 2 handoff — detect dropped/black frames and reproject instead of presenting black
Entry point for implementing the elegant fix to the in-game black. Read alongside the
auto-memory: `black-is-not-submit-timing-ue-renders-empty`, `ingame-black-render-content`,
`gameplay-eye-image-is-pure-black-confirmed`, `standing-vs-sitting-blackflash`.
## The settled root cause (do not re-litigate)
The in-game black is a **UE-internal frame-drop under GPU load**, NOT a shim bug:
- RenderDoc (`~/renderdoc-captures/RE4/work_frame.rdc`): under load UE renders only **~28
draws into its eye target vs ~198 in a normal frame** (a truncated frame), and the
**resolved eye = pure black** (MEAN [0,0,0], reliable ms=1 read).
- Ruled out conclusively: submit-timing (present-on-submit incl. full one-frame defer
still blacks — `debug.re4vr.submithook 2`/`defern 99`), image targeting (LAYER MISMATCH
count = 0, stage==acquiredIndex), empty-frame submission (SUBMIT-BLACK/COMPOSE-EMPTY = 0).
- Load-gated: bridge (sparse) never blacks; house/dense geometry blacks; "especially while
casting" (extra load). The game does NOT self-scale from our GPU-time feed.
## The idea
When UE hands us a truncated/black frame, **don't present it** — re-present the **last
good** eye image with the current frame's pose, so the OpenXR compositor **timewarps/
reprojects** the last good content to the new head pose. A reprojected (slightly stale)
frame is far better than a black flash. This is exactly the "app didn't produce a new
frame" path that compositors are built for; our black frames currently defeat it.
## Two hard parts
### A. A cheap "this frame is bad" signal (the crux)
Per-frame GPU readback to detect black is too costly + unreliable (observer effect; see
`texture-dump-is-unreliable-probe`). Candidate signals, cheapest first:
1. **Post-hitch heuristic:** the truncated frame is the RECOVERY frame after a stall
(RenderDoc: "recovery frame was 26 draws"). We already detect HITCH (dt>20ms in the
FRAME trace). Try: skip+repeat the 1–2 frames following a detected hitch. Coarse but
zero new cost; test first.
2. **Submit/command-buffer count via the vkQueueSubmit hook (already built,
`debug.re4vr.submithook 1`):** a truncated frame issues fewer submits / command buffers.
Instrument submits-per-frame (between END markers) and correlate with perceived black.
Under load we saw ~6 submits/frame normal — a dropped frame may show fewer. Needs a
black ground-truth to calibrate (hard without readback; use the post-hitch frames as a
proxy, or a one-off RenderDoc cross-check).
3. **GPU frame time over budget:** `g_gpuFrameMs` already tracked; but it's the PREVIOUS
frame's dt (lagging), so use it to predict the next frame is at-risk, not to gate the
current one.
Recommendation: start with (1) post-hitch repeat — simplest, no new signal — and measure.
If it helps but is too coarse, add (2) via the hook.
### B. Re-presenting the last good frame
OpenXR requires xrEndFrame every frame after xrBeginFrame; you can't simply skip present.
To reproject, present the LAST GOOD eye image again with the current `predictedDisplayTime`
+ located views (the runtime timewarps it). Mechanics:
- Keep a reference to the last-good eye image. Two options: (a) DON'T release frame N-1's
swapchain image and re-submit it (risky — holding across frames is what made
`pipeline=1` unstable; see `deferred-flush-unstable-abandoned`), or (b) **copy** the last
good eye image into a shim-owned VkImage (the copy-ring infra already exists:
`xrr_vk_alloc_images`, `shimImages[]`, the copy path in end_frame) and present a normal
fresh swapchain image blitted from the held copy. (b) avoids the cross-frame-hold
instability.
- On a bad frame: skip UE's (black) image, blit last-good copy → the acquired swapchain
image, submit the previous composition's layer with the CURRENT pose/displayTime.
- Gate the whole thing behind a new `debug.re4vr.skipblack` prop (default 0), like the
other levers, so the known-good path is untouched.
## Key code locations (shim/src/xr_runtime.c unless noted)
- `xrr_end_frame` sync branch (~1053): where present happens; add the skip+repeat here.
- FRAME/HITCH trace (~1123): the hitch signal (dt>20ms) for heuristic (1).
- `build_composition` (~620): the composition we'd re-submit with updated pose.
- Copy-ring infra: `xrr_vk_alloc_images` / `shimImages[]` (vk_session.c) + the copy-ring
path in end_frame (~922) — reuse for holding/blitting the last-good image.
- vkQueueSubmit hook + `xrr_on_ue_submit` (~820): submits-per-frame instrumentation for
signal (2). `debug.re4vr.submithook 1` = instrument.
- Pose update for reprojection: `xrr_eye_fov_tangents` + the located views in `g_xr.views`.
## Validation
Device: Quest 2 `<redacted-serial>` (USB). Build/deploy:
`./shim/build_android.sh && ./packaging/repack.sh && adb -s <redacted-serial> install -r packaging/out/re4vr-shim.apk`
(NOTE: repack silently bundles the LAST successful build — always confirm the build had no
errors and check `shim/build/arm64/libOVRPlugin.so` timestamp before repack.)
Test: Standing, walk off the bridge toward the house (reliable black trigger), `trace=1`.
Success = black flashes replaced by (at worst) brief reprojection judder, not black.
## Prerequisite vs Lever 1
If Lever 1 (force lower GPU load so UE completes frames: `ffr=3` + `debug.re4vr.resscale`
< 100 + `sscap=1`) sufficiently stops the drops at acceptable quality, Lever 2 may be
unnecessary or only needed for the worst spikes. Decide after the Lever 1 result.
```
debug.re4vr.resscale = percent of eye size (default 100; e.g. 75, 50). New this session.
```
+104
View File
@@ -0,0 +1,104 @@
# Related work — how others run Quest games elsewhere, and why this shim is different
A survey of the projects in the same space, why none of them is what this repo is, and
what (little) we'd adopt from them. Based on public repos + black-box surface inspection
of publicly-distributed binaries — no decompilation of anyone's proprietary internals.
## TL;DR
There is **no public, open-source reimplementation of `libOVRPlugin.so` on OpenXR.** A
whole-of-GitHub search for `ovrplugin` returns three repos, none a shim. The one tool that
*runs* VrApi/OVRPlugin Quest games on other headsets — **Overport** — does it by
**redistributing Meta's own newer OVRPlugin binary** plus a vendor OpenXR loader, not by
reimplementing anything. So this project (a clean-room, from-scratch `ovrp_*` engine on
OpenXR) appears to be the only open implementation of that translation layer.
## Overport (`ovrport/app`)
GPLv3, Kotlin/Compose, ~349★. **Two halves, only one of which is open:**
1. **The patcher (open, in the repo).** A Compose Multiplatform + ARSCLib app/CLI that
rewrites a Quest APK: strips entitlements (via injected **Frida** scripts + a SKU/asset
config), fixes the manifest, swaps icons/labels, and applies engine-specific smali
patches. Its VR-library handling is three tiny patches:
- `CopyOVRPluginVrApiPatch` — drop a bundled `libOVRPlugin.so` into any game that has
`libvrapi.so`
- `RemoveVrApiPatch` — delete the game's `libvrapi.so`
- `CopyLibrariesPatch` — copy in the rest of the bundle
2. **The translation libraries (closed, NOT in the repo).** The libraries it injects are
downloaded at patch time from the author's server
(`ovrp.crx.moe/api/v1/releases/index` -> `files.crx.moe/.../libraries.zip`), version-
managed separately. Source unpublished.
### What's actually in `libraries.zip` (black-box surface inspection)
The bundle is **Meta's / vendors' proprietary binaries**, not original code:
| File | What it is |
|---|---|
| `libOVRPlugin.so` (4.1 MB, 611 `ovrp_` exports) | **Meta's own OVRPlugin** — internal build paths intact in `.rodata` (`arvr/projects/integrations/OVRPlugin/Src/Util/CompositorOpenXR.cpp`). A *newer* build than RE4VR's bundled one (986 KB, VrApi-era), specifically one with the **OpenXR compositor backend**. |
| `libopenxr_loader_{meta,pico,yvr,generic}.so` | Per-vendor OpenXR loaders; the patcher picks one for the target headset. |
| `libovrplatformloader*.so`, `libpxrplatformloader.so` | Entitlement/platform loaders (the Frida-mocked entitlement piece). |
### Overport's actual strategy (now unambiguous)
For a VrApi-era title it: **replaces the game's old VrApi-routed Meta OVRPlugin with a newer
Meta OVRPlugin that has an OpenXR backend**, deletes `libvrapi.so`, drops in the target
vendor's OpenXR loader, and bypasses entitlement with Frida. The newer Meta OVRPlugin's
`CompositorOpenXR` path then talks to e.g. Pico's OpenXR runtime.
**Consequences:**
- It ships **Meta's (and Pico's/YVR's) proprietary binaries** verbatim. That is a very
different — and far more exposed — legal posture than a clean-room reimplementation.
- It depends on the **newer OVRPlugin's C ABI still matching what the old game's UE/Unity
build calls.** For an old VrApi-era UE4 title like RE4VR this is not guaranteed; the
surface has drifted across OVRPlugin versions.
- There is **no original translation code** to learn from — the hard part is Meta's, and
we already have RE4VR's real `libOVRPlugin_real.so` for reference.
### Why this matters for us
Our shim is the open, clean-room alternative to the one piece Overport keeps closed (and
which is, in fact, Meta's). It can't be replaced by their blob in a clean open product
because their blob *is* Meta's binary. Their **patcher**, however, is genuinely useful prior
art for the *packaging* layer (entitlement strip, manifest/engine smali patches) — see the
off-Quest patches we adopt below.
## Quake III Arena VR Edition (`GUNNM-VR/...`)
A **source port**, not a shim. Built on the open-source Quake3e engine + baseq3a, recompiled
for Android/Quest with Vulkan, using `#ifdef` to select OpenXR (PCVR) or VRAPI (Quest) at
compile time. This is the easy case and the exact opposite of ours: with engine source you
just compile against whichever runtime. We **can't** recompile RE4VR (closed UE4 binary), so
we must be a binary-compatible `libOVRPlugin.so` instead. Useful only as a contrast.
## Off-Quest patches worth adopting (packaging layer)
For **RE4VR on a real Quest** we need none of these — the shim alone is sufficient (good
confirmation). They become necessary only when porting the APK to a **non-Quest** Android VR
device (Pico / Steam Frame / Monado-on-Android). Reimplemented in our own packaging in
`packaging/steamframe_patches.sh` (see the `steamframe-port` branch):
| Patch (Overport name) | Why off-Quest | Status |
|---|---|---|
| `OculusUnrealPatch` | UE gates the Oculus HMD path on `Build.MANUFACTURER`/`MODEL`; off-Quest it's false so our shim is never called. Spoof them in `GameActivity` smali. | **adopt** |
| `RemoveUsesLibraryPatch` | Strip `<uses-(native-)library>` entries (except `libopenxr.google.so`) that name Meta-only libs and would block install/launch elsewhere. | **adopt** |
| `DisableControllerOffsetPatch` | Touch->other-controller pose offset; our `xr_input` has no offset, so non-Touch controllers may be misplaced. | **shim TODO** (runtime, not packaging) |
| `RemoveUnrealForceQuitPatch` | Strip `System.exit` from `AndroidThunkJava_ForceQuit` so a failed off-Quest check can't hard-kill the app. | adopt (defensive) |
| `FixUnrealCrashPatch` | Creates a stub `UnityPlayer.currentActivity` so an engine-agnostic injected blob can find the Activity. | **skip** — our shim gets the Activity natively from `Initialize5` + JNI. |
| `MetaXRAudioPatch` | Hex-NOPs `libMetaXRAudioWwise/Unity.so`. | **N/A** — RE4VR uses `libovraudio64.so`, not Meta XR Audio. |
| `DisableSpaceWarp` / `ForcePassthrough` | Config toggles. | N/A — RE4VR doesn't use AppSpaceWarp; equivalent to our `debug.re4vr.*` props. |
## Sources
- Overport patcher: <https://github.com/ovrport/app> (GPLv3)
- Overport library index: `https://ovrp.crx.moe/api/v1/releases/index`
- Quake III VR Edition: <https://github.com/GUNNM-VR/Quake-III-Arena-VR-Edition>
- Meta deprecates VrApi / "all-in on OpenXR":
<https://developers.meta.com/horizon/blog/oculus-all-in-on-openxr-deprecates-proprietary-apis/>
- OVRPlugin vs VRAPI vs LibOVR:
<https://developers.meta.com/horizon/documentation/unity/os-openxr-vrapi/>
- Allegations Meta's OVRPlugin blocks non-Meta runtimes (Voices of VR #1526):
<https://voicesofvr.com/1526-allegations-that-metas-ovrplugin-is-undermining-the-spirit-of-openxr-by-blocking-non-meta-headsets-on-pcvr/>
@@ -0,0 +1,90 @@
# Render-submit race — fix design (#2)
Pairs with `render-submit-sync-RE.md` (subagent RE of UE/OVRPlugin submit timing).
Status: design draft; final approach (A vs B) gated on the RE findings.
## Confirmed mechanism
- Our `ovrp_EndFrame4` → `xrEndFrame` presents **synchronously** (composites immediately).
- UE 4.25's RHI thread `vkQueueSubmit`s the eye-render command buffer **after**
`ovrp_EndFrame4` returns. **Proof:** `debug.re4vr.qwait` (a `vkQueueWaitIdle` on UE's
queue *before* we release/present) is a **no-op** — if UE's render were already on the
queue, draining it would turn the black image correct; it doesn't, so the submit isn't
on the queue yet when `end_frame` runs.
- The original VrApi path tolerates this because VrApi's EndFrame **also defers** the
present onto the RHI submit (render flush + present ride the same queue submission / RHI
flush), so render is naturally ordered before present. We broke that by presenting
eagerly inside `xrEndFrame`.
## Why the current sync can't fix it (`vk_session.c`)
`xrr_vk_flush_submit_ex` records a `COLOR_ATTACHMENT_WRITE→MEMORY_READ` barrier and
submits it on UE's queue (`s_queue`), then `xrr_vk_flush_wait` waits its fence. Vulkan
queue execution is in-order, so this is correct **iff UE already submitted the eye
render**. Under load UE hasn't, so the barrier resolves an un-rendered image and the fence
signals against empty content → we release+present black. Load-gated exactly as observed
(sparse bridge = render makes the deadline = no black; dense geo = late = black).
## Prior attempts and why they failed
- **Barrier-only (current default):** ordered before UE's later submit → black under load.
- **qwait (`vkQueueWaitIdle`):** no-op (UE hasn't submitted). Diagnostic only.
- **Deferred-flush pipeline (`pipeline=1`, present N-1):** conceptually right (gives UE a
full frame to land its submit) but holds the **OpenXR swapchain image acquired across
`xrEndFrame`** → Meta runtime mis-composites + crashes. Unstable; shelved.
## Fix options
**A. Observe UE's submit (hook/interpose `vkQueueSubmit`) — most robust.**
Wrap `vkQueueSubmit` so the shim sees exactly when UE flushes the eye render; record that
submit's fence (or a timeline value). In `end_frame`, wait that fence before releasing +
presenting. No swapchain-lifecycle hacks, minimal added latency (only the necessary wait).
Open question (→ RE): is UE's `vkQueueSubmit` interceptable from our in-process `.so`
(symbol interposition / a thin Vulkan layer), and which submit carries the eye render?
**B. Deferred present via a shim-owned copy (robust fallback, +1 frame latency).**
Decouple the deferral from the OpenXR swapchain lifecycle (the cause of pipeline=1's
instability): keep acquire→release **within a single frame**, but present one frame late.
At frame N+1, UE's render-N submit has landed; copy UE's frame-N eye image into a
shim-owned `VkImage` (barrier-ordered after UE's submit, on the same queue), then present
the shim copy via a normal same-frame acquire/release. Never holds an OpenXR image across
`xrEndFrame`. Costs 1 frame of latency + one image copy.
**C. Bounded spin-wait for the submit in `end_frame`** — fragile (no clean way to detect
the submit without a hook), adds latency/stalls. Not recommended.
**D. Use an ovrp call UE makes around submit as the sync point** — only viable if the RE
finds UE invokes an OVRPlugin entry point right after the render submit. Unlikely; → RE.
## RE outcome (`render-submit-sync-RE.md`) → Option A is feasible
- **Original = zero Vulkan sync, definitively:** libOVRPlugin imports *no* `vk*` (only
`vrapi_*`); libvrapi imports no `vk*` either. The compositor is a separate system
process; swapchains are cross-process system-owned (`vrapi_CreateTextureSwapChainCrossProcess`).
EndFrame4 only hands over swapchain handle + image index + pose; ordering is implicit via
system swapchain ownership — the exact analogue of OpenXR acquire/wait/release.
- **EndFrame4 runs on the RHI thread:** `FCustomPresent::FinishRendering_RHIThread` →
`FOculusHMD::FinishRHIFrame_RHIThread` → `ovrp_EndFrame4` (via PluginWrapper dispatch).
- **UE's eye-render submit is interceptable:** OculusHMD never calls `vkQueueSubmit`
directly; it goes through the FVulkan RHI's **global dispatch pointer**
`VulkanDynamicAPI::vkQueueSubmit` (a global PFN resolvable by symbol at runtime), batched/deferred on the RHI
thread. A global PFN we can patch → trampoline. **This makes Option A viable and portable**
(pure Vulkan + a UE symbol; no Quest/VrApi dependency, so it carries to Steam Frame).
## DECISION: Option A (hook `VulkanDynamicAPI::vkQueueSubmit`), but resolve the
## threading interleave FIRST (instrument before we sync)
Critical open question the RE could not pin from statics: **on the RHI thread, does the
eye-render `vkQueueSubmit` happen BEFORE or AFTER the `EndFrame4` call?**
- If **submit-before-EndFrame**: by EndFrame the render is on the queue; we just
`vkWaitForFences` on the recorded submit fence before release/present. No deadlock, no
added latency. (But the `qwait` no-op argues against this — nothing was on the queue.)
- If **EndFrame-before-submit** (what `qwait` implies, same thread): we must NOT block in
EndFrame (that thread does the later submit → self-deadlock). Instead **present-on-submit**:
EndFrame stores the pending composition; the `vkQueueSubmit` trampoline, on seeing the
eye-render submit, triggers the barrier+release+present. This mimics the original (present
rides the RHI submit) with **no fixed frame of latency** and no cross-frame swapchain hold.
The interleave decides wait-in-EndFrame vs present-on-submit, so **step 1 is the hook as
pure instrumentation** (no behavior change): patch the global PFN, log every submit with
tid + timestamp + a monotonic seq, and log EndFrame with the same clock. One device run off
the bridge confirms the order (and validates the patch offset + that `s_queue` is the queue
UE submits eyes on — the RE's two stated uncertainties). Then implement the matching
variant behind a `debug.re4vr.*` toggle.
Device test throughout: Standing, walk off the bridge toward the house (reliable black
trigger), `trace=1`.
+42
View File
@@ -0,0 +1,42 @@
// Decompile the real frame-submission path + FFR/SpaceWarp queries to find the
// GPU-sync contract the shim must replicate (the black-frame race).
// -> ~/dev/re4vr-port/analysis/endframe_impls.txt
// @category RE4VR
import ghidra.app.script.GhidraScript;
import ghidra.app.decompiler.*;
import ghidra.program.model.listing.*;
import java.io.PrintWriter;
import java.util.*;
public class DumpEndFrame extends GhidraScript {
public void run() throws Exception {
String out = System.getProperty("user.home") + "/dev/re4vr-port/analysis/endframe_impls.txt";
Set<String> targets = new HashSet<>(Arrays.asList(
"ovrp_EndFrame4", "ovrp_EndFrame3", "ovrp_EndFrame2", "ovrp_EndFrame",
"ovrp_BeginFrame", "ovrp_GetLayerTextureSpaceWarp",
"ovrp_GetLayerTextureFoveation", "ovrp_GetTiledMultiResLevel",
"ovrp_GetTiledMultiResDynamic", "ovrp_SetTiledMultiResLevel",
"ovrp_GetLayerTexture2"));
PrintWriter w = new PrintWriter(out);
DecompInterface dec = new DecompInterface();
DecompileOptions opts = new DecompileOptions();
dec.setOptions(opts);
dec.openProgram(currentProgram);
FunctionManager fm = currentProgram.getFunctionManager();
for (Function f : fm.getFunctions(true)) {
if (!targets.contains(f.getName())) continue;
Function real = f.isThunk() ? f.getThunkedFunction(true) : f;
w.println("\n/* ===== " + f.getName() + " thunk@" + f.getEntryPoint()
+ " -> real@" + (real != null ? real.getEntryPoint() : "?")
+ " ===== */");
if (real == null) { w.println(" (could not resolve thunk)"); continue; }
try {
DecompileResults r = dec.decompileFunction(real, 180, monitor);
DecompiledFunction df = r.getDecompiledFunction();
w.println(df != null ? df.getC() : " (decompile failed)");
} catch (Exception e) { w.println(" (exception: " + e + ")"); }
}
w.close();
println("DumpEndFrame: wrote " + out);
}
}
+55
View File
@@ -0,0 +1,55 @@
// Decompile the real Vulkan-init path: any function that calls the vrapi Vulkan
// creation imports, plus the Vk extension getters and Initialize impls.
// -> ~/dev/re4vr-port/analysis/init_vulkan.txt
// @category RE4VR
import ghidra.app.script.GhidraScript;
import ghidra.app.decompiler.*;
import ghidra.program.model.listing.*;
import java.io.PrintWriter;
import java.util.*;
public class DumpInit extends GhidraScript {
public void run() throws Exception {
String out = System.getProperty("user.home") + "/dev/re4vr-port/analysis/init_vulkan.txt";
PrintWriter w = new PrintWriter(out);
// imports whose callers we want to see
Set<String> wantCalls = new HashSet<>(Arrays.asList(
"vrapi_CreateSystemVulkan", "vrapi_CreateSystemVulkan2",
"vrapi_EnterVrMode", "vrapi_Initialize", "vrapi_GetDeviceExtensionsVulkan",
"vrapi_GetInstanceExtensionsVulkan"));
// also decompile functions whose own name matches these
String[] nameHits = { "InitializeVulkan", "GetInstanceExtensionsVk",
"GetDeviceExtensionsVk", "CreateSystemVulkan", "InitializeInternal" };
DecompInterface dec = new DecompInterface();
dec.openProgram(currentProgram);
FunctionManager fm = currentProgram.getFunctionManager();
Set<String> done = new HashSet<>();
for (Function f : fm.getFunctions(true)) {
boolean hit = false;
String nm = f.getName();
for (String s : nameHits) if (nm.contains(s)) { hit = true; break; }
if (!hit) {
try {
for (Function callee : f.getCalledFunctions(monitor)) {
if (wantCalls.contains(callee.getName())) { hit = true; break; }
}
} catch (Exception e) {}
}
if (!hit) continue;
if (!done.add(f.getEntryPoint().toString())) continue;
try {
DecompileResults r = dec.decompileFunction(f, 90, monitor);
DecompiledFunction df = r.getDecompiledFunction();
if (df != null) {
w.println("\n/* ===== " + nm + " @ " + f.getEntryPoint() + " ===== */");
w.println(df.getC());
}
} catch (Exception e) {}
}
w.close();
println("DumpInit: wrote " + done.size() + " functions to " + out);
}
}
+64
View File
@@ -0,0 +1,64 @@
// Ghidra headless post-script: dump decompiled ovrp_* functions.
// Signatures for all ovrp_*, full bodies for a core set ->
// ~/dev/re4vr-port/analysis/ovrp_decomp.txt
// @category RE4VR
import ghidra.app.script.GhidraScript;
import ghidra.app.decompiler.DecompInterface;
import ghidra.app.decompiler.DecompileResults;
import ghidra.app.decompiler.DecompiledFunction;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.FunctionManager;
import java.io.PrintWriter;
import java.util.*;
public class DumpOvrp extends GhidraScript {
public void run() throws Exception {
Set<String> CORE = new HashSet<>(Arrays.asList(
"ovrp_PreInitialize3","ovrp_Initialize5","ovrp_Shutdown2","ovrp_Update3",
"ovrp_BeginFrame4","ovrp_EndFrame4","ovrp_WaitToBeginFrame","ovrp_GetPredictedDisplayTime",
"ovrp_GetNodePoseState3","ovrp_GetNodePoseStateRaw","ovrp_GetControllerState4",
"ovrp_SetupLayer","ovrp_CalculateEyeLayerDesc2","ovrp_GetLayerTexture2",
"ovrp_GetHmdToEyeOffset2","ovrp_GetSystemHeadsetType2","ovrp_SetupDistortionWindow3"
));
String out = System.getProperty("user.home") + "/dev/re4vr-port/analysis/ovrp_decomp.txt";
DecompInterface dec = new DecompInterface();
dec.openProgram(currentProgram);
FunctionManager fm = currentProgram.getFunctionManager();
List<String> sigs = new ArrayList<>();
List<String> bodies = new ArrayList<>();
int count = 0;
for (Function f : fm.getFunctions(true)) {
String name = f.getName();
if (!name.startsWith("ovrp_")) continue;
count++;
try {
DecompileResults res = dec.decompileFunction(f, 60, monitor);
DecompiledFunction df = res.getDecompiledFunction();
if (df != null) {
sigs.add(df.getSignature());
if (CORE.contains(name)) {
bodies.add("/* ===== " + name + " @ " + f.getEntryPoint()
+ " ===== */\n" + df.getC());
}
continue;
}
} catch (Exception e) { /* fall through */ }
sigs.add("/* (decomp failed) */ " + name + " @ " + f.getEntryPoint());
}
Collections.sort(sigs);
PrintWriter w = new PrintWriter(out);
w.println("# ovrp_ functions decompiled from libOVRPlugin.so (v1.51 / pkg 19.0.0)");
w.println("# total ovrp_ functions: " + count + "\n");
w.println("## ===== SIGNATURES =====");
for (String s : sigs) w.println(s);
w.println("\n## ===== CORE FUNCTION BODIES =====\n");
for (String b : bodies) { w.println(b); w.println(); }
w.close();
println("DumpOvrp: wrote " + count + " signatures (" + bodies.size()
+ " core bodies) to " + out);
}
}
+36
View File
@@ -0,0 +1,36 @@
// Resolve export thunks to their real implementations and decompile them.
// -> ~/dev/re4vr-port/analysis/real_impls.txt
// @category RE4VR
import ghidra.app.script.GhidraScript;
import ghidra.app.decompiler.*;
import ghidra.program.model.listing.*;
import java.io.PrintWriter;
import java.util.*;
public class DumpReal extends GhidraScript {
public void run() throws Exception {
String out = System.getProperty("user.home") + "/dev/re4vr-port/analysis/real_impls.txt";
Set<String> targets = new HashSet<>(Arrays.asList(
"ovrp_Initialize5", "ovrp_PreInitialize3", "ovrp_SetupDistortionWindow3",
"ovrp_GetInstanceExtensionsVk", "ovrp_GetDeviceExtensionsVk"));
PrintWriter w = new PrintWriter(out);
DecompInterface dec = new DecompInterface();
dec.openProgram(currentProgram);
FunctionManager fm = currentProgram.getFunctionManager();
for (Function f : fm.getFunctions(true)) {
if (!targets.contains(f.getName())) continue;
Function real = f.isThunk() ? f.getThunkedFunction(true) : f;
w.println("\n/* ===== " + f.getName() + " thunk@" + f.getEntryPoint()
+ " -> real@" + (real != null ? real.getEntryPoint() : "?")
+ " ===== */");
if (real == null) { w.println(" (could not resolve thunk)"); continue; }
try {
DecompileResults r = dec.decompileFunction(real, 120, monitor);
DecompiledFunction df = r.getDecompiledFunction();
w.println(df != null ? df.getC() : " (decompile failed)");
} catch (Exception e) { w.println(" (exception)"); }
}
w.close();
println("DumpReal: wrote " + out);
}
}
+71
View File
@@ -0,0 +1,71 @@
// Dump ovrp* struct layouts Ghidra inferred + decompile the Compositor layer
// methods (ground-truth field offsets for ovrpLayerDesc / ovrpLayerSubmit).
// -> ~/dev/re4vr-port/analysis/struct_layouts.txt
// @category RE4VR
import ghidra.app.script.GhidraScript;
import ghidra.app.decompiler.*;
import ghidra.program.model.data.*;
import ghidra.program.model.listing.*;
import java.io.PrintWriter;
import java.util.*;
public class DumpStructs extends GhidraScript {
public void run() throws Exception {
String out = System.getProperty("user.home")
+ "/dev/re4vr-port/analysis/struct_layouts.txt";
PrintWriter w = new PrintWriter(out);
// 1) every struct/typedef whose name mentions ovrp or Layer
w.println("## ===== INFERRED STRUCT LAYOUTS (name ~ ovrp|Layer) =====");
DataTypeManager dtm = currentProgram.getDataTypeManager();
Iterator<DataType> it = dtm.getAllDataTypes();
while (it.hasNext()) {
DataType dt = it.next();
String nm = dt.getName();
if (!(nm.toLowerCase().contains("ovrp") || nm.contains("Layer"))) continue;
if (dt instanceof Structure) {
Structure s = (Structure) dt;
w.println("\nstruct " + nm + " /* size=" + s.getLength()
+ " (0x" + Integer.toHexString(s.getLength()) + ") */ {");
for (DataTypeComponent c : s.getComponents()) {
w.printf(" +0x%-4x %-24s %s%n", c.getOffset(),
c.getDataType().getName(),
c.getFieldName() == null ? "" : c.getFieldName());
}
w.println("};");
} else {
w.println(nm + " (" + dt.getClass().getSimpleName()
+ ", len=" + dt.getLength() + ")");
}
}
// 2) decompile the Compositor layer methods for real offsets
String[] targets = {
"ImportLayerDesc", "ExportEyeLayerDesc", "CalculateEyeLayerDesc",
"SetupLayer", "GetLayerTexture", "EndFrame", "SubmitLayer"
};
w.println("\n\n## ===== COMPOSITOR METHOD BODIES (field-offset ground truth) =====");
DecompInterface dec = new DecompInterface();
dec.openProgram(currentProgram);
FunctionManager fm = currentProgram.getFunctionManager();
Set<String> seen = new HashSet<>();
for (Function f : fm.getFunctions(true)) {
String name = f.getName();
boolean hit = false;
for (String t : targets) if (name.contains(t)) { hit = true; break; }
if (!hit) continue;
if (!seen.add(f.getEntryPoint().toString())) continue;
try {
DecompileResults r = dec.decompileFunction(f, 60, monitor);
DecompiledFunction df = r.getDecompiledFunction();
if (df != null) {
w.println("\n/* ===== " + name + " @ " + f.getEntryPoint()
+ " ===== */");
w.println(df.getC());
}
} catch (Exception e) { /* skip */ }
}
w.close();
println("DumpStructs: wrote " + out);
}
}
+66
View File
@@ -0,0 +1,66 @@
# Packaging — repack the RE4 VR APK with the shim
Produces an installable, re-signed APK with our OpenXR `libOVRPlugin.so` swapped in. The
original `libovrplatformloader.so` is kept untouched, so the platform's real entitlement
check runs unchanged — on Quest you own the title. Dump-your-own only; nothing here is
redistributed. Entitlement handling on hardware with no Meta backend is **out of scope** for
this project and is the user's responsibility. See ../TESTING.md for the on-device plan.
## One-time setup
```
./build_openxr_loader.sh # builds libopenxr_loader.so (arm64) -> libs/arm64/
./make_debug_keystore.sh # debug.keystore for re-signing (repack.sh auto-runs it)
```
Also build the shim libs first: `../shim/build_android.sh`.
## Repack
```
./repack.sh [input.apk]
# example:
./repack.sh # ../dump/base.apk
```
Swaps only `libOVRPlugin.so` + bundles `libopenxr_loader.so`, and keeps the original
`libovrplatformloader.so` so the platform's real entitlement check runs unchanged.
Output: `out/re4vr-shim.apk` (zipaligned, v1+v2+v3 signed with the debug key).
## Off-Quest port (Pico / Steam Frame / Monado-on-Android)
```
./steamframe_patches.sh [in.apk] [out.apk] # default: out/re4vr-shim.apk -> out/re4vr-steamframe.apk
```
Run AFTER `repack.sh` (which swaps in our shim). Needs `apktool`. Applies the Java/manifest
fixes a non-Quest target needs but a real Quest doesn't: spoofs `Build.MANUFACTURER`/`MODEL`
so UE takes the Oculus HMD path (else our shim is never called), neuters
`AndroidThunkJava_ForceQuit`, and strips Meta-only `<uses-(native-)library>` manifest entries
(keeps `libopenxr.google.so`). Rationale + the patches we deliberately skip:
`../docs/research/related-work.md`. Prepared for the bring-up; not yet hardware-validated.
⚠️ **Do NOT install the output on a Quest** — the manifest strip removes Meta libs the
Quest needs. The script warns + prompts for confirmation; pass `NOT_QUEST=1` to bypass the
prompt in automation. On a Quest, install the plain `repack.sh` output (`out/re4vr-shim.apk`).
## Install + run (Quest, dev mode)
```
adb install -r out/re4vr-shim.apk
# push the OBB you dumped (same versionCode 203):
adb push ../dump/obb/main.203.com.Armature.VR4.obb /sdcard/Android/obb/com.Armature.VR4/
adb push ../dump/obb/patch.203.com.Armature.VR4.obb /sdcard/Android/obb/com.Armature.VR4/
adb logcat | grep -iE 'xrr|openxr|OVRPlugin|Armature' # watch [xrr] logs
```
## Manifest
```
./inspect_manifest.sh [input.apk] # confirms VR/OpenXR declarations are present
```
RE4 VR is already a shipping Quest VR app, so its manifest almost certainly already
has the headtracking feature + VR intent category; switching vrapi->OpenXR usually
needs no manifest change. inspect_manifest.sh reports any gaps; edit the decoded
manifest + rebuild with apktool only if something's missing.
## Notes
- Re-signing with our own key is unavoidable (we modify a lib). That's what risks
the legit entitlement on Quest — see ../TESTING.md.
- Libs are added stored (-0) and the APK is `zipalign -p 4`'d so native libs stay
page-aligned (extractNativeLibs=false convention).
- Tools used: tools/android-14 (build-tools 34: zipalign/apksigner), tools/apktool.jar,
tools/jdk-21 (keytool), tools/android-ndk-r27c (loader build).
+20
View File
@@ -0,0 +1,20 @@
# Shared, portable path/tool detection for the packaging scripts. `source` this.
# Derives the repo root from this file's location and auto-detects tool versions
# under tools/ (or env overrides), so the repo builds/repacks anywhere.
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PKG="$ROOT/packaging"
SHIM_OUT="$ROOT/shim/build/arm64"
# JDK: prefer a bundled tools/jdk-*, else an existing JAVA_HOME, else system java.
_bundled_jdk="$(ls -d "$ROOT"/tools/jdk-* 2>/dev/null | head -1 || true)"
if [ -n "${_bundled_jdk:-}" ]; then export JAVA_HOME="$_bundled_jdk"; fi
KEYTOOL="${JAVA_HOME:+$JAVA_HOME/bin/}keytool"
# Android build-tools dir (zipalign / apksigner / aapt2), e.g. tools/android-14.
BT="$(ls -d "$ROOT"/tools/android-[0-9]* 2>/dev/null | head -1 || true)"
# apktool jar (optional, for manifest work).
APKTOOL_JAR="$ROOT/tools/apktool.jar"
# Android NDK (for building the OpenXR loader).
NDK="${ANDROID_NDK:-$(ls -d "$ROOT"/tools/android-ndk-* 2>/dev/null | head -1 || true)}"
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Build the Khronos OpenXR loader (libopenxr_loader.so) for Android arm64 so the
# shim's NEEDED dependency resolves at runtime. Output -> packaging/libs/arm64/.
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_env.sh"
[ -n "${NDK:-}" ] && [ -d "$NDK" ] || { echo "NDK not found. Set \$ANDROID_NDK or run scripts/fetch_deps.sh"; exit 1; }
SRC="$ROOT/tools/OpenXR-SDK"
if [ ! -d "$SRC" ]; then
echo "downloading OpenXR-SDK source..."
curl -fsSL -o "$ROOT/tools/oxrsdk.tgz" \
"https://github.com/KhronosGroup/OpenXR-SDK/archive/refs/heads/main.tar.gz"
tar xzf "$ROOT/tools/oxrsdk.tgz" -C "$ROOT/tools"
mv "$ROOT/tools/OpenXR-SDK-main" "$SRC"
fi
GEN="Unix Makefiles"; command -v ninja >/dev/null && GEN="Ninja"
cmake -S "$SRC" -B "$SRC/build-android" \
-DCMAKE_TOOLCHAIN_FILE="$NDK/build/cmake/android.toolchain.cmake" \
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-29 \
-DDYNAMIC_LOADER=ON -DBUILD_TESTS=OFF -DBUILD_API_LAYERS=OFF \
-DBUILD_CONFORMANCE_TESTS=OFF -DBUILD_WITH_SYSTEM_JSONCPP=OFF \
-G "$GEN" >/dev/null
cmake --build "$SRC/build-android" --target openxr_loader -j4
mkdir -p "$PKG/libs/arm64"
find "$SRC/build-android" -name 'libopenxr_loader.so' -exec cp {} "$PKG/libs/arm64/" \;
echo "loader -> $PKG/libs/arm64/libopenxr_loader.so"
file "$PKG/libs/arm64/libopenxr_loader.so" 2>/dev/null || true
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Decode the APK manifest and report whether the VR/OpenXR declarations Meta's
# runtime expects are present. For RE4 VR (already a shipping Quest VR app) these
# are almost certainly already there, so this is usually a no-op confirmation.
# If something IS missing, edit the decoded manifest and rebuild with apktool.
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_env.sh"
APKTOOL=("${JAVA_HOME:+$JAVA_HOME/bin/}java" -jar "$APKTOOL_JAR")
ORIG="${1:-$ROOT/dump/base.apk}"
WORK="$ROOT/packaging/work/decoded"
rm -rf "$WORK"
"${APKTOOL[@]}" d -f -s -o "$WORK" "$ORIG" >/dev/null
M="$WORK/AndroidManifest.xml"
echo "decoded manifest: $M"; echo
check() { grep -q "$2" "$M" && echo " [present] $1" || echo " [MISSING] $1 -> $2"; }
echo "VR / OpenXR declarations:"
check "headtracking feature" 'android.hardware.vr.headtracking'
check "VR intent category" 'com.oculus.intent.category.VR'
check "Samsung vr_only mode" 'com.samsung.android.vr.application.mode'
check "supportedDevices meta" 'com.oculus.supportedDevices'
check "handtracking permission" 'com.oculus.permission.HAND_TRACKING'
echo
echo "If any are MISSING, edit $M then rebuild:"
echo " ${APKTOOL[*]} b -o repacked.apk $WORK # then zipalign + apksigner (see repack.sh)"
echo "Note: switching vrapi->OpenXR usually needs NO manifest change; the loader"
echo " resolves the runtime. This is a confirmation step."
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Create a debug keystore for re-signing the repacked APK (one-time).
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_env.sh"
KS="$PKG/debug.keystore"
[ -f "$KS" ] && { echo "keystore exists: $KS"; exit 0; }
"$KEYTOOL" -genkeypair -v -keystore "$KS" -alias re4vrshim \
-keyalg RSA -keysize 2048 -validity 10000 \
-storepass android -keypass android \
-dname "CN=RE4VR Shim, OU=Dev, O=Homebrew, C=US"
echo "created $KS (storepass/keypass: android)"
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# Repack a dumped-your-own RE4 VR APK with the OpenXR shim + the OpenXR loader, then
# zipalign + re-sign. The original libovrplatformloader.so is kept untouched, so the
# platform's real entitlement check runs unchanged — on Quest you own the title.
# Entitlement handling on hardware with no Meta backend is out of scope for this repo.
#
# ./repack.sh [input.apk]
# input.apk : your dumped base.apk (default: ../dump/base.apk)
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_env.sh"
SHIM="$SHIM_OUT"
[ -n "${BT:-}" ] && [ -d "$BT" ] || { echo "Android build-tools not found. Run scripts/fetch_deps.sh"; exit 1; }
ORIG="${1:-$ROOT/dump/base.apk}"
OUT="$PKG/out/re4vr-shim.apk"
[ -f "$ORIG" ] || { echo "input APK not found: $ORIG"; exit 1; }
[ -f "$SHIM/libOVRPlugin.so" ] || { echo "build the arm64 shim first: shim/build_android.sh"; exit 1; }
mkdir -p "$PKG/out" "$PKG/work"
WORK="$PKG/work/re4vr.apk"
cp "$ORIG" "$WORK"
# 1) strip old signatures (we re-sign below)
zip -q -d "$WORK" 'META-INF/*.RSA' 'META-INF/*.SF' 'META-INF/*.MF' 'META-INF/*.EC' 2>/dev/null || true
# 2) stage the replacement libs under lib/arm64-v8a/
STAGE="$PKG/work/stage"; rm -rf "$STAGE"; mkdir -p "$STAGE/lib/arm64-v8a"
cp "$SHIM/libOVRPlugin.so" "$STAGE/lib/arm64-v8a/"
if [ -f "$PKG/libs/arm64/libopenxr_loader.so" ]; then
cp "$PKG/libs/arm64/libopenxr_loader.so" "$STAGE/lib/arm64-v8a/"
echo "bundling libopenxr_loader.so"
else
echo "WARN: packaging/libs/arm64/libopenxr_loader.so missing — shim NEEDs it at"
echo " runtime. Build it: packaging/build_openxr_loader.sh"
fi
# original libovrplatformloader.so is left untouched -> the real entitlement check runs.
echo "keeping original libovrplatformloader.so (real entitlement; you own the title)"
# P4 passthru RE: bundle the SONAME-patched REAL OVRPlugin so the shim can dlopen + forward
# to it (debug.re4vr.passthru*). Opt-in: only when staged in packaging/libs/arm64/.
if [ -f "$PKG/libs/arm64/libOVRPlugin_real.so" ]; then
cp "$PKG/libs/arm64/libOVRPlugin_real.so" "$STAGE/lib/arm64-v8a/"
echo "bundling libOVRPlugin_real.so (P4 passthru)"
fi
# 3) replace/add the libs, stored (-0) so zipalign -p can page-align them
( cd "$STAGE" && zip -q -0 -X "$WORK" lib/arm64-v8a/*.so )
# 4) align, then sign (v1+v2+v3)
"$BT/zipalign" -f -p 4 "$WORK" "$PKG/work/aligned.apk"
[ -f "$PKG/debug.keystore" ] || "$PKG/make_debug_keystore.sh"
"$BT/apksigner" sign --ks "$PKG/debug.keystore" --ks-pass pass:android \
--out "$OUT" "$PKG/work/aligned.apk"
"$BT/apksigner" verify "$OUT" && echo "signature OK"
echo
echo "built: $OUT"
echo "install: adb install -r \"$OUT\" (push the OBB too: dump/obb/* -> /sdcard/Android/obb/com.Armature.VR4/)"
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env bash
# steamframe_patches.sh — off-Quest APK patches for porting RE4 VR (UE4) to a NON-Meta
# Android VR device (Pico / Steam Frame / Monado-on-Android). These are NOT needed on a
# real Quest 2 (the shim alone suffices there); they only matter once Build.MANUFACTURER
# isn't "Oculus" and Meta-only manifest libs aren't present on the target.
#
# Our own implementation of the transforms; the set is informed by Overport's open-source
# patcher (docs/research/related-work.md). It does NOT use any of their binaries — we ship
# our own libOVRPlugin shim via repack.sh; this only fixes the APK's Java/manifest so UE
# takes the Oculus HMD path and the package installs off-Quest.
#
# Pipeline: apktool decode -> manifest + smali patches -> apktool build -> zipalign -> sign.
# Run repack.sh FIRST (it swaps in our shim libs); feed its output here.
#
# ./steamframe_patches.sh [in.apk] [out.apk]
# in.apk : shim-repacked APK (default: out/re4vr-shim.apk)
# out.apk : patched output (default: out/re4vr-steamframe.apk)
#
# Prereqs: apktool (https://apktool.org), plus the Android build-tools used by repack.sh.
# NOTE: prepared for the non-Quest bring-up; not yet validated on Steam Frame hardware.
set -euo pipefail
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_env.sh"
[ -n "${BT:-}" ] && [ -d "$BT" ] || { echo "Android build-tools not found. Run scripts/fetch_deps.sh"; exit 1; }
command -v apktool >/dev/null || { echo "apktool not found — install it (https://apktool.org) to run the smali/manifest patches"; exit 1; }
IN="${1:-$PKG/out/re4vr-shim.apk}"
OUT="${2:-$PKG/out/re4vr-steamframe.apk}"
[ -f "$IN" ] || { echo "input APK not found: $IN (run repack.sh first)"; exit 1; }
# --- DO NOT INSTALL ON QUEST guard ----------------------------------------------------
# This output is for NON-Quest devices. On a Quest the uses-(native-)library strip removes
# Meta libs the runtime relies on -> can break launch/features. The Quest build is the plain
# repack.sh output (out/re4vr-shim.apk). Require explicit acknowledgement so this can't be
# produced/installed against a Quest by accident. Bypass in automation with NOT_QUEST=1.
cat <<'WARN'
========================================================================
WARNING: NON-QUEST build. DO NOT install the output on a Meta Quest.
It strips Meta-only manifest libs the Quest needs (can break launch).
For Quest, install the plain repack.sh output: out/re4vr-shim.apk
========================================================================
WARN
if [ "${NOT_QUEST:-0}" != 1 ]; then
if [ -t 0 ]; then
read -r -p "Target is NOT a Quest and I understand this build will break on Quest [y/N] " ack
case "$ack" in y|Y|yes|YES) ;; *) echo "aborted (not confirmed)"; exit 1 ;; esac
else
echo "Refusing to run non-interactively. Re-run with NOT_QUEST=1 to confirm the target is not a Quest."
exit 1
fi
fi
WORK="$PKG/work/steamframe"
rm -rf "$WORK"; mkdir -p "$WORK"
DEC="$WORK/dec"
echo "== apktool decode =="
apktool d -f -o "$DEC" "$IN" >/dev/null
# Locate UE's GameActivity smali (ue4 or unreal namespace, across smali_classesN dirs).
mapfile -t GA < <(find "$DEC" -path '*/com/epicgames/ue4/GameActivity.smali' \
-o -path '*/com/epicgames/unreal/GameActivity.smali' 2>/dev/null)
[ "${#GA[@]}" -gt 0 ] || echo "WARN: no com/epicgames/{ue4,unreal}/GameActivity.smali found (UE patches skipped)"
# --- 1) Oculus device spoof: UE gates the Oculus HMD path on Build.MANUFACTURER/MODEL.
# Off-Quest these are wrong, so OculusHMD never inits and our shim is never called.
# Replace the field reads with constant "Oculus" / "Quest 2" in the same register. ---
spoofed=0
for f in "${GA[@]}"; do
perl -0777 -pe 's/sget-object (v\d+|p\d+), Landroid\/os\/Build;->MANUFACTURER:Ljava\/lang\/String;/const-string $1, "Oculus"/g' -i "$f"
perl -0777 -pe 's/sget-object (v\d+|p\d+), Landroid\/os\/Build;->MODEL:Ljava\/lang\/String;/const-string $1, "Quest 2"/g' -i "$f"
spoofed=1
done
[ "$spoofed" = 1 ] && echo "patched: Build.MANUFACTURER->\"Oculus\", MODEL->\"Quest 2\" (device spoof)"
# --- 2) Neuter AndroidThunkJava_ForceQuit: drop the System.exit(I) call so a failed
# off-Quest check can't hard-kill the app before we recover. ---
for f in "${GA[@]}"; do
perl -0777 -pe 's/(\.method public AndroidThunkJava_ForceQuit\(\)V.*?)(invoke-static \{[vp]\d+\}, Ljava\/lang\/System;->exit\(I\)V\n)(.*?\.end method)/$1$3/s' -i "$f" \
&& grep -q 'AndroidThunkJava_ForceQuit' "$f" && echo "patched: removed System.exit in AndroidThunkJava_ForceQuit ($(basename "$(dirname "$f")"))"
done
# --- 3) Manifest: strip <uses-library>/<uses-native-library> that name Meta-only libs
# (keep libopenxr.google.so) — they'd block install/launch off-Quest. ---
MAN="$DEC/AndroidManifest.xml"
if [ -f "$MAN" ]; then
before=$(grep -cE 'uses-(native-)?library' "$MAN" || true)
perl -0777 -pe 's/[ \t]*<uses-(native-)?library[^>]*android:name="(?!libopenxr\.google\.so)[^"]*"[^>]*\/>\n//g' -i "$MAN"
after=$(grep -cE 'uses-(native-)?library' "$MAN" || true)
echo "patched: manifest uses-(native-)library entries $before -> $after (kept libopenxr.google.so)"
fi
echo "== apktool build =="
apktool b -o "$WORK/unsigned.apk" "$DEC" >/dev/null
echo "== align + sign =="
"$BT/zipalign" -f -p 4 "$WORK/unsigned.apk" "$WORK/aligned.apk"
[ -f "$PKG/debug.keystore" ] || "$PKG/make_debug_keystore.sh"
"$BT/apksigner" sign --ks "$PKG/debug.keystore" --ks-pass pass:android --out "$OUT" "$WORK/aligned.apk"
"$BT/apksigner" verify "$OUT" && echo "signature OK"
echo
echo "built: $OUT"
echo "NOTE: NON-QUEST build — do NOT install on a Quest (use repack.sh's re4vr-shim.apk there)."
echo " prepared for the non-Quest bring-up; validate on the target headset."
echo "Still a shim TODO (runtime, not packaging): controller-pose offset for non-Touch"
echo "controllers (Overport's DisableControllerOffset) — handle in shim/src/xr_input.c."
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# Fetch build dependencies that aren't committed (permissively-licensed headers +
# the Android NDK). Run once after cloning.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
OXR="$ROOT/shim/third_party/openxr/openxr"
VK="$ROOT/shim/third_party/vulkan/vulkan"
echo "== OpenXR headers (Apache-2.0) =="
mkdir -p "$OXR"
OXRBASE="https://raw.githubusercontent.com/KhronosGroup/OpenXR-SDK/main/include/openxr"
for h in openxr.h openxr_platform.h openxr_platform_defines.h; do
curl -fsSL "$OXRBASE/$h" -o "$OXR/$h"
done
echo "== Vulkan headers (Apache-2.0) =="
mkdir -p "$VK/vk_video"
VKBASE="https://raw.githubusercontent.com/KhronosGroup/Vulkan-Headers/main/include/vulkan"
for h in vulkan_core.h vk_platform.h; do curl -fsSL "$VKBASE/$h" -o "$VK/$h"; done
printf '#ifndef VULKAN_H_\n#define VULKAN_H_\n#include "vk_platform.h"\n#include "vulkan_core.h"\n#endif\n' > "$VK/vulkan.h"
VVBASE="https://raw.githubusercontent.com/KhronosGroup/Vulkan-Headers/main/include/vk_video"
for f in $(grep -oE 'vk_video/[a-zA-Z0-9_]+\.h' "$VK/vulkan_core.h" | sed 's|vk_video/||' | sort -u); do
curl -fsSL "$VVBASE/$f" -o "$VK/vk_video/$f"
done
mkdir -p "$ROOT/tools"; cd "$ROOT/tools"
echo "== Android NDK r27c (large ~700MB) =="
[ -d "$ROOT"/tools/android-ndk-* ] 2>/dev/null || {
curl -L -o ndk.zip "https://dl.google.com/android/repository/android-ndk-r27c-linux.zip"
unzip -q ndk.zip && rm ndk.zip; }
echo "== JDK (Temurin 21, for keytool/apksigner) =="
ls -d "$ROOT"/tools/jdk-* >/dev/null 2>&1 || {
curl -L -o jdk.tgz "https://api.adoptium.net/v3/binary/latest/21/ga/linux/x64/jdk/hotspot/normal/eclipse"
tar xzf jdk.tgz && rm jdk.tgz; }
echo "== Android build-tools (zipalign/apksigner/aapt2) =="
ls -d "$ROOT"/tools/android-[0-9]* >/dev/null 2>&1 || {
curl -L -o bt.zip "https://dl.google.com/android/repository/build-tools_r34-linux.zip"
unzip -q bt.zip && rm bt.zip; }
echo "== apktool (manifest tooling) =="
[ -f "$ROOT/tools/apktool.jar" ] || curl -fsSL -o apktool.jar \
"https://github.com/iBotPeaches/Apktool/releases/download/v2.10.0/apktool_2.10.0.jar"
echo "done. Now: shim/build_android.sh (and packaging/repack.sh for an installable APK)."
echo "NOTE: URLs are Linux-x86_64; on macOS swap the NDK/JDK/build-tools archives."
+54
View File
@@ -0,0 +1,54 @@
# libOVRPlugin.so shim (OVRPlugin v1.51 -> OpenXR)
Drop-in replacement for RE4 VR's libOVRPlugin.so that re-exports the ovrp_* symbols
backed by OpenXR (Monado) instead of Meta's libvrapi.so. See ../HOST.md, ../SHIM-SCOPE.md.
## Layout
- include/ovrplugin_shim.h — OVRPlugin v1.51 C ABI (types + core protos; self-checking
_Static_asserts on binary-verified struct sizes).
- gen_stubs.sh — generates src/stubs.c from ../analysis/shim_surface.txt.
- src/stubs.c (generated) — 224 stubs: 101 Unsupported(-1004), 32 no-op Success(0),
91 NotYetImplemented(-1005).
- src/core.c — the 15 header-prototyped core fns (frame loop/init/poses/
controller/system), currently TODO stubs returning -1005 with outputs zeroed.
## Build (host, for validation)
```
cc -std=c11 -Wall -shared -fPIC -fvisibility=hidden -Iinclude src/stubs.c src/core.c \
-o build/libOVRPlugin.so
```
Status: builds; exports exactly 239 ovrp_ symbols (1:1 with shim_surface.txt, 0
missing/extra); dlopen+dlsym verified. This is the SKELETON — it loads and resolves,
it does not yet drive VR (core fns are TODO).
## Real target build (deployable) — TODO
Must be aarch64 / Android (bionic) since it runs inside Lepton. Use the Android NDK:
```
$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android29-clang \
-std=c11 -shared -fPIC -fvisibility=hidden -Iinclude src/stubs.c src/core.c \
-o build/arm64/libOVRPlugin.so
```
(host build only proves the C + symbol coverage; NDK build is the artifact that
replaces the real lib in the APK.)
## Modules (current)
- include/ovrplugin_shim.h — OVRPlugin v1.51 ABI (types, structs, core protos).
- src/stubs.c (generated) — 221 stubs (unsupported/no-op/TODO).
- src/core.c — lifecycle + frame loop + poses (-> xr_runtime).
- src/xr_runtime.{h,c} — OpenXR engine: session, frame loop, swapchains.
- src/vk_session.c — Vulkan-typed: xrCreateSession binding + image enum.
- src/layers.c — ovrp_SetupLayer / GetLayerTexture2 / StageCount.
- tests/harness.c — Path B smoke test (see ../TESTING.md).
Build adds: -Ithird_party/openxr -Ithird_party/vulkan, and src/{core,xr_runtime,
vk_session,layers,stubs}.c. 20 OpenXR fns used; 239/239 ovrp_ exported.
## Done so far
Session lifecycle, frame loop (xrWaitFrame/Begin/EndFrame), poses (xrLocateViews/
Space), swapchains (xrCreateSwapchain from ovrpLayerDesc, acquire/wait/release,
XrCompositionLayerProjection submit). Vulkan binding from Initialize5 args [VERIFIED].
## Next (see ../TESTING.md for the test plan)
1. NDK arm64 build (host build is validation only).
2. [ANDROID-TODO] JavaVM/activity -> XrInstanceCreateInfoAndroidKHR + xrInitializeLoaderKHR.
3. Input action sets (controllers/hands); depth layer; GetVulkan*ExtensionsKHR mapping.
4. Repack APK (real entitlement — you own it); test on Quest 2.
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Cross-build the shim to Android arm64 (the deployable target — runs inside the
# RE4 APK on Quest/Lepton). Host build is validation-only; this is the real artifact.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SHIM="$ROOT/shim"
NDK="${ANDROID_NDK:-$(ls -d "$ROOT"/tools/android-ndk-* 2>/dev/null | head -1)}"
[ -n "${NDK:-}" ] && [ -d "$NDK" ] || { echo "NDK not found. Set \$ANDROID_NDK or run scripts/fetch_deps.sh"; exit 1; }
TC="$(ls -d "$NDK"/toolchains/llvm/prebuilt/* 2>/dev/null | head -1)" # host tag (linux/darwin)
CC="$TC/bin/aarch64-linux-android29-clang" # API 29 (Quest is Android 10+; app targetSdk=29)
[ -x "$CC" ] || { echo "NDK clang not found at $CC"; exit 1; }
mkdir -p "$SHIM/build/arm64"
"$CC" -std=c11 -Wall -shared -fPIC -fvisibility=hidden \
-I"$SHIM/include" -I"$SHIM/third_party/openxr" -I"$SHIM/third_party/vulkan" \
"$SHIM/src/stubs.c" "$SHIM/src/core.c" "$SHIM/src/xr_runtime.c" \
"$SHIM/src/vk_session.c" "$SHIM/src/layers.c" "$SHIM/src/android_init.c" "$SHIM/src/xr_input.c" \
"$SHIM/src/passthru.c" \
-llog \
-o "$SHIM/build/arm64/libOVRPlugin.so"
# NOTE: entitlement handling is out of scope for this project. On Quest you own the title,
# so the platform's real entitlement check runs unchanged (keep the original
# libovrplatformloader.so). Running on hardware with no Meta backend requires a valid
# entitlement by some other means, which is the user's responsibility and not part of this repo.
# the xr* symbols resolve against libopenxr_loader.so at runtime; declare the dep so
# the dynamic linker loads it (bundle libopenxr_loader.so in the APK lib/arm64-v8a/).
PATCHELF="$(command -v patchelf || true)"
if [ -n "$PATCHELF" ]; then
"$PATCHELF" --add-needed libopenxr_loader.so "$SHIM/build/arm64/libOVRPlugin.so"
echo "added NEEDED libopenxr_loader.so"
else
echo "WARN: patchelf not found — add 'libopenxr_loader.so' as NEEDED before packaging"
fi
echo "built: $SHIM/build/arm64/libOVRPlugin.so"
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# build_host.sh — desktop (x86_64 Linux) build of the shim + the OpenXR test harness.
# Mirrors build_android.sh but targets the host so the OpenXR path can be driven against
# Monado's simulated HMD on a PC (see tools/desktop-harness/). NOT the shipping artifact —
# that's build_android.sh (arm64). Needs: a C compiler, libvulkan, and the OpenXR loader
# (Debian: libvulkan-dev libopenxr-loader1 libopenxr-dev). Run tools/desktop-harness/run.sh
# after this to launch it headless against monado-service.
set -euo pipefail
SHIM="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$SHIM/.." && pwd)"
OUT="$ROOT/build/host"
mkdir -p "$OUT"
CC="${CC:-cc}"
# _GNU_SOURCE: glibc gates dladdr/CLOCK_MONOTONIC behind it (the NDK exposes them by default).
INC="-I$SHIM/include -I$SHIM/third_party/openxr -I$SHIM/third_party/vulkan"
CFLAGS="-std=c11 -Wall -D_GNU_SOURCE -fPIC -O2 -g $INC"
echo "== compiling shim sources (host) =="
OBJS=()
for f in "$SHIM"/src/*.c; do
o="$OUT/$(basename "${f%.c}").o"
"$CC" $CFLAGS -c "$f" -o "$o"
OBJS+=("$o")
done
# The shim's xr* calls resolve against the system OpenXR loader; Vulkan against libvulkan.
# Detect the loader via ldconfig, falling back to a direct file probe (ldconfig reads
# /etc/ld.so.cache, which some sandboxes block -> false negative).
have_loader() {
ldconfig -p 2>/dev/null | grep -q libopenxr_loader && return 0
for d in /usr/lib /usr/lib/x86_64-linux-gnu /lib/x86_64-linux-gnu /usr/local/lib; do
[ -e "$d/libopenxr_loader.so" ] || [ -e "$d/libopenxr_loader.so.1" ] && return 0
done
return 1
}
if ! have_loader; then
echo "WARN: libopenxr_loader not found — install libopenxr-loader1 libopenxr-dev to link/run."
echo " (objects built OK; skipping the .so + harness link.)"
exit 0
fi
echo "== linking libOVRPlugin.so (host) =="
"$CC" -shared -o "$OUT/libOVRPlugin.so" "${OBJS[@]}" -lopenxr_loader -lvulkan -lpthread -ldl
echo "built: $OUT/libOVRPlugin.so"
echo "== building harness =="
"$CC" -std=c11 -Wall -D_GNU_SOURCE -O2 -g -I"$SHIM/include" -I"$SHIM/third_party/vulkan" \
"$ROOT/tools/desktop-harness/harness.c" \
-o "$OUT/harness" \
-L"$OUT" -lOVRPlugin -lvulkan -lm -Wl,-rpath,"$OUT"
echo "built: $OUT/harness"
echo
echo "run it headless against Monado: tools/desktop-harness/run.sh"
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# Generate shim/src/stubs.c from analysis/shim_surface.txt.
# Each ovrp_ function gets a stub returning a sensible ovrpResult, classified by
# name per SHIM-SCOPE.md buckets. Functions prototyped in the header (real sigs)
# are SKIPPED here and live in core.c instead.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# Use the ORIGINAL lib's FULL export set (438) so the shim is a complete drop-in:
# any dlsym of any ovrp_ resolves, even the 199 RE4 doesn't reference.
SURF="$ROOT/analysis/all_exports.txt"
OUT="$ROOT/shim/src/stubs.c"
mkdir -p "$ROOT/shim/src"
# functions with real prototypes in ovrplugin_shim.h -> handled in core.c
HEADER_FNS=" ovrp_GetNodePoseState3 ovrp_GetNodePoseStateRaw ovrp_GetControllerState4 \
ovrp_PreInitialize3 ovrp_Initialize5 ovrp_Shutdown2 ovrp_Update3 ovrp_WaitToBeginFrame \
ovrp_BeginFrame4 ovrp_EndFrame4 ovrp_GetPredictedDisplayTime ovrp_GetSystemHeadsetType2 \
ovrp_GetTrackingOriginType2 ovrp_SetTrackingOriginType2 ovrp_RecenterTrackingOrigin2 \
ovrp_SetupLayer ovrp_GetLayerTextureStageCount ovrp_GetLayerTexture2 \
ovrp_GetInstanceExtensionsVk ovrp_GetDeviceExtensionsVk \
ovrp_GetInitialized ovrp_GetSystemDisplayFrequency2 \
ovrp_GetAppHasVrFocus2 ovrp_GetAppShouldQuit2 ovrp_GetUserPresent2 \
ovrp_GetAppShouldRecenter2 ovrp_GetAppShouldRecreateDistortionWindow2 \
ovrp_GetSystemMultiViewSupported2 ovrp_GetAppHasInputFocus ovrp_SetupDistortionWindow3 \
ovrp_CalculateEyeLayerDesc2 ovrp_CalculateEyeViewportRect ovrp_GetMixedRealityInitialized ovrp_CalculateLayerDesc ovrp_GetNodePresent2 ovrp_GetNodeOrientationValid ovrp_GetNodePositionValid ovrp_GetNodeOrientationTracked2 ovrp_GetNodePositionTracked2 ovrp_SetControllerVibration2 \
ovrp_GetTiledMultiResSupported ovrp_GetTiledMultiResLevel ovrp_SetTiledMultiResLevel \
ovrp_GetTiledMultiResDynamic ovrp_SetTiledMultiResDynamic ovrp_GetGPUFrameTime \
ovrp_DestroyLayer ovrp_GetNodeFrustum2 ovrp_SetSystemCpuLevel2 ovrp_SetSystemGpuLevel2 \
ovrp_IsPerfMetricsSupported ovrp_GetPerfMetricsFloat ovrp_GetPerfMetricsInt \
ovrp_GetAdaptiveGpuPerformanceScale2 \
ovrp_DestroyDistortionWindow2 ovrp_SetupDisplayObjects2 ovrp_SetReorientHMDOnControllerRecenter \
ovrp_SetClientColorDesc ovrp_SetAppEngineInfo2 ovrp_SetAppCPUPriority2 ovrp_InitializeMixedReality \
ovrp_GetViewportStencil ovrp_GetSystemRecommendedMSAALevel2 ovrp_GetLocalTrackingSpaceRecenterCount \
ovrp_GetLayerTextureFoveation ovrp_GetControllerHapticsDesc2 "
# NB: the trailing 12 are game-called () stubs now owned by passthru.c — they keep their
# original return constant when passthru is off, and tail-call the real lib when it's on.
{
echo '/* AUTO-GENERATED by gen_stubs.sh from analysis/shim_surface.txt. Do not edit by hand. */'
echo '#include "ovrplugin_shim.h"'
echo '#include "log.h"'
echo
echo '/* Stub policy:'
echo ' * -1004 Unsupported : features we will not implement (MRC/camera/perf/boundary/hands)'
echo ' * 0 Success (no-op) : config setters we can safely accept-and-ignore for now'
echo ' * -1005 NotYetImplemented : real work still owed (getters/etc. the game needs)'
echo ' * Empty () = unspecified args: stub ignores args, returns constant in result reg. */'
echo
n_unsup=0; n_noop=0; n_todo=0; n_skip=0
while read -r fn; do
[ -z "$fn" ] && continue
case " $HEADER_FNS " in *" $fn "*) n_skip=$((n_skip+1)); continue;; esac
if echo "$fn" | grep -qE 'Media_|CameraDevice|ExternalCamera|HandState|Skeleton|GetMesh|HandTracking|HandNode|PerfMetric|GPUUtil|GPUFrameTime|ASW|TiledMultiRes|Boundary'; then
ret="ovrpFailure_Unsupported"; n_unsup=$((n_unsup+1))
elif echo "$fn" | grep -qE '^ovrp_Set[A-Z]|^ovrp_Reset|^ovrp_Recenter|^ovrp_SendEvent|^ovrp_Destroy|^ovrp_Override'; then
ret="ovrpSuccess"; n_noop=$((n_noop+1))
else
ret="ovrpFailure_NotYetImplemented"; n_todo=$((n_todo+1))
fi
printf 'OVRP_EXPORT ovrpResult %s() { static int o; if(!o){o=1;XRRLOG("stub %s -> %s");} return %s; }\n' "$fn" "$fn" "$ret" "$ret"
done < "$SURF"
echo
echo "/* generated: unsup=$n_unsup noop=$n_noop todo=$n_todo skipped(core)=$n_skip */"
} > "$OUT"
echo "wrote $OUT"
grep -c '^OVRP_EXPORT' "$OUT" | xargs echo "stub functions:"
tail -1 "$OUT"
+290
View File
@@ -0,0 +1,290 @@
/* ovrplugin_shim.h — OVRPlugin (v1.51) C ABI for the RE4 VR -> OpenXR shim.
*
* Goal: a drop-in replacement libOVRPlugin.so that re-exports the ovrp_* symbols
* RE4 VR (com.Armature.VR4, OVRPlugin 1.51 / pkg 19.0.0.449.531) calls, backed by
* OpenXR (Monado) on Steam Frame instead of Meta's libvrapi.so.
*
* Provenance of each declaration:
* [VERIFIED] struct size / arg shape confirmed from Ghidra decompilation of the
* actual binary (see analysis/ovrp_decomp.txt, RE-NOTES.md).
* [HEADER] taken from public OVRPlugin.cs @ v1.51; layout trusted because the
* VERIFIED structs matched it byte-for-byte, but not independently
* re-confirmed against this binary yet.
* [TODO] signature not yet finalized — placeholder, do not trust arg list.
*
* Status: scaffold. Base types + the fully-confirmed functions are real; the rest
* of the ~239-function surface (analysis/shim_surface.txt) is still to be filled.
*/
#ifndef OVRPLUGIN_SHIM_H
#define OVRPLUGIN_SHIM_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* The real lib's exported ovrp_* are thin thunks; we just need matching symbols
* with default visibility so the game's dlsym resolves to us. */
#define OVRP_EXPORT __attribute__((visibility("default")))
/* ------------------------------------------------------------------ result --- */
/* ovrpResult is a signed 32-bit int; success >= 0, failure < 0.
* Codes below VERIFIED from return constants in the decompiled binary:
* -1001 (0xfffffc17), -1002 (0xfffffc16), -1003 (0xfffffc15). */
typedef int32_t ovrpResult;
#define ovrpSuccess 0
#define ovrpSuccess_EventUnavailable 1
#define ovrpFailure -1000
#define ovrpFailure_InvalidParameter -1001 /* [VERIFIED] */
#define ovrpFailure_NotInitialized -1002 /* [VERIFIED] */
#define ovrpFailure_InvalidOperation -1003 /* [VERIFIED] */
#define ovrpFailure_Unsupported -1004
#define ovrpFailure_NotYetImplemented -1005
#define ovrpFailure_OperationFailed -1006
#define ovrpFailure_InsufficientSize -1007
#define ovrpFailure_DataIsInvalid -1008
#define ovrpFailure_DeprecatedOperation -1009
#define OVRP_SUCCESS(r) ((r) >= 0)
typedef enum { ovrpBool_False = 0, ovrpBool_True = 1 } ovrpBool;
/* -------------------------------------------------------------- math types --- */
typedef struct { float x, y; } ovrpVector2f;
typedef struct { int32_t x, y; } ovrpVector2i;
typedef struct { int32_t w, h; } ovrpSizei;
typedef struct { ovrpVector2i Pos; ovrpSizei Size; } ovrpRecti;
typedef struct { float x, y, z; } ovrpVector3f;
typedef struct { float x, y, z, w; } ovrpVector4f;
typedef struct { float x, y, z, w; } ovrpQuatf;
typedef struct { float w, h; } ovrpSizef;
typedef struct { ovrpVector2f Pos; ovrpSizef Size; } ovrpRectf;
typedef struct { float UpTan, DownTan, LeftTan, RightTan; } ovrpFovf;
typedef struct { float zNear, zFar; ovrpFovf Fov; } ovrpFrustum2f;
typedef struct { ovrpQuatf Orientation; ovrpVector3f Position; } ovrpPosef; /* 28 */
typedef struct {
ovrpPosef Pose;
ovrpVector3f Velocity;
ovrpVector3f Acceleration;
ovrpVector3f AngularVelocity;
ovrpVector3f AngularAcceleration;
double Time;
} ovrpPoseStatef; /* 88 */
/* binary-verified sizes — header is wrong if these fail */
_Static_assert(sizeof(ovrpPosef) == 28, "ovrpPosef must be 28 bytes");
_Static_assert(sizeof(ovrpPoseStatef) == 88, "ovrpPoseStatef must be 0x58=88 [VERIFIED]");
/* --------------------------------------------------------------- enums ------- */
typedef enum {
ovrpNode_None = -1, ovrpNode_EyeLeft = 0, ovrpNode_EyeRight = 1,
ovrpNode_EyeCenter = 2, ovrpNode_HandLeft = 3, ovrpNode_HandRight = 4,
ovrpNode_TrackerZero = 5, ovrpNode_TrackerOne = 6, ovrpNode_TrackerTwo = 7,
ovrpNode_TrackerThree = 8, ovrpNode_Head = 9, ovrpNode_DeviceObjectZero = 10,
ovrpNode_EnumSize = 0x7fffffff
} ovrpNode;
typedef enum {
ovrpStep_Render = -1, ovrpStep_Physics = 0, ovrpStep_EnumSize = 0x7fffffff
} ovrpStep;
typedef enum {
ovrpController_None = 0, ovrpController_LTouch = 0x01, ovrpController_RTouch = 0x02,
ovrpController_Touch = 0x03, ovrpController_Remote = 0x04, ovrpController_Gamepad = 0x10,
ovrpController_LTrackedRemote = 0x01000000, ovrpController_RTrackedRemote = 0x02000000,
ovrpController_Touchpad = 0x08000000, ovrpController_Active = 0x80000000u,
ovrpController_EnumSize = 0x7fffffff
} ovrpController;
typedef enum {
ovrpSystemHeadset_None = 0,
/* mobile */
ovrpSystemHeadset_Oculus_Quest = 8, ovrpSystemHeadset_Oculus_Quest_2 = 9,
ovrpSystemHeadset_EnumSize = 0x7fffffff
} ovrpSystemHeadset;
typedef enum {
ovrpTrackingOrigin_EyeLevel = 0, ovrpTrackingOrigin_FloorLevel = 1,
ovrpTrackingOrigin_Stage = 2, ovrpTrackingOrigin_EnumSize = 0x7fffffff
} ovrpTrackingOrigin;
/* ----------------------------------------------------------- controller ------ */
typedef struct {
uint32_t ConnectedControllers;
uint32_t Buttons;
uint32_t Touches;
uint32_t NearTouches;
float LIndexTrigger, RIndexTrigger;
float LHandTrigger, RHandTrigger;
ovrpVector2f LThumbstick, RThumbstick;
ovrpVector2f LTouchpad, RTouchpad;
uint8_t LBatteryPercentRemaining, RBatteryPercentRemaining;
uint8_t LRecenterCount, RRecenterCount;
uint8_t Reserved[28];
} ovrpControllerState4; /* 96 */
_Static_assert(sizeof(ovrpControllerState4) == 96,
"ovrpControllerState4 must be 0x60=96 [VERIFIED]");
/* ----------------------------------------------------------- layers ---------- */
/* ovrpShape: Compositor::ImportLayerDesc switches on field +0x00. Cases
* 0,1,2,4,5 verified as overlay shapes; case 3 = EyeFov (projection). [VERIFIED] */
typedef enum {
ovrpShape_Quad = 0, ovrpShape_Cylinder = 1, ovrpShape_Cubemap = 2,
ovrpShape_EyeFov = 3, ovrpShape_OffcenterCubemap = 4, ovrpShape_Equirect = 5,
ovrpShape_ReconstructionPassthrough = 7, ovrpShape_SurfaceProjectedPassthrough = 8,
ovrpShape_Fisheye = 9, ovrpShape_EnumSize = 0xF
} ovrpShape;
typedef enum {
ovrpLayout_Stereo = 0, ovrpLayout_Mono = 1, ovrpLayout_DoubleWide = 2,
ovrpLayout_Array = 3, ovrpLayout_EnumSize = 0xF
} ovrpLayout;
typedef enum {
ovrpTextureFormat_R8G8B8A8_sRGB = 0, ovrpTextureFormat_R8G8B8A8 = 1,
ovrpTextureFormat_R16G16B16A16_FP = 2, ovrpTextureFormat_R11G11B10_FP = 3,
ovrpTextureFormat_B8G8R8A8_sRGB = 4, ovrpTextureFormat_B8G8R8A8 = 5,
ovrpTextureFormat_R5G6B5 = 11, ovrpTextureFormat_EnumSize = 0x7fffffff
} ovrpTextureFormat;
/* ovrpLayerDesc — VERIFIED: Compositor::ImportLayerDesc memset's 0x7c=124 and
* copies 0x68/0x6c/0x7c by version (base / +DepthFormat / +MotionVector). The
* field layout below reproduces those three sizes exactly. */
typedef struct {
ovrpShape Shape; /* +0x00 switch field [VERIFIED] */
ovrpLayout Layout; /* +0x04 */
ovrpSizei TextureSize; /* +0x08 */
int MipLevels; /* +0x10 */
int SampleCount; /* +0x14 */
ovrpTextureFormat Format; /* +0x18 */
int LayerFlags; /* +0x1c (common header ends +0x20) */
ovrpFovf Fov[2]; /* +0x20 */
ovrpRectf VisibleRect[2]; /* +0x40 */
ovrpSizei MaxViewportSize; /* +0x60 */
ovrpTextureFormat DepthFormat; /* +0x68 (-> 0x6c = UE 4.25 EyeFov) */
/* NOTE: the binary's internal union also supports a 124-byte variant with
* MotionVector* fields, but UE 4.25's ovrpLayerDesc_EyeFov ends here (108).
* We match UE's size so we never write past the caller's buffer. */
} ovrpLayerDesc; /* 108 */
typedef ovrpLayerDesc ovrpLayerDesc_EyeFov;
_Static_assert(sizeof(ovrpLayerDesc) == 0x6c,
"ovrpLayerDesc must be 0x6c=108 [UE 4.25 ovrpLayerDesc_EyeFov]");
/* ovrpLayerSubmit — VERIFIED 0x130=304 bytes (EndFrame4 allocs count*0x130).
* Header fields are the public layout [HEADER]; the per-shape union tail is not
* yet broken out -> reserved bytes to reach 304. [TODO] reverse ShapeData. */
typedef struct {
int LayerId; /* +0x00 */
int TextureStage; /* +0x04 */
ovrpRecti ViewportRect[2]; /* +0x08 */
ovrpPosef Pose; /* +0x28 */
int LayerSubmitFlags; /* +0x44 (ovrpLayerSubmitFlag_HeadLocked=1<<0) */
ovrpVector4f ColorScale; /* +0x48 (added 1.31) */
ovrpVector4f ColorOffset; /* +0x58 */
int OverrideTextureRectMatrix; /* +0x68 (ovrpBool=int, added 1.34) */
float TextureRectMatrix[16]; /* +0x6C (ovrpTextureRectMatrixf=64B) */
int OverridePerLayerColorScaleAndOffset;/* +0xAC */
ovrpSizef QuadSize; /* +0xB0 (ovrpLayerSubmit_Quad tail; world meters) */
unsigned char _pad[0x130 - 0xB8]; /* reach the 304B union stride */
} ovrpLayerSubmit; /* 304 */
enum { ovrpLayerSubmitFlag_HeadLocked = (1 << 0) };
_Static_assert(sizeof(ovrpLayerSubmit) == 0x130,
"ovrpLayerSubmit must be 0x130=304 [VERIFIED EndFrame4 stride]");
/* =================================================================== API ===== */
/* CONFIRMED (arg shape matched in Ghidra) */
OVRP_EXPORT ovrpResult ovrp_GetNodePoseState3( /* [VERIFIED] 3 ints + out ptr */
ovrpStep step, int frameIndex, ovrpNode nodeId, ovrpPoseStatef *outState);
OVRP_EXPORT ovrpResult ovrp_GetNodePoseStateRaw( /* [VERIFIED] same shape, 0x58 */
ovrpStep step, int frameIndex, ovrpNode nodeId, ovrpPoseStatef *outState);
OVRP_EXPORT ovrpResult ovrp_GetControllerState4( /* [VERIFIED] mask + out 0x60 */
ovrpController controllerMask, ovrpControllerState4 *outState);
/* CORE — frame loop / lifecycle [HEADER: from v1.51, arg list to re-verify] */
/* ovrpRenderAPIType — apiType arg of Initialize5 */
typedef enum {
ovrpRenderAPI_None = 0, ovrpRenderAPI_OpenGL = 1, ovrpRenderAPI_Android_GLES = 2,
ovrpRenderAPI_Vulkan = 4, ovrpRenderAPI_EnumSize = 0x7fffffff
} ovrpRenderAPIType;
typedef void (*ovrpLogCallback)(int level, const char *message);
OVRP_EXPORT ovrpResult ovrp_PreInitialize3(void *logCallback); /* [TODO] */
/* [VERIFIED from UE OculusHMD.cpp InitializeSession()] exact arg order:
* (apiType, logCallback, activity, VkInstance, VkPhysicalDevice, VkDevice,
* VkQueue, flags, version). arg7 is a VkQueue handle, NOT a family index. */
OVRP_EXPORT ovrpResult ovrp_Initialize5(
ovrpRenderAPIType apiType, ovrpLogCallback logCallback, void *activity,
void *vkInstance, void *vkPhysicalDevice, void *vkDevice, void *queue,
unsigned int flags, const void *version); /* real 9th arg is const ovrpVersion& == a
64-bit pointer; declaring it as a 32-bit int truncates it (crashes P4 passthru forward). */
OVRP_EXPORT ovrpResult ovrp_Shutdown2(void);
OVRP_EXPORT ovrpResult ovrp_Update3(ovrpStep step, int frameIndex, double predictedTime);
OVRP_EXPORT ovrpResult ovrp_WaitToBeginFrame(int frameIndex);
OVRP_EXPORT ovrpResult ovrp_BeginFrame4(int frameIndex, void *commandQueue); /* [HEADER] */
OVRP_EXPORT ovrpResult ovrp_EndFrame4(int frameIndex,
const ovrpLayerSubmit *const *layerSubmitPtr, int layerSubmitCount,
void *commandQueue); /* [HEADER] */
OVRP_EXPORT ovrpResult ovrp_GetPredictedDisplayTime(int frameIndex, double *outTime);
/* CORE — system / eye params [HEADER] */
OVRP_EXPORT ovrpResult ovrp_GetSystemHeadsetType2(ovrpSystemHeadset *outType);
OVRP_EXPORT ovrpResult ovrp_GetTrackingOriginType2(ovrpTrackingOrigin *outOrigin);
OVRP_EXPORT ovrpResult ovrp_SetTrackingOriginType2(ovrpTrackingOrigin origin);
OVRP_EXPORT ovrpResult ovrp_RecenterTrackingOrigin2(unsigned int flags);
/* DYNAMIC PERF — tiled multi-resolution (Oculus name for Fixed Foveated Rendering)
* + GPU frame time. RE4 drives its own dynamic-perf loop through these; the shim
* forwards the requested FFR level onto OpenXR XR_FB_foveation (see xr_runtime.c)
* and answers GetGPUFrameTime with a measured frame-time estimate so the loop has
* an input. Signatures verified against libOVRPlugin.so (analysis/endframe_impls.txt,
* analysis/ovrp_decomp.txt): Get* take an out-pointer, Set* take a value. */
typedef enum {
ovrpTiledMultiResLevel_Off = 0,
ovrpTiledMultiResLevel_LMSLow = 1,
ovrpTiledMultiResLevel_LMSMedium = 2,
ovrpTiledMultiResLevel_LMSHigh = 3,
ovrpTiledMultiResLevel_LMSHighTop = 4,
ovrpTiledMultiResLevel_EnumSize = 0x7fffffff
} ovrpTiledMultiResLevel;
OVRP_EXPORT ovrpResult ovrp_GetTiledMultiResSupported(ovrpBool *outSupported);
OVRP_EXPORT ovrpResult ovrp_GetTiledMultiResLevel(ovrpTiledMultiResLevel *outLevel);
OVRP_EXPORT ovrpResult ovrp_SetTiledMultiResLevel(ovrpTiledMultiResLevel level);
OVRP_EXPORT ovrpResult ovrp_GetTiledMultiResDynamic(ovrpBool *outDynamic);
OVRP_EXPORT ovrpResult ovrp_SetTiledMultiResDynamic(ovrpBool isDynamic);
OVRP_EXPORT ovrpResult ovrp_GetGPUFrameTime(float *outGpuTimeMs);
/* Perf metrics. The game polls IsPerfMetricsSupported(metric) and, for supported
* metrics, GetPerfMetrics{Float,Int}(metric, &out). Signatures verified against
* libOVRPlugin.so (analysis/ovrp_decomp.txt): (uint metric, out-ptr). We answer per
* metric truthfully (see xr_runtime.c) rather than stubbing the whole call Unsupported. */
typedef enum {
ovrpPerfMetrics_App_CpuTime_Float = 0,
ovrpPerfMetrics_App_GpuTime_Float = 1,
ovrpPerfMetrics_Compositor_CpuTime_Float = 3,
ovrpPerfMetrics_Compositor_GpuTime_Float = 4,
ovrpPerfMetrics_Compositor_DroppedFrameCount_Int = 5,
ovrpPerfMetrics_System_GpuUtilPercentage_Float = 7,
ovrpPerfMetrics_System_CpuUtilAveragePercentage_Float= 8,
ovrpPerfMetrics_System_CpuUtilWorstPercentage_Float = 9,
ovrpPerfMetrics_Device_CpuClockFrequencyInMHz_Float = 10,
ovrpPerfMetrics_Device_GpuClockFrequencyInMHz_Float = 11,
ovrpPerfMetrics_Device_CpuClockLevel_Int = 12,
ovrpPerfMetrics_Device_GpuClockLevel_Int = 13,
ovrpPerfMetrics_Count = 14,
ovrpPerfMetrics_EnumSize = 0x7fffffff
} ovrpPerfMetrics;
OVRP_EXPORT ovrpResult ovrp_IsPerfMetricsSupported(ovrpPerfMetrics metric, ovrpBool *outSupported);
OVRP_EXPORT ovrpResult ovrp_GetPerfMetricsFloat(ovrpPerfMetrics metric, float *outValue);
OVRP_EXPORT ovrpResult ovrp_GetPerfMetricsInt(ovrpPerfMetrics metric, int *outValue);
/* TODO: remaining ~225 of the 239-function surface; see analysis/shim_surface.txt.
* Buckets (per SHIM-SCOPE.md): ~150 stub-to-constant (Media_, camera, perf,
* boundary, handtracking, system-getters), ~45 mechanical (rest of tracking,
* input, eye params), and the hard layer/swapchain set (SetupLayer,
* CalculateEyeLayerDesc2, GetLayerTexture2) which need the 128-byte
* ovrpLayerDesc reversed first. */
#ifdef __cplusplus
}
#endif
#endif /* OVRPLUGIN_SHIM_H */
+33
View File
@@ -0,0 +1,33 @@
#ifndef PASSTHRU_H
#define PASSTHRU_H
/* P4 passthru — run the REAL (SONAME-patched) libOVRPlugin inside our app and forward
* every game-called export to it, so we can LOG native's per-eye poses/FOV/submit and
* diff them against our clean-but-ghosting values. Gate: debug.re4vr.passthru=1.
*
* All-or-nothing: the real lib owns the WHOLE session, so every export the game calls
* must forward (partial forwarding crashes mid-frame because state lives in the real
* lib). Fully-prototyped exports (core.c/layers.c) forward via PT_FWD below; the handful
* of no-arg () stubs the game also hits forward via signature-agnostic asm trampolines
* in passthru.c. See HANDOFF-2026-06-27-native-parity.md. */
int pt_active(void); /* lazily inits on first call; 1 if real lib owns the session */
void *pt_real(const char *name); /* dlsym from the real lib (NULL if !passthru or not found) */
void pt_log_call(const char *name, long ret); /* rate-limited native call census (PTC log) */
/* Forward a fully-prototyped export to the real lib and return its result, caching the
* resolved pointer per call site. Drop as the FIRST statement of each game-called export
* in core.c / layers.c. Compiles to nothing reachable when passthru is off. Logs the
* native return value (rate-limited) so we can diff the full call surface vs our shim. */
#define PT_FWD(fn, ...) do { \
if (pt_active()) { \
static __typeof__(&fn) _pt_p; static int _pt_got; \
if (!_pt_got) { _pt_p = (__typeof__(&fn))pt_real(#fn); _pt_got = 1; } \
if (_pt_p) { \
__typeof__(_pt_p(__VA_ARGS__)) _pt_r = _pt_p(__VA_ARGS__); \
pt_log_call(#fn, (long)_pt_r); \
return _pt_r; \
} \
} \
} while (0)
#endif /* PASSTHRU_H */
+103
View File
@@ -0,0 +1,103 @@
/* android_init.c — Android OpenXR instance handshake.
*
* On Android the OpenXR loader must be primed with the JavaVM + a Context via
* xrInitializeLoaderKHR BEFORE xrCreateInstance, and xrCreateInstance needs
* XR_KHR_android_create_instance with XrInstanceCreateInfoAndroidKHR chained in.
*
* JavaVM: captured in JNI_OnLoad (called when the .so loads).
* Activity/Context: preferred from ovrp_Initialize5 arg4; but PreInitialize3
* creates the instance earlier, so we fall back to the Application context via
* ActivityThread reflection. [VERIFY-ON-HW] whether Meta's runtime accepts the
* Application context or wants the actual Activity.
*
* Host build: the #else stubs make this a no-op so xr_runtime.c is portable.
*/
#include "xr_runtime.h"
#ifdef __ANDROID__
#include <jni.h>
#define XR_USE_PLATFORM_ANDROID
#include <openxr/openxr_platform.h>
static JavaVM *g_vm;
static jobject g_context; /* global ref: Activity or Application context */
static XrInstanceCreateInfoAndroidKHR g_androidCreate;
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
(void)reserved;
g_vm = vm;
return JNI_VERSION_1_6;
}
/* P4 passthru: the real (dlopen'd) libOVRPlugin captures the JavaVM in its own
* JNI_OnLoad, which ART only invokes for System.loadLibrary — not for a native
* dlopen. passthru.c hands this VM to the real lib's JNI_OnLoad so its VrApi path
* doesn't deref a null VM in CompositorVRAPI::PreInitialize. */
void *xrr_android_get_vm(void) { return g_vm; }
static JNIEnv *get_env(void) {
JNIEnv *env = NULL;
if (!g_vm) return NULL;
if ((*g_vm)->GetEnv(g_vm, (void **)&env, JNI_VERSION_1_6) == JNI_OK) return env;
if ((*g_vm)->AttachCurrentThread(g_vm, &env, NULL) == JNI_OK) return env;
return NULL;
}
/* fallback: get the Application context via ActivityThread reflection */
static jobject get_app_context(JNIEnv *env) {
jclass at = (*env)->FindClass(env, "android/app/ActivityThread");
if (!at) return NULL;
jmethodID cur = (*env)->GetStaticMethodID(env, at,
"currentActivityThread", "()Landroid/app/ActivityThread;");
jobject atObj = (*env)->CallStaticObjectMethod(env, at, cur);
if (!atObj) return NULL;
jmethodID getApp = (*env)->GetMethodID(env, at,
"getApplication", "()Landroid/app/Application;");
jobject app = (*env)->CallObjectMethod(env, atObj, getApp);
return app ? (*env)->NewGlobalRef(env, app) : NULL;
}
static int g_realActivitySet;
void xrr_set_android_activity(void *activity) {
JNIEnv *env = get_env();
if (env && activity) {
g_context = (*env)->NewGlobalRef(env, (jobject)activity);
g_realActivitySet = 1; /* the real GameActivity, not the App context */
}
}
int xrr_android_have_real_activity(void) { return g_realActivitySet; }
static jobject ensure_context(void) {
if (g_context) return g_context;
JNIEnv *env = get_env();
if (env) g_context = get_app_context(env);
return g_context;
}
int xrr_android_init_loader(void) {
PFN_xrInitializeLoaderKHR init = NULL;
if (xrGetInstanceProcAddr(XR_NULL_HANDLE, "xrInitializeLoaderKHR",
(PFN_xrVoidFunction *)&init) != XR_SUCCESS || !init)
return 0;
XrLoaderInitInfoAndroidKHR li = { XR_TYPE_LOADER_INIT_INFO_ANDROID_KHR };
li.applicationVM = g_vm;
li.applicationContext = ensure_context();
return XR_SUCCEEDED(init((XrLoaderInitInfoBaseHeaderKHR *)&li));
}
void *xrr_android_instance_next(void) {
g_androidCreate = (XrInstanceCreateInfoAndroidKHR){
XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR };
g_androidCreate.applicationVM = g_vm;
g_androidCreate.applicationActivity = ensure_context();
return &g_androidCreate;
}
#else /* ----- host build: no-ops ----- */
int xrr_android_init_loader(void) { return 1; }
void *xrr_android_instance_next(void) { return 0; }
void xrr_set_android_activity(void *a) { (void)a; }
int xrr_android_have_real_activity(void) { return 0; }
void *xrr_android_get_vm(void) { return 0; }
#endif
+363
View File
@@ -0,0 +1,363 @@
/* core.c — the ovrp_* exports prototyped with real signatures in ovrplugin_shim.h:
* lifecycle, frame loop, poses, tracking/perf config — all wired to OpenXR via xr_runtime.c.
* (Session + Vulkan graphics binding are created in ovrp_Initialize5 -> xrr_init.)
* Each export forwards to the real libOVRPlugin first when passthru is active (PT_FWD).
*/
#include "ovrplugin_shim.h"
#include "xr_runtime.h"
#include "passthru.h"
#include "log.h"
#include <string.h>
#ifdef __ANDROID__
#include <sys/system_properties.h>
#endif
#define TODO_RET ovrpFailure_NotYetImplemented
/* --- lifecycle ---------------------------------------------------------- */
OVRP_EXPORT ovrpResult ovrp_PreInitialize3(void *garbage) {
PT_FWD(ovrp_PreInitialize3, garbage); /* first pt_active() lazily loads the real lib */
(void)garbage;
XRRLOG("ovrp_PreInitialize3 called");
ovrpResult r = xrr_pre_init();
XRRLOG("ovrp_PreInitialize3 -> %d", r);
return r;
}
OVRP_EXPORT ovrpResult ovrp_Initialize5(
ovrpRenderAPIType apiType, ovrpLogCallback logCallback, void *activity,
void *vkInstance, void *vkPhysicalDevice, void *vkDevice, void *queue,
unsigned int flags, const void *version) { /* version = const ovrpVersion& (ptr) */
PT_FWD(ovrp_Initialize5, apiType, logCallback, activity, vkInstance,
vkPhysicalDevice, vkDevice, queue, flags, version);
(void)apiType; (void)logCallback; (void)flags; (void)version;
XRRLOG("ovrp_Initialize5: api=%d act=%p vkInst=%p vkPhys=%p vkDev=%p queue=%p fl=%u",
apiType, activity, vkInstance, vkPhysicalDevice, vkDevice, queue, flags);
xrr_set_android_activity(activity);
xrr_vk_set_handles(vkDevice, queue, 0); /* for the end-of-frame flush barrier */
/* OpenXR wants queueFamilyIndex+queueIndex; OVRPlugin gives a VkQueue we can't
* decompose -> default family 0/index 0 (UE Vulkan on Quest uses graphics fam 0). */
ovrpResult r = xrr_init(vkInstance, vkPhysicalDevice, vkDevice, 0);
XRRLOG("ovrp_Initialize5 -> %d", r);
return r;
}
OVRP_EXPORT ovrpResult ovrp_Shutdown2(void) {
PT_FWD(ovrp_Shutdown2); xrr_shutdown(); return ovrpSuccess; }
/* --- frame loop --------------------------------------------------------- */
OVRP_EXPORT ovrpResult ovrp_Update3(ovrpStep step, int frameIndex, double predictedTime) {
PT_FWD(ovrp_Update3, step, frameIndex, predictedTime);
(void)step; (void)frameIndex; (void)predictedTime;
static int o; if (!o) { o = 1; XRRLOG("ovrp_Update3 first call"); }
xrr_poll_events(); /* advance the session state machine */
return ovrpSuccess;
}
OVRP_EXPORT ovrpResult ovrp_WaitToBeginFrame(int frameIndex) {
PT_FWD(ovrp_WaitToBeginFrame, frameIndex);
ovrpResult r = xrr_wait_frame(frameIndex);
static int o; if (!o) { o = 1; XRRLOG("ovrp_WaitToBeginFrame first call -> %d", r); }
return r;
}
OVRP_EXPORT ovrpResult ovrp_BeginFrame4(int frameIndex, void *commandQueue) {
PT_FWD(ovrp_BeginFrame4, frameIndex, commandQueue);
(void)commandQueue;
return xrr_begin_frame(frameIndex);
}
OVRP_EXPORT ovrpResult ovrp_EndFrame4(int frameIndex,
const ovrpLayerSubmit *const *layerSubmitPtr, int layerSubmitCount,
void *commandQueue) {
if (pt_active()) {
static __typeof__(&ovrp_EndFrame4) _r; static int _g;
if (!_g) { _r = (__typeof__(&ovrp_EndFrame4))pt_real("ovrp_EndFrame4"); _g = 1; }
if (_r) {
/* native per-layer submit: id, head-lock flag, world pose — diff vs ours */
static int po = 0;
if (po++ < 24 && layerSubmitPtr) {
for (int i = 0; i < layerSubmitCount; i++) {
const ovrpLayerSubmit *L = layerSubmitPtr[i];
if (!L) continue;
XRRLOG("PT EndFrame4 f=%d layer[%d/%d] id=%d flags=0x%x pos=(%.4f %.4f %.4f) quat=(%.4f %.4f %.4f %.4f)",
frameIndex, i, layerSubmitCount, L->LayerId, L->LayerSubmitFlags,
L->Pose.Position.x, L->Pose.Position.y, L->Pose.Position.z,
L->Pose.Orientation.x, L->Pose.Orientation.y,
L->Pose.Orientation.z, L->Pose.Orientation.w);
}
}
return _r(frameIndex, layerSubmitPtr, layerSubmitCount, commandQueue);
}
}
(void)commandQueue;
static int o; if (!o) { o = 1; XRRLOG("ovrp_EndFrame4 first call (layers=%d)", layerSubmitCount); }
return xrr_end_frame(frameIndex, layerSubmitPtr, layerSubmitCount);
}
OVRP_EXPORT ovrpResult ovrp_GetPredictedDisplayTime(int frameIndex, double *outTime) {
PT_FWD(ovrp_GetPredictedDisplayTime, frameIndex, outTime);
(void)frameIndex;
if (!outTime) return ovrpFailure_InvalidParameter;
*outTime = xrr_predicted_display_time_s();
return ovrpSuccess;
}
/* --- tracking / input --------------------------------------------------- */
OVRP_EXPORT ovrpResult ovrp_GetNodePoseState3(ovrpStep step, int frameIndex,
ovrpNode nodeId, ovrpPoseStatef *outState) {
if (pt_active()) {
static __typeof__(&ovrp_GetNodePoseState3) _r; static int _g;
if (!_g) { _r = (__typeof__(&ovrp_GetNodePoseState3))pt_real("ovrp_GetNodePoseState3"); _g = 1; }
if (_r) {
ovrpResult rr = _r(step, frameIndex, nodeId, outState);
/* native eye poses — the ghost diff target (cf. our STEREO/HEADvsEYE logs) */
if (outState && (nodeId == ovrpNode_EyeLeft || nodeId == ovrpNode_EyeRight)) {
static int po = 0;
if (po++ < 16)
XRRLOG("PT NodePoseState3 eye=%d pos=(%.4f %.4f %.4f) quat=(%.4f %.4f %.4f %.4f)",
(int)nodeId, outState->Pose.Position.x, outState->Pose.Position.y,
outState->Pose.Position.z, outState->Pose.Orientation.x,
outState->Pose.Orientation.y, outState->Pose.Orientation.z,
outState->Pose.Orientation.w);
}
return rr;
}
}
(void)step; (void)frameIndex;
return xrr_get_node_pose(nodeId, outState);
}
OVRP_EXPORT ovrpResult ovrp_GetNodePoseStateRaw(ovrpStep step, int frameIndex,
ovrpNode nodeId, ovrpPoseStatef *outState) {
PT_FWD(ovrp_GetNodePoseStateRaw, step, frameIndex, nodeId, outState);
(void)step; (void)frameIndex;
return xrr_get_node_pose(nodeId, outState);
}
OVRP_EXPORT ovrpResult ovrp_GetControllerState4(ovrpController controllerMask,
ovrpControllerState4 *outState) {
PT_FWD(ovrp_GetControllerState4, controllerMask, outState);
if (!outState) return ovrpFailure_InvalidParameter;
xrr_get_controller_state(controllerMask, outState);
return ovrpSuccess;
}
/* node presence/validity — the game gates controller-pose queries on these */
OVRP_EXPORT ovrpResult ovrp_SetControllerVibration2(ovrpController mask,
float frequency, float amplitude) {
PT_FWD(ovrp_SetControllerVibration2, mask, frequency, amplitude);
xrr_set_vibration(mask, frequency, amplitude);
return ovrpSuccess;
}
OVRP_EXPORT ovrpResult ovrp_GetNodePresent2(ovrpNode node, ovrpBool *out) {
PT_FWD(ovrp_GetNodePresent2, node, out);
if (out) *out = xrr_node_present(node) ? ovrpBool_True : ovrpBool_False;
return ovrpSuccess;
}
OVRP_EXPORT ovrpResult ovrp_GetNodeOrientationValid(ovrpNode node, ovrpBool *out) {
PT_FWD(ovrp_GetNodeOrientationValid, node, out);
if (out) *out = xrr_node_valid(node) ? ovrpBool_True : ovrpBool_False;
return ovrpSuccess;
}
OVRP_EXPORT ovrpResult ovrp_GetNodePositionValid(ovrpNode node, ovrpBool *out) {
PT_FWD(ovrp_GetNodePositionValid, node, out);
if (out) *out = xrr_node_valid(node) ? ovrpBool_True : ovrpBool_False;
return ovrpSuccess;
}
OVRP_EXPORT ovrpResult ovrp_GetNodeOrientationTracked2(ovrpNode node, ovrpBool *out) {
PT_FWD(ovrp_GetNodeOrientationTracked2, node, out);
if (out) *out = xrr_node_valid(node) ? ovrpBool_True : ovrpBool_False;
return ovrpSuccess;
}
OVRP_EXPORT ovrpResult ovrp_GetNodePositionTracked2(ovrpNode node, ovrpBool *out) {
PT_FWD(ovrp_GetNodePositionTracked2, node, out);
if (out) *out = xrr_node_valid(node) ? ovrpBool_True : ovrpBool_False;
return ovrpSuccess;
}
/* --- system / tracking config ------------------------------------------ */
OVRP_EXPORT ovrpResult ovrp_GetSystemHeadsetType2(ovrpSystemHeadset *outType) {
PT_FWD(ovrp_GetSystemHeadsetType2, outType);
if (outType) *outType = ovrpSystemHeadset_Oculus_Quest_2;
return ovrpSuccess;
}
/* [VERIFIED from UE OculusHMD.cpp + disasm] ovrp_GetInitialized takes NO args and
* returns ovrpBool DIRECTLY. My earlier (ovrpBool* out) signature wrote into x0
* (which still held `this`), corrupting FOculusHMD's vtable -> null virtual crash.
* Return false until the SESSION exists so FOculusHMD::InitDevice proceeds into
* InitializeSession() (which calls ovrp_Initialize5 to create it). */
OVRP_EXPORT ovrpBool ovrp_GetInitialized(void) {
PT_FWD(ovrp_GetInitialized);
/* called every frame, many times — don't log (it rolls the logcat buffer) */
return (g_xr.session != XR_NULL_HANDLE) ? ovrpBool_True : ovrpBool_False;
}
OVRP_EXPORT ovrpResult ovrp_GetSystemDisplayFrequency2(float *outFreq) {
PT_FWD(ovrp_GetSystemDisplayFrequency2, outFreq);
if (outFreq) *outFreq = 72.0f; /* Quest 2 default refresh */
return ovrpSuccess;
}
/* per-frame app-state getters the loading loop polls (all (ovrpBool* out)->result) */
OVRP_EXPORT ovrpResult ovrp_GetAppHasVrFocus2(ovrpBool *out) {
PT_FWD(ovrp_GetAppHasVrFocus2, out);
if (out) *out = ovrpBool_True; return ovrpSuccess; /* we have focus */
}
OVRP_EXPORT ovrpResult ovrp_GetAppShouldQuit2(ovrpBool *out) {
PT_FWD(ovrp_GetAppShouldQuit2, out);
if (out) *out = ovrpBool_False; return ovrpSuccess;
}
OVRP_EXPORT ovrpResult ovrp_GetUserPresent2(ovrpBool *out) {
PT_FWD(ovrp_GetUserPresent2, out);
if (out) *out = ovrpBool_True; return ovrpSuccess; /* headset worn */
}
OVRP_EXPORT ovrpResult ovrp_GetAppShouldRecenter2(ovrpBool *out) {
PT_FWD(ovrp_GetAppShouldRecenter2, out);
if (out) *out = ovrpBool_False; return ovrpSuccess;
}
OVRP_EXPORT ovrpResult ovrp_GetAppShouldRecreateDistortionWindow2(ovrpBool *out) {
PT_FWD(ovrp_GetAppShouldRecreateDistortionWindow2, out);
if (out) *out = ovrpBool_False; return ovrpSuccess;
}
OVRP_EXPORT ovrpResult ovrp_GetSystemMultiViewSupported2(ovrpBool *out) {
PT_FWD(ovrp_GetSystemMultiViewSupported2, out);
if (out) *out = ovrpBool_True; return ovrpSuccess; /* Quest supports multiview */
}
OVRP_EXPORT ovrpResult ovrp_GetAppHasInputFocus(ovrpBool *out) {
PT_FWD(ovrp_GetAppHasInputFocus, out);
if (out) *out = ovrpBool_True; return ovrpSuccess;
}
/* the OpenXR session IS our display; nothing extra to set up */
OVRP_EXPORT ovrpResult ovrp_SetupDistortionWindow3(unsigned int flags) {
PT_FWD(ovrp_SetupDistortionWindow3, flags);
(void)flags; return ovrpSuccess;
}
/* like GetInitialized, this returns ovrpBool DIRECTLY (no out-param) */
OVRP_EXPORT ovrpBool ovrp_GetMixedRealityInitialized(void) {
PT_FWD(ovrp_GetMixedRealityInitialized);
return ovrpBool_False; /* MR not used */
}
/* [VERIFIED: UE builds the eye projection matrix from this FOV — OculusHMD.cpp
* line ~2369 uses Fov.{Left,Right,Up,Down}Tan]. Must match the FOV we composite
* with (g_xr.views[].fov), or the image is reprojected to the wrong frustum. */
OVRP_EXPORT ovrpResult ovrp_GetNodeFrustum2(ovrpNode node, ovrpFrustum2f *out) {
if (pt_active()) {
static __typeof__(&ovrp_GetNodeFrustum2) _r; static int _g;
if (!_g) { _r = (__typeof__(&ovrp_GetNodeFrustum2))pt_real("ovrp_GetNodeFrustum2"); _g = 1; }
if (_r) {
ovrpResult rr = _r(node, out);
if (out) { static int po = 0;
if (po++ < 8)
XRRLOG("PT GetNodeFrustum2 node=%d -> Fov U=%.3f D=%.3f L=%.3f R=%.3f zN=%.3f zF=%.1f",
node, out->Fov.UpTan, out->Fov.DownTan, out->Fov.LeftTan,
out->Fov.RightTan, out->zNear, out->zFar);
}
return rr;
}
}
if (!out) return ovrpFailure_InvalidParameter;
int eye = (node == ovrpNode_EyeRight) ? 1 : 0; /* EyeLeft/EyeCenter -> 0 */
out->zNear = 0.01f;
out->zFar = 1000.0f;
xrr_eye_fov_tangents(eye, &out->Fov.UpTan, &out->Fov.DownTan,
&out->Fov.LeftTan, &out->Fov.RightTan);
static int o = 0;
if (o++ < 4) XRRLOG("GetNodeFrustum2 node=%d -> Fov U=%.3f D=%.3f L=%.3f R=%.3f",
node, out->Fov.UpTan, out->Fov.DownTan, out->Fov.LeftTan, out->Fov.RightTan);
return ovrpSuccess;
}
OVRP_EXPORT ovrpResult ovrp_GetTrackingOriginType2(ovrpTrackingOrigin *outOrigin) {
PT_FWD(ovrp_GetTrackingOriginType2, outOrigin);
if (outOrigin)
*outOrigin = xrr_get_tracking_origin() ? ovrpTrackingOrigin_FloorLevel
: ovrpTrackingOrigin_EyeLevel;
return ovrpSuccess;
}
OVRP_EXPORT ovrpResult ovrp_SetTrackingOriginType2(ovrpTrackingOrigin origin) {
PT_FWD(ovrp_SetTrackingOriginType2, origin);
/* roomscale games request FloorLevel/Stage; honor it so the floor height (and
* thus body-anchored inventory) is correct. */
XRRLOG("ovrp_SetTrackingOriginType2(%d) -> %s", (int)origin,
origin != ovrpTrackingOrigin_EyeLevel ? "floor" : "eye");
xrr_set_tracking_origin(origin != ovrpTrackingOrigin_EyeLevel);
return ovrpSuccess;
}
OVRP_EXPORT ovrpResult ovrp_RecenterTrackingOrigin2(unsigned int flags) {
PT_FWD(ovrp_RecenterTrackingOrigin2, flags);
(void)flags; return ovrpSuccess;
}
/* Perf levels: the game asks for CPU/GPU clock levels under load. We used to no-op
* these, letting the device underclock while CPU-bound -> frame drops. Forward them
* to XR_EXT_performance_settings so clocks boost. (real sig: one int level) */
OVRP_EXPORT ovrpResult ovrp_SetSystemCpuLevel2(int level) {
PT_FWD(ovrp_SetSystemCpuLevel2, level);
xrr_set_perf_level(0, level); return ovrpSuccess;
}
OVRP_EXPORT ovrpResult ovrp_SetSystemGpuLevel2(int level) {
PT_FWD(ovrp_SetSystemGpuLevel2, level);
xrr_set_perf_level(1, level); return ovrpSuccess;
}
/* Dynamic perf: tiled multi-res (FFR) + GPU frame time. Previously stubbed as
* Unsupported, which disabled RE4's own GPU-load mitigation. We now report FFR
* supported, forward the game's Set* requests onto OpenXR foveation (xr_runtime.c),
* and feed GetGPUFrameTime so the game's scaler has an input. */
OVRP_EXPORT ovrpResult ovrp_GetTiledMultiResSupported(ovrpBool *outSupported) {
PT_FWD(ovrp_GetTiledMultiResSupported, outSupported);
if (!outSupported) return ovrpFailure_InvalidParameter;
*outSupported = xrr_foveation_supported() ? ovrpBool_True : ovrpBool_False;
return ovrpSuccess;
}
OVRP_EXPORT ovrpResult ovrp_GetTiledMultiResLevel(ovrpTiledMultiResLevel *outLevel) {
PT_FWD(ovrp_GetTiledMultiResLevel, outLevel);
if (!outLevel) return ovrpFailure_InvalidParameter;
*outLevel = (ovrpTiledMultiResLevel)xrr_get_tiled_multires_level();
return ovrpSuccess;
}
OVRP_EXPORT ovrpResult ovrp_SetTiledMultiResLevel(ovrpTiledMultiResLevel level) {
PT_FWD(ovrp_SetTiledMultiResLevel, level);
xrr_set_tiled_multires_level((int)level);
return ovrpSuccess;
}
OVRP_EXPORT ovrpResult ovrp_GetTiledMultiResDynamic(ovrpBool *outDynamic) {
PT_FWD(ovrp_GetTiledMultiResDynamic, outDynamic);
if (!outDynamic) return ovrpFailure_InvalidParameter;
*outDynamic = xrr_get_tiled_multires_dynamic() ? ovrpBool_True : ovrpBool_False;
return ovrpSuccess;
}
OVRP_EXPORT ovrpResult ovrp_SetTiledMultiResDynamic(ovrpBool isDynamic) {
PT_FWD(ovrp_SetTiledMultiResDynamic, isDynamic);
xrr_set_tiled_multires_dynamic(isDynamic != ovrpBool_False);
return ovrpSuccess;
}
OVRP_EXPORT ovrpResult ovrp_GetGPUFrameTime(float *outGpuTimeMs) {
PT_FWD(ovrp_GetGPUFrameTime, outGpuTimeMs);
if (!outGpuTimeMs) return ovrpFailure_InvalidParameter;
*outGpuTimeMs = xrr_gpu_frame_time_ms();
return ovrpSuccess;
}
/* Lever A — dynamic resolution. Previously stubbed Unsupported, leaving the game's preset
* scale=1.0 (full res always). We return scale<1 under GPU pressure; the game self-downscales
* its eye buffer (resolution = baseDensity * sqrt(scale)). See xr_runtime.c / game-perf-RE.md. */
OVRP_EXPORT ovrpResult ovrp_GetAdaptiveGpuPerformanceScale2(float *outScale) {
PT_FWD(ovrp_GetAdaptiveGpuPerformanceScale2, outScale);
if (!outScale) return ovrpFailure_InvalidParameter;
*outScale = xrr_adaptive_gpu_scale();
return ovrpSuccess;
}
/* Perf metrics: per-metric support + read. Previously the whole call returned
* Unsupported, which the game (correctly) treats as "no perf metrics at all" and may
* gate its adaptive systems on. We answer per metric truthfully instead. */
OVRP_EXPORT ovrpResult ovrp_IsPerfMetricsSupported(ovrpPerfMetrics metric, ovrpBool *outSupported) {
PT_FWD(ovrp_IsPerfMetricsSupported, metric, outSupported);
if (!outSupported) return ovrpFailure_InvalidParameter;
*outSupported = xrr_perf_metric_supported((int)metric) ? ovrpBool_True : ovrpBool_False;
return ovrpSuccess;
}
OVRP_EXPORT ovrpResult ovrp_GetPerfMetricsFloat(ovrpPerfMetrics metric, float *outValue) {
PT_FWD(ovrp_GetPerfMetricsFloat, metric, outValue);
if (!outValue) return ovrpFailure_InvalidParameter;
return xrr_perf_metric_float((int)metric, outValue) ? ovrpSuccess : ovrpFailure_Unsupported;
}
OVRP_EXPORT ovrpResult ovrp_GetPerfMetricsInt(ovrpPerfMetrics metric, int *outValue) {
PT_FWD(ovrp_GetPerfMetricsInt, metric, outValue);
if (!outValue) return ovrpFailure_InvalidParameter;
return xrr_perf_metric_int((int)metric, outValue) ? ovrpSuccess : ovrpFailure_Unsupported;
}
+173
View File
@@ -0,0 +1,173 @@
/* layers.c — ovrp_* layer/swapchain functions, backed by xr_runtime swapchains.
* SetupLayer creates an XrSwapchain from the (reversed) ovrpLayerDesc; the app
* gets the per-stage VkImage handles via GetLayerTexture2 and wraps them in its
* RHI. Depth swapchains + foveation (FFR) are created in xr_runtime's setup_layer.
*/
#include "ovrplugin_shim.h"
#include "xr_runtime.h"
#include "passthru.h"
#include "log.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
/* [VERIFIED arg order from UE OculusHMD.cpp]: (layout, textureScale, mipLevels,
* sampleCount, colorFormat, depthFormat, layerFlags, out). Fills the EyeFov layer
* desc UE uses to create the eye swapchain (-> ovrp_SetupLayer). */
OVRP_EXPORT ovrpResult ovrp_CalculateEyeLayerDesc2(
ovrpLayout layout, float textureScale, int mipLevels, int sampleCount,
ovrpTextureFormat colorFormat, ovrpTextureFormat depthFormat,
int layerFlags, ovrpLayerDesc *out) {
if (pt_active()) {
static __typeof__(&ovrp_CalculateEyeLayerDesc2) _r; static int _g;
if (!_g) { _r = (__typeof__(&ovrp_CalculateEyeLayerDesc2))pt_real("ovrp_CalculateEyeLayerDesc2"); _g = 1; }
if (_r) {
ovrpResult rr = _r(layout, textureScale, mipLevels, sampleCount,
colorFormat, depthFormat, layerFlags, out);
if (out) { static int po = 0;
if (po++ < 4)
XRRLOG("PT EyeLayerDesc2 %dx%d FovL(U%.3f D%.3f L%.3f R%.3f) FovR(U%.3f D%.3f L%.3f R%.3f)",
out->TextureSize.w, out->TextureSize.h,
out->Fov[0].UpTan, out->Fov[0].DownTan, out->Fov[0].LeftTan, out->Fov[0].RightTan,
out->Fov[1].UpTan, out->Fov[1].DownTan, out->Fov[1].LeftTan, out->Fov[1].RightTan);
}
return rr;
}
}
if (!out) return ovrpFailure_InvalidParameter;
uint32_t w, h;
xrr_recommended_eye_size(&w, &h);
/* UE asks ~1.2x supersample (1728x1900). debug.re4vr.sscap=1 caps it to 1.0x to
* free GPU/bandwidth (the copy-ring copies fewer pixels too); the ghosting is a
* frame-drop/serialization issue not pixel count, but this is a cheap lever to
* stack with the perf/copy-ring fixes. Default off (full res for sharpness). */
if (textureScale > 1.0f) {
static int cap = -1;
if (cap < 0) {
#ifdef __ANDROID__
char s[92] = {0};
extern int __system_property_get(const char*, char*);
cap = (__system_property_get("debug.re4vr.sscap", s) > 0 && s[0] == '1') ? 1 : 0;
#else
cap = 0;
#endif
XRRLOG("supersample cap: %s (debug.re4vr.sscap), UE asked scale=%.2f", cap ? "1.0x" : "off", textureScale);
}
if (cap) textureScale = 1.0f;
}
if (textureScale > 0.0f) { w = (uint32_t)(w * textureScale); h = (uint32_t)(h * textureScale); }
/* Aggressive resolution lever (debug.re4vr.resscale = percent of the size above;
* default 100). Below 100 shrinks the eye buffer further to keep UE under GPU budget
* so it stops TRUNCATING frames -> black (the load-gated UE frame-drop; RenderDoc:
* 28 draws vs ~198 normal, resolved eye = pure black). Read once at layer setup. */
{
static int rs = -1;
if (rs < 0) {
#ifdef __ANDROID__
char s[92] = {0};
extern int __system_property_get(const char*, char*);
rs = (__system_property_get("debug.re4vr.resscale", s) > 0) ? atoi(s) : 100;
#else
rs = 100;
#endif
if (rs < 25) rs = 25;
if (rs > 200) rs = 200;
XRRLOG("resscale: %d%% (debug.re4vr.resscale)", rs);
}
if (rs != 100) { w = (uint32_t)((uint64_t)w * rs / 100); h = (uint32_t)((uint64_t)h * rs / 100); }
}
memset(out, 0, sizeof(*out));
out->Shape = ovrpShape_EyeFov;
out->Layout = layout;
out->TextureSize.w = (int)w;
out->TextureSize.h = (int)h;
out->MipLevels = mipLevels > 0 ? mipLevels : 1;
out->SampleCount = sampleCount > 0 ? sampleCount : 1;
out->Format = colorFormat;
out->LayerFlags = layerFlags;
for (int e = 0; e < 2; e++) {
/* real per-eye FOV so the layer desc matches GetNodeFrustum2 + our submit */
xrr_eye_fov_tangents(e, &out->Fov[e].UpTan, &out->Fov[e].DownTan,
&out->Fov[e].LeftTan, &out->Fov[e].RightTan);
out->VisibleRect[e].Pos.x = 0.0f; out->VisibleRect[e].Pos.y = 0.0f;
out->VisibleRect[e].Size.w = (float)w; out->VisibleRect[e].Size.h = (float)h;
}
out->MaxViewportSize.w = (int)w;
out->MaxViewportSize.h = (int)h;
out->DepthFormat = depthFormat;
{ /* log only when the parameters change — called every frame otherwise */
static int pw, ph, pl = -1, pf = -1;
if ((int)layout != pl || (int)w != pw || (int)h != ph || (int)colorFormat != pf) {
XRRLOG("CalculateEyeLayerDesc2: layout=%d %ux%u fmt=%d depth=%d flags=%d",
layout, w, h, colorFormat, depthFormat, layerFlags);
pl = layout; pw = (int)w; ph = (int)h; pf = colorFormat;
}
}
return ovrpSuccess;
}
/* [from UE: CalculateEyeViewportRect(EyeLayerDesc, eye, scale, &vpRect)]. desc is
* passed by value (>16B -> indirect, so a pointer in x0). Array layout: each eye
* is a full array layer, so the viewport is the full (scaled) texture rect. */
OVRP_EXPORT ovrpResult ovrp_CalculateEyeViewportRect(const ovrpLayerDesc *desc,
int eye, float scale, ovrpRecti *out) {
PT_FWD(ovrp_CalculateEyeViewportRect, desc, eye, scale, out);
(void)eye;
if (!desc || !out) return ovrpFailure_InvalidParameter;
out->Pos.x = 0; out->Pos.y = 0;
out->Size.w = (int)(desc->TextureSize.w * scale);
out->Size.h = (int)(desc->TextureSize.h * scale);
return ovrpSuccess;
}
/* [from OVR_Plugin.h] non-eye layer desc (quad/cylinder/etc, e.g. the splash).
* textureSize is const-ref => a pointer. Fills the common header; UE fills the
* shape-specific tail. Stubbing this left the desc zeroed -> SetupLayer 0x0 -> fail. */
OVRP_EXPORT ovrpResult ovrp_CalculateLayerDesc(ovrpShape shape, ovrpLayout layout,
const ovrpSizei *textureSize, int mipLevels, int sampleCount,
ovrpTextureFormat format, int layerFlags, ovrpLayerDesc *out) {
PT_FWD(ovrp_CalculateLayerDesc, shape, layout, textureSize, mipLevels, sampleCount, format, layerFlags, out);
if (!out || !textureSize) return ovrpFailure_InvalidParameter;
out->Shape = shape;
out->Layout = layout;
out->TextureSize = *textureSize;
out->MipLevels = mipLevels > 0 ? mipLevels : 1;
out->SampleCount = sampleCount > 0 ? sampleCount : 1;
out->Format = format;
out->LayerFlags = layerFlags;
XRRLOG("CalculateLayerDesc: shape=%d layout=%d %dx%d fmt=%d",
shape, layout, textureSize->w, textureSize->h, format);
return ovrpSuccess;
}
OVRP_EXPORT ovrpResult ovrp_SetupLayer(void *device, ovrpLayerDesc *desc,
int *outLayerId) {
PT_FWD(ovrp_SetupLayer, device, desc, outLayerId);
(void)device; /* the Vulkan device is already bound to the XrSession */
return xrr_setup_layer(desc, outLayerId);
}
/* [VERIFIED from UE OculusHMD.cpp: FOculusHMD::DestroyLayer(uint32 LayerId)] */
OVRP_EXPORT ovrpResult ovrp_DestroyLayer(int layerId) {
PT_FWD(ovrp_DestroyLayer, layerId);
xrr_destroy_layer(layerId);
return ovrpSuccess;
}
OVRP_EXPORT ovrpResult ovrp_GetLayerTextureStageCount(int layerId, int *outCount) {
PT_FWD(ovrp_GetLayerTextureStageCount, layerId, outCount);
if (!outCount) return ovrpFailure_InvalidParameter;
*outCount = xrr_layer_stage_count(layerId);
return ovrpSuccess;
}
/* (layerId, stage, eyeId, &colorTexHandle, &depthTexHandle); either out may be
* NULL. Handles are VkImage as uint64 [VERIFIED 5-arg shape from decompile]. */
OVRP_EXPORT ovrpResult ovrp_GetLayerTexture2(int layerId, int stage, int eyeId,
uint64_t *outColorTex, uint64_t *outDepthTex) {
PT_FWD(ovrp_GetLayerTexture2, layerId, stage, eyeId, outColorTex, outDepthTex);
if (!outColorTex && !outDepthTex) return ovrpFailure_InvalidParameter;
return xrr_get_layer_texture(layerId, stage, eyeId, outColorTex, outDepthTex);
}
+14
View File
@@ -0,0 +1,14 @@
/* log.h — XRRLOG goes to logcat (tag "xrr") on Android, stderr on host.
* grep it on-device: adb logcat | grep -i xrr */
#ifndef XRR_LOG_H
#define XRR_LOG_H
#ifdef __ANDROID__
#include <android/log.h>
#define XRRLOG(...) __android_log_print(ANDROID_LOG_INFO, "xrr", __VA_ARGS__)
#define XRRERR(...) __android_log_print(ANDROID_LOG_ERROR, "xrr", __VA_ARGS__)
#else
#include <stdio.h>
#define XRRLOG(...) do { fprintf(stderr, "[xrr] " __VA_ARGS__); fputc('\n', stderr); } while (0)
#define XRRERR(...) do { fprintf(stderr, "[xrr] ERR " __VA_ARGS__); fputc('\n', stderr); } while (0)
#endif
#endif
+128
View File
@@ -0,0 +1,128 @@
/* passthru.c — P4 native-parity forwarding. See passthru.h. Loads the real, SONAME-
* patched libOVRPlugin_real.so (deps incl. libvrapi ride along in the APK) and hands
* the whole OVRPlugin session to it when debug.re4vr.passthru=1, so we can capture
* native's per-eye poses/FOV/submit for the ghost diff. */
#include "ovrplugin_shim.h"
#include "passthru.h"
#include "xr_runtime.h"
#include "log.h"
#include <dlfcn.h>
#ifdef __ANDROID__
#include <sys/system_properties.h>
#include <jni.h>
#endif
/* The asm trampolines read this flag directly; hidden visibility keeps the load a
* direct adrp/ldr (no GOT, no interposition). */
__attribute__((visibility("hidden"))) int g_pt_active = 0;
static void *g_pt_handle = 0;
/* ---- signature-agnostic stub forwarders --------------------------------------------
* A few exports the game calls live only as no-arg () stubs in stubs.c (two aren't even
* in OVR_Plugin.h, so we have no C signature to forward through). A naked arm64 tail-call
* trampoline forwards them verbatim: it never touches the arg registers (x0-x7/v0-v7/x8),
* so it works for ANY signature. Passthru off (or symbol missing) -> falls through to the
* SAME return constant the original autogen stub used (non-passthru behavior unchanged). */
#if defined(__aarch64__)
#define PT_STUB(N, CONST) \
__attribute__((visibility("hidden"))) void *g_pt_real_##N = 0; \
__attribute__((naked, visibility("default"))) ovrpResult ovrp_##N(void) { \
__asm__ volatile( \
"adrp x16, g_pt_active\n\t" \
"ldr w17, [x16, :lo12:g_pt_active]\n\t" \
"cbz w17, 1f\n\t" \
"adrp x16, g_pt_real_" #N "\n\t" \
"ldr x16, [x16, :lo12:g_pt_real_" #N "]\n\t" \
"cbz x16, 1f\n\t" \
"br x16\n\t" \
"1:\n\t" \
"mov w0, #" #CONST "\n\t" \
"ret\n\t"); \
}
#else /* host validation build: plain stub, no forwarding */
#define PT_STUB(N, CONST) \
void *g_pt_real_##N = 0; \
ovrpResult ovrp_##N(void) { return (ovrpResult)(CONST); }
#endif
/* The game-called () stubs (confirmed in device log). CONST = the constant the original
* autogen stub returned -> non-passthru path is byte-identical. These names are removed
* from stubs.c via gen_stubs.sh HEADER_FNS so there's no duplicate symbol. */
PT_STUB(DestroyDistortionWindow2, 0) /* ovrpSuccess */
PT_STUB(SetupDisplayObjects2, -1005) /* ovrpFailure_NotYetImplemented */
PT_STUB(SetReorientHMDOnControllerRecenter, 0)
PT_STUB(SetClientColorDesc, 0)
PT_STUB(SetAppEngineInfo2, 0)
PT_STUB(SetAppCPUPriority2, 0)
PT_STUB(InitializeMixedReality, -1005)
PT_STUB(GetViewportStencil, -1005)
PT_STUB(GetSystemRecommendedMSAALevel2, -1005)
PT_STUB(GetLocalTrackingSpaceRecenterCount, -1005)
PT_STUB(GetLayerTextureFoveation, -1005)
PT_STUB(GetControllerHapticsDesc2, -1005)
static void pt_first_use(void) {
static int done = 0;
if (done) return;
done = 1;
#ifdef __ANDROID__
char s[PROP_VALUE_MAX] = {0};
if (__system_property_get("debug.re4vr.passthru", s) <= 0 || s[0] != '1') return;
void *h = dlopen("libOVRPlugin_real.so", RTLD_NOW | RTLD_LOCAL);
if (!h) { XRRLOG("PASSTHRU: dlopen real libOVRPlugin FAILED: %s", dlerror()); return; }
g_pt_handle = h;
/* ART only calls JNI_OnLoad for System.loadLibrary, not a native dlopen — so the
* real lib's cached JavaVM is null and its VrApi PreInitialize derefs it (SIGSEGV in
* CompositorVRAPI::PreInitialize). Replicate the loadLibrary handshake by hand. */
jint (*real_jni_onload)(JavaVM *, void *) =
(jint (*)(JavaVM *, void *))dlsym(h, "JNI_OnLoad");
JavaVM *vm = (JavaVM *)xrr_android_get_vm();
if (real_jni_onload && vm) {
jint v = real_jni_onload(vm, 0);
XRRLOG("PASSTHRU: primed real JNI_OnLoad(vm=%p) -> 0x%x", (void *)vm, v);
} else {
XRRLOG("PASSTHRU: WARN no real JNI_OnLoad/vm (onload=%p vm=%p) — PreInitialize may crash",
(void *)real_jni_onload, (void *)vm);
}
#define PT_RESOLVE(N) g_pt_real_##N = dlsym(h, "ovrp_" #N)
PT_RESOLVE(DestroyDistortionWindow2);
PT_RESOLVE(SetupDisplayObjects2);
PT_RESOLVE(SetReorientHMDOnControllerRecenter);
PT_RESOLVE(SetClientColorDesc);
PT_RESOLVE(SetAppEngineInfo2);
PT_RESOLVE(SetAppCPUPriority2);
PT_RESOLVE(InitializeMixedReality);
PT_RESOLVE(GetViewportStencil);
PT_RESOLVE(GetSystemRecommendedMSAALevel2);
PT_RESOLVE(GetLocalTrackingSpaceRecenterCount);
PT_RESOLVE(GetLayerTextureFoveation);
PT_RESOLVE(GetControllerHapticsDesc2);
#undef PT_RESOLVE
g_pt_active = 1; /* set last: trampolines must not forward until the table is built */
XRRLOG("PASSTHRU: ACTIVE — real libOVRPlugin owns the session (h=%p). Shim OpenXR path disabled.", h);
#endif
}
int pt_active(void) { pt_first_use(); return g_pt_active; }
void *pt_real(const char *name) { return g_pt_handle ? dlsym(g_pt_handle, name) : 0; }
/* Rate-limited native call census. Keyed by the string-literal pointer (each PT_FWD call
* site passes a stable literal), so per-function. Logs the first 3 calls then every 1200th
* — enough to capture the full set of game-called exports + native's return for each,
* without flooding on the per-frame getters. Diff `PTC` lines vs our shim's returns. */
void pt_log_call(const char *name, long ret) {
enum { MAXN = 320 };
static const char *seen[MAXN];
static unsigned cnt[MAXN];
static int n = 0;
int i;
for (i = 0; i < n; i++) if (seen[i] == name) break;
if (i == n) {
if (n >= MAXN) return;
seen[n] = name; cnt[n] = 0; i = n; n++;
}
unsigned c = ++cnt[i];
if (c <= 3 || (c % 1200) == 0)
XRRLOG("PTC %s -> %ld (#%u)", name, ret, c);
}
+384
View File
@@ -0,0 +1,384 @@
/* AUTO-GENERATED by gen_stubs.sh from analysis/shim_surface.txt. Do not edit by hand. */
#include "ovrplugin_shim.h"
#include "log.h"
/* Stub policy:
* -1004 Unsupported : features we will not implement (MRC/camera/perf/boundary/hands)
* 0 Success (no-op) : config setters we can safely accept-and-ignore for now
* -1005 NotYetImplemented : real work still owed (getters/etc. the game needs)
* Empty () = unspecified args: stub ignores args, returns constant in result reg. */
OVRP_EXPORT ovrpResult ovrp_AddCustomMetadata() { static int o; if(!o){o=1;XRRLOG("stub ovrp_AddCustomMetadata -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_AutoThreadScheduling() { static int o; if(!o){o=1;XRRLOG("stub ovrp_AutoThreadScheduling -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_BeginFrame() { static int o; if(!o){o=1;XRRLOG("stub ovrp_BeginFrame -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_BeginFrame2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_BeginFrame2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_BeginFrame3() { static int o; if(!o){o=1;XRRLOG("stub ovrp_BeginFrame3 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_CalculateEyeLayerDesc() { static int o; if(!o){o=1;XRRLOG("stub ovrp_CalculateEyeLayerDesc -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_CalculateEyeLayerDesc3() { static int o; if(!o){o=1;XRRLOG("stub ovrp_CalculateEyeLayerDesc3 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_CalculateEyePreviewRect() { static int o; if(!o){o=1;XRRLOG("stub ovrp_CalculateEyePreviewRect -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_CloseCameraDevice() { static int o; if(!o){o=1;XRRLOG("stub ovrp_CloseCameraDevice -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_ConvertPoseToCameraSpace() { static int o; if(!o){o=1;XRRLOG("stub ovrp_ConvertPoseToCameraSpace -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_DestroyDistortionWindow() { static int o; if(!o){o=1;XRRLOG("stub ovrp_DestroyDistortionWindow -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_DestroyEyeTexture() { static int o; if(!o){o=1;XRRLOG("stub ovrp_DestroyEyeTexture -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_DestroyMirrorTexture() { static int o; if(!o){o=1;XRRLOG("stub ovrp_DestroyMirrorTexture -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_DestroyMirrorTexture2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_DestroyMirrorTexture2 -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_DismissHSW() { static int o; if(!o){o=1;XRRLOG("stub ovrp_DismissHSW -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_DoesCameraDeviceSupportDepth() { static int o; if(!o){o=1;XRRLOG("stub ovrp_DoesCameraDeviceSupportDepth -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_EndEye() { static int o; if(!o){o=1;XRRLOG("stub ovrp_EndEye -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_EndEye2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_EndEye2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_EndFrame() { static int o; if(!o){o=1;XRRLOG("stub ovrp_EndFrame -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_EndFrame2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_EndFrame2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_EndFrame3() { static int o; if(!o){o=1;XRRLOG("stub ovrp_EndFrame3 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_EnqueueDestroyLayer() { static int o; if(!o){o=1;XRRLOG("stub ovrp_EnqueueDestroyLayer -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_EnqueueSetupLayer() { static int o; if(!o){o=1;XRRLOG("stub ovrp_EnqueueSetupLayer -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_EnqueueSetupLayer2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_EnqueueSetupLayer2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_EnqueueSubmitLayer() { static int o; if(!o){o=1;XRRLOG("stub ovrp_EnqueueSubmitLayer -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_EnqueueSubmitLayer2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_EnqueueSubmitLayer2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_EnumerateAllCameraDevices() { static int o; if(!o){o=1;XRRLOG("stub ovrp_EnumerateAllCameraDevices -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_EnumerateAvailableCameraDevices() { static int o; if(!o){o=1;XRRLOG("stub ovrp_EnumerateAvailableCameraDevices -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetActiveController() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetActiveController -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetActiveController2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetActiveController2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAdapterId() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAdapterId -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAdaptiveGpuPerformanceScale() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAdaptiveGpuPerformanceScale -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAppAsymmetricFov() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAppAsymmetricFov -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAppChromaticCorrection() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAppChromaticCorrection -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAppCPUPriority() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAppCPUPriority -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAppCPUPriority2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAppCPUPriority2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAppCpuStartToGpuEndTime() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAppCpuStartToGpuEndTime -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAppCpuStartToGpuEndTime2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAppCpuStartToGpuEndTime2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAppFramerate() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAppFramerate -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAppFramerate2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAppFramerate2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAppHasSystemOverlayPresent() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAppHasSystemOverlayPresent -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAppHasVrFocus() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAppHasVrFocus -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAppLatencyTimings() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAppLatencyTimings -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAppLatencyTimings2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAppLatencyTimings2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAppMonoscopic() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAppMonoscopic -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAppPerfStats() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAppPerfStats -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAppPerfStats2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAppPerfStats2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAppShouldQuit() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAppShouldQuit -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAppShouldRecenter() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAppShouldRecenter -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAppShouldRecreateDistortionWindow() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAppShouldRecreateDistortionWindow -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAppSRGB() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAppSRGB -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetASWAdaptiveMode() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetASWAdaptiveMode -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetASWDepthScale() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetASWDepthScale -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetASWEnable() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetASWEnable -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetASWVelocityScale() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetASWVelocityScale -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetAudioInDeviceId() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAudioInDeviceId -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAudioInDeviceId2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAudioInDeviceId2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAudioInId() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAudioInId -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAudioInId2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAudioInId2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAudioOutDeviceId() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAudioOutDeviceId -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAudioOutDeviceId2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAudioOutDeviceId2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAudioOutId() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAudioOutId -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetAudioOutId2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetAudioOutId2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetBatteryStatus() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetBatteryStatus -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetBoundaryConfigured() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetBoundaryConfigured -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetBoundaryConfigured2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetBoundaryConfigured2 -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetBoundaryDimensions() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetBoundaryDimensions -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetBoundaryDimensions2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetBoundaryDimensions2 -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetBoundaryGeometry() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetBoundaryGeometry -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetBoundaryGeometry2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetBoundaryGeometry2 -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetBoundaryGeometry3() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetBoundaryGeometry3 -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetBoundaryVisible() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetBoundaryVisible -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetBoundaryVisible2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetBoundaryVisible2 -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetBufferCount() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetBufferCount -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetCameraDeviceColorFrameBgraPixels() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetCameraDeviceColorFrameBgraPixels -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetCameraDeviceColorFrameSize() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetCameraDeviceColorFrameSize -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetCameraDeviceDepthConfidencePixels() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetCameraDeviceDepthConfidencePixels -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetCameraDeviceDepthFramePixels() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetCameraDeviceDepthFramePixels -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetCameraDeviceDepthFrameSize() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetCameraDeviceDepthFrameSize -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetCameraDeviceDepthSensingMode() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetCameraDeviceDepthSensingMode -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetCameraDeviceIntrinsicsParameters() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetCameraDeviceIntrinsicsParameters -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetCameraDevicePreferredDepthQuality() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetCameraDevicePreferredDepthQuality -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetCaps() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetCaps -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetCaps2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetCaps2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetConnectedControllers() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetConnectedControllers -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetConnectedControllers2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetConnectedControllers2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetControllerHapticsDesc() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetControllerHapticsDesc -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetControllerHapticsState() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetControllerHapticsState -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetControllerHapticsState2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetControllerHapticsState2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetControllerState() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetControllerState -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetControllerState2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetControllerState2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetControllerState3() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetControllerState3 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetCurrentTrackingTransformPose() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetCurrentTrackingTransformPose -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetDepthCompositingSupported() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetDepthCompositingSupported -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetDesiredEyeTextureFormat() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetDesiredEyeTextureFormat -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetDisplayAdapterId() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetDisplayAdapterId -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetDisplayAdapterId2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetDisplayAdapterId2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetDominantHand() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetDominantHand -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetExternalCameraCalibrationRawPose() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetExternalCameraCalibrationRawPose -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetExternalCameraCount() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetExternalCameraCount -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetExternalCameraExtrinsics() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetExternalCameraExtrinsics -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetExternalCameraIntrinsics() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetExternalCameraIntrinsics -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetExternalCameraName() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetExternalCameraName -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetExternalCameraPose() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetExternalCameraPose -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetEyeAcceleration() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetEyeAcceleration -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetEyeFovLayerId() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetEyeFovLayerId -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetEyeFrustum() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetEyeFrustum -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetEyeOcclusionMesh() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetEyeOcclusionMesh -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetEyeOcclusionMeshEnabled() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetEyeOcclusionMeshEnabled -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetEyePixelsPerTanAngleAtCenter() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetEyePixelsPerTanAngleAtCenter -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetEyePixelsPerTanAngleAtCenter2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetEyePixelsPerTanAngleAtCenter2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetEyePose() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetEyePose -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetEyePreviewRect() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetEyePreviewRect -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetEyeRecommendedResolutionScale() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetEyeRecommendedResolutionScale -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetEyeTextureArrayEnabled() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetEyeTextureArrayEnabled -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetEyeTextureArraySupported() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetEyeTextureArraySupported -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetEyeTextureArraySupported2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetEyeTextureArraySupported2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetEyeTextureFlippedY() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetEyeTextureFlippedY -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetEyeTextureScale() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetEyeTextureScale -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetEyeTextureShared() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetEyeTextureShared -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetEyeTextureSize() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetEyeTextureSize -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetEyeTextureStageCount() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetEyeTextureStageCount -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetEyeVelocity() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetEyeVelocity -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetEyeViewportScale() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetEyeViewportScale -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetFloat() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetFloat -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetGPUUtilLevel() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetGPUUtilLevel -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetGPUUtilSupported() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetGPUUtilSupported -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetHandNodePoseStateLatency() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetHandNodePoseStateLatency -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetHandState() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetHandState -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetHandState2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetHandState2 -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetHandTrackingEnabled() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetHandTrackingEnabled -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetHeadphonesPresent() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetHeadphonesPresent -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetHeadPoseModifier() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetHeadPoseModifier -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetHmdColorDesc() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetHmdColorDesc -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetHmdToEyeOffset() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetHmdToEyeOffset -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetHmdToEyeOffset2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetHmdToEyeOffset2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetInputState() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetInputState -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetLayerAndroidSurfaceObject() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetLayerAndroidSurfaceObject -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetLayerOcclusionMesh() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetLayerOcclusionMesh -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetLayerTexture() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetLayerTexture -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetLayerTexturePtr() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetLayerTexturePtr -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetLayerTextureSpaceWarp() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetLayerTextureSpaceWarp -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetMesh() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetMesh -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetNativePointer() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetNativePointer -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetNativeSDKPointer() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetNativeSDKPointer -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetNativeSDKPointer2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetNativeSDKPointer2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetNativeSDKVersion() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetNativeSDKVersion -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetNativeSDKVersion2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetNativeSDKVersion2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetNodeAcceleration() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetNodeAcceleration -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetNodeAcceleration2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetNodeAcceleration2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetNodeFrustum() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetNodeFrustum -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetNodeOrientationTracked() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetNodeOrientationTracked -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetNodePose() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetNodePose -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetNodePose2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetNodePose2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetNodePoseState() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetNodePoseState -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetNodePoseState2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetNodePoseState2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetNodePositionTracked() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetNodePositionTracked -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetNodePresent() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetNodePresent -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetNodeVelocity() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetNodeVelocity -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetNodeVelocity2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetNodeVelocity2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetReorientHMDOnControllerRecenter() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetReorientHMDOnControllerRecenter -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSkeleton() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSkeleton -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetStatus() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetStatus -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetStatus2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetStatus2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetString() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetString -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemBatteryLevel() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemBatteryLevel -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemBatteryLevel2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemBatteryLevel2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemBatteryStatus() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemBatteryStatus -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemBatteryStatus2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemBatteryStatus2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemBatteryTemperature() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemBatteryTemperature -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemBatteryTemperature2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemBatteryTemperature2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemCpuLevel() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemCpuLevel -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemCpuLevel2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemCpuLevel2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemDisplayAvailableFrequencies() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemDisplayAvailableFrequencies -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemDisplayFrequency() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemDisplayFrequency -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemGpuLevel() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemGpuLevel -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemGpuLevel2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemGpuLevel2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemHeadphonesPresent() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemHeadphonesPresent -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemHeadphonesPresent2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemHeadphonesPresent2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemHeadsetType() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemHeadsetType -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemHmd3DofModeEnabled() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemHmd3DofModeEnabled -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemMultiViewSupported() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemMultiViewSupported -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemPowerSavingMode() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemPowerSavingMode -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemPowerSavingMode2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemPowerSavingMode2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemProductName() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemProductName -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemProductName2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemProductName2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemRecommendedMSAALevel() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemRecommendedMSAALevel -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemRegion() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemRegion -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemRegion2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemRegion2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemVolume() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemVolume -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemVolume2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemVolume2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemVSyncCount() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemVSyncCount -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetSystemVSyncCount2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetSystemVSyncCount2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetTimeInSeconds() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetTimeInSeconds -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetTrackerFrustum() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetTrackerFrustum -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetTrackerPose() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetTrackerPose -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetTrackingCalibratedOrigin() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetTrackingCalibratedOrigin -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetTrackingCalibratedOrigin2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetTrackingCalibratedOrigin2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetTrackingIPDEnabled() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetTrackingIPDEnabled -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetTrackingIPDEnabled2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetTrackingIPDEnabled2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetTrackingOrientationEnabled() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetTrackingOrientationEnabled -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetTrackingOrientationEnabled2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetTrackingOrientationEnabled2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetTrackingOrientationSupported() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetTrackingOrientationSupported -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetTrackingOrientationSupported2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetTrackingOrientationSupported2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetTrackingOriginType() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetTrackingOriginType -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetTrackingPositionEnabled() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetTrackingPositionEnabled -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetTrackingPositionEnabled2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetTrackingPositionEnabled2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetTrackingPositionSupported() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetTrackingPositionSupported -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetTrackingPositionSupported2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetTrackingPositionSupported2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetTrackingTransformRawPose() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetTrackingTransformRawPose -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetTrackingTransformRelativePose() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetTrackingTransformRelativePose -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetUseOverriddenExternalCameraFov() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetUseOverriddenExternalCameraFov -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetUseOverriddenExternalCameraStaticPose() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetUseOverriddenExternalCameraStaticPose -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_GetUserEyeDepth() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetUserEyeDepth -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetUserEyeHeight() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetUserEyeHeight -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetUserEyeHeight2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetUserEyeHeight2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetUserIPD() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetUserIPD -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetUserIPD2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetUserIPD2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetUserNeckEyeDistance() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetUserNeckEyeDistance -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetUserNeckEyeDistance2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetUserNeckEyeDistance2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetUserPresent() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetUserPresent -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetVersion() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetVersion -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetVersion2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetVersion2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_GetVrApiPropertyInt() { static int o; if(!o){o=1;XRRLOG("stub ovrp_GetVrApiPropertyInt -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_HasCameraDeviceOpened2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_HasCameraDeviceOpened2 -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Initialize() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Initialize -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_Initialize2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Initialize2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_Initialize3() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Initialize3 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_Initialize4() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Initialize4 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_IsCameraDeviceAvailable2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_IsCameraDeviceAvailable2 -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_IsCameraDeviceColorFrameAvailable2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_IsCameraDeviceColorFrameAvailable2 -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_IsCameraDeviceDepthFrameAvailable() { static int o; if(!o){o=1;XRRLOG("stub ovrp_IsCameraDeviceDepthFrameAvailable -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_IsRequestingASWData() { static int o; if(!o){o=1;XRRLOG("stub ovrp_IsRequestingASWData -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_CreateCustomCameraAnchor() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_CreateCustomCameraAnchor -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_DestroyCustomCameraAnchor() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_DestroyCustomCameraAnchor -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_EncodeMrcFrame() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_EncodeMrcFrame -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_EncodeMrcFrameDualTexturesWithPoseTime() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_EncodeMrcFrameDualTexturesWithPoseTime -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_EncodeMrcFrameWithDualTextures() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_EncodeMrcFrameWithDualTextures -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_EncodeMrcFrameWithPoseTime() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_EncodeMrcFrameWithPoseTime -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_EnumerateCameraAnchorHandles() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_EnumerateCameraAnchorHandles -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_GetCameraAnchorHandle() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_GetCameraAnchorHandle -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_GetCameraAnchorName() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_GetCameraAnchorName -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_GetCameraAnchorType() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_GetCameraAnchorType -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_GetCameraMinMaxDistance() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_GetCameraMinMaxDistance -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_GetCurrentCameraAnchorHandle() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_GetCurrentCameraAnchorHandle -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_GetCustomCameraAnchorPose() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_GetCustomCameraAnchorPose -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_GetInitialized() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_GetInitialized -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_GetMrcActivationMode() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_GetMrcActivationMode -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_GetMrcAudioSampleRate() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_GetMrcAudioSampleRate -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_GetMrcFrameImageFlipped() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_GetMrcFrameImageFlipped -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_GetMrcFrameInverseAlpha() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_GetMrcFrameInverseAlpha -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_GetMrcFrameSize() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_GetMrcFrameSize -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_GetMrcInputVideoBufferType() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_GetMrcInputVideoBufferType -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_Initialize() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_Initialize -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_IsMrcActivated() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_IsMrcActivated -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_IsMrcEnabled() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_IsMrcEnabled -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_SetAvailableQueueIndexVulkan() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_SetAvailableQueueIndexVulkan -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_SetCameraMinMaxDistance() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_SetCameraMinMaxDistance -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_SetCustomCameraAnchorPose() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_SetCustomCameraAnchorPose -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_SetHeadsetControllerPose() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_SetHeadsetControllerPose -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_SetMrcActivationMode() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_SetMrcActivationMode -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_SetMrcAudioSampleRate() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_SetMrcAudioSampleRate -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_SetMrcFrameImageFlipped() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_SetMrcFrameImageFlipped -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_SetMrcFrameInverseAlpha() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_SetMrcFrameInverseAlpha -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_SetMrcFrameSize() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_SetMrcFrameSize -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_SetMrcInputVideoBufferType() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_SetMrcInputVideoBufferType -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_Shutdown() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_Shutdown -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_SyncMrcFrame() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_SyncMrcFrame -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_Update() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_Update -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Media_UseMrcDebugCamera() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Media_UseMrcDebugCamera -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_OpenCameraDevice() { static int o; if(!o){o=1;XRRLOG("stub ovrp_OpenCameraDevice -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_OverrideExternalCameraFov() { static int o; if(!o){o=1;XRRLOG("stub ovrp_OverrideExternalCameraFov -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_OverrideExternalCameraStaticPose() { static int o; if(!o){o=1;XRRLOG("stub ovrp_OverrideExternalCameraStaticPose -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_PreInitialize() { static int o; if(!o){o=1;XRRLOG("stub ovrp_PreInitialize -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_PreInitialize2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_PreInitialize2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_RecenterPose() { static int o; if(!o){o=1;XRRLOG("stub ovrp_RecenterPose -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_RecenterTrackingOrigin() { static int o; if(!o){o=1;XRRLOG("stub ovrp_RecenterTrackingOrigin -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_RecreateEyeTexture() { static int o; if(!o){o=1;XRRLOG("stub ovrp_RecreateEyeTexture -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_ReleaseEyeTexture() { static int o; if(!o){o=1;XRRLOG("stub ovrp_ReleaseEyeTexture -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_ResetAppPerfStats() { static int o; if(!o){o=1;XRRLOG("stub ovrp_ResetAppPerfStats -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_ResetAppPerfStats2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_ResetAppPerfStats2 -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_ResetDefaultExternalCamera() { static int o; if(!o){o=1;XRRLOG("stub ovrp_ResetDefaultExternalCamera -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_SendEvent() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SendEvent -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SendEvent2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SendEvent2 -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetAppAsymmetricFov() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetAppAsymmetricFov -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetAppChromaticCorrection() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetAppChromaticCorrection -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetAppCPUPriority() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetAppCPUPriority -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetAppEngineInfo() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetAppEngineInfo -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetAppIgnoreVrFocus() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetAppIgnoreVrFocus -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetAppMonoscopic() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetAppMonoscopic -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetAppSRGB() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetAppSRGB -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetASWAdaptiveMode() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetASWAdaptiveMode -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_SetASWEnable() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetASWEnable -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_SetBoundaryVisible() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetBoundaryVisible -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_SetBoundaryVisible2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetBoundaryVisible2 -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_SetCameraDeviceDepthSensingMode() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetCameraDeviceDepthSensingMode -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_SetCameraDevicePreferredColorFrameSize() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetCameraDevicePreferredColorFrameSize -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_SetCameraDevicePreferredDepthQuality() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetCameraDevicePreferredDepthQuality -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_SetCaps() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetCaps -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetColorScaleAndOffset() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetColorScaleAndOffset -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetControllerHaptics() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetControllerHaptics -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetControllerHaptics2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetControllerHaptics2 -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetControllerVibration() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetControllerVibration -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetDefaultExternalCamera() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetDefaultExternalCamera -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_SetDepthCompositingInfo() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetDepthCompositingInfo -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetDepthProjInfo() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetDepthProjInfo -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetDesiredEyeTextureFormat() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetDesiredEyeTextureFormat -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetDeveloperMode() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetDeveloperMode -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetExternalCameraProperties() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetExternalCameraProperties -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_SetEyeOcclusionMeshEnabled() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetEyeOcclusionMeshEnabled -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetEyeTexture() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetEyeTexture -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetEyeTextureArrayEnabled() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetEyeTextureArrayEnabled -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetEyeTextureFlippedY() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetEyeTextureFlippedY -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetEyeTextureScale() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetEyeTextureScale -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetEyeTextureShared() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetEyeTextureShared -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetEyeViewportScale() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetEyeViewportScale -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetFloat() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetFloat -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetHandNodePoseStateLatency() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetHandNodePoseStateLatency -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_SetHeadPoseModifier() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetHeadPoseModifier -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetInhibitSystemUX() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetInhibitSystemUX -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetInhibitSystemUX2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetInhibitSystemUX2 -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetNodePositionTracked() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetNodePositionTracked -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetNodePositionTracked2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetNodePositionTracked2 -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetOctilinearInfo() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetOctilinearInfo -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetOverlayQuad() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetOverlayQuad -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetOverlayQuad2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetOverlayQuad2 -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetOverlayQuad3() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetOverlayQuad3 -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetRemoteHandedness() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetRemoteHandedness -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetSystemCpuLevel() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetSystemCpuLevel -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetSystemDisplayFrequency() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetSystemDisplayFrequency -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetSystemGpuLevel() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetSystemGpuLevel -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetSystemVSyncCount() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetSystemVSyncCount -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetSystemVSyncCount2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetSystemVSyncCount2 -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetThreadPerformance() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetThreadPerformance -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetTrackingCalibratedOrigin2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetTrackingCalibratedOrigin2 -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetTrackingIPDEnabled() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetTrackingIPDEnabled -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetTrackingIPDEnabled2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetTrackingIPDEnabled2 -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetTrackingOrientationEnabled() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetTrackingOrientationEnabled -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetTrackingOrientationEnabled2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetTrackingOrientationEnabled2 -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetTrackingOriginType() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetTrackingOriginType -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetTrackingPositionEnabled() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetTrackingPositionEnabled -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetTrackingPositionEnabled2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetTrackingPositionEnabled2 -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetupDisplayObjects() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetupDisplayObjects -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_SetupDistortionWindow() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetupDistortionWindow -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_SetupDistortionWindow2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetupDistortionWindow2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_SetupEyeTexture() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetupEyeTexture -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_SetupEyeTexture2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetupEyeTexture2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_SetupLayerDepth() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetupLayerDepth -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_SetupMirrorTexture() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetupMirrorTexture -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_SetupMirrorTexture2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetupMirrorTexture2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_SetUserEyeDepth() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetUserEyeDepth -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetUserEyeHeight() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetUserEyeHeight -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetUserEyeHeight2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetUserEyeHeight2 -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetUserIPD() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetUserIPD -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetUserIPD2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetUserIPD2 -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetUserNeckEyeDistance() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetUserNeckEyeDistance -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetUserNeckEyeDistance2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetUserNeckEyeDistance2 -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetVrApiPropertyFloat() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetVrApiPropertyFloat -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_SetVrApiPropertyInt() { static int o; if(!o){o=1;XRRLOG("stub ovrp_SetVrApiPropertyInt -> ovrpSuccess");} return ovrpSuccess; }
OVRP_EXPORT ovrpResult ovrp_ShowSystemUI() { static int o; if(!o){o=1;XRRLOG("stub ovrp_ShowSystemUI -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_ShowSystemUI2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_ShowSystemUI2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_ShowUI() { static int o; if(!o){o=1;XRRLOG("stub ovrp_ShowUI -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_Shutdown() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Shutdown -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_ShutdownMixedReality() { static int o; if(!o){o=1;XRRLOG("stub ovrp_ShutdownMixedReality -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_TestBoundaryNode() { static int o; if(!o){o=1;XRRLOG("stub ovrp_TestBoundaryNode -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_TestBoundaryNode2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_TestBoundaryNode2 -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_TestBoundaryPoint() { static int o; if(!o){o=1;XRRLOG("stub ovrp_TestBoundaryPoint -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_TestBoundaryPoint2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_TestBoundaryPoint2 -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_Update() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Update -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_Update2() { static int o; if(!o){o=1;XRRLOG("stub ovrp_Update2 -> ovrpFailure_NotYetImplemented");} return ovrpFailure_NotYetImplemented; }
OVRP_EXPORT ovrpResult ovrp_UpdateCameraDevices() { static int o; if(!o){o=1;XRRLOG("stub ovrp_UpdateCameraDevices -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
OVRP_EXPORT ovrpResult ovrp_UpdateExternalCamera() { static int o; if(!o){o=1;XRRLOG("stub ovrp_UpdateExternalCamera -> ovrpFailure_Unsupported");} return ovrpFailure_Unsupported; }
/* generated: unsup=103 noop=67 todo=202 skipped(core)=66 */
+932
View File
@@ -0,0 +1,932 @@
/* vk_session.c — Vulkan-typed half of the OpenXR handshake. Uses XR_KHR_vulkan_enable
* (v1) because OVRPlugin's model is "the app creates the VkInstance/Device" (handles
* arrive via ovrp_Initialize5 args 5-8) and asks us which extensions to enable via
* ovrp_GetInstance/DeviceExtensionsVk — that's exactly the v1 get-extensions flow.
*/
#include <vulkan/vulkan.h>
#define XR_USE_GRAPHICS_API_VULKAN
#include <openxr/openxr_platform.h>
#include "xr_runtime.h"
#include "passthru.h"
#include "log.h"
#include <string.h>
#include <stdio.h>
#include <stdint.h>
#include <dlfcn.h>
/* fetch a KHR_vulkan_enable extension entry point by name */
static PFN_xrVoidFunction get_xr(const char *name) {
PFN_xrVoidFunction fn = NULL;
if (g_xr.instance != XR_NULL_HANDLE)
xrGetInstanceProcAddr(g_xr.instance, name, &fn);
return fn;
}
/* UE's Vulkan handles (from ovrp_Initialize5), used by both the graphics binding
* and the end-of-frame flush barrier. */
static VkDevice s_dev;
static VkQueue s_queue;
static uint32_t s_qfam;
static VkPhysicalDevice s_phys; /* for memory-type selection (texture readback) */
static VkInstance s_inst; /* to load instance-level entry points */
void xrr_vk_set_handles(void *device, void *queue, unsigned int family) {
s_dev = (VkDevice)device; s_queue = (VkQueue)queue; s_qfam = family;
}
/* libvulkan + its proc-addr loaders, opened ONCE. Previously every Vulkan helper dlopen'd
* libvulkan.so per call (xrr_vk_frame_luma did it per frame), leaking a dl handle each time.
* Returns 1 if vkGetDeviceProcAddr is available; fills gdpa/gipa (either may be NULL-arg). */
static void *s_vklib;
static PFN_vkGetDeviceProcAddr s_gdpa;
static PFN_vkGetInstanceProcAddr s_gipa;
static int vk_loaders(PFN_vkGetDeviceProcAddr *gdpa, PFN_vkGetInstanceProcAddr *gipa) {
if (!s_vklib) {
s_vklib = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL);
if (s_vklib) {
s_gdpa = (PFN_vkGetDeviceProcAddr)dlsym(s_vklib, "vkGetDeviceProcAddr");
s_gipa = (PFN_vkGetInstanceProcAddr)dlsym(s_vklib, "vkGetInstanceProcAddr");
}
}
if (gdpa) *gdpa = s_gdpa;
if (gipa) *gipa = s_gipa;
return s_gdpa != NULL;
}
/* The OpenXR runtime synchronizes its compositor against the queue named in the
* graphics binding. We only have UE's VkQueue handle, so scan (family,index) to
* find which one it is — a wrong queueIndex means the runtime waits on an idle
* queue and composites before UE finishes -> black unless we hard-wait ourselves. */
static PFN_vkGetDeviceQueue load_get_device_queue(VkDevice dev) {
PFN_vkGetDeviceProcAddr gdpa; vk_loaders(&gdpa, NULL);
return gdpa ? (PFN_vkGetDeviceQueue)gdpa(dev, "vkGetDeviceQueue") : NULL;
}
static void detect_ue_queue(VkDevice dev, uint32_t *family, uint32_t *index) {
*family = 0; *index = 0;
if (!s_queue) return;
PFN_vkGetDeviceQueue gdq = load_get_device_queue(dev);
if (!gdq) { XRRLOG("queue detect: no vkGetDeviceQueue"); return; }
for (uint32_t f = 0; f < 4; f++)
for (uint32_t i = 0; i < 4; i++) {
VkQueue q = VK_NULL_HANDLE;
gdq(dev, f, i, &q);
if (q == s_queue) {
*family = f; *index = i;
XRRLOG("queue detect: UE queue is family=%u index=%u", f, i);
return;
}
}
XRRLOG("queue detect: UE queue not matched, defaulting 0/0");
}
/* ----------------------------------------------- app-side extension queries -- */
/* [VERIFIED from real Compositor::GetInstanceExtensionsVk] ABI is:
* ovrp_Get*ExtensionsVk(const char** outArray, int* inoutCount)
* outArray = caller's array of char*, filled with `count` POINTERS to extension
* name strings (memcpy count<<3 bytes); inoutCount IN=capacity(entries), OUT=count;
* returns -1007 if capacity<count; NULL outArray = size query. UE iterates the
* result as char*[] (my single-buffer version made it deref chars as ptrs ->
* strcmp segfault). We split xrGetVulkan*ExtensionsKHR's space list into ptrs. */
#define XRR_MAX_EXTS 64
static char s_extBuf[2][4096]; /* [0]=instance [1]=device */
static const char *s_extPtrs[2][XRR_MAX_EXTS];
static int s_extCount[2] = { -1, -1 };
static int build_ext_list(int forDevice) {
if (s_extCount[forDevice] >= 0) return s_extCount[forDevice]; /* cached */
const char *fname = forDevice ? "xrGetVulkanDeviceExtensionsKHR"
: "xrGetVulkanInstanceExtensionsKHR";
union { PFN_xrGetVulkanInstanceExtensionsKHR i;
PFN_xrGetVulkanDeviceExtensionsKHR d;
PFN_xrVoidFunction v; } fn;
fn.v = get_xr(fname);
if (!fn.v) return -1;
char *buf = s_extBuf[forDevice];
uint32_t got = 0;
XrResult r = forDevice
? fn.d(g_xr.instance, g_xr.systemId, 4096, &got, buf)
: fn.i(g_xr.instance, g_xr.systemId, 4096, &got, buf);
if (XR_FAILED(r) || got == 0) { XRRERR("%s failed: %d", fname, (int)r); return -1; }
buf[got < 4096 ? got : 4095] = '\0';
/* split the space-separated string in place into pointers */
int n = 0;
char *p = buf;
while (*p && n < XRR_MAX_EXTS) {
s_extPtrs[forDevice][n++] = p;
char *sp = p;
while (*sp && *sp != ' ') sp++;
if (*sp == ' ') { *sp = '\0'; p = sp + 1; } else break;
}
s_extCount[forDevice] = n;
return n;
}
static ovrpResult get_vk_exts(int forDevice, const char **outArray, int *inoutCount) {
if (!inoutCount) return ovrpFailure_InvalidParameter;
if (g_xr.instance == XR_NULL_HANDLE || g_xr.systemId == XR_NULL_SYSTEM_ID)
return ovrpFailure_InvalidOperation;
int count = build_ext_list(forDevice);
if (count < 0) return ovrpFailure_Unsupported;
int cap = *inoutCount;
*inoutCount = count; /* always report the count */
if (outArray) {
if (cap < count) return ovrpFailure_InsufficientSize; /* -1007 */
memcpy(outArray, s_extPtrs[forDevice], (size_t)count * sizeof(char *));
}
return ovrpSuccess;
}
OVRP_EXPORT ovrpResult ovrp_GetInstanceExtensionsVk(const char **outArray, int *inoutCount) {
PT_FWD(ovrp_GetInstanceExtensionsVk, outArray, inoutCount); /* real vrapi extensions */
ovrpResult res = get_vk_exts(0, outArray, inoutCount);
XRRLOG("GetInstanceExtensionsVk -> %d (count=%d, fill=%d)", res,
inoutCount ? *inoutCount : -1, outArray != NULL);
return res;
}
OVRP_EXPORT ovrpResult ovrp_GetDeviceExtensionsVk(const char **outArray, int *inoutCount) {
PT_FWD(ovrp_GetDeviceExtensionsVk, outArray, inoutCount); /* real vrapi extensions */
ovrpResult res = get_vk_exts(1, outArray, inoutCount);
XRRLOG("GetDeviceExtensionsVk -> %d (count=%d, fill=%d)", res,
inoutCount ? *inoutCount : -1, outArray != NULL);
return res;
}
/* --------------------------------------------------------- session creation -- */
int xrr_create_session_vulkan(void *vkInstance, void *vkPhysicalDevice,
void *vkDevice, unsigned int queueFamilyIndex) {
if (g_xr.instance == XR_NULL_HANDLE || g_xr.systemId == XR_NULL_SYSTEM_ID)
return 0;
/* required before session creation per XR_KHR_vulkan_enable */
union { PFN_xrGetVulkanGraphicsRequirementsKHR f; PFN_xrVoidFunction v; } req;
req.v = get_xr("xrGetVulkanGraphicsRequirementsKHR");
XRRLOG("vk_session: reqProc=%p", (void *)req.v);
if (req.f) {
XrGraphicsRequirementsVulkanKHR gr = { XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN_KHR };
XrResult rr = req.f(g_xr.instance, g_xr.systemId, &gr);
XRRLOG("vk_session: GraphicsRequirements rc=%d minApi=0x%llx maxApi=0x%llx",
(int)rr, (unsigned long long)gr.minApiVersionSupported,
(unsigned long long)gr.maxApiVersionSupported);
}
/* runtime may require we query the graphics device too */
union { PFN_xrGetVulkanGraphicsDeviceKHR f; PFN_xrVoidFunction v; } gd;
gd.v = get_xr("xrGetVulkanGraphicsDeviceKHR");
if (gd.f) {
VkPhysicalDevice want = VK_NULL_HANDLE;
XrResult dr = gd.f(g_xr.instance, g_xr.systemId, (VkInstance)vkInstance, &want);
XRRLOG("vk_session: GraphicsDevice rc=%d want=%p got=%p match=%d", (int)dr,
(void *)want, vkPhysicalDevice, want == (VkPhysicalDevice)vkPhysicalDevice);
}
s_phys = (VkPhysicalDevice)vkPhysicalDevice;
s_inst = (VkInstance)vkInstance;
uint32_t qfam = queueFamilyIndex, qidx = 0;
detect_ue_queue((VkDevice)vkDevice, &qfam, &qidx);
s_qfam = qfam; /* the flush command pool must use the same family */
XrGraphicsBindingVulkanKHR binding = { XR_TYPE_GRAPHICS_BINDING_VULKAN_KHR };
binding.instance = (VkInstance)vkInstance;
binding.physicalDevice = (VkPhysicalDevice)vkPhysicalDevice;
binding.device = (VkDevice)vkDevice;
binding.queueFamilyIndex = qfam;
binding.queueIndex = qidx;
XRRLOG("vk_session: binding inst=%p phys=%p dev=%p queue fam=%u idx=%u",
vkInstance, vkPhysicalDevice, vkDevice, qfam, qidx);
XrSessionCreateInfo sci = { XR_TYPE_SESSION_CREATE_INFO };
sci.next = &binding;
sci.systemId = g_xr.systemId;
XrResult r = xrCreateSession(g_xr.instance, &sci, &g_xr.session);
if (XR_FAILED(r)) {
XRRERR("xrCreateSession(Vulkan) failed: %d", (int)r);
g_xr.session = XR_NULL_HANDLE;
return 0;
}
XRRLOG("vk_session: xrCreateSession OK session=%p", (void *)g_xr.session);
return 1;
}
/* Enumerate a swapchain's VkImages, returned as opaque uint64 handles. */
/* ---------------------------------------------------- tile-memory flush ----- *
* On Quest's tiler GPU, UE renders the eye into tile memory and relies on the
* compositor's submit to resolve it to main memory. We hand the image straight to
* the OpenXR compositor, so without an explicit barrier the compositor reads stale
* main-memory contents -> black / tearing / ghosting. This records+submits a
* pipeline barrier (COLOR_ATTACHMENT_WRITE -> MEMORY_READ) to force the resolve.
* Submitted on UE's own VkQueue from end_frame (same RHI thread) so there is no
* concurrent queue access.
*
* Split submit/wait: xrr_vk_flush_submit records+submits the barrier and returns a
* ring token WITHOUT blocking; xrr_vk_flush_wait(token) blocks on that submit's
* fence. The frame loop submits frame N's flush then waits frame N-1's, so the
* wait is off the critical path (the GPU finished it during this frame's work) ->
* xrEndFrame lands on schedule and the compositor stops reprojecting every frame.
* Ring is sized for >=1 full frame of in-flight tokens (every layer submits per
* frame, and a token is now waited a frame later). */
#define XRR_FLUSH_RING (XRR_MAX_LAYERS * 2)
static int s_vkReady, s_vkFailed;
static VkCommandPool s_pool;
static VkCommandBuffer s_cmd[XRR_FLUSH_RING];
static VkFence s_fence[XRR_FLUSH_RING];
static int s_ring;
static PFN_vkCreateCommandPool p_CreatePool;
static PFN_vkAllocateCommandBuffers p_AllocCmd;
static PFN_vkBeginCommandBuffer p_BeginCmd;
static PFN_vkCmdPipelineBarrier p_Barrier;
static PFN_vkEndCommandBuffer p_EndCmd;
static PFN_vkQueueSubmit p_Submit;
static PFN_vkResetCommandBuffer p_ResetCmd;
static PFN_vkCreateFence p_CreateFence;
static PFN_vkWaitForFences p_WaitFences;
static PFN_vkResetFences p_ResetFences;
static PFN_vkGetFenceStatus p_FenceStatus;
static PFN_vkQueueWaitIdle p_QueueWaitIdle;
static PFN_vkDeviceWaitIdle p_DeviceWaitIdle;
static int vk_lazy_init(void) {
if (s_vkReady) return 1;
if (s_vkFailed) return 0;
if (!s_dev || !s_queue) return 0;
PFN_vkGetDeviceProcAddr gdpa; vk_loaders(&gdpa, NULL);
if (!gdpa) { s_vkFailed = 1; XRRERR("vk flush: no vkGetDeviceProcAddr"); return 0; }
#define LOAD(p, n) p = (PFN_##n)gdpa(s_dev, #n); if (!p) { s_vkFailed = 1; XRRERR("vk flush: missing " #n); return 0; }
LOAD(p_CreatePool, vkCreateCommandPool)
LOAD(p_AllocCmd, vkAllocateCommandBuffers)
LOAD(p_BeginCmd, vkBeginCommandBuffer)
LOAD(p_Barrier, vkCmdPipelineBarrier)
LOAD(p_EndCmd, vkEndCommandBuffer)
LOAD(p_Submit, vkQueueSubmit)
LOAD(p_ResetCmd, vkResetCommandBuffer)
LOAD(p_CreateFence, vkCreateFence)
LOAD(p_WaitFences, vkWaitForFences)
LOAD(p_ResetFences, vkResetFences)
LOAD(p_FenceStatus, vkGetFenceStatus)
LOAD(p_QueueWaitIdle, vkQueueWaitIdle)
LOAD(p_DeviceWaitIdle, vkDeviceWaitIdle)
#undef LOAD
VkCommandPoolCreateInfo pci = { VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO };
pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
pci.queueFamilyIndex = s_qfam;
if (p_CreatePool(s_dev, &pci, NULL, &s_pool) != VK_SUCCESS) { s_vkFailed = 1; return 0; }
VkCommandBufferAllocateInfo ai = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO };
ai.commandPool = s_pool; ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
ai.commandBufferCount = XRR_FLUSH_RING;
if (p_AllocCmd(s_dev, &ai, s_cmd) != VK_SUCCESS) { s_vkFailed = 1; return 0; }
for (int i = 0; i < XRR_FLUSH_RING; i++) {
VkFenceCreateInfo fi = { VK_STRUCTURE_TYPE_FENCE_CREATE_INFO };
fi.flags = VK_FENCE_CREATE_SIGNALED_BIT; /* first use sees it ready */
if (p_CreateFence(s_dev, &fi, NULL, &s_fence[i]) != VK_SUCCESS) { s_vkFailed = 1; return 0; }
}
s_vkReady = 1;
XRRLOG("vk flush: barrier ring ready (qfam=%u)", s_qfam);
return 1;
}
/* Is the barrier ring up (handles bound + entry points loaded)? Lets the frame
* loop fall back to a synchronous present until Vulkan is ready, then engage the
* pipeline. Idempotent (vk_lazy_init only runs the setup once). */
int xrr_vk_flush_ready(void) { return vk_lazy_init(); }
/* Record + submit the tile-memory flush barrier for `image` WITHOUT waiting.
* Returns a ring token to pass to xrr_vk_flush_wait, or -1 if Vulkan isn't ready.
* isDepth selects the depth-stencil aspect/layout/access (depth swapchains resolve
* the same way but a color barrier on a depth image is invalid). */
int xrr_vk_flush_submit_ex(uint64_t image, unsigned int arrayLayers, int isDepth) {
if (!vk_lazy_init()) return -1;
int idx = s_ring++ % XRR_FLUSH_RING;
/* this slot's previous submit must be done before we re-record it (its fence
* was already waited at present time a frame ago, so this is ~instant) */
p_WaitFences(s_dev, 1, &s_fence[idx], VK_TRUE, 100000000 /*100ms*/);
p_ResetFences(s_dev, 1, &s_fence[idx]);
p_ResetCmd(s_cmd[idx], 0);
VkCommandBufferBeginInfo bi = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO };
bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
p_BeginCmd(s_cmd[idx], &bi);
VkImageMemoryBarrier b = { VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER };
b.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT;
b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
b.image = (VkImage)(uintptr_t)image;
b.subresourceRange.levelCount = 1;
b.subresourceRange.layerCount = arrayLayers ? arrayLayers : 1;
VkPipelineStageFlags srcStage;
if (isDepth) {
b.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
b.oldLayout = b.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
b.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
srcStage = VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
} else {
b.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
b.oldLayout = b.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
b.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
srcStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
}
p_Barrier(s_cmd[idx], srcStage,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, NULL, 0, NULL, 1, &b);
p_EndCmd(s_cmd[idx]);
VkSubmitInfo si = { VK_STRUCTURE_TYPE_SUBMIT_INFO };
si.commandBufferCount = 1; si.pCommandBuffers = &s_cmd[idx];
if (p_Submit(s_queue, 1, &si, s_fence[idx]) != VK_SUCCESS) return -1;
return idx;
}
/* color-image convenience wrapper (the common case) */
int xrr_vk_flush_submit(uint64_t image, unsigned int arrayLayers) {
return xrr_vk_flush_submit_ex(image, arrayLayers, 0);
}
/* Block until the flush submitted under `token` has completed. The Meta runtime
* does NOT synchronize its compositor against our submit, so the image must be
* fully resolved before we release it to the compositor (else it reads
* unresolved/invisible writes = black). Called a frame after submit, so the GPU
* has already finished -> this returns immediately and never stalls the submit. */
void xrr_vk_flush_wait(int token) {
if (token < 0 || !s_vkReady) return;
p_WaitFences(s_dev, 1, &s_fence[token], VK_TRUE, 100000000);
}
/* Probe (debug.re4vr.qwait): drain UE's VkQueue so any submitted render completes
* before we resolve/present. Distinguishes "UE submitted-but-incomplete" (this turns
* the black image correct) from "UE hadn't submitted yet" (still black -> ordering
* bug). A hard stall; diagnostic only. */
void xrr_vk_queue_wait_idle(void) {
if (!vk_lazy_init() || !p_QueueWaitIdle || !s_queue) return;
p_QueueWaitIdle(s_queue);
}
/* Full GPU completion across ALL queues. UE submits the eye render on its own queue(s)
* (two seen: s_queue + a second), so draining s_queue alone misses it. Used at the
* deferred present (which already runs AFTER UE's eye submit) to guarantee the render is
* COMPLETE before we release+composite — the production form of what the dump's fence-wait
* did. Heavier than a targeted fence wait; refine to the eye-render fence once confirmed. */
void xrr_vk_device_wait_idle(void) {
if (!vk_lazy_init() || !p_DeviceWaitIdle || !s_dev) return;
p_DeviceWaitIdle(s_dev);
}
/* ---- UE vkQueueSubmit hook (render-submit race fix) ----------------------- *
* RE finding: UE 4.25 submits the eye render through the
* FVulkan RHI's global PFN VulkanDynamicAPI::vkQueueSubmit AFTER ovrp_EndFrame4 on the
* RHI thread (proven by the qwait no-op). We patch that global to a trampoline so the
* shim observes the exact submit and can order present after it (present-on-submit, in
* xr_runtime.c). The symbol is an EXPORTED BSS global, so dlsym gives its real runtime
* address (robust to load bias) and the slot is writable (no mprotect). Our own resolve
* barrier uses p_Submit (a distinct driver pointer), so it never re-enters this hook. */
static PFN_vkQueueSubmit s_realQueueSubmit;
static void **s_ueSubmitSlot;
static int s_hookInstalled;
static VKAPI_ATTR VkResult VKAPI_CALL ue_submit_trampoline(
VkQueue queue, uint32_t count, const VkSubmitInfo *pSubmits, VkFence fence) {
VkResult r = s_realQueueSubmit(queue, count, pSubmits, fence); /* real submit first */
/* notify after it's on the queue, so a same-queue barrier orders FIFO after it */
xrr_on_ue_submit((uint64_t)(uintptr_t)queue, (uint64_t)(uintptr_t)fence, queue == s_queue);
return r;
}
int xrr_install_submit_hook(void) {
if (s_hookInstalled) return 1;
void *h = dlopen("libUE4.so", RTLD_NOLOAD | RTLD_NOW);
if (!h) { XRRERR("submithook: libUE4.so not loaded"); return 0; }
void **slot = (void **)dlsym(h, "_ZN16VulkanDynamicAPI13vkQueueSubmitE");
if (!slot || !*slot) {
XRRERR("submithook: global vkQueueSubmit slot=%p val=%p (RHI not up yet?)",
(void *)slot, slot ? *slot : NULL);
return 0;
}
/* sanity: the live value must look like a real vkQueueSubmit, not garbage */
Dl_info di; const char *snm = "?", *fnm = "?";
if (dladdr(*slot, &di)) { if (di.dli_sname) snm = di.dli_sname; if (di.dli_fname) fnm = di.dli_fname; }
XRRLOG("submithook: UE vkQueueSubmit slot=%p -> %p (%s in %s)", (void *)slot, *slot, snm, fnm);
if (*slot == (void *)&ue_submit_trampoline) { s_hookInstalled = 1; return 1; } /* already ours */
s_realQueueSubmit = (PFN_vkQueueSubmit)*slot;
s_ueSubmitSlot = slot;
*slot = (void *)&ue_submit_trampoline; /* patch the global */
s_hookInstalled = 1;
XRRLOG("submithook: INSTALLED (real=%p tramp=%p, s_queue=%p)",
(void *)s_realQueueSubmit, (void *)&ue_submit_trampoline, (void *)s_queue);
return 1;
}
/* ----------------------------------------------- one-shot texture readback -- *
* Copy one array layer of an eye swapchain image to a host buffer and dump a
* downsampled PPM, so we can SEE whether UE's rendered eye texture is itself
* doubled (=> game/UE render) or clean (=> the compositor adds the dupe). Slow &
* synchronous — gated behind debug.re4vr.dump, fired once. */
static PFN_vkGetPhysicalDeviceMemoryProperties p_MemProps;
static PFN_vkCreateBuffer p_CreateBuf;
static PFN_vkGetBufferMemoryRequirements p_BufReq;
static PFN_vkAllocateMemory p_AllocMem;
static PFN_vkBindBufferMemory p_BindBuf;
static PFN_vkMapMemory p_MapMem;
static PFN_vkCmdCopyImageToBuffer p_Copy2Buf;
static PFN_vkDestroyBuffer p_DestroyBuf;
static PFN_vkFreeMemory p_FreeMem;
static int dump_lazy(PFN_vkGetDeviceProcAddr gdpa) {
#define DL(p,n) if(!p){ p=(PFN_##n)gdpa(s_dev,#n); if(!p){XRRERR("dump: missing " #n); return 0;} }
DL(p_CreateBuf, vkCreateBuffer) DL(p_BufReq, vkGetBufferMemoryRequirements)
DL(p_AllocMem, vkAllocateMemory) DL(p_BindBuf, vkBindBufferMemory)
DL(p_MapMem, vkMapMemory) DL(p_Copy2Buf, vkCmdCopyImageToBuffer)
DL(p_DestroyBuf, vkDestroyBuffer) DL(p_FreeMem, vkFreeMemory)
#undef DL
return 1;
}
void xrr_vk_dump_image(uint64_t image, unsigned int w, unsigned int h,
unsigned int arrayLayer, const char *path) {
if (!vk_lazy_init() || !s_phys) { XRRERR("dump: not ready"); return; }
PFN_vkGetDeviceProcAddr gdpa; PFN_vkGetInstanceProcAddr gipa; vk_loaders(&gdpa, &gipa);
if (!gdpa || !dump_lazy(gdpa)) return;
if (!p_MemProps) p_MemProps = (gipa && s_inst) ? (PFN_vkGetPhysicalDeviceMemoryProperties)
gipa(s_inst, "vkGetPhysicalDeviceMemoryProperties") : NULL;
if (!p_MemProps) { XRRERR("dump: no MemProps (inst=%p)", (void*)s_inst); return; }
VkDeviceSize sz = (VkDeviceSize)w * h * 4;
VkBufferCreateInfo bci = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO };
bci.size = sz; bci.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VkBuffer buf = VK_NULL_HANDLE;
if (p_CreateBuf(s_dev, &bci, NULL, &buf) != VK_SUCCESS) { XRRERR("dump: CreateBuffer"); return; }
VkMemoryRequirements mr; p_BufReq(s_dev, buf, &mr);
VkPhysicalDeviceMemoryProperties mp; p_MemProps(s_phys, &mp);
uint32_t mt = UINT32_MAX;
for (uint32_t i = 0; i < mp.memoryTypeCount; i++)
if ((mr.memoryTypeBits & (1u<<i)) &&
(mp.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) &&
(mp.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) { mt = i; break; }
if (mt == UINT32_MAX) { XRRERR("dump: no host-visible mem"); p_DestroyBuf(s_dev,buf,NULL); return; }
VkMemoryAllocateInfo mai = { VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
mai.allocationSize = mr.size; mai.memoryTypeIndex = mt;
VkDeviceMemory mem = VK_NULL_HANDLE;
if (p_AllocMem(s_dev, &mai, NULL, &mem) != VK_SUCCESS) { XRRERR("dump: AllocMem"); p_DestroyBuf(s_dev,buf,NULL); return; }
p_BindBuf(s_dev, buf, mem, 0);
int idx = s_ring++ % XRR_FLUSH_RING;
p_WaitFences(s_dev, 1, &s_fence[idx], VK_TRUE, 100000000);
p_ResetFences(s_dev, 1, &s_fence[idx]);
p_ResetCmd(s_cmd[idx], 0);
VkCommandBufferBeginInfo cbi = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO };
cbi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
p_BeginCmd(s_cmd[idx], &cbi);
VkImageMemoryBarrier b = { VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER };
b.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
b.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
b.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
b.srcQueueFamilyIndex = b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
b.image = (VkImage)(uintptr_t)image;
b.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
b.subresourceRange.levelCount = 1; b.subresourceRange.baseArrayLayer = arrayLayer;
b.subresourceRange.layerCount = 1;
p_Barrier(s_cmd[idx], VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, NULL, 0, NULL, 1, &b);
VkBufferImageCopy rgn; memset(&rgn, 0, sizeof(rgn));
rgn.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
rgn.imageSubresource.mipLevel = 0;
rgn.imageSubresource.baseArrayLayer = arrayLayer;
rgn.imageSubresource.layerCount = 1;
rgn.imageExtent.width = w; rgn.imageExtent.height = h; rgn.imageExtent.depth = 1;
p_Copy2Buf(s_cmd[idx], (VkImage)(uintptr_t)image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, buf, 1, &rgn);
/* restore layout so the compositor/UE is unaffected */
b.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; b.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
b.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; b.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
p_Barrier(s_cmd[idx], VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, NULL, 0, NULL, 1, &b);
p_EndCmd(s_cmd[idx]);
VkSubmitInfo si = { VK_STRUCTURE_TYPE_SUBMIT_INFO };
si.commandBufferCount = 1; si.pCommandBuffers = &s_cmd[idx];
if (p_Submit(s_queue, 1, &si, s_fence[idx]) != VK_SUCCESS) { XRRERR("dump: submit"); goto cleanup; }
p_WaitFences(s_dev, 1, &s_fence[idx], VK_TRUE, 1000000000);
{ /* downsample to <=256 wide PPM (P6). Format is RGBA8; take RGB. */
uint8_t *px = NULL;
if (p_MapMem(s_dev, mem, 0, sz, 0, (void**)&px) != VK_SUCCESS || !px) { XRRERR("dump: map"); goto cleanup; }
unsigned step = (w > 256) ? (w / 256) : 1;
unsigned ow = w / step, oh = h / step;
FILE *f = fopen(path, "wb");
if (!f) { XRRERR("dump: fopen %s failed (errno path?)", path); goto cleanup; }
fprintf(f, "P6\n%u %u\n255\n", ow, oh);
for (unsigned y = 0; y < oh; y++)
for (unsigned x = 0; x < ow; x++) {
uint8_t *p = px + ((size_t)(y*step)*w + (x*step)) * 4;
fwrite(p, 1, 3, f);
}
fclose(f);
XRRLOG("dump: wrote %s (%ux%u from %ux%u layer=%u)", path, ow, oh, w, h, arrayLayer);
}
cleanup:
p_FreeMem(s_dev, mem, NULL);
p_DestroyBuf(s_dev, buf, NULL);
}
/* ---- frame-counter barcode overlay (debug.re4vr.barcode) -------------------
* Stamp a black/white binary barcode of `value` (the frameIndex we already log) into
* the top-left of the eye image AFTER UE's render is resolved, so it appears on EVERY
* frame including truncated/black ones. Lets a Meta Cast recording be aligned
* frame-exactly to the FRAME/SKIPBLACK logs: read the bits on a black flash -> frameIndex
* -> grep the log. Black/white survives cast-video compression; subtle color encoding
* does not. Validation-only; gated off by default. A persistent host-visible staging
* buffer is filled on the CPU each frame and copied in (no per-frame alloc). */
#define BC_BITS 16 /* frameIndex low 16 bits (~15 min @72Hz before wrap) */
#define BC_CELLS (BC_BITS + 1) /* +1 leading anchor cell (always white = strip locator) */
#define BC_CELL_W 24
#define BC_CELL_H 28
#define BC_GUTTER 2 /* black gap framing each cell so bits never merge */
#define BC_W (BC_CELLS * BC_CELL_W)
#define BC_H (BC_CELL_H + 2 * BC_GUTTER)
static PFN_vkCmdCopyBufferToImage p_Copy2Img;
static VkBuffer s_bcBuf;
static VkDeviceMemory s_bcMem;
static uint8_t *s_bcPx; /* persistent mapping of the staging buffer */
/* lazily create + map the persistent staging buffer and load the copy PFN. */
static int barcode_lazy(PFN_vkGetDeviceProcAddr gdpa) {
#define BL(p,n) if(!p){ p=(PFN_##n)gdpa(s_dev,#n); if(!p){XRRERR("barcode: missing " #n); return 0;} }
BL(p_CreateBuf, vkCreateBuffer) BL(p_BufReq, vkGetBufferMemoryRequirements)
BL(p_AllocMem, vkAllocateMemory) BL(p_BindBuf, vkBindBufferMemory)
BL(p_MapMem, vkMapMemory) BL(p_Copy2Img, vkCmdCopyBufferToImage)
#undef BL
if (s_bcBuf) return 1;
VkDeviceSize sz = (VkDeviceSize)BC_W * BC_H * 4;
VkBufferCreateInfo bci = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO };
bci.size = sz; bci.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (p_CreateBuf(s_dev, &bci, NULL, &s_bcBuf) != VK_SUCCESS) { XRRERR("barcode: CreateBuffer"); return 0; }
VkMemoryRequirements mr; p_BufReq(s_dev, s_bcBuf, &mr);
VkPhysicalDeviceMemoryProperties mp; p_MemProps(s_phys, &mp);
uint32_t mt = UINT32_MAX;
for (uint32_t i = 0; i < mp.memoryTypeCount; i++)
if ((mr.memoryTypeBits & (1u<<i)) &&
(mp.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) &&
(mp.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) { mt = i; break; }
if (mt == UINT32_MAX) { XRRERR("barcode: no host-visible mem"); return 0; }
VkMemoryAllocateInfo mai = { VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
mai.allocationSize = mr.size; mai.memoryTypeIndex = mt;
if (p_AllocMem(s_dev, &mai, NULL, &s_bcMem) != VK_SUCCESS) { XRRERR("barcode: AllocMem"); return 0; }
p_BindBuf(s_dev, s_bcBuf, s_bcMem, 0);
if (p_MapMem(s_dev, s_bcMem, 0, sz, 0, (void**)&s_bcPx) != VK_SUCCESS || !s_bcPx) {
XRRERR("barcode: map"); s_bcPx = NULL; return 0;
}
return 1;
}
/* ---- cheap per-frame black detector (debug.re4vr.lumagate) -----------------
* Sample a few full-width rows of the ALREADY-RESOLVED eye image and return the max
* luminance (0..255). A truncated/black frame reads ~0 across all rows; any lit scene has
* bright pixels somewhere on a sampled row -> high max. Unlike the onset-only post-hitch
* flag, this is PER-FRAME so it covers the whole sustained-black tail. The dump's
* observer-effect concern doesn't apply: the sync path already flush_waits the resolve, and
* the black is real content (UE renders empty) — we just read what's there. Call AFTER the
* resolve wait and BEFORE the barcode stamp (so the strip's white cells don't pollute it). */
#define LUMA_ROWS 5
static VkBuffer s_luBuf;
static VkDeviceMemory s_luMem;
static uint8_t *s_luPx;
static unsigned s_luCap; /* bytes allocated */
int xrr_vk_frame_luma(uint64_t image, unsigned int w, unsigned int h, unsigned int arrayLayer) {
if (!vk_lazy_init() || !s_phys) return -1;
PFN_vkGetDeviceProcAddr gdpa; PFN_vkGetInstanceProcAddr gipa; vk_loaders(&gdpa, &gipa);
if (!gdpa || !dump_lazy(gdpa)) return -1; /* dump_lazy loads CreateBuf/Copy2Buf/Map/etc */
if (!p_MemProps) p_MemProps = (gipa && s_inst) ? (PFN_vkGetPhysicalDeviceMemoryProperties)
gipa(s_inst, "vkGetPhysicalDeviceMemoryProperties") : NULL;
if (!p_MemProps) return -1;
unsigned need = LUMA_ROWS * w * 4;
if (!s_luBuf || need > s_luCap) {
if (s_luBuf) { p_DestroyBuf(s_dev, s_luBuf, NULL); p_FreeMem(s_dev, s_luMem, NULL); s_luBuf = VK_NULL_HANDLE; s_luPx = NULL; }
VkBufferCreateInfo bci = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO };
bci.size = need; bci.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (p_CreateBuf(s_dev, &bci, NULL, &s_luBuf) != VK_SUCCESS) { XRRERR("luma: CreateBuffer"); return -1; }
VkMemoryRequirements mr; p_BufReq(s_dev, s_luBuf, &mr);
VkPhysicalDeviceMemoryProperties mp; p_MemProps(s_phys, &mp);
uint32_t mt = UINT32_MAX;
for (uint32_t i = 0; i < mp.memoryTypeCount; i++)
if ((mr.memoryTypeBits & (1u<<i)) &&
(mp.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) &&
(mp.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) { mt = i; break; }
if (mt == UINT32_MAX) { XRRERR("luma: no host-visible mem"); return -1; }
VkMemoryAllocateInfo mai = { VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
mai.allocationSize = mr.size; mai.memoryTypeIndex = mt;
if (p_AllocMem(s_dev, &mai, NULL, &s_luMem) != VK_SUCCESS) { XRRERR("luma: AllocMem"); return -1; }
p_BindBuf(s_dev, s_luBuf, s_luMem, 0);
if (p_MapMem(s_dev, s_luMem, 0, need, 0, (void**)&s_luPx) != VK_SUCCESS || !s_luPx) { XRRERR("luma: map"); s_luPx = NULL; return -1; }
s_luCap = need;
}
int idx = s_ring++ % XRR_FLUSH_RING;
p_WaitFences(s_dev, 1, &s_fence[idx], VK_TRUE, 100000000);
p_ResetFences(s_dev, 1, &s_fence[idx]);
p_ResetCmd(s_cmd[idx], 0);
VkCommandBufferBeginInfo cbi = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO };
cbi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
p_BeginCmd(s_cmd[idx], &cbi);
VkImageMemoryBarrier b = { VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER };
b.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; b.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
b.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
b.srcQueueFamilyIndex = b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
b.image = (VkImage)(uintptr_t)image; b.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
b.subresourceRange.levelCount = 1; b.subresourceRange.baseArrayLayer = arrayLayer; b.subresourceRange.layerCount = 1;
p_Barrier(s_cmd[idx], VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,0,NULL,0,NULL,1,&b);
/* sample LUMA_ROWS rows spread down the frame (skip y~0.30h where the barcode sits). */
VkBufferImageCopy rgn[LUMA_ROWS]; memset(rgn, 0, sizeof(rgn));
const float fy[LUMA_ROWS] = { 0.15f, 0.40f, 0.55f, 0.70f, 0.85f };
for (int i = 0; i < LUMA_ROWS; i++) {
rgn[i].bufferOffset = (VkDeviceSize)i * w * 4;
rgn[i].bufferRowLength = w; rgn[i].bufferImageHeight = 1;
rgn[i].imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
rgn[i].imageSubresource.baseArrayLayer = arrayLayer; rgn[i].imageSubresource.layerCount = 1;
rgn[i].imageOffset.y = (int)(h * fy[i]);
rgn[i].imageExtent.width = w; rgn[i].imageExtent.height = 1; rgn[i].imageExtent.depth = 1;
}
p_Copy2Buf(s_cmd[idx], (VkImage)(uintptr_t)image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, s_luBuf, LUMA_ROWS, rgn);
b.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; b.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
b.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; b.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
p_Barrier(s_cmd[idx], VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0,0,NULL,0,NULL,1,&b);
p_EndCmd(s_cmd[idx]);
VkSubmitInfo si = { VK_STRUCTURE_TYPE_SUBMIT_INFO };
si.commandBufferCount = 1; si.pCommandBuffers = &s_cmd[idx];
if (p_Submit(s_queue, 1, &si, s_fence[idx]) != VK_SUCCESS) { XRRERR("luma: submit"); return -1; }
p_WaitFences(s_dev, 1, &s_fence[idx], VK_TRUE, 100000000);
/* max luminance over the sampled pixels (RGBA8; max channel is enough for "any light"). */
int mx = 0; unsigned npx = LUMA_ROWS * w;
for (unsigned i = 0; i < npx; i++) {
uint8_t *p = s_luPx + (size_t)i * 4;
int m = p[0]; if (p[1] > m) m = p[1]; if (p[2] > m) m = p[2];
if (m > mx) mx = m;
}
return mx;
}
void xrr_vk_stamp_barcode(uint64_t image, unsigned int w, unsigned int h,
unsigned int arrayLayer, unsigned int value, int flagged) {
if (!vk_lazy_init() || !s_phys) return;
PFN_vkGetDeviceProcAddr gdpa; PFN_vkGetInstanceProcAddr gipa; vk_loaders(&gdpa, &gipa);
if (!gdpa) return;
if (!p_MemProps) p_MemProps = (gipa && s_inst) ? (PFN_vkGetPhysicalDeviceMemoryProperties)
gipa(s_inst, "vkGetPhysicalDeviceMemoryProperties") : NULL;
if (!p_MemProps) { XRRERR("barcode: no MemProps"); return; }
if (!barcode_lazy(gdpa) || !s_bcPx) return;
/* paint the strip on the CPU as a GRAY ruler (so all 17 cell positions stay visible on
* ANY frame background, incl. pure black) with each cell BLACK(0) or WHITE(1) on top,
* gray gutters framing them. anchor cell c=0 = always white (strip locator); cells 1..16
* = bits of `value`, LSB at cell 1. Three levels (black/gray/white) survive cast-video
* compression. RGBA8 — these grays are channel-equal so RGBA/BGRA/sRGB don't matter. */
const uint32_t BLACK = 0xFF000000u, WHITE = 0xFFFFFFFFu, GRAY = 0xFF808080u;
/* RED when this frame is a flagged LIKELY-TRUNCATED candidate, else gray. (If the
* swapchain is BGRA the red reads as blue — still clearly != gray, so the flag is
* unambiguous either way.) Lets the recording show flag-vs-black by eye. */
const uint32_t REDBG = 0xFF0000FFu;
uint32_t bg = flagged ? REDBG : GRAY;
uint32_t *px = (uint32_t*)s_bcPx;
for (unsigned i = 0; i < (unsigned)(BC_W * BC_H); i++) px[i] = bg;
for (unsigned c = 0; c < BC_CELLS; c++) {
int on = (c == 0) ? 1 : (int)((value >> (c - 1)) & 1u);
uint32_t col = on ? WHITE : BLACK;
unsigned x0 = c * BC_CELL_W + BC_GUTTER, x1 = (c + 1) * BC_CELL_W - BC_GUTTER;
for (unsigned y = BC_GUTTER; y < BC_GUTTER + BC_CELL_H; y++)
for (unsigned x = x0; x < x1; x++) px[y * BC_W + x] = col;
}
int idx = s_ring++ % XRR_FLUSH_RING;
p_WaitFences(s_dev, 1, &s_fence[idx], VK_TRUE, 100000000);
p_ResetFences(s_dev, 1, &s_fence[idx]);
p_ResetCmd(s_cmd[idx], 0);
VkCommandBufferBeginInfo cbi = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO };
cbi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
p_BeginCmd(s_cmd[idx], &cbi);
/* eye color images live in COLOR_ATTACHMENT_OPTIMAL here (same assumption as the
* resolve path); flip the layer to TRANSFER_DST, copy the strip, flip back. */
VkImageMemoryBarrier b = { VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER };
b.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
b.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
b.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
b.srcQueueFamilyIndex = b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
b.image = (VkImage)(uintptr_t)image;
b.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
b.subresourceRange.levelCount = 1; b.subresourceRange.baseArrayLayer = arrayLayer;
b.subresourceRange.layerCount = 1;
p_Barrier(s_cmd[idx], VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, NULL, 0, NULL, 1, &b);
VkBufferImageCopy rgn; memset(&rgn, 0, sizeof(rgn));
rgn.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
rgn.imageSubresource.mipLevel = 0;
rgn.imageSubresource.baseArrayLayer = arrayLayer;
rgn.imageSubresource.layerCount = 1;
/* upper-center: the Meta Cast crops the eye render to 16:9 (top/bottom letterboxed
* off), and the extreme FOV corner is unviewable in-headset — so (0,0) is invisible.
* Center horizontally; place ~30% down, inside the cast's visible band. */
rgn.imageOffset.x = (w > BC_W) ? (int)((w - BC_W) / 2) : 0;
rgn.imageOffset.y = (h > BC_H) ? (int)(h * 30 / 100) : 0;
rgn.imageExtent.width = BC_W; rgn.imageExtent.height = BC_H; rgn.imageExtent.depth = 1;
p_Copy2Img(s_cmd[idx], s_bcBuf, (VkImage)(uintptr_t)image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &rgn);
b.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; b.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
b.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; b.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
p_Barrier(s_cmd[idx], VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, NULL, 0, NULL, 1, &b);
p_EndCmd(s_cmd[idx]);
VkSubmitInfo si = { VK_STRUCTURE_TYPE_SUBMIT_INFO };
si.commandBufferCount = 1; si.pCommandBuffers = &s_cmd[idx];
if (p_Submit(s_queue, 1, &si, s_fence[idx]) != VK_SUCCESS) { XRRERR("barcode: submit"); return; }
p_WaitFences(s_dev, 1, &s_fence[idx], VK_TRUE, 100000000); /* tiny copy; done before release */
}
/* ------------------------------------------------- shim-owned copy ring ----- *
* UE renders into these shim images (handed to it via GetLayerTexture2) on its own
* stage cadence; each frame we copy shimImages[TextureStage] into the freshly
* acquired OpenXR image. The copy both resolves tile memory AND lets the frame loop
* pipeline it (wait the PREVIOUS frame's copy, already done) so the CPU never blocks
* on the current frame's GPU -> breaks the flush-wait serialization. Decoupled from
* UE's stage so no stage-coupling break. Gated by debug.re4vr.copyring (off default). */
static PFN_vkCreateImage p_CreateImage;
static PFN_vkGetImageMemoryRequirements p_ImgReq;
static PFN_vkBindImageMemory p_BindImg;
static PFN_vkDestroyImage p_DestroyImage;
static PFN_vkCmdCopyImage p_CopyImg;
static int copyring_lazy(PFN_vkGetDeviceProcAddr gdpa) {
#define CL(p,n) if(!p){ p=(PFN_##n)gdpa(s_dev,#n); if(!p){XRRERR("copyring: missing " #n); return 0;} }
CL(p_CreateImage, vkCreateImage) CL(p_ImgReq, vkGetImageMemoryRequirements)
CL(p_BindImg, vkBindImageMemory) CL(p_DestroyImage, vkDestroyImage)
CL(p_CopyImg, vkCmdCopyImage)
/* memory PFNs reused from dump path */
CL(p_AllocMem, vkAllocateMemory) CL(p_FreeMem, vkFreeMemory)
#undef CL
return 1;
}
/* Allocate `count` device-local color images (matching the OpenXR eye swapchain) for
* UE to render into. Returns image handles in out[], backing memory in outMem[]. */
int xrr_vk_alloc_images_ex(uint64_t *out, uint64_t *outMem, int count,
unsigned int w, unsigned int h, unsigned int arraySize,
long long vkFormat, int isDepth) {
if (!vk_lazy_init() || !s_phys) { XRRERR("copyring: vk not ready"); return 0; }
PFN_vkGetDeviceProcAddr gdpa; PFN_vkGetInstanceProcAddr gipa; vk_loaders(&gdpa, &gipa);
if (!gdpa || !copyring_lazy(gdpa)) return 0;
if (!p_MemProps) p_MemProps = (gipa && s_inst) ? (PFN_vkGetPhysicalDeviceMemoryProperties)
gipa(s_inst, "vkGetPhysicalDeviceMemoryProperties") : NULL;
if (!p_MemProps) { XRRERR("copyring: no MemProps"); return 0; }
VkPhysicalDeviceMemoryProperties mp; p_MemProps(s_phys, &mp);
for (int i = 0; i < count; i++) {
VkImageCreateInfo ici = { VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
/* MUTABLE_FORMAT: UE renders the eye target through a linear (UNORM) image view
* but samples/displays it as sRGB — that needs format-mutable views, which
* OpenXR runtimes put on their swapchain images. Without it UE's gameplay
* render path (linear view) fails -> black, while the title path works. */
ici.flags = isDepth ? 0 : VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
ici.imageType = VK_IMAGE_TYPE_2D;
ici.format = (VkFormat)vkFormat;
ici.extent.width = w; ici.extent.height = h; ici.extent.depth = 1;
ici.mipLevels = 1; ici.arrayLayers = arraySize ? arraySize : 1;
ici.samples = VK_SAMPLE_COUNT_1_BIT; ici.tiling = VK_IMAGE_TILING_OPTIMAL;
/* Match the broad usage Meta gives its own swapchain images — UE's mobile
* gameplay path uses the eye target as an INPUT_ATTACHMENT (subpass resolve)
* and may clear it via transfer; a narrow usage makes gameplay render black
* while the simpler title path still works. Depth hold-images (Lever C) use the
* depth-stencil attachment usage instead so the layout transitions are valid. */
ici.usage = isDepth
? (VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT |
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT)
: (VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT |
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT);
ici.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
ici.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VkImage img = VK_NULL_HANDLE;
if (p_CreateImage(s_dev, &ici, NULL, &img) != VK_SUCCESS) { XRRERR("copyring: CreateImage %d", i); return 0; }
VkMemoryRequirements mr; p_ImgReq(s_dev, img, &mr);
uint32_t mt = UINT32_MAX;
for (uint32_t k = 0; k < mp.memoryTypeCount; k++)
if ((mr.memoryTypeBits & (1u<<k)) &&
(mp.memoryTypes[k].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)) { mt = k; break; }
if (mt == UINT32_MAX) { XRRERR("copyring: no device-local mem"); p_DestroyImage(s_dev,img,NULL); return 0; }
VkMemoryAllocateInfo mai = { VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
mai.allocationSize = mr.size; mai.memoryTypeIndex = mt;
VkDeviceMemory mem = VK_NULL_HANDLE;
if (p_AllocMem(s_dev, &mai, NULL, &mem) != VK_SUCCESS) { XRRERR("copyring: AllocMem %d", i); p_DestroyImage(s_dev,img,NULL); return 0; }
p_BindImg(s_dev, img, mem, 0);
out[i] = (uint64_t)(uintptr_t)img;
outMem[i] = (uint64_t)(uintptr_t)mem;
}
XRRLOG("copyring: allocated %d shim images %ux%u array=%u fmt=%lld depth=%d", count, w, h, arraySize, vkFormat, isDepth);
return 1;
}
/* color-image convenience wrapper (the common case) */
int xrr_vk_alloc_images(uint64_t *out, uint64_t *outMem, int count,
unsigned int w, unsigned int h, unsigned int arraySize,
long long vkFormat) {
return xrr_vk_alloc_images_ex(out, outMem, count, w, h, arraySize, vkFormat, 0);
}
void xrr_vk_free_images(uint64_t *imgs, uint64_t *mem, int count) {
if (!s_vkReady || !p_DestroyImage) return;
for (int i = 0; i < count; i++) {
if (imgs[i]) p_DestroyImage(s_dev, (VkImage)(uintptr_t)imgs[i], NULL);
if (mem[i]) p_FreeMem(s_dev, (VkDeviceMemory)(uintptr_t)mem[i], NULL);
imgs[i] = mem[i] = 0;
}
}
/* Copy image src -> dst (all array layers), resolving tile memory, WITHOUT waiting.
* Returns a ring token for xrr_vk_flush_wait. Used bidirectionally (swapchain<->shim hold
* image) for reproject save/restore. isDepth selects the depth aspect + depth-stencil
* attachment layouts (Lever C depth-hold); both endpoints rest in *_ATTACHMENT_OPTIMAL so
* the same barriers work either direction (dst content is discarded via UNDEFINED). */
int xrr_vk_copy_submit_ex(uint64_t srcShim, uint64_t dstXr,
unsigned int w, unsigned int h, unsigned int arrayLayers, int isDepth) {
if (!vk_lazy_init() || !p_CopyImg) return -1;
int idx = s_ring++ % XRR_FLUSH_RING;
p_WaitFences(s_dev, 1, &s_fence[idx], VK_TRUE, 100000000);
p_ResetFences(s_dev, 1, &s_fence[idx]);
p_ResetCmd(s_cmd[idx], 0);
VkCommandBufferBeginInfo bi = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO };
bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
p_BeginCmd(s_cmd[idx], &bi);
uint32_t layers = arrayLayers ? arrayLayers : 1;
VkImage src = (VkImage)(uintptr_t)srcShim, dst = (VkImage)(uintptr_t)dstXr;
VkImageAspectFlags aspect = isDepth ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT;
VkImageLayout attachLayout = isDepth ? VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL
: VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkAccessFlags attachWrite = isDepth ? VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT
: VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
VkPipelineStageFlags attachStage = isDepth
? (VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT)
: VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
VkImageMemoryBarrier b[2]; memset(b, 0, sizeof(b));
for (int i = 0; i < 2; i++) {
b[i].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
b[i].srcQueueFamilyIndex = b[i].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
b[i].subresourceRange.aspectMask = aspect;
b[i].subresourceRange.levelCount = 1;
b[i].subresourceRange.layerCount = layers;
}
/* src: *_ATTACHMENT -> TRANSFER_SRC ; dst: UNDEFINED -> TRANSFER_DST */
b[0].srcAccessMask = attachWrite; b[0].dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
b[0].oldLayout = attachLayout; b[0].newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
b[0].image = src;
b[1].srcAccessMask = 0; b[1].dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
b[1].oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; b[1].newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
b[1].image = dst;
p_Barrier(s_cmd[idx], attachStage | VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, NULL, 0, NULL, 2, b);
VkImageCopy rgn; memset(&rgn, 0, sizeof(rgn));
rgn.srcSubresource.aspectMask = aspect; rgn.srcSubresource.layerCount = layers;
rgn.dstSubresource.aspectMask = aspect; rgn.dstSubresource.layerCount = layers;
rgn.extent.width = w; rgn.extent.height = h; rgn.extent.depth = 1;
p_CopyImg(s_cmd[idx], src, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &rgn);
/* dst: TRANSFER_DST -> *_ATTACHMENT (compositor/UE reads) ; src: TRANSFER_SRC -> *_ATTACHMENT (reuse) */
b[0].srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; b[0].dstAccessMask = VK_ACCESS_MEMORY_READ_BIT;
b[0].oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; b[0].newLayout = attachLayout;
b[0].image = dst;
b[1].srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; b[1].dstAccessMask = attachWrite;
b[1].oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; b[1].newLayout = attachLayout;
b[1].image = src;
p_Barrier(s_cmd[idx], VK_PIPELINE_STAGE_TRANSFER_BIT,
attachStage | VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
0, 0, NULL, 0, NULL, 2, b);
p_EndCmd(s_cmd[idx]);
VkSubmitInfo si = { VK_STRUCTURE_TYPE_SUBMIT_INFO };
si.commandBufferCount = 1; si.pCommandBuffers = &s_cmd[idx];
if (p_Submit(s_queue, 1, &si, s_fence[idx]) != VK_SUCCESS) return -1;
return idx;
}
/* color-image convenience wrapper (the common case) */
int xrr_vk_copy_submit(uint64_t srcShim, uint64_t dstXr,
unsigned int w, unsigned int h, unsigned int arrayLayers) {
return xrr_vk_copy_submit_ex(srcShim, dstXr, w, h, arrayLayers, 0);
}
uint32_t xrr_vk_enumerate_images(XrSwapchain sc, uint64_t *out, uint32_t max) {
uint32_t n = 0;
if (xrEnumerateSwapchainImages(sc, 0, &n, NULL) != XR_SUCCESS) return 0;
if (n > max) n = max;
XrSwapchainImageVulkanKHR imgs[XRR_MAX_IMAGES];
for (uint32_t i = 0; i < n; i++) {
imgs[i].type = XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR;
imgs[i].next = NULL;
}
uint32_t got = 0;
if (xrEnumerateSwapchainImages(sc, n, &got,
(XrSwapchainImageBaseHeader *)imgs) != XR_SUCCESS)
return 0;
for (uint32_t i = 0; i < got; i++)
out[i] = (uint64_t)(uintptr_t)imgs[i].image;
return got;
}
/* Reset shim Vulkan state so a post-Shutdown2 re-init (new VkDevice from a fresh ovrp_Initialize5)
* rebuilds the command pool/fences/staging buffers instead of reusing handles from the dead device.
* Called from xrr_shutdown under g_xrlock. Objects from the old device are left to the driver/process
* to reclaim — destroying them needs PFNs we don't load, and Shutdown2 is normally process-exit. */
void xrr_vk_teardown(void) {
s_vkReady = 0; s_vkFailed = 0;
s_pool = VK_NULL_HANDLE;
for (int i = 0; i < XRR_FLUSH_RING; i++) { s_cmd[i] = VK_NULL_HANDLE; s_fence[i] = VK_NULL_HANDLE; }
s_bcBuf = VK_NULL_HANDLE; s_bcMem = VK_NULL_HANDLE; s_bcPx = NULL;
s_luBuf = VK_NULL_HANDLE; s_luMem = VK_NULL_HANDLE; s_luPx = NULL; s_luCap = 0;
}
+229
View File
@@ -0,0 +1,229 @@
/* xr_input.c — OpenXR action-based input mapped to ovrpControllerState4 + hand poses.
* Bindings target /interaction_profiles/oculus/touch_controller. ovrpButton bitmask
* values are [VERIFIED from OVR_Plugin_Types.h]. */
#include "xr_runtime.h"
#include "log.h"
#include <string.h>
/* ovrpButton bits */
#define OVRP_BTN_A 0x00000001
#define OVRP_BTN_B 0x00000002
#define OVRP_BTN_X 0x00000100
#define OVRP_BTN_Y 0x00000200
#define OVRP_BTN_START 0x00100000 /* left menu */
#define OVRP_BTN_LTHUMB 0x00000400
#define OVRP_BTN_RTHUMB 0x00000004
static XrActionSet s_set;
static XrAction a_A, a_B, a_X, a_Y, a_menu, a_lstickc, a_rstickc;
static XrAction a_ltrig, a_rtrig, a_lgrip, a_rgrip, a_lstick, a_rstick;
static XrAction a_lpose, a_rpose;
static XrAction a_lhaptic, a_rhaptic;
static XrSpace s_lspace, s_rspace;
static struct {
uint32_t buttons;
float ltrig, rtrig, lgrip, rgrip;
ovrpVector2f lstick, rstick;
XrPosef lpose, rpose; int lvalid, rvalid;
} s_in;
static XrAction mk(XrActionType t, const char *n) {
XrActionCreateInfo ci = { XR_TYPE_ACTION_CREATE_INFO };
ci.actionType = t;
strncpy(ci.actionName, n, XR_MAX_ACTION_NAME_SIZE - 1);
strncpy(ci.localizedActionName, n, XR_MAX_LOCALIZED_ACTION_NAME_SIZE - 1);
XrAction a = XR_NULL_HANDLE;
xrCreateAction(s_set, &ci, &a);
return a;
}
int xrr_input_init(void) {
if (g_xr.instance == XR_NULL_HANDLE || g_xr.session == XR_NULL_HANDLE) return 0;
XrActionSetCreateInfo asci = { XR_TYPE_ACTION_SET_CREATE_INFO };
strcpy(asci.actionSetName, "gameplay");
strcpy(asci.localizedActionSetName, "gameplay");
if (xrCreateActionSet(g_xr.instance, &asci, &s_set) != XR_SUCCESS) return 0;
a_A = mk(XR_ACTION_TYPE_BOOLEAN_INPUT, "a_btn");
a_B = mk(XR_ACTION_TYPE_BOOLEAN_INPUT, "b_btn");
a_X = mk(XR_ACTION_TYPE_BOOLEAN_INPUT, "x_btn");
a_Y = mk(XR_ACTION_TYPE_BOOLEAN_INPUT, "y_btn");
a_menu = mk(XR_ACTION_TYPE_BOOLEAN_INPUT, "menu_btn");
a_lstickc = mk(XR_ACTION_TYPE_BOOLEAN_INPUT, "lstick_click");
a_rstickc = mk(XR_ACTION_TYPE_BOOLEAN_INPUT, "rstick_click");
a_ltrig = mk(XR_ACTION_TYPE_FLOAT_INPUT, "ltrigger");
a_rtrig = mk(XR_ACTION_TYPE_FLOAT_INPUT, "rtrigger");
a_lgrip = mk(XR_ACTION_TYPE_FLOAT_INPUT, "lgrip");
a_rgrip = mk(XR_ACTION_TYPE_FLOAT_INPUT, "rgrip");
a_lstick = mk(XR_ACTION_TYPE_VECTOR2F_INPUT, "lstick");
a_rstick = mk(XR_ACTION_TYPE_VECTOR2F_INPUT, "rstick");
a_lpose = mk(XR_ACTION_TYPE_POSE_INPUT, "lpose");
a_rpose = mk(XR_ACTION_TYPE_POSE_INPUT, "rpose");
a_lhaptic = mk(XR_ACTION_TYPE_VIBRATION_OUTPUT, "lhaptic");
a_rhaptic = mk(XR_ACTION_TYPE_VIBRATION_OUTPUT, "rhaptic");
XrActionSuggestedBinding b[32]; int n = 0; XrPath p;
#define BIND(act, path) do { if (xrStringToPath(g_xr.instance, path, &p) == XR_SUCCESS) \
{ b[n].action = (act); b[n].binding = p; n++; } } while (0)
BIND(a_A, "/user/hand/right/input/a/click");
BIND(a_B, "/user/hand/right/input/b/click");
BIND(a_X, "/user/hand/left/input/x/click");
BIND(a_Y, "/user/hand/left/input/y/click");
BIND(a_menu, "/user/hand/left/input/menu/click");
BIND(a_lstickc, "/user/hand/left/input/thumbstick/click");
BIND(a_rstickc, "/user/hand/right/input/thumbstick/click");
BIND(a_ltrig, "/user/hand/left/input/trigger/value");
BIND(a_rtrig, "/user/hand/right/input/trigger/value");
BIND(a_lgrip, "/user/hand/left/input/squeeze/value");
BIND(a_rgrip, "/user/hand/right/input/squeeze/value");
BIND(a_lstick, "/user/hand/left/input/thumbstick");
BIND(a_rstick, "/user/hand/right/input/thumbstick");
BIND(a_lpose, "/user/hand/left/input/aim/pose");
BIND(a_rpose, "/user/hand/right/input/aim/pose");
BIND(a_lhaptic, "/user/hand/left/output/haptic");
BIND(a_rhaptic, "/user/hand/right/output/haptic");
XrPath profile;
xrStringToPath(g_xr.instance, "/interaction_profiles/oculus/touch_controller", &profile);
XrInteractionProfileSuggestedBinding sb = { XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING };
sb.interactionProfile = profile;
sb.suggestedBindings = b;
sb.countSuggestedBindings = n;
if (xrSuggestInteractionProfileBindings(g_xr.instance, &sb) != XR_SUCCESS)
XRRERR("suggest bindings failed");
XrActionSpaceCreateInfo spci = { XR_TYPE_ACTION_SPACE_CREATE_INFO };
spci.poseInActionSpace.orientation.w = 1.0f;
spci.action = a_lpose; xrCreateActionSpace(g_xr.session, &spci, &s_lspace);
spci.action = a_rpose; xrCreateActionSpace(g_xr.session, &spci, &s_rspace);
XrSessionActionSetsAttachInfo at = { XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO };
at.countActionSets = 1; at.actionSets = &s_set;
if (xrAttachSessionActionSets(g_xr.session, &at) != XR_SUCCESS) {
XRRERR("attach action sets failed"); return 0;
}
XRRLOG("input: action set attached (%d bindings)", n);
return 1;
}
static int bget(XrAction a) {
XrActionStateGetInfo gi = { XR_TYPE_ACTION_STATE_GET_INFO }; gi.action = a;
XrActionStateBoolean st = { XR_TYPE_ACTION_STATE_BOOLEAN };
xrGetActionStateBoolean(g_xr.session, &gi, &st);
return st.isActive && st.currentState;
}
static float fget(XrAction a) {
XrActionStateGetInfo gi = { XR_TYPE_ACTION_STATE_GET_INFO }; gi.action = a;
XrActionStateFloat st = { XR_TYPE_ACTION_STATE_FLOAT };
xrGetActionStateFloat(g_xr.session, &gi, &st);
return st.isActive ? st.currentState : 0.0f;
}
static ovrpVector2f v2get(XrAction a) {
XrActionStateGetInfo gi = { XR_TYPE_ACTION_STATE_GET_INFO }; gi.action = a;
XrActionStateVector2f st = { XR_TYPE_ACTION_STATE_VECTOR2F };
xrGetActionStateVector2f(g_xr.session, &gi, &st);
ovrpVector2f v = { 0, 0 };
if (st.isActive) { v.x = st.currentState.x; v.y = st.currentState.y; }
return v;
}
void xrr_input_sync(void) {
if (s_set == XR_NULL_HANDLE || !g_xr.running) return;
XrActiveActionSet aas = { s_set, XR_NULL_PATH };
XrActionsSyncInfo si = { XR_TYPE_ACTIONS_SYNC_INFO };
si.countActiveActionSets = 1; si.activeActionSets = &aas;
XrResult sr = xrSyncActions(g_xr.session, &si);
static int dbg = 0;
if (sr != XR_SUCCESS && dbg < 3) { dbg++; XRRLOG("xrSyncActions rc=%d", (int)sr); }
if (XR_FAILED(sr)) return;
uint32_t btn = 0;
if (bget(a_A)) btn |= OVRP_BTN_A;
if (bget(a_B)) btn |= OVRP_BTN_B;
if (bget(a_X)) btn |= OVRP_BTN_X;
if (bget(a_Y)) btn |= OVRP_BTN_Y;
if (bget(a_menu)) btn |= OVRP_BTN_START;
if (bget(a_lstickc)) btn |= OVRP_BTN_LTHUMB;
if (bget(a_rstickc)) btn |= OVRP_BTN_RTHUMB;
s_in.buttons = btn;
s_in.ltrig = fget(a_ltrig); s_in.rtrig = fget(a_rtrig);
s_in.lgrip = fget(a_lgrip); s_in.rgrip = fget(a_rgrip);
s_in.lstick = v2get(a_lstick); s_in.rstick = v2get(a_rstick);
XrSpaceLocation loc = { XR_TYPE_SPACE_LOCATION };
XrResult lr = xrLocateSpace(s_lspace, g_xr.appSpace, g_xr.frameState.predictedDisplayTime, &loc);
if (lr == XR_SUCCESS && (loc.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT)) {
s_in.lpose = loc.pose; s_in.lvalid = 1;
}
XrSpaceLocation rloc = { XR_TYPE_SPACE_LOCATION };
XrResult rr = xrLocateSpace(s_rspace, g_xr.appSpace, g_xr.frameState.predictedDisplayTime, &rloc);
if (rr == XR_SUCCESS && (rloc.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT)) {
s_in.rpose = rloc.pose; s_in.rvalid = 1;
}
static int pd = 0;
if (pd < 4) { pd++;
XRRLOG("input poses: L rc=%d flags=0x%x pos=(%.2f,%.2f,%.2f) | R rc=%d flags=0x%x pos=(%.2f,%.2f,%.2f)",
(int)lr, (unsigned)loc.locationFlags, loc.pose.position.x, loc.pose.position.y, loc.pose.position.z,
(int)rr, (unsigned)rloc.locationFlags, rloc.pose.position.x, rloc.pose.position.y, rloc.pose.position.z);
}
}
/* ovrp_SetControllerVibration2(mask, freq, amplitude) -> xrApplyHapticFeedback.
* amplitude 0 stops; >0 starts a short pulse (game re-issues for sustained rumble). */
void xrr_set_vibration(unsigned int mask, float frequency, float amplitude) {
if (s_set == XR_NULL_HANDLE || !g_xr.running) return;
XrHapticVibration hv = { XR_TYPE_HAPTIC_VIBRATION };
hv.amplitude = amplitude < 0 ? 0 : (amplitude > 1 ? 1 : amplitude);
hv.frequency = frequency > 0.0f ? frequency : XR_FREQUENCY_UNSPECIFIED;
hv.duration = 300000000; /* 0.3 s; replaced/stopped by the next call */
XrHapticActionInfo hai = { XR_TYPE_HAPTIC_ACTION_INFO };
for (int side = 0; side < 2; side++) {
if (!(mask & (side == 0 ? 0x01u : 0x02u))) continue;
hai.action = side == 0 ? a_lhaptic : a_rhaptic;
if (amplitude > 0.0f)
xrApplyHapticFeedback(g_xr.session, &hai, (const XrHapticBaseHeader *)&hv);
else
xrStopHapticFeedback(g_xr.session, &hai);
}
}
void xrr_get_controller_state(unsigned int mask, ovrpControllerState4 *out) {
(void)mask;
memset(out, 0, sizeof(*out));
out->ConnectedControllers = 0x01 | 0x02 | 0x80000000u; /* LTouch|RTouch|Active */
out->Buttons = s_in.buttons;
out->LIndexTrigger = s_in.ltrig; out->RIndexTrigger = s_in.rtrig;
out->LHandTrigger = s_in.lgrip; out->RHandTrigger = s_in.rgrip;
out->LThumbstick = s_in.lstick; out->RThumbstick = s_in.rstick;
}
/* node presence/validity — the game gates pose queries on these. Reporting the
* controllers as present+tracked is what makes it actually ASK for hand poses. */
int xrr_node_present(int node) {
switch (node) {
case ovrpNode_Head: case ovrpNode_EyeLeft: case ovrpNode_EyeRight:
case ovrpNode_EyeCenter: case ovrpNode_HandLeft: case ovrpNode_HandRight:
return 1;
default: return 0;
}
}
int xrr_node_valid(int node) {
switch (node) {
case ovrpNode_Head: case ovrpNode_EyeLeft: case ovrpNode_EyeRight:
case ovrpNode_EyeCenter:
return g_xr.running ? 1 : 0;
case ovrpNode_HandLeft: return s_in.lvalid;
case ovrpNode_HandRight: return s_in.rvalid;
default: return 0;
}
}
int xrr_get_hand_pose(int node, ovrpPoseStatef *out) {
const XrPosef *p = NULL;
if (node == ovrpNode_HandLeft && s_in.lvalid) p = &s_in.lpose;
if (node == ovrpNode_HandRight && s_in.rvalid) p = &s_in.rpose;
if (!p) return 0;
ovrp_pose_from_xr(p, &out->Pose);
return 1;
}
File diff suppressed because it is too large. Load diff
+195
View File
@@ -0,0 +1,195 @@
/* xr_runtime.h — OpenXR session + frame-loop engine behind the ovrp_* frame fns.
* Core OpenXR only (no graphics-API headers); the Vulkan graphics binding +
* swapchains are handled separately once the ovrp_Initialize5 handshake is
* reversed. This module owns the instance/system/session/spaces + frame pacing. */
#ifndef XR_RUNTIME_H
#define XR_RUNTIME_H
#include <openxr/openxr.h>
#include "ovrplugin_shim.h"
#define XRR_MAX_LAYERS 16
#define XRR_MAX_IMAGES 8
typedef struct {
XrSwapchain swapchain; /* color */
XrSwapchain depthSwapchain; /* optional (SetupLayerDepth) */
uint32_t imageCount; /* = stage count */
uint32_t width, height, arraySize;
int64_t colorFormat;
int64_t depthFormat; /* 0 = no depth swapchain */
uint64_t colorImages[XRR_MAX_IMAGES]; /* VkImage handles -> app */
uint64_t depthImages[XRR_MAX_IMAGES];
/* copy-ring: shim color images UE renders into (decoupled from OpenXR swapchain) */
uint64_t shimImages[XRR_MAX_IMAGES];
uint64_t shimMem[XRR_MAX_IMAGES];
int shimCount; /* >0 = copy-ring active for this layer */
uint32_t acquiredIndex; /* this frame's acquired (render) image */
uint32_t depthAcquiredIndex;
int depthAcquired; /* paired depth image held this frame */
int imageAcquired; /* this frame's render image is held */
/* render-ahead pipeline: the image rendered LAST frame is held one extra
* frame so its tile-memory flush completes off the critical path. */
int presentPending; /* holding last frame's image to present */
int presentToken; /* its flush fence token (xrr_vk_flush_*) */
uint32_t presentIndex; /* its swapchain index (diagnostics) */
/* present-on-submit (debug.re4vr.submithook=2): image held from this frame's
* acquire until UE's eye-render vkQueueSubmit fires, then resolved+released. */
int deferColor; /* color image awaiting deferred present */
int deferDepth; /* depth image awaiting deferred present */
uint32_t deferColorIndex; /* held color swapchain index */
uint32_t deferDepthIndex; /* held depth swapchain index */
int active;
int isEyeFov; /* projection vs overlay */
int layout; /* ovrpLayout (Array vs side-by-side) */
} XrLayer;
typedef struct {
XrInstance instance;
XrSystemId systemId;
XrSession session;
XrSpace appSpace; /* LOCAL or STAGE — tracking origin */
XrSpace viewSpace; /* VIEW — head pose */
XrSessionState sessionState;
XrViewConfigurationType viewConfigType;
/* per-frame state from the last xrWaitFrame */
XrFrameState frameState;
XrView views[2]; /* located eye views */
uint32_t viewCount;
int running; /* xrBeginSession done */
int inFrame; /* between BeginFrame and EndFrame */
XrLayer layers[XRR_MAX_LAYERS];
int layerCount;
} XrRuntime;
extern XrRuntime g_xr;
/* lifecycle (ovrp_PreInitialize3 / ovrp_Initialize5 / ovrp_Shutdown2) */
ovrpResult xrr_pre_init(void); /* create instance + pick system */
/* create session from the app's Vulkan handles (Initialize5 args 5-8) + spaces */
ovrpResult xrr_init(void *vkInstance, void *vkPhysicalDevice, void *vkDevice,
unsigned int queueFamilyIndex);
void xrr_shutdown(void);
/* defined in vk_session.c (Vulkan-typed, kept out of this core-only header) */
int xrr_create_session_vulkan(void *vkInstance, void *vkPhysicalDevice,
void *vkDevice, unsigned int queueFamilyIndex);
/* frame loop */
void xrr_poll_events(void); /* session state machine */
ovrpResult xrr_wait_frame(int frameIndex); /* xrWaitFrame + locate views */
ovrpResult xrr_begin_frame(int frameIndex); /* xrBeginFrame */
ovrpResult xrr_end_frame(int frameIndex,
const ovrpLayerSubmit *const *layers, int layerCount);
double xrr_predicted_display_time_s(void); /* seconds */
/* pose query (ovrp_GetNodePoseState3) */
ovrpResult xrr_get_node_pose(ovrpNode node, ovrpPoseStatef *out);
void xrr_eye_fov_tangents(int eye, float *up, float *down, float *left, float *right);
/* layers / swapchains (ovrp_SetupLayer / GetLayerTextureStageCount / GetLayerTexture2) */
ovrpResult xrr_setup_layer(const ovrpLayerDesc *desc, int *outLayerId);
void xrr_destroy_layer(int layerId);
ovrpResult xrr_setup_layer_depth(int layerId, const ovrpLayerDesc *depthDesc);
int xrr_layer_stage_count(int layerId);
ovrpResult xrr_get_layer_texture(int layerId, int stage, int eye,
uint64_t *outColor, uint64_t *outDepth);
/* defined in vk_session.c — enumerate swapchain VkImages as uint64 handles */
uint32_t xrr_vk_enumerate_images(XrSwapchain sc, uint64_t *out, uint32_t max);
/* recommended per-eye render size from the view configuration */
void xrr_recommended_eye_size(uint32_t *w, uint32_t *h);
/* tracking origin (xr_runtime.c) — 1 = floor (STAGE), 0 = eye level (LOCAL) */
void xrr_set_tracking_origin(int floor);
int xrr_get_tracking_origin(void);
/* apply the game's CPU/GPU perf-level request via XR_EXT_performance_settings */
void xrr_set_perf_level(int isGpu, int level);
/* game dynamic-perf bridge (xr_runtime.c <- core.c ovrp_*TiledMultiRes* / GPUFrameTime).
* Forwards RE4's own FFR scaling onto XR_FB_foveation and feeds it a GPU-time estimate. */
int xrr_foveation_supported(void); /* GetTiledMultiResSupported */
void xrr_set_tiled_multires_level(int ovrpLevel);/* SetTiledMultiResLevel (0..4)*/
int xrr_get_tiled_multires_level(void); /* GetTiledMultiResLevel */
void xrr_set_tiled_multires_dynamic(int on); /* SetTiledMultiResDynamic */
int xrr_get_tiled_multires_dynamic(void); /* GetTiledMultiResDynamic */
float xrr_gpu_frame_time_ms(void); /* GetGPUFrameTime source (ms) */
float xrr_adaptive_gpu_scale(void); /* GetAdaptiveGpuPerformanceScale2 (Lever A) */
/* perf metrics (ovrp_IsPerfMetricsSupported / GetPerfMetrics{Float,Int}); metric =
* ovrpPerfMetrics id. *_supported returns 1/0; getters return 1+write *out, else 0. */
int xrr_perf_metric_supported(int metric);
int xrr_perf_metric_float(int metric, float *out);
int xrr_perf_metric_int(int metric, int *out);
/* render-submit race fix (debug.re4vr.submithook). xrr_install_submit_hook patches UE's
* global VulkanDynamicAPI::vkQueueSubmit to a trampoline (vk_session.c); the trampoline
* calls xrr_on_ue_submit after each UE submit so present can be ordered after UE's
* eye-render submit. isRenderQueue = UE submitted to the shim's graphics queue. */
int xrr_install_submit_hook(void);
void xrr_on_ue_submit(uint64_t queue, uint64_t fence, int isRenderQueue);
/* Vulkan barrier infra (vk_session.c) — flush UE's tile-memory render to main
* memory before the OpenXR compositor reads the swapchain image. */
void xrr_vk_set_handles(void *device, void *queue, unsigned int family);
void xrr_vk_teardown(void); /* reset shim Vulkan state for clean re-init (called from shutdown) */
/* render-ahead flush: submit the barrier without blocking (returns a ring token,
* or -1 if Vulkan isn't ready), wait it a frame later, ready() probes the ring. */
int xrr_vk_flush_submit(uint64_t image, unsigned int arrayLayers);
int xrr_vk_flush_submit_ex(uint64_t image, unsigned int arrayLayers, int isDepth);
void xrr_vk_flush_wait(int token);
/* Block until UE's VkQueue is fully idle (vkQueueWaitIdle). Diagnostic probe for the
* render-submit race: forces all queue work to complete before we resolve/present. */
void xrr_vk_queue_wait_idle(void);
void xrr_vk_device_wait_idle(void);
/* copy-ring (vk_session.c): shim images UE renders into + pipelined resolve-copy */
int xrr_vk_alloc_images(uint64_t *out, uint64_t *outMem, int count,
unsigned int w, unsigned int h, unsigned int arraySize, long long vkFormat);
int xrr_vk_alloc_images_ex(uint64_t *out, uint64_t *outMem, int count,
unsigned int w, unsigned int h, unsigned int arraySize, long long vkFormat, int isDepth);
void xrr_vk_free_images(uint64_t *imgs, uint64_t *mem, int count);
int xrr_vk_copy_submit(uint64_t srcShim, uint64_t dstXr,
unsigned int w, unsigned int h, unsigned int arrayLayers);
int xrr_vk_copy_submit_ex(uint64_t srcShim, uint64_t dstXr,
unsigned int w, unsigned int h, unsigned int arrayLayers, int isDepth);
int xrr_vk_flush_ready(void);
/* one-shot debug readback of one array layer to a downsampled PPM (debug.re4vr.dump) */
void xrr_vk_dump_image(uint64_t image, unsigned int w, unsigned int h,
unsigned int arrayLayer, const char *path);
/* stamp a black/white binary barcode of `value` (the frameIndex) into the top-left of
* one array layer, AFTER UE's resolve, so it rides on every frame incl. black ones —
* frame-exact video<->log correlation (debug.re4vr.barcode). Validation-only. */
void xrr_vk_stamp_barcode(uint64_t image, unsigned int w, unsigned int h,
unsigned int arrayLayer, unsigned int value, int flagged);
/* per-frame black detector: max luminance (0..255) over a few rows of the resolved eye
* image; ~0 => truncated/black frame. Covers the whole black tail (debug.re4vr.lumagate). */
int xrr_vk_frame_luma(uint64_t image, unsigned int w, unsigned int h, unsigned int arrayLayer);
/* input (xr_input.c) — OpenXR action sets -> ovrpControllerState4 + hand poses */
int xrr_input_init(void);
void xrr_input_sync(void);
void xrr_get_controller_state(unsigned int mask, ovrpControllerState4 *out);
int xrr_get_hand_pose(int node, ovrpPoseStatef *out);
int xrr_node_present(int node);
int xrr_node_valid(int node);
void xrr_set_vibration(unsigned int mask, float frequency, float amplitude);
/* Android instance handshake (android_init.c). No-ops on the host build so the
* same xr_runtime.c serves both. JavaVM is captured via JNI_OnLoad; the activity
* comes from Initialize5 arg4, with an Application-context reflection fallback. */
int xrr_android_init_loader(void); /* xrInitializeLoaderKHR (vm+context) */
void *xrr_android_instance_next(void); /* &XrInstanceCreateInfoAndroidKHR | NULL */
void xrr_set_android_activity(void *activity);
int xrr_android_have_real_activity(void);
void *xrr_android_get_vm(void); /* JavaVM* captured in JNI_OnLoad (NULL on host) */
/* helpers */
static inline void ovrp_pose_from_xr(const XrPosef *in, ovrpPosef *out) {
out->Orientation.x = in->orientation.x; out->Orientation.y = in->orientation.y;
out->Orientation.z = in->orientation.z; out->Orientation.w = in->orientation.w;
out->Position.x = in->position.x; out->Position.y = in->position.y;
out->Position.z = in->position.z;
}
#endif /* XR_RUNTIME_H */
+52
View File
@@ -0,0 +1,52 @@
/* harness.c — drives the ovrp_* sequence against a real OpenXR runtime
* (Path B: Monado simulated/headless). Smoke-tests the shim's OpenXR usage
* without RE4 or a headset. See ../../TESTING.md.
*
* Build (inside a Linux env with an OpenXR loader):
* cc -std=c11 -I../include -I../third_party/openxr harness.c \
* ../src/stubs.c ../src/core.c ../src/xr_runtime.c ../src/vk_session.c \
* ../src/layers.c -lopenxr_loader -o harness
* XR_RUNTIME_JSON=/path/openxr_monado-dev.json ./harness
*
* NOTE: Initialize5 -> xrCreateSession needs real Vulkan handles. For a first
* smoke test we stop after PreInitialize3 (instance+system create) unless real
* VkInstance/Device handles are provided. Set WITH_VK=1 + fill the handles to go
* further once a headless Vulkan device is available.
*/
#include "ovrplugin_shim.h"
#include <stdio.h>
#define CHECK(expr) do { \
ovrpResult _r = (expr); \
printf(" %-34s -> %d %s\n", #expr, _r, OVRP_SUCCESS(_r) ? "OK" : "(fail)"); \
} while (0)
int main(void) {
printf("== ovrp shim smoke test ==\n");
printf("[lifecycle]\n");
CHECK(ovrp_PreInitialize3(NULL)); /* xrCreateInstance + xrGetSystem */
/* Session creation needs real Vulkan handles; pass NULLs and expect failure
* until a headless Vulkan device is wired (see TESTING.md Path B note). */
printf("[init — expect fail without Vulkan handles]\n");
CHECK(ovrp_Initialize5(ovrpRenderAPI_Vulkan, NULL, NULL, NULL,
NULL, NULL, NULL, 0, 0));
printf("[frame loop — will report not-ready until session exists]\n");
for (int f = 0; f < 3; f++) {
ovrp_Update3(ovrpStep_Render, f, 0.0);
ovrp_WaitToBeginFrame(f);
ovrp_BeginFrame4(f, NULL);
ovrpPoseStatef head;
ovrp_GetNodePoseState3(ovrpStep_Render, f, ovrpNode_Head, &head);
double t = 0; ovrp_GetPredictedDisplayTime(f, &t);
ovrp_EndFrame4(f, NULL, 0, NULL);
printf(" frame %d: head.w=%.3f t=%.6f\n", f, head.Pose.Orientation.w, t);
}
printf("[shutdown]\n");
CHECK(ovrp_Shutdown2());
printf("== done ==\n");
return 0;
}
+96
View File
@@ -0,0 +1,96 @@
# Desktop harness — drive the shim against Monado on a PC
The shim is normally exercised only on a Quest (inside RE4's APK). This harness lets you
run its **OpenXR path on a Linux desktop**, headless, against
[Monado](https://monado.freedesktop.org/)'s *simulated HMD* — no headset, no game, no
`libUE4`. It's the fast iteration loop for the Steam Frame / Monado / Lepton bring-up.
## What it is
`harness.c` stands in for the game (UE4 + `OculusHMD`). It creates a Vulkan
instance/device the way UE's VulkanRHI does, then calls our `ovrp_*` exports **in UE's
order**:
```
PreInitialize3 -> Get{Instance,Device}ExtensionsVk -> Initialize5
-> CalculateEyeLayerDesc2 -> SetupLayer (once)
-> per frame: Update3, WaitToBeginFrame, BeginFrame4, GetLayerTexture2,
(clear the eye image), EndFrame4
-> Shutdown2
```
The shim does the real OpenXR work underneath — `xrCreateInstance`, `xrGetSystem`,
`xrCreateSession` (Vulkan binding), `xrCreateSwapchain`, the `xrWaitFrame/Begin/EndFrame`
loop — against whatever runtime the OpenXR loader selects. Here that's Monado's simulated
HMD with the **NULL compositor** (renders nowhere), so it runs over SSH / in CI.
It ships nothing from Capcom/Epic/Meta — it only calls our own public `ovrp_*` ABI.
## Prereqs (Debian/Ubuntu)
```sh
sudo apt-get install monado-service libopenxr1-monado libopenxr-loader1 libopenxr-dev \
libvulkan-dev
```
## Build & run
```sh
shim/build_host.sh # builds build/host/libOVRPlugin.so + build/host/harness
tools/desktop-harness/run.sh # brings up monado-service headless, runs 300 frames
tools/desktop-harness/run.sh 1000 # custom frame count
```
Logs land in `build/host/monado.log` and `build/host/harness.log`.
### The scene (pose→view validation)
Each frame the harness queries the shim's per-eye pose (`ovrp_GetNodePoseState3` for
`EyeLeft`/`EyeRight`, with true IPD separation) and FOV, builds per-pixel world rays, and
renders a **world-locked procedural scene** — checkerboard floor 1.6 m below the eye, sky
gradient, and an orbiting sun — into the acquired eye image. This exercises the shim's
pose/FOV math: the two eyes show correct stereo parallax, and the world counter-moves as the
head pose changes (Monado's simulated HMD sways, so there's real motion). The first few
frames log per-eye pose + FOV. CPU-rendered (fine at the sim's 128×128; it's a test tool, not
a fast path).
### Watch it (windowed)
`VISIBLE=1` uses Monado's main compositor (mirror window) + the imgui debug GUI instead of
the NULL compositor, so you can watch the scene (floor grid, horizon, orbiting sun, stereo
parallax) and inspect swapchains. Needs a display — run it from the physical desktop session,
not over SSH, and give it a big frame count:
```sh
VISIBLE=1 tools/desktop-harness/run.sh 3600
```
## What "pass" looks like
`harness.log` should show the lifecycle succeed and frames present:
```
[harness] PreInitialize3 OK (XrInstance + system up)
[harness] VkInstance created
[harness] VkDevice + graphics queue (family 0) created
[harness] Initialize5 OK (XrSession created)
[harness] SetupLayer OK layerId=0 swapchainStages=3
[harness] ...
[harness] loop done: NNN/NNN frames presented
[harness] Shutdown2 OK — clean exit
```
Exit code 0 = frames presented; 2 = ran but presented nothing (session never reached the
running state — check `monado.log`); 1 = a hard failure.
## Limits / notes
- This validates the **OpenXR + Vulkan binding + frame loop + swapchain** path. It does
*not* reproduce the game-thread/render-thread pacing or GPU load that drove the on-Quest
"ghost"; those are device-side behaviours. It's for ABI/path correctness and porting to
new runtimes, not perf tuning.
- The Android session path (`XR_KHR_android_create_instance`, `xrInitializeLoaderKHR`, the
JavaVM/Activity chain) is `#ifdef __ANDROID__`-guarded in `xr_runtime.c` /
`android_init.c`, so the same sources serve both targets.
- `passthru.c` (P4 native forwarding) is Android-only; on host its arm64 trampolines fall
back to plain stubs and passthru stays inactive.
+366
View File
@@ -0,0 +1,366 @@
/* harness.c — desktop OpenXR harness for the OVRPlugin->OpenXR shim.
*
* Stands in for Resident Evil 4 VR (UE4 + OculusHMD) on a Linux desktop: it creates
* a Vulkan instance/device the way UE's VulkanRHI does, then drives the shim's ovrp_*
* entry points in the exact order UE calls them — PreInitialize3 -> Get*ExtensionsVk
* -> Initialize5 -> (per frame) Update3/WaitToBeginFrame/BeginFrame4/EndFrame4 -> a
* one-time CalculateEyeLayerDesc2/SetupLayer -> Shutdown2. The shim creates the real
* XrInstance/session/swapchains against whatever OpenXR runtime the loader selects
* (here: Monado's simulated HMD, headless via XRT_COMPOSITOR_NULL).
*
* This exercises the whole non-Android OpenXR path of the shim on a PC — no Quest, no
* libUE4 — so the Steam Frame / Monado / Lepton bring-up can be iterated on a laptop.
*
* It is NOT the game and ships nothing from Capcom/Epic/Meta: it only calls our own
* public ovrp_* ABI. Build: shim/build_host.sh. Run: tools/desktop-harness/run.sh. */
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <math.h>
#include <vulkan/vulkan.h>
#include "ovrplugin_shim.h" /* shim public types/enums (ovrpLayerDesc, ovrpLayout, ...) */
/* ovrp_* the shim exports but doesn't declare in the public header — declare here so we
* call them with the verified ABI without depending on header completeness. */
extern ovrpResult ovrp_GetInstanceExtensionsVk(const char **outArray, int *inoutCount);
extern ovrpResult ovrp_GetDeviceExtensionsVk(const char **outArray, int *inoutCount);
extern ovrpResult ovrp_CalculateEyeLayerDesc2(ovrpLayout layout, float textureScale,
int mipLevels, int sampleCount, ovrpTextureFormat colorFormat,
ovrpTextureFormat depthFormat, int layerFlags, ovrpLayerDesc *out);
extern ovrpResult ovrp_SetupLayer(void *device, ovrpLayerDesc *desc, int *outLayerId);
extern ovrpResult ovrp_GetLayerTextureStageCount(int layerId, int *outCount);
extern ovrpResult ovrp_GetLayerTexture2(int layerId, int stage, int eyeId,
uint64_t *outColorTex, uint64_t *outDepthTex);
#define VKOK(call) do { VkResult _r = (call); if (_r != VK_SUCCESS) { \
fprintf(stderr, "[harness] FAIL %s = %d\n", #call, _r); exit(1); } } while (0)
#define LOG(...) do { fprintf(stderr, "[harness] " __VA_ARGS__); fputc('\n', stderr); } while (0)
static VkInstance g_inst;
static VkPhysicalDevice g_phys;
static VkDevice g_dev;
static VkQueue g_queue;
static uint32_t g_gfxFamily;
static VkCommandPool g_cmdPool;
/* CPU-side staging for the scene renderer (host-visible; both eye layers, RGBA8). */
static VkBuffer g_stageBuf;
static VkDeviceMemory g_stageMem;
static void *g_stagePtr;
static uint32_t g_stageW, g_stageH;
/* The ovrp_*ExtensionsVk getters report a count then fill a caller array of char*. */
static const char **query_exts(int forDevice, int *outCount) {
int n = 0;
ovrpResult r = forDevice ? ovrp_GetDeviceExtensionsVk(NULL, &n)
: ovrp_GetInstanceExtensionsVk(NULL, &n);
if (!OVRP_SUCCESS(r) || n <= 0) { *outCount = 0; return NULL; }
const char **arr = calloc((size_t)n, sizeof(char *));
int cap = n;
r = forDevice ? ovrp_GetDeviceExtensionsVk(arr, &cap)
: ovrp_GetInstanceExtensionsVk(arr, &cap);
if (!OVRP_SUCCESS(r)) { free(arr); *outCount = 0; return NULL; }
*outCount = n;
LOG("%s extensions required by runtime (%d):", forDevice ? "device" : "instance", n);
for (int i = 0; i < n; i++) LOG(" %s", arr[i]);
return arr;
}
static void make_vk_instance(void) {
int n = 0;
const char **exts = query_exts(0, &n); /* needs the XrInstance (PreInitialize3 done) */
VkApplicationInfo ai = { VK_STRUCTURE_TYPE_APPLICATION_INFO };
ai.pApplicationName = "re4vr-shim-harness";
ai.apiVersion = VK_API_VERSION_1_1; /* UE/Quest Vulkan baseline */
VkInstanceCreateInfo ci = { VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO };
ci.pApplicationInfo = &ai;
ci.enabledExtensionCount = (uint32_t)n;
ci.ppEnabledExtensionNames = exts;
VKOK(vkCreateInstance(&ci, NULL, &g_inst));
free(exts);
LOG("VkInstance created");
}
static void pick_physical_and_device(void) {
uint32_t pc = 0;
VKOK(vkEnumeratePhysicalDevices(g_inst, &pc, NULL));
if (!pc) { LOG("no Vulkan physical devices"); exit(1); }
VkPhysicalDevice *pd = calloc(pc, sizeof(*pd));
VKOK(vkEnumeratePhysicalDevices(g_inst, &pc, pd));
g_phys = pd[0]; /* shim picks the runtime's preferred device internally; smoke test = [0] */
VkPhysicalDeviceProperties props;
vkGetPhysicalDeviceProperties(g_phys, &props);
LOG("physical device: %s", props.deviceName);
free(pd);
uint32_t qf = 0;
vkGetPhysicalDeviceQueueFamilyProperties(g_phys, &qf, NULL);
VkQueueFamilyProperties *qp = calloc(qf, sizeof(*qp));
vkGetPhysicalDeviceQueueFamilyProperties(g_phys, &qf, qp);
g_gfxFamily = UINT32_MAX;
for (uint32_t i = 0; i < qf; i++)
if (qp[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { g_gfxFamily = i; break; }
free(qp);
if (g_gfxFamily == UINT32_MAX) { LOG("no graphics queue family"); exit(1); }
int n = 0;
const char **exts = query_exts(1, &n);
float prio = 1.0f;
VkDeviceQueueCreateInfo qci = { VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO };
qci.queueFamilyIndex = g_gfxFamily;
qci.queueCount = 1;
qci.pQueuePriorities = &prio;
VkDeviceCreateInfo dci = { VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO };
dci.queueCreateInfoCount = 1;
dci.pQueueCreateInfos = &qci;
dci.enabledExtensionCount = (uint32_t)n;
dci.ppEnabledExtensionNames = exts;
VKOK(vkCreateDevice(g_phys, &dci, NULL, &g_dev));
free(exts);
vkGetDeviceQueue(g_dev, g_gfxFamily, 0, &g_queue);
VkCommandPoolCreateInfo pci = { VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO };
pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
pci.queueFamilyIndex = g_gfxFamily;
VKOK(vkCreateCommandPool(g_dev, &pci, NULL, &g_cmdPool));
LOG("VkDevice + graphics queue (family %u) created", g_gfxFamily);
}
static uint32_t find_mem(uint32_t typeBits, VkMemoryPropertyFlags want) {
VkPhysicalDeviceMemoryProperties mp;
vkGetPhysicalDeviceMemoryProperties(g_phys, &mp);
for (uint32_t i = 0; i < mp.memoryTypeCount; i++)
if ((typeBits & (1u << i)) && (mp.memoryTypes[i].propertyFlags & want) == want) return i;
return UINT32_MAX;
}
/* Host-visible staging buffer big enough for both eye layers (RGBA8). Persistently mapped. */
static int make_staging(uint32_t w, uint32_t h) {
VkDeviceSize sz = (VkDeviceSize)w * h * 4u * 2u; /* 2 array layers */
VkBufferCreateInfo bci = { VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO };
bci.size = sz; bci.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (vkCreateBuffer(g_dev, &bci, NULL, &g_stageBuf) != VK_SUCCESS) return 0;
VkMemoryRequirements mr; vkGetBufferMemoryRequirements(g_dev, g_stageBuf, &mr);
uint32_t mt = find_mem(mr.memoryTypeBits,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
if (mt == UINT32_MAX) return 0;
VkMemoryAllocateInfo mai = { VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
mai.allocationSize = mr.size; mai.memoryTypeIndex = mt;
if (vkAllocateMemory(g_dev, &mai, NULL, &g_stageMem) != VK_SUCCESS) return 0;
vkBindBufferMemory(g_dev, g_stageBuf, g_stageMem, 0);
if (vkMapMemory(g_dev, g_stageMem, 0, sz, 0, &g_stagePtr) != VK_SUCCESS) return 0;
g_stageW = w; g_stageH = h;
return 1;
}
/* rotate vector v by quaternion q (x,y,z,w): v + 2*qw*(qv x v) + 2*(qv x (qv x v)) */
static void qrot(float qx, float qy, float qz, float qw,
float vx, float vy, float vz, float *ox, float *oy, float *oz) {
float tx = 2.0f * (qy * vz - qz * vy);
float ty = 2.0f * (qz * vx - qx * vz);
float tz = 2.0f * (qx * vy - qy * vx);
*ox = vx + qw * tx + (qy * tz - qz * ty);
*oy = vy + qw * ty + (qz * tx - qx * tz);
*oz = vz + qw * tz + (qx * ty - qy * tx);
}
/* Procedural world-locked scene along a world-space ray: checkerboard floor 1.6m below the
* eye, sky gradient, and an orbiting sun (the motion). Writes linear RGB into r/g/b. */
static void shade(float ox, float oy, float oz, float dx, float dy, float dz, float t,
float *r, float *g, float *b) {
if (dy < -1e-3f) {
float floorY = oy - 1.6f;
float tt = (floorY - oy) / dy; /* = 1.6 / -dy > 0 */
if (tt > 0.0f) {
float hx = ox + dx * tt, hz = oz + dz * tt;
int chk = (((int)floorf(hx)) + ((int)floorf(hz))) & 1;
float base = chk ? 0.85f : 0.25f;
float fog = 1.0f / (1.0f + tt * 0.04f); /* fade distant floor into sky */
*r = base * fog + 0.55f * (1.0f - fog);
*g = base * fog + 0.65f * (1.0f - fog);
*b = base * fog + 0.85f * (1.0f - fog);
return;
}
}
float up = dy * 0.5f + 0.5f; /* sky gradient */
*r = 0.30f + 0.20f * up; *g = 0.50f + 0.30f * up; *b = 0.70f + 0.30f * up;
float sx = cosf(t), sy = 0.40f, sz = sinf(t); /* orbiting sun */
float sl = 1.0f / sqrtf(sx * sx + sy * sy + sz * sz); sx *= sl; sy *= sl; sz *= sl;
if (dx * sx + dy * sy + dz * sz > 0.995f) { *r = 1.0f; *g = 0.95f; *b = 0.70f; }
}
/* Best-effort pose-driven render: for each eye, build per-pixel world rays from the shim's
* located eye pose + FOV, shade the procedural scene, and copy into that array layer. This
* exercises the shim's pose/FOV math (stereo parallax between eyes; world-locked content
* counter-moves as the head pose changes). Failures here don't fail the harness. */
static void render_scene(uint64_t image, uint32_t arrayLayers, const ovrpLayerDesc *desc,
const ovrpPoseStatef pose[2], float t) {
if (!image || !g_stagePtr) return;
uint32_t W = g_stageW, H = g_stageH;
for (uint32_t eye = 0; eye < arrayLayers; eye++) {
const ovrpPosef *p = &pose[eye].Pose;
float ox = p->Position.x, oy = p->Position.y, oz = p->Position.z;
float lt = desc->Fov[eye].LeftTan, rt = desc->Fov[eye].RightTan;
float ut = desc->Fov[eye].UpTan, dt = desc->Fov[eye].DownTan;
uint8_t *px = (uint8_t *)g_stagePtr + (size_t)eye * W * H * 4u;
/* ovrpFovf tangents are positive magnitudes: horizontal spans -LeftTan..+RightTan,
* vertical spans +UpTan (top) ..-DownTan (bottom). */
for (uint32_t y = 0; y < H; y++) {
float v = ut - (ut + dt) * ((y + 0.5f) / H);
for (uint32_t x = 0; x < W; x++) {
float u = -lt + (rt + lt) * ((x + 0.5f) / W);
float il = 1.0f / sqrtf(u * u + v * v + 1.0f);
float ex = u * il, ey = v * il, ez = -1.0f * il; /* OpenXR: -Z forward */
float dx, dy, dz;
qrot(p->Orientation.x, p->Orientation.y, p->Orientation.z, p->Orientation.w,
ex, ey, ez, &dx, &dy, &dz);
float r, g, b; shade(ox, oy, oz, dx, dy, dz, t, &r, &g, &b);
uint8_t *o = px + ((size_t)y * W + x) * 4u;
o[0] = (uint8_t)(r * 255.0f); o[1] = (uint8_t)(g * 255.0f);
o[2] = (uint8_t)(b * 255.0f); o[3] = 255;
}
}
}
VkCommandBufferAllocateInfo ai = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO };
ai.commandPool = g_cmdPool; ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; ai.commandBufferCount = 1;
VkCommandBuffer cb;
if (vkAllocateCommandBuffers(g_dev, &ai, &cb) != VK_SUCCESS) return;
VkCommandBufferBeginInfo bi = { VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO };
bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
vkBeginCommandBuffer(cb, &bi);
VkImageSubresourceRange range = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, arrayLayers };
VkImageMemoryBarrier toDst = { VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER };
toDst.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
toDst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
toDst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toDst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toDst.image = (VkImage)image; toDst.subresourceRange = range;
toDst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
0, 0, NULL, 0, NULL, 1, &toDst);
VkBufferImageCopy region[2]; uint32_t nr = 0;
for (uint32_t eye = 0; eye < arrayLayers; eye++) {
VkBufferImageCopy c; memset(&c, 0, sizeof c);
c.bufferOffset = (VkDeviceSize)eye * W * H * 4u;
c.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
c.imageSubresource.mipLevel = 0; c.imageSubresource.baseArrayLayer = eye;
c.imageSubresource.layerCount = 1;
c.imageExtent.width = W; c.imageExtent.height = H; c.imageExtent.depth = 1;
region[nr++] = c;
}
vkCmdCopyBufferToImage(cb, g_stageBuf, (VkImage)image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, nr, region);
VkImageMemoryBarrier toRead = toDst;
toRead.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
toRead.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; /* what the compositor reads */
toRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
toRead.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
0, 0, NULL, 0, NULL, 1, &toRead);
vkEndCommandBuffer(cb);
VkSubmitInfo si = { VK_STRUCTURE_TYPE_SUBMIT_INFO };
si.commandBufferCount = 1; si.pCommandBuffers = &cb;
vkQueueSubmit(g_queue, 1, &si, VK_NULL_HANDLE);
vkQueueWaitIdle(g_queue);
vkFreeCommandBuffers(g_dev, g_cmdPool, 1, &cb);
}
int main(int argc, char **argv) {
int frames = (argc > 1) ? atoi(argv[1]) : 300;
if (frames < 1) frames = 1;
LOG("starting; %d frames. Runtime via OpenXR loader (XR_RUNTIME_JSON / active_runtime.json).", frames);
/* 1. lifecycle: PreInitialize3 creates the XrInstance + picks the system */
if (!OVRP_SUCCESS(ovrp_PreInitialize3(NULL))) { LOG("PreInitialize3 failed"); return 1; }
LOG("PreInitialize3 OK (XrInstance + system up)");
/* 2. Vulkan, created with the runtime-required extensions (UE's VulkanRHI order) */
make_vk_instance();
pick_physical_and_device();
/* 3. Initialize5 hands the shim our Vulkan handles -> it creates the XrSession */
long versionStub[4] = {0}; /* arg9 = const ovrpVersion& — shim ignores the contents */
ovrpResult ir = ovrp_Initialize5(ovrpRenderAPI_Vulkan, NULL, NULL,
(void *)g_inst, (void *)g_phys, (void *)g_dev,
(void *)g_queue, 0, versionStub);
if (!OVRP_SUCCESS(ir)) { LOG("Initialize5 failed (%d)", ir); return 1; }
LOG("Initialize5 OK (XrSession created)");
/* 4. eye-fov layer (UE: CalculateEyeLayerDesc2 -> SetupLayer once) */
ovrpLayerDesc desc;
ovrpResult dr = ovrp_CalculateEyeLayerDesc2(ovrpLayout_Array, 1.0f, 1, 1,
ovrpTextureFormat_R8G8B8A8_sRGB, (ovrpTextureFormat)0, 0, &desc);
if (!OVRP_SUCCESS(dr)) { LOG("CalculateEyeLayerDesc2 failed (%d)", dr); return 1; }
LOG("EyeLayerDesc %dx%d arraylayout, fmt=%d", desc.TextureSize.w, desc.TextureSize.h, desc.Format);
int layerId = -1;
ovrpResult sr = ovrp_SetupLayer((void *)g_dev, &desc, &layerId);
if (!OVRP_SUCCESS(sr) || layerId < 0) { LOG("SetupLayer failed (%d)", sr); return 1; }
int stageCount = 0; ovrp_GetLayerTextureStageCount(layerId, &stageCount);
LOG("SetupLayer OK layerId=%d swapchainStages=%d", layerId, stageCount);
uint32_t arrayLayers = (desc.Layout == ovrpLayout_Array) ? 2u : 1u;
if (make_staging((uint32_t)desc.TextureSize.w, (uint32_t)desc.TextureSize.h))
LOG("scene renderer ready (%dx%d, %u eye layers)", desc.TextureSize.w, desc.TextureSize.h, arrayLayers);
else
LOG("WARN: staging buffer alloc failed — frames will be submitted blank");
/* 5. frame loop. Update3 advances the session state machine (IDLE->READY->FOCUSED);
* Wait/Begin/EndFrame no-op until the session is running, so early frames are fine. */
if (stageCount < 1) stageCount = 1;
int presented = 0, renderStage = 0; /* stage advances per presented frame, in lockstep
* with the shim's one-acquire-per-running-frame */
for (int f = 0; f < frames; f++) {
ovrp_Update3(ovrpStep_Render, f, 0.0);
ovrp_WaitToBeginFrame(f);
ovrp_BeginFrame4(f, NULL);
int stage = renderStage % stageCount; /* = the image begin_frame just acquired */
/* per-eye pose from the shim's located views (true IPD separation) + the eye FOV
* from the layer desc -> render a world-locked scene into the acquired eye image. */
ovrpPoseStatef eyePose[2]; memset(eyePose, 0, sizeof eyePose);
eyePose[0].Pose.Orientation.w = eyePose[1].Pose.Orientation.w = 1.0f;
ovrp_GetNodePoseState3(ovrpStep_Render, f, ovrpNode_EyeLeft, &eyePose[0]);
ovrp_GetNodePoseState3(ovrpStep_Render, f, ovrpNode_EyeRight, &eyePose[1]);
if (f < 3)
LOG("frame %d eyeL pos=(%.3f %.3f %.3f) eyeR pos=(%.3f %.3f %.3f) fovL(R%.3f L%.3f)",
f, eyePose[0].Pose.Position.x, eyePose[0].Pose.Position.y, eyePose[0].Pose.Position.z,
eyePose[1].Pose.Position.x, eyePose[1].Pose.Position.y, eyePose[1].Pose.Position.z,
desc.Fov[0].RightTan, desc.Fov[0].LeftTan);
uint64_t color = 0, depthTex = 0;
if (OVRP_SUCCESS(ovrp_GetLayerTexture2(layerId, stage, 0, &color, &depthTex)) && color)
render_scene(color, arrayLayers, &desc, eyePose, (float)f * 0.03f);
ovrpLayerSubmit submit;
memset(&submit, 0, sizeof submit);
submit.LayerId = layerId;
submit.TextureStage = stage;
submit.Pose.Orientation.w = 1.0f; /* pose/FOV come from the shim's located views */
const ovrpLayerSubmit *ptrs[1] = { &submit };
ovrpResult er = ovrp_EndFrame4(f, ptrs, 1, NULL);
if (OVRP_SUCCESS(er)) {
if (presented == 0) /* views are located now — refresh FOV (setup value was the fallback) */
ovrp_CalculateEyeLayerDesc2(ovrpLayout_Array, 1.0f, 1, 1,
ovrpTextureFormat_R8G8B8A8_sRGB, (ovrpTextureFormat)0, 0, &desc);
presented++; renderStage++;
}
if (f < 5 || (f % 60) == 0)
LOG("frame %d: end=%d (presented=%d)", f, er, presented);
}
LOG("loop done: %d/%d frames presented", presented, frames);
ovrp_Shutdown2();
LOG("Shutdown2 OK — clean exit");
return presented > 0 ? 0 : 2;
}
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# run.sh — launch the desktop harness against Monado's simulated HMD, headless.
#
# Brings up monado-service with the NULL compositor (no display needed -> works over SSH /
# in CI), points the OpenXR loader at Monado, then runs the harness which drives the shim's
# ovrp_* -> OpenXR path. Output: build/host/monado.log + build/host/harness.log.
#
# Prereqs (Debian/Ubuntu): sudo apt-get install monado-service libopenxr1-monado \
# libopenxr-loader1 libopenxr-dev
# Build first: shim/build_host.sh
set -uo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
OUT="$ROOT/build/host"
FRAMES="${1:-300}"
[ -x "$OUT/harness" ] || { echo "no $OUT/harness — run shim/build_host.sh first"; exit 1; }
# Point the loader at Monado explicitly (in case another runtime is also registered).
MONADO_JSON="$(ls /usr/share/openxr/1/openxr_monado*.json 2>/dev/null | head -1 || true)"
[ -n "$MONADO_JSON" ] && export XR_RUNTIME_JSON="$MONADO_JSON"
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/tmp/re4vr-monado-rt}"
mkdir -p "$XDG_RUNTIME_DIR"
# Simulated HMD driver is auto-selected when no real hardware is present.
# Default = headless: NULL compositor renders to nothing, so it needs no X/Wayland (SSH/CI).
# VISIBLE=1 = windowed: the main compositor opens a mirror window + Monado's imgui debug GUI
# (swapchain preview), so you can WATCH the harness's animated eye fill. Needs a display, so
# run it from the physical desktop session, not over SSH, and give it a big frame count to
# watch, e.g. VISIBLE=1 tools/desktop-harness/run.sh 3600
if [ "${VISIBLE:-0}" = 1 ]; then
export XRT_DEBUG_GUI=1
echo "VISIBLE mode: main compositor + debug GUI (needs DISPLAY/WAYLAND — run locally)"
else
export XRT_COMPOSITOR_NULL=1
fi
export QWERTY_ENABLE=0
export U_PACING_APP_USE_MIN_FRAME_PERIOD=1
SOCK="$XDG_RUNTIME_DIR/monado_comp_ipc"
rm -f "$SOCK"
# monado-service adds stdin to its epoll loop (for its "press a key to quit" handler).
# A redirected /dev/null or regular file isn't epoll-able -> epoll_ctl fails -> the IPC
# loop dies. Feed it a real FIFO held open by a writer that never sends data.
FIFO="$XDG_RUNTIME_DIR/monado_stdin"
rm -f "$FIFO"; mkfifo "$FIFO"
sleep 100000 >"$FIFO" &
HOLD=$!
echo "== starting monado-service (headless, NULL compositor) =="
monado-service <"$FIFO" >"$OUT/monado.log" 2>&1 &
SVC=$!
trap 'kill $SVC $HOLD 2>/dev/null; wait $SVC 2>/dev/null; rm -f "$FIFO"' EXIT
# wait up to ~10s for the IPC socket
for _ in $(seq 1 100); do [ -S "$SOCK" ] && break; kill -0 $SVC 2>/dev/null || { echo "monado-service died:"; cat "$OUT/monado.log"; exit 1; }; sleep 0.1; done
[ -S "$SOCK" ] || { echo "monado-service IPC socket never appeared:"; tail -20 "$OUT/monado.log"; exit 1; }
echo "monado-service up (pid $SVC)"
echo "== running harness ($FRAMES frames) =="
XRRLOG_STDERR=1 "$OUT/harness" "$FRAMES" 2>&1 | tee "$OUT/harness.log"
rc=${PIPESTATUS[0]}
echo "== harness exit: $rc =="
exit $rc
+31
View File
@@ -0,0 +1,31 @@
/* xrexts.c — list the OpenXR instance extensions the active runtime advertises.
* Recon for the foveation bring-up: tells us which foveation / FDM / eye-tracking
* extensions THIS runtime (Monado/Lepton) exposes, so we know what to wire into the
* shim's apply_foveation() extension point. No Vulkan, no session — just enumerate.
* cc xrexts.c -lopenxr_loader -o xrexts && XR_RUNTIME_JSON=.../openxr_monado.json ./xrexts */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openxr/openxr.h>
int main(void) {
uint32_t n = 0;
if (xrEnumerateInstanceExtensionProperties(NULL, 0, &n, NULL) != XR_SUCCESS || !n) {
fprintf(stderr, "xrEnumerateInstanceExtensionProperties failed (runtime selected? service up?)\n");
return 1;
}
XrExtensionProperties *p = calloc(n, sizeof *p);
for (uint32_t i = 0; i < n; i++) p[i].type = XR_TYPE_EXTENSION_PROPERTIES;
if (xrEnumerateInstanceExtensionProperties(NULL, n, &n, p) != XR_SUCCESS) return 1;
printf("runtime advertises %u instance extensions:\n", n);
for (uint32_t i = 0; i < n; i++) {
const char *e = p[i].extensionName;
int hot = strcasestr(e, "fov") || strcasestr(e, "foveat") || strcasestr(e, "density")
|| strcasestr(e, "fdm") || strcasestr(e, "eye") || strcasestr(e, "gaze")
|| strcasestr(e, "vrs") || strcasestr(e, "shading_rate") || strcasestr(e, "quad");
printf(" %s %s (v%u)\n", hot ? "**" : " ", e, p[i].extensionVersion);
}
return 0;
}