* Add VITURE glasses support: hardware control, 3D, head tracking Glasses get their own controller over the MCU (permission, 120 Hz default, brightness, volume, sunblock, 2D/3D) and the game moves onto them when plugged in. The IMU drives Cardboard head tracking and the SteamVR driver. 3D falls back to one eye's view on phones that can't send the 3840x1080 signal. Options open from a glasses button in game and on the Steam page. * Drop the glasses settings like Steam's: Play-style button, Settings lip, tabs * Hang the glasses Settings lip over the top of the button * Space glasses MCU commands so the IMU rate sticks, fit fixed resolutions to the glasses * Keep Steam alive with SteamVR registered, run SteamVR at the glasses' rate, keep the glasses lip on top * Fit VR games on the glasses: full view, seated origin at the head, pad hands reach with the d-pad * Let the pad hands reach a metre out so buttons with deep colliders press * Show every VR frame in order at 120 fps: wait for SteamVR's GPU work, present only finished copies, skip the grace on idle shared counters * Glasses: SteamVR starts and stops with them, closer view with a field of view setting, Recenter on the pad * Glasses: SteamVR field of view defaults to 90° * VR pad hands: sticks move and aim the hands, left stick click switches the sticks to the game, d-pad pulls a held hand * Glasses: Play SteamVR beside the glasses button instead of starting SteamVR with them, pad hands drawn as Index controllers and off while other VR controllers are connected, glasses sheet tabs sized like Steam's * Track glasses mode, refresh-aware glasses window, align Play SteamVR with Play Steam * Fix 3D on the glasses: full-res SteamVR presenter, revert VR 3D at session end, coalesce vsync ticks, no session restart after end * Address review: glasses output after installs and session end, VR revert retry, display fallback, presenter swapchain retry, agent hex checks, drawer bumpers, glasses translations, docs * Harden VITURE stereo negotiation and output refresh tracking * Localize stereo recovery feedback and record Astra reconnect tests * Improve test failure diagnostics and refresh Astra firmware findings * Handle VITURE docks separately and reject truncated USB frames * Glasses: remember a 3D fallback per model, docks as display only, VR timing fixes - A 3D attempt that ends at the 640x480 fallback is remembered for that glasses model, so Play SteamVR no longer blacks out the glasses after every restart; Retry clears it and "3D for VR games" stays reachable - A VITURE Mobile Dock on its own no longer counts as glasses; through a dock Play SteamVR is hidden and the sheet shows the output and why there is no control - HDR is probed on the glasses when the session starts there - Glasses frames with a mismatched checksum are marked in the status frame log - SteamVR layer and driver vblank counters stay monotonic across refresh changes; one-eye render scale applies from activation and on mono/3D switches - Compositor binds announce the refresh clients were last told; the presenter retries a failed first swapchain - Dead strings and helpers removed; docs updated * Glasses: drop USB frames with a bad checksum * Glasses: count frames dropped for a bad checksum * Feat/viture xr (#907) * Add RayNeo Air and GT support to the XR glasses pipeline * Add RayNeo Air and GT support to the XR glasses pipeline * Checkpoint glasses tracking, calibration, and SteamVR launch integration * Checkpoint glasses tracking, calibration, and SteamVR launch integration * Add XREAL 1S support and correct GT and Pro 2 tracking * Add XREAL 1S support and correct GT and Pro 2 tracking * Fix mirrored glasses tracking and recover SteamVR after device loss * Fix mirrored glasses tracking and recover SteamVR after device loss * Document confirmed XREAL 1S brightness control * Document confirmed XREAL 1S brightness control * Fix SteamVR glasses defaults and rendering controls * Fix SteamVR glasses defaults and rendering controls * chore: keep only the 3 newest session log folders (#852) * chore: add agent comment rules (#748) Co-authored-by: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> * Fix SteamVR polling, VITURE checksums, and fence tracking Keep manual SteamVR launches polled, discard corrupt VITURE packets, and retire Vulkan fences out of order without blocking submissions at a fixed capacity. Preserve the contiguous completion watermark used by the presenter. Fix optional Windows launcher configuration and audited helper staging. Add Android, Windows, and native tracker regression coverage. * Fix SteamVR polling, VITURE checksums, and fence tracking Keep manual SteamVR launches polled, discard corrupt VITURE packets, and retire Vulkan fences out of order without blocking submissions at a fixed capacity. Preserve the contiguous completion watermark used by the presenter. Fix optional Windows launcher configuration and audited helper staging. Add Android, Windows, and native tracker regression coverage. * fix: wireless ADB page names the child-process switch the way Android shows it (#848) * l10n: refresh stale and untranslated strings in es, fr, ko, ru (#850) * l10n: refresh stale and untranslated strings in es, fr, ko, ru - mode_added_art_hint: now mentions Steam then SteamGridDB (all four locales). - wincomp_needs_installer / wincomp_installers_note: es and ko still described the old "coming soon" behaviour; align with the current English text. - summary_remove_game: was still English in all four locales. Machine-assisted translation; native-speaker review is welcome. * l10n(es): say registro, not grabación, for the missing installer record --------- Co-authored-by: ovurrsl <ovurrsl@users.noreply.github.com> * fix: a failed cloud save recovery no longer ends the app (#846) The cloud-recovery thread started at every app start had nothing to catch an error, so one went to CrashHandler, which halts the process with status 2. In unit tests the thread can outlive the test that started it, fail on the torn-down preferences, and take the whole test run down. The error is now logged and the thread ends. Co-authored-by: Kirk <12691869+KirkPatrickJunsay@users.noreply.github.com> * fix: an interrupted Linux runtime download resumes instead of starting again (#847) * fix: an interrupted Linux runtime download resumes instead of starting again installOnce() asked the downloader to resume but always deleted the archive in its finally block, so the next attempt began at zero. The archive is now named after the release version and checksum, a failed download keeps what it fetched, and archives of other releases (and the old fixed name) are removed. A checksum or extraction failure still deletes the file. * test: Downloader resumes a cut-off download from the bytes it kept Starts a socket server that promises the whole file, hangs up part way and then answers a Range request, and checks that the second downloadFile call resumes with the right offset and ends with identical bytes. * fix: a resumed download discards a 206 that does not start where the kept bytes end Downloader appended every 206 response to the partial file without looking at Content-Range. A server or proxy that answers a Range request with a different offset would leave a corrupt file. Check that the range starts at the kept length; otherwise delete the partial file and start again without a Range header. Adds a test with a server that answers the Range request from offset 0. --------- Co-authored-by: ovurrsl <ovurrsl@users.noreply.github.com> * l10n: German, French, Spanish, Italian, Portuguese and Russian for the zRAM, reset-settings and store free-space strings (#849) * l10n: German, French, Spanish, Italian, Portuguese and Russian for the zRAM, reset-settings and store free-space strings Adds the strings values-de, values-it and values-pt were missing (14 each) and the six that values-fr, values-es and values-ru were missing. Machine-assisted drafts that follow each file's existing register and terms; native review is welcome. * l10n: correct missing active swap translations --------- Co-authored-by: ovurrsl <ovurrsl@users.noreply.github.com> Co-authored-by: xXJsonDeruloXx <danielhimebauch@gmail.com> * fix: droiddeckctl reaches variant-package builds (recover #842) (#854) fix: droiddeckctl reaches builds shipped under a variant package The provider authority and the start Activity's component were fixed to com.droiddeck.launcher, so droiddeckctl could not talk to the antutu, ludashi or pubg builds from tools/release/variants.txt. DROIDDECK_PACKAGE now picks the package; the Activity's class name stays the same in every variant. Co-authored-by: angelrenefm <17342133+angelrenefm@users.noreply.github.com> * feat: droiddeckctl reports on-screen pacing (recover #843) (#855) feat: droiddeckctl pacing reports the frames that reached the screen Reads SurfaceFlinger's timestats for the game's layer (or the session's SurfaceView) over a capture: frames on screen, fps on screen, the present-to-present histogram and how many intervals were off the usual cadence. Records split by a refresh-rate change are summed. A game's own fps counter counts frames handed over, so it can't show dropped or doubled frames. Co-authored-by: angelrenefm <17342133+angelrenefm@users.noreply.github.com> * fix: Steam can restart more than twice in a session (#754) * fix: Steam can restart more than twice in a session Every exit 42 counted toward the cap that stops an update which will not install, and the count was never cleared, so the third restart of a session ended it however far apart they were. A client that ran for a minute before asking is not that loop: the count starts again. * fix: measure the client's run time as it exits The time spent collecting logs and checking FEX after the client exits counted toward its run, so a slow cleanup could make a quick exit 42 look like a long one and clear the count. --------- Co-authored-by: Kirk <12691869+KirkPatrickJunsay@users.noreply.github.com> Co-authored-by: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> * fix: the front end keeps its page when the Fold is folded or unfolded (#832) * fix: the front end keeps its page when the Fold is folded or unfolded MainActivity, FileManagerActivity, FilePickerActivity and StoreLoginActivity did not declare the screen-size configuration changes, so folding or unfolding a foldable recreated them and dropped their in-memory state. They now handle orientation, screenSize, screenLayout, smallestScreenSize and density themselves, like SessionActivity already does. uiMode is left out on purpose. * fix: store login keeps rebuilding its toolbar when the display density changes StoreLoginActivity builds its toolbar in code with dp-to-pixel values taken at creation time and has no onConfigurationChanged. It no longer declares density in configChanges, so a density change recreates it as before this PR. Size and layout changes are still handled in place. --------- Co-authored-by: ovurrsl <ovurrsl@users.noreply.github.com> * Release pointer buttons without replaying stale motion (#803) * Fix cursor warping on native touch down * Preserve input cleanup without replaying stale pointer motion --------- Co-authored-by: siyuan.qsy <siyuan.qsy@alibaba-inc.com> * l10n: translate the strings added since #284 into zh-CN, zh-HK, zh-TW and ja (#539) * l10n: translate the 184 strings added since #284 (zh-CN/HK/TW, ja) The stores, cloud saves, Windows components, Steam touch, desktop removal and background-downloads strings landed after #284's translation snapshot, so every one of them fell back to English. Translate all 184 into Simplified Chinese, Traditional Chinese (Taiwan and Hong Kong) and Japanese, keeping each locale file's own vocabulary (工作階段/會話, 解除安裝/卸載, 元件/組件, 設定). Also fix four Hong Kong strings that kept the simplified 着色器. * l10n(zh): fix stale and ambiguous strings found in review Severe: summary_remove_game was untranslated English in all three files; mode_added_art_hint, wincomp_installers_note and wincomp_needs_installer still described the pre-#480/#492 English. Also: distinguish the Setup wizard from Android system settings in zh-HK/zh-TW (初始設定 / 系統設定 / Android 設定), unify library as 收藏庫, component as 組件 in zh-HK, session as 會話 in zh-HK, account as 帳戶 in zh-HK, Trending as 趋势/趨勢, Forget as 清除, reword the re-applied-at-launch, action-failed and already-in-Games strings, and play as 執行/開啟遊戲 in zh-TW/zh-HK. * l10n(ja): fix stale and ambiguous strings found in review --------- Co-authored-by: Dantalian <Dantalian-dev@users.noreply.github.com> Co-authored-by: xXJsonDeruloXx <danielhimebauch@gmail.com> * Rebuild and verify Windows VR runtime components * Rebuild and verify Windows VR runtime components * Add top, bottom, and centered picture positions for Steam sessions (#871) * fix: dependency scan ignores folders above the game folder (#859) * DroidDeck-Proton: get releases from the Droid-Deck org repo DroidDeck-Proton is now built and released on Droid-Deck/DroidDeck-Proton (11-9.1 is Latest there), so the Components page, the update check and the guest installer read releases from the org repo instead of The412Banner's. * feat: low-latency controller and touch toggles in the session drawer (#808) * fix: SteamGridDB lookups no longer stick as "no match" after a failure; add a search to the art chooser (#876) * feat: Steam home wall fills from the signed-in library instead of store picks (#878) * test: cover GPU family and support classification (#880) * Fix local builds on Linux failing after the first Comet build (#866) * feat: sort the store library and installed tabs, and switch between card and list views (#798) * feat: three-finger tap toggles Steam's on-screen keyboard in game mode (#782) * fix: keep Quest-patched Turnip as the auto default (#853) * Fix deploy_local.sh and droiddeckctl not finding a phone connected over wireless debugging (#864) * fix: keep proot sessions alive when the kernel loses fork events (#457) * fix(quest): correct desktop and Steam display corruption across sessions (#733) fix(quest): stabilize desktop and Steam layer presentation Keep Quest SDR scenes and the black backdrop off the Android Vulkan swapchain regardless of session order or HDR preferences. Only report the first frame after client content is submitted. Add backdrop lifecycle and first-frame regression coverage. Record the user's successful Quest 3 Test 2 result. Co-authored-by: xXJsonDeruloXx <danielhimebauch@gmail.com> * fix: keep SteamVR syscall hooks and report Steam crashes (#905) * fix: preserve SteamVR syscall hooks * fix: report Steam crashes through gamescope * fix: honor glasses pacing and resolution in SteamVR launches --------- Co-authored-by: connor g <connorsspam1992@gmail.com> Co-authored-by: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Co-authored-by: Yassine Gherbi <y.gherbi.dev@gmail.com> Co-authored-by: Resul Övür <92828257+ovurrsl@users.noreply.github.com> Co-authored-by: ovurrsl <ovurrsl@users.noreply.github.com> Co-authored-by: Kirk <kirtoy.junsay@gmail.com> Co-authored-by: Kirk <12691869+KirkPatrickJunsay@users.noreply.github.com> Co-authored-by: xXJsonDeruloXx <danielhimebauch@gmail.com> Co-authored-by: angelrenefm <17342133+angelrenefm@users.noreply.github.com> Co-authored-by: asdf17128 <asdf17128@gmail.com> Co-authored-by: siyuan.qsy <siyuan.qsy@alibaba-inc.com> Co-authored-by: Dantalian <wsu091227@qq.com> Co-authored-by: Dantalian <Dantalian-dev@users.noreply.github.com> Co-authored-by: The412Banner <the412banner@users.noreply.github.com> Co-authored-by: The412Banner <205237651+The412Banner@users.noreply.github.com> Co-authored-by: FX1922 <120828496+FX192@users.noreply.github.com> Co-authored-by: Muaz Kadan <32595066+muazkadan@users.noreply.github.com> Co-authored-by: kingokksa <47888451+kingokksa@users.noreply.github.com> Co-authored-by: Jamie Schenker <jam1esch3nk3r@gmail.com> Co-authored-by: Jagger15 <jagger2333@live.com> * Glasses: keep a pending 2D restore and the SteamVR recovery offer, show Play SteamVR only with a picture * Translate the glasses calibration, XREAL and SteamVR recovery strings * SteamVR recovery: give a finished teardown time to collect logs before restarting * Credit the projects the XR glasses support builds on * Joy-Cons: SteamVR hands from their gyro while SteamVR runs, one combined pad otherwise * Add experimental Luma tracking and stabilize SteamVR launches Add native Luma Ultra spatial tracking, optical controllers, and stereo calibration with DroidDeck styling, imperial/metric measurements, and pending recenter recovery. Include the pinned hand model and calibration board. Keep live guest and SDK files outside Android's evictable cache. Filter the Adreno 840 sysmem override in the shared SteamVR helper, serialize timeline mirroring, retain invalid optical controllers without event floods, and prevent an unprivileged scheduling fallback from lowering compositor RTTIME. Validation: latest APK build and runtime freshness checks pass; 247 affected glasses/calibration tests pass. Native VR, helper, and packaging regressions pass. Two earlier phone library launches sustained 60 fps for 308 and 188 seconds without recovery or restart. The newer calibration APK is installed and checksum-verified; calibrated-hand and sustained gameplay acceptance remain pending. One existing Android test requires Windows symlink privileges. * Headsets: settings badge on Play SteamVR instead of a top-right button, both shown only once connected * Hand tracking: camera-tracked Joy-Cons and bare hands in SteamVR, drawn as white gloves - Joy-Con movement from the accelerometer (report reordering, per-Joy-Con calibration, zero-velocity updates) - MediaPipe pose and hand landmarks from the device camera, placement check, per-hand Kalman filter with the Joy-Con - No controller connected: bare camera hands with Quest-style gestures, 1€ filtered, rendered as a white glove model built from a distance field - SteamVR safe mode cleared before each start so one crash no longer blocks the driver * Improve Luma Ultra tracking and isolate device-specific controls Refine stereo hand and Joy-Con tracking, gesture release, calibration, recovery UI, diagnostics, display modes, and compositor handling. Keep Ultra controls and input routing behind a live SDK-confirmed Carina connection; clear retained spatial mode when other glasses connect. Add optional UVC transport with pinned dependencies and packaged notices. Validation: 456 Android tests, ARM64 VR build and native regressions, 47 tooling tests, SDK/UVC-enabled and disabled builds, and 150 packaged runtime checksums. Two shader compilation tests skipped because the compiler is unavailable. Hardware interaction gates remain experimental. * Delay Luma Ultra 6DoF until SteamVR settles Suspend spatial SDK startup while SteamVR launches, then restore it two seconds after RUNNING is confirmed. Preserve saved tracking preferences and automatically recenter same-device restoration, including cancellation and early stop. Discard superseded launch requests and status replies after session teardown. Validation: 128 focused JVM tests passed on the isolated commit snapshot; 129 passed with the current local hand-tracking work. Debug build and runtime checks passed, and the reviewed APK was installed and hash-verified on the phone. * Clarify glasses and phone camera tracking options Separate Luma Ultra camera controls from phone-camera tracking so the two hand-tracking switches have clear sources. Label bare-hand controllers and Joy-Con position tracking explicitly, explain physical Joy-Con inputs and camera priority, and update the camera prompt and calibration guidance. Use fresh phone section and toggle labels so existing translations do not hide the distinction. Preserve tracking behavior and saved preferences. Validation: 39 existing UI tests, debug build, and packaged runtime verification passed. Reviewed changes and installed the APK on the connected phone. * Fix tracking handoff and shared runtime regressions Release spatial ownership and controller overrides when output returns to the phone. Preserve USB-loss recovery, keep Vulkan submissions asynchronous, clean persistent guest scratch on runtime removal, and clarify the 6DoF head-tracking control. * Apply smart-glasses virtual screens across DroidDeck menus * Finalize Luma hand baseline and document smart-glasses support Keep raw grayscale-to-RGB Full hand inference, wrist-only Joy-Con reconstruction, bounded visual holds, and the dead-zone rollback. Retain opt-in steady aim, recenter controls, reproducible Lite models, and the JPEG-padding fix. Remove unsuccessful benchmark-only and USB transport experiments. Document the cross-brand 3DoF matrix, Luma Ultra 6DoF, SDK requirements, and app-wide screen behavior. * Fix executable permission for SteamVR launcher * Address hand tracking and VR review findings * Bound stereo recovery test image comparison memory * Localize smart-glasses setup and tracking diagnostics Move calibration controls, status messages, validation errors, distance labels, and printed-board instructions into Android string resources. Add and update machine-assisted translations in all 32 supported non-English locale catalogs, with corrections for recentering, hand tracking, product names, and measurements. Keep print instructions within the page using text layout that supports wrapping and right-to-left text. Correct the Lite model toggle description to reflect its Lite palm detector and Lite landmark model. Add locale coverage, format-argument, line-break, and measurement checks, plus French and Arabic calibration UI tests. Preserve tracking calculations and the shared runtime storage behavior. Validation: Android resource and Kotlin compilation; 3 localization checks; 68 focused tests; 1,597 JVM tests at a 512 MB heap; French and Arabic UI renders. * Correct smart-glasses translations from PR review Fix the 42 localization findings covering hand and head terminology, disabled states, calibration dimensions, camera visibility, capture rejection, and menu references. Correct the repeated capture-error ambiguity in related locales and clarify its English source. Keep Romanian setup instructions consistent with the existing informal voice. Extend localization measurement validation to distance hints and retry guidance. Verify the recovery screen's palette-dependent text color with one row-sized pixel buffer and guaranteed bitmap cleanup. Validation: Android resource and Kotlin compilation; 3 localization checks across all supported locales; 68 focused JVM tests at a 512 MB heap. * Work around SteamVR device loss with Adreno 840 Turnip v0.2.0 * Withdraw ineffective SteamVR flushall workaround --------- Co-authored-by: maxjivi05 <maxjividen@gmail.com> Co-authored-by: MaxsTechReview <6025299+maxjivi05@users.noreply.github.com> Co-authored-by: connor g <connorsspam1992@gmail.com> Co-authored-by: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Co-authored-by: Yassine Gherbi <y.gherbi.dev@gmail.com> Co-authored-by: Resul Övür <92828257+ovurrsl@users.noreply.github.com> Co-authored-by: ovurrsl <ovurrsl@users.noreply.github.com> Co-authored-by: Kirk <kirtoy.junsay@gmail.com> Co-authored-by: Kirk <12691869+KirkPatrickJunsay@users.noreply.github.com> Co-authored-by: xXJsonDeruloXx <danielhimebauch@gmail.com> Co-authored-by: angelrenefm <17342133+angelrenefm@users.noreply.github.com> Co-authored-by: asdf17128 <asdf17128@gmail.com> Co-authored-by: siyuan.qsy <siyuan.qsy@alibaba-inc.com> Co-authored-by: Dantalian <wsu091227@qq.com> Co-authored-by: Dantalian <Dantalian-dev@users.noreply.github.com> Co-authored-by: The412Banner <the412banner@users.noreply.github.com> Co-authored-by: The412Banner <205237651+The412Banner@users.noreply.github.com> Co-authored-by: FX1922 <120828496+FX192@users.noreply.github.com> Co-authored-by: Muaz Kadan <32595066+muazkadan@users.noreply.github.com> Co-authored-by: kingokksa <47888451+kingokksa@users.noreply.github.com> Co-authored-by: Jamie Schenker <jam1esch3nk3r@gmail.com> Co-authored-by: Jagger15 <jagger2333@live.com>
26 KiB
Luma Ultra spatial tracking
For supported glasses across brands, feature levels, and app-wide setup, see the smart-glasses support matrix.
This is an experimental implementation of native Luma Ultra head tracking, positional virtual screens, and optical SteamVR controllers. Positional tracking, bare-hand controllers, and Luma Joy-Con tracking all default to off and are grouped under Experimental. Combined release acceptance remains pending until the calibration, accuracy, latency, gameplay, and sustained performance targets below have been measured on hardware.
Build and setup
Luma Ultra now includes a shared starting calibration exported from the user's verified Fold7 profile on 2026-10-16. The bundled calibration/luma-ultra-640x480.json contains the original lens, stereo, and camera-to-head geometry without the source glasses' serial identifier. It applies only to the SDK-confirmed Luma Ultra primary left0/right0 pair at 640 by 480. Matching saved device calibration takes precedence, including an unfinished personal fit that still requires distance validation. Missing, corrupt, or foreign profiles fall back to the model base; unknown serials can also use the base.
The base enables experimental hand tracking without requiring every user to repeat board collection. It is not marked individually verified on other units. Tracking and setup show Shared Luma Ultra calibration while it is in use; diagnostics distinguish calibrationSource (luma-ultra-base, device, or none) from calibrationVerified. The existing calibrated availability field is true for either the base or a verified personal profile. The following board steps are optional refinement for a new device using the base; a newly calculated personal profile still needs the unchanged distance check before use. A successful distance check saves the profile for the connected identified glasses. Source validation does not establish manufacturing consistency or aiming accuracy across other units.
Use the existing optional droiddeck.vitureSdk local property with the external VITURE Android SDK 2.4.0. No vendor headers or libraries are checked into the repository. -PvitureSdkDir= explicitly builds without the SDK. The ordinary runtime freshness checks remain enabled. Rebuild the ARM64 VR components with tools/vr/build_windows.py; its manifest includes spatialpose_shm.h.
The hand pipeline pins MediaPipe Tasks Vision 1.0.0, OpenCV 4.13.0, and the model and SHA-256 recorded in app/src/main/assets/models/hand_landmarker.provenance.json. Both Gradle and the inference worker verify the model checksum. CPU inference, the full model, concurrent camera inference, scheduled hand discovery, and automatic input-rate adjustment are the defaults, including after app restarts, reconnects, and provider restarts. Show diagnostics → GPU hand inference temporarily enables GPU until the next tracking restart; GPU remains experimental because SteamVR restarts and Vulkan device loss were observed during testing. Diagnostics report the selection as handGpuEnabled and the initialized detectors as handInferenceBackend; not initialized means detector startup has not completed. Sustained performance acceptance remains unresolved.
Hand input starts by copying 500 permille of stereo callbacks, approximately 12.5 pairs per second at the SDK's documented 25 Hz camera rate. On CPU, automatic adjustment can raise this to 600 or 800 permille (about 15 or 20 Hz) only after eight consecutive valid observations have sufficient processing and freshness headroom. Promotion requires average processing at most 55 ms and observation age at most 110 ms for 15 Hz, then at most 40 ms and 90 ms for 20 Hz. A moving average allows occasional scheduled discovery frames while preserving a sustained processing budget. An average above the selected period reduces the rate; any single processing cost of 80 ms or observation age of 140 ms immediately returns it to 500 permille. Tracking failure and detector resets also return it to 500. This policy requests more input only when processing can accommodate it; it does not guarantee that many completed or valid hand updates.
Skipping happens before copying camera pixels into DroidDeck, so skipped pairs also avoid JNI transfer, preprocessing, and inference. The SDK exposes no tracking-camera rate setter: sensor capture, vendor VIO, pose callbacks, and head polling retain their normal rates. Calibration collection, distance checks, explicit diagnostic capture, and preview-only operation use full-rate input. Diagnostics keep incoming cameraHz distinct from cameraCopyPermille, rateLimitedFrames (intentional skips), and dropped (eligible pairs replaced while the worker is busy). The compatibility field cameraFrameDivisor is 2 at 500 permille, 1 at 1000, and 0 for intermediate fractions. The bounded latest-pair queue and original capture timestamps remain intact. Automatic hand input rate can be disabled to expose the manual half/full-rate switch; both choices reset with tracking. GPU uses that manual preference and does not adapt. Thermal, latency, and gameplay benefits require hardware comparison.
- Connect a Luma Ultra and grant its existing USB permission. Spatial controls appear only after the SDK identifies the device as Carina.
- Enable Positional tracking — experimental. This restarts the provider in 6DoF mode. Switching picture modes does not restart it.
- Enter the current seated or standing eye height (shown in centimeters and feet/inches) and choose Set floor from current height. Normal VR recentering then preserves that floor, including while crouched. After reconnecting or explicitly restarting tracking, accept the new origin through setup/recenter. A recenter request during lost/stale tracking or recovery remains pending until 250 ms of fresh, steady poses arrive; repeated taps are unnecessary. The pending request is canceled on disconnect.
- Open Stereo camera setup and diagnostics → Open setup. The page uses the selected DroidDeck theme and app scale, with a single compact Back/title row so the controls have room to scroll in landscape. Check head tracking and camera synchronization first; capture controls become available when both are ready. Expand Show diagnostics for frame rate, frame age, dropped frames, processing time, and recovery tools.
- Print the 5 by 7 ChArUco board at actual size. Measure a square: it must be 30 mm (approximately 1.181 inches). Keep the board stationary and capture at least 24 sharp views by moving and rotating the glasses, covering the image center and edges. Include both yaw and pitch, roll, and varied distances. If the underlying tracking frame changes, start a new capture set; software recentering in an unchanged frame preserves the set.
- Calculate calibration. Accepted captures confirm board detection, not alignment quality. The fitter compares pinhole and fisheye models on held-out views. It rejects poor stereo reprojection, an implausible baseline, insufficient rotational diversity, and inconsistent stationary-board alignment. Turning and tilting the glasses is required; pause before each capture. If the board stayed fixed but alignment varies beyond 5 cm (about 2 inches), the head-tracking frame may have shifted during collection. Wait for steady tracking and start a new set; do not remove the quality gate to accept the old one.
- Start with 12 in (30.48 cm) or 30 cm (11.81 in) under Measured distance; 20 in and 50 cm remain available. The closer settings make the small printed markers easier to resolve in the 640 by 480 tracking cameras. Show the complete board with its center exactly the selected distance from the left camera, then choose Validate distance. You can hold or support the glasses without wearing them. Progress and the result appear beside this button. The count refers to detected checkerboard intersections, not the paper's edges; a fully visible but small or blurred board can still have too few detections. Validation detects corners in the original images, then corrects those coordinates for lens distortion before triangulating, avoiding marker blur from resampling the images. Rejected stereo geometry reports that depth could not be measured; a valid reconstruction shows measured and selected distances in both units. Each imperial or metric choice uses its exact distance. The complete 24-corner board and physical-distance check within 5 cm (about 2 inches) are still required before a newly fitted personal profile becomes usable. Profiles are keyed to the SDK board-serial SHA-256 identifier (falling back to a nonblank Android USB serial), dimensions, and format version. Verified profiles reload automatically after an app restart or reconnect to the same identified glasses. If neither identifier is available, profiles remain connection-local.
- Enable Hand controllers — experimental. Present an open hand first so inputs can arm. Aim the virtual controller at the target, pinch thumb and index finger together to press the trigger/click, then separate them to release. Moving the SteamVR dashboard by its handle uses this same pinch-and-release action. Keep the other fingers open when only clicking; curling the middle, ring, and little fingers activates the separate grip input, which the installed Vive dashboard binding maps to Back. Games can use grip for holding objects. After tracking loss, open the hand again before another press. Gamepad sticks supply locomotion and turning; buttons retain the Vive-compatible mapping. External VR controllers retain precedence.
The diagnostic capture button saves one stereo pair and metadata to the app-private files/spatial/captures directory. Merely opening diagnostics does not save images. Calibration profiles stay in files/spatial. A brief missed hand detection holds the last observed controller transform only until its original observation reaches 350 ms old. Input freshness remains limited to 200 ms. Pinch and grip release immediately on a detected miss and require an observed release before rearming. The hold does not extrapolate motion, refresh timestamps, or substitute a gamepad pose. Head tracking loss, origin/connection changes, disconnects, and controller-mode changes clear it. Gamepad menus and locomotion remain available during optical hand loss. Switch hand controllers off explicitly to return to gamepad-positioned controllers.
Accepted board landmarks and their capture-time head poses are saved separately in app-private files/spatial/*-captures.json, without camera images. A matching SDK or USB device identity and image geometry allow automatic recovery for recalculation. Restored observations cannot be appended to a new tracking frame; start a new set to capture again. Devices without either stable identifier keep a session-labelled diagnostic backup and require explicit developer recovery onto the same physical device. They do not automatically reuse another connection's unidentified capture set or personal calibration; they can still use the bundled model base.
The vendor SDK working directory is noBackupFilesDir/viture. Android can evict cacheDir during a live session, so active tracking files must not depend on that location. The SDK initializes its own files in the new directory on the next provider start; existing printed-board calibration profiles remain in files/spatial.
Coordinates and lifecycle
SpatialPose uses meters and right/up/back OpenGL coordinates. Quaternions are normalized xyzw internally; SDK arrays are position followed by wxyz. Legacy orientation conversion and the 128-byte head-pose channel remain intact. The session coordinator survives HeadTracker recreation and owns separate full-pose screen and yaw/floor VR origins. The camera worker uses a generation guard, a bounded latest-frame queue, and no SDK lifecycle operations in callbacks.
Head polling is approximately every 4 ms with prediction disabled; this is not a claim of a 250 Hz sensor. Camera-rate callbacks provide a liveness check. Stale or unstable poses retain the last rendered transform and produce an invalid SteamVR pose. While a positional session is active, DroidDeck covers the resulting grey view with a themed recovery screen on the phone and glasses, including one readable copy per eye in SBS. It distinguishes tracking loss, USB disconnection, required recentering, and stabilization after recentering. Tracking-loss instructions ask the user to look around slowly in a well-lit room with visible detail, keep the cameras clear, and open the setup diagnostics to restart tracking if recovery stalls. Phone controls remain accessible, and the screen closes automatically when tracking is valid. Freshness is checked even if pose callbacks stop. Repeated USB timeouts require connection recovery rather than a synthetic valid pose. Large pose discontinuities latch a setup requirement. Provider shutdown invalidates queued work before closing the vendor provider and Java USB connection.
The 176-byte droiddeck-spatialpose snapshot uses protocol version 4 and sequence-counter publication. The driver validates file length, version, finite normalized poses, and monotonic age. Head and both hands use one snapshot and one VR origin. Hand pose flags independently identify valid palm tracking (bit 0), pinch (bit 1), and grip (bit 2); head flags use only bit 0. The formerly reserved 32-bit word at byte offset 36 is now input_epochs, containing independent 8-bit loss counters for left trigger, left grip, right trigger, and right grip, from least to most significant byte. The layout size and pose offsets are unchanged, but versions 1 through 3 are rejected; app and driver must be rebuilt together.
Snapshot flags select positional tracking (bit 0), bare-hand controllers (bit 1), or Luma Joy-Con
controllers (bit 2). The latter two selections are mutually exclusive; either enables stereo inference.
The Joy-Con mode consumes only wrist landmark 0 and the original capture timestamp; rotation and inputs
come from the paired Joy-Con. luma_joycon.h selects fresh stereo positions or a 6DoF-head-anchored arm
model independently per hand, rejects observations over 1.5 m from the head, and blends source switches
with a 180 ms time constant. It never integrates accelerometer position or uses the phone-camera
correction in this mode. Stale head poses invalidate output and release inputs. Selected mode flags
survive disconnect so a lost Luma cannot silently switch to a fixed-origin controller. Origin/connection
changes reset both positional blending and controller yaw alignment. This is hand-assisted tracking,
not a detector for the controller shell; see Joy-Con setup and limits.
Each counter advances when its gesture becomes ineligible, including invalid finger data, hand/head loss, and observation expiry. It survives later valid snapshots, allowing the driver to notice a short loss even if it never read the intervening invalid snapshot. Counters wrap independently within their bytes. Connection/origin changes reset driver input state, and a gap beyond 200 ms from its last accepted observation also releases inputs. Every hand retains its original camera observation timestamp: experimental hand poses and inputs expire after 200 ms, while head poses expire after 100 ms. A changed loss counter disarms only its affected gesture. Duplicate or older observations cannot rearm an input or replay a press; a new, valid released observation is required after loss before another press. Missing finger data never counts as a release gesture. Head updates cannot extend a hand observation's lifetime. Diagnostics expose the hand limit as handMaxAgeMs.
The driver submits trigger scalar, trigger click, and grip updates before submitting the controller pose, including when the pose is becoming invalid. Disconnect and deactivation also clear inputs first. Changes in input state, loss counters, or API error codes produce concise driver log records with raw gestures, output values, armed state, pose/input validity, observation age, connection, and origin. Successful input API calls confirm submission to SteamVR; they do not prove that the dashboard received its mouse-up event. The installed dashboard grab handler retains a hand attachment until mouse-up and has no tracking-loss cleanup in that JavaScript component. Recovery of the reported stuck dashboard drag therefore still requires hardware validation; the implementation does not manufacture another press or a valid pose to force release.
Controller display smoothing has no stationary anchors or dead zones. Small intentional motion remains visible; smoothing and prediction do not change the measured pose used for gesture validation.
The displayed controller converges toward each measured palm pose on every driver update with one 15 ms correction time constant and normalized, hemisphere-corrected quaternions. There is no additional per-observation smoothing stage. After three consecutive observations with consistent motion, translation advances from the result's receipt time for at most 80 ms and 2 cm; capture timestamps still determine velocity and freshness. Receipt-relative prediction can continue between delayed inference results instead of consuming its entire horizon before a result arrives. Prediction is disabled above 1.5 m/s or 6 m/s², on a missed/invalid observation, head loss, or changed origin/connection. A missed observation freezes the last displayed transform through duplicate old samples until a newer observation arrives or the original 350 ms visual-hold expiry is reached. Fast motion reaches the 2 cm bound sooner, and correction adds a small settling delay. Stabilization and prediction do not alter gesture values or observation timestamps and cannot replace measured latency validation.
The screen shader puts translation in extra.yzw, rotates the eye offset, and rejects intersections behind the eye. SBS and flat branches ignore translation. The push-constant allocation stays unchanged. A screen reference can include pitch and roll, preserving placement while reclining.
Stereo reconstruction and limitations
Skipped camera pairs do not require clock warmup again when the source/host offset remains consistent. A source gap above 200 ms checks that offset against the existing 20 ms bound instead of unconditionally resetting synchronization. Nonmonotonic or invalid timestamps and inconsistent long gaps require 16 fresh observations; normal short-gap jitter retains the existing rolling-minimum and p90 policy. This preserves synchronization through a processing stall without publishing expired hand observations.
Palm orientation uses an explicit hand side. The lateral axis reverses for the right hand so mirrored palms have the same normal and finger-direction convention; the existing left-hand mapping is retained. This corrects the mathematical right-hand inversion, while physical controller aiming still needs confirmation.
The stock two-hand video graph can repeat whole-image palm detection when fewer than two hands were tracked previously. With both hands tracked it can skip that search; see the MediaPipe graph. CPU scheduling maintains two persistent detector banks, each containing one detector per camera: one bank permits one hand, and the other permits two. After three observations agree on exactly one confident hand in both cameras, the one-hand bank handles intervening frames, and a two-hand search becomes due after 240 ms of camera time, running on the next processed frame. Both hands, a miss, handedness disagreement, nonmonotonic time, or a gap above 400 ms return selection to the two-hand bank. Second-hand discovery therefore remains enabled but can wait for the next scheduled search and processing. Banks retain their video tracking state between selections; they are not rebuilt per frame. GPU retains the ordinary two-hand bank. Diagnostics expose the active bank as inferenceHandLimit, alongside raw and filtered hand counts and per-camera timing. These choices reduce unnecessary searches by design; their benefit and extra model-memory cost require device measurement.
The primary left0/right0 images are copied only for diagnostics, calibration, or enabled hands. At most one pair is pending while the worker processes another. Automatic exposure is retained because the cameras also serve native VIO. SDK 2.4.0 documents the tracking-camera rate as 25 Hz and provides no stereo frame-rate setter. Polling more frequently does not create additional camera frames. The camera-rate readout accounts for skipped source-frame sequence numbers instead of treating processed-pair throughput as sensor rate. The selected video-mode Hand Landmarker pair receives matching timestamps. CPU inference runs the cameras concurrently with separate image buffers; each bank's right detector is created, used, and closed on one dedicated worker. Both calls must finish before pixels or detectors can be reused, including after failure or shutdown interruption. GPU inference remains serial on its owning thread. The pipeline detects landmarks in the original grayscale images replicated into RGB channels, without image normalization or fisheye remapping, corrects landmark coordinates for stereo reconstruction, matches unambiguous same-handed candidates, rejects excessive reprojection or implausible depth, and derives the palm pose from triangulated landmarks. Gesture recognition uses agreement between the two image-space finger estimates. MediaPipe world landmarks are not treated as headset-relative positions.
The public header does not document the coordinate basis of the camera-rate pose callback. Its pose values are therefore not used as canonical head transforms. A bounded history of polled OpenGL poses supplies the capture-time transform. Source-to-host clock offsets must first stabilize over at least 16 observations; callback timestamp agreement and a nearby canonical head sample are required. Hand inference starts without waiting for the matching VIO callback, then checks for that exact camera timestamp before reconstruction can publish a valid pose. Missing confirmation, changed connections/tracking frames, invalid head tracking, and hand observations older than 200 ms still discard the result. Calibration and preview-only acquisition retain their bounded pose wait. Receipt-based offset estimation cannot establish absolute camera latency. External motion-to-visible-controller measurement is still required. Increasing expiry can reduce disappearing controllers at the cost of displaying older poses; it does not resolve GPU device loss or establish the latency/performance gates.
Stereo validation retains the original 3-pixel reprojection, positive-depth, and 15–150 cm depth checks per landmark. Bare-hand controller position and orientation require a valid triangulated wrist and all four palm knuckles (0, 5, 9, 13, 17). Joy-Con mode reconstructs wrist landmark 0 only and takes orientation and inputs from the paired controller. Live pinch and grip no longer require trustworthy fingertip depth: each image estimates palm-normalized pinch and middle/ring/little finger curl, then both views must agree. Grip combines fingertip reach and finger-chain bending, with separate press/release thresholds and three curled fingers to press versus two to retain a press. Invalid, degenerate, or conflicting image evidence releases only its affected gesture and requires an observed release before rearming. A valid 3D palm is never sufficient by itself to invent a grab or click. Calibration and distance validation still require every board point to pass. Conservative stereo association rejects ambiguous hands rather than inventing depth. Crossed hands, edge-on palms, and occlusion may reduce availability. The 2D gesture consensus and aiming still require physical testing through grabs, pulls, and releases; they do not establish an accuracy or latency pass. No skeleton output, desktop gestures, fast-action guarantee, or hands-only locomotion is provided.
Diagnostics include automatic or manual half/full input rates and a Lite-model comparison. Choices reset with the tracking session; the default is full-model parallel CPU with scheduled discovery and an adaptive rate starting at 500 permille. The Lite bundle uses Google's Lite palm detector and Lite landmark model with matching Tasks metadata. Both bundles are checksum-pinned in Gradle and at runtime; tools/build_hand_lite.py reproduces the candidate from pinned inputs. Saved-pair CPU replay compares full-model two-hand inference, scheduled full, scheduled lite, then full-model two-hand inference again, all with concurrent cameras. Rows identify parallel and scheduled; samples identify inferenceHandLimit. Its timings exclude model construction and cannot establish live detection coverage, external latency, second-hand discovery delay, or game performance.
Combined release gates
| Area | Required evidence |
|---|---|
| Translation | Measured right/up/back movement and approximately 1:1 meter scale; lean and crouch; no double application |
| Origins | Crouched recenter preserves floor; picture switching preserves independent screen and VR references |
| Screens | Anchoring while moving sideways, toward and away; stable output after crossing the screen plane |
| Hands | At least 95% valid slow unobstructed observations at 30–80 cm; position error at most 5 cm; stationary jitter at most 2 cm |
| Latency | External physical-motion-to-visible-controller p95 below 100 ms |
| Recovery | Immediate release on detected loss; observation expiry extended to 200 ms by user request; clear-hand reacquisition within 500 ms |
| Performance | At most 10% regression in p95 game frame time over 20 minutes, with baseline frame rate maintained |
| Gameplay | Dashboard selection and Angry Birds VR aim, grip, pull, release |
| Robustness | Crossed hands, partial occlusion, head motion, lighting, USB reconnect, thermal load |
Record results in luma-spatial-evidence.md. A build or head-tracking pass alone does not satisfy this combined gate.