# 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).