Skip to content

Latest commit

 

History

History
499 lines (433 loc) · 29 KB

File metadata and controls

499 lines (433 loc) · 29 KB

Bridge findings — the working path, gate by gate

Engineering record of the risk-retirement gates behind the demo in the README: Beat Saber 1.29.4 → CrossOver/DXMT → wineopenxr → oxrsys → embedded ALVR → Quest 3 at 72 fps and ~79 ms motion-to-photon (the pre-1.3.0 baseline; see README for the current WiFi baseline with the native arm64 HEVC helper — 102.9 ms p50 on-head, USB 93.5 as the gate-era transport-isolation figure) — what each gate proved and which fixes were load-bearing. Condensed; the full transcript is in the git history of FINDINGS-oxrsys.md (repo root). The dead SteamVR-under-Wine path: steamvr-blocked.

Environment

Component Value
Host Apple M3 Max, macOS 26.5.1
CrossOver 26.2.0 — bottle MoltenVK 1.2.10 (x86_64, Vulkan api 1.2.290)
Native arm64 Vulkan Homebrew MoltenVK 1.4.1 + vulkan-loader 1.4.350
Toolchain clang 21, mingw-w64 16.1, cmake/ninja/meson + Metal toolchain

Gate 0 — IOSurface + MTLSharedEvent, cross-process and cross-arch — GREEN

Question: can a cross-process GPU surface — the blocking primitive for Mac VR — be shared via VK_EXT_metal_objects/IOSurface, across the Rosetta-x86_64 ↔ native-arm64 boundary between the Wine bottle and oxrsys?

Method: src/gate0_iosurf.mm — Metal fills an IOSurface-backed texture and signals an MTLSharedEvent; Vulkan imports both (VkImage + timeline VkSemaphore), waits, copies out, byte-compares. Cross-process mode shares by global IOSurfaceID. Built for arm64 (MoltenVK 1.4.1) and for x86_64 against CrossOver's own MoltenVK 1.2.10, run under Rosetta.

Results — all byte-exact, 0/16384 mismatched (evidence/gate0-*.txt, local artifacts, not in the repo):

Test MoltenVK Result
0a single-process import + MTLSharedEvent→VkSemaphore sync 1.4.1 arm64 PASS
0a single-process import + sync 1.2.10 x86_64 (Rosetta) PASS
0b cross-process IOSurface share arm64 → arm64 PASS
0b cross-process + cross-arch: arm64 creator → x86_64/Rosetta importer mixed PASS
0b cross-process + cross-arch: x86_64/Rosetta creator → arm64 importer mixed PASS

Determination: the zero-copy mechanism works — through the bottle's own MoltenVK under Rosetta and across the x86↔arm64 boundary in both directions; no MoltenVK upgrade needed. MTLSharedEvent sync was proven single-process on both arches; cross-process event transport is covered by the OpenXR swapchain-release protocol. Residual risk: low.

Gate 1 — oxrsys native → Quest 3 — GREEN (live)

A minimal native Metal OpenXR client (src/oxrsys_cubes.mm, full xrWaitFrame/Begin/EndFrame loop) drove oxrsys to a Quest 3 over USB ADB reverse TCP: 2272×1264 H.265 @ 72 Hz, every frame decoded, render-pose match 100%, client latency ≈11–12 ms (evidence/gate1-*.txt, local artifacts, not in the repo). The headset pose streams back and the app re-renders from it — 6DoF world-locked rendering confirmed visually in-headset, hand tracking active. Launcher: scripts/dev/run_quest_gate1.sh. Fast-motion artifacting is an oxrsys quality limitation, not a gate blocker.

Verdict: the existential dependency — a native macOS OpenXR runtime that streams to the Quest with tracking — holds on this machine.

Pivot — monofunc/wineopenxr (D3D11) supersedes the Vulkan plan

Mid-project we found monofunc/wineopenxr: a working D3D11 OpenXR→native-runtime bridge for CrossOver, sharing D3D11 swapchain textures zero-copy as MTLTextures via a DXMT interop fork (IMTLD3D11InteropDevice) — no winevulkan patch, no Wine-from-source build, real D3D11 PCVR games. It retired the original Gate 2 (Wine source) and Gate 4 (winevulkan) outright.

Gate 2′ — the D3D11 stack builds

Component How Result
monofunc/dxmt (feature/openxr, interop) fork + GitHub CI artifact d3d11.dll, dxgi.dll, d3d10core.dll, winemetal.dll + winemetal.so (x86_64)
wineopenxr local cmake + mingw (Wine headers only) wineopenxr.dll (PE) + wineopenxr.so (Mach-O x86_64)
oxrsys-x64 backend rebuilt x86_64 with H.264 ported for Rosetta liboxrsys-runtime.dylib, all tests pass under Rosetta
D3D11 test app src/d3d11_clear.cpp (shader-free clear-color OpenXR client) + cross-built loader d3d11_clear.exe + libopenxr_loader.dll

Gate 3′ — D3D11 bridge end-to-end — GREEN

Chain: d3d11_clear.exelibopenxr_loader.dllwineopenxr.dllwineopenxr.so → oxrsys-x64, D3D11↔Metal via DXMT interop, in a dedicated bottle with the DXMT fork overlaid. Headless: wine exit 0, 150 frames, ImportMTLTexture2D succeeded for both eyes (evidence/bridge-SUCCESS-*.txt, local artifact, not in the repo). Compositor pixel readback then proved the rendered content arrives byte-exact and live — the app's pulsing red, snapshot src[0,0] BGRA=25,25,213,255 (evidence/blackscreen-ROOTCAUSE.txt, local artifact, not in the repo).

