diff --git a/scripts/rifts-steamvr.desktop.in b/scripts/rifts-steamvr.desktop.in new file mode 100644 index 000000000..a7d1da75a --- /dev/null +++ b/scripts/rifts-steamvr.desktop.in @@ -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; diff --git a/scripts/rifts-steamvr.in b/scripts/rifts-steamvr.in new file mode 100644 index 000000000..6c0ddc474 --- /dev/null +++ b/scripts/rifts-steamvr.in @@ -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 "$@" diff --git a/src/xrt/drivers/rift_s/rift_s_slam_guard.c b/src/xrt/drivers/rift_s/rift_s_slam_guard.c index fb794055f..bc74d475c 100644 --- a/src/xrt/drivers/rift_s/rift_s_slam_guard.c +++ b/src/xrt/drivers/rift_s/rift_s_slam_guard.c @@ -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 + diff --git a/src/xrt/drivers/rift_s/rift_s_slam_guard.h b/src/xrt/drivers/rift_s/rift_s_slam_guard.h index 55afdf291..3ab0baf47 100644 --- a/src/xrt/drivers/rift_s/rift_s_slam_guard.h +++ b/src/xrt/drivers/rift_s/rift_s_slam_guard.h @@ -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 diff --git a/src/xrt/drivers/rift_s/rift_s_tracker.c b/src/xrt/drivers/rift_s/rift_s_tracker.c index b36a6aa06..764edcb4c 100644 --- a/src/xrt/drivers/rift_s/rift_s_tracker.c +++ b/src/xrt/drivers/rift_s/rift_s_tracker.c @@ -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)", diff --git a/src/xrt/drivers/rift_s/rift_s_tracker.h b/src/xrt/drivers/rift_s/rift_s_tracker.h index 0fa5f352a..3749058d9 100644 --- a/src/xrt/drivers/rift_s/rift_s_tracker.h +++ b/src/xrt/drivers/rift_s/rift_s_tracker.h @@ -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; diff --git a/src/xrt/state_trackers/steamvr_drv/ovrd_driver.cpp b/src/xrt/state_trackers/steamvr_drv/ovrd_driver.cpp index e78def11f..2152ed106 100644 --- a/src/xrt/state_trackers/steamvr_drv/ovrd_driver.cpp +++ b/src/xrt/state_trackers/steamvr_drv/ovrd_driver.cpp @@ -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; diff --git a/src/xrt/state_trackers/steamvr_drv/ovrd_world_universe.hpp b/src/xrt/state_trackers/steamvr_drv/ovrd_world_universe.hpp index c3dfaef11..b6cacd195 100644 --- a/src/xrt/state_trackers/steamvr_drv/ovrd_world_universe.hpp +++ b/src/xrt/state_trackers/steamvr_drv/ovrd_world_universe.hpp @@ -4,6 +4,7 @@ #include #include +#include // 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(next_generation); + if (next_generation >= 0) { + generation = static_cast(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; + } +}; diff --git a/src/xrt/targets/steamvr_drv/CMakeLists.txt b/src/xrt/targets/steamvr_drv/CMakeLists.txt index 363565f5a..3e6c41d3d 100644 --- a/src/xrt/targets/steamvr_drv/CMakeLists.txt +++ b/src/xrt/targets/steamvr_drv/CMakeLists.txt @@ -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) diff --git a/tests/tests_rift_s_slam_guard.cpp b/tests/tests_rift_s_slam_guard.cpp index 79b8ad908..34a7eae31 100644 --- a/tests/tests_rift_s_slam_guard.cpp +++ b/tests/tests_rift_s_slam_guard.cpp @@ -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); +}