Rusty XR Companion

Windows and Android companion utilities for public Quest development workflows.

APK Install And Launch

The companion installs local APKs and launches package targets through ADB. Published Windows installs also include the public Rusty XR Quest camera composite-layer and broker APKs. On first launch, the WPF app auto-loads the bundled catalog from catalogs/rusty-xr-quest-composite-layer.catalog.json, selects the rusty-xr-quest-composite-layer app by default, and resolves bundled APKs under catalogs/apks/.

Install


dotnet run --project src/RustyXr.Companion.Cli -- install --serial <serial> --apk C:\path\app.apk

The WPF app provides the same path through Browse and Install.

Launch

If the package has a normal launcher activity, package-only launch is enough:


dotnet run --project src/RustyXr.Companion.Cli -- launch --serial <serial> --package com.example.questapp

If you know the exact activity:


dotnet run --project src/RustyXr.Companion.Cli -- launch --serial <serial> --package com.example.questapp --activity .MainActivity

Headset Launchers Vs ADB Launching

A normal 2D app running on the headset can launch another installed app when the target exposes a normal front-door launch Activity. That is the right baseline for a headset-local side-loaded APK organizer: it can keep a curated library, show icons/tags/favorites, and launch packages through Android PackageManager APIs. Package visibility still affects discovery on modern Android, and an APK with no public launch Activity may not be launchable from normal app mode.

The Companion uses ADB, so it has a different launch lane. ADB can call Android Activity Manager commands such as am start, and source-workspace tooling can also push/start a Rusty XR shell helper with app_process. That helper runs as Android shell only because the user authorized an external ADB host to start it. A normal headset APK cannot start that shell helper by itself and does not inherit shell privileges.

Use this distinction when designing launch tools:

foreground checks, helper bootstrap, and diagnostics

Companion, a phone companion, or a developer PC

For the public Rusty XR core explanation, see Quest App Launching And ADB Shell Helpers.

Stop


dotnet run --project src/RustyXr.Companion.Cli -- stop --serial <serial> --package com.example.questapp

Catalogs

Catalogs are public JSON metadata, not a requirement to commit APK bytes. The published app payload has a default release catalog for the bundled composite APK. The example catalog under samples/quest-session-kit/ still uses local build-output paths so downstream projects can adapt the format safely from source.

The catalog shape is aligned with Rusty XR core's quest-app-catalog schema. Use schemaVersion: "rusty.xr.quest-app-catalog.v1" for new public catalogs. APK files can be local paths or https:// GitHub Release asset URLs. Remote assets are downloaded into %LOCALAPPDATA%\RustyXrCompanion\apk-cache before install. Use --apk-cache <folder> to override the cache and --refresh-apk-download to force a fresh download.

For local source builds, keep Rusty-XR and Rusty-XR-Companion-Apps as sibling folders. Then run:


dotnet run --project src\RustyXr.Companion.Cli -- workspace guide

The guide reports the expected catalog and APK output paths for the Rusty XR minimal, composite-layer, and broker examples.


dotnet run --project src/RustyXr.Companion.Cli -- catalog list --json

Install and launch a catalog app by ID:


dotnet run --project src/RustyXr.Companion.Cli -- catalog install --path samples\quest-session-kit\apk-catalog.example.json --app rusty-xr-quest-minimal --serial <serial>
dotnet run --project src/RustyXr.Companion.Cli -- catalog launch --path samples\quest-session-kit\apk-catalog.example.json --app rusty-xr-quest-minimal --serial <serial>

Catalog runtime profile values are passed as Activity extras on launch. Use them for public example toggles such as camera enabled/disabled, requested camera size, or whether the app should request MediaProjection streaming. The bundled Rusty XR composite-layer catalog also includes native passthrough hotload profiles and intentional strobe profiles. Strobing profiles can trigger seizures or other adverse reactions in people with photosensitive epilepsy or light-sensitive conditions; launch them only with explicit informed opt-in. For public background on why safety-gated rhythmic light tools exist as a design space, see Brain Candy, a Meta Quest VR experience that presents its own strobing-lights warning and frames rhythmic audio-visual stimulation as non-clinical perception exploration.

Use osc-udp-listener when validating the generic OSC control/sensor adapter. It starts a UDP listener on 0.0.0.0:9000, enables the headset diagnostic HUD, and keeps camera and MediaProjection sources disabled. Use osc-udp-listener-no-overlay when you want the same listener without drawing the HUD.

Run a launch verification pass with a device profile and a diagnostics bundle:


