Files
Daniel LynchandClaude Opus 5 1f3dc40c07 docs+build: hygiene pass — close leak risk, de-drift docs, fix build prereqs
Repo hygiene round following a full review. No shim behaviour changes.

Leak risk:
- .gitignore: ignore CLAUDE.md (personal assistant-lane config, was one
  `git add -A` away from a public commit) and scratch_obj/.

Docs vs. reality:
- shim/README.md: rewritten. It described a pre-implementation skeleton with
  "core fns are TODO stubs returning -1005", three mutually inconsistent stub
  counts, and four completed milestones listed as open. Now carries the verified
  breakdown: 438/438 exports = 371 generated stubs + 46 core + 7 layers + 2
  Vulkan queries + 12 passthru trampolines.
- TESTING.md: dropped the self-contradicting "NOT yet" block (5 of 6 items were
  done or misstated, and contradicted the same file 45 lines above). Path B now
  points at tools/desktop-harness, which exists, instead of the orphaned
  shim/tests/harness.c. Path A prereqs marked as the record they are.
- HOST.md: corrected the runtime assumption. The OpenXR runtime inside Lepton is
  SteamVR (vendor/etc/openxr/1/active_runtime.json -> vrclient.so), not Monado.
  Favourable: SteamVR emulates Oculus Touch by default and advertises the
  XR_FB_foveation family, so the existing input and foveation paths should carry
  over. The old "remaining unknowns" are resolved by Lepton's published source
  and replaced with the items to check before a first Frame boot.
- README.md: same runtime correction.
- docs/research/RECON.md: the four passages prescribing an entitlement
  NOP/stub/bypass are corrected in place rather than merely disclaimed by the
  top banner, which they contradicted.

Build correctness:
- shim/build_android.sh: missing patchelf is now fatal. It warned and exited 0,
  producing a .so that cannot resolve the OpenXR loader at runtime.
- scripts/fetch_deps.sh + packaging/build_openxr_loader.sh: pin the OpenXR and
  Vulkan header versions (were tracking `main`), overridable via OPENXR_TAG /
  VULKAN_HEADERS_TAG; require cmake for the loader build.
- packaging/steamframe_patches.sh: use the apktool.jar that fetch_deps.sh
  downloads. Its prereq check demanded an `apktool` binary on PATH that the
  documented setup never provides, so it could not run after a clean setup.
- shim/gen_stubs.sh: it reads all_exports.txt, not shim_surface.txt; comment and
  emitted banner corrected. stubs.c regenerated (banner line only).
- shim/src/core.c: split seven `if (out) ...; return ...;` one-liners. Host
  build now compiles with zero warnings, down from seven.

Verified: host build 0 warnings; gen_stubs.sh output identical on regeneration;
bash -n clean on all edited scripts; pinned header/tarball URLs return 200 and
the tag tarball extracts to the expected directory name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 02:15:10 -04:00

8.8 KiB

Testing the shim — strategy & plan

We do NOT need Steam Frame to validate the hard part. The shim's whole job is OVRPlugin -> OpenXR, and the Quest 2 already runs an OpenXR runtime (Meta's Horizon OS runtime — the one that replaced VrApi). So the shim can be tested on hardware we own, today.

The three paths

Path Tests what Available Effort
A. Quest 2 + shim swap the real shim, real HW, real game, on Meta's OpenXR runtime now NDK arm64 build + Android instance handshake + manifest (real entitlement — you own it)
B. Monado-sim harness the shim's OpenXR call logic, fast iteration now small C harness + Linux arm64 (VM on the Apple-Silicon MacBook)
C. Steam Frame + Lepton the actual target (Monado under Lepton) ~summer 2026 everything

Recommended order: B for fast logic iteration, then A for the real proof. A is the thesis-validator; if RE4 renders on the Quest through our OpenXR shim instead of libvrapi.so, the project is essentially proven.


Path A — Quest 2 (the real test)

