An independent reimplementation of Meta's libOVRPlugin ABI on top of OpenXR, so VrApi-era Meta Quest VR titles can run on non-Meta OpenXR runtimes (Monado, Steam Frame) instead of being locked to Meta hardware. Original code only — no Meta/Epic/Capcom binaries, headers, or assets. Includes a desktop harness that drives the shim against Monado headless. Scope/legal: interoperability; entitlement handling is out of scope. See README for the legal/scope section and docs/ for the research trail and design notes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6sFYGXZPsq3v7xtcDES6g
4.8 KiB
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>20mshitch 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 sankpipeline=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 (useXR_FOVEATION_LEVEL_NONE_FB). - UE vkQueueSubmit hook (
5fa59f4): dlsym-patch UE's exported globalVulkanDynamicAPI::vkQueueSubmit-> trampoline ->xrr_on_ue_submit. Robust + reusable (e.g. for Lever 2 instrumentation). Gated:debug.re4vr.submithook0/1/2. - resscale (
5fa59f4):debug.re4vr.resscale= % of eye size. Doesn't fix the black; keep as a load knob.
Settled side-findings (don't re-investigate)
- Title ghost = cold-load reprojection judder (cold-launch only; self-resolves warm;
single eye-fov layer, no quad). See memory
title-ghost-is-coldload-reprojection-judder. - Bridge "vignette" = the GAME's comfort vignette (user toggled it off; not a shim bug).
- An in-game "crash" was an OOM SIGKILL (lmkd), worsened by in-game recording (which is
broken — empty mp4). Capture via Meta Cast -> desktop record instead (scrcpy/screenrecord
can't see VR: display 0 is FLAG_SECURE). RenderDoc recipe: memory
renderdoc-remote-replay.
Build / deploy / config
./shim/build_android.sh && ./packaging/repack.sh
adb -s <redacted-serial> install -r packaging/out/re4vr-shim.apk
adb -s <redacted-serial> logcat | grep -i xrr
WARNING: repack.sh silently bundles the LAST successful build — confirm the build had no
errors + check shim/build/arm64/libOVRPlugin.so timestamp before repack.
Device Quest 2, USB serial <redacted-serial>. Pkg com.Armature.VR4, activity
com.epicgames.ue4.GameActivity (launching needs controllers awake, else a system dialog
blocks am start).
Props reset to baseline: ffr=2, resscale=100, sscap=0, submithook=0, defern=3, trace=0
(none fix the black — all cosmetic/diagnostic now).
Key runtime toggles (system props; relaunch unless noted)
debug.re4vr.submithook 0|1|2— UE submit hook: 0 off, 1 instrument, 2 present-on-submit.debug.re4vr.defern N— present-on-submit: present on Nth render submit (default 3).debug.re4vr.resscale N— eye buffer = N% of native (default 100).debug.re4vr.ffr 0..3,debug.re4vr.sscap 1,debug.re4vr.trace 1|2— FFR / supersample cap / frame+hitch trace.debug.re4vr.pipeline 1(deferred-flush, unstable, shelved).