Files
daniel-lynch--ovrplugin-ope…/docs/handoffs/HANDOFF-2026-06-27.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

7.7 KiB
Raw Blame History

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 <redacted-serial> install -r packaging/out/re4vr-shim.apk
adb -s <redacted-serial> 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.