Files
daniel-lynch--ovrplugin-ope…/docs/handoffs/HANDOFF-2026-06-26.md
T
Daniel LynchandClaude Opus 4.8 a72a79ad29 Initial public release: OVRPlugin→OpenXR interoperability shim
An independent reimplementation of Meta's libOVRPlugin ABI on top of OpenXR, so
VrApi-era Meta Quest VR titles can run on non-Meta OpenXR runtimes (Monado,
Steam Frame) instead of being locked to Meta hardware. Original code only — no
Meta/Epic/Capcom binaries, headers, or assets. Includes a desktop harness that
drives the shim against Monado headless.

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6sFYGXZPsq3v7xtcDES6g
2026-06-29 00:48:48 -04:00

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>20ms hitch we already detect). The vkQueueSubmit hook's submit-count probably WON'T distinguish bad frames (it's draw-bound; UE may batch).
  • Re-present: reuse the copy-ring infra (xrr_vk_alloc_images, shimImages[], the copy path in end_frame) — hold a copy of the last-good eye image, blit it into the acquired image on a bad frame. Avoids the cross-frame-hold instability that sank pipeline=1.
  • Discipline: implement the detector as instrument-only first (log "likely truncated frame" after each hitch), validate the signal correlates with perceived black, THEN build the reproject. Gate behind debug.re4vr.skipblack (default 0).

What was built this session (all committed, gated off by default)

  • Dynamic-perf bridge (68a7093): ovrp_*TiledMultiRes* (FFR) + GetGPUFrameTime + IsPerfMetricsSupported/GetPerfMetrics{Float,Int} forwarded to OpenXR. Re-enables RE4's perf APIs (were Unsupported). The game accepts them but does NOT self-scale from the feed. Fixed a NULL-foveation-profile runtime crash (use XR_FOVEATION_LEVEL_NONE_FB).
  • UE vkQueueSubmit hook (5fa59f4): dlsym-patch UE's exported global VulkanDynamicAPI::vkQueueSubmit -> trampoline -> xrr_on_ue_submit. Robust + reusable (e.g. for Lever 2 instrumentation). Gated: debug.re4vr.submithook 0/1/2.
  • resscale (5fa59f4): debug.re4vr.resscale = % of eye size. Doesn't fix the black; keep as a load knob.

Settled side-findings (don't re-investigate)

  • Title ghost = cold-load reprojection judder (cold-launch only; self-resolves warm; single eye-fov layer, no quad). See memory title-ghost-is-coldload-reprojection-judder.
  • Bridge "vignette" = the GAME's comfort vignette (user toggled it off; not a shim bug).
  • An in-game "crash" was an OOM SIGKILL (lmkd), worsened by in-game recording (which is broken — empty mp4). Capture via Meta Cast -> desktop record instead (scrcpy/screenrecord can't see VR: display 0 is FLAG_SECURE). RenderDoc recipe: memory renderdoc-remote-replay.

Build / deploy / config

./shim/build_android.sh && ./packaging/repack.sh
adb -s <redacted-serial> install -r packaging/out/re4vr-shim.apk
adb -s <redacted-serial> logcat | grep -i xrr

WARNING: repack.sh silently bundles the LAST successful build — confirm the build had no errors + check shim/build/arm64/libOVRPlugin.so timestamp before repack. Device Quest 2, USB serial <redacted-serial>. Pkg com.Armature.VR4, activity com.epicgames.ue4.GameActivity (launching needs controllers awake, else a system dialog blocks am start). Props reset to baseline: ffr=2, resscale=100, sscap=0, submithook=0, defern=3, trace=0 (none fix the black — all cosmetic/diagnostic now).

Key runtime toggles (system props; relaunch unless noted)

  • debug.re4vr.submithook 0|1|2 — UE submit hook: 0 off, 1 instrument, 2 present-on-submit.
  • debug.re4vr.defern N — present-on-submit: present on Nth render submit (default 3).
  • debug.re4vr.resscale N — eye buffer = N% of native (default 100).
  • debug.re4vr.ffr 0..3, debug.re4vr.sscap 1, debug.re4vr.trace 1|2 — FFR / supersample cap / frame+hitch trace. debug.re4vr.pipeline 1 (deferred-flush, unstable, shelved).