diff --git a/LICENSES/BSD-3-Clause-Rectus.txt b/LICENSES/BSD-3-Clause-Rectus.txt
new file mode 100644
index 000000000..d08aa70e9
--- /dev/null
+++ b/LICENSES/BSD-3-Clause-Rectus.txt
@@ -0,0 +1,28 @@
+
+Copyright (c) 2026, Rectus
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation and/or
+other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+may be used to endorse or promote products derived from this software without
+specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/README-monado.md b/README-monado.md
new file mode 100644
index 000000000..a23335933
--- /dev/null
+++ b/README-monado.md
@@ -0,0 +1,278 @@
+# Monado - XR Runtime (XRT)
+
+
+
+> * Main homepage and documentation:
+> * Promotional homepage:
+> * Maintained at
+> * Latest API documentation:
+> * Continuously-updated changelog of the default branch:
+>
+
+Monado is an open source XR runtime delivering immersive experiences such as VR
+and AR on mobile, PC/desktop, and any other device
+(because gosh darn people
+come up with a lot of weird hardware).
+Monado aims to be a complete and conforming implementation
+of the OpenXR API made by Khronos.
+The project is primarily developed on GNU/Linux, but also runs on Android and Windows.
+"Monado" has no specific meaning and is just a name.
+
+## Monado source tree
+
+* `src/xrt/include` - headers that define the internal interfaces of Monado.
+* `src/xrt/compositor` - code for doing distortion and driving the display hardware of a device.
+* `src/xrt/auxiliary` - utilities and other larger components.
+* `src/xrt/drivers` - hardware drivers.
+* `src/xrt/state_trackers/oxr` - OpenXR API implementation.
+* `src/xrt/targets` - glue code and build logic to produce final binaries.
+* `src/external` - a small collection of external code and headers.
+
+## Getting Started
+
+Dependencies include:
+
+* [CMake][] 3.13 or newer (Note Ubuntu 18.04 only has 3.10)
+* Python 3.6 or newer
+* Vulkan headers and loader - Fedora package `vulkan-loader-devel`
+* OpenGL headers
+* Eigen3 - Debian/Ubuntu package `libeigen3-dev`
+* glslangValidator - Debian/Ubuntu package `glslang-tools`, Fedora package `glslang`.
+* libusb
+* libudev - Debian/Ubuntu package `libudev-dev`, Fedora package `systemd-devel`
+* Video 4 Linux - Debian/Ubuntu package `libv4l-dev`.
+
+Optional (but recommended) dependencies:
+
+* libxcb and xcb-xrandr development packages
+* [OpenHMD][] 0.3.0 or newer (found using pkg-config)
+
+Truly optional dependencies, useful for some drivers, app support, etc.:
+
+* Doxygen - Debian/Ubuntu package `doxygen` and `graphviz`
+* Wayland development packages
+* Xlib development packages
+* libhidapi - Debian/Ubuntu package `libhidapi-dev`
+* OpenCV
+* libuvc - Debian/Ubuntu package `libuvc-dev`
+* libjpeg
+* libbluetooth - Debian/Ubuntu package `libbluetooth-dev`
+* libsdl - Debian/Ubuntu package `libsdl2-dev`
+
+Experimental Windows support requires the Vulkan SDK and also needs or works
+best with the following vcpkg packages installed:
+
+* pthreads eigen3 libusb hidapi zlib doxygen
+
+If you have a recent [vcpkg](https://vcpkg.io) installed and use the appropriate
+CMake toolchain file, the vcpkg manifest in the Monado repository will instruct
+vcpkg to locally install the dependencies automatically. The Vulkan SDK
+installer should set the `VULKAN_SDK` Windows environment variable to point
+at the installation location (for example, `C:/VulkanSDK/1.3.250.1`), though
+make sure you open a new terminal (or open the CMake GUI) *after* doing that
+install to make sure it is available.
+
+Monado has been tested on these distributions, but is expected to work on almost
+any modern distribution.
+
+* Ubuntu 24.04, 22.04, 20.04, (18.04 may not be fully supported)
+* Debian 11 `bookworm`, 10 `buster`
+ * Up-to-date package lists can be found in our CI config file,
+ `.gitlab-ci.yml`
+* Archlinux
+
+These distributions include recent-enough versions of all the
+software to use direct mode,
+without using any external, third-party, or backported
+package sources.
+
+See also [Status of DRM Leases][drm-lease]
+for more details on specific packages, versions, and commits.
+
+Due to the lack of a OpenGL extension: GL_EXT_memory_object_fd on Intel's
+OpenGL driver, only the AMD
+radeonsi driver and the proprietary NVIDIA driver will work for OpenGL OpenXR
+clients. This is due to a requirement of the Compositor. Support status of the
+extension can be found on the [mesamatrix website][mesamatrix-ext].
+
+### CMake
+
+Build process is similar to other CMake builds,
+so something like the following will build it.
+
+Go into the source directory, create a build directory,
+and change into it.
+
+```bash
+mkdir build
+cd build
+```
+
+Then, invoke [CMake to generate a project][cmake-generate].
+Feel free to change the build type or generator ("Ninja" is fast and parallel) as you see fit.
+
+```bash
+cmake .. -DCMAKE_BUILD_TYPE=Debug -G "Unix Makefiles"
+```
+
+If you plan to install the runtime,
+append something like `-DCMAKE_INSTALL_PREFIX=~/.local`
+to specify the root of the install directory.
+(The default install prefix is `/usr/local`.)
+
+To build, [the generic CMake build commands][cmake-build] below will work on all systems,
+though you can manually invoke your build tool (`make`, `ninja`, etc.) if you prefer.
+The first command builds the runtime and docs,
+and the second, which is optional, installs the runtime under `${CMAKE_INSTALL_PREFIX}`.
+
+```bash
+cmake --build .
+cmake --build . --target install
+```
+
+Alternately, if using Make, the following will build the runtime and docs, then install.
+Replace `make` with `ninja` if you used the Ninja generator.
+
+```bash
+make
+make install
+```
+
+Documentation can be browsed by opening `doc/html/index.html` in the build directory in a web browser.
+
+## Getting started using OpenXR with Monado
+
+This implements the [OpenXR][] API,
+so to do anything with it, you'll need an application
+that uses OpenXR, along with the OpenXR loader.
+The OpenXR loader is a glue library that connects OpenXR applications to OpenXR runtimes such as Monado
+It determines which runtime to use by looking for the config file `active_runtime.json` (either a symlink to
+or a copy of a runtime manifest) in the usual XDG config paths
+and processes environment variables such as `XR_RUNTIME_JSON=/usr/share/openxr/0/openxr_monado.json`.
+It can also insert OpenXR API Layers without the application or the runtime having to do it.
+
+You can use the `hello_xr` sample provided with the OpenXR SDK.
+
+The OpenXR loader can be pointed to a runtime json file in a nonstandard location with the environment variable `XR_RUNTIME_JSON`. Example:
+
+```bash
+XR_RUNTIME_JSON=~/monado/build/openxr_monado-dev.json ./openxr-example
+```
+
+For ease of development Monado creates a runtime manifest file in its build directory using an absolute path to the
+Monado runtime in the build directory called `openxr_monado-dev.json`. Pointing `XR_RUNTIME_JSON` to this
+file allows using Monado after building, without installing.
+
+Note that the loader can always find and load the runtime
+if the path to the runtime library given in the json manifest is an absolute path,
+but if a relative path like `libopenxr_monado.so.0` is given,
+then `LD_LIBRARY_PATH` must include the directory that contains `libopenxr_monado.so.0`.
+The absolute path in `openxr_monado-dev.json` takes care of this for you.
+
+Distribution packages for monado may provide the "active runtime" file `/etc/xdg/openxr/1/active_runtime.json`.
+In this case the loader will automatically use Monado when starting an OpenXR application. This global configuration
+can be overridden on a per user basis by creating `~/.config/openxr/1/active_runtime.json`.
+
+## Direct mode
+
+On AMD and Intel GPUs our direct mode code requires a connected HMD to have
+the `non-desktop` xrandr property set to 1.
+Only the most common HMDs have the needed quirks added to the linux kernel.
+
+If you know that your HMD lacks the quirk you can run this command **before** or
+after connecting the HMD and it will have it. Where `HDMI-A-0` is the xrandr
+output name where you plug the HMD in.
+
+```bash
+xrandr --output HDMI-A-0 --prop --set non-desktop 1
+```
+
+You can verify that it stuck with the command.
+
+```bash
+xrandr --prop
+```
+
+## Running Vulkan Validation
+
+To run Monado with Vulkan validation the loader's layer functionality can be used.
+```
+VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation ./build/src/xrt/targets/service/monado-service
+```
+The same can be done when launching a Vulkan client.
+
+If you want a backtrace to be produced at validation errors, create a `vk_layer_settings.txt`
+file with the following content:
+```
+khronos_validation.debug_action = VK_DBG_LAYER_ACTION_LOG_MSG,VK_DBG_LAYER_ACTION_BREAK
+khronos_validation.report_flags = error,warn
+khronos_validation.log_filename = stdout
+```
+
+## Coding style and formatting
+
+[clang-format][] is used,
+and a `.clang-format` config file is present in the repo
+to allow your editor to use them.
+
+To manually apply clang-format to every non-external source file in the tree,
+run this command in the source dir with a `sh`-compatible shell
+(Git for Windows git-bash should be OK):
+
+```bash
+scripts/format-project.sh
+```
+
+You can optionally put something like `CLANG_FORMAT=clang-format-7` before that command
+if your clang-format binary isn't named `clang-format`.
+**Note that you'll typically prefer** to use something like `git clang-format`
+to re-format only your changes, in case version differences in tools result in overall format changes.
+The CI "style" job currently runs on Debian Bullseye, so it has clang-format-11.
+
+[OpenHMD]: http://openhmd.net
+[drm-lease]: https://haagch.frickel.club/#!drmlease%2Emd
+[OpenXR]: https://khronos.org/openxr
+[clang-format]: https://releases.llvm.org/7.0.0/tools/clang/docs/ClangFormat.html
+[cmake-build]: https://cmake.org/cmake/help/v3.12/manual/cmake.1.html#build-tool-mode
+[cmake-generate]: https://cmake.org/cmake/help/v3.12/manual/cmake.1.html
+[CMake]: https://cmake.org
+[mesamatrix-ext]: https://mesamatrix.net/#Version_ExtensionsthatarenotpartofanyOpenGLorOpenGLESversion
+
+## Contributing, Code of Conduct
+
+See `CONTRIBUTING.md` for details of contribution guidelines. GitLab Issues and
+Merge Requests are the preferred way to discuss problems, suggest enhancements,
+or submit changes for review. **In case of a security issue**, you should choose
+the "confidential" option when using the GitLab issues page. For highest
+security, you can send encrypted email (using GPG/OpenPGP) to Rylie Pavlik at
+ and using the associated key from
+.
+
+Please note that this project is released with a Contributor Code of Conduct.
+By participating in this project you agree to abide by its terms.
+
+We follow the standard freedesktop.org code of conduct,
+available at ,
+which is based on the [Contributor Covenant](https://www.contributor-covenant.org).
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting:
+
+* First-line project contacts:
+ * Rylie Pavlik
+ * Frederic Plourde
+ * Jakob Bornecrantz
+* freedesktop.org contacts: see most recent list at
+
+Code of Conduct section excerpt adapted from the
+[Contributor Covenant](https://www.contributor-covenant.org), version 1.4.1,
+available at
+, and
+from the freedesktop.org-specific version of that code, available at
+, used under CC-BY-4.0.
diff --git a/README.md b/README.md
index a23335933..9867d687f 100644
--- a/README.md
+++ b/README.md
@@ -1,278 +1,50 @@
-# Monado - XR Runtime (XRT)
+# Rift S on Linux
-
+## Status
-> * Main homepage and documentation:
-> * Promotional homepage:
-> * Maintained at
-> * Latest API documentation:
-> * Continuously-updated changelog of the default branch:
->
+Early project. It already works and is usable in SteamVR with the headset and both controllers. Good changes will keep landing here.
-Monado is an open source XR runtime delivering immersive experiences such as VR
-and AR on mobile, PC/desktop, and any other device
-(because gosh darn people
-come up with a lot of weird hardware).
-Monado aims to be a complete and conforming implementation
-of the OpenXR API made by Khronos.
-The project is primarily developed on GNU/Linux, but also runs on Android and Windows.
-"Monado" has no specific meaning and is just a name.
+Known rough edges:
-## Monado source tree
+- Controller behavior when the controllers leave the camera view is still rough.
+- The boundary does not persist reliably across restarts.
-* `src/xrt/include` - headers that define the internal interfaces of Monado.
-* `src/xrt/compositor` - code for doing distortion and driving the display hardware of a device.
-* `src/xrt/auxiliary` - utilities and other larger components.
-* `src/xrt/drivers` - hardware drivers.
-* `src/xrt/state_trackers/oxr` - OpenXR API implementation.
-* `src/xrt/targets` - glue code and build logic to produce final binaries.
-* `src/external` - a small collection of external code and headers.
+## Build and install
-## Getting Started
+Head tracking needs Basalt. Clone Basalt, apply the patches in `patches/basalt/` in order, then build and install it by its own readme so that `libbasalt.so` is on the library path. If it is somewhere else, set `VIT_SYSTEM_LIBRARY_PATH`.
-Dependencies include:
-
-* [CMake][] 3.13 or newer (Note Ubuntu 18.04 only has 3.10)
-* Python 3.6 or newer
-* Vulkan headers and loader - Fedora package `vulkan-loader-devel`
-* OpenGL headers
-* Eigen3 - Debian/Ubuntu package `libeigen3-dev`
-* glslangValidator - Debian/Ubuntu package `glslang-tools`, Fedora package `glslang`.
-* libusb
-* libudev - Debian/Ubuntu package `libudev-dev`, Fedora package `systemd-devel`
-* Video 4 Linux - Debian/Ubuntu package `libv4l-dev`.
-
-Optional (but recommended) dependencies:
-
-* libxcb and xcb-xrandr development packages
-* [OpenHMD][] 0.3.0 or newer (found using pkg-config)
-
-Truly optional dependencies, useful for some drivers, app support, etc.:
-
-* Doxygen - Debian/Ubuntu package `doxygen` and `graphviz`
-* Wayland development packages
-* Xlib development packages
-* libhidapi - Debian/Ubuntu package `libhidapi-dev`
-* OpenCV
-* libuvc - Debian/Ubuntu package `libuvc-dev`
-* libjpeg
-* libbluetooth - Debian/Ubuntu package `libbluetooth-dev`
-* libsdl - Debian/Ubuntu package `libsdl2-dev`
-
-Experimental Windows support requires the Vulkan SDK and also needs or works
-best with the following vcpkg packages installed:
-
-* pthreads eigen3 libusb hidapi zlib doxygen
-
-If you have a recent [vcpkg](https://vcpkg.io) installed and use the appropriate
-CMake toolchain file, the vcpkg manifest in the Monado repository will instruct
-vcpkg to locally install the dependencies automatically. The Vulkan SDK
-installer should set the `VULKAN_SDK` Windows environment variable to point
-at the installation location (for example, `C:/VulkanSDK/1.3.250.1`), though
-make sure you open a new terminal (or open the CMake GUI) *after* doing that
-install to make sure it is available.
-
-Monado has been tested on these distributions, but is expected to work on almost
-any modern distribution.
-
-* Ubuntu 24.04, 22.04, 20.04, (18.04 may not be fully supported)
-* Debian 11 `bookworm`, 10 `buster`
- * Up-to-date package lists can be found in our CI config file,
- `.gitlab-ci.yml`
-* Archlinux
-
-These distributions include recent-enough versions of all the
-software to use direct mode,
-without using any external, third-party, or backported
-package sources.
-
-See also [Status of DRM Leases][drm-lease]
-for more details on specific packages, versions, and commits.
-
-Due to the lack of a OpenGL extension: GL_EXT_memory_object_fd on Intel's
-OpenGL driver, only the AMD
-radeonsi driver and the proprietary NVIDIA driver will work for OpenGL OpenXR
-clients. This is due to a requirement of the Compositor. Support status of the
-extension can be found on the [mesamatrix website][mesamatrix-ext].
-
-### CMake
-
-Build process is similar to other CMake builds,
-so something like the following will build it.
-
-Go into the source directory, create a build directory,
-and change into it.
-
-```bash
-mkdir build
-cd build
+```
+git clone https://gitlab.freedesktop.org/mateosss/basalt.git
+cd basalt
+git submodule update --init --recursive
+for p in /path/to/this/repo/patches/basalt/*.patch; do patch -p1 < "$p"; done
```
-Then, invoke [CMake to generate a project][cmake-generate].
-Feel free to change the build type or generator ("Ninja" is fast and parallel) as you see fit.
+The patches were made against Basalt main at df6e970.
-```bash
-cmake .. -DCMAKE_BUILD_TYPE=Debug -G "Unix Makefiles"
+Then build Monado. OpenCV, Vulkan and Eigen are required on top of Monado's usual dependencies.
+
+```
+cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo \
+ -DXRT_BUILD_DRIVER_RIFT_S=ON -DXRT_FEATURE_SLAM=ON -DXRT_FEATURE_STEAMVR_PLUGIN=ON
+ninja -C build
+sudo ninja -C build install
```
-If you plan to install the runtime,
-append something like `-DCMAKE_INSTALL_PREFIX=~/.local`
-to specify the root of the install directory.
-(The default install prefix is `/usr/local`.)
+The install puts the SteamVR driver in `/usr/local/share/steamvr-monado`. Register it with SteamVR and start SteamVR with the headset plugged in.
-To build, [the generic CMake build commands][cmake-build] below will work on all systems,
-though you can manually invoke your build tool (`make`, `ninja`, etc.) if you prefer.
-The first command builds the runtime and docs,
-and the second, which is optional, installs the runtime under `${CMAKE_INSTALL_PREFIX}`.
-
-```bash
-cmake --build .
-cmake --build . --target install
+```
+~/.local/share/Steam/steamapps/common/SteamVR/bin/vrpathreg.sh adddriver /usr/local/share/steamvr-monado
```
-Alternately, if using Make, the following will build the runtime and docs, then install.
-Replace `make` with `ninja` if you used the Ninja generator.
+Unit tests build with the default `BUILD_TESTING=ON` and run with `ctest --test-dir build`.
-```bash
-make
-make install
-```
+## License
-Documentation can be browsed by opening `doc/html/index.html` in the build directory in a web browser.
+Boost Software License 1.0, the same as Monado. See `LICENSE`. Third-party files keep their own licenses in `LICENSES/`.
-## Getting started using OpenXR with Monado
-
-This implements the [OpenXR][] API,
-so to do anything with it, you'll need an application
-that uses OpenXR, along with the OpenXR loader.
-The OpenXR loader is a glue library that connects OpenXR applications to OpenXR runtimes such as Monado
-It determines which runtime to use by looking for the config file `active_runtime.json` (either a symlink to
-or a copy of a runtime manifest) in the usual XDG config paths
-and processes environment variables such as `XR_RUNTIME_JSON=/usr/share/openxr/0/openxr_monado.json`.
-It can also insert OpenXR API Layers without the application or the runtime having to do it.
-
-You can use the `hello_xr` sample provided with the OpenXR SDK.
-
-The OpenXR loader can be pointed to a runtime json file in a nonstandard location with the environment variable `XR_RUNTIME_JSON`. Example:
-
-```bash
-XR_RUNTIME_JSON=~/monado/build/openxr_monado-dev.json ./openxr-example
-```
-
-For ease of development Monado creates a runtime manifest file in its build directory using an absolute path to the
-Monado runtime in the build directory called `openxr_monado-dev.json`. Pointing `XR_RUNTIME_JSON` to this
-file allows using Monado after building, without installing.
-
-Note that the loader can always find and load the runtime
-if the path to the runtime library given in the json manifest is an absolute path,
-but if a relative path like `libopenxr_monado.so.0` is given,
-then `LD_LIBRARY_PATH` must include the directory that contains `libopenxr_monado.so.0`.
-The absolute path in `openxr_monado-dev.json` takes care of this for you.
-
-Distribution packages for monado may provide the "active runtime" file `/etc/xdg/openxr/1/active_runtime.json`.
-In this case the loader will automatically use Monado when starting an OpenXR application. This global configuration
-can be overridden on a per user basis by creating `~/.config/openxr/1/active_runtime.json`.
-
-## Direct mode
-
-On AMD and Intel GPUs our direct mode code requires a connected HMD to have
-the `non-desktop` xrandr property set to 1.
-Only the most common HMDs have the needed quirks added to the linux kernel.
-
-If you know that your HMD lacks the quirk you can run this command **before** or
-after connecting the HMD and it will have it. Where `HDMI-A-0` is the xrandr
-output name where you plug the HMD in.
-
-```bash
-xrandr --output HDMI-A-0 --prop --set non-desktop 1
-```
-
-You can verify that it stuck with the command.
-
-```bash
-xrandr --prop
-```
-
-## Running Vulkan Validation
-
-To run Monado with Vulkan validation the loader's layer functionality can be used.
-```
-VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation ./build/src/xrt/targets/service/monado-service
-```
-The same can be done when launching a Vulkan client.
-
-If you want a backtrace to be produced at validation errors, create a `vk_layer_settings.txt`
-file with the following content:
-```
-khronos_validation.debug_action = VK_DBG_LAYER_ACTION_LOG_MSG,VK_DBG_LAYER_ACTION_BREAK
-khronos_validation.report_flags = error,warn
-khronos_validation.log_filename = stdout
-```
-
-## Coding style and formatting
-
-[clang-format][] is used,
-and a `.clang-format` config file is present in the repo
-to allow your editor to use them.
-
-To manually apply clang-format to every non-external source file in the tree,
-run this command in the source dir with a `sh`-compatible shell
-(Git for Windows git-bash should be OK):
-
-```bash
-scripts/format-project.sh
-```
-
-You can optionally put something like `CLANG_FORMAT=clang-format-7` before that command
-if your clang-format binary isn't named `clang-format`.
-**Note that you'll typically prefer** to use something like `git clang-format`
-to re-format only your changes, in case version differences in tools result in overall format changes.
-The CI "style" job currently runs on Debian Bullseye, so it has clang-format-11.
-
-[OpenHMD]: http://openhmd.net
-[drm-lease]: https://haagch.frickel.club/#!drmlease%2Emd
-[OpenXR]: https://khronos.org/openxr
-[clang-format]: https://releases.llvm.org/7.0.0/tools/clang/docs/ClangFormat.html
-[cmake-build]: https://cmake.org/cmake/help/v3.12/manual/cmake.1.html#build-tool-mode
-[cmake-generate]: https://cmake.org/cmake/help/v3.12/manual/cmake.1.html
-[CMake]: https://cmake.org
-[mesamatrix-ext]: https://mesamatrix.net/#Version_ExtensionsthatarenotpartofanyOpenGLorOpenGLESversion
-
-## Contributing, Code of Conduct
-
-See `CONTRIBUTING.md` for details of contribution guidelines. GitLab Issues and
-Merge Requests are the preferred way to discuss problems, suggest enhancements,
-or submit changes for review. **In case of a security issue**, you should choose
-the "confidential" option when using the GitLab issues page. For highest
-security, you can send encrypted email (using GPG/OpenPGP) to Rylie Pavlik at
- and using the associated key from
-.
-
-Please note that this project is released with a Contributor Code of Conduct.
-By participating in this project you agree to abide by its terms.
-
-We follow the standard freedesktop.org code of conduct,
-available at ,
-which is based on the [Contributor Covenant](https://www.contributor-covenant.org).
-
-Instances of abusive, harassing, or otherwise unacceptable behavior may be
-reported by contacting:
-
-* First-line project contacts:
- * Rylie Pavlik
- * Frederic Plourde
- * Jakob Bornecrantz
-* freedesktop.org contacts: see most recent list at
-
-Code of Conduct section excerpt adapted from the
-[Contributor Covenant](https://www.contributor-covenant.org), version 1.4.1,
-available at
-, and
-from the freedesktop.org-specific version of that code, available at
-, used under CC-BY-4.0.
+Contact lbgos at contact@lbgos.dev.
diff --git a/patches/basalt/0001-keep-imu-frequency-per-tracker.patch b/patches/basalt/0001-keep-imu-frequency-per-tracker.patch
new file mode 100644
index 000000000..a8fd4c7cf
--- /dev/null
+++ b/patches/basalt/0001-keep-imu-frequency-per-tracker.patch
@@ -0,0 +1,27 @@
+--- a/src/vit/vit_tracker.cpp
++++ b/src/vit/vit_tracker.cpp
+@@ -127,6 +127,7 @@
+ vector cam;
+ } calib_data_ready;
+ uint32_t cam_count;
++ double imu_calibration_frequency = -1;
+
+ Calibration calib;
+ VioConfig vio_config;
+@@ -311,13 +312,13 @@
+ int i = imu_calib.imu_index;
+ ASSERT(i == 0, "More than one IMU unsupported (%d)", i);
+
+- static double frequency = -1;
++ double &frequency = imu_calibration_frequency;
+ if (frequency == -1) {
+ frequency = imu_calib.frequency;
+ calib.imu_update_rate = frequency;
+ } else {
+- ASSERT(frequency == calib.imu_update_rate, "Unsupported mix of IMU frequencies %lf != %lf", frequency,
+- calib.imu_update_rate);
++ ASSERT(frequency == imu_calib.frequency, "Unsupported mix of IMU frequencies %lf != %lf", frequency,
++ imu_calib.frequency);
+ }
+
+ // Accelerometer calibration
diff --git a/patches/basalt/0002-limit-workers-with-driver-calibration.patch b/patches/basalt/0002-limit-workers-with-driver-calibration.patch
new file mode 100644
index 000000000..e319232a1
--- /dev/null
+++ b/patches/basalt/0002-limit-workers-with-driver-calibration.patch
@@ -0,0 +1,12 @@
+--- a/src/vit/vit_tracker.cpp
++++ b/src/vit/vit_tracker.cpp
+@@ -170,6 +170,9 @@
+ calib_data_ready.cam.resize(cam_count, false);
+
+ if (config->file == nullptr) {
++ // Driver-provided calibration still uses the default one-thread CPU budget.
++ tbb_global_control =
++ std::make_unique(tbb::global_control::max_allowed_parallelism, num_threads);
+ // For the pipeline to work now, the user will need to use add_cam/imu_calibration
+ return;
+ }
diff --git a/patches/basalt/0003-safe-recreation-and-realtime-worker-budget.patch b/patches/basalt/0003-safe-recreation-and-realtime-worker-budget.patch
new file mode 100644
index 000000000..9fe78adac
--- /dev/null
+++ b/patches/basalt/0003-safe-recreation-and-realtime-worker-budget.patch
@@ -0,0 +1,193 @@
+--- a/src/vit/vit_tracker.cpp
++++ b/src/vit/vit_tracker.cpp
+@@ -55,6 +55,7 @@
+ #include
+
+ #include
++#include
+ #include
+ #include
+ #include
+@@ -119,7 +120,7 @@
+ bool print_queue = false;
+ bool use_double = false;
+ bool deterministic = false;
+- int num_threads = 1;
++ int num_threads = 4;
+
+ // VIO members
+ struct {
+@@ -170,7 +171,14 @@
+ calib_data_ready.cam.resize(cam_count, false);
+
+ if (config->file == nullptr) {
+- // Driver-provided calibration still uses the default one-thread CPU budget.
++ // Bound the pool even without a unified config. A single thread can leave
++ // both five-camera pipeline stages behind; benchmarks may select 1..4 for comparison.
++ if (const char *value = std::getenv("BASALT_MAX_THREADS")) {
++ char *end = nullptr;
++ const long requested = std::strtol(value, &end, 10);
++ if (end != value && *end == '\0' && requested >= 1 && requested <= 4) num_threads = int(requested);
++ }
++ cout << "Basalt max parallelism=" << num_threads << "\n";
+ tbb_global_control =
+ std::make_unique(tbb::global_control::max_allowed_parallelism, num_threads);
+ // For the pipeline to work now, the user will need to use add_cam/imu_calibration
+@@ -421,16 +429,37 @@
+
+ void stop() {
+ running = false;
++ // Discard queued images. The in-flight image finishes before the sentinel.
++ OpticalFlowInput::Ptr image;
++ while (image_data_queue->try_pop(image)) {}
+ image_data_queue->push(nullptr);
++ ImuData::Ptr imu;
++ while (imu_data_queue->try_pop(imu)) {}
++ while (opt_flow_ptr->input_imu_queue.try_pop(imu)) {}
+ imu_data_queue->push(nullptr);
+ opt_flow_ptr->input_imu_queue.push(nullptr);
++
++ // VIO can exit on its IMU sentinel before flow finishes. Once it has exited,
++ // drain its bounded vision queue so the remaining producer cannot block.
++ std::atomic flow_joined{false};
++ thread drain_vision([&] {
++ while (!flow_joined.load()) {
++ if (vio->finished.load()) {
++ OpticalFlowResult::Ptr result;
++ while (vio->vision_data_queue.try_pop(result)) {}
++ }
++ std::this_thread::sleep_for(std::chrono::milliseconds(1));
++ }
++ });
++ // Join while dynamic types and all connected queues still exist.
++ opt_flow_ptr->maybe_join();
++ flow_joined = true;
++ drain_vision.join();
++ vio->maybe_join();
+
+ if (print_queue) queues_printer_thread.join();
+ if (!deterministic) state_consumer_thread.join();
+ if (show_gui) ui.stop();
+-
+- // TODO: There is a segfault when closing monado without starting the stream
+- // happens in a lambda from keypoint_vio.cpp and ends at line c1alib_bias.hpp:112
+ }
+
+ void finalize() const {
+@@ -439,6 +468,16 @@
+ }
+
+ void reset() { vio->scheduleResetState(); }
++
++ // VIT submissions must not block restart when another input stream stalls.
++ // Keep bounded queues current by discarding their oldest pending sample.
++ template
++ static void push_latest(tbb::concurrent_bounded_queue &queue, const T &sample) {
++ while (!queue.try_push(sample)) {
++ T discarded;
++ queue.try_pop(discarded);
++ }
++ }
+
+ void push_imu_sample(const vit::ImuSample *s) {
+ // concurrent_bounded_queue expects Erasable and Allocator named
+@@ -448,8 +487,8 @@
+ data->t_ns = s->timestamp;
+ data->accel = {s->ax, s->ay, s->az};
+ data->gyro = {s->wx, s->wy, s->wz};
+- imu_data_queue->push(data);
+- opt_flow_ptr->input_imu_queue.push(data);
++ push_latest(*imu_data_queue, data);
++ push_latest(opt_flow_ptr->input_imu_queue, data);
+ }
+
+ void push_frame(const vit::ImgSample *s) {
+@@ -498,7 +537,7 @@
+ if (i == cam_count - 1) {
+ partial_frame->addTime("frames_read");
+ partial_frame->addTime("frames_pushed");
+- image_data_queue->push(partial_frame);
++ push_latest(*image_data_queue, partial_frame);
+ if (show_gui) ui.update_last_image(partial_frame);
+ if (deterministic) pop_state();
+ }
+@@ -609,7 +648,6 @@
+ }
+
+ vit::Result Tracker::stop() {
+- impl_->finalize();
+ impl_->stop();
+ return vit::Result::VIT_SUCCESS;
+ }
+@@ -718,3 +756,8 @@
+
+ return VIT_SUCCESS;
+ }
++
++// Optional Monado capability. This build joins flow/VIO before destroying their
++// connected queues and accepts shutdown sentinels while waiting for IMU.
++extern "C" __attribute__((visibility("default"))) bool
++vit_tracker_supports_safe_recreation() { return true; }
+--- a/include/basalt/optical_flow/optical_flow.h
++++ b/include/basalt/optical_flow/optical_flow.h
+@@ -128,7 +128,11 @@
+ // patch_coord = PatchT::pattern2.template cast();
+ depth_guess = config.optical_flow_matching_default_depth;
+ }
+- virtual ~OpticalFlowBase() { processing_thread->join(); }
++ virtual ~OpticalFlowBase() { maybe_join(); }
++
++ void maybe_join() {
++ if (processing_thread && processing_thread->joinable()) processing_thread->join();
++ }
+
+ virtual void processingLoop() = 0;
+
+--- a/src/vi_estimator/sqrt_keypoint_vio.cpp
++++ b/src/vi_estimator/sqrt_keypoint_vio.cpp
+@@ -60,6 +60,7 @@
+ const VioConfig& config_)
+ : take_kf(true),
+ frames_after_kf(0),
++ take_ltkf(false),
+ g(g_.cast()),
+ initialized(false),
+ schedule_reset(false),
+@@ -145,6 +146,7 @@
+ lmdb.clear();
+
+ take_kf = true;
++ take_ltkf = false;
+ frames_after_kf = 0;
+ kf_ids.clear();
+ ltkfs.clear();
+@@ -271,6 +273,9 @@
+ // std::cout << "Skipping IMU data.." << std::endl;
+ }
+
++ // A shutdown sentinel abandons this frame instead of dereferencing it.
++ if (!data) break;
++
+ Vec3 vel_w_i_init;
+ vel_w_i_init.setZero();
+
+@@ -318,6 +323,9 @@
+ data->accel = calib.calib_accel_bias.getCalibrated(data->accel);
+ data->gyro = calib.calib_gyro_bias.getCalibrated(data->gyro);
+ }
++
++ // A shutdown sentinel abandons this frame instead of dereferencing it.
++ if (!data) break;
+
+ while (data->t_ns <= curr_frame->t_ns) {
+ meas->integrate(*data, accel_cov, gyro_cov);
+@@ -326,6 +334,9 @@
+ data->accel = calib.calib_accel_bias.getCalibrated(data->accel);
+ data->gyro = calib.calib_gyro_bias.getCalibrated(data->gyro);
+ }
++
++ // A shutdown sentinel abandons this frame instead of dereferencing it.
++ if (!data) break;
+
+ if (meas->get_start_t_ns() + meas->get_dt_ns() < curr_frame->t_ns) {
+ if (!data.get()) break;
diff --git a/scripts/install-rifts-passthrough-overlay.py b/scripts/install-rifts-passthrough-overlay.py
new file mode 100755
index 000000000..d67a571d8
--- /dev/null
+++ b/scripts/install-rifts-passthrough-overlay.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python3
+# Copyright 2026, lbgos
+# SPDX-License-Identifier: BSL-1.0
+"""Install and enable the optional passthrough overlay in the user's systemd session."""
+import argparse
+from pathlib import Path
+import shutil
+import subprocess
+import sys
+
+
+def quote(value):
+ return '"' + str(value).replace('\\', '\\\\').replace('"', '\\"').replace('%', '%%') + '"'
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('--python', type=Path, default=Path(sys.executable), help='Python interpreter with openvr installed')
+ args = parser.parse_args()
+ python = args.python.expanduser().absolute()
+ subprocess.run([str(python), '-c', 'import openvr'], check=True)
+ target = Path.home() / '.local/lib/rifts-passthrough'
+ target.mkdir(parents=True, exist_ok=True)
+ source = Path(__file__).resolve().parent
+ for name in ('rifts-passthrough-overlay.py', 'rifts_passthrough.py'):
+ shutil.copy2(source / name, target / name)
+ units = Path.home() / '.config/systemd/user'
+ units.mkdir(parents=True, exist_ok=True)
+ unit = f'''[Unit]
+Description=Rift S passthrough overlay
+
+[Service]
+ExecStart={quote(python)} -u {quote(target / 'rifts-passthrough-overlay.py')} --start-hidden
+Restart=on-failure
+RestartSec=5
+NoNewPrivileges=true
+
+[Install]
+WantedBy=default.target
+'''
+ (units / 'rifts-passthrough-overlay.service').write_text(unit)
+ subprocess.run(['systemctl', '--user', 'daemon-reload'], check=True)
+ subprocess.run(['systemctl', '--user', 'enable', '--now', 'rifts-passthrough-overlay.service'], check=True)
+ print('Overlay installed and enabled; starts hidden. Native Room View is preferred.')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/rift-s-controller-capture.sh b/scripts/rift-s-controller-capture.sh
new file mode 100755
index 000000000..e07c3151a
--- /dev/null
+++ b/scripts/rift-s-controller-capture.sh
@@ -0,0 +1,62 @@
+#!/usr/bin/env bash
+# Copyright 2026, lbgos
+# SPDX-License-Identifier: BSL-1.0
+set -euo pipefail
+# Run with SteamVR stopped and the headset ready. Needs the xr-probe OpenXR pose logger; set XR_PROBE to its path.
+# Usage: rift-s-controller-capture.sh OUTPUT [SECONDS=80] [ENV=VALUE ...]
+# RIFTS_LOCK_OWNER names the hardware lock holder (default rift-s-capture).
+# Keep unworn runs to SECONDS <= 20, for a total service lifetime <= 30 s.
+root=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
+out=$(realpath -m "${1:?output}")
+seconds=${2:-80}
+shift "$(( $# > 1 ? 2 : 1 ))"
+if pgrep -x monado-service >/dev/null || pgrep -x vrserver >/dev/null; then
+ echo 'Hardware busy: monado-service or vrserver is running' >&2
+ exit 1
+fi
+owner=${RIFTS_LOCK_OWNER:-rift-s-capture}
+# A lock already written under our own name is accepted, so a caller can queue for it.
+if [[ $(cat /tmp/rifts-hw.lock 2>/dev/null) != "$owner" ]]; then
+ (set -o noclobber; printf '%s\n' "$owner" > /tmp/rifts-hw.lock) || exit 1
+fi
+pid=''
+cleanup() {
+ if [[ -n "$pid" ]]; then
+ kill -INT "$pid" 2>/dev/null || true
+ wait "$pid" 2>/dev/null || true
+ fi
+ rm -f /tmp/rifts-hw.lock
+}
+trap cleanup EXIT
+mkdir -p "$out/led"
+start=$(date --iso-8601=seconds)
+printf "%s\n" "$start" > "$out/started-at.txt"
+export XDG_RUNTIME_DIR=/run/user/$(id -u)
+export WAYLAND_DISPLAY=${WAYLAND_DISPLAY:-wayland-0}
+export DBUS_SESSION_BUS_ADDRESS=unix:path=$XDG_RUNTIME_DIR/bus
+export XRT_COMPOSITOR_FORCE_WAYLAND_DIRECT=1
+export SLAM_SUBMIT_FROM_START=true
+export VIT_SYSTEM_LIBRARY_PATH=/usr/local/lib/libbasalt.so
+export XRT_NO_STDIN=true
+export RIFT_S_LOG=trace CONSTELLATION_LOG=trace SLAM_LOG=info
+export RIFT_S_CONTROLLER_DUMP_DIR="$out/led"
+export XR_RUNTIME_JSON="$root/build/openxr_monado-dev.json"
+env "$@" timeout -s INT "$((seconds+10))" "$root/build/src/xrt/targets/service/monado-service" > "$out/monado.log" 2>&1 &
+pid=$!
+sleep 8
+if ! grep -q 'Controller IMU device=2 ' "$out/monado.log" ||
+ ! grep -q 'Controller IMU device=3 ' "$out/monado.log"; then
+ echo 'Both controllers must stream IMU. Stopping; wake them before the next capture.' >&2
+ exit 1
+fi
+printf "Pose capture starts now: %s seconds\n" "$seconds"
+"${XR_PROBE:?set XR_PROBE to the xr-probe binary}" --seconds "$seconds" > "$out/poses.jsonl" 2> "$out/probe.log"
+set +e
+wait "$pid"
+rc=$?
+set -e
+pid=''
+printf '%s\n' "$rc" > "$out/monado.exit"
+[[ $rc == 124 || $rc == 0 ]]
+sudo -n journalctl -k --since "$start" --no-pager -o short-monotonic > "$out/kernel.log" 2> "$out/kernel.error" || true
+printf "%s\n" "$out"
diff --git a/scripts/rifts-passthrough-overlay.py b/scripts/rifts-passthrough-overlay.py
new file mode 100755
index 000000000..68a82df0d
--- /dev/null
+++ b/scripts/rifts-passthrough-overlay.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python3
+# Copyright 2026, lbgos
+# SPDX-License-Identifier: BSL-1.0
+"""Optional head-relative stereo overlay; native Room View is preferred."""
+import argparse
+import ctypes
+import json
+import signal
+import statistics
+import time
+
+import openvr
+from rifts_passthrough import Stream
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--distance", type=float, default=1.0)
+ parser.add_argument("--width", type=float, default=2.0)
+ parser.add_argument("--start-hidden", action="store_true")
+ parser.add_argument("--test-frames", type=int, default=0)
+ parser.add_argument("--timeout", type=float, default=0)
+ args = parser.parse_args()
+ if args.distance <= 0 or args.width <= 0:
+ parser.error("distance and width must be positive")
+ stream = Stream()
+ initialized = False
+ handle = None
+ visible = not args.start_hidden
+ running = True
+ delivered = 0
+ previous = 0
+ toggles = stream.toggles
+ arrivals, ages, upload_ms = [], [], []
+
+ def toggle(*_):
+ nonlocal visible
+ visible = not visible
+
+ def stop(*_):
+ nonlocal running
+ running = False
+
+ signal.signal(signal.SIGUSR1, toggle)
+ signal.signal(signal.SIGTERM, stop)
+ signal.signal(signal.SIGINT, stop)
+ start = time.monotonic()
+ try:
+ openvr.init(openvr.VRApplication_Overlay)
+ initialized = True
+ overlay = openvr.VROverlay()
+ handle = overlay.createOverlay("rifts_passthrough", "Rift S Passthrough")
+ overlay.setOverlayFlag(handle, openvr.VROverlayFlags_SideBySide_Parallel, True)
+ overlay.setOverlayWidthInMeters(handle, args.width)
+ # The packed texture is twice as wide as one eye. Keep the quad square.
+ overlay.setOverlayTexelAspect(handle, 0.5)
+ overlay.setOverlaySortOrder(handle, 1000)
+ matrix = openvr.HmdMatrix34_t()
+ for i in range(3):
+ matrix.m[i][i] = 1
+ matrix.m[2][3] = -args.distance
+ overlay.setOverlayTransformTrackedDeviceRelative(handle, openvr.k_unTrackedDeviceIndex_Hmd, matrix)
+ vr_system = openvr.VRSystem()
+ event = openvr.VREvent_t()
+ shown = False
+ buffer = (ctypes.c_uint8 * stream.size)()
+ print("Ready: right Oculus button or left menu double-click", flush=True)
+ while running:
+ while vr_system.pollNextEvent(event):
+ if event.eventType == openvr.VREvent_Quit:
+ running = False
+ current = stream.toggles
+ if (current - toggles) & 1:
+ toggle()
+ toggles = current
+ stream.activate(visible)
+ frame = stream.frame(previous) if visible else None
+ if frame:
+ previous, timestamp, pixels, age = frame
+ # A stable private copy remains unchanged throughout SetOverlayRaw.
+ ctypes.memmove(buffer, pixels, stream.size)
+ before = time.monotonic()
+ overlay.setOverlayRaw(handle, ctypes.cast(buffer, ctypes.c_void_p), stream.width, stream.height, 4)
+ upload_ms.append((time.monotonic() - before) * 1000)
+ arrivals.append(time.monotonic())
+ ages.append(age)
+ if len(arrivals) > 600:
+ arrivals.pop(0)
+ ages.pop(0)
+ upload_ms.pop(0)
+ delivered += 1
+ if not shown:
+ overlay.showOverlay(handle)
+ shown = True
+ elif shown and (not visible or time.monotonic() - arrivals[-1] > 0.25):
+ overlay.hideOverlay(handle)
+ shown = False
+ if args.test_frames and delivered >= args.test_frames:
+ break
+ if args.timeout and time.monotonic() - start >= args.timeout:
+ break
+ time.sleep(0.005 if visible else 0.025)
+ finally:
+ stream.close()
+ try:
+ if handle is not None:
+ overlay.hideOverlay(handle)
+ overlay.destroyOverlay(handle)
+ finally:
+ if initialized:
+ openvr.shutdown()
+ intervals = [(b - a) * 1000 for a, b in zip(arrivals, arrivals[1:])]
+ print(json.dumps({"submitted_frames": delivered, "last_sequence": previous,
+ "median_interval_ms": statistics.median(intervals) if intervals else None,
+ "max_interval_ms": max(intervals) if intervals else None,
+ "median_capture_age_ms": statistics.median(ages) if ages else None,
+ "median_upload_ms": statistics.median(upload_ms) if upload_ms else None}), flush=True)
+ if args.test_frames and delivered < args.test_frames:
+ raise SystemExit("Camera did not deliver the requested frames")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/rifts-passthrough-probe.py b/scripts/rifts-passthrough-probe.py
new file mode 100755
index 000000000..6eaed25f7
--- /dev/null
+++ b/scripts/rifts-passthrough-probe.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+# Copyright 2026, lbgos
+# SPDX-License-Identifier: BSL-1.0
+"""Check advancing, non-black camera export frames and capture-to-reader age."""
+import argparse
+import hashlib
+import json
+import statistics
+import time
+from rifts_passthrough import Stream
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--timeout", type=float, default=5.0)
+ parser.add_argument("--min-frames", type=int, default=5)
+ args = parser.parse_args()
+ stream = Stream()
+ frames, ages, hashes, sequences = [], [], set(), []
+ black = 0
+ previous = 0
+ try:
+ end = time.monotonic() + args.timeout
+ while time.monotonic() < end:
+ stream.activate(True)
+ frame = stream.frame(previous)
+ if frame:
+ previous, timestamp, pixels, age = frame
+ frames.append(time.monotonic())
+ ages.append(age)
+ sequences.append(previous)
+ hashes.add(hashlib.sha256(pixels).digest())
+ black += not any(pixels[c::4].strip(b"\0") for c in range(3))
+ time.sleep(0.002)
+ finally:
+ stream.close()
+ intervals = [(b - a) * 1000 for a, b in zip(frames, frames[1:])]
+ success = len(frames) >= args.min_frames and black < len(frames)
+ print(json.dumps({"width": stream.width, "height": stream.height, "advancing_frames": len(frames),
+ "first_sequence": sequences[0] if sequences else None,
+ "last_sequence": previous, "distinct_images": len(hashes), "black_frames": black,
+ "median_delivery_hz": 1000 / statistics.median(intervals) if intervals else None,
+ "median_capture_age_ms": statistics.median(ages) if ages else None,
+ "max_capture_age_ms": max(ages) if ages else None, "success": success}, indent=2))
+ if not success:
+ raise SystemExit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/rifts_passthrough.py b/scripts/rifts_passthrough.py
new file mode 100644
index 000000000..143b0f7e5
--- /dev/null
+++ b/scripts/rifts_passthrough.py
@@ -0,0 +1,58 @@
+# Copyright 2026, lbgos
+# SPDX-License-Identifier: BSL-1.0
+"""Versioned camera export reader. One consumer owns the activation flag."""
+import fcntl
+import mmap
+import os
+import struct
+import time
+
+PATH = "/dev/shm/rifts_passthrough"
+HEADER = struct.Struct("= HEADER.size + self.size):
+ raise ValueError("Invalid camera export dimensions")
+ except BaseException:
+ if hasattr(self, "mm"):
+ self.mm.close()
+ os.close(self.fd)
+ raise
+
+ def activate(self, visible):
+ # Lease expires if the consumer crashes, so rectification cannot stay on.
+ heartbeat = int(time.monotonic() * 1000) & 0xffffffff if visible else 0
+ struct.pack_into(" size:
+ raise SystemExit("Unexpected camera buffer layout")
+ stride = max(1, header.nWidth * header.nHeight // 1000) * channels
+ black += not any(pixels[offset + c] for offset in range(0, size - channels + 1, stride)
+ for c in range(3))
+ except openvr.error_code.TrackedCameraError as error:
+ last_error = type(error).__name__
+ time.sleep(0.01)
+ intervals = [b - a for a, b in zip(arrivals, arrivals[1:])]
+ print(json.dumps({"width": width, "height": height, "bytes": size,
+ "unique_frames": len(seen), "black_frames": black,
+ "median_delivery_hz": 1 / statistics.median(intervals) if intervals else None,
+ "last_error": last_error}))
+ if len(seen) < 2:
+ raise SystemExit("No advancing video stream; inspect vrserver camera logs")
+finally:
+ if handle is not None:
+ camera.releaseVideoStreamingService(handle)
+ openvr.shutdown()
diff --git a/src/external/openvr_includes/openvr_driver.h b/src/external/openvr_includes/openvr_driver.h
index 703243fb1..b3f1ceb98 100644
--- a/src/external/openvr_includes/openvr_driver.h
+++ b/src/external/openvr_includes/openvr_driver.h
@@ -387,6 +387,7 @@ enum ETrackedDeviceProperty
Prop_ManufacturerSerialNumber_String = 1049,
Prop_ComputedSerialNumber_String = 1050,
Prop_EstimatedDeviceFirstUseTime_Int32 = 1051,
+ Prop_AllowCameraToggle_Bool = 1055, // SteamVR camera toggle control
// Properties that are unique to TrackedDeviceClass_HMD
Prop_ReportsTimeSinceVSync_Bool = 2000,
@@ -2078,6 +2079,7 @@ enum ECameraVideoStreamFormat
CVS_FORMAT_YUYV16 = 5, // 16 bits per pixel
CVS_FORMAT_BAYER16BG = 6, // 16 bits per pixel, 10-bit BG-format Bayer, see https://docs.opencv.org/3.1.0/de/d25/imgproc_color_conversions.html
CVS_FORMAT_MJPEG = 7, // variable-sized MJPEG Open DML format, see https://www.loc.gov/preservation/digital/formats/fdd/fdd000063.shtml
+ CVS_FORMAT_RGBX32 = 8, // Full-sized pixels, 4BPP, LSB = RED
CVS_MAX_FORMATS
};
diff --git a/src/xrt/auxiliary/bindings/steamvr_profiles.py b/src/xrt/auxiliary/bindings/steamvr_profiles.py
index f54a303f9..ddd516247 100644
--- a/src/xrt/auxiliary/bindings/steamvr_profiles.py
+++ b/src/xrt/auxiliary/bindings/steamvr_profiles.py
@@ -82,14 +82,15 @@ def main():
for idx, component in enumerate(p.components):
subpath_name = component.steamvr_path
- input_source[subpath_name] = {
+ # Optional extension components must not replace the base source type.
+ source = input_source.setdefault(subpath_name, {
"type": component.subpath_type,
"binding_image_point": [0, 0], # TODO
"order": idx
- }
+ })
for req in get_required_components(component.subpath_type):
- input_source[subpath_name][req] = req in component.components_for_subpath
+ source[req] = source.get(req, False) or req in component.components_for_subpath
j = {
"json_id": "input_profile",
@@ -110,6 +111,26 @@ def main():
}
+ # Monado owns the compositor binding, including native room setup.
+ if p.name == "/interaction_profiles/oculus/touch_controller":
+ j["controller_type"] = "monado_oculus_touch"
+ j["compatibility_mode_controller_type"] = "oculus_touch"
+ j["legacy_binding"] = "{oculus}/input/legacy_bindings_touch.json"
+ # The driver exposes Touch's menu button as the system button.
+ j["input_source"]["/input/system"] = j["input_source"].pop("/input/menu")
+ # SteamVR resolves these poses through the Touch render model. The
+ # compositor's Pointer action binds /pose/tip, not OpenXR /pose/aim.
+ for pose in ("raw", "tip", "handgrip", "openxr_aim", "openxr_grip"):
+ input_source["/pose/" + pose] = {
+ "type": "pose",
+ "binding_image_point": [0, 0],
+ "order": len(input_source),
+ }
+ j["default_bindings"] = [{
+ "app_key": "openvr.component.vrcompositor",
+ "binding_url": "monado_touch_compositor.json",
+ }]
+
f = open_file(args, fname)
# print("Creating SteamVR input profile", f.name)
diff --git a/src/xrt/auxiliary/math/m_clock_tracking.c b/src/xrt/auxiliary/math/m_clock_tracking.c
index 73fe5a44a..3df999bbc 100644
--- a/src/xrt/auxiliary/math/m_clock_tracking.c
+++ b/src/xrt/auxiliary/math/m_clock_tracking.c
@@ -176,7 +176,10 @@ m_clock_windowed_skew_tracker_push(struct m_clock_windowed_skew_tracker *t,
t->current_window_pos = (t->current_window_pos + 1) % t->max_window_samples;
/* Update the moving average skew */
- size_t w = t->current_window_samples;
+ /* The skew is negative whenever the remote clock runs ahead of the local one. Keep the
+ * window size signed: with a size_t, a negative skew is multiplied and divided as an unsigned
+ * 64-bit value and the average lands near INT64_MAX. */
+ time_duration_ns w = (time_duration_ns)t->current_window_samples;
t->current_skew = (t->current_min_skew + t->current_skew * (w - 1)) / w;
t->have_skew_estimate = true;
}
diff --git a/src/xrt/auxiliary/math/m_vec3.h b/src/xrt/auxiliary/math/m_vec3.h
index 4fefeffe4..d805f9b59 100644
--- a/src/xrt/auxiliary/math/m_vec3.h
+++ b/src/xrt/auxiliary/math/m_vec3.h
@@ -119,7 +119,9 @@ m_vec3_angle(struct xrt_vec3 l, struct xrt_vec3 r)
}
lengths = sqrtf(lengths);
- return acosf(dot / lengths);
+ // Nearly parallel vectors can round outside acos's domain.
+ float cosine = dot / lengths;
+ return acosf(cosine > 1 ? 1 : cosine < -1 ? -1 : cosine);
}
static inline struct xrt_vec3
diff --git a/src/xrt/auxiliary/tracking/t_camera_models.h b/src/xrt/auxiliary/tracking/t_camera_models.h
index 951fbe713..7ade3b562 100644
--- a/src/xrt/auxiliary/tracking/t_camera_models.h
+++ b/src/xrt/auxiliary/tracking/t_camera_models.h
@@ -134,7 +134,8 @@ static inline float
kb4_solve_theta(const struct t_camera_model_params *dist, const float *r_theta, float *d_func_d_theta)
{
float theta = *r_theta;
- for (int i = 4; i > 0; i--) {
+ // Wide-angle pixels need more than four Newton steps to recover their bearing.
+ for (int i = 12; i > 0; i--) {
float theta2 = theta * theta;
float func = dist->fisheye.k4 * theta2;
@@ -157,7 +158,10 @@ kb4_solve_theta(const struct t_camera_model_params *dist, const float *r_theta,
*d_func_d_theta += 1.0f;
// Iteration of Newton method
- theta += ((*r_theta) - func) / (*d_func_d_theta);
+ float step = ((*r_theta) - func) / (*d_func_d_theta);
+ theta += step;
+ if (fabsf(step) < 1e-7f)
+ break;
}
return theta;
diff --git a/src/xrt/auxiliary/tracking/t_tracker_slam.cpp b/src/xrt/auxiliary/tracking/t_tracker_slam.cpp
index 7add1f6f9..57c8b9fa8 100644
--- a/src/xrt/auxiliary/tracking/t_tracker_slam.cpp
+++ b/src/xrt/auxiliary/tracking/t_tracker_slam.cpp
@@ -40,12 +40,15 @@
#include
#include
+#include
+#include
#include
#include
#include
#include
#include