Keep recognized SteamVR room stable and enable Wayland desktop capture

This commit is contained in:
lbgos committed 2026-10-13 16:51:47 +03:00
1 parent 4024175ac8
commit b7ee38860c
10 files changed
+235 -25

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
[Desktop Entry]
Type=Application
Name=Rift S SteamVR
Exec=@CMAKE_INSTALL_FULL_BINDIR@/rifts-steamvr
Icon=steam
Terminal=false
Categories=Game;
+7
View File
@@ -0,0 +1,7 @@
#!/bin/sh
# Steam owns desktop capture. Enable its portal path for native Wayland windows.
set -eu
if [ "${XDG_SESSION_TYPE:-}" = wayland ] || [ -n "${WAYLAND_DISPLAY:-}" ]; then
exec steam -pipewire -applaunch 250820 -pipewire "$@"
fi
exec steam -applaunch 250820 "$@"
+3 -1
View File
@@ -246,8 +246,10 @@ rift_s_slam_guard_suspend(struct rift_s_slam_guard *g, const struct xrt_quat *fu
}
bool
rift_s_slam_guard_verify_world(struct rift_s_slam_guard *g, const struct xrt_pose *world_from_slam)
rift_s_slam_guard_verify_world(struct rift_s_slam_guard *g, const struct xrt_pose *world_from_slam,
uint32_t confirmations)
{
if (confirmations < 3) return true;
float dot = fabsf(g->align.orientation.x * world_from_slam->orientation.x +
g->align.orientation.y * world_from_slam->orientation.y +
g->align.orientation.z * world_from_slam->orientation.z +
+3 -2
View File
@@ -166,9 +166,10 @@ rift_s_slam_guard_fallback_pose(const struct rift_s_slam_guard *g,
void
rift_s_slam_guard_suspend(struct rift_s_slam_guard *g, const struct xrt_quat *fusion_orientation);
//! Compare a fresh visual match with the current alignment; revoke saved bounds on disagreement.
//! Revoke saved bounds only after three agreeing visual votes disagree with the current alignment.
bool
rift_s_slam_guard_verify_world(struct rift_s_slam_guard *g, const struct xrt_pose *world_from_slam);
rift_s_slam_guard_verify_world(struct rift_s_slam_guard *g, const struct xrt_pose *world_from_slam,
uint32_t confirmations);
//! Start the replacement backend's recovery interval without changing the held world pose.
void
+19 -9
View File
@@ -1320,6 +1320,9 @@ world_write_status(struct rift_s_tracker *t)
: "unanchored";
struct rift_s_world_diagnostics diagnostics = t->world_diagnostics;
bool standby = t->standby, paused = t->slam_paused, restarting = t->slam_restart_pending;
bool tracking = t->slam_guard.state == RIFT_S_SLAM_GUARD_TRACKING;
bool world_valid = t->slam_guard.world_valid;
bool alignment_changed = t->world_alignment_changed;
uint64_t generation = t->slam_generation;
uint32_t confirmations = t->world_confirmations;
os_mutex_unlock(&t->mutex);
@@ -1363,14 +1366,14 @@ world_write_status(struct rift_s_tracker *t)
// available without a debug launch or stopping tracking with a debugger.
fprintf(file, "%lld %s attempts=%" PRIu64 " usable=%" PRIu64
" capture_ns=%lld sample_ns=%lld pose_valid=%d flags=%u history=%d standby=%d paused=%d restart=%d"
" generation=%" PRIu64 " confirmations=%u keyframes=%u"
" tracking=%d world_valid=%d alignment_changed=%d generation=%" PRIu64 " confirmations=%u keyframes=%u"
" features=%u/%u/%u/%u/%u matches=%u/%u/%u/%u/%u inliers=%u/%u/%u/%u/%u"
" matched=%d rejection=%s"
" fail_paused=%" PRIu64 " fail_sample=%" PRIu64 " fail_gap=%" PRIu64 " fail_flags=%" PRIu64
" fail_generation=%" PRIu64 " fail_guard=%" PRIu64 "\n",
(long long)os_monotonic_get_ns(), state, diagnostics.attempts, diagnostics.usable,
(long long)diagnostics.capture_ns, (long long)diagnostics.sample_ns, diagnostics.pose_valid,
diagnostics.pose_flags, diagnostics.history_result, standby, paused, restarting, generation, confirmations, result->keyframes,
diagnostics.pose_flags, diagnostics.history_result, standby, paused, restarting, tracking, world_valid, alignment_changed, generation, confirmations, 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],
@@ -1398,6 +1401,7 @@ world_read_command(struct rift_s_tracker *t)
rift_s_world_anchor_clear(t->world_anchor);
os_mutex_lock(&t->mutex);
t->world_setup = true;
t->world_alignment_changed = false;
t->slam_guard.boundary_confirmed = false;
t->slam_guard.require_relocalization = false;
t->world_confirmations = 0;
@@ -1423,6 +1427,7 @@ world_read_command(struct rift_s_tracker *t)
t->slam_guard.boundary_confirmed = saved && t->slam_guard.world_valid &&
rift_s_world_anchor_has_map(t->world_anchor);
t->world_setup = false;
t->world_alignment_changed = 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)
@@ -1444,6 +1449,7 @@ world_read_command(struct rift_s_tracker *t)
rift_s_slam_guard_suspend(&t->slam_guard, &t->pose.orientation);
}
t->world_setup = false;
t->world_alignment_changed = 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;
@@ -1626,12 +1632,6 @@ rift_s_world_thread(void *ptr)
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);
if (t->slam_guard.boundary_confirmed &&
!rift_s_slam_guard_verify_world(&t->slam_guard, &candidate)) {
t->world_confirmations = 0;
t->world_last_verify_ns = 0;
RIFT_S_WARN("Saved room alignment changed; boundary unavailable until reconfirmed");
}
bool consistent =
t->world_confirmations > 0 && t->world_candidate_generation == generation &&
capture_ns > t->world_candidate_ns &&
@@ -1644,8 +1644,18 @@ rift_s_world_thread(void *ptr)
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) {
// Verification needs the same three agreeing votes as recognition. One
// displaced solution must not revoke bounds and re-place the dashboard.
if (t->slam_guard.boundary_confirmed) {
if (!rift_s_slam_guard_verify_world(&t->slam_guard, &candidate, t->world_confirmations)) {
t->world_alignment_changed = true;
t->world_last_verify_ns = 0;
RIFT_S_WARN("Saved room alignment changed across three votes; boundary unavailable until reconfirmed");
}
if (t->world_confirmations >= 3) t->world_confirmations = 0;
} else if (t->world_confirmations >= 3) {
rift_s_slam_guard_set_world(&t->slam_guard, &candidate);
t->world_alignment_changed = false;
RIFT_S_INFO(
"World relocalized inliers=%u/%u error_px=%.3f "
"transform=(%.4f,%.4f,%.4f)",
+1
View File
@@ -73,6 +73,7 @@ struct rift_s_tracker
bool world_setup;
bool world_map_available;
bool world_storage_failed;
bool world_alignment_changed;
uint64_t world_command_id;
uint32_t world_confirmations;
struct xrt_pose world_candidate;
@@ -90,7 +90,8 @@ is_rift_s_touch(const struct xrt_device *xdev)
}
static void
update_rift_s_universe(vr::PropertyContainerHandle_t container, uint64_t &universe, int64_t &last_poll)
update_rift_s_universe(vr::PropertyContainerHandle_t container, uint64_t &universe, int64_t &last_poll,
RiftSWorldUniverse &selection)
{
int64_t now = os_monotonic_get_ns();
if (now - last_poll < 500000000LL) return;
@@ -101,9 +102,10 @@ update_rift_s_universe(vr::PropertyContainerHandle_t container, uint64_t &univer
int64_t stamp = 0;
std::string status;
stream >> stamp >> status;
if (!stream) return; // Missing or half-written status: keep the current universe, do not flap.
static const int64_t started = now;
uint64_t next = rift_s_world_universe(status, stamp, now, started);
std::string details;
if (stream) std::getline(stream, details);
else { stamp = 0; status.clear(); }
uint64_t next = selection.update(status, stamp, now, details, universe);
if (next == universe) return;
auto error = vr::VRProperties()->SetUint64Property(container, vr::Prop_CurrentUniverseId_Uint64, next);
if (error == vr::TrackedProp_Success) {
@@ -928,7 +930,7 @@ public:
if (m_unObjectId != vr::k_unTrackedDeviceIndexInvalid) {
if (is_rift_s_touch(m_xdev))
update_rift_s_universe(m_ulPropertyContainer, m_worldUniverse, m_worldPoll);
update_rift_s_universe(m_ulPropertyContainer, m_worldUniverse, m_worldPoll, m_worldSelection);
vr::VRServerDriverHost()->TrackedDevicePoseUpdated(m_unObjectId, GetPose(),
sizeof(vr::DriverPose_t));
}
@@ -961,8 +963,14 @@ public:
m_ulPropertyContainer = vr::VRProperties()->TrackedDeviceToPropertyContainer(m_unObjectId);
// return a constant that's not 0 (invalid) or 1 (reserved for Oculus)
m_worldUniverse = is_rift_s_touch(m_xdev) ? 3 : 2;
vr::VRProperties()->SetUint64Property(m_ulPropertyContainer, vr::Prop_CurrentUniverseId_Uint64, m_worldUniverse);
if (m_poseUpdateThread == nullptr) {
m_worldSelection = {};
m_worldPoll = 0;
m_worldUniverse = is_rift_s_touch(m_xdev) ? 3 : 2;
auto error = vr::VRProperties()->SetUint64Property(m_ulPropertyContainer,
vr::Prop_CurrentUniverseId_Uint64, m_worldUniverse);
if (error != vr::TrackedProp_Success) m_worldUniverse = 0; // Retry on the pose thread.
}
vr::VRProperties()->SetInt32Property(m_ulPropertyContainer, vr::Prop_DeviceClass_Int32, vr::TrackedDeviceClass_Controller);
if (m_hand == XRT_HAND_LEFT) {
@@ -1316,6 +1324,7 @@ public:
vr::PropertyContainerHandle_t m_ulPropertyContainer = vr::k_ulInvalidPropertyContainer;
uint64_t m_worldUniverse = 3;
int64_t m_worldPoll = 0;
RiftSWorldUniverse m_worldSelection;
bool m_emulate_index_controller = false;
@@ -1430,6 +1439,7 @@ private:
vr::PropertyContainerHandle_t m_ulPropertyContainer = vr::k_ulInvalidPropertyContainer;
uint64_t m_worldUniverse = 3;
int64_t m_worldPoll = 0;
RiftSWorldUniverse m_worldSelection;
float m_flSecondsFromVsyncToPhotons = -1;
float m_flDisplayFrequency = -1;
@@ -1513,7 +1523,7 @@ CDeviceDriver_Monado::PoseUpdateThreadFunction()
vr::VRDriverInput()->UpdateBooleanComponent(m_proximity, detected, 0);
}
if (strncmp(m_xdev->str, "Oculus Rift S", 13) == 0)
update_rift_s_universe(m_ulPropertyContainer, m_worldUniverse, m_worldPoll);
update_rift_s_universe(m_ulPropertyContainer, m_worldUniverse, m_worldPoll, m_worldSelection);
vr::VRServerDriverHost()->TrackedDevicePoseUpdated(m_trackedDeviceIndex, GetPose(),
sizeof(vr::DriverPose_t));
}
@@ -1555,8 +1565,14 @@ CDeviceDriver_Monado::Activate(vr::TrackedDeviceIndex_t unObjectId)
// return a constant that's not 0 (invalid) or 1 (reserved for Oculus)
if (strncmp(m_xdev->str, "Oculus Rift S", 13) == 0 && !rift_s_prepare_session_origin())
ovrd_log("Could not prepare unbounded Rift S session origin\n");
m_worldUniverse = strncmp(m_xdev->str, "Oculus Rift S", 13) == 0 ? 3 : 2;
vr::VRProperties()->SetUint64Property(m_ulPropertyContainer, vr::Prop_CurrentUniverseId_Uint64, m_worldUniverse);
if (m_poseUpdateThread == nullptr) {
m_worldSelection = {};
m_worldPoll = 0;
m_worldUniverse = strncmp(m_xdev->str, "Oculus Rift S", 13) == 0 ? 3 : 2;
auto error = vr::VRProperties()->SetUint64Property(m_ulPropertyContainer,
vr::Prop_CurrentUniverseId_Uint64, m_worldUniverse);
if (error != vr::TrackedProp_Success) m_worldUniverse = 0; // Retry on the pose thread.
}
if (m_xdev->camera) {
vr::EVRInitError queue_error = vr::VRInitError_None;
@@ -4,6 +4,7 @@
#include <cstdint>
#include <string_view>
#include <charconv>
// Universe 2 contains the saved room. Universe 3 has only an unbounded session origin.
// A session frame must never use universe 2's floor, heading or collision bounds.
@@ -13,3 +14,66 @@ rift_s_world_universe(std::string_view status, int64_t stamp, int64_t now, int64
bool fresh = stamp > 0 && stamp >= started && stamp <= now && now - stamp <= 2000000000LL;
return fresh && (status == "localized" || status == "setup") ? 2 : 3;
}
// Keep an admitted room through visual-matching misses in the same tracked frame.
// Coordinate-frame replacement is immediate; continuous tracking/status loss gets five seconds.
class RiftSWorldUniverse
{
int64_t started = 0;
int64_t lost_since = 0;
uint64_t generation = 0;
bool have_generation = false;
static int64_t field(std::string_view details, std::string_view key)
{
while (!details.empty()) {
auto end = details.find(' ');
auto token = details.substr(0, end);
if (token.substr(0, key.size()) == key && token.size() > key.size() && token[key.size()] == '=') {
int64_t value = -1;
auto number = token.substr(key.size() + 1);
auto result = std::from_chars(number.data(), number.data() + number.size(), value);
return result.ec == std::errc{} && result.ptr == number.data() + number.size() ? value : -1;
}
if (end == std::string_view::npos) break;
details.remove_prefix(end + 1);
}
return -1;
}
public:
uint64_t update(std::string_view status, int64_t stamp, int64_t now,
std::string_view details, uint64_t current)
{
if (!started) started = now;
current = current == 2 ? 2 : 3;
bool fresh = stamp > 0 && stamp >= started && stamp <= now && now - stamp <= 2000000000LL;
if (fresh) {
auto next_generation = field(details, "generation");
bool replaced = next_generation >= 0 && have_generation &&
generation != static_cast<uint64_t>(next_generation);
if (next_generation >= 0) {
generation = static_cast<uint64_t>(next_generation);
have_generation = true;
}
if (replaced || field(details, "standby") == 1 || field(details, "restart") == 1 ||
field(details, "world_valid") == 0 || field(details, "alignment_changed") == 1 ||
status == "unanchored" || status == "save_failed") {
lost_since = 0;
return 3;
}
if ((status == "setup" || status == "localized") && field(details, "tracking") == 1 && field(details, "world_valid") == 1 &&
field(details, "paused") == 0) {
lost_since = 0;
return 2;
}
if (status == "relocalizing" && field(details, "tracking") == 1 && field(details, "world_valid") == 1 &&
field(details, "paused") == 0) {
lost_since = 0;
return current;
}
}
if (!lost_since) lost_since = now;
return now - lost_since >= 5000000000LL ? 3 : current;
}
};
@@ -88,6 +88,10 @@ if(TARGET monado-roomsetup-guard)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
configure_file(${PROJECT_SOURCE_DIR}/scripts/rifts-setup.in ${PROJECT_BINARY_DIR}/rifts-setup @ONLY)
configure_file(${PROJECT_SOURCE_DIR}/scripts/rifts-steamvr.in ${PROJECT_BINARY_DIR}/rifts-steamvr @ONLY)
configure_file(${PROJECT_SOURCE_DIR}/scripts/rifts-steamvr.desktop.in ${PROJECT_BINARY_DIR}/rifts-steamvr.desktop @ONLY)
install(PROGRAMS ${PROJECT_BINARY_DIR}/rifts-steamvr DESTINATION ${CMAKE_INSTALL_BINDIR})
install(FILES ${PROJECT_BINARY_DIR}/rifts-steamvr.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
configure_file(${PROJECT_SOURCE_DIR}/scripts/rifts-session-install.in ${PROJECT_BINARY_DIR}/rifts-session-install @ONLY)
configure_file(${PROJECT_SOURCE_DIR}/scripts/rifts-steamvr-registration.service.in
${PROJECT_BINARY_DIR}/rifts-steamvr-registration.service @ONLY)
+101 -3
View File
@@ -537,18 +537,116 @@ TEST_CASE("A fresh visual match detects saved boundary translation and yaw drift
xrt_pose candidate = saved;
SECTION("Near alignment remains confirmed") {
candidate.position.x += .03f;
CHECK(rift_s_slam_guard_verify_world(&s.g, &candidate));
CHECK(rift_s_slam_guard_verify_world(&s.g, &candidate, 3));
CHECK(s.g.boundary_confirmed);
}
SECTION("Translation drift hides bounds without moving the held pose") {
candidate.position.x += .05f;
CHECK_FALSE(rift_s_slam_guard_verify_world(&s.g, &candidate));
CHECK_FALSE(rift_s_slam_guard_verify_world(&s.g, &candidate, 3));
CHECK_FALSE(s.g.boundary_confirmed);
}
SECTION("Yaw drift hides bounds") {
candidate.orientation = yaw_quat(yaw_of(saved.orientation) + .06f);
CHECK_FALSE(rift_s_slam_guard_verify_world(&s.g, &candidate));
CHECK_FALSE(rift_s_slam_guard_verify_world(&s.g, &candidate, 3));
CHECK_FALSE(s.g.boundary_confirmed);
}
CHECK(dist(s.g.align.position, saved.position) < 1e-6f);
}
TEST_CASE("Admitted SteamVR room survives alternating matching status without universe flips")
{
RiftSWorldUniverse selection;
const int64_t start = 10000000000LL;
uint64_t universe = selection.update("localized", start, start,
" generation=6 tracking=1 world_valid=1 standby=0 paused=0 restart=0", 3);
REQUIRE(universe == 2);
unsigned flips = 0;
for (int i = 1; i <= 120; ++i) {
auto now = start + i * 500 * MS;
auto next = selection.update(i % 2 ? "relocalizing" : "localized", now, now,
"generation=6 tracking=1 world_valid=1 standby=0 paused=0 restart=0", universe);
flips += next != universe;
universe = next;
}
CHECK(flips == 0);
// Matching failure alone does not prove another room, even after a minute.
for (int i = 121; i <= 240; ++i) {
auto now = start + i * 500 * MS;
universe = selection.update("relocalizing", now, now,
"generation=6 tracking=1 world_valid=1 paused=0", universe);
CHECK(universe == 2);
}
}
TEST_CASE("SteamVR room hysteresis releases real tracking or coordinate-frame loss")
{
RiftSWorldUniverse selection;
const int64_t start = 10000000000LL;
const auto tracked = "generation=6 tracking=1 world_valid=1 paused=0";
REQUIRE(selection.update("localized", start, start, tracked, 3) == 2);
SECTION("continuous tracking loss has a five second grace") {
CHECK(selection.update("relocalizing", start + MS, start + MS,
"generation=6 tracking=0 world_valid=1", 2) == 2);
CHECK(selection.update("relocalizing", start + 5000 * MS, start + 5000 * MS,
"generation=6 tracking=0 world_valid=1", 2) == 2);
CHECK(selection.update("relocalizing", start + 5001 * MS, start + 5001 * MS,
"generation=6 tracking=0 world_valid=1", 2) == 3);
}
SECTION("a brief loss followed by tracked matching misses clears the timer") {
CHECK(selection.update("relocalizing", start + MS, start + MS,
"tracking=0 world_valid=1", 2) == 2);
CHECK(selection.update("relocalizing", start + 4000 * MS, start + 4000 * MS, tracked, 2) == 2);
CHECK(selection.update("relocalizing", start + 6000 * MS, start + 6000 * MS,
"tracking=0 world_valid=1", 2) == 2);
}
SECTION("missing and stale status eventually release bounds") {
CHECK(selection.update("", 0, start + MS, "", 2) == 2);
CHECK(selection.update("", 0, start + 5001 * MS, "", 2) == 3);
CHECK(selection.update("localized", start, start + 6000 * MS, tracked, 3) == 3);
}
SECTION("standby and replacement frames release immediately") {
CHECK(selection.update("localized", start + MS, start + MS, "standby=1", 2) == 3);
CHECK(selection.update("localized", start + MS, start + MS, "generation=7", 2) == 3);
CHECK(selection.update("relocalizing", start + MS, start + MS, "world_valid=0", 2) == 3);
CHECK(selection.update("relocalizing", start + MS, start + MS, "alignment_changed=1", 2) == 3);
}
}
TEST_CASE("SteamVR room admission rejects a prior runtime's status")
{
RiftSWorldUniverse selection;
const int64_t start = 10000000000LL;
CHECK(selection.update("localized", start - 1, start, "tracking=1 world_valid=1 paused=0", 3) == 3);
CHECK(selection.update("relocalizing", start + MS, start + MS, "tracking=1 world_valid=1 paused=0", 3) == 3);
CHECK(selection.update("localized", start + 2 * MS, start + 2 * MS, "tracking=1 world_valid=1 paused=0", 3) == 2);
}
TEST_CASE("One displaced alignment vote cannot revoke saved bounds")
{
Sim s;
xrt_pose raw = XRT_POSE_IDENTITY;
REQUIRE(s.settle(raw) > 0);
xrt_pose candidate = s.g.align;
rift_s_slam_guard_set_world(&s.g, &candidate);
candidate.position.x += .1f;
for (uint32_t votes = 1; votes < 3; ++votes) {
CHECK(rift_s_slam_guard_verify_world(&s.g, &candidate, votes));
CHECK(s.g.boundary_confirmed);
}
CHECK_FALSE(rift_s_slam_guard_verify_world(&s.g, &candidate, 3));
CHECK_FALSE(s.g.boundary_confirmed);
}
TEST_CASE("SteamVR room admission needs complete live tracking metadata")
{
const int64_t start = 10000000000LL;
for (auto details : {"", "tracking=garbage world_valid=1 paused=0",
"tracking=2 world_valid=1 paused=0", "tracking=1 paused=0",
"tracking=1 world_valid=1", "tracking=1 world_valid=1 paused=1"}) {
RiftSWorldUniverse selection;
CHECK(selection.update("localized", start, start, details, 3) == 3);
}
RiftSWorldUniverse selection;
CHECK(selection.update("setup", start, start, "tracking=1 world_valid=1 paused=0", 3) == 2);
CHECK(selection.update("localized", start + MS, start + MS, "restart=1", 2) == 3);
}