Idea: build the shim as an Android arm64 .so, drop it into the RE4 APK in place of the real libOVRPlugin.so, sideload, run. Our shim calls the Quest's own libopenxr_loader -> Meta's OpenXR runtime.

Prereqs (all done — kept as the record of what Path A needed):

  1. NDK arm64 build of the shim — DONE. shim/build_android.sh -> NDK r27c -> build/arm64/libOVRPlugin.so (aarch64, 438/438 drop-in, NEEDED libopenxr_loader).
  2. Instance handshake — DONE. src/android_init.c: JNI_OnLoad captures the JavaVM; xrr_pre_init calls xrInitializeLoaderKHR + enables XR_KHR_android_create_instance
    • chains XrInstanceCreateInfoAndroidKHR. Activity from Initialize5 arg4 with an Application-context reflection fallback. [VERIFY-ON-HW] whether Meta's runtime accepts the Application context vs requiring the real Activity, and the PreInitialize3-creates-instance-before-activity ordering.
  3. Manifest — no change needed in the end. RE4 VR is already a shipping Quest VR app and declares <uses-feature android:name="android.hardware.vr.headtracking">; packaging/inspect_manifest.sh reports any gaps.
  4. Entitlement — on Quest you OWN RE4 and the Quest has the real Meta Horizon platform service, so leave the ORIGINAL libovrplatformloader.so untouched and only swap libOVRPlugin.so. Logged into the owning account, the real ovr_Entitlement check passes legitimately ("you own it"). CONFIRMED on device 2026-06-29: a debug-re-signed, legit-mode build (original libovrplatformloader.so, no stub) launches into the game on a Quest 2 — re-signing does not break the entitlement check. Entitlement handling on hardware with no Meta backend (e.g. Steam Frame) is out of scope for this repo and is the user's responsibility.

Then:

# repack (you own the copy; patch-only distribution)
unzip base.apk -d apk/
cp shim/build/arm64/libOVRPlugin.so apk/lib/arm64-v8a/libOVRPlugin.so
# rebuild + zipalign + sign with your own debug key, then:
adb install -r re4vr-shim.apk     # or push OBB + sideload
adb logcat | grep -iE 'xrr|OVRPlugin|openxr'   # watch the [xrr] logs

packaging/repack.sh now does the repack, align and re-sign in one step; the manual sequence above is kept only to show what it does.

First-run signal to look for: instance and session create succeeding in logcat, then the frame loop spinning and the projection layer submitting. (Historically the rough edges on a first run were depth, input and the swapchain index lockstep; input and rendering are now working — see "Current state" below.)

Path B — desktop OpenXR harness (fast iteration) — BUILT

tools/desktop-harness/ drives the ovrp_* sequence against a real OpenXR runtime with no headset and no RE4: PreInitialize3 -> Initialize5 -> SetupLayer -> [WaitToBeginFrame -> BeginFrame4 -> GetNodePoseState3 -> EndFrame4] xN -> Shutdown2 asserting each returns ovrpSuccess. Build and run:

shim/build_host.sh                 # -> build/host/{libOVRPlugin.so,harness}
tools/desktop-harness/run.sh       # headless against monado-service

Needs libvulkan and an OpenXR loader (Debian: libvulkan-dev libopenxr-loader1 libopenxr-dev); without the loader build_host.sh compiles the objects and stops.

Worth adding when chasing a portability bug: run it with VK_LAYER_KHRONOS_validation and the OpenXR core-validation API layer enabled. The harness currently passes without them, so it does not yet catch usage-flag or queue-family mistakes that a strict runtime would reject.

Note: shim/tests/harness.c is the original superseded smoke test; its build line is stale and no script references it.

Path C — Steam Frame (the target)

