mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-17 18:02:19 +02:00
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:
14 files changed
+423
-53
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user