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