commit a72a79ad29953bc7e6c04a63ff05a2dbc3ebf545 Author: Daniel Lynch Date: Mon Jun 29 00:48:48 2026 -0400 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 Claude-Session: https://claude.ai/code/session_01D6sFYGXZPsq3v7xtcDES6g diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..17606eb --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/HOST.md b/HOST.md new file mode 100644 index 0000000..fab045c --- /dev/null +++ b/HOST.md @@ -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. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..504dea9 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c5783f0 --- /dev/null +++ b/README.md @@ -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/-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. diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..9477dbd --- /dev/null +++ b/TESTING.md @@ -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 + (`` already there; + add OpenXR ``/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). diff --git a/analysis/all_exports.txt b/analysis/all_exports.txt new file mode 100644 index 0000000..319c438 --- /dev/null +++ b/analysis/all_exports.txt @@ -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 diff --git a/analysis/shim_surface.txt b/analysis/shim_surface.txt new file mode 100644 index 0000000..3ae7f7c --- /dev/null +++ b/analysis/shim_surface.txt @@ -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 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..c2f51d9 --- /dev/null +++ b/docs/README.md @@ -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. diff --git a/docs/handoffs/HANDOFF-2026-06-24.md b/docs/handoffs/HANDOFF-2026-06-24.md new file mode 100644 index 0000000..01ecf04 --- /dev/null +++ b/docs/handoffs/HANDOFF-2026-06-24.md @@ -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 , or wireless :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://` → 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). diff --git a/docs/handoffs/HANDOFF-2026-06-25.md b/docs/handoffs/HANDOFF-2026-06-25.md new file mode 100644 index 0000000..f7abae2 --- /dev/null +++ b/docs/handoffs/HANDOFF-2026-06-25.md @@ -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 install -r packaging/out/re4vr-shim.apk +adb -s logcat | grep -i xrr +``` +Device Quest (USB serial ``, or `: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. diff --git a/docs/handoffs/HANDOFF-2026-06-26-reproject.md b/docs/handoffs/HANDOFF-2026-06-26-reproject.md new file mode 100644 index 0000000..0e5176c --- /dev/null +++ b/docs/handoffs/HANDOFF-2026-06-26-reproject.md @@ -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=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 install -r packaging/out/re4vr-shim.apk +adb -s 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). diff --git a/docs/handoffs/HANDOFF-2026-06-26-skipblack.md b/docs/handoffs/HANDOFF-2026-06-26-skipblack.md new file mode 100644 index 0000000..45b582f --- /dev/null +++ b/docs/handoffs/HANDOFF-2026-06-26-skipblack.md @@ -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 install -r packaging/out/re4vr-shim.apk +adb -s 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. diff --git a/docs/handoffs/HANDOFF-2026-06-26.md b/docs/handoffs/HANDOFF-2026-06-26.md new file mode 100644 index 0000000..c41030b --- /dev/null +++ b/docs/handoffs/HANDOFF-2026-06-26.md @@ -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 install -r packaging/out/re4vr-shim.apk +adb -s 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 ``. 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). diff --git a/docs/handoffs/HANDOFF-2026-06-27-native-parity.md b/docs/handoffs/HANDOFF-2026-06-27-native-parity.md new file mode 100644 index 0000000..2e356d4 --- /dev/null +++ b/docs/handoffs/HANDOFF-2026-06-27-native-parity.md @@ -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_()` 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 install -r packaging/out/re4vr-shim.apk # -r over SAME-signed shim PRESERVES data (no OBB/save dance) +adb -s logcat -G 16M # big buffer so brief windows don't rotate +adb -s 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. diff --git a/docs/handoffs/HANDOFF-2026-06-27.md b/docs/handoffs/HANDOFF-2026-06-27.md new file mode 100644 index 0000000..9596a68 --- /dev/null +++ b/docs/handoffs/HANDOFF-2026-06-27.md @@ -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 install -r packaging/out/re4vr-shim.apk +adb -s 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. diff --git a/docs/research/NEXT-SESSION-LATENCY.md b/docs/research/NEXT-SESSION-LATENCY.md new file mode 100644 index 0000000..767bbbf --- /dev/null +++ b/docs/research/NEXT-SESSION-LATENCY.md @@ -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 :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://` + 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. diff --git a/docs/research/RE-NOTES.md b/docs/research/RE-NOTES.md new file mode 100644 index 0000000..d205448 --- /dev/null +++ b/docs/research/RE-NOTES.md @@ -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>. +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"). diff --git a/docs/research/RECON.md b/docs/research/RECON.md new file mode 100644 index 0000000..d0d6d42 --- /dev/null +++ b/docs/research/RECON.md @@ -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 (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) diff --git a/docs/research/SHIM-SCOPE.md b/docs/research/SHIM-SCOPE.md new file mode 100644 index 0000000..41475a6 --- /dev/null +++ b/docs/research/SHIM-SCOPE.md @@ -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). diff --git a/docs/research/ghost-fix-2026-06-27.md b/docs/research/ghost-fix-2026-06-27.md new file mode 100644 index 0000000..7af859c --- /dev/null +++ b/docs/research/ghost-fix-2026-06-27.md @@ -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). diff --git a/docs/research/lever2-detect-skip-black-handoff.md b/docs/research/lever2-detect-skip-black-handoff.md new file mode 100644 index 0000000..fc7eb0a --- /dev/null +++ b/docs/research/lever2-detect-skip-black-handoff.md @@ -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 `` (USB). Build/deploy: +`./shim/build_android.sh && ./packaging/repack.sh && adb -s 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. +``` diff --git a/docs/research/related-work.md b/docs/research/related-work.md new file mode 100644 index 0000000..3a06814 --- /dev/null +++ b/docs/research/related-work.md @@ -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 `` 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: (GPLv3) +- Overport library index: `https://ovrp.crx.moe/api/v1/releases/index` +- Quake III VR Edition: +- Meta deprecates VrApi / "all-in on OpenXR": + +- OVRPlugin vs VRAPI vs LibOVR: + +- Allegations Meta's OVRPlugin blocks non-Meta runtimes (Voices of VR #1526): + diff --git a/docs/research/render-submit-sync-design.md b/docs/research/render-submit-sync-design.md new file mode 100644 index 0000000..e9742ee --- /dev/null +++ b/docs/research/render-submit-sync-design.md @@ -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`. diff --git a/ghidra_scripts/DumpEndFrame.java b/ghidra_scripts/DumpEndFrame.java new file mode 100644 index 0000000..c5e2025 --- /dev/null +++ b/ghidra_scripts/DumpEndFrame.java @@ -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 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); + } +} diff --git a/ghidra_scripts/DumpInit.java b/ghidra_scripts/DumpInit.java new file mode 100644 index 0000000..1209393 --- /dev/null +++ b/ghidra_scripts/DumpInit.java @@ -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 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 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); + } +} diff --git a/ghidra_scripts/DumpOvrp.java b/ghidra_scripts/DumpOvrp.java new file mode 100644 index 0000000..543e528 --- /dev/null +++ b/ghidra_scripts/DumpOvrp.java @@ -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 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 sigs = new ArrayList<>(); + List 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); + } +} diff --git a/ghidra_scripts/DumpReal.java b/ghidra_scripts/DumpReal.java new file mode 100644 index 0000000..f0aa776 --- /dev/null +++ b/ghidra_scripts/DumpReal.java @@ -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 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); + } +} diff --git a/ghidra_scripts/DumpStructs.java b/ghidra_scripts/DumpStructs.java new file mode 100644 index 0000000..b884ecb --- /dev/null +++ b/ghidra_scripts/DumpStructs.java @@ -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 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 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); + } +} diff --git a/packaging/README.md b/packaging/README.md new file mode 100644 index 0000000..9451300 --- /dev/null +++ b/packaging/README.md @@ -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 `` 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). diff --git a/packaging/_env.sh b/packaging/_env.sh new file mode 100644 index 0000000..18a0bbf --- /dev/null +++ b/packaging/_env.sh @@ -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)}" diff --git a/packaging/build_openxr_loader.sh b/packaging/build_openxr_loader.sh new file mode 100755 index 0000000..a203c51 --- /dev/null +++ b/packaging/build_openxr_loader.sh @@ -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 diff --git a/packaging/inspect_manifest.sh b/packaging/inspect_manifest.sh new file mode 100755 index 0000000..69b68e6 --- /dev/null +++ b/packaging/inspect_manifest.sh @@ -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." diff --git a/packaging/make_debug_keystore.sh b/packaging/make_debug_keystore.sh new file mode 100755 index 0000000..428efb8 --- /dev/null +++ b/packaging/make_debug_keystore.sh @@ -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)" diff --git a/packaging/repack.sh b/packaging/repack.sh new file mode 100755 index 0000000..a1710ec --- /dev/null +++ b/packaging/repack.sh @@ -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/)" diff --git a/packaging/steamframe_patches.sh b/packaging/steamframe_patches.sh new file mode 100755 index 0000000..194296f --- /dev/null +++ b/packaging/steamframe_patches.sh @@ -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 / 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]*]*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." diff --git a/scripts/fetch_deps.sh b/scripts/fetch_deps.sh new file mode 100644 index 0000000..9cbb1db --- /dev/null +++ b/scripts/fetch_deps.sh @@ -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." diff --git a/shim/README.md b/shim/README.md new file mode 100644 index 0000000..79f9f20 --- /dev/null +++ b/shim/README.md @@ -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. diff --git a/shim/build_android.sh b/shim/build_android.sh new file mode 100755 index 0000000..c7b8f22 --- /dev/null +++ b/shim/build_android.sh @@ -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" diff --git a/shim/build_host.sh b/shim/build_host.sh new file mode 100755 index 0000000..39ee16a --- /dev/null +++ b/shim/build_host.sh @@ -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" diff --git a/shim/gen_stubs.sh b/shim/gen_stubs.sh new file mode 100755 index 0000000..4b707fd --- /dev/null +++ b/shim/gen_stubs.sh @@ -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" diff --git a/shim/include/ovrplugin_shim.h b/shim/include/ovrplugin_shim.h new file mode 100644 index 0000000..36d1978 --- /dev/null +++ b/shim/include/ovrplugin_shim.h @@ -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 + +#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 */ diff --git a/shim/include/passthru.h b/shim/include/passthru.h new file mode 100644 index 0000000..ab8b1b7 --- /dev/null +++ b/shim/include/passthru.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 */ diff --git a/shim/src/android_init.c b/shim/src/android_init.c new file mode 100644 index 0000000..1f3c92a --- /dev/null +++ b/shim/src/android_init.c @@ -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 +#define XR_USE_PLATFORM_ANDROID +#include + +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 diff --git a/shim/src/core.c b/shim/src/core.c new file mode 100644 index 0000000..828afdd --- /dev/null +++ b/shim/src/core.c @@ -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 +#ifdef __ANDROID__ +#include +#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; +} diff --git a/shim/src/layers.c b/shim/src/layers.c new file mode 100644 index 0000000..28d2075 --- /dev/null +++ b/shim/src/layers.c @@ -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 +#include +#include + +/* [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); +} diff --git a/shim/src/log.h b/shim/src/log.h new file mode 100644 index 0000000..f23a2c6 --- /dev/null +++ b/shim/src/log.h @@ -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 +#define XRRLOG(...) __android_log_print(ANDROID_LOG_INFO, "xrr", __VA_ARGS__) +#define XRRERR(...) __android_log_print(ANDROID_LOG_ERROR, "xrr", __VA_ARGS__) +#else +#include +#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 diff --git a/shim/src/passthru.c b/shim/src/passthru.c new file mode 100644 index 0000000..e1ba101 --- /dev/null +++ b/shim/src/passthru.c @@ -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 +#ifdef __ANDROID__ +#include +#include +#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); +} diff --git a/shim/src/stubs.c b/shim/src/stubs.c new file mode 100644 index 0000000..c86f4a1 --- /dev/null +++ b/shim/src/stubs.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 */ diff --git a/shim/src/vk_session.c b/shim/src/vk_session.c new file mode 100644 index 0000000..18052db --- /dev/null +++ b/shim/src/vk_session.c @@ -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 +#define XR_USE_GRAPHICS_API_VULKAN +#include +#include "xr_runtime.h" +#include "passthru.h" +#include "log.h" +#include +#include +#include +#include + +/* 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 + * 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< 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< 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< 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< 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; +} diff --git a/shim/src/xr_input.c b/shim/src/xr_input.c new file mode 100644 index 0000000..37e301d --- /dev/null +++ b/shim/src/xr_input.c @@ -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 + +/* 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; +} diff --git a/shim/src/xr_runtime.c b/shim/src/xr_runtime.c new file mode 100644 index 0000000..0fbc871 --- /dev/null +++ b/shim/src/xr_runtime.c @@ -0,0 +1,2150 @@ +/* xr_runtime.c — OpenXR session + frame-loop engine. See xr_runtime.h. + * + * Full pipeline is live: instance/session creation, Vulkan graphics binding (from the + * ovrp_Initialize5 handshake), per-eye swapchains, composition, and the frame loop. + * Frame pacing runs on the GAME thread (xrWaitFrame in xrr_wait_frame) and hands the + * frameState to the RENDER thread via the g_fsRing FIFO — see the frame-loop section. + */ +#include "xr_runtime.h" +#include "log.h" +#include +#include +#include +#include +#include +#ifdef __ANDROID__ +#include +#endif + +/* CLOCK_MONOTONIC ns — Meta's XrTime on Android is this clock, so we can compare + * our submit moment directly against frameState.predictedDisplayTime. */ +static int64_t now_ns(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (int64_t)ts.tv_sec * 1000000000LL + ts.tv_nsec; +} + +XrRuntime g_xr; + +/* Serializes all session/frame OpenXR calls. ovrp_WaitToBeginFrame/Update3 run on + * the game thread, BeginFrame4/EndFrame4 on the render thread; without this the + * session teardown (xrEndSession) races the frame loop and crashes the runtime. */ +static pthread_mutex_t g_xrlock = PTHREAD_MUTEX_INITIALIZER; + +/* Game-thread frame pacing (vrapi-style). UE's loop is pipelined: ovrp_WaitToBeginFrame(N) runs on + * the GAME thread one frame ahead of ovrp_BeginFrame4/EndFrame4(N-1) on the RENDER thread (proven by + * the FLOOP trace). OpenXR's recommended pipelined model is: xrWaitFrame on the sim/game thread + * (it BLOCKS = paces the game, like vrapi), xrBeginFrame/xrEndFrame on the render thread. We hand the + * xrWaitFrame'd frameState from the game thread to the render thread via this 1:1 FIFO ring. Pacing + * the game thread (instead of our old no-op + xrWaitFrame-on-render-thread workaround) is what lets + * UE's internal pipeline run smoothly -> stops the render-thread stalls that drop frames -> judder. */ +#define FS_RING 8 +static XrFrameState g_fsRing[FS_RING]; +static unsigned g_fsHead, g_fsTail; /* head=pushed by wait, tail=popped by begin */ +static pthread_cond_t g_fsCond = PTHREAD_COND_INITIALIZER; + +/* Published per-frame view snapshot for the GAME-thread pose/time getters + * (xrr_get_node_pose / xrr_eye_fov_tangents / xrr_predicted_display_time_s). The render thread + * (single writer, begin_frame after xrLocateViews) publishes it; the game-thread getters read it. + * A SEQLOCK, not a mutex: an A/B soak showed a per-frame mutex on these hot getters cost real + * smoothness, so readers spin on a version counter instead (no syscall, no contention, no torn + * read). g_poseSeq is even when stable, odd while the writer is mid-update. */ +static volatile unsigned g_poseSeq; +static XrView g_pubViews[2]; +static uint32_t g_pubViewCount; +static XrTime g_pubDisplayTime; + +/* Lock-free snapshot of the published views (seqlock reader). Retries if it catches a write. */ +static void pose_snapshot(XrView outViews[2], uint32_t *outCount, XrTime *outTime) { + for (;;) { + unsigned s1 = __atomic_load_n(&g_poseSeq, __ATOMIC_ACQUIRE); + if (s1 & 1u) continue; /* writer mid-update -> retry */ + if (outViews) { outViews[0] = g_pubViews[0]; outViews[1] = g_pubViews[1]; } + if (outCount) *outCount = g_pubViewCount; + if (outTime) *outTime = g_pubDisplayTime; + __atomic_thread_fence(__ATOMIC_ACQUIRE); /* data reads complete before re-check */ + if (__atomic_load_n(&g_poseSeq, __ATOMIC_RELAXED) == s1) return; /* no write intervened */ + } +} + +/* ----------------------------------------------- render-ahead pipeline ----- * + * The tile-memory flush (vk_session.c) must complete before the compositor reads + * the image, but the Meta runtime won't sync against our submit — so a synchronous + * flush-wait inside end_frame pushes xrEndFrame past predictedDisplayTime and the + * compositor reprojects every frame (= ghosting on head motion). Instead we render + * one frame ahead: submit frame N's flush without waiting, present frame N-1's + * image (whose flush finished during this frame's work) with its STORED views/time. + * Net +1 frame latency (absorbed by normal reprojection), but xrEndFrame lands on + * schedule. g_pending holds the composition built last frame, presented this frame. + * Single-buffered: xrEndFrame consumes the layer structs synchronously, so we can + * rebuild g_pending in place right after submitting it. submit[]/proj.views point + * back into this struct's own (file-static, stable) arrays. */ +typedef struct { + int valid; + XrTime displayTime; + XrCompositionLayerProjection proj; + XrCompositionLayerProjectionView pviews[2]; + XrCompositionLayerDepthInfoKHR pdepth[2]; /* chained on pviews when present */ + XrCompositionLayerQuad quads[XRR_MAX_LAYERS]; + const XrCompositionLayerBaseHeader *submit[XRR_MAX_LAYERS]; + int nLayers; +} PendingFrame; +static PendingFrame g_pending; +static int g_pipelineActive; /* engaged once flush ring is up + layers double-buffered */ +static int g_copyRingEngaged; /* this frame is using the copy-ring path (drops quads) */ +static double g_gpuFrameMs; /* measured end-to-end frame dt (ms), fed to ovrp_GetGPUFrameTime */ +static int g_postHitch; /* Lever 2: >0 => this frame is a post-hitch LIKELY-TRUNCATED candidate (skipblack) */ +static int g_frameLuma = -1; /* Lever 2: max luma (0..255) of this frame's resolved eye image (lumagate) */ +/* black-frame detection (debug.re4vr.blackcount / lumagate diagnostics) */ +static int g_frameBlack; /* this frame is a truncation black (luma fewer */ + +/* present-on-submit: present the deferred frame after the Nth render-queue submit + * following end_frame (the eye render is split across ~3 submits that all land before + * the next END; presenting on the 1st was too early -> still black). Tunable to pin the + * exact submit; default 3 = wait for the whole render cluster. debug.re4vr.defern. */ +static int defer_submit_count(void) { + int v = 3; +#ifdef __ANDROID__ + char s[PROP_VALUE_MAX] = {0}; + if (__system_property_get("debug.re4vr.defern", s) > 0) v = atoi(s); +#endif + return v < 1 ? 1 : v; +} + +/* Render-ahead is OFF by default (the synchronous flush-wait path is known-good and + * playable; the pipeline regressed to black — Meta appears to mis-composite a + * swapchain image held acquired across xrEndFrame). Opt in at runtime WITHOUT a + * rebuild for A/B testing: adb shell setprop debug.re4vr.pipeline 1 then relaunch. + * Read once and cached. */ +static int pipeline_wanted(void) { + static int cached = -1; + if (cached >= 0) return cached; + cached = 0; +#ifdef __ANDROID__ + char v[PROP_VALUE_MAX] = {0}; + if (__system_property_get("debug.re4vr.pipeline", v) > 0 && v[0] == '1') cached = 1; +#endif + XRRLOG("render-ahead pipeline: %s (debug.re4vr.pipeline)", cached ? "ENABLED" : "off (synchronous)"); + return cached; +} + +/* Visual A/B probe for the stereo "dupe" (no rebuild — adb shell setprop + * debug.re4vr.diag N; relaunch). 0=off (normal), 1=projection only (drop quads, + * tests layer overlap), 2=force mono (both eyes sample array layer 0; if the dupe + * disappears it's stereo divergence, if it persists it's a single-eye/temporal + * ghost). Re-read each call so it can be flipped without relaunch where possible. */ +static int diag_mode(void) { + static int cached = -2; + int v = cached; +#ifdef __ANDROID__ + char s[PROP_VALUE_MAX] = {0}; + if (__system_property_get("debug.re4vr.diag", s) > 0) v = atoi(s); else v = 0; +#else + v = 0; +#endif + if (v != cached) { cached = v; XRRLOG("diag_mode -> %d", v); } + return cached < 0 ? 0 : cached; +} + +/* Frame-lifecycle trace verbosity (debug.re4vr.trace, re-read each frame): + * 0 = off (default; only the always-on starvation/hitch warnings fire) + * 1 = one FRAME summary line per end_frame (frameIndex, shouldRender, layer counts, + * dt, late) — the per-frame pacing trace + * 2 = + per-call WAIT/BEGIN and a per-layer submit dump (id/stage/flags/pose) — the + * "log everything going through the shim" mode for spotting an old-lib workaround + * Independent of debug.re4vr.diag (visual A/B). */ +static int trace_level(void) { + static int cached = -2; + int v = cached; +#ifdef __ANDROID__ + char s[PROP_VALUE_MAX] = {0}; + if (__system_property_get("debug.re4vr.trace", s) > 0) v = atoi(s); else v = 0; +#else + v = 0; +#endif + if (v != cached) { cached = v; XRRLOG("trace_level -> %d", v); } + return cached < 0 ? 0 : cached; +} + +/* Depth-submission toggle + tuning (read once, before any layer is created so the + * depth swapchain decision is stable). debug.re4vr.depth=1 enables it. We don't + * have UE's exact depth-projection params (they're in the un-reversed ovrpLayerSubmit + * tail), so assume Quest-UE convention: reverse-Z, infinite far. Tunable on-device: + * debug.re4vr.depth_nearz_mm (near plane in mm, default 100 = 0.1m) + * debug.re4vr.depth_revz (1 = reverse-Z/infinite far [default], 0 = standard) */ +static int g_haveDepthExt = 0; /* XR_KHR_composition_layer_depth advertised (set in pre_init) */ +static int g_depthOn = -1; +static float g_depthNearZ = 0.1f; +static int g_depthRevZ = 1; +static int depth_wanted(void) { + if (g_depthOn >= 0) return g_depthOn; + g_depthOn = 0; +#ifdef __ANDROID__ + char s[PROP_VALUE_MAX] = {0}; + if (__system_property_get("debug.re4vr.depth", s) > 0 && s[0] == '1') g_depthOn = 1; + if (__system_property_get("debug.re4vr.depth_nearz_mm", s) > 0) { + int mm = atoi(s); if (mm > 0) g_depthNearZ = (float)mm / 1000.0f; + } + if (__system_property_get("debug.re4vr.depth_revz", s) > 0) g_depthRevZ = (s[0] != '0'); +#endif + XRRLOG("depth: %s nearZ=%.3fm reverseZ=%d (ext=%d)", + g_depthOn ? "ENABLED" : "off", g_depthNearZ, g_depthRevZ, g_haveDepthExt); + return g_depthOn && g_haveDepthExt; +} + +/* Copy-ring toggle (debug.re4vr.copyring=1): UE renders into shim images, we copy -> + * OpenXR + pipeline the resolve so the CPU never blocks on the current frame's GPU + * (breaks the flush-wait serialization that drops frames). Read once at layer setup. */ +static int g_copyringOn = -1; +static int copyring_wanted(void) { + if (g_copyringOn >= 0) return g_copyringOn; + g_copyringOn = 0; +#ifdef __ANDROID__ + char s[PROP_VALUE_MAX] = {0}; + if (__system_property_get("debug.re4vr.copyring", s) > 0 && s[0] == '1') g_copyringOn = 1; +#endif + XRRLOG("copy-ring: %s (debug.re4vr.copyring)", g_copyringOn ? "ENABLED" : "off"); + return g_copyringOn; +} + +/* PERF test: skip the flush-wait (debug.re4vr.noflushwait=1). Re-read each frame. */ +static int noflushwait_wanted(void) { + int v = 0; +#ifdef __ANDROID__ + char s[PROP_VALUE_MAX] = {0}; + if (__system_property_get("debug.re4vr.noflushwait", s) > 0 && s[0] == '1') v = 1; +#endif + return v; +} + +/* Render-submit race fix (debug.re4vr.submithook, re-read each frame): + * 0 = off (current synchronous present; default, unchanged path) + * 1 = install the UE vkQueueSubmit hook + INSTRUMENT only (log submit-vs-EndFrame + * order, queue, fence; NO behavior change) — validates the hook + the threading + * interleave before we depend on it + * 2 = present-on-submit: end_frame defers; the hook fires the present after UE's + * eye-render submit lands (orders present after render, no fixed latency) */ +static int submithook_level(void) { + int v = 0; +#ifdef __ANDROID__ + char s[PROP_VALUE_MAX] = {0}; + if (__system_property_get("debug.re4vr.submithook", s) > 0) v = atoi(s); +#endif + return v < 0 ? 0 : v; +} + +/* Lever 2 — detect dropped/black frames and (eventually) reproject last-good instead of + * presenting UE's truncated/black frame (debug.re4vr.skipblack, re-read each frame): + * 0 = off (default, unchanged path) + * 1 = INSTRUMENT only: flag the recovery frame(s) after a HITCH as LIKELY-TRUNCATED and + * log them; NO behavior change. Validate these logs correlate with perceived black + * (walk off the bridge into the house, standing) before depending on the signal. + * 2 = (future) reproject: re-present the last-good eye image on a flagged frame. + * Root cause is settled: UE renders a truncated (~28 vs ~198 draws) black frame under load; + * the truncated frame is the RECOVERY frame following a stall, which we already detect as a + * dt>20ms HITCH. See analysis/lever2-detect-skip-black-handoff.md. */ +static int skipblack_level(void) { + int v = 0; +#ifdef __ANDROID__ + char s[PROP_VALUE_MAX] = {0}; + if (__system_property_get("debug.re4vr.skipblack", s) > 0) v = atoi(s); +#endif + return v < 0 ? 0 : v; +} + +/* How many frames after a HITCH to flag as candidate-truncated (debug.re4vr.skipblackn, + * default 2 — RenderDoc showed the recovery frame is the bad one; 1-2 covers the window). */ +static int skipblack_count(void) { + int v = 2; +#ifdef __ANDROID__ + char s[PROP_VALUE_MAX] = {0}; + if (__system_property_get("debug.re4vr.skipblackn", s) > 0) { int n = atoi(s); if (n > 0) v = n; } +#endif + return v; +} + +/* Frame-counter barcode overlay (debug.re4vr.barcode=1): stamp the frameIndex as a + * black/white barcode into the eye image each frame so a Meta Cast recording aligns + * frame-exactly to the FRAME/SKIPBLACK logs (read bits on a black flash -> grep f=N). + * Validation tool for Lever 2; re-read each frame, off by default. */ +static int barcode_wanted(void) { + int v = 0; +#ifdef __ANDROID__ + char s[PROP_VALUE_MAX] = {0}; + if (__system_property_get("debug.re4vr.barcode", s) > 0 && s[0] == '1') v = 1; +#endif + return v; +} + +/* Per-frame luminance black gate (debug.re4vr.lumagate=1): after the resolve, sample a few + * rows of the eye image; max luma ~0 => this frame is truncated/black. Covers the sustained + * black tail that the onset-only post-hitch flag misses. Re-read each frame; off by default + * (adds a small readback submit/wait per frame). */ +static int lumagate_wanted(void) { + int v = 0; +#ifdef __ANDROID__ + char s[PROP_VALUE_MAX] = {0}; + if (__system_property_get("debug.re4vr.lumagate", s) > 0 && s[0] == '1') v = 1; +#endif + return v; +} +/* black threshold for the luma gate (debug.re4vr.lumathr, default 12): max-luma below this + * = black. Tunable while we calibrate against the recording. */ +static int lumagate_thr(void) { + int v = 12; +#ifdef __ANDROID__ + char s[PROP_VALUE_MAX] = {0}; + if (__system_property_get("debug.re4vr.lumathr", s) > 0) { int n = atoi(s); if (n >= 0) v = n; } +#endif + return v; +} + +/* (Lever 2 reproject + Lever C depth-hold were removed — they were a black-frame mitigation, + * not the eye-ghost fix; the ghost was a frame-pacing bug, fixed in the frame loop. The luma + * read + blackcount/lumagate black-detection diagnostics below are kept.) */ +/* Min one-frame luma drop to treat a black onset as a TRUNCATION (debug.re4vr.abruptdrop). + * DEFAULT 0 = reproject ALL black (proven no-flash behavior). Raise it (e.g. 25) to skip + * gradual fades (comfort/scene/load ramp ~5-15/frame): truncation snaps lit->black (drop + * 50-77) so it still triggers, but the bigger the value the more shallow truncations are + * missed (black returns) — a fragile balance, hence opt-in. */ +static int abruptdrop(void) { + int v = 0; +#ifdef __ANDROID__ + char s[PROP_VALUE_MAX] = {0}; + if (__system_property_get("debug.re4vr.abruptdrop", s) > 0) { int n = atoi(s); if (n >= 0) v = n; } +#endif + return v; +} +/* Lever B: always-on truncation-black counter (debug.re4vr.blackcount, default 1). Counts the + * per-frame bad-black verdict so we can compare black/min between sync and render-ahead. Cheap + * (just needs the luma read, which it forces on if nothing else wants it). */ +static int blackcount_wanted(void) { + int v = 0; /* default OFF: the per-frame luma readback (GPU copy+fence) is shipping overhead; + * opt in for black-frame measurement with debug.re4vr.blackcount=1. */ +#ifdef __ANDROID__ + char s[PROP_VALUE_MAX] = {0}; + if (__system_property_get("debug.re4vr.blackcount", s) > 0 && s[0] == '1') v = 1; +#endif + return v; +} +/* Lever B: engage the render-ahead (deferred-flush) pipeline (debug.re4vr.renderahead, default 0). + * Kept separate from the legacy debug.re4vr.pipeline escape hatch so the old path stays available + * for comparison. Gives UE a full extra frame of GPU budget before we resolve -> fewer/shorter + * truncations; reproject+luma ride along as the safety net for any residual black. */ +static int renderahead_wanted(void) { + int v = 0; +#ifdef __ANDROID__ + char s[PROP_VALUE_MAX] = {0}; + if (__system_property_get("debug.re4vr.renderahead", s) > 0 && s[0] == '1') v = 1; +#endif + return v; +} +/* Layer z-order fix (debug.re4vr.eyebottom, default ON). OVRPlugin/VrApi treat the eye-fov as the + * BASE world layer regardless of the order the app submits layers in; OpenXR composites STRICTLY in + * array order (last = on top). RE4's splash/logo path submits the logo QUAD before the eye-fov, so + * without reordering our opaque eye-fov projection draws ON TOP of the logo quad -> black logos. + * Reorder so projection layers sit at the bottom, quads on top — matching native. */ +static int eyebottom_wanted(void) { + int v = 1; +#ifdef __ANDROID__ + char s[PROP_VALUE_MAX] = {0}; + if (__system_property_get("debug.re4vr.eyebottom", s) > 0 && s[0] == '0') v = 0; +#endif + return v; +} + +/* DIAG probe (debug.re4vr.qwait=1): vkQueueWaitIdle on UE's queue before we resolve/ + * present, to test the render-submit race (black under load). Re-read each frame. */ +static int qwait_wanted(void) { + int v = 0; +#ifdef __ANDROID__ + char s[PROP_VALUE_MAX] = {0}; + if (__system_property_get("debug.re4vr.qwait", s) > 0 && s[0] == '1') v = 1; +#endif + return v; +} + +/* Texture readback. Setting debug.re4vr.dump=N captures a BURST of the next N consecutive + * frames (intermittent/flashing artifacts need many frames to catch). Returns the burst frame + * index 1..N while a burst is in progress (used to frame-number the dump files), else 0. */ +static int dump_pending(void) { + static int last = 0, remaining = 0, seq = 0; int v = last; +#ifdef __ANDROID__ + char s[PROP_VALUE_MAX] = {0}; + if (__system_property_get("debug.re4vr.dump", s) > 0) v = atoi(s); else v = 0; +#endif + if (v != last && v != 0) { last = v; remaining = v; seq = 0; XRRLOG("dump trigger: burst %d frames", v); } + else last = v; + if (remaining > 0) { remaining--; return ++seq; } + return 0; +} + +/* Drop any in-flight pipelined state and release held images (session teardown). + * Caller must hold g_xrlock and the session must still be valid. */ +static void pipeline_reset(void) { + for (int i = 0; i < g_xr.layerCount; i++) { + XrLayer *L = &g_xr.layers[i]; + XrSwapchainImageReleaseInfo ri = { XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO }; + if (L->presentPending) { xrReleaseSwapchainImage(L->swapchain, &ri); L->presentPending = 0; } + if (L->imageAcquired) { xrReleaseSwapchainImage(L->swapchain, &ri); L->imageAcquired = 0; } + } + g_pending.valid = 0; + g_pipelineActive = 0; +} + +#define XR_OK(x) (XR_SUCCEEDED(x)) +static int xrfail(const char *what, XrResult r) { + if (XR_SUCCEEDED(r)) return 0; + XRRERR("%s failed: XrResult=%d", what, (int)r); + return 1; +} + +/* Tracking origin. FLOOR (LOCAL_FLOOR) gives correct head-height-above-floor so + * body-anchored inventory lines up; EYE (LOCAL) is head-height origin. Default to + * eye level (the original working seated behaviour) and let the game switch to + * floor via SetTrackingOriginType2 when it wants roomscale/standing. */ +static int g_floorOrigin = 0; +static int g_haveLocalFloor = 0; /* set in pre_init from ext_supported() */ +static int g_havePerfExt = 0; /* XR_EXT_performance_settings advertised */ +static int g_haveFoveation = 0; /* XR_FB_foveation (+config+swapchain_update_state) advertised */ +static int g_haveEyeTrackedFov = 0;/* XR_META_foveation_eye_tracked advertised (Steam Frame/Quest Pro) */ + +static void make_app_space(int floor) { + if (g_xr.appSpace != XR_NULL_HANDLE) { xrDestroySpace(g_xr.appSpace); g_xr.appSpace = XR_NULL_HANDLE; } + XrReferenceSpaceCreateInfo ci = { XR_TYPE_REFERENCE_SPACE_CREATE_INFO }; + ci.poseInReferenceSpace.orientation.w = 1.0f; + + /* Floor (LOCAL_FLOOR) when the game asks (RE4 standing) so head height above + * the floor is correct; eye level (LOCAL) otherwise. The earlier "floor -> + * black" was a red herring (a FOV mismatch, since fixed), not the origin. */ + if (floor && g_haveLocalFloor) { + ci.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL_FLOOR; + if (xrCreateReferenceSpace(g_xr.session, &ci, &g_xr.appSpace) == XR_SUCCESS) { + XRRLOG("app space: LOCAL_FLOOR (floor, player-centred)"); + g_floorOrigin = floor; return; + } + XRRLOG("app space: LOCAL_FLOOR create failed, falling back to LOCAL"); + } + ci.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_LOCAL; + xrCreateReferenceSpace(g_xr.session, &ci, &g_xr.appSpace); + XRRLOG("app space: LOCAL (eye level) [requested floor=%d]", floor); + g_floorOrigin = floor; +} + +void xrr_set_tracking_origin(int floor) { + pthread_mutex_lock(&g_xrlock); + if (g_xr.session != XR_NULL_HANDLE) make_app_space(floor); + else g_floorOrigin = floor; + pthread_mutex_unlock(&g_xrlock); +} +int xrr_get_tracking_origin(void) { return g_floorOrigin; } + +/* Map the game's ovrp CPU/GPU perf level (~0-4) to an OpenXR perf-settings level and + * apply it via XR_EXT_performance_settings, so the device boosts clocks under load + * (the game was requesting this; we used to no-op it). isGpu=0 -> CPU domain. */ +void xrr_set_perf_level(int isGpu, int level) { + static union { PFN_xrPerfSettingsSetPerformanceLevelEXT f; PFN_xrVoidFunction v; } set = {0}; + static int loaded = 0, lastCpu = -99, lastGpu = -99; + /* Record the game's requested clock level (its intent) up front, so perf metrics + * 12/13 report it even if we can't forward to XR_EXT_performance_settings. */ + if (isGpu) g_gpuPerfLevel = level; else g_cpuPerfLevel = level; + if (!g_havePerfExt || g_xr.session == XR_NULL_HANDLE) return; + if (!loaded) { + loaded = 1; + xrGetInstanceProcAddr(g_xr.instance, "xrPerfSettingsSetPerformanceLevelEXT", &set.v); + } + if (!set.f) return; + int *last = isGpu ? &lastGpu : &lastCpu; + if (level == *last) return; /* only call on change */ + *last = level; + XrPerfSettingsLevelEXT lvl = (level <= 0) ? XR_PERF_SETTINGS_LEVEL_POWER_SAVINGS_EXT + : (level == 1) ? XR_PERF_SETTINGS_LEVEL_SUSTAINED_LOW_EXT + : (level == 2) ? XR_PERF_SETTINGS_LEVEL_SUSTAINED_HIGH_EXT + : XR_PERF_SETTINGS_LEVEL_BOOST_EXT; + XrPerfSettingsDomainEXT dom = isGpu ? XR_PERF_SETTINGS_DOMAIN_GPU_EXT + : XR_PERF_SETTINGS_DOMAIN_CPU_EXT; + pthread_mutex_lock(&g_xrlock); + XrResult r = set.f(g_xr.session, dom, lvl); + pthread_mutex_unlock(&g_xrlock); + XRRLOG("perf level: %s ovrp=%d -> xr=%d rc=%d", isGpu ? "GPU" : "CPU", level, (int)lvl, (int)r); +} + +/* Is an instance extension advertised by the runtime? (query before enabling) */ +static int ext_supported(const char *name) { + uint32_t n = 0; + if (xrEnumerateInstanceExtensionProperties(NULL, 0, &n, NULL) != XR_SUCCESS || n == 0) return 0; + XrExtensionProperties *props = calloc(n, sizeof(*props)); + if (!props) return 0; + for (uint32_t i = 0; i < n; i++) props[i].type = XR_TYPE_EXTENSION_PROPERTIES; + int found = 0; + if (xrEnumerateInstanceExtensionProperties(NULL, n, &n, props) == XR_SUCCESS) + for (uint32_t i = 0; i < n; i++) + if (strcmp(props[i].extensionName, name) == 0) { found = 1; break; } + free(props); + return found; +} + +/* ----------------------------------------------------------- lifecycle ----- */ +ovrpResult xrr_pre_init(void) { + if (g_xr.instance != XR_NULL_HANDLE) return ovrpSuccess; + memset(&g_xr, 0, sizeof(g_xr)); + g_xr.viewConfigType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO; + + /* Build the extension list. Vulkan binding is what RE4 needs (VulkanRHI); + * on Android we also prime the loader and chain the Android create info. */ + const char *exts[12]; + uint32_t ne = 0; + exts[ne++] = "XR_KHR_vulkan_enable"; /* v1: app creates VkInstance/Device */ + void *instNext = NULL; +#ifdef __ANDROID__ + xrr_android_init_loader(); /* xrInitializeLoaderKHR */ + exts[ne++] = "XR_KHR_android_create_instance"; + instNext = xrr_android_instance_next(); /* JavaVM + activity */ +#endif + /* LOCAL_FLOOR = player-centred origin (same x/z + facing as you) dropped to + * the floor. Roomscale games need floor height; unlike STAGE this doesn't + * reorient to the Guardian, so it won't reverse movement. */ + g_haveLocalFloor = ext_supported("XR_EXT_local_floor"); + if (g_haveLocalFloor) exts[ne++] = "XR_EXT_local_floor"; + /* Submit per-eye depth so the compositor can do POSITIONAL reprojection + * (XR_KHR_composition_layer_depth). Without it, head translation can't be + * reprojected -> the scene ghosts during head motion (artifact B). Enabling + * the ext is harmless; the depth swapchain/submit is gated by debug.re4vr.depth. */ + g_haveDepthExt = ext_supported(XR_KHR_COMPOSITION_LAYER_DEPTH_EXTENSION_NAME); + if (g_haveDepthExt) exts[ne++] = XR_KHR_COMPOSITION_LAYER_DEPTH_EXTENSION_NAME; + /* Honor the game's CPU/GPU perf-level requests (it calls ovrp_SetSystemCpu/GpuLevel2, + * which we used to no-op) so the device boosts clocks under load instead of + * sagging while CPU-bound -> fewer frame drops. */ + g_havePerfExt = ext_supported(XR_EXT_PERFORMANCE_SETTINGS_EXTENSION_NAME); + if (g_havePerfExt) exts[ne++] = XR_EXT_PERFORMANCE_SETTINGS_EXTENSION_NAME; + /* Fixed Foveated Rendering: the game asks for foveation (ovrp_GetLayerTextureFoveation, + * which we stub) — instead we apply runtime FFR to the eye swapchain, cutting periphery + * shading load (the GPU-bound black/stutter). Needs FB_foveation + its level profiles + + * swapchain_update_state to apply. Other HMDs (Steam Frame) will expose different + * foveation exts; apply_foveation() is the single extension point to add them. */ + g_haveFoveation = ext_supported(XR_FB_FOVEATION_EXTENSION_NAME) + && ext_supported(XR_FB_FOVEATION_CONFIGURATION_EXTENSION_NAME) + && ext_supported(XR_FB_SWAPCHAIN_UPDATE_STATE_EXTENSION_NAME); + if (g_haveFoveation) { + exts[ne++] = XR_FB_FOVEATION_EXTENSION_NAME; + exts[ne++] = XR_FB_FOVEATION_CONFIGURATION_EXTENSION_NAME; + exts[ne++] = XR_FB_SWAPCHAIN_UPDATE_STATE_EXTENSION_NAME; + /* Eye-tracked foveation (Steam Frame / Quest Pro): extends the FB framework so the + * runtime moves the high-res region with the user's gaze. Optional upgrade on top of + * fixed FFR — request only if advertised; without it we still get fixed FFR. The game + * (RE4) only asks for fixed TiledMultiRes; this transparently upgrades it to gaze-driven + * where the runtime supports it. (Absent on stock/upstream Monado; present on SteamVR/ + * Lepton and Quest Pro.) */ + g_haveEyeTrackedFov = ext_supported(XR_META_FOVEATION_EYE_TRACKED_EXTENSION_NAME); + if (g_haveEyeTrackedFov) exts[ne++] = XR_META_FOVEATION_EYE_TRACKED_EXTENSION_NAME; + } + XRRLOG("foveation: FB_foveation=%d eye_tracked(META)=%d", g_haveFoveation, g_haveEyeTrackedFov); + XrInstanceCreateInfo ici = { XR_TYPE_INSTANCE_CREATE_INFO }; + ici.next = instNext; + strcpy(ici.applicationInfo.applicationName, "RE4VR-OVRPlugin-shim"); + ici.applicationInfo.apiVersion = XR_API_VERSION_1_0; + ici.enabledExtensionCount = ne; + ici.enabledExtensionNames = exts; + XRRLOG("pre_init: xrCreateInstance (exts=%u, android=%d)", ne, instNext != NULL); + if (xrfail("xrCreateInstance", xrCreateInstance(&ici, &g_xr.instance))) + return ovrpFailure_OperationFailed; + + XrSystemGetInfo sgi = { XR_TYPE_SYSTEM_GET_INFO }; + sgi.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY; + if (xrfail("xrGetSystem", xrGetSystem(g_xr.instance, &sgi, &g_xr.systemId))) + return ovrpFailure_OperationFailed; + XRRLOG("pre_init OK: instance=%p systemId=%llu", (void *)g_xr.instance, + (unsigned long long)g_xr.systemId); + return ovrpSuccess; +} + +ovrpResult xrr_init(void *vkInstance, void *vkPhysicalDevice, void *vkDevice, + unsigned int queueFamilyIndex) { + /* The instance was created in PreInitialize3 with the Application context + * (the real Activity isn't available until here). Meta's runtime needs the + * real Activity to drive the session IDLE->READY transition, so recreate the + * instance with it before creating the session. */ + static int s_recreated = 0; + if (!s_recreated && xrr_android_have_real_activity() && + g_xr.instance != XR_NULL_HANDLE && g_xr.session == XR_NULL_HANDLE) { + s_recreated = 1; + XRRLOG("init: recreating XrInstance with the real Activity"); + xrDestroyInstance(g_xr.instance); + g_xr.instance = XR_NULL_HANDLE; + g_xr.systemId = XR_NULL_SYSTEM_ID; + } + if (g_xr.instance == XR_NULL_HANDLE) { + ovrpResult r = xrr_pre_init(); + if (!OVRP_SUCCESS(r)) return r; + } + /* Build the Vulkan graphics binding from the app's handles and create the + * session (vk_session.c). ABI of the handle args is [VERIFIED] from the real + * ovrp_Initialize5 impl. [ANDROID-TODO] xrCreateInstance also needs the JavaVM + * + activity (Initialize5 args 2 & 4) via XrInstanceCreateInfoAndroidKHR on the + * NDK build — captured separately. */ + XRRLOG("init: vkInstance=%p vkPhys=%p vkDevice=%p qfi=%u", + vkInstance, vkPhysicalDevice, vkDevice, queueFamilyIndex); + if (g_xr.session == XR_NULL_HANDLE) { + if (!xrr_create_session_vulkan(vkInstance, vkPhysicalDevice, vkDevice, + queueFamilyIndex)) + return ovrpFailure_OperationFailed; + } + XRRLOG("init OK: session=%p", (void *)g_xr.session); + + make_app_space(g_floorOrigin); /* floor-level (STAGE) by default */ + XrReferenceSpaceCreateInfo view = { XR_TYPE_REFERENCE_SPACE_CREATE_INFO }; + view.referenceSpaceType = XR_REFERENCE_SPACE_TYPE_VIEW; + view.poseInReferenceSpace.orientation.w = 1.0f; + xrCreateReferenceSpace(g_xr.session, &view, &g_xr.viewSpace); + + xrr_input_init(); /* action set must be attached before first xrSyncActions */ + return ovrpSuccess; +} + +void xrr_shutdown(void) { + /* Tear down under g_xrlock so we don't free the session/instance out from under the render + * thread (begin/end) or the game-thread getters, which all take the lock. Set running=0 + + * broadcast first so a render thread parked in the frameState cond-wait exits promptly. + * (Narrow residual: xrWaitFrame runs lock-free on the game thread for pacing, so a shutdown + * in its ~ns window after the running check could still race the destroy — acceptable for an + * app-exit-only path.) */ + /* Graceful exit handshake. xrEndSession is only legal in the STOPPING state, so we + * can't just call it — we ask the runtime to exit (xrRequestExitSession), then pump + * events until it transitions RUNNING -> ... -> STOPPING; poll_events() owns the + * xrEndSession on that transition (and closes any in-flight frame first). Done BEFORE + * taking g_xrlock for the destroy, since poll_events() takes the lock itself. Bounded + * (~1s) so a runtime that never transitions can't hang app exit; the destroy then + * proceeds regardless (xrDestroySession is valid from any state). */ + pthread_mutex_lock(&g_xrlock); + int running = g_xr.running && g_xr.session != XR_NULL_HANDLE; + pthread_mutex_unlock(&g_xrlock); + if (running && !xrfail("xrRequestExitSession", xrRequestExitSession(g_xr.session))) { + for (int i = 0; i < 200; i++) { /* 200 * 5ms = ~1s cap */ + xrr_poll_events(); /* STOPPING -> xrEndSession + running=0 */ + pthread_mutex_lock(&g_xrlock); + int stopped = !g_xr.running; + pthread_mutex_unlock(&g_xrlock); + if (stopped) break; + nanosleep(&(struct timespec){ 0, 5 * 1000 * 1000 }, NULL); + } + } + + pthread_mutex_lock(&g_xrlock); + g_xr.running = 0; + pthread_cond_broadcast(&g_fsCond); + /* poll_events already issued xrEndSession on STOPPING; destroy is valid from any state. */ + if (g_xr.session) xrDestroySession(g_xr.session); + if (g_xr.instance) xrDestroyInstance(g_xr.instance); + xrr_vk_teardown(); /* free shim-owned Vulkan objects + allow clean re-init */ + memset(&g_xr, 0, sizeof(g_xr)); + pthread_mutex_unlock(&g_xrlock); +} + +/* ---------------------------------------------------- session state machine */ +void xrr_poll_events(void) { + if (g_xr.instance == XR_NULL_HANDLE) return; + pthread_mutex_lock(&g_xrlock); + XrEventDataBuffer ev = { XR_TYPE_EVENT_DATA_BUFFER }; + while (xrPollEvent(g_xr.instance, &ev) == XR_SUCCESS) { + if (ev.type == XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED) { + XrEventDataSessionStateChanged *s = (XrEventDataSessionStateChanged *)&ev; + g_xr.sessionState = s->state; + static const char *snames[] = {"UNKNOWN","IDLE","READY","SYNCHRONIZED", + "VISIBLE","FOCUSED","STOPPING","LOSS_PENDING","EXITING"}; + int si = (int)s->state; + XRRLOG("session state -> %d (%s)", si, + (si >= 0 && si <= 8) ? snames[si] : "?"); + if (s->state == XR_SESSION_STATE_READY && !g_xr.running) { + XrSessionBeginInfo bi = { XR_TYPE_SESSION_BEGIN_INFO }; + bi.primaryViewConfigurationType = g_xr.viewConfigType; + if (!xrfail("xrBeginSession", xrBeginSession(g_xr.session, &bi))) { + g_xr.running = 1; + XRRLOG("xrBeginSession OK -> running"); + } + } else if (s->state == XR_SESSION_STATE_STOPPING && g_xr.running) { + /* close any in-flight frame first — xrEndSession while a frame is + * open crashes the runtime. */ + if (g_xr.inFrame) { + XrFrameEndInfo ei = { XR_TYPE_FRAME_END_INFO }; + ei.displayTime = g_xr.frameState.predictedDisplayTime; + ei.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE; + xrEndFrame(g_xr.session, &ei); + g_xr.inFrame = 0; + } + pipeline_reset(); /* release held images, drop stale composition */ + xrEndSession(g_xr.session); + g_xr.running = 0; + } + } + ev.type = XR_TYPE_EVENT_DATA_BUFFER; + } + pthread_mutex_unlock(&g_xrlock); +} + +/* ----------------------------------------------------------- frame loop ---- */ +/* Game-thread pacing (OpenXR's recommended pipelined model, like vrapi): xrWaitFrame runs + * here on the GAME thread (ovrp_WaitToBeginFrame) and BLOCKS to pace it; the frameState is + * handed to the RENDER thread (ovrp_BeginFrame4/EndFrame4) via the g_fsRing FIFO. The render + * thread pops it in xrr_begin_frame instead of calling xrWaitFrame. This freed the render + * thread from pacing back-pressure and fixed the dropped-frame judder. wait/begin stay 1:1. */ +ovrpResult xrr_wait_frame(int frameIndex) { + if (trace_level() >= 2) XRRLOG("WAIT f=%d", frameIndex); + xrr_poll_events(); + pthread_mutex_lock(&g_xrlock); + int ready = g_xr.running && g_xr.session != XR_NULL_HANDLE; + pthread_mutex_unlock(&g_xrlock); + if (!ready) return ovrpFailure_NotYetImplemented; + + /* xrWaitFrame paces the GAME thread (vrapi model) and is NOT held under g_xrlock, so it never + * blocks the render thread's begin/end. The runtime synchronizes wait<->begin 1:1; we hand the + * resulting frameState to the render thread via the FIFO ring. */ + XrFrameWaitInfo fwi = { XR_TYPE_FRAME_WAIT_INFO }; + XrFrameState fs = { XR_TYPE_FRAME_STATE }; + if (xrfail("xrWaitFrame", xrWaitFrame(g_xr.session, &fwi, &fs))) + return ovrpFailure_OperationFailed; + + pthread_mutex_lock(&g_xrlock); + if (g_fsHead - g_fsTail >= FS_RING) g_fsTail++; /* drop oldest if render thread fell behind */ + g_fsRing[g_fsHead % FS_RING] = fs; + g_fsHead++; + pthread_cond_signal(&g_fsCond); + pthread_mutex_unlock(&g_xrlock); + return ovrpSuccess; +} + +ovrpResult xrr_begin_frame(int frameIndex) { + if (trace_level() >= 2) XRRLOG("BEGIN f=%d", frameIndex); + xrr_poll_events(); /* locks internally */ + pthread_mutex_lock(&g_xrlock); + if (!g_xr.running || g_xr.inFrame) { + static int rj = 0; + if (rj++ < 60 || (rj % 240) == 0) + XRRLOG("begin_frame REJECT: running=%d inFrame=%d (count=%d)", + g_xr.running, g_xr.inFrame, rj); + pthread_mutex_unlock(&g_xrlock); + return ovrpFailure_NotYetImplemented; + } + + /* Take the frameState from the matching xrWaitFrame done on the GAME thread (FIFO, 1:1). The + * game leads by ~1 frame so it's normally already queued; wait briefly otherwise. xrWaitFrame is + * NO LONGER called here — pacing now blocks the game thread (vrapi model), freeing this render + * thread from pacing back-pressure that was stalling UE's render -> dropped frames -> judder. */ + while (g_fsHead == g_fsTail && g_xr.running) { + struct timespec to; clock_gettime(CLOCK_REALTIME, &to); + to.tv_nsec += 20L * 1000000L; + if (to.tv_nsec >= 1000000000L) { to.tv_sec++; to.tv_nsec -= 1000000000L; } + pthread_cond_timedwait(&g_fsCond, &g_xrlock, &to); + } + if (g_fsHead == g_fsTail) { /* shutting down or game produced no wait */ + pthread_mutex_unlock(&g_xrlock); return ovrpFailure_NotYetImplemented; + } + g_xr.frameState = g_fsRing[g_fsTail % FS_RING]; + g_fsTail++; + + XrViewLocateInfo vli = { XR_TYPE_VIEW_LOCATE_INFO }; + vli.viewConfigurationType = g_xr.viewConfigType; + vli.displayTime = g_xr.frameState.predictedDisplayTime; + vli.space = g_xr.appSpace; + XrViewState vs = { XR_TYPE_VIEW_STATE }; + for (int i = 0; i < 2; i++) g_xr.views[i] = (XrView){ XR_TYPE_VIEW }; + xrLocateViews(g_xr.session, &vli, &vs, 2, &g_xr.viewCount, g_xr.views); + /* publish the view snapshot for the game-thread getters (seqlock writer; single writer = us) */ + { + unsigned s = g_poseSeq; + __atomic_store_n(&g_poseSeq, s + 1, __ATOMIC_RELEASE); /* odd: write in progress */ + g_pubViews[0] = g_xr.views[0]; g_pubViews[1] = g_xr.views[1]; + g_pubViewCount = g_xr.viewCount; g_pubDisplayTime = g_xr.frameState.predictedDisplayTime; + __atomic_store_n(&g_poseSeq, s + 2, __ATOMIC_RELEASE); /* even: stable */ + } + { /* DIAG: is the head pose UE renders from (xrLocateSpace VIEW, node=Head) + * consistent with the eye poses we submit (xrLocateViews)? UE builds its + * cameras from Head; we composite with the raw eye views. If Head != eye + * midpoint, or eye orientation != head orientation, UE renders one camera + * and the compositor reprojects to another => ghost/double. */ + static int hv = 0; + if ((hv++ % 120) == 0 && g_xr.viewCount == 2) { + XrSpaceLocation h = { XR_TYPE_SPACE_LOCATION }; + xrLocateSpace(g_xr.viewSpace, g_xr.appSpace, + g_xr.frameState.predictedDisplayTime, &h); + XrVector3f e0 = g_xr.views[0].pose.position, e1 = g_xr.views[1].pose.position; + XrQuaternionf hq = h.pose.orientation, q0 = g_xr.views[0].pose.orientation, + q1 = g_xr.views[1].pose.orientation; + XRRLOG("HEADvsEYE: head.pos=(%.3f,%.3f,%.3f) eyeMid=(%.3f,%.3f,%.3f) " + "head.q=(%.3f,%.3f,%.3f,%.3f) e0.q=(%.3f,%.3f,%.3f,%.3f) e1.q=(%.3f,%.3f,%.3f,%.3f)", + h.pose.position.x, h.pose.position.y, h.pose.position.z, + (e0.x+e1.x)*0.5f, (e0.y+e1.y)*0.5f, (e0.z+e1.z)*0.5f, + hq.x,hq.y,hq.z,hq.w, q0.x,q0.y,q0.z,q0.w, q1.x,q1.y,q1.z,q1.w); + } + } + { /* DIAG: stereo sanity — both eye poses + IPD. Bad IPD/identical eyes -> + * double vision / "duped at a different spot". */ + static int vc = 0; + if ((vc++ % 240) == 0) { + XrPosef *l = &g_xr.views[0].pose, *r = &g_xr.views[1].pose; + float ipd = sqrtf((l->position.x-r->position.x)*(l->position.x-r->position.x) + + (l->position.y-r->position.y)*(l->position.y-r->position.y) + + (l->position.z-r->position.z)*(l->position.z-r->position.z)); + XRRLOG("views flags=0x%x L=(%.3f,%.3f,%.3f) R=(%.3f,%.3f,%.3f) IPD=%.3f viewCount=%u", + (unsigned)vs.viewStateFlags, l->position.x,l->position.y,l->position.z, + r->position.x,r->position.y,r->position.z, ipd, g_xr.viewCount); + } + } + + XrFrameBeginInfo bi = { XR_TYPE_FRAME_BEGIN_INFO }; + XrResult bfr = xrBeginFrame(g_xr.session, &bi); + if (bfr == XR_FRAME_DISCARDED) { /* our wait/begin pairing slipped — count it (should be 0) */ + static long d = 0; XRRLOG("OUR xrBeginFrame XR_FRAME_DISCARDED (#%ld)", ++d); + } else if (xrfail("xrBeginFrame", bfr)) { + pthread_mutex_unlock(&g_xrlock); return ovrpFailure_OperationFailed; + } + g_xr.inFrame = 1; + + xrr_input_sync(); /* refresh controller state + hand poses for this frame */ + + /* acquire+wait this frame's image for each layer */ + for (int i = 0; i < g_xr.layerCount; i++) { + XrLayer *L = &g_xr.layers[i]; + if (!L->active || L->swapchain == XR_NULL_HANDLE || L->imageAcquired) continue; + XrSwapchainImageAcquireInfo ai = { XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO }; + XrResult ar = xrAcquireSwapchainImage(L->swapchain, &ai, &L->acquiredIndex); + { /* DIAG: index sequence + any acquire failure (e.g. second acquire while + * the pipeline holds an image -> would explain black). */ + static int ac = 0; + if (ac++ < 24) XRRLOG("pipe: acquire layer=%d rc=%d acquiredIndex=%u presentPending=%d imgCount=%u", + i, (int)ar, L->acquiredIndex, L->presentPending, L->imageCount); + } + if (ar != XR_SUCCESS) + continue; + XrSwapchainImageWaitInfo wi = { XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO }; + wi.timeout = 100000000; /* 100 ms — never block the render thread forever */ + XrResult wr = xrWaitSwapchainImage(L->swapchain, &wi); + if (wr == XR_TIMEOUT_EXPIRED) { + /* couldn't get the image in time: release the acquire so we stay + * balanced and skip this layer this frame rather than deadlock. */ + static int tw = 0; + if (tw++ < 40) XRRLOG("WaitSwapchainImage TIMEOUT layer=%d idx=%u", i, L->acquiredIndex); + XrSwapchainImageReleaseInfo ri = { XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO }; + xrReleaseSwapchainImage(L->swapchain, &ri); + L->imageAcquired = 0; + continue; + } + L->imageAcquired = 1; + /* acquire+wait the paired depth image (lockstep with color) so UE renders + * depth into it; released together in end_frame. Depth only runs in the + * synchronous path (the pipeline holds images and doesn't manage depth). */ + L->depthAcquired = 0; + if (L->depthSwapchain != XR_NULL_HANDLE && !g_pipelineActive) { + XrSwapchainImageAcquireInfo dai = { XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO }; + if (xrAcquireSwapchainImage(L->depthSwapchain, &dai, &L->depthAcquiredIndex) == XR_SUCCESS) { + XrSwapchainImageWaitInfo dwi = { XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO }; + dwi.timeout = 100000000; + if (xrWaitSwapchainImage(L->depthSwapchain, &dwi) == XR_TIMEOUT_EXPIRED) { + XrSwapchainImageReleaseInfo dri = { XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO }; + xrReleaseSwapchainImage(L->depthSwapchain, &dri); /* stay balanced */ + } else { + L->depthAcquired = 1; + } + } + } + } + static int hb = 0; + if ((hb++ % 240) == 0) + XRRLOG("begin_frame heartbeat #%d shouldRender=%d layers=%d", + hb, g_xr.frameState.shouldRender, g_xr.layerCount); + pthread_mutex_unlock(&g_xrlock); + return ovrpSuccess; +} + +/* Build the composition (projection + quads) the app submitted into `pf`, using + * the views/time current at call time. Captured at frame N, presented at frame N+1 + * so the stored views/time stay matched to the image they were rendered for (the + * pipeline must never pair an image with a different frame's poses -> reprojection + * to the wrong frustum). proj.views/submit[] point into pf's own arrays. */ +static void build_composition(PendingFrame *pf, + const ovrpLayerSubmit *const *layers, int layerCount) { + int diag = diag_mode(); + pf->nLayers = 0; + pf->displayTime = g_xr.frameState.predictedDisplayTime; + pf->valid = 1; + if (!layers || !g_xr.frameState.shouldRender) { + /* STARVATION: this frame will composite NOTHING -> compositor shows black + * (or reprojects). The title black-strobe / menu smear live here. Always + * log (capped), or every time under trace. */ + static int ce = 0; + if (ce++ < 300 || trace_level()) + XRRLOG("COMPOSE-EMPTY: layers=%p layerCount=%d shouldRender=%d -> BLACK frame", + (const void *)layers, layerCount, g_xr.frameState.shouldRender); + return; + } + + for (int k = 0; k < layerCount && pf->nLayers < XRR_MAX_LAYERS; k++) { + const ovrpLayerSubmit *s = layers[k]; + if (!s || s->LayerId < 0 || s->LayerId >= g_xr.layerCount) continue; + XrLayer *L = &g_xr.layers[s->LayerId]; + if (!L->active || L->swapchain == XR_NULL_HANDLE) continue; + /* projection only: drop quads (diag=1, or copy-ring mode which is eye-only) */ + if ((diag == 1 || g_copyRingEngaged) && !L->isEyeFov) continue; + + if (L->isEyeFov) { + /* DIAG: the game rendered into colorImages[TextureStage]; we acquire one + * image per frame in lockstep, so acquiredIndex must equal TextureStage. + * The render-ahead pipeline keeps this 1:1 cadence (one acquire + one + * release per frame), just offset by a frame. */ + static int mm = 0, ok = 0; + if ((uint32_t)s->TextureStage != L->acquiredIndex) { + if (mm++ < 40) + XRRLOG("LAYER MISMATCH: id=%d submitStage=%d acquiredIndex=%u imgCount=%u", + s->LayerId, s->TextureStage, L->acquiredIndex, L->imageCount); + } else if (ok++ < 5) { + XRRLOG("layer ok: id=%d stage==acquired=%u", s->LayerId, L->acquiredIndex); + } + { /* DIAG: does UE submit a sub-full ViewportRect (dynamic resolution)? + * If the rendered rect is smaller than the swapchain we currently sample + * the FULL texture (imageRect=L->width/height) -> shrunk/letterboxed image. + * Log a periodic baseline + ALWAYS log non-full rects (capped). */ + ovrpRecti v0 = s->ViewportRect[0], v1 = s->ViewportRect[1]; + int full0 = (v0.Size.w == (int)L->width && v0.Size.h == (int)L->height); + int full1 = (v1.Size.w == (int)L->width && v1.Size.h == (int)L->height); + static int vbase = 0, vsub = 0; + if (!full0 || !full1) { + if (vsub++ < 200) + XRRLOG("VIEWPORT SUB: id=%d tex=%ux%u L=(%d,%d %dx%d) R=(%d,%d %dx%d)", + s->LayerId, L->width, L->height, + v0.Pos.x, v0.Pos.y, v0.Size.w, v0.Size.h, + v1.Pos.x, v1.Pos.y, v1.Size.w, v1.Size.h); + } else if ((vbase++ % 60) == 0) { + XRRLOG("VIEWPORT full: id=%d tex=%ux%u rect=%dx%d", + s->LayerId, L->width, L->height, v0.Size.w, v0.Size.h); + } + } + for (int eye = 0; eye < 2; eye++) { + const XrView *vw = &g_xr.views[eye]; /* reproject removed: always live views */ + pf->pviews[eye] = (XrCompositionLayerProjectionView){ + XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW }; + pf->pviews[eye].pose = vw->pose; + pf->pviews[eye].fov = vw->fov; + pf->pviews[eye].subImage.swapchain = L->swapchain; + /* diag 2 = force mono: both eyes sample array layer 0. If the dupe + * vanishes -> stereo divergence; if it persists -> single-eye ghost. */ + pf->pviews[eye].subImage.imageArrayIndex = + (diag == 2) ? 0 : ((L->arraySize > 1) ? eye : 0); + if (diag == 3) { /* 2x center-zoom: sample only the center quarter. + * Dupe gap GROWS => offset is in the texture (UE render); gap + * UNCHANGED => compositor adds it after sampling. */ + pf->pviews[eye].subImage.imageRect.offset.x = (int)(L->width / 4); + pf->pviews[eye].subImage.imageRect.offset.y = (int)(L->height / 4); + pf->pviews[eye].subImage.imageRect.extent.width = (int)(L->width / 2); + pf->pviews[eye].subImage.imageRect.extent.height = (int)(L->height / 2); + } else { + pf->pviews[eye].subImage.imageRect.extent.width = (int)L->width; + pf->pviews[eye].subImage.imageRect.extent.height = (int)L->height; + } + /* Chain per-eye depth so the compositor can positionally reproject + * (artifact B). Reverse-Z/infinite-far is UE's Quest convention; + * params tunable via debug.re4vr.depth_* . Only in normal mode. */ + if (diag == 0 && !g_pipelineActive && L->depthSwapchain != XR_NULL_HANDLE) { + XrCompositionLayerDepthInfoKHR *d = &pf->pdepth[eye]; + *d = (XrCompositionLayerDepthInfoKHR){ XR_TYPE_COMPOSITION_LAYER_DEPTH_INFO_KHR }; + d->subImage.swapchain = L->depthSwapchain; + d->subImage.imageArrayIndex = (L->arraySize > 1) ? eye : 0; + d->subImage.imageRect.extent.width = (int)L->width; + d->subImage.imageRect.extent.height = (int)L->height; + if (g_depthRevZ) { d->minDepth = 0.0f; d->maxDepth = 1.0f; + d->nearZ = INFINITY; d->farZ = g_depthNearZ; } + else { d->minDepth = 0.0f; d->maxDepth = 1.0f; + d->nearZ = g_depthNearZ; d->farZ = INFINITY; } + pf->pviews[eye].next = d; + } + } + pf->proj = (XrCompositionLayerProjection){ XR_TYPE_COMPOSITION_LAYER_PROJECTION }; + pf->proj.space = g_xr.appSpace; + pf->proj.viewCount = 2; + pf->proj.views = pf->pviews; + pf->submit[pf->nLayers++] = (const XrCompositionLayerBaseHeader *)&pf->proj; + } else { + /* quad (menu / loading). Place it where the GAME asked: its submitted + * Pose + world Size, in our tracking space — unless the game flags it + * HeadLocked, in which case pin it to the view. Head-locking everything + * (the old hack) made world-anchored menus appear doubled/misplaced. */ + int headLocked = (s->LayerSubmitFlags & ovrpLayerSubmitFlag_HeadLocked) != 0; + float sw = s->QuadSize.w, sh = s->QuadSize.h; + int sizeSane = (sw > 0.05f && sw < 50.0f && sh > 0.05f && sh < 50.0f); + XrCompositionLayerQuad *q = &pf->quads[pf->nLayers]; + *q = (XrCompositionLayerQuad){ XR_TYPE_COMPOSITION_LAYER_QUAD }; + q->layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT; + q->eyeVisibility = XR_EYE_VISIBILITY_BOTH; + q->subImage.swapchain = L->swapchain; + q->subImage.imageRect.extent.width = (int)L->width; + q->subImage.imageRect.extent.height = (int)L->height; + XrPosef ap; /* ovrpPosef and XrPosef share layout */ + ap.orientation.x = s->Pose.Orientation.x; + ap.orientation.y = s->Pose.Orientation.y; + ap.orientation.z = s->Pose.Orientation.z; + ap.orientation.w = s->Pose.Orientation.w; + ap.position.x = s->Pose.Position.x; + ap.position.y = s->Pose.Position.y; + ap.position.z = s->Pose.Position.z; + q->pose = ap; + q->space = headLocked ? g_xr.viewSpace : g_xr.appSpace; + if (diag == 4) { /* TEST: head-lock the quad centered in view. If the + * dupe collapses, the duplicate was a world-locked quad separating + * from a head-locked eye-buffer copy of the same UI. */ + q->space = g_xr.viewSpace; + q->pose = (XrPosef){ {0,0,0,1}, {0,0,-2.0f} }; + float aspect = L->height ? (float)L->width / (float)L->height : 1.0f; + q->size.width = 2.0f; q->size.height = 2.0f / aspect; + pf->submit[pf->nLayers++] = (const XrCompositionLayerBaseHeader *)q; + continue; + } + if (sizeSane) { q->size.width = sw; q->size.height = sh; } + else { /* fall back to old readable hack */ + q->space = g_xr.viewSpace; + q->pose = (XrPosef){ {0,0,0,1}, {0,0,-1.5f} }; + float aspect = L->height ? (float)L->width / (float)L->height : 1.0f; + q->size.width = 1.5f; q->size.height = 1.5f / aspect; + } + static int qd = 0; + if (qd++ < 6) + XRRLOG("quad id=%d tex %ux%u flags=0x%x headLocked=%d app-pose=(%.2f,%.2f,%.2f) size=(%.2f,%.2f) sane=%d", + s->LayerId, L->width, L->height, s->LayerSubmitFlags, headLocked, + s->Pose.Position.x, s->Pose.Position.y, s->Pose.Position.z, sw, sh, sizeSane); + pf->submit[pf->nLayers++] = (const XrCompositionLayerBaseHeader *)q; + } + } + /* Layer z-order fix: emit projection (eye-fov, opaque world) layers FIRST (bottom of the + * OpenXR layer stack), quads/overlays AFTER (top), regardless of the app's submit order. + * Stable partition (preserves order within each group). Without this, the splash submits + * [logo-quad, eye-fov] and the opaque eye-fov composites on top -> black logos. See + * eyebottom_wanted(). No-op for single-layer frames (gameplay/title). */ + if (eyebottom_wanted() && pf->nLayers > 1) { + const XrCompositionLayerBaseHeader *ord[XRR_MAX_LAYERS]; int n = 0; + for (int k = 0; k < pf->nLayers; k++) + if (pf->submit[k]->type == XR_TYPE_COMPOSITION_LAYER_PROJECTION) ord[n++] = pf->submit[k]; + for (int k = 0; k < pf->nLayers; k++) + if (pf->submit[k]->type != XR_TYPE_COMPOSITION_LAYER_PROJECTION) ord[n++] = pf->submit[k]; + for (int k = 0; k < pf->nLayers; k++) pf->submit[k] = ord[k]; + } + if (pf->nLayers == 0 && layerCount > 0) { + /* STARVATION (filtered): app DID submit layers but every one was dropped + * (inactive/no-swapchain/bad id, or diag/copy-ring filter) -> BLACK frame + * despite app intent. Distinct from COMPOSE-EMPTY (no input / !shouldRender). */ + static int cf = 0; + if (cf++ < 300 || trace_level()) + XRRLOG("COMPOSE-FILTERED: appLayerCount=%d all dropped -> BLACK (diag=%d copyRing=%d)", + layerCount, diag, g_copyRingEngaged); + } + { /* DIAG: exactly what goes to xrEndFrame. >1 projection layer (or a stray + * eye-fov layer the app still submits) would composite offset -> the + * in-projection "dupe". Logs the count + each app submit's id/stage/flags. */ + static int sl = 0; + if ((sl++ % 120) == 0) { + XRRLOG("SUBMITLIST: nLayers=%d (appLayerCount=%d) projViews=%d", + pf->nLayers, layerCount, pf->proj.viewCount); + for (int k = 0; k < layerCount && layers && k < 6; k++) { + const ovrpLayerSubmit *s = layers[k]; + if (!s) continue; + int id = s->LayerId; + int eye = (id >= 0 && id < g_xr.layerCount) ? g_xr.layers[id].isEyeFov : -1; + XRRLOG(" app submit[%d]: LayerId=%d isEyeFov=%d stage=%d flags=0x%x", + k, id, eye, s->TextureStage, s->LayerSubmitFlags); + } + } + } +} + +/* Submit g_pending to the compositor (or an empty frame if there's none). Returns + * the XrResult of xrEndFrame. Caller holds g_xrlock. */ +static XrResult submit_pending(void) { + XrFrameEndInfo ei = { XR_TYPE_FRAME_END_INFO }; + ei.environmentBlendMode = XR_ENVIRONMENT_BLEND_MODE_OPAQUE; + if (g_pending.valid) { + ei.displayTime = g_pending.displayTime; + ei.layerCount = g_pending.nLayers; + ei.layers = g_pending.nLayers ? g_pending.submit : NULL; + } else { + ei.displayTime = g_xr.frameState.predictedDisplayTime; + } + if (ei.layerCount == 0 || ei.layers == NULL) { + /* THE black frame, measured at the point it hits the compositor. This is the + * title strobe / menu starvation. Always log (capped) + every time under trace. */ + static int sb = 0; + if (sb++ < 400 || trace_level()) + XRRLOG("SUBMIT-BLACK: valid=%d nLayers=%d shouldRender=%d displayTime=%lld", + g_pending.valid, g_pending.nLayers, g_xr.frameState.shouldRender, + (long long)ei.displayTime); + } + XrResult r = xrEndFrame(g_xr.session, &ei); + if (!XR_SUCCEEDED(r)) { + static int ef = 0; + if (ef++ < 40) XRRLOG("xrEndFrame FAILED rc=%d nLayers=%d shouldRender=%d", + (int)r, g_pending.nLayers, g_xr.frameState.shouldRender); + } + return r; +} + +/* Complete a present-on-submit (deferred) frame: UE's eye-render submit has now landed, + * so the same-queue resolve barrier orders after it and captures real content. Resolve + + * release the held image(s), then present the stashed composition. Caller holds g_xrlock. */ +static void present_deferred_locked(void) { + for (int i = 0; i < g_xr.layerCount; i++) { + XrLayer *L = &g_xr.layers[i]; + if (L->deferColor) { + if (L->deferColorIndex < L->imageCount) + xrr_vk_flush_wait(xrr_vk_flush_submit(L->colorImages[L->deferColorIndex], L->arraySize)); + XrSwapchainImageReleaseInfo ri = { XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO }; + xrReleaseSwapchainImage(L->swapchain, &ri); + L->deferColor = 0; + } + if (L->deferDepth) { + if (L->deferDepthIndex < L->imageCount) + xrr_vk_flush_wait(xrr_vk_flush_submit_ex(L->depthImages[L->deferDepthIndex], L->arraySize, 1)); + XrSwapchainImageReleaseInfo dri = { XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO }; + xrReleaseSwapchainImage(L->depthSwapchain, &dri); + L->deferDepth = 0; + } + } + submit_pending(); + g_pending.valid = 0; +} + +/* Trampoline callback (vk_session.c) after each UE vkQueueSubmit. Level 1 just logs the + * submit-vs-EndFrame ordering (validates the hook + interleave). Level 2 fires the + * deferred present on the first graphics-queue submit after end_frame stashed a frame — + * i.e., present rides UE's eye-render submit, ordered after it. */ +void xrr_on_ue_submit(uint64_t queue, uint64_t fence, int isRenderQueue) { + int lvl = submithook_level(); + if (lvl < 1) return; + long seq = __atomic_add_fetch(&g_submitSeq, 1, __ATOMIC_RELAXED); + /* per-frame submit tally (reset each end_frame): a truncated/black frame issues fewer + * GPU submits, so this is a candidate PER-FRAME black signal that (unlike the onset-only + * post-hitch flag) should stay low for the whole sustained-black tail. */ + __atomic_add_fetch(&g_frameSubmits, 1, __ATOMIC_RELAXED); + if (isRenderQueue) __atomic_add_fetch(&g_frameRenderSubmits, 1, __ATOMIC_RELAXED); + static int n = 0; + if (n++ < 600 || trace_level()) { + int64_t since = g_lastEndFrameNs ? (now_ns() - g_lastEndFrameNs) : 0; + XRRLOG("UE-SUBMIT seq=%ld q=0x%llx renderQ=%d fence=0x%llx +%.2fms-after-END defer=%d", + seq, (unsigned long long)queue, isRenderQueue, + (unsigned long long)fence, (double)since / 1e6, g_deferPresent); + } + if (lvl < 2 || !isRenderQueue) return; + pthread_mutex_lock(&g_xrlock); + if (g_deferPresent && ++g_deferSubmits >= defer_submit_count()) { + present_deferred_locked(); + g_deferPresent = 0; + } + pthread_mutex_unlock(&g_xrlock); +} + +/* blackcount/lumagate diagnostic: read the RESOLVED eye image's luma and set the bad-black verdict + * g_frameBlack (truncation = abrupt lit->black, not a gradual fade), and tick the blackcount window. + * No-op unless debug.re4vr.blackcount or .lumagate is set. (The old Lever-2 reproject hold/restore + * that consumed this verdict was removed — the ghost was a frame-pacing bug, fixed in the loop.) */ +static void update_black_verdict(XrLayer *L, uint32_t idx, int layerCount) { + if (!L->isEyeFov || idx >= L->imageCount) return; + if (!(blackcount_wanted() || lumagate_wanted())) return; + + g_frameLuma = xrr_vk_frame_luma(L->colorImages[idx], L->width, L->height, 0); + int isBlack = (g_frameLuma >= 0 && g_frameLuma < lumagate_thr() && layerCount <= 1); + if (!isBlack) { + g_reprojActive = 0; + } else if (!g_reprojActive && g_prevLuma >= 0 && + (g_prevLuma - g_frameLuma) >= abruptdrop()) { + g_reprojActive = 1; /* abrupt lit->black = truncation onset */ + } + g_frameBlack = isBlack && g_reprojActive; + if (g_frameLuma >= 0) g_prevLuma = g_frameLuma; + + if (blackcount_wanted()) { + g_frameWindow++; + if (g_frameBlack) { g_blackWindow++; g_blackTotal++; } + if (g_frameWindow >= 360) { + XRRLOG("BLACKCOUNT mode=%s black=%ld/%ld (%.1f%%) total=%ld", + g_pipelineActive ? "render-ahead" : "sync", g_blackWindow, g_frameWindow, + 100.0 * (double)g_blackWindow / (double)g_frameWindow, g_blackTotal); + g_blackWindow = 0; g_frameWindow = 0; + } + } +} + +ovrpResult xrr_end_frame(int frameIndex, + const ovrpLayerSubmit *const *layers, int layerCount) { + pthread_mutex_lock(&g_xrlock); + if (!g_xr.running) { pthread_mutex_unlock(&g_xrlock); return ovrpFailure_NotYetImplemented; } + /* only xrEndFrame if we actually began this frame (pairs 1:1 with BeginFrame) */ + if (!g_xr.inFrame) { pthread_mutex_unlock(&g_xrlock); return ovrpSuccess; } + + /* render-submit hook: install once when enabled (UE's RHI is up by first end_frame). */ + { static int tried = 0; + if (!tried && submithook_level() >= 1) { tried = 1; xrr_install_submit_hook(); } } + /* present-on-submit safety: if last frame deferred but UE's submit never fired (e.g. + * a shouldRender=0 frame, or the eye render landed on a non-graphics queue), flush the + * held frame now so the swapchain image can't stay stuck across frames. */ + if (g_deferPresent) { + static int sf = 0; + if (sf++ < 200 || trace_level()) + XRRLOG("submithook: stale deferred frame (no UE submit), flushing (%.2fms held)", + (double)(now_ns() - g_deferStartNs) / 1e6); + present_deferred_locked(); + g_deferPresent = 0; + } + g_lastEndFrameNs = now_ns(); + + if (trace_level() >= 2) { /* dump EVERY layer the app handed us this frame — + * id/stage/flags/pose/quad-size. The "log everything" view for spotting an + * old-lib workaround (odd cadence, repeated stage, sentinel flags, etc.). */ + XRRLOG("END f=%d appLayerCount=%d shouldRender=%d", frameIndex, layerCount, + g_xr.frameState.shouldRender); + for (int k = 0; k < layerCount && layers && k < XRR_MAX_LAYERS; k++) { + const ovrpLayerSubmit *s = layers[k]; + if (!s) { XRRLOG(" layer[%d]=NULL", k); continue; } + XRRLOG(" layer[%d] id=%d stage=%d flags=0x%x pose=(%.2f,%.2f,%.2f) quad=(%.2f,%.2f)", + k, s->LayerId, s->TextureStage, s->LayerSubmitFlags, + s->Pose.Position.x, s->Pose.Position.y, s->Pose.Position.z, + s->QuadSize.w, s->QuadSize.h); + } + } + + /* ---- Copy-ring path (debug.re4vr.copyring): UE rendered into shim images; copy + * shim[TextureStage] -> the acquired OpenXR image (resolves tile memory) WITHOUT + * waiting, then present the PREVIOUS frame's image (its copy finished a frame ago + * -> no CPU stall) => breaks the flush-wait serialization. Eye layers only; quads + * are dropped in this mode (menus hidden — validation cut). ------------------- */ + int copyActive = xrr_vk_flush_ready(); + if (copyActive) { + int anyShim = 0; + for (int i = 0; i < g_xr.layerCount; i++) + if (g_xr.layers[i].active && g_xr.layers[i].shimCount > 0) { anyShim = 1; break; } + copyActive = anyShim; + } + if (copyActive) { + g_copyRingEngaged = 1; + int tok[XRR_MAX_LAYERS]; for (int i = 0; i < XRR_MAX_LAYERS; i++) tok[i] = -1; + /* (A) copy this frame's eye shim render -> acquired OpenXR image (no wait) */ + for (int k = 0; k < layerCount && layers; k++) { + const ovrpLayerSubmit *s = layers[k]; + if (!s || s->LayerId < 0 || s->LayerId >= g_xr.layerCount) continue; + XrLayer *L = &g_xr.layers[s->LayerId]; + if (!L->isEyeFov || L->shimCount <= 0 || !L->imageAcquired) continue; + if (L->acquiredIndex >= L->imageCount) continue; + /* single-shim: UE rendered into shimImages[0] (the only one we hand it) */ + tok[s->LayerId] = xrr_vk_copy_submit(L->shimImages[0], + L->colorImages[L->acquiredIndex], L->width, L->height, L->arraySize); + if (dump_pending()) { /* DIAG: dump ALL shim stages — which one did UE + * actually render into vs the submitStage we copy? (off-by-one check) */ + char p[160]; + for (int st = 0; st < L->shimCount && st < 3; st++) { + snprintf(p, sizeof(p), "/sdcard/Android/data/com.Armature.VR4/files/cr_shim%d.ppm", st); + xrr_vk_dump_image(L->shimImages[st], L->width, L->height, 0, p); + } + XRRLOG("copy-ring dump: app submitStage=%d (we copy this one) acquiredIdx=%u shimCount=%d", + s->TextureStage, L->acquiredIndex, L->shimCount); + } + } + /* (B) present the PREVIOUS frame's eye image (held); its copy is already done. + * Track whether we actually released a fresh backing image — if a prior frame + * timed out (begin_frame couldn't acquire) the chain has no present-pending, + * and presenting g_pending then would reference a swapchain with no freshly + * released image -> XR_ERROR_LAYER_INVALID -> black, no recovery. In that case + * present EMPTY (valid) so xrEndFrame succeeds and the chain self-heals. */ + int releasedPresent = 0; + for (int i = 0; i < g_xr.layerCount; i++) { + XrLayer *L = &g_xr.layers[i]; + if (!L->presentPending) continue; + xrr_vk_flush_wait(L->presentToken); + XrSwapchainImageReleaseInfo ri = { XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO }; + xrReleaseSwapchainImage(L->swapchain, &ri); + L->presentPending = 0; + releasedPresent++; + } + /* If no fresh image was released (acquire failed under image contention), DON'T + * present black — re-present the last composition. We didn't release a new + * image, so the runtime re-shows its last-released image (freeze on the last + * good frame) instead of flashing black. Only force-empty on the very first + * frame (no composition built yet). */ + if (!releasedPresent && !g_pending.valid) { + static int cb = 0; + if (cb++ < 20) XRRLOG("copy-ring: no composition yet -> empty frame"); + } else if (!releasedPresent) { + static int fr = 0; + if (fr++ % 120 == 0) XRRLOG("copy-ring: chain broken -> re-present last frame (freeze)"); + } + XrResult rc = submit_pending(); /* present last composition (fresh or repeated) */ + /* (C) build this frame's eye-only composition; promote eye images, release + * any acquired non-eye (quad) images we dropped to stay swapchain-balanced. */ + build_composition(&g_pending, layers, layerCount); + for (int i = 0; i < g_xr.layerCount; i++) { + XrLayer *L = &g_xr.layers[i]; + if (L->isEyeFov && L->shimCount > 0 && L->imageAcquired) { + L->presentPending = 1; L->presentToken = tok[i]; + L->presentIndex = L->acquiredIndex; L->imageAcquired = 0; + } else if (L->imageAcquired) { + XrSwapchainImageReleaseInfo ri = { XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO }; + xrReleaseSwapchainImage(L->swapchain, &ri); + L->imageAcquired = 0; + } + } + g_xr.inFrame = 0; + static int cr = 0; + if ((cr++ % 240) == 0) XRRLOG("end_frame copy-ring #%d nLayers=%d", cr, g_pending.nLayers); + pthread_mutex_unlock(&g_xrlock); + return XR_SUCCEEDED(rc) ? ovrpSuccess : ovrpFailure_OperationFailed; + } + g_copyRingEngaged = 0; + + /* Engage the render-ahead pipeline once the flush ring is up AND every active + * layer is at least double-buffered (we hold one image while acquiring the + * next). Until then run synchronously (flush+wait+release in this frame) — same + * as before, correct but with the latency that causes ghosting. Lever B exposes + * it via debug.re4vr.renderahead (the legacy debug.re4vr.pipeline still works). */ + int wantPipeline = (pipeline_wanted() || renderahead_wanted()); + if (!g_pipelineActive && wantPipeline && xrr_vk_flush_ready()) { + int dbuf = 1; + for (int i = 0; i < g_xr.layerCount; i++) + if (g_xr.layers[i].active && g_xr.layers[i].imageCount < 2) { dbuf = 0; break; } + if (dbuf) { g_pipelineActive = 1; XRRLOG("frame loop: render-ahead pipeline engaged"); } + } else if (g_pipelineActive && !wantPipeline) { + /* Clean disengage (Lever B stabilization): drain any image held from last frame so the + * swapchain can't get stuck acquired across the mode switch, then fall back to the sync + * path for THIS frame. Costs a one-frame hiccup (the held composition is dropped). */ + for (int i = 0; i < g_xr.layerCount; i++) { + XrLayer *L = &g_xr.layers[i]; + if (L->presentPending && L->presentIndex < L->imageCount) { + xrr_vk_flush_wait(xrr_vk_flush_submit(L->colorImages[L->presentIndex], L->arraySize)); + XrSwapchainImageReleaseInfo ri = { XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO }; + xrReleaseSwapchainImage(L->swapchain, &ri); + } + L->presentPending = 0; + } + g_pipelineActive = 0; + XRRLOG("frame loop: render-ahead pipeline disengaged (drained held images)"); + } + + XrResult r; + if (g_pipelineActive) { + /* DEFERRED-FLUSH pipeline (fixes the render-submit race that blacked the old + * pipeline). Root cause: UE 4.25's RHI submits the eye render AFTER our + * EndFrame under load, so flushing the just-acquired image at END(N) resolved + * an un-submitted (black) image. Fix: hold frame N's rendered image one frame + * and flush+present it at END(N+1) — by then UE has had a full frame to + * vkQueueSubmit it, so the resolve barrier orders AFTER UE's writes and + * captures real content. We present frame N-1 each frame (1 frame latency). */ + static int pd = 0; /* DIAG: trace hold/flush/release across frames */ + int diag = (pd < 24 || (pd % 120) == 0); pd++; + int doDump = dump_pending(); /* pipeline-path texture probe (debug.re4vr.dump) */ + for (int i = 0; i < g_xr.layerCount; i++) { + XrLayer *L = &g_xr.layers[i]; + if (!L->presentPending) continue; + if (L->presentIndex >= L->imageCount) { /* stabilization guard: never release a bad index */ + XRRLOG("pipe(deferred): WARN presentPending with bad presentIndex=%u (imgCount=%u) -> dropped", + L->presentIndex, L->imageCount); + L->presentPending = 0; + continue; + } + /* (A) flush the HELD image (rendered last frame) NOW — a full frame after + * UE rendered it, so its vkQueueSubmit has landed and this barrier resolves + * the completed render — then wait for the resolve and release it. */ + int t = xrr_vk_flush_submit(L->colorImages[L->presentIndex], L->arraySize); + xrr_vk_flush_wait(t); + /* blackcount/lumagate verdict on the resolved HELD image (the content we present). */ + update_black_verdict(L, L->presentIndex, layerCount); + if (doDump && L->isEyeFov) { + char p[160]; + snprintf(p, sizeof(p), "/sdcard/Android/data/com.Armature.VR4/files/pipe_present_l%d.ppm", i); + xrr_vk_dump_image(L->colorImages[L->presentIndex], L->width, L->height, 0, p); + XRRLOG("pipe dump: layer=%d presentIndex=%u (deferred-flush held image)", i, L->presentIndex); + } + XrSwapchainImageReleaseInfo ri = { XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO }; + xrReleaseSwapchainImage(L->swapchain, &ri); + if (diag) XRRLOG("pipe(deferred): release held presentIndex=%u (this-frame acquiredIndex=%u) gpend(valid=%d nLayers=%d)", + L->presentIndex, L->acquiredIndex, g_pending.valid, g_pending.nLayers); + L->presentPending = 0; + } + /* (B) present frame N-1's composition (paired with the image just released). */ + r = submit_pending(); + if (diag) XRRLOG("pipe(deferred): submit_pending valid=%d nLayers=%d displayTime=%lld", + g_pending.valid, g_pending.nLayers, (long long)g_pending.displayTime); + + /* (C) capture THIS frame's composition for next frame, then (D) HOLD this + * frame's rendered image (do NOT flush it yet — defer to next frame's (A)). */ + build_composition(&g_pending, layers, layerCount); + for (int i = 0; i < g_xr.layerCount; i++) { + XrLayer *L = &g_xr.layers[i]; + if (!L->imageAcquired) continue; + L->presentPending = 1; + L->presentIndex = L->acquiredIndex; + L->imageAcquired = 0; + } + } else { + /* present-on-submit (debug.re4vr.submithook=2): instead of presenting now (before + * UE has submitted the eye render -> the black race), stash the composition and + * hold the rendered image; xrr_on_ue_submit fires present_deferred_locked once + * UE's eye-render vkQueueSubmit lands, ordering present after the render. */ + int haveEye = 0; + for (int i = 0; i < g_xr.layerCount; i++) + if (g_xr.layers[i].imageAcquired && g_xr.layers[i].isEyeFov) { haveEye = 1; break; } + if (submithook_level() >= 2 && g_xr.frameState.shouldRender && haveEye) { + build_composition(&g_pending, layers, layerCount); + for (int i = 0; i < g_xr.layerCount; i++) { + XrLayer *L = &g_xr.layers[i]; + if (L->imageAcquired) { L->deferColor = 1; L->deferColorIndex = L->acquiredIndex; L->imageAcquired = 0; } + if (L->depthAcquired) { L->deferDepth = 1; L->deferDepthIndex = L->depthAcquiredIndex; L->depthAcquired = 0; } + } + g_deferPresent = 1; g_deferSubmits = 0; g_deferStartNs = now_ns(); + r = XR_SUCCESS; /* present happens later in xrr_on_ue_submit */ + static int dl = 0; + if (dl++ < 200 || trace_level()) XRRLOG("submithook: present deferred (awaiting UE eye submit)"); + } else { + /* synchronous fallback: flush+wait then release this frame's images, and + * present the composition immediately (no extra latency, but ghosts). */ + int64_t waitNs = 0; /* PROBE: time spent blocked on the flush fence */ + int doDump = dump_pending(); + /* PERF VALIDATION (debug.re4vr.noflushwait=1): submit the flush but DON'T wait, + * to test whether the flush-wait is what serializes CPU/GPU. Rendering will be + * black/garbage (compositor reads unresolved), but VrApi FPS tells us if the + * framerate recovers -> confirms the flush-wait is the frame-drop cause. */ + int skipWait = noflushwait_wanted(); + /* PROBE (debug.re4vr.qwait=1): drain UE's queue BEFORE we resolve/release, so + * any submitted eye render completes first. If this turns the black gameplay + * image correct, UE's render was submitted-but-incomplete; if still black, it + * wasn't submitted yet when end_frame ran (ordering bug). See render-submit race. */ + if (qwait_wanted()) { + int64_t qw0 = now_ns(); + xrr_vk_queue_wait_idle(); + static int ql = 0; + if (ql++ < 20) XRRLOG("qwait: vkQueueWaitIdle %.2fms", (double)(now_ns() - qw0) / 1e6); + } + for (int i = 0; i < g_xr.layerCount; i++) { + XrLayer *L = &g_xr.layers[i]; + if (!L->imageAcquired) continue; + if (L->acquiredIndex < L->imageCount) { + int t0tok = xrr_vk_flush_submit(L->colorImages[L->acquiredIndex], L->arraySize); + int64_t w0 = now_ns(); + if (!skipWait) xrr_vk_flush_wait(t0tok); + waitNs += now_ns() - w0; + /* blackcount/lumagate black verdict for the resolved eye image (diagnostic only). + * Skipped under the noflushwait probe — luma of an unresolved image is meaningless. */ + if (!skipWait) + update_black_verdict(L, L->acquiredIndex, layerCount); + /* Diagnostic (trace): log each QUAD layer's luma per frame. The eye-fov luma + * probe above doesn't cover overlays, so this tells us whether a submitted quad + * (e.g. the intro logos) has real content or is arriving black — disambiguates a + * compositing/placement bug (quad has content) from a content/sync bug (black). */ + if (!skipWait && !L->isEyeFov && trace_level() >= 1) { + int ql = xrr_vk_frame_luma(L->colorImages[L->acquiredIndex], L->width, L->height, 0); + static int qc = 0; + if (qc++ < 4000) + XRRLOG("QUADLUMA: layer=%d tex=%ux%u luma=%d %s", i, L->width, L->height, ql, + (ql <= 0) ? "BLACK" : "HAS-CONTENT"); + } + if (doDump && L->isEyeFov) { /* burst-dump the eye array slices (both eyes) */ + uint64_t img = L->colorImages[L->acquiredIndex]; + char p[176]; + for (uint32_t a = 0; a < L->arraySize; a++) { + snprintf(p, sizeof(p), + "/sdcard/Android/data/com.Armature.VR4/files/eye_a%u_f%03d.ppm", + a, doDump); + xrr_vk_dump_image(img, L->width, L->height, a, p); + } + } + /* Lever 2 validation: stamp the frameIndex barcode into the eye image + * AFTER the resolve, so it rides on every frame incl. truncated/black + * ones — frame-exact video<->log correlation (debug.re4vr.barcode). */ + if (barcode_wanted() && L->isEyeFov) { + /* stamp turns red on a flagged frame. With lumagate on, "flagged" = the + * luma gate's per-frame black verdict (should stay red through the whole + * black tail); otherwise the onset-only post-hitch flag. */ + int flagged = lumagate_wanted() ? g_frameBlack + : (skipblack_level() >= 1 && g_postHitch > 0); + for (uint32_t a = 0; a < L->arraySize; a++) + xrr_vk_stamp_barcode(L->colorImages[L->acquiredIndex], + L->width, L->height, a, (unsigned)frameIndex, flagged); + } + } + XrSwapchainImageReleaseInfo ri = { XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO }; + xrReleaseSwapchainImage(L->swapchain, &ri); + L->imageAcquired = 0; + /* flush + release the paired depth image (tile memory must resolve before + * the compositor samples it for reprojection), in lockstep with color. */ + if (L->depthAcquired && L->depthAcquiredIndex < L->imageCount) { + xrr_vk_flush_wait(xrr_vk_flush_submit_ex(L->depthImages[L->depthAcquiredIndex], L->arraySize, 1)); + XrSwapchainImageReleaseInfo dri = { XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO }; + xrReleaseSwapchainImage(L->depthSwapchain, &dri); + L->depthAcquired = 0; + } + } + build_composition(&g_pending, layers, layerCount); + /* PROBE: is the latency theory real? Measure (a) how long the flush-wait + * blocks the render thread, and (b) how late we call xrEndFrame relative to + * the predictedDisplayTime we located views for (now - predicted; positive + * => we submit AFTER the intended display moment => the compositor reprojects). + * Both confirm/deny the premise behind render-ahead before we build it. */ + int64_t lateNs = now_ns() - (int64_t)g_xr.frameState.predictedDisplayTime; + static int64_t accWait = 0, maxWait = 0; static int pc = 0; + accWait += waitNs; if (waitNs > maxWait) maxWait = waitNs; + if (++pc % 120 == 0) { + XRRLOG("PROBE: flush-wait avg=%.2fms max=%.2fms | submit-vs-display=%.2fms (>0 late) | predicted=%lld now-rel", + (double)accWait / pc / 1e6, (double)maxWait / 1e6, (double)lateNs / 1e6, + (long long)g_xr.frameState.predictedDisplayTime); + accWait = 0; maxWait = 0; pc = 0; + } + r = submit_pending(); + g_pending.valid = 0; /* consumed now; nothing carried to next frame */ + } /* end synchronous-present (non-deferred) path */ + } + + g_xr.inFrame = 0; + static int hb = 0; + if ((hb++ % 240) == 0) + XRRLOG("end_frame heartbeat #%d pipelined=%d nLayers=%d appLayers=%d", + hb, g_pipelineActive, g_pending.nLayers, layerCount); + { /* Per-frame pacing trace + hitch detection. dt = wall time since the last + * end_frame; >20ms means we missed a 72Hz (13.9ms) cadence -> the compositor + * had to reproject/hold, the visual signature of the menu smear. The HITCH + * line is always-on (capped); full per-frame lines only at trace>=1. */ + static int64_t lastEnd = 0; static int hc = 0; + int64_t tnow = now_ns(); + double dt_ms = lastEnd ? (double)(tnow - lastEnd) / 1e6 : 0.0; + lastEnd = tnow; + if (dt_ms > 0.0) g_gpuFrameMs = dt_ms; /* source for ovrp_GetGPUFrameTime */ + int hitch = (dt_ms > 20.0); + /* snapshot+reset the per-frame UE submit tally for this end_frame window (atomic + * exchange; the hook may add concurrently). subs/rsubs let us test whether + * truncated/black frames issue fewer submits — a per-frame black signal. */ + int subs = __atomic_exchange_n(&g_frameSubmits, 0, __ATOMIC_RELAXED); + int rsubs = __atomic_exchange_n(&g_frameRenderSubmits, 0, __ATOMIC_RELAXED); + int tl = trace_level(); + if (tl >= 1 || (hitch && hc++ < 400)) + XRRLOG("FRAME f=%d shouldRender=%d appLayers=%d nLayers=%d pipelined=%d dt=%.1fms subs=%d rsubs=%d luma=%d%s%s", + frameIndex, g_xr.frameState.shouldRender, layerCount, g_pending.nLayers, + g_pipelineActive, dt_ms, subs, rsubs, g_frameLuma, + g_frameBlack ? " BLACK" : "", hitch ? " HITCH" : ""); + + /* Lever 2 (debug.re4vr.skipblack>=1): flag the recovery frame(s) after a HITCH as + * LIKELY-TRUNCATED. Instrument-only at level 1 — these logs are the ground-truth + * we validate against perceived black before building the reproject (level 2). + * The hitch frame ARMS the window; the following skipblack_count() frames are the + * candidates UE likely rendered truncated/black. */ + int sbl = skipblack_level(); + if (sbl >= 1) { + if (g_postHitch > 0) { + XRRLOG("SKIPBLACK: LIKELY-TRUNCATED frame f=%d (post-hitch, %d left) dt=%.1fms shouldRender=%d", + frameIndex, g_postHitch, dt_ms, g_xr.frameState.shouldRender); + g_postHitch--; + } + if (hitch) g_postHitch = skipblack_count(); /* (re)arm window for next frames */ + } else { + g_postHitch = 0; + } + } + { /* STEREO DIAG (periodic, survives any capture window): the eye layer's + * layout + the exact per-eye fov/pose we submit. A side-by-side layout + * (arraySize=1) submitted with full-width per-eye rects => each eye samples + * the whole double-wide texture => literal "two copies" doubling. */ + static int sd = 0; + if ((sd++ % 120) == 0) { + for (int i = 0; i < g_xr.layerCount; i++) { + XrLayer *L = &g_xr.layers[i]; + if (!L->active || !L->isEyeFov) continue; + XrFovf f0 = g_xr.views[0].fov, f1 = g_xr.views[1].fov; + XrVector3f p0 = g_xr.views[0].pose.position, p1 = g_xr.views[1].pose.position; + float ipd = sqrtf((p0.x-p1.x)*(p0.x-p1.x)+(p0.y-p1.y)*(p0.y-p1.y)+(p0.z-p1.z)*(p0.z-p1.z)); + XRRLOG("STEREO: layer=%d layout=%d arraySize=%u tex=%ux%u imgArrIdx=%s | " + "L.fov(l,r,u,d)=(%.3f,%.3f,%.3f,%.3f) R.fov=(%.3f,%.3f,%.3f,%.3f) IPD=%.3fm", + i, L->layout, L->arraySize, L->width, L->height, + (L->arraySize > 1) ? "eye" : "0(SHARED!)", + f0.angleLeft,f0.angleRight,f0.angleUp,f0.angleDown, + f1.angleLeft,f1.angleRight,f1.angleUp,f1.angleDown, ipd); + } + } + } + pthread_mutex_unlock(&g_xrlock); + return XR_SUCCEEDED(r) ? ovrpSuccess : ovrpFailure_OperationFailed; +} + +void xrr_recommended_eye_size(uint32_t *w, uint32_t *h) { + *w = 1440; *h = 1584; /* Quest 2 fallback */ + if (g_xr.instance == XR_NULL_HANDLE || g_xr.systemId == XR_NULL_SYSTEM_ID) return; + uint32_t n = 0; + if (xrEnumerateViewConfigurationViews(g_xr.instance, g_xr.systemId, + g_xr.viewConfigType, 0, &n, NULL) != XR_SUCCESS || n == 0) return; + XrViewConfigurationView v[2] = { { XR_TYPE_VIEW_CONFIGURATION_VIEW }, + { XR_TYPE_VIEW_CONFIGURATION_VIEW } }; + if (n > 2) n = 2; + if (xrEnumerateViewConfigurationViews(g_xr.instance, g_xr.systemId, + g_xr.viewConfigType, n, &n, v) == XR_SUCCESS && n >= 1) { + *w = v[0].recommendedImageRectWidth; + *h = v[0].recommendedImageRectHeight; + } +} + +/* ------------------------------------------------------ layers / swapchains */ +static int64_t vkformat_from_ovrp(ovrpTextureFormat f) { + switch (f) { /* VkFormat numeric values */ + case ovrpTextureFormat_R8G8B8A8_sRGB: return 43; /* RGBA8_SRGB */ + case ovrpTextureFormat_R8G8B8A8: return 37; /* RGBA8_UNORM */ + case ovrpTextureFormat_R16G16B16A16_FP: return 97; /* RGBA16_SFLOAT*/ + case ovrpTextureFormat_R11G11B10_FP: return 122; /* B10G11R11_UF */ + case ovrpTextureFormat_B8G8R8A8_sRGB: return 50; /* BGRA8_SRGB */ + case ovrpTextureFormat_B8G8R8A8: return 44; /* BGRA8_UNORM */ + case ovrpTextureFormat_R5G6B5: return 4; /* R5G6B5_PACK16*/ + default: return 43; + } +} + +/* ovrp depth formats -> VkFormat. [CORRECTED from real OVR_Plugin_Types.h]: + * D16=6, D24_S8=7, D32_FP=8, D32_S824_FP=9, None=10. RE4 passes 10 (None) -> UE does + * NOT render depth, so the depth layer never engages (earlier 10->D32 was a bug that + * forced an unwanted depth target and blacked the cast). 0 = none. */ +static int64_t vk_depthformat_from_ovrp(int f) { + switch (f) { + case 6: return 124; /* D16 -> VK_FORMAT_D16_UNORM */ + case 7: return 129; /* D24_S8 -> VK_FORMAT_D24_UNORM_S8_UINT */ + case 8: return 126; /* D32_FP -> VK_FORMAT_D32_SFLOAT */ + case 9: return 130; /* D32_S824_FP -> VK_FORMAT_D32_SFLOAT_S8_UINT */ + default: return 0; /* None(10) or unknown -> no depth */ + } +} + +/* Free a layer the app destroyed (DestroyLayer). Must be serialized against the + * frame loop, which acquires/composites by layer. Releasing any in-flight image + * first keeps the swapchain balanced before xrDestroySwapchain. */ +void xrr_destroy_layer(int layerId) { + pthread_mutex_lock(&g_xrlock); + if (layerId >= 0 && layerId < g_xr.layerCount && g_xr.layers[layerId].active) { + XrLayer *L = &g_xr.layers[layerId]; + /* drain both in-flight images (FIFO releases the older present-pending one + * first, then this frame's). */ + XrSwapchainImageReleaseInfo ri = { XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO }; + if (L->presentPending) { + xrr_vk_flush_wait(L->presentToken); + xrReleaseSwapchainImage(L->swapchain, &ri); + L->presentPending = 0; + } + if (L->imageAcquired) { + xrReleaseSwapchainImage(L->swapchain, &ri); + L->imageAcquired = 0; + } + if (L->swapchain != XR_NULL_HANDLE) xrDestroySwapchain(L->swapchain); + if (L->depthSwapchain != XR_NULL_HANDLE) xrDestroySwapchain(L->depthSwapchain); + if (L->shimCount > 0) xrr_vk_free_images(L->shimImages, L->shimMem, L->shimCount); + memset(L, 0, sizeof(*L)); /* active=0 -> skipped by begin/end frame */ + XRRLOG("destroy_layer: freed id=%d", layerId); + } + pthread_mutex_unlock(&g_xrlock); +} + +/* --- Game-driven Fixed Foveated Rendering (TiledMultiRes) ------------------- + * RE4 runs its own dynamic-perf loop: it polls GPU frame time and, under load, + * lowers foveation via ovrp_SetTiledMultiResLevel/Dynamic (the Oculus name for + * FFR). We map that onto OpenXR XR_FB_foveation. Source of truth for the level + * actually applied, in priority order: + * 1. debug.re4vr.ffr prop — manual test override (-1/unset = let the game drive) + * 2. g_ffrGameLevel — the game's last ovrp_SetTiledMultiResLevel request + * 3. MEDIUM (default) — sane baseline if neither set + * ovrpTiledMultiResLevel: 0=Off 1=LMSLow 2=LMSMedium 3=LMSHigh 4=LMSHighTop. */ +static int g_ffrGameLevel = -1; /* ovrp level the game last Set (-1 = unset) */ +static int g_ffrGameDynamic = 1; /* game's dynamic flag (runtime-dynamic on) */ + +/* debug.re4vr.ffr override: returns -1 when unset (let the game drive), else 0..3. */ +static int ffr_prop_override(void) { + int v = -1; +#ifdef __ANDROID__ + char s[PROP_VALUE_MAX] = {0}; + if (__system_property_get("debug.re4vr.ffr", s) > 0 && s[0]) v = atoi(s); +#endif + if (v > 3) v = 3; + return v; +} + +/* ovrpTiledMultiResLevel (0..4) -> our FFR strength (0..3). HighTop folds to high. */ +static int ovrp_level_to_ffr(int lvl) { + if (lvl <= 0) return 0; + if (lvl >= 4) return 3; + return lvl; /* Low/Med/High map 1:1 */ +} + +/* Resolve the FFR strength (0..3) to apply right now (override > game > default). */ +static int ffr_resolve(void) { + int p = ffr_prop_override(); + if (p >= 0) return p; + if (g_ffrGameLevel >= 0) return ovrp_level_to_ffr(g_ffrGameLevel); + return 2; /* default medium */ +} + +/* Load the XR_FB_foveation entry points once. Returns 1 if usable. */ +static int foveation_entrypoints(PFN_xrCreateFoveationProfileFB *create, + PFN_xrDestroyFoveationProfileFB *destroy, + PFN_xrUpdateSwapchainFB *update) { + static PFN_xrCreateFoveationProfileFB createProf; + static PFN_xrDestroyFoveationProfileFB destroyProf; + static PFN_xrUpdateSwapchainFB updateSc; + static int loaded = 0, loadFail = 0; + if (loadFail) return 0; + if (!loaded) { + union { PFN_xrVoidFunction v; PFN_xrCreateFoveationProfileFB f; } a = {0}; + union { PFN_xrVoidFunction v; PFN_xrDestroyFoveationProfileFB f; } b = {0}; + union { PFN_xrVoidFunction v; PFN_xrUpdateSwapchainFB f; } c = {0}; + xrGetInstanceProcAddr(g_xr.instance, "xrCreateFoveationProfileFB", &a.v); + xrGetInstanceProcAddr(g_xr.instance, "xrDestroyFoveationProfileFB", &b.v); + xrGetInstanceProcAddr(g_xr.instance, "xrUpdateSwapchainFB", &c.v); + createProf = a.f; destroyProf = b.f; updateSc = c.f; + if (!createProf || !updateSc) { loadFail = 1; XRRERR("FFR: entry points missing"); return 0; } + loaded = 1; + } + *create = createProf; *destroy = destroyProf; *update = updateSc; + return 1; +} + +/* Eye-tracked foveation toggle: default ON when XR_META_foveation_eye_tracked is available, + * set debug.re4vr.etfr=0 to force fixed FFR (A/B the gaze-driven upgrade on device). */ +static int etfr_enabled(void) { + static int v = -1; + if (v < 0) { +#ifdef __ANDROID__ + char s[92] = {0}; + extern int __system_property_get(const char *, char *); + v = (__system_property_get("debug.re4vr.etfr", s) > 0 && s[0] == '0') ? 0 : 1; /* default on */ +#else + v = 1; +#endif + } + return v; +} + +/* Apply FFR strength `ffr` (0..3, 0=disable) to one eye swapchain via XR_FB_foveation. + * `dynamicOn` lets the runtime ease foveation when there's GPU headroom. This is the + * single extension point for foveation — add other HMDs' schemes (e.g. Steam Frame / + * eye-tracked) here behind their own availability flags. */ +static void apply_foveation_sc(XrSwapchain sc, int ffr, int dynamicOn) { + if (!g_haveFoveation || sc == XR_NULL_HANDLE) return; + PFN_xrCreateFoveationProfileFB createProf; + PFN_xrDestroyFoveationProfileFB destroyProf; + PFN_xrUpdateSwapchainFB updateSc; + if (!foveation_entrypoints(&createProf, &destroyProf, &updateSc)) return; + + /* ffr 0 = disable. Apply a real LEVEL_NONE profile rather than a NULL profile: + * the Meta runtime null-derefs on profile==XR_NULL_HANDLE (crashes in + * libvrapiimpl) even though the spec says NULL means "default". So we always + * create a valid profile and map Off -> XR_FOVEATION_LEVEL_NONE_FB. */ + XrFoveationLevelProfileCreateInfoFB lvl = { XR_TYPE_FOVEATION_LEVEL_PROFILE_CREATE_INFO_FB }; + lvl.level = (ffr <= 0) ? XR_FOVEATION_LEVEL_NONE_FB + : (ffr == 1) ? XR_FOVEATION_LEVEL_LOW_FB + : (ffr == 2) ? XR_FOVEATION_LEVEL_MEDIUM_FB + : XR_FOVEATION_LEVEL_HIGH_FB; + lvl.verticalOffset = 0.0f; + lvl.dynamic = dynamicOn ? XR_FOVEATION_DYNAMIC_LEVEL_ENABLED_FB + : XR_FOVEATION_DYNAMIC_DISABLED_FB; + /* Eye-tracked upgrade: chain XrFoveationEyeTrackedProfileCreateInfoMETA onto the FB level + * profile so the runtime centres foveation on gaze (falls back to fixed when gaze is + * invalid). Only when the ext is advertised, FFR is on, and not disabled via prop. */ + int eyeTracked = g_haveEyeTrackedFov && etfr_enabled() && ffr > 0; + XrFoveationEyeTrackedProfileCreateInfoMETA et = { XR_TYPE_FOVEATION_EYE_TRACKED_PROFILE_CREATE_INFO_META }; + if (eyeTracked) lvl.next = &et; + XrFoveationProfileCreateInfoFB pci = { XR_TYPE_FOVEATION_PROFILE_CREATE_INFO_FB }; + pci.next = &lvl; + XrFoveationProfileFB profile = XR_NULL_HANDLE; + if (xrfail("xrCreateFoveationProfileFB", createProf(g_xr.session, &pci, &profile))) return; + + XrSwapchainStateFoveationFB st = { XR_TYPE_SWAPCHAIN_STATE_FOVEATION_FB }; + st.profile = profile; + XrResult r = updateSc(sc, (XrSwapchainStateBaseHeaderFB *)&st); + if (destroyProf) destroyProf(profile); /* safe to free once applied */ + if (xrfail("xrUpdateSwapchainFB", r)) return; + XRRLOG("FFR: applied level=%d dynamic=%d eyeTracked=%d to eye swapchain", ffr, dynamicOn, eyeTracked); +} + +/* Apply the currently-resolved FFR to an eye swapchain (used at layer creation). */ +static void apply_foveation(XrSwapchain sc) { + apply_foveation_sc(sc, ffr_resolve(), g_ffrGameDynamic); +} + +/* Re-apply the resolved FFR to the live eye swapchain(s). Caller holds g_xrlock so + * this can't race the render thread's acquire/release (all under the same lock). */ +static void reapply_foveation_locked(void) { + for (int i = 0; i < g_xr.layerCount; i++) { + XrLayer *L = &g_xr.layers[i]; + if (L->active && L->isEyeFov && L->swapchain != XR_NULL_HANDLE) + apply_foveation_sc(L->swapchain, ffr_resolve(), g_ffrGameDynamic); + } +} + +/* ---- ovrp_*TiledMultiRes* / GetGPUFrameTime bridge (called from core.c) ---- */ +int xrr_foveation_supported(void) { return g_haveFoveation ? 1 : 0; } + +void xrr_set_tiled_multires_level(int ovrpLevel) { + pthread_mutex_lock(&g_xrlock); + int changed = (ovrpLevel != g_ffrGameLevel); + g_ffrGameLevel = ovrpLevel; + if (changed) { + XRRLOG("game SetTiledMultiResLevel=%d -> applied ffr=%d", ovrpLevel, ffr_resolve()); + reapply_foveation_locked(); + } + pthread_mutex_unlock(&g_xrlock); +} + +int xrr_get_tiled_multires_level(void) { + return g_ffrGameLevel < 0 ? 0 : g_ffrGameLevel; /* report what the game Set */ +} + +void xrr_set_tiled_multires_dynamic(int on) { + pthread_mutex_lock(&g_xrlock); + on = on ? 1 : 0; + int changed = (on != g_ffrGameDynamic); + g_ffrGameDynamic = on; + if (changed) { + XRRLOG("game SetTiledMultiResDynamic=%d", on); + reapply_foveation_locked(); + } + pthread_mutex_unlock(&g_xrlock); +} + +int xrr_get_tiled_multires_dynamic(void) { return g_ffrGameDynamic; } + +/* GPU frame time (ms) fed to the game's dynamic-perf loop. We have no portable GPU + * timestamp query, so we approximate with the measured end-to-end frame dt (updated + * in xrr_end_frame). The handoff accepts an approximate value to engage the scaler. + * NOTE: unit (ms vs s) and the dt-vs-true-GPU-time proxy both need on-device + * confirmation — watch whether the game's FFR engages under Standing load. */ +float xrr_gpu_frame_time_ms(void) { + /* Throttled correlation log: the GPU-time we feed the game vs. the foveation it + * asks for in response. If level stays low while fed time is high (or vice-versa), + * the frame-dt proxy / unit is the problem. ~1/sec under trace, capped otherwise. */ + static int n = 0; + if ((n++ % 72) == 0 && (trace_level() >= 1 || n < 720)) + XRRLOG("GPU-TIME fed=%.2fms gameLevel=%d dynamic=%d -> applied ffr=%d", + g_gpuFrameMs, g_ffrGameLevel, g_ffrGameDynamic, ffr_resolve()); + return (float)g_gpuFrameMs; +} + +/* ---- Lever A: dynamic resolution via ovrp_GetAdaptiveGpuPerformanceScale2 ---- + * RE finding: the game presets scale=1.0, calls this API, then + * sets render resolution = baseDensity * sqrt(scale). Returning <1 under GPU pressure makes + * the GAME self-downscale its eye buffer — no game patch. Gated game-side on Settings.byte + * [0x19] bit4 (adaptive-res enabled): if that bit is clear the game ignores us and uses a + * fixed screen-percentage CVar, so this lever is inert (verify on device by watching whether + * the viewport shrinks). Caveat: the truncation black is DRAW-bound, so quarter-res still + * blacks — this is a throughput/headroom win, not a guaranteed black fix (measure with the + * BLACKCOUNT counter). */ +static int adaptivescale_wanted(void) { + int v = 1; /* default ON — game ignores it unless its own bit4 is set */ +#ifdef __ANDROID__ + char s[PROP_VALUE_MAX] = {0}; + if (__system_property_get("debug.re4vr.adaptivescale", s) > 0 && s[0] == '0') v = 0; +#endif + return v; +} +/* Lower bound on scale (debug.re4vr.adaptivefloor, percent; default 60 = 0.60). Bounds how far + * the game may shrink resolution: res ~ sqrt(scale), so floor 0.60 -> ~0.77x linear res. */ +static float adaptivefloor(void) { + int v = 60; +#ifdef __ANDROID__ + char s[PROP_VALUE_MAX] = {0}; + if (__system_property_get("debug.re4vr.adaptivefloor", s) > 0) { int n = atoi(s); if (n >= 10 && n <= 100) v = n; } +#endif + return (float)v / 100.0f; +} + +/* Compute the adaptive GPU scale the game multiplies into its resolution. budget = 72Hz frame + * (13.9ms); an overloaded frame returns budget/gpuMs (<1). EMA-smoothed to avoid resolution + * thrash (sqrt-quantized to /1024 game-side, but the raw value still oscillates with dt). */ +float xrr_adaptive_gpu_scale(void) { + static float ema = 1.0f; + if (!adaptivescale_wanted()) { ema = 1.0f; return 1.0f; } + const float budget = 13.9f; + float floor = adaptivefloor(); + float inst = 1.0f; + if (g_gpuFrameMs > 0.1) { + inst = budget / (float)g_gpuFrameMs; + if (inst > 1.0f) inst = 1.0f; + if (inst < floor) inst = floor; + } + ema = ema * 0.85f + inst * 0.15f; /* ~7-frame time constant */ + if (ema < floor) ema = floor; + if (ema > 1.0f) ema = 1.0f; + static int n = 0; + if ((n++ % 72) == 0 && (trace_level() >= 1 || n < 720)) + XRRLOG("ADAPTIVE-SCALE gpuMs=%.2f inst=%.3f smoothed=%.3f floor=%.2f (game res ~sqrt)", + g_gpuFrameMs, inst, ema, floor); + return ema; +} + +/* ---- ovrp_*PerfMetrics* backend (called from core.c) ---------------------- + * Per-metric perf query (ovrpPerfMetrics ids). The game polls IsPerfMetricsSupported + * per metric, then GetPerfMetrics{Float,Int} for the supported ones. We answer + * TRUTHFULLY per metric: supported only where we have a real measurement, and we log + * every query so we learn exactly which metrics RE4 wants (then we can wire more real + * sources). Today the one real source is App GPU time (= our measured frame dt). */ +enum { OVRP_PM_APP_CPUTIME_F = 0, OVRP_PM_APP_GPUTIME_F = 1, + OVRP_PM_DEV_CPULEVEL_I = 12, OVRP_PM_DEV_GPULEVEL_I = 13 }; + +int xrr_perf_metric_supported(int metric) { + int sup = (metric == OVRP_PM_APP_GPUTIME_F) /* measured frame dt */ + || (metric == OVRP_PM_DEV_CPULEVEL_I && g_cpuPerfLevel >= 0) /* game's CPU clock level */ + || (metric == OVRP_PM_DEV_GPULEVEL_I && g_gpuPerfLevel >= 0); /* game's GPU clock level */ + static int n = 0; + if (n++ < 120 || trace_level()) + XRRLOG("PerfMetrics IsSupported(metric=%d) -> %d", metric, sup); + return sup; +} + +/* Returns 1 and writes *out if we provide this float metric, else 0 (-> Unsupported). */ +int xrr_perf_metric_float(int metric, float *out) { + float v = 0.0f; int ok = 0; + if (metric == OVRP_PM_APP_GPUTIME_F) { v = (float)g_gpuFrameMs; ok = 1; } /* ms */ + if (out) *out = v; + static int n = 0; + if (n++ < 240 || trace_level()) + XRRLOG("PerfMetrics GetFloat(metric=%d) -> %.3f ok=%d", metric, v, ok); + return ok; +} + +/* Returns 1 and writes *out if we provide this int metric, else 0 (-> Unsupported). */ +int xrr_perf_metric_int(int metric, int *out) { + int v = 0, ok = 0; + if (metric == OVRP_PM_DEV_CPULEVEL_I && g_cpuPerfLevel >= 0) { v = g_cpuPerfLevel; ok = 1; } + if (metric == OVRP_PM_DEV_GPULEVEL_I && g_gpuPerfLevel >= 0) { v = g_gpuPerfLevel; ok = 1; } + if (out) *out = v; + static int n = 0; + if (n++ < 120 || trace_level()) + XRRLOG("PerfMetrics GetInt(metric=%d) -> %d ok=%d", metric, v, ok); + return ok; +} + +ovrpResult xrr_setup_layer(const ovrpLayerDesc *desc, int *outLayerId) { + if (!desc || !outLayerId) return ovrpFailure_InvalidParameter; + if (g_xr.session == XR_NULL_HANDLE) return ovrpFailure_NotInitialized; + + /* Hold g_xrlock across slot allocation + the whole build/publish so the render-thread frame + * loop (which iterates g_xr.layers under the same lock) never sees a half-built layer — and + * for memory-ordering symmetry with xrr_destroy_layer. setup is infrequent (init / menu layer + * create), so the brief contention with the frame loop is fine. */ + pthread_mutex_lock(&g_xrlock); + /* reuse a freed slot if one exists, else grow */ + int slot = -1; + for (int i = 0; i < g_xr.layerCount; i++) + if (!g_xr.layers[i].active) { slot = i; break; } + if (slot < 0) { + if (g_xr.layerCount >= XRR_MAX_LAYERS) { + pthread_mutex_unlock(&g_xrlock); return ovrpFailure_InsufficientSize; + } + slot = g_xr.layerCount++; + } + + XrLayer *L = &g_xr.layers[slot]; + memset(L, 0, sizeof(*L)); + L->width = (uint32_t)desc->TextureSize.w; + L->height = (uint32_t)desc->TextureSize.h; + L->arraySize = (desc->Layout == ovrpLayout_Array) ? 2 : 1; + L->layout = desc->Layout; + L->isEyeFov = (desc->Shape == ovrpShape_EyeFov); + L->colorFormat = vkformat_from_ovrp(desc->Format); + + /* the runtime only accepts formats from its supported list; validate ours and + * fall back to the first supported (preferring an sRGB color format). */ + int64_t fmts[128]; uint32_t nf = 0; + xrEnumerateSwapchainFormats(g_xr.session, 128, &nf, fmts); + int supported = 0; int64_t fallback = nf ? fmts[0] : L->colorFormat; + for (uint32_t i = 0; i < nf; i++) { + if (fmts[i] == L->colorFormat) supported = 1; + if (fmts[i] == 43 || fmts[i] == 50) fallback = fmts[i]; /* RGBA8/BGRA8 sRGB */ + } + if (!supported) { + XRRLOG("setup_layer: format %lld unsupported, using %lld (of %u)", + (long long)L->colorFormat, (long long)fallback, nf); + L->colorFormat = fallback; + } + + int useCopyRing = L->isEyeFov && copyring_wanted(); + XrSwapchainCreateInfo ci = { XR_TYPE_SWAPCHAIN_CREATE_INFO }; + ci.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | + XR_SWAPCHAIN_USAGE_SAMPLED_BIT | + /* copy-ring copies into the OpenXR image -> needs TRANSFER_DST */ + (useCopyRing ? XR_SWAPCHAIN_USAGE_TRANSFER_DST_BIT : 0); + ci.format = L->colorFormat; + ci.sampleCount = desc->SampleCount ? (uint32_t)desc->SampleCount : 1; + ci.width = L->width; + ci.height = L->height; + ci.faceCount = 1; + ci.arraySize = L->arraySize; + ci.mipCount = desc->MipLevels ? (uint32_t)desc->MipLevels : 1; + XRRLOG("setup_layer: fmt=%lld %ux%u samples=%u array=%u mips=%u", + (long long)ci.format, ci.width, ci.height, ci.sampleCount, + ci.arraySize, ci.mipCount); + if (xrfail("xrCreateSwapchain", xrCreateSwapchain(g_xr.session, &ci, &L->swapchain))) { + pthread_mutex_unlock(&g_xrlock); return ovrpFailure_OperationFailed; + } + + /* Fixed Foveated Rendering on the eye buffer only (foveating menus/quads is + * pointless and would blur readable UI). The game's foveation request is stubbed; + * this applies it at the runtime instead. */ + if (L->isEyeFov) apply_foveation(L->swapchain); + + L->imageCount = xrr_vk_enumerate_images(L->swapchain, L->colorImages, XRR_MAX_IMAGES); + + /* copy-ring: allocate shim images UE renders into (same count/format/size as the + * OpenXR eye swapchain). If allocation fails, fall back to direct rendering. */ + if (useCopyRing && L->imageCount > 0) { + if (xrr_vk_alloc_images(L->shimImages, L->shimMem, (int)L->imageCount, + L->width, L->height, L->arraySize, (long long)L->colorFormat)) { + L->shimCount = (int)L->imageCount; + XRRLOG("setup_layer: copy-ring active, %d shim images for layer %d", L->shimCount, slot); + } else { + XRRLOG("setup_layer: copy-ring shim alloc FAILED -> direct render"); + } + } + + /* Depth swapchain for positional reprojection (eye layers only). UE requested a + * DepthFormat; if depth is enabled and the format is a real depth format the + * runtime supports, create it + hand UE the depth images via GetLayerTexture2. */ + int64_t dfmt = vk_depthformat_from_ovrp(desc->DepthFormat); + if (L->isEyeFov && dfmt && depth_wanted()) { + int dsupported = 0; + for (uint32_t i = 0; i < nf; i++) if (fmts[i] == dfmt) { dsupported = 1; break; } + if (!dsupported) { + XRRLOG("setup_layer: depth fmt %lld unsupported -> no depth layer", (long long)dfmt); + } else { + XrSwapchainCreateInfo dci = { XR_TYPE_SWAPCHAIN_CREATE_INFO }; + dci.usageFlags = XR_SWAPCHAIN_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | + XR_SWAPCHAIN_USAGE_SAMPLED_BIT; + dci.format = dfmt; + dci.sampleCount = ci.sampleCount; + dci.width = L->width; dci.height = L->height; + dci.faceCount = 1; dci.arraySize = L->arraySize; + dci.mipCount = 1; + if (xrfail("xrCreateSwapchain(depth)", xrCreateSwapchain(g_xr.session, &dci, &L->depthSwapchain))) { + L->depthSwapchain = XR_NULL_HANDLE; + } else { + uint32_t dn = xrr_vk_enumerate_images(L->depthSwapchain, L->depthImages, XRR_MAX_IMAGES); + L->depthFormat = dfmt; + XRRLOG("setup_layer: DEPTH swapchain fmt=%lld %ux%u array=%u images=%u (color images=%u)", + (long long)dfmt, L->width, L->height, L->arraySize, dn, L->imageCount); + if (dn < L->imageCount) { /* must stay in lockstep with color */ + XRRLOG("setup_layer: depth image count %u < color %u -> disabling depth", dn, L->imageCount); + xrDestroySwapchain(L->depthSwapchain); L->depthSwapchain = XR_NULL_HANDLE; + } + } + } + } + + L->active = 1; /* set LAST: the lock + active-gate publish the fully-built layer */ + *outLayerId = slot; + pthread_mutex_unlock(&g_xrlock); + return ovrpSuccess; +} + +ovrpResult xrr_setup_layer_depth(int layerId, const ovrpLayerDesc *depthDesc) { + if (layerId < 0 || layerId >= g_xr.layerCount) return ovrpFailure_InvalidParameter; + XrLayer *L = &g_xr.layers[layerId]; + if (!L->active) return ovrpFailure_InvalidParameter; + /* depth swapchain: D24S8/D32 — submitted via XR_KHR_composition_layer_depth. + * [TODO] create + wire the depth ext; for now report unsupported so the app + * falls back to no-depth submission. */ + (void)depthDesc; + return ovrpFailure_Unsupported; +} + +int xrr_layer_stage_count(int layerId) { + pthread_mutex_lock(&g_xrlock); + int n = (layerId >= 0 && layerId < g_xr.layerCount && g_xr.layers[layerId].active) + ? (int)g_xr.layers[layerId].imageCount : 0; + pthread_mutex_unlock(&g_xrlock); + return n; +} + +ovrpResult xrr_get_layer_texture(int layerId, int stage, int eye, + uint64_t *outColor, uint64_t *outDepth) { + (void)eye; /* array layout: one image, eye = array layer chosen at submit */ + pthread_mutex_lock(&g_xrlock); /* read layer state consistently vs setup/destroy/frame loop */ + ovrpResult res = ovrpFailure_InvalidParameter; + if (layerId >= 0 && layerId < g_xr.layerCount) { + XrLayer *L = &g_xr.layers[layerId]; + if (L->active && stage >= 0 && (uint32_t)stage < L->imageCount) { + static int gc = 0; /* DIAG: how often / with what stage is this called? */ + if (gc++ < 30) XRRLOG("GetLayerTexture: id=%d stage=%d eye=%d (call #%d)", + layerId, stage, eye, gc); + /* copy-ring: hand UE the per-stage shim image so it renders there (we copy -> + * OpenXR later). Per-stage (distinct images) is required — collapsing to one + * image breaks UE's multi-buffering/TAA. */ + if (outColor) *outColor = (L->shimCount > 0) ? L->shimImages[stage] : L->colorImages[stage]; + if (outDepth) *outDepth = L->depthSwapchain ? L->depthImages[stage] : 0; + res = ovrpSuccess; + } + } + pthread_mutex_unlock(&g_xrlock); + return res; +} + +double xrr_predicted_display_time_s(void) { + /* OpenXR time is ns (XrTime) -> seconds. Read the published snapshot (seqlock, lock-free). */ + XrTime t; pose_snapshot(NULL, NULL, &t); + return (double)t * 1e-9; +} + +/* ------------------------------------------------------------- poses ------- */ +/* Real per-eye FOV as ovrp tangents (positive magnitudes), from the runtime's + * located views. The game MUST render its projection with these exact tangents or + * the compositor reprojects to a different frustum -> ghosting/tearing/black. */ +void xrr_eye_fov_tangents(int eye, float *up, float *down, float *left, float *right) { + XrView v[2]; pose_snapshot(v, NULL, NULL); + XrFovf f = v[eye & 1].fov; + if (f.angleLeft == 0.0f && f.angleRight == 0.0f) { /* not located yet */ + *up = *down = *left = *right = 1.19f; /* ~50deg fallback */ + return; + } + *up = tanf(f.angleUp); + *down = tanf(-f.angleDown); + *left = tanf(-f.angleLeft); + *right = tanf(f.angleRight); +} + +ovrpResult xrr_get_node_pose(ovrpNode node, ovrpPoseStatef *out) { + if (!out) return ovrpFailure_InvalidParameter; + memset(out, 0, sizeof(*out)); + out->Pose.Orientation.w = 1.0f; + out->Time = xrr_predicted_display_time_s(); + if (!g_xr.running) return ovrpFailure_NotYetImplemented; + + { /* DIAG: which nodes does UE query to build its view matrices? */ + static int nq = 0; + if (nq++ < 24) XRRLOG("GetNodePose node=%d", node); + } + if (node == ovrpNode_EyeLeft || node == ovrpNode_EyeRight) { + int i = (node == ovrpNode_EyeRight); + XrView v[2]; uint32_t vc; pose_snapshot(v, &vc, NULL); + if (vc > (uint32_t)i) + ovrp_pose_from_xr(&v[i].pose, &out->Pose); + return ovrpSuccess; + } + if (node == ovrpNode_Head || node == ovrpNode_EyeCenter) { + XrTime dt; pose_snapshot(NULL, NULL, &dt); + XrSpaceLocation loc = { XR_TYPE_SPACE_LOCATION }; + if (xrLocateSpace(g_xr.viewSpace, g_xr.appSpace, dt, &loc) == XR_SUCCESS) + ovrp_pose_from_xr(&loc.pose, &out->Pose); + return ovrpSuccess; + } + if (node == ovrpNode_HandLeft || node == ovrpNode_HandRight) { + int got = xrr_get_hand_pose(node, out); + static int hd = 0; + if (hd < 6) { hd++; XRRLOG("GetNodePose hand node=%d got=%d", node, got); } + return got ? ovrpSuccess : ovrpFailure_NotYetImplemented; + } + return ovrpFailure_NotYetImplemented; +} diff --git a/shim/src/xr_runtime.h b/shim/src/xr_runtime.h new file mode 100644 index 0000000..f9f5b2f --- /dev/null +++ b/shim/src/xr_runtime.h @@ -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 +#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 */ diff --git a/shim/tests/harness.c b/shim/tests/harness.c new file mode 100644 index 0000000..34813c5 --- /dev/null +++ b/shim/tests/harness.c @@ -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 + +#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; +} diff --git a/tools/desktop-harness/README.md b/tools/desktop-harness/README.md new file mode 100644 index 0000000..ea21f9e --- /dev/null +++ b/tools/desktop-harness/README.md @@ -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. diff --git a/tools/desktop-harness/harness.c b/tools/desktop-harness/harness.c new file mode 100644 index 0000000..3f8e778 --- /dev/null +++ b/tools/desktop-harness/harness.c @@ -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 +#include +#include +#include +#include + +#include +#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; +} diff --git a/tools/desktop-harness/run.sh b/tools/desktop-harness/run.sh new file mode 100755 index 0000000..813efec --- /dev/null +++ b/tools/desktop-harness/run.sh @@ -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 diff --git a/tools/desktop-harness/xrexts.c b/tools/desktop-harness/xrexts.c new file mode 100644 index 0000000..d517f1d --- /dev/null +++ b/tools/desktop-harness/xrexts.c @@ -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 +#include +#include +#include + +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; +}