mirror of
https://github.com/daniel-lynch/ovrplugin-openxr-shim.git
synced 2026-10-06 03:00:05 +02:00
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
106 lines
7.7 KiB
Markdown
106 lines
7.7 KiB
Markdown
# 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.
|