Skip to content

rxdemo Android USB-fd path + autonomous Quest RX-ring harness (issue #330)#333

Merged
josephnef merged 1 commit into
masterfrom
issue-330-rx-ring-starvation
Jul 24, 2026
Merged

rxdemo Android USB-fd path + autonomous Quest RX-ring harness (issue #330)#333
josephnef merged 1 commit into
masterfrom
issue-330-rx-ring-starvation

Conversation

@josephnef

Copy link
Copy Markdown
Collaborator

What

Makes rxdemo runnable on Android and adds a fully autonomous Quest 3 harness for the issue #330 async-RX-ring residual-loss investigation.

Android USB-fd path (the core fix)

The RX demo could never run on Android: libusb's device enumeration is SELinux-denied under the untrusted_app domain, and only the TX demo had the wrap-a-pre-opened-fd path. Ported into examples/rx/main.cpp:

  • argv[1] = a numeric USB fd; LIBUSB_OPTION_NO_DEVICE_DISCOVERY + WEAK_AUTHORITY before libusb_init; libusb_wrap_sys_device; skip the reset/reopen (a wrapped app-owned fd can't be re-enumerated).
  • CMakeLists.txt links Android log (was previously unlinked).

Autonomous Quest harness (tests/quest_apk/)

A minimal auto-grant APK, hand-built with aapt/d8/apksigner (no gradle), modelled on how PixelPilot avoids the USB permission dialog:

  • a USB_DEVICE_ATTACHED intent-filter + device_filter auto-grants USB permission on attach — no dialog;
  • a JNI fork()+execve() helper hands the UsbDeviceConnection fd to rxdemo (Android's ProcessBuilder closes fds ≥3 before exec, so the fd must be inherited across an in-process fork).

Driven headlessly from the desktop via am start extras — no in-headset interaction. tests/quest_rxq_run.sh + quest_rxq_analyze.py run the per-mode matrix; tests/build_android.sh cross-builds the arm64 bundle; runbook in tests/quest_rxq.md.

Instrumentation + modes (all off by default; async byte-unchanged when unset)

rx.ring telemetry (posted-URB depth, cb_max_us consumer stall, resubmit_fail, completions), rx.seq per-frame delivery, DEVOURER_RX_MODE {async|sync|reorder-pool|spsc-fat} with a spare-buffer pool, and a demo-side sink-spin / TX keyframe-burst stimulus. Plus the desktop constrained-host repro (tests/rxq_starve.sh, rxq_analyze.py, rxq_preempt.c).

Findings

Documented in docs/experiments/issue-330-rx-ring-starvation.md. The ~1–2% residual is not attributable to ring-servicing strategy on either the desktop or a genuinely constrained XR2 host: at load the ring absorbs the (real, large — up to ~15 ms cb_max_us on the Quest) consumer stalls, and delivery is RF- or consumer-throughput-limited. Across three load regimes async/reorder-pool/spsc-fat tie within run-to-run noise (±1.5 pts, no consistent ordering). The shippable product is the rx.ring telemetry as a field diagnostic a reporter runs on their own phone.

Test

  • Native build clean; ctest 37/37 pass.
  • async path unchanged when the new knobs are unset.
  • On-device: full RTL8812AU bring-up + RX ring live on a Quest 3, driven entirely over adb.

🤖 Generated with Claude Code

…330)

The RX demo could never run on Android: libusb's device enumeration is
SELinux-denied under the untrusted_app domain, and only the TX demo had the
wrap-a-pre-opened-fd path. Port it into examples/rx/main.cpp — argv[1] = a
numeric USB fd, LIBUSB_OPTION_NO_DEVICE_DISCOVERY + WEAK_AUTHORITY before
libusb_init, libusb_wrap_sys_device, and skip the reset/reopen (a wrapped
app-owned fd can't be re-enumerated). CMakeLists links Android `log`.

Autonomous Quest 3 test vehicle (tests/quest_apk/, hand-built with
aapt/d8/apksigner — no gradle): a USB_DEVICE_ATTACHED intent-filter +
device_filter auto-grants USB permission with no dialog (the PixelPilot
mechanism), and a JNI fork()+execve() helper hands the UsbDeviceConnection fd
to rxdemo — Android's ProcessBuilder closes fds >= 3 before exec, so the fd
must be inherited across an in-process fork, not a spawned child. Driven
headlessly from the desktop via `am start` extras; no in-headset interaction.

Instrumentation the harness drives (all off by default; the async path is
byte-unchanged when unset): rx.ring telemetry (posted-URB depth, cb_max_us
consumer stall, resubmit_fail, completions), rx.seq per-frame delivery,
DEVOURER_RX_MODE {async|sync|reorder-pool|spsc-fat} with a spare-buffer pool,
and a demo-side sink-spin / TX keyframe-burst stimulus.

Harnesses: tests/build_android.sh (NDK cross-build), tests/quest_rxq_run.sh +
quest_rxq_analyze.py (per-mode delivery from the 802.11 seq_num + ring
telemetry), tests/rxq_starve.sh + rxq_analyze.py + rxq_preempt.c (the
desktop constrained-host repro). Findings in
docs/experiments/issue-330-rx-ring-starvation.md: the residual is not
attributable to ring-servicing strategy on either the desktop or the Quest —
at load the ring absorbs the (real, large: up to ~15 ms cb_max_us on the
Quest) consumer stalls, and loss is RF- or consumer-throughput-limited. The
shippable product is the rx.ring telemetry as a field diagnostic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@josephnef
josephnef merged commit e9b22a7 into master Jul 24, 2026
18 checks passed
@josephnef
josephnef deleted the issue-330-rx-ring-starvation branch July 24, 2026 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant