Files
daniel-lynch--ovrplugin-ope…/docs/handoffs/HANDOFF-2026-06-26.md
T
Daniel LynchandClaude Opus 5.5 da7dd08640 docs: fix runtime claim, stale header notes and dangling doc references
- ovrplugin_shim.h: runtime on Steam Frame is SteamVR (HOST.md), not Monado;
  drop the "Status: scaffold" line and the note that SetupLayer /
  GetLayerTexture2 await a 128-byte desc (implemented against the 108-byte
  ovrpLayerDesc); point RE-NOTES/SHIM-SCOPE at docs/research/.
- lever2-detect-skip-black-handoff.md lives in docs/research/, not analysis/.
- References to unpublished RE notes now say so instead of naming files that
  are not in the repo.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 22:44:34 -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 docs/research/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).