mirror of
https://github.com/daniel-lynch/ovrplugin-openxr-shim.git
synced 2026-10-06 02: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
105 lines
7.6 KiB
Markdown
105 lines
7.6 KiB
Markdown
# 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).
|