dotnet run --project src/RustyXr.Companion.Cli -- catalog verify --path samples\quest-session-kit\apk-catalog.example.json --app rusty-xr-quest-minimal --serial <serial> --launch --device-profile perf-smoke-test --settle-ms 4000 --out .\artifacts\verify

The verifier records headset snapshots plus process, foreground, gfxinfo, and meminfo signals. Visual confirmation still requires the headset or a cast stream.

For immersive OpenXR examples, a successful launch should also show logcat signals such as OpenXR state READY, OpenXR state FOCUSED, swapchain creation, and recurring frame messages. For camera-driven examples, verify camera session and frame-upload logs before enabling MediaProjection. If the app remains at OpenXR state IDLE, diagnose OpenXR Activity-context and Android lifecycle readiness before debugging media streaming.

For the public camera-driven custom-layer example, first validate the camera renderer without screen streaming:


dotnet run --project src\RustyXr.Companion.Cli -- catalog verify --path samples\quest-session-kit\apk-catalog.example.json --app rusty-xr-quest-composite-layer --serial <serial> --stop-catalog-apps --install --launch --device-profile xr-composite-smoke-test --runtime-profile camera-diagnostic-cpu-copy --settle-ms 7000 --logcat-lines 1000 --out .\artifacts\verify

For examples that stream final-screen MediaProjection frames, add the receiver flags:


dotnet run --project src\RustyXr.Companion.Cli -- catalog verify --path samples\quest-session-kit\apk-catalog.example.json --app rusty-xr-quest-composite-layer --serial <serial> --stop-catalog-apps --install --launch --device-profile xr-composite-smoke-test --runtime-profile media-projection-stream --media-receiver --settle-ms 7000 --logcat-lines 1000 --out .\artifacts\verify

The verifier configures adb reverse, starts a local receiver, and records how many media frames arrived. It cannot accept the headset MediaProjection consent popup for the user.

For OSC probes, launch the listener profile and send a datagram to the Quest LAN IP. OSC is UDP, so this path does not use adb reverse:


dotnet run --project src\RustyXr.Companion.Cli -- catalog verify --path samples\quest-session-kit\apk-catalog.example.json --app rusty-xr-quest-composite-layer --serial <serial> --stop-catalog-apps --install --launch --device-profile xr-composite-smoke-test --runtime-profile osc-udp-listener --settle-ms 5000 --logcat-lines 1000 --out .\artifacts\verify
dotnet run --project src\RustyXr.Companion.Cli -- osc send --host <quest-lan-ip> --port 9000 --address /rusty-xr/probe --arg string:hello

The diagnostic HUD can also be toggled on a running compatible APK through a runtime-profile launch extra such as rustyxr.diagnosticHudCommand=toggle.

For the public broker proof-of-concept, use the sibling Rusty XR source catalog after building examples\quest-broker-apk. The broker-latency-websocket-lsl profile starts the localhost HTTP/WebSocket API and forwards samples to LSL only when the APK was built with a compliant Android liblsl.so. The broker-osc-drive-ingress profile listens for /rusty-xr/drive/radius on UDP port 9000 and rebroadcasts accepted values to localhost WebSocket clients:


dotnet run --project src\RustyXr.Companion.Cli -- catalog verify --path ..\Rusty-XR\examples\quest-broker-apk\catalog\rusty-xr-quest-broker.catalog.json --app rusty-xr-quest-broker --serial <serial> --stop-catalog-apps --install --launch --device-profile broker-smoke-test --runtime-profile broker-osc-drive-ingress --settle-ms 5000 --logcat-lines 1000 --out .\artifacts\verify
dotnet run --project src\RustyXr.Companion.Cli -- osc send --host <quest-lan-ip> --port 9000 --address /rusty-xr/drive/radius --arg float:0.75

The sibling Rusty XR source catalog also exposes a visible broker-console target for headset-local inspection. Use broker-console-system-page when the launch should open the broker console directly on the System page:


dotnet run --project src\RustyXr.Companion.Cli -- catalog launch --path ..\Rusty-XR\examples\quest-broker-apk\catalog\rusty-xr-quest-broker.catalog.json --app rusty-xr-quest-broker-console --serial <serial> --runtime-profile broker-console-system-page

That OSC ingress path has been validated with The Big Red Button Institute, the public Unity Quest example for comparing direct Unity OSC/BLE input against broker-routed stream events.

For passthrough style hotload, launch a passthrough profile once and then send another catalog profile to the running activity:


dotnet run --project src\RustyXr.Companion.Cli -- catalog launch --path catalogs\rusty-xr-quest-composite-layer.catalog.json --app rusty-xr-quest-composite-layer --serial <serial> --runtime-profile passthrough-underlay-hotload-neutral
dotnet run --project src\RustyXr.Companion.Cli -- catalog launch --path catalogs\rusty-xr-quest-composite-layer.catalog.json --app rusty-xr-quest-composite-layer --serial <serial> --runtime-profile passthrough-underlay-hotload-lut-opponent

The full-field strobe profiles are full-field-red-black-flicker-10hz, full-field-red-black-flicker-40hz, and full-field-red-black-flicker-60hz. They request 120 Hz through the app's OpenXR refresh-rate path. Treat the request as advisory until logcat confirms activeDisplayRefreshHz, observedOpenXrFps, and full-field flicker stats. The passthrough LUT strobe profiles use the same frequency suffixes under passthrough-underlay-hotload-lut-flicker-* and should be treated with the same warning.

The camera-gpu-buffer-probe runtime profile requests the public GPU-buffer path with CPU fallback disabled. In the current public example that path imports and samples Camera2 PRIVATE hardware buffers, then logs the remaining projection blockers instead of treating the diagnostic surface as a stereo projection renderer.

Run camera-source-diagnostics before treating a Quest runtime as mono-only. When --out is enabled, the verifier pulls the app-private camera-source diagnostics payload into camera-source-diagnostics.json. The camera-stereo-gpu-composite profile requires logcat to show real paired left/right GPU buffers, stereoLayout=Separate, activeTier=gpu-projected, alignedProjection=true, no CPU upload, OpenXR FOCUSED, a platform or public estimated-profile pose source, and a logged cameraTextureTransform with orientationCheck=true. The accepted public reference profile also passes visualReleaseAccepted=true with the manual acceptance token after headset inspection confirmed upright feed, correct source-eye mapping, stable head-motion projection, and a visible camera-driven border. New device/runtime variants should rerun diagnostics and keep that manual visual gate closed until the same conditions are inspected again.

For renderer-parity checks, use camera-stereo-gpu-composite-fast075. It keeps the same direct Camera2 stereo source, projection geometry, and GPU-buffer import contract, but selects the fast public raw-projection shader at rustyxr.xrRenderScale=0.75. Use the xr-composite-comparison-level-4 device profile when comparing this lane with broker H.264 Q2Q profiles so CPU/GPU levels are declared in the verification bundle.

For projection A/B checks, use camera-stereo-gpu-composite-quad-surface. It keeps the same GPU-buffer stereo camera path and visual gate, but passes rustyxr.cameraProjectionMode=quad-surface so the renderer reconstructs the content-surface UV that a head-anchored quad would rasterize before camera projection. It also passes rustyxr.cameraColorMode=external-rgb plus a small public contrast/brightness lift. Compare it with the default display-screen-homography mode when investigating projection geometry, sampler, tone, or color differences. This profile is intentionally not marked as the final performance or color reference; keep visualReleaseAccepted=false until a headset/cast inspection confirms the remaining mode-specific differences are acceptable.

Camera delivery cadence is controlled through ordinary catalog launch extras. The public Quest composite sample accepts rustyxr.cameraTargetFps or rustyxr.cameraFpsMin / rustyxr.cameraFpsMax and forwards the nearest supported value to Camera2 CONTROL_AE_TARGET_FPS_RANGE. The companion does not treat that as a hard frame-rate guarantee; inspect logcat lines beginning with Camera2 AE FPS range and Camera2 delivery stats to compare requested, applied, and observed camera-buffer cadence. The current Quest 3S stereo Camera2 validation found that 30-30 delivered about 29.85 FPS, while 60-60 was accepted by Camera2 but delivered about 49.9 FPS in the paired 1280x1280 GPU-buffer profile. This matches Android's CONTROL_AE_TARGET_FPS_RANGE warning that actual max frame rate can still be capped by stream min-frame-duration and runtime constraints.

If the headset shows Select view you want to share, select Entire view and press Share in the headset. The companion intentionally treats that panel as manual because current Quest system UI does not reliably accept shell-driven selection or Share-button taps.

Use --stop-catalog-apps when validating immersive examples so another app from the same catalog cannot keep the headset compositor busy. Use --logcat-lines <n> to save a scoped logcat.txt beside verification.json and verification.md; logcat is cleared first unless --keep-logcat is also passed.