Seven integration fixes were the actual work:

  1. App OpenXR apiVersion → 1.1.0 (oxrsys ≤1.1.57 rejects the PE headers' 1.1.60 with XR_ERROR_API_VERSION_UNSUPPORTED).
  2. active_runtime.x86_64.json must live at /usr/local/share/openxr/1/ — the macOS loader ignores XR_RUNTIME_JSON under wine's secure-exec.
  3. H.264 under Rosetta (HEVC hardware encode unavailable translated); PreferredVideoCodec() auto-selects.
  4. Request a non-sRGB swapchain format: an sRGB host texture trips ImportMTLTexture2D's ORIGINAL_FORMAT check (DXMT expects typeless-parent → linear).
  5. oxrsys swapchain MTLTextures need MTLTextureUsagePixelFormatView (DXMT requires it).
  6. Fence sync at xrReleaseSwapchainImage: oxrsys snapshots without waiting for the producer, and DXMT renders on its own queue → black frames; wineopenxr now waits on the DXMT/Metal fence before releasing.
  7. Discovery loopback: broadcasts to 255.255.255.255 don't loop back on macOS; a 127.0.0.1 beacon was added for the local simulator.

Fixes 4–5 are oxrsys↔DXMT interop findings (reported upstream). Remaining simulator black screens were a codec mismatch, not the bridge: the simulator only decoded H.265 while the Rosetta path sends H.264 — an H264Decoder + codec router were added. (The legacy streamer's pacing and watchdog fixes are in the git history; the ALVR backend below replaced it.)

VideoToolbox under Rosetta

Hardware H.264 encode works under Rosetta (UsingHardwareAcceleratedVideoEncoder=true); only HEVC hardware encode is unavailable translated (still true on macOS 27.0 beta — see the 2026-08-01 re-verification below). The rest is subtler.

Low-latency rate control: zero-chroma bug, root-caused and fixed. EnableLowLatencyRateControl halves encode latency (~14.8 → 7.8 ms p50) but under Rosetta produced correct luma with all-zero chroma — a green image. Decoded-plane scans (Y[max≈59], Cb/Cr=[0..0]) ruled out color interpretation; the flag was first gated off. tools/vt-llrc-probe (four-config matrix, {LL-RC on/off} × {BGRA/NV12 input}) then isolated the failing stage: LL+BGRA = dead chroma, LL+NV12 = healthy. The bug is VT's internal RGB→YCbCr conversion inside the low-latency (rtvc) encoder under Rosetta — undocumented: production VT users all feed 4:2:0. Report: apple-feedback-1-lowlatency-bgra-zero-chroma. Fix (oxrsys 47dc2a2): 420v biplanar encoder pool + rgb_to_nv12 Metal kernel (BT.709 video-range) on every compose path; LL-RC re-enabled with a retry-without fallback. Encode p50 33 → 10.3 ms, live motion-to-photon 311 → 79 ms. (Apple fixed the underlying VT bug on macOS 27.0 beta — LL+BGRA now decodes healthy chroma — and the NV12 conversion was subsequently removed on the oxrsys bgra-direct branch; see the re-verification and the decision note that follows it.)

ConstantBitRate — banned, but the stall claim was retracted (2026-07-04). kVTCompressionPropertyKey_ConstantBitRate is still never used, but the original "accepted then stalls the pipeline" claim was retracted: an instrumented probe (--cbr, including an exact production-config mirror) shows classic RC accepts-and-ignores CBR while LL-RC rejects it (-12900); the "stall" evidence was RealTime frame drops miscounted as missing callbacks, and the live freeze matched the same-day use-after-free below. The header also documents CBR as incompatible with AverageBitRate/DataRateLimits, which we set. Retraction record: apple-feedback-2-constantbitrate-pipeline-stall.

Bitrate is not enforced. VT does not reliably hold AverageBitRate/DataRateLimits on hard content in any mode under Rosetta (probe: 77–122 Mbps at a 42 Mbps target on noise). Mitigated with ALVR's Adaptive bitrate feedback loop, not VT-side caps.

Rejected properties. MaxFrameDelayCount and PrioritizeEncodingSpeedOverQuality are rejected (-12900) by the LL encoder.

Use-after-free postmortem. Frame-context fields were written after VTCompressionSessionEncodeFrame hands the refcon to VT; LL-RC makes callbacks near-synchronous, so the callback (which frees the context) can run before EncodeFrame returns. Rule: all refcon writes precede submission.

Re-verified on macOS 27.0 beta (26A5388g), 2026-08-01

Re-ran tools/vt-llrc-probe (rebuilt fresh, x86_64/Rosetta) plus one-off HEVC and per-frame-latency probes on the newer macOS beta. Three findings:

  • The LL-RC zero-chroma "green" bug no longer reproduces. LL+BGRA now decodes healthy chroma (Cb[42..236] Cr[60..245], deterministic across 4 runs) where it was previously CHROMA DEAD. Apple appears to have fixed VT's internal RGB→YCbCr path under Rosetta. We are not removing the NV12 conversion — it's beta-only, could regress, is still needed on stable macOS, and also fixes color-matrix correctness, not just the bug.

  • Hardware HEVC is still unavailable under Rosetta — unchanged. Creating a session with RequireHardwareAcceleratedVideoEncoder=YES fails (H.264 succeeds; HEVC returns -12908 classic / -12902 LL-RC). VTCopyVideoEncoderList under x86_64 shows only HEVC … hardware=no (software); native arm64 on the same machine shows Apple HEVC (HW) … hardware=YES, proving the silicon has the encoder — VT just won't hand it to a translated process. The software HEVC fallback is ~600 ms/frame p50 (40× over the 14 ms 72fps budget) and can't use LL-RC at all, so it's not viable. This is why the native-arm64 out-of-process encoder (noted under the ALVR WiFi backend section) is the real path to HEVC.

  • "LL-RC has a worse latency tail" is an artifact, not a regression. Steady-state per-frame latency is identical to classic RC (~10 ms p50; both flat ~9–11 ms from frame 6 on). The apparent gap is three separable things: (1) a 5–6-frame warmup ramp (large IDR + HW spin-up) that dominates p95/max in a 48-frame sample — LL-RC's ramp peaks lower but stays elevated longer, inflating its p95; (2) the probe's classic-only DataRateLimits cap made the original bitrate comparison unfair (fixed: same cap → same conclusion); and (3) LL-RC drops frames by design to hold its latency ceiling — on the probe's incompressible per-pixel-noise content it sheds every other frame from ~frame 20 (14/48), while classic RC absorbs the load and drops none. Real temporally-coherent game frames (~21 KB inter-frames here) don't trigger this, which is why LL-RC+NV12 is the live production path.

Decision update, 2026-08-01: NV12 conversion removed (supersedes the note above)

The "we are not removing the NV12 conversion" position above was reversed the same day, with the color-matrix concern resolved by measurement rather than assumption. A new tools/vt-llrc-probe --matrix mode encodes flat color bands and compares decoded Y/Cb/Cr against the same BT.709 limited-range reference the rgb_to_nv12 kernel implemented: on 26A5388g, VT's internal conversion of BGRA matches within ±1 across all bands (NV12 controls exact at 0.0; deterministic across runs, both classic and LL-RC). With correctness proven, the oxrsys bgra-direct branch deletes the kernel, the NV12 pool/plane views, and the convert pass; the BGRA composite target is now the encoder's CVPixelBuffer itself. Consequences: the pipeline requires macOS 27+ under Rosetta (doctor checks this; older hosts must pin oxrsys ≤ cf5f926), and the fork carries the risk that the beta fix regresses before GA — re-run --matrix on each macOS beta bump.

Live-verified 2026-08-02 (Quest 3, stock client, 3008x1664@72): colors correct, 72 fps, zero packet loss. A same-day live A/B against the NV12-era build (only the encoder reverted) measured a real latency cost that the CPU-written offline probe cannot see: VT's internal RGB→YCbCr of GPU-written IOSurfaces under Rosetta takes ~25 ms inside VT (vs ~2 ms for the old Metal kernel), so encode is ~35-40 ms vs ~13-22 ms and client-measured motion-to-photon ~114 ms vs ~82-96 ms. Accepted (decision 2026-08-02) in favor of the simpler path — the NV12 kernel was retroactively also a Rosetta performance optimization, and the planned native-arm64 out-of-process encoder is the roadmap item that recovers the latency (plus HW HEVC). Merged to fork main as ab070b8.

Gate 5 — decision — go with the D3D11 path

The original Vulkan+winevulkan plan is unnecessary: the D3D11 path works end-to-end and is the real-PCVR-game path. Remaining work is polish, not architecture.

Gate 6 — real game (Beat Saber 1.29.4) — PLAYABLE on Quest 3

A real Unity-OpenXR title runs through the bridge in-headset with no real Steam:

  • Version: Beat Saber 1.29.4 — the first native-OpenXR build; predates the Meta account gate that hard-crashes newer builds (GetXPlatformAccessTokenAsync timeout → NullReferenceException in AppInit). Pinned depot download: see the README.
  • DRM: Goldberg Steam emulator satisfies steam_api64.dll offline (no SteamStub on the exe) — also sidesteps CrossOver's Steam/CEF instability.
  • Launch: ./demo.sh run (run_beatsaber_1294.sh remains in scripts/dev/ as a superseded reference).
  • Confirmed in-headset: menu navigation (laser + trigger) and gameplay.

Four runtime fixes were needed beyond the clear-app bridge (all in ext/oxrsys, one paired with ext/wineopenxr):

  1. XR_KHR_convert_timespec_time advertised and implemented — Unity hard-requires the Win32 perf-counter time extension wineopenxr synthesizes from it; without it xrCreateInstance fails.
  2. XrEventDataInteractionProfileChanged emitted when the controller profile resolves — the key input fix: without it Unity's Input System stayed on the KHR Simple-Controller fallback and the menu trigger never fired.
  3. Profile + float→bool: report oculus/touch_controller, and threshold float sources (trigger/value, squeeze/value, select/value) to boolean in GetButtonClick.
  4. Aim pose streamed distinctly from grip so menu lasers point where you aim.

Audio and haptics landed later with the ALVR backend, verified live. In-song pause resolved as a game/Unity limitation on every OpenXR runtime — pause works via X/A or the Quest system button; see menu-button.

ALVR WiFi backend

oxrsys's built-in streamer was replaced by embedding alvr_server_core v20.14.1 (its C API) into the runtime, with the stock ALVR client (v20.14.1, version-matched sideload) on the headset. ext/ALVR is a submodule pinned to the fork branch oxrsys-v20.14.1, which carries the reliability patches; patches/alvr-v20.14.1-oxrsys.patch mirrors that branch as a reviewable diff (patches/README).

The first WiFi session ran end-to-end — video, tracking, buttons, haptics and audio — but with ~2.5 stutter bursts/sec, blur from ALVR's 30 Mbps default overriding the configured bitrate, and a stream that died on headset sleep. After three fix rounds: 72 fps, total latency 78–82 ms (motion-to-photon p50 ≈79 ms), encode p50 10.3 ms, Quest decode 23 ms (226 ms at first connect), ~60 Mbps Adaptive bitrate, zero encoder drops.

Reconnect took three distinct fixes:

  • A macOS-only ABBA deadlock: the CoreAudio capture callback blocked on the session lock while connection_pipeline held it across cpal device enumeration, which needs the HAL mutex the callback's thread holds — every reconnect wedged as a client "error 11" timeout. Fix: a non-blocking is_streaming for the audio callback.
  • Stale StreamSender clones pinned the UDP port after disconnect → every re-bind failed EADDRINUSE. Fix: a closeable stream writer (close_writer() on every disconnect flavor + an RAII guard) and a lock-free disconnecting flag.
  • Headset sleep killed tracking: the tracking/statistics receive loops exited permanently on socket error; they now retry.

Smaller patches: socket buffers actually sized (macOS rejects u32::MAX with EINVAL, silently leaving 9216 bytes; now 8 MiB with a halving retry), audio teardown polling 500 → 50 ms, a leftover client.wired entry no longer starves WiFi discovery, and video-send / manual-IP failures are logged. Full inventory: patches/README.

Top remaining quality lever is stream resolution (currently 0.75-scale of client-native), then a native-arm64 out-of-process encoder: running VideoToolbox natively removes the Rosetta fragility (hardware HEVC, LL-RC without the NV12 workaround) and reuses Gate 0's cross-arch IOSurface hand-off.

Gate A — native-arm64 encoder plan (2026-08-02)

First gate of the out-of-process encoder effort (plan: gpt-5.6-sol design + Codex adversarial review). vt-llrc-probe gained a --gpu mode: a Metal compute kernel writes each frame into one of 3 rotating IOSurface-backed BGRA pixel buffers and VTCompressionSessionEncodeFrame runs from the command buffer's completed handler — the production oxrsys shape, on both arches (--size 3008x1664 = live stream dims; vt-llrc-probe-arm64 is the native build). Results:

  • HW HEVC Main LL-RC works natively with GPU-written surfaces: session creates with hardware required (com.apple.videotoolbox.videoencoder.hevc.rtvc), sustains 72 Hz at 3008x1664, and under deliberately incompressible noise content degrades better than H.264 (p95 20.5 ms vs 49.6 ms; the heavy frame-shedding on this content is LL-RC's documented load behavior, same as the 2026-08-01 CPU-probe note). Under Rosetta the same config still fails -12902 (re-verified with this exact code path).
  • LL-RC keyframe contract holds: a mid-run ForceKeyFrame produces a sync sample at exactly the requested PTS on both codecs, and parameter sets extract cleanly — HEVC VPS/SPS/PPS (NAL types 32/33/34, 26/53/7 bytes), NALUnitHeaderLengthOut = 4 on both codecs.
  • BT.709 color contract holds natively: --gpu --matrix worst band delta 1.1/255 (H.264) and 1.0/255 (HEVC) vs the probe's own limited-range reference.
  • The probe still cannot reproduce the live Rosetta conversion penalty. GPU-written x86_64 H.264 shows p50 13.6 ms — nowhere near the live ~35-40 ms. GPU-written buffers alone are not sufficient; the live cost evidently needs the game's concurrent GPU load contending with VT's conversion. Consequence: offline probe latency numbers bound the pipeline optimistically in BOTH environments; the BGRA-vs-NV12 helper input decision and all latency acceptance move to the live A/B gates (D/E).
  • Curiosity, same code/content/config: the Rosetta H.264 encoder held the 42 Mbps target with 0 drops; the native one ran at ~79 Mbps and shed 136/300 frames. The two rtvc builds have different quality/rate floors — expect live HEVC bitrate behavior to need Gate E tuning, not config parity alone.
  • Client HEVC capability resolved (was a plan unknown): ALVR v20.14.1 never capability-checks H.264/HEVC — VideoStreamingCapabilities has flags only for AV1/10-bit/high-profile (connection.rs:787-800; 10-bit gate at :751). H.264 and HEVC are assumed universal in the protocol, so HEVC is safe to select for the stock Quest client, and the negotiated codec is echoed into session.json openvr_config/codec — the same file RefreshNegotiatedConfig already reads for dims.

Gate A verdict: PASS — existence proof complete (native HW HEVC LL-RC + IDR contract + color), with the latency criterion explicitly deferred to the live gates per the both-ways probe/live divergence above.

Gate B/B2 — cross-arch IPC + the foreign-surface VT seam (2026-08-03)

tools/xarch-ipc-probe (one source, two builds): an x86_64 parent (simulating the Rosetta/Wine runtime) spawns the arm64 build as the future encoder helper, hands it 3 IOSurface-backed 32BGRA 3008x1664 CVPixelBuffers by Mach send right, and drives the production frame loop across the arch boundary — parent GPU-writes, socketpair notify after the command buffer's completed handler, child verifies/encodes. Logs: evidence/xarch-ipc-probe-gateB-100k.log, -gateB2-noise.log, -gateB2-matrix.log.

  • The preferred rendezvous route is dead — by XPC poisoning, not by rendezvous. Installing the parent's receive right as the child's bootstrap port (posix_spawnattr_setspecialport_np(TASK_BOOTSTRAP_PORT)) delivers the check-in message fine, but libxpc latches the bootstrap port during libSystem init, before main(); restoring the real port afterwards (sent by the parent in the first Mach message, then task_set_special_port + bootstrap_port) does not heal it. The child's first XPC-touching call hangs forever — concretely IOSurfaceLookupFromMachPort → os_log_create → _os_trace_get_logd_port → bootstrap_look_up2 against a port nobody launchd-serves (sampled stack in the probe header). Metal's shader compiler and VideoToolbox sit behind the same door, so this route cannot host an encoder helper unless the parent proxies the launchd protocol. The probe detects the hang with a 10 s watchdog (_exit(3)) and falls back automatically.
  • The fallback is the design: bootstrap namespace left intact. Child bootstrap_registers a per-spawn name (xarch-ipc-probe.<pid>.<seq>), parent bootstrap_look_ups it — worked first try, every spawn (11 spawns/run). bootstrap_register is deprecated; the production helper should invert it (parent bootstrap_check_in a well-known per-pid name before spawning, child looks it up), same namespace-intact principle.
  • IOSurface transfer + rotation: 100,000/100,000 rotations, zero stale. Each surface validated in the child (3008x1664 'BGRA', rowBytes 12032, alloc 20 021 312) and re-verified per rotation by a child-side Metal compute pass over all 5M pixels (frame index encoded in a pixel block and hashed into every pixel). Notify+verify round-trip p50 0.76 ms / p95 4.2 ms / p99 5.4 ms / max 7.9 ms — the IPC hop itself is noise against a 13.9 ms frame budget (early Gate C signal).
  • Kill/respawn leaks nothing. 5 cycles of SIGKILL mid-stream (frames in flight) → respawn → re-register the same surfaces → 200/200 clean rotations. Parent Mach port count flat at 69 across all 5 cycles; parent-held IOSurfaces unaffected by child death.
  • Clock-domain trap for the helper protocol: mach_absolute_time ticks are not comparable across the Rosetta boundary — the x86_64 side sees a 1/1 ns timebase, native arm64 125/3. First B2 run produced 7.7e14 ms "latencies". Wire timestamps must be normalized (we use nanoseconds); same underlying counter, so deltas are then valid cross-process.
  • Gate B2 — hardware HEVC accepts foreign surfaces. The child wraps each received IOSurface once with CVPixelBufferCreateWithIOSurface (+ BT.709 ShouldPropagate attachments) and feeds the wrapped buffers to a hardware-required HEVC Main LL-RC session (com.apple.videotoolbox.videoencoder.hevc.rtvc, Gate A property set): 300/300 frames accounted, decode-back chroma healthy, and the flat-band pass decodes to BT.709 limited-range with worst band delta 1.0/255 (tolerance 8), 0 drops at 0.2 Mbps (static content). GPU-done → child-VT-callback p50 15.7 ms / p95 19.7 ms / p99 67 ms on band content; noise content reproduces Gate A's native-HEVC floor exactly (ran ~80 Mbps against the 42 Mbps target, shed 143/300) — Gate E bitrate tuning, not a seam problem. Two arm64 LL-RC quirks: PrioritizeEncodingSpeedOverQuality is rejected (-12900), and the UsingHardwareAcceleratedVideoEncoder query itself fails (-12900) — hardware is proven by the RequireHardware create succeeding plus the rtvc encoder ID.

Gate B verdict: PASS — via the bootstrap-namespace route; the special-port route is a documented FAIL (rendezvous works, XPC dies) and the helper design moves to bootstrap-based rendezvous. Gate B2 verdict: PASS — the foreign-surface wrap-once seam is production-viable; hardware HEVC LL-RC encodes parent-written surfaces cross-arch with correct color.

Gates D/E/F — native helper LIVE (2026-08-03)

USB-wired session (adb forward tcp:9943/9944 + client.wired pinned to 127.0.0.1 — the ALVR wired shape; the "searching for streamer" WiFi symptom earlier that night was these same leftover adb forwards squatting the Mac's ports). Helper spawned by the runtime from INSIDE the Wine process via posix_spawn (arm64 native, capsH264=0x3 capsH265=0x3).

  • Gate D (helper H.264, isolates IPC) PASS: enc totalMs p50 24-35ms / p95 28-40ms — same band as the in-process baseline (~35-40ms), 0-2 drops/s. Live IPC cost is invisible, as Gates B/C predicted. Corollary: the "~25ms Rosetta conversion penalty" is mostly VT's conversion contending with the game's GPU load — native VT pays it too under load; the remaining encoder lever is the NV12 Metal kernel in the parent (untested trade), not the arch.
  • Gate E (helper HEVC) PASS — the codec thesis confirmed: 30s dashboard sample (n=1997): total M2P 93.5ms p50 (vs ~114ms H.264/WiFi baseline); Quest decoder 16.6ms p50 (was ~28-32); decoder_queue 1.1ms p50 (was ~30 — collapsed exactly as predicted: HEVC decodes faster than realtime so the queue can't build). decode+queue: ~55-60ms → ~18ms. Note vsync_queue 22.9ms p50 — frames now arrive EARLY and wait for display; frame-pacer tuning could reclaim another ~10-15ms. Encoder unchanged (37.6ms p50); native encoder sheds ~6 frames/s pushing against the 80Mbps cap (its quality floor is higher than Rosetta's — bitrate/preset tuning open).
  • Gate F (SIGKILL helper mid-HEVC-stream) PASS: EOF detected → in-flight frames reclaimed → new helper up and HEVC re-ready 383ms after the kill → codec config re-sent → stream continued; EndFrame never blocked, tracking uninterrupted, "failure 1 this session" budget accounting correct. Same-codec respawn, no codec flip needed. (Cosmetic: "VideoEncoder: Shut down" logs twice during the swap.) Parent-kill and generation-chaos legs deferred to the soak gate at the time — both completed 2026-08-04, see the batch A / soak section below.

Deployed config now ships encoder_process = "auto" + video_codec = "auto". Player verdict: "feels rly good" — and Gate D over USB already felt better tracking-wise (wired network 2.7ms p50 vs WiFi ~6ms spiky, independent of codec).

2026-08-04 — live batch A + 60-min soak (WiFi): validation closed

Full record: evidence/batchA-soak-20260804-results.md + evidence/soak-20260804-samples.csv (local artifacts). Stock ALVR Quest client v20.14.1, WiFi discovery, 3008x1664@72, 80 Mbps, oxrsys 1ca5ef6.

  • WiFi baseline (desk-idle, 231,484 frames): total 106.9 ms p50 / 130.7 p95 — encoder 38.2, network 7.4, decoder 16.7, decoder_queue 1.9, vsync_queue 24.4 (pacing headroom). On-head (1 min head-motion): 102.9 / 130.5 — motion state doesn't move the number. Never compared against the USB 93.5 (different transport).
  • V4 codec/fallback matrix 5/5: h264-forced on the helper; silent auto fallback with the helper absent; spawn-fail ×2 → pin (13 ms failover) with the hands-free 30 s budget retry landing to the second; reconnect no-inherit; codec-churn identity rebuilds.
  • V5 kill legs: SIGKILL→HEVC-ready 398 ms (matches the 383 ms USB gate figure; 515 ms on a mid-soak repeat); parent kill-9 → helper EOF-exit 0.32 s, no orphan, no alvr_shutdown; EndFrame enqueue ≤0.03 ms p95 throughout.
  • V7 soak (60 min): 0 uninduced helper deaths; RSS/fd flat (helper ~48 MB / 20-21 fds per generation); 6 IDR storms limiter-capped at ~1/s; one labeled generation rebuild (515 ms).
  • Found and since fixed (oxrsys a0d37b8 + the wine-vr preflight commits): the configure-time sweep deleting the staged helper, the warn-only auto preflight, the toml trailing-comment parser bug, and the silent auto→in-process downgrade log gap. Still open: the Quest display-standby freeze (client-side experiments next) and the upstream dashboard egui wedge (restart recovers).