feat(vr): add vr.mode with on, off and auto

vr.mode in config.yaml picks how Nightcap starts: off on the desktop, on in
VR, auto in VR only when this user's SteamVR is running and its OpenXR
runtime reports a headset. auto never starts SteamVR, and asks its runtime
from a short-lived copy of the process so the runtime's libraries do not load
before the VR backend arms. vr.enabled still works as on/off, and --vr or
--no-vr still override the file for one launch.
This commit is contained in:
dim-ghub committed 2026-10-15 19:37:32 -05:00
1 parent 35033454b0
commit ea248b1ecf
14 files changed
+423 -53

No files matched your search

+1
View File
@@ -27,6 +27,7 @@ endif()
add_library(mocktail_vr STATIC
src/vr/diagnostics.cc
src/vr/headset_detection.cc
src/vr/openxr_backend_mode.cc
src/vr/vr_perf.cc
src/vr/vr_mirror.cc
+8 -9
View File
@@ -53,15 +53,14 @@ graphics:
# vsync: off
# vr:
# Boolean (default: false). Same as launching with --vr.
# Experimental VR for verified builds 2998 and 3092. Mocktail initializes
# the guest DebugDeviceVR eye resources (two
# distinct 500x500 eye framebuffers), the Roblox renderer draws both eyes
# every frame, and the frames are submitted to the active OpenXR runtime as
# a stereo projection layer with head pose (runtime-tracked, or scripted for
# desktop testing). Controllers use the native VR bridge. Standalone OpenXR
# test scene: mocktail-vr-probe --scene. Use --no-vr to launch normally.
# enabled: false
# String (default: off): experimental VR through OpenXR on supported Roblox
# builds. off always starts on the desktop. on always starts in VR, like
# --vr. auto starts in VR when SteamVR is already running with a connected
# headset (ALVR included), and on the desktop otherwise. auto never starts
# SteamVR. --vr and --no-vr override this for one launch. The older
# enabled: true/false still works as on/off; set one or the other.
# Standalone OpenXR test scene: mocktail-vr-probe --scene.
# mode: off
# Optional string or integer: frame-rate limit used instead of
# graphics.frame_rate_limit while VR is active. Same values: -1 (Roblox's
# own cap, usually 60), display, unlimited, or any positive integer. The
+35
View File
@@ -0,0 +1,35 @@
#ifndef MOCKTAIL_VR_HEADSET_DETECTION_H_
#define MOCKTAIL_VR_HEADSET_DETECTION_H_
#include <chrono>
#include <string>
namespace mocktail::vr {
// Set on the child process that answers DetectSteamVrHeadset. main() checks
// it first and returns RunHeadsetProbeChild() without starting Roblox.
inline constexpr const char* kHeadsetProbeEnvironment =
"MOCKTAIL_VR_HEADSET_PROBE";
struct HeadsetDetection {
bool headset = false;
std::string reason;
};
// vr.mode: auto. True only when this user's SteamVR is running and its OpenXR
// runtime reports a head-mounted display. SteamVR is never started. The
// runtime is asked from a child process (`self_executable` with
// kHeadsetProbeEnvironment set) so its libraries never load into this one
// before the VR backend arms.
HeadsetDetection DetectSteamVrHeadset(
const std::string& self_executable,
std::chrono::milliseconds timeout = std::chrono::seconds(5),
const std::string& proc_root = "/proc");
// The child side: probes the OpenXR runtime in XR_RUNTIME_JSON and returns
// the probe's exit code (0 when a headset is available).
int RunHeadsetProbeChild();
} // namespace mocktail::vr
#endif // MOCKTAIL_VR_HEADSET_DETECTION_H_
+2
View File
@@ -76,6 +76,8 @@ VrRuntimeDiscovery DiscoverVrRuntime(
const std::string& config_home, const std::string& config_dirs,
const std::string& runtime_dir, const std::string& proc_root = "/proc");
std::string VrRuntimeUnavailableHint(bool user_selected, const std::string& manifest);
// SteamVR's OpenXR manifest when this user's vrserver is running, else empty.
std::string RunningSteamVrManifest(const std::string& proc_root = "/proc");
VrBackendMode ResolveVrBackendMode(bool vr_enabled);
const char* VrBackendModeName(VrBackendMode mode);
+8
View File
@@ -20,6 +20,11 @@ namespace runtime {
// must not try to reach an OpenXR runtime on a normal desktop launch.
inline constexpr bool kDefaultVrEnabled = false;
// vr.mode: on and off force VR or the desktop. auto starts in VR only when
// SteamVR is running with a headset, which main() checks before launch.
enum class VrMode { kOff, kOn, kAuto };
const char* VrModeName(VrMode mode);
enum class GraphicsBackend {
kAuto,
kSystem,
@@ -101,6 +106,8 @@ class RuntimeConfig {
bool headless() const { return headless_; }
bool vr_enabled() const { return vr_enabled_; }
// MOCKTAIL_VR_ENABLED (--vr, --no-vr) wins over MOCKTAIL_VR_MODE.
VrMode vr_mode() const { return vr_mode_; }
bool vr_valid() const { return vr_valid_; }
const std::filesystem::path& roblox_library_path() const {
return roblox_library_path_;
@@ -174,6 +181,7 @@ class RuntimeConfig {
private:
bool headless_ = false;
bool vr_enabled_ = kDefaultVrEnabled;
VrMode vr_mode_ = kDefaultVrEnabled ? VrMode::kOn : VrMode::kOff;
bool vr_valid_ = true;
std::filesystem::path roblox_library_path_ = "rbx_bin/libroblox.so";
GraphicsBackend graphics_backend_ = GraphicsBackend::kVulkan;
+35 -1
View File
@@ -24,6 +24,7 @@
#include "mocktail/audio/fmod_jni_audio_bridge.h"
#include "mocktail/audio/webrtc_jni_audio_bridge.h"
#include "mocktail/audio/roblox_output_device_bridge.h"
#include "mocktail/vr/headset_detection.h"
#include "mocktail/vr/openxr_backend.h"
#include "mocktail/vr/openxr_probe.h"
#include "mocktail/vr/roblox_vr_device_bridge.h"
@@ -251,6 +252,11 @@ void ConfigureHostDriverEnvironment() {
int main(int argc, char* argv[]) {
ConfigureHostDriverEnvironment();
// vr.mode: auto asks SteamVR for a headset from a copy of this process.
if (const char* probe = std::getenv(mocktail::vr::kHeadsetProbeEnvironment);
probe != nullptr && std::string_view(probe) == "1") {
return mocktail::vr::RunHeadsetProbeChild();
}
const auto process_started_at = std::chrono::system_clock::now();
mocktail::runtime::CommandLineParseResult command_line =
mocktail::runtime::ParseCommandLine(argc, argv);
@@ -409,10 +415,38 @@ int main(int argc, char* argv[]) {
}
return EXIT_FAILURE;
}
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun &&
runtime_config.config.vr_mode() == mocktail::runtime::VrMode::kAuto) {
std::error_code self_error;
const std::filesystem::path self =
std::filesystem::read_symlink("/proc/self/exe", self_error);
const mocktail::vr::HeadsetDetection detection =
mocktail::vr::DetectSteamVrHeadset(self_error ? std::string()
: self.string());
std::cerr << " [vr] vr.mode is auto: " << detection.reason
<< (detection.headset ? "; starting in VR\n"
: "; starting on the desktop\n");
if (detection.headset) {
// A detected headset is not an explicit --vr: a Roblox build without
// VR support still starts on the desktop.
if (setenv("MOCKTAIL_VR_ENABLED", "1", 1) != 0) {
std::cerr << "[FATAL] Cannot enable VR for the detected headset\n";
return EXIT_FAILURE;
}
runtime_config = mocktail::runtime::LoadRuntimeConfig(
environment, paths.config_file());
if (!runtime_config) {
std::cerr << "[FATAL] Cannot load " << paths.config_file() << ": "
<< runtime_config.error << '\n';
return EXIT_FAILURE;
}
}
}
if (runtime_config.config.vr_enabled() &&
!mocktail::vr::IsOpenXrSupportCompiled()) {
std::cerr << " [vr] This build does not include OpenXR support, but "
"vr.enabled is set; disable it in " << paths.config_file()
"VR is on; set vr.mode to off or auto in "
<< paths.config_file()
<< ".\n";
return EXIT_FAILURE;
}
+26 -2
View File
@@ -173,11 +173,35 @@ std::string BuildNetworkProxyUrl(const NetworkProxyConfig& proxy) {
(ipv6 ? "]" : "") + ":" + std::to_string(proxy.port);
}
const char* VrModeName(VrMode mode) {
switch (mode) {
case VrMode::kOn:
return "on";
case VrMode::kAuto:
return "auto";
case VrMode::kOff:
break;
}
return "off";
}
RuntimeConfig RuntimeConfig::FromEnvironment(const Environment& environment) {
RuntimeConfig config;
config.headless_ = LegacyEnabled(environment, "MOCKTAIL_HEADLESS");
config.vr_enabled_ =
ReadBoolean(environment, "MOCKTAIL_VR_ENABLED", kDefaultVrEnabled, &config.vr_valid_);
if (environment.Get("MOCKTAIL_VR_ENABLED").has_value()) {
config.vr_enabled_ = ReadBoolean(environment, "MOCKTAIL_VR_ENABLED",
kDefaultVrEnabled, &config.vr_valid_);
config.vr_mode_ = config.vr_enabled_ ? VrMode::kOn : VrMode::kOff;
} else if (const auto mode = environment.Get("MOCKTAIL_VR_MODE");
mode.has_value()) {
if (*mode == "auto") {
config.vr_mode_ = VrMode::kAuto;
} else {
config.vr_enabled_ = ReadBoolean(environment, "MOCKTAIL_VR_MODE",
kDefaultVrEnabled, &config.vr_valid_);
config.vr_mode_ = config.vr_enabled_ ? VrMode::kOn : VrMode::kOff;
}
}
config.roblox_library_path_ = environment.GetOr(
"ROBLOX_LIB_PATH", config.roblox_library_path_.string());
config.graphics_backend_name_ = environment.GetOr(
+8 -9
View File
@@ -72,15 +72,14 @@ graphics:
# vsync: off
# vr:
# Boolean (default: false). Same as launching with --vr.
# Experimental VR for verified builds 2998 and 3092. Mocktail initializes
# the guest DebugDeviceVR eye resources (two
# distinct 500x500 eye framebuffers), the Roblox renderer draws both eyes
# every frame, and the frames are submitted to the active OpenXR runtime as
# a stereo projection layer with head pose (runtime-tracked, or scripted for
# desktop testing). Controllers use the native VR bridge. Standalone OpenXR
# test scene: mocktail-vr-probe --scene. Use --no-vr to launch normally.
# enabled: false
# String (default: off): experimental VR through OpenXR on supported Roblox
# builds. off always starts on the desktop. on always starts in VR, like
# --vr. auto starts in VR when SteamVR is already running with a connected
# headset (ALVR included), and on the desktop otherwise. auto never starts
# SteamVR. --vr and --no-vr override this for one launch. The older
# enabled: true/false still works as on/off; set one or the other.
# Standalone OpenXR test scene: mocktail-vr-probe --scene.
# mode: off
# Optional string or integer: frame-rate limit used instead of
# graphics.frame_rate_limit while VR is active. Same values: -1 (Roblox's
# own cap, usually 60), display, unlimited, or any positive integer. The
+20 -3
View File
@@ -169,6 +169,7 @@ bool ValidateAndMap(const ValueMap& yaml, ValueMap* environment,
"runtime.headless",
"runtime.roblox_library",
"vr.enabled",
"vr.mode",
"vr.frame_rate_limit",
"appearance.theme",
"graphics.backend",
@@ -320,13 +321,28 @@ bool ValidateAndMap(const ValueMap& yaml, ValueMap* environment,
}
(*environment)["MOCKTAIL_THEME"] = *theme;
}
if (const auto enabled = value("vr.enabled"); enabled.has_value()) {
// The file only sets MOCKTAIL_VR_MODE, so --vr and --no-vr
// (MOCKTAIL_VR_ENABLED) always win over it.
const auto enabled = value("vr.enabled");
const auto mode = value("vr.mode");
if (enabled.has_value() && mode.has_value()) {
*error = "set either vr.mode or vr.enabled, not both";
return false;
}
if (enabled.has_value()) {
bool parsed = false;
if (!ParseBoolean(*enabled, &parsed)) {
*error = "vr.enabled must be true or false";
return false;
}
(*environment)["MOCKTAIL_VR_ENABLED"] = parsed ? "1" : "0";
(*environment)["MOCKTAIL_VR_MODE"] = parsed ? "on" : "off";
}
if (mode.has_value()) {
if (*mode != "on" && *mode != "off" && *mode != "auto") {
*error = "vr.mode must be on, off, or auto";
return false;
}
(*environment)["MOCKTAIL_VR_MODE"] = *mode;
}
if (const auto frame_rate = value("vr.frame_rate_limit");
frame_rate.has_value()) {
@@ -803,7 +819,8 @@ RuntimeConfigLoadResult LoadRuntimeConfig(
result.error = "VR frame-rate policy is invalid";
} else if (!result.config.vr_valid()) {
result.error = "VR configuration is invalid: MOCKTAIL_VR_ENABLED must be "
"1/0, true/false, or on/off";
"1/0, true/false, or on/off, and MOCKTAIL_VR_MODE on, off, "
"or auto";
} else if (!result.config.device_profile_valid()) {
result.error = "device profile is invalid";
} else if (!result.config.input_capabilities().touch_enabled &&
+101
View File
@@ -0,0 +1,101 @@
#include "mocktail/vr/headset_detection.h"
#include <fcntl.h>
#include <signal.h>
#include <spawn.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cstring>
#include <filesystem>
#include <thread>
#include <vector>
#include "mocktail/vr/openxr_backend.h"
#include "mocktail/vr/openxr_probe.h"
extern char** environ;
namespace mocktail::vr {
HeadsetDetection DetectSteamVrHeadset(const std::string& self_executable,
std::chrono::milliseconds timeout,
const std::string& proc_root) {
if (!IsOpenXrSupportCompiled()) {
return {false, "this build has no OpenXR support"};
}
const std::string manifest = RunningSteamVrManifest(proc_root);
if (manifest.empty()) {
return {false, "SteamVR is not running"};
}
std::error_code error;
if (!std::filesystem::is_regular_file(manifest, error)) {
return {false, "SteamVR's OpenXR runtime is missing at " + manifest};
}
// The child inherits this environment with the runtime and probe mode set.
const std::string runtime = "XR_RUNTIME_JSON=" + manifest;
const std::string probe = std::string(kHeadsetProbeEnvironment) + "=1";
std::vector<char*> environment;
for (char** entry = environ; entry != nullptr && *entry != nullptr; ++entry) {
if (std::strncmp(*entry, "XR_RUNTIME_JSON=", 16) != 0 &&
std::strncmp(*entry, probe.c_str(), probe.size() - 1) != 0) {
environment.push_back(*entry);
}
}
environment.push_back(const_cast<char*>(runtime.c_str()));
environment.push_back(const_cast<char*>(probe.c_str()));
environment.push_back(nullptr);
std::vector<char*> arguments = {const_cast<char*>(self_executable.c_str()),
nullptr};
posix_spawn_file_actions_t actions;
posix_spawn_file_actions_init(&actions);
posix_spawn_file_actions_addopen(&actions, STDOUT_FILENO, "/dev/null",
O_WRONLY, 0);
posix_spawn_file_actions_addopen(&actions, STDERR_FILENO, "/dev/null",
O_WRONLY, 0);
pid_t child = -1;
const int spawned =
posix_spawn(&child, self_executable.c_str(), &actions, nullptr,
arguments.data(), environment.data());
posix_spawn_file_actions_destroy(&actions);
if (spawned != 0) {
return {false, std::string("cannot start the headset check: ") +
std::strerror(spawned)};
}
const auto deadline = std::chrono::steady_clock::now() + timeout;
int status = 0;
while (true) {
const pid_t done = waitpid(child, &status, WNOHANG);
if (done == child) break;
if (done < 0) {
return {false, "the headset check was lost"};
}
if (std::chrono::steady_clock::now() >= deadline) {
kill(child, SIGKILL);
(void)waitpid(child, &status, 0);
return {false, "SteamVR did not answer the headset check in time"};
}
std::this_thread::sleep_for(std::chrono::milliseconds(20));
}
if (!WIFEXITED(status)) {
return {false, "the headset check crashed"};
}
const int code = WEXITSTATUS(status);
if (code == ProbeExitCode(ProbeStatus::kReady)) {
return {true, "SteamVR reports a connected headset"};
}
if (code == ProbeExitCode(ProbeStatus::kHeadsetUnavailable)) {
return {false, "SteamVR is running without a connected headset"};
}
if (code == ProbeExitCode(ProbeStatus::kRuntimeUnavailable)) {
return {false, "SteamVR's OpenXR runtime is unavailable"};
}
return {false, "SteamVR's OpenXR runtime failed the headset check"};
}
int RunHeadsetProbeChild() { return ProbeExitCode(ProbeOpenXr().status); }
} // namespace mocktail::vr
+51 -29
View File
@@ -82,6 +82,55 @@ std::vector<std::string> VrRuntimeManifestCandidates(
return candidates;
}
namespace {
// Restrict process discovery to this user. In particular, never select a
// root server for a desktop client or another user's SteamVR installation.
std::vector<std::pair<std::string, std::filesystem::path>>
UserVrServers(const std::string &proc_root) {
std::vector<std::pair<std::string, std::filesystem::path>> servers;
std::error_code error;
std::filesystem::directory_iterator processes(proc_root, error);
for (; !error && processes != std::filesystem::directory_iterator();
processes.increment(error)) {
const auto name = processes->path().filename().string();
if (name.empty() || !std::all_of(name.begin(), name.end(), [](char c) {
return c >= '0' && c <= '9';
}))
continue;
struct stat metadata{};
if (stat(processes->path().c_str(), &metadata) != 0 ||
metadata.st_uid != getuid())
continue;
std::ifstream comm_file(processes->path() / "comm");
std::string comm;
std::getline(comm_file, comm);
if (comm != "vrserver" && comm != "wivrn-server")
continue;
std::error_code exe_error;
const auto executable =
std::filesystem::read_symlink(processes->path() / "exe", exe_error);
if (exe_error || !executable.is_absolute() || executable.filename() != comm)
continue;
servers.emplace_back(comm, executable);
}
return servers;
}
// vrserver lives in <SteamVR>/bin/linux64/.
std::string SteamVrManifestForServer(const std::filesystem::path &vrserver) {
return (vrserver.parent_path().parent_path().parent_path() /
"steamxr_linux64.json")
.string();
}
} // namespace
std::string RunningSteamVrManifest(const std::string &proc_root) {
for (const auto &[comm, executable] : UserVrServers(proc_root))
if (comm == "vrserver")
return SteamVrManifestForServer(executable);
return {};
}
VrRuntimeDiscovery DiscoverVrRuntime(const std::string &preference,
const std::string &home,
const std::string &config_home,
@@ -118,37 +167,10 @@ VrRuntimeDiscovery DiscoverVrRuntime(const std::string &preference,
}
}
}
// Restrict process discovery to this user. In particular, never select a
// root server for a desktop client or another user's SteamVR installation.
std::error_code error;
std::filesystem::directory_iterator processes(proc_root, error);
for (; !error && processes != std::filesystem::directory_iterator();
processes.increment(error)) {
const auto name = processes->path().filename().string();
if (name.empty() || !std::all_of(name.begin(), name.end(), [](char c) {
return c >= '0' && c <= '9';
}))
continue;
struct stat metadata{};
if (stat(processes->path().c_str(), &metadata) != 0 ||
metadata.st_uid != getuid())
continue;
std::ifstream comm_file(processes->path() / "comm");
std::string comm;
std::getline(comm_file, comm);
if (comm != "vrserver" && comm != "wivrn-server")
continue;
std::error_code exe_error;
const auto executable =
std::filesystem::read_symlink(processes->path() / "exe", exe_error);
if (exe_error || !executable.is_absolute() || executable.filename() != comm)
continue;
for (const auto &[comm, executable] : UserVrServers(proc_root)) {
if (comm == "vrserver") {
steam_running = true;
steam.insert(steam.begin(),
(executable.parent_path().parent_path().parent_path() /
"steamxr_linux64.json")
.string());
steam.insert(steam.begin(), SteamVrManifestForServer(executable));
} else {
// Native developer builds keep this manifest at their build root.
auto directory = executable.parent_path();
+42
View File
@@ -134,6 +134,48 @@ TEST(RuntimeConfigFileTest,
EXPECT_NE(invalid.error.find("VR configuration"), std::string::npos);
}
TEST(RuntimeConfigFileTest, VrModeSelectsOnOffOrAuto) {
TemporaryDirectory temporary;
const auto load = [&](const std::string& vr, MapEnvironment environment =
MapEnvironment()) {
return LoadRuntimeConfig(environment,
temporary.Write("version: 1\nvr:\n" + vr));
};
const auto on = load(" mode: on\n");
ASSERT_TRUE(on) << on.error;
EXPECT_EQ(on.config.vr_mode(), VrMode::kOn);
EXPECT_TRUE(on.config.vr_enabled());
const auto off = load(" mode: off\n");
ASSERT_TRUE(off) << off.error;
EXPECT_EQ(off.config.vr_mode(), VrMode::kOff);
EXPECT_FALSE(off.config.vr_enabled());
// auto stays on the desktop until main() finds a headset.
const auto automatic = load(" mode: auto\n");
ASSERT_TRUE(automatic) << automatic.error;
EXPECT_EQ(automatic.config.vr_mode(), VrMode::kAuto);
EXPECT_FALSE(automatic.config.vr_enabled());
// The older key still means on/off.
EXPECT_EQ(load(" enabled: true\n").config.vr_mode(), VrMode::kOn);
EXPECT_EQ(load(" enabled: false\n").config.vr_mode(), VrMode::kOff);
// --vr / --no-vr (and a detected headset) win over the file.
const auto forced = load(" mode: auto\n",
MapEnvironment({{"MOCKTAIL_VR_ENABLED", "1"}}));
ASSERT_TRUE(forced) << forced.error;
EXPECT_EQ(forced.config.vr_mode(), VrMode::kOn);
EXPECT_TRUE(forced.config.vr_enabled());
const auto desktop = load(" mode: on\n",
MapEnvironment({{"MOCKTAIL_VR_ENABLED", "0"}}));
ASSERT_TRUE(desktop) << desktop.error;
EXPECT_FALSE(desktop.config.vr_enabled());
const auto both = load(" mode: auto\n enabled: true\n");
EXPECT_FALSE(both);
EXPECT_NE(both.error.find("vr.mode"), std::string::npos);
const auto invalid = load(" mode: sometimes\n");
EXPECT_FALSE(invalid);
EXPECT_NE(invalid.error.find("vr.mode"), std::string::npos);
}
TEST(RuntimeConfigFileTest, VrFrameRateLimitIsSeparateFromDesktopLimit) {
TemporaryDirectory temporary;
const auto defaults = LoadRuntimeConfig(
+20
View File
@@ -106,6 +106,26 @@ TEST(RuntimeConfigTest, RejectsMalformedVrEnvironmentInsteadOfEnablingIt) {
EXPECT_EQ(config.vr_enabled(), kDefaultVrEnabled);
}
TEST(RuntimeConfigTest, ReadsVrModeAndLetsVrEnabledWin) {
EXPECT_EQ(RuntimeConfig::FromEnvironment(MapEnvironment()).vr_mode(),
kDefaultVrEnabled ? VrMode::kOn : VrMode::kOff);
const auto automatic = RuntimeConfig::FromEnvironment(
MapEnvironment({{"MOCKTAIL_VR_MODE", "auto"}}));
EXPECT_TRUE(automatic.vr_valid());
EXPECT_EQ(automatic.vr_mode(), VrMode::kAuto);
EXPECT_FALSE(automatic.vr_enabled());
EXPECT_TRUE(RuntimeConfig::FromEnvironment(
MapEnvironment({{"MOCKTAIL_VR_MODE", "on"}}))
.vr_enabled());
const auto overridden = RuntimeConfig::FromEnvironment(MapEnvironment(
{{"MOCKTAIL_VR_MODE", "auto"}, {"MOCKTAIL_VR_ENABLED", "0"}}));
EXPECT_EQ(overridden.vr_mode(), VrMode::kOff);
EXPECT_FALSE(RuntimeConfig::FromEnvironment(
MapEnvironment({{"MOCKTAIL_VR_MODE", "sometimes"}}))
.vr_valid());
EXPECT_STREQ(VrModeName(VrMode::kAuto), "auto");
}
TEST(RuntimeConfigTest, ReadsTypedRuntimeValues) {
const MapEnvironment environment({
{"MOCKTAIL_HEADLESS", "1"},
+66
View File
@@ -1,4 +1,6 @@
#include "mocktail/vr/headset_detection.h"
#include "mocktail/vr/openxr_backend.h"
#include "mocktail/vr/openxr_probe.h"
#include <algorithm>
#include <cstring>
#include <filesystem>
@@ -137,5 +139,69 @@ TEST_F(VrRuntimeDetectionTest,
File(root / "config/openvr/openvrpaths.vrpath", "invalid json");
EXPECT_NE(Selected("alvr"), (install / "steamxr_linux64.json").string());
}
TEST_F(VrRuntimeDetectionTest, RunningSteamVrManifestNeedsThisUsersServer) {
EXPECT_EQ(RunningSteamVrManifest((root / "proc").string()), "");
const auto manifest = Steam();
EXPECT_EQ(RunningSteamVrManifest((root / "proc").string()), manifest);
}
class VrHeadsetDetectionTest : public VrRuntimeDetectionTest {
protected:
// Stands in for the probe child: a script that sees the same environment.
std::string Probe(const std::string &body) {
const auto path = root / "probe.sh";
File(path, "#!/bin/sh\n" + body + "\n");
std::filesystem::permissions(path, std::filesystem::perms::owner_all);
return path.string();
}
HeadsetDetection Detect(const std::string &probe,
std::chrono::milliseconds timeout =
std::chrono::seconds(5)) {
return DetectSteamVrHeadset(probe, timeout, (root / "proc").string());
}
};
TEST_F(VrHeadsetDetectionTest, NoSteamVrMeansDesktopWithoutStartingIt) {
if (!IsOpenXrSupportCompiled())
GTEST_SKIP() << "built without OpenXR";
const auto marker = root / "started";
const auto result = Detect(Probe("touch " + marker.string() + "; exit 0"));
EXPECT_FALSE(result.headset);
EXPECT_NE(result.reason.find("not running"), std::string::npos);
EXPECT_FALSE(std::filesystem::exists(marker));
}
TEST_F(VrHeadsetDetectionTest, AsksSteamVrsRuntimeInAProbeChild) {
if (!IsOpenXrSupportCompiled())
GTEST_SKIP() << "built without OpenXR";
const auto manifest = Steam();
const auto result = Detect(Probe(
"[ \"$XR_RUNTIME_JSON\" = \"" + manifest + "\" ] && [ \"$" +
kHeadsetProbeEnvironment + "\" = 1 ] && exit 0; exit 9"));
EXPECT_TRUE(result.headset) << result.reason;
}
TEST_F(VrHeadsetDetectionTest, MapsProbeOutcomesToDesktop) {
if (!IsOpenXrSupportCompiled())
GTEST_SKIP() << "built without OpenXR";
Steam();
const auto missing =
Detect(Probe("exit " + std::to_string(ProbeExitCode(
ProbeStatus::kHeadsetUnavailable))));
EXPECT_FALSE(missing.headset);
EXPECT_NE(missing.reason.find("without a connected headset"),
std::string::npos);
EXPECT_FALSE(Detect(Probe("exit 1")).headset);
EXPECT_FALSE(Detect(Probe("kill -9 $$")).headset);
EXPECT_FALSE(Detect(root / "no-such-probe").headset);
}
TEST_F(VrHeadsetDetectionTest, GivesUpOnASteamVrThatDoesNotAnswer) {
if (!IsOpenXrSupportCompiled())
GTEST_SKIP() << "built without OpenXR";
Steam();
const auto started = std::chrono::steady_clock::now();
const auto result =
Detect(Probe("exec sleep 30"), std::chrono::milliseconds(200));
EXPECT_FALSE(result.headset);
EXPECT_NE(result.reason.find("in time"), std::string::npos);
EXPECT_LT(std::chrono::steady_clock::now() - started, std::chrono::seconds(5));
}
} // namespace
} // namespace mocktail::vr