Files
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

106 lines
7.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.