7 Commits
Author SHA1 Message Date
lbgos edaa507b59 Arch packages: release r29 2026-10-16 22:22:55 +03:00
lbgos 41e1aabe06 Rift S: keep Quick Calibrate from putting the floor at head height
Quick Calibrate places the floor where the headset is, but off-head the
Rift S is in standby and holds its last head pose, so the floor ended up
at head height (#5). The Room Setup guard now keeps the calibration made
while the headset was in use. If the standing origin changes in the same
universe while the headset is in standby, the guard restores that
calibration and explains that Room Setup sets the floor. The unbounded
session universe cannot be committed through IVRChaperoneSetup, so this
restore reloads the saved file.

Each session also re-bases tracking, so offsets saved in universe 3 sent
the floor to the wrong height in later sessions. The session origin is
now reset to the plain origin at startup. The saved room (universe 2) is
unchanged.

Fixes #5
2026-10-16 21:23:22 +03:00
lbgos 68c8b14a36 roomsetup: give first-time setup a floor to adjust
Full Room Setup (step 7) resets the floor to pending detection and waits
for floor_detection_result. The guard only published the saved plane, and
on a first setup it does not trust that plane, so no result ever arrived:
the step showed no floor, and pulling the trigger off the panel just
hid the dashboard, which cancels setup (#4).

When the saved plane is untrusted, publish this session's nominal floor
(raw Y=0, the floor the unanchored universe already uses). The user then
adjusts it as usual. The old plane and the manual-floor reset are still
never sent from an untrusted session.

Refs #4
2026-10-16 21:23:22 +03:00
lbgos 0863f9eb27 rifts-setup: keep SteamVR on the host so the driver loads
Steam starts SteamVR 2.17 inside the Steam Linux Runtime (sniper)
container, which only sees the host's /usr under /run/host. vrserver then
skips /usr/share/steamvr-monado as "not a directory" and reports
HmdNotFound (#3).

The registration service now sets SteamVR's launch option to
vrmonitor.sh %command%, which starts SteamVR on the host. Steam rewrites
localconfig.vdf on exit, so the option is written only while Steam is
closed, and the service logs a restart hint until then. Custom launch
options are left alone. Uninstall removes only this option.

Fixes #3
2026-10-16 21:05:22 +03:00
Ryan FordandClaude Opus 5.5 b7826d277f steamvr_drv: set SteamVR's preferred refresh rate to the Rift S rate
SteamVR's default steamvr/preferredRefreshRate is 90. On start the
compositor logs "Trying to match desired rate of 90.000000Hz", only
finds the 1440x2560@80 mode, sets Prop_DisplayFrequency_Float to 90
and paces frames for 90 Hz. UpdateDisplayFrequency() then puts the
property back to 80, but the frames are still paced wrong. A light
app (SteamVR Home at ~5 ms CPU/GPU) still dropped ~9% of frames with
~1 timed-out frame per second; with no reprojection on Linux each
dropped frame is a visible hitch.

Write the device's display frequency to steamvr/preferredRefreshRate
when the Rift S activates, before the compositor starts. The compositor
then requests 80 Hz and the frequency override no longer fires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:54:32 +03:00
Ryan FordandClaude Opus 5.5 e9a098e313 rift_s: resync camera frame time after a clock discontinuity
IMU samples are ignored until the first SLAM frame is accepted, so the
first frame's epoch offset is computed against an IMU timestamp of 0.
When the extended IMU clock is a 2^32 us epoch ahead of the camera
clock, that frame maps to a negative monotonic time and is rejected as
"went backward". last_frame_time then stays 0, IMU stays ignored, the
epoch offset is never corrected, and every following frame is rejected
too: no head tracking, an empty grey Room View in Room Setup.

Seen on 8 of 8 starts on one headset, e.g.:
  frame_ts=1625067443000 imu_ts=0 hw2mono=-3395444712931
  -> mono=-1770377269931 (now=2524604154978)
with the IMU clock at 5920 s vs the camera at 1625 s once IMU ran.

Treat a backward step of more than a second as a discontinuity and
accept the frame. It is not fresh, so it is not submitted to SLAM, but
it ungates IMU; the next frame gets the correct epoch offset and maps
to ~now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:54:32 +03:00
lbgos ad1553f855 Arch packages: prebuilt release package and PKGBUILDs 2026-10-14 15:43:18 +03:00
25 changed files with 1204 additions and 55 deletions

No files matched your search

+16 -5
View File
@@ -24,12 +24,23 @@ Free and open source, built on Monado and Basalt. Steam and SteamVR are installe
- An x86-64 Linux desktop with a systemd user session and local USB seat access.
- Native Steam with SteamVR installed from the Steam library.
- A Vulkan-capable GPU and drivers supported by SteamVR's direct display mode. Current testing uses an NVIDIA RTX 3090.
- The patched Basalt build below. An unpatched Basalt lacks the required reconnect support.
- The patched Basalt build. The Arch packages include it; an unpatched Basalt lacks the required reconnect support.
- KDE Plasma Wayland for the Desktop overlay.
## Install from source
## Install
AUR packages are planned and have not been published. Build from source for now. Close SteamVR and Settings before installing or upgrading.
Close SteamVR and Settings before installing or upgrading.
On Arch Linux and derivatives, install the prebuilt package from the [latest release](https://github.com/lbgos/rift-s-linux/releases/latest):
```sh
curl -LO https://github.com/lbgos/rift-s-linux/releases/download/r29/rift-s-linux-bin-29-1-x86_64.pkg.tar.zst
sudo pacman -U rift-s-linux-bin-29-1-x86_64.pkg.tar.zst
```
To build the latest `main` instead, run `makepkg -si` in [packaging/aur/basalt-monado-rifts](packaging/aur/basalt-monado-rifts), then in [packaging/aur/rift-s-linux-git](packaging/aur/rift-s-linux-git). AUR submission is pending. Other distributions build from source.
## Build from source
Install a C/C++ toolchain, CMake, Ninja, Eigen, TBB, fmt, OpenCV, Vulkan headers and loader, glslang, libusb, SDL2, cJSON, libcap, systemd and the X11, Wayland and DRM development libraries. The Desktop overlay also needs PipeWire development files, wayland-scanner, wayland-protocols and plasma-wayland-protocols. Settings needs Rust/Cargo, Node.js, pnpm, GTK3, WebKitGTK 4.1, shaderc's `glslc` and desktop-file-utils.
@@ -80,7 +91,7 @@ EOF
sudo udevadm control --reload-rules
```
The install registers the SteamVR driver automatically through a systemd user service, including when SteamVR is installed later or in another library. It also installs the Room Setup layer, session guard and Desktop overlay. Check that CMake enables `rifts-desktop`; it skips the overlay if its dependencies are missing. SteamVR loads the driver in-process, so no separate `monado-service` needs to run.
The install registers the SteamVR driver automatically through a systemd user service, including when SteamVR is installed later or in another library. The same service sets SteamVR's launch option to `vrmonitor.sh %command%`, because Steam otherwise starts SteamVR in the Steam Linux Runtime container, where the driver is not visible. If SteamVR already has custom launch options, they are left alone and the service logs the command to add. It also installs the Room Setup layer, session guard and Desktop overlay. Check that CMake enables `rifts-desktop`; it skips the overlay if its dependencies are missing. SteamVR loads the driver in-process, so no separate `monado-service` needs to run.
Build and install Settings:
@@ -94,7 +105,7 @@ pnpm tauri build --no-bundle -- --locked
## First run
1. Connect USB and DisplayPort. Reconnect the headset after installation to apply USB access rules.
2. Open Steam and click SteamVR in the library.
2. Open Steam and click SteamVR in the library. If Steam was running during the install, restart Steam once first.
3. Put on the headset and complete Room Setup. Set the floor and draw the boundary for your physical play area.
4. On later starts, look around the saved room to recognize it. Check the floor and boundary before playing.
5. Open Rift S Settings from the desktop launcher. For the full-resolution preview, enable its next-start preference and relaunch SteamVR once. On KDE, the native VR View opens minimized to supply the preview.
@@ -0,0 +1,49 @@
pkgbase = basalt-monado-rifts
pkgdesc = Basalt tracking library with Rift S lifecycle and worker defaults
pkgver = 0.1.r544.gdf6e970
pkgrel = 1
url = https://gitlab.freedesktop.org/mateosss/basalt
arch = x86_64
license = BSD-2-Clause
license = BSD-3-Clause
license = MIT
license = BSL-1.0
makedepends = cmake
makedepends = ninja
makedepends = git
makedepends = eigen
makedepends = boost
depends = glibc
depends = libgcc
depends = libstdc++
depends = opencv
depends = onetbb
depends = fmt
provides = basalt-monado
provides = basalt-monado-git
conflicts = basalt-monado
conflicts = basalt-monado-git
options = !debug
options = !lto
source = basalt::git+https://gitlab.freedesktop.org/mateosss/basalt.git#commit=df6e970c8da7636eb401a09e3317fbeaaf829b9a
source = CLI11::git+https://github.com/CLIUtils/CLI11.git#commit=37bb6edc5317e99af72ef48405e65d9ca5218861
source = basalt-headers::git+https://gitlab.freedesktop.org/mateosss/basalt-headers.git#commit=e6db0fb84c69614bc4923fde6c52154c221c1768
source = opengv::git+https://github.com/laurentkneip/opengv.git#commit=91f4b19c73450833a40e463ad3648aae80b3a7f3
source = magic_enum::git+https://github.com/Neargye/magic_enum.git#commit=e046b69a3736d314fad813e159b1c192eaef92cd
source = Sophus::git+https://github.com/strasdat/Sophus.git#commit=d0b7315a0d90fc6143defa54596a3a95d9fa10ec
source = cereal::git+https://github.com/USCiLab/cereal.git#commit=ebef1e929807629befafbb2918ea1a08c7194554
source = 0001-keep-imu-frequency-per-tracker.patch
source = 0002-limit-workers-with-driver-calibration.patch
source = 0003-safe-recreation-and-realtime-worker-budget.patch
sha256sums = SKIP
sha256sums = SKIP
sha256sums = SKIP
sha256sums = SKIP
sha256sums = SKIP
sha256sums = SKIP
sha256sums = SKIP
sha256sums = a0bc2a766ad6ae5fdfa33cd80d45d12798d43efa4c34605ee6cf20bae157d18d
sha256sums = 607d53dadbd8ca5b0978d10378e631539bc8047ca0fce317b90d68a52f785a52
sha256sums = a434e76c66c129aa3a900ed151dd2dd9bc98b671451dda26855a438cff08b2ac
pkgname = basalt-monado-rifts
@@ -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;
@@ -0,0 +1,81 @@
# Maintainer: lbgos <285507185+lbgos@users.noreply.github.com>
pkgname=basalt-monado-rifts
pkgver=0.1.r544.gdf6e970
pkgrel=1
pkgdesc='Basalt tracking library with Rift S lifecycle and worker defaults'
arch=('x86_64')
url='https://gitlab.freedesktop.org/mateosss/basalt'
license=('BSD-2-Clause' 'BSD-3-Clause' 'MIT' 'BSL-1.0')
depends=('glibc' 'libgcc' 'libstdc++' 'opencv' 'onetbb' 'fmt')
makedepends=('cmake' 'ninja' 'git' 'eigen' 'boost')
provides=('basalt-monado' 'basalt-monado-git')
conflicts=('basalt-monado' 'basalt-monado-git')
options=('!debug' '!lto')
source=('basalt::git+https://gitlab.freedesktop.org/mateosss/basalt.git#commit=df6e970c8da7636eb401a09e3317fbeaaf829b9a'
'CLI11::git+https://github.com/CLIUtils/CLI11.git#commit=37bb6edc5317e99af72ef48405e65d9ca5218861'
'basalt-headers::git+https://gitlab.freedesktop.org/mateosss/basalt-headers.git#commit=e6db0fb84c69614bc4923fde6c52154c221c1768'
'opengv::git+https://github.com/laurentkneip/opengv.git#commit=91f4b19c73450833a40e463ad3648aae80b3a7f3'
'magic_enum::git+https://github.com/Neargye/magic_enum.git#commit=e046b69a3736d314fad813e159b1c192eaef92cd'
'Sophus::git+https://github.com/strasdat/Sophus.git#commit=d0b7315a0d90fc6143defa54596a3a95d9fa10ec'
'cereal::git+https://github.com/USCiLab/cereal.git#commit=ebef1e929807629befafbb2918ea1a08c7194554'
'0001-keep-imu-frequency-per-tracker.patch'
'0002-limit-workers-with-driver-calibration.patch'
'0003-safe-recreation-and-realtime-worker-budget.patch')
sha256sums=('SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP' 'SKIP'
'a0bc2a766ad6ae5fdfa33cd80d45d12798d43efa4c34605ee6cf20bae157d18d' '607d53dadbd8ca5b0978d10378e631539bc8047ca0fce317b90d68a52f785a52' 'a434e76c66c129aa3a900ed151dd2dd9bc98b671451dda26855a438cff08b2ac')
prepare() {
cd basalt
# Only the shared-library dependencies are needed. Use makepkg's pinned sources
# instead of fetching unrelated ROS/Pangolin/test submodules during prepare.
local module
for module in CLI11 basalt-headers opengv magic_enum; do
git submodule init "thirdparty/$module"
git config "submodule.thirdparty/$module.url" "$srcdir/$module"
done
git -c protocol.file.allow=always submodule update --no-fetch thirdparty/{CLI11,basalt-headers,opengv,magic_enum}
cd thirdparty/basalt-headers
for module in Sophus cereal; do
git submodule init "thirdparty/$module"
git config "submodule.thirdparty/$module.url" "$srcdir/$module"
done
git -c protocol.file.allow=always submodule update --no-fetch thirdparty/{Sophus,cereal}
cd ../..
local patchfile
for patchfile in "$srcdir"/000*.patch; do patch -Np1 -i "$patchfile"; done
}
build() {
cmake -S basalt -B build -G Ninja -DCMAKE_INSTALL_PREFIX=/usr \
-DCMAKE_INSTALL_LIBDIR=lib -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTS=OFF -DBUILD_TESTING=OFF \
-DBASALT_INSTANTIATIONS_DOUBLE=OFF -DBASALT_BUILTIN_EIGEN=OFF \
-DEIGEN_ROOT=/usr/include/eigen3 -DBASALT_BUILD_SHARED_LIBRARY_ONLY=ON \
-DBASALT_ENABLE_ROSBAG2=OFF -DBASALT_BUILD_VISUALIZATION=OFF \
-DCXX_MARCH=x86-64 -DCMAKE_POLICY_VERSION_MINIMUM=3.5
cmake --build build --parallel 1
}
check() {
# Reconnect requires this capability; an unpatched Basalt build is insufficient.
nm -D build/libbasalt.so | grep -Eq ' T vit_tracker_supports_safe_recreation(@|$)'
}
package() {
DESTDIR="$pkgdir" cmake --install build
install -Dm644 basalt/LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
local item
for item in thirdparty/basalt-headers/LICENSE \
thirdparty/basalt-headers/thirdparty/Sophus/LICENSE.txt \
thirdparty/basalt-headers/thirdparty/cereal/LICENSE \
thirdparty/CLI11/LICENSE thirdparty/magic_enum/LICENSE \
thirdparty/basalt-headers/thirdparty/cereal/include/cereal/external/LICENSE \
thirdparty/basalt-headers/thirdparty/cereal/include/cereal/external/rapidxml/license.txt \
thirdparty/basalt-headers/thirdparty/cereal/include/cereal/external/rapidjson/LICENSE \
thirdparty/basalt-headers/thirdparty/cereal/include/cereal/external/rapidjson/msinttypes/LICENSE \
thirdparty/opengv/License.txt thirdparty/vit/LICENSE; do
install -Dm644 "basalt/$item" "$pkgdir/usr/share/licenses/$pkgname/$item"
done
# The amalgamated header contains the JSON library and embedded-code notices.
install -Dm644 basalt/thirdparty/json/nlohmann/json.hpp \
"$pkgdir/usr/share/licenses/$pkgname/json-source-notices.hpp"
}
+78
View File
@@ -0,0 +1,78 @@
pkgbase = rift-s-linux-bin
pkgdesc = Oculus Rift S on Linux SteamVR: driver, Room Setup, Settings app and in-headset desktop (prebuilt)
pkgver = 29
pkgrel = 1
url = https://github.com/lbgos/rift-s-linux
install = rift-s-linux.install
arch = x86_64
license = BSL-1.0
license = MIT
license = BSD-2-Clause
license = BSD-3-Clause
license = OFL-1.1
license = Apache-2.0
license = CC-BY-4.0
license = CC0-1.0
license = Unlicense
license = Zlib
license = ISC
license = MPL-2.0
license = Unicode-3.0
license = Apache-2.0 WITH LLVM-exception
license = LicenseRef-Khronos-Free-Use-License-for-Software-and-Documentation
depends = glibc
depends = libgcc
depends = libstdc++
depends = libusb
depends = opencv
depends = onetbb
depends = fmt
depends = sdl2
depends = libglvnd
depends = vulkan-icd-loader
depends = libx11
depends = libxcb
depends = libxrandr
depends = wayland
depends = libdrm
depends = systemd-libs
depends = libpipewire
depends = cjson
depends = python
depends = libcap
depends = systemd
depends = util-linux
depends = procps-ng
depends = coreutils
depends = tar
depends = webkit2gtk-4.1
depends = gtk3
depends = cairo
depends = libsoup3
depends = gdk-pixbuf2
depends = glib2
depends = dbus
depends = hicolor-icon-theme
optdepends = steam: install and run SteamVR from the Steam library (native installation)
optdepends = kwin: in-headset Desktop overlay and minimized VR View preview source
optdepends = kconfig: write the VR View window rule (kwriteconfig6)
optdepends = qt6-tools: apply the VR View window rule (qdbus6)
provides = rift-s-linux
provides = monado
provides = rifts-settings
provides = basalt-monado-rifts
provides = basalt-monado
conflicts = rift-s-linux
conflicts = monado
conflicts = monado-git
conflicts = rifts-settings
conflicts = basalt-monado-rifts
conflicts = basalt-monado
conflicts = basalt-monado-git
conflicts = monado-rifts-git
options = !strip
options = !debug
source = https://github.com/lbgos/rift-s-linux/releases/download/r29/rift-s-linux-r29-x86_64.tar.zst
sha256sums = c0833f3f3ffa0537be444bfd52ea5f731e764420bd0249b505891a387b64c754
pkgname = rift-s-linux-bin
+32
View File
@@ -0,0 +1,32 @@
# Maintainer: lbgos <285507185+lbgos@users.noreply.github.com>
pkgname=rift-s-linux-bin
pkgver=29
pkgrel=1
pkgdesc='Oculus Rift S on Linux SteamVR: driver, Room Setup, Settings app and in-headset desktop (prebuilt)'
arch=('x86_64')
url='https://github.com/lbgos/rift-s-linux'
license=('BSL-1.0' 'MIT' 'BSD-2-Clause' 'BSD-3-Clause' 'OFL-1.1' 'Apache-2.0' 'CC-BY-4.0'
'CC0-1.0' 'Unlicense' 'Zlib' 'ISC' 'MPL-2.0' 'Unicode-3.0'
'Apache-2.0 WITH LLVM-exception' 'LicenseRef-Khronos-Free-Use-License-for-Software-and-Documentation')
depends=('glibc' 'libgcc' 'libstdc++' 'libusb' 'opencv' 'onetbb' 'fmt' 'sdl2' 'libglvnd'
'vulkan-icd-loader' 'libx11' 'libxcb' 'libxrandr' 'wayland' 'libdrm'
'systemd-libs' 'libpipewire' 'cjson' 'python' 'libcap' 'systemd' 'util-linux' 'procps-ng'
'coreutils' 'tar' 'webkit2gtk-4.1' 'gtk3' 'cairo' 'libsoup3' 'gdk-pixbuf2' 'glib2' 'dbus'
'hicolor-icon-theme')
optdepends=('steam: install and run SteamVR from the Steam library (native installation)'
'kwin: in-headset Desktop overlay and minimized VR View preview source'
'kconfig: write the VR View window rule (kwriteconfig6)'
'qt6-tools: apply the VR View window rule (qdbus6)')
# Bundles the patched Basalt tracking library that rift-s-linux-git takes from basalt-monado-rifts.
provides=('rift-s-linux' 'monado' 'rifts-settings' 'basalt-monado-rifts' 'basalt-monado')
conflicts=('rift-s-linux' 'monado' 'monado-git' 'rifts-settings' 'basalt-monado-rifts' 'basalt-monado'
'basalt-monado-git' 'monado-rifts-git')
install=rift-s-linux.install
options=('!strip' '!debug')
source=("https://github.com/lbgos/rift-s-linux/releases/download/r$pkgver/rift-s-linux-r$pkgver-x86_64.tar.zst")
sha256sums=('c0833f3f3ffa0537be444bfd52ea5f731e764420bd0249b505891a387b64c754')
package() {
cp -a usr "$pkgdir/"
mv "$pkgdir/usr/share/licenses/rift-s-linux" "$pkgdir/usr/share/licenses/$pkgname"
}
@@ -0,0 +1,31 @@
post_install() {
/usr/lib/rifts-session-install || return 1
udevadm control --reload-rules || true
echo 'Reconnect the Rift S. If Steam is running, restart it once, then start SteamVR.'
echo 'Driver registration runs automatically in desktop user sessions.'
}
post_upgrade() { post_install; }
pre_remove() {
# Stop the watcher before removing registration, or it would add the driver back.
loginctl list-users --no-legend 2>/dev/null | while read -r uid username rest; do
case "$uid" in ''|*[!0-9]*) continue ;; esac
[ "$uid" -ge 1000 ] && [ -S "/run/user/$uid/bus" ] || continue
runuser -u "$username" -- env "XDG_RUNTIME_DIR=/run/user/$uid" \
systemctl --user stop rifts-steamvr-registration.service || continue
runuser -u "$username" -- env "XDG_RUNTIME_DIR=/run/user/$uid" \
/usr/bin/rifts-setup uninstall || true
done
}
post_remove() {
udevadm control --reload-rules || true
loginctl list-users --no-legend 2>/dev/null | while read -r uid username rest; do
case "$uid" in ''|*[!0-9]*) continue ;; esac
[ "$uid" -ge 1000 ] && [ -S "/run/user/$uid/bus" ] || continue
runuser -u "$username" -- env "XDG_RUNTIME_DIR=/run/user/$uid" systemctl --user daemon-reload
done
echo 'Users without a reachable user session may retain a stale SteamVR driver path.'
echo 'Remove /usr/share/steamvr-monado with SteamVR vrpathreg removedriver if needed.'
}
+94
View File
@@ -0,0 +1,94 @@
pkgbase = rift-s-linux-git
pkgdesc = Oculus Rift S on Linux SteamVR: driver, Room Setup, Settings app and in-headset desktop
pkgver = 25.1.0.r8702.gd82b8cbda
pkgrel = 1
url = https://github.com/lbgos/rift-s-linux
install = rift-s-linux.install
arch = x86_64
license = BSL-1.0
license = MIT
license = BSD-2-Clause
license = BSD-3-Clause
license = OFL-1.1
license = Apache-2.0
license = CC-BY-4.0
license = CC0-1.0
license = Unlicense
license = Zlib
license = ISC
license = MPL-2.0
license = Unicode-3.0
license = Apache-2.0 WITH LLVM-exception
license = LicenseRef-Khronos-Free-Use-License-for-Software-and-Documentation
makedepends = git
makedepends = cmake
makedepends = ninja
makedepends = eigen
makedepends = glslang
makedepends = vulkan-headers
makedepends = wayland-protocols
makedepends = plasma-wayland-protocols
makedepends = pipewire
makedepends = cargo
makedepends = pnpm
makedepends = nodejs
makedepends = pkgconf
makedepends = shaderc
makedepends = desktop-file-utils
depends = glibc
depends = libgcc
depends = libstdc++
depends = libusb
depends = opencv
depends = sdl2
depends = libglvnd
depends = vulkan-icd-loader
depends = libx11
depends = libxcb
depends = libxrandr
depends = wayland
depends = libdrm
depends = systemd-libs
depends = libpipewire
depends = cjson
depends = python
depends = libcap
depends = systemd
depends = util-linux
depends = procps-ng
depends = coreutils
depends = tar
depends = basalt-monado-rifts
depends = webkit2gtk-4.1
depends = gtk3
depends = cairo
depends = libsoup3
depends = gdk-pixbuf2
depends = glib2
depends = dbus
depends = hicolor-icon-theme
optdepends = steam: install and run SteamVR from the Steam library (native installation)
optdepends = kwin: in-headset Desktop overlay and minimized VR View preview source
optdepends = kconfig: write the VR View window rule (kwriteconfig6)
optdepends = qt6-tools: apply the VR View window rule (qdbus6)
provides = rift-s-linux
provides = monado
provides = monado-git
provides = rifts-settings
conflicts = rift-s-linux
conflicts = monado
conflicts = monado-git
conflicts = rifts-settings
conflicts = monado-rifts-git
options = !debug
options = !lto
source = monado::git+https://github.com/lbgos/rift-s-linux.git#branch=main
source = 70-monado-rifts.rules
source = settings-licenses.py
source = settings-license-fallbacks.json
sha256sums = SKIP
sha256sums = 3fbda3da3bb6fdd909d6a507a1a49ede88479ab47e5b7bf88aba6d5365005ba6
sha256sums = f3eaa7437858d201c5e48c6ec62e38a28c5286112d6a994a695821db78462c2b
sha256sums = 2093d3111612e01f10ea0d0478d100faea010edaa738997d2e4866a7072c1e06
pkgname = rift-s-linux-git
@@ -0,0 +1,4 @@
# Oculus USB devices, including Rift S HMD/cameras and hidraw interfaces.
# Seat access only; do not grant all users world-writable USB access.
SUBSYSTEM=="usb", ATTRS{idVendor}=="2833", TAG+="uaccess"
KERNEL=="hidraw*", ATTRS{idVendor}=="2833", TAG+="uaccess"
+119
View File
@@ -0,0 +1,119 @@
# Maintainer: lbgos <285507185+lbgos@users.noreply.github.com>
pkgname=rift-s-linux-git
pkgver=25.1.0.r8702.gd82b8cbda
pkgrel=1
pkgdesc='Oculus Rift S on Linux SteamVR: driver, Room Setup, Settings app and in-headset desktop'
arch=('x86_64')
url='https://github.com/lbgos/rift-s-linux'
license=('BSL-1.0' 'MIT' 'BSD-2-Clause' 'BSD-3-Clause' 'OFL-1.1' 'Apache-2.0' 'CC-BY-4.0'
'CC0-1.0' 'Unlicense' 'Zlib' 'ISC' 'MPL-2.0' 'Unicode-3.0'
'Apache-2.0 WITH LLVM-exception' 'LicenseRef-Khronos-Free-Use-License-for-Software-and-Documentation')
depends=('glibc' 'libgcc' 'libstdc++' 'libusb' 'opencv' 'sdl2' 'libglvnd'
'vulkan-icd-loader' 'libx11' 'libxcb' 'libxrandr' 'wayland' 'libdrm'
'systemd-libs' 'libpipewire' 'cjson' 'python' 'libcap' 'systemd' 'util-linux' 'procps-ng' 'coreutils' 'tar'
'basalt-monado-rifts' 'webkit2gtk-4.1' 'gtk3' 'cairo'
'libsoup3' 'gdk-pixbuf2' 'glib2' 'dbus' 'hicolor-icon-theme')
optdepends=('steam: install and run SteamVR from the Steam library (native installation)'
'kwin: in-headset Desktop overlay and minimized VR View preview source'
'kconfig: write the VR View window rule (kwriteconfig6)'
'qt6-tools: apply the VR View window rule (qdbus6)')
makedepends=('git' 'cmake' 'ninja' 'eigen' 'glslang' 'vulkan-headers'
'wayland-protocols' 'plasma-wayland-protocols' 'pipewire' 'cargo' 'pnpm' 'nodejs' 'pkgconf' 'shaderc'
'desktop-file-utils')
provides=('rift-s-linux' 'monado' 'monado-git' 'rifts-settings')
conflicts=('rift-s-linux' 'monado' 'monado-git' 'rifts-settings' 'monado-rifts-git')
install=rift-s-linux.install
options=('!debug' '!lto')
source=('monado::git+https://github.com/lbgos/rift-s-linux.git#branch=main'
'70-monado-rifts.rules' 'settings-licenses.py' 'settings-license-fallbacks.json')
sha256sums=('SKIP' '3fbda3da3bb6fdd909d6a507a1a49ede88479ab47e5b7bf88aba6d5365005ba6' 'f3eaa7437858d201c5e48c6ec62e38a28c5286112d6a994a695821db78462c2b' '2093d3111612e01f10ea0d0478d100faea010edaa738997d2e4866a7072c1e06')
pkgver() {
cd monado
printf '25.1.0.r%s.g%s' "$(git rev-list --count HEAD)" "$(git rev-parse --short=9 HEAD)"
}
prepare() {
cd monado/settings-app
# Fetch locked dependencies before the build; both compilers then run offline.
pnpm fetch --frozen-lockfile --store-dir "$srcdir/pnpm-store"
cargo fetch --manifest-path src-tauri/Cargo.toml --locked --target x86_64-unknown-linux-gnu
}
build() {
local drivers=() option
# Keep optional host libraries from enabling unrelated hardware drivers.
while read -r option; do drivers+=("-D${option}=OFF"); done < <(
sed -n 's/.*(\(XRT_BUILD_DRIVER_[A-Z0-9_]*\) .*/\1/p' monado/CMakeLists.txt)
cmake -S monado -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr \
-DCMAKE_INSTALL_LIBDIR=lib -DCMAKE_INSTALL_LIBEXECDIR=lib \
-DBUILD_TESTING=ON -DXRT_BUILD_SAMPLES=OFF "${drivers[@]}" \
-DXRT_BUILD_DRIVER_RIFT_S=ON -DXRT_BUILD_DRIVER_SIMULATED=ON \
-DXRT_FEATURE_STEAMVR_PLUGIN=ON -DXRT_FEATURE_SLAM=ON \
-DXRT_HAVE_SYSTEM_CJSON=ON -DXRT_HAVE_GST=OFF -DXRT_HAVE_BLUETOOTH=OFF \
-DXRT_HAVE_DBUS=OFF -DXRT_HAVE_HIDAPI=OFF -DXRT_HAVE_JPEG=OFF -DXRT_HAVE_LIBBSD=OFF \
-DXRT_HAVE_LIBUVC=OFF -DXRT_HAVE_OPENVR=OFF -DXRT_MODULE_MERCURY_HANDTRACKING=OFF \
-DRIFTS_DESKTOP_OVERLAY=ON
cmake --build build --parallel 2
cd monado/settings-app
pnpm install --offline --frozen-lockfile --store-dir "$srcdir/pnpm-store"
pnpm build
# custom-protocol embeds the built frontend instead of using the dev server.
cargo build --manifest-path src-tauri/Cargo.toml --release --frozen --features custom-protocol -j2
cargo metadata --manifest-path src-tauri/Cargo.toml --frozen --features custom-protocol --format-version 1 \
--filter-platform x86_64-unknown-linux-gnu > "$srcdir/settings-rust-metadata.json"
pnpm licenses list --prod --json > "$srcdir/settings-js-metadata.json"
python "$srcdir/settings-licenses.py" "$srcdir/settings-rust-metadata.json" \
"$srcdir/settings-js-metadata.json" "$srcdir/settings-license-fallbacks.json" > "$srcdir/settings-third-party-notices.txt"
}
check() {
VIT_SYSTEM_LIBRARY_PATH=/usr/lib/libbasalt.so ctest --test-dir build --output-on-failure
cd monado/settings-app
cargo test --manifest-path src-tauri/Cargo.toml --release --frozen --features custom-protocol -j2
}
package() {
DESTDIR="$pkgdir" cmake --install build
# CMake skips the overlay with only a warning when its dependencies are missing.
test -x "$pkgdir/usr/bin/rifts-desktop"
# SteamVR needs this layout; executable code belongs under /usr/lib on Arch.
install -d "$pkgdir/usr/lib/steamvr-monado"
mv "$pkgdir/usr/share/steamvr-monado/bin" "$pkgdir/usr/lib/steamvr-monado/bin"
ln -s ../../lib/steamvr-monado/bin "$pkgdir/usr/share/steamvr-monado/bin"
sed -i 's|/usr/share/steamvr-monado/bin/|/usr/lib/steamvr-monado/bin/|' \
"$pkgdir/usr/share/vulkan/implicit_layer.d/monado_roomsetup_vulkan.json"
install -Dm644 "$srcdir/70-monado-rifts.rules" "$pkgdir/usr/lib/udev/rules.d/70-monado-rifts.rules"
local app=monado/settings-app
install -Dm755 "$app/src-tauri/target/release/rifts-settings" "$pkgdir/usr/bin/rifts-settings"
install -Dm644 "$app/rifts-settings.desktop" "$pkgdir/usr/share/applications/rifts-settings.desktop"
sed -i -e 's|^Exec=.*|Exec=/usr/bin/rifts-settings|' -e 's|^Icon=.*|Icon=rifts-settings|' \
"$pkgdir/usr/share/applications/rifts-settings.desktop"
desktop-file-validate "$pkgdir/usr/share/applications/rifts-settings.desktop"
install -Dm644 "$app/src-tauri/icons/icon.png" "$pkgdir/usr/share/icons/hicolor/128x128/apps/rifts-settings.png"
# Settings applies this per-user KWin rule when the full-resolution preview is enabled.
install -Dm755 "$app/install-vrview-rule.sh" "$pkgdir/usr/share/rifts-settings/install-vrview-rule.sh"
install -Dm644 "$app/vrview.kwinrule" "$pkgdir/usr/share/rifts-settings/vrview.kwinrule"
install -Dm644 monado/LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
install -d "$pkgdir/usr/share/licenses/$pkgname/Monado"
install -m644 monado/LICENSES/*.txt "$pkgdir/usr/share/licenses/$pkgname/Monado/"
local notice
while IFS= read -r -d '' notice; do
install -Dm644 "$notice" "$pkgdir/usr/share/licenses/$pkgname/${notice#monado/}"
done < <(find monado/src/external -type f \( -iname LICENSE -o -iname LICENSE.txt -o -iname COPYING -o -iname '*.license' \) -print0)
# These single-file libraries keep their grants and copyright notices inline.
for notice in monado/src/external/bcdec/bcdec.h monado/src/external/stb/stb_image_write.h \
monado/src/external/imgui/imgui/imstb*.h \
monado/src/external/glad/include/{KHR/khrplatform,EGL/eglplatform}.h; do
install -Dm644 "$notice" "$pkgdir/usr/share/licenses/$pkgname/${notice#monado/}"
done
install -Dm644 monado/src/external/openvr_includes/LICENSE "$pkgdir/usr/share/licenses/$pkgname/OpenVR"
install -Dm644 "$app/LICENSE" "$pkgdir/usr/share/licenses/$pkgname/Settings-MIT"
install -Dm644 "$app/src/fonts/OFL.txt" "$pkgdir/usr/share/licenses/$pkgname/OpenSans-OFL"
install -Dm644 "$app/src-tauri/native/OpenVR-LICENSE" "$pkgdir/usr/share/licenses/$pkgname/Settings-OpenVR"
install -Dm644 "$srcdir/settings-third-party-notices.txt" "$pkgdir/usr/share/licenses/$pkgname/Settings-third-party-notices.txt"
}
@@ -0,0 +1,31 @@
post_install() {
/usr/lib/rifts-session-install || return 1
udevadm control --reload-rules || true
echo 'Reconnect the Rift S. If Steam is running, restart it once, then start SteamVR.'
echo 'Driver registration runs automatically in desktop user sessions.'
}
post_upgrade() { post_install; }
pre_remove() {
# Stop the watcher before removing registration, or it would add the driver back.
loginctl list-users --no-legend 2>/dev/null | while read -r uid username rest; do
case "$uid" in ''|*[!0-9]*) continue ;; esac
[ "$uid" -ge 1000 ] && [ -S "/run/user/$uid/bus" ] || continue
runuser -u "$username" -- env "XDG_RUNTIME_DIR=/run/user/$uid" \
systemctl --user stop rifts-steamvr-registration.service || continue
runuser -u "$username" -- env "XDG_RUNTIME_DIR=/run/user/$uid" \
/usr/bin/rifts-setup uninstall || true
done
}
post_remove() {
udevadm control --reload-rules || true
loginctl list-users --no-legend 2>/dev/null | while read -r uid username rest; do
case "$uid" in ''|*[!0-9]*) continue ;; esac
[ "$uid" -ge 1000 ] && [ -S "/run/user/$uid/bus" ] || continue
runuser -u "$username" -- env "XDG_RUNTIME_DIR=/run/user/$uid" systemctl --user daemon-reload
done
echo 'Users without a reachable user session may retain a stale SteamVR driver path.'
echo 'Remove /usr/share/steamvr-monado with SteamVR vrpathreg removedriver if needed.'
}
@@ -0,0 +1,26 @@
{
"alloc-stdlib@0.3.0": {
"source": "https://raw.githubusercontent.com/dropbox/rust-alloc-no-stdlib/0a81fd6928ea3b33c8cd484aa4575d50ffb98012/LICENSE",
"notice": "Copyright (c) 2016 Dropbox, Inc.\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n\n2. 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.\n\n3. 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.\n\nTHIS 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.\n"
},
"defmt-parser@1.0.0": {
"source": "https://raw.githubusercontent.com/knurling-rs/defmt/4a8cdb44891ed57b8ff5a023b6bec7137c48708f/LICENSE-MIT",
"notice": "Copyright (c) Ferrous Systems\n\nPermission is hereby granted, free of charge, to any\nperson obtaining a copy of this software and associated\ndocumentation files (the \"Software\"), to deal in the\nSoftware without restriction, including without\nlimitation the rights to use, copy, modify, merge,\npublish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software\nis furnished to do so, subject to the following\nconditions:\n\nThe above copyright notice and this permission notice\nshall be included in all copies or substantial portions\nof the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF\nANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED\nTO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A\nPARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT\nSHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR\nIN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\nDEALINGS IN THE SOFTWARE."
},
"dlopen2@0.8.2": {
"source": "https://raw.githubusercontent.com/OpenByteDev/dlopen2/cc80e4a0a90d499b677fdf7743699b4b3a43a989/LICENSE",
"notice": "MIT License\n\nCopyright (c) 2017 Szymon Wieloch\nCopyright (C) 2019 Ahmed Masud <ahmed.masud@saf.ai>\nCopyright (C) 2022 OpenByte <development.openbyte@gmail.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
},
"dlopen2_derive@0.4.3": {
"source": "https://raw.githubusercontent.com/OpenByteDev/dlopen2/cc80e4a0a90d499b677fdf7743699b4b3a43a989/LICENSE",
"notice": "MIT License\n\nCopyright (c) 2017 Szymon Wieloch\nCopyright (C) 2019 Ahmed Masud <ahmed.masud@saf.ai>\nCopyright (C) 2022 OpenByte <development.openbyte@gmail.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
},
"libappindicator-sys@0.9.0": {
"source": "https://raw.githubusercontent.com/tauri-apps/libappindicator-rs/eafd1e3682a1247f595410266091e9684021cb6f/LICENSE-MIT",
"notice": "MIT License\n\nCopyright (c) 2017-2021 qDot\nCopyright (c) 2021 Tauri Apps Contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
},
"selectors@0.38.0": {
"source": "https://www.mozilla.org/media/MPL/2.0/index.txt",
"notice": "Mozilla Public License Version 2.0\n==================================\n\n1. Definitions\n--------------\n\n1.1. \"Contributor\"\n means each individual or legal entity that creates, contributes to\n the creation of, or owns Covered Software.\n\n1.2. \"Contributor Version\"\n means the combination of the Contributions of others (if any) used\n by a Contributor and that particular Contributor's Contribution.\n\n1.3. \"Contribution\"\n means Covered Software of a particular Contributor.\n\n1.4. \"Covered Software\"\n means Source Code Form to which the initial Contributor has attached\n the notice in Exhibit A, the Executable Form of such Source Code\n Form, and Modifications of such Source Code Form, in each case\n including portions thereof.\n\n1.5. \"Incompatible With Secondary Licenses\"\n means\n\n (a) that the initial Contributor has attached the notice described\n in Exhibit B to the Covered Software; or\n\n (b) that the Covered Software was made available under the terms of\n version 1.1 or earlier of the License, but not also under the\n terms of a Secondary License.\n\n1.6. \"Executable Form\"\n means any form of the work other than Source Code Form.\n\n1.7. \"Larger Work\"\n means a work that combines Covered Software with other material, in\n a separate file or files, that is not Covered Software.\n\n1.8. \"License\"\n means this document.\n\n1.9. \"Licensable\"\n means having the right to grant, to the maximum extent possible,\n whether at the time of the initial grant or subsequently, any and\n all of the rights conveyed by this License.\n\n1.10. \"Modifications\"\n means any of the following:\n\n (a) any file in Source Code Form that results from an addition to,\n deletion from, or modification of the contents of Covered\n Software; or\n\n (b) any new file in Source Code Form that contains any Covered\n Software.\n\n1.11. \"Patent Claims\" of a Contributor\n means any patent claim(s), including without limitation, method,\n process, and apparatus claims, in any patent Licensable by such\n Contributor that would be infringed, but for the grant of the\n License, by the making, using, selling, offering for sale, having\n made, import, or transfer of either its Contributions or its\n Contributor Version.\n\n1.12. \"Secondary License\"\n means either the GNU General Public License, Version 2.0, the GNU\n Lesser General Public License, Version 2.1, the GNU Affero General\n Public License, Version 3.0, or any later versions of those\n licenses.\n\n1.13. \"Source Code Form\"\n means the form of the work preferred for making modifications.\n\n1.14. \"You\" (or \"Your\")\n means an individual or a legal entity exercising rights under this\n License. For legal entities, \"You\" includes any entity that\n controls, is controlled by, or is under common control with You. For\n purposes of this definition, \"control\" means (a) the power, direct\n or indirect, to cause the direction or management of such entity,\n whether by contract or otherwise, or (b) ownership of more than\n fifty percent (50%) of the outstanding shares or beneficial\n ownership of such entity.\n\n2. License Grants and Conditions\n--------------------------------\n\n2.1. Grants\n\nEach Contributor hereby grants You a world-wide, royalty-free,\nnon-exclusive license:\n\n(a) under intellectual property rights (other than patent or trademark)\n Licensable by such Contributor to use, reproduce, make available,\n modify, display, perform, distribute, and otherwise exploit its\n Contributions, either on an unmodified basis, with Modifications, or\n as part of a Larger Work; and\n\n(b) under Patent Claims of such Contributor to make, use, sell, offer\n for sale, have made, import, and otherwise transfer either its\n Contributions or its Contributor Version.\n\n2.2. Effective Date\n\nThe licenses granted in Section 2.1 with respect to any Contribution\nbecome effective for each Contribution on the date the Contributor first\ndistributes such Contribution.\n\n2.3. Limitations on Grant Scope\n\nThe licenses granted in this Section 2 are the only rights granted under\nthis License. No additional rights or licenses will be implied from the\ndistribution or licensing of Covered Software under this License.\nNotwithstanding Section 2.1(b) above, no patent license is granted by a\nContributor:\n\n(a) for any code that a Contributor has removed from Covered Software;\n or\n\n(b) for infringements caused by: (i) Your and any other third party's\n modifications of Covered Software, or (ii) the combination of its\n Contributions with other software (except as part of its Contributor\n Version); or\n\n(c) under Patent Claims infringed by Covered Software in the absence of\n its Contributions.\n\nThis License does not grant any rights in the trademaLine truncated
}
}
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
"""Collect locked Settings dependencies' declarations and bundled notices."""
import json
from pathlib import Path
import sys
fallbacks = json.loads(Path(sys.argv[3]).read_text())
def emit(name, version, license_id, roots, license_file=None, source=None):
print(f"\n{name} {version}\nLicense: {license_id or 'See bundled notice'}\n")
if source:
print(f"Source: {source}\n")
notices = set()
for root in roots:
root = Path(root)
for item in root.rglob('*'):
if item.name.lower().startswith(('license', 'licence', 'copying', 'copyright', 'notice')):
if item.is_file():
notices.add(item)
elif item.is_dir():
notices.update(p for p in item.rglob('*') if p.is_file())
if license_file:
notices.add(root / license_file)
for notice in sorted(notices):
print(f"--- {notice.relative_to(next(Path(root) for root in roots if notice.is_relative_to(root)))} ---")
print(notice.read_text(encoding='utf-8', errors='replace'))
if not notices:
fallback = fallbacks.get(f'{name}@{version}')
if not fallback:
raise SystemExit(f'Missing license notice for {name} {version}')
print(f"Notice source: {fallback['source']}\n")
print(fallback['notice'])
rust = json.loads(Path(sys.argv[1]).read_text())
resolved = {node['id'] for node in rust['resolve']['nodes']}
for package in sorted(rust['packages'], key=lambda p: (p['name'], p['version'])):
# The application's own MIT notice is installed separately.
if package['id'] in resolved and package['id'] not in rust['workspace_members']:
emit(package['name'], package['version'], package['license'],
[Path(package['manifest_path']).parent], package.get('license_file'),
f"https://crates.io/api/v1/crates/{package['name']}/{package['version']}/download")
javascript = json.loads(Path(sys.argv[2]).read_text())
for packages in javascript.values():
for package in sorted(packages, key=lambda p: p['name']):
emit(package['name'], ', '.join(package['versions']), package['license'], package['paths'])
+165 -19
View File
@@ -14,6 +14,12 @@ import time
DRIVER = Path('@CMAKE_INSTALL_FULL_DATADIR@/steamvr-monado')
DESKTOP = Path('@CMAKE_INSTALL_FULL_BINDIR@/rifts-desktop')
STEAMVR_APP = '250820'
# Steam starts SteamVR inside the Steam Linux Runtime container, which cannot see the
# host's /usr: the driver and its libraries would be missing. vrmonitor.sh starts
# SteamVR on the host instead.
LAUNCH_SUFFIX = '/bin/vrmonitor.sh %command%'
LAUNCH_PATH = ['UserLocalConfigStore', 'Software', 'Valve', 'Steam', 'apps', STEAMVR_APP]
def read_registry(path):
@@ -66,41 +72,175 @@ def register(home, registry_path, uninstall=False):
registry = read_registry(registry_path)
tool, roots, libraries = find_runtime(home, registry)
if tool is None:
return False, roots, libraries
return False, roots, libraries, None
runtime = tool.parents[2]
drivers = registry.get('external_drivers') or []
if uninstall:
if str(DRIVER) in drivers:
pathreg(tool, 'removedriver', str(DRIVER))
return True, roots, libraries
return True, roots, libraries, host_launch(roots, runtime, uninstall=True)
if not (DRIVER / 'driver.vrdrivermanifest').is_file():
raise RuntimeError('Installed Rift S driver manifest is missing.')
if str(DRIVER) in drivers:
return True, roots, libraries
# Bootstrap only missing native paths. Existing runtime/config/log paths remain intact.
if not registry.get('runtime'):
pathreg(tool, 'setthis')
root = roots[0] if roots else home / '.local/share/Steam'
if not registry.get('config'):
pathreg(tool, 'setconfig', str(root / 'config'))
if not registry.get('log'):
pathreg(tool, 'setlog', str(root / 'logs'))
pathreg(tool, 'adddriver', str(DRIVER))
return True, roots, libraries
if str(DRIVER) not in drivers:
# Bootstrap only missing native paths. Existing runtime/config/log paths remain intact.
if not registry.get('runtime'):
pathreg(tool, 'setthis')
root = roots[0] if roots else home / '.local/share/Steam'
if not registry.get('config'):
pathreg(tool, 'setconfig', str(root / 'config'))
if not registry.get('log'):
pathreg(tool, 'setlog', str(root / 'logs'))
pathreg(tool, 'adddriver', str(DRIVER))
return True, roots, libraries, host_launch(roots, runtime)
def desktop_server_pid():
def vdf_tokens(text):
"""Return (kind, start, end, value) for the quoted strings and braces of Steam KeyValues text."""
tokens, i = [], 0
while i < len(text):
if text[i] == '"':
j, value = i + 1, []
while j < len(text) and text[j] != '"':
if text[j] == '\\' and j + 1 < len(text):
j += 1
value.append(text[j])
j += 1
if j >= len(text):
raise ValueError('unterminated string')
tokens.append(('str', i, j + 1, ''.join(value)))
i = j + 1
elif text[i] in '{}':
tokens.append((text[i], i, i + 1, text[i]))
i += 1
elif text.startswith('//', i):
end = text.find('\n', i)
i = len(text) if end < 0 else end
else:
i += 1
return tokens
def vdf_block(tokens, i=0, close=None):
"""Parse items into {'items': [(key, value_token | block)], 'close': offset of the closing brace}."""
items = []
while i < len(tokens):
kind, start, _, key = tokens[i]
if kind == '}':
return {'items': items, 'close': start}, i + 1
if kind != 'str' or i + 1 >= len(tokens):
raise ValueError('expected key')
if tokens[i + 1][0] == 'str':
items.append((key, tokens[i + 1]))
i += 2
elif tokens[i + 1][0] == '{':
child, i = vdf_block(tokens, i + 2)
items.append((key, child))
else:
raise ValueError('expected value')
if close is None:
raise ValueError('unterminated block')
return {'items': items, 'close': close}, i
def vdf_quote(value):
return '"' + value.replace('\\', '\\\\').replace('"', '\\"') + '"'
def with_launch_options(text, value):
"""Return text with SteamVR's LaunchOptions set to value (None removes ours), or None if the
user has custom options. Only the one line changes, so the rest of Steam's file stays byte-identical."""
block, _ = vdf_block(vdf_tokens(text), close=len(text))
for depth, key in enumerate(LAUNCH_PATH):
child = next((v for k, v in block['items'] if k.lower() == key.lower() and isinstance(v, dict)), None)
if child is None:
if value is None:
return text
# Insert the missing blocks before this block's closing brace, one level deeper.
at = text.rfind('\n', 0, block['close']) + 1
indent = '\t' * depth
lines = []
for level, missing in enumerate(LAUNCH_PATH[depth:]):
pad = indent + '\t' * level
lines += [f'{pad}{vdf_quote(missing)}\n', f'{pad}{{\n']
pad = indent + '\t' * (len(LAUNCH_PATH) - depth)
lines.append(f'{pad}"LaunchOptions"\t\t{vdf_quote(value)}\n')
lines += [f'{indent}{chr(9) * level}}}\n' for level in reversed(range(len(LAUNCH_PATH) - depth))]
return text[:at] + ''.join(lines) + text[at:]
block = child
current = next((v for k, v in block['items'] if k.lower() == 'launchoptions' and isinstance(v, tuple)), None)
ours = current is not None and current[3].replace('"', '').endswith(LAUNCH_SUFFIX)
if current is not None and current[3].strip() and not ours:
return None
if value is None:
if not ours:
return text
# Drop our whole line, keeping Steam's own entries.
start = text.rfind('\n', 0, current[1]) + 1
end = text.find('\n', current[2])
return text[:start] + text[end + 1 if end >= 0 else len(text):]
if current is not None:
return text[:current[1]] + vdf_quote(value) + text[current[2]:]
at = text.rfind('\n', 0, block['close']) + 1
pad = '\t' * len(LAUNCH_PATH)
return text[:at] + f'{pad}"LaunchOptions"\t\t{vdf_quote(value)}\n' + text[at:]
def user_process(name):
# Only comm and ownership, never process arguments or environments.
for entry in Path('/proc').iterdir():
if not entry.name.isdigit():
continue
try:
if entry.stat().st_uid == os.getuid() and (entry / 'comm').read_text().strip() == 'vrserver':
if entry.stat().st_uid == os.getuid() and (entry / 'comm').read_text().strip() == name:
return int(entry.name)
except (OSError, ProcessLookupError):
pass
return None
def host_launch(roots, runtime, uninstall=False):
"""Set (or on uninstall remove) the SteamVR launch option that keeps SteamVR on the host.
Steam rewrites localconfig.vdf on exit, so it is edited only while Steam is closed.
Returns a notice for the journal when the option cannot be applied yet."""
command = str(runtime) + LAUNCH_SUFFIX
if any(c.isspace() for c in str(runtime)):
command = vdf_quote(str(runtime) + LAUNCH_SUFFIX[:-len(' %command%')]) + ' %command%'
configs = sorted({path.resolve() for root in roots for path in root.glob('userdata/*/config/localconfig.vdf')})
if user_process('steam'):
if uninstall:
# A leftover vrmonitor.sh option still starts SteamVR normally.
return None
pending = []
for config in configs:
try:
text = config.read_text()
if with_launch_options(text, None if uninstall else command) not in (text, None):
pending.append(config)
except (OSError, ValueError):
pass
if pending:
return 'Restart Steam once so SteamVR starts with the Rift S driver.'
return None
notice = None
for config in configs:
try:
text = config.read_text()
updated = with_launch_options(text, None if uninstall else command)
except (OSError, ValueError):
continue
if updated is None:
if not uninstall:
notice = f'SteamVR has custom launch options. Add {command} to start it with the Rift S driver.'
continue
if updated == text:
continue
temporary = config.with_name(config.name + '.rifts-tmp')
temporary.write_text(updated)
os.chmod(temporary, config.stat().st_mode & 0o777)
os.replace(temporary, config)
return notice
def launch_desktop():
# The client registers its manifest and preserves the user's autolaunch choice.
# It also locks out duplicates when SteamVR has already started it.
@@ -156,14 +296,18 @@ def main():
events = DirectoryEvents()
last_error = None
last_desktop_server = None
last_notice = None
with (state / 'registration.lock').open('w') as lock:
while True:
with_lock = False
try:
fcntl.flock(lock, fcntl.LOCK_EX)
with_lock = True
done, roots, libraries = register(home, registry_path, args.action == 'uninstall')
done, roots, libraries, notice = register(home, registry_path, args.action == 'uninstall')
last_error = None
if notice and notice != last_notice:
print(notice, file=sys.stderr, flush=True)
last_notice = notice
except (OSError, RuntimeError, subprocess.TimeoutExpired) as error:
if args.action != 'watch':
raise SystemExit(str(error))
@@ -176,7 +320,7 @@ def main():
if with_lock:
fcntl.flock(lock, fcntl.LOCK_UN)
if done and DESKTOP.is_file():
server = desktop_server_pid()
server = user_process('vrserver')
if args.action == 'uninstall':
if server:
subprocess.run([str(DESKTOP), '--unregister'], stdout=subprocess.DEVNULL,
@@ -191,7 +335,9 @@ def main():
return
directories = [config / 'openvr', home / '.local/share/Steam', home / '.steam']
for root in roots:
directories.extend([root, root / 'steamapps'])
# Steam writes localconfig.vdf on exit; that is when the launch option can be set.
directories.extend([root, root / 'steamapps', root / 'userdata'])
directories.extend(root.glob('userdata/*/config'))
for library in libraries:
runtime = library / 'steamapps/common/SteamVR'
directories.extend([library, library / 'steamapps', library / 'steamapps/common',
+9 -1
View File
@@ -934,7 +934,15 @@ rift_s_tracker_push_slam_frames(struct rift_s_tracker *t,
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) {
if (t->last_frame_time - frame_mono_ns > (timepoint_ns)U_TIME_1S_IN_NS) {
/* A clock discontinuity, not jitter. At startup IMU samples are ignored until the
* first frame is accepted, so the first frame's epoch offset is computed without
* IMU time; when the IMU clock is a 2^32 us epoch ahead that frame maps far into
* the past (or before 0). Rejecting it would keep last_frame_time at 0, keep IMU
* ignored and the epoch uncorrected, so every later frame would be rejected too. */
RIFT_S_WARN("Camera clock discontinuity of %" PRId64 " ns; resyncing frame time",
frame_mono_ns - t->last_frame_time);
} else 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);
@@ -42,14 +42,22 @@ bool rift_s_prepare_session_origin(const std::string &path)
auto universes = cJSON_GetObjectItemCaseSensitive(document.get(), "universes");
if (!cJSON_IsString(id) || std::strcmp(id->valuestring, "chaperone_info") || !cJSON_IsArray(universes))
return false;
const cJSON *entry;
Json origin(cJSON_Parse("{\"universeID\":\"3\",\"seated\":{\"translation\":[0,0,0],\"yaw\":0},\"standing\":{\"translation\":[0,0,0],\"yaw\":0}}"), cJSON_Delete);
if (!origin) return false;
// Each session re-bases tracking, so offsets saved in universe 3 (for example by
// Quick Calibrate) would misplace the floor next time. Reset it to the plain origin.
int index = 0;
cJSON *entry;
cJSON_ArrayForEach(entry, universes) {
auto universe = cJSON_GetObjectItemCaseSensitive(entry, "universeID");
if (cJSON_IsString(universe) && std::strcmp(universe->valuestring, "3") == 0)
return true;
if (cJSON_IsString(universe) && std::strcmp(universe->valuestring, "3") == 0) {
if (cJSON_Compare(entry, origin.get(), true)) return true;
if (!cJSON_ReplaceItemInArray(universes, index, origin.get())) return false;
break;
}
++index;
}
Json origin(cJSON_Parse("{\"universeID\":\"3\",\"seated\":{\"translation\":[0,0,0],\"yaw\":0},\"standing\":{\"translation\":[0,0,0],\"yaw\":0}}"), cJSON_Delete);
if (!origin || !cJSON_AddItemToArray(universes, origin.get())) return false;
if (!entry && !cJSON_AddItemToArray(universes, origin.get())) return false;
origin.release();
std::unique_ptr<char, decltype(&cJSON_free)> bytes(cJSON_PrintUnformatted(document.get()), cJSON_free);
if (!bytes) return false;
@@ -3,7 +3,7 @@
#pragma once
#include <string>
// Add a parseable, unbounded session origin before publishing universe 3.
// The saved universe and its geometry remain unchanged. Existing entries are preserved.
// Write a parseable, unbounded session origin before publishing universe 3, replacing any
// offsets left in universe 3 by an earlier session. Other universes remain unchanged.
bool rift_s_prepare_session_origin(const std::string &path);
bool rift_s_prepare_session_origin();
@@ -1550,6 +1550,10 @@ CDeviceDriver_Monado::Activate(vr::TrackedDeviceIndex_t unObjectId)
vr::VRProperties()->SetFloatProperty(m_ulPropertyContainer, vr::Prop_UserHeadToEyeDepthMeters_Float, 0.f);
vr::VRProperties()->SetFloatProperty(m_ulPropertyContainer, vr::Prop_DisplayFrequency_Float, m_flDisplayFrequency);
vr::VRProperties()->SetFloatProperty( m_ulPropertyContainer, vr::Prop_SecondsFromVsyncToPhotons_Float, m_flSecondsFromVsyncToPhotons);
// SteamVR's default steamvr/preferredRefreshRate is 90: the compositor asks for 90 Hz, finds only the Rift S
// 80 Hz mode and paces frames for the wrong rate (periodic timed-out frames). The Rift S has one mode.
if (strcmp(m_xdev->str, "Oculus Rift S") == 0)
vr::VRSettings()->SetFloat(vr::k_pch_SteamVR_Section, "preferredRefreshRate", m_flDisplayFrequency);
for (uint32_t i = 0; i < m_xdev->input_count; i++) {
if (m_xdev->inputs[i].name == XRT_INPUT_GENERIC_HEAD_DETECT)
@@ -86,12 +86,16 @@ public:
bool
publish(bool initialize_manual = false, bool saved_plane_trusted = true)
{
if (!saved_plane_trusted || !handle || !paths)
if (!handle || !paths)
return false;
vr::HmdMatrix34_t standing{}, setup{};
if (!transforms(standing, setup))
return false;
auto y = saved_floor(standing, setup);
// An untrusted saved plane belongs to another tracking session. Offer this
// session's nominal floor instead (raw Y=0, where the unanchored universe
// puts it): without any result the native step shows no floor to adjust.
const vr::HmdMatrix34_t session_origin{{{1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}}};
auto y = saved_floor(saved_plane_trusted ? standing : session_origin, setup);
if (!y)
return false;
char message[128];
@@ -99,11 +103,12 @@ public:
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)
if (sent && initialize_manual && saved_plane_trusted)
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);
fprintf(stderr, "monado-roomsetup-floor: %s_floor_setup_y=%.6f sent=%d\n",
saved_plane_trusted ? "saved" : "session", *y, sent);
return sent;
}
};
@@ -370,6 +370,14 @@ public:
};
static bool same_transform(const vr::HmdMatrix34_t &a, const vr::HmdMatrix34_t &b)
{
for (int row = 0; row < 3; ++row)
for (int column = 0; column < 4; ++column)
if (std::fabs(a.m[row][column] - b.m[row][column]) > 1e-4f) return false;
return true;
}
struct Snapshot
{
std::string file;
@@ -387,11 +395,14 @@ static std::optional<Snapshot> snapshot(vr::IVRChaperoneSetup &setup, const fs::
return saved;
}
static bool restore(vr::IVRChaperoneSetup &setup, const fs::path &path, const Snapshot &saved)
// The unbounded Rift S session universe has no perimeter, which CommitWorkingCopy rejects;
// require_commit=false restores it from the file alone.
static bool restore(vr::IVRChaperoneSetup &setup, const fs::path &path, const Snapshot &saved, bool require_commit = true)
{
setup.RevertWorkingCopy();
if (!setup.ImportFromBufferToWorking(saved.exported.data(), 0) ||
!setup.CommitWorkingCopy(vr::EChaperoneConfigFile_Live)) return false;
bool committed = setup.ImportFromBufferToWorking(saved.exported.data(), 0) &&
setup.CommitWorkingCopy(vr::EChaperoneConfigFile_Live);
if (!committed && require_commit) 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;
@@ -528,6 +539,21 @@ int main(int argc, char **argv)
auto next_notification_attempt = std::chrono::steady_clock::time_point{};
DisplayRecovery display_recovery(log_path);
auto next_display_check = std::chrono::steady_clock::time_point{};
// Quick Calibrate puts the floor where the headset is. Off-head the Rift S is in standby
// and holds its last head pose, so a calibration made then puts the floor at head height.
// Keep the last calibration made while worn and restore it over such a change.
const bool guard_standby_calibration = is_rift_s && setup_available;
std::optional<Snapshot> worn_calibration;
vr::HmdMatrix34_t worn_standing{};
uint64_t worn_universe = 0;
auto remember_calibration = [&] {
worn_calibration = snapshot(*setup, path);
worn_standing = system->GetRawZeroPoseToStandingAbsoluteTrackingPose();
worn_universe = system->GetUint64TrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd,
vr::Prop_CurrentUniverseId_Uint64);
};
if (guard_standby_calibration) remember_calibration();
bool calibration_changed = false;
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;
@@ -599,9 +625,37 @@ int main(int argc, char **argv)
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 (guard_standby_calibration && (event.eventType == vr::VREvent_ChaperoneUniverseHasChanged ||
event.eventType == vr::VREvent_ChaperoneDataHasChanged ||
event.eventType == vr::VREvent_StandingZeroPoseReset))
calibration_changed = true;
if (event.eventType == vr::VREvent_Quit) { display_recovery.note_quit(); running = false; }
}
if (!running) break;
if (calibration_changed) {
calibration_changed = false;
bool standby = system->GetTrackedDeviceActivityLevel(vr::k_unTrackedDeviceIndex_Hmd) ==
vr::k_EDeviceActivityLevel_Standby;
auto universe = system->GetUint64TrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd,
vr::Prop_CurrentUniverseId_Uint64);
if (standby && step == 0 && worn_calibration && universe == worn_universe &&
!same_transform(system->GetRawZeroPoseToStandingAbsoluteTrackingPose(), worn_standing)) {
// SteamVR applies the reload asynchronously; its change event re-checks the result.
bool ok = restore(*setup, path, *worn_calibration, false);
fprintf(stderr, "monado-roomsetup-guard: standby calibration reverted=%s\n", ok ? "ok" : "FAILED");
if (notifications && overlay != vr::k_ulOverlayHandleInvalid) {
vr::VRNotificationId id;
notifications->CreateNotification(overlay, 0, vr::EVRNotificationType_Transient,
ok ? "Quick Calibrate needs a tracked headset, and the Rift S stops tracking off your head. "
"The previous floor and boundary were kept. Use Room Setup to set the floor."
: "Quick Calibrate ran while the headset was off. Restoring the previous calibration failed. "
"Run Room Setup before playing.",
vr::EVRNotificationStyle_Application, nullptr, &id);
}
} else if (!standby || universe != worn_universe) {
remember_calibration();
}
}
if (world_setup && world_cancel_deadline != std::chrono::steady_clock::time_point{} &&
std::chrono::steady_clock::now() >= world_cancel_deadline) {
world_command("cancel");
+9
View File
@@ -257,3 +257,12 @@ if(XRT_BUILD_DRIVER_RIFT_S AND XRT_MODULE_CONSTELLATION_TRACKING)
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()
# rifts-setup edits Steam's own config; cover its KeyValues editing.
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
find_package(Python3 COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
add_test(NAME rifts_setup COMMAND Python3::Interpreter -I ${CMAKE_CURRENT_SOURCE_DIR}/rifts_setup_test.py
${PROJECT_BINARY_DIR}/rifts-setup)
endif()
endif()
+68
View File
@@ -0,0 +1,68 @@
# Copyright 2026, lbgos
# SPDX-License-Identifier: BSL-1.0
"""Checks how rifts-setup edits SteamVR's launch option in Steam's localconfig.vdf.
Usage: rifts_setup_test.py <configured rifts-setup script>
"""
import importlib.machinery
import importlib.util
import sys
import unittest
loader = importlib.machinery.SourceFileLoader('rifts_setup', sys.argv.pop(1))
setup = importlib.util.module_from_spec(importlib.util.spec_from_loader(loader.name, loader))
loader.exec_module(setup)
OURS = '/home/u/.local/share/Steam/steamapps/common/SteamVR/bin/vrmonitor.sh %command%'
def config(app_lines):
body = ''.join('\t\t\t\t\t\t' + line + '\n' for line in app_lines)
return ('"UserLocalConfigStore"\n{\n\t"Software"\n\t{\n\t\t"Valve"\n\t\t{\n\t\t\t"Steam"\n\t\t\t{\n'
'\t\t\t\t"apps"\n\t\t\t\t{\n\t\t\t\t\t"250820"\n\t\t\t\t\t{\n' + body +
'\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n')
def launch_options(text):
block, _ = setup.vdf_block(setup.vdf_tokens(text), close=len(text))
for key in setup.LAUNCH_PATH:
block = next(v for k, v in block['items'] if k.lower() == key.lower())
return next((v[3] for k, v in block['items'] if k == 'LaunchOptions'), None)
class LaunchOptions(unittest.TestCase):
def test_adds_line_and_keeps_the_rest(self):
before = config(['"LastPlayed"\t\t"1"'])
after = setup.with_launch_options(before, OURS)
self.assertEqual(after, config(['"LastPlayed"\t\t"1"', '"LaunchOptions"\t\t"' + OURS + '"']))
def test_fills_empty_option(self):
after = setup.with_launch_options(config(['"LaunchOptions"\t\t""']), OURS)
self.assertEqual(after, config(['"LaunchOptions"\t\t"' + OURS + '"']))
def test_keeps_custom_option(self):
self.assertIsNone(setup.with_launch_options(config(['"LaunchOptions"\t\t"gamemoderun %command%"']), OURS))
def test_updates_our_option_after_steamvr_moves(self):
old = config(['"LaunchOptions"\t\t"/old/SteamVR/bin/vrmonitor.sh %command%"'])
self.assertEqual(launch_options(setup.with_launch_options(old, OURS)), OURS)
def test_uninstall_removes_only_our_line(self):
installed = config(['"LaunchOptions"\t\t"' + OURS + '"', '"LastPlayed"\t\t"1"'])
self.assertEqual(setup.with_launch_options(installed, None), config(['"LastPlayed"\t\t"1"']))
custom = config(['"LaunchOptions"\t\t"gamemoderun %command%"'])
self.assertIsNone(setup.with_launch_options(custom, None))
def test_creates_missing_app_block(self):
before = config([]).replace('\t\t\t\t\t"250820"\n\t\t\t\t\t{\n\t\t\t\t\t}\n', '')
after = setup.with_launch_options(before, OURS)
self.assertEqual(launch_options(after), OURS)
self.assertEqual(after, config(['"LaunchOptions"\t\t"' + OURS + '"']))
def test_quoted_values_round_trip(self):
spaced = '"/my games/SteamVR/bin/vrmonitor.sh" %command%'
self.assertEqual(launch_options(setup.with_launch_options(config([]), spaced)), spaced)
if __name__ == '__main__':
unittest.main()
+26 -16
View File
@@ -108,7 +108,7 @@ TEST_CASE("Setup RGB projection keeps eye rays independent of surface depth")
}
}
TEST_CASE("Saved floor sends a numeric detection and only seeds manual entry once")
TEST_CASE("Floor detection uses the saved plane only when trusted and seeds manual entry from it")
{
struct Messages : roomsetup::Mailbox
{
@@ -153,26 +153,29 @@ TEST_CASE("Saved floor sends a numeric detection and only seeds manual entry onc
}
} paths;
roomsetup::SavedFloor floor{&messages, &paths};
// An unlocalized room must not inject either the old plane or a manual-floor reset.
CHECK_FALSE(floor.publish(true, false));
CHECK_FALSE(floor.publish(false, false));
CHECK(messages.sent.empty());
REQUIRE(floor.publish());
auto position = [&](size_t index) {
auto *result = cJSON_Parse(messages.sent.at(index).c_str());
REQUIRE(result);
CHECK(cJSON_IsNumber(cJSON_GetObjectItem(result, "pos")));
CHECK(cJSON_GetObjectItem(result, "position") == nullptr);
double value = cJSON_GetNumberValue(cJSON_GetObjectItem(result, "pos"));
cJSON_Delete(result);
return value;
};
// An unlocalized room must not inject the old plane or a manual-floor reset,
// but first setup still needs the session's nominal floor at raw Y=0.
REQUIRE(floor.publish(true, false));
REQUIRE(messages.sent.size() == 1);
auto *result = cJSON_Parse(messages.sent[0].c_str());
REQUIRE(result);
CHECK(cJSON_IsNumber(cJSON_GetObjectItem(result, "pos")));
CHECK(cJSON_GetNumberValue(cJSON_GetObjectItem(result, "pos")) == Catch::Approx(.9));
CHECK(cJSON_GetObjectItem(result, "position") == nullptr);
cJSON_Delete(result);
CHECK(position(0) == Catch::Approx(.7));
REQUIRE(floor.publish());
REQUIRE(messages.sent.size() == 2);
CHECK(position(1) == Catch::Approx(.9));
REQUIRE(floor.publish(true));
REQUIRE(messages.sent.size() == 3);
auto *reset = cJSON_Parse(messages.sent[2].c_str());
REQUIRE(messages.sent.size() == 4);
auto *reset = cJSON_Parse(messages.sent[3].c_str());
REQUIRE(reset);
CHECK(cJSON_GetNumberValue(cJSON_GetObjectItem(reset, "value_source")) == 2);
cJSON_Delete(reset);
CHECK_FALSE(floor.publish(true, false));
CHECK(messages.sent.size() == 3);
}
TEST_CASE("Boundary warning retries failed delivery and removal")
@@ -225,6 +228,13 @@ TEST_CASE("Native session origin has no geometry and preserves the saved room")
CHECK(cJSON_GetObjectItemCaseSensitive(origin, "collision_bounds") == nullptr);
REQUIRE(rift_s_prepare_session_origin(fixture.path.string()));
CHECK(read_file(fixture.path) == bytes);
// A Quick Calibrate offset from an earlier session must not survive the re-based origin.
fixture.write(R"({"jsonid":"chaperone_info","version":5,"universes":[)" + saved +
R"(,{"universeID":"3","standing":{"translation":[0,-1.6,0],"yaw":0.3}}]})");
REQUIRE(rift_s_prepare_session_origin(fixture.path.string()));
auto calibrated = read_file(fixture.path);
CHECK(calibrated.find(saved) != std::string::npos);
CHECK(calibrated == bytes);
fixture.write("broken json");
CHECK_FALSE(rift_s_prepare_session_origin(fixture.path.string()));
CHECK(read_file(fixture.path) == "broken json");