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
This commit is contained in:
Daniel LynchandClaude Opus 4.8 committed 2026-06-29 00:48:48 -04:00
commit a72a79ad29
57 files changed
+9304

No files matched your search

@@ -0,0 +1,104 @@
# RE4 VR Shim — Handoff (2026-06-26, Lever 2 reproject session)
Continuation of `HANDOFF-2026-06-26-skipblack.md`. Branch `latency-perffix-copyring`.
This session **built and shipped the working in-game black fix (Lever 2 reproject)** and hit
the tuning wall that points to a render-ahead pivot. Detail in auto-memory (MEMORY.md index):
`luma-gate-works-bad-vs-benign-black`, `reproject-works-playable`,
`reproject-tuning-and-renderahead-pivot`, `submit-count-cannot-detect-black`,
`skipblack-blacks-are-sustained-onset-only`.
## TL;DR — the in-game black is FIXED (reproject), "1000x better than black, playable"
The whole project's black-hunt is functionally solved. On a truncation-black frame we blit the
held last-good eye image back over it and present with the SAVED pose; the compositor timewarps
it to the current head pose instead of flashing black. Committed through `3522e5c`.
## What was built this session (commits a45362c..3522e5c)
1. **Post-hitch detector + barcode validation tool** (a45362c, c2a6c29): instrument-only black
flag; on-screen frameIndex barcode that turns red on a flagged frame for frame-exact video
correlation (`debug.re4vr.barcode`).
2. **Submit-count ruled out** (508f55a): UE issues a flat 3 vkQueueSubmits/frame on black AND
normal frames — truncation is fewer DRAWS, not fewer submits. Dead end, don't retry.
3. **Per-frame luma black gate** (571aa00): `xrr_vk_frame_luma` reads max luma of a few rows of
the resolved eye image; covers the whole sustained-black TAIL (post-hitch only caught the
onset). `debug.re4vr.lumagate`, `lumathr` (default 12). **Bad black = luma<thr AND
appLayers==1** (appLayers>=2 = a menu's black eye-fov behind a UI quad — benign).
4. **Reproject** (7c0882b): `debug.re4vr.reproject=1`. Copy-ring hold (`L->holdImage`, no
cross-frame swapchain hold so no pipeline=1 instability). Save acquired->hold on good frames
(with pose), restore hold->acquired on black frames, submit with saved pose
(`g_lastGoodViews`, used in `build_composition`).
5. **Tuning** (e3bbe85, 3522e5c): `savethr` (default 40, only hold clearly-lit frames — fixes
dim reprojected frames); `abruptdrop` (default 0 = reproject all; raise to skip gradual
fades); `holdevery` (default 4, throttle the save copy for perf).
## State of play (device-tested)
- **Black flashes GONE, playable.** Stable, no crashes over multiple soaks.
- **Perf ~17ms (~59fps).** Breakdown measured: resolve-wait ~7ms + luma ~3.5ms + save ~0.6ms.
Folding luma into the resolve cmd buffer = WASH (luma's cost is a forced Adreno tile-resolve
from the TRANSFER_SRC transition, not shareable wait overhead — tried + reverted). 72Hz needs
attacking the 7ms resolve-wait = render-ahead.
- **Remaining artifacts (all inherent to single-frame reprojection):** (a) reproject OVERRIDES
the game's intentional fades (comfort/teleport/scene/load) — they're black too; abruptdrop
helps but can't cleanly separate fade from shallow truncation. (b) long ~1s holds during
loads are jarring. (c) head-rotation during a hold shows timewarp edge-black; depth-hold (not
done) would add positional reprojection.
## NEXT — revisit RENDER-AHEAD (user's strategic call), but verify the premise first
Why now: render-ahead adds 1 frame latency -> UE gets more GPU budget/frame -> FEWER/shorter
truncations -> reproject (and its artifacts/long-holds) needed far less; and reproject+luma are
now a SAFETY NET for render-ahead's old instability (detect+hide any black instead of flashing).
- **GATE (do this first):** resolve the contradiction — memory `black-is-not-submit-timing`
says UE renders empty regardless of timing, but the user observed black DROPPED when the luma
readback (a per-frame GPU sync) turned on. Experiment: use the luma gate to COUNT
appLayers==1 truncation-black frames per minute at different added-latency/sync levels (e.g.
baseline vs lumagate-on vs an added sync). If latency reduces truncation frequency ->
render-ahead is justified; if not -> stay with reproject + tuning + maybe depth-hold.
- If justified: build render-ahead informed by the luma signal (present held frame only when
needed, not blindly like the old `pipeline=1`), with reproject as the fallback. See
`deferred-flush-unstable-abandoned` for the old failure modes (acquire/release imbalance).
## Game perf RE (done this session) — one new lever, several myths busted
Static RE of the GAME binary `dump/apk_libs/lib/arm64-v8a/libUE4.so` via capstone. Full report:
`analysis/game-perf-RE.md`; memory `game-perf-RE-findings`. This REPLACES the behavioral
inferences in `game-doesnt-drive-dynamic-perf-from-feed` with code proof:
- **No game-side dynamic-FFR loop exists** — `SetTiledMultiRes{Level,Dynamic}` args are config
bytes (`SetTiledMultiResDynamic(Settings.byte[0x11d])`), never compared to GPU time/metrics. So
shim-side dynamic FFR is the ONLY path (nothing we return makes the game loop). Boot `Dynamic(0)`
is just that config value.
- **Perf-metrics gate hypothesis REFUTED** — game never gates FFR on `IsPerfMetricsSupported`;
perf-metrics feed only a stats/HUD collector. Commit 68a7093 unlocked no behavior.
- **AppSW dead** (`GetLayerTextureSpaceWarp` not even a string in the binary).
- **`GetSystemRecommendedMSAALevel2` = hazard** (over-report -> more MSAA -> more load). Keep at 1.
### NEW game-driven lever — implement `ovrp_GetAdaptiveGpuPerformanceScale2` (do next session)
At its call site the game does `resolution = baseDensity * sqrt(scale)` where `scale` comes from
this API (table slot g+0x130). We return Unsupported -> scale=1.0 -> "viewport always full under
load". **If the shim returns `scale < 1.0` under GPU pressure (derive from `g_gpuFrameMs` vs the
13.9ms budget), the game self-downscales resolution — NO game patch.** Gated on `Settings.byte[0x19]
bit4` (adaptive-res enabled) — VERIFY on device. CAVEAT: this is a GPU-LOAD (pixel) lever; the
truncation is DRAW-bound (quarter-res still blacks), so it's a throughput/comfort/headroom win that
*might* indirectly slacken the render thread — NOT a guaranteed truncation fix. Easy + game-native;
worth stacking, measure truncation effect with the luma counter. (Find the API's stub in
`shim/src/stubs.c`; we already own `g_gpuFrameMs` + apply-foveation path.)
## Secondary follow-ups (the user "likes them all")
- **Depth-hold** (artifact reduction): enable depth (works on device, gameplay renders), hold
+restore depth alongside color, chain it (`build_composition` already chains depth) -> proper
positional reprojection. Helps the head-rotation swim/edge-black.
- **Verify the ghosting-fixed-by-readback** observation independently.
## Build / deploy / config
```
./shim/build_android.sh && ./packaging/repack.sh # repack bundles LAST build — check timestamp
adb -s <redacted-serial> install -r packaging/out/re4vr-shim.apk
adb -s <redacted-serial> logcat | grep -iE "xrr|REPROJECT"
```
Clean device state set this session: `reproject=1 holdevery=4 savethr=40 abruptdrop=0 depth=0
trace=0 barcode=0`. Launch blocked by controllers-asleep dialog — wake controllers first.
## Key reproject toggles (system props, re-read each frame unless noted)
- `debug.re4vr.reproject 1` — the fix (implies luma readback).
- `debug.re4vr.lumathr N` (12) — black threshold; `savethr N` (40) — min luma to HOLD a frame.
- `debug.re4vr.abruptdrop N` (0=reproject all; ~25 skips fades but misses shallow truncations).
- `debug.re4vr.holdevery N` (4) — save last-good every Nth frame (perf throttle).
- `debug.re4vr.barcode 1` — on-screen frameIndex barcode (red = reprojected frame), validation.
- `debug.re4vr.depth 1` — experimental depth swapchain (renders OK; not yet held for reproject).