mirror of
https://github.com/daniel-lynch/ovrplugin-openxr-shim.git
synced 2026-10-04 22:00:05 +02:00
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:
commit
a72a79ad29
57 files changed
+9304
No files matched your search
+36
@@ -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/
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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).
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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").
|
||||
@@ -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)
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
```
|
||||
@@ -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`.
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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).
|
||||
@@ -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)}"
|
||||
Executable
+29
@@ -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
|
||||
Executable
+28
@@ -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."
|
||||
Executable
+11
@@ -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)"
|
||||
Executable
+58
@@ -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/)"
|
||||
Executable
+106
@@ -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."
|
||||
@@ -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."
|
||||
@@ -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.
|
||||
Executable
+37
@@ -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"
|
||||
Executable
+54
@@ -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"
|
||||
Executable
+68
@@ -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"
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
Executable
+62
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user