Merge pull request #92 from olekolek1000/wayvr_extprocess

WayVR: External process support, various tweaks and bugfixes
This commit is contained in:
Aleksander
2024-10-27 21:41:17 +01:00
committed by GitHub
10 changed files with 397 additions and 123 deletions

View File

@@ -328,9 +328,7 @@ pub fn openvr_run(running: Arc<AtomicBool>, show_by_default: bool) -> Result<(),
};
#[cfg(feature = "wayvr")]
if let Some(wayvr) = &state.wayvr {
wayvr.borrow_mut().tick_events()?;
}
crate::overlays::wayvr::tick_events::<OpenVrOverlayData>(&mut state, &mut overlays)?;
log::trace!("Rendering frame");

View File

@@ -364,9 +364,7 @@ pub fn openxr_run(running: Arc<AtomicBool>, show_by_default: bool) -> Result<(),
}
#[cfg(feature = "wayvr")]
if let Some(wayvr) = &app_state.wayvr {
wayvr.borrow_mut().tick_events()?;
}
crate::overlays::wayvr::tick_events::<OpenXrOverlayData>(&mut app_state, &mut overlays)?;
for o in overlays.iter_mut() {
if !o.state.want_visible {

View File

@@ -1,4 +1,4 @@
use std::{io::Read, os::unix::net::UnixStream, sync::Arc};
use std::{io::Read, os::unix::net::UnixStream, path::PathBuf, sync::Arc};
use smithay::{
backend::input::Keycode,
@@ -7,9 +7,11 @@ use smithay::{
utils::SerialCounter,
};
use crate::backend::wayvr::{ExternalProcessRequest, WayVRTask};
use super::{
comp::{self},
display, process,
display, process, ProcessWayVREnv,
};
pub struct WayVRClient {
@@ -33,22 +35,29 @@ pub struct WayVRManager {
pub clients: Vec<WayVRClient>,
}
fn get_display_auth_from_pid(pid: i32) -> anyhow::Result<String> {
fn get_wayvr_env_from_pid(pid: i32) -> anyhow::Result<ProcessWayVREnv> {
let path = format!("/proc/{}/environ", pid);
let mut env_data = String::new();
std::fs::File::open(path)?.read_to_string(&mut env_data)?;
let lines: Vec<&str> = env_data.split('\0').filter(|s| !s.is_empty()).collect();
let mut env = ProcessWayVREnv {
display_auth: None,
display_name: None,
};
for line in lines {
if let Some((key, value)) = line.split_once('=') {
if key == "WAYVR_DISPLAY_AUTH" {
return Ok(String::from(value));
env.display_auth = Some(String::from(value));
} else if key == "WAYVR_DISPLAY_NAME" {
env.display_name = Some(String::from(value));
}
}
}
anyhow::bail!("Failed to get display auth from PID {}", pid);
Ok(env)
}
impl WayVRManager {
@@ -73,6 +82,10 @@ impl WayVRManager {
})
}
pub fn add_client(&mut self, client: WayVRClient) {
self.clients.push(client);
}
fn accept_connection(
&mut self,
stream: UnixStream,
@@ -86,18 +99,19 @@ impl WayVRManager {
.unwrap();
let creds = client.get_credentials(&self.display.handle())?;
let auth_key = get_display_auth_from_pid(creds.pid)?;
let process_env = get_wayvr_env_from_pid(creds.pid)?;
// Find suitable auth key from the process list
for process in processes.vec.iter().flatten() {
let process = &process.obj;
for p in processes.vec.iter().flatten() {
if let process::Process::Managed(process) = &p.obj {
if let Some(auth_key) = &process_env.display_auth {
// Find process with matching auth key
if process.auth_key.as_str() == auth_key {
// Check if display handle is valid
if displays.get(&process.display_handle).is_some() {
// Add client
self.clients.push(WayVRClient {
self.add_client(WayVRClient {
client,
display_handle: process.display_handle,
pid: creds.pid as u32,
@@ -106,7 +120,25 @@ impl WayVRManager {
}
}
}
anyhow::bail!("Process auth key is invalid or selected display is non-existent");
}
}
// This is a new process which we didn't met before.
// Treat external processes exclusively (spawned by the user or external program)
log::warn!(
"External process ID {} connected to this Wayland server",
creds.pid
);
self.state
.wayvr_tasks
.send(WayVRTask::NewExternalProcess(ExternalProcessRequest {
env: process_env,
client,
pid: creds.pid as u32,
}));
Ok(())
}
fn accept_connections(
@@ -164,6 +196,15 @@ impl WayVRManager {
const STARTING_WAYLAND_ADDR_IDX: u32 = 20;
fn export_display_number(display_num: u32) -> anyhow::Result<()> {
let mut path = std::env::var("XDG_RUNTIME_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("/tmp"));
path.push("wayvr.disp");
std::fs::write(path, format!("{}\n", display_num)).unwrap();
Ok(())
}
fn create_wayland_listener() -> anyhow::Result<(super::WaylandEnv, wayland_server::ListeningSocket)>
{
let mut env = super::WaylandEnv {
@@ -194,5 +235,7 @@ fn create_wayland_listener() -> anyhow::Result<(super::WaylandEnv, wayland_serve
}
};
let _ = export_display_number(env.display_num);
Ok((env, listener))
}

View File

@@ -50,6 +50,7 @@ pub struct Display {
pub visible: bool,
pub overlay_id: Option<OverlayID>,
pub wants_redraw: bool,
pub primary: bool,
wm: Rc<RefCell<window::WindowManager>>,
pub displayed_windows: Vec<DisplayWindow>,
wayland_env: super::WaylandEnv,
@@ -81,6 +82,7 @@ impl Display {
width: u32,
height: u32,
name: &str,
primary: bool,
) -> anyhow::Result<Self> {
let tex_format = ffi::RGBA;
let internal_format = ffi::RGBA8;
@@ -116,6 +118,7 @@ impl Display {
gles_texture,
wayland_env,
visible: true,
primary,
overlay_id: None,
tasks: SyncEventQueue::new(),
})
@@ -239,7 +242,12 @@ impl Display {
pub fn set_visible(&mut self, visible: bool) {
log::info!("Display \"{}\" visible: {}", self.name.as_str(), visible);
if self.visible != visible {
self.visible = visible;
if visible {
self.wants_redraw = true;
}
}
}
pub fn send_mouse_move(&self, manager: &mut WayVRManager, x: u32, y: u32) {

View File

@@ -1,4 +1,4 @@
mod client;
pub mod client;
mod comp;
pub mod display;
pub mod egl_data;
@@ -45,9 +45,23 @@ impl WaylandEnv {
}
}
#[derive(Clone)]
pub struct ProcessWayVREnv {
pub display_auth: Option<String>,
pub display_name: Option<String>, // Externally spawned process by a user script
}
#[derive(Clone)]
pub struct ExternalProcessRequest {
pub env: ProcessWayVREnv,
pub client: wayland_server::Client,
pub pid: u32,
}
#[derive(Clone)]
pub enum WayVRTask {
NewToplevel(ClientId, ToplevelSurface),
NewExternalProcess(ExternalProcessRequest),
ProcessTerminationRequest(process::ProcessHandle),
}
@@ -56,7 +70,7 @@ pub struct WayVR {
time_start: u64,
gles_renderer: GlesRenderer,
pub displays: display::DisplayVec,
manager: client::WayVRManager,
pub manager: client::WayVRManager,
wm: Rc<RefCell<window::WindowManager>>,
egl_data: Rc<egl_data::EGLData>,
pub processes: process::ProcessVec,
@@ -70,8 +84,13 @@ pub enum MouseIndex {
Right,
}
pub enum TickResult {
NewExternalProcess(ExternalProcessRequest), // Call WayVRManager::add_client after receiving this message
}
impl WayVR {
pub fn new() -> anyhow::Result<Self> {
log::info!("Initializing WayVR");
let display: wayland_server::Display<Application> = wayland_server::Display::new()?;
let dh = display.handle();
let compositor = compositor::CompositorState::new::<Application>(&dh);
@@ -140,7 +159,9 @@ impl WayVR {
Ok(())
}
pub fn tick_events(&mut self) -> anyhow::Result<()> {
pub fn tick_events(&mut self) -> anyhow::Result<Vec<TickResult>> {
let mut res: Vec<TickResult> = Vec::new();
// Check for redraw events
self.displays.iter_mut(&mut |_, disp| {
for disp_window in &disp.displayed_windows {
@@ -159,17 +180,18 @@ impl WayVR {
SmallVec::new();
self.processes.iter_mut(&mut |handle, process| {
if !process.is_running() {
to_remove.push((handle, process.display_handle));
to_remove.push((handle, process.display_handle()));
}
});
for (p_handle, disp_handle) in to_remove {
self.processes.remove(&p_handle);
if let Some(display) = self.displays.get(&disp_handle) {
if let Some(display) = self.displays.get_mut(&disp_handle) {
display
.tasks
.send(display::DisplayTask::ProcessCleanup(p_handle));
display.wants_redraw = true;
}
}
@@ -179,6 +201,9 @@ impl WayVR {
while let Some(task) = self.tasks.read() {
match task {
WayVRTask::NewExternalProcess(req) => {
res.push(TickResult::NewExternalProcess(req));
}
WayVRTask::NewToplevel(client_id, toplevel) => {
// Attach newly created toplevel surfaces to displays
for client in &self.manager.clients {
@@ -197,7 +222,7 @@ impl WayVR {
}
} else {
log::error!(
"Failed to find process by PID {}. It was probably spawned externally.",
"WayVR window creation failed: Unexpected process PID {}. It wasn't registered before.",
client.pid
);
}
@@ -215,7 +240,9 @@ impl WayVR {
}
self.manager
.tick_wayland(&mut self.displays, &mut self.processes)
.tick_wayland(&mut self.displays, &mut self.processes)?;
Ok(res)
}
pub fn tick_finish(&mut self) -> anyhow::Result<()> {
@@ -266,8 +293,22 @@ impl WayVR {
.map(|display| display.dmabuf_data.clone())
}
pub fn get_display_by_name(&self, name: &str) -> Option<display::DisplayHandle> {
for (idx, cell) in self.displays.vec.iter().enumerate() {
pub fn get_primary_display(displays: &DisplayVec) -> Option<display::DisplayHandle> {
for (idx, cell) in displays.vec.iter().enumerate() {
if let Some(cell) = cell {
if cell.obj.primary {
return Some(DisplayVec::get_handle(cell, idx));
}
}
}
None
}
pub fn get_display_by_name(
displays: &DisplayVec,
name: &str,
) -> Option<display::DisplayHandle> {
for (idx, cell) in displays.vec.iter().enumerate() {
if let Some(cell) = cell {
if cell.obj.name == name {
return Some(DisplayVec::get_handle(cell, idx));
@@ -276,11 +317,13 @@ impl WayVR {
}
None
}
pub fn create_display(
&mut self,
width: u32,
height: u32,
name: &str,
primary: bool,
) -> anyhow::Result<display::DisplayHandle> {
let display = display::Display::new(
self.wm.clone(),
@@ -290,6 +333,7 @@ impl WayVR {
width,
height,
name,
primary,
)?;
Ok(self.displays.add(display))
}
@@ -308,17 +352,17 @@ impl WayVR {
) -> Option<process::ProcessHandle> {
for (idx, cell) in self.processes.vec.iter().enumerate() {
if let Some(cell) = &cell {
let process = &cell.obj;
if let process::Process::Managed(process) = &cell.obj {
if process.display_handle != display_handle
|| process.exec_path != exec_path
|| process.args != args
{
continue;
}
return Some(process::ProcessVec::get_handle(cell, idx));
}
}
}
None
}
@@ -328,6 +372,18 @@ impl WayVR {
.send(WayVRTask::ProcessTerminationRequest(process_handle));
}
pub fn add_external_process(
&mut self,
display_handle: display::DisplayHandle,
pid: u32,
) -> process::ProcessHandle {
self.processes
.add(process::Process::External(process::ExternalProcess {
display_handle,
pid,
}))
}
pub fn spawn_process(
&mut self,
display_handle: display::DisplayHandle,
@@ -341,7 +397,9 @@ impl WayVR {
.ok_or(anyhow::anyhow!(STR_INVALID_HANDLE_DISP))?;
let res = display.spawn_process(exec_path, args, env)?;
Ok(self.processes.add(process::Process {
Ok(self
.processes
.add(process::Process::Managed(process::WayVRProcess {
auth_key: res.auth_key,
child: res.child,
display_handle,
@@ -351,6 +409,6 @@ impl WayVR {
.iter()
.map(|(a, b)| (String::from(*a), String::from(*b)))
.collect(),
}))
})))
}
}

View File

@@ -2,7 +2,7 @@ use crate::gen_id;
use super::display;
pub struct Process {
pub struct WayVRProcess {
pub auth_key: String,
pub child: std::process::Child,
pub display_handle: display::DisplayHandle,
@@ -12,7 +12,40 @@ pub struct Process {
pub env: Vec<(String, String)>,
}
impl Drop for Process {
pub struct ExternalProcess {
pub pid: u32,
pub display_handle: display::DisplayHandle,
}
pub enum Process {
Managed(WayVRProcess), // Process spawned by WayVR
External(ExternalProcess), // External process not directly controlled by us
}
impl Process {
pub fn display_handle(&self) -> display::DisplayHandle {
match self {
Process::Managed(p) => p.display_handle,
Process::External(p) => p.display_handle,
}
}
pub fn is_running(&mut self) -> bool {
match self {
Process::Managed(p) => p.is_running(),
Process::External(p) => p.is_running(),
}
}
pub fn terminate(&mut self) {
match self {
Process::Managed(p) => p.terminate(),
Process::External(p) => p.terminate(),
}
}
}
impl Drop for WayVRProcess {
fn drop(&mut self) {
log::info!(
"Sending SIGTERM (graceful exit) to process {}",
@@ -22,8 +55,8 @@ impl Drop for Process {
}
}
impl Process {
pub fn is_running(&mut self) -> bool {
impl WayVRProcess {
fn is_running(&mut self) -> bool {
match self.child.try_wait() {
Ok(Some(_exit_status)) => false,
Ok(None) => true,
@@ -35,7 +68,7 @@ impl Process {
}
}
pub fn terminate(&mut self) {
fn terminate(&mut self) {
unsafe {
// Gracefully stop process
libc::kill(self.child.id() as i32, libc::SIGTERM);
@@ -43,15 +76,44 @@ impl Process {
}
}
impl ExternalProcess {
fn is_running(&self) -> bool {
if self.pid == 0 {
false
} else {
std::fs::metadata(format!("/proc/{}", self.pid)).is_ok()
}
}
fn terminate(&mut self) {
if self.pid != 0 {
unsafe {
// send SIGINT (^C)
libc::kill(self.pid as i32, libc::SIGINT);
}
}
self.pid = 0;
}
}
gen_id!(ProcessVec, Process, ProcessCell, ProcessHandle);
pub fn find_by_pid(processes: &ProcessVec, pid: u32) -> Option<ProcessHandle> {
for (idx, cell) in processes.vec.iter().enumerate() {
if let Some(cell) = cell {
if cell.obj.child.id() == pid {
match &cell.obj {
Process::Managed(wayvr_process) => {
if wayvr_process.child.id() == pid {
return Some(ProcessVec::get_handle(cell, idx));
}
}
Process::External(external_process) => {
if external_process.pid == pid {
return Some(ProcessVec::get_handle(cell, idx));
}
}
}
}
}
None
}

View File

@@ -2,7 +2,9 @@
compile_error!("WayVR feature is not enabled");
use std::{
cell::RefCell,
collections::{BTreeMap, HashMap},
rc::Rc,
sync::Arc,
};
@@ -12,6 +14,7 @@ use crate::{
backend::{
overlay::RelativeTo,
task::{TaskContainer, TaskType},
wayvr,
},
config::{load_known_yaml, ConfigType},
overlays::wayvr::WayVRAction,
@@ -63,6 +66,7 @@ pub struct WayVRDisplay {
pub rotation: Option<Rotation>,
pub pos: Option<[f32; 3]>,
pub attach_to: Option<AttachTo>,
pub primary: Option<bool>,
}
#[derive(Clone, Deserialize, Serialize)]
@@ -79,6 +83,7 @@ impl WayVRCatalog {
#[derive(Deserialize, Serialize)]
pub struct WayVRConfig {
pub version: u32,
pub run_compositor_at_start: bool,
pub catalogs: HashMap<String, WayVRCatalog>,
pub displays: BTreeMap<String, WayVRDisplay>, // sorted alphabetically
}
@@ -92,7 +97,31 @@ impl WayVRConfig {
self.displays.get(name)
}
pub fn post_load(&self, tasks: &mut TaskContainer) {
pub fn get_default_display(&self) -> Option<(String, &WayVRDisplay)> {
for (disp_name, disp) in &self.displays {
if disp.primary.unwrap_or(false) {
return Some((disp_name.clone(), disp));
}
}
None
}
pub fn post_load(
&self,
tasks: &mut TaskContainer,
) -> anyhow::Result<Option<Rc<RefCell<wayvr::WayVR>>>> {
let primary_count = self
.displays
.iter()
.filter(|d| d.1.primary.unwrap_or(false))
.count();
if primary_count > 1 {
anyhow::bail!("Number of primary displays is more than 1")
} else if primary_count == 0 {
log::warn!("No primary display specified");
}
for (catalog_name, catalog) in &self.catalogs {
for app in &catalog.apps {
if let Some(b) = app.shown_at_start {
@@ -105,6 +134,14 @@ impl WayVRConfig {
}
}
}
if self.run_compositor_at_start {
// Start Wayland server instantly
Ok(Some(Rc::new(RefCell::new(wayvr::WayVR::new()?))))
} else {
// Lazy-init WayVR later if the user requested
Ok(None)
}
}
}

View File

@@ -9,7 +9,7 @@ use crate::{
common::OverlayContainer,
input::{self, InteractionHandler},
overlay::{ui_transform, OverlayData, OverlayRenderer, OverlayState, SplitOverlayBackend},
wayvr,
wayvr::{self, display, WayVR},
},
graphics::WlxGraphics,
state::{self, AppState, KeyboardFocus},
@@ -119,6 +119,121 @@ impl WayVRRenderer {
}
}
fn get_or_create_display<O>(
app: &mut AppState,
wayvr: &mut WayVR,
disp_name: &str,
) -> anyhow::Result<(display::DisplayHandle, Option<OverlayData<O>>)>
where
O: Default,
{
let created_overlay: Option<OverlayData<O>>;
let disp_handle = if let Some(disp) = WayVR::get_display_by_name(&wayvr.displays, disp_name) {
created_overlay = None;
disp
} else {
let conf_display = app
.session
.wayvr_config
.get_display(disp_name)
.ok_or(anyhow::anyhow!(
"Cannot find display named \"{}\"",
disp_name
))?
.clone();
let disp_handle = wayvr.create_display(
conf_display.width,
conf_display.height,
disp_name,
conf_display.primary.unwrap_or(false),
)?;
let mut overlay = create_wayvr_display_overlay::<O>(
app,
conf_display.width,
conf_display.height,
disp_handle,
conf_display.scale.unwrap_or(1.0),
)?;
if let Some(attach_to) = &conf_display.attach_to {
overlay.state.relative_to = attach_to.get_relative_to();
}
if let Some(rot) = &conf_display.rotation {
overlay.state.spawn_rotation = glam::Quat::from_axis_angle(
Vec3::from_slice(&rot.axis),
f32::to_radians(rot.angle),
);
}
if let Some(pos) = &conf_display.pos {
overlay.state.spawn_point = Vec3A::from_slice(pos);
}
let display = wayvr.displays.get_mut(&disp_handle).unwrap(); // Never fails
display.overlay_id = Some(overlay.state.id);
created_overlay = Some(overlay);
disp_handle
};
Ok((disp_handle, created_overlay))
}
pub fn tick_events<O>(app: &mut AppState, overlays: &mut OverlayContainer<O>) -> anyhow::Result<()>
where
O: Default,
{
if let Some(wayvr) = app.wayvr.clone() {
let res = wayvr.borrow_mut().tick_events()?;
for result in res {
match result {
wayvr::TickResult::NewExternalProcess(req) => {
let config = &app.session.wayvr_config;
let disp_name = if let Some(display_name) = req.env.display_name {
config
.get_display(display_name.as_str())
.map(|_| display_name)
} else {
config
.get_default_display()
.map(|(display_name, _)| display_name)
};
if let Some(disp_name) = disp_name {
let mut wayvr = wayvr.borrow_mut();
log::info!("Registering external process with PID {}", req.pid);
let (disp_handle, created_overlay) =
get_or_create_display::<O>(app, &mut wayvr, &disp_name)?;
wayvr.add_external_process(disp_handle, req.pid);
wayvr.manager.add_client(wayvr::client::WayVRClient {
client: req.client,
display_handle: disp_handle,
pid: req.pid,
});
if let Some(created_overlay) = created_overlay {
overlays.add(created_overlay);
}
}
}
}
}
}
Ok(())
}
impl WayVRRenderer {
fn ensure_dmabuf(&mut self, data: wayvr::egl_data::DMAbufData) -> anyhow::Result<()> {
if self.dmabuf_image.is_none() {
@@ -285,10 +400,6 @@ fn action_app_click<O>(
where
O: Default,
{
use crate::overlays::wayvr::create_wayvr_display_overlay;
let mut created_overlay: Option<OverlayData<O>> = None;
let wayvr = app.get_wayvr()?.clone();
let catalog = app
@@ -304,54 +415,8 @@ where
if let Some(app_entry) = catalog.get_app(app_name) {
let mut wayvr = wayvr.borrow_mut();
let disp_handle = if let Some(disp) = wayvr.get_display_by_name(&app_entry.target_display) {
disp
} else {
let conf_display = app
.session
.wayvr_config
.get_display(&app_entry.target_display)
.ok_or(anyhow::anyhow!(
"Cannot find display named \"{}\"",
app_entry.target_display
))?
.clone();
let display_handle = wayvr.create_display(
conf_display.width,
conf_display.height,
&app_entry.target_display,
)?;
let mut overlay = create_wayvr_display_overlay::<O>(
app,
conf_display.width,
conf_display.height,
display_handle,
conf_display.scale.unwrap_or(1.0),
)?;
if let Some(attach_to) = &conf_display.attach_to {
overlay.state.relative_to = attach_to.get_relative_to();
}
if let Some(rot) = &conf_display.rotation {
overlay.state.spawn_rotation = glam::Quat::from_axis_angle(
Vec3::from_slice(&rot.axis),
f32::to_radians(rot.angle),
);
}
if let Some(pos) = &conf_display.pos {
overlay.state.spawn_point = Vec3A::from_slice(pos);
}
let display = wayvr.displays.get_mut(&display_handle).unwrap(); // Never fails
display.overlay_id = Some(overlay.state.id);
created_overlay = Some(overlay);
display_handle
};
let (disp_handle, created_overlay) =
get_or_create_display::<O>(app, &mut wayvr, &app_entry.target_display)?;
// Parse additional args
let args_vec: Vec<&str> = if let Some(args) = &app_entry.args {
@@ -380,9 +445,10 @@ where
// Spawn process
wayvr.spawn_process(disp_handle, &app_entry.exec, &args_vec, &env_vec)?;
}
}
Ok(created_overlay)
} else {
Ok(None)
}
}
pub fn action_display_click<O>(
@@ -397,7 +463,7 @@ where
let wayvr = app.get_wayvr()?;
let mut wayvr = wayvr.borrow_mut();
if let Some(handle) = wayvr.get_display_by_name(display_name) {
if let Some(handle) = WayVR::get_display_by_name(&wayvr.displays, display_name) {
if let Some(display) = wayvr.displays.get_mut(&handle) {
if let Some(overlay_id) = display.overlay_id {
if let Some(overlay) = overlays.mut_by_id(overlay_id) {

View File

@@ -5,6 +5,10 @@
version: 1
# Set to true if you want to make Wyland server instantly available
# (used for remote starting external processes)
run_compositor_at_start: false
displays:
Watch:
width: 400
@@ -16,6 +20,7 @@ displays:
Disp1:
width: 640
height: 480
primary: true # Required if you want to attach external processes (not spawned by WayVR itself) without WAYVR_DISPLAY_NAME set
Disp2:
width: 1280
height: 720

View File

@@ -100,7 +100,7 @@ impl AppState {
let session = AppSession::load();
#[cfg(feature = "wayvr")]
session.wayvr_config.post_load(&mut tasks);
let wayvr = session.wayvr_config.post_load(&mut tasks)?;
Ok(AppState {
fc: FontCache::new(session.config.primary_font.clone())?,
@@ -116,7 +116,7 @@ impl AppState {
keyboard_focus: KeyboardFocus::PhysicalScreen,
#[cfg(feature = "wayvr")]
wayvr: None,
wayvr,
})
}
@@ -126,7 +126,6 @@ impl AppState {
if let Some(wvr) = &self.wayvr {
Ok(wvr.clone())
} else {
log::info!("Initializing WayVR");
let wayvr = Rc::new(RefCell::new(WayVR::new()?));
self.wayvr = Some(wayvr.clone());
Ok(wayvr)