Same arm64 shim .so, but the APK runs under Lepton (Valve's Waydroid fork, now open source) and the OpenXR runtime inside the container is SteamVR — Lepton's vendor/etc/openxr/1/active_runtime.json names steamvr and points at a host-mounted vrclient.so. (Earlier notes here and in HOST.md assumed Monado; that was wrong.)

What that buys us: SteamVR presents Frame controllers as emulating Oculus Touch by default, so the shim's Touch bindings should bind; and it advertises the XR_FB_foveation family plus XR_META_foveation_eye_tracked, so the existing foveation path survives. Lepton also mounts the host's mesa/turnip/zink and gralloc into the container, which answers the old HOST.md unknown about sharing swapchain images across the container boundary.

Steam Frame shipped 2026-09-14. Flow: NDK build -> repack.sh -> steamframe_patches.sh (the Build spoof is required — Lepton reports ro.product.manufacturer=Valve) -> sideload via adb into a Lepton container. Path A having worked makes this mostly packaging and runtime-plumbing, but see HOST.md for the specific items to check first.


Current state

Path A is done and field-verified. RE4 VR boots and is playable on a Quest 2 through the shim on Meta's OpenXR runtime: stereo rendering, head and controller tracking, buttons, grips, haptics, save loading. The frame-pacing "ghost" that dominated development is fixed (game-thread pacing; see docs/research/ghost-fix-2026-06-27.md). Shipped and verified:

  • Session lifecycle, event-driven state machine, Android instance handshake (src/android_init.c).
  • Frame loop with xrWaitFrame on the game thread and a FIFO frameState handoff to the render thread.
  • Poses via xrLocateViews and xrLocateSpace; LOCAL_FLOOR when the game asks for floor level.
  • Swapchains from ovrpLayerDesc, per-frame acquire/wait/release, real projection-layer submit, layer z-order fix so splash quads composite above the eye layer.
  • Input action sets for Touch controllers plus haptics (src/xr_input.c).
  • Vulkan extension queries mapped to xrGetVulkan*ExtensionsKHR (src/vk_session.c).
  • CPU/GPU perf levels forwarded via XR_EXT_performance_settings; game-driven foveation.
  • 438/438 ovrp_ symbols; around 48 OpenXR entry points.

Open / known gaps:

  • Depth layer submission is built but gated off (debug.re4vr.depth); Meta's runtime accepts but does not use plain KHR depth for reprojection.
  • Swapchain index lockstep with UE's own TextureStage is assumed, not enforced — a mismatch is logged but not corrected.
  • Residual XR_FRAME_DISCARDED hiccups from the frameState ring dropping its oldest entry under overflow instead of applying back-pressure.
  • In-game black near load zones is a level-streaming / memory stall, mitigated rather than fixed. See docs/handoffs/HANDOFF-2026-06-27.md.
  • Several debug.re4vr.* paths are documented dead ends kept for reference.

Next: Path C (Steam Frame)

Frame shipped 2026-09-14, so this is the live front. Highest-value items before a first boot attempt, in order:

  1. Page alignment. The shim's ELF segments and the repacked APK are 4 KB-aligned (zipalign -p 4); Valve's Unreal docs reference a 16 KB page-alignment requirement. If the Frame kernel uses 16 KB pages the library will not load. Check first: it is a -Wl,-z,max-page-size=16384 plus zipalign -P 16 fix, but it would present as a mystery launch failure.
  2. Swapchain usage flags. setup_layer requests only COLOR_ATTACHMENT and SAMPLED. Meta over-provisions mutable, broadly-usable images; a spec-following runtime gives you exactly what you asked for, and UE 4.25 needs a mutable format for its linear UNORM view.
  3. Refresh rate. 72 Hz is hardcoded in ovrp_GetSystemDisplayFrequency2 and in the frame-budget constant. Frame runs 72/90/120/144. Derive it from predictedDisplayPeriod.
  4. Session events. Only READY and STOPPING are handled, so focus loss and quit from the Steam overlay never reach the game.
  5. The per-frame GPU flush-wait. It exists because Meta's compositor does not sync against our submit. A/B it on Frame with debug.re4vr.noflushwait.