mirror of
https://github.com/lbgos/rift-s-linux.git
synced 2026-10-17 13:00:59 +02:00
Rift S: SLAM guard, Touch controller tracking, native room setup and SteamVR integration
Squashed from the working branch. Adds the Basalt patches, constrained constellation pose solver, Rift S optics, world anchor and boundary persistence, SteamVR camera queue and room setup guard, and tests.
This commit is contained in:
1 parent
b1c850a015
commit
dff6cab92c
114 files changed
+24951
-1227
No files matched your search
@@ -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.
|
||||
@@ -0,0 +1,278 @@
|
||||
# Monado - XR Runtime (XRT)
|
||||
|
||||
<!--
|
||||
Copyright 2018-2021, Collabora, Ltd.
|
||||
|
||||
SPDX-License-Identifier: CC-BY-4.0
|
||||
|
||||
This must stay in sync with the last section!
|
||||
-->
|
||||
|
||||
> * Main homepage and documentation: <https://monado.freedesktop.org/>
|
||||
> * Promotional homepage: <https://monado.dev>
|
||||
> * Maintained at <https://gitlab.freedesktop.org/monado/monado>
|
||||
> * Latest API documentation: <https://monado.pages.freedesktop.org/monado>
|
||||
> * Continuously-updated changelog of the default branch:
|
||||
> <https://monado.pages.freedesktop.org/monado/_c_h_a_n_g_e_l_o_g.html>
|
||||
|
||||
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
|
||||
<rylie.pavlik@collabora.com> and using the associated key from
|
||||
<https://keys.openpgp.org/vks/v1/by-fingerprint/45207B2B1E53E1F2755FF63CC5A2D593A61DBC9D>.
|
||||
|
||||
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 <https://www.freedesktop.org/wiki/CodeOfConduct/>,
|
||||
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 <rylie.pavlik@collabora.com>
|
||||
* Frederic Plourde <frederic.plourde@collabora.com>
|
||||
* Jakob Bornecrantz <tbornecrantz@nvidia.com>
|
||||
* freedesktop.org contacts: see most recent list at <https://www.freedesktop.org/wiki/CodeOfConduct/>
|
||||
|
||||
Code of Conduct section excerpt adapted from the
|
||||
[Contributor Covenant](https://www.contributor-covenant.org), version 1.4.1,
|
||||
available at
|
||||
<https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>, and
|
||||
from the freedesktop.org-specific version of that code, available at
|
||||
<https://www.freedesktop.org/wiki/CodeOfConduct/>, used under CC-BY-4.0.
|
||||
@@ -1,278 +1,50 @@
|
||||
# Monado - XR Runtime (XRT)
|
||||
# Rift S on Linux
|
||||
|
||||
<!--
|
||||
Copyright 2018-2021, Collabora, Ltd.
|
||||
Oculus Rift S support for Monado and SteamVR on Linux. It has inside-out head tracking with Basalt SLAM, Touch controller tracking from the controller LEDs, and room setup with a play-area boundary.
|
||||
|
||||
SPDX-License-Identifier: CC-BY-4.0
|
||||
This is a fork of thaytan's Monado Rift S branch, `dev-constellation-controller-tracking`. The original Monado readme is in `README-monado.md`.
|
||||
|
||||
This must stay in sync with the last section!
|
||||
-->
|
||||
## Status
|
||||
|
||||
> * Main homepage and documentation: <https://monado.freedesktop.org/>
|
||||
> * Promotional homepage: <https://monado.dev>
|
||||
> * Maintained at <https://gitlab.freedesktop.org/monado/monado>
|
||||
> * Latest API documentation: <https://monado.pages.freedesktop.org/monado>
|
||||
> * Continuously-updated changelog of the default branch:
|
||||
> <https://monado.pages.freedesktop.org/monado/_c_h_a_n_g_e_l_o_g.html>
|
||||
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
|
||||
<rylie.pavlik@collabora.com> and using the associated key from
|
||||
<https://keys.openpgp.org/vks/v1/by-fingerprint/45207B2B1E53E1F2755FF63CC5A2D593A61DBC9D>.
|
||||
|
||||
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 <https://www.freedesktop.org/wiki/CodeOfConduct/>,
|
||||
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 <rylie.pavlik@collabora.com>
|
||||
* Frederic Plourde <frederic.plourde@collabora.com>
|
||||
* Jakob Bornecrantz <tbornecrantz@nvidia.com>
|
||||
* freedesktop.org contacts: see most recent list at <https://www.freedesktop.org/wiki/CodeOfConduct/>
|
||||
|
||||
Code of Conduct section excerpt adapted from the
|
||||
[Contributor Covenant](https://www.contributor-covenant.org), version 1.4.1,
|
||||
available at
|
||||
<https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>, and
|
||||
from the freedesktop.org-specific version of that code, available at
|
||||
<https://www.freedesktop.org/wiki/CodeOfConduct/>, used under CC-BY-4.0.
|
||||
Contact lbgos at contact@lbgos.dev.
|
||||
@@ -0,0 +1,27 @@
|
||||
--- a/src/vit/vit_tracker.cpp
|
||||
+++ b/src/vit/vit_tracker.cpp
|
||||
@@ -127,6 +127,7 @@
|
||||
vector<bool> cam;
|
||||
} calib_data_ready;
|
||||
uint32_t cam_count;
|
||||
+ double imu_calibration_frequency = -1;
|
||||
|
||||
Calibration<double> 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
|
||||
@@ -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>(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;
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
--- a/src/vit/vit_tracker.cpp
|
||||
+++ b/src/vit/vit_tracker.cpp
|
||||
@@ -55,6 +55,7 @@
|
||||
#include <sophus/se3.hpp>
|
||||
|
||||
#include <cstdio>
|
||||
+#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
@@ -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>(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<double>::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<bool> 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 <typename T>
|
||||
+ static void push_latest(tbb::concurrent_bounded_queue<T> &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<float>();
|
||||
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<Scalar>()),
|
||||
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;
|
||||
Executable
+48
@@ -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()
|
||||
Executable
+62
@@ -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"
|
||||
Executable
+123
@@ -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()
|
||||
Executable
+50
@@ -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()
|
||||
@@ -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("<IIIIIIIIQQ")
|
||||
|
||||
|
||||
class Stream:
|
||||
def __init__(self):
|
||||
self.fd = os.open(PATH, os.O_RDWR | os.O_NOFOLLOW | os.O_CLOEXEC)
|
||||
try:
|
||||
fcntl.flock(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
self.mm = mmap.mmap(self.fd, 0)
|
||||
magic, version, self.width, self.height, stride, fmt, *_ = HEADER.unpack_from(self.mm)
|
||||
if magic != 0x54535052 or version != 2 or fmt != 0:
|
||||
raise ValueError("Incompatible camera export; install matching driver and scripts")
|
||||
self.size = stride * self.height
|
||||
if not (0 < self.width <= 640 and 0 < self.height <= 320 and stride == self.width * 4
|
||||
and len(self.mm) >= 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("<I", self.mm, 24, heartbeat)
|
||||
|
||||
@property
|
||||
def toggles(self):
|
||||
return struct.unpack_from("<I", self.mm, 28)[0]
|
||||
|
||||
def frame(self, previous):
|
||||
for _ in range(3):
|
||||
sequence = struct.unpack_from("<Q", self.mm, 32)[0]
|
||||
if sequence == previous or sequence == 0 or sequence & 1:
|
||||
return None
|
||||
timestamp = struct.unpack_from("<Q", self.mm, 40)[0]
|
||||
pixels = self.mm[HEADER.size:HEADER.size + self.size]
|
||||
if sequence == struct.unpack_from("<Q", self.mm, 32)[0]:
|
||||
age_ms = (time.monotonic_ns() - timestamp) / 1e6
|
||||
if 0 <= age_ms <= 250:
|
||||
return sequence, timestamp, pixels, age_ms
|
||||
return None
|
||||
|
||||
def close(self):
|
||||
self.activate(False)
|
||||
self.mm.close()
|
||||
os.close(self.fd)
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright 2026, lbgos
|
||||
# SPDX-License-Identifier: BSL-1.0
|
||||
"""Check driver_monado's tracked camera through an existing SteamVR session.
|
||||
|
||||
Requires Python openvr. Opens no V4L2/HID devices and writes no camera images.
|
||||
"""
|
||||
import ctypes
|
||||
import json
|
||||
import statistics
|
||||
import time
|
||||
import openvr
|
||||
|
||||
openvr.init(openvr.VRApplication_Background)
|
||||
handle = None
|
||||
try:
|
||||
camera = openvr.VRTrackedCamera()
|
||||
if not camera.hasCamera(0):
|
||||
raise SystemExit("The active SteamVR driver does not advertise a camera")
|
||||
kind = openvr.VRTrackedCameraFrameType_Undistorted
|
||||
width, height, size = camera.getCameraFrameSize(0, kind)
|
||||
pixels = (ctypes.c_uint8 * size)()
|
||||
handle = camera.acquireVideoStreamingService(0)
|
||||
seen = set()
|
||||
arrivals = []
|
||||
black = 0
|
||||
last_error = None
|
||||
end = time.monotonic() + 5
|
||||
while time.monotonic() < end:
|
||||
try:
|
||||
header = camera.getVideoStreamFrameBuffer(handle, kind, pixels, size)
|
||||
if header.nFrameSequence not in seen:
|
||||
seen.add(header.nFrameSequence)
|
||||
arrivals.append(time.monotonic())
|
||||
# Ignore alpha when SteamVR converts RGBX32 to public RGBA buffers.
|
||||
channels = header.nBytesPerPixel
|
||||
if channels not in (3, 4) or header.nWidth * header.nHeight * channels > 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()
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -40,12 +40,15 @@
|
||||
#include <opencv2/core/mat.hpp>
|
||||
#include <opencv2/core/version.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <deque>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -83,9 +86,13 @@ DEBUG_GET_ONCE_LOG_OPTION(slam_log, "SLAM_LOG", U_LOGGING_INFO)
|
||||
DEBUG_GET_ONCE_OPTION(vit_system_library_path, "VIT_SYSTEM_LIBRARY_PATH", PREFERRED_VIT_SYSTEM_LIBRARY)
|
||||
DEBUG_GET_ONCE_OPTION(slam_config, "SLAM_CONFIG", nullptr)
|
||||
DEBUG_GET_ONCE_BOOL_OPTION(slam_ui, "SLAM_UI", false)
|
||||
// Unknown backends remain opt-in; safety-patched backends advertise recreation support.
|
||||
DEBUG_GET_ONCE_OPTION(slam_backend_recreate_override, "SLAM_BACKEND_RECREATE", nullptr)
|
||||
DEBUG_GET_ONCE_BOOL_OPTION(slam_backend_recreate, "SLAM_BACKEND_RECREATE", false)
|
||||
DEBUG_GET_ONCE_BOOL_OPTION(slam_submit_from_start, "SLAM_SUBMIT_FROM_START", false)
|
||||
DEBUG_GET_ONCE_NUM_OPTION(slam_openvr_groundtruth_device, "SLAM_OPENVR_GROUNDTRUTH_DEVICE", 0)
|
||||
DEBUG_GET_ONCE_NUM_OPTION(slam_prediction_type, "SLAM_PREDICTION_TYPE", long(SLAM_PRED_DEAD_RECKONING))
|
||||
DEBUG_GET_ONCE_OPTION(slam_record_path, "SLAM_RECORD_PATH", nullptr)
|
||||
DEBUG_GET_ONCE_BOOL_OPTION(slam_write_csvs, "SLAM_WRITE_CSVS", false)
|
||||
DEBUG_GET_ONCE_OPTION(slam_csv_path, "SLAM_CSV_PATH", "evaluation/")
|
||||
DEBUG_GET_ONCE_BOOL_OPTION(slam_timing_stat, "SLAM_TIMING_STAT", true)
|
||||
@@ -233,6 +240,11 @@ struct TimingWriter : public CSVWriter<timing_sample>
|
||||
TimingWriter(const string &dir, const string &fn, bool e, const vector<string> &cn)
|
||||
: CSVWriter<timing_sample>(dir, fn, e, cn)
|
||||
{}
|
||||
void
|
||||
set_columns(const vector<string> &columns)
|
||||
{
|
||||
column_names = columns;
|
||||
}
|
||||
};
|
||||
|
||||
//! Writes feature information specific to a particular estimated pose
|
||||
@@ -260,6 +272,16 @@ struct FeaturesWriter : public CSVWriter<feature_count_sample>
|
||||
*/
|
||||
struct TrackerSlam
|
||||
{
|
||||
// Backend replacement excludes users; camera and IMU submission may run concurrently.
|
||||
std::shared_mutex backend_mutex;
|
||||
// Protect pose history and prediction/filter state without holding it during queue pushes.
|
||||
std::mutex state_mutex;
|
||||
vit_config system_config{};
|
||||
t_slam_calibration calibration{};
|
||||
bool use_driver_calibration = false;
|
||||
bool started = false;
|
||||
bool backend_recreate = false;
|
||||
|
||||
struct xrt_tracked_slam base = {};
|
||||
struct xrt_frame_node node = {}; //!< Will be called on destruction
|
||||
struct t_vit_bundle vit; //!< VIT system function pointers
|
||||
@@ -390,6 +412,9 @@ struct TrackerSlam
|
||||
u_var_curves fcs_ui; //!< Display of `fcs` in UI
|
||||
|
||||
bool enabled = false; //!< Whether the features extension is enabled
|
||||
//! Features tracked over all cameras in the latest pose, -1 when unknown.
|
||||
int last_total = -1; // Protected with last_total_ts by state_mutex.
|
||||
timepoint_ns last_total_ts = 0;
|
||||
struct u_var_button enable_btn; //!< Toggle extension
|
||||
} features;
|
||||
|
||||
@@ -423,15 +448,19 @@ timing_ui_setup(TrackerSlam &t)
|
||||
static const char *msg[2] = {"[OFF] Enable timing", "[ON] Disable timing"};
|
||||
u_var_button_cb cb = [](void *t_ptr) {
|
||||
TrackerSlam *t = (TrackerSlam *)t_ptr;
|
||||
std::unique_lock backend_lock(t->backend_mutex);
|
||||
std::lock_guard lock(t->state_mutex);
|
||||
if (t->tracker == nullptr)
|
||||
return;
|
||||
u_var_button &btn = t->timing.enable_btn;
|
||||
bool e = !t->timing.enabled;
|
||||
snprintf(btn.label, sizeof(btn.label), "%s", msg[e]);
|
||||
vit_result_t vres = t->vit.tracker_enable_extension(t->tracker, VIT_TRACKER_EXTENSION_POSE_TIMING, e);
|
||||
if (vres != VIT_SUCCESS) {
|
||||
U_LOG_IFL_E(t->log_level, "Failed to set tracker timing extension");
|
||||
return;
|
||||
}
|
||||
t->timing.enabled = e;
|
||||
snprintf(btn.label, sizeof(btn.label), "%s", msg[e]);
|
||||
};
|
||||
t.timing.enable_btn.cb = cb;
|
||||
t.timing.enable_btn.disabled = !t.exts.has_pose_timing;
|
||||
@@ -543,15 +572,19 @@ features_ui_setup(TrackerSlam &t)
|
||||
static const char *msg[2] = {"[OFF] Enable features info", "[ON] Disable features info"};
|
||||
u_var_button_cb cb = [](void *t_ptr) {
|
||||
TrackerSlam *t = (TrackerSlam *)t_ptr;
|
||||
std::unique_lock backend_lock(t->backend_mutex);
|
||||
std::lock_guard lock(t->state_mutex);
|
||||
if (t->tracker == nullptr)
|
||||
return;
|
||||
u_var_button &btn = t->features.enable_btn;
|
||||
bool e = !t->features.enabled;
|
||||
snprintf(btn.label, sizeof(btn.label), "%s", msg[e]);
|
||||
vit_result_t vres = t->vit.tracker_enable_extension(t->tracker, VIT_TRACKER_EXTENSION_POSE_FEATURES, e);
|
||||
if (vres != VIT_SUCCESS) {
|
||||
U_LOG_IFL_E(t->log_level, "Failed to set tracker features extension");
|
||||
return;
|
||||
}
|
||||
t->features.enabled = e;
|
||||
snprintf(btn.label, sizeof(btn.label), "%s", msg[e]);
|
||||
};
|
||||
t.features.enable_btn.cb = cb;
|
||||
t.features.enable_btn.disabled = !t.exts.has_pose_features;
|
||||
@@ -773,8 +806,8 @@ flush_poses(TrackerSlam &t)
|
||||
|
||||
// Last relation
|
||||
xrt_space_relation lr = XRT_SPACE_RELATION_ZERO;
|
||||
int64_t lts;
|
||||
t.slam_rels.get_latest(<s, &lr);
|
||||
int64_t lts = nts;
|
||||
bool have_previous = t.slam_rels.get_latest(<s, &lr);
|
||||
xrt_quat lrot = lr.pose.orientation;
|
||||
|
||||
double dt = time_ns_to_s(nts - lts);
|
||||
@@ -787,7 +820,9 @@ flush_poses(TrackerSlam &t)
|
||||
rel.relation_flags = XRT_SPACE_RELATION_BITMASK_ALL;
|
||||
rel.pose = {nrot, npos};
|
||||
rel.linear_velocity = nvel;
|
||||
math_quat_finite_difference(&lrot, &nrot, dt, &rel.angular_velocity);
|
||||
if (have_previous && dt > 0) {
|
||||
math_quat_finite_difference(&lrot, &nrot, dt, &rel.angular_velocity);
|
||||
}
|
||||
|
||||
// Push to relationship history unless we are debugging prediction
|
||||
if (t.dbg_pred_counter % t.dbg_pred_every == 0) {
|
||||
@@ -806,6 +841,14 @@ flush_poses(TrackerSlam &t)
|
||||
if (t.features.enabled) {
|
||||
vector feat_count = features_ui_push(t, pose, nts);
|
||||
t.slam_features_writer->push({nts, feat_count});
|
||||
if (!feat_count.empty()) {
|
||||
int total = 0;
|
||||
for (int c : feat_count) {
|
||||
total += c;
|
||||
}
|
||||
t.features.last_total = total;
|
||||
t.features.last_total_ts = nts;
|
||||
}
|
||||
}
|
||||
|
||||
t.vit.pose_destroy(pose);
|
||||
@@ -972,8 +1015,9 @@ setup_ui(TrackerSlam &t)
|
||||
u_var_button_cb reset_state_cb = [](void *t_ptr) {
|
||||
TrackerSlam &t = *(TrackerSlam *)t_ptr;
|
||||
|
||||
vit_result_t vres = t.vit.tracker_reset(t.tracker);
|
||||
if (vres != VIT_SUCCESS) {
|
||||
std::unique_lock backend_lock(t.backend_mutex);
|
||||
std::lock_guard lock(t.state_mutex);
|
||||
if (t.started && t.vit.tracker_reset(t.tracker) != VIT_SUCCESS) {
|
||||
SLAM_WARN("Failed to reset VIT tracker");
|
||||
}
|
||||
};
|
||||
@@ -1084,6 +1128,10 @@ add_camera_calibration(const TrackerSlam &t, const t_slam_camera_calibration *ca
|
||||
for (size_t i = 0; i < ARRAY_SIZE(params.transform); ++i)
|
||||
params.transform[i] = T.v[i];
|
||||
|
||||
SLAM_DEBUG("DIAG VIT cam%u %ux%u fx=%f fy=%f cx=%f cy=%f hz=%f", cam_index, params.width,
|
||||
params.height, params.fx, params.fy, params.cx, params.cy, params.frequency);
|
||||
for (size_t i = 0; i < 16; i++) SLAM_DEBUG("DIAG VIT cam%u T[%zu]=%.9g", cam_index, i, params.transform[i]);
|
||||
for (size_t i = 0; i < params.distortion_count; i++) SLAM_DEBUG("DIAG VIT cam%u d[%zu]=%.9g", cam_index, i, params.distortion[i]);
|
||||
vit_result_t vres = t.vit.tracker_add_camera_calibration(t.tracker, ¶ms);
|
||||
if (vres != VIT_SUCCESS) {
|
||||
SLAM_ERROR("Failed to add camera calibration for camera %u", cam_index);
|
||||
@@ -1149,6 +1197,146 @@ using namespace xrt::auxiliary::tracking::slam;
|
||||
*
|
||||
*/
|
||||
|
||||
extern "C" int
|
||||
t_slam_restart(struct xrt_tracked_slam *xts)
|
||||
{
|
||||
auto &t = *container_of(xts, TrackerSlam, base);
|
||||
if (!t.backend_recreate) {
|
||||
SLAM_WARN("SLAM_BACKEND_RECREATE is disabled; enable only with a restart-compatible backend");
|
||||
return -1;
|
||||
}
|
||||
std::unique_lock backend_lock(t.backend_mutex);
|
||||
std::lock_guard lock(t.state_mutex);
|
||||
if (t.started && t.vit.tracker_stop(t.tracker) != VIT_SUCCESS) {
|
||||
SLAM_ERROR("Failed to stop VIT session for restart");
|
||||
return -1;
|
||||
}
|
||||
t.started = false;
|
||||
if (t.tracker != nullptr) {
|
||||
t.vit.tracker_destroy(t.tracker);
|
||||
t.tracker = nullptr;
|
||||
}
|
||||
|
||||
/* VIT reset is asynchronous in Basalt and leaves its input queues and
|
||||
* optical flow alive. Recreate the backend so no pre-gap state can escape. */
|
||||
t.slam_rels.clear();
|
||||
t.last_rel = XRT_SPACE_RELATION_ZERO;
|
||||
t.last_ts = 0;
|
||||
t.last_imu_ts = INT64_MIN;
|
||||
std::fill(t.last_cam_ts.begin(), t.last_cam_ts.end(), INT64_MIN);
|
||||
t.dbg_pred_counter = 0;
|
||||
t.features.last_total = -1;
|
||||
{
|
||||
unique_lock lock(t.last_hand_masks_mutex);
|
||||
t.last_hand_masks = {};
|
||||
}
|
||||
{
|
||||
unique_lock lock(t.last_controller_masks_mutex);
|
||||
t.last_controller_masks = {};
|
||||
}
|
||||
os_mutex_lock(&t.lock_ff);
|
||||
m_ff_vec3_f32_free(&t.gyro_ff);
|
||||
m_ff_vec3_f32_free(&t.accel_ff);
|
||||
m_ff_vec3_f32_alloc(&t.gyro_ff, 1000);
|
||||
m_ff_vec3_f32_alloc(&t.accel_ff, 1000);
|
||||
os_mutex_unlock(&t.lock_ff);
|
||||
m_ff_vec3_f32_free(&t.filter.pos_ff);
|
||||
m_ff_vec3_f32_free(&t.filter.rot_ff);
|
||||
m_ff_vec3_f32_alloc(&t.filter.pos_ff, 1000);
|
||||
m_ff_vec3_f32_alloc(&t.filter.rot_ff, 1000);
|
||||
t.filter.last = XRT_SPACE_RELATION_ZERO;
|
||||
t.filter.target = XRT_SPACE_RELATION_ZERO;
|
||||
m_filter_euro_vec3_init(&t.filter.pos_oe, t.filter.min_cutoff, t.filter.min_dcutoff, t.filter.beta);
|
||||
m_filter_euro_quat_init(&t.filter.rot_oe, t.filter.min_cutoff, t.filter.min_dcutoff, t.filter.beta);
|
||||
|
||||
if (t.vit.tracker_create(&t.system_config, &t.tracker) != VIT_SUCCESS) {
|
||||
SLAM_ERROR("Failed to recreate VIT tracker");
|
||||
return -1;
|
||||
}
|
||||
if (t.use_driver_calibration) {
|
||||
send_calibration(t, t.calibration);
|
||||
}
|
||||
if (t.timing.enabled) {
|
||||
t.vit.tracker_enable_extension(t.tracker, VIT_TRACKER_EXTENSION_POSE_TIMING, true);
|
||||
}
|
||||
if (t.features.enabled) {
|
||||
t.vit.tracker_enable_extension(t.tracker, VIT_TRACKER_EXTENSION_POSE_FEATURES, true);
|
||||
}
|
||||
if (t.vit.tracker_start(t.tracker) != VIT_SUCCESS) {
|
||||
SLAM_ERROR("Failed to start replacement VIT tracker");
|
||||
return -1;
|
||||
}
|
||||
t.started = true;
|
||||
SLAM_INFO("VIT session restarted; cleared input queues and prediction history");
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" int
|
||||
t_slam_reset(struct xrt_tracked_slam *xts)
|
||||
{
|
||||
auto &t = *container_of(xts, TrackerSlam, base);
|
||||
std::unique_lock backend_lock(t.backend_mutex);
|
||||
std::lock_guard lock(t.state_mutex);
|
||||
if (!t.started || t.vit.tracker_reset(t.tracker) != VIT_SUCCESS) {
|
||||
SLAM_WARN("Failed to reset VIT tracker");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Forget the poses of the previous map so prediction and filters start from the new one.
|
||||
t.slam_rels.clear();
|
||||
t.last_rel = XRT_SPACE_RELATION_ZERO;
|
||||
t.last_ts = 0;
|
||||
t.filter.last = XRT_SPACE_RELATION_ZERO;
|
||||
t.filter.target = XRT_SPACE_RELATION_ZERO;
|
||||
m_filter_euro_vec3_init(&t.filter.pos_oe, t.filter.min_cutoff, t.filter.min_dcutoff, t.filter.beta);
|
||||
m_filter_euro_quat_init(&t.filter.rot_oe, t.filter.min_cutoff, t.filter.min_dcutoff, t.filter.beta);
|
||||
t.features.last_total = -1;
|
||||
|
||||
SLAM_INFO("VIT tracker reset; cleared prediction history");
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" bool
|
||||
t_slam_get_feature_count(struct xrt_tracked_slam *xts, int *out_count, timepoint_ns *out_ts)
|
||||
{
|
||||
auto &t = *container_of(xts, TrackerSlam, base);
|
||||
std::unique_lock lock(t.state_mutex, std::try_to_lock);
|
||||
if (!lock.owns_lock())
|
||||
return false;
|
||||
int count = t.features.last_total;
|
||||
if (count < 0) {
|
||||
return false;
|
||||
}
|
||||
*out_count = count;
|
||||
*out_ts = t.features.last_total_ts;
|
||||
return true;
|
||||
}
|
||||
|
||||
extern "C" bool
|
||||
t_slam_get_latest_sample(struct xrt_tracked_slam *xts,
|
||||
timepoint_ns when_ns,
|
||||
timepoint_ns *out_ts,
|
||||
struct xrt_space_relation *out_relation,
|
||||
struct xrt_space_relation *out_query_relation)
|
||||
{
|
||||
auto &t = *container_of(xts, TrackerSlam, base);
|
||||
*out_relation = XRT_SPACE_RELATION_ZERO;
|
||||
*out_query_relation = XRT_SPACE_RELATION_ZERO;
|
||||
std::shared_lock backend_lock(t.backend_mutex, std::try_to_lock);
|
||||
if (!backend_lock.owns_lock()) {
|
||||
return false;
|
||||
}
|
||||
std::lock_guard lock(t.state_mutex);
|
||||
if (!t.started) {
|
||||
return false;
|
||||
}
|
||||
flush_poses(t);
|
||||
// Keep the configured IMU prediction for display poses. The guard consumes only
|
||||
// the source sample below, so historical camera queries cannot change its cadence.
|
||||
predict_pose(t, when_ns, out_query_relation);
|
||||
return t.slam_rels.get_latest(out_ts, out_relation);
|
||||
}
|
||||
|
||||
//! Get a filtered prediction from the SLAM tracked poses.
|
||||
extern "C" void
|
||||
t_slam_get_tracked_pose(struct xrt_tracked_slam *xts, timepoint_ns when_ns, struct xrt_space_relation *out_relation)
|
||||
@@ -1157,6 +1345,17 @@ t_slam_get_tracked_pose(struct xrt_tracked_slam *xts, timepoint_ns when_ns, stru
|
||||
|
||||
auto &t = *container_of(xts, TrackerSlam, base);
|
||||
|
||||
*out_relation = XRT_SPACE_RELATION_ZERO;
|
||||
// Report no pose rather than stall the caller while the backend is being replaced.
|
||||
std::shared_lock backend_lock(t.backend_mutex, std::try_to_lock);
|
||||
if (!backend_lock.owns_lock()) {
|
||||
return;
|
||||
}
|
||||
std::lock_guard lock(t.state_mutex);
|
||||
if (!t.started) {
|
||||
return;
|
||||
}
|
||||
|
||||
//! @todo This should not be cached, the same timestamp can be requested at a
|
||||
//! later time on the frame for a better prediction.
|
||||
if (when_ns == t.last_ts) {
|
||||
@@ -1227,19 +1426,33 @@ t_slam_receive_imu(struct xrt_imu_sink *sink, struct xrt_imu_sample *s)
|
||||
|
||||
auto &t = *container_of(sink, TrackerSlam, imu_sink);
|
||||
|
||||
std::shared_lock backend_lock(t.backend_mutex, std::try_to_lock);
|
||||
if (!backend_lock.owns_lock() || !t.started) {
|
||||
return;
|
||||
}
|
||||
|
||||
timepoint_ns ts = s->timestamp_ns;
|
||||
xrt_vec3_f64 a = s->accel_m_s2;
|
||||
xrt_vec3_f64 w = s->gyro_rad_secs;
|
||||
|
||||
timepoint_ns now = (timepoint_ns)os_monotonic_get_ns();
|
||||
SLAM_TRACE("[%ld] imu t=%ld a=[%f,%f,%f] w=[%f,%f,%f]", now, ts, a.x, a.y, a.z, w.x, w.y, w.z);
|
||||
// Check monotonically increasing timestamps
|
||||
if (ts <= t.last_imu_ts) {
|
||||
SLAM_WARN("Sample (%" PRId64 ") is older than last (%" PRId64 ") by %" PRId64 " ns", ts, t.last_imu_ts,
|
||||
t.last_imu_ts - ts);
|
||||
return;
|
||||
{
|
||||
std::lock_guard lock(t.state_mutex);
|
||||
// Check monotonically increasing timestamps.
|
||||
if (ts <= t.last_imu_ts) {
|
||||
SLAM_WARN("Sample (%" PRId64 ") is older than last (%" PRId64 ") by %" PRId64 " ns", ts,
|
||||
t.last_imu_ts, t.last_imu_ts - ts);
|
||||
return;
|
||||
}
|
||||
t.last_imu_ts = ts;
|
||||
struct xrt_vec3 gyro = {(float)w.x, (float)w.y, (float)w.z};
|
||||
struct xrt_vec3 accel = {(float)a.x, (float)a.y, (float)a.z};
|
||||
os_mutex_lock(&t.lock_ff);
|
||||
m_ff_vec3_f32_push(t.gyro_ff, &gyro, ts);
|
||||
m_ff_vec3_f32_push(t.accel_ff, &accel, ts);
|
||||
os_mutex_unlock(&t.lock_ff);
|
||||
}
|
||||
t.last_imu_ts = ts;
|
||||
|
||||
//! @todo There are many conversions like these between xrt and
|
||||
//! slam_tracker.hpp types. Implement a casting mechanism to avoid copies.
|
||||
@@ -1257,13 +1470,6 @@ t_slam_receive_imu(struct xrt_imu_sink *sink, struct xrt_imu_sample *s)
|
||||
}
|
||||
|
||||
xrt_sink_push_imu(t.euroc_recorder->imu, s);
|
||||
|
||||
struct xrt_vec3 gyro = {(float)w.x, (float)w.y, (float)w.z};
|
||||
struct xrt_vec3 accel = {(float)a.x, (float)a.y, (float)a.z};
|
||||
os_mutex_lock(&t.lock_ff);
|
||||
m_ff_vec3_f32_push(t.gyro_ff, &gyro, ts);
|
||||
m_ff_vec3_f32_push(t.accel_ff, &accel, ts);
|
||||
os_mutex_unlock(&t.lock_ff);
|
||||
}
|
||||
|
||||
//! Push the frame to the external SLAM system
|
||||
@@ -1272,6 +1478,11 @@ receive_frame(TrackerSlam &t, struct xrt_frame *frame, uint32_t cam_index)
|
||||
{
|
||||
XRT_TRACE_MARKER();
|
||||
|
||||
std::shared_lock backend_lock(t.backend_mutex);
|
||||
if (!t.started) {
|
||||
return;
|
||||
}
|
||||
|
||||
SLAM_DASSERT_(frame->timestamp < INT64_MAX);
|
||||
|
||||
// Return early if we don't submit
|
||||
@@ -1279,21 +1490,21 @@ receive_frame(TrackerSlam &t, struct xrt_frame *frame, uint32_t cam_index)
|
||||
return;
|
||||
}
|
||||
|
||||
if (cam_index == t.cam_count - 1) {
|
||||
flush_poses(t); // Useful to flush SLAM poses when no openxr app is open
|
||||
}
|
||||
|
||||
SLAM_DASSERT(t.last_cam_ts[0] != INT64_MIN || cam_index == 0, "First frame was not a cam0 frame");
|
||||
|
||||
// Check monotonically increasing timestamps
|
||||
timepoint_ns &last_ts = t.last_cam_ts[cam_index];
|
||||
timepoint_ns ts = (int64_t)frame->timestamp;
|
||||
SLAM_TRACE("[%" PRId64 "] cam%d frame t=%" PRId64, os_monotonic_get_ns(), cam_index, ts);
|
||||
if (last_ts >= ts) {
|
||||
SLAM_WARN("Frame (%" PRId64 ") is older than last (%" PRId64 ") by %" PRId64 " ns", ts, last_ts,
|
||||
last_ts - ts);
|
||||
{
|
||||
std::lock_guard lock(t.state_mutex);
|
||||
if (cam_index == t.cam_count - 1) {
|
||||
flush_poses(t); // Flush even when no app requests poses.
|
||||
}
|
||||
SLAM_DASSERT(t.last_cam_ts[0] != INT64_MIN || cam_index == 0, "First frame was not a cam0 frame");
|
||||
timepoint_ns &last_ts = t.last_cam_ts[cam_index];
|
||||
SLAM_TRACE("[%" PRId64 "] cam%d frame t=%" PRId64, os_monotonic_get_ns(), cam_index, ts);
|
||||
if (last_ts >= ts) {
|
||||
SLAM_WARN("Frame (%" PRId64 ") is older than last (%" PRId64 ") by %" PRId64 " ns", ts, last_ts,
|
||||
last_ts - ts);
|
||||
}
|
||||
last_ts = ts;
|
||||
}
|
||||
last_ts = ts;
|
||||
|
||||
// Construct and send the image sample
|
||||
vit_img_sample sample = {};
|
||||
@@ -1382,12 +1593,18 @@ t_slam_node_break_apart(struct xrt_frame_node *node)
|
||||
t_openvr_tracker_stop(t.ovr_tracker);
|
||||
}
|
||||
|
||||
std::unique_lock backend_lock(t.backend_mutex);
|
||||
std::lock_guard lock(t.state_mutex);
|
||||
if (!t.started) {
|
||||
return;
|
||||
}
|
||||
vit_result_t vres = t.vit.tracker_stop(t.tracker);
|
||||
if (vres != VIT_SUCCESS) {
|
||||
SLAM_ERROR("Failed to stop VIT tracker");
|
||||
return;
|
||||
}
|
||||
|
||||
t.started = false;
|
||||
SLAM_DEBUG("SLAM tracker dismantled");
|
||||
}
|
||||
|
||||
@@ -1397,6 +1614,7 @@ t_slam_node_destroy(struct xrt_frame_node *node)
|
||||
auto t_ptr = container_of(node, TrackerSlam, node);
|
||||
auto &t = *t_ptr; // Needed by SLAM_DEBUG
|
||||
SLAM_DEBUG("Destroying SLAM tracker");
|
||||
u_var_remove_root(t_ptr);
|
||||
if (t.ovr_tracker != NULL) {
|
||||
t_openvr_tracker_destroy(t.ovr_tracker);
|
||||
}
|
||||
@@ -1406,7 +1624,6 @@ t_slam_node_destroy(struct xrt_frame_node *node)
|
||||
delete t.slam_traj_writer;
|
||||
delete t.pred_traj_writer;
|
||||
delete t.filt_traj_writer;
|
||||
u_var_remove_root(t_ptr);
|
||||
for (size_t i = 0; i < t.ui_sink.size(); i++) {
|
||||
u_sink_debug_destroy(&t.ui_sink[i]);
|
||||
}
|
||||
@@ -1416,8 +1633,15 @@ t_slam_node_destroy(struct xrt_frame_node *node)
|
||||
m_ff_vec3_f32_free(&t.filter.pos_ff);
|
||||
m_ff_vec3_f32_free(&t.filter.rot_ff);
|
||||
|
||||
t_ptr->vit.tracker_destroy(t_ptr->tracker);
|
||||
t_vit_bundle_unload(&t_ptr->vit);
|
||||
{
|
||||
std::unique_lock backend_lock(t.backend_mutex);
|
||||
std::lock_guard lock(t.state_mutex);
|
||||
if (t.tracker != nullptr) {
|
||||
t.vit.tracker_destroy(t.tracker);
|
||||
t.tracker = nullptr;
|
||||
}
|
||||
t_vit_bundle_unload(&t.vit);
|
||||
}
|
||||
|
||||
delete t_ptr;
|
||||
}
|
||||
@@ -1426,12 +1650,14 @@ extern "C" int
|
||||
t_slam_start(struct xrt_tracked_slam *xts)
|
||||
{
|
||||
auto &t = *container_of(xts, TrackerSlam, base);
|
||||
std::unique_lock backend_lock(t.backend_mutex);
|
||||
vit_result_t vres = t.vit.tracker_start(t.tracker);
|
||||
if (vres != VIT_SUCCESS) {
|
||||
SLAM_ERROR("Failed to start VIT tracker");
|
||||
return -1;
|
||||
}
|
||||
|
||||
t.started = true;
|
||||
SLAM_DEBUG("SLAM tracker started");
|
||||
return 0;
|
||||
}
|
||||
@@ -1480,6 +1706,12 @@ t_slam_create(struct xrt_frame_context *xfctx,
|
||||
return -1;
|
||||
}
|
||||
|
||||
t.backend_recreate = debug_get_option_slam_backend_recreate_override() != nullptr
|
||||
? debug_get_bool_option_slam_backend_recreate()
|
||||
: t.vit.tracker_supports_safe_recreation && t.vit.tracker_supports_safe_recreation();
|
||||
SLAM_INFO("Backend recreation enabled=%d advertised_safe=%d", t.backend_recreate,
|
||||
t.vit.tracker_supports_safe_recreation != nullptr);
|
||||
|
||||
// Check the user has provided a SLAM_CONFIG file
|
||||
const char *config_file = config->slam_config;
|
||||
bool some_calib = config->slam_calib != nullptr;
|
||||
@@ -1506,6 +1738,12 @@ t_slam_create(struct xrt_frame_context *xfctx,
|
||||
}
|
||||
|
||||
t.base.get_tracked_pose = t_slam_get_tracked_pose;
|
||||
t.base.restart = t_slam_restart;
|
||||
t.system_config = system_config;
|
||||
t.use_driver_calibration = !config_file;
|
||||
if (t.use_driver_calibration) {
|
||||
t.calibration = *config->slam_calib;
|
||||
}
|
||||
|
||||
if (!config_file) {
|
||||
SLAM_INFO("Using calibration from driver and default pipeline settings");
|
||||
@@ -1541,7 +1779,8 @@ t_slam_create(struct xrt_frame_context *xfctx,
|
||||
|
||||
xrt_frame_context_add(xfctx, &t.node);
|
||||
|
||||
t.euroc_recorder = euroc_recorder_create(xfctx, NULL, t.cam_count, false);
|
||||
t.euroc_recorder = euroc_recorder_create(xfctx, debug_get_option_slam_record_path(), t.cam_count,
|
||||
debug_get_option_slam_record_path() != nullptr);
|
||||
|
||||
t.last_imu_ts = INT64_MIN;
|
||||
t.last_cam_ts = vector<timepoint_ns>(t.cam_count, INT64_MIN);
|
||||
@@ -1565,6 +1804,20 @@ t_slam_create(struct xrt_frame_context *xfctx,
|
||||
t.filt_traj_writer = new TrajectoryWriter(dir, "filtering.csv", write_csvs);
|
||||
|
||||
setup_ui(t);
|
||||
t.slam_times_writer->set_columns(t.timing.columns);
|
||||
if (config->timing_stat && t.exts.has_pose_timing &&
|
||||
t.vit.tracker_enable_extension(t.tracker, VIT_TRACKER_EXTENSION_POSE_TIMING, true) == VIT_SUCCESS) {
|
||||
t.timing.enabled = true;
|
||||
snprintf(t.timing.enable_btn.label, sizeof(t.timing.enable_btn.label), "%s", "[ON] Disable timing");
|
||||
}
|
||||
|
||||
// Feature counts let drivers notice cameras that see nothing (headset face down or covered).
|
||||
if (config->features_stat && t.exts.has_pose_features &&
|
||||
t.vit.tracker_enable_extension(t.tracker, VIT_TRACKER_EXTENSION_POSE_FEATURES, true) == VIT_SUCCESS) {
|
||||
t.features.enabled = true;
|
||||
snprintf(t.features.enable_btn.label, sizeof(t.features.enable_btn.label), "%s",
|
||||
"[ON] Disable features info");
|
||||
}
|
||||
|
||||
// Setup OpenVR groundtruth tracker
|
||||
if (config->openvr_groundtruth_device > 0) {
|
||||
|
||||
@@ -699,6 +699,28 @@ t_slam_create(struct xrt_frame_context *xfctx,
|
||||
int
|
||||
t_slam_start(struct xrt_tracked_slam *xts);
|
||||
|
||||
/*!
|
||||
* Re-initialise the SLAM system and drop the poses of the previous map.
|
||||
*
|
||||
* @public @memberof xrt_tracked_slam
|
||||
*/
|
||||
int
|
||||
t_slam_reset(struct xrt_tracked_slam *xts);
|
||||
|
||||
/*!
|
||||
* Features tracked over all cameras in the latest SLAM pose. Returns false while unknown (the
|
||||
* backend has no feature extension, or no pose since the last reset).
|
||||
*
|
||||
* @public @memberof xrt_tracked_slam
|
||||
*/
|
||||
bool
|
||||
t_slam_get_feature_count(struct xrt_tracked_slam *xts, int *out_count, timepoint_ns *out_ts);
|
||||
|
||||
//! Read source and prediction under one backend/history lock. Guard only the source timestamp.
|
||||
bool
|
||||
t_slam_get_latest_sample(struct xrt_tracked_slam *xts, timepoint_ns when_ns, timepoint_ns *out_ts,
|
||||
struct xrt_space_relation *out_relation, struct xrt_space_relation *out_query_relation);
|
||||
|
||||
/*
|
||||
*
|
||||
* Camera calibration
|
||||
|
||||
@@ -92,6 +92,12 @@ t_vit_bundle_load(struct t_vit_bundle *vit, const char *path)
|
||||
GET_PROC(pose_get_features);
|
||||
#undef GET_PROC
|
||||
|
||||
// Older backends remain opt-in. Missing optional symbols are not load errors.
|
||||
dlerror();
|
||||
*(void **)&vit->tracker_supports_safe_recreation = dlsym(vit->handle, "vit_tracker_supports_safe_recreation");
|
||||
if (dlerror() != NULL)
|
||||
vit->tracker_supports_safe_recreation = NULL;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ struct t_vit_bundle
|
||||
} version;
|
||||
|
||||
PFN_vit_api_get_version api_get_version;
|
||||
// Optional private capability: all workers/queue users are joined by stop.
|
||||
bool (*tracker_supports_safe_recreation)(void);
|
||||
PFN_vit_tracker_create tracker_create;
|
||||
PFN_vit_tracker_destroy tracker_destroy;
|
||||
PFN_vit_tracker_has_image_format tracker_has_image_format;
|
||||
|
||||
@@ -1017,6 +1017,42 @@ dispatch_compute(struct comp_renderer *r,
|
||||
*
|
||||
*/
|
||||
|
||||
/* Mesh topology and FOV are fixed. Refresh CPU UVs and the existing VBO in place. */
|
||||
static bool
|
||||
renderer_update_distortion(struct comp_renderer *r)
|
||||
{
|
||||
struct comp_compositor *c = r->c;
|
||||
if (c->xdev->update_distortion == NULL || !c->xdev->update_distortion(c->xdev)) {
|
||||
return true;
|
||||
}
|
||||
uint64_t start = os_monotonic_get_ns();
|
||||
renderer_wait_for_last_fence(r);
|
||||
struct xrt_hmd_parts *hmd = c->xdev->hmd;
|
||||
float *vertices = hmd->distortion.mesh.vertices;
|
||||
uint32_t stride = hmd->distortion.mesh.stride / sizeof(float);
|
||||
uint32_t per_view = hmd->distortion.mesh.vertex_count / hmd->view_count;
|
||||
uint64_t hash = 14695981039346656037ull;
|
||||
for (uint32_t i = 0; i < hmd->distortion.mesh.vertex_count; i++) {
|
||||
float *v = &vertices[i * stride];
|
||||
if (xrt_device_compute_distortion(c->xdev, i / per_view, (v[0] + 1) * 0.5f, (v[1] + 1) * 0.5f,
|
||||
(struct xrt_uv_triplet *)&v[2]) != XRT_SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const unsigned char *bytes = (const unsigned char *)vertices;
|
||||
size_t size = hmd->distortion.mesh.vertex_count * hmd->distortion.mesh.stride;
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
hash = (hash ^ bytes[i]) * 1099511628211ull;
|
||||
}
|
||||
if (render_buffer_write(&c->base.vk, &c->nr.mesh.vbo, vertices, size) != VK_SUCCESS ||
|
||||
!render_distortion_images_update(&c->nr, c->xdev)) {
|
||||
return false;
|
||||
}
|
||||
COMP_INFO(c, "Live distortion mesh hash=%016" PRIx64 " update_ms=%.3f", hash,
|
||||
(os_monotonic_get_ns() - start) / 1000000.0);
|
||||
return true;
|
||||
}
|
||||
|
||||
XRT_CHECK_RESULT xrt_result_t
|
||||
comp_renderer_draw(struct comp_renderer *r)
|
||||
{
|
||||
@@ -1058,6 +1094,10 @@ comp_renderer_draw(struct comp_renderer *r)
|
||||
|
||||
comp_target_update_timings(ct);
|
||||
|
||||
if (!renderer_update_distortion(r)) {
|
||||
return XRT_ERROR_VULKAN;
|
||||
}
|
||||
|
||||
// Hardcoded for now.
|
||||
const uint32_t view_count = c->nr.view_count;
|
||||
enum comp_target_fov_source fov_source = COMP_TARGET_FOV_SOURCE_DISTORTION;
|
||||
|
||||
@@ -288,10 +288,10 @@ render_distortion_buffer_init(struct render_resources *r,
|
||||
struct xrt_device *xdev,
|
||||
bool pre_rotate)
|
||||
{
|
||||
struct render_buffer bufs[RENDER_DISTORTION_IMAGES_SIZE];
|
||||
VkDeviceMemory device_memories[RENDER_DISTORTION_IMAGES_SIZE];
|
||||
VkImage images[RENDER_DISTORTION_IMAGES_SIZE];
|
||||
VkImageView image_views[RENDER_DISTORTION_IMAGES_SIZE];
|
||||
struct render_buffer bufs[RENDER_DISTORTION_IMAGES_SIZE] = {0};
|
||||
VkDeviceMemory device_memories[RENDER_DISTORTION_IMAGES_SIZE] = {0};
|
||||
VkImage images[RENDER_DISTORTION_IMAGES_SIZE] = {0};
|
||||
VkImageView image_views[RENDER_DISTORTION_IMAGES_SIZE] = {0};
|
||||
VkCommandBuffer upload_buffer = VK_NULL_HANDLE;
|
||||
VkResult ret;
|
||||
|
||||
@@ -417,3 +417,40 @@ render_distortion_images_ensure(struct render_resources *r,
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* The caller has waited for rendering to finish. Reuse images/descriptors and
|
||||
* upload new UVs; no Vulkan pipeline, swapchain or session rebuild is needed. */
|
||||
bool
|
||||
render_distortion_images_update(struct render_resources *r, struct xrt_device *xdev)
|
||||
{
|
||||
struct vk_bundle *vk = r->vk;
|
||||
struct render_buffer bufs[RENDER_DISTORTION_IMAGES_SIZE] = {0};
|
||||
VkCommandBuffer cmd = VK_NULL_HANDLE;
|
||||
VkResult ret = VK_SUCCESS;
|
||||
for (uint32_t i = 0; i < r->view_count; i++) {
|
||||
ret = create_and_fill_in_distortion_buffer_for_view(vk, xdev, &bufs[i], &bufs[r->view_count + i],
|
||||
&bufs[2 * r->view_count + i], i,
|
||||
r->distortion.pre_rotated);
|
||||
if (ret != VK_SUCCESS) {
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
struct vk_cmd_pool *pool = &r->distortion_pool;
|
||||
vk_cmd_pool_lock(pool);
|
||||
ret = vk_cmd_pool_create_and_begin_cmd_buffer_locked(vk, pool, 0, &cmd);
|
||||
if (ret == VK_SUCCESS) {
|
||||
VkExtent2D extent = {RENDER_DISTORTION_IMAGE_DIMENSIONS, RENDER_DISTORTION_IMAGE_DIMENSIONS};
|
||||
for (uint32_t i = 0; i < RENDER_DISTORTION_IMAGES_COUNT(r); i++) {
|
||||
/* UNDEFINED discards the completed image contents; handles remain stable. */
|
||||
queue_upload_for_first_level_and_layer_locked(vk, cmd, bufs[i].buffer, r->distortion.images[i],
|
||||
extent);
|
||||
}
|
||||
ret = vk_cmd_pool_end_submit_wait_and_free_cmd_buffer_locked(vk, pool, cmd);
|
||||
}
|
||||
vk_cmd_pool_unlock(pool);
|
||||
cleanup:
|
||||
for (uint32_t i = 0; i < RENDER_DISTORTION_IMAGES_COUNT(r); i++) {
|
||||
render_buffer_fini(vk, &bufs[i]);
|
||||
}
|
||||
return ret == VK_SUCCESS;
|
||||
}
|
||||
@@ -1431,6 +1431,10 @@ render_compute_clear(struct render_compute *render,
|
||||
*/
|
||||
|
||||
|
||||
/* Refresh existing UV textures after the caller waits for GPU readers. */
|
||||
bool
|
||||
render_distortion_images_update(struct render_resources *r, struct xrt_device *xdev);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -348,14 +348,27 @@ if(XRT_BUILD_DRIVER_RIFT_S)
|
||||
rift_s/rift_s_camera.h
|
||||
rift_s/rift_s_controller.c
|
||||
rift_s/rift_s_controller.h
|
||||
rift_s/rift_s_position_filter.c
|
||||
rift_s/rift_s_position_filter.h
|
||||
rift_s/rift_s_slam_guard.c
|
||||
rift_s/rift_s_slam_guard.h
|
||||
rift_s/rift_s_firmware.c
|
||||
rift_s/rift_s_firmware.h
|
||||
rift_s/rift_s_hmd.c
|
||||
rift_s/rift_s_hmd.h
|
||||
rift_s/rift_s_model_check.cpp
|
||||
rift_s/rift_s_model_check.h
|
||||
rift_s/rift_s_passthrough.c
|
||||
rift_s/rift_s_passthrough.h
|
||||
rift_s/rift_s_optics.c
|
||||
rift_s/rift_s_optics.h
|
||||
rift_s/rift_s_protocol.c
|
||||
rift_s/rift_s_protocol.h
|
||||
rift_s/rift_s_radio.c
|
||||
rift_s/rift_s_radio.h
|
||||
rift_s/rift_s_world_anchor.cpp
|
||||
rift_s/rift_s_world_anchor.h
|
||||
rift_s/rift_s_world_control.h
|
||||
rift_s/rift_s_tracker.c
|
||||
rift_s/rift_s_tracker.h
|
||||
rift_s/rift_s_util.cpp
|
||||
@@ -374,6 +387,8 @@ if(XRT_BUILD_DRIVER_RIFT_S)
|
||||
xrt-external-tinyceres
|
||||
constellation_tracker
|
||||
)
|
||||
target_include_directories(drv_rift_s PRIVATE ${OpenCV_INCLUDE_DIRS})
|
||||
target_link_libraries(drv_rift_s PRIVATE ${OpenCV_LIBRARIES})
|
||||
if(XRT_BUILD_DRIVER_HANDTRACKING)
|
||||
target_link_libraries(drv_rift_s PRIVATE drv_ht)
|
||||
endif()
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
|
||||
#include "os/os_time.h"
|
||||
|
||||
#include "util/u_debug.h"
|
||||
#include "util/u_device.h"
|
||||
#include "util/u_distortion_mesh.h"
|
||||
#include "util/u_trace_marker.h"
|
||||
@@ -50,6 +51,8 @@
|
||||
#include "rift_s_controller.h"
|
||||
#include "rift_s_camera.h"
|
||||
|
||||
DEBUG_GET_ONCE_BOOL_OPTION(steamvr_bench, "MONADO_STEAMVR_BENCH", false)
|
||||
|
||||
static void *
|
||||
rift_s_run_thread(void *ptr);
|
||||
static void
|
||||
@@ -145,6 +148,17 @@ read_hmd_config(struct os_hid_device *hid_hmd, struct rift_s_hmd_config *config)
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Optional and informational: the optics use the Oculus runtime lens centres, not these */
|
||||
char *lens_json = NULL;
|
||||
int lens_json_len = 0;
|
||||
if (rift_s_read_firmware_block(hid_hmd, RIFT_S_FIRMWARE_BLOCK_LENS_CALIB, &lens_json, &lens_json_len) >= 0) {
|
||||
config->lens_centers_valid = rift_s_parse_lens_centers(lens_json, config->lens_center_px) == 0;
|
||||
free(lens_json);
|
||||
}
|
||||
if (!config->lens_centers_valid) {
|
||||
RIFT_S_DEBUG("No lens calibration block");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -219,6 +233,11 @@ rift_s_system_create(struct xrt_prober *xp,
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
// Bench sessions must power the panel even if no new proximity packet arrives.
|
||||
if (debug_get_bool_option_steamvr_bench()) {
|
||||
rift_s_hmd_set_proximity(sys->hmd, true);
|
||||
}
|
||||
|
||||
// Allow time for enumeration of available displays by host system, so the compositor can select among them.
|
||||
RIFT_S_INFO(
|
||||
"Sleeping until the HMD display is powered up so, the available displays "
|
||||
@@ -542,6 +561,12 @@ handle_packets(struct rift_s_system *sys)
|
||||
/* System state packet. Enable the screen if the prox sensor is
|
||||
* triggered. */
|
||||
bool prox_sensor = (buf[1] == 0) ? false : true;
|
||||
if (prox_sensor != sys->physical_proximity || now - sys->last_proximity_log_ns >= 5000000000ULL) {
|
||||
RIFT_S_INFO("HMD physical proximity=%d bench=%d", prox_sensor,
|
||||
debug_get_bool_option_steamvr_bench());
|
||||
sys->physical_proximity = prox_sensor;
|
||||
sys->last_proximity_log_ns = now;
|
||||
}
|
||||
os_mutex_lock(&sys->dev_mutex);
|
||||
if (sys->hmd != NULL) {
|
||||
rift_s_hmd_set_proximity(sys->hmd, prox_sensor);
|
||||
|
||||
@@ -62,6 +62,11 @@ struct rift_s_hmd_config
|
||||
|
||||
struct rift_s_imu_config_info_t imu_config_info;
|
||||
struct rift_s_imu_calibration imu_calibration;
|
||||
|
||||
/* Lens optical centres from firmware block 0x12, in native panel pixels
|
||||
* (2560x1440, left eye first). Only valid if lens_centers_valid. */
|
||||
bool lens_centers_valid;
|
||||
struct xrt_vec2 lens_center_px[2];
|
||||
};
|
||||
|
||||
/* Structure to track online devices and type */
|
||||
@@ -80,6 +85,9 @@ struct rift_s_system
|
||||
struct os_thread_helper oth;
|
||||
struct os_hid_device *handles[3];
|
||||
uint64_t last_keep_alive;
|
||||
// Raw proximity diagnostics must stay independent of the bench override.
|
||||
uint64_t last_proximity_log_ns;
|
||||
bool physical_proximity;
|
||||
|
||||
/* state tracking for tracked devices on our radio link */
|
||||
int num_active_tracked_devices;
|
||||
|
||||
@@ -16,12 +16,13 @@
|
||||
* @ingroup drv_rift_s
|
||||
*/
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "rift_s.h"
|
||||
#include "rift_s_camera.h"
|
||||
#include "rift_s_camera_metadata.h"
|
||||
|
||||
#include "os/os_threading.h"
|
||||
#include "xrt/xrt_byte_order.h"
|
||||
|
||||
#include "xrt/xrt_defines.h"
|
||||
#include "xrt/xrt_frame.h"
|
||||
@@ -46,6 +47,9 @@
|
||||
//! Specifies whether the user wants to enable autoexposure from the start.
|
||||
DEBUG_GET_ONCE_BOOL_OPTION(rift_s_autoexposure, "RIFT_S_AUTOEXPOSURE", true)
|
||||
|
||||
// Save up to ten raw LED frames, one per second, to an existing directory.
|
||||
DEBUG_GET_ONCE_OPTION(rift_s_controller_dump_dir, "RIFT_S_CONTROLLER_DUMP_DIR", NULL)
|
||||
|
||||
struct rift_s_camera
|
||||
{
|
||||
struct os_mutex lock;
|
||||
@@ -55,6 +59,18 @@ struct rift_s_camera
|
||||
struct rift_s_camera_calibration_block *camera_calibration;
|
||||
|
||||
uint64_t last_frame_ts_ns;
|
||||
uint64_t last_diag_ns[2];
|
||||
uint64_t last_arrival_ns[2];
|
||||
uint64_t max_arrival_gap_ns[2];
|
||||
bool have_frame_counter;
|
||||
uint16_t last_frame_counter;
|
||||
struct
|
||||
{
|
||||
uint64_t accepted[2]; // controller, SLAM
|
||||
uint64_t bad_layout, bad_magic, bad_crc, bad_timestamp;
|
||||
uint64_t missing_frames, counter_discontinuities, bad_crop, bad_guard;
|
||||
} metadata_stats;
|
||||
uint32_t controller_dump_frames;
|
||||
|
||||
struct xrt_frame_sink in_sink; // Receive raw frames and split them
|
||||
|
||||
@@ -78,27 +94,6 @@ struct rift_s_camera_finder
|
||||
struct xrt_frame_context *xfctx;
|
||||
};
|
||||
|
||||
union rift_s_frame_data {
|
||||
struct
|
||||
{
|
||||
uint8_t frame_type; // 0x06 or 0x86 (controller or SLAM exposure)
|
||||
__le16 magic_abcd; // 0xabcd
|
||||
__le16 frame_ctr; // Increments every exposure
|
||||
__le32 const1; // QHWH
|
||||
uint8_t pad1[7]; // all zeroes padding to 16 bytes
|
||||
__le64 frame_ts; // microseconds
|
||||
__le32 frame_ctr2; // Another frame counter, but only increments on alternate frames @ 30Hz
|
||||
__le16 slam_exposure[5]; // One 16-bit per camera. Exposure duration?
|
||||
uint8_t pad2[2]; // zero padding
|
||||
uint8_t slam_gain[5]; // One byte per camera. 0x40 or 0xf0 depending on frame type
|
||||
uint8_t pad3; // zero padding
|
||||
__le16 unknown1; // changes every frame. No clear pattern
|
||||
__le16 magic_face; // 0xface
|
||||
|
||||
} __attribute__((packed)) data;
|
||||
uint8_t raw[50];
|
||||
};
|
||||
|
||||
static void
|
||||
update_expgain(struct rift_s_camera *cam, struct xrt_frame *xf);
|
||||
|
||||
@@ -269,56 +264,55 @@ void
|
||||
rift_s_camera_destroy(struct rift_s_camera *cam)
|
||||
{
|
||||
u_var_remove_root(cam);
|
||||
u_autoexpgain_destroy(&cam->aeg);
|
||||
os_mutex_destroy(&cam->lock);
|
||||
free(cam);
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_frame_data(const struct xrt_frame *xf, union rift_s_frame_data *row_data)
|
||||
crop_in_bounds(const struct xrt_frame *xf, struct xrt_rect roi)
|
||||
{
|
||||
/* Parse out the bits encoded as 8x8 blocks in the top rows */
|
||||
unsigned int x, out_x;
|
||||
|
||||
if (xf->width != 50 * 8 * 8 || xf->height < 8)
|
||||
if (roi.offset.w < 0 || roi.offset.h < 0 || roi.extent.w <= 0 || roi.extent.h <= 0) {
|
||||
return false;
|
||||
}
|
||||
uint32_t x = roi.offset.w, y = roi.offset.h, w = roi.extent.w, h = roi.extent.h;
|
||||
if (x > xf->width || y > xf->height || w > xf->width - x || h > xf->height - y || xf->stride < xf->width ||
|
||||
xf->size < x + w || xf->stride == 0) {
|
||||
return false;
|
||||
}
|
||||
return (size_t)y + h - 1 <= (xf->size - x - w) / xf->stride;
|
||||
}
|
||||
|
||||
uint8_t *pix = &xf->data[xf->width * 4];
|
||||
|
||||
int bit = 7;
|
||||
for (x = 4, out_x = 0; x < xf->width; x += 8) {
|
||||
uint8_t val = 0;
|
||||
if (pix[x] > 128)
|
||||
val = 1 << bit;
|
||||
|
||||
if (bit == 7) {
|
||||
row_data->raw[out_x] = val;
|
||||
} else {
|
||||
row_data->raw[out_x] |= val;
|
||||
// Neighboring black rows are transport guards, not LED segmentation thresholds.
|
||||
static bool
|
||||
crop_guards_valid(const struct xrt_frame *xf, struct xrt_rect roi)
|
||||
{
|
||||
uint32_t rows[2] = {roi.offset.h - 1, roi.offset.h + roi.extent.h};
|
||||
for (int i = 0; i < 2; i++) {
|
||||
if (rows[i] >= xf->height) {
|
||||
continue;
|
||||
}
|
||||
if (bit > 0)
|
||||
bit--;
|
||||
else {
|
||||
bit = 7;
|
||||
out_x++;
|
||||
struct xrt_rect guard = {.offset = {roi.offset.w, rows[i]}, .extent = {roi.extent.w, 1}};
|
||||
if (!crop_in_bounds(xf, guard)) {
|
||||
return false;
|
||||
}
|
||||
const uint8_t *row = xf->data + rows[i] * xf->stride + roi.offset.w;
|
||||
for (int x = 0; x < roi.extent.w; x++) {
|
||||
if (row[x] > 20) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Check magic numbers */
|
||||
if (__le16_to_cpu(row_data->data.magic_abcd) != 0xabcd)
|
||||
return false;
|
||||
if (__le16_to_cpu(row_data->data.magic_face) != 0xface)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static int
|
||||
get_y_offset(struct rift_s_camera *cam, enum rift_s_camera_id cam_id, union rift_s_frame_data *row_data)
|
||||
get_y_offset(struct rift_s_camera *cam, enum rift_s_camera_id cam_id, struct rift_s_frame_metadata *row_data)
|
||||
{
|
||||
/* There's a magic formula for computing the vertical offset of each camera view
|
||||
* based on exposure, due to some internals of the headset. This formula extracted
|
||||
* through trial and error */
|
||||
int exposure = __le16_to_cpu(row_data->data.slam_exposure[cam_id]);
|
||||
int exposure = row_data->exposure[cam_id];
|
||||
int y_offset = (exposure + 275) / 38;
|
||||
|
||||
if (y_offset > 375) {
|
||||
@@ -334,7 +328,7 @@ static struct xrt_frame *
|
||||
rift_s_camera_extract_frame(struct rift_s_camera *cam,
|
||||
enum rift_s_camera_id cam_id,
|
||||
struct xrt_frame *full_frame,
|
||||
union rift_s_frame_data *row_data)
|
||||
struct rift_s_frame_metadata *row_data)
|
||||
{
|
||||
struct rift_s_camera_calibration *calib = &cam->camera_calibration->cameras[cam_id];
|
||||
struct xrt_rect roi = calib->roi;
|
||||
@@ -342,12 +336,53 @@ rift_s_camera_extract_frame(struct rift_s_camera *cam,
|
||||
roi.offset.h = get_y_offset(cam, cam_id, row_data);
|
||||
|
||||
struct xrt_frame *xf_crop = NULL;
|
||||
|
||||
if (!crop_in_bounds(full_frame, roi)) {
|
||||
cam->metadata_stats.bad_crop++;
|
||||
RIFT_S_DEBUG("DIAG camcrop camera=%u rejected=bounds", cam_id);
|
||||
return NULL;
|
||||
}
|
||||
if (!crop_guards_valid(full_frame, roi)) {
|
||||
cam->metadata_stats.bad_guard++;
|
||||
// The empirical SLAM crop formula has no raw-frame fixture yet. Count the guard
|
||||
// failure without suppressing SLAM until its neighboring rows are verified.
|
||||
RIFT_S_DEBUG("DIAG camcrop camera=%u guard=nonblack y=%d count=%" PRIu64, cam_id, roi.offset.h,
|
||||
cam->metadata_stats.bad_guard);
|
||||
}
|
||||
u_frame_create_roi(full_frame, roi, &xf_crop);
|
||||
|
||||
return xf_crop;
|
||||
}
|
||||
|
||||
static void
|
||||
dump_controller_frame(struct rift_s_camera *cam, struct xrt_frame *xf, uint64_t hw_ns)
|
||||
{
|
||||
const char *dir = debug_get_option_rift_s_controller_dump_dir();
|
||||
if (dir == NULL || cam->controller_dump_frames >= 300) {
|
||||
return;
|
||||
}
|
||||
if (cam->controller_dump_frames++ % 30 != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
char path[1024];
|
||||
int len = snprintf(path, sizeof(path), "%s/led-%" PRIu64 ".pgm", dir, hw_ns);
|
||||
if (len < 0 || (size_t)len >= sizeof(path)) {
|
||||
return;
|
||||
}
|
||||
FILE *file = fopen(path, "wb");
|
||||
if (file == NULL) {
|
||||
RIFT_S_WARN("Unable to write controller frame to %s", path);
|
||||
return;
|
||||
}
|
||||
bool ok = fprintf(file, "P5\n%u %u\n255\n", xf->width, xf->height) > 0;
|
||||
for (uint32_t y = 0; ok && y < xf->height; y++) {
|
||||
ok = fwrite(xf->data + y * xf->stride, 1, xf->width, file) == xf->width;
|
||||
}
|
||||
if (fclose(file) != 0 || !ok) {
|
||||
RIFT_S_WARN("Incomplete controller frame at %s", path);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
receive_cam_frame(struct xrt_frame_sink *sink, struct xrt_frame *xf)
|
||||
{
|
||||
@@ -371,43 +406,101 @@ receive_cam_frame(struct xrt_frame_sink *sink, struct xrt_frame *xf)
|
||||
release_xf = true;
|
||||
}
|
||||
|
||||
// Dump mid-row of the 8 pix data line
|
||||
union rift_s_frame_data row_data;
|
||||
|
||||
if (!parse_frame_data(xf, &row_data)) {
|
||||
RIFT_S_TRACE("Invalid frame top-row data. Skipping");
|
||||
return;
|
||||
struct rift_s_frame_metadata row_data;
|
||||
enum rift_s_metadata_status status =
|
||||
rift_s_metadata_extract(xf->data, xf->width, xf->height, xf->stride, xf->size, &row_data);
|
||||
if (status != RIFT_S_METADATA_OK) {
|
||||
if (status == RIFT_S_METADATA_BAD_LAYOUT)
|
||||
cam->metadata_stats.bad_layout++;
|
||||
if (status == RIFT_S_METADATA_BAD_MAGIC)
|
||||
cam->metadata_stats.bad_magic++;
|
||||
if (status == RIFT_S_METADATA_BAD_CRC)
|
||||
cam->metadata_stats.bad_crc++;
|
||||
RIFT_S_DEBUG("DIAG cammeta rejected=%s layout=%" PRIu64 " magic=%" PRIu64 " crc=%" PRIu64,
|
||||
status == RIFT_S_METADATA_BAD_LAYOUT ? "layout"
|
||||
: status == RIFT_S_METADATA_BAD_MAGIC ? "magic"
|
||||
: "crc",
|
||||
cam->metadata_stats.bad_layout, cam->metadata_stats.bad_magic,
|
||||
cam->metadata_stats.bad_crc);
|
||||
goto out;
|
||||
}
|
||||
if (row_data.frame_ts_us > UINT64_MAX / OS_NS_PER_USEC) {
|
||||
cam->metadata_stats.bad_timestamp++;
|
||||
RIFT_S_DEBUG("DIAG cammeta rejected=timestamp_overflow ctr=%u", row_data.frame_ctr);
|
||||
goto out;
|
||||
}
|
||||
uint64_t frame_ts_ns = row_data.frame_ts_us * OS_NS_PER_USEC;
|
||||
if (frame_ts_ns == 0 || frame_ts_ns <= cam->last_frame_ts_ns) {
|
||||
cam->metadata_stats.bad_timestamp++;
|
||||
RIFT_S_DEBUG("DIAG cammeta rejected=timestamp ctr=%u hw_us=%" PRIu64, row_data.frame_ctr,
|
||||
row_data.frame_ts_us);
|
||||
goto out;
|
||||
}
|
||||
cam->last_frame_ts_ns = frame_ts_ns;
|
||||
if (cam->have_frame_counter) {
|
||||
uint16_t missing;
|
||||
if (rift_s_metadata_counter_delta(cam->last_frame_counter, row_data.frame_ctr, &missing)) {
|
||||
cam->metadata_stats.missing_frames += missing;
|
||||
} else {
|
||||
cam->metadata_stats.counter_discontinuities++;
|
||||
}
|
||||
}
|
||||
cam->last_frame_counter = row_data.frame_ctr;
|
||||
cam->have_frame_counter = true;
|
||||
unsigned int kind = (row_data.frame_type & 0x80) != 0;
|
||||
cam->metadata_stats.accepted[kind]++;
|
||||
uint64_t now_ns = os_monotonic_get_ns();
|
||||
if (cam->last_arrival_ns[kind] != 0) {
|
||||
cam->max_arrival_gap_ns[kind] = MAX(cam->max_arrival_gap_ns[kind], now_ns - cam->last_arrival_ns[kind]);
|
||||
}
|
||||
cam->last_arrival_ns[kind] = now_ns;
|
||||
if (now_ns - cam->last_diag_ns[kind] >= 5 * (uint64_t)U_TIME_1S_IN_NS) {
|
||||
cam->last_diag_ns[kind] = now_ns;
|
||||
RIFT_S_INFO("Camera timing kind=%s arrival_ns=%" PRIu64 " hw_us=%" PRIu64
|
||||
" max_arrival_gap_ms=%.3f missing=%" PRIu64 " discontinuities=%" PRIu64
|
||||
" exposure=%u,%u,%u,%u,%u gain16=%u,%u,%u,%u,%u",
|
||||
kind ? "slam" : "controller", now_ns, row_data.frame_ts_us,
|
||||
(double)cam->max_arrival_gap_ns[kind] / U_TIME_1MS_IN_NS,
|
||||
cam->metadata_stats.missing_frames, cam->metadata_stats.counter_discontinuities,
|
||||
row_data.exposure[0], row_data.exposure[1], row_data.exposure[2], row_data.exposure[3], row_data.exposure[4],
|
||||
row_data.gain[0], row_data.gain[1], row_data.gain[2], row_data.gain[3], row_data.gain[4]);
|
||||
cam->max_arrival_gap_ns[kind] = 0;
|
||||
}
|
||||
|
||||
RIFT_S_DEBUG("frame ctr %u ts %" PRIu64
|
||||
" µS pair ctr %u "
|
||||
"exposure[0] %u gain[0] %u unk %u",
|
||||
(uint16_t)__le16_to_cpu(row_data.data.frame_ctr), (uint64_t)__le64_to_cpu(row_data.data.frame_ts),
|
||||
(uint32_t)__le32_to_cpu(row_data.data.frame_ctr2),
|
||||
(uint16_t)__le16_to_cpu(row_data.data.slam_exposure[0]), row_data.data.slam_gain[0],
|
||||
(uint16_t)__le16_to_cpu(row_data.data.unknown1));
|
||||
|
||||
// rift_s_hexdump_buffer("Row data", row_data.raw, sizeof(row_data.row));
|
||||
uint64_t frame_ts_ns = (uint64_t)__le64_to_cpu(row_data.data.frame_ts) * OS_NS_PER_USEC;
|
||||
if (frame_ts_ns == cam->last_frame_ts_ns) {
|
||||
RIFT_S_WARN("Camera frame TS didn't advance. Probably the headset crashed");
|
||||
return;
|
||||
RIFT_S_DEBUG("DIAG cammeta kind=%s ctr=%u pair=%u hw_us=%" PRIu64 " controller=%" PRIu64 " slam=%" PRIu64
|
||||
" missing=%" PRIu64 " discontinuities=%" PRIu64,
|
||||
kind ? "slam" : "controller", row_data.frame_ctr, row_data.frame_ctr2, row_data.frame_ts_us,
|
||||
cam->metadata_stats.accepted[0], cam->metadata_stats.accepted[1],
|
||||
cam->metadata_stats.missing_frames, cam->metadata_stats.counter_discontinuities);
|
||||
for (int i = 0; i < RIFT_S_CAMERA_COUNT; i++) {
|
||||
RIFT_S_TRACE("DIAG cammeta camera=%d kind=%s ctr=%u exposure=%u gain=%f", i,
|
||||
kind ? "slam" : "controller", row_data.frame_ctr, row_data.exposure[i],
|
||||
row_data.gain[i] / 16.0f);
|
||||
}
|
||||
|
||||
// If the top left pixel is > 128, send as SLAM frame else controller
|
||||
if (row_data.data.frame_type & 0x80) {
|
||||
// Route by metadata kind, independent of arrival order or pixel brightness.
|
||||
if (kind) {
|
||||
int y_offset = get_y_offset(cam, 0, &row_data);
|
||||
struct xrt_rect roi = {.offset = {0, y_offset}, .extent = {.w = xf->width, .h = 480}};
|
||||
|
||||
struct xrt_frame *xf_crop = NULL;
|
||||
u_frame_create_roi(xf, roi, &xf_crop);
|
||||
u_sink_debug_push_frame(&cam->debug_sinks[0], xf_crop);
|
||||
if (crop_in_bounds(xf, roi)) {
|
||||
u_frame_create_roi(xf, roi, &xf_crop);
|
||||
u_sink_debug_push_frame(&cam->debug_sinks[0], xf_crop);
|
||||
}
|
||||
xrt_frame_reference(&xf_crop, NULL);
|
||||
|
||||
/* Extract camera frames and push to the tracker */
|
||||
struct xrt_frame *frames[RIFT_S_CAMERA_COUNT] = {0};
|
||||
bool valid = true;
|
||||
for (int i = 0; i < RIFT_S_CAMERA_COUNT; i++) {
|
||||
frames[i] = rift_s_camera_extract_frame(cam, CAM_IDX_TO_ID[i], xf, &row_data);
|
||||
valid &= frames[i] != NULL;
|
||||
}
|
||||
if (!valid) {
|
||||
for (int i = 0; i < RIFT_S_CAMERA_COUNT; i++)
|
||||
xrt_frame_reference(&frames[i], NULL);
|
||||
goto out;
|
||||
}
|
||||
|
||||
/* Update the exposure for all cameras based on the auto exposure for the left camera view */
|
||||
@@ -420,15 +513,29 @@ receive_cam_frame(struct xrt_frame_sink *sink, struct xrt_frame *xf)
|
||||
xrt_frame_reference(&frames[i], NULL);
|
||||
}
|
||||
} else {
|
||||
dump_controller_frame(cam, xf, frame_ts_ns);
|
||||
struct xrt_rect roi = {.offset = {0, 40}, .extent = {.w = xf->width, .h = 480}};
|
||||
struct xrt_frame *xf_crop = NULL;
|
||||
|
||||
if (!crop_in_bounds(xf, roi)) {
|
||||
cam->metadata_stats.bad_crop++;
|
||||
RIFT_S_DEBUG("DIAG camcrop kind=controller rejected=bounds count=%" PRIu64,
|
||||
cam->metadata_stats.bad_crop);
|
||||
goto out;
|
||||
}
|
||||
if (!crop_guards_valid(xf, roi)) {
|
||||
cam->metadata_stats.bad_guard++;
|
||||
RIFT_S_DEBUG("DIAG camcrop kind=controller rejected=guard count=%" PRIu64,
|
||||
cam->metadata_stats.bad_guard);
|
||||
goto out;
|
||||
}
|
||||
u_frame_create_roi(xf, roi, &xf_crop);
|
||||
u_sink_debug_push_frame(&cam->debug_sinks[1], xf_crop);
|
||||
|
||||
rift_s_tracker_push_controller_frameset(cam->tracker, frame_ts_ns, xf_crop);
|
||||
xrt_frame_reference(&xf_crop, NULL);
|
||||
}
|
||||
out:
|
||||
if (release_xf)
|
||||
xrt_frame_reference(&xf, NULL);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define RIFT_S_METADATA_SIZE 50
|
||||
#define RIFT_S_METADATA_WIDTH (RIFT_S_METADATA_SIZE * 8 * 8)
|
||||
|
||||
struct rift_s_frame_metadata
|
||||
{
|
||||
uint8_t frame_type;
|
||||
uint16_t frame_ctr;
|
||||
uint64_t frame_ts_us;
|
||||
uint32_t frame_ctr2;
|
||||
uint16_t exposure[5];
|
||||
uint8_t gain[5];
|
||||
};
|
||||
|
||||
enum rift_s_metadata_status
|
||||
{
|
||||
RIFT_S_METADATA_OK,
|
||||
RIFT_S_METADATA_BAD_LAYOUT,
|
||||
RIFT_S_METADATA_BAD_MAGIC,
|
||||
RIFT_S_METADATA_BAD_CRC,
|
||||
};
|
||||
|
||||
static inline uint16_t
|
||||
rift_s_metadata_crc(const uint8_t *data, size_t len)
|
||||
{
|
||||
uint16_t crc = 0xffff;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
crc ^= (uint16_t)data[i] << 8;
|
||||
for (int bit = 0; bit < 8; bit++) {
|
||||
crc = (uint16_t)((crc << 1) ^ ((crc & 0x8000) ? 0x1021 : 0));
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
static inline uint64_t
|
||||
rift_s_metadata_le(const uint8_t *data, size_t len)
|
||||
{
|
||||
uint64_t result = 0;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
result |= (uint64_t)data[i] << (8 * i);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static inline enum rift_s_metadata_status
|
||||
rift_s_metadata_decode(const uint8_t raw[RIFT_S_METADATA_SIZE], struct rift_s_frame_metadata *out)
|
||||
{
|
||||
if (rift_s_metadata_le(raw + 1, 2) != 0xabcd || rift_s_metadata_le(raw + 48, 2) != 0xface) {
|
||||
return RIFT_S_METADATA_BAD_MAGIC;
|
||||
}
|
||||
if (rift_s_metadata_crc(raw + 16, 30) != rift_s_metadata_le(raw + 46, 2)) {
|
||||
return RIFT_S_METADATA_BAD_CRC;
|
||||
}
|
||||
|
||||
out->frame_type = raw[0];
|
||||
out->frame_ctr = (uint16_t)rift_s_metadata_le(raw + 3, 2);
|
||||
out->frame_ts_us = rift_s_metadata_le(raw + 16, 8);
|
||||
out->frame_ctr2 = (uint32_t)rift_s_metadata_le(raw + 24, 4);
|
||||
for (int i = 0; i < 5; i++) {
|
||||
out->exposure[i] = (uint16_t)rift_s_metadata_le(raw + 28 + 2 * i, 2);
|
||||
out->gain[i] = raw[40 + i];
|
||||
}
|
||||
return RIFT_S_METADATA_OK;
|
||||
}
|
||||
|
||||
// Metadata bits occupy 8x8 blocks. Sample their centers using the transport stride.
|
||||
static inline enum rift_s_metadata_status
|
||||
rift_s_metadata_extract(
|
||||
const uint8_t *data, uint32_t width, uint32_t height, size_t stride, size_t size, struct rift_s_frame_metadata *out)
|
||||
{
|
||||
if (data == NULL || width != RIFT_S_METADATA_WIDTH || height < 8 || stride < width || stride > SIZE_MAX / 4 ||
|
||||
size < width || 4 * stride > size - width) {
|
||||
return RIFT_S_METADATA_BAD_LAYOUT;
|
||||
}
|
||||
uint8_t raw[RIFT_S_METADATA_SIZE] = {0};
|
||||
const uint8_t *row = data + 4 * stride;
|
||||
for (size_t i = 0; i < RIFT_S_METADATA_SIZE * 8; i++) {
|
||||
if (row[4 + 8 * i] > 128) {
|
||||
raw[i / 8] |= 1u << (7 - i % 8);
|
||||
}
|
||||
}
|
||||
return rift_s_metadata_decode(raw, out);
|
||||
}
|
||||
|
||||
// Unsigned subtraction accepts 65535 -> 0. Half-range jumps indicate a reset/backwards counter.
|
||||
static inline bool
|
||||
rift_s_metadata_counter_delta(uint16_t previous, uint16_t current, uint16_t *missing)
|
||||
{
|
||||
uint16_t delta = (uint16_t)(current - previous);
|
||||
*missing = 0;
|
||||
if (delta == 0 || delta >= 0x8000) {
|
||||
return false;
|
||||
}
|
||||
*missing = delta - 1;
|
||||
return true;
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -22,19 +22,150 @@
|
||||
#include "xrt/xrt_device.h"
|
||||
|
||||
#include "rift_s.h"
|
||||
#include "rift_s_position_filter.h"
|
||||
|
||||
#define MAX_LOG_SIZE 1024
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint16_t accel_limit;
|
||||
uint16_t gyro_limit;
|
||||
uint16_t accel_hz;
|
||||
uint16_t gyro_hz;
|
||||
// About two seconds at the observed 500 Hz report rate. Not a runtime constant.
|
||||
#define RIFT_S_ATTITUDE_HISTORY_CAPACITY 1024
|
||||
|
||||
float accel_scale;
|
||||
float gyro_scale;
|
||||
} rift_s_controller_config;
|
||||
struct rift_s_attitude_sample
|
||||
{
|
||||
timepoint_ns device_capture_ns;
|
||||
timepoint_ns capture_ns;
|
||||
timepoint_ns arrival_ns;
|
||||
struct xrt_quat orientation;
|
||||
struct xrt_vec3 gyro;
|
||||
bool tilt_valid;
|
||||
};
|
||||
|
||||
// Monado policy: a heading prior needs an optical yaw measurement no older than this.
|
||||
#define RIFT_S_HEADING_PRIOR_MAX_AGE_NS (2 * U_TIME_1S_IN_NS)
|
||||
|
||||
//! Why an IMU sample's time was rejected; the parts of reject_clock.
|
||||
enum rift_s_clock_reject
|
||||
{
|
||||
//! The 32-bit device timestamp went backwards.
|
||||
RIFT_S_CLOCK_BACKWARDS,
|
||||
//! Device capture time not after the last attitude history sample.
|
||||
RIFT_S_CLOCK_HISTORY_ORDER,
|
||||
//! The skew tracker could not map device time to host time.
|
||||
RIFT_S_CLOCK_UNMAPPED,
|
||||
//! The mapping jumped by more than 2 ms; the attitude history restarts.
|
||||
RIFT_S_CLOCK_DISCONTINUITY,
|
||||
//! The mapped host capture time was not after the previous one.
|
||||
RIFT_S_CLOCK_MAPPED_ORDER,
|
||||
RIFT_S_CLOCK_REJECT_COUNT,
|
||||
};
|
||||
|
||||
enum rift_s_attitude_rejection
|
||||
{
|
||||
RIFT_S_ATTITUDE_EMPTY,
|
||||
RIFT_S_ATTITUDE_INVALID_TIME,
|
||||
RIFT_S_ATTITUDE_TOO_OLD,
|
||||
RIFT_S_ATTITUDE_TOO_NEW,
|
||||
RIFT_S_ATTITUDE_TILT_UNREADY,
|
||||
RIFT_S_ATTITUDE_RADIO_GAP,
|
||||
RIFT_S_ATTITUDE_REJECT_COUNT,
|
||||
};
|
||||
|
||||
struct rift_s_fusion_diagnostics
|
||||
{
|
||||
//! Optical observations that updated position.
|
||||
uint64_t optical_accepted;
|
||||
//! Accepted observations whose fit measured yaw from at least three distinct LEDs.
|
||||
uint64_t optical_reliable;
|
||||
//! Accepted observations whose orientation was the IMU prior (position-only update).
|
||||
uint64_t optical_position_only;
|
||||
//! Entries into the unacquired position state.
|
||||
uint64_t fallback_entries;
|
||||
//! Calibration JSON blocks that failed to parse (bad transfer or unexpected FlsVersion).
|
||||
uint64_t calibration_parse_failures;
|
||||
timepoint_ns last_summary_ns;
|
||||
uint64_t reject_stale;
|
||||
uint64_t reject_nonfinite;
|
||||
//! Device timestamp went backwards, or the host clock mapping was discontinuous.
|
||||
uint64_t reject_clock;
|
||||
uint64_t reject_clock_by[RIFT_S_CLOCK_REJECT_COUNT];
|
||||
//! Device time step of the last accepted IMU sample, and consecutive backwards samples.
|
||||
uint32_t last_step_us;
|
||||
uint32_t backwards_run;
|
||||
//! Device clock re-anchored after a run of backwards samples.
|
||||
uint32_t clock_resyncs;
|
||||
timepoint_ns last_clock_log_ns;
|
||||
//! Repeated IMU reports with the same device timestamp (radio retransmits); harmless.
|
||||
uint64_t reject_duplicate;
|
||||
uint64_t reject_history;
|
||||
uint64_t attitude_rejected[RIFT_S_ATTITUDE_REJECT_COUNT];
|
||||
enum rift_s_attitude_rejection last_attitude_reject;
|
||||
timepoint_ns last_attitude_reject_ns;
|
||||
timepoint_ns last_history_log_ns;
|
||||
timepoint_ns last_health_log_ns;
|
||||
const char *last_optical_result;
|
||||
timepoint_ns last_optical_processing_ns;
|
||||
|
||||
};
|
||||
|
||||
/*!
|
||||
* Resting accelerometer magnitude across the session. A calibrated accelerometer reads 9.81 m/s^2
|
||||
* at rest in every orientation; a magnitude that changes with orientation means an uncorrected
|
||||
* bias or per-axis scale, which also tilts the IMU attitude by up to a few degrees.
|
||||
*/
|
||||
struct rift_s_rest_stats
|
||||
{
|
||||
// Current stationary window.
|
||||
struct xrt_vec3 sum;
|
||||
unsigned int samples;
|
||||
timepoint_ns start_ns, last_ns;
|
||||
// Completed 250 ms windows.
|
||||
unsigned int windows;
|
||||
float min_mps2, max_mps2;
|
||||
//! Gravity direction in the IMU frame at the smallest and largest reading.
|
||||
struct xrt_vec3 min_dir, max_dir;
|
||||
//! Mean acceleration of the window completed last.
|
||||
struct xrt_vec3 last_mean;
|
||||
};
|
||||
|
||||
#define RIFT_S_ACCEL_BIAS_WINDOWS 16
|
||||
|
||||
/*!
|
||||
* Accelerometer bias from rest windows in different orientations. At rest the corrected reading
|
||||
* must have the magnitude of gravity whatever the orientation, so windows spread over enough
|
||||
* directions determine the bias along them. A weak prior keeps unobserved axes at zero. A
|
||||
* measured residual bias of 2 m/s^2 can tilt the IMU attitude by up to 12 degrees depending on
|
||||
* orientation.
|
||||
*/
|
||||
struct rift_s_accel_bias
|
||||
{
|
||||
//! Rest-window means, at most one per 15 degree cone of directions.
|
||||
struct xrt_vec3 means[RIFT_S_ACCEL_BIAS_WINDOWS];
|
||||
unsigned int count;
|
||||
struct xrt_vec3 bias;
|
||||
//! Enough spread and a consistent fit: the bias is applied.
|
||||
bool valid;
|
||||
//! RMS of |mean - bias| - g over the stored windows, before and after the correction.
|
||||
float rms_before_mps2, rms_after_mps2;
|
||||
//! Largest angle between two stored window directions.
|
||||
float spread_deg;
|
||||
};
|
||||
|
||||
/*!
|
||||
* Accelerometer offset from rest windows at a known attitude: the window mean minus gravity
|
||||
* rotated into the IMU frame gives the full offset from one window, without the spread of
|
||||
* orientations rift_s_accel_bias needs. The attitude is levelled by optical tilt.
|
||||
*/
|
||||
#define RIFT_S_ATTITUDE_BIAS_SAMPLES 32
|
||||
#define RIFT_S_ATTITUDE_BIAS_MIN_SAMPLES 8
|
||||
|
||||
struct rift_s_attitude_bias
|
||||
{
|
||||
struct xrt_vec3 samples[RIFT_S_ATTITUDE_BIAS_SAMPLES];
|
||||
unsigned int count;
|
||||
struct xrt_vec3 bias;
|
||||
//! RMS distance of the samples from their mean, m/s^2.
|
||||
float rms_mps2;
|
||||
bool valid;
|
||||
};
|
||||
|
||||
struct rift_s_controller
|
||||
{
|
||||
@@ -59,6 +190,29 @@ struct rift_s_controller
|
||||
struct xrt_pose last_tracked_pose;
|
||||
//! debug boolean - enable yaw updates
|
||||
bool update_yaw_from_optical;
|
||||
bool optical_heading_valid;
|
||||
//! Capture time of the last observation whose fit measured yaw.
|
||||
timepoint_ns last_optical_yaw_ts;
|
||||
//! Position covariance (m^2, row-major world) of the last accepted optical fit.
|
||||
float last_position_covariance[9];
|
||||
//! Aim/grip position: IMU acceleration integrated at IMU rate, corrected by optical fixes.
|
||||
struct rift_s_position_filter position_filter;
|
||||
//! Presentation-only translation correction after held position reacquires optics.
|
||||
struct xrt_vec3 reacquire_offset;
|
||||
timepoint_ns reacquire_started_ns;
|
||||
//! Successful and reliable optical fixes in the current track (inputs of the loss ladder).
|
||||
uint32_t track_vision_updates, track_reliable_updates;
|
||||
//! Capture time of the newest optical fix given to position_filter.
|
||||
timepoint_ns last_fix_ns;
|
||||
//! Host capture time and world-frame acceleration (gravity removed) of the newest IMU sample.
|
||||
timepoint_ns last_imu_capture_ns;
|
||||
struct xrt_vec3 world_accel;
|
||||
bool world_accel_valid;
|
||||
//! Last reported aim/grip position source, for logging transitions.
|
||||
const char *position_source;
|
||||
//! No optical position has been acquired yet.
|
||||
bool fallback_active;
|
||||
struct rift_s_fusion_diagnostics diagnostics;
|
||||
|
||||
/* Debug logs */
|
||||
/* 0x04 = new log line
|
||||
@@ -73,6 +227,14 @@ struct rift_s_controller
|
||||
uint32_t imu_timestamp32;
|
||||
timepoint_ns last_imu_device_time_ns;
|
||||
timepoint_ns last_imu_local_time_ns;
|
||||
//! Device-to-host IMU clock estimate (minimum transport delay); allocated on the first sample.
|
||||
struct m_clock_windowed_skew_tracker *imu_clock;
|
||||
//! Current offset from imu_clock, kept to detect discontinuities.
|
||||
time_duration_ns imu_to_host_ns;
|
||||
unsigned int clock_stable_samples;
|
||||
struct rift_s_attitude_sample attitude_history[RIFT_S_ATTITUDE_HISTORY_CAPACITY];
|
||||
unsigned int attitude_history_start;
|
||||
unsigned int attitude_history_count;
|
||||
|
||||
uint16_t imu_unknown_varying2;
|
||||
int16_t raw_accel[3];
|
||||
@@ -83,13 +245,41 @@ struct rift_s_controller
|
||||
struct xrt_vec3 mag;
|
||||
struct m_imu_3dof fusion;
|
||||
|
||||
// Gyro-compensated startup gravity, before constellation uses the tilt prior.
|
||||
bool gravity_initialized;
|
||||
bool gravity_moving;
|
||||
struct rift_s_rest_stats rest;
|
||||
struct rift_s_accel_bias accel_bias;
|
||||
timepoint_ns last_bias_log_ns;
|
||||
/*!
|
||||
* The stationary startup reading was not gravity's magnitude: the accelerometer carries an
|
||||
* offset that tilts its gravity direction by up to ~12 degrees depending on orientation.
|
||||
* Until a bias is known, the fusion does not level to it; optical tilt levels instead.
|
||||
*/
|
||||
bool accel_offset;
|
||||
struct rift_s_attitude_bias attitude_bias;
|
||||
//! Reliable optical tilt corrections while accel_offset, their latest capture time and a
|
||||
//! smoothed residual tilt in degrees.
|
||||
uint32_t optical_tilt_updates;
|
||||
timepoint_ns last_optical_tilt_ns;
|
||||
float optical_tilt_residual_deg;
|
||||
timepoint_ns gravity_start_ns;
|
||||
unsigned int gravity_samples;
|
||||
struct xrt_vec3 gravity_accel_sum;
|
||||
struct xrt_vec3 gravity_gyro_sum;
|
||||
float gravity_accel_scale;
|
||||
|
||||
//! Offset for IMU pose from device (from json calibration)
|
||||
struct xrt_pose P_device_imu;
|
||||
//! Offset for device pose from imu
|
||||
struct xrt_pose P_imu_device;
|
||||
|
||||
//! Offset for grip pose
|
||||
struct xrt_pose P_aim_grip;
|
||||
//! Published OpenXR aim and grip in the firmware LED-model frame.
|
||||
struct xrt_pose P_device_aim;
|
||||
struct xrt_pose P_device_grip;
|
||||
|
||||
//! Rate limit for the unconfirmed-fix debug line.
|
||||
timepoint_ns last_candidate_log_ns;
|
||||
|
||||
/* Controls / buttons state */
|
||||
timepoint_ns last_controls_local_time_ns;
|
||||
@@ -116,16 +306,115 @@ struct rift_s_controller
|
||||
|
||||
bool reading_config;
|
||||
bool have_config;
|
||||
//! Earliest time to re-request the calibration block after a parse failure.
|
||||
timepoint_ns calibration_retry_ns;
|
||||
rift_s_controller_config config;
|
||||
//! Attempt 0x31 once per physical controller, with unchanged gyro calibration on failure.
|
||||
bool imu_descriptor_requested;
|
||||
//! The IR LED timing (register 0x28) was queued for this physical controller.
|
||||
bool irled_requested;
|
||||
//! The IMU may start: the descriptor was read, or waiting for it gave up.
|
||||
bool imu_descriptor_done;
|
||||
//! IMU samples wait for the descriptor until this time, then use the factory gyro calibration.
|
||||
timepoint_ns imu_descriptor_deadline_ns;
|
||||
char imu_descriptor[RIFT_S_CONTROLLER_IMU_DESCRIPTOR_SIZE + 1];
|
||||
struct rift_s_tracked_imu_calibration gyro_calibration;
|
||||
|
||||
bool reading_calibration;
|
||||
bool have_calibration;
|
||||
struct rift_s_controller_imu_calibration calibration;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct rift_s_controller *
|
||||
rift_s_controller_create(struct rift_s_system *sys, enum xrt_device_type device_type);
|
||||
|
||||
/*!
|
||||
* Rift S Touch frames in the SteamVR render-model frame (oculus_rifts_controller_left/right):
|
||||
* the firmware LED-model ("device") frame and the OpenXR grip.
|
||||
*/
|
||||
void
|
||||
rift_s_controller_model_frames(bool left, struct xrt_pose *out_model_device, struct xrt_pose *out_model_grip);
|
||||
|
||||
//! OpenXR aim and grip in the firmware LED-model frame.
|
||||
void
|
||||
rift_s_controller_pose_frames(bool left, struct xrt_pose *device_aim, struct xrt_pose *device_grip);
|
||||
|
||||
// Bootstrap tilt from gyro-compensated acceleration; estimate gyro bias only at rest.
|
||||
void
|
||||
rift_s_controller_update_fusion(struct rift_s_controller *ctrl, timepoint_ns timestamp_ns);
|
||||
|
||||
// Record the current fused attitude with both clock readings; caller holds the mutex.
|
||||
bool
|
||||
rift_s_controller_record_attitude(struct rift_s_controller *ctrl,
|
||||
timepoint_ns device_capture_ns,
|
||||
timepoint_ns arrival_ns);
|
||||
|
||||
//! Feed one rectified, unscaled accelerometer sample and the gyro rate. True when a window completed.
|
||||
bool
|
||||
rift_s_rest_stats_push(struct rift_s_rest_stats *s,
|
||||
timepoint_ns timestamp_ns,
|
||||
const struct xrt_vec3 *accel,
|
||||
const struct xrt_vec3 *gyro);
|
||||
|
||||
//! Add one completed rest window and re-estimate the bias.
|
||||
void
|
||||
rift_s_accel_bias_add_window(struct rift_s_accel_bias *b, const struct xrt_vec3 *mean);
|
||||
|
||||
//! Consecutive backwards IMU samples that re-anchor the device clock.
|
||||
#define RIFT_S_IMU_RESYNC_RUN 16
|
||||
//! A backwards step this large re-anchors after two samples in a row.
|
||||
#define RIFT_S_IMU_RESYNC_BACKWARDS_US 1000000u
|
||||
|
||||
//! Whether a backwards IMU timestamp means the device clock anchor is wrong.
|
||||
bool
|
||||
rift_s_imu_clock_needs_resync(uint32_t backwards_us, uint32_t backwards_run);
|
||||
|
||||
//! Add a rest-window mean taken at a known IMU @p attitude (IMU to world).
|
||||
void
|
||||
rift_s_attitude_bias_add(struct rift_s_attitude_bias *b,
|
||||
const struct xrt_vec3 *rest_mean,
|
||||
const struct xrt_quat *attitude);
|
||||
|
||||
/*!
|
||||
* World-frame rotation that moves @p imu's tilt a @p fraction of the way to @p optical's, leaving
|
||||
* heading alone. Returns the full tilt difference in radians.
|
||||
*/
|
||||
float
|
||||
rift_s_optical_tilt_correction(const struct xrt_quat *imu,
|
||||
const struct xrt_quat *optical,
|
||||
float fraction,
|
||||
struct xrt_quat *out_correction);
|
||||
|
||||
// Interpolate a capture-time attitude, with up to 20 ms of gyro prediction for radio scheduling.
|
||||
bool
|
||||
rift_s_controller_get_attitude(struct rift_s_controller *ctrl,
|
||||
timepoint_ns capture_ns,
|
||||
struct rift_s_attitude_sample *out);
|
||||
|
||||
// A heading prior requires recent optical yaw agreement as well as capture-time tilt.
|
||||
bool
|
||||
rift_s_controller_has_heading_prior(struct rift_s_controller *ctrl, timepoint_ns capture_ns);
|
||||
|
||||
void
|
||||
rift_s_controller_push_observed_pose(struct xrt_device *xdev, timepoint_ns frame_mono_ns, const struct xrt_pose *pose);
|
||||
// Build the constellation LED model in the OpenCV-flipped IMU frame, firmware order as LED id.
|
||||
bool
|
||||
rift_s_controller_get_led_model(struct xrt_device *xdev, struct t_constellation_led_model *led_model);
|
||||
// Accept an optical fit. Yaw is corrected only when the fit itself observed it.
|
||||
void
|
||||
rift_s_controller_push_pose_observation(struct xrt_device *xdev, const struct t_constellation_pose_observation *obs);
|
||||
|
||||
// Controller pose callback. Optical loss preserves IMU orientation and estimates hand position.
|
||||
xrt_result_t
|
||||
rift_s_controller_get_tracked_pose(struct xrt_device *xdev,
|
||||
enum xrt_input_name name,
|
||||
int64_t at_timestamp_ns,
|
||||
struct xrt_space_relation *out_relation);
|
||||
|
||||
void
|
||||
rift_s_controller_update_configuration(struct rift_s_controller *ctrl, uint64_t device_id);
|
||||
bool
|
||||
@@ -133,4 +422,8 @@ rift_s_controller_handle_report(struct rift_s_controller *ctrl,
|
||||
timepoint_ns local_ts,
|
||||
rift_s_controller_report_t *report);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -18,6 +18,7 @@
|
||||
/* Oculus Rift S Driver - firmware JSON parsing functions */
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <math.h>
|
||||
|
||||
#include "util/u_json.h"
|
||||
#include "util/u_misc.h"
|
||||
@@ -32,6 +33,43 @@
|
||||
#define JSON_MATRIX_3X3_ARRAY(a, b, c) u_json_get_float_array(u_json_get(a, b), c.v, 9)
|
||||
#define JSON_MATRIX_4x4_ARRAY(a, b, c) u_json_get_float_array(u_json_get(a, b), c.v, 16)
|
||||
|
||||
struct rift_s_tracked_imu_calibration
|
||||
rift_s_controller_gyro_calibration_for_imu(const struct rift_s_tracked_imu_calibration *factory,
|
||||
const char *descriptor)
|
||||
{
|
||||
struct rift_s_tracked_imu_calibration gyro = *factory;
|
||||
if (strcmp(descriptor, "LSM6DSL") == 0) {
|
||||
// Windows multiplies in double precision, then stores each coefficient as float.
|
||||
const double factor = 0.8714285714285714;
|
||||
for (unsigned i = 0; i < 9; i++)
|
||||
gyro.matrix.v[i] = (float)(gyro.matrix.v[i] * factor);
|
||||
gyro.offset.x = (float)(gyro.offset.x * factor);
|
||||
gyro.offset.y = (float)(gyro.offset.y * factor);
|
||||
gyro.offset.z = (float)(gyro.offset.z * factor);
|
||||
}
|
||||
return gyro;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_tracked_imu_calibration(const cJSON *obj, const char *name, struct rift_s_tracked_imu_calibration *out)
|
||||
{
|
||||
float values[12];
|
||||
const cJSON *array = u_json_get(obj, name);
|
||||
if (!cJSON_IsArray(array) || cJSON_GetArraySize(array) != 12)
|
||||
return false;
|
||||
size_t count = u_json_get_float_array(array, values, 12);
|
||||
if (count != 12)
|
||||
return false;
|
||||
for (unsigned i = 0; i < 12; i++) {
|
||||
if (!isfinite(values[i]))
|
||||
return false;
|
||||
}
|
||||
*out = (struct rift_s_tracked_imu_calibration){.num_values = (unsigned)count};
|
||||
memcpy(out->matrix.v, values, sizeof(out->matrix.v));
|
||||
out->offset = (struct xrt_vec3){values[9], values[10], values[11]};
|
||||
return true;
|
||||
}
|
||||
|
||||
int
|
||||
rift_s_parse_proximity_threshold(char *json_string, int *proximity_threshold)
|
||||
{
|
||||
@@ -54,6 +92,28 @@ fail:
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Parse LensOpticalCenterPixelCoord from the lens calibration block (0x12). */
|
||||
int
|
||||
rift_s_parse_lens_centers(char *json_string, struct xrt_vec2 centers[2])
|
||||
{
|
||||
cJSON *json_root = cJSON_Parse(json_string);
|
||||
const cJSON *coords = cJSON_IsObject(json_root) ? u_json_get(json_root, "LensOpticalCenterPixelCoord") : NULL;
|
||||
const char *names[2] = {"Left", "Right"};
|
||||
|
||||
for (int eye = 0; eye < 2; eye++) {
|
||||
float xy[2];
|
||||
if (coords == NULL || u_json_get_float_array(u_json_get(coords, names[eye]), xy, 2) != 2) {
|
||||
RIFT_S_WARN("Unrecognised Rift S lens calibration JSON data.");
|
||||
cJSON_Delete(json_root);
|
||||
return -1;
|
||||
}
|
||||
centers[eye] = (struct xrt_vec2){xy[0], xy[1]};
|
||||
}
|
||||
|
||||
cJSON_Delete(json_root);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool
|
||||
check_file_format_version(cJSON *json_root, float expected_version, float *version_number)
|
||||
{
|
||||
@@ -312,7 +372,12 @@ json_read_lensing_model(const cJSON *lensing_model, struct rift_s_lensing_model
|
||||
return false;
|
||||
}
|
||||
|
||||
model->num_points = cJSON_GetArrayItem(array, 0)->valueint;
|
||||
const cJSON *count = cJSON_GetArrayItem(array, 0);
|
||||
if (!cJSON_IsNumber(count) || count->valuedouble < 0 || count->valuedouble > 4 ||
|
||||
count->valuedouble != count->valueint) {
|
||||
return false;
|
||||
}
|
||||
model->num_points = count->valueint;
|
||||
|
||||
for (int j = 0; j < 4; j++) {
|
||||
const cJSON *item = cJSON_GetArrayItem(array, j + 1);
|
||||
@@ -355,10 +420,10 @@ rift_s_controller_parse_imu_calibration(char *json_string, struct rift_s_control
|
||||
if (!JSON_VEC3(obj, "ImuPosition", &c->imu_position))
|
||||
goto fail;
|
||||
|
||||
if (!JSON_MATRIX_4x4_ARRAY(obj, "AccCalibration", c->accel_calibration))
|
||||
if (!parse_tracked_imu_calibration(obj, "AccCalibration", &c->accel_calibration))
|
||||
goto fail;
|
||||
|
||||
if (!JSON_MATRIX_4x4_ARRAY(obj, "GyroCalibration", c->gyro_calibration))
|
||||
if (!parse_tracked_imu_calibration(obj, "GyroCalibration", &c->gyro_calibration))
|
||||
goto fail;
|
||||
|
||||
/* LED positions */
|
||||
@@ -367,8 +432,15 @@ rift_s_controller_parse_imu_calibration(char *json_string, struct rift_s_control
|
||||
goto fail;
|
||||
}
|
||||
|
||||
c->num_leds = cJSON_GetArraySize(leds);
|
||||
int num_leds = cJSON_GetArraySize(leds);
|
||||
if (num_leds <= 0 || num_leds > UINT8_MAX) {
|
||||
goto fail;
|
||||
}
|
||||
c->num_leds = num_leds;
|
||||
c->leds = calloc(c->num_leds, sizeof(struct rift_s_led));
|
||||
if (c->leds == NULL) {
|
||||
goto fail;
|
||||
}
|
||||
i = 0;
|
||||
cJSON_ArrayForEach(item, leds)
|
||||
{
|
||||
@@ -384,12 +456,19 @@ rift_s_controller_parse_imu_calibration(char *json_string, struct rift_s_control
|
||||
}
|
||||
|
||||
c->num_lensing_models = cJSON_GetArraySize(leds);
|
||||
if (c->num_lensing_models <= 0) {
|
||||
goto fail;
|
||||
}
|
||||
c->lensing_models = calloc(c->num_lensing_models, sizeof(struct rift_s_lensing_model));
|
||||
if (c->lensing_models == NULL) {
|
||||
goto fail;
|
||||
}
|
||||
i = 0;
|
||||
cJSON_ArrayForEach(item, leds)
|
||||
{
|
||||
if (!json_read_lensing_model(leds, c->lensing_models + i, i))
|
||||
goto fail;
|
||||
i++;
|
||||
}
|
||||
|
||||
if (!JSON_MATRIX_3X3_ARRAY(json_root, "gyro_m", c->gyro.rectification) ||
|
||||
|
||||
@@ -109,8 +109,20 @@ struct rift_s_lensing_model
|
||||
float points[4];
|
||||
};
|
||||
|
||||
/* Factory TrackedObject AccCalibration/GyroCalibration: a row-major 3x3 matrix (0..8)
|
||||
* and sensor-space bias (9..11), applied as matrix * (sample - bias).
|
||||
* Acceleration is in m/s^2 and angular velocity in rad/s before this transform. */
|
||||
struct rift_s_tracked_imu_calibration
|
||||
{
|
||||
struct xrt_matrix_3x3 matrix;
|
||||
struct xrt_vec3 offset;
|
||||
//! Numbers in the firmware array; the parser requires exactly 12.
|
||||
unsigned num_values;
|
||||
};
|
||||
|
||||
struct rift_s_controller_imu_calibration
|
||||
{
|
||||
//! Legacy top-level calibration retained for comparison with TrackedObject.
|
||||
struct
|
||||
{
|
||||
struct xrt_matrix_3x3 rectification;
|
||||
@@ -128,12 +140,7 @@ struct rift_s_controller_imu_calibration
|
||||
uint8_t num_leds;
|
||||
struct rift_s_led *leds;
|
||||
|
||||
/* For some reason we have a separate calibration
|
||||
* 4x4 matrix on top of the separate rectification
|
||||
* and offset for gyro and accel
|
||||
*/
|
||||
struct xrt_matrix_4x4 gyro_calibration;
|
||||
struct xrt_matrix_4x4 accel_calibration;
|
||||
struct rift_s_tracked_imu_calibration gyro_calibration, accel_calibration;
|
||||
|
||||
/* Lensing models */
|
||||
int num_lensing_models;
|
||||
@@ -143,11 +150,18 @@ struct rift_s_controller_imu_calibration
|
||||
int
|
||||
rift_s_parse_proximity_threshold(char *json, int *proximity_threshold);
|
||||
int
|
||||
rift_s_parse_lens_centers(char *json, struct xrt_vec2 centers[2]);
|
||||
int
|
||||
rift_s_parse_imu_calibration(char *json, struct rift_s_imu_calibration *c);
|
||||
int
|
||||
rift_s_parse_camera_calibration_block(char *json, struct rift_s_camera_calibration_block *c);
|
||||
int
|
||||
rift_s_controller_parse_imu_calibration(char *json, struct rift_s_controller_imu_calibration *c);
|
||||
/* Derive an effective gyro calibration from the unchanged factory values.
|
||||
* The Windows tracker scales both matrix and bias for exactly "LSM6DSL". */
|
||||
struct rift_s_tracked_imu_calibration
|
||||
rift_s_controller_gyro_calibration_for_imu(const struct rift_s_tracked_imu_calibration *factory,
|
||||
const char *descriptor);
|
||||
void
|
||||
rift_s_controller_free_imu_calibration(struct rift_s_controller_imu_calibration *c);
|
||||
|
||||
|
||||
@@ -26,13 +26,17 @@
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
#include <assert.h>
|
||||
#include <ctype.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "math/m_api.h"
|
||||
#include "util/u_debug.h"
|
||||
#include "math/m_vec3.h"
|
||||
|
||||
#include "os/os_time.h"
|
||||
|
||||
#include "util/u_device.h"
|
||||
#include "util/u_distortion_mesh.h"
|
||||
#include "util/u_trace_marker.h"
|
||||
#include "util/u_var.h"
|
||||
|
||||
@@ -41,6 +45,8 @@
|
||||
#include "rift_s_hmd.h"
|
||||
|
||||
|
||||
DEBUG_GET_ONCE_BOOL_OPTION(steamvr_bench, "MONADO_STEAMVR_BENCH", false)
|
||||
|
||||
static xrt_result_t
|
||||
rift_s_get_tracked_pose(struct xrt_device *xdev,
|
||||
enum xrt_input_name name,
|
||||
@@ -125,8 +131,8 @@ rift_s_hmd_handle_report(struct rift_s_hmd *hmd, timepoint_ns local_ts, rift_s_h
|
||||
accel = m_vec3_sub(raw_accel, imu_calibration->accel.offset_at_0C);
|
||||
gyro = m_vec3_sub(raw_gyro, imu_calibration->gyro.offset);
|
||||
|
||||
math_matrix_3x3_transform_vec3(&imu_calibration->accel.rectification, &raw_accel, &accel);
|
||||
math_matrix_3x3_transform_vec3(&imu_calibration->gyro.rectification, &raw_gyro, &gyro);
|
||||
math_matrix_3x3_transform_vec3(&imu_calibration->accel.rectification, &accel, &accel);
|
||||
math_matrix_3x3_transform_vec3(&imu_calibration->gyro.rectification, &gyro, &gyro);
|
||||
|
||||
/* FIXME: This doesn't seem to produce the right numbers, but it's OK - we don't use it anyway */
|
||||
hmd->temperature = temperature_scale * (s->temperature - temperature_offset) + 25;
|
||||
@@ -153,7 +159,7 @@ static xrt_result_t
|
||||
rift_s_compute_distortion(struct xrt_device *xdev, uint32_t view, float u, float v, struct xrt_uv_triplet *result)
|
||||
{
|
||||
struct rift_s_hmd *hmd = (struct rift_s_hmd *)(xdev);
|
||||
u_compute_distortion_panotools(&hmd->distortion_vals[view], u, v, result);
|
||||
rift_s_optics_compute(&hmd->optics[view], u, v, result);
|
||||
return XRT_SUCCESS;
|
||||
}
|
||||
|
||||
@@ -173,6 +179,69 @@ dump_fw_block(struct os_hid_device *handle, uint8_t block_id) {
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Parse a positive projection-tangent scale from the environment; 1.0 if unset or invalid. */
|
||||
DEBUG_GET_ONCE_NUM_OPTION(rift_s_ipd_mm, "RIFT_S_IPD_MM", 63.5)
|
||||
|
||||
static float
|
||||
parse_fov_scale(const char *name)
|
||||
{
|
||||
const char *text = getenv(name);
|
||||
if (text == NULL) {
|
||||
return 1.0f;
|
||||
}
|
||||
char *end;
|
||||
const float parsed = strtof(text, &end);
|
||||
const bool consumed = end != text;
|
||||
while (isspace((unsigned char)*end)) {
|
||||
end++;
|
||||
}
|
||||
struct rift_s_optics check;
|
||||
if (consumed && *end == '\0' && rift_s_optics_init(&check, 0, 0.0635f, parsed, parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
RIFT_S_WARN("Invalid %s; using 1.0", name);
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
/* Re-read the IPD file at most twice a second; ignores missing or out-of-range values. */
|
||||
static void
|
||||
rift_s_poll_ipd(struct rift_s_hmd *hmd)
|
||||
{
|
||||
int64_t now = os_monotonic_get_ns();
|
||||
if (now < hmd->ipd_next_poll_ns || hmd->ipd_path[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
hmd->ipd_next_poll_ns = now + 500 * 1000000ll;
|
||||
FILE *f = fopen(hmd->ipd_path, "r");
|
||||
if (f == NULL) {
|
||||
return;
|
||||
}
|
||||
float mm = 0;
|
||||
if (fscanf(f, "%f", &mm) == 1 && mm >= 50.0f && mm <= 80.0f && fabsf(mm / 1000.0f - hmd->ipd_m) > 1e-5f) {
|
||||
hmd->ipd_m = mm / 1000.0f;
|
||||
RIFT_S_INFO("Rift S IPD set to %.1f mm", mm);
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
static xrt_result_t
|
||||
rift_s_get_view_poses(struct xrt_device *xdev,
|
||||
const struct xrt_vec3 *default_eye_relation,
|
||||
int64_t at_timestamp_ns,
|
||||
enum xrt_view_type view_type,
|
||||
uint32_t view_count,
|
||||
struct xrt_space_relation *out_head_relation,
|
||||
struct xrt_fov *out_fovs,
|
||||
struct xrt_pose *out_poses)
|
||||
{
|
||||
struct rift_s_hmd *hmd = (struct rift_s_hmd *)xdev;
|
||||
rift_s_poll_ipd(hmd);
|
||||
struct xrt_vec3 eye_relation = *default_eye_relation;
|
||||
eye_relation.x = hmd->ipd_m;
|
||||
return u_device_get_view_poses(xdev, &eye_relation, at_timestamp_ns, view_type, view_count, out_head_relation,
|
||||
out_fovs, out_poses);
|
||||
}
|
||||
|
||||
static void
|
||||
rift_s_hmd_destroy(struct xrt_device *xdev)
|
||||
{
|
||||
@@ -218,12 +287,22 @@ rift_s_hmd_create(struct rift_s_system *sys, const unsigned char *hmd_serial_no,
|
||||
|
||||
hmd->base.update_inputs = u_device_noop_update_inputs;
|
||||
hmd->base.get_tracked_pose = rift_s_get_tracked_pose;
|
||||
hmd->base.get_view_poses = u_device_get_view_poses;
|
||||
hmd->base.get_view_poses = rift_s_get_view_poses;
|
||||
hmd->ipd_m = debug_get_num_option_rift_s_ipd_mm() / 1000.0f;
|
||||
if (hmd->ipd_m < 0.05f || hmd->ipd_m > 0.08f) {
|
||||
hmd->ipd_m = 0.0635f;
|
||||
}
|
||||
snprintf(hmd->ipd_path, sizeof(hmd->ipd_path), "%s/rift-s-ipd.conf", getenv("HOME"));
|
||||
rift_s_poll_ipd(hmd);
|
||||
RIFT_S_INFO("Rift S IPD %.1f mm", hmd->ipd_m * 1000.0f);
|
||||
hmd->base.destroy = rift_s_hmd_destroy;
|
||||
hmd->base.name = XRT_DEVICE_GENERIC_HMD;
|
||||
hmd->base.device_type = XRT_DEVICE_TYPE_HMD;
|
||||
|
||||
hmd->tracker = rift_s_system_get_tracker(sys);
|
||||
hmd->base.camera = hmd->tracker->base.camera;
|
||||
hmd->base.supported.orientation_tracking = true;
|
||||
hmd->base.supported.position_tracking = hmd->tracker->tracking.slam_enabled;
|
||||
|
||||
// Print name.
|
||||
snprintf(hmd->base.str, XRT_DEVICE_NAME_LEN, "Oculus Rift S");
|
||||
@@ -248,9 +327,7 @@ rift_s_hmd_create(struct rift_s_system *sys, const unsigned char *hmd_serial_no,
|
||||
dump_fw_block(hid_hmd, 0x12);
|
||||
#endif
|
||||
|
||||
// Set up display details
|
||||
// FIXME: These are all wrong and should be derived from HMD reports
|
||||
// refresh rate
|
||||
// Native display layout and 80 Hz refresh rate.
|
||||
hmd->base.hmd->screens[0].nominal_frame_interval_ns = time_s_to_ns(1.0f / 80.0f);
|
||||
|
||||
/* In the Rift S, there's one panel that is rotated
|
||||
@@ -280,45 +357,24 @@ rift_s_hmd_create(struct rift_s_system *sys, const unsigned char *hmd_serial_no,
|
||||
hmd->base.hmd->views[0].viewport.y_pixels = 0;
|
||||
hmd->base.hmd->views[1].viewport.y_pixels = view_h;
|
||||
|
||||
/* FIXME: Incorrection distortion taken from the Rift CV1 for now */
|
||||
const double display_w_meters = 0.149760f / 2.0; // Per-eye width
|
||||
const double display_h_meters = 0.093600f;
|
||||
const double lens_sep = 0.074f;
|
||||
const double hFOV = DEG_TO_RAD(105.0);
|
||||
|
||||
// center of projection
|
||||
const double hCOP = lens_sep / 2.0;
|
||||
const double vCOP = display_h_meters / 2.0;
|
||||
|
||||
struct u_panotools_values distortion_vals = {
|
||||
.distortion_k = {0.819f, -0.241f, 0.324f, 0.098f, 0.0},
|
||||
.aberration_k = {0.9952420f, 1.0f, 1.0008074f},
|
||||
.scale = display_w_meters -
|
||||
lens_sep / 2.0, // Assume distortion is across the larger distance from lens center to edge
|
||||
.lens_center = {display_w_meters - hCOP, vCOP},
|
||||
.viewport_size = {display_w_meters, display_h_meters},
|
||||
};
|
||||
|
||||
if (
|
||||
/* right eye */
|
||||
!math_compute_fovs(display_w_meters, hCOP, hFOV, display_h_meters, vCOP, 0.0,
|
||||
&hmd->base.hmd->distortion.fov[1]) ||
|
||||
/*
|
||||
* left eye - same as right eye, except the horizontal center of projection is moved in the opposite
|
||||
* direction now
|
||||
*/
|
||||
!math_compute_fovs(display_w_meters, display_w_meters - hCOP, hFOV, display_h_meters, vCOP, 0.0,
|
||||
&hmd->base.hmd->distortion.fov[0])) {
|
||||
// If those failed, it means our math was impossible.
|
||||
RIFT_S_ERROR("Failed to setup basic device info");
|
||||
goto cleanup;
|
||||
// RIFT_S_FOV_SCALE scales all projection tangents, RIFT_S_FOV_SCALE_Y only the vertical ones.
|
||||
const float fov_scale = parse_fov_scale("RIFT_S_FOV_SCALE");
|
||||
const float fov_scale_y = parse_fov_scale("RIFT_S_FOV_SCALE_Y");
|
||||
for (uint8_t eye = 0; eye < 2; eye++) {
|
||||
// The lens centre follows the IPD at creation; the mesh is built once.
|
||||
if (!rift_s_optics_init(&hmd->optics[eye], eye, hmd->ipd_m, fov_scale, fov_scale_y)) {
|
||||
RIFT_S_ERROR("Failed to initialise Rift S optics");
|
||||
goto cleanup;
|
||||
}
|
||||
hmd->base.hmd->distortion.fov[eye] = hmd->optics[eye].fov;
|
||||
const struct xrt_fov *fov = &hmd->optics[eye].fov;
|
||||
RIFT_S_INFO("Rift S eye %u FOV tan: left=%.6f right=%.6f up=%.6f down=%.6f; scale=%.6f, vertical scale=%.6f",
|
||||
eye, tanf(fov->angle_left), tanf(fov->angle_right), tanf(fov->angle_up), tanf(fov->angle_down),
|
||||
fov_scale, fov_scale_y);
|
||||
RIFT_S_INFO("Rift S eye %u lens centre (%.2f, %.2f) px at IPD %.1f mm", eye,
|
||||
hmd->optics[eye].lens_center_px.x, hmd->optics[eye].lens_center_px.y, hmd->ipd_m * 1000.0f);
|
||||
}
|
||||
|
||||
hmd->distortion_vals[0] = distortion_vals;
|
||||
// Move the lens center for the right view
|
||||
distortion_vals.lens_center.x = hCOP;
|
||||
hmd->distortion_vals[1] = distortion_vals;
|
||||
|
||||
hmd->base.hmd->distortion.models = XRT_DISTORTION_MODEL_COMPUTE;
|
||||
hmd->base.hmd->distortion.preferred = XRT_DISTORTION_MODEL_COMPUTE;
|
||||
hmd->base.compute_distortion = rift_s_compute_distortion;
|
||||
@@ -349,6 +405,11 @@ cleanup:
|
||||
void
|
||||
rift_s_hmd_set_proximity(struct rift_s_hmd *hmd, bool prox_sensor)
|
||||
{
|
||||
if (debug_get_bool_option_steamvr_bench()) {
|
||||
prox_sensor = true;
|
||||
}
|
||||
rift_s_tracker_set_standby(hmd->tracker, !prox_sensor);
|
||||
|
||||
/* Enable the screen if the prox sensor is triggered, or turn it off otherwise. */
|
||||
if (prox_sensor != hmd->display_on) {
|
||||
struct os_hid_device *hid_hmd = rift_s_system_hid_handle(hmd->sys);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "math/m_imu_3dof.h"
|
||||
#include "util/u_distortion_mesh.h"
|
||||
#include "rift_s_optics.h"
|
||||
#include "xrt/xrt_defines.h"
|
||||
#include "xrt/xrt_device.h"
|
||||
|
||||
@@ -45,8 +45,14 @@ struct rift_s_hmd
|
||||
float temperature;
|
||||
bool display_on;
|
||||
|
||||
/* Temporary distortion values for mesh calc */
|
||||
struct u_panotools_values distortion_vals[2];
|
||||
/* Per-eye projection and lens model. */
|
||||
struct rift_s_optics optics[2];
|
||||
|
||||
/* Eye separation in metres. Rift S lenses are fixed, IPD is a rendering setting.
|
||||
* Set by RIFT_S_IPD_MM, live-updated from ~/rift-s-ipd.conf (millimetres). */
|
||||
float ipd_m;
|
||||
char ipd_path[4096];
|
||||
int64_t ipd_next_poll_ns;
|
||||
};
|
||||
|
||||
struct rift_s_hmd *
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
/*!
|
||||
* @file
|
||||
* @brief Consistency checks for Rift S controller firmware models.
|
||||
* @ingroup drv_rift_s
|
||||
*/
|
||||
#include "rift_s_model_check.h"
|
||||
|
||||
#include <Eigen/Core>
|
||||
#include <Eigen/LU>
|
||||
#include <Eigen/SVD>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
namespace {
|
||||
using V3 = Eigen::Vector3f;
|
||||
using M3 = Eigen::Matrix3f;
|
||||
|
||||
V3
|
||||
vec(const xrt_vec3 &v)
|
||||
{
|
||||
return V3(v.x, v.y, v.z);
|
||||
}
|
||||
|
||||
float
|
||||
degrees(float radians)
|
||||
{
|
||||
return radians * 180.0f / float(M_PI);
|
||||
}
|
||||
|
||||
rift_s_matrix_summary
|
||||
summarise(const M3 &m)
|
||||
{
|
||||
rift_s_matrix_summary s{};
|
||||
s.determinant = m.determinant();
|
||||
s.reflection = s.determinant < 0;
|
||||
Eigen::JacobiSVD<M3> svd(m, Eigen::ComputeFullU | Eigen::ComputeFullV);
|
||||
s.singular_min = svd.singularValues().minCoeff();
|
||||
s.singular_max = svd.singularValues().maxCoeff();
|
||||
M3 d = M3::Identity();
|
||||
d(2, 2) = (svd.matrixU() * svd.matrixV().transpose()).determinant() < 0 ? -1 : 1;
|
||||
M3 r = svd.matrixU() * d * svd.matrixV().transpose();
|
||||
s.rotation_deg = degrees(std::acos(std::clamp((r.trace() - 1) / 2, -1.0f, 1.0f)));
|
||||
return s;
|
||||
}
|
||||
|
||||
M3
|
||||
nearest_rotation(const M3 &m)
|
||||
{
|
||||
Eigen::JacobiSVD<M3> svd(m, Eigen::ComputeFullU | Eigen::ComputeFullV);
|
||||
M3 d = M3::Identity();
|
||||
d(2, 2) = (svd.matrixU() * svd.matrixV().transpose()).determinant() < 0 ? -1 : 1;
|
||||
return svd.matrixU() * d * svd.matrixV().transpose();
|
||||
}
|
||||
|
||||
/* Firmware matrices are stored row-major, as parsed. */
|
||||
M3
|
||||
matrix3(const xrt_matrix_3x3 &m)
|
||||
{
|
||||
M3 out;
|
||||
for (int r = 0; r < 3; r++)
|
||||
for (int c = 0; c < 3; c++)
|
||||
out(r, c) = m.v[3 * r + c];
|
||||
return out;
|
||||
}
|
||||
|
||||
float
|
||||
rotation_between_deg(const M3 &a, const M3 &b)
|
||||
{
|
||||
M3 between = nearest_rotation(a).transpose() * nearest_rotation(b);
|
||||
return degrees(std::acos(std::clamp((between.trace() - 1) / 2, -1.0f, 1.0f)));
|
||||
}
|
||||
|
||||
V3
|
||||
relate(const V3 &v, rift_s_hand_relation relation)
|
||||
{
|
||||
V3 out = v;
|
||||
if (relation != RIFT_S_HANDS_IDENTICAL)
|
||||
out(relation - RIFT_S_HANDS_MIRROR_X) = -out(relation - RIFT_S_HANDS_MIRROR_X);
|
||||
return out;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
extern "C" void
|
||||
rift_s_controller_model_check(const struct rift_s_controller_imu_calibration *c,
|
||||
struct rift_s_controller_model_report *out)
|
||||
{
|
||||
*out = {};
|
||||
if (c == nullptr || c->leds == nullptr || c->num_leds == 0)
|
||||
return;
|
||||
out->num_leds = c->num_leds;
|
||||
out->num_lensing_models = c->num_lensing_models;
|
||||
// The parser reads "Point%d" for 0..n-1, so a gap fails parsing instead of renumbering.
|
||||
out->ids_sequential = true;
|
||||
V3 centroid = V3::Zero();
|
||||
for (unsigned i = 0; i < c->num_leds; i++)
|
||||
centroid += vec(c->leds[i].pos);
|
||||
centroid /= float(c->num_leds);
|
||||
out->centroid = {centroid.x(), centroid.y(), centroid.z()};
|
||||
out->normal_length_min = std::numeric_limits<float>::infinity();
|
||||
out->angle_x_min = out->angle_y_min = std::numeric_limits<float>::infinity();
|
||||
out->angle_x_max = out->angle_y_max = -std::numeric_limits<float>::infinity();
|
||||
for (unsigned i = 0; i < c->num_leds; i++) {
|
||||
const rift_s_led &led = c->leds[i];
|
||||
float length = vec(led.dir).norm();
|
||||
out->normal_length_min = std::min(out->normal_length_min, length);
|
||||
out->normal_length_max = std::max(out->normal_length_max, length);
|
||||
out->outward_normals += vec(led.dir).dot(vec(led.pos) - centroid) > 0;
|
||||
out->angle_x_min = std::min(out->angle_x_min, led.angles.x);
|
||||
out->angle_x_max = std::max(out->angle_x_max, led.angles.x);
|
||||
out->angle_y_min = std::min(out->angle_y_min, led.angles.y);
|
||||
out->angle_y_max = std::max(out->angle_y_max, led.angles.y);
|
||||
}
|
||||
out->imu_position = c->imu_position;
|
||||
out->imu_to_centroid_m = (vec(c->imu_position) - centroid).norm();
|
||||
M3 accel = matrix3(c->accel.rectification), gyro = matrix3(c->gyro.rectification);
|
||||
out->accel_rectification = summarise(accel);
|
||||
out->gyro_rectification = summarise(gyro);
|
||||
out->accel_gyro_axes_deg = rotation_between_deg(accel, gyro);
|
||||
M3 accel_tracked = matrix3(c->accel_calibration.matrix), gyro_tracked = matrix3(c->gyro_calibration.matrix);
|
||||
out->accel_calibration = summarise(accel_tracked);
|
||||
out->gyro_calibration = summarise(gyro_tracked);
|
||||
out->accel_calibration_offset_norm = vec(c->accel_calibration.offset).norm();
|
||||
out->gyro_calibration_offset_norm = vec(c->gyro_calibration.offset).norm();
|
||||
out->accel_calibration_values = c->accel_calibration.num_values;
|
||||
out->gyro_calibration_values = c->gyro_calibration.num_values;
|
||||
out->accel_calibration_vs_rectification_deg = rotation_between_deg(accel, accel_tracked);
|
||||
out->gyro_calibration_vs_rectification_deg = rotation_between_deg(gyro, gyro_tracked);
|
||||
}
|
||||
|
||||
extern "C" void
|
||||
rift_s_controller_gravity_check(const struct rift_s_controller_imu_calibration *c,
|
||||
const struct xrt_vec3 *calibrated_mean,
|
||||
struct rift_s_controller_gravity_report *out)
|
||||
{
|
||||
*out = {};
|
||||
if (c == nullptr || calibrated_mean == nullptr || c->accel_calibration.num_values != 12)
|
||||
return;
|
||||
M3 factory = matrix3(c->accel_calibration.matrix);
|
||||
if (std::abs(factory.determinant()) < 1e-6f)
|
||||
return;
|
||||
// Recover sensor units from the factory-calibrated sample, then compare the legacy fields.
|
||||
V3 tracked = vec(*calibrated_mean);
|
||||
V3 sample = factory.inverse() * tracked + vec(c->accel_calibration.offset);
|
||||
V3 rectified = matrix3(c->accel.rectification) * (sample - vec(c->accel.offset));
|
||||
out->rectified_mps2 = rectified.norm();
|
||||
out->tracked_mps2 = tracked.norm();
|
||||
if (out->rectified_mps2 <= 0 || out->tracked_mps2 <= 0)
|
||||
return;
|
||||
float dot = rectified.normalized().dot(tracked.normalized());
|
||||
out->direction_deg = degrees(std::acos(std::clamp(dot, -1.0f, 1.0f)));
|
||||
out->valid = true;
|
||||
}
|
||||
|
||||
extern "C" void
|
||||
rift_s_controller_compare_hands(const struct rift_s_controller_imu_calibration *left,
|
||||
const struct rift_s_controller_imu_calibration *right,
|
||||
struct rift_s_controller_hand_comparison *out)
|
||||
{
|
||||
*out = {};
|
||||
if (left == nullptr || right == nullptr || left->leds == nullptr || right->leds == nullptr ||
|
||||
left->num_leds == 0 || right->num_leds == 0)
|
||||
return;
|
||||
float best = std::numeric_limits<float>::infinity();
|
||||
for (int r = 0; r < RIFT_S_HANDS_RELATION_COUNT; r++) {
|
||||
auto relation = rift_s_hand_relation(r);
|
||||
double squared = 0, normal = 0;
|
||||
for (unsigned i = 0; i < right->num_leds; i++) {
|
||||
V3 p = relate(vec(right->leds[i].pos), relation);
|
||||
V3 n = relate(vec(right->leds[i].dir), relation).normalized();
|
||||
float nearest = std::numeric_limits<float>::infinity();
|
||||
unsigned match = 0;
|
||||
for (unsigned j = 0; j < left->num_leds; j++) {
|
||||
float d = (vec(left->leds[j].pos) - p).norm();
|
||||
if (d < nearest) {
|
||||
nearest = d;
|
||||
match = j;
|
||||
}
|
||||
}
|
||||
squared += double(nearest) * nearest;
|
||||
float dot = std::clamp(n.dot(vec(left->leds[match].dir).normalized()), -1.0f, 1.0f);
|
||||
normal += degrees(std::acos(dot));
|
||||
}
|
||||
out->position_rms_m[r] = float(std::sqrt(squared / right->num_leds));
|
||||
out->normal_mean_deg[r] = float(normal / right->num_leds);
|
||||
out->imu_difference_m[r] = (relate(vec(right->imu_position), relation) - vec(left->imu_position)).norm();
|
||||
if (out->position_rms_m[r] < best) {
|
||||
best = out->position_rms_m[r];
|
||||
out->best = relation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" const char *
|
||||
rift_s_hand_relation_name(enum rift_s_hand_relation relation)
|
||||
{
|
||||
switch (relation) {
|
||||
case RIFT_S_HANDS_IDENTICAL: return "identical";
|
||||
case RIFT_S_HANDS_MIRROR_X: return "mirror_x";
|
||||
case RIFT_S_HANDS_MIRROR_Y: return "mirror_y";
|
||||
case RIFT_S_HANDS_MIRROR_Z: return "mirror_z";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
/*!
|
||||
* @file
|
||||
* @brief Consistency checks for Rift S controller firmware models.
|
||||
*
|
||||
* These checks do not change tracking. They summarise per-device firmware data so left/right
|
||||
* identity, normals, IMU placement, IMU axes and mirroring can be compared on real hardware.
|
||||
*
|
||||
* @ingroup drv_rift_s
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "rift_s_firmware.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
//! Rotation, reflection and scale summary of a calibration matrix.
|
||||
struct rift_s_matrix_summary
|
||||
{
|
||||
float determinant;
|
||||
//! Angle of the nearest proper rotation (polar decomposition), degrees.
|
||||
float rotation_deg;
|
||||
//! Singular values: away from 1 means scale or non-orthogonality.
|
||||
float singular_min, singular_max;
|
||||
//! The matrix contains a reflection (determinant < 0).
|
||||
bool reflection;
|
||||
};
|
||||
|
||||
struct rift_s_controller_model_report
|
||||
{
|
||||
unsigned num_leds;
|
||||
unsigned num_lensing_models;
|
||||
//! LED ids are the PointN indices: always 0..num_leds-1 when parsing succeeded.
|
||||
bool ids_sequential;
|
||||
float normal_length_min, normal_length_max;
|
||||
//! Normals pointing away from the LED centroid (dot(dir, pos - centroid) > 0).
|
||||
unsigned outward_normals;
|
||||
struct xrt_vec3 centroid;
|
||||
struct xrt_vec3 imu_position;
|
||||
float imu_to_centroid_m;
|
||||
//! Range of the two per-LED angle fields (85/80 so far); Monado uses one global limit.
|
||||
float angle_x_min, angle_x_max, angle_y_min, angle_y_max;
|
||||
struct rift_s_matrix_summary accel_rectification;
|
||||
struct rift_s_matrix_summary gyro_rectification;
|
||||
//! Angle between the rotation parts of the accelerometer and gyro rectifications, degrees.
|
||||
float accel_gyro_axes_deg;
|
||||
//! TrackedObject AccCalibration/GyroCalibration 3x3 part: parsed, not applied by Monado.
|
||||
struct rift_s_matrix_summary accel_calibration;
|
||||
struct rift_s_matrix_summary gyro_calibration;
|
||||
float accel_calibration_offset_norm, gyro_calibration_offset_norm;
|
||||
//! Numbers in each firmware array (the 3x3 + offset layout needs 12).
|
||||
unsigned accel_calibration_values, gyro_calibration_values;
|
||||
//! Angle between the rotation parts of the TrackedObject matrix and the rectification, degrees.
|
||||
float accel_calibration_vs_rectification_deg, gyro_calibration_vs_rectification_deg;
|
||||
};
|
||||
|
||||
enum rift_s_hand_relation
|
||||
{
|
||||
RIFT_S_HANDS_IDENTICAL,
|
||||
RIFT_S_HANDS_MIRROR_X,
|
||||
RIFT_S_HANDS_MIRROR_Y,
|
||||
RIFT_S_HANDS_MIRROR_Z,
|
||||
RIFT_S_HANDS_RELATION_COUNT,
|
||||
};
|
||||
|
||||
struct rift_s_controller_hand_comparison
|
||||
{
|
||||
//! Nearest-neighbour RMS (metres) from right LEDs to left LEDs under each relation.
|
||||
float position_rms_m[RIFT_S_HANDS_RELATION_COUNT];
|
||||
//! Mean angle between matched normals under each relation, degrees.
|
||||
float normal_mean_deg[RIFT_S_HANDS_RELATION_COUNT];
|
||||
//! IMU position difference under each relation, metres.
|
||||
float imu_difference_m[RIFT_S_HANDS_RELATION_COUNT];
|
||||
enum rift_s_hand_relation best;
|
||||
};
|
||||
|
||||
/*!
|
||||
* The resting accelerometer under both firmware calibrations. Monado uses the top-level
|
||||
* rectification and offset; the TrackedObject calibration is the alternative. On a stationary
|
||||
* controller the right one reads 9.81 m/s^2, and the direction difference is the tilt error the
|
||||
* other would introduce.
|
||||
*/
|
||||
struct rift_s_controller_gravity_report
|
||||
{
|
||||
//! |g| under acc_m/acc_b (the driver's calibration), m/s^2.
|
||||
float rectified_mps2;
|
||||
//! |g| under the TrackedObject AccCalibration, m/s^2.
|
||||
float tracked_mps2;
|
||||
//! Angle between the two gravity directions in the IMU frame, degrees.
|
||||
float direction_deg;
|
||||
bool valid;
|
||||
};
|
||||
|
||||
//! @p calibrated_mean is the mean resting accelerometer after factory TrackedObject calibration.
|
||||
void
|
||||
rift_s_controller_gravity_check(const struct rift_s_controller_imu_calibration *calibration,
|
||||
const struct xrt_vec3 *calibrated_mean,
|
||||
struct rift_s_controller_gravity_report *out);
|
||||
|
||||
void
|
||||
rift_s_controller_model_check(const struct rift_s_controller_imu_calibration *calibration,
|
||||
struct rift_s_controller_model_report *out);
|
||||
|
||||
void
|
||||
rift_s_controller_compare_hands(const struct rift_s_controller_imu_calibration *left,
|
||||
const struct rift_s_controller_imu_calibration *right,
|
||||
struct rift_s_controller_hand_comparison *out);
|
||||
|
||||
const char *
|
||||
rift_s_hand_relation_name(enum rift_s_hand_relation relation);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,149 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
|
||||
#include "rift_s_optics.h"
|
||||
|
||||
#include <math.h>
|
||||
|
||||
#define VIEW_W 1280.0
|
||||
#define VIEW_H 1440.0
|
||||
|
||||
/* Oculus runtime 1.82.0, hmdq 2.1.5, Rift S native 80 Hz, IPD 63 mm:
|
||||
* https://risa2000.github.io/hmdgdb/hmd_cfgs/OculusRiftS_Native_R80.html
|
||||
* Left eye FOV tangents; the right eye is mirrored.
|
||||
*/
|
||||
static const double left_extent = 0.965689;
|
||||
static const double right_extent = 1.035530;
|
||||
static const double up_extent = 1.000000;
|
||||
static const double down_extent = 1.150368;
|
||||
|
||||
/* Fitted to the runtime's distortion mesh cache (Meta Horizon runtime 1.208, firmware 2.2), identical for both
|
||||
* eyes: panel pixel radius = k0*t + k1*t^3 + k2*t^5 + k3*t^7 + k4*t^9 for tangent radius t. Max fit error is
|
||||
* below 0.45 px inside the visible FOV. Monotonic on [0, TAN_MAX].
|
||||
*/
|
||||
#define TAN_MAX 1.7
|
||||
static const double channel_k[3][5] = {
|
||||
{828.9789, -281.8288, 92.5256, -18.9440, 1.7376}, // R
|
||||
{823.6001, -288.6719, 104.1311, -25.0075, 2.8234}, // G
|
||||
{820.1631, -289.5132, 107.2478, -26.7999, 3.1539}, // B
|
||||
};
|
||||
|
||||
/* Left eye lens centre x in the per-eye view for IPDs 53.5, 55.5, ... 73.5 mm. The runtime shifts the lens
|
||||
* centre with IPD ("eye shift swim"); y is the same for all of them. The right eye is the mirror image.
|
||||
* Firmware block 0x12 centres differ from these by ~2 px and are not what the runtime renders with.
|
||||
*/
|
||||
#define CENTER_IPD_MIN_MM 53.5
|
||||
#define CENTER_IPD_STEP_MM 2.0
|
||||
#define CENTER_COUNT 11
|
||||
static const double left_center_x[CENTER_COUNT] = {
|
||||
605.495, 605.366, 605.671, 606.293, 607.116, 608.039, 608.962, 609.785, 610.407, 610.712, 610.583,
|
||||
};
|
||||
#define CENTER_Y 719.992
|
||||
|
||||
static double
|
||||
radius_px(const double *k, double t)
|
||||
{
|
||||
const double t2 = t * t;
|
||||
return t * (k[0] + t2 * (k[1] + t2 * (k[2] + t2 * (k[3] + t2 * k[4]))));
|
||||
}
|
||||
|
||||
static double
|
||||
radius_px_slope(const double *k, double t)
|
||||
{
|
||||
const double t2 = t * t;
|
||||
return k[0] + t2 * (3 * k[1] + t2 * (5 * k[2] + t2 * (7 * k[3] + t2 * 9 * k[4])));
|
||||
}
|
||||
|
||||
/* Inverts radius_px on [0, TAN_MAX] with a bracketed Newton iteration. Beyond the range it continues linearly;
|
||||
* those points are outside the FOV anyway.
|
||||
*/
|
||||
static double
|
||||
tangent_radius(const double *k, double r)
|
||||
{
|
||||
const double r_max = radius_px(k, TAN_MAX);
|
||||
if (r >= r_max) {
|
||||
return TAN_MAX + (r - r_max) / radius_px_slope(k, TAN_MAX);
|
||||
}
|
||||
double lo = 0, hi = TAN_MAX;
|
||||
double t = r / k[0];
|
||||
for (int i = 0; i < 50; i++) {
|
||||
if (!(t > lo && t < hi)) {
|
||||
t = 0.5 * (lo + hi);
|
||||
}
|
||||
const double err = radius_px(k, t) - r;
|
||||
if (fabs(err) < 1e-9) {
|
||||
break;
|
||||
}
|
||||
if (err > 0) {
|
||||
hi = t;
|
||||
} else {
|
||||
lo = t;
|
||||
}
|
||||
t -= err / radius_px_slope(k, t);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
/* Piecewise-linear lookup, clamped to the table's IPD range. */
|
||||
static double
|
||||
lens_center_x(double ipd_mm)
|
||||
{
|
||||
double pos = (ipd_mm - CENTER_IPD_MIN_MM) / CENTER_IPD_STEP_MM;
|
||||
pos = fmin(fmax(pos, 0.0), CENTER_COUNT - 1);
|
||||
const int i = pos < CENTER_COUNT - 1 ? (int)pos : CENTER_COUNT - 2;
|
||||
return left_center_x[i] + (pos - i) * (left_center_x[i + 1] - left_center_x[i]);
|
||||
}
|
||||
|
||||
bool
|
||||
rift_s_optics_init(struct rift_s_optics *optics, uint32_t eye, float ipd_m, float fov_scale, float fov_scale_y)
|
||||
{
|
||||
if (eye > 1 || !isfinite(ipd_m) || !isfinite(fov_scale) || fov_scale <= 0 || !isfinite(fov_scale_y) ||
|
||||
fov_scale_y <= 0) {
|
||||
return false;
|
||||
}
|
||||
const float left = (eye == 0 ? left_extent : right_extent) * fov_scale;
|
||||
const float right = (eye == 0 ? right_extent : left_extent) * fov_scale;
|
||||
const float up = up_extent * fov_scale * fov_scale_y;
|
||||
const float down = down_extent * fov_scale * fov_scale_y;
|
||||
const struct xrt_fov fov = {
|
||||
.angle_left = -atanf(left),
|
||||
.angle_right = atanf(right),
|
||||
.angle_up = atanf(up),
|
||||
.angle_down = -atanf(down),
|
||||
};
|
||||
/* Reject scales that overflow or round a projection angle to pi/2. */
|
||||
if (!(tanf(fov.angle_left) < 0 && tanf(fov.angle_right) > 0 && tanf(fov.angle_up) > 0 &&
|
||||
tanf(fov.angle_down) < 0)) {
|
||||
return false;
|
||||
}
|
||||
const double cx = lens_center_x(ipd_m * 1000.0);
|
||||
*optics = (struct rift_s_optics){
|
||||
.fov = fov,
|
||||
.lens_center_px = {eye == 0 ? cx : VIEW_W - cx, CENTER_Y},
|
||||
.tan_left = -tanf(fov.angle_left),
|
||||
.tan_right = tanf(fov.angle_right),
|
||||
.tan_up = tanf(fov.angle_up),
|
||||
.tan_down = -tanf(fov.angle_down),
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
rift_s_optics_compute(const struct rift_s_optics *optics, float u, float v, struct xrt_uv_triplet *result)
|
||||
{
|
||||
/* UVs are in the upright per-eye view, top-left origin and Y down.
|
||||
* driver_monado/compositor already apply the physical panel rotation.
|
||||
* Out-of-FOV samples are normal near the corners and are not clamped.
|
||||
*/
|
||||
const double dx = u * VIEW_W - optics->lens_center_px.x;
|
||||
const double dy = v * VIEW_H - optics->lens_center_px.y;
|
||||
const double r = hypot(dx, dy);
|
||||
const double width = (double)optics->tan_left + optics->tan_right;
|
||||
const double height = (double)optics->tan_up + optics->tan_down;
|
||||
struct xrt_vec2 *rgb[3] = {&result->r, &result->g, &result->b};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
const double scale = r > 0 ? tangent_radius(channel_k[i], r) / r : 0.0;
|
||||
rgb[i]->x = (float)((dx * scale + optics->tan_left) / width);
|
||||
rgb[i]->y = (float)((dy * scale + optics->tan_up) / height);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
#pragma once
|
||||
|
||||
#include "xrt/xrt_defines.h"
|
||||
#include "xrt/xrt_device.h"
|
||||
|
||||
/*!
|
||||
* Rift S lens model fitted to the Oculus runtime's own distortion mesh.
|
||||
*
|
||||
* Each colour channel is purely radial around the lens centre:
|
||||
* panel pixel radius = odd polynomial of the tangent-space radius.
|
||||
*/
|
||||
struct rift_s_optics
|
||||
{
|
||||
struct xrt_fov fov;
|
||||
//! Lens centre in the upright 1280x1440 per-eye view, top-left origin, Y down.
|
||||
struct xrt_vec2 lens_center_px;
|
||||
//! Positive tangents of the published fov, used to map tangent space to UV.
|
||||
float tan_left, tan_right, tan_up, tan_down;
|
||||
};
|
||||
|
||||
/*!
|
||||
* @param eye 0 left, 1 right
|
||||
* @param ipd_m Configured eye separation in metres; shifts the lens centre like the Oculus runtime does
|
||||
* @param fov_scale Scales all projection tangents (changes the fov only, not the lens)
|
||||
* @param fov_scale_y Additionally scales the vertical tangents
|
||||
*/
|
||||
bool
|
||||
rift_s_optics_init(struct rift_s_optics *optics, uint32_t eye, float ipd_m, float fov_scale, float fov_scale_y);
|
||||
|
||||
/*!
|
||||
* @param u,v Position in the upright per-eye view, top-left origin, Y down, in 0..1
|
||||
* @param result Per-channel UV into the eye's rendered image
|
||||
*/
|
||||
void
|
||||
rift_s_optics_compute(const struct rift_s_optics *optics, float u, float v, struct xrt_uv_triplet *result);
|
||||
@@ -0,0 +1,263 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
#include "rift_s_passthrough.h"
|
||||
#include "os/os_threading.h"
|
||||
#include "os/os_time.h"
|
||||
#include "util/u_frame.h"
|
||||
#include "util/u_logging.h"
|
||||
#include <inttypes.h>
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#define VIEW_SIZE 320
|
||||
#define MAX_AGE_NS (250 * U_TIME_1MS_IN_NS)
|
||||
|
||||
struct rift_s_passthrough
|
||||
{
|
||||
struct xrt_camera base;
|
||||
struct os_mutex mutex;
|
||||
struct os_mutex convert_mutex;
|
||||
bool active;
|
||||
struct xrt_frame *raw[2];
|
||||
struct xrt_frame *converted;
|
||||
uint64_t rejected_pairs;
|
||||
uint64_t last_diag_ns;
|
||||
struct xrt_vec2 map[2][VIEW_SIZE * VIEW_SIZE];
|
||||
};
|
||||
|
||||
static bool
|
||||
valid_payload(const struct xrt_frame *frame)
|
||||
{
|
||||
return frame && frame->data && frame->format == XRT_FORMAT_L8 && frame->width >= 2 &&
|
||||
frame->height >= 2 && frame->stride >= frame->width && frame->size >= frame->width &&
|
||||
frame->height - 1 <= (frame->size - frame->width) / frame->stride;
|
||||
}
|
||||
|
||||
struct sensor_pixels
|
||||
{
|
||||
uint8_t min, max;
|
||||
uint64_t sum;
|
||||
};
|
||||
|
||||
static bool
|
||||
has_pixels(const struct xrt_frame *frame)
|
||||
{
|
||||
for (uint32_t y = 0; y < frame->height; ++y)
|
||||
for (uint32_t x = 0; x < frame->width; ++x)
|
||||
if (frame->data[y * frame->stride + x] != 0)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static struct sensor_pixels
|
||||
measure_pixels(const struct xrt_frame *frame)
|
||||
{
|
||||
struct sensor_pixels pixels = {.min = UINT8_MAX};
|
||||
// Inspect image rows, not the transport padding between sensor crops.
|
||||
for (uint32_t y = 0; y < frame->height; ++y) {
|
||||
const uint8_t *row = frame->data + y * frame->stride;
|
||||
for (uint32_t x = 0; x < frame->width; ++x) {
|
||||
uint8_t value = row[x];
|
||||
pixels.min = value < pixels.min ? value : pixels.min;
|
||||
pixels.max = value > pixels.max ? value : pixels.max;
|
||||
pixels.sum += value;
|
||||
}
|
||||
}
|
||||
return pixels;
|
||||
}
|
||||
|
||||
bool
|
||||
rift_s_passthrough_project(const struct rift_s_camera_calibration *c, float x, float y, struct xrt_vec2 *pixel)
|
||||
{
|
||||
/* Firmware DeviceFromCamera maps native +Y down/+Z forward camera
|
||||
* rays into the +Y up/-Z forward headset. Include sensor roll/pitch. */
|
||||
const float *m = c->device_from_camera.v;
|
||||
double cx = m[0] * x - m[1] * y - m[2];
|
||||
double cy = m[4] * x - m[5] * y - m[6];
|
||||
double cz = m[8] * x - m[9] * y - m[10];
|
||||
if (cz <= 0 || !isfinite(cx) || !isfinite(cy) || !isfinite(cz))
|
||||
return false;
|
||||
cx /= cz;
|
||||
cy /= cz;
|
||||
double radius = hypot(cx, cy);
|
||||
double theta = atan(radius);
|
||||
double theta2 = theta * theta;
|
||||
double scale = radius > 1e-8 ? theta / radius : 1;
|
||||
double u = cx * scale, v = cy * scale;
|
||||
double radial = c->distortion.k[5];
|
||||
for (int i = 4; i >= 0; --i)
|
||||
radial = radial * theta2 + c->distortion.k[i];
|
||||
radial = radial * theta2 + 1;
|
||||
double p1 = c->distortion.p1, p2 = c->distortion.p2;
|
||||
pixel->x = c->projection.fx * (u * radial + 2 * p1 * u * v + p2 * (theta2 + 2 * u * u)) + c->projection.cx;
|
||||
pixel->y = c->projection.fy * (v * radial + 2 * p2 * u * v + p1 * (theta2 + 2 * v * v)) + c->projection.cy;
|
||||
return isfinite(pixel->x) && isfinite(pixel->y);
|
||||
}
|
||||
|
||||
static void
|
||||
set_active(struct xrt_camera *camera, bool active)
|
||||
{
|
||||
struct rift_s_passthrough *p = (struct rift_s_passthrough *)camera;
|
||||
os_mutex_lock(&p->mutex);
|
||||
p->active = active;
|
||||
if (!active)
|
||||
for (int i = 0; i < 2; ++i)
|
||||
xrt_frame_reference(&p->raw[i], NULL);
|
||||
os_mutex_unlock(&p->mutex);
|
||||
}
|
||||
|
||||
void
|
||||
rift_s_passthrough_push(struct xrt_camera *camera, struct xrt_frame *left, struct xrt_frame *right)
|
||||
{
|
||||
if (!camera)
|
||||
return;
|
||||
struct rift_s_passthrough *p = (struct rift_s_passthrough *)camera;
|
||||
/* This handoff runs on camera ingestion, which also delivers controller
|
||||
* exposures. A slow export consumer must lose a video frame, not block it. */
|
||||
if (os_mutex_trylock(&p->mutex) != 0)
|
||||
return;
|
||||
if (p->active) {
|
||||
bool valid = valid_payload(left) && valid_payload(right) && left->width == right->width &&
|
||||
left->height == right->height && left->timestamp == right->timestamp &&
|
||||
left->source_sequence == right->source_sequence;
|
||||
bool nonempty[2] = {false};
|
||||
if (valid) {
|
||||
nonempty[0] = has_pixels(left);
|
||||
nonempty[1] = has_pixels(right);
|
||||
// Missing sensor payloads can have valid packed-frame metadata. A dark
|
||||
// stereo scene is valid; a unilateral all-zero payload is not a pair.
|
||||
valid = nonempty[0] == nonempty[1];
|
||||
}
|
||||
if (!valid) {
|
||||
++p->rejected_pairs;
|
||||
if ((p->rejected_pairs & (p->rejected_pairs - 1)) == 0)
|
||||
U_LOG_W("Passthrough rejected stereo pair count=%" PRIu64 " left_nonzero=%d right_nonzero=%d",
|
||||
p->rejected_pairs, nonempty[0], nonempty[1]);
|
||||
// Preserve only the previous complete pair. get_frame still expires it
|
||||
// at its original capture timestamp, never at the rejected timestamp.
|
||||
os_mutex_unlock(&p->mutex);
|
||||
return;
|
||||
}
|
||||
uint64_t now = os_monotonic_get_ns();
|
||||
if (now - p->last_diag_ns >= 5ULL * U_TIME_1S_IN_NS) {
|
||||
p->last_diag_ns = now;
|
||||
struct sensor_pixels pixels[2] = {measure_pixels(left), measure_pixels(right)};
|
||||
double count = (double)left->width * left->height;
|
||||
u_log(__FILE__, __LINE__, __func__, U_LOGGING_INFO,
|
||||
"Passthrough stereo sequence=%" PRIu64 " timestamp=%" PRIu64
|
||||
" left=%u,%u,%.3f right=%u,%u,%.3f rejected=%" PRIu64,
|
||||
left->source_sequence, left->timestamp, pixels[0].min, pixels[0].max,
|
||||
pixels[0].sum / count, pixels[1].min, pixels[1].max, pixels[1].sum / count,
|
||||
p->rejected_pairs);
|
||||
}
|
||||
xrt_frame_reference(&p->raw[0], left);
|
||||
xrt_frame_reference(&p->raw[1], right);
|
||||
}
|
||||
os_mutex_unlock(&p->mutex);
|
||||
}
|
||||
|
||||
static bool
|
||||
get_frame(struct xrt_camera *camera, struct xrt_frame **out)
|
||||
{
|
||||
struct rift_s_passthrough *p = (struct rift_s_passthrough *)camera;
|
||||
struct xrt_frame *raw[2] = {0};
|
||||
os_mutex_lock(&p->convert_mutex);
|
||||
os_mutex_lock(&p->mutex);
|
||||
int64_t now = os_monotonic_get_ns();
|
||||
bool available = p->active && p->raw[0] && p->raw[1] && now >= p->raw[0]->timestamp &&
|
||||
now - p->raw[0]->timestamp <= MAX_AGE_NS;
|
||||
if (available)
|
||||
for (int i = 0; i < 2; ++i)
|
||||
xrt_frame_reference(&raw[i], p->raw[i]);
|
||||
os_mutex_unlock(&p->mutex);
|
||||
if (!available) {
|
||||
os_mutex_unlock(&p->convert_mutex);
|
||||
return false;
|
||||
}
|
||||
if (!p->converted || p->converted->timestamp != raw[0]->timestamp) {
|
||||
struct xrt_frame *frame = NULL;
|
||||
u_frame_create_one_off(XRT_FORMAT_R8G8B8, VIEW_SIZE * 2, VIEW_SIZE, &frame);
|
||||
frame->timestamp = raw[0]->timestamp;
|
||||
frame->source_sequence = raw[0]->source_sequence;
|
||||
frame->stereo_format = XRT_STEREO_FORMAT_SBS;
|
||||
for (int eye = 0; eye < 2; ++eye) {
|
||||
struct xrt_frame *src = raw[eye];
|
||||
for (uint32_t y = 0; y < VIEW_SIZE; ++y)
|
||||
for (uint32_t x = 0; x < VIEW_SIZE; ++x) {
|
||||
struct xrt_vec2 uv = p->map[eye][y * VIEW_SIZE + x];
|
||||
uint8_t value = 0;
|
||||
if (uv.x >= 0 && uv.y >= 0 && uv.x < src->width - 1 && uv.y < src->height - 1) {
|
||||
uint32_t ix = (uint32_t)uv.x, iy = (uint32_t)uv.y;
|
||||
float dx = uv.x - ix, dy = uv.y - iy;
|
||||
const uint8_t *a = src->data + iy * src->stride + ix;
|
||||
value =
|
||||
(uint8_t)((a[0] * (1 - dx) + a[1] * dx) * (1 - dy) +
|
||||
(a[src->stride] * (1 - dx) + a[src->stride + 1] * dx) *
|
||||
dy);
|
||||
}
|
||||
uint8_t *dst = frame->data + y * frame->stride + (eye * VIEW_SIZE + x) * 3;
|
||||
dst[0] = dst[1] = dst[2] = value;
|
||||
}
|
||||
}
|
||||
xrt_frame_reference(&p->converted, frame);
|
||||
xrt_frame_reference(&frame, NULL);
|
||||
}
|
||||
xrt_frame_reference(out, p->converted);
|
||||
for (int i = 0; i < 2; ++i)
|
||||
xrt_frame_reference(&raw[i], NULL);
|
||||
os_mutex_unlock(&p->convert_mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
struct xrt_camera *
|
||||
rift_s_passthrough_create(const struct rift_s_camera_calibration_block *calibration)
|
||||
{
|
||||
struct rift_s_passthrough *p = calloc(1, sizeof(*p));
|
||||
if (!p)
|
||||
return NULL;
|
||||
if (os_mutex_init(&p->mutex) != 0) {
|
||||
free(p);
|
||||
return NULL;
|
||||
}
|
||||
if (os_mutex_init(&p->convert_mutex) != 0) {
|
||||
os_mutex_destroy(&p->mutex);
|
||||
free(p);
|
||||
return NULL;
|
||||
}
|
||||
p->base.view_width = p->base.view_height = VIEW_SIZE;
|
||||
// Both front sensors cover this head-aligned square after firmware rectification.
|
||||
// Use the same pinhole focal for the resampling map and the exported intrinsics.
|
||||
float focal = VIEW_SIZE / (2.0f * tanf(55.0f * (float)M_PI / 180.0f));
|
||||
p->base.focal_length = (struct xrt_vec2){focal, focal};
|
||||
p->base.center = (struct xrt_vec2){(VIEW_SIZE - 1) / 2.0f, (VIEW_SIZE - 1) / 2.0f};
|
||||
p->base.set_active = set_active;
|
||||
p->base.get_frame = get_frame;
|
||||
for (int eye = 0; eye < 2; ++eye) {
|
||||
const struct rift_s_camera_calibration *c = &calibration->cameras[CAM_IDX_TO_ID[eye]];
|
||||
p->base.head_from_camera[eye] = (struct xrt_pose)XRT_POSE_IDENTITY;
|
||||
p->base.head_from_camera[eye].position = (struct xrt_vec3){
|
||||
c->device_from_camera.v[12], c->device_from_camera.v[13], c->device_from_camera.v[14]};
|
||||
for (uint32_t y = 0; y < VIEW_SIZE; ++y)
|
||||
for (uint32_t x = 0; x < VIEW_SIZE; ++x) {
|
||||
struct xrt_vec2 *uv = &p->map[eye][y * VIEW_SIZE + x];
|
||||
float rx = (x - p->base.center.x) / p->base.focal_length.x;
|
||||
float ry = (y - p->base.center.y) / p->base.focal_length.y;
|
||||
if (!rift_s_passthrough_project(c, rx, ry, uv))
|
||||
*uv = (struct xrt_vec2){-1, -1};
|
||||
}
|
||||
}
|
||||
return &p->base;
|
||||
}
|
||||
|
||||
void
|
||||
rift_s_passthrough_destroy(struct xrt_camera *camera)
|
||||
{
|
||||
if (!camera)
|
||||
return;
|
||||
struct rift_s_passthrough *p = (struct rift_s_passthrough *)camera;
|
||||
set_active(camera, false);
|
||||
xrt_frame_reference(&p->converted, NULL);
|
||||
os_mutex_destroy(&p->convert_mutex);
|
||||
os_mutex_destroy(&p->mutex);
|
||||
free(p);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
#pragma once
|
||||
#include "xrt/xrt_camera.h"
|
||||
#include "rift_s_firmware.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
struct xrt_camera *
|
||||
rift_s_passthrough_create(const struct rift_s_camera_calibration_block *calibration);
|
||||
void
|
||||
rift_s_passthrough_push(struct xrt_camera *camera, struct xrt_frame *left, struct xrt_frame *right);
|
||||
void
|
||||
rift_s_passthrough_destroy(struct xrt_camera *camera);
|
||||
/* Project an upright, head-aligned pinhole ray into a native calibrated camera. */
|
||||
bool
|
||||
rift_s_passthrough_project(const struct rift_s_camera_calibration *calibration,
|
||||
float x,
|
||||
float y,
|
||||
struct xrt_vec2 *pixel);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,334 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
/*!
|
||||
* @file
|
||||
* @brief Controller position/velocity filter driven by IMU acceleration and corrected by optical fixes.
|
||||
*
|
||||
* Three independent per-axis Kalman filters with state [position, velocity, acceleration bias]
|
||||
* and the world-frame IMU acceleration as control input. Optical fixes arrive tens of
|
||||
* milliseconds after their capture time; they are fused at capture time and the retained IMU
|
||||
* inputs are replayed, as the Windows tracker does for delayed vision.
|
||||
* @ingroup drv_rift_s
|
||||
*/
|
||||
#include "rift_s_position_filter.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
//! Velocity decay time constant while coasting without optical fixes.
|
||||
#define COAST_VELOCITY_TAU_S 0.5
|
||||
#define INITIAL_VELOCITY_VARIANCE (0.5 * 0.5)
|
||||
#define INITIAL_BIAS_VARIANCE (0.5 * 0.5)
|
||||
//! Longest single integration step; larger gaps are dropped radio packets.
|
||||
#define MAX_STEP_S 0.05
|
||||
|
||||
static struct rift_s_position_filter_input *
|
||||
input_at(struct rift_s_position_filter *f, unsigned int i)
|
||||
{
|
||||
return &f->history[(f->history_start + i) % RIFT_S_POSITION_FILTER_HISTORY];
|
||||
}
|
||||
|
||||
static void
|
||||
propagate(const struct rift_s_position_filter *f,
|
||||
struct rift_s_position_filter_state *s,
|
||||
const struct xrt_vec3 *accel,
|
||||
timepoint_ns t)
|
||||
{
|
||||
const bool coasting = f->coasting;
|
||||
double dt = (double)(t - s->t) / U_TIME_1S_IN_NS;
|
||||
s->t = t;
|
||||
if (dt <= 0) {
|
||||
return;
|
||||
}
|
||||
if (dt > MAX_STEP_S) {
|
||||
dt = MAX_STEP_S;
|
||||
}
|
||||
if (coasting && f->track_updates < RIFT_S_POSITION_FILTER_MATURE_UPDATES) {
|
||||
// A young track's velocity comes from a handful of fixes, possibly a wrong one: hold.
|
||||
for (int axis = 0; axis < 3; axis++) {
|
||||
s->x[axis][1] = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const double a[3] = {accel->x, accel->y, accel->z};
|
||||
const double d = coasting ? exp(-dt / COAST_VELOCITY_TAU_S) : 1.0;
|
||||
const double F[3][3] = {{1, dt, -0.5 * dt * dt}, {0, d, -d * dt}, {0, 0, 1}};
|
||||
for (int axis = 0; axis < 3; axis++) {
|
||||
double *x = s->x[axis];
|
||||
double u = a[axis] - x[2];
|
||||
x[0] += x[1] * dt + 0.5 * u * dt * dt;
|
||||
x[1] = d * (x[1] + u * dt);
|
||||
|
||||
double FP[3][3] = {{0}};
|
||||
double (*P)[3] = s->P[axis];
|
||||
for (int i = 0; i < 3; i++)
|
||||
for (int j = 0; j < 3; j++)
|
||||
for (int k = 0; k < 3; k++)
|
||||
FP[i][j] += F[i][k] * P[k][j];
|
||||
for (int i = 0; i < 3; i++)
|
||||
for (int j = 0; j < 3; j++) {
|
||||
double v = 0;
|
||||
for (int k = 0; k < 3; k++)
|
||||
v += FP[i][k] * F[j][k];
|
||||
P[i][j] = v;
|
||||
}
|
||||
P[0][0] += f->accel_noise_density * dt * dt * dt / 3;
|
||||
P[0][1] += f->accel_noise_density * dt * dt / 2;
|
||||
P[1][0] += f->accel_noise_density * dt * dt / 2;
|
||||
P[1][1] += f->accel_noise_density * dt;
|
||||
P[2][2] += f->bias_random_walk * dt;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
init_state(struct rift_s_position_filter_state *s, timepoint_ns t, const struct xrt_vec3 *z, const struct xrt_vec3 *r)
|
||||
{
|
||||
const double zz[3] = {z->x, z->y, z->z}, rr[3] = {r->x, r->y, r->z};
|
||||
s->t = t;
|
||||
memset(s->P, 0, sizeof(s->P));
|
||||
for (int axis = 0; axis < 3; axis++) {
|
||||
s->x[axis][0] = zz[axis];
|
||||
s->x[axis][1] = 0;
|
||||
s->x[axis][2] = 0;
|
||||
s->P[axis][0][0] = rr[axis];
|
||||
s->P[axis][1][1] = INITIAL_VELOCITY_VARIANCE;
|
||||
s->P[axis][2][2] = INITIAL_BIAS_VARIANCE;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
update(struct rift_s_position_filter_state *s, const struct xrt_vec3 *z, const struct xrt_vec3 *r)
|
||||
{
|
||||
const double zz[3] = {z->x, z->y, z->z}, rr[3] = {r->x, r->y, r->z};
|
||||
for (int axis = 0; axis < 3; axis++) {
|
||||
double *x = s->x[axis];
|
||||
double (*P)[3] = s->P[axis];
|
||||
double S = P[0][0] + rr[axis];
|
||||
double K[3] = {P[0][0] / S, P[1][0] / S, P[2][0] / S};
|
||||
double innovation = zz[axis] - x[0];
|
||||
for (int i = 0; i < 3; i++)
|
||||
x[i] += K[i] * innovation;
|
||||
double row0[3] = {P[0][0], P[0][1], P[0][2]};
|
||||
for (int i = 0; i < 3; i++)
|
||||
for (int j = 0; j < 3; j++)
|
||||
P[i][j] -= K[i] * row0[j];
|
||||
for (int i = 0; i < 3; i++)
|
||||
for (int j = i + 1; j < 3; j++)
|
||||
P[i][j] = P[j][i] = 0.5 * (P[i][j] + P[j][i]);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
rift_s_position_filter_reset(struct rift_s_position_filter *f)
|
||||
{
|
||||
memset(f, 0, sizeof(*f));
|
||||
f->accel_noise_density = RIFT_S_POSITION_FILTER_ACCEL_NOISE_DENSITY;
|
||||
f->bias_random_walk = RIFT_S_POSITION_FILTER_BIAS_RANDOM_WALK;
|
||||
}
|
||||
|
||||
void
|
||||
rift_s_position_filter_imu(struct rift_s_position_filter *f, timepoint_ns t, const struct xrt_vec3 *accel_world)
|
||||
{
|
||||
if (!f->initialized || t <= f->state.t) {
|
||||
f->last_accel = *accel_world;
|
||||
return;
|
||||
}
|
||||
if (f->frozen) {
|
||||
f->state.t = t;
|
||||
} else {
|
||||
propagate(f, &f->state, accel_world, t);
|
||||
}
|
||||
f->last_accel = *accel_world;
|
||||
if (f->initialized && t - f->last_fix_ns > 120 * U_TIME_1MS_IN_NS) {
|
||||
// No measurement can correct acceleration/tilt error while occluded. Bound the
|
||||
// displacement from the last optical fix, including before coasting was noticed.
|
||||
double delta[3] = {f->state.x[0][0] - f->last_fix_position.x, f->state.x[1][0] - f->last_fix_position.y,
|
||||
f->state.x[2][0] - f->last_fix_position.z};
|
||||
double distance = sqrt(delta[0] * delta[0] + delta[1] * delta[1] + delta[2] * delta[2]);
|
||||
if (distance > 0.1) {
|
||||
const double base[3] = {f->last_fix_position.x, f->last_fix_position.y, f->last_fix_position.z};
|
||||
for (int axis = 0; axis < 3; ++axis) {
|
||||
f->state.x[axis][0] = base[axis] + delta[axis] * 0.1 / distance;
|
||||
f->state.x[axis][1] = 0;
|
||||
}
|
||||
}
|
||||
if (t - f->last_fix_ns > RIFT_S_POSITION_FILTER_BRIDGE_NS)
|
||||
rift_s_position_filter_freeze(f);
|
||||
}
|
||||
if (f->history_count == RIFT_S_POSITION_FILTER_HISTORY) {
|
||||
f->history_start = (f->history_start + 1) % RIFT_S_POSITION_FILTER_HISTORY;
|
||||
f->history_count--;
|
||||
}
|
||||
struct rift_s_position_filter_input *in = input_at(f, f->history_count++);
|
||||
in->t = t;
|
||||
in->accel = *accel_world;
|
||||
in->after = f->state;
|
||||
}
|
||||
|
||||
// Continue from @p s (at or before the IMU input @p first) through the retained inputs.
|
||||
static void
|
||||
replay(struct rift_s_position_filter *f, struct rift_s_position_filter_state s, unsigned int first)
|
||||
{
|
||||
for (unsigned int i = first; i < f->history_count; i++) {
|
||||
struct rift_s_position_filter_input *in = input_at(f, i);
|
||||
propagate(f, &s, &in->accel, in->t);
|
||||
in->after = s;
|
||||
}
|
||||
f->state = s;
|
||||
}
|
||||
|
||||
// Count @p position towards replacing the estimate; true once enough consecutive candidates agree.
|
||||
static bool
|
||||
confirm_candidate(struct rift_s_position_filter *f,
|
||||
timepoint_ns capture,
|
||||
const struct xrt_vec3 *position,
|
||||
float reset_distance_m)
|
||||
{
|
||||
bool consistent = false;
|
||||
if (f->candidate_count > 0 && capture > f->candidate_t &&
|
||||
capture - f->candidate_t <= RIFT_S_POSITION_FILTER_CONFIRM_GAP_NS) {
|
||||
struct xrt_vec3 d = {position->x - f->candidate_position.x, position->y - f->candidate_position.y,
|
||||
position->z - f->candidate_position.z};
|
||||
consistent = sqrtf(d.x * d.x + d.y * d.y + d.z * d.z) <= reset_distance_m;
|
||||
}
|
||||
f->candidate_count = consistent ? f->candidate_count + 1 : 1;
|
||||
f->candidate_position = *position;
|
||||
f->candidate_t = capture;
|
||||
return f->candidate_count >= RIFT_S_POSITION_FILTER_CONFIRM_FIXES;
|
||||
}
|
||||
|
||||
enum rift_s_position_fix_result
|
||||
rift_s_position_filter_fix(struct rift_s_position_filter *f,
|
||||
timepoint_ns capture,
|
||||
const struct xrt_vec3 *position,
|
||||
const struct xrt_vec3 *variance,
|
||||
float reset_distance_m)
|
||||
{
|
||||
// First retained input after the capture time.
|
||||
unsigned int next = 0;
|
||||
while (next < f->history_count && input_at(f, next)->t <= capture) {
|
||||
next++;
|
||||
}
|
||||
// A fix needs the state at or before its capture time; the oldest retained input only has
|
||||
// the state after it.
|
||||
bool reset = !f->initialized || f->frozen;
|
||||
bool stale = f->history_count != 0 ? next == 0 : capture < f->state.t;
|
||||
if (f->initialized && stale) {
|
||||
f->stale++;
|
||||
return RIFT_S_POSITION_FIX_STALE;
|
||||
}
|
||||
|
||||
struct rift_s_position_filter_state s;
|
||||
if (!reset) {
|
||||
s = next > 0 ? input_at(f, next - 1)->after : f->state;
|
||||
const struct xrt_vec3 *accel = next < f->history_count ? &input_at(f, next)->accel : &f->last_accel;
|
||||
propagate(f, &s, accel, capture);
|
||||
struct xrt_vec3 innovation = {(float)(position->x - s.x[0][0]), (float)(position->y - s.x[1][0]),
|
||||
(float)(position->z - s.x[2][0])};
|
||||
float distance =
|
||||
sqrtf(innovation.x * innovation.x + innovation.y * innovation.y + innovation.z * innovation.z);
|
||||
if (!(distance <= reset_distance_m)) {
|
||||
f->gated++;
|
||||
reset = true;
|
||||
} else {
|
||||
f->innovation_sq_sum += (double)distance * distance;
|
||||
f->innovation_count++;
|
||||
}
|
||||
}
|
||||
if (reset && f->initialized && !confirm_candidate(f, capture, position, reset_distance_m)) {
|
||||
f->rejected++;
|
||||
return RIFT_S_POSITION_FIX_PENDING;
|
||||
}
|
||||
f->candidate_count = 0;
|
||||
if (reset) {
|
||||
init_state(&s, capture, position, variance);
|
||||
f->resets++;
|
||||
f->track_updates = 1;
|
||||
} else {
|
||||
update(&s, position, variance);
|
||||
f->updates++;
|
||||
f->track_updates++;
|
||||
}
|
||||
f->initialized = true;
|
||||
f->last_fix_ns = capture;
|
||||
f->last_fix_position = *position;
|
||||
f->frozen = false;
|
||||
f->coasting = false;
|
||||
// Inputs before the capture keep their states; later ones are replayed from the fused state.
|
||||
replay(f, s, next);
|
||||
return reset ? RIFT_S_POSITION_FIX_RESET : RIFT_S_POSITION_FIX_UPDATED;
|
||||
}
|
||||
|
||||
void
|
||||
rift_s_position_filter_freeze(struct rift_s_position_filter *f)
|
||||
{
|
||||
if (!f->initialized || f->frozen) {
|
||||
return;
|
||||
}
|
||||
f->frozen = true;
|
||||
for (int axis = 0; axis < 3; axis++) {
|
||||
f->state.x[axis][1] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
rift_s_position_filter_predict(const struct rift_s_position_filter *f,
|
||||
timepoint_ns t,
|
||||
time_duration_ns max_horizon_ns,
|
||||
struct xrt_vec3 *out_position,
|
||||
struct xrt_vec3 *out_velocity)
|
||||
{
|
||||
time_duration_ns ahead = t - f->state.t;
|
||||
if (ahead < 0 || f->frozen || (f->initialized && t - f->last_fix_ns > RIFT_S_POSITION_FILTER_BRIDGE_NS)) {
|
||||
ahead = 0;
|
||||
} else if (ahead > max_horizon_ns) {
|
||||
ahead = max_horizon_ns;
|
||||
}
|
||||
double dt = (double)ahead / U_TIME_1S_IN_NS;
|
||||
const struct rift_s_position_filter_state *s = &f->state;
|
||||
out_velocity->x = (float)s->x[0][1];
|
||||
out_velocity->y = (float)s->x[1][1];
|
||||
out_velocity->z = (float)s->x[2][1];
|
||||
out_position->x = (float)(s->x[0][0] + s->x[0][1] * dt);
|
||||
out_position->y = (float)(s->x[1][0] + s->x[1][1] * dt);
|
||||
out_position->z = (float)(s->x[2][0] + s->x[2][1] * dt);
|
||||
if (f->initialized && t - f->last_fix_ns > 120 * U_TIME_1MS_IN_NS) {
|
||||
// Queries can run beyond the last IMU sample, including during a radio gap.
|
||||
double delta[3] = {out_position->x - f->last_fix_position.x, out_position->y - f->last_fix_position.y,
|
||||
out_position->z - f->last_fix_position.z};
|
||||
double distance = sqrt(delta[0] * delta[0] + delta[1] * delta[1] + delta[2] * delta[2]);
|
||||
if (distance > 0.1) {
|
||||
out_position->x = (float)(f->last_fix_position.x + delta[0] * 0.1 / distance);
|
||||
out_position->y = (float)(f->last_fix_position.y + delta[1] * 0.1 / distance);
|
||||
out_position->z = (float)(f->last_fix_position.z + delta[2] * 0.1 / distance);
|
||||
*out_velocity = (struct xrt_vec3){0};
|
||||
}
|
||||
if (t - f->last_fix_ns > RIFT_S_POSITION_FILTER_BRIDGE_NS) {
|
||||
*out_velocity = (struct xrt_vec3){0};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
rift_s_tracking_loss_durations(uint32_t vision_updates,
|
||||
uint32_t reliable_updates,
|
||||
time_duration_ns *out_position,
|
||||
time_duration_ns *out_velocity)
|
||||
{
|
||||
double position_s = 0, velocity_s = 0;
|
||||
if (reliable_updates > 20) {
|
||||
position_s = 3.0;
|
||||
velocity_s = 5.0;
|
||||
} else if (vision_updates > 10) {
|
||||
position_s = 2.0;
|
||||
velocity_s = 4.0;
|
||||
} else if (vision_updates > 2) {
|
||||
position_s = 0.5;
|
||||
velocity_s = 0.5;
|
||||
} else if (vision_updates >= 1) {
|
||||
position_s = 0.25;
|
||||
velocity_s = 0.25;
|
||||
}
|
||||
*out_position = (time_duration_ns)(position_s * U_TIME_1S_IN_NS);
|
||||
*out_velocity = (time_duration_ns)(velocity_s * U_TIME_1S_IN_NS);
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
/*!
|
||||
* @file
|
||||
* @brief Controller position/velocity filter driven by IMU acceleration and corrected by optical fixes.
|
||||
* @ingroup drv_rift_s
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "xrt/xrt_defines.h"
|
||||
#include "util/u_time.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*!
|
||||
* Defaults chosen on the synthetic cases in tests_rift_s_position_filter (noise at rest, delayed
|
||||
* fixes during a hand swing, slowly varying acceleration error); no Meta values are used.
|
||||
* Continuous white acceleration noise density, (m/s^2)^2 per Hz, and bias random walk,
|
||||
* (m/s^2)^2 per second.
|
||||
*/
|
||||
#define RIFT_S_POSITION_FILTER_ACCEL_NOISE_DENSITY (0.1 * 0.1)
|
||||
#define RIFT_S_POSITION_FILTER_BIAS_RANDOM_WALK (0.004)
|
||||
|
||||
/*!
|
||||
* Mutually consistent fixes needed before a fix that disagrees with the estimate (a gated
|
||||
* innovation, or any fix while the position is held after loss) replaces it. A single wrong
|
||||
* constellation fit, e.g. a partial ring seen behind the back, must not move the hand.
|
||||
*/
|
||||
#define RIFT_S_POSITION_FILTER_CONFIRM_FIXES 3
|
||||
//! Longest gap between consecutive candidate fixes of one confirmation.
|
||||
#define RIFT_S_POSITION_FILTER_CONFIRM_GAP_NS (250 * U_TIME_1MS_IN_NS)
|
||||
/*!
|
||||
* Fixes before a track's velocity is trusted for coasting; younger tracks hold their position
|
||||
* when fixes stop. Matches the "more than 10 updates" step of the loss ladder.
|
||||
*/
|
||||
#define RIFT_S_POSITION_FILTER_MATURE_UPDATES 11
|
||||
|
||||
/*!
|
||||
* Longest optical gap the IMU bridges before the position is frozen and reported lost: the
|
||||
* 0.5 s step of the loss ladder. Fixes arrive every 33 ms, so in view only real occlusion
|
||||
* reaches it; displacement while bridging stays bounded to 10 cm from the last fix.
|
||||
*/
|
||||
#define RIFT_S_POSITION_FILTER_BRIDGE_NS (500 * U_TIME_1MS_IN_NS)
|
||||
|
||||
//! IMU inputs retained for replaying after a delayed optical fix: 1 s at 500 Hz.
|
||||
#define RIFT_S_POSITION_FILTER_HISTORY 512
|
||||
|
||||
/*!
|
||||
* Per axis state: world position, velocity and a world-frame acceleration bias, which absorbs
|
||||
* accelerometer bias and residual tilt error while optical fixes arrive.
|
||||
*/
|
||||
struct rift_s_position_filter_state
|
||||
{
|
||||
timepoint_ns t;
|
||||
double x[3][3]; //!< [axis][p, v, b]
|
||||
double P[3][3][3]; //!< [axis] 3x3 covariance
|
||||
};
|
||||
|
||||
struct rift_s_position_filter_input
|
||||
{
|
||||
timepoint_ns t;
|
||||
struct xrt_vec3 accel; //!< World-frame acceleration, gravity removed.
|
||||
struct rift_s_position_filter_state after; //!< State after integrating up to t.
|
||||
};
|
||||
|
||||
struct rift_s_position_filter
|
||||
{
|
||||
bool initialized;
|
||||
//! Velocity damping and no bias update: no optical fix for longer than the coast threshold.
|
||||
bool coasting;
|
||||
//! Position frozen and velocity zero: tracking was lost; waits for an optical reset.
|
||||
bool frozen;
|
||||
//! Process noise; set by reset(), may be overridden afterwards.
|
||||
double accel_noise_density, bias_random_walk;
|
||||
struct rift_s_position_filter_state state;
|
||||
struct xrt_vec3 last_accel;
|
||||
struct xrt_vec3 last_fix_position;
|
||||
timepoint_ns last_fix_ns;
|
||||
|
||||
struct rift_s_position_filter_input history[RIFT_S_POSITION_FILTER_HISTORY];
|
||||
unsigned int history_start, history_count;
|
||||
|
||||
//! Fixes fused since the last (re)initialization.
|
||||
uint32_t track_updates;
|
||||
//! Candidate replacement position awaiting confirmation.
|
||||
uint32_t candidate_count;
|
||||
struct xrt_vec3 candidate_position;
|
||||
timepoint_ns candidate_t;
|
||||
|
||||
uint64_t resets, updates, stale, gated, rejected;
|
||||
//! Squared innovation norm of fused fixes, m^2, since the caller last cleared it.
|
||||
double innovation_sq_sum;
|
||||
uint32_t innovation_count;
|
||||
};
|
||||
|
||||
//! Result of applying one optical position fix.
|
||||
enum rift_s_position_fix_result
|
||||
{
|
||||
RIFT_S_POSITION_FIX_RESET, //!< (Re)initialized from the fix.
|
||||
RIFT_S_POSITION_FIX_UPDATED, //!< Fused at capture time and replayed.
|
||||
RIFT_S_POSITION_FIX_STALE, //!< Older than the retained IMU history.
|
||||
RIFT_S_POSITION_FIX_PENDING, //!< Disagrees with the estimate; held until confirmed.
|
||||
};
|
||||
|
||||
void
|
||||
rift_s_position_filter_reset(struct rift_s_position_filter *f);
|
||||
|
||||
//! Integrate one IMU sample, @p accel_world in m/s^2 with gravity removed, at host time @p t.
|
||||
void
|
||||
rift_s_position_filter_imu(struct rift_s_position_filter *f, timepoint_ns t, const struct xrt_vec3 *accel_world);
|
||||
|
||||
/*!
|
||||
* Fuse an optical position at its capture time. @p variance is the per-axis measurement variance
|
||||
* in m^2. The first fix initializes the filter. An innovation beyond @p reset_distance_m, or any fix
|
||||
* while frozen, is a candidate: it re-initializes position and velocity (the Windows "6DoF reset")
|
||||
* only once RIFT_S_POSITION_FILTER_CONFIRM_FIXES consecutive candidates agree within
|
||||
* @p reset_distance_m of each other; until then the estimate is kept and PENDING returned.
|
||||
*/
|
||||
enum rift_s_position_fix_result
|
||||
rift_s_position_filter_fix(struct rift_s_position_filter *f,
|
||||
timepoint_ns capture,
|
||||
const struct xrt_vec3 *position,
|
||||
const struct xrt_vec3 *variance,
|
||||
float reset_distance_m);
|
||||
|
||||
//! Stop integrating: hold the current position with zero velocity until the next reset.
|
||||
void
|
||||
rift_s_position_filter_freeze(struct rift_s_position_filter *f);
|
||||
|
||||
/*!
|
||||
* Position and velocity at @p t. Times after the last IMU sample extrapolate with constant
|
||||
* velocity, at most @p max_horizon_ns; earlier times return the current state.
|
||||
*/
|
||||
void
|
||||
rift_s_position_filter_predict(const struct rift_s_position_filter *f,
|
||||
timepoint_ns t,
|
||||
time_duration_ns max_horizon_ns,
|
||||
struct xrt_vec3 *out_position,
|
||||
struct xrt_vec3 *out_velocity);
|
||||
|
||||
/*!
|
||||
* Windows controller loss ladder (its default configuration): how long position and velocity
|
||||
* stay tracked after the last optical update, by the number of successful and reliable optical
|
||||
* updates in the current track.
|
||||
*/
|
||||
void
|
||||
rift_s_tracking_loss_durations(uint32_t vision_updates,
|
||||
uint32_t reliable_updates,
|
||||
time_duration_ns *out_position,
|
||||
time_duration_ns *out_velocity);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
|
||||
#include "os/os_hid.h"
|
||||
#include "os/os_time.h"
|
||||
@@ -34,6 +35,116 @@
|
||||
#include "rift_s.h"
|
||||
#include "rift_s_protocol.h"
|
||||
|
||||
static bool
|
||||
controller_register_response_valid(const uint8_t *response, size_t size)
|
||||
{
|
||||
return response != NULL && size >= 21 && response[0] == 0 && response[1] == 0 && response[2] == 0 &&
|
||||
response[3] == 0 && response[4] >= 16 && response[4] <= 32 && size >= 5u + response[4];
|
||||
}
|
||||
|
||||
bool
|
||||
rift_s_decode_controller_config(const uint8_t *response, size_t size, rift_s_controller_config *out)
|
||||
{
|
||||
if (!controller_register_response_valid(response, size))
|
||||
return false;
|
||||
const uint8_t *data = response + 5;
|
||||
rift_s_controller_config config = {0};
|
||||
config.accel_limit = (uint16_t)data[0] | (uint16_t)data[1] << 8;
|
||||
config.gyro_limit = (uint16_t)data[2] | (uint16_t)data[3] << 8;
|
||||
config.accel_hz = (uint16_t)data[4] | (uint16_t)data[5] << 8;
|
||||
config.gyro_hz = (uint16_t)data[6] | (uint16_t)data[7] << 8;
|
||||
uint32_t accel_bits = (uint32_t)data[8] | (uint32_t)data[9] << 8 | (uint32_t)data[10] << 16 |
|
||||
(uint32_t)data[11] << 24;
|
||||
uint32_t gyro_bits = (uint32_t)data[12] | (uint32_t)data[13] << 8 | (uint32_t)data[14] << 16 |
|
||||
(uint32_t)data[15] << 24;
|
||||
memcpy(&config.accel_scale, &accel_bits, sizeof(accel_bits));
|
||||
memcpy(&config.gyro_scale, &gyro_bits, sizeof(gyro_bits));
|
||||
if (!isfinite(config.accel_scale) || config.accel_scale <= 0 || !isfinite(config.gyro_scale) ||
|
||||
config.gyro_scale <= 0)
|
||||
return false;
|
||||
*out = config;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
rift_s_decode_controller_imu_descriptor(const uint8_t *response,
|
||||
size_t size,
|
||||
char out[RIFT_S_CONTROLLER_IMU_DESCRIPTOR_SIZE + 1])
|
||||
{
|
||||
if (!controller_register_response_valid(response, size) || response[4] != 16)
|
||||
return false;
|
||||
const uint8_t *data = response + 5;
|
||||
size_t len = 0;
|
||||
while (len < RIFT_S_CONTROLLER_IMU_DESCRIPTOR_SIZE && data[len] != 0) {
|
||||
if (data[len] < 0x20 || data[len] > 0x7e)
|
||||
return false;
|
||||
len++;
|
||||
}
|
||||
if (len == 0)
|
||||
return false;
|
||||
memcpy(out, data, len);
|
||||
out[len] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint32_t
|
||||
read_le32(const uint8_t *p)
|
||||
{
|
||||
return (uint32_t)p[0] | (uint32_t)p[1] << 8 | (uint32_t)p[2] << 16 | (uint32_t)p[3] << 24;
|
||||
}
|
||||
|
||||
static void
|
||||
write_le32(uint8_t *p, uint32_t v)
|
||||
{
|
||||
p[0] = v & 0xff;
|
||||
p[1] = (v >> 8) & 0xff;
|
||||
p[2] = (v >> 16) & 0xff;
|
||||
p[3] = (v >> 24) & 0xff;
|
||||
}
|
||||
|
||||
size_t
|
||||
rift_s_encode_controller_irled_read(uint8_t out[4])
|
||||
{
|
||||
// The register read passes a zero payload length, as the Windows driver does.
|
||||
out[0] = RIFT_S_CONTROLLER_REG_IRLED;
|
||||
out[1] = 0;
|
||||
out[2] = RIFT_S_RADIO_TIMEOUT_MS & 0xff;
|
||||
out[3] = RIFT_S_RADIO_TIMEOUT_MS >> 8;
|
||||
return 4;
|
||||
}
|
||||
|
||||
size_t
|
||||
rift_s_encode_controller_irled_write(const rift_s_controller_irled_config *cfg, uint8_t out[12])
|
||||
{
|
||||
out[0] = RIFT_S_CONTROLLER_REG_IRLED;
|
||||
out[1] = 8;
|
||||
out[2] = RIFT_S_RADIO_TIMEOUT_MS & 0xff;
|
||||
out[3] = RIFT_S_RADIO_TIMEOUT_MS >> 8;
|
||||
write_le32(out + 4, cfg->period_us);
|
||||
write_le32(out + 8, cfg->ontime_us);
|
||||
return 12;
|
||||
}
|
||||
|
||||
bool
|
||||
rift_s_decode_controller_irled_config(const uint8_t *response, size_t size, rift_s_controller_irled_config *out)
|
||||
{
|
||||
// Status 0, then a length of at least 8 and that many bytes.
|
||||
if (response == NULL || size < 13 || read_le32(response) != 0 || response[4] < 8 || size < 5u + response[4])
|
||||
return false;
|
||||
out->period_us = read_le32(response + 5);
|
||||
out->ontime_us = read_le32(response + 9);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
rift_s_controller_write_succeeded(const uint8_t *response, size_t size, uint32_t *status_out)
|
||||
{
|
||||
if (response == NULL || size < 4)
|
||||
return false;
|
||||
*status_out = read_le32(response);
|
||||
return *status_out == 0;
|
||||
}
|
||||
|
||||
/* FIXME: The code in this file is not portable to big-endian as-is - it needs endian swaps */
|
||||
bool
|
||||
rift_s_parse_hmd_report(rift_s_hmd_report_t *report, const unsigned char *buf, int size)
|
||||
|
||||
@@ -45,6 +45,55 @@
|
||||
#define RIFT_S_FINGER_STICK_WEAK 0x40
|
||||
#define RIFT_S_FINGER_TRIGGER_WEAK 0x80
|
||||
|
||||
#define RIFT_S_CONTROLLER_IMU_DESCRIPTOR_SIZE 16
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint16_t accel_limit;
|
||||
uint16_t gyro_limit;
|
||||
uint16_t accel_hz;
|
||||
uint16_t gyro_hz;
|
||||
float accel_scale;
|
||||
float gyro_scale;
|
||||
} rift_s_controller_config;
|
||||
|
||||
/* Radio callback bytes: LE32 status, u8 payload length, then register data.
|
||||
* 0x32 is four LE16 limits/rates and two LE float scales; 0x31 is a 16-byte ASCII name. */
|
||||
bool
|
||||
rift_s_decode_controller_config(const uint8_t *response, size_t size, rift_s_controller_config *out);
|
||||
bool
|
||||
rift_s_decode_controller_imu_descriptor(const uint8_t *response,
|
||||
size_t size,
|
||||
char out[RIFT_S_CONTROLLER_IMU_DESCRIPTOR_SIZE + 1]);
|
||||
|
||||
/* Controller IR LED timing, register 0x28: LE32 period_us, LE32 ontime_us.
|
||||
* The Windows driver reads it, keeps ontime_us (19 if the read fails), sets
|
||||
* period_us = 1000000 / controller exposure rate (25 or 30 Hz) and writes it back.
|
||||
* Windows logs "updated ontime_us/period_us to 19/40000" for both hands. */
|
||||
#define RIFT_S_CONTROLLER_REG_IRLED 0x28
|
||||
#define RIFT_S_CONTROLLER_IRLED_DEFAULT_ONTIME_US 19
|
||||
#define RIFT_S_RADIO_REPORT_READ 0x12
|
||||
#define RIFT_S_RADIO_REPORT_WRITE 0x13
|
||||
#define RIFT_S_RADIO_TIMEOUT_MS 1000
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint32_t period_us;
|
||||
uint32_t ontime_us;
|
||||
} rift_s_controller_irled_config;
|
||||
|
||||
/* Request bytes after the device id for report 0x12 (read) or 0x13 (write), as the Windows
|
||||
* driver builds them: register, payload length, LE16 timeout ms, payload. */
|
||||
size_t
|
||||
rift_s_encode_controller_irled_read(uint8_t out[4]);
|
||||
size_t
|
||||
rift_s_encode_controller_irled_write(const rift_s_controller_irled_config *cfg, uint8_t out[12]);
|
||||
bool
|
||||
rift_s_decode_controller_irled_config(const uint8_t *response, size_t size, rift_s_controller_irled_config *out);
|
||||
//! A write reply only carries the LE32 status, 0 on success.
|
||||
bool
|
||||
rift_s_controller_write_succeeded(const uint8_t *response, size_t size, uint32_t *status_out);
|
||||
|
||||
typedef enum
|
||||
{
|
||||
RIFT_S_CTRL_MASK08 = 0x08, /* Unknown. Vals seen 0x28, 0x0a, 0x32, 0x46, 0x00... */
|
||||
|
||||
@@ -33,7 +33,7 @@ struct rift_s_radio_command
|
||||
rift_s_radio_command *prev;
|
||||
rift_s_radio_command *next;
|
||||
|
||||
/* Request packet data */
|
||||
/* Request packet data; read_command.cmd is the HID report id */
|
||||
rift_s_hmd_radio_command_t read_command;
|
||||
|
||||
/* Completion callback */
|
||||
@@ -64,7 +64,6 @@ rift_s_radio_update(rift_s_radio_state *state, struct os_hid_device *hid)
|
||||
rift_s_radio_command *cmd = state->pending_commands;
|
||||
rift_s_hmd_radio_command_t *pkt = &cmd->read_command;
|
||||
|
||||
pkt->cmd = 0x12;
|
||||
os_hid_set_feature(hid, (uint8_t *)pkt, sizeof(*pkt));
|
||||
// rift_s_hexdump_buffer ("ControllerFWSend", (unsigned char *)(pkt), sizeof(*pkt));
|
||||
state->command_result_pending = true;
|
||||
@@ -150,11 +149,24 @@ rift_s_radio_queue_command(rift_s_radio_state *state,
|
||||
const int cmd_bytes_len,
|
||||
rift_s_radio_completion_fn cb,
|
||||
void *cb_data)
|
||||
{
|
||||
rift_s_radio_queue_report(state, RIFT_S_RADIO_REPORT_READ, device_id, cmd_bytes, cmd_bytes_len, cb, cb_data);
|
||||
}
|
||||
|
||||
void
|
||||
rift_s_radio_queue_report(rift_s_radio_state *state,
|
||||
uint8_t report_id,
|
||||
const uint64_t device_id,
|
||||
const uint8_t *cmd_bytes,
|
||||
const int cmd_bytes_len,
|
||||
rift_s_radio_completion_fn cb,
|
||||
void *cb_data)
|
||||
{
|
||||
rift_s_radio_command *cmd = U_TYPED_CALLOC(rift_s_radio_command);
|
||||
|
||||
assert(cmd_bytes_len <= (int)sizeof(cmd->read_command.cmd_bytes));
|
||||
|
||||
cmd->read_command.cmd = report_id;
|
||||
cmd->read_command.device_id = device_id;
|
||||
memcpy(cmd->read_command.cmd_bytes, cmd_bytes, cmd_bytes_len);
|
||||
cmd->cb = cb;
|
||||
|
||||
@@ -60,6 +60,15 @@ rift_s_radio_queue_command(rift_s_radio_state *state,
|
||||
const int cmd_bytes_len,
|
||||
rift_s_radio_completion_fn cb,
|
||||
void *cb_data);
|
||||
//! Like rift_s_radio_queue_command, but sent with an explicit report id (0x13 writes a register).
|
||||
void
|
||||
rift_s_radio_queue_report(rift_s_radio_state *state,
|
||||
uint8_t report_id,
|
||||
const uint64_t device_id,
|
||||
const uint8_t *cmd_bytes,
|
||||
const int cmd_bytes_len,
|
||||
rift_s_radio_completion_fn cb,
|
||||
void *cb_data);
|
||||
void
|
||||
rift_s_radio_get_json_block(rift_s_radio_state *state,
|
||||
const uint64_t device_id,
|
||||
|
||||
@@ -0,0 +1,463 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
/*!
|
||||
* @file
|
||||
* @brief Rejects diverged SLAM poses and supplies the fallback HMD pose.
|
||||
*
|
||||
* Validate source samples, separately from historical and predicted queries. A lost pose holds
|
||||
* position and follows IMU attitude. Ordinary loss retains the map alignment; a backend reset
|
||||
* invalidates it. A saved world requires visual relocalization, never a held-head realignment.
|
||||
* @ingroup drv_rift_s
|
||||
*/
|
||||
#include "rift_s_slam_guard.h"
|
||||
|
||||
#include "math/m_api.h"
|
||||
#include "math/m_vec3.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
static const struct xrt_quat identity_quat = {0, 0, 0, 1};
|
||||
|
||||
static bool
|
||||
pose_is_finite(const struct xrt_pose *p)
|
||||
{
|
||||
return isfinite(p->position.x) && isfinite(p->position.y) && isfinite(p->position.z) &&
|
||||
isfinite(p->orientation.x) && isfinite(p->orientation.y) && isfinite(p->orientation.z) &&
|
||||
isfinite(p->orientation.w);
|
||||
}
|
||||
|
||||
bool
|
||||
rift_s_slam_guard_pose_usable(const struct xrt_pose *p, const struct xrt_quat *fusion)
|
||||
{
|
||||
if (!pose_is_finite(p)) return false;
|
||||
float norm = p->orientation.x * p->orientation.x + p->orientation.y * p->orientation.y +
|
||||
p->orientation.z * p->orientation.z + p->orientation.w * p->orientation.w;
|
||||
if (!(norm > 0.9f && norm < 1.1f)) return false;
|
||||
// Compare gravity in the body frame: world yaw is arbitrary, physical head tilt is not.
|
||||
struct xrt_vec3 up = {0, 1, 0}, slam_up, imu_up;
|
||||
struct xrt_quat inverse;
|
||||
math_quat_invert(&p->orientation, &inverse);
|
||||
math_quat_rotate_vec3(&inverse, &up, &slam_up);
|
||||
math_quat_invert(fusion, &inverse);
|
||||
math_quat_rotate_vec3(&inverse, &up, &imu_up);
|
||||
return m_vec3_dot(slam_up, imu_up) >= 0.70710678f;
|
||||
}
|
||||
|
||||
void
|
||||
rift_s_slam_guard_set_world(struct rift_s_slam_guard *g, const struct xrt_pose *world_from_slam)
|
||||
{
|
||||
g->boundary_confirmed = true;
|
||||
g->world_transition = g->world_valid && g->have_good && g->state == RIFT_S_SLAM_GUARD_TRACKING;
|
||||
if (g->world_transition) {
|
||||
g->transition_from = g->align;
|
||||
g->transition_to = *world_from_slam;
|
||||
g->transition_start_ns = g->good_ns;
|
||||
} else {
|
||||
g->align = *world_from_slam;
|
||||
}
|
||||
g->world_valid = true;
|
||||
}
|
||||
|
||||
static void
|
||||
apply_align(const struct xrt_pose *align, const struct xrt_pose *raw, struct xrt_pose *out)
|
||||
{
|
||||
struct xrt_pose result;
|
||||
math_quat_rotate(&align->orientation, &raw->orientation, &result.orientation);
|
||||
math_quat_normalize(&result.orientation);
|
||||
math_quat_rotate_vec3(&align->orientation, &raw->position, &result.position);
|
||||
result.position = m_vec3_add(result.position, align->position);
|
||||
*out = result;
|
||||
}
|
||||
|
||||
//! Checks a step from @p from to @p to against the jump and speed limits.
|
||||
static enum rift_s_slam_guard_reason
|
||||
check_step(const struct xrt_vec3 *from, int64_t from_ns, const struct xrt_vec3 *to, int64_t to_ns, float *out_step)
|
||||
{
|
||||
float step = m_vec3_len(m_vec3_sub(*to, *from));
|
||||
double dt_s = fabs((double)(to_ns - from_ns)) / 1e9;
|
||||
*out_step = step;
|
||||
if (step > RIFT_S_SLAM_GUARD_MAX_STEP_M) {
|
||||
return RIFT_S_SLAM_GUARD_JUMP;
|
||||
}
|
||||
if (step > RIFT_S_SLAM_GUARD_STEP_TOLERANCE_M + RIFT_S_SLAM_GUARD_MAX_SPEED_MPS * dt_s) {
|
||||
return RIFT_S_SLAM_GUARD_SPEED;
|
||||
}
|
||||
return RIFT_S_SLAM_GUARD_OK;
|
||||
}
|
||||
|
||||
//! Height outside the room (below the nominal floor or far above it).
|
||||
static bool
|
||||
outside_room(const struct xrt_vec3 *p, const struct xrt_vec3 *no_pose_position)
|
||||
{
|
||||
float floor_y = no_pose_position->y - RIFT_S_SLAM_GUARD_EYE_HEIGHT_M;
|
||||
return p->y < floor_y - RIFT_S_SLAM_GUARD_BELOW_FLOOR_M || p->y > floor_y + RIFT_S_SLAM_GUARD_ABOVE_FLOOR_M;
|
||||
}
|
||||
|
||||
bool
|
||||
rift_s_slam_guard_query(const struct rift_s_slam_guard *g, const struct xrt_pose *raw, int64_t query_ns,
|
||||
const struct xrt_quat *fusion, const struct xrt_vec3 *no_pose_position,
|
||||
struct xrt_pose *out)
|
||||
{
|
||||
if (g->state != RIFT_S_SLAM_GUARD_TRACKING || !g->world_valid ||
|
||||
(g->require_relocalization && !g->boundary_confirmed) ||
|
||||
!rift_s_slam_guard_pose_usable(raw, fusion)) return false;
|
||||
struct xrt_pose aligned;
|
||||
apply_align(&g->align, raw, &aligned);
|
||||
float step;
|
||||
if (m_vec3_len(aligned.position) > RIFT_S_SLAM_GUARD_MAX_DISTANCE_M ||
|
||||
outside_room(&aligned.position, no_pose_position) ||
|
||||
check_step(&g->good.position, g->good_ns, &aligned.position, query_ns, &step) != RIFT_S_SLAM_GUARD_OK)
|
||||
return false;
|
||||
*out = aligned;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
reset_due(const struct rift_s_slam_guard *g, int64_t ts_ns)
|
||||
{
|
||||
if (g->reset_pending) {
|
||||
return false;
|
||||
}
|
||||
return !g->have_reset || (ts_ns - g->last_reset_ns) >= RIFT_S_SLAM_GUARD_RESET_INTERVAL_NS;
|
||||
}
|
||||
|
||||
void
|
||||
rift_s_slam_guard_init(struct rift_s_slam_guard *g)
|
||||
{
|
||||
memset(g, 0, sizeof(*g));
|
||||
g->state = RIFT_S_SLAM_GUARD_ACQUIRING;
|
||||
g->good.orientation = identity_quat;
|
||||
g->align.orientation = identity_quat;
|
||||
g->fallback_yaw = identity_quat;
|
||||
g->stable_last.orientation = identity_quat;
|
||||
}
|
||||
|
||||
struct xrt_vec3
|
||||
rift_s_slam_guard_no_pose_position(float origin_offset_y)
|
||||
{
|
||||
struct xrt_vec3 p = {0, RIFT_S_SLAM_GUARD_EYE_HEIGHT_M - origin_offset_y, 0};
|
||||
return p;
|
||||
}
|
||||
|
||||
void
|
||||
rift_s_slam_guard_yaw_of(const struct xrt_quat *q, struct xrt_quat *out_yaw)
|
||||
{
|
||||
struct xrt_quat yaw = {0, q->y, 0, q->w};
|
||||
float len = sqrtf(yaw.y * yaw.y + yaw.w * yaw.w);
|
||||
if (!(len > 1e-6f)) {
|
||||
*out_yaw = identity_quat;
|
||||
return;
|
||||
}
|
||||
yaw.y /= len;
|
||||
yaw.w /= len;
|
||||
*out_yaw = yaw;
|
||||
}
|
||||
|
||||
void
|
||||
rift_s_slam_guard_fallback_pose(const struct rift_s_slam_guard *g,
|
||||
const struct xrt_quat *fusion_orientation,
|
||||
const struct xrt_vec3 *no_pose_position,
|
||||
struct xrt_pose *out_pose)
|
||||
{
|
||||
math_quat_rotate(&g->fallback_yaw, fusion_orientation, &out_pose->orientation);
|
||||
float norm = out_pose->orientation.x * out_pose->orientation.x + out_pose->orientation.y * out_pose->orientation.y +
|
||||
out_pose->orientation.z * out_pose->orientation.z + out_pose->orientation.w * out_pose->orientation.w;
|
||||
if (isfinite(norm) && norm > 0.5f) math_quat_normalize(&out_pose->orientation);
|
||||
else out_pose->orientation = g->have_good ? g->good.orientation : identity_quat;
|
||||
out_pose->position = g->have_good ? g->good.position : *no_pose_position;
|
||||
}
|
||||
|
||||
void
|
||||
rift_s_slam_guard_backend_started(struct rift_s_slam_guard *g, int64_t ts_ns)
|
||||
{
|
||||
g->reset_pending = false;
|
||||
g->invalid_since_ns = 0;
|
||||
g->have_reset = true;
|
||||
g->last_reset_ns = ts_ns;
|
||||
g->stable_samples = 0;
|
||||
}
|
||||
|
||||
bool
|
||||
rift_s_slam_guard_take_reset(struct rift_s_slam_guard *g, int64_t ts_ns)
|
||||
{
|
||||
if (!g->reset_pending) {
|
||||
return false;
|
||||
}
|
||||
g->reset_pending = false;
|
||||
g->have_reset = true;
|
||||
g->last_reset_ns = ts_ns;
|
||||
g->resets++;
|
||||
g->world_valid = false;
|
||||
g->boundary_confirmed = false;
|
||||
g->world_transition = false;
|
||||
// Samples queued before the reset belong to the diverged run.
|
||||
g->stable_samples = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
enter_diverged(struct rift_s_slam_guard *g,
|
||||
int64_t ts_ns,
|
||||
const struct xrt_quat *fusion_orientation,
|
||||
bool diverged,
|
||||
float distance,
|
||||
float step)
|
||||
{
|
||||
g->state = RIFT_S_SLAM_GUARD_DIVERGED;
|
||||
g->stable_samples = 0;
|
||||
|
||||
// Continue the 3DoF orientation in the heading SLAM last showed.
|
||||
struct xrt_quat inv_fusion;
|
||||
struct xrt_quat delta;
|
||||
math_quat_invert(fusion_orientation, &inv_fusion);
|
||||
math_quat_rotate(&g->good.orientation, &inv_fusion, &delta);
|
||||
rift_s_slam_guard_yaw_of(&delta, &g->fallback_yaw);
|
||||
|
||||
if (diverged) {
|
||||
g->divergences++;
|
||||
g->last_reject_distance_m = distance;
|
||||
g->last_reject_step_m = step;
|
||||
if (reset_due(g, ts_ns)) {
|
||||
g->reset_pending = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
rift_s_slam_guard_suspend(struct rift_s_slam_guard *g, const struct xrt_quat *fusion_orientation)
|
||||
{
|
||||
if (g->state == RIFT_S_SLAM_GUARD_TRACKING) {
|
||||
enter_diverged(g, 0, fusion_orientation, false, 0.0f, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
//! Feeds a raw sample into the stability run used while acquiring or diverged.
|
||||
static enum rift_s_slam_guard_reason
|
||||
settle(struct rift_s_slam_guard *g, bool slam_valid, const struct xrt_pose *raw, int64_t ts_ns)
|
||||
{
|
||||
if (!slam_valid) {
|
||||
g->stable_samples = 0;
|
||||
if (g->have_good && ts_ns - g->good_ns > RIFT_S_SLAM_GUARD_RESET_INTERVAL_NS && reset_due(g, ts_ns))
|
||||
g->reset_pending = true;
|
||||
return RIFT_S_SLAM_GUARD_NO_POSE;
|
||||
}
|
||||
|
||||
enum rift_s_slam_guard_reason reason = RIFT_S_SLAM_GUARD_OK;
|
||||
float step = 0.0f;
|
||||
if (!pose_is_finite(raw)) {
|
||||
reason = RIFT_S_SLAM_GUARD_NOT_FINITE;
|
||||
} else if (m_vec3_len(raw->position) > RIFT_S_SLAM_GUARD_MAX_DISTANCE_M) {
|
||||
reason = RIFT_S_SLAM_GUARD_TOO_FAR;
|
||||
} else if (g->stable_samples > 0) {
|
||||
reason = check_step(&g->stable_last.position, g->stable_last_ns, &raw->position, ts_ns, &step);
|
||||
}
|
||||
|
||||
bool far_off = reason == RIFT_S_SLAM_GUARD_NOT_FINITE || reason == RIFT_S_SLAM_GUARD_TOO_FAR;
|
||||
bool still_diverging = far_off || (reason != RIFT_S_SLAM_GUARD_OK && g->state == RIFT_S_SLAM_GUARD_DIVERGED);
|
||||
if (still_diverging && reset_due(g, ts_ns)) {
|
||||
// Retry the re-initialisation at the rate limit.
|
||||
g->reset_pending = true;
|
||||
}
|
||||
if (far_off) {
|
||||
g->stable_samples = 0;
|
||||
return reason;
|
||||
}
|
||||
|
||||
if (reason != RIFT_S_SLAM_GUARD_OK || g->stable_samples == 0) {
|
||||
// A jump starts a new run here: a re-initialised SLAM snaps to its new origin.
|
||||
g->stable_samples = 0;
|
||||
g->stable_start_ns = ts_ns;
|
||||
}
|
||||
g->stable_samples++;
|
||||
g->stable_last = *raw;
|
||||
g->stable_last_ns = ts_ns;
|
||||
|
||||
if (reason != RIFT_S_SLAM_GUARD_OK) {
|
||||
return reason;
|
||||
}
|
||||
if (g->stable_samples < RIFT_S_SLAM_GUARD_STABLE_SAMPLES ||
|
||||
(ts_ns - g->stable_start_ns) < RIFT_S_SLAM_GUARD_STABLE_NS) {
|
||||
return RIFT_S_SLAM_GUARD_SETTLING;
|
||||
}
|
||||
return RIFT_S_SLAM_GUARD_OK;
|
||||
}
|
||||
|
||||
bool
|
||||
rift_s_slam_guard_update(struct rift_s_slam_guard *g,
|
||||
bool slam_valid,
|
||||
const struct xrt_pose *slam_pose,
|
||||
int64_t ts_ns,
|
||||
const struct xrt_quat *fusion_orientation,
|
||||
const struct xrt_vec3 *no_pose_position,
|
||||
struct xrt_pose *out_pose,
|
||||
enum rift_s_slam_guard_reason *out_reason)
|
||||
{
|
||||
if (g->require_relocalization && !g->boundary_confirmed && g->world_valid) {
|
||||
// A failed/cancelled setup can restore saved bounds while the fresh frame is still
|
||||
// active. Revoke that frame before either source or predicted poses are published.
|
||||
g->world_valid = false;
|
||||
g->world_transition = false;
|
||||
if (g->state == RIFT_S_SLAM_GUARD_TRACKING)
|
||||
enter_diverged(g, ts_ns, fusion_orientation, false, 0, 0);
|
||||
}
|
||||
bool usable = slam_valid && rift_s_slam_guard_pose_usable(slam_pose, fusion_orientation);
|
||||
if (usable) {
|
||||
g->invalid_since_ns = 0;
|
||||
} else {
|
||||
if (g->invalid_since_ns == 0)
|
||||
g->invalid_since_ns = ts_ns;
|
||||
if (ts_ns - g->invalid_since_ns >= RIFT_S_SLAM_GUARD_RESET_INTERVAL_NS && reset_due(g, ts_ns))
|
||||
g->reset_pending = true;
|
||||
}
|
||||
if (slam_valid && pose_is_finite(slam_pose) && !usable) {
|
||||
*out_reason = RIFT_S_SLAM_GUARD_TILT;
|
||||
if (g->state == RIFT_S_SLAM_GUARD_TRACKING)
|
||||
enter_diverged(g, ts_ns, fusion_orientation, true, 0, 0);
|
||||
g->stable_samples = 0;
|
||||
rift_s_slam_guard_fallback_pose(g, fusion_orientation, no_pose_position, out_pose);
|
||||
return false;
|
||||
}
|
||||
if (g->state == RIFT_S_SLAM_GUARD_TRACKING) {
|
||||
// Duplicate/out-of-order reads cannot change the source anchor or trigger a reset.
|
||||
if (slam_valid && ts_ns <= g->good_ns) {
|
||||
*out_pose = g->good;
|
||||
*out_reason = RIFT_S_SLAM_GUARD_OK;
|
||||
return true;
|
||||
}
|
||||
if (!slam_valid || !pose_is_finite(slam_pose)) {
|
||||
*out_reason = slam_valid ? RIFT_S_SLAM_GUARD_NOT_FINITE : RIFT_S_SLAM_GUARD_NO_POSE;
|
||||
// A lost SLAM pose is not a divergence; a non-finite one is.
|
||||
enter_diverged(g, ts_ns, fusion_orientation, slam_valid, NAN, NAN);
|
||||
rift_s_slam_guard_fallback_pose(g, fusion_orientation, no_pose_position, out_pose);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (g->world_transition) {
|
||||
// Move the published frame over two seconds. Reframe history too, so this deliberate
|
||||
// correction cannot masquerade as a physical head jump in the source guard.
|
||||
float t = fminf(1.0f, fmaxf(0.0f, (float)(ts_ns - g->transition_start_ns) / 2e9f));
|
||||
struct xrt_pose previous = g->align, inverse, raw_good;
|
||||
math_pose_invert(&previous, &inverse);
|
||||
math_pose_transform(&inverse, &g->good, &raw_good);
|
||||
math_pose_interpolate(&g->transition_from, &g->transition_to, t, &g->align);
|
||||
apply_align(&g->align, &raw_good, &g->good);
|
||||
g->world_transition = t < 1.0f;
|
||||
}
|
||||
struct xrt_pose aligned;
|
||||
apply_align(&g->align, slam_pose, &aligned);
|
||||
float distance = m_vec3_len(aligned.position);
|
||||
float step = 0.0f;
|
||||
enum rift_s_slam_guard_reason reason =
|
||||
check_step(&g->good.position, g->good_ns, &aligned.position, ts_ns, &step);
|
||||
if (distance > RIFT_S_SLAM_GUARD_MAX_DISTANCE_M) {
|
||||
reason = RIFT_S_SLAM_GUARD_TOO_FAR;
|
||||
} else if (outside_room(&aligned.position, no_pose_position)) {
|
||||
reason = RIFT_S_SLAM_GUARD_HEIGHT;
|
||||
}
|
||||
|
||||
if (reason == RIFT_S_SLAM_GUARD_OK) {
|
||||
g->good = aligned;
|
||||
g->good_ns = ts_ns;
|
||||
*out_pose = aligned;
|
||||
*out_reason = RIFT_S_SLAM_GUARD_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
*out_reason = reason;
|
||||
enter_diverged(g, ts_ns, fusion_orientation, true, distance, step);
|
||||
rift_s_slam_guard_fallback_pose(g, fusion_orientation, no_pose_position, out_pose);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Acquiring or diverged: publish the fallback until SLAM has been consistent for a while.
|
||||
struct xrt_pose fallback;
|
||||
rift_s_slam_guard_fallback_pose(g, fusion_orientation, no_pose_position, &fallback);
|
||||
|
||||
if (g->stable_samples > 0 && ts_ns <= g->stable_last_ns) {
|
||||
*out_reason = RIFT_S_SLAM_GUARD_SETTLING;
|
||||
*out_pose = fallback;
|
||||
return false;
|
||||
}
|
||||
enum rift_s_slam_guard_reason reason = settle(g, slam_valid, slam_pose, ts_ns);
|
||||
if (reason != RIFT_S_SLAM_GUARD_OK) {
|
||||
*out_reason = reason;
|
||||
*out_pose = fallback;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!g->world_valid) {
|
||||
if (g->require_relocalization && !g->boundary_confirmed) {
|
||||
// SteamVR's saved standing origin belongs to the persisted map, not this VIO
|
||||
// run's arbitrary yaw/translation. Only a visual match can join those frames.
|
||||
*out_reason = RIFT_S_SLAM_GUARD_RELOCALIZING;
|
||||
*out_pose = fallback;
|
||||
return false;
|
||||
}
|
||||
// An explicitly new room can start a fresh frame before it has saved landmarks.
|
||||
struct xrt_quat inv_raw, delta;
|
||||
math_quat_invert(&slam_pose->orientation, &inv_raw);
|
||||
math_quat_rotate(&fallback.orientation, &inv_raw, &delta);
|
||||
rift_s_slam_guard_yaw_of(&delta, &g->align.orientation);
|
||||
struct xrt_vec3 rotated;
|
||||
math_quat_rotate_vec3(&g->align.orientation, &slam_pose->position, &rotated);
|
||||
g->align.position = m_vec3_sub(fallback.position, rotated);
|
||||
g->world_valid = true;
|
||||
}
|
||||
if (g->state == RIFT_S_SLAM_GUARD_DIVERGED) {
|
||||
g->recoveries++;
|
||||
}
|
||||
|
||||
g->state = RIFT_S_SLAM_GUARD_TRACKING;
|
||||
g->stable_samples = 0;
|
||||
apply_align(&g->align, slam_pose, &g->good);
|
||||
g->good_ns = ts_ns;
|
||||
g->have_good = true;
|
||||
*out_pose = g->good;
|
||||
*out_reason = RIFT_S_SLAM_GUARD_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
const char *
|
||||
rift_s_slam_guard_reason_str(enum rift_s_slam_guard_reason reason)
|
||||
{
|
||||
switch (reason) {
|
||||
case RIFT_S_SLAM_GUARD_OK: return "ok";
|
||||
case RIFT_S_SLAM_GUARD_NO_POSE: return "no_pose";
|
||||
case RIFT_S_SLAM_GUARD_NOT_FINITE: return "not_finite";
|
||||
case RIFT_S_SLAM_GUARD_TOO_FAR: return "too_far";
|
||||
case RIFT_S_SLAM_GUARD_HEIGHT: return "height";
|
||||
case RIFT_S_SLAM_GUARD_JUMP: return "jump";
|
||||
case RIFT_S_SLAM_GUARD_SPEED: return "speed";
|
||||
case RIFT_S_SLAM_GUARD_SETTLING: return "settling";
|
||||
case RIFT_S_SLAM_GUARD_TILT: return "gravity_mismatch";
|
||||
case RIFT_S_SLAM_GUARD_RELOCALIZING: return "relocalizing";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
enum rift_s_slam_feature_event
|
||||
rift_s_slam_feature_gate_update(struct rift_s_slam_feature_gate *gate, bool known, int count, int64_t ts_ns)
|
||||
{
|
||||
if (!known || ts_ns <= gate->last_ts_ns) {
|
||||
return RIFT_S_SLAM_FEATURES_NO_CHANGE;
|
||||
}
|
||||
gate->last_ts_ns = ts_ns;
|
||||
gate->last_count = count;
|
||||
|
||||
bool low = count < RIFT_S_SLAM_FEATURES_MIN;
|
||||
if (low != gate->lost) {
|
||||
if (!gate->crossing) {
|
||||
gate->crossing = true;
|
||||
gate->crossing_since_ns = ts_ns;
|
||||
}
|
||||
if (ts_ns - gate->crossing_since_ns >= RIFT_S_SLAM_FEATURES_DWELL_NS) {
|
||||
gate->lost = low;
|
||||
gate->crossing = false;
|
||||
return low ? RIFT_S_SLAM_FEATURES_LOST : RIFT_S_SLAM_FEATURES_RETURNED;
|
||||
}
|
||||
} else {
|
||||
gate->crossing = false;
|
||||
}
|
||||
return RIFT_S_SLAM_FEATURES_NO_CHANGE;
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
/*!
|
||||
* @file
|
||||
* @brief Rejects diverged SLAM poses and supplies the fallback HMD pose.
|
||||
* @ingroup drv_rift_s
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "xrt/xrt_defines.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
//! SLAM positions farther than this from the tracking origin are treated as divergence.
|
||||
#define RIFT_S_SLAM_GUARD_MAX_DISTANCE_M 5.0f
|
||||
//! A position step larger than this between two SLAM samples is treated as divergence.
|
||||
#define RIFT_S_SLAM_GUARD_MAX_STEP_M 0.5f
|
||||
//! A head speed above this between two SLAM samples is treated as divergence.
|
||||
#define RIFT_S_SLAM_GUARD_MAX_SPEED_MPS 5.0f
|
||||
//! Step allowed on top of the speed limit, for samples queried very close in time.
|
||||
#define RIFT_S_SLAM_GUARD_STEP_TOLERANCE_M 0.01f
|
||||
//! Consecutive consistent SLAM samples needed before SLAM is trusted (again).
|
||||
#define RIFT_S_SLAM_GUARD_STABLE_SAMPLES 30
|
||||
//! Time over which those samples must stay consistent.
|
||||
#define RIFT_S_SLAM_GUARD_STABLE_NS (1000LL * 1000LL * 1000LL)
|
||||
//! Minimum interval between two SLAM re-initialisation requests.
|
||||
#define RIFT_S_SLAM_GUARD_RESET_INTERVAL_NS (2000LL * 1000LL * 1000LL)
|
||||
//! Eye height above the floor reported before SLAM ever gave a good position.
|
||||
#define RIFT_S_SLAM_GUARD_EYE_HEIGHT_M 1.6f
|
||||
//! A head this far below the (nominal) floor is underground: divergence.
|
||||
#define RIFT_S_SLAM_GUARD_BELOW_FLOOR_M 0.3f
|
||||
//! A head this high above the (nominal) floor is divergence.
|
||||
#define RIFT_S_SLAM_GUARD_ABOVE_FLOOR_M 2.5f
|
||||
|
||||
enum rift_s_slam_guard_state
|
||||
{
|
||||
//! No trusted SLAM pose yet since start.
|
||||
RIFT_S_SLAM_GUARD_ACQUIRING,
|
||||
//! SLAM poses are published.
|
||||
RIFT_S_SLAM_GUARD_TRACKING,
|
||||
//! SLAM diverged or stopped; 3DoF with the held position until SLAM is consistent again.
|
||||
RIFT_S_SLAM_GUARD_DIVERGED,
|
||||
};
|
||||
|
||||
struct rift_s_slam_guard
|
||||
{
|
||||
//! Saved world alignment is usable only until the backend changes its map.
|
||||
bool world_valid;
|
||||
//! Saved-boundary confirmation is independent of temporary SLAM tracking.
|
||||
bool boundary_confirmed;
|
||||
bool world_transition;
|
||||
struct xrt_pose transition_from, transition_to;
|
||||
int64_t transition_start_ns;
|
||||
//! A saved standing frame requires visual confirmation before publishing position tracking.
|
||||
bool require_relocalization;
|
||||
enum rift_s_slam_guard_state state;
|
||||
|
||||
//! Last accepted source SLAM pose in the world frame and its source timestamp.
|
||||
bool have_good;
|
||||
struct xrt_pose good;
|
||||
int64_t good_ns;
|
||||
|
||||
//! Maps raw SLAM poses into the published frame (yaw and translation only).
|
||||
struct xrt_pose align;
|
||||
|
||||
//! Yaw applied to the 3DoF orientation while falling back, so it continues the SLAM heading.
|
||||
struct xrt_quat fallback_yaw;
|
||||
|
||||
//! Stability run of raw SLAM samples while acquiring or diverged.
|
||||
uint32_t stable_samples;
|
||||
int64_t stable_start_ns;
|
||||
struct xrt_pose stable_last;
|
||||
int64_t stable_last_ns;
|
||||
|
||||
//! Re-initialisation bookkeeping.
|
||||
bool reset_pending;
|
||||
bool have_reset;
|
||||
int64_t last_reset_ns;
|
||||
//! Invalid output must also recover before a first trusted pose exists.
|
||||
int64_t invalid_since_ns;
|
||||
|
||||
//! Diagnostics.
|
||||
uint32_t divergences;
|
||||
uint32_t resets;
|
||||
uint32_t recoveries;
|
||||
float last_reject_distance_m;
|
||||
float last_reject_step_m;
|
||||
};
|
||||
|
||||
//! Why a SLAM sample was not published.
|
||||
enum rift_s_slam_guard_reason
|
||||
{
|
||||
RIFT_S_SLAM_GUARD_OK,
|
||||
RIFT_S_SLAM_GUARD_NO_POSE,
|
||||
RIFT_S_SLAM_GUARD_NOT_FINITE,
|
||||
RIFT_S_SLAM_GUARD_TOO_FAR,
|
||||
RIFT_S_SLAM_GUARD_HEIGHT,
|
||||
RIFT_S_SLAM_GUARD_JUMP,
|
||||
RIFT_S_SLAM_GUARD_SPEED,
|
||||
RIFT_S_SLAM_GUARD_SETTLING,
|
||||
RIFT_S_SLAM_GUARD_TILT,
|
||||
RIFT_S_SLAM_GUARD_RELOCALIZING,
|
||||
};
|
||||
|
||||
//! Finite, unit-length orientation whose gravity direction agrees with capture-time IMU fusion.
|
||||
bool
|
||||
rift_s_slam_guard_pose_usable(const struct xrt_pose *pose, const struct xrt_quat *fusion);
|
||||
|
||||
//! Validate a predicted/historical query without changing source history or world alignment.
|
||||
bool
|
||||
rift_s_slam_guard_query(const struct rift_s_slam_guard *g, const struct xrt_pose *raw, int64_t query_ns,
|
||||
const struct xrt_quat *fusion, const struct xrt_vec3 *no_pose_position,
|
||||
struct xrt_pose *out);
|
||||
|
||||
//! Install a visually recovered world transform. No head-height or held-position adjustment.
|
||||
void
|
||||
rift_s_slam_guard_set_world(struct rift_s_slam_guard *g, const struct xrt_pose *world_from_slam);
|
||||
|
||||
void
|
||||
rift_s_slam_guard_init(struct rift_s_slam_guard *g);
|
||||
|
||||
/*!
|
||||
* Feed one SLAM sample and get the IMU pose to publish.
|
||||
*
|
||||
* @param g Guard state.
|
||||
* @param slam_valid Whether the SLAM tracker returned a pose with a valid position.
|
||||
* @param slam_pose SLAM IMU pose, already converted to the Monado frame.
|
||||
* @param ts_ns Query time of the sample.
|
||||
* @param fusion_orientation Current 3DoF orientation.
|
||||
* @param no_pose_position Position to report while no SLAM position was ever trusted. It is at eye
|
||||
* height, so it also gives the nominal floor for the height bound.
|
||||
* @param[out] out_pose Pose to publish.
|
||||
* @param[out] out_reason Why SLAM was not used, or OK.
|
||||
* @return true when @p out_pose comes from SLAM (position tracked).
|
||||
*/
|
||||
bool
|
||||
rift_s_slam_guard_update(struct rift_s_slam_guard *g,
|
||||
bool slam_valid,
|
||||
const struct xrt_pose *slam_pose,
|
||||
int64_t ts_ns,
|
||||
const struct xrt_quat *fusion_orientation,
|
||||
const struct xrt_vec3 *no_pose_position,
|
||||
struct xrt_pose *out_pose,
|
||||
enum rift_s_slam_guard_reason *out_reason);
|
||||
|
||||
/*!
|
||||
* The pose to publish without SLAM: the 3DoF orientation, continued in the SLAM heading, at the
|
||||
* last good position (or @p no_pose_position).
|
||||
*/
|
||||
void
|
||||
rift_s_slam_guard_fallback_pose(const struct rift_s_slam_guard *g,
|
||||
const struct xrt_quat *fusion_orientation,
|
||||
const struct xrt_vec3 *no_pose_position,
|
||||
struct xrt_pose *out_pose);
|
||||
|
||||
/*!
|
||||
* Stop publishing SLAM without counting a divergence (3DoF chosen by the user). SLAM is aligned onto
|
||||
* the shown pose again once it is consistent.
|
||||
*/
|
||||
void
|
||||
rift_s_slam_guard_suspend(struct rift_s_slam_guard *g, const struct xrt_quat *fusion_orientation);
|
||||
|
||||
//! Start the replacement backend's recovery interval without changing the held world pose.
|
||||
void
|
||||
rift_s_slam_guard_backend_started(struct rift_s_slam_guard *g, int64_t ts_ns);
|
||||
|
||||
/*!
|
||||
* Returns true once per required SLAM re-initialisation, and records it as done at @p ts_ns.
|
||||
*/
|
||||
bool
|
||||
rift_s_slam_guard_take_reset(struct rift_s_slam_guard *g, int64_t ts_ns);
|
||||
|
||||
/*!
|
||||
* Position, in the tracker frame, that puts the head at eye height above the floor when the tracking
|
||||
* origin is already offset by @p origin_offset_y.
|
||||
*/
|
||||
struct xrt_vec3
|
||||
rift_s_slam_guard_no_pose_position(float origin_offset_y);
|
||||
|
||||
//! The yaw-only part (about +Y) of a rotation.
|
||||
void
|
||||
rift_s_slam_guard_yaw_of(const struct xrt_quat *q, struct xrt_quat *out_yaw);
|
||||
|
||||
const char *
|
||||
rift_s_slam_guard_reason_str(enum rift_s_slam_guard_reason reason);
|
||||
|
||||
//! Fewer features than this over all SLAM cameras means the cameras see nothing usable.
|
||||
#define RIFT_S_SLAM_FEATURES_MIN 20
|
||||
//! How long the count must stay on one side of the limit before the state changes.
|
||||
#define RIFT_S_SLAM_FEATURES_DWELL_NS (500LL * 1000LL * 1000LL)
|
||||
|
||||
//! Tracks whether the SLAM cameras see enough features (not face down, covered or dark).
|
||||
struct rift_s_slam_feature_gate
|
||||
{
|
||||
bool lost;
|
||||
bool crossing;
|
||||
int64_t crossing_since_ns;
|
||||
int64_t last_ts_ns;
|
||||
int last_count;
|
||||
};
|
||||
|
||||
enum rift_s_slam_feature_event
|
||||
{
|
||||
RIFT_S_SLAM_FEATURES_NO_CHANGE,
|
||||
//! Features gone: hold the head pose.
|
||||
RIFT_S_SLAM_FEATURES_LOST,
|
||||
//! Features back: re-initialise SLAM and realign.
|
||||
RIFT_S_SLAM_FEATURES_RETURNED,
|
||||
};
|
||||
|
||||
/*!
|
||||
* Feed the latest feature count. @p known false (no count yet) changes nothing. Samples with a
|
||||
* timestamp not newer than the last one are ignored.
|
||||
*/
|
||||
enum rift_s_slam_feature_event
|
||||
rift_s_slam_feature_gate_update(struct rift_s_slam_feature_gate *gate, bool known, int count, int64_t ts_ns);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
#include <math.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "math/m_api.h"
|
||||
@@ -34,6 +35,8 @@
|
||||
#include "util/u_sink.h"
|
||||
#include "util/u_trace_marker.h"
|
||||
#include "util/u_var.h"
|
||||
#include "util/u_file.h"
|
||||
#include "rift_s_world_control.h"
|
||||
|
||||
#include "xrt/xrt_config_build.h"
|
||||
#include "xrt/xrt_config_drivers.h"
|
||||
@@ -48,6 +51,7 @@
|
||||
#include "rift_s_interface.h"
|
||||
#include "rift_s_util.h"
|
||||
#include "rift_s_tracker.h"
|
||||
#include "rift_s_passthrough.h"
|
||||
|
||||
#ifdef XRT_FEATURE_SLAM
|
||||
static const bool slam_supported = true;
|
||||
@@ -71,6 +75,9 @@ DEBUG_GET_ONCE_BOOL_OPTION(rift_s_handtracking, "RIFT_S_HANDTRACKING", true)
|
||||
DEBUG_GET_ONCE_OPTION(slam_submit_from_start, "SLAM_SUBMIT_FROM_START", NULL)
|
||||
#endif
|
||||
|
||||
static void *
|
||||
rift_s_world_thread(void *ptr);
|
||||
|
||||
static xrt_result_t
|
||||
rift_s_tracker_get_tracked_pose_imu(struct xrt_device *xdev,
|
||||
enum xrt_input_name name,
|
||||
@@ -94,6 +101,7 @@ rift_s_tracker_switch_method_cb(void *t_ptr)
|
||||
os_mutex_lock(&t->mutex);
|
||||
m_imu_3dof_reset(&t->fusion.i3dof);
|
||||
t->fusion.i3dof.rot = t->pose.orientation;
|
||||
rift_s_slam_guard_suspend(&t->slam_guard, &t->fusion.i3dof.rot);
|
||||
os_mutex_unlock(&t->mutex);
|
||||
}
|
||||
}
|
||||
@@ -172,6 +180,12 @@ rift_s_fill_slam_cameras_calibration(struct rift_s_tracker *t, struct rift_s_hmd
|
||||
enum rift_s_camera_id cam_id = CAM_IDX_TO_ID[i];
|
||||
struct rift_s_camera_calibration *cam = &camera_calibration->cameras[cam_id];
|
||||
|
||||
RIFT_S_DEBUG("DIAG FW cam%d id%d %ux%u fx=%.9g fy=%.9g cx=%.9g cy=%.9g", i, cam_id,
|
||||
cam->roi.extent.w, cam->roi.extent.h, cam->projection.fx, cam->projection.fy,
|
||||
cam->projection.cx, cam->projection.cy);
|
||||
for (int j = 0; j < 16; j++) RIFT_S_DEBUG("DIAG FW cam%d device_from_cam[%d]=%.9g", i, j, cam->device_from_camera.v[j]);
|
||||
for (int j = 0; j < 6; j++) RIFT_S_DEBUG("DIAG FW cam%d k[%d]=%.9g", i, j, cam->distortion.k[j]);
|
||||
RIFT_S_DEBUG("DIAG FW cam%d p1=%.9g p2=%.9g", i, cam->distortion.p1, cam->distortion.p2);
|
||||
struct xrt_pose device_from_cam;
|
||||
math_pose_from_isometry(&cam->device_from_camera, &device_from_cam);
|
||||
|
||||
@@ -197,6 +211,13 @@ rift_s_fill_slam_cameras_calibration(struct rift_s_tracker *t, struct rift_s_hmd
|
||||
static void
|
||||
rift_s_fill_slam_calibration(struct rift_s_tracker *t, struct rift_s_hmd_config *hmd_config)
|
||||
{
|
||||
const struct rift_s_imu_calibration *c = &hmd_config->imu_calibration;
|
||||
RIFT_S_DEBUG("DIAG FW accel offset %.9g %.9g %.9g tempcoeff %.9g %.9g %.9g gyro offset %.9g %.9g %.9g",
|
||||
c->accel.offset_at_0C.x, c->accel.offset_at_0C.y, c->accel.offset_at_0C.z,
|
||||
c->accel.temp_coeff.x, c->accel.temp_coeff.y, c->accel.temp_coeff.z,
|
||||
c->gyro.offset.x, c->gyro.offset.y, c->gyro.offset.z);
|
||||
for (int i = 0; i < 9; i++) RIFT_S_DEBUG("DIAG FW rect[%d] accel=%.9g gyro=%.9g", i, c->accel.rectification.v[i], c->gyro.rectification.v[i]);
|
||||
for (int i = 0; i < 16; i++) RIFT_S_DEBUG("DIAG FW device_from_imu[%d]=%.9g", i, c->device_from_imu.v[i]);
|
||||
rift_s_fill_slam_imu_calibration(t, hmd_config);
|
||||
rift_s_fill_slam_cameras_calibration(t, hmd_config);
|
||||
}
|
||||
@@ -313,6 +334,12 @@ rift_s_fill_constellation_calibration(struct rift_s_tracker *t, struct rift_s_hm
|
||||
struct rift_s_camera_calibration *cam = &camera_calibration->cameras[cam_id];
|
||||
|
||||
/* Compute the cam from IMU transform for each cam */
|
||||
RIFT_S_DEBUG("DIAG FW cam%d id%d %ux%u fx=%.9g fy=%.9g cx=%.9g cy=%.9g", i, cam_id,
|
||||
cam->roi.extent.w, cam->roi.extent.h, cam->projection.fx, cam->projection.fy,
|
||||
cam->projection.cx, cam->projection.cy);
|
||||
for (int j = 0; j < 16; j++) RIFT_S_DEBUG("DIAG FW cam%d device_from_cam[%d]=%.9g", i, j, cam->device_from_camera.v[j]);
|
||||
for (int j = 0; j < 6; j++) RIFT_S_DEBUG("DIAG FW cam%d k[%d]=%.9g", i, j, cam->distortion.k[j]);
|
||||
RIFT_S_DEBUG("DIAG FW cam%d p1=%.9g p2=%.9g", i, cam->distortion.p1, cam->distortion.p2);
|
||||
struct xrt_pose device_from_cam;
|
||||
math_pose_from_isometry(&cam->device_from_camera, &device_from_cam);
|
||||
|
||||
@@ -333,8 +360,14 @@ rift_s_fill_constellation_calibration(struct rift_s_tracker *t, struct rift_s_hm
|
||||
};
|
||||
out->cams[i] = (struct t_constellation_camera){
|
||||
.P_imu_cam = P_imu_camcv,
|
||||
.slam_tracking_index = i,
|
||||
.roi = roi,
|
||||
.calibration = rift_s_get_cam_calib(&hmd_config->camera_calibration, cam_id),
|
||||
.fisheye62_valid = true,
|
||||
.fisheye62_radial = {cam->distortion.k[0], cam->distortion.k[1], cam->distortion.k[2],
|
||||
cam->distortion.k[3], cam->distortion.k[4], cam->distortion.k[5]},
|
||||
.fisheye62_p1 = cam->distortion.p1,
|
||||
.fisheye62_p2 = cam->distortion.p2,
|
||||
.blob_min_threshold = BLOB_PIXEL_THRESHOLD,
|
||||
.blob_detect_threshold = BLOB_THRESHOLD_MIN};
|
||||
|
||||
@@ -408,10 +441,20 @@ rift_s_tracker_create(struct xrt_tracking_origin *origin,
|
||||
int ret = os_mutex_init(&t->mutex);
|
||||
if (ret != 0) {
|
||||
RIFT_S_ERROR("Failed to init mutex!");
|
||||
rift_s_tracker_destroy(t);
|
||||
free(t);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ret = os_mutex_init(&t->slam_mutex);
|
||||
if (ret != 0) {
|
||||
os_mutex_destroy(&t->mutex);
|
||||
free(t);
|
||||
return NULL;
|
||||
}
|
||||
rift_s_slam_guard_init(&t->slam_guard);
|
||||
t->slam_guard.require_relocalization = false; // Set from the saved map below; no map means a fresh world.
|
||||
t->world_command_id = os_monotonic_get_ns();
|
||||
|
||||
// Compute IMU and camera device poses for get_tracked_pose relations
|
||||
math_pose_from_isometry(&hmd_config->imu_calibration.device_from_imu, &t->device_from_imu);
|
||||
|
||||
@@ -453,6 +496,7 @@ rift_s_tracker_create(struct xrt_tracking_origin *origin,
|
||||
|
||||
// Initialize 3DoF tracker
|
||||
m_imu_3dof_init(&t->fusion.i3dof, M_IMU_3DOF_USE_GRAVITY_DUR_20MS);
|
||||
m_relation_history_create(&t->fusion.history);
|
||||
|
||||
t->pose.orientation.w = 1.0f; // All other values set to zero by U_DEVICE_ALLOCATE (which calls U_CALLOC)
|
||||
|
||||
@@ -516,8 +560,44 @@ rift_s_tracker_create(struct xrt_tracking_origin *origin,
|
||||
entry_sinks = (struct xrt_slam_sinks){0};
|
||||
}
|
||||
|
||||
t->base.camera = rift_s_passthrough_create(&hmd_config->camera_calibration);
|
||||
t->slam_sinks = entry_sinks;
|
||||
t->handtracker = hand_device;
|
||||
struct rift_s_world_anchor_config world_config = {.camera_count = RIFT_S_CAMERA_COUNT,
|
||||
.require_boundary = true};
|
||||
struct xrt_pose imu_from_device;
|
||||
math_pose_invert(&t->device_from_imu, &imu_from_device);
|
||||
for (int eye = 0; eye < RIFT_S_CAMERA_COUNT; eye++) {
|
||||
world_config.camera[eye] = hmd_config->camera_calibration.cameras[CAM_IDX_TO_ID[eye]];
|
||||
struct xrt_pose device_from_camera;
|
||||
math_pose_from_isometry(&world_config.camera[eye].device_from_camera, &device_from_camera);
|
||||
math_pose_transform(&imu_from_device, &device_from_camera, &world_config.imu_from_camera[eye]);
|
||||
}
|
||||
char map_path[1024];
|
||||
const char *bench = getenv("MONADO_STEAMVR_BENCH");
|
||||
const char *bench_map = getenv("RIFT_S_WORLD_BENCH_MAP");
|
||||
if (bench && strcmp(bench, "1") == 0 && bench_map && bench_map[0] == '/') {
|
||||
// Offline test maps use their capture's SLAM gauge, independent of the live boundary.
|
||||
world_config.require_boundary = false;
|
||||
t->world_anchor = rift_s_world_anchor_create(&world_config, bench_map);
|
||||
RIFT_S_INFO("World bench map override active");
|
||||
} else if (u_file_get_path_in_config_dir("rift-s-world.bin", map_path, sizeof(map_path)) > 0) {
|
||||
t->world_anchor = rift_s_world_anchor_create(&world_config, map_path);
|
||||
}
|
||||
t->world_map_available = rift_s_world_anchor_has_map(t->world_anchor);
|
||||
t->slam_guard.require_relocalization =
|
||||
t->world_map_available || rift_s_world_anchor_has_boundary(t->world_anchor);
|
||||
if (t->world_anchor && os_thread_helper_init(&t->world_thread) == 0) {
|
||||
t->world_thread_initialized = true;
|
||||
if (os_thread_helper_start(&t->world_thread, rift_s_world_thread, t) != 0) {
|
||||
os_thread_helper_destroy(&t->world_thread);
|
||||
t->world_thread_initialized = false;
|
||||
}
|
||||
}
|
||||
RIFT_S_INFO("World worker started=%d map_available=%d require_relocalization=%d", t->world_thread_initialized,
|
||||
t->world_map_available, t->slam_guard.require_relocalization);
|
||||
if (t->slam_guard.require_relocalization && !t->world_map_available)
|
||||
RIFT_S_WARN("Saved boundary has no compatible visual map; room setup is required to replace its frame");
|
||||
|
||||
return t;
|
||||
}
|
||||
@@ -525,9 +605,17 @@ rift_s_tracker_create(struct xrt_tracking_origin *origin,
|
||||
void
|
||||
rift_s_tracker_destroy(struct rift_s_tracker *t)
|
||||
{
|
||||
if (t->world_thread_initialized)
|
||||
os_thread_helper_destroy(&t->world_thread);
|
||||
for (int eye = 0; eye < RIFT_S_CAMERA_COUNT; eye++)
|
||||
xrt_frame_reference(&t->world_frames[eye], NULL);
|
||||
rift_s_world_anchor_destroy(t->world_anchor);
|
||||
rift_s_passthrough_destroy(t->base.camera);
|
||||
t_stereo_camera_calibration_reference(&t->stereo_calib, NULL);
|
||||
|
||||
m_imu_3dof_close(&t->fusion.i3dof);
|
||||
m_relation_history_destroy(&t->fusion.history);
|
||||
os_mutex_destroy(&t->slam_mutex);
|
||||
os_mutex_destroy(&t->mutex);
|
||||
}
|
||||
|
||||
@@ -582,6 +670,47 @@ clock_hw2mono_get(struct rift_s_tracker *t, uint64_t device_ts, timepoint_ns *ou
|
||||
*out = t->hw2mono + device_ts;
|
||||
}
|
||||
|
||||
/* Three missing 30 Hz SLAM frames are enough to stop submitting IMU before
|
||||
* Basalt's bounded IMU queue fills. Caller holds mutex. */
|
||||
#define RIFT_S_CAMERA_TIMEOUT_NS (100 * U_TIME_1MS_IN_NS)
|
||||
|
||||
static void
|
||||
pause_slam_locked(struct rift_s_tracker *t)
|
||||
{
|
||||
if (t->slam_paused) {
|
||||
return;
|
||||
}
|
||||
t->slam_paused = true;
|
||||
t->slam_restart_pending = t->tracking.slam_enabled && t->last_camera_arrival_ns != 0;
|
||||
// SLAM is realigned onto the held pose once it is consistent again.
|
||||
rift_s_slam_guard_suspend(&t->slam_guard, &t->pose.orientation);
|
||||
RIFT_S_INFO("Head tracking paused (%s); holding last pose", t->standby ? "standby" : "camera timeout");
|
||||
}
|
||||
|
||||
static void
|
||||
check_camera_timeout_locked(struct rift_s_tracker *t, timepoint_ns now_ns)
|
||||
{
|
||||
if (t->standby ||
|
||||
(t->last_camera_arrival_ns != 0 && now_ns - t->last_camera_arrival_ns > RIFT_S_CAMERA_TIMEOUT_NS)) {
|
||||
pause_slam_locked(t);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
rift_s_tracker_set_standby(struct rift_s_tracker *t, bool standby)
|
||||
{
|
||||
os_mutex_lock(&t->mutex);
|
||||
if (standby != t->standby) {
|
||||
t->standby = standby;
|
||||
if (standby) {
|
||||
pause_slam_locked(t);
|
||||
} else {
|
||||
RIFT_S_INFO("Headset awake; waiting for fresh camera frames");
|
||||
}
|
||||
}
|
||||
os_mutex_unlock(&t->mutex);
|
||||
}
|
||||
|
||||
void
|
||||
rift_s_tracker_imu_update(struct rift_s_tracker *t,
|
||||
uint64_t device_timestamp_ns,
|
||||
@@ -590,6 +719,8 @@ rift_s_tracker_imu_update(struct rift_s_tracker *t,
|
||||
{
|
||||
os_mutex_lock(&t->mutex);
|
||||
|
||||
check_camera_timeout_locked(t, os_monotonic_get_ns());
|
||||
|
||||
/* Ignore packets before we're ready and clock is stable */
|
||||
if (!t->ready_for_data || !t->have_hw2mono || t->last_frame_time == 0) {
|
||||
os_mutex_unlock(&t->mutex);
|
||||
@@ -613,15 +744,36 @@ rift_s_tracker_imu_update(struct rift_s_tracker *t,
|
||||
(double)(device_timestamp_ns - t->fusion.last_imu_timestamp_ns) / 1000000000.0, local_timestamp_ns,
|
||||
(double)(local_timestamp_ns - t->fusion.last_imu_local_timestamp_ns) / 1000000000.0, t->hw2mono);
|
||||
|
||||
if (local_timestamp_ns / U_TIME_1S_IN_NS != t->fusion.last_imu_local_timestamp_ns / U_TIME_1S_IN_NS) {
|
||||
RIFT_S_TRACE("HMD IMU accel=%f,%f,%f fusion=%f,%f,%f,%f", accel->x, accel->y, accel->z,
|
||||
t->fusion.i3dof.rot.x, t->fusion.i3dof.rot.y, t->fusion.i3dof.rot.z,
|
||||
t->fusion.i3dof.rot.w);
|
||||
}
|
||||
|
||||
uint64_t sample_generation = t->slam_generation;
|
||||
t->fusion.last_angular_velocity = *gyro;
|
||||
t->fusion.last_imu_timestamp_ns = device_timestamp_ns;
|
||||
t->fusion.last_imu_local_timestamp_ns = local_timestamp_ns;
|
||||
|
||||
t->pose.orientation = t->fusion.i3dof.rot;
|
||||
struct xrt_space_relation fusion_relation = XRT_SPACE_RELATION_ZERO;
|
||||
fusion_relation.pose.orientation = t->pose.orientation;
|
||||
math_quat_rotate_derivative(&t->pose.orientation, gyro, &fusion_relation.angular_velocity);
|
||||
fusion_relation.relation_flags = XRT_SPACE_RELATION_ORIENTATION_VALID_BIT |
|
||||
XRT_SPACE_RELATION_ORIENTATION_TRACKED_BIT |
|
||||
XRT_SPACE_RELATION_ANGULAR_VELOCITY_VALID_BIT;
|
||||
if (t->fusion.history) m_relation_history_push(t->fusion.history, &fusion_relation, local_timestamp_ns);
|
||||
|
||||
os_mutex_unlock(&t->mutex);
|
||||
|
||||
if (t->slam_sinks.imu) {
|
||||
// A delayed old-generation IMU must never enter the replacement backend. Do not
|
||||
// stall HID ingestion while the camera thread is replacing a session.
|
||||
if (os_mutex_trylock(&t->slam_mutex) != 0)
|
||||
return;
|
||||
os_mutex_lock(&t->mutex);
|
||||
bool submit = !t->standby && !t->slam_paused && sample_generation == t->slam_generation;
|
||||
os_mutex_unlock(&t->mutex);
|
||||
if (submit && t->slam_sinks.imu) {
|
||||
/* Push IMU sample to the SLAM tracker */
|
||||
struct xrt_vec3_f64 accel64 = {accel->x, accel->y, accel->z};
|
||||
struct xrt_vec3_f64 gyro64 = {gyro->x, gyro->y, gyro->z};
|
||||
@@ -630,6 +782,7 @@ rift_s_tracker_imu_update(struct rift_s_tracker *t,
|
||||
|
||||
xrt_sink_push_imu(t->slam_sinks.imu, &sample);
|
||||
}
|
||||
os_mutex_unlock(&t->slam_mutex);
|
||||
}
|
||||
|
||||
#define UPPER_32BITS(x) ((x) & 0xffffffff00000000ULL)
|
||||
@@ -663,40 +816,103 @@ rift_s_tracker_push_slam_frames(struct rift_s_tracker *t,
|
||||
uint64_t frame_ts_ns,
|
||||
struct xrt_frame *frames[RIFT_S_CAMERA_COUNT])
|
||||
{
|
||||
|
||||
os_mutex_lock(&t->slam_mutex);
|
||||
os_mutex_lock(&t->mutex);
|
||||
|
||||
/* Ignore packets before we're ready */
|
||||
if (!t->ready_for_data) {
|
||||
/* Ignore packets before we're ready, and drop any frames before we have IMU */
|
||||
if (!t->ready_for_data || !t->have_hw2mono) {
|
||||
os_mutex_unlock(&t->mutex);
|
||||
os_mutex_unlock(&t->slam_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!t->have_hw2mono) {
|
||||
/* Drop any frames before we have IMU */
|
||||
os_mutex_unlock(&t->mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
timepoint_ns now_ns = os_monotonic_get_ns();
|
||||
timepoint_ns frame_mono_ns = raw_frame_ts_to_mono_ts(t, frame_ts_ns);
|
||||
if (frame_mono_ns < t->last_frame_time) {
|
||||
RIFT_S_WARN("Camera frame time went backward by %" PRId64 " ns", frame_mono_ns - t->last_frame_time);
|
||||
os_mutex_unlock(&t->mutex);
|
||||
os_mutex_unlock(&t->slam_mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
RIFT_S_TRACE("SLAM frame timestamp %" PRIu64 " local %" PRIu64, frame_ts_ns, frame_mono_ns);
|
||||
|
||||
t->last_frame_time = frame_mono_ns;
|
||||
|
||||
/* Queued pre-standby frames must not wake tracking. */
|
||||
bool fresh =
|
||||
now_ns - frame_mono_ns <= RIFT_S_CAMERA_TIMEOUT_NS && frame_mono_ns - now_ns <= RIFT_S_CAMERA_TIMEOUT_NS;
|
||||
bool submit = fresh && !t->standby;
|
||||
bool restart = submit && t->tracking.slam_enabled && t->slam_restart_pending;
|
||||
if (restart) {
|
||||
t->slam_generation++;
|
||||
t->slam_min_sample_ns = frame_mono_ns;
|
||||
t->slam_guard.world_valid = false;
|
||||
t->slam_guard.boundary_confirmed = false;
|
||||
t->slam_guard.world_transition = false;
|
||||
}
|
||||
os_mutex_unlock(&t->mutex);
|
||||
|
||||
for (int i = 0; i < RIFT_S_CAMERA_COUNT; i++) {
|
||||
if (t->slam_sinks.cams[i]) {
|
||||
frames[i]->timestamp = frame_mono_ns;
|
||||
xrt_sink_push_frame(t->slam_sinks.cams[i], frames[i]);
|
||||
frames[i]->timestamp = frame_mono_ns;
|
||||
}
|
||||
rift_s_passthrough_push(t->base.camera, frames[0], frames[1]);
|
||||
if (fresh && t->world_thread_initialized && frame_mono_ns - t->world_last_queued_ns >= 200 * U_TIME_1MS_IN_NS) {
|
||||
os_thread_helper_lock(&t->world_thread);
|
||||
for (int eye = 0; eye < RIFT_S_CAMERA_COUNT; eye++)
|
||||
xrt_frame_reference(&t->world_frames[eye], frames[eye]);
|
||||
t->world_last_queued_ns = frame_mono_ns;
|
||||
os_thread_helper_unlock(&t->world_thread);
|
||||
}
|
||||
|
||||
if (restart) {
|
||||
/* Safety-patched Basalt advertises recreation support. Replacement clears flow,
|
||||
* estimator and queues; unknown backends keep the estimator-reset fallback. */
|
||||
int ret = -1;
|
||||
const char *how = "none";
|
||||
timepoint_ns restart_begin_ns = os_monotonic_get_ns();
|
||||
#ifdef XRT_FEATURE_SLAM
|
||||
if (t->tracking.slam->restart != NULL && t->tracking.slam->restart(t->tracking.slam) == 0) {
|
||||
ret = 0;
|
||||
how = "recreate";
|
||||
} else if (t_slam_reset(t->tracking.slam) == 0) {
|
||||
ret = 0;
|
||||
how = "reset";
|
||||
}
|
||||
#endif
|
||||
os_mutex_lock(&t->mutex);
|
||||
t->slam_restart_pending = false;
|
||||
/* Samples from before the restart belong to the old session. */
|
||||
t->slam_guard.stable_samples = 0;
|
||||
if (ret == 0)
|
||||
rift_s_slam_guard_backend_started(&t->slam_guard, os_monotonic_get_ns());
|
||||
t->feature_gate = (struct rift_s_slam_feature_gate){0};
|
||||
os_mutex_unlock(&t->mutex);
|
||||
if (ret == 0) {
|
||||
RIFT_S_INFO("SLAM session restarted method=%s elapsed_ms=%.3f; waiting for consistent poses",
|
||||
how, (os_monotonic_get_ns() - restart_begin_ns) / 1e6);
|
||||
} else {
|
||||
RIFT_S_WARN("Unable to restart SLAM; the guard keeps holding until its poses are consistent");
|
||||
}
|
||||
}
|
||||
|
||||
if (submit) {
|
||||
os_mutex_lock(&t->mutex);
|
||||
/* Proximity may change while the backend is restarting. */
|
||||
submit = !t->standby;
|
||||
if (submit) {
|
||||
t->last_camera_arrival_ns = os_monotonic_get_ns();
|
||||
t->slam_paused = false;
|
||||
}
|
||||
os_mutex_unlock(&t->mutex);
|
||||
}
|
||||
|
||||
if (submit) {
|
||||
for (int i = 0; i < RIFT_S_CAMERA_COUNT; i++) {
|
||||
if (t->slam_sinks.cams[i]) {
|
||||
xrt_sink_push_frame(t->slam_sinks.cams[i], frames[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
os_mutex_unlock(&t->slam_mutex);
|
||||
}
|
||||
|
||||
struct t_constellation_tracked_device_connection *
|
||||
@@ -728,9 +944,17 @@ rift_s_tracker_push_controller_frameset(struct rift_s_tracker *t, uint64_t frame
|
||||
}
|
||||
|
||||
timepoint_ns frame_mono_ns = raw_frame_ts_to_mono_ts(t, frame_ts_ns);
|
||||
bool usable = !t->standby && !t->slam_paused &&
|
||||
(timepoint_ns)os_monotonic_get_ns() - frame_mono_ns <= RIFT_S_CAMERA_TIMEOUT_NS;
|
||||
os_mutex_unlock(&t->mutex);
|
||||
if (!usable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (t->controller_sink) {
|
||||
RIFT_S_TRACE("Controller frame seq=%" PRIu64 " hw=%" PRIu64 " mono=%" PRIu64 " age_ms=%.3f",
|
||||
frameset->source_sequence, frame_ts_ns, frame_mono_ns,
|
||||
(double)((int64_t)os_monotonic_get_ns() - frame_mono_ns) / U_TIME_1MS_IN_NS);
|
||||
frameset->timestamp = frame_mono_ns;
|
||||
xrt_sink_push_frame(t->controller_sink, frameset);
|
||||
}
|
||||
@@ -771,46 +995,447 @@ rift_s_tracker_get_tracked_pose(struct rift_s_tracker *t,
|
||||
struct xrt_relation_chain xrc = {0};
|
||||
|
||||
if (pose == RIFT_S_TRACKER_POSE_DEVICE) {
|
||||
m_relation_chain_push_pose(&xrc, &t->device_from_imu);
|
||||
m_relation_chain_push_inverted_pose_if_not_identity(&xrc, &t->device_from_imu);
|
||||
} else if (pose == RIFT_S_TRACKER_POSE_LEFT_CAMERA) {
|
||||
m_relation_chain_push_pose(&xrc, &t->left_cam_from_imu);
|
||||
m_relation_chain_push_inverted_pose_if_not_identity(&xrc, &t->left_cam_from_imu);
|
||||
}
|
||||
|
||||
if (t->tracking.slam_enabled && t->slam_over_3dof) {
|
||||
struct xrt_space_relation imu_relation = XRT_SPACE_RELATION_ZERO;
|
||||
// Off-head (proximity) or without camera frames, SLAM input is stopped and the head pose is held.
|
||||
os_mutex_lock(&t->mutex);
|
||||
check_camera_timeout_locked(t, os_monotonic_get_ns());
|
||||
bool query_slam = t->tracking.slam_enabled && t->slam_over_3dof;
|
||||
bool paused = t->tracking.slam_enabled && (t->standby || t->slam_paused || t->slam_restart_pending);
|
||||
uint64_t sample_generation = t->slam_generation;
|
||||
os_mutex_unlock(&t->mutex);
|
||||
|
||||
// Get the IMU pose from the SLAM tracker
|
||||
xrt_tracked_slam_get_tracked_pose(t->tracking.slam, at_timestamp_ns, &imu_relation);
|
||||
// Basalt can diverge (tens of metres to ~1e7 m away within seconds, seen with the headset left idle).
|
||||
// The guard publishes SLAM only while it stays near the origin and moves like a head; otherwise it
|
||||
// falls back to the 3DoF orientation at the last good position and asks for a SLAM re-initialisation.
|
||||
struct xrt_space_relation slam_relation = XRT_SPACE_RELATION_ZERO;
|
||||
struct xrt_space_relation query_relation = XRT_SPACE_RELATION_ZERO;
|
||||
timepoint_ns sample_ts = 0;
|
||||
bool slam_valid = false;
|
||||
bool features_known = false;
|
||||
int feature_count = 0;
|
||||
timepoint_ns feature_ts = 0;
|
||||
if (query_slam && !paused) {
|
||||
#ifdef XRT_FEATURE_SLAM
|
||||
slam_valid = t_slam_get_latest_sample(t->tracking.slam, at_timestamp_ns, &sample_ts,
|
||||
&slam_relation, &query_relation) &&
|
||||
(slam_relation.relation_flags & XRT_SPACE_RELATION_POSITION_VALID_BIT) != 0;
|
||||
timepoint_ns now_ns = os_monotonic_get_ns();
|
||||
slam_valid = slam_valid && sample_ts <= now_ns && now_ns - sample_ts < 250 * U_TIME_1MS_IN_NS;
|
||||
// !todo Correct pose depending on the VIT system in use, this should be done in the system itself.
|
||||
// For now, assume that we are using Basalt.
|
||||
rift_s_tracker_correct_pose_from_basalt(&imu_relation.pose);
|
||||
rift_s_tracker_correct_pose_from_basalt(&slam_relation.pose);
|
||||
rift_s_tracker_correct_pose_from_basalt(&query_relation.pose);
|
||||
struct xrt_quat basalt_to_xr = {0.70710678, 0, 0, -0.70710678};
|
||||
math_quat_rotate_vec3(&basalt_to_xr, &slam_relation.linear_velocity, &slam_relation.linear_velocity);
|
||||
math_quat_rotate_vec3(&basalt_to_xr, &slam_relation.angular_velocity, &slam_relation.angular_velocity);
|
||||
math_quat_rotate_vec3(&basalt_to_xr, &query_relation.linear_velocity, &query_relation.linear_velocity);
|
||||
math_quat_rotate_vec3(&basalt_to_xr, &query_relation.angular_velocity, &query_relation.angular_velocity);
|
||||
features_known = t_slam_get_feature_count(t->tracking.slam, &feature_count, &feature_ts);
|
||||
#endif
|
||||
imu_relation.relation_flags = (enum xrt_space_relation_flags)(
|
||||
XRT_SPACE_RELATION_ORIENTATION_VALID_BIT | XRT_SPACE_RELATION_POSITION_VALID_BIT |
|
||||
XRT_SPACE_RELATION_ORIENTATION_TRACKED_BIT | XRT_SPACE_RELATION_POSITION_TRACKED_BIT);
|
||||
|
||||
m_relation_chain_push_relation(&xrc, &imu_relation);
|
||||
} else {
|
||||
struct xrt_space_relation imu_relation = XRT_SPACE_RELATION_ZERO;
|
||||
|
||||
os_mutex_lock(&t->mutex);
|
||||
// TODO: Estimate pose at timestamp at_timestamp_ns
|
||||
math_quat_normalize(&t->pose.orientation);
|
||||
imu_relation.pose = t->pose;
|
||||
imu_relation.angular_velocity = t->fusion.last_angular_velocity;
|
||||
imu_relation.relation_flags = (enum xrt_space_relation_flags)(
|
||||
XRT_SPACE_RELATION_ORIENTATION_VALID_BIT | XRT_SPACE_RELATION_POSITION_VALID_BIT |
|
||||
XRT_SPACE_RELATION_ORIENTATION_TRACKED_BIT);
|
||||
|
||||
m_relation_chain_push_relation(&xrc, &imu_relation);
|
||||
|
||||
os_mutex_unlock(&t->mutex);
|
||||
}
|
||||
|
||||
// Without any good SLAM position, put the head at eye height above the floor. The tracking origin
|
||||
// may already carry that height as its offset (the Rift S builder sets 1.6 m).
|
||||
struct xrt_vec3 no_pose_position = rift_s_slam_guard_no_pose_position(
|
||||
t->base.tracking_origin != NULL ? t->base.tracking_origin->initial_offset.position.y : 0.0f);
|
||||
|
||||
struct xrt_space_relation imu_relation = XRT_SPACE_RELATION_ZERO;
|
||||
const enum xrt_space_relation_flags held_flags = (enum xrt_space_relation_flags)(
|
||||
XRT_SPACE_RELATION_ORIENTATION_VALID_BIT | XRT_SPACE_RELATION_POSITION_VALID_BIT);
|
||||
|
||||
os_mutex_lock(&t->mutex);
|
||||
math_quat_normalize(&t->pose.orientation);
|
||||
paused = paused || t->standby || t->slam_paused || t->slam_restart_pending ||
|
||||
sample_generation != t->slam_generation;
|
||||
slam_valid = slam_valid && sample_ts >= t->slam_min_sample_ns;
|
||||
struct xrt_quat sample_fusion = t->pose.orientation;
|
||||
struct xrt_quat query_fusion = t->pose.orientation;
|
||||
if (t->fusion.history) {
|
||||
struct xrt_space_relation queried;
|
||||
if (m_relation_history_get(t->fusion.history, at_timestamp_ns, &queried) != M_RELATION_HISTORY_RESULT_INVALID)
|
||||
query_fusion = queried.pose.orientation;
|
||||
if (m_relation_history_get(t->fusion.history, sample_ts, &queried) != M_RELATION_HISTORY_RESULT_INVALID)
|
||||
sample_fusion = queried.pose.orientation;
|
||||
}
|
||||
struct rift_s_slam_guard *g = &t->slam_guard;
|
||||
uint32_t divergences = g->divergences;
|
||||
uint32_t recoveries = g->recoveries;
|
||||
enum rift_s_slam_guard_state previous_state = g->state;
|
||||
uint32_t resets = g->resets;
|
||||
int64_t previous_good_ns = g->good_ns;
|
||||
enum rift_s_slam_guard_reason reason = RIFT_S_SLAM_GUARD_OK;
|
||||
|
||||
enum rift_s_slam_feature_event feature_event = RIFT_S_SLAM_FEATURES_NO_CHANGE;
|
||||
if (query_slam && !paused) {
|
||||
feature_event = rift_s_slam_feature_gate_update(&t->feature_gate, features_known, feature_count,
|
||||
(int64_t)feature_ts);
|
||||
if (feature_event == RIFT_S_SLAM_FEATURES_LOST) {
|
||||
rift_s_slam_guard_suspend(g, &t->pose.orientation);
|
||||
} else if (feature_event == RIFT_S_SLAM_FEATURES_RETURNED) {
|
||||
t->slam_restart_pending = true;
|
||||
}
|
||||
}
|
||||
bool hold = paused || (query_slam && t->feature_gate.lost);
|
||||
if (query_slam && !paused && t->feature_gate.lost) {
|
||||
// Keep the health clock running through a feature hold without accepting new world poses.
|
||||
if (slam_valid && rift_s_slam_guard_pose_usable(&slam_relation.pose, &sample_fusion)) {
|
||||
g->invalid_since_ns = 0;
|
||||
} else {
|
||||
struct xrt_pose ignored;
|
||||
rift_s_slam_guard_update(g, slam_valid, &slam_relation.pose,
|
||||
slam_valid ? sample_ts : os_monotonic_get_ns(), &sample_fusion,
|
||||
&no_pose_position, &ignored, &reason);
|
||||
if (rift_s_slam_guard_take_reset(g, os_monotonic_get_ns()))
|
||||
t->slam_restart_pending = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hold) {
|
||||
// Freeze the head where it was last shown: untracked, no motion.
|
||||
if (!t->have_published_imu_pose) {
|
||||
rift_s_slam_guard_fallback_pose(g, &t->pose.orientation, &no_pose_position,
|
||||
&t->published_imu_pose);
|
||||
t->have_published_imu_pose = true;
|
||||
}
|
||||
imu_relation.pose = t->published_imu_pose;
|
||||
imu_relation.relation_flags = held_flags;
|
||||
} else {
|
||||
bool use_slam = false;
|
||||
if (query_slam) {
|
||||
use_slam = rift_s_slam_guard_update(g, slam_valid, &slam_relation.pose,
|
||||
slam_valid ? sample_ts : os_monotonic_get_ns(), &sample_fusion,
|
||||
&no_pose_position, &imu_relation.pose, &reason);
|
||||
if (rift_s_slam_guard_take_reset(g, os_monotonic_get_ns())) {
|
||||
t->slam_restart_pending = true;
|
||||
}
|
||||
} else {
|
||||
// TODO: Estimate pose at timestamp at_timestamp_ns
|
||||
rift_s_slam_guard_fallback_pose(g, &t->pose.orientation, &no_pose_position, &imu_relation.pose);
|
||||
}
|
||||
|
||||
if (use_slam && rift_s_slam_guard_query(g, &query_relation.pose, at_timestamp_ns, &query_fusion,
|
||||
&no_pose_position, &imu_relation.pose)) {
|
||||
math_quat_rotate_vec3(&g->align.orientation, &query_relation.linear_velocity,
|
||||
&imu_relation.linear_velocity);
|
||||
math_quat_rotate_vec3(&g->align.orientation, &query_relation.angular_velocity,
|
||||
&imu_relation.angular_velocity);
|
||||
imu_relation.relation_flags =
|
||||
(enum xrt_space_relation_flags)(held_flags | XRT_SPACE_RELATION_ORIENTATION_TRACKED_BIT |
|
||||
XRT_SPACE_RELATION_POSITION_TRACKED_BIT);
|
||||
} else {
|
||||
rift_s_slam_guard_fallback_pose(g, &query_fusion, &no_pose_position, &imu_relation.pose);
|
||||
imu_relation.angular_velocity = t->fusion.last_angular_velocity;
|
||||
imu_relation.relation_flags =
|
||||
(enum xrt_space_relation_flags)(held_flags | XRT_SPACE_RELATION_ORIENTATION_TRACKED_BIT);
|
||||
}
|
||||
if (pose == RIFT_S_TRACKER_POSE_DEVICE || !t->have_published_imu_pose) {
|
||||
t->published_imu_pose = imu_relation.pose;
|
||||
t->have_published_imu_pose = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (at_timestamp_ns < (uint64_t)t->slam_min_sample_ns)
|
||||
imu_relation.relation_flags = held_flags;
|
||||
|
||||
if (feature_event == RIFT_S_SLAM_FEATURES_LOST) {
|
||||
RIFT_S_WARN("SLAM cameras see %d features (< %d); holding head pose", feature_count,
|
||||
RIFT_S_SLAM_FEATURES_MIN);
|
||||
} else if (feature_event == RIFT_S_SLAM_FEATURES_RETURNED) {
|
||||
RIFT_S_INFO("SLAM cameras see %d features again; re-initialising SLAM", feature_count);
|
||||
}
|
||||
if (g->divergences != divergences) {
|
||||
RIFT_S_WARN(
|
||||
"SLAM diverged reason=%s distance_m=%.2f step_m=%.2f slam=(%.2f,%.2f,%.2f) "
|
||||
"held=(%.2f,%.2f,%.2f) query_ns=%lld previous_ns=%lld delta_ms=%.3f pose_kind=%d divergences=%u: 3DoF until SLAM is consistent again",
|
||||
rift_s_slam_guard_reason_str(reason), g->last_reject_distance_m, g->last_reject_step_m,
|
||||
slam_relation.pose.position.x, slam_relation.pose.position.y, slam_relation.pose.position.z,
|
||||
imu_relation.pose.position.x, imu_relation.pose.position.y, imu_relation.pose.position.z,
|
||||
(long long)at_timestamp_ns, (long long)previous_good_ns,
|
||||
((int64_t)at_timestamp_ns - previous_good_ns) / 1e6, pose, g->divergences);
|
||||
}
|
||||
if (g->recoveries != recoveries) {
|
||||
RIFT_S_INFO("SLAM recovered position=(%.2f,%.2f,%.2f) recoveries=%u resets=%u",
|
||||
imu_relation.pose.position.x, imu_relation.pose.position.y, imu_relation.pose.position.z,
|
||||
g->recoveries, g->resets);
|
||||
}
|
||||
if (previous_state != g->state || recoveries != g->recoveries || resets != g->resets) {
|
||||
float origin_y = t->base.tracking_origin ? t->base.tracking_origin->initial_offset.position.y : 0;
|
||||
RIFT_S_INFO("SLAM alignment state=%d->%d query_ns=%lld origin_y=%.6f nominal_floor_y=%.6f align_p=(%.6f,%.6f,%.6f) align_q=(%.6f,%.6f,%.6f,%.6f) raw_p=(%.6f,%.6f,%.6f) published_p=(%.6f,%.6f,%.6f) recoveries=%u resets=%u",
|
||||
previous_state, g->state, (long long)at_timestamp_ns, origin_y,
|
||||
no_pose_position.y - RIFT_S_SLAM_GUARD_EYE_HEIGHT_M,
|
||||
g->align.position.x, g->align.position.y, g->align.position.z,
|
||||
g->align.orientation.x, g->align.orientation.y, g->align.orientation.z, g->align.orientation.w,
|
||||
slam_relation.pose.position.x, slam_relation.pose.position.y, slam_relation.pose.position.z,
|
||||
imu_relation.pose.position.x, imu_relation.pose.position.y, imu_relation.pose.position.z,
|
||||
g->recoveries, g->resets);
|
||||
}
|
||||
timepoint_ns pose_log_now = os_monotonic_get_ns();
|
||||
if (pose == RIFT_S_TRACKER_POSE_DEVICE &&
|
||||
pose_log_now - t->last_published_pose_log_ns >= 5LL * U_TIME_1S_IN_NS) {
|
||||
t->last_published_pose_log_ns = pose_log_now;
|
||||
RIFT_S_INFO("HMD pose flags=0x%x paused=%d slam_valid=%d sample_age_ms=%.1f guard=%d "
|
||||
"boundary_confirmed=%d position=(%.3f,%.3f,%.3f)",
|
||||
imu_relation.relation_flags, hold, slam_valid,
|
||||
sample_ts ? (pose_log_now - sample_ts) / 1e6 : -1.0, g->state,
|
||||
g->boundary_confirmed, imu_relation.pose.position.x,
|
||||
imu_relation.pose.position.y, imu_relation.pose.position.z);
|
||||
}
|
||||
os_mutex_unlock(&t->mutex);
|
||||
|
||||
m_relation_chain_push_relation(&xrc, &imu_relation);
|
||||
m_relation_chain_resolve(&xrc, out_relation);
|
||||
}
|
||||
|
||||
// Run feature matching off the camera/pose threads. A single latest-frame pair bounds backlog.
|
||||
static float
|
||||
world_rotation_difference(const struct xrt_quat *a, const struct xrt_quat *b)
|
||||
{
|
||||
float dot = fabsf(a->x * b->x + a->y * b->y + a->z * b->z + a->w * b->w);
|
||||
return 2 * acosf(fminf(1, dot));
|
||||
}
|
||||
|
||||
static void
|
||||
world_write_status(struct rift_s_tracker *t)
|
||||
{
|
||||
os_mutex_lock(&t->mutex);
|
||||
const char *state = t->world_storage_failed ? "save_failed"
|
||||
: t->world_setup ? "setup"
|
||||
: (t->slam_guard.boundary_confirmed && !t->slam_guard.world_transition) ? "localized"
|
||||
: t->world_map_available ? "relocalizing"
|
||||
: "unanchored";
|
||||
os_mutex_unlock(&t->mutex);
|
||||
static timepoint_ns last_write_ns;
|
||||
static const char *last_state;
|
||||
timepoint_ns now_ns = os_monotonic_get_ns();
|
||||
if (state == last_state && now_ns - last_write_ns < 500 * U_TIME_1MS_IN_NS) return;
|
||||
last_write_ns = now_ns;
|
||||
last_state = state;
|
||||
char path[1024], temp[1050];
|
||||
rift_s_world_control_path("status", path, sizeof(path));
|
||||
snprintf(temp, sizeof(temp), "%s.tmp", path);
|
||||
FILE *file = fopen(temp, "w");
|
||||
if (!file) return;
|
||||
fprintf(file, "%lld %s\n", (long long)os_monotonic_get_ns(), state);
|
||||
if (fclose(file) == 0) rename(temp, path);
|
||||
}
|
||||
|
||||
static void
|
||||
world_read_command(struct rift_s_tracker *t)
|
||||
{
|
||||
char path[1024], command[16] = {0};
|
||||
rift_s_world_control_path("command", path, sizeof(path));
|
||||
FILE *file = fopen(path, "r");
|
||||
if (!file)
|
||||
return;
|
||||
unsigned long long id = 0;
|
||||
int count = fscanf(file, "%llu %15s", &id, command);
|
||||
fclose(file);
|
||||
if (count != 2 || id <= t->world_command_id)
|
||||
return;
|
||||
t->world_command_id = id;
|
||||
if (strcmp(command, "begin") == 0) {
|
||||
rift_s_world_anchor_clear(t->world_anchor);
|
||||
os_mutex_lock(&t->mutex);
|
||||
t->world_setup = true;
|
||||
t->slam_guard.boundary_confirmed = false;
|
||||
t->slam_guard.require_relocalization = false;
|
||||
t->world_confirmations = 0;
|
||||
os_mutex_unlock(&t->mutex);
|
||||
RIFT_S_INFO("New room setup may establish a world; saved anchors retained until Accept");
|
||||
} else if (strcmp(command, "save") == 0) {
|
||||
os_mutex_lock(&t->mutex);
|
||||
bool setup = t->world_setup;
|
||||
os_mutex_unlock(&t->mutex);
|
||||
if (!setup) {
|
||||
RIFT_S_WARN("Ignoring world save without a room setup begin");
|
||||
return;
|
||||
}
|
||||
bool saved = rift_s_world_anchor_save(t->world_anchor);
|
||||
bool requires_relocalization =
|
||||
rift_s_world_anchor_has_map(t->world_anchor) || rift_s_world_anchor_has_boundary(t->world_anchor);
|
||||
os_mutex_lock(&t->mutex);
|
||||
t->world_storage_failed = !saved;
|
||||
t->slam_guard.boundary_confirmed = saved && rift_s_world_anchor_has_map(t->world_anchor);
|
||||
t->world_setup = false;
|
||||
t->world_map_available = rift_s_world_anchor_has_map(t->world_anchor);
|
||||
t->slam_guard.require_relocalization = requires_relocalization;
|
||||
if (!saved || !t->world_map_available)
|
||||
t->slam_guard.boundary_confirmed = false;
|
||||
t->slam_guard.world_transition = false;
|
||||
os_mutex_unlock(&t->mutex);
|
||||
RIFT_S_INFO("Boundary world anchors saved=%d map_available=%d", saved, t->world_map_available);
|
||||
} else if (strcmp(command, "cancel") == 0) {
|
||||
rift_s_world_anchor_reload(t->world_anchor);
|
||||
bool requires_relocalization =
|
||||
rift_s_world_anchor_has_map(t->world_anchor) || rift_s_world_anchor_has_boundary(t->world_anchor);
|
||||
os_mutex_lock(&t->mutex);
|
||||
if (t->world_setup) {
|
||||
t->slam_guard.world_valid = false;
|
||||
t->slam_guard.boundary_confirmed = false;
|
||||
t->slam_guard.world_transition = false;
|
||||
rift_s_slam_guard_suspend(&t->slam_guard, &t->pose.orientation);
|
||||
}
|
||||
t->world_setup = false;
|
||||
t->world_map_available = rift_s_world_anchor_has_map(t->world_anchor);
|
||||
t->slam_guard.require_relocalization = requires_relocalization;
|
||||
t->world_confirmations = 0;
|
||||
os_mutex_unlock(&t->mutex);
|
||||
}
|
||||
}
|
||||
|
||||
static void *
|
||||
rift_s_world_thread(void *ptr)
|
||||
{
|
||||
struct rift_s_tracker *t = ptr;
|
||||
timepoint_ns last_attempt_log_ns = 0;
|
||||
uint64_t attempts = 0, usable = 0;
|
||||
os_thread_helper_lock(&t->world_thread);
|
||||
while (os_thread_helper_is_running_locked(&t->world_thread)) {
|
||||
struct xrt_frame *frames[RIFT_S_CAMERA_COUNT] = {0};
|
||||
for (int eye = 0; eye < RIFT_S_CAMERA_COUNT; eye++) {
|
||||
frames[eye] = t->world_frames[eye];
|
||||
t->world_frames[eye] = NULL;
|
||||
}
|
||||
os_thread_helper_unlock(&t->world_thread);
|
||||
world_read_command(t);
|
||||
if (frames[0] && frames[1] && t->tracking.slam_enabled) {
|
||||
timepoint_ns capture_ns = frames[0]->timestamp;
|
||||
struct xrt_space_relation source = XRT_SPACE_RELATION_ZERO, capture = XRT_SPACE_RELATION_ZERO;
|
||||
timepoint_ns source_ns = 0;
|
||||
os_mutex_lock(&t->mutex);
|
||||
uint64_t generation = t->slam_generation;
|
||||
os_mutex_unlock(&t->mutex);
|
||||
bool valid = false;
|
||||
#ifdef XRT_FEATURE_SLAM
|
||||
// Wait briefly for this exposure's SLAM sample, rather than triangulate against a prediction.
|
||||
for (int attempt = 0; attempt < 12; attempt++) {
|
||||
valid = t_slam_get_latest_sample(t->tracking.slam, capture_ns, &source_ns, &source,
|
||||
&capture);
|
||||
if (valid && source_ns >= capture_ns)
|
||||
break;
|
||||
os_nanosleep(10 * U_TIME_1MS_IN_NS);
|
||||
}
|
||||
rift_s_tracker_correct_pose_from_basalt(&capture.pose);
|
||||
#endif
|
||||
os_mutex_lock(&t->mutex);
|
||||
struct xrt_quat fusion = t->pose.orientation;
|
||||
struct xrt_space_relation fusion_capture;
|
||||
if (m_relation_history_get(t->fusion.history, capture_ns, &fusion_capture) !=
|
||||
M_RELATION_HISTORY_RESULT_INVALID)
|
||||
fusion = fusion_capture.pose.orientation;
|
||||
valid = valid && source_ns >= capture_ns && source_ns - capture_ns <= 150 * U_TIME_1MS_IN_NS &&
|
||||
(capture.relation_flags & XRT_SPACE_RELATION_POSITION_VALID_BIT) &&
|
||||
capture_ns >= t->slam_min_sample_ns && generation == t->slam_generation &&
|
||||
!t->standby && !t->slam_paused && !t->slam_restart_pending &&
|
||||
rift_s_slam_guard_pose_usable(&capture.pose, &fusion);
|
||||
bool standby = t->standby;
|
||||
struct xrt_pose world_capture;
|
||||
struct xrt_vec3 no_pose_position = rift_s_slam_guard_no_pose_position(
|
||||
t->base.tracking_origin ? t->base.tracking_origin->initial_offset.position.y : 0.0f);
|
||||
// Recording must pass the same displacement/gravity checks as a displayed pose.
|
||||
bool record = valid && (t->world_setup || t->slam_guard.boundary_confirmed) &&
|
||||
!t->slam_guard.world_transition &&
|
||||
rift_s_slam_guard_query(&t->slam_guard, &capture.pose, capture_ns, &fusion,
|
||||
&no_pose_position, &world_capture);
|
||||
os_mutex_unlock(&t->mutex);
|
||||
rift_s_world_anchor_record_rig_frames(t->world_anchor, (const struct xrt_frame *const *)frames,
|
||||
RIFT_S_CAMERA_COUNT, valid ? &capture.pose : NULL);
|
||||
struct rift_s_world_anchor_result result = {0};
|
||||
bool processed = (record || rift_s_world_anchor_has_map(t->world_anchor)) &&
|
||||
rift_s_world_anchor_process_rig_gravity(
|
||||
t->world_anchor, (const struct xrt_frame *const *)frames,
|
||||
RIFT_S_CAMERA_COUNT, record ? &world_capture : NULL,
|
||||
valid ? &capture.pose.orientation : NULL, &result);
|
||||
attempts++;
|
||||
usable += valid;
|
||||
if (capture_ns - last_attempt_log_ns >= U_TIME_1S_IN_NS) {
|
||||
last_attempt_log_ns = capture_ns;
|
||||
RIFT_S_INFO("World attempt=%" PRIu64 " usable=%" PRIu64
|
||||
" pose_valid=%d sample_gap_ms=%.1f"
|
||||
" standby=%d keyframes=%u features=%u/%u/%u/%u/%u matches=%u/%u/%u/%u/%u "
|
||||
"inliers=%u/%u/%u/%u/%u"
|
||||
" points_by_camera=%u/%u/%u/%u/%u support=%u stereo=%u triangulated=%u "
|
||||
"error_px=%.3f record=%d matched=%d confirmations=%u rejection=%s",
|
||||
attempts, usable, valid,
|
||||
source_ns > 0 ? (double)(source_ns - capture_ns) / U_TIME_1MS_IN_NS : -1.0,
|
||||
standby, result.keyframes, result.features[0], result.features[1],
|
||||
result.features[2], result.features[3], result.features[4],
|
||||
result.matches[0], result.matches[1], result.matches[2], result.matches[3],
|
||||
result.matches[4], result.inliers[0], result.inliers[1], result.inliers[2],
|
||||
result.inliers[3], result.inliers[4], result.camera_points[0],
|
||||
result.camera_points[1], result.camera_points[2], result.camera_points[3],
|
||||
result.camera_points[4], result.supporting_cameras, result.stereo_matches,
|
||||
result.triangulated, result.max_reprojection_px, record, result.relocalized,
|
||||
t->world_confirmations, result.rejection ? result.rejection : "none");
|
||||
}
|
||||
if (processed && result.recorded) {
|
||||
os_mutex_lock(&t->mutex);
|
||||
bool setup = t->world_setup;
|
||||
t->world_map_available = true;
|
||||
os_mutex_unlock(&t->mutex);
|
||||
if (!setup) {
|
||||
bool saved = rift_s_world_anchor_save(t->world_anchor);
|
||||
os_mutex_lock(&t->mutex);
|
||||
t->world_storage_failed = !saved;
|
||||
os_mutex_unlock(&t->mutex);
|
||||
}
|
||||
RIFT_S_INFO("World keyframe recorded count=%u", result.keyframes);
|
||||
}
|
||||
if (processed && result.relocalized && valid && !record) {
|
||||
struct xrt_pose inverse, candidate;
|
||||
math_pose_invert(&capture.pose, &inverse);
|
||||
math_pose_transform(&result.world_from_imu, &inverse, &candidate);
|
||||
struct xrt_vec3 up = {0, 1, 0}, world_up;
|
||||
math_quat_rotate_vec3(&candidate.orientation, &up, &world_up);
|
||||
os_mutex_lock(&t->mutex);
|
||||
if (generation == t->slam_generation && !t->standby && !t->slam_restart_pending &&
|
||||
world_up.y > 0.9961947f) {
|
||||
rift_s_slam_guard_yaw_of(&candidate.orientation, &candidate.orientation);
|
||||
struct xrt_vec3 rotated;
|
||||
math_quat_rotate_vec3(&candidate.orientation, &capture.pose.position, &rotated);
|
||||
candidate.position = m_vec3_sub(result.world_from_imu.position, rotated);
|
||||
bool consistent =
|
||||
t->world_confirmations > 0 && t->world_candidate_generation == generation &&
|
||||
capture_ns > t->world_candidate_ns &&
|
||||
capture_ns - t->world_candidate_ns <= 3LL * U_TIME_1S_IN_NS &&
|
||||
m_vec3_len(m_vec3_sub(candidate.position, t->world_candidate.position)) <
|
||||
0.04f &&
|
||||
world_rotation_difference(&candidate.orientation,
|
||||
&t->world_candidate.orientation) < DEG_TO_RAD(3);
|
||||
t->world_confirmations = consistent ? t->world_confirmations + 1 : 1;
|
||||
t->world_candidate = candidate;
|
||||
t->world_candidate_generation = generation;
|
||||
t->world_candidate_ns = capture_ns;
|
||||
if (t->world_confirmations >= 3 && !t->slam_guard.boundary_confirmed) {
|
||||
rift_s_slam_guard_set_world(&t->slam_guard, &candidate);
|
||||
RIFT_S_INFO(
|
||||
"World relocalized inliers=%u/%u error_px=%.3f "
|
||||
"transform=(%.4f,%.4f,%.4f)",
|
||||
result.inliers[0], result.inliers[1], result.max_reprojection_px,
|
||||
candidate.position.x, candidate.position.y, candidate.position.z);
|
||||
}
|
||||
}
|
||||
os_mutex_unlock(&t->mutex);
|
||||
} else if (!record && (!valid || capture_ns - t->world_candidate_ns > 3LL * U_TIME_1S_IN_NS)) {
|
||||
os_mutex_lock(&t->mutex);
|
||||
t->world_confirmations = 0;
|
||||
os_mutex_unlock(&t->mutex);
|
||||
}
|
||||
}
|
||||
for (int eye = 0; eye < RIFT_S_CAMERA_COUNT; eye++)
|
||||
xrt_frame_reference(&frames[eye], NULL);
|
||||
world_write_status(t);
|
||||
os_nanosleep(20 * U_TIME_1MS_IN_NS);
|
||||
os_thread_helper_lock(&t->world_thread);
|
||||
}
|
||||
os_thread_helper_unlock(&t->world_thread);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void
|
||||
rift_s_tracker_start(struct rift_s_tracker *t)
|
||||
{
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "math/m_imu_3dof.h"
|
||||
#include "math/m_relation_history.h"
|
||||
#include "os/os_threading.h"
|
||||
#include "util/u_var.h"
|
||||
#include "xrt/xrt_defines.h"
|
||||
@@ -25,6 +26,8 @@
|
||||
#include "tracking/t_constellation_tracking.h"
|
||||
|
||||
#include "rift_s_firmware.h"
|
||||
#include "rift_s_slam_guard.h"
|
||||
#include "rift_s_world_anchor.h"
|
||||
|
||||
/* Oculus Rift S HMD Tracking */
|
||||
#ifndef RIFT_S_TRACKER_H
|
||||
@@ -46,6 +49,42 @@ struct rift_s_tracker
|
||||
//! Protects shared access to 3dof and pose storage
|
||||
struct os_mutex mutex;
|
||||
|
||||
//! Serializes camera submission and SLAM session replacement.
|
||||
struct os_mutex slam_mutex;
|
||||
//! Latest-frame queue; feature matching never blocks USB, SLAM or controller ingestion.
|
||||
struct os_thread_helper world_thread;
|
||||
bool world_thread_initialized;
|
||||
struct rift_s_world_anchor *world_anchor;
|
||||
struct xrt_frame *world_frames[RIFT_S_CAMERA_COUNT];
|
||||
timepoint_ns world_last_queued_ns;
|
||||
bool world_setup;
|
||||
bool world_map_available;
|
||||
bool world_storage_failed;
|
||||
uint64_t world_command_id;
|
||||
uint32_t world_confirmations;
|
||||
struct xrt_pose world_candidate;
|
||||
uint64_t world_candidate_generation;
|
||||
timepoint_ns world_candidate_ns;
|
||||
|
||||
//! Camera arrival watchdog uses the host clock, not predicted pose time.
|
||||
timepoint_ns last_camera_arrival_ns;
|
||||
//! Proximity sensor reports the headset off-head.
|
||||
bool standby;
|
||||
//! SLAM input stopped (standby or camera timeout).
|
||||
bool slam_paused;
|
||||
//! Re-initialise SLAM before submitting the next fresh camera frame.
|
||||
bool slam_restart_pending;
|
||||
uint64_t slam_generation;
|
||||
timepoint_ns slam_min_sample_ns;
|
||||
|
||||
//! Whether the SLAM cameras see enough features. Under @ref mutex.
|
||||
struct rift_s_slam_feature_gate feature_gate;
|
||||
|
||||
//! Last published IMU pose, frozen while the head pose is held.
|
||||
bool have_published_imu_pose;
|
||||
timepoint_ns last_published_pose_log_ns;
|
||||
struct xrt_pose published_imu_pose;
|
||||
|
||||
//! Don't process IMU / video until started
|
||||
bool ready_for_data;
|
||||
|
||||
@@ -53,6 +92,7 @@ struct rift_s_tracker
|
||||
{
|
||||
//! Main fusion calculator.
|
||||
struct m_imu_3dof i3dof;
|
||||
struct m_relation_history *history;
|
||||
|
||||
//! The last angular velocity from the IMU, for prediction.
|
||||
struct xrt_vec3 last_angular_velocity;
|
||||
@@ -101,6 +141,9 @@ struct rift_s_tracker
|
||||
//! Last tracked pose
|
||||
struct xrt_pose pose;
|
||||
|
||||
//! Rejects diverged SLAM poses and holds the position while falling back to 3DoF. Under @ref mutex.
|
||||
struct rift_s_slam_guard slam_guard;
|
||||
|
||||
/* Stereo calibration for the front 2 cameras */
|
||||
struct t_stereo_camera_calibration *stereo_calib;
|
||||
struct t_slam_calibration slam_calib;
|
||||
@@ -128,6 +171,10 @@ struct rift_s_tracker
|
||||
} gui;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct rift_s_tracker *
|
||||
rift_s_tracker_create(struct xrt_tracking_origin *origin,
|
||||
struct xrt_frame_context *xfctx,
|
||||
@@ -135,6 +182,8 @@ rift_s_tracker_create(struct xrt_tracking_origin *origin,
|
||||
void
|
||||
rift_s_tracker_start(struct rift_s_tracker *t);
|
||||
void
|
||||
rift_s_tracker_set_standby(struct rift_s_tracker *t, bool standby);
|
||||
void
|
||||
rift_s_tracker_destroy(struct rift_s_tracker *t);
|
||||
void
|
||||
rift_s_tracker_add_debug_ui(struct rift_s_tracker *t, void *root);
|
||||
@@ -175,4 +224,8 @@ rift_s_tracker_get_tracked_pose(struct rift_s_tracker *t,
|
||||
uint64_t at_timestamp_ns,
|
||||
struct xrt_space_relation *out_relation);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -214,7 +214,7 @@ struct DistortParamKB4CostFunctor
|
||||
struct t_camera_calibration
|
||||
rift_s_get_cam_calib(struct rift_s_camera_calibration_block *camera_calibration, enum rift_s_camera_id cam_id)
|
||||
{
|
||||
struct t_camera_calibration tcc;
|
||||
struct t_camera_calibration tcc{};
|
||||
|
||||
struct rift_s_camera_calibration *rift_s_cam = &camera_calibration->cameras[cam_id];
|
||||
tcc.image_size_pixels.h = rift_s_cam->roi.extent.h;
|
||||
@@ -341,3 +341,31 @@ rift_s_create_stereo_camera_calib_rotated(struct rift_s_camera_calibration_block
|
||||
|
||||
return calib;
|
||||
}
|
||||
|
||||
float
|
||||
rift_s_apply_optical_yaw(struct xrt_quat *imu_orientation, const struct xrt_quat *optical_orientation)
|
||||
{
|
||||
struct xrt_quat inverse, delta;
|
||||
math_quat_invert(imu_orientation, &inverse);
|
||||
math_quat_rotate(optical_orientation, &inverse, &delta);
|
||||
|
||||
// Project the world-space difference onto Y. q and -q must take the same shortest path.
|
||||
if (delta.w < 0) {
|
||||
delta.y = -delta.y;
|
||||
delta.w = -delta.w;
|
||||
}
|
||||
if (fabsf(delta.y) + fabsf(delta.w) < 1e-6f) {
|
||||
return 0;
|
||||
}
|
||||
float yaw = 2 * atan2f(delta.y, delta.w);
|
||||
if (fabsf(yaw) <= DEG_TO_RAD(0.25f)) {
|
||||
return 0;
|
||||
}
|
||||
if (fabsf(yaw) > DEG_TO_RAD(5.0f)) {
|
||||
yaw *= 0.10f;
|
||||
}
|
||||
struct xrt_quat correction = {0, sinf(yaw / 2), 0, cosf(yaw / 2)};
|
||||
math_quat_rotate(&correction, imu_orientation, imu_orientation);
|
||||
math_quat_normalize(imu_orientation);
|
||||
return yaw;
|
||||
}
|
||||
@@ -20,6 +20,11 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Apply optical yaw around world Y, preserving the IMU's gravity alignment.
|
||||
// Returns the applied angle in radians. Large discrepancies are corrected by 10%.
|
||||
float
|
||||
rift_s_apply_optical_yaw(struct xrt_quat *imu_orientation, const struct xrt_quat *optical_orientation);
|
||||
|
||||
struct t_camera_calibration
|
||||
rift_s_get_cam_calib(struct rift_s_camera_calibration_block *camera_calibration, enum rift_s_camera_id cam_id);
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,126 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
#pragma once
|
||||
#include "rift_s_firmware.h"
|
||||
#include "xrt/xrt_frame.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct rift_s_world_anchor;
|
||||
#define RIFT_S_WORLD_MAX_CAMERAS 5
|
||||
struct rift_s_world_anchor_config
|
||||
{
|
||||
// Raw sensor calibration in tracker camera order. Native camera axes are +Y down, +Z forward.
|
||||
struct rift_s_camera_calibration camera[RIFT_S_WORLD_MAX_CAMERAS];
|
||||
struct xrt_pose imu_from_camera[RIFT_S_WORLD_MAX_CAMERAS];
|
||||
uint32_t camera_count; // Zero selects the legacy two-camera rig.
|
||||
// Bind a persisted map to the committed boundary. Empty path resolves Steam's registry.
|
||||
bool require_boundary;
|
||||
char boundary_path[1024];
|
||||
};
|
||||
struct rift_s_world_anchor_observation
|
||||
{
|
||||
struct xrt_vec3 world_point;
|
||||
struct xrt_vec2 pixel;
|
||||
};
|
||||
struct rift_s_world_anchor_result
|
||||
{
|
||||
struct xrt_pose world_from_imu;
|
||||
uint32_t features[RIFT_S_WORLD_MAX_CAMERAS], matches[RIFT_S_WORLD_MAX_CAMERAS],
|
||||
inliers[RIFT_S_WORLD_MAX_CAMERAS], camera_points[RIFT_S_WORLD_MAX_CAMERAS], keyframes;
|
||||
uint32_t supporting_cameras;
|
||||
uint32_t stereo_matches, triangulated;
|
||||
float max_reprojection_px;
|
||||
const char *rejection; // Static diagnostic string, NULL on success.
|
||||
bool recorded, relocalized;
|
||||
};
|
||||
|
||||
// Opt-in bench recorder. Writes at most 600 synchronized camera sets and capture-time SLAM poses.
|
||||
// RIFT_S_WORLD_RECORD_DIR is honored only with MONADO_STEAMVR_BENCH enabled.
|
||||
void
|
||||
rift_s_world_anchor_record_frames(struct rift_s_world_anchor *anchor,
|
||||
const struct xrt_frame *left,
|
||||
const struct xrt_frame *right,
|
||||
const struct xrt_pose *raw_from_imu);
|
||||
|
||||
void
|
||||
rift_s_world_anchor_record_rig_frames(struct rift_s_world_anchor *anchor,
|
||||
const struct xrt_frame *const frames[],
|
||||
uint32_t camera_count,
|
||||
const struct xrt_pose *raw_from_imu);
|
||||
bool
|
||||
rift_s_world_anchor_process_rig(struct rift_s_world_anchor *anchor,
|
||||
const struct xrt_frame *const frames[],
|
||||
uint32_t camera_count,
|
||||
const struct xrt_pose *trusted_world_from_imu,
|
||||
struct rift_s_world_anchor_result *result);
|
||||
// Recovery fixes roll/pitch to the exposure-time VIO attitude and solves only yaw/translation.
|
||||
bool
|
||||
rift_s_world_anchor_process_rig_gravity(struct rift_s_world_anchor *anchor,
|
||||
const struct xrt_frame *const frames[],
|
||||
uint32_t camera_count,
|
||||
const struct xrt_pose *trusted_world_from_imu,
|
||||
const struct xrt_quat *raw_attitude,
|
||||
struct rift_s_world_anchor_result *result);
|
||||
|
||||
// Worker-owned: none of these functions are thread safe. No frame references are retained.
|
||||
// An absent/invalid file creates an empty map, never a nominal world alignment.
|
||||
struct rift_s_world_anchor *
|
||||
rift_s_world_anchor_create(const struct rift_s_world_anchor_config *config, const char *path);
|
||||
void
|
||||
rift_s_world_anchor_destroy(struct rift_s_world_anchor *anchor);
|
||||
bool
|
||||
rift_s_world_anchor_has_map(const struct rift_s_world_anchor *anchor);
|
||||
// A committed standing frame must not be paired with a fresh, arbitrary VIO frame, even if
|
||||
// its visual map is missing or rejected. Only room setup may establish a replacement frame.
|
||||
bool
|
||||
rift_s_world_anchor_has_boundary(const struct rift_s_world_anchor *anchor);
|
||||
// Clear an in-memory draft, without changing the saved map. Reload restores it after Cancel.
|
||||
void
|
||||
rift_s_world_anchor_clear(struct rift_s_world_anchor *anchor);
|
||||
// Save also accepts an empty draft, atomically invalidating the old world after a new calibration.
|
||||
bool
|
||||
rift_s_world_anchor_save(struct rift_s_world_anchor *anchor);
|
||||
bool
|
||||
rift_s_world_anchor_reload(struct rift_s_world_anchor *anchor);
|
||||
// Non-null trusted_world_from_imu records stereo keyframes. NULL only attempts recovery.
|
||||
// Pose must correspond to the exposure timestamp, not the latest predicted head pose.
|
||||
bool
|
||||
rift_s_world_anchor_process(struct rift_s_world_anchor *anchor,
|
||||
const struct xrt_frame *left,
|
||||
const struct xrt_frame *right,
|
||||
const struct xrt_pose *trusted_world_from_imu,
|
||||
struct rift_s_world_anchor_result *result);
|
||||
|
||||
// Geometry shared by recording/recovery and focused calibration tests.
|
||||
bool
|
||||
rift_s_world_anchor_project(const struct rift_s_camera_calibration *camera,
|
||||
const struct xrt_vec3 *native_point,
|
||||
struct xrt_vec2 *pixel);
|
||||
bool
|
||||
rift_s_world_anchor_unproject(const struct rift_s_camera_calibration *camera,
|
||||
const struct xrt_vec2 *pixel,
|
||||
struct xrt_vec3 *native_ray);
|
||||
bool
|
||||
rift_s_world_anchor_solve(const struct rift_s_world_anchor_config *config,
|
||||
const struct rift_s_world_anchor_observation *observations[2],
|
||||
const uint32_t counts[2],
|
||||
struct rift_s_world_anchor_result *result);
|
||||
bool
|
||||
rift_s_world_anchor_solve_rig(const struct rift_s_world_anchor_config *config,
|
||||
const struct rift_s_world_anchor_observation *const observations[],
|
||||
const uint32_t counts[],
|
||||
uint32_t camera_count,
|
||||
struct rift_s_world_anchor_result *result);
|
||||
bool
|
||||
rift_s_world_anchor_solve_rig_gravity(const struct rift_s_world_anchor_config *config,
|
||||
const struct rift_s_world_anchor_observation *const observations[],
|
||||
const uint32_t counts[],
|
||||
uint32_t camera_count,
|
||||
const struct xrt_quat *raw_attitude,
|
||||
struct rift_s_world_anchor_result *result);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
#pragma once
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
// Same-user native setup guard and tracker exchange only command IDs/status. Maps live in config.
|
||||
static inline void
|
||||
rift_s_world_control_path(const char *name, char *out, size_t size)
|
||||
{
|
||||
const char *runtime = getenv("XDG_RUNTIME_DIR");
|
||||
if (runtime && runtime[0]) snprintf(out, size, "%s/monado-rifts-world-%s", runtime, name);
|
||||
else snprintf(out, size, "/run/user/%u/monado-rifts-world-%s", (unsigned)getuid(), name);
|
||||
}
|
||||
@@ -46,6 +46,9 @@ struct t_constellation_camera
|
||||
struct xrt_rect roi;
|
||||
//! Intrinsics and distortion parameters
|
||||
struct t_camera_calibration calibration;
|
||||
//! Native Fisheye62 coefficients, when a KB4 approximation would discard calibration.
|
||||
bool fisheye62_valid;
|
||||
float fisheye62_radial[6], fisheye62_p1, fisheye62_p2;
|
||||
//! Minimum blob brightness threshold
|
||||
uint8_t min_threshold;
|
||||
//! Minimum blob brightness threshold for pixel inclusion
|
||||
@@ -70,12 +73,42 @@ t_constellation_tracker_create(struct xrt_frame_context *xfctx,
|
||||
struct xrt_frame_sink **out_sink,
|
||||
struct xrt_device_masks_sink *controller_mask_sink);
|
||||
|
||||
/*!
|
||||
* One verified optical measurement of a tracked device at image capture time.
|
||||
*
|
||||
* The pose is the device pose in the OpenXR world frame, as passed to push_observed_pose.
|
||||
* Covariances describe the optical fit only; there is no velocity estimate yet.
|
||||
*/
|
||||
struct t_constellation_pose_observation
|
||||
{
|
||||
timepoint_ns capture_ns;
|
||||
struct xrt_pose pose;
|
||||
//! The optical fit itself constrained yaw. Otherwise orientation is the IMU prior echoed back.
|
||||
bool orientation_observed;
|
||||
//! Accepted observations (repeated camera views counted) and distinct physical LEDs.
|
||||
uint32_t inliers;
|
||||
uint32_t distinct_leds;
|
||||
//! Information rank of the optical fit: 3 (translation) or 4 (translation and yaw).
|
||||
uint32_t information_rank;
|
||||
//! Row-major world position covariance in m^2.
|
||||
float position_covariance[9];
|
||||
//! Yaw variance in rad^2, INFINITY when not observed.
|
||||
float yaw_variance;
|
||||
//! Object-space RMS cost in metres and the hypothesis that produced the pose.
|
||||
float cost_m;
|
||||
const char *hypothesis;
|
||||
};
|
||||
|
||||
struct t_constellation_tracked_device_callbacks
|
||||
{
|
||||
bool (*get_led_model)(struct xrt_device *xdev, struct t_constellation_led_model *led_model);
|
||||
void (*notify_frame_received)(struct xrt_device *xdev, uint64_t frame_mono_ns, uint64_t frame_sequence);
|
||||
void (*push_observed_pose)(struct xrt_device *xdev, timepoint_ns frame_mono_ns, const struct xrt_pose *pose);
|
||||
void (*push_brightness_update)(struct xrt_device *xdev, uint8_t average_brightness);
|
||||
//! True only when optical heading and capture-time attitude support a rotation prior.
|
||||
bool (*has_heading_prior)(struct xrt_device *xdev, timepoint_ns capture_ns);
|
||||
//! Optional: preferred over push_observed_pose when set, carries fit quality and yaw observability.
|
||||
void (*push_pose_observation)(struct xrt_device *xdev, const struct t_constellation_pose_observation *obs);
|
||||
};
|
||||
|
||||
struct t_constellation_tracked_device_connection *
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
|
||||
#include "xrt/xrt_device.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* This is the angle that Rift CV1 LEDs are visible at. Let's see if
|
||||
* it works for other controller types... */
|
||||
#define LED_ANGLE 82
|
||||
@@ -49,6 +53,9 @@ struct t_constellation_led_model
|
||||
struct t_constellation_led *leds;
|
||||
uint8_t num_leds;
|
||||
|
||||
// Minimum for gravity-constrained acquisition. Zero keeps the seven-LED default.
|
||||
uint8_t min_acquisition_leds;
|
||||
|
||||
// bounding box for the device itself, not it's LEDs
|
||||
struct t_constellation_bounding_point *bounding_points;
|
||||
uint8_t num_bounding_points;
|
||||
@@ -101,3 +108,7 @@ struct t_constellation_search_model *
|
||||
t_constellation_search_model_new(struct t_constellation_led_model *led_model);
|
||||
void
|
||||
t_constellation_search_model_free(struct t_constellation_search_model *model);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
#pragma once
|
||||
#include "xrt/xrt_defines.h"
|
||||
#include "xrt/xrt_frame.h"
|
||||
|
||||
/* Optional local-device camera provider. Frames are rectified RGB8, left/right
|
||||
* side by side, with monotonic capture timestamps. Owned by the device; callers
|
||||
* release returned frame references. No IPC transport is implied. */
|
||||
struct xrt_camera
|
||||
{
|
||||
uint32_t view_width, view_height;
|
||||
struct xrt_vec2 focal_length, center;
|
||||
struct xrt_pose head_from_camera[2];
|
||||
void (*set_active)(struct xrt_camera *camera, bool active);
|
||||
bool (*get_frame)(struct xrt_camera *camera, struct xrt_frame **out_frame);
|
||||
};
|
||||
@@ -610,6 +610,15 @@ struct xrt_device
|
||||
struct xrt_fov *out_fovs,
|
||||
struct xrt_pose *out_poses);
|
||||
|
||||
/* Optional: called only by the compositor render thread, before recording a frame.
|
||||
* Return true when UVs changed. Projection FOV/topology must remain unchanged.
|
||||
* Other consumers may load configuration at device creation instead. */
|
||||
bool (*update_distortion)(struct xrt_device *xdev);
|
||||
|
||||
//! Optional tracking-camera video, owned by this device.
|
||||
struct xrt_camera *camera;
|
||||
|
||||
|
||||
/**
|
||||
* Compute the distortion at a single point.
|
||||
*
|
||||
@@ -624,6 +633,7 @@ struct xrt_device
|
||||
* @param v vertical texture coordinate
|
||||
* @param[out] out_result corresponding u,v pairs for all three color channels.
|
||||
*/
|
||||
|
||||
xrt_result_t (*compute_distortion)(
|
||||
struct xrt_device *xdev, uint32_t view, float u, float v, struct xrt_uv_triplet *out_result);
|
||||
|
||||
|
||||
@@ -297,6 +297,13 @@ struct xrt_tracked_psvr
|
||||
*/
|
||||
struct xrt_tracked_slam
|
||||
{
|
||||
/*!
|
||||
* Discard the current tracking session, including queued sensor data and
|
||||
* predicted poses. Sinks remain valid. Returns zero on success.
|
||||
* Optional: drivers must hold an untracked pose if restart is unavailable.
|
||||
*/
|
||||
int (*restart)(struct xrt_tracked_slam *);
|
||||
|
||||
/*!
|
||||
* Called by the owning @ref xrt_device to get the last estimated pose
|
||||
* of the SLAM tracker.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
add_subdirectory(steamvr_bindings)
|
||||
|
||||
add_library(st_ovrd STATIC ovrd_driver.cpp ovrd_interface.h)
|
||||
add_library(st_ovrd STATIC ovrd_driver.cpp ovrd_interface.h ovrd_pose.hpp ovrd_camera.cpp ovrd_camera.hpp rifts_passthrough_shm.h)
|
||||
|
||||
target_include_directories(st_ovrd INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(
|
||||
@@ -15,3 +15,20 @@ target_link_libraries(
|
||||
aux_generated_bindings_helpers
|
||||
ovrd_generated_bindings
|
||||
)
|
||||
|
||||
# Linux SteamVR's sparse SDF staging images need initialized storage on NVIDIA.
|
||||
if(XRT_HAVE_VULKAN AND CMAKE_SYSTEM_NAME STREQUAL "Linux" AND XRT_BUILD_DRIVER_RIFT_S AND CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
add_library(monado_roomsetup_vulkan SHARED ovrd_roomsetup_vulkan.cpp)
|
||||
target_include_directories(monado_roomsetup_vulkan PRIVATE ${Vulkan_INCLUDE_DIR})
|
||||
target_link_options(monado_roomsetup_vulkan PRIVATE "-Wl,-Bsymbolic")
|
||||
set_target_properties(monado_roomsetup_vulkan PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/steamvr-monado/bin/linux64")
|
||||
set(ROOMSETUP_LAYER_LIBRARY "${CMAKE_INSTALL_PREFIX}/share/steamvr-monado/bin/linux64/libmonado_roomsetup_vulkan.so")
|
||||
configure_file(monado_roomsetup_vulkan.json.in ${CMAKE_CURRENT_BINARY_DIR}/monado_roomsetup_vulkan.json @ONLY)
|
||||
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/monado_roomsetup_vulkan.json DESTINATION ${CMAKE_INSTALL_DATADIR}/vulkan/implicit_layer.d)
|
||||
endif()
|
||||
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND XRT_BUILD_DRIVER_RIFT_S AND CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
add_executable(monado-roomsetup-guard ovrd_roomsetup_guard.cpp)
|
||||
target_link_libraries(monado-roomsetup-guard PRIVATE xrt-external-openvr aux_util ${CMAKE_DL_LIBS})
|
||||
set_target_properties(monado-roomsetup-guard PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/steamvr-monado/bin/linux64")
|
||||
endif()
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"file_format_version": "1.2.0",
|
||||
"layer": {
|
||||
"name": "VK_LAYER_MONADO_roomsetup",
|
||||
"type": "GLOBAL",
|
||||
"library_path": "@ROOMSETUP_LAYER_LIBRARY@",
|
||||
"library_arch": "64",
|
||||
"api_version": "1.3.0",
|
||||
"implementation_version": "1",
|
||||
"description": "Monado SteamVR room setup SDF storage compatibility",
|
||||
"disable_environment": {"MONADO_DISABLE_ROOMSETUP_COMPAT": "1"}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,573 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
#include "ovrd_camera.hpp"
|
||||
#include "math/m_api.h"
|
||||
#include "math/m_space.h"
|
||||
#include "os/os_time.h"
|
||||
#include "util/u_debug.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/resource.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <pthread.h>
|
||||
#include <sched.h>
|
||||
#include <unistd.h>
|
||||
|
||||
DEBUG_GET_ONCE_BOOL_OPTION(steamvr_bench, "MONADO_STEAMVR_BENCH", false)
|
||||
DEBUG_GET_ONCE_BOOL_OPTION(bench_camera, "MONADO_STEAMVR_BENCH_CAMERA", false)
|
||||
|
||||
std::atomic<MonadoCamera *> MonadoCamera::s_instance{nullptr};
|
||||
|
||||
MonadoCamera::MonadoCamera(xrt_device *device, vr::IVRBlockQueue *queue, vr::IVRPaths *paths)
|
||||
: device(device), camera(device->camera), queue(queue), paths(paths)
|
||||
{
|
||||
s_instance.store(this);
|
||||
shm_fd = open(RIFTS_PASSTHROUGH_SHM_PATH, O_CREAT | O_RDWR | O_NOFOLLOW | O_CLOEXEC, 0600);
|
||||
if (shm_fd >= 0) {
|
||||
// Hold the export ownership lock for the camera's lifetime. Consumers can
|
||||
// distinguish this producer from a stale export left by an earlier session.
|
||||
struct flock owner{};
|
||||
owner.l_type = F_WRLCK;
|
||||
owner.l_whence = SEEK_SET;
|
||||
if (fcntl(shm_fd, F_SETLK, &owner) < 0) {
|
||||
close(shm_fd);
|
||||
shm_fd = -1;
|
||||
}
|
||||
}
|
||||
if (shm_fd >= 0) {
|
||||
fchmod(shm_fd, 0600);
|
||||
if (ftruncate(shm_fd, sizeof(struct rifts_passthrough_shm)) == 0) {
|
||||
shm = static_cast<struct rifts_passthrough_shm *>(
|
||||
mmap(nullptr, sizeof(struct rifts_passthrough_shm), PROT_READ | PROT_WRITE, MAP_SHARED,
|
||||
shm_fd, 0));
|
||||
if (shm == MAP_FAILED) {
|
||||
shm = nullptr;
|
||||
} else {
|
||||
memset(shm, 0, sizeof(*shm));
|
||||
shm->magic = RIFTS_PASSTHROUGH_MAGIC;
|
||||
shm->version = RIFTS_PASSTHROUGH_VERSION;
|
||||
shm->width = camera ? camera->view_width * 2 : RIFTS_PASSTHROUGH_WIDTH;
|
||||
shm->height = camera ? camera->view_height : RIFTS_PASSTHROUGH_HEIGHT;
|
||||
shm->stride = shm->width * 4;
|
||||
shm->format = 0; // RGBA32
|
||||
shm->client_active = 0;
|
||||
shm->toggle_count = 0;
|
||||
shm->sequence = 0;
|
||||
shm->timestamp_ns = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (queue && paths && !initialize_queue()) {
|
||||
if (queue_handle)
|
||||
queue->Destroy(queue_handle);
|
||||
queue_handle = 0;
|
||||
}
|
||||
worker_running = true;
|
||||
worker = std::thread(&MonadoCamera::run, this);
|
||||
}
|
||||
|
||||
MonadoCamera::~MonadoCamera()
|
||||
{
|
||||
s_instance.store(nullptr);
|
||||
StopVideoStream();
|
||||
worker_running = false;
|
||||
if (worker.joinable())
|
||||
worker.join();
|
||||
if (queue_handle)
|
||||
queue->Destroy(queue_handle);
|
||||
if (shm) {
|
||||
shm->client_active = 0;
|
||||
munmap(shm, sizeof(struct rifts_passthrough_shm));
|
||||
shm = nullptr;
|
||||
}
|
||||
if (shm_fd >= 0) {
|
||||
close(shm_fd);
|
||||
shm_fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
MonadoCamera::NotifyButton(enum xrt_input_name name, bool pressed)
|
||||
{
|
||||
MonadoCamera *self = s_instance.load();
|
||||
if (!self || !self->shm || (name != XRT_INPUT_TOUCH_SYSTEM_CLICK && name != XRT_INPUT_TOUCH_MENU_CLICK))
|
||||
return;
|
||||
// RunFrame reports the held state repeatedly. Toggle only on rising edges.
|
||||
bool &previous = name == XRT_INPUT_TOUCH_SYSTEM_CLICK ? self->system_pressed : self->menu_pressed;
|
||||
if (pressed == previous)
|
||||
return;
|
||||
previous = pressed;
|
||||
if (!pressed)
|
||||
return;
|
||||
|
||||
int64_t now = os_monotonic_get_ns();
|
||||
|
||||
bool toggle = false;
|
||||
if (name == XRT_INPUT_TOUCH_SYSTEM_CLICK) {
|
||||
toggle = true;
|
||||
} else if (name == XRT_INPUT_TOUCH_MENU_CLICK) {
|
||||
if (self->last_menu_press_ns != 0 && now - self->last_menu_press_ns < 400 * 1000 * 1000) {
|
||||
toggle = true;
|
||||
self->last_menu_press_ns = 0;
|
||||
} else {
|
||||
self->last_menu_press_ns = now;
|
||||
}
|
||||
}
|
||||
|
||||
if (toggle) {
|
||||
__atomic_fetch_add(&self->shm->toggle_count, 1, __ATOMIC_SEQ_CST);
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
MonadoCamera::GetCameraFrameDimensions(vr::ECameraVideoStreamFormat format, uint32_t *width, uint32_t *height)
|
||||
{
|
||||
if (format != vr::CVS_FORMAT_RGBX32 || !width || !height)
|
||||
return false;
|
||||
*width = camera->view_width * 2;
|
||||
*height = camera->view_height;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
MonadoCamera::GetCameraFrameBufferingRequirements(int *count, uint32_t *size)
|
||||
{
|
||||
if (!count || !size)
|
||||
return false;
|
||||
*count = 3;
|
||||
*size = bytes();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
MonadoCamera::SetCameraFrameBuffering(int count, void **data, uint32_t size)
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
if (active || count < 2 || count > 32 || !data || size < bytes())
|
||||
return false;
|
||||
for (const auto &b : buffers)
|
||||
if (b.acquired)
|
||||
return false;
|
||||
for (int i = 0; i < count; ++i)
|
||||
if (!data[i])
|
||||
return false;
|
||||
buffers.clear();
|
||||
storage.clear();
|
||||
buffers.resize(count);
|
||||
for (int i = 0; i < count; ++i)
|
||||
buffers[i].data = data[i];
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
MonadoCamera::SetCameraVideoStreamFormat(vr::ECameraVideoStreamFormat format)
|
||||
{
|
||||
return format == vr::CVS_FORMAT_RGBX32;
|
||||
}
|
||||
|
||||
bool
|
||||
MonadoCamera::StartVideoStream()
|
||||
{
|
||||
std::lock_guard callback_lock(callback_mutex);
|
||||
std::lock_guard lock(mutex);
|
||||
if (active)
|
||||
return true;
|
||||
for (const auto &b : buffers)
|
||||
if (b.acquired)
|
||||
return false;
|
||||
if (buffers.empty()) {
|
||||
storage.resize(3, std::vector<uint8_t>(bytes()));
|
||||
buffers.resize(3);
|
||||
for (size_t i = 0; i < buffers.size(); ++i)
|
||||
buffers[i].data = storage[i].data();
|
||||
}
|
||||
for (auto &b : buffers)
|
||||
b.ready = false;
|
||||
start_ns = os_monotonic_get_ns();
|
||||
++stream_generation;
|
||||
frame_sequence = 0;
|
||||
native_sequence = 0;
|
||||
paused = false;
|
||||
active = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
MonadoCamera::StopVideoStream()
|
||||
{
|
||||
// Serialize completion with publication and callbacks, but never wait for get_frame.
|
||||
std::lock_guard callback_lock(callback_mutex);
|
||||
std::lock_guard lock(mutex);
|
||||
active = false;
|
||||
++stream_generation;
|
||||
for (auto &b : buffers)
|
||||
b.ready = false;
|
||||
}
|
||||
|
||||
bool
|
||||
MonadoCamera::IsVideoStreamActive(bool *is_paused, float *elapsed)
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
if (is_paused)
|
||||
*is_paused = paused;
|
||||
if (elapsed)
|
||||
*elapsed = active ? (os_monotonic_get_ns() - start_ns) / 1e9f : 0;
|
||||
return active;
|
||||
}
|
||||
|
||||
bool
|
||||
MonadoCamera::PauseVideoStream()
|
||||
{
|
||||
std::lock_guard callback_lock(callback_mutex);
|
||||
std::lock_guard lock(mutex);
|
||||
paused = true;
|
||||
return active;
|
||||
}
|
||||
|
||||
bool
|
||||
MonadoCamera::ResumeVideoStream()
|
||||
{
|
||||
std::lock_guard callback_lock(callback_mutex);
|
||||
std::lock_guard lock(mutex);
|
||||
if (!active)
|
||||
return false;
|
||||
paused = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
const vr::CameraVideoStreamFrame_t *
|
||||
MonadoCamera::GetVideoStreamFrame()
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
if (!active || paused)
|
||||
return nullptr;
|
||||
Buffer *latest = nullptr;
|
||||
uint64_t now = os_monotonic_get_ns();
|
||||
for (auto &b : buffers) {
|
||||
if (b.ready && !b.acquired && now >= b.frame.m_nFrameCaptureTicks_ServerAbsolute &&
|
||||
now - b.frame.m_nFrameCaptureTicks_ServerAbsolute <= 250 * U_TIME_1MS_IN_NS &&
|
||||
(!latest || b.frame.m_nFrameSequence > latest->frame.m_nFrameSequence))
|
||||
latest = &b;
|
||||
}
|
||||
if (!latest)
|
||||
return nullptr;
|
||||
for (auto &b : buffers)
|
||||
if (!b.acquired)
|
||||
b.ready = false;
|
||||
latest->acquired = true;
|
||||
return &latest->frame;
|
||||
}
|
||||
|
||||
void
|
||||
MonadoCamera::ReleaseVideoStreamFrame(const vr::CameraVideoStreamFrame_t *frame)
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
for (auto &b : buffers)
|
||||
if (&b.frame == frame)
|
||||
b.acquired = false;
|
||||
}
|
||||
|
||||
bool
|
||||
MonadoCamera::SetCameraVideoSinkCallback(vr::ICameraVideoSinkCallback *sink)
|
||||
{
|
||||
std::lock_guard lock(callback_mutex);
|
||||
callback = sink;
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
MonadoCamera::run()
|
||||
{
|
||||
// Export must not inherit a tracking thread's realtime policy or priority.
|
||||
pthread_setname_np(pthread_self(), "rift-s-export");
|
||||
sched_param scheduling{};
|
||||
pthread_setschedparam(pthread_self(), SCHED_OTHER, &scheduling);
|
||||
setpriority(PRIO_PROCESS, static_cast<id_t>(syscall(SYS_gettid)), 10);
|
||||
int64_t last_timestamp = 0;
|
||||
std::vector<uint8_t> rgba(bytes());
|
||||
while (worker_running) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||||
bool stream_active;
|
||||
uint64_t generation;
|
||||
int64_t stream_start_ns;
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
stream_active = active && !paused;
|
||||
generation = stream_generation;
|
||||
stream_start_ns = start_ns;
|
||||
}
|
||||
uint32_t heartbeat = shm ? __atomic_load_n(&shm->client_active, __ATOMIC_ACQUIRE) : 0;
|
||||
uint32_t now_ms = os_monotonic_get_ns() / U_TIME_1MS_IN_NS;
|
||||
const bool export_active = heartbeat != 0 && static_cast<uint32_t>(now_ms - heartbeat) < 1500;
|
||||
bool want_frames = stream_active || export_active;
|
||||
if (!want_frames || !camera) {
|
||||
if (camera)
|
||||
camera->set_active(camera, false);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(8));
|
||||
continue;
|
||||
}
|
||||
|
||||
camera->set_active(camera, true);
|
||||
xrt_frame *frame = nullptr;
|
||||
if (!camera->get_frame(camera, &frame))
|
||||
continue;
|
||||
if (frame->format != XRT_FORMAT_R8G8B8 || frame->width != camera->view_width * 2 ||
|
||||
frame->height != camera->view_height || frame->stride < frame->width * 3 ||
|
||||
frame->size < frame->stride * frame->height) {
|
||||
xrt_frame_reference(&frame, nullptr);
|
||||
continue;
|
||||
}
|
||||
if (frame->timestamp <= last_timestamp) {
|
||||
xrt_frame_reference(&frame, nullptr);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert once for both the compositor queue and the overlay export.
|
||||
for (uint32_t y = 0; y < frame->height; ++y) {
|
||||
const uint8_t *src = frame->data + y * frame->stride;
|
||||
uint8_t *dst = rgba.data() + y * frame->width * 4;
|
||||
for (uint32_t x = 0; x < frame->width; ++x) {
|
||||
memcpy(dst + x * 4, src + x * 3, 3);
|
||||
dst[x * 4 + 3] = 255;
|
||||
}
|
||||
}
|
||||
if (debug_get_bool_option_steamvr_bench() && debug_get_bool_option_bench_camera()) {
|
||||
// Distinct diagnostic cameras identify eye selection and packed UVs.
|
||||
for (uint32_t y = 0; y < frame->height; ++y) {
|
||||
for (uint32_t x = 0; x < frame->width; ++x) {
|
||||
bool right = x >= camera->view_width;
|
||||
bool grid = x % 40 < 2 || y % 40 < 2;
|
||||
auto *pixel = rgba.data() + (y * frame->width + x) * 4;
|
||||
pixel[0] = right ? (grid ? 255 : 0) : 255;
|
||||
pixel[1] = grid ? 255 : 0;
|
||||
pixel[2] = right ? 255 : (grid ? 255 : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (export_active) {
|
||||
// Odd while writing; readers accept only equal, even sequence values.
|
||||
__atomic_store_n(&shm->sequence, ++export_sequence, __ATOMIC_SEQ_CST);
|
||||
memcpy(shm->pixels, rgba.data(), rgba.size());
|
||||
shm->timestamp_ns = frame->timestamp;
|
||||
__atomic_store_n(&shm->sequence, ++export_sequence, __ATOMIC_RELEASE);
|
||||
}
|
||||
// A frame fetched outside the stream lock may belong to a stopped generation.
|
||||
if (stream_active && frame->timestamp >= stream_start_ns) {
|
||||
xrt_space_relation rel = XRT_SPACE_RELATION_ZERO;
|
||||
xrt_device_get_tracked_pose(device, XRT_INPUT_GENERIC_HEAD_POSE, frame->timestamp, &rel);
|
||||
xrt_relation_chain chain{};
|
||||
m_relation_chain_push_relation(&chain, &rel);
|
||||
m_relation_chain_push_pose_if_not_identity(&chain, &device->tracking_origin->initial_offset);
|
||||
m_relation_chain_resolve(&chain, &rel);
|
||||
bool delivered = false;
|
||||
std::lock_guard callback_lock(callback_mutex);
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
if (!active || paused || generation != stream_generation) {
|
||||
xrt_frame_reference(&frame, nullptr);
|
||||
continue;
|
||||
}
|
||||
if (queue_handle) {
|
||||
double interval =
|
||||
last_timestamp ? (frame->timestamp - last_timestamp) / 1e9 : 1.0 / 30;
|
||||
publish_queue(rgba.data(), frame->timestamp, native_sequence++, interval);
|
||||
}
|
||||
for (size_t i = 0; i < buffers.size(); ++i) {
|
||||
auto &b = buffers[i];
|
||||
if (b.acquired)
|
||||
continue;
|
||||
memcpy(b.data, rgba.data(), rgba.size());
|
||||
b.frame = {};
|
||||
b.frame.m_nStreamFormat = vr::CVS_FORMAT_RGBX32;
|
||||
b.frame.m_nWidth = frame->width;
|
||||
b.frame.m_nHeight = frame->height;
|
||||
b.frame.m_nImageDataSize = bytes();
|
||||
b.frame.m_nFrameSequence = frame_sequence++;
|
||||
b.frame.m_nBufferIndex = i;
|
||||
b.frame.m_nBufferCount = buffers.size();
|
||||
b.frame.m_flFrameElapsedTime = (frame->timestamp - start_ns) / 1e9;
|
||||
b.frame.m_flFrameDeliveryRate = 30;
|
||||
b.frame.m_flFrameCaptureTime_DriverAbsolute = frame->timestamp / 1e9;
|
||||
b.frame.m_flFrameCaptureTime_ServerRelative =
|
||||
(static_cast<int64_t>(frame->timestamp) -
|
||||
static_cast<int64_t>(os_monotonic_get_ns())) /
|
||||
1e9;
|
||||
b.frame.m_nFrameCaptureTicks_ServerAbsolute = frame->timestamp;
|
||||
xrt_matrix_4x4 matrix;
|
||||
math_matrix_4x4_isometry_from_pose(&rel.pose, &matrix);
|
||||
for (int row = 0; row < 3; ++row)
|
||||
for (int col = 0; col < 4; ++col)
|
||||
b.frame.m_RawTrackedDevicePose.mDeviceToAbsoluteTracking
|
||||
.m[row][col] = matrix.v[col * 4 + row];
|
||||
auto required = XRT_SPACE_RELATION_ORIENTATION_VALID_BIT |
|
||||
XRT_SPACE_RELATION_POSITION_VALID_BIT;
|
||||
b.frame.m_RawTrackedDevicePose.bPoseIsValid =
|
||||
(rel.relation_flags & required) == required;
|
||||
b.frame.m_RawTrackedDevicePose.bDeviceIsConnected = true;
|
||||
b.frame.m_RawTrackedDevicePose.eTrackingResult =
|
||||
b.frame.m_RawTrackedDevicePose.bPoseIsValid
|
||||
? vr::TrackingResult_Running_OK
|
||||
: vr::TrackingResult_Running_OutOfRange;
|
||||
b.frame.m_pImageData = reinterpret_cast<uintptr_t>(b.data);
|
||||
b.ready = true;
|
||||
delivered = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (delivered && callback)
|
||||
callback->OnCameraVideoSinkCallback();
|
||||
}
|
||||
|
||||
last_timestamp = frame->timestamp;
|
||||
xrt_frame_reference(&frame, nullptr);
|
||||
}
|
||||
|
||||
if (camera)
|
||||
camera->set_active(camera, false);
|
||||
}
|
||||
|
||||
bool
|
||||
MonadoCamera::GetCameraDistortion(uint32_t eye, float u, float v, float *out_u, float *out_v)
|
||||
{
|
||||
if (eye >= 2 || !out_u || !out_v)
|
||||
return false;
|
||||
*out_u = u;
|
||||
*out_v = v;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
MonadoCamera::GetCameraProjection(
|
||||
uint32_t eye, vr::EVRTrackedCameraFrameType type, float near, float far, vr::HmdMatrix44_t *out)
|
||||
{
|
||||
if (eye >= 2 || type < 0 || type >= vr::MAX_CAMERA_FRAME_TYPES || !std::isfinite(near) || !std::isfinite(far) ||
|
||||
near <= 0 || far <= near || !out)
|
||||
return false;
|
||||
*out = {};
|
||||
// SteamVR 2D selects the raw stereo half before projection. Each eye
|
||||
// then uses the same camera-local region of a packed-sized texture.
|
||||
float width = camera->view_width * 2.0f;
|
||||
float center_x = camera->center.x;
|
||||
out->m[0][0] = 2 * camera->focal_length.x / width;
|
||||
out->m[1][1] = 2 * camera->focal_length.y / camera->view_height;
|
||||
out->m[0][2] = 1 - 2 * center_x / width;
|
||||
out->m[1][2] = 2 * camera->center.y / camera->view_height - 1;
|
||||
// OpenVR camera projections use a zero-to-one depth range.
|
||||
out->m[2][2] = -double(far) / (double(far) - near);
|
||||
out->m[2][3] = -double(far) * near / (double(far) - near);
|
||||
if (!std::isfinite(out->m[2][2]) || !std::isfinite(out->m[2][3])) {
|
||||
*out = {};
|
||||
return false;
|
||||
}
|
||||
out->m[3][2] = -1;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
MonadoCamera::GetCameraCompatibilityMode(vr::ECameraCompatibilityMode *out)
|
||||
{
|
||||
if (!out)
|
||||
return false;
|
||||
*out = vr::CAMERA_COMPAT_MODE_BULK_DEFAULT;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
MonadoCamera::GetCameraFrameBounds(
|
||||
vr::EVRTrackedCameraFrameType type, uint32_t *left, uint32_t *top, uint32_t *width, uint32_t *height)
|
||||
{
|
||||
if (type < 0 || type >= vr::MAX_CAMERA_FRAME_TYPES || !left || !top || !width || !height)
|
||||
return false;
|
||||
*left = *top = 0;
|
||||
*width = camera->view_width * 2;
|
||||
*height = camera->view_height;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
MonadoCamera::GetCameraIntrinsics(uint32_t eye,
|
||||
vr::EVRTrackedCameraFrameType type,
|
||||
vr::HmdVector2_t *focal,
|
||||
vr::HmdVector2_t *center,
|
||||
vr::EVRDistortionFunctionType *distortion,
|
||||
double *coefficients)
|
||||
{
|
||||
if (eye >= 2 || type < 0 || type >= vr::MAX_CAMERA_FRAME_TYPES || !focal || !center || !distortion ||
|
||||
!coefficients)
|
||||
return false;
|
||||
*focal = {{camera->focal_length.x, camera->focal_length.y}};
|
||||
*center = {{camera->center.x, camera->center.y}};
|
||||
*distortion = vr::VRDistortionFunctionType_None;
|
||||
std::fill_n(coefficients, vr::k_unMaxDistortionFunctionParameters, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Queue storage and synchronization belong to SteamVR. Metadata is typed path
|
||||
// data in its 512-byte header, not a CameraVideoStreamFrame_t memory dump.
|
||||
bool
|
||||
MonadoCamera::initialize_queue()
|
||||
{
|
||||
vr::PropertyContainerHandle_t created = 0;
|
||||
if (queue->Create(&created, "/lighthouse/camera/raw_frames", bytes(), 512, 4, 0) != 0)
|
||||
return false;
|
||||
queue_handle = created;
|
||||
const char *names[] = {"/format", "/width", "/height"};
|
||||
int32_t values[] = {vr::CVS_FORMAT_RGBX32, static_cast<int32_t>(camera->view_width * 2),
|
||||
static_cast<int32_t>(camera->view_height)};
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
vr::PathWrite_t write{};
|
||||
if (paths->StringToHandle(&write.ulPath, names[i]) != vr::TrackedProp_Success)
|
||||
return false;
|
||||
write.writeType = vr::PropertyWrite_Set;
|
||||
write.pvBuffer = &values[i];
|
||||
write.unBufferSize = sizeof(values[i]);
|
||||
write.unTag = vr::k_unInt32PropertyTag;
|
||||
if (paths->WritePathBatch(queue_handle, &write, 1) != vr::TrackedProp_Success ||
|
||||
write.eError != vr::TrackedProp_Success)
|
||||
return false;
|
||||
}
|
||||
const char *metadata[] = {"/frame_size", "/frame_sequence", "/frame_time_monotonic",
|
||||
"/server_time_ticks", "/delivery_rate", "/elapsed_time"};
|
||||
for (int i = 0; i < 6; ++i)
|
||||
if (paths->StringToHandle(&metadata_paths[i], metadata[i]) != vr::TrackedProp_Success)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
MonadoCamera::publish_queue(const uint8_t *pixels, int64_t timestamp, uint64_t sequence, double interval)
|
||||
{
|
||||
vr::PropertyContainerHandle_t block = 0;
|
||||
void *data = nullptr;
|
||||
if (queue->AcquireWriteOnlyBlock(queue_handle, &block, &data) != 0)
|
||||
return;
|
||||
if (!data) {
|
||||
queue->ReleaseWriteOnlyBlock(queue_handle, block);
|
||||
return;
|
||||
}
|
||||
memcpy(data, pixels, bytes());
|
||||
int32_t size = bytes();
|
||||
double capture = timestamp / 1e9;
|
||||
uint64_t ticks = timestamp;
|
||||
double elapsed = (timestamp - start_ns) / 1e9;
|
||||
void *values[] = {&size, &sequence, &capture, &ticks, &interval, &elapsed};
|
||||
uint32_t sizes[] = {sizeof(size), sizeof(sequence), sizeof(capture),
|
||||
sizeof(ticks), sizeof(interval), sizeof(elapsed)};
|
||||
vr::PropertyTypeTag_t tags[] = {vr::k_unInt32PropertyTag, vr::k_unUint64PropertyTag,
|
||||
vr::k_unDoublePropertyTag, vr::k_unUint64PropertyTag,
|
||||
vr::k_unDoublePropertyTag, vr::k_unDoublePropertyTag};
|
||||
vr::PathWrite_t writes[6]{};
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
writes[i].ulPath = metadata_paths[i];
|
||||
writes[i].writeType = vr::PropertyWrite_Set;
|
||||
writes[i].pvBuffer = values[i];
|
||||
writes[i].unBufferSize = sizes[i];
|
||||
writes[i].unTag = tags[i];
|
||||
}
|
||||
paths->WritePathBatch(block, writes, 6);
|
||||
queue->ReleaseWriteOnlyBlock(queue_handle, block);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
#pragma once
|
||||
#include "openvr_driver.h"
|
||||
#include "ovrd_camera_queue.hpp"
|
||||
#include "rifts_passthrough_shm.h"
|
||||
#include "xrt/xrt_device.h"
|
||||
#include "xrt/xrt_camera.h"
|
||||
#include "xrt/xrt_tracking.h"
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
class MonadoCamera final : public vr::IVRCameraComponent
|
||||
{
|
||||
public:
|
||||
explicit MonadoCamera(xrt_device *device, vr::IVRBlockQueue *queue = nullptr, vr::IVRPaths *paths = nullptr);
|
||||
bool
|
||||
native_ready() const
|
||||
{
|
||||
return queue_handle != 0;
|
||||
}
|
||||
~MonadoCamera();
|
||||
bool
|
||||
GetCameraFrameDimensions(vr::ECameraVideoStreamFormat, uint32_t *, uint32_t *) override;
|
||||
bool
|
||||
GetCameraFrameBufferingRequirements(int *, uint32_t *) override;
|
||||
bool
|
||||
SetCameraFrameBuffering(int, void **, uint32_t) override;
|
||||
bool
|
||||
SetCameraVideoStreamFormat(vr::ECameraVideoStreamFormat format) override;
|
||||
vr::ECameraVideoStreamFormat
|
||||
GetCameraVideoStreamFormat() override
|
||||
{
|
||||
return vr::CVS_FORMAT_RGBX32;
|
||||
}
|
||||
bool
|
||||
StartVideoStream() override;
|
||||
void
|
||||
StopVideoStream() override;
|
||||
bool
|
||||
IsVideoStreamActive(bool *, float *) override;
|
||||
const vr::CameraVideoStreamFrame_t *
|
||||
GetVideoStreamFrame() override;
|
||||
void
|
||||
ReleaseVideoStreamFrame(const vr::CameraVideoStreamFrame_t *) override;
|
||||
// Exposure belongs to tracking. Accept its existing automatic mode only.
|
||||
bool
|
||||
SetAutoExposure(bool enable) override
|
||||
{
|
||||
return enable;
|
||||
}
|
||||
bool
|
||||
PauseVideoStream() override;
|
||||
bool
|
||||
ResumeVideoStream() override;
|
||||
bool
|
||||
GetCameraDistortion(uint32_t, float, float, float *, float *) override;
|
||||
bool
|
||||
GetCameraProjection(uint32_t, vr::EVRTrackedCameraFrameType, float, float, vr::HmdMatrix44_t *) override;
|
||||
bool
|
||||
SetFrameRate(int isp, int sensor) override
|
||||
{
|
||||
return isp == 30 && (sensor == 30 || sensor == 60);
|
||||
}
|
||||
bool
|
||||
SetCameraVideoSinkCallback(vr::ICameraVideoSinkCallback *) override;
|
||||
bool
|
||||
GetCameraCompatibilityMode(vr::ECameraCompatibilityMode *) override;
|
||||
bool
|
||||
SetCameraCompatibilityMode(vr::ECameraCompatibilityMode mode) override
|
||||
{
|
||||
return mode == vr::CAMERA_COMPAT_MODE_BULK_DEFAULT;
|
||||
}
|
||||
bool
|
||||
GetCameraFrameBounds(vr::EVRTrackedCameraFrameType, uint32_t *, uint32_t *, uint32_t *, uint32_t *) override;
|
||||
bool
|
||||
GetCameraIntrinsics(uint32_t,
|
||||
vr::EVRTrackedCameraFrameType,
|
||||
vr::HmdVector2_t *,
|
||||
vr::HmdVector2_t *,
|
||||
vr::EVRDistortionFunctionType *,
|
||||
double *) override;
|
||||
|
||||
static void
|
||||
NotifyButton(enum xrt_input_name name, bool pressed);
|
||||
|
||||
private:
|
||||
struct Buffer
|
||||
{
|
||||
vr::CameraVideoStreamFrame_t frame{};
|
||||
void *data = nullptr;
|
||||
bool ready = false, acquired = false;
|
||||
};
|
||||
xrt_device *device;
|
||||
xrt_camera *camera;
|
||||
std::mutex mutex;
|
||||
std::recursive_mutex callback_mutex;
|
||||
vr::ICameraVideoSinkCallback *callback = nullptr;
|
||||
std::vector<Buffer> buffers;
|
||||
std::vector<std::vector<uint8_t>> storage;
|
||||
bool active = false, paused = false; // Protected by mutex.
|
||||
std::thread worker;
|
||||
int64_t start_ns = 0;
|
||||
uint64_t stream_generation = 0, native_sequence = 0;
|
||||
uint32_t frame_sequence = 0;
|
||||
|
||||
int shm_fd = -1;
|
||||
struct rifts_passthrough_shm *shm = nullptr;
|
||||
std::atomic<bool> worker_running{false};
|
||||
uint64_t export_sequence = 0;
|
||||
int64_t last_menu_press_ns = 0;
|
||||
bool system_pressed = false, menu_pressed = false;
|
||||
vr::IVRBlockQueue *queue;
|
||||
vr::IVRPaths *paths;
|
||||
vr::PropertyContainerHandle_t queue_handle = 0;
|
||||
vr::PathHandle_t metadata_paths[6]{};
|
||||
bool
|
||||
initialize_queue();
|
||||
void
|
||||
publish_queue(const uint8_t *pixels, int64_t timestamp, uint64_t sequence, double interval);
|
||||
static std::atomic<MonadoCamera *> s_instance;
|
||||
|
||||
void
|
||||
run();
|
||||
uint32_t
|
||||
bytes() const
|
||||
{
|
||||
return camera->view_width * 2 * camera->view_height * 4;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
// Public OpenVR C API declarations adapted by Rectus, openvr_camera_sim.
|
||||
// https://github.com/Rectus/openvr_camera_sim/blob/4415906bfa4a97a3ae97acaa6b6025560dd140f8/vr_blockqueue.h
|
||||
// Converted to C++ from the OpenVR C API.
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef _OPENVR_BLOCKQUEUE
|
||||
#define _OPENVR_BLOCKQUEUE
|
||||
|
||||
#include "openvr_driver.h"
|
||||
|
||||
namespace vr {
|
||||
typedef uint64_t PathHandle_t;
|
||||
|
||||
struct PathWrite_t
|
||||
{
|
||||
PathHandle_t ulPath;
|
||||
enum EPropertyWriteType writeType;
|
||||
enum ETrackedPropertyError eSetError;
|
||||
void *pvBuffer;
|
||||
uint32_t unBufferSize;
|
||||
PropertyTypeTag_t unTag;
|
||||
enum ETrackedPropertyError eError;
|
||||
const char *pszPath;
|
||||
};
|
||||
|
||||
struct PathRead_t
|
||||
{
|
||||
PathHandle_t ulPath;
|
||||
void *pvBuffer;
|
||||
uint32_t unBufferSize;
|
||||
PropertyTypeTag_t unTag;
|
||||
uint32_t unRequiredBufferSize;
|
||||
enum ETrackedPropertyError eError;
|
||||
const char *pszPath;
|
||||
};
|
||||
|
||||
typedef enum EBlockQueueError
|
||||
{
|
||||
EBlockQueueError_BlockQueueError_None = 0,
|
||||
EBlockQueueError_BlockQueueError_QueueAlreadyExists = 1,
|
||||
EBlockQueueError_BlockQueueError_QueueNotFound = 2,
|
||||
EBlockQueueError_BlockQueueError_BlockNotAvailable = 3,
|
||||
EBlockQueueError_BlockQueueError_InvalidHandle = 4,
|
||||
EBlockQueueError_BlockQueueError_InvalidParam = 5,
|
||||
EBlockQueueError_BlockQueueError_ParamMismatch = 6,
|
||||
EBlockQueueError_BlockQueueError_InternalError = 7,
|
||||
EBlockQueueError_BlockQueueError_AlreadyInitialized = 8,
|
||||
EBlockQueueError_BlockQueueError_OperationIsServerOnly = 9,
|
||||
EBlockQueueError_BlockQueueError_TooManyConnections = 10,
|
||||
} EBlockQueueError;
|
||||
|
||||
typedef enum EBlockQueueReadType
|
||||
{
|
||||
EBlockQueueReadType_BlockQueueRead_Latest = 0,
|
||||
EBlockQueueReadType_BlockQueueRead_New = 1,
|
||||
EBlockQueueReadType_BlockQueueRead_Next = 2,
|
||||
} EBlockQueueReadType;
|
||||
|
||||
typedef enum EBlockQueueCreationFlag
|
||||
{
|
||||
EBlockQueueCreationFlag_BlockQueueFlag_OwnerIsReader = 1,
|
||||
} EBlockQueueCreationFlag;
|
||||
|
||||
class IVRPaths
|
||||
{
|
||||
public:
|
||||
virtual ETrackedPropertyError
|
||||
ReadPathBatch(PropertyContainerHandle_t ulRootHandle, PathRead_t *pBatch, uint32_t unBatchEntryCount) = 0;
|
||||
virtual ETrackedPropertyError
|
||||
WritePathBatch(PropertyContainerHandle_t ulRootHandle, PathWrite_t *pBatch, uint32_t unBatchEntryCount) = 0;
|
||||
virtual ETrackedPropertyError
|
||||
StringToHandle(PathHandle_t *pHandle, const char *pchPath) = 0;
|
||||
virtual ETrackedPropertyError
|
||||
HandleToString(PathHandle_t pHandle,
|
||||
const char *pchBuffer,
|
||||
uint32_t unBufferSize,
|
||||
uint32_t *punBufferSizeUsed) = 0;
|
||||
};
|
||||
|
||||
class IVRBlockQueue
|
||||
{
|
||||
public:
|
||||
virtual EBlockQueueError
|
||||
Create(PropertyContainerHandle_t *pulQueueHandle,
|
||||
const char *pchPath,
|
||||
uint32_t unBlockDataSize,
|
||||
uint32_t unBlockHeaderSize,
|
||||
uint32_t unBlockCount,
|
||||
uint32_t unFlags) = 0;
|
||||
|
||||
virtual EBlockQueueError
|
||||
Connect(PropertyContainerHandle_t *pulQueueHandle, const char *pchPath) = 0;
|
||||
|
||||
virtual EBlockQueueError
|
||||
Destroy(PropertyContainerHandle_t ulQueueHandle) = 0;
|
||||
|
||||
virtual EBlockQueueError
|
||||
AcquireWriteOnlyBlock(PropertyContainerHandle_t ulQueueHandle,
|
||||
PropertyContainerHandle_t *pulBlockHandle,
|
||||
void **ppvBuffer) = 0;
|
||||
|
||||
virtual EBlockQueueError
|
||||
ReleaseWriteOnlyBlock(PropertyContainerHandle_t ulQueueHandle, PropertyContainerHandle_t ulBlockHandle) = 0;
|
||||
|
||||
virtual EBlockQueueError
|
||||
WaitAndAcquireReadOnlyBlock(PropertyContainerHandle_t ulQueueHandle,
|
||||
PropertyContainerHandle_t *pulBlockHandle,
|
||||
void **ppvBuffer,
|
||||
EBlockQueueReadType eReadType,
|
||||
uint32_t unTimeoutMs) = 0;
|
||||
|
||||
virtual EBlockQueueError
|
||||
AcquireReadOnlyBlock(PropertyContainerHandle_t ulQueueHandle,
|
||||
PropertyContainerHandle_t *pulBlockHandle,
|
||||
void **ppvBuffer,
|
||||
EBlockQueueReadType eReadType) = 0;
|
||||
|
||||
virtual EBlockQueueError
|
||||
ReleaseReadOnlyBlock(PropertyContainerHandle_t ulQueueHandle, PropertyContainerHandle_t ulBlockHandle) = 0;
|
||||
|
||||
virtual EBlockQueueError
|
||||
QueueHasReader(PropertyContainerHandle_t ulQueueHandle, bool *pbHasReaders) = 0;
|
||||
};
|
||||
|
||||
|
||||
inline constexpr const char *IVRPaths_Version = "IVRPaths_001";
|
||||
inline constexpr const char *IVRBlockQueue_Version = "IVRBlockQueue_005";
|
||||
|
||||
|
||||
} // namespace vr
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
#pragma once
|
||||
|
||||
#include "xrt/xrt_device.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
// Match the upright eye FOV to the green-channel pixel density at the panel centre.
|
||||
// Physical panel dimensions and rotation describe the output, not the source texture.
|
||||
inline void
|
||||
ovrd_get_recommended_render_target_size(xrt_device *xdev, float scale, uint32_t *width, uint32_t *height)
|
||||
{
|
||||
double max_width = 0, max_height = 0;
|
||||
for (uint32_t eye = 0; eye < xdev->hmd->view_count; eye++) {
|
||||
const auto &display = xdev->hmd->views[eye].display;
|
||||
double view_width = display.w_pixels, view_height = display.h_pixels;
|
||||
const auto &fov = xdev->hmd->distortion.fov[eye];
|
||||
const double tan_width = std::tan(fov.angle_right) - std::tan(fov.angle_left);
|
||||
const double tan_height = std::tan(fov.angle_up) - std::tan(fov.angle_down);
|
||||
if (xdev->compute_distortion != nullptr && view_width > 0 && view_height > 0) {
|
||||
xrt_uv_triplet left{}, right{}, up{}, down{};
|
||||
const float du = 0.5f / view_width, dv = 0.5f / view_height;
|
||||
if (xdev->compute_distortion(xdev, eye, 0.5f - du, 0.5f, &left) == XRT_SUCCESS &&
|
||||
xdev->compute_distortion(xdev, eye, 0.5f + du, 0.5f, &right) == XRT_SUCCESS &&
|
||||
xdev->compute_distortion(xdev, eye, 0.5f, 0.5f - dv, &up) == XRT_SUCCESS &&
|
||||
xdev->compute_distortion(xdev, eye, 0.5f, 0.5f + dv, &down) == XRT_SUCCESS) {
|
||||
// Each pair spans one upright panel pixel. UV differences become tangent differences.
|
||||
const double dx = std::hypot((right.g.x - left.g.x) * tan_width,
|
||||
(right.g.y - left.g.y) * tan_height);
|
||||
const double dy = std::hypot((down.g.x - up.g.x) * tan_width,
|
||||
(down.g.y - up.g.y) * tan_height);
|
||||
const double density = 1.0 / std::min(dx, dy);
|
||||
if (std::isfinite(density) && dx > 0 && dy > 0 && tan_width > 0 && tan_height > 0) {
|
||||
view_width = tan_width * density;
|
||||
view_height = tan_height * density;
|
||||
}
|
||||
}
|
||||
}
|
||||
max_width = std::max(max_width, view_width);
|
||||
max_height = std::max(max_height, view_height);
|
||||
}
|
||||
*width = (uint32_t)std::ceil(max_width * scale);
|
||||
*height = (uint32_t)std::ceil(max_height * scale);
|
||||
}
|
||||
|
||||
// OpenVR top/bottom are the minimum/maximum Y-up tangents, despite their names.
|
||||
// Verified against SteamVR's projection matrix and a Rift S headset A/B test.
|
||||
inline void
|
||||
ovrd_get_projection_raw(const xrt_fov &fov, float *left, float *right, float *top, float *bottom)
|
||||
{
|
||||
*left = std::tan(fov.angle_left);
|
||||
*right = std::tan(fov.angle_right);
|
||||
*top = std::tan(fov.angle_down);
|
||||
*bottom = std::tan(fov.angle_up);
|
||||
}
|
||||
@@ -15,9 +15,18 @@
|
||||
#include <cassert>
|
||||
#include <array>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <cstdio>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
|
||||
#include "math/m_api.h"
|
||||
#include "ovrd_log.hpp"
|
||||
#include "ovrd_pose.hpp"
|
||||
#include "ovrd_display.hpp"
|
||||
#include "ovrd_camera.hpp"
|
||||
#include "ovrd_roomsetup_guard_process.hpp"
|
||||
#include <memory>
|
||||
#include "openvr_driver.h"
|
||||
|
||||
extern "C" {
|
||||
@@ -52,12 +61,79 @@ extern "C" {
|
||||
//! controllers, but input mapping may be incomplete or not ideal.
|
||||
DEBUG_GET_ONCE_BOOL_OPTION(emulate_index_controller, "STEAMVR_EMULATE_INDEX_CONTROLLER", false)
|
||||
|
||||
DEBUG_GET_ONCE_NUM_OPTION(scale_percentage, "XRT_COMPOSITOR_SCALE_PERCENTAGE", 140)
|
||||
DEBUG_GET_ONCE_BOOL_OPTION(steamvr_bench, "MONADO_STEAMVR_BENCH", false)
|
||||
|
||||
DEBUG_GET_ONCE_BOOL_OPTION(native_room_setup, "STEAMVR_NATIVE_ROOM_SETUP", true)
|
||||
DEBUG_GET_ONCE_BOOL_OPTION(native_room_setup_vrlink, "STEAMVR_NATIVE_ROOM_SETUP_VRLINK", false)
|
||||
|
||||
// The recommended size already matches the panel-centre pixel density. SteamVR adds its own GPU-based
|
||||
// supersampling on top, so the upstream 140 % default for Monado's compositor would apply twice.
|
||||
DEBUG_GET_ONCE_NUM_OPTION(scale_percentage, "XRT_COMPOSITOR_SCALE_PERCENTAGE", 100)
|
||||
|
||||
#define MODELNUM_LEN (XRT_DEVICE_NAME_LEN + 9) // "[Monado] "
|
||||
|
||||
#define OPENVR_BONE_COUNT 31
|
||||
|
||||
static bool
|
||||
is_rift_s_touch(const struct xrt_device *xdev)
|
||||
{
|
||||
static const char prefix[] = "Oculus Rift S";
|
||||
return xdev->name == XRT_DEVICE_TOUCH_CONTROLLER && strncmp(xdev->str, prefix, sizeof(prefix) - 1) == 0;
|
||||
}
|
||||
|
||||
// DebugRequest bench commands expire so a stopped script cannot hold a trigger.
|
||||
// Commands: bench x y z pitch trigger; bench6 x y z pitch yaw roll trigger.
|
||||
// Angles are radians. "bench off" returns to tracking.
|
||||
struct BenchInput
|
||||
{
|
||||
std::mutex mutex;
|
||||
xrt_pose pose = XRT_POSE_IDENTITY;
|
||||
float trigger = 0;
|
||||
int64_t until_ns = 0;
|
||||
|
||||
void request(const char *request, char *response, uint32_t size)
|
||||
{
|
||||
if (size) response[0] = 0;
|
||||
if (!debug_get_bool_option_steamvr_bench()) return;
|
||||
std::lock_guard lock(mutex);
|
||||
if (strcmp(request, "bench off") == 0) {
|
||||
until_ns = 0;
|
||||
if (size) snprintf(response, size, "ok");
|
||||
return;
|
||||
}
|
||||
float x, y, z, pitch, yaw = 0, roll = 0, value;
|
||||
char extra;
|
||||
bool parsed = sscanf(request, "bench6 %f %f %f %f %f %f %f %c", &x, &y, &z, &pitch, &yaw, &roll, &value, &extra) == 7;
|
||||
if (!parsed) parsed = sscanf(request, "bench %f %f %f %f %f %c", &x, &y, &z, &pitch, &value, &extra) == 5;
|
||||
if (!parsed || !std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z) ||
|
||||
!std::isfinite(pitch) || !std::isfinite(yaw) || !std::isfinite(roll) || !std::isfinite(value)) return;
|
||||
pose = XRT_POSE_IDENTITY;
|
||||
pose.position = {x, y, z};
|
||||
const xrt_vec3 angles{pitch, yaw, roll};
|
||||
math_quat_from_euler_angles(&angles, &pose.orientation);
|
||||
trigger = std::clamp(value, 0.0f, 1.0f);
|
||||
until_ns = os_monotonic_get_ns() + 500 * U_TIME_1MS_IN_NS;
|
||||
if (size) snprintf(response, size, "ok");
|
||||
}
|
||||
|
||||
bool sample(xrt_pose &out, float &value)
|
||||
{
|
||||
if (!debug_get_bool_option_steamvr_bench()) return false;
|
||||
std::lock_guard lock(mutex);
|
||||
if (os_monotonic_get_ns() >= until_ns) return false;
|
||||
out = pose;
|
||||
value = trigger;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
static void bench_relation(xrt_space_relation &relation, const xrt_pose &pose)
|
||||
{
|
||||
relation = XRT_SPACE_RELATION_ZERO;
|
||||
relation.pose = pose;
|
||||
relation.relation_flags = XRT_SPACE_RELATION_BITMASK_ALL;
|
||||
}
|
||||
|
||||
// Debug define(s), always off.
|
||||
#undef DUMP_POSE
|
||||
#undef DUMP_POSE_CONTROLLERS
|
||||
@@ -76,14 +152,14 @@ struct MonadoInputComponent
|
||||
|
||||
struct SteamVRDriverControl
|
||||
{
|
||||
const char *steamvr_control_path;
|
||||
std::string steamvr_control_path;
|
||||
vr::VRInputComponentHandle_t control_handle;
|
||||
};
|
||||
|
||||
struct SteamVRDriverControlInput : SteamVRDriverControl
|
||||
{
|
||||
enum xrt_input_type monado_input_type;
|
||||
enum xrt_input_name monado_input_name;
|
||||
struct xrt_input *monado_input;
|
||||
|
||||
struct MonadoInputComponent component;
|
||||
};
|
||||
@@ -94,56 +170,6 @@ struct SteamVRDriverControlOutput : SteamVRDriverControl
|
||||
enum xrt_output_name monado_output_name;
|
||||
};
|
||||
|
||||
static void
|
||||
copy_vec3(struct xrt_vec3 *from, double *to)
|
||||
{
|
||||
to[0] = from->x;
|
||||
to[1] = from->y;
|
||||
to[2] = from->z;
|
||||
}
|
||||
|
||||
static void
|
||||
copy_quat(struct xrt_quat *from, vr::HmdQuaternion_t *to)
|
||||
{
|
||||
to->x = from->x;
|
||||
to->y = from->y;
|
||||
to->z = from->z;
|
||||
to->w = from->w;
|
||||
}
|
||||
|
||||
static void
|
||||
apply_pose(struct xrt_space_relation *rel, vr::DriverPose_t *m_pose)
|
||||
{
|
||||
if ((rel->relation_flags & XRT_SPACE_RELATION_ORIENTATION_TRACKED_BIT) != 0) {
|
||||
copy_quat(&rel->pose.orientation, &m_pose->qRotation);
|
||||
} else {
|
||||
m_pose->result = vr::TrackingResult_Running_OutOfRange;
|
||||
m_pose->poseIsValid = false;
|
||||
}
|
||||
|
||||
if ((rel->relation_flags & XRT_SPACE_RELATION_POSITION_TRACKED_BIT) != 0) {
|
||||
copy_vec3(&rel->pose.position, m_pose->vecPosition);
|
||||
} else {
|
||||
}
|
||||
|
||||
if ((rel->relation_flags & XRT_SPACE_RELATION_LINEAR_VELOCITY_VALID_BIT) != 0) {
|
||||
// linear velocity in world space
|
||||
copy_vec3(&rel->linear_velocity, m_pose->vecVelocity);
|
||||
}
|
||||
|
||||
if ((rel->relation_flags & XRT_SPACE_RELATION_ANGULAR_VELOCITY_VALID_BIT) != 0) {
|
||||
// angular velocity reported by monado in world space,
|
||||
// expected by steamvr to be in "controller space"
|
||||
struct xrt_quat orientation_inv;
|
||||
math_quat_invert(&rel->pose.orientation, &orientation_inv);
|
||||
|
||||
struct xrt_vec3 vel;
|
||||
math_quat_rotate_derivative(&orientation_inv, &rel->angular_velocity, &vel);
|
||||
|
||||
copy_vec3(&vel, m_pose->vecAngularVelocity);
|
||||
}
|
||||
}
|
||||
|
||||
#define OPENVR_BONE_COUNT 31
|
||||
|
||||
// these are currently only used for the root and wrist transforms, but are kept here as they are useful for debugging.
|
||||
@@ -368,8 +394,11 @@ hand_joint_set_to_bone_transform(struct xrt_hand_joint_set hand_joint_set,
|
||||
class CDeviceDriver_Monado_Controller : public vr::ITrackedDeviceServerDriver
|
||||
{
|
||||
public:
|
||||
CDeviceDriver_Monado_Controller(struct xrt_instance *xinst, struct xrt_device *xdev, enum xrt_hand hand)
|
||||
: m_xdev(xdev), m_hand(hand)
|
||||
CDeviceDriver_Monado_Controller(struct xrt_instance *xinst,
|
||||
struct xrt_device *xdev,
|
||||
enum xrt_hand hand,
|
||||
struct xrt_device *xhmd = NULL)
|
||||
: m_xdev(xdev), m_xhmd(xhmd), m_hand(hand)
|
||||
{
|
||||
ovrd_log("Creating Controller %s\n", xdev->str);
|
||||
|
||||
@@ -403,6 +432,16 @@ public:
|
||||
}
|
||||
break;
|
||||
case XRT_DEVICE_TOUCH_CONTROLLER:
|
||||
if (is_rift_s_touch(xdev)) {
|
||||
if (hand == XRT_HAND_LEFT) {
|
||||
m_render_model = "oculus_rifts_controller_left";
|
||||
}
|
||||
if (hand == XRT_HAND_RIGHT) {
|
||||
m_render_model = "oculus_rifts_controller_right";
|
||||
}
|
||||
m_grip_to_model = rift_s_touch_grip_to_model(hand);
|
||||
break;
|
||||
}
|
||||
if (hand == XRT_HAND_LEFT) {
|
||||
m_render_model = "oculus_cv1_controller_left";
|
||||
}
|
||||
@@ -444,39 +483,55 @@ public:
|
||||
{
|
||||
enum xrt_input_type monado_input_type = XRT_GET_INPUT_TYPE(monado_input_name);
|
||||
|
||||
SteamVRDriverControlInput in;
|
||||
SteamVRDriverControlInput in{};
|
||||
|
||||
in.monado_input_type = monado_input_type;
|
||||
in.steamvr_control_path = steamvr_control_path;
|
||||
in.monado_input_name = monado_input_name;
|
||||
in.monado_input = NULL;
|
||||
for (uint32_t i = 0; i < m_xdev->input_count; i++) {
|
||||
if (m_xdev->inputs[i].name == monado_input_name) {
|
||||
in.monado_input = &m_xdev->inputs[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (in.monado_input == NULL) {
|
||||
ovrd_log("Skipping unsupported input %s on %s\n", steamvr_control_path, m_xdev->str);
|
||||
return;
|
||||
}
|
||||
if (component != NULL) {
|
||||
in.component = *component;
|
||||
} else {
|
||||
in.component.has_component = false;
|
||||
}
|
||||
|
||||
vr::EVRInputError err = vr::VRInputError_None;
|
||||
if (monado_input_type == XRT_INPUT_TYPE_BOOLEAN) {
|
||||
vr::VRDriverInput()->CreateBooleanComponent(m_ulPropertyContainer, steamvr_control_path,
|
||||
err = vr::VRDriverInput()->CreateBooleanComponent(m_ulPropertyContainer, steamvr_control_path,
|
||||
&in.control_handle);
|
||||
|
||||
} else if (monado_input_type == XRT_INPUT_TYPE_VEC1_MINUS_ONE_TO_ONE) {
|
||||
vr::VRDriverInput()->CreateScalarComponent(m_ulPropertyContainer, steamvr_control_path,
|
||||
err = vr::VRDriverInput()->CreateScalarComponent(m_ulPropertyContainer, steamvr_control_path,
|
||||
&in.control_handle, vr::VRScalarType_Absolute,
|
||||
vr::VRScalarUnits_NormalizedTwoSided);
|
||||
|
||||
} else if (monado_input_type == XRT_INPUT_TYPE_VEC1_ZERO_TO_ONE) {
|
||||
vr::VRDriverInput()->CreateScalarComponent(m_ulPropertyContainer, steamvr_control_path,
|
||||
err = vr::VRDriverInput()->CreateScalarComponent(m_ulPropertyContainer, steamvr_control_path,
|
||||
&in.control_handle, vr::VRScalarType_Absolute,
|
||||
vr::VRScalarUnits_NormalizedOneSided);
|
||||
|
||||
} else if (monado_input_type == XRT_INPUT_TYPE_VEC2_MINUS_ONE_TO_ONE) {
|
||||
// 2D values are added as 2 1D values
|
||||
// usually those are [-1,1]
|
||||
vr::VRDriverInput()->CreateScalarComponent(m_ulPropertyContainer, steamvr_control_path,
|
||||
err = vr::VRDriverInput()->CreateScalarComponent(m_ulPropertyContainer, steamvr_control_path,
|
||||
&in.control_handle, vr::VRScalarType_Absolute,
|
||||
vr::VRScalarUnits_NormalizedTwoSided);
|
||||
}
|
||||
|
||||
if (err != vr::VRInputError_None) {
|
||||
ovrd_log("Failed to create input %s on %s: %d\n", steamvr_control_path, m_xdev->str, err);
|
||||
return;
|
||||
}
|
||||
|
||||
m_input_controls.push_back(in);
|
||||
ovrd_log("Added input %s\n", steamvr_control_path);
|
||||
}
|
||||
@@ -493,7 +548,7 @@ public:
|
||||
out.steamvr_control_path = steamvr_control_path;
|
||||
out.monado_output_name = monado_output_name;
|
||||
|
||||
vr::VRDriverInput()->CreateHapticComponent(m_ulPropertyContainer, out.steamvr_control_path,
|
||||
vr::VRDriverInput()->CreateHapticComponent(m_ulPropertyContainer, out.steamvr_control_path.c_str(),
|
||||
&out.control_handle);
|
||||
|
||||
m_output_controls.push_back(out);
|
||||
@@ -507,11 +562,11 @@ public:
|
||||
{
|
||||
enum xrt_input_type monado_input_type = XRT_GET_INPUT_TYPE(monado_input_name);
|
||||
|
||||
SteamVRDriverControlInput in;
|
||||
SteamVRDriverControlInput in{};
|
||||
|
||||
in.monado_input_type = monado_input_type;
|
||||
in.steamvr_control_path = steamvr_control_path;
|
||||
in.monado_input_name = monado_input_name;
|
||||
in.monado_input = NULL;
|
||||
in.component.has_component = false;
|
||||
|
||||
vr::EVRInputError err = vr::VRDriverInput()->CreateSkeletonComponent(
|
||||
@@ -772,6 +827,14 @@ public:
|
||||
enum xrt_input_name monado_input_name = b->input;
|
||||
const char *steamvr_path = b->steamvr_path;
|
||||
|
||||
// SteamVR's oculus_touch bindings toggle the dashboard from the left /input/system and leave the
|
||||
// right one (Oculus button) unbound. The Touch left menu button is the equivalent, so publish it
|
||||
// as system. Without this nothing can open the dashboard.
|
||||
if (m_xdev->name == XRT_DEVICE_TOUCH_CONTROLLER && m_hand == XRT_HAND_LEFT &&
|
||||
strcmp(steamvr_path, "/input/menu/click") == 0) {
|
||||
steamvr_path = "/input/system/click";
|
||||
}
|
||||
|
||||
enum xrt_input_type monado_input_type = XRT_GET_INPUT_TYPE(monado_input_name);
|
||||
|
||||
switch (monado_input_type) {
|
||||
@@ -809,6 +872,10 @@ public:
|
||||
|
||||
for (size_t i = 0; i < p->binding_count; i++) {
|
||||
struct binding_template *b = &p->bindings[i];
|
||||
const char *hand_path = m_hand == XRT_HAND_LEFT ? "/user/hand/left" : "/user/hand/right";
|
||||
if (strcmp(b->subaction_path, hand_path) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (b->input != 0) {
|
||||
AddMonadoInput(b);
|
||||
@@ -894,12 +961,14 @@ public:
|
||||
}
|
||||
|
||||
m_input_profile = std::string("{monado}/input/") + std::string(p->steamvr_input_profile_path);
|
||||
m_controller_type = p->steamvr_controller_type;
|
||||
m_controller_type = m_xdev->name == XRT_DEVICE_TOUCH_CONTROLLER ? "monado_oculus_touch"
|
||||
: p->steamvr_controller_type;
|
||||
}
|
||||
|
||||
ovrd_log("Using input profile %s\n", m_input_profile.c_str());
|
||||
ovrd_log("Using render model%s\n", m_render_model);
|
||||
vr::VRProperties()->SetStringProperty(m_ulPropertyContainer, vr::Prop_InputProfilePath_String, m_input_profile.c_str());
|
||||
vr::VRProperties()->SetStringProperty(m_ulPropertyContainer, vr::Prop_ControllerType_String, m_controller_type);
|
||||
vr::VRProperties()->SetStringProperty(m_ulPropertyContainer, vr::Prop_RenderModelName_String, m_render_model);
|
||||
vr::VRProperties()->SetStringProperty(m_ulPropertyContainer, vr::Prop_ModelNumber_String, m_xdev->str);
|
||||
|
||||
@@ -951,8 +1020,7 @@ public:
|
||||
void
|
||||
DebugRequest(const char *pchRequest, char *pchResponseBuffer, uint32_t unResponseBufferSize)
|
||||
{
|
||||
if (unResponseBufferSize >= 1)
|
||||
pchResponseBuffer[0] = 0;
|
||||
m_bench.request(pchRequest, pchResponseBuffer, unResponseBufferSize);
|
||||
}
|
||||
|
||||
vr::DriverPose_t
|
||||
@@ -1002,12 +1070,42 @@ public:
|
||||
|
||||
struct xrt_pose *offset = &m_xdev->tracking_origin->initial_offset;
|
||||
|
||||
struct xrt_relation_chain chain = {};
|
||||
m_relation_chain_push_relation(&chain, &rel);
|
||||
m_relation_chain_push_pose_if_not_identity(&chain, offset);
|
||||
m_relation_chain_resolve(&chain, &rel);
|
||||
if (is_rift_s_touch(m_xdev)) {
|
||||
transform_rift_s_touch_relation(&rel, &m_grip_to_model, offset);
|
||||
} else {
|
||||
struct xrt_relation_chain chain = {};
|
||||
m_relation_chain_push_pose_if_not_identity(&chain, &m_grip_to_model);
|
||||
m_relation_chain_push_relation(&chain, &rel);
|
||||
m_relation_chain_push_pose_if_not_identity(&chain, offset);
|
||||
m_relation_chain_resolve(&chain, &rel);
|
||||
}
|
||||
|
||||
apply_pose(&rel, &m_pose);
|
||||
xrt_pose bench_pose;
|
||||
float bench_trigger;
|
||||
if (is_rift_s_touch(m_xdev) && m_bench.sample(bench_pose, bench_trigger))
|
||||
bench_relation(rel, bench_pose);
|
||||
|
||||
if (is_rift_s_touch(m_xdev))
|
||||
apply_rift_s_touch_pose(&rel, &m_pose);
|
||||
else
|
||||
apply_pose(&rel, &m_pose, XRT_SPACE_RELATION_POSITION_VALID_BIT,
|
||||
m_xdev->name == XRT_DEVICE_TOUCH_CONTROLLER);
|
||||
if ((rel.relation_flags & XRT_SPACE_RELATION_POSITION_VALID_BIT) != 0) {
|
||||
m_had_position = true;
|
||||
} else if (is_rift_s_touch(m_xdev) && !m_had_position && m_xhmd != NULL) {
|
||||
// Never fixed: SteamVR may still draw the invalid pose, at the tracking origin under
|
||||
// the floor. Keep it at a resting hand position below the head instead.
|
||||
struct xrt_space_relation head;
|
||||
xrt_device_get_tracked_pose(m_xhmd, XRT_INPUT_GENERIC_HEAD_POSE, now_ns, &head);
|
||||
struct xrt_relation_chain head_chain = {};
|
||||
m_relation_chain_push_relation(&head_chain, &head);
|
||||
m_relation_chain_push_pose_if_not_identity(&head_chain, offset);
|
||||
m_relation_chain_resolve(&head_chain, &head);
|
||||
if ((head.relation_flags & XRT_SPACE_RELATION_POSITION_VALID_BIT) != 0) {
|
||||
struct xrt_vec3 hand = nominal_hand_position(head.pose, m_hand);
|
||||
copy_vec3(&hand, m_pose.vecPosition);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef DUMP_POSE_CONTROLLERS
|
||||
ovrd_log("get controller %d pose %f %f %f %f, %f %f %f\n", m_unObjectId, m_pose.qRotation.x,
|
||||
@@ -1028,33 +1126,23 @@ public:
|
||||
RunFrame()
|
||||
{
|
||||
m_xdev->update_inputs(m_xdev);
|
||||
|
||||
xrt_pose bench_pose;
|
||||
float bench_trigger = 0;
|
||||
bool synthetic = is_rift_s_touch(m_xdev) && m_bench.sample(bench_pose, bench_trigger);
|
||||
|
||||
for (const auto &in : m_input_controls) {
|
||||
|
||||
// ovrd_log("Update %d: %s\n", i,
|
||||
// m_controls[i].steamvr_control_path);
|
||||
|
||||
enum xrt_input_name binding_name = in.monado_input_name;
|
||||
|
||||
struct xrt_input *input = NULL;
|
||||
for (uint32_t ii = 0; ii < m_xdev->input_count; ii++) {
|
||||
if (m_xdev->inputs[ii].name == binding_name) {
|
||||
input = &m_xdev->inputs[ii];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (input == NULL) {
|
||||
ovrd_log("Input for %s not found!\n", in.steamvr_control_path);
|
||||
continue;
|
||||
}
|
||||
struct xrt_input *input = in.monado_input;
|
||||
|
||||
vr::VRInputComponentHandle_t handle = in.control_handle;
|
||||
|
||||
if (in.monado_input_type == XRT_INPUT_TYPE_BOOLEAN) {
|
||||
bool state = input->value.boolean;
|
||||
bool state = synthetic ? false : input->value.boolean;
|
||||
vr::VRDriverInput()->UpdateBooleanComponent(handle, state, 0);
|
||||
MonadoCamera::NotifyButton(input->name, state);
|
||||
// ovrd_log("Update %s: %d\n",
|
||||
// m_controls[i].steamvr_control_path, state);
|
||||
// U_LOG_D("Update %s: %d",
|
||||
@@ -1074,6 +1162,8 @@ public:
|
||||
value = input->value.vec1.x;
|
||||
}
|
||||
|
||||
if (synthetic)
|
||||
value = in.steamvr_control_path == "/input/trigger/value" ? bench_trigger : 0;
|
||||
vr::VRDriverInput()->UpdateScalarComponent(handle, value, 0);
|
||||
// ovrd_log("Update %s: %f\n",
|
||||
// m_controls[i].steamvr_control_path,
|
||||
@@ -1166,6 +1256,7 @@ public:
|
||||
|
||||
|
||||
struct xrt_device *m_xdev;
|
||||
BenchInput m_bench;
|
||||
vr::DriverPose_t m_pose;
|
||||
vr::TrackedDeviceIndex_t m_unObjectId;
|
||||
vr::PropertyContainerHandle_t m_ulPropertyContainer;
|
||||
@@ -1184,6 +1275,11 @@ private:
|
||||
const char *m_controller_type = NULL;
|
||||
|
||||
const char *m_render_model = NULL;
|
||||
//! Render-model origin in the grip frame; identity where the model origin is the grip.
|
||||
struct xrt_pose m_grip_to_model = XRT_POSE_IDENTITY;
|
||||
//! Head, for the resting position of a controller that has never had a position fix.
|
||||
struct xrt_device *m_xhmd = NULL;
|
||||
bool m_had_position = false;
|
||||
enum xrt_hand m_hand;
|
||||
bool m_handed_controller;
|
||||
|
||||
@@ -1247,6 +1343,8 @@ public:
|
||||
virtual void *GetComponent(const char *pchComponentNameAndVersion);
|
||||
virtual void DebugRequest(const char *pchRequest, char *pchResponseBuffer, uint32_t unResponseBufferSize);
|
||||
virtual vr::DriverPose_t GetPose();
|
||||
void UpdateRoomSetupGuard();
|
||||
void UpdateDisplayFrequency();
|
||||
|
||||
// IVRDisplayComponent
|
||||
virtual void GetWindowBounds(int32_t *pnX, int32_t *pnY, uint32_t *pnWidth, uint32_t *pnHeight);
|
||||
@@ -1261,6 +1359,12 @@ public:
|
||||
|
||||
private:
|
||||
struct xrt_device *m_xdev = NULL;
|
||||
BenchInput m_bench;
|
||||
std::unique_ptr<MonadoCamera> m_camera;
|
||||
vr::VRInputComponentHandle_t m_benchProximity = 0;
|
||||
RoomSetupGuardProcess m_roomSetupGuard;
|
||||
bool m_nativeSetupRequested = false;
|
||||
bool m_nativeSetupEnabled = false;
|
||||
|
||||
// clang-format off
|
||||
|
||||
@@ -1269,6 +1373,7 @@ private:
|
||||
|
||||
float m_flSecondsFromVsyncToPhotons = -1;
|
||||
float m_flDisplayFrequency = -1;
|
||||
uint64_t m_nextFrequencyCheckNs = 0;
|
||||
float m_flIPD = -1;
|
||||
|
||||
struct xrt_fov m_fovs[2];
|
||||
@@ -1300,6 +1405,39 @@ create_translation_rotation_matrix(struct xrt_pose *pose, struct vr::HmdMatrix34
|
||||
res->m[2][3] = t.z;
|
||||
}
|
||||
|
||||
void
|
||||
CDeviceDriver_Monado::UpdateDisplayFrequency()
|
||||
{
|
||||
// SteamVR's Vulkan renderer falls back to 90 Hz even after selecting the Rift S 80 Hz mode.
|
||||
// The Rift S has one display mode; do not override dynamically selected modes on other HMDs.
|
||||
if (strcmp(m_xdev->str, "Oculus Rift S") != 0) return;
|
||||
uint64_t now = os_monotonic_get_ns();
|
||||
if (now < m_nextFrequencyCheckNs) return;
|
||||
m_nextFrequencyCheckNs = now + 1000000000ULL;
|
||||
vr::ETrackedPropertyError error;
|
||||
auto properties = vr::VRProperties();
|
||||
float reported = properties->GetFloatProperty(m_ulPropertyContainer, vr::Prop_DisplayFrequency_Float, &error);
|
||||
if (error == vr::TrackedProp_Success && (!std::isfinite(reported) || std::abs(reported - m_flDisplayFrequency) > 0.01f)) {
|
||||
properties->SetFloatProperty(m_ulPropertyContainer, vr::Prop_DisplayFrequency_Float, m_flDisplayFrequency);
|
||||
ovrd_log("Restored Rift S display frequency %.3f Hz after compositor reported %.3f Hz\n",
|
||||
m_flDisplayFrequency, reported);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
CDeviceDriver_Monado::UpdateRoomSetupGuard()
|
||||
{
|
||||
bool enabled = m_nativeSetupRequested && m_roomSetupGuard.ready();
|
||||
if (enabled == m_nativeSetupEnabled) return;
|
||||
m_nativeSetupEnabled = enabled;
|
||||
auto properties = vr::VRProperties();
|
||||
properties->SetBoolProperty(m_ulPropertyContainer, static_cast<vr::ETrackedDeviceProperty>(11003), enabled);
|
||||
properties->SetBoolProperty(m_ulPropertyContainer, static_cast<vr::ETrackedDeviceProperty>(11004),
|
||||
enabled && debug_get_bool_option_native_room_setup_vrlink());
|
||||
vr::VRSettings()->SetBool("steamvr", "inHMDRoomSetupDebug", enabled);
|
||||
ovrd_log("Native room setup guard ready=%d\n", enabled);
|
||||
}
|
||||
|
||||
void
|
||||
CDeviceDriver_Monado::PoseUpdateThreadFunction()
|
||||
{
|
||||
@@ -1308,6 +1446,8 @@ CDeviceDriver_Monado::PoseUpdateThreadFunction()
|
||||
while (m_poseUpdating) {
|
||||
//! @todo figure out the best pose update rate
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
if (m_benchProximity)
|
||||
vr::VRDriverInput()->UpdateBooleanComponent(m_benchProximity, true, 0);
|
||||
vr::VRServerDriverHost()->TrackedDevicePoseUpdated(m_trackedDeviceIndex, GetPose(),
|
||||
sizeof(vr::DriverPose_t));
|
||||
}
|
||||
@@ -1335,11 +1475,84 @@ CDeviceDriver_Monado::Activate(vr::TrackedDeviceIndex_t unObjectId)
|
||||
vr::VRProperties()->SetFloatProperty(m_ulPropertyContainer, vr::Prop_DisplayFrequency_Float, m_flDisplayFrequency);
|
||||
vr::VRProperties()->SetFloatProperty( m_ulPropertyContainer, vr::Prop_SecondsFromVsyncToPhotons_Float, m_flSecondsFromVsyncToPhotons);
|
||||
|
||||
// return a constant that's not 0 (invalid) or 1 (reserved for Oculus)
|
||||
if (debug_get_bool_option_steamvr_bench()) {
|
||||
vr::VRProperties()->SetBoolProperty(m_ulPropertyContainer, vr::Prop_ContainsProximitySensor_Bool, true);
|
||||
auto proximity_error = vr::VRDriverInput()->CreateBooleanComponent(m_ulPropertyContainer, "/input/proximity", &m_benchProximity);
|
||||
ovrd_log("Bench proximity component: error=%d\n", proximity_error);
|
||||
ovrd_log("MONADO_STEAMVR_BENCH enabled: synthetic fallback and awake HMD\n");
|
||||
}
|
||||
// return a constant that's not 0 (invalid) or 1 (reserved for Oculus)
|
||||
vr::VRProperties()->SetUint64Property( m_ulPropertyContainer, vr::Prop_CurrentUniverseId_Uint64, 2);
|
||||
|
||||
if (m_xdev->camera) {
|
||||
vr::EVRInitError queue_error = vr::VRInitError_None;
|
||||
auto queue = static_cast<vr::IVRBlockQueue *>(vr::VRDriverContext()->GetGenericInterface(vr::IVRBlockQueue_Version, &queue_error));
|
||||
auto paths = static_cast<vr::IVRPaths *>(vr::VRDriverContext()->GetGenericInterface(vr::IVRPaths_Version, &queue_error));
|
||||
m_camera = std::make_unique<MonadoCamera>(m_xdev, queue, paths);
|
||||
ovrd_log("Rift S native camera queue: %s\n", m_camera->native_ready() ? "ready" : "unavailable");
|
||||
auto properties = vr::VRProperties();
|
||||
properties->SetBoolProperty(m_ulPropertyContainer, vr::Prop_HasCamera_Bool, true);
|
||||
properties->SetBoolProperty(m_ulPropertyContainer, vr::Prop_AllowCameraToggle_Bool, true);
|
||||
if (m_camera->native_ready()) {
|
||||
// SteamVR's legacy Room View gate treats these as compatibility
|
||||
// revisions. They do not describe the Rift S firmware.
|
||||
properties->SetUint64Property(m_ulPropertyContainer, vr::Prop_FPGAVersion_Uint64, 260);
|
||||
properties->SetUint64Property(m_ulPropertyContainer, vr::Prop_FirmwareVersion_Uint64, 1447390112);
|
||||
properties->SetUint64Property(m_ulPropertyContainer, vr::Prop_CameraFirmwareVersion_Uint64, 8590196809ULL);
|
||||
}
|
||||
// SteamVR 2.17's local setup entry gate requires its experimental flag.
|
||||
// Keep the VRLink request property off unless explicitly testing that path.
|
||||
vr::EVRSettingsError settings_error = vr::VRSettingsError_None;
|
||||
bool enable_setup = vr::VRSettings()->GetBool("driver_monado", "enableNativeRoomSetup", &settings_error);
|
||||
if (settings_error != vr::VRSettingsError_None) enable_setup = true;
|
||||
const bool native_setup = m_camera->native_ready() && enable_setup && debug_get_bool_option_native_room_setup();
|
||||
const bool vrlink = native_setup && debug_get_bool_option_native_room_setup_vrlink();
|
||||
constexpr auto supports_setup = static_cast<vr::ETrackedDeviceProperty>(11003);
|
||||
constexpr auto supports_vrlink_requests = static_cast<vr::ETrackedDeviceProperty>(11004);
|
||||
m_nativeSetupRequested = native_setup;
|
||||
if (native_setup && !m_roomSetupGuard.start()) ovrd_log("Native room setup guard could not start\n");
|
||||
properties->SetBoolProperty(m_ulPropertyContainer, supports_setup, false);
|
||||
properties->SetBoolProperty(m_ulPropertyContainer, supports_vrlink_requests, false);
|
||||
vr::EVRSettingsError flag_error = vr::VRSettingsError_None;
|
||||
vr::VRSettings()->SetBool("steamvr", "inHMDRoomSetupDebug", false, &flag_error);
|
||||
ovrd_log("Native room setup requested=%d: VRLink requests=%d, settings error=%d\n", native_setup, vrlink, flag_error);
|
||||
if (native_setup) {
|
||||
// The setup scene override still requires Room View to be enabled.
|
||||
// Use 2D reprojection: this camera supplies rectified stereo, not depth.
|
||||
auto settings = vr::VRSettings();
|
||||
settings->SetBool("camera", "enableCamera", true);
|
||||
settings->SetBool("camera", "enableConstructRoomView", true);
|
||||
settings->SetInt32("camera", "roomView", 1);
|
||||
settings->SetInt32("camera", "roomViewStyle", 4); // Opaque video.
|
||||
ovrd_log("Native room setup camera: enabled 2D opaque Room View\n");
|
||||
}
|
||||
properties->SetBoolProperty(m_ulPropertyContainer, vr::Prop_HasCameraComponent_Bool, true);
|
||||
properties->SetInt32Property(m_ulPropertyContainer, vr::Prop_NumCameras_Int32, 2);
|
||||
properties->SetBoolProperty(m_ulPropertyContainer, vr::Prop_SupportsRoomViewDepthProjection_Bool, false);
|
||||
properties->SetInt32Property(m_ulPropertyContainer, vr::Prop_CameraFrameLayout_Int32,
|
||||
vr::EVRTrackedCameraFrameLayout_Stereo | vr::EVRTrackedCameraFrameLayout_HorizontalLayout);
|
||||
properties->SetInt32Property(m_ulPropertyContainer, vr::Prop_CameraStreamFormat_Int32, vr::CVS_FORMAT_RGBX32);
|
||||
// The exported images are already rectified pinhole views. Public raw
|
||||
// camera clients need these properties as well as component intrinsics.
|
||||
int32_t distortion[2] = {vr::VRDistortionFunctionType_None, vr::VRDistortionFunctionType_None};
|
||||
double coefficients[2][vr::k_unMaxDistortionFunctionParameters]{};
|
||||
vr::HmdVector4_t white_balance[2] = {{{1, 1, 1, 1}}, {{1, 1, 1, 1}}};
|
||||
properties->SetProperty(m_ulPropertyContainer, vr::Prop_CameraDistortionFunction_Int32_Array,
|
||||
distortion, sizeof(distortion), vr::k_unInt32PropertyTag);
|
||||
properties->SetProperty(m_ulPropertyContainer, vr::Prop_CameraDistortionCoefficients_Float_Array,
|
||||
coefficients, sizeof(coefficients), vr::k_unFloatPropertyTag);
|
||||
properties->SetProperty(m_ulPropertyContainer, vr::Prop_CameraWhiteBalance_Vector4_Array,
|
||||
white_balance, sizeof(white_balance), vr::k_unHmdVector4PropertyTag);
|
||||
vr::HmdMatrix34_t transforms[2];
|
||||
for (int i = 0; i < 2; ++i) create_translation_rotation_matrix(&m_xdev->camera->head_from_camera[i], &transforms[i]);
|
||||
properties->SetProperty(m_ulPropertyContainer, vr::Prop_CameraToHeadTransform_Matrix34,
|
||||
&transforms[0], sizeof(transforms[0]), vr::k_unHmdMatrix34PropertyTag);
|
||||
properties->SetProperty(m_ulPropertyContainer, vr::Prop_CameraToHeadTransforms_Matrix34_Array,
|
||||
transforms, sizeof(transforms), vr::k_unHmdMatrix34PropertyTag);
|
||||
}
|
||||
|
||||
// avoid "not fullscreen" warnings from vrmonitor
|
||||
//vr::VRProperties()->SetBoolProperty(m_ulPropertyContainer, vr::Prop_IsOnDesktop_Bool, false);
|
||||
vr::VRProperties()->SetBoolProperty(m_ulPropertyContainer, vr::Prop_IsOnDesktop_Bool, IsDisplayOnDesktop());
|
||||
|
||||
// clang-format on
|
||||
|
||||
@@ -1365,6 +1578,8 @@ CDeviceDriver_Monado::Activate(vr::TrackedDeviceIndex_t unObjectId)
|
||||
void
|
||||
CDeviceDriver_Monado::Deactivate()
|
||||
{
|
||||
if (m_camera)
|
||||
m_camera->StopVideoStream();
|
||||
m_poseUpdating = false;
|
||||
m_poseUpdateThread->join();
|
||||
ovrd_log("Deactivate\n");
|
||||
@@ -1379,6 +1594,8 @@ CDeviceDriver_Monado::EnterStandby()
|
||||
void *
|
||||
CDeviceDriver_Monado::GetComponent(const char *pchComponentNameAndVersion)
|
||||
{
|
||||
if (m_camera && strcmp(pchComponentNameAndVersion, vr::IVRCameraComponent_Version) == 0)
|
||||
return m_camera.get();
|
||||
// clang-format off
|
||||
if (strcmp(pchComponentNameAndVersion, vr::IVRDisplayComponent_Version) == 0) {
|
||||
return (vr::IVRDisplayComponent *)this;
|
||||
@@ -1391,7 +1608,7 @@ CDeviceDriver_Monado::GetComponent(const char *pchComponentNameAndVersion)
|
||||
void
|
||||
CDeviceDriver_Monado::DebugRequest(const char *pchRequest, char *pchResponseBuffer, uint32_t unResponseBufferSize)
|
||||
{
|
||||
//! @todo
|
||||
m_bench.request(pchRequest, pchResponseBuffer, unResponseBufferSize);
|
||||
}
|
||||
|
||||
static constexpr inline vr::HmdQuaternion_t
|
||||
@@ -1420,6 +1637,16 @@ CDeviceDriver_Monado::GetPose()
|
||||
m_relation_chain_push_pose_if_not_identity(&chain, offset);
|
||||
m_relation_chain_resolve(&chain, &rel);
|
||||
|
||||
if (debug_get_bool_option_steamvr_bench()) {
|
||||
xrt_pose bench_pose = XRT_POSE_IDENTITY;
|
||||
bench_pose.position.y = 1.6f;
|
||||
float trigger;
|
||||
const auto tracked = XRT_SPACE_RELATION_ORIENTATION_TRACKED_BIT | XRT_SPACE_RELATION_POSITION_TRACKED_BIT |
|
||||
XRT_SPACE_RELATION_ORIENTATION_VALID_BIT | XRT_SPACE_RELATION_POSITION_VALID_BIT;
|
||||
if (m_bench.sample(bench_pose, trigger) || (rel.relation_flags & tracked) != tracked)
|
||||
bench_relation(rel, bench_pose);
|
||||
}
|
||||
|
||||
vr::DriverPose_t t = {
|
||||
// monado predicts pose "now", see xrt_device_get_tracked_pose
|
||||
.poseTimeOffset = 0,
|
||||
@@ -1445,7 +1672,12 @@ CDeviceDriver_Monado::GetPose()
|
||||
.shouldApplyHeadModel = !m_xdev->supported.position_tracking,
|
||||
.deviceIsConnected = true,
|
||||
};
|
||||
apply_pose(&rel, &t);
|
||||
// A loss of positional tracking still has a finite held position. Dropping it sent Y=0 to games.
|
||||
apply_pose(&rel, &t, XRT_SPACE_RELATION_POSITION_VALID_BIT);
|
||||
if (!(rel.relation_flags & XRT_SPACE_RELATION_POSITION_TRACKED_BIT)) {
|
||||
t.result = vr::TrackingResult_Running_OutOfRange;
|
||||
t.poseIsValid = false;
|
||||
}
|
||||
|
||||
#ifdef DUMP_POSE
|
||||
ovrd_log("get hmd pose %f %f %f %f, %f %f %f\n", t.qRotation.x, t.qRotation.y, t.qRotation.z, t.qRotation.w,
|
||||
@@ -1496,10 +1728,7 @@ CDeviceDriver_Monado::GetRecommendedRenderTargetSize(uint32_t *pnWidth, uint32_t
|
||||
int scale = debug_get_num_option_scale_percentage();
|
||||
float fscale = (float)scale / 100.f;
|
||||
|
||||
|
||||
|
||||
*pnWidth = m_xdev->hmd->screens[0].w_pixels * fscale;
|
||||
*pnHeight = m_xdev->hmd->screens[0].h_pixels * fscale;
|
||||
ovrd_get_recommended_render_target_size(m_xdev, fscale, pnWidth, pnHeight);
|
||||
|
||||
ovrd_log("Render Target Size: %dx%d (%fx)\n", *pnWidth, *pnHeight, fscale);
|
||||
}
|
||||
@@ -1520,10 +1749,7 @@ CDeviceDriver_Monado::GetEyeOutputViewport(
|
||||
void
|
||||
CDeviceDriver_Monado::GetProjectionRaw(vr::EVREye eEye, float *pfLeft, float *pfRight, float *pfTop, float *pfBottom)
|
||||
{
|
||||
*pfLeft = tanf(m_xdev->hmd->distortion.fov[eEye].angle_left);
|
||||
*pfRight = tanf(m_xdev->hmd->distortion.fov[eEye].angle_right);
|
||||
*pfTop = tanf(-m_xdev->hmd->distortion.fov[eEye].angle_up);
|
||||
*pfBottom = tanf(-m_xdev->hmd->distortion.fov[eEye].angle_down);
|
||||
ovrd_get_projection_raw(m_xdev->hmd->distortion.fov[eEye], pfLeft, pfRight, pfTop, pfBottom);
|
||||
ovrd_log("Projection Raw: L%f R%f T%f B%f\n", *pfLeft, *pfRight, *pfTop, *pfBottom);
|
||||
}
|
||||
|
||||
@@ -1592,7 +1818,7 @@ public:
|
||||
virtual void Cleanup();
|
||||
virtual const char *const * GetInterfaceVersions() { return vr::k_InterfaceVersions; }
|
||||
virtual void RunFrame();
|
||||
virtual bool ShouldBlockStandbyMode() { return false; }
|
||||
virtual bool ShouldBlockStandbyMode() { return debug_get_bool_option_steamvr_bench(); }
|
||||
virtual void EnterStandby() {}
|
||||
virtual void LeaveStandby() {}
|
||||
virtual void HandleHapticEvent(vr::VREvent_t *event);
|
||||
@@ -1605,6 +1831,8 @@ private:
|
||||
struct xrt_space_overseer *m_xso = NULL;
|
||||
struct xrt_device *m_xhmd = NULL;
|
||||
|
||||
bool m_restoreBenchPause = false;
|
||||
bool m_savedBenchPause = true;
|
||||
CDeviceDriver_Monado *m_MonadoDeviceDriver = NULL;
|
||||
CDeviceDriver_Monado_Controller *m_left = NULL;
|
||||
CDeviceDriver_Monado_Controller *m_right = NULL;
|
||||
@@ -1622,6 +1850,12 @@ CServerDriver_Monado::Init(vr::IVRDriverContext *pDriverContext)
|
||||
ovrd_log_init(vr::VRDriverLog());
|
||||
|
||||
ovrd_log("Initializing Monado driver\n");
|
||||
if (debug_get_bool_option_steamvr_bench()) {
|
||||
vr::EVRSettingsError error = vr::VRSettingsError_None;
|
||||
m_savedBenchPause = vr::VRSettings()->GetBool("power", "pauseCompositorOnStandby", &error);
|
||||
m_restoreBenchPause = error == vr::VRSettingsError_None;
|
||||
vr::VRSettings()->SetBool("power", "pauseCompositorOnStandby", false);
|
||||
}
|
||||
|
||||
//! @todo instance initialization is difficult to replicate
|
||||
|
||||
@@ -1671,11 +1905,11 @@ CServerDriver_Monado::Init(vr::IVRDriverContext *pDriverContext)
|
||||
u_builder_setup_tracking_origins(m_xhmd, nullptr, left_xdev, right_xdev, nullptr, &offset);
|
||||
|
||||
if (left_xdev) {
|
||||
m_left = new CDeviceDriver_Monado_Controller(m_xinst, left_xdev, XRT_HAND_LEFT);
|
||||
m_left = new CDeviceDriver_Monado_Controller(m_xinst, left_xdev, XRT_HAND_LEFT, m_xhmd);
|
||||
ovrd_log("Added left Controller: %s\n", left_xdev->str);
|
||||
}
|
||||
if (right_xdev) {
|
||||
m_right = new CDeviceDriver_Monado_Controller(m_xinst, right_xdev, XRT_HAND_RIGHT);
|
||||
m_right = new CDeviceDriver_Monado_Controller(m_xinst, right_xdev, XRT_HAND_RIGHT, m_xhmd);
|
||||
ovrd_log("Added right Controller: %s\n", right_xdev->str);
|
||||
}
|
||||
|
||||
@@ -1685,6 +1919,10 @@ CServerDriver_Monado::Init(vr::IVRDriverContext *pDriverContext)
|
||||
void
|
||||
CServerDriver_Monado::Cleanup()
|
||||
{
|
||||
if (m_restoreBenchPause) {
|
||||
vr::VRSettings()->SetBool("power", "pauseCompositorOnStandby", m_savedBenchPause);
|
||||
m_restoreBenchPause = false;
|
||||
}
|
||||
if (m_MonadoDeviceDriver != NULL) {
|
||||
delete m_MonadoDeviceDriver;
|
||||
m_MonadoDeviceDriver = NULL;
|
||||
@@ -1742,13 +1980,17 @@ CServerDriver_Monado::HandleHapticEvent(vr::VREvent_t *event)
|
||||
SteamVRDriverControlOutput *control = &controller->m_output_controls.at(0);
|
||||
|
||||
enum xrt_output_name name = control->monado_output_name;
|
||||
ovrd_log("Haptic vibration %s, %d\n", control->steamvr_control_path, name);
|
||||
ovrd_log("Haptic vibration %s, %d\n", control->steamvr_control_path.c_str(), name);
|
||||
controller->m_xdev->set_output(controller->m_xdev, name, &out);
|
||||
}
|
||||
|
||||
void
|
||||
CServerDriver_Monado::RunFrame()
|
||||
{
|
||||
if (m_MonadoDeviceDriver) {
|
||||
m_MonadoDeviceDriver->UpdateRoomSetupGuard();
|
||||
m_MonadoDeviceDriver->UpdateDisplayFrequency();
|
||||
}
|
||||
if (m_left) {
|
||||
m_left->RunFrame();
|
||||
}
|
||||
@@ -1762,7 +2004,8 @@ CServerDriver_Monado::RunFrame()
|
||||
switch (event.eventType) {
|
||||
case vr::VREvent_Input_HapticVibration: HandleHapticEvent(&event); break;
|
||||
case vr::VREvent_PropertyChanged:
|
||||
// ovrd_log("Property changed\n");
|
||||
if (m_MonadoDeviceDriver && event.data.property.prop == vr::Prop_DisplayFrequency_Float)
|
||||
m_MonadoDeviceDriver->UpdateDisplayFrequency();
|
||||
break;
|
||||
case vr::VREvent_TrackedDeviceActivated:
|
||||
ovrd_log("Device activated %d\n", event.trackedDeviceIndex);
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
// Copyright 2020,2024 Collabora, Ltd.
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
#pragma once
|
||||
|
||||
#include "math/m_api.h"
|
||||
#include "math/m_space.h"
|
||||
#include "openvr_driver.h"
|
||||
#include <cmath>
|
||||
|
||||
inline void
|
||||
copy_vec3(struct xrt_vec3 *from, double *to)
|
||||
{
|
||||
to[0] = from->x;
|
||||
to[1] = from->y;
|
||||
to[2] = from->z;
|
||||
}
|
||||
|
||||
inline void
|
||||
copy_quat(struct xrt_quat *from, vr::HmdQuaternion_t *to)
|
||||
{
|
||||
to->x = from->x;
|
||||
to->y = from->y;
|
||||
to->z = from->z;
|
||||
to->w = from->w;
|
||||
}
|
||||
|
||||
inline void
|
||||
copy_pose_velocities(struct xrt_space_relation *rel, vr::DriverPose_t *m_pose)
|
||||
{
|
||||
// Controller GetPose reuses DriverPose_t. SteamVR has no velocity validity bits,
|
||||
// so an invalid Monado velocity must not leave the previous frame's prediction active.
|
||||
for (auto &velocity : m_pose->vecVelocity)
|
||||
velocity = 0;
|
||||
for (auto &velocity : m_pose->vecAngularVelocity)
|
||||
velocity = 0;
|
||||
if ((rel->relation_flags & XRT_SPACE_RELATION_LINEAR_VELOCITY_VALID_BIT) != 0 &&
|
||||
std::isfinite(rel->linear_velocity.x) && std::isfinite(rel->linear_velocity.y) &&
|
||||
std::isfinite(rel->linear_velocity.z)) {
|
||||
// linear velocity in world space
|
||||
copy_vec3(&rel->linear_velocity, m_pose->vecVelocity);
|
||||
}
|
||||
|
||||
if ((rel->relation_flags & XRT_SPACE_RELATION_ANGULAR_VELOCITY_VALID_BIT) != 0 &&
|
||||
std::isfinite(rel->angular_velocity.x) && std::isfinite(rel->angular_velocity.y) &&
|
||||
std::isfinite(rel->angular_velocity.z)) {
|
||||
// angular velocity reported by monado in world space,
|
||||
// expected by steamvr to be in "controller space"
|
||||
struct xrt_quat orientation_inv;
|
||||
math_quat_invert(&rel->pose.orientation, &orientation_inv);
|
||||
|
||||
struct xrt_vec3 vel;
|
||||
math_quat_rotate_derivative(&orientation_inv, &rel->angular_velocity, &vel);
|
||||
|
||||
if (std::isfinite(vel.x) && std::isfinite(vel.y) && std::isfinite(vel.z))
|
||||
copy_vec3(&vel, m_pose->vecAngularVelocity);
|
||||
}
|
||||
}
|
||||
|
||||
inline bool
|
||||
finite_pose(const struct xrt_pose &p)
|
||||
{
|
||||
float norm = p.orientation.x * p.orientation.x + p.orientation.y * p.orientation.y +
|
||||
p.orientation.z * p.orientation.z + p.orientation.w * p.orientation.w;
|
||||
return std::isfinite(norm) && norm >= 0.9f && norm <= 1.1f && std::isfinite(p.position.x) &&
|
||||
std::isfinite(p.position.y) && std::isfinite(p.position.z);
|
||||
}
|
||||
|
||||
inline void
|
||||
apply_pose(struct xrt_space_relation *rel,
|
||||
vr::DriverPose_t *m_pose,
|
||||
enum xrt_space_relation_flags position_flag = XRT_SPACE_RELATION_POSITION_TRACKED_BIT,
|
||||
bool require_position = false)
|
||||
{
|
||||
if (!finite_pose(rel->pose)) {
|
||||
for (auto &velocity : m_pose->vecVelocity)
|
||||
velocity = 0;
|
||||
for (auto &velocity : m_pose->vecAngularVelocity)
|
||||
velocity = 0;
|
||||
m_pose->result = vr::TrackingResult_Running_OutOfRange;
|
||||
m_pose->poseIsValid = false;
|
||||
return;
|
||||
}
|
||||
if ((rel->relation_flags & XRT_SPACE_RELATION_ORIENTATION_TRACKED_BIT) != 0) {
|
||||
copy_quat(&rel->pose.orientation, &m_pose->qRotation);
|
||||
} else {
|
||||
m_pose->result = vr::TrackingResult_Running_OutOfRange;
|
||||
m_pose->poseIsValid = false;
|
||||
}
|
||||
if (require_position && (rel->relation_flags & position_flag) == 0) {
|
||||
m_pose->result = vr::TrackingResult_Running_OutOfRange;
|
||||
m_pose->poseIsValid = false;
|
||||
}
|
||||
if ((rel->relation_flags & position_flag) != 0)
|
||||
copy_vec3(&rel->pose.position, m_pose->vecPosition);
|
||||
copy_pose_velocities(rel, m_pose);
|
||||
}
|
||||
|
||||
inline void
|
||||
transform_rift_s_touch_relation(struct xrt_space_relation *rel,
|
||||
const struct xrt_pose *grip_to_model,
|
||||
const struct xrt_pose *origin)
|
||||
{
|
||||
const auto position_flags = rel->relation_flags &
|
||||
(XRT_SPACE_RELATION_POSITION_VALID_BIT | XRT_SPACE_RELATION_POSITION_TRACKED_BIT);
|
||||
// m_relation_chain promotes orientation-only relations to positioned 3DoF devices and
|
||||
// otherwise discards their estimated coordinates. Transform the estimate, then restore
|
||||
// the controller's own confidence instead of publishing that promotion as a tracked fix.
|
||||
if ((rel->relation_flags & XRT_SPACE_RELATION_ORIENTATION_VALID_BIT) != 0)
|
||||
rel->relation_flags = static_cast<enum xrt_space_relation_flags>(
|
||||
rel->relation_flags | XRT_SPACE_RELATION_POSITION_VALID_BIT);
|
||||
struct xrt_relation_chain chain = {};
|
||||
m_relation_chain_push_pose_if_not_identity(&chain, grip_to_model);
|
||||
m_relation_chain_push_relation(&chain, rel);
|
||||
m_relation_chain_push_pose_if_not_identity(&chain, origin);
|
||||
m_relation_chain_resolve(&chain, rel);
|
||||
rel->relation_flags = static_cast<enum xrt_space_relation_flags>(
|
||||
(rel->relation_flags & ~(XRT_SPACE_RELATION_POSITION_VALID_BIT | XRT_SPACE_RELATION_POSITION_TRACKED_BIT)) |
|
||||
position_flags);
|
||||
}
|
||||
|
||||
// Oculus' controller adapter admits orientation-valid poses, copies their estimated
|
||||
// position even without PositionValid, and reports that case as Running_OutOfRange.
|
||||
inline void
|
||||
apply_rift_s_touch_pose(struct xrt_space_relation *rel, vr::DriverPose_t *m_pose)
|
||||
{
|
||||
bool connected = m_pose->deviceIsConnected;
|
||||
*m_pose = {};
|
||||
m_pose->deviceIsConnected = connected;
|
||||
bool position_valid = (rel->relation_flags & XRT_SPACE_RELATION_POSITION_VALID_BIT) != 0;
|
||||
m_pose->willDriftInYaw = !position_valid;
|
||||
m_pose->result = vr::TrackingResult_Uninitialized;
|
||||
if ((rel->relation_flags & XRT_SPACE_RELATION_ORIENTATION_VALID_BIT) == 0)
|
||||
return;
|
||||
if (!finite_pose(rel->pose)) {
|
||||
m_pose->result = vr::TrackingResult_Running_OutOfRange;
|
||||
return;
|
||||
}
|
||||
m_pose->poseIsValid = true;
|
||||
m_pose->result = position_valid ? vr::TrackingResult_Running_OK : vr::TrackingResult_Running_OutOfRange;
|
||||
copy_quat(&rel->pose.orientation, &m_pose->qRotation);
|
||||
copy_vec3(&rel->pose.position, m_pose->vecPosition);
|
||||
copy_pose_velocities(rel, m_pose);
|
||||
}
|
||||
|
||||
/*!
|
||||
* SteamVR draws a render model with its origin at the device pose, and its OpenXR runtime takes
|
||||
* grip from the model's openxr_grip component. Monado reports grip, so the device pose is moved
|
||||
* from grip to the model origin. Rift S Touch (oculus_rifts_controller_*): grip is +20.6 degrees
|
||||
* about X at (+-0.007, -0.00183, 0.10195) m in the model, in the handle 10 cm behind the origin.
|
||||
*/
|
||||
inline struct xrt_pose
|
||||
rift_s_touch_grip_to_model(enum xrt_hand hand)
|
||||
{
|
||||
struct xrt_pose model_grip = XRT_POSE_IDENTITY;
|
||||
const struct xrt_vec3 axis = {1, 0, 0};
|
||||
math_quat_from_angle_vector((float)DEG_TO_RAD(20.6), &axis, &model_grip.orientation);
|
||||
model_grip.position = {hand == XRT_HAND_LEFT ? 0.007f : -0.007f, -0.00182941f, 0.1019482f};
|
||||
struct xrt_pose grip_model;
|
||||
math_pose_invert(&model_grip, &grip_model);
|
||||
return grip_model;
|
||||
}
|
||||
|
||||
/*!
|
||||
* Resting hand position for a controller that has never had a position fix: 0.2 m to the side,
|
||||
* 0.5 m below and 0.3 m in front of the head, following its heading only. The pose stays
|
||||
* invalid; this only keeps SteamVR from drawing it at the tracking origin under the floor.
|
||||
*/
|
||||
inline struct xrt_vec3
|
||||
nominal_hand_position(const struct xrt_pose &head, enum xrt_hand hand)
|
||||
{
|
||||
struct xrt_vec3 forward;
|
||||
const struct xrt_vec3 minus_z = {0, 0, -1};
|
||||
math_quat_rotate_vec3(&head.orientation, &minus_z, &forward);
|
||||
float length = std::hypot(forward.x, forward.z);
|
||||
float fx = length > 1e-3f ? forward.x / length : 0, fz = length > 1e-3f ? forward.z / length : -1;
|
||||
// Right is forward x up.
|
||||
float side = hand == XRT_HAND_LEFT ? -0.2f : 0.2f;
|
||||
return {head.position.x + 0.3f * fx - side * fz, head.position.y - 0.5f, head.position.z + 0.3f * fz + side * fx};
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
#pragma once
|
||||
#include "openvr.h"
|
||||
#include "ovrd_roomsetup_frame.hpp"
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <optional>
|
||||
|
||||
namespace roomsetup {
|
||||
// OpenVR mailbox and path interfaces use the same client connection as
|
||||
// IVRSystem.
|
||||
class Mailbox
|
||||
{
|
||||
public:
|
||||
virtual int
|
||||
Register(const char *name, uint64_t *handle) = 0;
|
||||
virtual int
|
||||
Unregister(uint64_t handle) = 0;
|
||||
virtual int
|
||||
Send(uint64_t handle, const char *destination, const char *json) = 0;
|
||||
virtual int
|
||||
Read(uint64_t handle, char *buffer, uint32_t bytes, uint32_t *required) = 0;
|
||||
};
|
||||
struct PathRead
|
||||
{
|
||||
uint64_t path;
|
||||
void *buffer;
|
||||
uint32_t size, tag, required;
|
||||
int error;
|
||||
const char *name;
|
||||
};
|
||||
class Paths
|
||||
{
|
||||
public:
|
||||
virtual int
|
||||
Read(uint64_t root, PathRead *batch, uint32_t count) = 0;
|
||||
};
|
||||
|
||||
inline std::optional<float>
|
||||
saved_floor(const vr::HmdMatrix34_t &standing, const vr::HmdMatrix34_t &setup)
|
||||
{
|
||||
float raw_floor[3]{};
|
||||
for (int i = 0; i < 3; ++i)
|
||||
for (int j = 0; j < 3; ++j)
|
||||
raw_floor[i] -= standing.m[j][i] * standing.m[j][3];
|
||||
float y = setup.m[1][3];
|
||||
for (int i = 0; i < 3; ++i)
|
||||
y += setup.m[1][i] * raw_floor[i];
|
||||
return std::isfinite(y) ? std::optional<float>{y} : std::nullopt;
|
||||
}
|
||||
|
||||
class SavedFloor
|
||||
{
|
||||
Mailbox *mailbox;
|
||||
Paths *paths;
|
||||
uint64_t handle = 0;
|
||||
SharedFrame frame{true};
|
||||
|
||||
public:
|
||||
SavedFloor(Mailbox *messages, Paths *properties) : mailbox(messages), paths(properties)
|
||||
{
|
||||
if (mailbox && mailbox->Register("monado/roomsetup-floor", &handle) != 0)
|
||||
handle = 0;
|
||||
}
|
||||
~SavedFloor()
|
||||
{
|
||||
if (handle)
|
||||
mailbox->Unregister(handle);
|
||||
}
|
||||
bool
|
||||
transforms(vr::HmdMatrix34_t &standing, vr::HmdMatrix34_t &setup)
|
||||
{
|
||||
if (!paths)
|
||||
return false;
|
||||
PathRead read[] = {{0, &standing, sizeof(standing), 0, 0, 0, "/chaperone/standing/transform"},
|
||||
{0, &setup, sizeof(setup), 0, 0, 0, "/chaperone/room_setup/transform"}};
|
||||
return paths->Read(0x600000003, read, 2) == 0 && !read[0].error && !read[1].error &&
|
||||
read[0].tag == 20 && read[1].tag == 20;
|
||||
}
|
||||
void
|
||||
update_frame(bool active)
|
||||
{
|
||||
frame.write(active);
|
||||
}
|
||||
bool
|
||||
publish(bool initialize_manual = false)
|
||||
{
|
||||
if (!handle || !paths)
|
||||
return false;
|
||||
vr::HmdMatrix34_t standing{}, setup{};
|
||||
if (!transforms(standing, setup))
|
||||
return false;
|
||||
auto y = saved_floor(standing, setup);
|
||||
if (!y)
|
||||
return false;
|
||||
char message[128];
|
||||
snprintf(message, sizeof(message), "{\"type\":\"floor_detection_result\",\"pos\":%.9g}", *y);
|
||||
bool sent = mailbox->Send(handle, "vrcompositor_systemlayer", message) == 0;
|
||||
// The native manual step starts at tracking-origin Y=0. Seed it from the
|
||||
// saved plane once; the first stick/laser adjustment selects Manual again.
|
||||
if (sent && initialize_manual)
|
||||
sent = mailbox->Send(handle, "vrcompositor_systemlayer",
|
||||
"{\"type\":\"reset_playspace_floorheight\",\"value_"
|
||||
"source\":2}") == 0;
|
||||
fprintf(stderr, "monado-roomsetup-floor: saved_floor_setup_y=%.6f sent=%d\n", *y, sent);
|
||||
return sent;
|
||||
}
|
||||
};
|
||||
} // namespace roomsetup
|
||||
@@ -0,0 +1,182 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
#pragma once
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <string>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace roomsetup {
|
||||
using Matrix = std::array<float, 16>;
|
||||
inline Matrix
|
||||
identity()
|
||||
{
|
||||
return {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
|
||||
}
|
||||
inline Matrix
|
||||
multiply(const Matrix &a, const Matrix &b)
|
||||
{
|
||||
Matrix out{};
|
||||
for (int i = 0; i < 4; ++i)
|
||||
for (int j = 0; j < 4; ++j)
|
||||
for (int k = 0; k < 4; ++k)
|
||||
out[4 * i + j] += a[4 * i + k] * b[4 * k + j];
|
||||
return out;
|
||||
}
|
||||
inline Matrix
|
||||
inverse_rigid(const Matrix &a)
|
||||
{
|
||||
auto out = identity();
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
for (int j = 0; j < 3; ++j)
|
||||
out[4 * i + j] = a[4 * j + i];
|
||||
for (int j = 0; j < 3; ++j)
|
||||
out[4 * i + 3] -= out[4 * i + j] * a[4 * j + 3];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
inline uint64_t
|
||||
monotonic_ns()
|
||||
{
|
||||
timespec now{};
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
return uint64_t(now.tv_sec) * 1000000000 + now.tv_nsec;
|
||||
}
|
||||
struct FrameState
|
||||
{
|
||||
uint64_t sequence, timestamp;
|
||||
uint32_t active, reserved;
|
||||
};
|
||||
class SharedFrame
|
||||
{
|
||||
FrameState *state = nullptr;
|
||||
std::string path = "/monado-roomsetup-frame-" + std::to_string(getuid());
|
||||
bool writer;
|
||||
|
||||
public:
|
||||
explicit SharedFrame(bool write) : writer(write) {}
|
||||
~SharedFrame()
|
||||
{
|
||||
if (state)
|
||||
munmap(state, sizeof(FrameState));
|
||||
if (writer && state)
|
||||
shm_unlink(path.c_str());
|
||||
}
|
||||
bool
|
||||
open()
|
||||
{
|
||||
if (state)
|
||||
return true;
|
||||
int fd = shm_open(path.c_str(), (writer ? O_CREAT | O_RDWR : O_RDONLY) | O_CLOEXEC, 0600);
|
||||
if (fd < 0)
|
||||
return false;
|
||||
if (writer && ftruncate(fd, sizeof(FrameState)) != 0) {
|
||||
close(fd);
|
||||
return false;
|
||||
}
|
||||
struct stat info{};
|
||||
if (fstat(fd, &info) != 0 || info.st_size != sizeof(FrameState) || info.st_uid != getuid()) {
|
||||
close(fd);
|
||||
return false;
|
||||
}
|
||||
void *mapped =
|
||||
mmap(nullptr, sizeof(FrameState), writer ? PROT_READ | PROT_WRITE : PROT_READ, MAP_SHARED, fd, 0);
|
||||
close(fd);
|
||||
if (mapped == MAP_FAILED)
|
||||
return false;
|
||||
state = static_cast<FrameState *>(mapped);
|
||||
return true;
|
||||
}
|
||||
void
|
||||
write(bool active)
|
||||
{
|
||||
if (!writer || !open())
|
||||
return;
|
||||
auto sequence = __atomic_load_n(&state->sequence, __ATOMIC_RELAXED);
|
||||
__atomic_store_n(&state->sequence, sequence | 1, __ATOMIC_SEQ_CST);
|
||||
state->timestamp = monotonic_ns();
|
||||
state->active = active;
|
||||
__atomic_store_n(&state->sequence, (sequence | 1) + 1, __ATOMIC_RELEASE);
|
||||
}
|
||||
bool
|
||||
read()
|
||||
{
|
||||
if (writer || !open())
|
||||
return false;
|
||||
auto sequence = __atomic_load_n(&state->sequence, __ATOMIC_ACQUIRE);
|
||||
if (sequence & 1)
|
||||
return false;
|
||||
FrameState value;
|
||||
memcpy(&value, state, sizeof(value));
|
||||
if (sequence != __atomic_load_n(&state->sequence, __ATOMIC_ACQUIRE) || !value.active ||
|
||||
monotonic_ns() - value.timestamp > 200000000)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// RGB supplies no measured depth. In every Room View mode, retain capture-to-eye
|
||||
// rotation but remove translation against the assumed dome and floor surface.
|
||||
// The same eye ray then samples the same camera pixel at every mesh depth.
|
||||
inline bool
|
||||
correct_camera(void *bytes, size_t size)
|
||||
{
|
||||
if (size != 400)
|
||||
return false;
|
||||
std::array<float, 98> values;
|
||||
memcpy(values.data(), bytes, sizeof(values));
|
||||
if (!std::all_of(values.begin(), values.end(), [](float v) { return std::isfinite(v); }))
|
||||
return false;
|
||||
auto model = identity();
|
||||
for (int i = 0; i < 16; ++i)
|
||||
if (std::abs(values[i] - model[i]) > 1e-5f)
|
||||
return false;
|
||||
// Exclude UI uniforms and the depth path, which has a nonidentity model.
|
||||
if (std::abs(values[46]) < .5f || std::abs(values[47]) > .01f)
|
||||
return false;
|
||||
Matrix view, camera;
|
||||
memcpy(view.data(), values.data() + 16, sizeof(view));
|
||||
memcpy(camera.data(), values.data() + 48, sizeof(camera));
|
||||
for (int i = 12; i < 16; ++i)
|
||||
if (std::abs(view[i] - model[i]) > 1e-5f)
|
||||
return false;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
for (int j = 0; j < 3; ++j) {
|
||||
float dot = 0;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
dot += view[4 * i + k] * view[4 * j + k];
|
||||
if (std::abs(dot - (i == j ? 1.f : 0.f)) > .001f)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
float dot = 0, norm = 0;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
dot += camera[8 + i] * camera[12 + i];
|
||||
norm += camera[12 + i] * camera[12 + i];
|
||||
}
|
||||
if (!std::isfinite(norm) || norm < .25f)
|
||||
return false;
|
||||
float depth_scale = dot / norm;
|
||||
if (!std::isfinite(depth_scale))
|
||||
return false;
|
||||
for (int i = 0; i < 3; ++i)
|
||||
if (std::abs(camera[8 + i] - depth_scale * camera[12 + i]) > .001f)
|
||||
return false;
|
||||
auto relative = multiply(camera, inverse_rigid(view));
|
||||
// Preserve the projection's near/far depth term while dropping translation.
|
||||
relative[11] -= depth_scale * relative[15];
|
||||
relative[3] = relative[7] = relative[15] = 0;
|
||||
auto fixed = multiply(relative, view);
|
||||
if (!std::all_of(fixed.begin(), fixed.end(), [](float v) { return std::isfinite(v); }))
|
||||
return false;
|
||||
memcpy(static_cast<unsigned char *>(bytes) + 192, fixed.data(), sizeof(fixed));
|
||||
return true;
|
||||
}
|
||||
} // namespace roomsetup
|
||||
@@ -0,0 +1,414 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
// Keep the pre-setup calibration if SteamVR's compositor dies during native drawing.
|
||||
#include "openvr.h"
|
||||
#include "ovrd_roomsetup_floor.hpp"
|
||||
#include "../../drivers/rift_s/rift_s_world_control.h"
|
||||
#include <cjson/cJSON.h>
|
||||
#include <chrono>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <dlfcn.h>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <memory>
|
||||
#include <poll.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/stat.h>
|
||||
#include <thread>
|
||||
#include <unistd.h>
|
||||
#include <vector>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static std::string read_file(const fs::path &path)
|
||||
{
|
||||
std::ifstream stream(path, std::ios::binary);
|
||||
if (!stream) return {};
|
||||
stream.seekg(0, std::ios::end);
|
||||
auto size = stream.tellg();
|
||||
if (size <= 0 || size > 4 * 1024 * 1024) return {};
|
||||
std::string bytes(static_cast<size_t>(size), '\0');
|
||||
stream.seekg(0);
|
||||
stream.read(bytes.data(), bytes.size());
|
||||
return stream ? bytes : std::string{};
|
||||
}
|
||||
|
||||
static fs::path calibration_path(fs::path &log)
|
||||
{
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) return {};
|
||||
const char *config = getenv("XDG_CONFIG_HOME");
|
||||
fs::path registry = (config ? fs::path(config) : fs::path(home) / ".config") / "openvr/openvrpaths.vrpath";
|
||||
auto bytes = read_file(registry);
|
||||
cJSON *json = cJSON_Parse(bytes.c_str());
|
||||
if (!json) return {};
|
||||
auto *entry = cJSON_GetArrayItem(cJSON_GetObjectItemCaseSensitive(json, "config"), 0);
|
||||
fs::path path;
|
||||
if (cJSON_IsString(entry) && entry->valuestring) path = fs::path(entry->valuestring) / "chaperone_info.vrchap";
|
||||
auto *log_entry = cJSON_GetArrayItem(cJSON_GetObjectItemCaseSensitive(json, "log"), 0);
|
||||
if (cJSON_IsString(log_entry) && log_entry->valuestring) log = fs::path(log_entry->valuestring) / "vrcompositor.txt";
|
||||
cJSON_Delete(json);
|
||||
return path;
|
||||
}
|
||||
|
||||
// Room setup is the only action that may establish a new physical world.
|
||||
static void world_command(const char *command)
|
||||
{
|
||||
char name[1024];
|
||||
rift_s_world_control_path("command", name, sizeof(name));
|
||||
fs::path path(name), temp = path.string() + ".tmp";
|
||||
std::ofstream stream(temp, std::ios::trunc);
|
||||
if (!stream) return;
|
||||
auto ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
stream << ns << " " << command << "\n";
|
||||
stream.close();
|
||||
if (stream) { std::error_code error; fs::rename(temp, path, error); }
|
||||
}
|
||||
|
||||
static std::string world_status()
|
||||
{
|
||||
char path[1024];
|
||||
rift_s_world_control_path("status", path, sizeof(path));
|
||||
std::ifstream stream(path);
|
||||
int64_t stamp = 0;
|
||||
std::string status;
|
||||
stream >> stamp >> status;
|
||||
auto ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
if (!stream || stamp > ns || ns - stamp > 2000000000LL) return {};
|
||||
return status;
|
||||
}
|
||||
|
||||
// Retain the delivered state only after OpenVR accepts it, so a transient failure retries.
|
||||
static bool update_world_notification(vr::IVRNotifications ¬ifications,
|
||||
vr::VROverlayHandle_t overlay,
|
||||
const std::string &warning,
|
||||
std::string &delivered,
|
||||
vr::VRNotificationId &id)
|
||||
{
|
||||
if (warning == delivered) return true;
|
||||
if (id) {
|
||||
auto error = notifications.RemoveNotification(id);
|
||||
if (error != vr::VRNotificationError_OK && error != vr::VRNotificationError_InvalidNotificationId)
|
||||
return false;
|
||||
id = 0;
|
||||
delivered.clear();
|
||||
}
|
||||
if (!warning.empty()) {
|
||||
vr::VRNotificationId created = 0;
|
||||
if (notifications.CreateNotification(overlay, 0, vr::EVRNotificationType_Persistent,
|
||||
warning.c_str(), vr::EVRNotificationStyle_Application, nullptr, &created) !=
|
||||
vr::VRNotificationError_OK) return false;
|
||||
id = created;
|
||||
}
|
||||
delivered = warning;
|
||||
return true;
|
||||
}
|
||||
|
||||
static int compositor_fd()
|
||||
{
|
||||
std::error_code error;
|
||||
for (const auto &entry : fs::directory_iterator("/proc", error)) {
|
||||
auto name = entry.path().filename().string();
|
||||
if (name.empty() || name.find_first_not_of("0123456789") != std::string::npos) continue;
|
||||
std::ifstream comm(entry.path() / "comm");
|
||||
std::string process;
|
||||
std::getline(comm, process);
|
||||
if (process != "vrcompositor") continue;
|
||||
// Ignore another user's compositor and retain this process generation, not its reusable PID.
|
||||
struct stat state{};
|
||||
if (stat(entry.path().c_str(), &state) != 0 || state.st_uid != getuid()) continue;
|
||||
return static_cast<int>(syscall(SYS_pidfd_open, std::stoi(name), 0));
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool exited(int fd)
|
||||
{
|
||||
if (fd < 0) return true;
|
||||
pollfd process{fd, POLLIN, 0};
|
||||
return poll(&process, 1, 0) > 0;
|
||||
}
|
||||
|
||||
// Read only new compositor diagnostics for this setup attempt. The assertion is
|
||||
// visible before the process finishes crash handling and its pidfd becomes ready.
|
||||
class OverflowLog
|
||||
{
|
||||
fs::path path;
|
||||
uint64_t cursor = 0;
|
||||
std::string suffix;
|
||||
public:
|
||||
explicit OverflowLog(fs::path value) : path(std::move(value)) {}
|
||||
void begin()
|
||||
{
|
||||
std::error_code error;
|
||||
cursor = fs::file_size(path, error);
|
||||
if (error) cursor = 0;
|
||||
suffix.clear();
|
||||
}
|
||||
bool overflowed()
|
||||
{
|
||||
std::error_code error;
|
||||
uint64_t size = fs::file_size(path, error);
|
||||
if (error) return false;
|
||||
if (size < cursor) { cursor = 0; suffix.clear(); }
|
||||
if (size == cursor) return false;
|
||||
std::ifstream stream(path, std::ios::binary);
|
||||
stream.seekg(cursor);
|
||||
std::string bytes(static_cast<size_t>(std::min<uint64_t>(size - cursor, 65536)), '\0');
|
||||
stream.read(bytes.data(), bytes.size());
|
||||
bytes.resize(static_cast<size_t>(stream.gcount()));
|
||||
cursor += bytes.size();
|
||||
suffix += bytes;
|
||||
bool found = suffix.find("Vulkan transfer ring buffer overflow") != std::string::npos;
|
||||
if (suffix.size() > 128) suffix.erase(0, suffix.size() - 128);
|
||||
return found;
|
||||
}
|
||||
};
|
||||
|
||||
struct Snapshot
|
||||
{
|
||||
std::string file;
|
||||
std::vector<char> exported;
|
||||
};
|
||||
|
||||
static std::optional<Snapshot> snapshot(vr::IVRChaperoneSetup &setup, const fs::path &path)
|
||||
{
|
||||
Snapshot saved{read_file(path), {}};
|
||||
uint32_t size = 0;
|
||||
setup.ExportLiveToBuffer(nullptr, &size);
|
||||
if (saved.file.empty() || size == 0 || size > 4 * 1024 * 1024) return std::nullopt;
|
||||
saved.exported.resize(size + 1, 0);
|
||||
if (!setup.ExportLiveToBuffer(saved.exported.data(), &size)) return std::nullopt;
|
||||
return saved;
|
||||
}
|
||||
|
||||
static bool restore(vr::IVRChaperoneSetup &setup, const fs::path &path, const Snapshot &saved)
|
||||
{
|
||||
setup.RevertWorkingCopy();
|
||||
if (!setup.ImportFromBufferToWorking(saved.exported.data(), 0) ||
|
||||
!setup.CommitWorkingCopy(vr::EChaperoneConfigFile_Live)) return false;
|
||||
// Commit updates the server's live calibration. Restore the complete file too,
|
||||
// including other universes and formatting that OpenVR's export does not retain.
|
||||
auto temporary = path;
|
||||
temporary += ".monado-guard-" + std::to_string(getpid());
|
||||
{
|
||||
std::ofstream stream(temporary, std::ios::binary | std::ios::trunc);
|
||||
stream.write(saved.file.data(), saved.file.size());
|
||||
stream.flush();
|
||||
if (!stream) return false;
|
||||
}
|
||||
std::error_code error;
|
||||
fs::rename(temporary, path, error);
|
||||
if (error) { fs::remove(temporary, error); return false; }
|
||||
setup.ReloadFromDisk(vr::EChaperoneConfigFile_Live);
|
||||
return read_file(path) == saved.file;
|
||||
}
|
||||
|
||||
// Capture public coordinate state on transitions without changing calibration.
|
||||
static void log_coordinates(vr::IVRSystem &system, vr::IVRChaperoneSetup &setup, uint32_t event, uint32_t step)
|
||||
{
|
||||
auto raw_to_standing = system.GetRawZeroPoseToStandingAbsoluteTrackingPose();
|
||||
auto seated_to_standing = system.GetSeatedZeroPoseToStandingAbsoluteTrackingPose();
|
||||
vr::HmdMatrix34_t standing_to_raw{};
|
||||
bool working = setup.GetWorkingStandingZeroPoseToRawTrackingPose(&standing_to_raw);
|
||||
vr::TrackedDevicePose_t head{};
|
||||
system.GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseRawAndUncalibrated, 0, &head, 1);
|
||||
auto universe = system.GetUint64TrackedDeviceProperty(0, vr::Prop_CurrentUniverseId_Uint64);
|
||||
auto ticks = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
fprintf(stderr, "monado-coordinates: event=%u step=%u monotonic_ns=%lld universe=%llu head_valid=%d tracking=%d raw_head=(%.6f,%.6f,%.6f) floor_raw_y=%.6f working=%d\n",
|
||||
event, step, (long long)ticks, (unsigned long long)universe, head.bPoseIsValid, head.eTrackingResult,
|
||||
head.mDeviceToAbsoluteTracking.m[0][3], head.mDeviceToAbsoluteTracking.m[1][3],
|
||||
head.mDeviceToAbsoluteTracking.m[2][3], standing_to_raw.m[1][3], working);
|
||||
auto log_matrix = [](const char *name, const vr::HmdMatrix34_t &m) {
|
||||
fprintf(stderr, "monado-coordinates: %s=[%.6f %.6f %.6f %.6f; %.6f %.6f %.6f %.6f; %.6f %.6f %.6f %.6f]\n",
|
||||
name, m.m[0][0], m.m[0][1], m.m[0][2], m.m[0][3], m.m[1][0], m.m[1][1], m.m[1][2], m.m[1][3],
|
||||
m.m[2][0], m.m[2][1], m.m[2][2], m.m[2][3]);
|
||||
};
|
||||
log_matrix("raw_to_standing", raw_to_standing);
|
||||
log_matrix("seated_to_standing", seated_to_standing);
|
||||
if (working) log_matrix("working_standing_to_raw", standing_to_raw);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc != 4) return 1;
|
||||
int parent = static_cast<int>(syscall(SYS_pidfd_open, std::atoi(argv[2]), 0));
|
||||
int ready = std::atoi(argv[3]);
|
||||
void *library = dlopen(argv[1], RTLD_NOW | RTLD_LOCAL);
|
||||
if (!library || parent < 0) return 1;
|
||||
auto init = reinterpret_cast<uint32_t (*)(vr::EVRInitError *, vr::EVRApplicationType, const char *)>(dlsym(library, "VR_InitInternal2"));
|
||||
auto interface = reinterpret_cast<void *(*)(const char *, vr::EVRInitError *)>(dlsym(library, "VR_GetGenericInterface"));
|
||||
auto shutdown = reinterpret_cast<void (*)()>(dlsym(library, "VR_ShutdownInternal"));
|
||||
if (!init || !interface || !shutdown) return 1;
|
||||
vr::EVRInitError error = vr::VRInitError_Unknown;
|
||||
for (unsigned attempt = 0; attempt < 100 && !exited(parent); ++attempt) {
|
||||
init(&error, vr::VRApplication_Background, nullptr);
|
||||
if (error == vr::VRInitError_None) break;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
if (error != vr::VRInitError_None) return 1;
|
||||
auto *system = static_cast<vr::IVRSystem *>(interface(vr::IVRSystem_Version, &error));
|
||||
auto *setup = static_cast<vr::IVRChaperoneSetup *>(interface(vr::IVRChaperoneSetup_Version, &error));
|
||||
auto *overlays = static_cast<vr::IVROverlay *>(interface(vr::IVROverlay_Version, &error));
|
||||
auto *notifications = static_cast<vr::IVRNotifications *>(interface(vr::IVRNotifications_Version, &error));
|
||||
fs::path log_path;
|
||||
fs::path path = calibration_path(log_path);
|
||||
OverflowLog diagnostics(log_path);
|
||||
if (!system || !setup || path.empty() || !snapshot(*setup, path)) { shutdown(); return 1; }
|
||||
vr::VROverlayHandle_t overlay = vr::k_ulOverlayHandleInvalid;
|
||||
if (overlays) overlays->CreateOverlay("monado.roomsetup.guard", "Monado playspace setup", &overlay);
|
||||
if (write(ready, "1", 1) != 1) { shutdown(); return 1; }
|
||||
fprintf(stderr, "monado-roomsetup-guard: ready\n");
|
||||
std::optional<Snapshot> saved;
|
||||
int compositor = -1;
|
||||
uint32_t step = 0;
|
||||
bool failed = false, failure_handled = false, native_commit = false;
|
||||
bool overflow_detected = false;
|
||||
auto commit_deadline = std::chrono::steady_clock::time_point{};
|
||||
auto exit_deadline = std::chrono::steady_clock::time_point{};
|
||||
auto compositor_failed = [&] {
|
||||
overflow_detected |= diagnostics.overflowed();
|
||||
return overflow_detected || exited(compositor);
|
||||
};
|
||||
auto drawing = [](uint32_t value) { return value == 5 || value == 7; };
|
||||
auto repair = [&] {
|
||||
failure_handled = true;
|
||||
// Hide setup until a fresh SteamVR session, while retaining late-Accept protection.
|
||||
if (write(ready, "0", 1) != 1) return false;
|
||||
bool ok = saved && restore(*setup, path, *saved);
|
||||
fprintf(stderr, "monado-roomsetup-guard: previous calibration restore=%s\n", ok ? "ok" : "FAILED");
|
||||
if (notifications && overlay != vr::k_ulOverlayHandleInvalid) {
|
||||
vr::VRNotificationId id;
|
||||
notifications->CreateNotification(overlay, 0, vr::EVRNotificationType_Persistent,
|
||||
ok ? "Playspace setup failed. Previous boundary restored. Restart SteamVR before drawing again."
|
||||
: "Playspace setup failed. Calibration restore failed. Stop using VR and restore your saved boundary.",
|
||||
vr::EVRNotificationStyle_Application, nullptr, &id);
|
||||
}
|
||||
return ok;
|
||||
};
|
||||
auto *mailbox = static_cast<roomsetup::Mailbox *>(interface("IVRMailbox_001", &error));
|
||||
auto *paths = static_cast<roomsetup::Paths *>(interface("IVRPaths_001", &error));
|
||||
auto floor = std::make_unique<roomsetup::SavedFloor>(mailbox, paths);
|
||||
floor->publish();
|
||||
log_coordinates(*system, *setup, 0, step);
|
||||
bool floor_pending = false;
|
||||
auto floor_deadline = std::chrono::steady_clock::time_point{};
|
||||
bool running = true;
|
||||
bool world_setup = false;
|
||||
auto world_cancel_deadline = std::chrono::steady_clock::time_point{};
|
||||
std::string world_warning;
|
||||
vr::VRNotificationId world_notification = 0;
|
||||
auto next_notification_attempt = std::chrono::steady_clock::time_point{};
|
||||
while (running && !exited(parent)) {
|
||||
// Test before consuming Accept's events, including a compositor that exits immediately before Accept.
|
||||
bool recent_exit = std::chrono::steady_clock::now() <= exit_deadline;
|
||||
if ((drawing(step) || recent_exit) && saved && !failure_handled &&
|
||||
compositor_failed()) {
|
||||
failed = true;
|
||||
fprintf(stderr, "monado-roomsetup-guard: native setup failure=%s\n",
|
||||
overflow_detected ? "transfer_ring_overflow" : "compositor_exit");
|
||||
if (!repair()) break;
|
||||
}
|
||||
vr::VREvent_t event{};
|
||||
while (system->PollNextEvent(&event, sizeof(event))) {
|
||||
if (event.eventType == 809) {
|
||||
// SteamVR 2.17's native setup step event is reserved in the public header.
|
||||
uint32_t next = 0;
|
||||
memcpy(&next, &event.data, sizeof(next));
|
||||
if (drawing(next) && !drawing(step) && !failed) {
|
||||
saved = snapshot(*setup, path);
|
||||
diagnostics.begin();
|
||||
overflow_detected = false;
|
||||
exit_deadline = {};
|
||||
if (compositor >= 0) close(compositor);
|
||||
compositor = compositor_fd();
|
||||
if (!saved || exited(compositor)) { running = false; break; }
|
||||
fprintf(stderr, "monado-roomsetup-guard: snapshot step=%u\n", next);
|
||||
}
|
||||
if (next == 0 && drawing(step)) {
|
||||
native_commit = true;
|
||||
exit_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
commit_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
|
||||
if (saved && compositor_failed()) failed = true;
|
||||
}
|
||||
if ((next == 2 || next == 3 || drawing(next)) && !world_setup && !failed) {
|
||||
world_setup = true;
|
||||
world_cancel_deadline = {};
|
||||
world_command("begin");
|
||||
}
|
||||
if (next == 0 && world_setup)
|
||||
world_cancel_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
|
||||
step = next;
|
||||
// Native Manual mode retains stick adjustments even when a result arrives.
|
||||
if (next == 2 || next == 3 || next == 5 || next == 7) {
|
||||
floor_pending = true;
|
||||
floor_deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1);
|
||||
}
|
||||
}
|
||||
if (event.eventType == vr::VREvent_ChaperoneRoomSetupFinished && native_commit) {
|
||||
// Cancel has the same step-zero event, but no immediate commit.
|
||||
// A later unrelated calibration edit must not inherit that attempt.
|
||||
bool immediate = std::chrono::steady_clock::now() <= commit_deadline;
|
||||
native_commit = false; // Our restore also emits this event.
|
||||
if (immediate && saved && compositor_failed()) failed = true;
|
||||
if (immediate && failed && !repair()) { running = false; break; }
|
||||
if (immediate && world_setup && !failed) {
|
||||
world_command("save");
|
||||
world_setup = false;
|
||||
world_cancel_deadline = {};
|
||||
}
|
||||
}
|
||||
if (event.eventType == 809 || event.eventType == vr::VREvent_ChaperoneUniverseHasChanged ||
|
||||
event.eventType == vr::VREvent_ChaperoneDataHasChanged || event.eventType == vr::VREvent_ChaperoneRoomSetupFinished ||
|
||||
event.eventType == vr::VREvent_StandingZeroPoseReset || event.eventType == vr::VREvent_SeatedZeroPoseReset ||
|
||||
event.eventType == vr::VREvent_RoomViewShown || event.eventType == vr::VREvent_RoomViewHidden ||
|
||||
event.eventType == vr::VREvent_DashboardActivated || event.eventType == vr::VREvent_DashboardDeactivated)
|
||||
log_coordinates(*system, *setup, event.eventType, step);
|
||||
if (event.eventType == vr::VREvent_Quit) running = false;
|
||||
}
|
||||
if (world_setup && world_cancel_deadline != std::chrono::steady_clock::time_point{} &&
|
||||
std::chrono::steady_clock::now() >= world_cancel_deadline) {
|
||||
world_command("cancel");
|
||||
world_setup = false;
|
||||
world_cancel_deadline = {};
|
||||
}
|
||||
auto status = world_status();
|
||||
std::string warning;
|
||||
if (status == "save_failed")
|
||||
warning = "Visual boundary map could not be saved. Check Monado config write access before restarting VR.";
|
||||
else if (status == "unanchored")
|
||||
warning = "Boundary has no visual anchor. Complete room setup once to save its physical location.";
|
||||
else if (status == "relocalizing")
|
||||
warning = "Boundary not confirmed, look around. Temporary tracking is active.";
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
if (warning != world_warning && notifications && overlays && now >= next_notification_attempt) {
|
||||
next_notification_attempt = now + std::chrono::seconds(1);
|
||||
if (overlay == vr::k_ulOverlayHandleInvalid)
|
||||
overlays->CreateOverlay("monado.roomsetup.guard", "Monado playspace setup", &overlay);
|
||||
if (overlay != vr::k_ulOverlayHandleInvalid)
|
||||
update_world_notification(*notifications, overlay, warning, world_warning, world_notification);
|
||||
}
|
||||
if (floor_pending) {
|
||||
if (step == 0 || floor->publish(step == 2) || std::chrono::steady_clock::now() > floor_deadline)
|
||||
floor_pending = false;
|
||||
}
|
||||
floor->update_frame(step == 2 || step == 3 || step == 4 || step == 5 || step == 7);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
}
|
||||
if (world_setup) world_command("cancel");
|
||||
if (world_notification && notifications) notifications->RemoveNotification(world_notification);
|
||||
if (overlays && overlay != vr::k_ulOverlayHandleInvalid) overlays->DestroyOverlay(overlay);
|
||||
if (compositor >= 0) close(compositor);
|
||||
close(parent);
|
||||
close(ready);
|
||||
floor.reset();
|
||||
shutdown();
|
||||
dlclose(library);
|
||||
return running ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
#pragma once
|
||||
#ifdef __linux__
|
||||
#include <dlfcn.h>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <spawn.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
extern char **environ;
|
||||
static void roomsetup_guard_module() {}
|
||||
|
||||
// Native setup is advertised only after the session-owned guard acknowledges readiness.
|
||||
class RoomSetupGuardProcess
|
||||
{
|
||||
pid_t pid = -1;
|
||||
int acknowledgement = -1;
|
||||
bool acknowledged = false;
|
||||
public:
|
||||
~RoomSetupGuardProcess()
|
||||
{
|
||||
if (acknowledgement >= 0) close(acknowledgement);
|
||||
if (pid > 0 && waitpid(pid, nullptr, WNOHANG) == 0) {
|
||||
kill(pid, SIGTERM);
|
||||
waitpid(pid, nullptr, 0);
|
||||
}
|
||||
}
|
||||
bool start()
|
||||
{
|
||||
Dl_info module{};
|
||||
char server[4096];
|
||||
ssize_t length = readlink("/proc/self/exe", server, sizeof(server) - 1);
|
||||
if (length <= 0 || !dladdr(reinterpret_cast<void *>(roomsetup_guard_module), &module)) return false;
|
||||
server[length] = 0;
|
||||
auto directory = std::filesystem::path(module.dli_fname).parent_path();
|
||||
std::string executable = (directory / "monado-roomsetup-guard").string();
|
||||
std::string library = (std::filesystem::path(server).parent_path() / "libopenvr_api.so").string();
|
||||
std::string parent = std::to_string(getpid());
|
||||
int descriptors[2];
|
||||
if (pipe2(descriptors, O_CLOEXEC | O_NONBLOCK) != 0) return false;
|
||||
posix_spawn_file_actions_t actions;
|
||||
posix_spawn_file_actions_init(&actions);
|
||||
constexpr int child_acknowledgement = 198;
|
||||
posix_spawn_file_actions_adddup2(&actions, descriptors[1], child_acknowledgement);
|
||||
char destination[] = "198";
|
||||
char *arguments[] = {executable.data(), library.data(), parent.data(), destination, nullptr};
|
||||
int error = posix_spawn(&pid, executable.c_str(), &actions, nullptr, arguments, environ);
|
||||
posix_spawn_file_actions_destroy(&actions);
|
||||
close(descriptors[1]);
|
||||
if (error) { fprintf(stderr, "monado-roomsetup-guard: spawn %s: %s\n", executable.c_str(), strerror(error)); close(descriptors[0]); pid = -1; return false; }
|
||||
acknowledgement = descriptors[0];
|
||||
return true;
|
||||
}
|
||||
bool ready()
|
||||
{
|
||||
if (pid <= 0) return false;
|
||||
if (waitpid(pid, nullptr, WNOHANG) == pid) { pid = -1; acknowledged = false; }
|
||||
if (acknowledgement >= 0) {
|
||||
char value = 0;
|
||||
ssize_t count = read(acknowledgement, &value, 1);
|
||||
if (count == 1) acknowledged = pid > 0 && value == '1';
|
||||
if (count == 0) {
|
||||
acknowledged = false;
|
||||
close(acknowledgement);
|
||||
acknowledgement = -1;
|
||||
}
|
||||
}
|
||||
return acknowledged;
|
||||
}
|
||||
};
|
||||
#else
|
||||
class RoomSetupGuardProcess
|
||||
{
|
||||
public:
|
||||
bool start() { return false; }
|
||||
bool ready() { return false; }
|
||||
};
|
||||
#endif
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,36 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifndef RIFTS_PASSTHROUGH_SHM_PATH
|
||||
#define RIFTS_PASSTHROUGH_SHM_PATH "/dev/shm/rifts_passthrough"
|
||||
#endif
|
||||
#define RIFTS_PASSTHROUGH_MAGIC 0x54535052 /* "RPST" */
|
||||
#define RIFTS_PASSTHROUGH_VERSION 2
|
||||
#define RIFTS_PASSTHROUGH_WIDTH 640
|
||||
#define RIFTS_PASSTHROUGH_HEIGHT 320
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct rifts_passthrough_shm
|
||||
{
|
||||
uint32_t magic; /* RIFTS_PASSTHROUGH_MAGIC */
|
||||
uint32_t version; /* RIFTS_PASSTHROUGH_VERSION */
|
||||
uint32_t width; /* 640 */
|
||||
uint32_t height; /* 320 */
|
||||
uint32_t stride; /* 640 * 4 = 2560 */
|
||||
uint32_t format; /* 0 = RGBA32 */
|
||||
uint32_t client_active; /* Consumer monotonic-ms heartbeat; 0 = idle */
|
||||
uint32_t toggle_count; /* Incremented on controller button toggle */
|
||||
uint64_t sequence; /* Seqlock: odd during write, even when complete */
|
||||
uint64_t timestamp_ns; /* Monotonic capture timestamp */
|
||||
uint8_t pixels[RIFTS_PASSTHROUGH_WIDTH * RIFTS_PASSTHROUGH_HEIGHT * 4];
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -28,7 +28,7 @@ def ovrd_generate_bindings_c(file, b):
|
||||
hw_name = str(profile.name.split("/")[-1])
|
||||
vendor_name = str(profile.name.split("/")[-2])
|
||||
fname = vendor_name + "_" + hw_name + "_profile.json"
|
||||
controller_type = "monado_" + vendor_name + "_" + hw_name
|
||||
controller_type = profile.steamvr_controller_type or "monado_" + vendor_name + "_" + hw_name
|
||||
|
||||
binding_count = len(profile.components)
|
||||
wl(f,
|
||||
@@ -47,7 +47,7 @@ def ovrd_generate_bindings_c(file, b):
|
||||
|
||||
# @todo Doesn't handle pose yet.
|
||||
steamvr_path = component.steamvr_path
|
||||
if component.component_name in ["click", "touch", "force", "value", "proximity"]:
|
||||
if component.component_name in ["click", "touch", "force", "value", "proximity", "proximity_fb"]:
|
||||
steamvr_path += "/" + component.component_name
|
||||
|
||||
wl(f,
|
||||
|
||||
@@ -44,7 +44,7 @@ enum u_logging_level rift_s_log_level;
|
||||
*
|
||||
*/
|
||||
|
||||
DEBUG_GET_ONCE_LOG_OPTION(rift_s_log, "RIFT_S_LOG", U_LOGGING_WARN)
|
||||
DEBUG_GET_ONCE_LOG_OPTION(rift_s_log, "RIFT_S_LOG", U_LOGGING_INFO)
|
||||
|
||||
#ifdef XRT_BUILD_DRIVER_HANDTRACKING
|
||||
DEBUG_GET_ONCE_BOOL_OPTION(rift_s_hand_tracking_as_controller, "RIFT_S_HAND_TRACKING_AS_CONTROLLERS", false)
|
||||
@@ -166,7 +166,8 @@ rift_s_open_system_impl(struct xrt_builder *xb,
|
||||
/* We'll give everyone a shared tracking origin using the system allocated one */
|
||||
origin->type = XRT_TRACKING_TYPE_OTHER;
|
||||
origin->initial_offset.orientation.w = 1.0f;
|
||||
origin->initial_offset.position.y = 1.6;
|
||||
// The SLAM guard already supplies nominal eye height in this tracking frame.
|
||||
origin->initial_offset.position.y = 0.0f;
|
||||
snprintf(origin->name, XRT_TRACKING_NAME_LEN, "%s", "Oculus Rift S SLAM Tracking");
|
||||
|
||||
// Create and add to list.
|
||||
|
||||
@@ -10,7 +10,8 @@ add_custom_command(
|
||||
COMMAND
|
||||
${PYTHON_EXECUTABLE} ${INPUT_PROFILES_INPUT_DIR}/steamvr_profiles.py
|
||||
${INPUT_PROFILES_INPUT_DIR}/bindings.json "${INPUT_PROFILES_OUTPUT_DIR}" "-s"
|
||||
DEPENDS ${INPUT_PROFILES_INPUT_DIR}/bindings.py ${INPUT_PROFILES_INPUT_DIR}/bindings.json
|
||||
DEPENDS ${INPUT_PROFILES_INPUT_DIR}/steamvr_profiles.py
|
||||
${INPUT_PROFILES_INPUT_DIR}/bindings.py ${INPUT_PROFILES_INPUT_DIR}/bindings.json
|
||||
COMMENT "Generating SteamVR input profiles to ${INPUT_PROFILES_OUTPUT_DIR}"
|
||||
)
|
||||
add_custom_target(
|
||||
@@ -36,6 +37,17 @@ target_link_libraries(
|
||||
# meta data that the steamvr plugin needs in the base directory of the steamvr plugin
|
||||
file(COPY driver.vrdrivermanifest DESTINATION ${PROJECT_BINARY_DIR}/steamvr-monado)
|
||||
file(COPY resources DESTINATION ${PROJECT_BINARY_DIR}/steamvr-monado)
|
||||
configure_file(
|
||||
resources/input/monado_touch_compositor.json
|
||||
${INPUT_PROFILES_OUTPUT_DIR}/monado_touch_compositor.json
|
||||
COPYONLY
|
||||
)
|
||||
|
||||
configure_file(
|
||||
resources/settings/default.vrsettings
|
||||
${PROJECT_BINARY_DIR}/steamvr-monado/resources/settings/default.vrsettings
|
||||
COPYONLY
|
||||
)
|
||||
|
||||
# determine the output directory for the steamvr plugin
|
||||
if(WIN32)
|
||||
@@ -69,3 +81,8 @@ install(
|
||||
DIRECTORY "${PROJECT_BINARY_DIR}/steamvr-monado"
|
||||
DESTINATION "${CMAKE_INSTALL_PREFIX}/share"
|
||||
)
|
||||
|
||||
# The directory install treats files as data; install the guard as an executable last.
|
||||
if(TARGET monado-roomsetup-guard)
|
||||
install(TARGETS monado-roomsetup-guard RUNTIME DESTINATION ${CMAKE_INSTALL_DATADIR}/steamvr-monado/bin/linux64)
|
||||
endif()
|
||||
@@ -0,0 +1,279 @@
|
||||
{
|
||||
"action_manifest_version": 0,
|
||||
"app_key": "openvr.component.vrcompositor",
|
||||
"controller_type": "monado_oculus_touch",
|
||||
"category": "steamvr_input",
|
||||
"name": "Monado Oculus Touch compositor",
|
||||
"description": "Dashboard and native playspace setup for Oculus Touch.",
|
||||
"bindings": {
|
||||
"/actions/system": {
|
||||
"sources": [
|
||||
{
|
||||
"path": "/user/hand/left/input/system",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/system/in/ToggleDashboard"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"/actions/lasermouse": {
|
||||
"sources": [
|
||||
{
|
||||
"path": "/user/hand/left/input/trigger",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/lasermouse/in/LeftClick"
|
||||
}
|
||||
},
|
||||
"parameters": {
|
||||
"click_activate_threshold": "0.65",
|
||||
"click_deactivate_threshold": "0.6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/trigger",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/lasermouse/in/LockMousePosition"
|
||||
}
|
||||
},
|
||||
"parameters": {
|
||||
"click_activate_threshold": "0.1",
|
||||
"click_deactivate_threshold": "0.05",
|
||||
"haptic_amplitude": "0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/x",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/lasermouse/in/Home"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/y",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/lasermouse/in/Back"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/joystick",
|
||||
"mode": "joystick",
|
||||
"inputs": {
|
||||
"position": {
|
||||
"output": "/actions/lasermouse/in/TrackpadValue"
|
||||
},
|
||||
"touch": {
|
||||
"output": "/actions/lasermouse/in/TrackpadTouch"
|
||||
},
|
||||
"click": {
|
||||
"output": "/actions/lasermouse/in/RightClick"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/trigger",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/lasermouse/in/LeftClick"
|
||||
}
|
||||
},
|
||||
"parameters": {
|
||||
"click_activate_threshold": "0.65",
|
||||
"click_deactivate_threshold": "0.6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/trigger",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/lasermouse/in/LockMousePosition"
|
||||
}
|
||||
},
|
||||
"parameters": {
|
||||
"click_activate_threshold": "0.1",
|
||||
"click_deactivate_threshold": "0.05",
|
||||
"haptic_amplitude": "0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/a",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/lasermouse/in/Home"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/b",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/lasermouse/in/Back"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/joystick",
|
||||
"mode": "joystick",
|
||||
"inputs": {
|
||||
"position": {
|
||||
"output": "/actions/lasermouse/in/TrackpadValue"
|
||||
},
|
||||
"touch": {
|
||||
"output": "/actions/lasermouse/in/TrackpadTouch"
|
||||
},
|
||||
"click": {
|
||||
"output": "/actions/lasermouse/in/RightClick"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"poses": [
|
||||
{
|
||||
"path": "/user/hand/left/pose/tip",
|
||||
"output": "/actions/lasermouse/in/Pointer"
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/pose/tip",
|
||||
"output": "/actions/lasermouse/in/Pointer"
|
||||
}
|
||||
],
|
||||
"haptics": [
|
||||
{
|
||||
"path": "/user/hand/left/output/haptic",
|
||||
"output": "/actions/lasermouse/out/Haptic"
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/output/haptic",
|
||||
"output": "/actions/lasermouse/out/Haptic"
|
||||
}
|
||||
]
|
||||
},
|
||||
"/actions/roomsetup": {
|
||||
"sources": [
|
||||
{
|
||||
"path": "/user/hand/left/input/x",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/roomsetup/in/Accept"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/y",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/roomsetup/in/Back"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/left/input/joystick",
|
||||
"mode": "joystick",
|
||||
"inputs": {
|
||||
"position": {
|
||||
"output": "/actions/roomsetup/in/FloorFineAdjust"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/a",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/roomsetup/in/Accept"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/b",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/roomsetup/in/Back"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/joystick",
|
||||
"mode": "joystick",
|
||||
"inputs": {
|
||||
"position": {
|
||||
"output": "/actions/roomsetup/in/FloorFineAdjust"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"/actions/scroll_smooth": {
|
||||
"sources": [
|
||||
{
|
||||
"path": "/user/hand/left/input/joystick",
|
||||
"mode": "joystick",
|
||||
"inputs": {
|
||||
"position": {
|
||||
"output": "/actions/scroll_smooth/in/Scroll"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/joystick",
|
||||
"mode": "joystick",
|
||||
"inputs": {
|
||||
"position": {
|
||||
"output": "/actions/scroll_smooth/in/Scroll"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"/actions/lasermouse_secondary": {
|
||||
"sources": [
|
||||
{
|
||||
"path": "/user/hand/left/input/trigger",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/lasermouse_secondary/in/SwitchLaserHand"
|
||||
}
|
||||
},
|
||||
"parameters": {
|
||||
"click_activate_threshold": "0.05",
|
||||
"click_deactivate_threshold": "0.02",
|
||||
"haptic_amplitude": "0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/user/hand/right/input/trigger",
|
||||
"mode": "button",
|
||||
"inputs": {
|
||||
"click": {
|
||||
"output": "/actions/lasermouse_secondary/in/SwitchLaserHand"
|
||||
}
|
||||
},
|
||||
"parameters": {
|
||||
"click_activate_threshold": "0.05",
|
||||
"click_deactivate_threshold": "0.02",
|
||||
"haptic_amplitude": "0"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"driver_monado" : {
|
||||
"enable" : true
|
||||
"enable" : true,
|
||||
"enableNativeRoomSetup" : true
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ add_library(
|
||||
internal/blobwatch.h
|
||||
internal/correspondence_search.c
|
||||
internal/correspondence_search.h
|
||||
internal/constrained_pose.cpp
|
||||
internal/constrained_pose.h
|
||||
internal/debug_draw.cpp
|
||||
internal/debug_draw.h
|
||||
internal/lambdatwist/cubic.h
|
||||
@@ -31,6 +33,7 @@ target_link_libraries(
|
||||
constellation_tracker
|
||||
PRIVATE
|
||||
xrt-interfaces
|
||||
Eigen3::Eigen
|
||||
aux_os
|
||||
aux_util
|
||||
aux_math
|
||||
|
||||
@@ -272,16 +272,14 @@ process_scanline(uint8_t *line,
|
||||
struct xrt_frame *frame,
|
||||
blobservation *ob)
|
||||
{
|
||||
struct extent *le_end = prev_el->extents;
|
||||
struct extent *le = prev_el->extents;
|
||||
struct extent *le = prev_el ? prev_el->extents : NULL;
|
||||
struct extent *le_end = prev_el ? le + prev_el->num : NULL;
|
||||
struct extent *extent = el->extents;
|
||||
int num_extents = MAX_EXTENTS_PER_LINE;
|
||||
float center;
|
||||
uint32_t x;
|
||||
int e = 0;
|
||||
|
||||
if (prev_el)
|
||||
le_end += prev_el->num;
|
||||
|
||||
for (x = 0; x < frame->width; x++) {
|
||||
int start, end;
|
||||
@@ -292,6 +290,7 @@ process_scanline(uint8_t *line,
|
||||
if (line[x] <= bw->pixel_threshold)
|
||||
continue;
|
||||
|
||||
max_pixel = line[x];
|
||||
start = x++;
|
||||
|
||||
/* Loop until pixel value falls below threshold */
|
||||
@@ -316,7 +315,7 @@ process_scanline(uint8_t *line,
|
||||
* bottom of finished blobs. Store them into an array.
|
||||
*/
|
||||
while (le < le_end && le->end < center) {
|
||||
extent_to_blobs(bw, ob, le, y, frame);
|
||||
extent_to_blobs(bw, ob, le, y - 1, frame);
|
||||
le++;
|
||||
}
|
||||
|
||||
@@ -357,7 +356,7 @@ process_scanline(uint8_t *line,
|
||||
* extents in the previous line are finished blobs. Store them.
|
||||
*/
|
||||
while (le < le_end) {
|
||||
extent_to_blobs(bw, ob, le, y, frame);
|
||||
extent_to_blobs(bw, ob, le, y - 1, frame);
|
||||
le++;
|
||||
}
|
||||
}
|
||||
@@ -389,8 +388,8 @@ process_frame(blobwatch *bw, blobservation *ob, struct xrt_frame *frame)
|
||||
process_scanline(line, bw, 0, &el1, NULL, frame, ob);
|
||||
|
||||
for (uint32_t y = 1; y < frame->height; y++) {
|
||||
process_scanline(line, bw, y, y & 1 ? &el2 : &el1, y & 1 ? &el1 : &el2, frame, ob);
|
||||
line += frame->stride;
|
||||
process_scanline(line, bw, y, y & 1 ? &el2 : &el1, y & 1 ? &el1 : &el2, frame, ob);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,4 +16,102 @@ struct camera_model
|
||||
|
||||
/* Distortion / projection parameters */
|
||||
struct t_camera_model_params calib;
|
||||
bool fisheye62_valid;
|
||||
float fisheye62_radial[6], fisheye62_p1, fisheye62_p2;
|
||||
};
|
||||
|
||||
/* Native six-radial/two-tangential fisheye model. Work in the angular plane so
|
||||
* inversion stays finite at the wide sensor edges, unlike view-plane x/z. */
|
||||
static inline void
|
||||
camera_model_fisheye62(const struct camera_model *camera, double x, double y, double *u, double *v, double jacobian[4])
|
||||
{
|
||||
double r2 = x * x + y * y, radial = 1, derivative = 0, power = 1;
|
||||
for (unsigned i = 0; i < 6; ++i) {
|
||||
derivative += (i + 1) * camera->fisheye62_radial[i] * power;
|
||||
power *= r2;
|
||||
radial += camera->fisheye62_radial[i] * power;
|
||||
}
|
||||
// The two tangential coefficients act on X/Y after radial distortion.
|
||||
double p1 = camera->fisheye62_p1, p2 = camera->fisheye62_p2;
|
||||
double rx = x * radial, ry = y * radial, rr2 = rx * rx + ry * ry;
|
||||
double tangent = 2 * (p1 * rx + p2 * ry);
|
||||
*u = rx * (1 + tangent) + p1 * rr2;
|
||||
*v = ry * (1 + tangent) + p2 * rr2;
|
||||
if (jacobian) {
|
||||
double rxx = radial + 2 * x * x * derivative;
|
||||
double rxy = 2 * x * y * derivative;
|
||||
double ryy = radial + 2 * y * y * derivative;
|
||||
double txx = 1 + 6 * p1 * rx + 2 * p2 * ry;
|
||||
double txy = 2 * p1 * ry + 2 * p2 * rx;
|
||||
double tyy = 1 + 2 * p1 * rx + 6 * p2 * ry;
|
||||
// Row-major J_tangential * J_radial is generally not symmetric.
|
||||
jacobian[0] = txx * rxx + txy * rxy;
|
||||
jacobian[1] = txx * rxy + txy * ryy;
|
||||
jacobian[2] = txy * rxx + tyy * rxy;
|
||||
jacobian[3] = txy * rxy + tyy * ryy;
|
||||
}
|
||||
}
|
||||
|
||||
static inline bool
|
||||
camera_model_project(const struct camera_model *camera, float x, float y, float z, float *u, float *v)
|
||||
{
|
||||
*u = *v = NAN;
|
||||
if (!camera->fisheye62_valid)
|
||||
return t_camera_models_project(&camera->calib, x, y, z, u, v) && isfinite(*u) && isfinite(*v);
|
||||
if (!isfinite(x) || !isfinite(y) || !isfinite(z) || z <= 0)
|
||||
return false;
|
||||
double r = hypot(x, y), scale = r > 1e-12 ? atan2(r, z) / r : 1 / z;
|
||||
double a, b;
|
||||
camera_model_fisheye62(camera, x * scale, y * scale, &a, &b, NULL);
|
||||
*u = camera->calib.fx * a + camera->calib.cx;
|
||||
*v = camera->calib.fy * b + camera->calib.cy;
|
||||
return isfinite(*u) && isfinite(*v);
|
||||
}
|
||||
|
||||
static inline bool
|
||||
camera_model_unproject(const struct camera_model *camera, float u, float v, float *x, float *y, float *z)
|
||||
{
|
||||
*x = *y = *z = NAN;
|
||||
if (!isfinite(u) || !isfinite(v) || !(camera->calib.fx > 0) || !(camera->calib.fy > 0))
|
||||
return false;
|
||||
if (!camera->fisheye62_valid)
|
||||
return t_camera_models_unproject(&camera->calib, u, v, x, y, z) && isfinite(*x) && isfinite(*y) &&
|
||||
isfinite(*z);
|
||||
double target_x = (u - camera->calib.cx) / camera->calib.fx;
|
||||
double target_y = (v - camera->calib.cy) / camera->calib.fy;
|
||||
double a = target_x, b = target_y;
|
||||
for (unsigned iteration = 0; iteration < 20; ++iteration) {
|
||||
double px, py, j[4];
|
||||
camera_model_fisheye62(camera, a, b, &px, &py, j);
|
||||
double ex = px - target_x, ey = py - target_y;
|
||||
if (hypot(ex, ey) < 1e-9)
|
||||
break;
|
||||
double det = j[0] * j[3] - j[1] * j[2];
|
||||
if (!isfinite(det) || det <= 1e-12)
|
||||
return false;
|
||||
a -= (j[3] * ex - j[1] * ey) / det;
|
||||
b -= (j[0] * ey - j[2] * ex) / det;
|
||||
}
|
||||
double px, py;
|
||||
camera_model_fisheye62(camera, a, b, &px, &py, NULL);
|
||||
double theta = hypot(a, b);
|
||||
if (!isfinite(theta) || theta >= M_PI || hypot(px - target_x, py - target_y) > 1e-7)
|
||||
return false;
|
||||
double scale = theta > 1e-12 ? sin(theta) / theta : 1;
|
||||
*x = a * scale;
|
||||
*y = b * scale;
|
||||
*z = cos(theta);
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline bool
|
||||
camera_model_undistort(const struct camera_model *camera, float u, float v, float *x, float *y)
|
||||
{
|
||||
float xp, yp, z;
|
||||
*x = *y = NAN;
|
||||
if (!camera_model_unproject(camera, u, v, &xp, &yp, &z) || z <= 1e-6f)
|
||||
return false;
|
||||
*x = xp / z;
|
||||
*y = yp / z;
|
||||
return isfinite(*x) && isfinite(*y);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,424 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
#pragma once
|
||||
|
||||
#include "blobwatch.h"
|
||||
#include "camera_model.h"
|
||||
#include "tracking/t_led_models.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define CONSTELLATION_CONSTRAINED_MAX_VIEWS 5
|
||||
#define CONSTELLATION_CONSTRAINED_MAX_ASSIGNMENTS (CONSTELLATION_CONSTRAINED_MAX_VIEWS * MAX_BLOBS_PER_FRAME)
|
||||
|
||||
/* Both transforms use the camera's optical (+Z forward) coordinates. */
|
||||
struct constellation_constrained_view
|
||||
{
|
||||
unsigned camera_index;
|
||||
struct xrt_pose P_world_cam;
|
||||
struct xrt_pose P_cam_world;
|
||||
const struct camera_model *calib;
|
||||
const struct blobservation *observation;
|
||||
//! Same-exposure published claims to omit from acquisition; temporal LED labels stay advisory.
|
||||
bool excluded_blobs[MAX_BLOBS_PER_FRAME];
|
||||
};
|
||||
|
||||
enum constellation_constrained_origin
|
||||
{
|
||||
CONSTELLATION_CONSTRAINED_GRAVITY,
|
||||
CONSTELLATION_CONSTRAINED_HEADING,
|
||||
CONSTELLATION_CONSTRAINED_PRIOR,
|
||||
CONSTELLATION_CONSTRAINED_P3P,
|
||||
};
|
||||
|
||||
enum constellation_constrained_rejection
|
||||
{
|
||||
CONSTELLATION_CONSTRAINED_ACCEPTED,
|
||||
CONSTELLATION_CONSTRAINED_INVALID_INPUT,
|
||||
CONSTELLATION_CONSTRAINED_NO_PAIR,
|
||||
CONSTELLATION_CONSTRAINED_TOO_FEW_INLIERS,
|
||||
CONSTELLATION_CONSTRAINED_RANK_DEFICIENT,
|
||||
CONSTELLATION_CONSTRAINED_UNCERTAIN,
|
||||
CONSTELLATION_CONSTRAINED_AMBIGUOUS,
|
||||
CONSTELLATION_CONSTRAINED_SHARED_BLOB,
|
||||
//! Cold fit fails the angular goodness-of-fit test or leaves blobs unexplained inside its footprint.
|
||||
CONSTELLATION_CONSTRAINED_INCONSISTENT,
|
||||
};
|
||||
|
||||
//! Outcome of one minimal two-point solve, so degenerate geometry is never silently resolved.
|
||||
enum constellation_constrained_solver_status
|
||||
{
|
||||
CONSTELLATION_SOLVER_OK,
|
||||
CONSTELLATION_SOLVER_INVALID_INPUT,
|
||||
//! Bearings closer than the minimum separation (or anti-parallel).
|
||||
CONSTELLATION_SOLVER_DEGENERATE_BEARINGS,
|
||||
//! The two model points coincide.
|
||||
CONSTELLATION_SOLVER_DEGENERATE_MODEL,
|
||||
//! The pair carries no heading information: every yaw explains it equally.
|
||||
CONSTELLATION_SOLVER_UNRESOLVED_YAW,
|
||||
//! No real yaw root exists for this assignment.
|
||||
CONSTELLATION_SOLVER_NO_ROOT,
|
||||
//! Roots exist but put an LED behind the camera or leave translation rank deficient.
|
||||
CONSTELLATION_SOLVER_NONPOSITIVE_DEPTH,
|
||||
};
|
||||
|
||||
struct constellation_constrained_config
|
||||
{
|
||||
//! Object-space score gate in metres (stock default, not an active setting).
|
||||
double score_gate_m;
|
||||
//! Broad object-space association gate in metres for candidate edges.
|
||||
double association_gate_m;
|
||||
/* With a trusted prior, hypotheses whose model origin lands farther than this from the prior
|
||||
* position are discarded. The prior only selects candidate edges; without this bound a
|
||||
* hypothesis anywhere in space could win on inlier count and either be published as a wrong
|
||||
* identity or make the correct one look ambiguous. */
|
||||
double maximum_prior_distance_m;
|
||||
//! Bound association rotation only while a confirmed track has a verified heading.
|
||||
double maximum_prior_rotation_rad;
|
||||
/* Refinement may change the hypothesis' tilt (from the IMU, or from P3P) under a Gaussian
|
||||
* prior of this standard deviation, and never by more than the maximum: the accelerometer and
|
||||
* the attitude history at the exposure time are off by a few degrees, which otherwise fails
|
||||
* the goodness-of-fit test on well-observed rings. Zero disables the adjustment, and fits of
|
||||
* fewer distinct LEDs than the minimum keep the prior tilt. */
|
||||
double tilt_prior_std_rad;
|
||||
double maximum_tilt_correction_rad;
|
||||
unsigned minimum_tilt_distinct_leds;
|
||||
double minimum_bearing_separation_rad;
|
||||
double minimum_depth_m;
|
||||
double maximum_depth_m;
|
||||
/* Proposed noise/uncertainty policy, not device calibration. */
|
||||
double ray_noise_std_m;
|
||||
double maximum_position_std_m;
|
||||
double maximum_yaw_std_rad;
|
||||
//! Minimum likelihood gap (chi-square units of ray_noise_std_m) to the best competing assignment.
|
||||
double ambiguity_chi_square;
|
||||
/* Monado verification policy, beyond the stock scorer count (inliers > sample size):
|
||||
* accepted observations required when acquiring without a trusted position prior, and the
|
||||
* distinct physical LEDs among them. Repeated views of one LED do not fix its identity. */
|
||||
unsigned minimum_cold_inliers;
|
||||
unsigned minimum_cold_distinct_leds;
|
||||
/* The same counts for searches around a trusted prior. The stock minimum (inliers > sample
|
||||
* size, so 2 for the retained prior) let one stale prior keep re-verifying itself on two blobs
|
||||
* in worn use; a published pose now needs this much evidence on every path. */
|
||||
unsigned minimum_tracking_inliers;
|
||||
unsigned minimum_tracking_distinct_leds;
|
||||
/* Unmatched blobs within conflict_gate_m (object space) of an LED the hypothesis predicts
|
||||
* visible, ignoring fragments next to an accepted LED. A wrong fit typically puts predicted
|
||||
* LEDs next to the ring's own blobs without explaining them. Applies on every path. */
|
||||
unsigned maximum_cold_unexplained_blobs;
|
||||
double conflict_gate_m;
|
||||
/* Goodness of fit on every path: angular residuals divided by this centroid bearing noise
|
||||
* must pass a chi-square test at the given standard-normal quantile (Wilson-Hilferty). The noise
|
||||
* is a proposed value pending measurement from replayed frames; zero disables the test. */
|
||||
double bearing_noise_std_rad;
|
||||
double cold_fit_quantile_z;
|
||||
//! Cap on minimal-sample pair trials without a trusted position prior.
|
||||
unsigned max_pair_trials;
|
||||
//! Cap on pair trials drawn from edges near a trusted position prior.
|
||||
unsigned max_prior_pair_trials;
|
||||
unsigned time_budget_us;
|
||||
//! Unused by the deterministic enumeration; kept so callers can record the exposure identity.
|
||||
uint64_t seed;
|
||||
};
|
||||
|
||||
struct constellation_constrained_assignment
|
||||
{
|
||||
unsigned camera_index;
|
||||
unsigned blob_index;
|
||||
unsigned led_index;
|
||||
uint8_t led_id;
|
||||
double residual_m;
|
||||
};
|
||||
|
||||
struct constellation_constrained_result
|
||||
{
|
||||
struct xrt_pose P_world_model;
|
||||
enum constellation_constrained_origin origin;
|
||||
enum constellation_constrained_rejection rejection;
|
||||
unsigned candidate_observations;
|
||||
unsigned inliers;
|
||||
unsigned distinct_leds;
|
||||
unsigned projected_visible_leds;
|
||||
unsigned unexplained_blobs;
|
||||
//! Chi-square of angular residuals and its threshold for the selected hypothesis.
|
||||
double fit_chi_square;
|
||||
double fit_chi_square_limit;
|
||||
unsigned pair_trials;
|
||||
unsigned hypotheses;
|
||||
unsigned gravity_hypotheses;
|
||||
unsigned heading_hypotheses;
|
||||
unsigned degenerate_pairs;
|
||||
unsigned unresolved_yaw_pairs;
|
||||
unsigned no_root_pairs;
|
||||
unsigned depth_pairs;
|
||||
unsigned seed_rejections;
|
||||
unsigned insufficient_hypotheses;
|
||||
unsigned invisible_rejections;
|
||||
unsigned depth_rejections;
|
||||
//! Trusted-prior hypotheses discarded for landing outside maximum_prior_distance_m.
|
||||
unsigned prior_distance_rejections;
|
||||
unsigned prior_rotation_rejections;
|
||||
unsigned num_camera_diagnostics;
|
||||
struct
|
||||
{
|
||||
unsigned camera_index;
|
||||
unsigned detected_blobs;
|
||||
unsigned eligible_blobs;
|
||||
unsigned projected_visible_leds;
|
||||
unsigned candidate_observations;
|
||||
unsigned inliers;
|
||||
} camera_diagnostics[CONSTELLATION_CONSTRAINED_MAX_VIEWS];
|
||||
bool budget_exhausted;
|
||||
/* Cost is clipped object-space RMS in metres, normalized by candidates - sample size. */
|
||||
double cost_m;
|
||||
double second_best_cost_m;
|
||||
double ambiguity_likelihood_gap;
|
||||
unsigned information_rank;
|
||||
/* Row-major [world translation, yaw about world +Y]; absent yaw has infinite variance. */
|
||||
double covariance[16];
|
||||
//! True only when the optical fit itself constrains yaw within maximum_yaw_std_rad.
|
||||
bool vision_determines_yaw;
|
||||
//! How far refinement moved the tilt from the hypothesis' prior tilt.
|
||||
double tilt_correction_rad;
|
||||
unsigned num_assignments;
|
||||
struct constellation_constrained_assignment assignments[CONSTELLATION_CONSTRAINED_MAX_ASSIGNMENTS];
|
||||
};
|
||||
|
||||
void
|
||||
constellation_constrained_default_config(struct constellation_constrained_config *config);
|
||||
|
||||
const char *
|
||||
constellation_constrained_origin_name(enum constellation_constrained_origin origin);
|
||||
|
||||
const char *
|
||||
constellation_constrained_rejection_name(enum constellation_constrained_rejection rejection);
|
||||
|
||||
const char *
|
||||
constellation_constrained_solver_status_name(enum constellation_constrained_solver_status status);
|
||||
|
||||
/* Enumerate every real same-camera yaw/depth root for a gravity-aligned tilt. Returns the
|
||||
* number of poses (model to camera). Zero roots always come with a non-OK status. */
|
||||
unsigned
|
||||
constellation_constrained_gravity_two_point(const struct xrt_vec3 points[2],
|
||||
const struct xrt_vec3 bearings[2],
|
||||
const struct xrt_quat *tilt_model_to_cam,
|
||||
const struct xrt_vec3 *gravity_cam,
|
||||
double minimum_bearing_separation_rad,
|
||||
struct xrt_pose out_poses[2],
|
||||
enum constellation_constrained_solver_status *status);
|
||||
|
||||
/* Translation from two bearings with a known model-to-camera rotation. */
|
||||
bool
|
||||
constellation_constrained_heading_two_point(const struct xrt_vec3 points[2],
|
||||
const struct xrt_vec3 bearings[2],
|
||||
const struct xrt_quat *model_to_cam,
|
||||
double minimum_bearing_separation_rad,
|
||||
struct xrt_pose *out_pose,
|
||||
enum constellation_constrained_solver_status *status);
|
||||
|
||||
/* Low-count acquisition. Gravity-constrained two-point hypotheses are always generated from
|
||||
* the tilt of prior_world. With reliable_heading, known-orientation translation hypotheses are
|
||||
* added. With trusted_position, pairs come from edges near the prior and, if heading is also
|
||||
* reliable, the prior pose itself is scored as a one-point hypothesis. */
|
||||
bool
|
||||
constellation_constrained_search(struct t_constellation_led_model *model,
|
||||
const struct constellation_constrained_view *views,
|
||||
unsigned num_views,
|
||||
const struct xrt_pose *prior_world,
|
||||
bool reliable_heading,
|
||||
bool trusted_position,
|
||||
const struct constellation_constrained_config *config,
|
||||
struct constellation_constrained_result *result);
|
||||
|
||||
/* Jointly verify/refine an existing hypothesis. Its sample size follows origin:
|
||||
* prior 1, gravity/heading 2, P3P 3. P3P hypotheses are cold acquisitions and also get the
|
||||
* cold verification policy. */
|
||||
bool
|
||||
constellation_constrained_verify_pose(struct t_constellation_led_model *model,
|
||||
const struct constellation_constrained_view *views,
|
||||
unsigned num_views,
|
||||
const struct xrt_pose *pose_world,
|
||||
enum constellation_constrained_origin origin,
|
||||
const struct constellation_constrained_config *config,
|
||||
struct constellation_constrained_result *result);
|
||||
|
||||
bool
|
||||
constellation_constrained_results_share_blob(const struct constellation_constrained_result *a,
|
||||
const struct constellation_constrained_result *b);
|
||||
|
||||
//! What kind of claim on a shared blob blocked a candidate.
|
||||
enum constellation_constrained_claim
|
||||
{
|
||||
CONSTELLATION_CLAIM_NONE,
|
||||
//! Another device already published a pose using the blob in this exposure.
|
||||
CONSTELLATION_CLAIM_PUBLISHED,
|
||||
//! Another candidate explains the blob and neither has clearly more support.
|
||||
CONSTELLATION_CLAIM_CANDIDATE,
|
||||
//! Another device's rejected hypothesis passed the consistency test and explains at least as
|
||||
//! many observations.
|
||||
CONSTELLATION_CLAIM_REJECTED,
|
||||
};
|
||||
|
||||
/* Cross-device ownership. A candidate whose observations share a blob with another device's
|
||||
* claim loses (candidates[i] cleared, blocked[i] set) when the claim is a published pose; when it
|
||||
* is another candidate that does not have clearly less support (minimum_support_margin more
|
||||
* inliers and no fewer distinct LEDs wins); or when it is a competitive rejected hypothesis, that
|
||||
* is one rejected only for count, ambiguity or uncertainty with at least as many inliers.
|
||||
* Hypotheses rejected as inconsistent or without a pair are not evidence of ownership.
|
||||
* Published results are never revoked. */
|
||||
void
|
||||
constellation_constrained_arbitrate(const struct constellation_constrained_result *const *results,
|
||||
const bool *published,
|
||||
bool *candidates,
|
||||
enum constellation_constrained_claim *blocked,
|
||||
unsigned count,
|
||||
unsigned minimum_support_margin);
|
||||
|
||||
// Optional blocker indices name the claim actually responsible for rejection; count means none.
|
||||
void
|
||||
constellation_constrained_arbitrate_with_owners(const struct constellation_constrained_result *const *results,
|
||||
const bool *published,
|
||||
bool *candidates,
|
||||
enum constellation_constrained_claim *blocked,
|
||||
unsigned *blockers,
|
||||
unsigned count,
|
||||
unsigned minimum_support_margin);
|
||||
|
||||
const char *
|
||||
constellation_constrained_claim_name(enum constellation_constrained_claim claim);
|
||||
|
||||
/* True when a candidate blocked by a published pose (CONSTELLATION_CLAIM_PUBLISHED) shares a blob
|
||||
* with it and has clearly more support by the same margin rule. The published pose stays
|
||||
* published, but its track should end: a lock on the other controller's ring otherwise keeps
|
||||
* that controller's blobs forever. */
|
||||
bool
|
||||
constellation_constrained_contests_published(const struct constellation_constrained_result *candidate,
|
||||
const struct constellation_constrained_result *published,
|
||||
unsigned minimum_support_margin);
|
||||
|
||||
/* One accepted optical pose with the IMU attitude at the same capture time, both in world. Read
|
||||
* both points' IMU attitudes from the same, current attitude history: a world-yaw correction
|
||||
* applied in between then cancels instead of looking like an optical jump. */
|
||||
struct constellation_track_point
|
||||
{
|
||||
struct xrt_pose P_world_model;
|
||||
struct xrt_quat imu_world;
|
||||
int64_t capture_ns;
|
||||
};
|
||||
|
||||
/* Monado policy for publishing a pose after an earlier one. A hand can move fast, but an optical
|
||||
* identity error moves the estimate by about one LED spacing or flips it onto the other ring
|
||||
* without the controller's gyro seeing the rotation. */
|
||||
struct constellation_motion_limits
|
||||
{
|
||||
double max_speed_mps;
|
||||
double position_slack_m;
|
||||
/* Bound on the difference between the optical and the IMU rotation over the same interval,
|
||||
* both expressed in the controller's own frame. The IMU's world yaw may be far from the
|
||||
* optical one (the driver corrects it gradually); a world-frame comparison would then see
|
||||
* every wrist rotation about a non-vertical axis as an error. */
|
||||
double max_rotation_error_rad;
|
||||
//! Older reference poses do not constrain the new one.
|
||||
int64_t max_gap_ns;
|
||||
};
|
||||
|
||||
void
|
||||
constellation_constrained_default_motion_limits(struct constellation_motion_limits *limits);
|
||||
|
||||
/* True when next is a plausible continuation of prev. Outputs the position change beyond the
|
||||
* allowed motion (<= 0 when inside) and the body-frame rotation disagreement between optics and
|
||||
* IMU. */
|
||||
bool
|
||||
constellation_constrained_motion_consistent(const struct constellation_track_point *prev,
|
||||
const struct constellation_track_point *next,
|
||||
const struct constellation_motion_limits *limits,
|
||||
double *position_excess_m,
|
||||
double *rotation_error_rad);
|
||||
|
||||
/*!
|
||||
* Publication state of one device. A pose is published only when it continues an earlier one: a
|
||||
* cold acquisition waits for a consistent second result, and while tracking every result must be a
|
||||
* plausible continuation of the last published pose. Missing a pose is preferred over publishing a
|
||||
* wrong one. This is Monado policy.
|
||||
*/
|
||||
enum constellation_track_state
|
||||
{
|
||||
CONSTELLATION_TRACK_LOST,
|
||||
//! One unpublished result is waiting for a consistent second one.
|
||||
CONSTELLATION_TRACK_CONFIRMING,
|
||||
CONSTELLATION_TRACK_TRACKING,
|
||||
CONSTELLATION_TRACK_STATE_COUNT,
|
||||
};
|
||||
|
||||
enum constellation_track_verdict
|
||||
{
|
||||
CONSTELLATION_TRACK_PUBLISH,
|
||||
//! First result after loss; held until a consistent second result confirms it.
|
||||
CONSTELLATION_TRACK_HOLD_CONFIRMING,
|
||||
//! Inconsistent with the held result; it replaces that result as the one to confirm.
|
||||
CONSTELLATION_TRACK_HOLD_UNCONFIRMED,
|
||||
//! Inconsistent with the published track; the track is kept.
|
||||
CONSTELLATION_TRACK_HOLD_JUMP,
|
||||
//! Repeatedly inconsistent: the track ended and this result is held for confirmation.
|
||||
CONSTELLATION_TRACK_HOLD_RESTART,
|
||||
//! Not newer than the reference.
|
||||
CONSTELLATION_TRACK_HOLD_STALE,
|
||||
};
|
||||
|
||||
struct constellation_track
|
||||
{
|
||||
enum constellation_track_state state;
|
||||
//! Last published pose while tracking, or the result awaiting confirmation.
|
||||
struct constellation_track_point ref;
|
||||
//! Last published location remains a depth/position constraint during re-confirmation.
|
||||
struct constellation_track_point published;
|
||||
bool have_published;
|
||||
//! The reference orientation came from optics rather than echoing the IMU prior.
|
||||
bool ref_orientation_observed;
|
||||
unsigned consecutive_jumps;
|
||||
//! Consecutive jumps that end a track.
|
||||
unsigned max_jumps;
|
||||
struct constellation_motion_limits limits;
|
||||
};
|
||||
|
||||
void
|
||||
constellation_track_init(struct constellation_track *track);
|
||||
|
||||
//! End the track now; the next verified result must confirm itself again.
|
||||
void
|
||||
constellation_track_reset(struct constellation_track *track);
|
||||
|
||||
//! End a track whose reference is too old to vouch for anything. Returns true when it ended.
|
||||
bool
|
||||
constellation_track_expire(struct constellation_track *track, int64_t capture_ns);
|
||||
|
||||
/* Decide whether a verified result may be published and advance the state. next->imu_world and
|
||||
* ref_imu_world (the IMU attitude at track->ref.capture_ns) must be read from the same, current
|
||||
* attitude history; ref_imu_world may be NULL when unavailable, which makes the result
|
||||
* discontinuous. A result whose orientation was not observed is compared with the IMU's own. */
|
||||
enum constellation_track_verdict
|
||||
constellation_track_update(struct constellation_track *track,
|
||||
const struct constellation_track_point *next,
|
||||
bool orientation_observed,
|
||||
const struct xrt_quat *ref_imu_world,
|
||||
double *position_excess_m,
|
||||
double *rotation_error_rad);
|
||||
|
||||
const char *
|
||||
constellation_track_state_name(enum constellation_track_state state);
|
||||
|
||||
const char *
|
||||
constellation_track_verdict_name(enum constellation_track_verdict verdict);
|
||||
|
||||
/* Select jointly verified P3P candidates with the same discrete-assignment ambiguity test as acquisition. */
|
||||
bool
|
||||
constellation_constrained_select_results(const struct constellation_constrained_result *results,
|
||||
unsigned count,
|
||||
const struct constellation_constrained_config *config,
|
||||
struct constellation_constrained_result *out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -106,12 +106,14 @@ correspondence_search_new(struct camera_model *camera_calib)
|
||||
static int
|
||||
compare_blobs_distance(const void *elem1, const void *elem2, void *arg);
|
||||
|
||||
static void
|
||||
static bool
|
||||
undistort_blob_points(struct blob *blobs, int num_blobs, struct xrt_vec2 *out_points, struct camera_model *calib)
|
||||
{
|
||||
for (int i = 0; i < num_blobs; i++) {
|
||||
t_camera_models_undistort(&calib->calib, blobs[i].x, blobs[i].y, &out_points[i].x, &out_points[i].y);
|
||||
if (!camera_model_undistort(calib, blobs[i].x, blobs[i].y, &out_points[i].x, &out_points[i].y))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#if CHECK_ALL_PROJECTIONS
|
||||
@@ -123,7 +125,7 @@ project_led_point(struct xrt_vec3 *led_pos,
|
||||
{
|
||||
struct xrt_vec3 tmp;
|
||||
math_pose_transform_point(pose, led_pos, &tmp);
|
||||
t_camera_models_project(&calib->calib, tmp.x, tmp.y, tmp.z, &out_point->x, &out_point->y);
|
||||
camera_model_project(calib, tmp.x, tmp.y, tmp.z, &out_point->x, &out_point->y);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -144,7 +146,10 @@ correspondence_search_set_blobs(struct correspondence_search *cs, struct blob *b
|
||||
cs->blobs = blobs;
|
||||
|
||||
/* Undistort points so we can project / match properly */
|
||||
undistort_blob_points(blobs, num_blobs, undistorted_points, cs->calib);
|
||||
if (!undistort_blob_points(blobs, num_blobs, undistorted_points, cs->calib)) {
|
||||
cs->num_points = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
#if DUMP_BLOBS
|
||||
printf("Building blobs search array\n");
|
||||
@@ -276,27 +281,16 @@ correspondence_search_project_pose(struct correspondence_search *cs,
|
||||
|
||||
/* See if we need to make a gravity vector alignment check */
|
||||
if (mi->search_flags & CS_FLAG_MATCH_GRAVITY) {
|
||||
struct xrt_quat pose_gravity_swing, pose_gravity_twist;
|
||||
|
||||
math_quat_decompose_swing_twist(&pose->orientation, &mi->gravity_vector, &pose_gravity_swing,
|
||||
&pose_gravity_twist);
|
||||
|
||||
// Calculate the difference between the amount of gravity swing, ignoring axis
|
||||
float pose_angle = fabs(acosf(pose_gravity_swing.w)) - fabs(acosf(mi->gravity_swing.w));
|
||||
if (pose_angle > mi->gravity_tolerance_rad) {
|
||||
DEBUG(
|
||||
"model %d failed pose match - orientation was not within tolerance (error %f deg > %f "
|
||||
"deg)\n"
|
||||
"gravity vec %f %f %f pose %f %f %f %f swing %f %f %f %f prior swing %f %f %f %f\n",
|
||||
mi->id, RAD_TO_DEG(pose_angle), RAD_TO_DEG(mi->gravity_tolerance_rad), mi->gravity_vector.x,
|
||||
mi->gravity_vector.y, mi->gravity_vector.z, pose->orientation.x, pose->orientation.y,
|
||||
pose->orientation.z, pose->orientation.w, pose_gravity_swing.x, pose_gravity_swing.y,
|
||||
pose_gravity_swing.z, pose_gravity_swing.w, mi->gravity_swing.x, mi->gravity_swing.y,
|
||||
mi->gravity_swing.z, mi->gravity_swing.w);
|
||||
float pose_angle = pose_metrics_gravity_error(pose, &mi->pose_prior, &mi->gravity_vector);
|
||||
if (!isfinite(pose_angle) || pose_angle > mi->gravity_tolerance_rad) {
|
||||
cs->num_gravity_rejects++;
|
||||
DEBUG("model %d failed gravity match (error %f deg > %f deg)\n", mi->id, RAD_TO_DEG(pose_angle),
|
||||
RAD_TO_DEG(mi->gravity_tolerance_rad));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
cs->num_metric_checks++;
|
||||
struct pose_metrics score;
|
||||
|
||||
/* Check how many LEDs have matching blobs in this pose,
|
||||
@@ -311,8 +305,24 @@ correspondence_search_project_pose(struct correspondence_search *cs,
|
||||
pose_metrics_evaluate_pose(&score, pose, cs->blobs, cs->num_points, leds, cs->calib, NULL);
|
||||
}
|
||||
|
||||
/* A gravity prior constrains tilt even before the first positional lock. Some
|
||||
* controller rings expose fewer than seven LEDs, so use their requested
|
||||
* acquisition minimum only with a tight, mostly complete geometric match.
|
||||
* This remains a weak match: keep searching for a stronger candidate. */
|
||||
if ((mi->search_flags & CS_FLAG_MATCH_GRAVITY) && pose_metrics_can_acquire_with_gravity(&score, leds)) {
|
||||
score.match_flags |= POSE_MATCH_GOOD;
|
||||
}
|
||||
if ((mi->search_flags & CS_FLAG_JOINT_P3P) && cs->pose_candidate_cb != NULL) {
|
||||
// The rig verifier requires a fourth observation and checks ambiguity itself.
|
||||
// A single view's acquisition count must not hide a valid split-camera root.
|
||||
cs->pose_candidate_cb(cs->pose_candidate_userdata, pose, &score);
|
||||
}
|
||||
|
||||
/* If this pose is any good, test it further */
|
||||
if (POSE_HAS_FLAGS(&score, POSE_MATCH_GOOD)) {
|
||||
if (cs->pose_candidate_cb != NULL && !(mi->search_flags & CS_FLAG_JOINT_P3P)) {
|
||||
cs->pose_candidate_cb(cs->pose_candidate_userdata, pose, &score);
|
||||
}
|
||||
if (pose_metrics_score_is_better_pose(&mi->best_score, &score)) {
|
||||
mi->best_score = score;
|
||||
mi->best_pose = *pose;
|
||||
@@ -413,6 +423,15 @@ quat_from_rotation_matrix(struct xrt_quat *me, double R[9])
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
search_budget_expired(struct correspondence_search *cs)
|
||||
{
|
||||
if (cs->search_deadline_ns != 0 && (uint64_t)os_monotonic_get_ns() >= cs->search_deadline_ns) {
|
||||
cs->budget_exhausted = true;
|
||||
}
|
||||
return cs->budget_exhausted;
|
||||
}
|
||||
|
||||
static void
|
||||
check_led_against_model_subset(struct correspondence_search *cs,
|
||||
struct cs_model_info *mi,
|
||||
@@ -420,6 +439,10 @@ check_led_against_model_subset(struct correspondence_search *cs,
|
||||
struct t_constellation_led *model_leds[4],
|
||||
int depth)
|
||||
{
|
||||
if (search_budget_expired(cs)) {
|
||||
return;
|
||||
}
|
||||
|
||||
struct t_constellation_search_model *model = mi->model;
|
||||
double x[3][3];
|
||||
struct xrt_vec3 *xcheck;
|
||||
@@ -438,7 +461,6 @@ check_led_against_model_subset(struct correspondence_search *cs,
|
||||
x[i][1] = model_leds[i]->pos.y;
|
||||
x[i][2] = model_leds[i]->pos.z;
|
||||
}
|
||||
xcheck = &model_leds[3]->pos;
|
||||
|
||||
/* FIXME: It would be better if this spat out quaternions,
|
||||
* then we wouldn't need to convert below */
|
||||
@@ -536,8 +558,13 @@ check_led_against_model_subset(struct correspondence_search *cs,
|
||||
if (checks_failed) {
|
||||
continue;
|
||||
}
|
||||
if (mi->search_flags & CS_FLAG_JOINT_P3P) {
|
||||
correspondence_search_project_pose(cs, model, &pose, mi, depth);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* check against the 4th point to check the proposed P3P solution */
|
||||
xcheck = &model_leds[3]->pos;
|
||||
math_quat_rotate_vec3(&pose.orientation, xcheck, &checkpos);
|
||||
math_vec3_accum(&pose.position, &checkpos);
|
||||
math_vec3_scalar_mul(1.0 / checkpos.z, &checkpos);
|
||||
@@ -600,6 +627,10 @@ select_k_blobs_from_n(struct correspondence_search *cs,
|
||||
int n,
|
||||
int depth)
|
||||
{
|
||||
if (search_budget_expired(cs)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (k == 1) {
|
||||
output_list[0] = candidate_list[0];
|
||||
check_led_against_model_subset(cs, mi, result_list, model_leds, depth);
|
||||
@@ -645,14 +676,15 @@ check_leds_against_anchor(struct correspondence_search *cs,
|
||||
struct t_constellation_led **model_leds,
|
||||
struct cs_image_point *anchor)
|
||||
{
|
||||
struct cs_image_point *work_list[MAX_BLOB_SEARCH_DEPTH + 1];
|
||||
struct cs_image_point *work_list[MAX_BLOB_SEARCH_DEPTH + 1] = {0};
|
||||
int max_blob_search_depth = MIN(anchor->num_neighbours, mi->max_blob_depth);
|
||||
int neighbours = (mi->search_flags & CS_FLAG_JOINT_P3P) ? 2 : 3;
|
||||
|
||||
if (max_blob_search_depth < 3)
|
||||
if (max_blob_search_depth < neighbours)
|
||||
return; // Not enough blobs to compare against
|
||||
|
||||
work_list[0] = anchor;
|
||||
select_k_blobs_from_n(cs, mi, model_leds, work_list, work_list + 1, anchor->neighbours, 3,
|
||||
select_k_blobs_from_n(cs, mi, model_leds, work_list, work_list + 1, anchor->neighbours, neighbours,
|
||||
max_blob_search_depth, 1);
|
||||
}
|
||||
|
||||
@@ -663,6 +695,10 @@ check_led_match(struct correspondence_search *cs,
|
||||
struct t_constellation_led **model_leds,
|
||||
int depth)
|
||||
{
|
||||
if (search_budget_expired(cs)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int b;
|
||||
|
||||
mi->led_depth = depth;
|
||||
@@ -686,6 +722,10 @@ select_k_leds_from_n(struct correspondence_search *cs,
|
||||
int n,
|
||||
int depth)
|
||||
{
|
||||
if (search_budget_expired(cs)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (k == 1) {
|
||||
struct t_constellation_led *swap_list[4];
|
||||
|
||||
@@ -744,14 +784,15 @@ generate_led_match_candidates(struct correspondence_search *cs,
|
||||
struct cs_model_info *mi,
|
||||
struct t_constellation_search_led_candidate *c)
|
||||
{
|
||||
struct t_constellation_led *work_list[MAX_LED_SEARCH_DEPTH + 1];
|
||||
struct t_constellation_led *work_list[MAX_LED_SEARCH_DEPTH + 1] = {0};
|
||||
|
||||
int max_search_depth = MIN(mi->max_led_depth, c->num_neighbours) - mi->min_led_depth + 1;
|
||||
if (max_search_depth < 3)
|
||||
int neighbours = (mi->search_flags & CS_FLAG_JOINT_P3P) ? 2 : 3;
|
||||
if (max_search_depth < neighbours)
|
||||
return; // Not enough LEDs to compare against
|
||||
|
||||
work_list[0] = c->led;
|
||||
select_k_leds_from_n(cs, mi, work_list, work_list + 1, c->neighbours + mi->min_led_depth - 1, 3,
|
||||
select_k_leds_from_n(cs, mi, work_list, work_list + 1, c->neighbours + mi->min_led_depth - 1, neighbours,
|
||||
max_search_depth, mi->min_led_depth);
|
||||
}
|
||||
|
||||
@@ -766,7 +807,9 @@ search_pose_for_model(struct correspondence_search *cs, struct cs_model_info *mi
|
||||
mi->match_flags = 0;
|
||||
|
||||
/* Clear stats */
|
||||
cs->num_trials = cs->num_pose_checks = 0;
|
||||
cs->budget_exhausted = false;
|
||||
cs->search_deadline_ns = cs->max_search_ns ? os_monotonic_get_ns() + cs->max_search_ns : 0;
|
||||
cs->num_trials = cs->num_pose_checks = cs->num_gravity_rejects = cs->num_metric_checks = 0;
|
||||
|
||||
/* Configure search params from the flags */
|
||||
if (mi->search_flags & CS_FLAG_SHALLOW_SEARCH) {
|
||||
@@ -796,13 +839,15 @@ search_pose_for_model(struct correspondence_search *cs, struct cs_model_info *mi
|
||||
int out_index = 0, in_index;
|
||||
uint16_t led_id = anchor->blob->led_id;
|
||||
|
||||
if ((mi->search_flags & CS_FLAG_MATCH_ALL_BLOBS) || led_id == LED_INVALID_ID ||
|
||||
LED_OBJECT_ID(led_id) == mi->id) {
|
||||
if ((!cs->excluded_blobs || !cs->excluded_blobs[b]) &&
|
||||
((mi->search_flags & CS_FLAG_MATCH_ALL_BLOBS) || led_id == LED_INVALID_ID ||
|
||||
LED_OBJECT_ID(led_id) == mi->id)) {
|
||||
for (in_index = 0; in_index < cs->num_points && out_index < MAX_BLOB_SEARCH_DEPTH; in_index++) {
|
||||
struct cs_image_point *p = all_neighbours[in_index];
|
||||
|
||||
/* Don't include the blob in its own neighbours */
|
||||
if (anchor->blob == p->blob)
|
||||
if (anchor->blob == p->blob ||
|
||||
(cs->excluded_blobs && cs->excluded_blobs[p->blob - cs->blobs]))
|
||||
continue;
|
||||
|
||||
led_id = p->blob->led_id;
|
||||
@@ -828,7 +873,7 @@ search_pose_for_model(struct correspondence_search *cs, struct cs_model_info *mi
|
||||
|
||||
/* Start correspondence search for this model */
|
||||
/* At this stage, each image point has a list of the nearest neighbours filtered for this model */
|
||||
for (l = 0; l < model->num_points; l++) {
|
||||
for (l = 0; l < model->num_points && !search_budget_expired(cs); l++) {
|
||||
struct t_constellation_search_led_candidate *c = model->points[l];
|
||||
mi->led_index = l;
|
||||
|
||||
@@ -862,7 +907,7 @@ correspondence_search_find_one_pose(struct correspondence_search *cs,
|
||||
if ((search_flags & (CS_FLAG_SHALLOW_SEARCH | CS_FLAG_DEEP_SEARCH)) == 0)
|
||||
search_flags |= CS_FLAG_SHALLOW_SEARCH | CS_FLAG_DEEP_SEARCH;
|
||||
|
||||
struct cs_model_info mi;
|
||||
struct cs_model_info mi = {0};
|
||||
|
||||
mi.id = model->id;
|
||||
mi.model = model;
|
||||
@@ -879,17 +924,13 @@ correspondence_search_find_one_pose(struct correspondence_search *cs,
|
||||
}
|
||||
|
||||
if (search_flags & CS_FLAG_MATCH_GRAVITY) {
|
||||
struct xrt_quat pose_gravity_twist;
|
||||
|
||||
/* We need a pose prior to extract the gravity swing to match */
|
||||
assert((search_flags & CS_FLAG_HAVE_POSE_PRIOR) != 0);
|
||||
/* Cold acquisition knows tilt before position. Keep the supplied orientation
|
||||
* without enabling the positional/heading scoring of HAVE_POSE_PRIOR. */
|
||||
assert(gravity_vector != NULL);
|
||||
|
||||
mi.pose_prior.orientation = pose->orientation;
|
||||
mi.gravity_vector = *gravity_vector;
|
||||
mi.gravity_tolerance_rad = gravity_tolerance_rad;
|
||||
|
||||
math_quat_decompose_swing_twist(&pose->orientation, gravity_vector, &mi.gravity_swing,
|
||||
&pose_gravity_twist);
|
||||
}
|
||||
|
||||
if (search_pose_for_model(cs, &mi) && (mi.match_flags & POSE_MATCH_GOOD)) {
|
||||
@@ -909,7 +950,6 @@ correspondence_search_find_one_pose(struct correspondence_search *cs,
|
||||
return true;
|
||||
}
|
||||
|
||||
*pose = mi.best_pose;
|
||||
*score = mi.best_score;
|
||||
return false;
|
||||
}
|
||||
@@ -17,6 +17,10 @@
|
||||
#include "camera_model.h"
|
||||
#include "pose_metrics.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MAX_BLOB_SEARCH_DEPTH 5
|
||||
|
||||
enum correspondence_search_flags
|
||||
@@ -29,7 +33,8 @@ enum correspondence_search_flags
|
||||
CS_FLAG_MATCH_ALL_BLOBS =
|
||||
0x8, /* Allow matching against all blobs, not just unlabelled ones or for the current device */
|
||||
CS_FLAG_HAVE_POSE_PRIOR = 0x10, /* If the input obj_cam_pose contains a valid prior */
|
||||
CS_FLAG_MATCH_GRAVITY = 0x20, /* Use the provided gravity vector to check pose verticality */
|
||||
CS_FLAG_MATCH_GRAVITY = 0x20, /* Use pose orientation and gravity even without a position prior. */
|
||||
CS_FLAG_JOINT_P3P = 0x40, /* Callback verifies three-point roots across the complete camera rig. */
|
||||
};
|
||||
|
||||
struct cs_image_point
|
||||
@@ -76,7 +81,6 @@ struct cs_model_info
|
||||
|
||||
/* Used when CS_FLAG_MATCH_GRAVITY is set */
|
||||
struct xrt_vec3 gravity_vector;
|
||||
struct xrt_quat gravity_swing;
|
||||
float gravity_tolerance_rad;
|
||||
};
|
||||
|
||||
@@ -85,9 +89,22 @@ struct correspondence_search
|
||||
int num_points;
|
||||
struct cs_image_point *points;
|
||||
struct blob *blobs; /* Original blobs structs [num_points] */
|
||||
//! Optional same-exposure ownership mask, indexed like blobs. Null searches all observations.
|
||||
const bool *excluded_blobs;
|
||||
|
||||
//! Optional elapsed-time budget per search, zero preserves the caller default.
|
||||
uint64_t max_search_ns;
|
||||
uint64_t search_deadline_ns;
|
||||
bool budget_exhausted;
|
||||
|
||||
//! Called for each geometry-verified candidate, before selecting a per-view winner.
|
||||
void (*pose_candidate_cb)(void *userdata, const struct xrt_pose *pose, const struct pose_metrics *score);
|
||||
void *pose_candidate_userdata;
|
||||
|
||||
unsigned int num_trials;
|
||||
unsigned int num_pose_checks;
|
||||
unsigned int num_gravity_rejects;
|
||||
unsigned int num_metric_checks;
|
||||
|
||||
struct camera_model *calib;
|
||||
|
||||
@@ -118,3 +135,7 @@ correspondence_search_have_pose(struct correspondence_search *cs,
|
||||
int model_id,
|
||||
struct xrt_pose *pose,
|
||||
struct pose_metrics *score);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -427,9 +427,9 @@ debug_draw_blobs_leds(struct xrt_frame *rgb_out,
|
||||
|
||||
// Draw cross at pose center
|
||||
struct xrt_vec2 pose_center;
|
||||
t_camera_models_project(&calib->calib, P_cam_obj_prior.position.x, P_cam_obj_prior.position.y,
|
||||
P_cam_obj_prior.position.z, &pose_center.x, &pose_center.y);
|
||||
draw_rgb_marker(dest, width, out_stride, height, pose_center.x, pose_center.y, 5, 5, c);
|
||||
if (camera_model_project(calib, P_cam_obj_prior.position.x, P_cam_obj_prior.position.y,
|
||||
P_cam_obj_prior.position.z, &pose_center.x, &pose_center.y))
|
||||
draw_rgb_marker(dest, width, out_stride, height, pose_center.x, pose_center.y, 5, 5, c);
|
||||
|
||||
pose_metrics_match_pose_to_blobs(&P_cam_obj_prior, NULL, 0, dev_state->led_model, calib,
|
||||
&blob_match_info);
|
||||
@@ -448,9 +448,9 @@ debug_draw_blobs_leds(struct xrt_frame *rgb_out,
|
||||
|
||||
// Draw cross at pose center
|
||||
struct xrt_vec2 pose_center;
|
||||
t_camera_models_project(&calib->calib, P_cam_obj.position.x, P_cam_obj.position.y,
|
||||
P_cam_obj.position.z, &pose_center.x, &pose_center.y);
|
||||
draw_rgb_marker(dest, width, out_stride, height, pose_center.x, pose_center.y, 5, 5, c);
|
||||
if (camera_model_project(calib, P_cam_obj.position.x, P_cam_obj.position.y, P_cam_obj.position.z,
|
||||
&pose_center.x, &pose_center.y))
|
||||
draw_rgb_marker(dest, width, out_stride, height, pose_center.x, pose_center.y, 5, 5, c);
|
||||
|
||||
struct pose_metrics_blob_match_info blob_match_info;
|
||||
|
||||
|
||||
@@ -144,7 +144,11 @@ project_led_points(struct t_constellation_led_model *led_model,
|
||||
for (int i = 0; i < led_model->num_leds; i++) {
|
||||
struct xrt_vec3 *tmp = out_positions + i;
|
||||
math_pose_transform_point(pose, &led_model->leds[i].pos, tmp);
|
||||
if (!t_camera_models_project(&calib->calib, tmp->x, tmp->y, tmp->z, &out_points[i].x, &out_points[i].y))
|
||||
if (tmp->z <= 0) {
|
||||
out_points[i] = (struct xrt_vec2){NAN, NAN};
|
||||
continue; // Visibility below skips LEDs behind the camera.
|
||||
}
|
||||
if (!camera_model_project(calib, tmp->x, tmp->y, tmp->z, &out_points[i].x, &out_points[i].y))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -165,6 +169,7 @@ get_visible_leds_and_bounds(struct xrt_pose *pose,
|
||||
struct t_constellation_led *leds = led_model->leds;
|
||||
const int num_leds = led_model->num_leds;
|
||||
|
||||
*bounds = (struct pose_rect){0, 0, 0, 0};
|
||||
/* Project LEDs into the distorted image space */
|
||||
if (!project_led_points(led_model, calib, pose, led_out_positions, led_out_points)) {
|
||||
*num_visible_leds = 0;
|
||||
@@ -268,7 +273,11 @@ project_bounding_points(struct t_constellation_led_model *led_model,
|
||||
for (int i = 0; i < led_model->num_bounding_points; i++) {
|
||||
struct xrt_vec3 *tmp = out_positions + i;
|
||||
math_pose_transform_point(P_cam_obj, &led_model->bounding_points[i].pos, tmp);
|
||||
if (!t_camera_models_project(&calib->calib, tmp->x, tmp->y, tmp->z, &out_points[i].x,
|
||||
if (tmp->z <= 0) {
|
||||
out_points[i] = (struct xrt_vec2){NAN, NAN};
|
||||
continue;
|
||||
}
|
||||
if (!camera_model_project(calib, tmp->x, tmp->y, tmp->z, &out_points[i].x,
|
||||
&out_points[i].y)) {
|
||||
return false;
|
||||
}
|
||||
@@ -372,10 +381,16 @@ pose_metrics_match_pose_to_blobs(struct xrt_pose *pose,
|
||||
continue;
|
||||
}
|
||||
|
||||
struct pose_metrics_visible_led_info *led_info = match_info->visible_leds + match_led_index;
|
||||
/* A split blob or reflection cannot count as another LED correspondence. */
|
||||
if (led_info->matched_blob != NULL) {
|
||||
match_info->unmatched_blobs++;
|
||||
continue;
|
||||
}
|
||||
|
||||
match_info->reprojection_error += sqerror;
|
||||
match_info->matched_blobs++;
|
||||
|
||||
struct pose_metrics_visible_led_info *led_info = match_info->visible_leds + match_led_index;
|
||||
led_info->matched_blob = b;
|
||||
|
||||
if (b->led_id != LED_INVALID_ID) {
|
||||
@@ -538,3 +553,31 @@ pose_metrics_score_is_better_pose(struct pose_metrics *old_score, struct pose_me
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
float
|
||||
pose_metrics_gravity_error(const struct xrt_pose *pose,
|
||||
const struct xrt_pose *prior,
|
||||
const struct xrt_vec3 *camera_gravity)
|
||||
{
|
||||
struct xrt_quat inverse, prior_inverse;
|
||||
struct xrt_vec3 gravity, prior_gravity;
|
||||
math_quat_invert(&pose->orientation, &inverse);
|
||||
math_quat_invert(&prior->orientation, &prior_inverse);
|
||||
math_quat_rotate_vec3(&inverse, camera_gravity, &gravity);
|
||||
math_quat_rotate_vec3(&prior_inverse, camera_gravity, &prior_gravity);
|
||||
|
||||
// Compare directions in model space, preserving tilt axis and ignoring camera-space yaw.
|
||||
float dot = m_vec3_dot(gravity, prior_gravity);
|
||||
return isfinite(dot) ? acosf(fmaxf(-1.0f, fminf(1.0f, dot))) : INFINITY;
|
||||
}
|
||||
|
||||
/* The caller must independently validate the IMU gravity prior. */
|
||||
bool
|
||||
pose_metrics_can_acquire_with_gravity(const struct pose_metrics *score,
|
||||
const struct t_constellation_led_model *led_model)
|
||||
{
|
||||
const int min_leds = led_model->min_acquisition_leds;
|
||||
return min_leds >= 5 && min_leds < 7 && score->matched_blobs >= min_leds &&
|
||||
POSE_HAS_FLAGS(score, POSE_MATCH_LED_IDS) && score->reprojection_error / score->matched_blobs < 1.5 &&
|
||||
score->unmatched_blobs <= 1 && 5 * score->matched_blobs >= 4 * score->visible_leds;
|
||||
}
|
||||
@@ -89,6 +89,12 @@ struct pose_metrics_blob_match_info
|
||||
struct pose_rect bounds;
|
||||
};
|
||||
|
||||
// Angular disagreement of gravity in model space. Camera-space yaw is unconstrained.
|
||||
float
|
||||
pose_metrics_gravity_error(const struct xrt_pose *pose,
|
||||
const struct xrt_pose *prior,
|
||||
const struct xrt_vec3 *camera_gravity);
|
||||
|
||||
void
|
||||
pose_metrics_get_device_bounds(struct xrt_pose *P_cam_obj,
|
||||
struct t_constellation_led_model *led_model,
|
||||
@@ -127,6 +133,11 @@ pose_metrics_evaluate_pose_with_prior(struct pose_metrics *score,
|
||||
struct camera_model *calib,
|
||||
struct pose_rect *out_bounds);
|
||||
|
||||
// Additional acquisition path for models with fewer visible LEDs. Caller must validate gravity.
|
||||
bool
|
||||
pose_metrics_can_acquire_with_gravity(const struct pose_metrics *score,
|
||||
const struct t_constellation_led_model *led_model);
|
||||
|
||||
bool
|
||||
pose_metrics_score_is_better_pose(struct pose_metrics *old_score, struct pose_metrics *new_score);
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#endif
|
||||
|
||||
#include <iostream>
|
||||
#include <cmath>
|
||||
|
||||
using namespace std;
|
||||
|
||||
@@ -49,15 +50,17 @@ quat_to_3x3(cv::Mat &mat, struct xrt_quat *me)
|
||||
mat.at<double>(2, 2) = 1 - 2 * me->x * me->x - 2 * me->y * me->y;
|
||||
}
|
||||
|
||||
static void
|
||||
static bool
|
||||
undistort_blob_points(std::vector<cv::Point2f> in_points,
|
||||
std::vector<cv::Point2f> &out_points,
|
||||
struct camera_model *calib)
|
||||
{
|
||||
for (size_t i = 0; i < in_points.size(); i++) {
|
||||
t_camera_models_undistort(&calib->calib, in_points[i].x, in_points[i].y, &out_points[i].x,
|
||||
&out_points[i].y);
|
||||
if (!camera_model_undistort(calib, in_points[i].x, in_points[i].y, &out_points[i].x,
|
||||
&out_points[i].y))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -69,6 +72,9 @@ ransac_pnp_pose(struct xrt_pose *pose,
|
||||
int *num_leds_out,
|
||||
int *num_inliers)
|
||||
{
|
||||
if (num_inliers) {
|
||||
*num_inliers = 0;
|
||||
}
|
||||
int i, j;
|
||||
int num_leds = 0;
|
||||
uint64_t taken = 0;
|
||||
@@ -142,25 +148,47 @@ ransac_pnp_pose(struct xrt_pose *pose,
|
||||
|
||||
// we undistort the image points manually before passing them to the PnpRansac solver
|
||||
// and we give the solver identity camera + null distortion matrices
|
||||
undistort_blob_points(list_points2d, list_points2d_undistorted, calib);
|
||||
if (!undistort_blob_points(list_points2d, list_points2d_undistorted, calib))
|
||||
return false;
|
||||
|
||||
/* 3 pixel reprojection threshold */
|
||||
float reprojectionError = 3.0 / calib->calib.fx;
|
||||
|
||||
cv::solvePnPRansac(list_points3d, list_points2d_undistorted, dummyK, dummyD, rvec, tvec, false, iterationsCount,
|
||||
reprojectionError, confidence, inliers, flags);
|
||||
bool solved = false;
|
||||
try {
|
||||
solved = cv::solvePnPRansac(list_points3d, list_points2d_undistorted, dummyK, dummyD, rvec, tvec, false,
|
||||
iterationsCount, reprojectionError, confidence, inliers, flags);
|
||||
} catch (const cv::Exception &error) {
|
||||
U_LOG_D("PnP rejected correspondences: %s", error.what());
|
||||
return false;
|
||||
}
|
||||
if (!solved || inliers.rows < 4 || !cv::checkRange(rvec) || !cv::checkRange(tvec)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
cv::Rodrigues(rvec, R);
|
||||
for (int row = 0; row < inliers.rows; row++) {
|
||||
const cv::Point3f &point = list_points3d[inliers.at<int>(row)];
|
||||
double depth = R.at<double>(2, 0) * point.x + R.at<double>(2, 1) * point.y +
|
||||
R.at<double>(2, 2) * point.z + tvec.at<double>(2);
|
||||
if (!(depth > 0.0)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (num_inliers)
|
||||
*num_inliers = inliers.rows;
|
||||
|
||||
struct xrt_vec3 v;
|
||||
double angle = sqrt(rvec.dot(rvec));
|
||||
double inorm = 1.0f / angle;
|
||||
|
||||
v.x = rvec.at<double>(0) * inorm;
|
||||
v.y = rvec.at<double>(1) * inorm;
|
||||
v.z = rvec.at<double>(2) * inorm;
|
||||
math_quat_from_angle_vector(angle, &v, &pose->orientation);
|
||||
if (angle < 1e-8) {
|
||||
pose->orientation = {0, 0, 0, 1};
|
||||
} else {
|
||||
v.x = rvec.at<double>(0) / angle;
|
||||
v.y = rvec.at<double>(1) / angle;
|
||||
v.z = rvec.at<double>(2) / angle;
|
||||
math_quat_from_angle_vector(angle, &v, &pose->orientation);
|
||||
}
|
||||
pose->position.x = tvec.at<double>(0);
|
||||
pose->position.y = tvec.at<double>(1);
|
||||
pose->position.z = tvec.at<double>(2);
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
*/
|
||||
#include "sample.h"
|
||||
|
||||
#include "math/m_api.h"
|
||||
#include "math/m_vec3.h"
|
||||
|
||||
#include <math.h>
|
||||
|
||||
struct constellation_tracking_sample *
|
||||
constellation_tracking_sample_new(void)
|
||||
{
|
||||
@@ -16,6 +21,28 @@ constellation_tracking_sample_new(void)
|
||||
return sample;
|
||||
}
|
||||
|
||||
bool
|
||||
constellation_tracking_sample_set_hmd_pose(struct constellation_tracking_sample *sample,
|
||||
xrt_result_t result,
|
||||
const struct xrt_space_relation *relation)
|
||||
{
|
||||
sample->have_hmd_pose = false;
|
||||
const enum xrt_space_relation_flags required =
|
||||
XRT_SPACE_RELATION_POSITION_VALID_BIT | XRT_SPACE_RELATION_POSITION_TRACKED_BIT |
|
||||
XRT_SPACE_RELATION_ORIENTATION_VALID_BIT | XRT_SPACE_RELATION_ORIENTATION_TRACKED_BIT;
|
||||
if (result != XRT_SUCCESS || (relation->relation_flags & required) != required)
|
||||
return false;
|
||||
const struct xrt_pose *p = &relation->pose;
|
||||
float norm = p->orientation.x * p->orientation.x + p->orientation.y * p->orientation.y +
|
||||
p->orientation.z * p->orientation.z + p->orientation.w * p->orientation.w;
|
||||
if (!isfinite(p->position.x) || !isfinite(p->position.y) || !isfinite(p->position.z) ||
|
||||
!isfinite(norm) || norm < 0.9f || norm > 1.1f)
|
||||
return false;
|
||||
sample->P_xrworld_hmd = *p;
|
||||
sample->have_hmd_pose = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
constellation_tracking_sample_free(struct constellation_tracking_sample *sample)
|
||||
{
|
||||
@@ -33,3 +60,45 @@ constellation_tracking_sample_free(struct constellation_tracking_sample *sample)
|
||||
|
||||
free(sample);
|
||||
}
|
||||
|
||||
unsigned
|
||||
constellation_tracking_sample_exclude_published(const struct constellation_tracking_sample *sample,
|
||||
unsigned device_index,
|
||||
struct constellation_constrained_view *views,
|
||||
unsigned num_views)
|
||||
{
|
||||
unsigned excluded = 0;
|
||||
for (unsigned d = 0; d < sample->n_devices; d++) {
|
||||
const struct tracking_sample_device_state *owner = &sample->devices[d];
|
||||
if (d == device_index || !owner->found_device_pose) {
|
||||
continue;
|
||||
}
|
||||
for (unsigned a = 0; a < owner->joint_result.num_assignments; a++) {
|
||||
const struct constellation_constrained_assignment *claim = &owner->joint_result.assignments[a];
|
||||
for (unsigned v = 0; v < num_views; v++) {
|
||||
if (views[v].camera_index != claim->camera_index || !views[v].observation ||
|
||||
claim->blob_index >= (unsigned)views[v].observation->num_blobs ||
|
||||
claim->blob_index >= MAX_BLOBS_PER_FRAME || views[v].excluded_blobs[claim->blob_index]) {
|
||||
continue;
|
||||
}
|
||||
views[v].excluded_blobs[claim->blob_index] = true;
|
||||
excluded++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return excluded;
|
||||
}
|
||||
|
||||
void
|
||||
constellation_head_relative_position(const struct xrt_pose *P_xrworld_hmd,
|
||||
const struct xrt_vec3 *position,
|
||||
struct xrt_vec3 *out)
|
||||
{
|
||||
const struct xrt_vec3 forward = {0, 0, -1}, up = {0, 1, 0};
|
||||
struct xrt_vec3 head_forward;
|
||||
math_quat_rotate_vec3(&P_xrworld_hmd->orientation, &forward, &head_forward);
|
||||
struct xrt_quat inverse_heading;
|
||||
math_quat_from_angle_vector(-atan2f(-head_forward.x, -head_forward.z), &up, &inverse_heading);
|
||||
struct xrt_vec3 offset = m_vec3_sub(*position, P_xrworld_hmd->position);
|
||||
math_quat_rotate_vec3(&inverse_heading, &offset, out);
|
||||
}
|
||||
@@ -13,9 +13,11 @@
|
||||
|
||||
#include "xrt/xrt_defines.h"
|
||||
#include "xrt/xrt_frame.h"
|
||||
#include "xrt/xrt_results.h"
|
||||
|
||||
#include "blobwatch.h"
|
||||
#include "pose_metrics.h"
|
||||
#include "constrained_pose.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@@ -37,6 +39,8 @@ struct tracking_sample_device_state
|
||||
struct xrt_vec3 prior_pos_error;
|
||||
struct xrt_vec3 prior_rot_error;
|
||||
float gravity_error_rad; /* Gravity vector uncertainty in radians 0..M_PI */
|
||||
bool trusted_position;
|
||||
bool reliable_heading;
|
||||
|
||||
/* Last observed pose, in world space */
|
||||
bool have_last_seen_pose;
|
||||
@@ -46,6 +50,7 @@ struct tracking_sample_device_state
|
||||
int found_pose_view_id; /* Set to the camera ID where the device was found */
|
||||
struct xrt_pose final_pose; /* Global pose that was detected */
|
||||
|
||||
struct constellation_constrained_result joint_result;
|
||||
struct pose_metrics score;
|
||||
struct pose_metrics_blob_match_info blob_match_info;
|
||||
};
|
||||
@@ -79,6 +84,10 @@ struct constellation_tracking_sample
|
||||
struct tracking_sample_frame views[CONSTELLATION_MAX_CAMERAS];
|
||||
uint8_t n_views;
|
||||
|
||||
/* HMD pose at capture time in the OpenXR world, for logging positions relative to the head */
|
||||
struct xrt_pose P_xrworld_hmd;
|
||||
bool have_hmd_pose;
|
||||
|
||||
bool need_long_analysis;
|
||||
|
||||
bool long_analysis_found_new_blobs;
|
||||
@@ -89,6 +98,28 @@ constellation_tracking_sample_new(void);
|
||||
void
|
||||
constellation_tracking_sample_free(struct constellation_tracking_sample *sample);
|
||||
|
||||
// Reject held or failed HMD queries before constructing world-space camera views.
|
||||
bool
|
||||
constellation_tracking_sample_set_hmd_pose(struct constellation_tracking_sample *sample,
|
||||
xrt_result_t result,
|
||||
const struct xrt_space_relation *relation);
|
||||
|
||||
/* Mark the other devices' published assignments in this exposure. Original blob indices are
|
||||
* preserved for joint verification and arbitration; inherited temporal labels are ignored. */
|
||||
unsigned
|
||||
constellation_tracking_sample_exclude_published(const struct constellation_tracking_sample *sample,
|
||||
unsigned device_index,
|
||||
struct constellation_constrained_view *views,
|
||||
unsigned num_views);
|
||||
|
||||
/* A world position relative to the head, in the head's heading frame (OpenXR axes): x right, y up,
|
||||
* -z forward. Head pitch and roll are removed, so "forward" and "below" mean the same thing
|
||||
* wherever the user looks. */
|
||||
void
|
||||
constellation_head_relative_position(const struct xrt_pose *P_xrworld_hmd,
|
||||
const struct xrt_vec3 *position,
|
||||
struct xrt_vec3 *out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
File diff suppressed because it is too large.
Load diff
@@ -31,6 +31,7 @@ t_constellation_led_model_init(uint8_t device_id,
|
||||
}
|
||||
led_model->leds = calloc(num_leds, sizeof(struct t_constellation_led));
|
||||
led_model->num_leds = num_leds;
|
||||
led_model->min_acquisition_leds = 0;
|
||||
led_model->bounding_points = calloc(num_bounding_points, sizeof(struct t_constellation_bounding_point));
|
||||
led_model->num_bounding_points = num_bounding_points;
|
||||
}
|
||||
@@ -55,7 +56,11 @@ void
|
||||
t_constellation_led_model_clear(struct t_constellation_led_model *led_model)
|
||||
{
|
||||
free(led_model->leds);
|
||||
free(led_model->bounding_points);
|
||||
led_model->leds = NULL;
|
||||
led_model->bounding_points = NULL;
|
||||
led_model->num_leds = 0;
|
||||
led_model->num_bounding_points = 0;
|
||||
}
|
||||
|
||||
struct led_candidate_sort_entry
|
||||
|
||||
@@ -119,3 +119,125 @@ endif()
|
||||
if(XRT_HAVE_VULKAN AND XRT_HAVE_D3D12)
|
||||
target_link_libraries(tests_aux_d3d_d3d12 PRIVATE comp_util aux_vk)
|
||||
endif()
|
||||
|
||||
if(XRT_BUILD_DRIVER_RIFT_S)
|
||||
add_executable(
|
||||
tests_rift_s_position_filter tests_rift_s_position_filter.cpp
|
||||
../src/xrt/drivers/rift_s/rift_s_position_filter.c
|
||||
)
|
||||
target_include_directories(tests_rift_s_position_filter PRIVATE ../src/xrt/drivers)
|
||||
target_link_libraries(tests_rift_s_position_filter PRIVATE xrt-external-catch2 aux_util)
|
||||
add_test(NAME tests_rift_s_position_filter COMMAND tests_rift_s_position_filter)
|
||||
set_target_properties(tests_rift_s_position_filter PROPERTIES FOLDER monado_tests)
|
||||
|
||||
add_executable(tests_rift_s_optics tests_rift_s_optics.cpp ../src/xrt/drivers/rift_s/rift_s_optics.c)
|
||||
target_include_directories(tests_rift_s_optics PRIVATE ../src/xrt/drivers)
|
||||
target_link_libraries(tests_rift_s_optics PRIVATE xrt-external-catch2 aux_util)
|
||||
add_test(NAME tests_rift_s_optics COMMAND tests_rift_s_optics)
|
||||
set_target_properties(tests_rift_s_optics PROPERTIES FOLDER monado_tests)
|
||||
endif()
|
||||
|
||||
if(XRT_MODULE_CONSTELLATION_TRACKING)
|
||||
add_executable(tests_constellation tests_constellation.cpp)
|
||||
target_include_directories(tests_constellation PRIVATE ../src/xrt/tracking/constellation)
|
||||
target_link_libraries(tests_constellation PRIVATE xrt-external-catch2 constellation_tracker aux_math)
|
||||
add_test(NAME tests_constellation COMMAND tests_constellation)
|
||||
endif()
|
||||
|
||||
if(XRT_BUILD_DRIVER_RIFT_S)
|
||||
add_executable(tests_rift_s_tracking tests_rift_s_tracking.cpp)
|
||||
target_include_directories(tests_rift_s_tracking PRIVATE ../src/xrt/drivers/rift_s)
|
||||
target_link_libraries(tests_rift_s_tracking PRIVATE xrt-external-catch2 drv_rift_s aux_math aux_tracking)
|
||||
add_test(NAME tests_rift_s_tracking COMMAND tests_rift_s_tracking)
|
||||
endif()
|
||||
|
||||
if(XRT_BUILD_DRIVER_RIFT_S AND XRT_FEATURE_STEAMVR_PLUGIN)
|
||||
target_include_directories(tests_rift_s_tracking PRIVATE ../src/xrt/state_trackers/steamvr_drv)
|
||||
target_link_libraries(tests_rift_s_tracking PRIVATE xrt-external-openvr)
|
||||
target_compile_definitions(tests_rift_s_tracking PRIVATE TEST_RIFT_S_STEAMVR_POSE)
|
||||
endif()
|
||||
|
||||
if(XRT_BUILD_DRIVER_RIFT_S)
|
||||
add_executable(tests_rift_s_metadata tests_rift_s_metadata.cpp)
|
||||
target_include_directories(tests_rift_s_metadata PRIVATE ../src/xrt/drivers/rift_s)
|
||||
target_link_libraries(tests_rift_s_metadata PRIVATE xrt-external-catch2 drv_rift_s aux_math)
|
||||
add_test(NAME tests_rift_s_metadata COMMAND tests_rift_s_metadata)
|
||||
endif()
|
||||
|
||||
if(XRT_MODULE_CONSTELLATION_TRACKING)
|
||||
add_executable(tests_constellation_constrained tests_constellation_constrained.cpp)
|
||||
target_include_directories(tests_constellation_constrained PRIVATE ../src/xrt/tracking/constellation)
|
||||
target_link_libraries(tests_constellation_constrained PRIVATE xrt-external-catch2 constellation_tracker aux_math)
|
||||
add_test(NAME tests_constellation_constrained COMMAND tests_constellation_constrained)
|
||||
endif()
|
||||
|
||||
if(XRT_BUILD_DRIVER_RIFT_S)
|
||||
add_executable(tests_rift_s_passthrough tests_rift_s_passthrough.cpp)
|
||||
target_include_directories(tests_rift_s_passthrough PRIVATE ../src/xrt/drivers/rift_s)
|
||||
target_link_libraries(tests_rift_s_passthrough PRIVATE xrt-external-catch2 drv_rift_s aux_math)
|
||||
add_test(NAME tests_rift_s_passthrough COMMAND tests_rift_s_passthrough)
|
||||
endif()
|
||||
|
||||
if(XRT_FEATURE_STEAMVR_PLUGIN)
|
||||
add_executable(tests_steamvr_camera tests_steamvr_camera.cpp ../src/xrt/state_trackers/steamvr_drv/ovrd_camera.cpp)
|
||||
target_include_directories(tests_steamvr_camera PRIVATE ../src/xrt/state_trackers/steamvr_drv)
|
||||
target_link_libraries(tests_steamvr_camera PRIVATE xrt-external-catch2 xrt-external-openvr aux_math aux_util)
|
||||
target_compile_definitions(tests_steamvr_camera PRIVATE RIFTS_PASSTHROUGH_SHM_PATH="${CMAKE_CURRENT_BINARY_DIR}/rifts-passthrough-test.shm")
|
||||
add_test(NAME tests_steamvr_camera COMMAND tests_steamvr_camera)
|
||||
endif()
|
||||
|
||||
if(XRT_BUILD_DRIVER_RIFT_S)
|
||||
add_executable(tests_rift_s_slam_guard tests_rift_s_slam_guard.cpp ../src/xrt/drivers/rift_s/rift_s_slam_guard.c)
|
||||
target_include_directories(tests_rift_s_slam_guard PRIVATE ../src/xrt/drivers)
|
||||
target_link_libraries(tests_rift_s_slam_guard PRIVATE xrt-external-catch2 aux_math)
|
||||
add_test(NAME tests_rift_s_slam_guard COMMAND tests_rift_s_slam_guard)
|
||||
set_target_properties(tests_rift_s_slam_guard PROPERTIES FOLDER monado_tests)
|
||||
endif()
|
||||
|
||||
if(XRT_FEATURE_SLAM)
|
||||
add_executable(tests_slam_recovery tests_slam_recovery.cpp)
|
||||
target_link_libraries(tests_slam_recovery PRIVATE aux_tracking aux_util xrt-interfaces)
|
||||
add_test(NAME tests_slam_recovery COMMAND tests_slam_recovery)
|
||||
set_tests_properties(tests_slam_recovery PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 45)
|
||||
add_executable(tests_slam_restart tests_slam_restart.cpp)
|
||||
target_link_libraries(tests_slam_restart PRIVATE aux_tracking aux_util xrt-interfaces)
|
||||
find_package(TBB QUIET)
|
||||
if(TARGET TBB::tbb)
|
||||
target_link_libraries(tests_slam_restart PRIVATE TBB::tbb)
|
||||
target_compile_definitions(tests_slam_restart PRIVATE SLAM_TEST_HAVE_TBB)
|
||||
endif()
|
||||
add_test(NAME tests_slam_restart COMMAND tests_slam_restart)
|
||||
set_tests_properties(tests_slam_restart PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 45)
|
||||
endif()
|
||||
|
||||
if(TARGET monado_roomsetup_vulkan)
|
||||
add_executable(tests_steamvr_roomsetup_vulkan tests_steamvr_roomsetup_vulkan.cpp)
|
||||
target_include_directories(tests_steamvr_roomsetup_vulkan PRIVATE ${Vulkan_INCLUDE_DIR})
|
||||
target_link_libraries(tests_steamvr_roomsetup_vulkan PRIVATE xrt-external-catch2)
|
||||
add_test(NAME tests_steamvr_roomsetup_vulkan COMMAND tests_steamvr_roomsetup_vulkan)
|
||||
endif()
|
||||
|
||||
if(TARGET monado-roomsetup-guard)
|
||||
add_executable(tests_steamvr_roomsetup_guard tests_steamvr_roomsetup_guard.cpp)
|
||||
target_link_libraries(tests_steamvr_roomsetup_guard PRIVATE xrt-external-catch2 xrt-external-openvr aux_util ${CMAKE_DL_LIBS})
|
||||
add_test(NAME tests_steamvr_roomsetup_guard COMMAND tests_steamvr_roomsetup_guard)
|
||||
endif()
|
||||
|
||||
if(XRT_BUILD_DRIVER_RIFT_S)
|
||||
add_executable(tests_rift_s_world_anchor tests_rift_s_world_anchor.cpp)
|
||||
target_include_directories(tests_rift_s_world_anchor PRIVATE ../src/xrt/drivers/rift_s ${OpenCV_INCLUDE_DIRS})
|
||||
target_link_libraries(tests_rift_s_world_anchor PRIVATE xrt-external-catch2 drv_rift_s aux_math aux_util ${OpenCV_LIBRARIES})
|
||||
add_test(NAME tests_rift_s_world_anchor COMMAND tests_rift_s_world_anchor)
|
||||
endif()
|
||||
|
||||
if(XRT_BUILD_DRIVER_RIFT_S AND XRT_MODULE_CONSTELLATION_TRACKING)
|
||||
add_executable(tests_rift_s_controller_geometry tests_rift_s_controller_geometry.cpp)
|
||||
target_include_directories(tests_rift_s_controller_geometry PRIVATE ../src/xrt/tracking/constellation)
|
||||
target_link_libraries(tests_rift_s_controller_geometry PRIVATE xrt-external-catch2 constellation_tracker aux_math)
|
||||
add_test(NAME tests_rift_s_controller_geometry COMMAND tests_rift_s_controller_geometry)
|
||||
|
||||
add_executable(tests_rift_s_controller_capture tests_rift_s_controller_capture.cpp)
|
||||
target_include_directories(tests_rift_s_controller_capture PRIVATE ../src/xrt/drivers/rift_s)
|
||||
target_link_libraries(tests_rift_s_controller_capture PRIVATE xrt-external-catch2 drv_rift_s aux_math aux_tracking aux_util_sink)
|
||||
add_test(NAME tests_rift_s_controller_capture COMMAND tests_rift_s_controller_capture)
|
||||
endif()
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
#pragma once
|
||||
|
||||
// Left controller (device type 3), firmware ImuPosition.x = -0.005584116 m.
|
||||
// Camera 1, raw LED frame 42358443082000 from the recorded Rift S table run.
|
||||
// The down vector is the controller accelerometer direction with X negated:
|
||||
// model axes are flipped Y/Z, and specific force is opposite gravity.
|
||||
static const std::array<t_constellation_led, 15> rift_s_ring_leds = {
|
||||
{{0, {-0.016087244f, -0.003874289f, 0.03376532f}, {-0.52706841f, 0.71386452f, 0.46108171f}, 3.5f},
|
||||
{1, {-0.026308544f, -0.012725549f, 0.03197808f}, {-0.71941994f, 0.53832866f, 0.43890456f}, 3.5f},
|
||||
{2, {-0.031247054f, -0.016434289f, 0.01680927f}, {-0.75763735f, 0.6234486f, -0.19312559f}, 3.5f},
|
||||
{3, {-0.037196064f, -0.033725319f, 0.02171323f}, {-0.95565641f, -0.00827838f, 0.29436762f}, 3.5f},
|
||||
{4, {-0.036077004f, -0.043122619f, -5.757e-05f}, {-0.89943476f, 0.04857372f, -0.43434745f}, 3.5f},
|
||||
{5, {-0.023619914f, -0.068960599f, 0.0035286f}, {-0.57938915f, -0.80424722f, 0.13226727f}, 3.5f},
|
||||
{6, {-0.008917564f, -0.070212489f, -0.01661147f}, {-0.32401356f, -0.5869508f, -0.74195955f}, 3.5f},
|
||||
{7, {0.013352056f, -0.078358879f, -0.00087879f}, {0.14082806f, -0.97575588f, 0.16753482f}, 3.5f},
|
||||
{8, {0.035553556f, -0.062195189f, -0.01110704f}, {0.66436362f, -0.41503629f, -0.62158335f}, 3.5f},
|
||||
{9, {0.042824396f, -0.059756309f, 0.00886611f}, {0.77126662f, -0.61174447f, 0.17583089f}, 3.5f},
|
||||
{10, {0.048170526f, -0.037286439f, 0.00373308f}, {0.90904575f, 0.17393345f, -0.37865945f}, 3.5f},
|
||||
{11, {0.047769726f, -0.030031509f, 0.02358948f}, {0.9435189f, 0.10477919f, 0.31431419f}, 3.5f},
|
||||
{12, {0.038411006f, -0.010875009f, 0.01950788f}, {0.7051038f, 0.6950803f, -0.14032818f}, 3.5f},
|
||||
{13, {0.035797386f, -0.010987899f, 0.03273204f}, {0.67315478f, 0.5810967f, 0.45737213f}, 3.5f},
|
||||
{14, {0.025487206f, -0.003224659f, 0.0348245f}, {0.49720891f, 0.70839529f, 0.5009585f}, 3.5f}}};
|
||||
static const std::array<std::array<float, 4>, 8> rift_s_ring_blobs = {{{433.487f, 83, 2, 1},
|
||||
{438.231f, 98.808f, 6, 7},
|
||||
{372.969f, 107.138f, 3, 5},
|
||||
{380.289f, 115.5f, 8, 8},
|
||||
{402.197f, 115.247f, 9, 9},
|
||||
{423.824f, 121.102f, 7, 5},
|
||||
{375.462f, 121.945f, 4, 5},
|
||||
{393.509f, 127.601f, 8, 6}}};
|
||||
static const std::array<float, 4> rift_s_ring_intrinsics = {190.178f, 190.178f, 315.85f, 237.907f};
|
||||
static const std::array<float, 4> rift_s_ring_kb4 = {0.288808f, -0.026387f, -0.0690462f, 0.0238345f};
|
||||
static const xrt_vec3 rift_s_ring_gravity = {-0.944092375f, -0.0402524732f, 0.327214497f};
|
||||
static const xrt_vec3 rift_s_ring_down = {0.792603882f, 0.601792372f, 0.0981072222f};
|
||||
@@ -0,0 +1,437 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
#include "catch_amalgamated.hpp"
|
||||
#include "internal/pose_metrics.h"
|
||||
#include "internal/ransac_pnp.h"
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include "math/m_vec3.h"
|
||||
#include "internal/correspondence_search.h"
|
||||
#include "constellation_rift_s_data.hpp"
|
||||
#include "tracking/t_led_models.h"
|
||||
|
||||
TEST_CASE("Constellation gravity rejects opposite tilt and is symmetric")
|
||||
{
|
||||
const xrt_vec3 gravity = {0, 1, 0};
|
||||
xrt_pose prior = XRT_POSE_IDENTITY;
|
||||
xrt_pose pose = XRT_POSE_IDENTITY;
|
||||
const xrt_vec3 axis = {1, 0, 0};
|
||||
math_quat_from_angle_vector(M_PI / 3, &axis, &prior.orientation);
|
||||
math_quat_from_angle_vector(-M_PI / 3, &axis, &pose.orientation);
|
||||
CHECK(pose_metrics_gravity_error(&pose, &prior, &gravity) == Catch::Approx(2 * M_PI / 3));
|
||||
pose.orientation = {0, 0, 0, 1};
|
||||
CHECK(pose_metrics_gravity_error(&pose, &prior, &gravity) == Catch::Approx(M_PI / 3));
|
||||
CHECK(pose_metrics_gravity_error(&prior, &pose, &gravity) == Catch::Approx(M_PI / 3));
|
||||
pose.orientation = {-prior.orientation.x, -prior.orientation.y, -prior.orientation.z, -prior.orientation.w};
|
||||
CHECK(pose_metrics_gravity_error(&pose, &prior, &gravity) == Catch::Approx(0).margin(1e-5));
|
||||
}
|
||||
|
||||
TEST_CASE("Constellation gravity permits yaw about camera gravity")
|
||||
{
|
||||
const xrt_vec3 gravity = {0.6f, 0.8f, 0};
|
||||
xrt_pose prior = XRT_POSE_IDENTITY;
|
||||
xrt_pose pose = XRT_POSE_IDENTITY;
|
||||
const xrt_vec3 axis = {1, 0, 0};
|
||||
math_quat_from_angle_vector(0.7, &axis, &prior.orientation);
|
||||
xrt_quat yaw;
|
||||
math_quat_from_angle_vector(1.2, &gravity, &yaw);
|
||||
math_quat_rotate(&yaw, &prior.orientation, &pose.orientation);
|
||||
CHECK(pose_metrics_gravity_error(&pose, &prior, &gravity) == Catch::Approx(0).margin(1e-5));
|
||||
}
|
||||
|
||||
TEST_CASE("Constellation PnP preserves seed on degenerate correspondences")
|
||||
{
|
||||
std::array<t_constellation_led, 5> leds = {};
|
||||
std::array<blob, 5> blobs = {};
|
||||
for (size_t i = 0; i < leds.size(); i++) {
|
||||
leds[i].id = i;
|
||||
blobs[i].led_id = LED_MAKE_ID(2, i);
|
||||
blobs[i].x = 320;
|
||||
blobs[i].y = 240;
|
||||
}
|
||||
t_constellation_led_model model = {};
|
||||
model.id = 2;
|
||||
model.num_leds = leds.size();
|
||||
model.leds = leds.data();
|
||||
camera_model cam = {};
|
||||
cam.calib.fx = cam.calib.fy = 190;
|
||||
cam.calib.cx = 320;
|
||||
cam.calib.cy = 240;
|
||||
cam.calib.model = T_DISTORTION_FISHEYE_KB4;
|
||||
xrt_pose pose = XRT_POSE_IDENTITY;
|
||||
pose.position = {0.1f, 0.2f, 0.6f};
|
||||
int count = 0, inliers = 0;
|
||||
CHECK_FALSE(ransac_pnp_pose(&pose, blobs.data(), blobs.size(), &model, &cam, &count, &inliers));
|
||||
CHECK(pose.position.x == Catch::Approx(0.1));
|
||||
CHECK(pose.position.y == Catch::Approx(0.2));
|
||||
CHECK(pose.position.z == Catch::Approx(0.6));
|
||||
CHECK(pose.orientation.w == Catch::Approx(1));
|
||||
}
|
||||
|
||||
TEST_CASE("Constellation PnP recovers a known camera pose")
|
||||
{
|
||||
std::array<t_constellation_led, 8> leds = {};
|
||||
std::array<blob, 8> blobs = {};
|
||||
camera_model cam = {};
|
||||
cam.calib.fx = cam.calib.fy = 190;
|
||||
cam.calib.cx = 320;
|
||||
cam.calib.cy = 240;
|
||||
cam.calib.model = T_DISTORTION_FISHEYE_KB4;
|
||||
for (size_t i = 0; i < leds.size(); i++) {
|
||||
leds[i].id = i;
|
||||
leds[i].pos = {i & 1 ? 0.03f : -0.03f, i & 2 ? 0.03f : -0.03f, i & 4 ? 0.02f : -0.02f};
|
||||
blobs[i].led_id = LED_MAKE_ID(2, i);
|
||||
REQUIRE(t_camera_models_project(&cam.calib, leds[i].pos.x, leds[i].pos.y, leds[i].pos.z + 0.6f,
|
||||
&blobs[i].x, &blobs[i].y));
|
||||
}
|
||||
t_constellation_led_model model = {};
|
||||
model.id = 2;
|
||||
model.num_leds = leds.size();
|
||||
model.leds = leds.data();
|
||||
xrt_pose pose = XRT_POSE_IDENTITY;
|
||||
pose.position.z = 0.5f;
|
||||
int count = 0, inliers = 0;
|
||||
REQUIRE(ransac_pnp_pose(&pose, blobs.data(), blobs.size(), &model, &cam, &count, &inliers));
|
||||
CHECK(std::isfinite(pose.orientation.w));
|
||||
CHECK(pose.position.z == Catch::Approx(0.6).margin(1e-4));
|
||||
CHECK(count == 8);
|
||||
CHECK(inliers == 8);
|
||||
}
|
||||
|
||||
TEST_CASE("Rift S acquires a recorded five-LED ring with an unknown position and yaw")
|
||||
{
|
||||
auto leds = rift_s_ring_leds;
|
||||
t_constellation_led_model model = {};
|
||||
model.id = XRT_DEVICE_TYPE_LEFT_HAND_CONTROLLER;
|
||||
model.num_leds = leds.size();
|
||||
model.leds = leds.data();
|
||||
camera_model cam = {};
|
||||
cam.width = 640;
|
||||
cam.height = 480;
|
||||
cam.calib.fx = rift_s_ring_intrinsics[0];
|
||||
cam.calib.fy = rift_s_ring_intrinsics[1];
|
||||
cam.calib.cx = rift_s_ring_intrinsics[2];
|
||||
cam.calib.cy = rift_s_ring_intrinsics[3];
|
||||
cam.calib.model = T_DISTORTION_FISHEYE_KB4;
|
||||
cam.calib.fisheye = {rift_s_ring_kb4[0], rift_s_ring_kb4[1], rift_s_ring_kb4[2], rift_s_ring_kb4[3]};
|
||||
std::array<blob, 8> blobs = {};
|
||||
for (size_t i = 0; i < blobs.size(); i++) {
|
||||
blobs[i].x = rift_s_ring_blobs[i][0];
|
||||
blobs[i].y = rift_s_ring_blobs[i][1];
|
||||
blobs[i].width = rift_s_ring_blobs[i][2];
|
||||
blobs[i].height = rift_s_ring_blobs[i][3];
|
||||
blobs[i].led_id = LED_INVALID_ID;
|
||||
}
|
||||
auto *search_model = t_constellation_search_model_new(&model);
|
||||
auto *cs = correspondence_search_new(&cam);
|
||||
correspondence_search_set_blobs(cs, blobs.data(), blobs.size());
|
||||
xrt_vec3 pos_error = {0.1f, 0.1f, 0.1f}, rot_error = {0.5f, 0.5f, 0.5f};
|
||||
xrt_vec3 gravity = rift_s_ring_gravity;
|
||||
xrt_pose prior = XRT_POSE_IDENTITY;
|
||||
prior.position = {42, 42, 42};
|
||||
math_quat_from_vec_a_to_vec_b(&rift_s_ring_down, &gravity, &prior.orientation);
|
||||
auto flags = static_cast<correspondence_search_flags>(CS_FLAG_SHALLOW_SEARCH | CS_FLAG_HAVE_POSE_PRIOR |
|
||||
CS_FLAG_MATCH_GRAVITY);
|
||||
xrt_pose pose = prior;
|
||||
pose_metrics score = {};
|
||||
CHECK_FALSE(correspondence_search_find_one_pose(cs, search_model, flags, &pose, &pos_error, &rot_error,
|
||||
&gravity, M_PI / 6, &score));
|
||||
CHECK(pose.position.x == 42);
|
||||
model.min_acquisition_leds = 5;
|
||||
cs->max_search_ns = 1;
|
||||
pose = prior;
|
||||
CHECK_FALSE(correspondence_search_find_one_pose(cs, search_model, flags, &pose, &pos_error, &rot_error,
|
||||
&gravity, M_PI / 6, &score));
|
||||
CHECK(cs->budget_exhausted);
|
||||
CHECK(cs->num_trials == 0);
|
||||
CHECK(pose.position.x == 42);
|
||||
cs->max_search_ns = 0;
|
||||
unsigned candidates = 0;
|
||||
cs->pose_candidate_userdata = &candidates;
|
||||
cs->pose_candidate_cb = [](void *userdata, const xrt_pose *candidate, const pose_metrics *metrics) {
|
||||
++*static_cast<unsigned *>(userdata);
|
||||
CHECK(std::isfinite(candidate->position.z));
|
||||
CHECK(metrics->matched_blobs >= 5);
|
||||
};
|
||||
pose = prior;
|
||||
REQUIRE(correspondence_search_find_one_pose(cs, search_model, flags, &pose, &pos_error, &rot_error, &gravity,
|
||||
M_PI / 6, &score));
|
||||
CHECK(candidates > 0);
|
||||
cs->pose_candidate_cb = nullptr;
|
||||
cs->pose_candidate_userdata = nullptr;
|
||||
CHECK(score.matched_blobs == 5);
|
||||
CHECK(score.visible_leds == 5);
|
||||
CHECK(score.unmatched_blobs == 0);
|
||||
CHECK(score.reprojection_error / score.matched_blobs < 1.5);
|
||||
CHECK_FALSE(POSE_HAS_FLAGS(&score, POSE_MATCH_POSITION));
|
||||
CHECK_FALSE(POSE_HAS_FLAGS(&score, POSE_MATCH_STRONG));
|
||||
CHECK(pose.position.z == Catch::Approx(0.220).margin(0.003));
|
||||
CHECK(pose_metrics_gravity_error(&pose, &prior, &gravity) < M_PI / 6);
|
||||
pose = prior;
|
||||
xrt_vec3 opposite_down = {-rift_s_ring_down.x, -rift_s_ring_down.y, -rift_s_ring_down.z};
|
||||
math_quat_from_vec_a_to_vec_b(&opposite_down, &gravity, &pose.orientation);
|
||||
CHECK_FALSE(correspondence_search_find_one_pose(cs, search_model, flags, &pose, &pos_error, &rot_error,
|
||||
&gravity, M_PI / 6, &score));
|
||||
pose = prior;
|
||||
flags = static_cast<correspondence_search_flags>(CS_FLAG_SHALLOW_SEARCH | CS_FLAG_HAVE_POSE_PRIOR);
|
||||
CHECK_FALSE(correspondence_search_find_one_pose(cs, search_model, flags, &pose, &pos_error, &rot_error,
|
||||
&gravity, M_PI / 6, &score));
|
||||
correspondence_search_free(cs);
|
||||
t_constellation_search_model_free(search_model);
|
||||
}
|
||||
|
||||
TEST_CASE("Split blobs cannot count as additional LED correspondences")
|
||||
{
|
||||
auto leds = rift_s_ring_leds;
|
||||
t_constellation_led_model model = {};
|
||||
model.id = XRT_DEVICE_TYPE_LEFT_HAND_CONTROLLER;
|
||||
model.leds = leds.data();
|
||||
model.num_leds = leds.size();
|
||||
model.min_acquisition_leds = 5;
|
||||
camera_model cam = {};
|
||||
cam.width = 640;
|
||||
cam.height = 480;
|
||||
cam.calib.fx = cam.calib.fy = 190;
|
||||
cam.calib.cx = 320;
|
||||
cam.calib.cy = 240;
|
||||
cam.calib.model = T_DISTORTION_FISHEYE_KB4;
|
||||
xrt_pose pose = XRT_POSE_IDENTITY;
|
||||
pose.position.z = 0.5;
|
||||
pose.orientation = {0.98480775f, 0, 0, 0.17364818f};
|
||||
pose_metrics_blob_match_info info = {};
|
||||
pose_metrics_match_pose_to_blobs(&pose, nullptr, 0, &model, &cam, &info);
|
||||
REQUIRE(info.num_visible_leds >= 5);
|
||||
std::array<blob, 6> blobs = {};
|
||||
for (size_t i = 0; i < blobs.size(); i++) {
|
||||
size_t led = i < 4 ? i : 0;
|
||||
blobs[i].x = info.visible_leds[led].pos_px.x;
|
||||
blobs[i].y = info.visible_leds[led].pos_px.y;
|
||||
blobs[i].width = blobs[i].height = 2;
|
||||
blobs[i].led_id = LED_INVALID_ID;
|
||||
}
|
||||
pose_metrics score = {};
|
||||
pose_metrics_evaluate_pose(&score, &pose, blobs.data(), blobs.size(), &model, &cam, nullptr);
|
||||
CHECK(score.matched_blobs == 4);
|
||||
CHECK(score.unmatched_blobs == 2);
|
||||
CHECK_FALSE(pose_metrics_can_acquire_with_gravity(&score, &model));
|
||||
}
|
||||
|
||||
TEST_CASE("Blob extraction keeps first-column peaks and actual scanline coordinates")
|
||||
{
|
||||
std::array<uint8_t, 400> pixels = {};
|
||||
pixels[7 * 20 + 5] = 255;
|
||||
pixels[8 * 20 + 5] = 100;
|
||||
xrt_frame frame = {};
|
||||
frame.width = frame.height = 20;
|
||||
frame.stride = 20;
|
||||
frame.format = XRT_FORMAT_L8;
|
||||
frame.data = pixels.data();
|
||||
auto *watch = blobwatch_new(96, 128);
|
||||
blobservation *observation = nullptr;
|
||||
blobwatch_process(watch, &frame, &observation);
|
||||
REQUIRE(observation != nullptr);
|
||||
REQUIRE(observation->num_blobs == 1);
|
||||
const auto &b = observation->blobs[0];
|
||||
CHECK(b.brightness == 255);
|
||||
CHECK(b.x == 5);
|
||||
CHECK(b.y == Catch::Approx(7 + 100.0 / 355));
|
||||
CHECK(b.top == 7);
|
||||
CHECK(b.height == 2);
|
||||
blobwatch_free(watch);
|
||||
}
|
||||
|
||||
TEST_CASE("Constellation search acquires synthetic Rift S rings without a pose prior")
|
||||
{
|
||||
for (bool mirror : {false, true}) {
|
||||
auto leds = rift_s_ring_leds;
|
||||
if (mirror) {
|
||||
for (auto &led : leds) {
|
||||
led.pos.x = -led.pos.x;
|
||||
led.dir.x = -led.dir.x;
|
||||
}
|
||||
}
|
||||
t_constellation_led_model model = {};
|
||||
model.id = mirror ? XRT_DEVICE_TYPE_RIGHT_HAND_CONTROLLER : XRT_DEVICE_TYPE_LEFT_HAND_CONTROLLER;
|
||||
model.num_leds = leds.size();
|
||||
model.leds = leds.data();
|
||||
camera_model cam = {};
|
||||
cam.width = 640;
|
||||
cam.height = 480;
|
||||
cam.calib.fx = rift_s_ring_intrinsics[0];
|
||||
cam.calib.fy = rift_s_ring_intrinsics[1];
|
||||
cam.calib.cx = rift_s_ring_intrinsics[2];
|
||||
cam.calib.cy = rift_s_ring_intrinsics[3];
|
||||
cam.calib.model = T_DISTORTION_FISHEYE_KB4;
|
||||
cam.calib.fisheye = {rift_s_ring_kb4[0], rift_s_ring_kb4[1], rift_s_ring_kb4[2], rift_s_ring_kb4[3]};
|
||||
xrt_pose truth = XRT_POSE_IDENTITY;
|
||||
truth.orientation = {0.98480775f, 0, 0, 0.17364818f};
|
||||
truth.position.z = 0.5f;
|
||||
pose_metrics_blob_match_info info = {};
|
||||
pose_metrics_match_pose_to_blobs(&truth, nullptr, 0, &model, &cam, &info);
|
||||
REQUIRE(info.num_visible_leds >= 7);
|
||||
std::vector<blob> blobs(info.num_visible_leds);
|
||||
for (size_t i = 0; i < blobs.size(); i++) {
|
||||
blobs[i].x = info.visible_leds[i].pos_px.x;
|
||||
blobs[i].y = info.visible_leds[i].pos_px.y;
|
||||
blobs[i].width = blobs[i].height = 3;
|
||||
blobs[i].led_id = LED_INVALID_ID;
|
||||
}
|
||||
auto *search_model = t_constellation_search_model_new(&model);
|
||||
auto *cs = correspondence_search_new(&cam);
|
||||
correspondence_search_set_blobs(cs, blobs.data(), blobs.size());
|
||||
xrt_pose recovered = XRT_POSE_IDENTITY;
|
||||
pose_metrics score = {};
|
||||
REQUIRE(correspondence_search_find_one_pose(cs, search_model, CS_FLAG_SHALLOW_SEARCH, &recovered,
|
||||
nullptr, nullptr, nullptr, 0, &score));
|
||||
CHECK(score.matched_blobs == info.num_visible_leds);
|
||||
CHECK(score.reprojection_error < 1e-5);
|
||||
CHECK(recovered.position.z == Catch::Approx(0.5).margin(1e-4));
|
||||
correspondence_search_free(cs);
|
||||
t_constellation_search_model_free(search_model);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Rift S recorded accelerometer tilt matches the optical tilt in the LED model frame")
|
||||
{
|
||||
// The recorded down vector comes from the driver's top-level IMU calibration (acc_m/acc_b)
|
||||
// alone. If the TrackedObject AccCalibration rotation also belonged between the IMU and the LED
|
||||
// model, this unconstrained optical fit would disagree with it by that rotation's tilt.
|
||||
auto leds = rift_s_ring_leds;
|
||||
t_constellation_led_model model = {};
|
||||
model.id = XRT_DEVICE_TYPE_LEFT_HAND_CONTROLLER;
|
||||
model.num_leds = leds.size();
|
||||
model.leds = leds.data();
|
||||
model.min_acquisition_leds = 5;
|
||||
camera_model cam = {};
|
||||
cam.width = 640;
|
||||
cam.height = 480;
|
||||
cam.calib.fx = rift_s_ring_intrinsics[0];
|
||||
cam.calib.fy = rift_s_ring_intrinsics[1];
|
||||
cam.calib.cx = rift_s_ring_intrinsics[2];
|
||||
cam.calib.cy = rift_s_ring_intrinsics[3];
|
||||
cam.calib.model = T_DISTORTION_FISHEYE_KB4;
|
||||
cam.calib.fisheye = {rift_s_ring_kb4[0], rift_s_ring_kb4[1], rift_s_ring_kb4[2], rift_s_ring_kb4[3]};
|
||||
std::array<blob, 8> blobs = {};
|
||||
for (size_t i = 0; i < blobs.size(); i++) {
|
||||
blobs[i].x = rift_s_ring_blobs[i][0];
|
||||
blobs[i].y = rift_s_ring_blobs[i][1];
|
||||
blobs[i].width = rift_s_ring_blobs[i][2];
|
||||
blobs[i].height = rift_s_ring_blobs[i][3];
|
||||
blobs[i].led_id = LED_INVALID_ID;
|
||||
}
|
||||
auto *search_model = t_constellation_search_model_new(&model);
|
||||
auto *cs = correspondence_search_new(&cam);
|
||||
correspondence_search_set_blobs(cs, blobs.data(), blobs.size());
|
||||
xrt_vec3 pos_error = {1, 1, 1}, rot_error = {3.2f, 3.2f, 3.2f};
|
||||
xrt_vec3 gravity = rift_s_ring_gravity;
|
||||
xrt_pose prior = XRT_POSE_IDENTITY;
|
||||
math_quat_from_vec_a_to_vec_b(&rift_s_ring_down, &gravity, &prior.orientation);
|
||||
// Any tilt is allowed: the optical fit must find the tilt on its own.
|
||||
auto flags = static_cast<correspondence_search_flags>(CS_FLAG_SHALLOW_SEARCH | CS_FLAG_DEEP_SEARCH |
|
||||
CS_FLAG_HAVE_POSE_PRIOR | CS_FLAG_MATCH_GRAVITY);
|
||||
xrt_pose pose = prior;
|
||||
pose_metrics score = {};
|
||||
REQUIRE(correspondence_search_find_one_pose(cs, search_model, flags, &pose, &pos_error, &rot_error, &gravity,
|
||||
M_PI, &score));
|
||||
CHECK(score.matched_blobs == 5);
|
||||
|
||||
pose_metrics_blob_match_info info;
|
||||
pose_metrics_match_pose_to_blobs(&pose, blobs.data(), blobs.size(), &model, &cam, &info);
|
||||
for (int i = 0; i < info.num_visible_leds; i++) {
|
||||
if (info.visible_leds[i].matched_blob) {
|
||||
info.visible_leds[i].matched_blob->led_id = LED_MAKE_ID(model.id, info.visible_leds[i].led->id);
|
||||
}
|
||||
}
|
||||
xrt_pose refined = pose;
|
||||
int num_leds = 0, inliers = 0;
|
||||
REQUIRE(ransac_pnp_pose(&refined, blobs.data(), blobs.size(), &model, &cam, &num_leds, &inliers));
|
||||
CHECK(inliers >= 5);
|
||||
|
||||
xrt_vec3 imu_down = rift_s_ring_down;
|
||||
math_vec3_normalize(&imu_down);
|
||||
for (const xrt_pose *p : {&pose, &refined}) {
|
||||
xrt_quat inverse;
|
||||
math_quat_invert(&p->orientation, &inverse);
|
||||
xrt_vec3 optical_down;
|
||||
math_quat_rotate_vec3(&inverse, &gravity, &optical_down);
|
||||
math_vec3_normalize(&optical_down);
|
||||
double angle = std::acos(std::min(1.0, (double)m_vec3_dot(optical_down, imu_down)));
|
||||
CHECK(angle < 3 * M_PI / 180);
|
||||
}
|
||||
correspondence_search_free(cs);
|
||||
t_constellation_search_model_free(search_model);
|
||||
}
|
||||
|
||||
TEST_CASE("Native constellation search rejects noninvertible pixels without a pose change")
|
||||
{
|
||||
camera_model camera{};
|
||||
camera.calib.fx = camera.calib.fy = 190;
|
||||
camera.calib.cx = 320;
|
||||
camera.calib.cy = 240;
|
||||
camera.fisheye62_valid = true;
|
||||
std::array<blob, 5> blobs{};
|
||||
std::array<t_constellation_led, 5> leds{};
|
||||
t_constellation_led_model model{};
|
||||
model.id = 2;
|
||||
model.leds = leds.data();
|
||||
model.num_leds = leds.size();
|
||||
for (unsigned i = 0; i < blobs.size(); ++i) {
|
||||
blobs[i].x = NAN;
|
||||
blobs[i].y = 240;
|
||||
blobs[i].led_id = LED_MAKE_ID(2, i);
|
||||
leds[i].id = i;
|
||||
}
|
||||
auto search = correspondence_search_new(&camera);
|
||||
correspondence_search_set_blobs(search, blobs.data(), blobs.size());
|
||||
CHECK(search->num_points == 0);
|
||||
correspondence_search_free(search);
|
||||
xrt_pose pose = XRT_POSE_IDENTITY;
|
||||
pose.position = {.1f, .2f, .6f};
|
||||
int count = 0, inliers = 99;
|
||||
CHECK_FALSE(ransac_pnp_pose(&pose, blobs.data(), blobs.size(), &model, &camera, &count, &inliers));
|
||||
CHECK(inliers == 0);
|
||||
CHECK(pose.position.x == Catch::Approx(.1));
|
||||
CHECK(pose.position.y == Catch::Approx(.2));
|
||||
CHECK(pose.position.z == Catch::Approx(.6));
|
||||
CHECK(pose.orientation.w == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("Blob extraction merges a top-left peak without a previous scanline")
|
||||
{
|
||||
std::array<uint8_t, 400> pixels = {};
|
||||
pixels[0] = 255;
|
||||
pixels[20] = 100;
|
||||
xrt_frame frame = {};
|
||||
frame.width = frame.height = 20;
|
||||
frame.stride = 20;
|
||||
frame.format = XRT_FORMAT_L8;
|
||||
frame.data = pixels.data();
|
||||
auto *watch = blobwatch_new(96, 128);
|
||||
blobservation *observation = nullptr;
|
||||
blobwatch_process(watch, &frame, &observation);
|
||||
REQUIRE(observation != nullptr);
|
||||
REQUIRE(observation->num_blobs == 1);
|
||||
CHECK(observation->blobs[0].brightness == 255);
|
||||
CHECK(observation->blobs[0].x == 0);
|
||||
CHECK(observation->blobs[0].y == Catch::Approx(100.0 / 355));
|
||||
CHECK(observation->blobs[0].top == 0);
|
||||
CHECK(observation->blobs[0].height == 2);
|
||||
blobwatch_free(watch);
|
||||
}
|
||||
|
||||
TEST_CASE("LED model cleanup releases both arrays and supports repeated cleanup")
|
||||
{
|
||||
t_constellation_led_model model = {};
|
||||
t_constellation_led_model_init(2, nullptr, &model, 3, 4);
|
||||
REQUIRE(model.leds != nullptr);
|
||||
REQUIRE(model.bounding_points != nullptr);
|
||||
t_constellation_led_model_clear(&model);
|
||||
CHECK(model.leds == nullptr);
|
||||
CHECK(model.bounding_points == nullptr);
|
||||
CHECK(model.num_leds == 0);
|
||||
CHECK(model.num_bounding_points == 0);
|
||||
t_constellation_led_model_clear(&model);
|
||||
t_constellation_led_model_init(2, nullptr, &model, 0, 0);
|
||||
t_constellation_led_model_clear(&model);
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,39 @@
|
||||
// Copyright 2026, lbgos
|
||||
// SPDX-License-Identifier: BSL-1.0
|
||||
#include "catch_amalgamated.hpp"
|
||||
#include "rift_s_controller.h"
|
||||
#include <cmath>
|
||||
|
||||
enum u_logging_level rift_s_log_level = U_LOGGING_ERROR;
|
||||
|
||||
TEST_CASE("Rift S camera attitude bridges one radio scheduling gap with local gyro")
|
||||
{
|
||||
rift_s_controller ctrl{};
|
||||
ctrl.attitude_history_count = 2;
|
||||
xrt_vec3 axis = {.3f, .8f, -.4f};
|
||||
math_vec3_normalize(&axis);
|
||||
xrt_quat orientation;
|
||||
math_quat_from_angle_vector(.7f, &axis, &orientation);
|
||||
math_quat_normalize(&orientation);
|
||||
ctrl.attitude_history[0] = {1000000000, 2000000000, 2000000000, orientation, {.8f, -1.2f, .4f}, true};
|
||||
ctrl.attitude_history[1] = {1002000000, 2002000000, 2002000000, orientation, {.8f, -1.2f, .4f}, true};
|
||||
rift_s_attitude_sample result{};
|
||||
REQUIRE(rift_s_controller_get_attitude(&ctrl, 2007000000, &result));
|
||||
xrt_quat expected;
|
||||
math_quat_integrate_velocity(&orientation, &ctrl.attitude_history[1].gyro, .005f, &expected);
|
||||
CHECK(result.capture_ns == 2007000000);
|
||||
CHECK(result.device_capture_ns == 1007000000);
|
||||
CHECK(result.orientation.x == Catch::Approx(expected.x).margin(1e-7));
|
||||
CHECK(result.orientation.y == Catch::Approx(expected.y).margin(1e-7));
|
||||
CHECK(result.orientation.z == Catch::Approx(expected.z).margin(1e-7));
|
||||
CHECK(result.orientation.w == Catch::Approx(expected.w).margin(1e-7));
|
||||
CHECK(rift_s_controller_get_attitude(&ctrl, 2022000000, &result));
|
||||
CHECK_FALSE(rift_s_controller_get_attitude(&ctrl, 2022000001, &result));
|
||||
CHECK(ctrl.diagnostics.last_attitude_reject == RIFT_S_ATTITUDE_TOO_NEW);
|
||||
ctrl.attitude_history[0].capture_ns -= 20000000;
|
||||
CHECK_FALSE(rift_s_controller_get_attitude(&ctrl, 2007000000, &result));
|
||||
ctrl.attitude_history[0].capture_ns += 20000000;
|
||||
ctrl.attitude_history[1].tilt_valid = false;
|
||||
CHECK_FALSE(rift_s_controller_get_attitude(&ctrl, 2007000000, &result));
|
||||
CHECK_FALSE(rift_s_controller_get_attitude(&ctrl, 1999999999, &result));
|
||||
}
|
||||
Loaded 100 of 114 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user