new workspace

This commit is contained in:
galister
2025-06-18 01:14:04 +09:00
parent 95f2ae4296
commit f05d3a8251
252 changed files with 24618 additions and 184 deletions

View File

@@ -0,0 +1,370 @@
use std::sync::{Arc, LazyLock};
#[cfg(feature = "openxr")]
use openxr as xr;
use glam::{Affine3A, Vec3, Vec3A};
use idmap::IdMap;
use serde::Deserialize;
use thiserror::Error;
use crate::{
config::AStrSetExt,
hid::{get_keymap_wl, get_keymap_x11},
overlays::{
anchor::create_anchor,
keyboard::{create_keyboard, KEYBOARD_NAME},
screen::WlxClientAlias,
watch::{create_watch, WATCH_NAME},
},
state::AppState,
};
use super::overlay::{OverlayData, OverlayID};
#[derive(Error, Debug)]
pub enum BackendError {
#[error("backend not supported")]
NotSupported,
#[cfg(feature = "openxr")]
#[error("OpenXR Error: {0:?}")]
OpenXrError(#[from] xr::sys::Result),
#[error("Shutdown")]
Shutdown,
#[error("Restart")]
Restart,
#[error("Fatal: {0:?}")]
Fatal(#[from] anyhow::Error),
}
#[cfg(feature = "wayland")]
fn create_wl_client() -> Option<WlxClientAlias> {
wlx_capture::wayland::WlxClient::new()
}
#[cfg(not(feature = "wayland"))]
fn create_wl_client() -> Option<WlxClientAlias> {
None
}
pub struct OverlayContainer<T>
where
T: Default,
{
overlays: IdMap<usize, OverlayData<T>>,
wl: Option<WlxClientAlias>,
}
impl<T> OverlayContainer<T>
where
T: Default,
{
pub fn new(app: &mut AppState, headless: bool) -> anyhow::Result<Self> {
let mut overlays = IdMap::new();
let mut show_screens = app.session.config.show_screens.clone();
let mut wl = None;
let mut keymap = None;
app.screens.clear();
if headless {
log::info!("Running in headless mode; keyboard will be en-US");
} else {
wl = create_wl_client();
let data = if let Some(wl) = wl.as_mut() {
log::info!("Wayland detected.");
keymap = get_keymap_wl()
.map_err(|f| log::warn!("Could not load keyboard layout: {f}"))
.ok();
crate::overlays::screen::create_screens_wayland(wl, app)
} else {
log::info!("Wayland not detected, assuming X11.");
keymap = get_keymap_x11()
.map_err(|f| log::warn!("Could not load keyboard layout: {f}"))
.ok();
match crate::overlays::screen::create_screens_x11pw(app) {
Ok(data) => data,
Err(e) => {
log::info!("Will not use X11 PipeWire capture: {e:?}");
crate::overlays::screen::create_screens_xshm(app)?
}
}
};
if show_screens.is_empty() {
if let Some((_, s, _)) = data.screens.first() {
show_screens.arc_set(s.name.clone());
}
}
for (meta, mut state, backend) in data.screens {
if show_screens.arc_get(state.name.as_ref()) {
state.show_hide = true;
}
overlays.insert(
state.id.0,
OverlayData::<T> {
state,
backend,
..Default::default()
},
);
app.screens.push(meta);
}
}
let anchor = create_anchor(app)?;
overlays.insert(anchor.state.id.0, anchor);
let mut watch = create_watch::<T>(app)?;
watch.state.want_visible = true;
overlays.insert(watch.state.id.0, watch);
let mut keyboard = create_keyboard(app, keymap)?;
keyboard.state.show_hide = show_screens.arc_get(KEYBOARD_NAME);
keyboard.state.want_visible = false;
overlays.insert(keyboard.state.id.0, keyboard);
Ok(Self { overlays, wl })
}
#[cfg(not(feature = "wayland"))]
pub fn update(&mut self, _app: &mut AppState) -> anyhow::Result<Vec<OverlayData<T>>> {
Ok(vec![])
}
#[cfg(feature = "wayland")]
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
#[allow(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
pub fn update(&mut self, app: &mut AppState) -> anyhow::Result<Vec<OverlayData<T>>> {
use crate::overlays::screen::{
create_screen_interaction, create_screen_renderer_wl, load_pw_token_config,
};
use glam::vec2;
use wlx_capture::wayland::OutputChangeEvent;
let mut removed_overlays = vec![];
let Some(wl) = self.wl.as_mut() else {
return Ok(removed_overlays);
};
wl.dispatch_pending();
let mut create_ran = false;
let mut extent_dirty = false;
let mut watch_dirty = false;
let mut maybe_token_store = None;
for ev in wl.iter_events().collect::<Vec<_>>() {
match ev {
OutputChangeEvent::Create(_) => {
if create_ran {
continue;
}
let data = crate::overlays::screen::create_screens_wayland(wl, app);
create_ran = true;
for (meta, state, backend) in data.screens {
self.overlays.insert(
state.id.0,
OverlayData::<T> {
state,
backend,
..Default::default()
},
);
app.screens.push(meta);
watch_dirty = true;
}
}
OutputChangeEvent::Destroy(id) => {
let Some(idx) = app.screens.iter().position(|s| s.native_handle == id) else {
continue;
};
let meta = &app.screens[idx];
let removed = self.overlays.remove(meta.id.0).unwrap();
removed_overlays.push(removed);
log::info!("{}: Destroyed", meta.name);
app.screens.remove(idx);
watch_dirty = true;
extent_dirty = true;
}
OutputChangeEvent::Logical(id) => {
let Some(meta) = app.screens.iter().find(|s| s.native_handle == id) else {
continue;
};
let output = wl.outputs.get(id).unwrap();
let Some(overlay) = self.overlays.get_mut(meta.id.0) else {
continue;
};
let logical_pos =
vec2(output.logical_pos.0 as f32, output.logical_pos.1 as f32);
let logical_size =
vec2(output.logical_size.0 as f32, output.logical_size.1 as f32);
let transform = output.transform.into();
overlay
.backend
.set_interaction(Box::new(create_screen_interaction(
logical_pos,
logical_size,
transform,
)));
extent_dirty = true;
}
OutputChangeEvent::Physical(id) => {
let Some(meta) = app.screens.iter().find(|s| s.native_handle == id) else {
continue;
};
let output = wl.outputs.get(id).unwrap();
let Some(overlay) = self.overlays.get_mut(meta.id.0) else {
continue;
};
let has_wlr_dmabuf = wl.maybe_wlr_dmabuf_mgr.is_some();
let has_wlr_screencopy = wl.maybe_wlr_screencopy_mgr.is_some();
let pw_token_store = maybe_token_store.get_or_insert_with(|| {
load_pw_token_config().unwrap_or_else(|e| {
log::warn!("Failed to load PipeWire token config: {:?}", e);
Default::default()
})
});
if let Some(renderer) = create_screen_renderer_wl(
output,
has_wlr_dmabuf,
has_wlr_screencopy,
pw_token_store,
&app,
) {
overlay.backend.set_renderer(Box::new(renderer));
}
extent_dirty = true;
}
}
}
if extent_dirty && !create_ran {
let extent = wl.get_desktop_extent();
let origin = wl.get_desktop_origin();
app.hid_provider
.set_desktop_extent(vec2(extent.0 as f32, extent.1 as f32));
app.hid_provider
.set_desktop_origin(vec2(origin.0 as f32, origin.1 as f32));
}
if watch_dirty {
let _watch = self.mut_by_name(WATCH_NAME).unwrap(); // want panic
todo!();
}
Ok(removed_overlays)
}
pub fn mut_by_selector(&mut self, selector: &OverlaySelector) -> Option<&mut OverlayData<T>> {
match selector {
OverlaySelector::Id(id) => self.mut_by_id(*id),
OverlaySelector::Name(name) => self.mut_by_name(name),
}
}
pub fn remove_by_selector(&mut self, selector: &OverlaySelector) -> Option<OverlayData<T>> {
match selector {
OverlaySelector::Id(id) => self.overlays.remove(id.0),
OverlaySelector::Name(name) => {
let id = self
.overlays
.iter()
.find(|(_, o)| *o.state.name == **name)
.map(|(id, _)| *id);
id.and_then(|id| self.overlays.remove(id))
}
}
}
pub fn get_by_id(&mut self, id: OverlayID) -> Option<&OverlayData<T>> {
self.overlays.get(id.0)
}
pub fn mut_by_id(&mut self, id: OverlayID) -> Option<&mut OverlayData<T>> {
self.overlays.get_mut(id.0)
}
pub fn get_by_name<'a>(&'a mut self, name: &str) -> Option<&'a OverlayData<T>> {
self.overlays.values().find(|o| *o.state.name == *name)
}
pub fn mut_by_name<'a>(&'a mut self, name: &str) -> Option<&'a mut OverlayData<T>> {
self.overlays.values_mut().find(|o| *o.state.name == *name)
}
pub fn iter(&self) -> impl Iterator<Item = &'_ OverlayData<T>> {
self.overlays.values()
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &'_ mut OverlayData<T>> {
self.overlays.values_mut()
}
pub fn add(&mut self, overlay: OverlayData<T>) {
self.overlays.insert(overlay.state.id.0, overlay);
}
pub fn show_hide(&mut self, app: &mut AppState) {
let any_shown = self
.overlays
.values()
.any(|o| o.state.show_hide && o.state.want_visible);
if !any_shown {
static ANCHOR_LOCAL: LazyLock<Affine3A> =
LazyLock::new(|| Affine3A::from_translation(Vec3::NEG_Z));
let hmd = snap_upright(app.input_state.hmd, Vec3A::Y);
app.anchor = hmd * *ANCHOR_LOCAL;
}
self.overlays.values_mut().for_each(|o| {
if o.state.show_hide {
o.state.want_visible = !any_shown;
if o.state.want_visible
&& app.session.config.realign_on_showhide
&& o.state.recenter
{
o.state.reset(app, false);
}
}
// toggle watch back on if it was hidden
if !any_shown && *o.state.name == *WATCH_NAME {
o.state.reset(app, true);
}
});
}
}
#[derive(Clone, Deserialize, Debug)]
#[serde(untagged)]
pub enum OverlaySelector {
Id(OverlayID),
Name(Arc<str>),
}
pub fn snap_upright(transform: Affine3A, up_dir: Vec3A) -> Affine3A {
if transform.x_axis.dot(up_dir).abs() < 0.2 {
let scale = transform.x_axis.length();
let col_z = transform.z_axis.normalize();
let col_y = up_dir;
let col_x = col_y.cross(col_z);
let col_y = col_z.cross(col_x).normalize();
let col_x = col_x.normalize();
Affine3A::from_cols(
col_x * scale,
col_y * scale,
col_z * scale,
transform.translation,
)
} else {
transform
}
}

View File

@@ -0,0 +1,744 @@
use std::f32::consts::PI;
use std::process::{Child, Command};
use std::{collections::VecDeque, time::Instant};
use glam::{Affine3A, Vec2, Vec3, Vec3A, Vec3Swizzles};
use smallvec::{smallvec, SmallVec};
use crate::backend::common::OverlaySelector;
use crate::backend::overlay::Positioning;
use crate::config::AStrMapExt;
use crate::overlays::anchor::ANCHOR_NAME;
use crate::state::{AppSession, AppState, KeyboardFocus};
use super::overlay::{OverlayID, OverlayState};
use super::task::{TaskContainer, TaskType};
use super::{common::OverlayContainer, overlay::OverlayData};
pub struct TrackedDevice {
pub soc: Option<f32>,
pub charging: bool,
pub role: TrackedDeviceRole,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrackedDeviceRole {
None,
Hmd,
LeftHand,
RightHand,
Tracker,
}
pub struct InputState {
pub hmd: Affine3A,
pub ipd: f32,
pub pointers: [Pointer; 2],
pub devices: Vec<TrackedDevice>,
processes: Vec<Child>,
}
impl InputState {
pub fn new() -> Self {
Self {
hmd: Affine3A::IDENTITY,
ipd: 0.0,
pointers: [Pointer::new(0), Pointer::new(1)],
devices: Vec::new(),
processes: Vec::new(),
}
}
pub const fn pre_update(&mut self) {
self.pointers[0].before = self.pointers[0].now;
self.pointers[1].before = self.pointers[1].now;
}
pub fn post_update(&mut self, session: &AppSession) {
for hand in &mut self.pointers {
#[cfg(debug_assertions)]
debug_print_hand(hand);
if hand.now.click {
hand.last_click = Instant::now();
}
if hand.now.click_modifier_right {
hand.interaction.mode = PointerMode::Right;
continue;
}
if hand.now.click_modifier_middle {
hand.interaction.mode = PointerMode::Middle;
continue;
}
let hmd_up = self.hmd.transform_vector3a(Vec3A::Y);
let dot = hmd_up.dot(hand.pose.transform_vector3a(Vec3A::X))
* 2.0f32.mul_add(-(hand.idx as f32), 1.0);
hand.interaction.mode = if dot < -0.85 {
PointerMode::Right
} else if dot > 0.7 {
PointerMode::Middle
} else {
PointerMode::Left
};
let middle_click_orientation = false;
let right_click_orientation = false;
match hand.interaction.mode {
PointerMode::Middle => {
if !middle_click_orientation {
hand.interaction.mode = PointerMode::Left;
}
}
PointerMode::Right => {
if !right_click_orientation {
hand.interaction.mode = PointerMode::Left;
}
}
_ => {}
}
if hand.now.alt_click != hand.before.alt_click {
// Reap previous processes
self.processes
.retain_mut(|child| !matches!(child.try_wait(), Ok(Some(_))));
let mut args = if hand.now.alt_click {
session.config.alt_click_down.iter()
} else {
session.config.alt_click_up.iter()
};
if let Some(program) = args.next() {
if let Ok(child) = Command::new(program).args(args).spawn() {
self.processes.push(child);
}
}
}
}
}
}
#[cfg(debug_assertions)]
fn debug_print_hand(hand: &Pointer) {
{
if hand.now.click != hand.before.click {
log::debug!("Hand {}: click {}", hand.idx, hand.now.click);
}
if hand.now.grab != hand.before.grab {
log::debug!("Hand {}: grab {}", hand.idx, hand.now.grab);
}
if hand.now.alt_click != hand.before.alt_click {
log::debug!("Hand {}: alt_click {}", hand.idx, hand.now.alt_click);
}
if hand.now.show_hide != hand.before.show_hide {
log::debug!("Hand {}: show_hide {}", hand.idx, hand.now.show_hide);
}
if hand.now.toggle_dashboard != hand.before.toggle_dashboard {
log::debug!(
"Hand {}: toggle_dashboard {}",
hand.idx,
hand.now.toggle_dashboard
);
}
if hand.now.space_drag != hand.before.space_drag {
log::debug!("Hand {}: space_drag {}", hand.idx, hand.now.space_drag);
}
if hand.now.space_rotate != hand.before.space_rotate {
log::debug!("Hand {}: space_rotate {}", hand.idx, hand.now.space_rotate);
}
if hand.now.space_reset != hand.before.space_reset {
log::debug!("Hand {}: space_reset {}", hand.idx, hand.now.space_reset);
}
if hand.now.click_modifier_right != hand.before.click_modifier_right {
log::debug!(
"Hand {}: click_modifier_right {}",
hand.idx,
hand.now.click_modifier_right
);
}
if hand.now.click_modifier_middle != hand.before.click_modifier_middle {
log::debug!(
"Hand {}: click_modifier_middle {}",
hand.idx,
hand.now.click_modifier_middle
);
}
}
}
pub struct InteractionState {
pub mode: PointerMode,
pub grabbed: Option<GrabData>,
pub clicked_id: Option<OverlayID>,
pub hovered_id: Option<OverlayID>,
pub release_actions: VecDeque<Box<dyn Fn()>>,
pub next_push: Instant,
pub haptics: Option<f32>,
}
impl Default for InteractionState {
fn default() -> Self {
Self {
mode: PointerMode::Left,
grabbed: None,
clicked_id: None,
hovered_id: None,
release_actions: VecDeque::new(),
next_push: Instant::now(),
haptics: None,
}
}
}
pub struct Pointer {
pub idx: usize,
pub pose: Affine3A,
pub raw_pose: Affine3A,
pub now: PointerState,
pub before: PointerState,
pub last_click: Instant,
pub(super) interaction: InteractionState,
}
impl Pointer {
pub fn new(idx: usize) -> Self {
debug_assert!(idx == 0 || idx == 1);
Self {
idx,
pose: Affine3A::IDENTITY,
raw_pose: Affine3A::IDENTITY,
now: PointerState::default(),
before: PointerState::default(),
last_click: Instant::now(),
interaction: InteractionState::default(),
}
}
}
#[derive(Clone, Copy, Default)]
pub struct PointerState {
pub scroll_x: f32,
pub scroll_y: f32,
pub click: bool,
pub grab: bool,
pub alt_click: bool,
pub show_hide: bool,
pub toggle_dashboard: bool,
pub space_drag: bool,
pub space_rotate: bool,
pub space_reset: bool,
pub click_modifier_right: bool,
pub click_modifier_middle: bool,
pub move_mouse: bool,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct PointerHit {
pub pointer: usize,
pub overlay: OverlayID,
pub mode: PointerMode,
pub primary: bool,
pub uv: Vec2,
pub dist: f32,
}
#[derive(Clone)]
pub struct Haptics {
pub intensity: f32,
pub duration: f32,
pub frequency: f32,
}
pub trait InteractionHandler {
fn on_hover(&mut self, app: &mut AppState, hit: &PointerHit) -> Option<Haptics>;
fn on_left(&mut self, app: &mut AppState, pointer: usize);
fn on_pointer(&mut self, app: &mut AppState, hit: &PointerHit, pressed: bool);
fn on_scroll(&mut self, app: &mut AppState, hit: &PointerHit, delta_y: f32, delta_x: f32);
}
pub struct DummyInteractionHandler;
impl InteractionHandler for DummyInteractionHandler {
fn on_left(&mut self, _app: &mut AppState, _pointer: usize) {}
fn on_hover(&mut self, _app: &mut AppState, _hit: &PointerHit) -> Option<Haptics> {
None
}
fn on_pointer(&mut self, _app: &mut AppState, _hit: &PointerHit, _pressed: bool) {}
fn on_scroll(&mut self, _app: &mut AppState, _hit: &PointerHit, _delta_y: f32, _delta_x: f32) {}
}
#[derive(Debug, Clone, Copy, Default)]
struct RayHit {
overlay: OverlayID,
global_pos: Vec3A,
local_pos: Vec2,
dist: f32,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct GrabData {
pub offset: Vec3A,
pub grabbed_id: OverlayID,
pub old_curvature: Option<f32>,
pub grab_all: bool,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, Default)]
pub enum PointerMode {
#[default]
Left,
Right,
Middle,
Special,
}
fn update_focus(focus: &mut KeyboardFocus, state: &OverlayState) {
if let Some(f) = &state.keyboard_focus {
if *focus != *f {
log::info!("Setting keyboard focus to {:?}", *f);
*focus = *f;
}
}
}
pub fn interact<O>(
overlays: &mut OverlayContainer<O>,
app: &mut AppState,
) -> [(f32, Option<Haptics>); 2]
where
O: Default,
{
if app.input_state.pointers[1].last_click > app.input_state.pointers[0].last_click {
let right = interact_hand(1, overlays, app);
let left = interact_hand(0, overlays, app);
[left, right]
} else {
let left = interact_hand(0, overlays, app);
let right = interact_hand(1, overlays, app);
[left, right]
}
}
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
fn interact_hand<O>(
idx: usize,
overlays: &mut OverlayContainer<O>,
app: &mut AppState,
) -> (f32, Option<Haptics>)
where
O: Default,
{
let mut pointer = &mut app.input_state.pointers[idx];
if let Some(grab_data) = pointer.interaction.grabbed {
if let Some(grabbed) = overlays.mut_by_id(grab_data.grabbed_id) {
Pointer::handle_grabbed(idx, grabbed, app);
} else {
log::warn!("Grabbed overlay {} does not exist", grab_data.grabbed_id.0);
pointer.interaction.grabbed = None;
}
return (0.1, None);
}
let Some(mut hit) = pointer.get_nearest_hit(overlays) else {
if let Some(hovered_id) = pointer.interaction.hovered_id.take() {
if let Some(hovered) = overlays.mut_by_id(hovered_id) {
hovered.backend.on_left(app, idx);
}
pointer = &mut app.input_state.pointers[idx];
pointer.interaction.hovered_id = None;
}
if !pointer.now.click && pointer.before.click {
if let Some(clicked_id) = pointer.interaction.clicked_id.take() {
if let Some(clicked) = overlays.mut_by_id(clicked_id) {
let hit = PointerHit {
pointer: pointer.idx,
overlay: clicked_id,
mode: pointer.interaction.mode,
..Default::default()
};
clicked.backend.on_pointer(app, &hit, false);
}
}
}
return (0.0, None); // no hit
};
if let Some(hovered_id) = pointer.interaction.hovered_id {
if hovered_id != hit.overlay {
if let Some(old_hovered) = overlays.mut_by_id(hovered_id) {
if Some(pointer.idx) == old_hovered.primary_pointer {
old_hovered.primary_pointer = None;
}
old_hovered.backend.on_left(app, idx);
pointer = &mut app.input_state.pointers[idx];
}
}
}
let Some(hovered) = overlays.mut_by_id(hit.overlay) else {
log::warn!("Hit overlay {} does not exist", hit.overlay.0);
return (0.0, None); // no hit
};
pointer.interaction.hovered_id = Some(hit.overlay);
if let Some(primary_pointer) = hovered.primary_pointer {
if hit.pointer <= primary_pointer {
hovered.primary_pointer = Some(hit.pointer);
hit.primary = true;
}
} else {
hovered.primary_pointer = Some(hit.pointer);
hit.primary = true;
}
#[cfg(debug_assertions)]
log::trace!("Hit: {} {:?}", hovered.state.name, hit);
if pointer.now.grab && !pointer.before.grab && hovered.state.grabbable {
update_focus(&mut app.keyboard_focus, &hovered.state);
pointer.start_grab(hovered, &mut app.tasks);
return (
hit.dist,
Some(Haptics {
intensity: 0.25,
duration: 0.1,
frequency: 0.1,
}),
);
}
// Pass mouse motion events only if not scrolling
// (allows scrolling on all Chromium-based applications)
let haptics = hovered.backend.on_hover(app, &hit);
pointer = &mut app.input_state.pointers[idx];
if pointer.now.scroll_x.abs() > 0.1 || pointer.now.scroll_y.abs() > 0.1 {
let scroll_x = pointer.now.scroll_x;
let scroll_y = pointer.now.scroll_y;
if app.input_state.pointers[1 - idx]
.interaction
.grabbed
.is_some_and(|x| x.grabbed_id == hit.overlay)
{
let can_curve = hovered
.frame_meta()
.is_some_and(|e| e.extent[0] >= e.extent[1]);
if can_curve {
let cur = hovered.state.curvature.unwrap_or(0.0);
let new = scroll_y.mul_add(-0.01, cur).min(0.5);
if new <= f32::EPSILON {
hovered.state.curvature = None;
} else {
hovered.state.curvature = Some(new);
}
} else {
hovered.state.curvature = None;
}
} else {
hovered.backend.on_scroll(app, &hit, scroll_y, scroll_x);
}
pointer = &mut app.input_state.pointers[idx];
}
if pointer.now.click && !pointer.before.click {
pointer.interaction.clicked_id = Some(hit.overlay);
update_focus(&mut app.keyboard_focus, &hovered.state);
hovered.backend.on_pointer(app, &hit, true);
} else if !pointer.now.click && pointer.before.click {
if let Some(clicked_id) = pointer.interaction.clicked_id.take() {
if let Some(clicked) = overlays.mut_by_id(clicked_id) {
clicked.backend.on_pointer(app, &hit, false);
}
} else {
hovered.backend.on_pointer(app, &hit, false);
}
}
(hit.dist, haptics)
}
impl Pointer {
fn get_nearest_hit<O>(&mut self, overlays: &mut OverlayContainer<O>) -> Option<PointerHit>
where
O: Default,
{
let mut hits: SmallVec<[RayHit; 8]> = smallvec!();
for overlay in overlays.iter() {
if !overlay.state.want_visible || !overlay.state.interactable {
continue;
}
if let Some(hit) = self.ray_test(
overlay.state.id,
&overlay.state.transform,
overlay.state.curvature.as_ref(),
) {
if hit.dist.is_infinite() || hit.dist.is_nan() {
continue;
}
hits.push(hit);
}
}
hits.sort_by(|a, b| a.dist.total_cmp(&b.dist));
for hit in &hits {
let overlay = overlays.get_by_id(hit.overlay).unwrap(); // safe because we just got the id from the overlay
let uv = overlay
.state
.interaction_transform
.transform_point2(hit.local_pos);
if uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0 {
continue;
}
return Some(PointerHit {
pointer: self.idx,
overlay: hit.overlay,
mode: self.interaction.mode,
primary: false,
uv,
dist: hit.dist,
});
}
None
}
fn start_grab<O>(&mut self, overlay: &mut OverlayData<O>, tasks: &mut TaskContainer)
where
O: Default,
{
let offset = self
.pose
.inverse()
.transform_point3a(overlay.state.transform.translation);
self.interaction.grabbed = Some(GrabData {
offset,
grabbed_id: overlay.state.id,
old_curvature: overlay.state.curvature,
grab_all: matches!(self.interaction.mode, PointerMode::Right),
});
overlay.state.positioning = match overlay.state.positioning {
Positioning::FollowHand { hand, lerp } => Positioning::FollowHandPaused { hand, lerp },
Positioning::FollowHead { lerp } => Positioning::FollowHeadPaused { lerp },
x => x,
};
// Show anchor
tasks.enqueue(TaskType::Overlay(
OverlaySelector::Name(ANCHOR_NAME.clone()),
Box::new(|app, o| {
o.transform = app.anchor
* Affine3A::from_scale_rotation_translation(
Vec3::ONE * o.spawn_scale,
o.spawn_rotation,
o.spawn_point.into(),
);
o.dirty = true;
o.want_visible = true;
}),
));
log::info!("Hand {}: grabbed {}", self.idx, overlay.state.name);
}
fn handle_grabbed<O>(idx: usize, overlay: &mut OverlayData<O>, app: &mut AppState)
where
O: Default,
{
let mut pointer = &mut app.input_state.pointers[idx];
if pointer.now.grab {
if let Some(grab_data) = pointer.interaction.grabbed.as_mut() {
if pointer.now.click {
pointer.interaction.mode = PointerMode::Special;
let cur_scale = overlay.state.transform.x_axis.length();
if cur_scale < 0.1 && pointer.now.scroll_y > 0.0 {
return;
}
if cur_scale > 20. && pointer.now.scroll_y < 0.0 {
return;
}
overlay.state.transform.matrix3 = overlay
.state
.transform
.matrix3
.mul_scalar(0.025f32.mul_add(-pointer.now.scroll_y, 1.0));
} else if app.session.config.allow_sliding && pointer.now.scroll_y.is_finite() {
grab_data.offset.z -= pointer.now.scroll_y * 0.05;
}
overlay.state.transform.translation =
pointer.pose.transform_point3a(grab_data.offset);
overlay.state.realign(&app.input_state.hmd);
overlay.state.dirty = true;
} else {
log::error!("Grabbed overlay {} does not exist", overlay.state.id.0);
pointer.interaction.grabbed = None;
}
} else {
overlay.state.positioning = match overlay.state.positioning {
Positioning::FollowHandPaused { hand, lerp } => {
Positioning::FollowHand { hand, lerp }
}
Positioning::FollowHeadPaused { lerp } => Positioning::FollowHead { lerp },
x => x,
};
let save_success = overlay.state.save_transform(app);
// re-borrow
pointer = &mut app.input_state.pointers[idx];
if save_success {
if let Some(grab_data) = pointer.interaction.grabbed.as_ref() {
if overlay.state.curvature != grab_data.old_curvature {
if let Some(val) = overlay.state.curvature {
app.session
.config
.curve_values
.arc_set(overlay.state.name.clone(), val);
} else {
let ref_name = overlay.state.name.as_ref();
app.session.config.curve_values.arc_rm(ref_name);
}
}
}
app.session.config.transform_values.arc_set(
overlay.state.name.clone(),
overlay.state.saved_transform.unwrap(), // safe
);
}
pointer.interaction.grabbed = None;
// Hide anchor
app.tasks.enqueue(TaskType::Overlay(
OverlaySelector::Name(ANCHOR_NAME.clone()),
Box::new(|_app, o| {
o.want_visible = false;
}),
));
log::info!("Hand {}: dropped {}", idx, overlay.state.name);
}
}
fn ray_test(
&self,
overlay: OverlayID,
transform: &Affine3A,
curvature: Option<&f32>,
) -> Option<RayHit> {
let (dist, local_pos) = curvature.map_or_else(
|| {
Some(raycast_plane(
&self.pose,
Vec3A::NEG_Z,
transform,
Vec3A::NEG_Z,
))
},
|curvature| raycast_cylinder(&self.pose, Vec3A::NEG_Z, transform, *curvature),
)?;
if dist < 0.0 {
// hit is behind us
return None;
}
Some(RayHit {
overlay,
global_pos: self.pose.transform_point3a(Vec3A::NEG_Z * dist),
local_pos,
dist,
})
}
}
fn raycast_plane(
source: &Affine3A,
source_fwd: Vec3A,
plane: &Affine3A,
plane_norm: Vec3A,
) -> (f32, Vec2) {
let plane_normal = plane.transform_vector3a(plane_norm);
let ray_dir = source.transform_vector3a(source_fwd);
let d = plane.translation.dot(-plane_normal);
let dist = -(d + source.translation.dot(plane_normal)) / ray_dir.dot(plane_normal);
let hit_local = plane
.inverse()
.transform_point3a(source.translation + ray_dir * dist)
.xy();
(dist, hit_local)
}
fn raycast_cylinder(
source: &Affine3A,
source_fwd: Vec3A,
plane: &Affine3A,
curvature: f32,
) -> Option<(f32, Vec2)> {
// this is solved locally; (0,0) is the center of the cylinder, and the cylinder is aligned with the Y axis
let size = plane.x_axis.length();
let to_local = Affine3A {
matrix3: plane.matrix3.mul_scalar(1.0 / size),
translation: plane.translation,
}
.inverse();
let radius = size / (2.0 * PI * curvature);
let ray_dir = to_local.transform_vector3a(source.transform_vector3a(source_fwd));
let ray_origin = to_local.transform_point3a(source.translation) + Vec3A::NEG_Z * radius;
let v_dir = ray_dir.xz();
let v_pos = ray_origin.xz();
let l_dir = v_dir.dot(v_dir);
let l_pos = v_dir.dot(v_pos);
let c = radius.mul_add(-radius, v_pos.dot(v_pos));
let d = l_pos.mul_add(l_pos, -(l_dir * c));
if d < f32::EPSILON {
return None;
}
let sqrt_d = d.sqrt();
let t1 = (-l_pos - sqrt_d) / l_dir;
let t2 = (-l_pos + sqrt_d) / l_dir;
let t = t1.max(t2);
if t < f32::EPSILON {
return None;
}
let mut hit_local = ray_origin + ray_dir * t;
if hit_local.z > 0.0 {
// hitting the opposite half of the cylinder
return None;
}
let max_angle = 2.0 * (size / (2.0 * radius));
let x_angle = (hit_local.x / radius).asin();
hit_local.x = x_angle / max_angle;
hit_local.y /= size;
Some((t, hit_local.xy()))
}

View File

@@ -0,0 +1,22 @@
pub mod common;
pub mod input;
pub mod notifications;
#[allow(clippy::all)]
mod notifications_dbus;
#[cfg(feature = "openvr")]
pub mod openvr;
#[cfg(feature = "openxr")]
pub mod openxr;
#[cfg(feature = "osc")]
pub mod osc;
#[cfg(feature = "wayvr")]
pub mod wayvr;
pub mod overlay;
pub mod task;

View File

@@ -0,0 +1,292 @@
use dbus::{
arg::{PropMap, Variant},
blocking::Connection,
channel::MatchingReceiver,
message::MatchRule,
};
use serde::Deserialize;
use std::{
sync::{
atomic::{AtomicBool, Ordering},
mpsc, Arc,
},
time::Duration,
};
use crate::{
backend::notifications_dbus::OrgFreedesktopNotifications,
overlays::toast::{Toast, ToastTopic},
state::AppState,
};
pub struct NotificationManager {
rx_toast: mpsc::Receiver<Toast>,
tx_toast: mpsc::SyncSender<Toast>,
dbus_data: Option<Connection>,
running: Arc<AtomicBool>,
}
impl NotificationManager {
pub fn new() -> Self {
let (tx_toast, rx_toast) = mpsc::sync_channel(10);
Self {
rx_toast,
tx_toast,
dbus_data: None,
running: Arc::new(AtomicBool::new(true)),
}
}
pub fn submit_pending(&self, app: &mut AppState) {
if let Some(c) = &self.dbus_data {
let _ = c.process(Duration::ZERO);
}
if app.session.config.notifications_enabled {
self.rx_toast.try_iter().for_each(|toast| {
toast.submit(app);
});
} else {
// consume without submitting
self.rx_toast.try_iter().last();
}
}
pub fn run_dbus(&mut self) {
let c = match Connection::new_session() {
Ok(c) => c,
Err(e) => {
log::error!(
"Failed to connect to dbus. Desktop notifications will not work. Cause: {e:?}"
);
return;
}
};
let mut rule = MatchRule::new_method_call();
rule.member = Some("Notify".into());
rule.interface = Some("org.freedesktop.Notifications".into());
rule.path = Some("/org/freedesktop/Notifications".into());
rule.eavesdrop = true;
let proxy = c.with_proxy(
"org.freedesktop.DBus",
"/org/freedesktop/DBus",
Duration::from_millis(5000),
);
let result: Result<(), dbus::Error> = proxy.method_call(
"org.freedesktop.DBus.Monitoring",
"BecomeMonitor",
(vec![rule.match_str()], 0u32),
);
if matches!(result, Ok(())) {
let sender = self.tx_toast.clone();
c.start_receive(
rule,
Box::new(move |msg, _| {
if let Ok(toast) = parse_dbus(&msg) {
match sender.try_send(toast) {
Ok(()) => {}
Err(e) => {
log::error!("Failed to send notification: {e:?}");
}
}
}
true
}),
);
log::info!("Listening to DBus notifications via BecomeMonitor.");
} else {
let rule_with_eavesdrop = {
let mut rule = rule.clone();
rule.eavesdrop = true;
rule
};
let sender2 = self.tx_toast.clone();
let result = c.add_match(rule_with_eavesdrop, move |(): (), _, msg| {
if let Ok(toast) = parse_dbus(msg) {
match sender2.try_send(toast) {
Ok(()) => {}
Err(e) => {
log::error!("Failed to send notification: {e:?}");
}
}
}
true
});
match result {
Ok(_) => {
log::info!("Listening to DBus notifications via eavesdrop.");
}
Err(_) => {
log::error!("Failed to add DBus match. Desktop notifications will not work.",);
}
}
}
self.dbus_data = Some(c);
}
pub fn run_udp(&mut self) {
let sender = self.tx_toast.clone();
let running = self.running.clone();
let _ = std::thread::spawn(move || {
let addr = "127.0.0.1:42069";
let socket = match std::net::UdpSocket::bind(addr) {
Ok(s) => s,
Err(e) => {
log::error!("Failed to bind notification socket @ {addr}: {e:?}");
return;
}
};
if let Err(err) = socket.set_read_timeout(Some(Duration::from_millis(200))) {
log::error!("Failed to set read timeout: {err:?}");
}
let mut buf = [0u8; 1024 * 16]; // vrcx embeds icons as b64
while running.load(Ordering::Relaxed) {
if let Ok((num_bytes, _)) = socket.recv_from(&mut buf) {
let json_str = match std::str::from_utf8(&buf[..num_bytes]) {
Ok(s) => s,
Err(e) => {
log::error!("Failed to receive notification message: {e:?}");
continue;
}
};
let msg = match serde_json::from_str::<XsoMessage>(json_str) {
Ok(m) => m,
Err(e) => {
log::error!("Failed to parse notification message: {e:?}");
continue;
}
};
if msg.messageType != 1 {
continue;
}
let toast = Toast::new(
ToastTopic::XSNotification,
msg.title,
msg.content.unwrap_or_else(|| "".into()),
)
.with_timeout(msg.timeout.unwrap_or(5.))
.with_sound(msg.volume.unwrap_or(-1.) >= 0.); // XSOverlay still plays at 0,
match sender.try_send(toast) {
Ok(()) => {}
Err(e) => {
log::error!("Failed to send notification: {e:?}");
}
}
}
}
log::info!("Notification listener stopped.");
});
}
}
impl Drop for NotificationManager {
fn drop(&mut self) {
self.running.store(false, Ordering::Relaxed);
}
}
pub struct DbusNotificationSender {
connection: Connection,
}
impl DbusNotificationSender {
pub fn new() -> anyhow::Result<Self> {
Ok(Self {
connection: Connection::new_session()?,
})
}
pub fn notify_send(
&self,
summary: &str,
body: &str,
urgency: u8,
timeout: i32,
replaces_id: u32,
transient: bool,
) -> anyhow::Result<u32> {
let proxy = self.connection.with_proxy(
"org.freedesktop.Notifications",
"/org/freedesktop/Notifications",
Duration::from_millis(1000),
);
let mut hints = PropMap::new();
hints.insert("urgency".to_string(), Variant(Box::new(urgency)));
hints.insert("transient".to_string(), Variant(Box::new(transient)));
Ok(proxy.notify(
"WlxOverlay-S",
replaces_id,
"",
summary,
body,
vec![],
hints,
timeout,
)?)
}
pub fn notify_close(&self, id: u32) -> anyhow::Result<()> {
let proxy = self.connection.with_proxy(
"org.freedesktop.Notifications",
"/org/freedesktop/Notifications",
Duration::from_millis(1000),
);
proxy.close_notification(id)?;
Ok(())
}
}
fn parse_dbus(msg: &dbus::Message) -> anyhow::Result<Toast> {
let mut args = msg.iter_init();
let app_name: String = args.read()?;
let _replaces_id: u32 = args.read()?;
let _app_icon: String = args.read()?;
let summary: String = args.read()?;
let body: String = args.read()?;
let title = if summary.is_empty() {
app_name
} else {
summary
};
Ok(
Toast::new(ToastTopic::DesktopNotification, title.into(), body.into())
.with_timeout(5.0)
.with_opacity(1.0),
)
// leave the audio part to the desktop env
}
#[allow(dead_code)]
#[allow(non_snake_case)]
#[derive(Debug, Deserialize)]
struct XsoMessage {
messageType: i32,
index: Option<i32>,
volume: Option<f32>,
audioPath: Option<String>,
timeout: Option<f32>,
title: String,
content: Option<String>,
icon: Option<String>,
height: Option<f32>,
opacity: Option<f32>,
useBase64Icon: Option<bool>,
sourceApp: Option<String>,
alwaysShow: Option<bool>,
}

View File

@@ -0,0 +1,353 @@
// This code was autogenerated with `dbus-codegen-rust -g -m None -d org.freedesktop.Notifications -p /org/freedesktop/Notifications`, see https://github.com/diwic/dbus-rs
use dbus;
#[allow(unused_imports)]
use dbus::arg;
use dbus::blocking;
pub trait OrgFreedesktopDBusProperties {
fn get<R0: for<'b> arg::Get<'b> + 'static>(
&self,
interface_name: &str,
property_name: &str,
) -> Result<R0, dbus::Error>;
fn get_all(&self, interface_name: &str) -> Result<arg::PropMap, dbus::Error>;
fn set<I2: arg::Arg + arg::Append>(
&self,
interface_name: &str,
property_name: &str,
value: I2,
) -> Result<(), dbus::Error>;
}
#[derive(Debug)]
pub struct OrgFreedesktopDBusPropertiesPropertiesChanged {
pub interface_name: String,
pub changed_properties: arg::PropMap,
pub invalidated_properties: Vec<String>,
}
impl arg::AppendAll for OrgFreedesktopDBusPropertiesPropertiesChanged {
fn append(&self, i: &mut arg::IterAppend) {
arg::RefArg::append(&self.interface_name, i);
arg::RefArg::append(&self.changed_properties, i);
arg::RefArg::append(&self.invalidated_properties, i);
}
}
impl arg::ReadAll for OrgFreedesktopDBusPropertiesPropertiesChanged {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(Self {
interface_name: i.read()?,
changed_properties: i.read()?,
invalidated_properties: i.read()?,
})
}
}
impl dbus::message::SignalArgs for OrgFreedesktopDBusPropertiesPropertiesChanged {
const NAME: &'static str = "PropertiesChanged";
const INTERFACE: &'static str = "org.freedesktop.DBus.Properties";
}
impl<T: blocking::BlockingSender, C: ::std::ops::Deref<Target = T>> OrgFreedesktopDBusProperties
for blocking::Proxy<'_, C>
{
fn get<R0: for<'b> arg::Get<'b> + 'static>(
&self,
interface_name: &str,
property_name: &str,
) -> Result<R0, dbus::Error> {
self.method_call(
"org.freedesktop.DBus.Properties",
"Get",
(interface_name, property_name),
)
.and_then(|r: (arg::Variant<R0>,)| Ok((r.0).0))
}
fn get_all(&self, interface_name: &str) -> Result<arg::PropMap, dbus::Error> {
self.method_call(
"org.freedesktop.DBus.Properties",
"GetAll",
(interface_name,),
)
.and_then(|r: (arg::PropMap,)| Ok(r.0))
}
fn set<I2: arg::Arg + arg::Append>(
&self,
interface_name: &str,
property_name: &str,
value: I2,
) -> Result<(), dbus::Error> {
self.method_call(
"org.freedesktop.DBus.Properties",
"Set",
(interface_name, property_name, arg::Variant(value)),
)
}
}
pub trait OrgFreedesktopDBusIntrospectable {
fn introspect(&self) -> Result<String, dbus::Error>;
}
impl<T: blocking::BlockingSender, C: ::std::ops::Deref<Target = T>> OrgFreedesktopDBusIntrospectable
for blocking::Proxy<'_, C>
{
fn introspect(&self) -> Result<String, dbus::Error> {
self.method_call("org.freedesktop.DBus.Introspectable", "Introspect", ())
.and_then(|r: (String,)| Ok(r.0))
}
}
pub trait OrgFreedesktopDBusPeer {
fn ping(&self) -> Result<(), dbus::Error>;
fn get_machine_id(&self) -> Result<String, dbus::Error>;
}
impl<T: blocking::BlockingSender, C: ::std::ops::Deref<Target = T>> OrgFreedesktopDBusPeer
for blocking::Proxy<'_, C>
{
fn ping(&self) -> Result<(), dbus::Error> {
self.method_call("org.freedesktop.DBus.Peer", "Ping", ())
}
fn get_machine_id(&self) -> Result<String, dbus::Error> {
self.method_call("org.freedesktop.DBus.Peer", "GetMachineId", ())
.and_then(|r: (String,)| Ok(r.0))
}
}
pub trait OrgFreedesktopNotifications {
fn set_noti_window_visibility(&self, value: bool) -> Result<(), dbus::Error>;
fn toggle_dnd(&self) -> Result<bool, dbus::Error>;
fn set_dnd(&self, state: bool) -> Result<(), dbus::Error>;
fn get_dnd(&self) -> Result<bool, dbus::Error>;
fn manually_close_notification(&self, id: u32, timeout: bool) -> Result<(), dbus::Error>;
fn close_all_notifications(&self) -> Result<(), dbus::Error>;
fn hide_latest_notification(&self, close: bool) -> Result<(), dbus::Error>;
fn get_capabilities(&self) -> Result<Vec<String>, dbus::Error>;
fn notify(
&self,
app_name: &str,
replaces_id: u32,
app_icon: &str,
summary: &str,
body: &str,
actions: Vec<&str>,
hints: arg::PropMap,
expire_timeout: i32,
) -> Result<u32, dbus::Error>;
fn close_notification(&self, id: u32) -> Result<(), dbus::Error>;
fn get_server_information(&self) -> Result<(String, String, String, String), dbus::Error>;
fn dnd(&self) -> Result<bool, dbus::Error>;
fn set_dnd_(&self, value: bool) -> Result<(), dbus::Error>;
}
#[derive(Debug)]
pub struct OrgFreedesktopNotificationsOnDndToggle {
pub dnd: bool,
}
impl arg::AppendAll for OrgFreedesktopNotificationsOnDndToggle {
fn append(&self, i: &mut arg::IterAppend) {
arg::RefArg::append(&self.dnd, i);
}
}
impl arg::ReadAll for OrgFreedesktopNotificationsOnDndToggle {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(Self { dnd: i.read()? })
}
}
impl dbus::message::SignalArgs for OrgFreedesktopNotificationsOnDndToggle {
const NAME: &'static str = "OnDndToggle";
const INTERFACE: &'static str = "org.freedesktop.Notifications";
}
#[derive(Debug)]
pub struct OrgFreedesktopNotificationsNotificationClosed {
pub id: u32,
pub reason: u32,
}
impl arg::AppendAll for OrgFreedesktopNotificationsNotificationClosed {
fn append(&self, i: &mut arg::IterAppend) {
arg::RefArg::append(&self.id, i);
arg::RefArg::append(&self.reason, i);
}
}
impl arg::ReadAll for OrgFreedesktopNotificationsNotificationClosed {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(Self {
id: i.read()?,
reason: i.read()?,
})
}
}
impl dbus::message::SignalArgs for OrgFreedesktopNotificationsNotificationClosed {
const NAME: &'static str = "NotificationClosed";
const INTERFACE: &'static str = "org.freedesktop.Notifications";
}
#[derive(Debug)]
pub struct OrgFreedesktopNotificationsActionInvoked {
pub id: u32,
pub action_key: String,
}
impl arg::AppendAll for OrgFreedesktopNotificationsActionInvoked {
fn append(&self, i: &mut arg::IterAppend) {
arg::RefArg::append(&self.id, i);
arg::RefArg::append(&self.action_key, i);
}
}
impl arg::ReadAll for OrgFreedesktopNotificationsActionInvoked {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(Self {
id: i.read()?,
action_key: i.read()?,
})
}
}
impl dbus::message::SignalArgs for OrgFreedesktopNotificationsActionInvoked {
const NAME: &'static str = "ActionInvoked";
const INTERFACE: &'static str = "org.freedesktop.Notifications";
}
#[derive(Debug)]
pub struct OrgFreedesktopNotificationsNotificationReplied {
pub id: u32,
pub text: String,
}
impl arg::AppendAll for OrgFreedesktopNotificationsNotificationReplied {
fn append(&self, i: &mut arg::IterAppend) {
arg::RefArg::append(&self.id, i);
arg::RefArg::append(&self.text, i);
}
}
impl arg::ReadAll for OrgFreedesktopNotificationsNotificationReplied {
fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
Ok(Self {
id: i.read()?,
text: i.read()?,
})
}
}
impl dbus::message::SignalArgs for OrgFreedesktopNotificationsNotificationReplied {
const NAME: &'static str = "NotificationReplied";
const INTERFACE: &'static str = "org.freedesktop.Notifications";
}
impl<T: blocking::BlockingSender, C: ::std::ops::Deref<Target = T>> OrgFreedesktopNotifications
for blocking::Proxy<'_, C>
{
fn set_noti_window_visibility(&self, value: bool) -> Result<(), dbus::Error> {
self.method_call(
"org.freedesktop.Notifications",
"SetNotiWindowVisibility",
(value,),
)
}
fn toggle_dnd(&self) -> Result<bool, dbus::Error> {
self.method_call("org.freedesktop.Notifications", "ToggleDnd", ())
.and_then(|r: (bool,)| Ok(r.0))
}
fn set_dnd(&self, state: bool) -> Result<(), dbus::Error> {
self.method_call("org.freedesktop.Notifications", "SetDnd", (state,))
}
fn get_dnd(&self) -> Result<bool, dbus::Error> {
self.method_call("org.freedesktop.Notifications", "GetDnd", ())
.and_then(|r: (bool,)| Ok(r.0))
}
fn manually_close_notification(&self, id: u32, timeout: bool) -> Result<(), dbus::Error> {
self.method_call(
"org.freedesktop.Notifications",
"ManuallyCloseNotification",
(id, timeout),
)
}
fn close_all_notifications(&self) -> Result<(), dbus::Error> {
self.method_call("org.freedesktop.Notifications", "CloseAllNotifications", ())
}
fn hide_latest_notification(&self, close: bool) -> Result<(), dbus::Error> {
self.method_call(
"org.freedesktop.Notifications",
"HideLatestNotification",
(close,),
)
}
fn get_capabilities(&self) -> Result<Vec<String>, dbus::Error> {
self.method_call("org.freedesktop.Notifications", "GetCapabilities", ())
.and_then(|r: (Vec<String>,)| Ok(r.0))
}
fn notify(
&self,
app_name: &str,
replaces_id: u32,
app_icon: &str,
summary: &str,
body: &str,
actions: Vec<&str>,
hints: arg::PropMap,
expire_timeout: i32,
) -> Result<u32, dbus::Error> {
self.method_call(
"org.freedesktop.Notifications",
"Notify",
(
app_name,
replaces_id,
app_icon,
summary,
body,
actions,
hints,
expire_timeout,
),
)
.and_then(|r: (u32,)| Ok(r.0))
}
fn close_notification(&self, id: u32) -> Result<(), dbus::Error> {
self.method_call("org.freedesktop.Notifications", "CloseNotification", (id,))
}
fn get_server_information(&self) -> Result<(String, String, String, String), dbus::Error> {
self.method_call("org.freedesktop.Notifications", "GetServerInformation", ())
}
fn dnd(&self) -> Result<bool, dbus::Error> {
<Self as blocking::stdintf::org_freedesktop_dbus::Properties>::get(
self,
"org.freedesktop.Notifications",
"Dnd",
)
}
fn set_dnd_(&self, value: bool) -> Result<(), dbus::Error> {
<Self as blocking::stdintf::org_freedesktop_dbus::Properties>::set(
self,
"org.freedesktop.Notifications",
"Dnd",
value,
)
}
}

View File

@@ -0,0 +1,188 @@
use std::ffi::CStr;
use glam::Affine3A;
use ovr_overlay::{pose::Matrix3x4, settings::SettingsManager, sys::HmdMatrix34_t};
use thiserror::Error;
use crate::backend::{common::BackendError, task::ColorChannel};
pub trait Affine3AConvert {
fn from_affine(affine: &Affine3A) -> Self;
fn to_affine(&self) -> Affine3A;
}
impl Affine3AConvert for Matrix3x4 {
fn from_affine(affine: &Affine3A) -> Self {
Self([
[
affine.matrix3.x_axis.x,
affine.matrix3.y_axis.x,
affine.matrix3.z_axis.x,
affine.translation.x,
],
[
affine.matrix3.x_axis.y,
affine.matrix3.y_axis.y,
affine.matrix3.z_axis.y,
affine.translation.y,
],
[
affine.matrix3.x_axis.z,
affine.matrix3.y_axis.z,
affine.matrix3.z_axis.z,
affine.translation.z,
],
])
}
fn to_affine(&self) -> Affine3A {
Affine3A::from_cols_array_2d(&[
[self.0[0][0], self.0[1][0], self.0[2][0]],
[self.0[0][1], self.0[1][1], self.0[2][1]],
[self.0[0][2], self.0[1][2], self.0[2][2]],
[self.0[0][3], self.0[1][3], self.0[2][3]],
])
}
}
impl Affine3AConvert for HmdMatrix34_t {
fn from_affine(affine: &Affine3A) -> Self {
Self {
m: [
[
affine.matrix3.x_axis.x,
affine.matrix3.y_axis.x,
affine.matrix3.z_axis.x,
affine.translation.x,
],
[
affine.matrix3.x_axis.y,
affine.matrix3.y_axis.y,
affine.matrix3.z_axis.y,
affine.translation.y,
],
[
affine.matrix3.x_axis.z,
affine.matrix3.y_axis.z,
affine.matrix3.z_axis.z,
affine.translation.z,
],
],
}
}
fn to_affine(&self) -> Affine3A {
Affine3A::from_cols_array_2d(&[
[self.m[0][0], self.m[1][0], self.m[2][0]],
[self.m[0][1], self.m[1][1], self.m[2][1]],
[self.m[0][2], self.m[1][2], self.m[2][2]],
[self.m[0][3], self.m[1][3], self.m[2][3]],
])
}
}
#[derive(Error, Debug)]
pub(super) enum OVRError {
#[error("ovr input error: {0}")]
InputError(&'static str),
}
impl From<ovr_overlay::errors::EVRInputError> for OVRError {
fn from(e: ovr_overlay::errors::EVRInputError) -> Self {
Self::InputError(e.description())
}
}
impl From<OVRError> for BackendError {
fn from(e: OVRError) -> Self {
Self::Fatal(anyhow::Error::new(e))
}
}
const STEAMVR_SECTION: &CStr = c"steamvr";
const COLOR_GAIN_CSTR: [&CStr; 3] = [
c"hmdDisplayColorGainR",
c"hmdDisplayColorGainG",
c"hmdDisplayColorGainB",
];
pub(super) fn adjust_gain(
settings: &mut SettingsManager,
ch: ColorChannel,
delta: f32,
) -> Option<()> {
let current = [
settings
.get_float(STEAMVR_SECTION, COLOR_GAIN_CSTR[0])
.ok()?,
settings
.get_float(STEAMVR_SECTION, COLOR_GAIN_CSTR[1])
.ok()?,
settings
.get_float(STEAMVR_SECTION, COLOR_GAIN_CSTR[2])
.ok()?,
];
// prevent user from turning everything black
let mut min = if current[0] + current[1] + current[2] < 0.11 {
0.1
} else {
0.0
};
match ch {
ColorChannel::R => {
settings
.set_float(
STEAMVR_SECTION,
COLOR_GAIN_CSTR[0],
(current[0] + delta).clamp(min, 1.0),
)
.ok()?;
}
ColorChannel::G => {
settings
.set_float(
STEAMVR_SECTION,
COLOR_GAIN_CSTR[1],
(current[1] + delta).clamp(min, 1.0),
)
.ok()?;
}
ColorChannel::B => {
settings
.set_float(
STEAMVR_SECTION,
COLOR_GAIN_CSTR[2],
(current[2] + delta).clamp(min, 1.0),
)
.ok()?;
}
ColorChannel::All => {
min *= 0.3333;
settings
.set_float(
STEAMVR_SECTION,
COLOR_GAIN_CSTR[0],
(current[0] + delta).clamp(min, 1.0),
)
.ok()?;
settings
.set_float(
STEAMVR_SECTION,
COLOR_GAIN_CSTR[1],
(current[1] + delta).clamp(min, 1.0),
)
.ok()?;
settings
.set_float(
STEAMVR_SECTION,
COLOR_GAIN_CSTR[2],
(current[2] + delta).clamp(min, 1.0),
)
.ok()?;
}
}
Some(())
}

View File

@@ -0,0 +1,360 @@
use std::{array, fs::File, io::Write, time::Duration};
use anyhow::bail;
use ovr_overlay::{
input::{ActionHandle, ActionSetHandle, ActiveActionSet, InputManager, InputValueHandle},
sys::{
ETrackedControllerRole, ETrackedDeviceClass, ETrackedDeviceProperty,
ETrackingUniverseOrigin,
},
system::SystemManager,
TrackedDeviceIndex,
};
use crate::{
backend::input::{Haptics, TrackedDevice, TrackedDeviceRole},
config_io,
state::AppState,
};
use super::helpers::{Affine3AConvert, OVRError};
const SET_DEFAULT: &str = "/actions/default";
const INPUT_SOURCES: [&str; 2] = ["/user/hand/left", "/user/hand/right"];
const PATH_POSES: [&str; 2] = [
"/actions/default/in/LeftHand",
"/actions/default/in/RightHand",
];
const PATH_HAPTICS: [&str; 2] = [
"/actions/default/out/HapticsLeft",
"/actions/default/out/HapticsRight",
];
const PATH_ALT_CLICK: &str = "/actions/default/in/AltClick";
const PATH_CLICK_MODIFIER_MIDDLE: &str = "/actions/default/in/ClickModifierMiddle";
const PATH_CLICK_MODIFIER_RIGHT: &str = "/actions/default/in/ClickModifierRight";
const PATH_CLICK: &str = "/actions/default/in/Click";
const PATH_GRAB: &str = "/actions/default/in/Grab";
const PATH_MOVE_MOUSE: &str = "/actions/default/in/MoveMouse";
const PATH_SCROLL: &str = "/actions/default/in/Scroll";
const PATH_SHOW_HIDE: &str = "/actions/default/in/ShowHide";
const PATH_SPACE_DRAG: &str = "/actions/default/in/SpaceDrag";
const PATH_SPACE_ROTATE: &str = "/actions/default/in/SpaceRotate";
const PATH_TOGGLE_DASHBOARD: &str = "/actions/default/in/ToggleDashboard";
const INPUT_ANY: InputValueHandle = InputValueHandle(ovr_overlay::sys::k_ulInvalidInputValueHandle);
pub(super) struct OpenVrInputSource {
hands: [OpenVrHandSource; 2],
set_hnd: ActionSetHandle,
click_hnd: ActionHandle,
grab_hnd: ActionHandle,
scroll_hnd: ActionHandle,
alt_click_hnd: ActionHandle,
show_hide_hnd: ActionHandle,
toggle_dashboard_hnd: ActionHandle,
space_drag_hnd: ActionHandle,
space_rotate_hnd: ActionHandle,
click_modifier_right_hnd: ActionHandle,
click_modifier_middle_hnd: ActionHandle,
move_mouse_hnd: ActionHandle,
}
pub(super) struct OpenVrHandSource {
has_pose: bool,
device: Option<TrackedDeviceIndex>,
input_hnd: InputValueHandle,
pose_hnd: ActionHandle,
haptics_hnd: ActionHandle,
}
impl OpenVrInputSource {
pub fn new(input: &mut InputManager) -> Result<Self, OVRError> {
let set_hnd = input.get_action_set_handle(SET_DEFAULT)?;
let click_hnd = input.get_action_handle(PATH_CLICK)?;
let grab_hnd = input.get_action_handle(PATH_GRAB)?;
let scroll_hnd = input.get_action_handle(PATH_SCROLL)?;
let alt_click_hnd = input.get_action_handle(PATH_ALT_CLICK)?;
let show_hide_hnd = input.get_action_handle(PATH_SHOW_HIDE)?;
let toggle_dashboard_hnd = input.get_action_handle(PATH_TOGGLE_DASHBOARD)?;
let space_drag_hnd = input.get_action_handle(PATH_SPACE_DRAG)?;
let space_rotate_hnd = input.get_action_handle(PATH_SPACE_ROTATE)?;
let click_modifier_right_hnd = input.get_action_handle(PATH_CLICK_MODIFIER_RIGHT)?;
let click_modifier_middle_hnd = input.get_action_handle(PATH_CLICK_MODIFIER_MIDDLE)?;
let move_mouse_hnd = input.get_action_handle(PATH_MOVE_MOUSE)?;
let input_hnd: Vec<InputValueHandle> = INPUT_SOURCES
.iter()
.map(|path| Ok((input.get_input_source_handle(path))?))
.collect::<Result<_, OVRError>>()?;
let pose_hnd: Vec<ActionHandle> = PATH_POSES
.iter()
.map(|path| Ok((input.get_action_handle(path))?))
.collect::<Result<_, OVRError>>()?;
let haptics_hnd: Vec<ActionHandle> = PATH_HAPTICS
.iter()
.map(|path| Ok((input.get_action_handle(path))?))
.collect::<Result<_, OVRError>>()?;
let hands: [OpenVrHandSource; 2] = array::from_fn(|i| OpenVrHandSource {
has_pose: false,
device: None,
input_hnd: input_hnd[i],
pose_hnd: pose_hnd[i],
haptics_hnd: haptics_hnd[i],
});
Ok(Self {
hands,
set_hnd,
click_hnd,
grab_hnd,
scroll_hnd,
alt_click_hnd,
show_hide_hnd,
toggle_dashboard_hnd,
space_drag_hnd,
space_rotate_hnd,
click_modifier_right_hnd,
click_modifier_middle_hnd,
move_mouse_hnd,
})
}
pub fn haptics(&mut self, input: &mut InputManager, hand: usize, haptics: &Haptics) {
let action_handle = self.hands[hand].haptics_hnd;
let _ = input.trigger_haptic_vibration_action(
action_handle,
0.0,
Duration::from_secs_f32(haptics.duration),
haptics.frequency,
haptics.intensity,
INPUT_ANY,
);
}
pub fn update(
&mut self,
universe: ETrackingUniverseOrigin,
input: &mut InputManager,
system: &mut SystemManager,
app: &mut AppState,
) {
let aas = ActiveActionSet(ovr_overlay::sys::VRActiveActionSet_t {
ulActionSet: self.set_hnd.0,
ulRestrictedToDevice: 0,
ulSecondaryActionSet: 0,
unPadding: 0,
nPriority: 0,
});
let _ = input.update_actions(&mut [aas]);
let devices = system.get_device_to_absolute_tracking_pose(universe.clone(), 0.005);
app.input_state.hmd = devices[0].mDeviceToAbsoluteTracking.to_affine();
for i in 0..2 {
let hand = &mut self.hands[i];
let app_hand = &mut app.input_state.pointers[i];
if let Some(device) = hand.device {
app_hand.raw_pose = devices[device.0 as usize]
.mDeviceToAbsoluteTracking
.to_affine();
}
hand.has_pose = false;
let _ = input
.get_pose_action_data_relative_to_now(
hand.pose_hnd,
universe.clone(),
0.005,
INPUT_ANY,
)
.map(|pose| {
app_hand.pose = pose.0.pose.mDeviceToAbsoluteTracking.to_affine();
hand.has_pose = true;
});
app_hand.now.click = input
.get_digital_action_data(self.click_hnd, hand.input_hnd)
.map(|x| x.0.bState)
.unwrap_or(false);
app_hand.now.grab = input
.get_digital_action_data(self.grab_hnd, hand.input_hnd)
.map(|x| x.0.bState)
.unwrap_or(false);
app_hand.now.alt_click = input
.get_digital_action_data(self.alt_click_hnd, hand.input_hnd)
.map(|x| x.0.bState)
.unwrap_or(false);
app_hand.now.show_hide = input
.get_digital_action_data(self.show_hide_hnd, hand.input_hnd)
.map(|x| x.0.bState)
.unwrap_or(false);
app_hand.now.toggle_dashboard = input
.get_digital_action_data(self.toggle_dashboard_hnd, hand.input_hnd)
.map(|x| x.0.bState)
.unwrap_or(false);
app_hand.now.space_drag = input
.get_digital_action_data(self.space_drag_hnd, hand.input_hnd)
.map(|x| x.0.bState)
.unwrap_or(false);
app_hand.now.space_rotate = input
.get_digital_action_data(self.space_rotate_hnd, hand.input_hnd)
.map(|x| x.0.bState)
.unwrap_or(false);
app_hand.now.click_modifier_right = input
.get_digital_action_data(self.click_modifier_right_hnd, hand.input_hnd)
.map(|x| x.0.bState)
.unwrap_or(false);
app_hand.now.click_modifier_middle = input
.get_digital_action_data(self.click_modifier_middle_hnd, hand.input_hnd)
.map(|x| x.0.bState)
.unwrap_or(false);
app_hand.now.move_mouse = input
.get_digital_action_data(self.move_mouse_hnd, hand.input_hnd)
.map(|x| x.0.bState)
.unwrap_or(false);
let scroll = input
.get_analog_action_data(self.scroll_hnd, hand.input_hnd)
.map(|x| (x.0.x, x.0.y))
.unwrap_or((0.0, 0.0));
app_hand.now.scroll_x = scroll.0;
app_hand.now.scroll_y = scroll.1;
}
}
pub fn update_devices(&mut self, system: &mut SystemManager, app: &mut AppState) {
app.input_state.devices.clear();
for idx in 0..TrackedDeviceIndex::MAX {
let device = TrackedDeviceIndex::new(idx as _).unwrap(); // safe
if !system.is_tracked_device_connected(device) {
continue;
}
let class = system.get_tracked_device_class(device);
let role = match class {
ETrackedDeviceClass::TrackedDeviceClass_HMD => TrackedDeviceRole::Hmd,
ETrackedDeviceClass::TrackedDeviceClass_Controller => {
let role = system.get_controller_role_for_tracked_device_index(device);
match role {
ETrackedControllerRole::TrackedControllerRole_LeftHand => {
self.hands[0].device = Some(device);
TrackedDeviceRole::LeftHand
}
ETrackedControllerRole::TrackedControllerRole_RightHand => {
self.hands[1].device = Some(device);
TrackedDeviceRole::RightHand
}
_ => continue,
}
}
ETrackedDeviceClass::TrackedDeviceClass_GenericTracker => {
TrackedDeviceRole::Tracker
}
_ => continue,
};
if let Some(device) = get_tracked_device(system, device, role) {
app.input_state.devices.push(device);
}
}
app.input_state.devices.sort_by(|a, b| {
u8::from(a.soc.is_none())
.cmp(&u8::from(b.soc.is_none()))
.then((a.role as u8).cmp(&(b.role as u8)))
.then(a.soc.unwrap_or(999.).total_cmp(&b.soc.unwrap_or(999.)))
});
}
}
fn get_tracked_device(
system: &mut SystemManager,
index: TrackedDeviceIndex,
role: TrackedDeviceRole,
) -> Option<TrackedDevice> {
let soc = system
.get_tracked_device_property(
index,
ETrackedDeviceProperty::Prop_DeviceBatteryPercentage_Float,
)
.ok();
let charging = if soc.is_some() {
system
.get_tracked_device_property(index, ETrackedDeviceProperty::Prop_DeviceIsCharging_Bool)
.unwrap_or(false)
} else {
false
};
// TODO: cache this
let is_alvr = system
.get_tracked_device_property(
index,
ETrackedDeviceProperty::Prop_TrackingSystemName_String,
)
.map(|x: String| x.contains("ALVR"))
.unwrap_or(false);
if is_alvr {
// don't show ALVR's fake trackers on battery panel
return None;
}
Some(TrackedDevice {
soc,
charging,
role,
})
}
pub fn set_action_manifest(input: &mut InputManager) -> anyhow::Result<()> {
let action_path = config_io::get_config_root().join("actions.json");
if let Err(e) = File::create(&action_path)
.and_then(|mut f| f.write_all(include_bytes!("../../res/actions.json")))
{
log::warn!("Could not write action manifest: {e}");
}
let binding_path = config_io::get_config_root().join("actions_binding_knuckles.json");
if !binding_path.is_file() {
File::create(&binding_path)?
.write_all(include_bytes!("../../res/actions_binding_knuckles.json"))?;
}
let binding_path = config_io::get_config_root().join("actions_binding_vive.json");
if !binding_path.is_file() {
File::create(&binding_path)?
.write_all(include_bytes!("../../res/actions_binding_vive.json"))?;
}
let binding_path = config_io::get_config_root().join("actions_binding_oculus.json");
if !binding_path.is_file() {
File::create(&binding_path)?
.write_all(include_bytes!("../../res/actions_binding_oculus.json"))?;
}
if let Err(e) = input.set_action_manifest(action_path.as_path()) {
bail!("Failed to set action manifest: {}", e);
}
Ok(())
}

View File

@@ -0,0 +1,264 @@
use std::f32::consts::PI;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use ash::vk::SubmitInfo;
use glam::{Affine3A, Vec3, Vec3A, Vec4};
use idmap::IdMap;
use ovr_overlay::overlay::OverlayManager;
use ovr_overlay::sys::ETrackingUniverseOrigin;
use vulkano::{
command_buffer::{
CommandBufferBeginInfo, CommandBufferLevel, CommandBufferUsage, RecordingCommandBuffer,
},
format::Format,
image::view::ImageView,
image::{Image, ImageLayout},
sync::{
fence::{Fence, FenceCreateInfo},
AccessFlags, DependencyInfo, ImageMemoryBarrier, PipelineStages,
},
VulkanObject,
};
use wgui::gfx::WGfx;
use crate::backend::overlay::{
FrameMeta, OverlayData, OverlayRenderer, OverlayState, ShouldRender, SplitOverlayBackend,
Z_ORDER_LINES,
};
use crate::graphics::CommandBuffers;
use crate::state::AppState;
use super::overlay::OpenVrOverlayData;
static LINE_AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(1);
pub(super) struct LinePool {
lines: IdMap<usize, OverlayData<OpenVrOverlayData>>,
view: Arc<ImageView>,
colors: [Vec4; 5],
}
impl LinePool {
pub fn new(graphics: Arc<WGfx>) -> anyhow::Result<Self> {
let mut command_buffer =
graphics.create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
let buf = vec![255; 16];
let texture = command_buffer.upload_image(2, 2, Format::R8G8B8A8_UNORM, &buf)?;
command_buffer.build_and_execute_now()?;
transition_layout(
&graphics,
texture.clone(),
ImageLayout::ShaderReadOnlyOptimal,
ImageLayout::TransferSrcOptimal,
)?
.wait(None)?;
let view = ImageView::new_default(texture)?;
Ok(Self {
lines: IdMap::new(),
view,
colors: [
Vec4::new(1., 1., 1., 1.),
Vec4::new(0., 0.375, 0.5, 1.),
Vec4::new(0.69, 0.188, 0., 1.),
Vec4::new(0.375, 0., 0.5, 1.),
Vec4::new(1., 0., 0., 1.),
],
})
}
pub fn allocate(&mut self) -> usize {
let id = LINE_AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed);
let mut data = OverlayData::<OpenVrOverlayData> {
state: OverlayState {
name: Arc::from(format!("wlx-line{id}")),
show_hide: true,
..Default::default()
},
backend: Box::new(SplitOverlayBackend {
renderer: Box::new(StaticRenderer {
view: self.view.clone(),
}),
..Default::default()
}),
data: OpenVrOverlayData {
width: 0.002,
override_width: true,
image_view: Some(self.view.clone()),
image_dirty: true,
..Default::default()
},
..Default::default()
};
data.state.z_order = Z_ORDER_LINES;
data.state.dirty = true;
self.lines.insert(id, data);
id
}
pub fn draw_from(
&mut self,
id: usize,
mut from: Affine3A,
len: f32,
color: usize,
hmd: &Affine3A,
) {
let rotation = Affine3A::from_axis_angle(Vec3::X, -PI * 0.5);
from.translation += from.transform_vector3a(Vec3A::NEG_Z) * (len * 0.5);
let mut transform = from * rotation * Affine3A::from_scale(Vec3::new(1., len / 0.002, 1.));
let to_hmd = hmd.translation - from.translation;
let sides = [Vec3A::Z, Vec3A::X, Vec3A::NEG_Z, Vec3A::NEG_X];
let rotations = [
Affine3A::IDENTITY,
Affine3A::from_axis_angle(Vec3::Y, PI * 0.5),
Affine3A::from_axis_angle(Vec3::Y, PI * 1.0),
Affine3A::from_axis_angle(Vec3::Y, PI * 1.5),
];
let mut closest = (0, 0.0);
for (i, &side) in sides.iter().enumerate() {
let dot = to_hmd.dot(transform.transform_vector3a(side));
if i == 0 || dot > closest.1 {
closest = (i, dot);
}
}
transform *= rotations[closest.0];
debug_assert!(color < self.colors.len());
self.draw_transform(id, transform, self.colors[color]);
}
fn draw_transform(&mut self, id: usize, transform: Affine3A, color: Vec4) {
if let Some(data) = self.lines.get_mut(id) {
data.state.want_visible = true;
data.state.transform = transform;
data.data.color = color;
} else {
log::warn!("Line {id} does not exist");
}
}
pub fn update(
&mut self,
universe: ETrackingUniverseOrigin,
overlay: &mut OverlayManager,
app: &mut AppState,
) -> anyhow::Result<()> {
for data in self.lines.values_mut() {
data.after_input(overlay, app)?;
if data.state.want_visible {
if data.state.dirty {
data.upload_texture(overlay, &app.gfx);
data.state.dirty = false;
}
data.upload_transform(universe.clone(), overlay);
data.upload_color(overlay);
}
}
Ok(())
}
pub fn mark_dirty(&mut self) {
for data in self.lines.values_mut() {
data.state.dirty = true;
}
}
}
struct StaticRenderer {
view: Arc<ImageView>,
}
impl OverlayRenderer for StaticRenderer {
fn init(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
Ok(())
}
fn pause(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
Ok(())
}
fn resume(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
Ok(())
}
fn should_render(&mut self, _app: &mut AppState) -> anyhow::Result<ShouldRender> {
Ok(ShouldRender::Unable)
}
fn render(
&mut self,
_app: &mut AppState,
_tgt: Arc<ImageView>,
_buf: &mut CommandBuffers,
_alpha: f32,
) -> anyhow::Result<bool> {
Ok(false)
}
fn frame_meta(&mut self) -> Option<FrameMeta> {
Some(FrameMeta {
extent: self.view.image().extent(),
..Default::default()
})
}
}
pub fn transition_layout(
gfx: &WGfx,
image: Arc<Image>,
old_layout: ImageLayout,
new_layout: ImageLayout,
) -> anyhow::Result<Fence> {
let barrier = ImageMemoryBarrier {
src_stages: PipelineStages::ALL_TRANSFER,
src_access: AccessFlags::TRANSFER_WRITE,
dst_stages: PipelineStages::ALL_TRANSFER,
dst_access: AccessFlags::TRANSFER_READ,
old_layout,
new_layout,
subresource_range: image.subresource_range(),
..ImageMemoryBarrier::image(image)
};
let command_buffer = unsafe {
let mut builder = RecordingCommandBuffer::new(
gfx.command_buffer_allocator.clone(),
gfx.queue_gfx.queue_family_index(),
CommandBufferLevel::Primary,
CommandBufferBeginInfo {
usage: CommandBufferUsage::OneTimeSubmit,
inheritance_info: None,
..Default::default()
},
)?;
builder.pipeline_barrier(&DependencyInfo {
image_memory_barriers: smallvec::smallvec![barrier],
..Default::default()
})?;
builder.end()?
};
let fence = Fence::new(gfx.device.clone(), FenceCreateInfo::default())?;
let fns = gfx.device.fns();
unsafe {
(fns.v1_0.queue_submit)(
gfx.queue_gfx.handle(),
1,
[SubmitInfo::default().command_buffers(&[command_buffer.handle()])].as_ptr(),
fence.handle(),
)
}
.result()?;
Ok(fence)
}

View File

@@ -0,0 +1,88 @@
use std::{fs::File, io::Read};
use anyhow::bail;
use json::{array, object};
use ovr_overlay::applications::ApplicationsManager;
use crate::config_io;
const APP_KEY: &str = "galister.wlxoverlay-s";
pub(super) fn install_manifest(app_mgr: &mut ApplicationsManager) -> anyhow::Result<()> {
let manifest_path = config_io::get_config_root().join("wlx-overlay-s.vrmanifest");
let appimage_path = std::env::var("APPIMAGE");
let executable_pathbuf = std::env::current_exe()?;
let executable_path = match appimage_path {
Ok(ref path) => path,
Err(_) => executable_pathbuf
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid executable path"))?,
};
if app_mgr.is_application_installed(APP_KEY) == Ok(true) {
if let Ok(mut file) = File::open(&manifest_path) {
let mut buf = String::new();
if file.read_to_string(&mut buf).is_ok() {
let manifest: json::JsonValue = json::parse(&buf)?;
if manifest["applications"][0]["binary_path_linux"] == executable_path {
log::info!("Manifest already up to date");
return Ok(());
}
}
}
}
let manifest = object! {
source: "builtin",
applications: array![
object! {
app_key: APP_KEY,
launch_type: "binary",
binary_path_linux: executable_path,
is_dashboard_overlay: true,
strings: object!{
"en_us": object!{
name: "WlxOverlay-S",
description: "A lightweight Wayland desktop overlay for OpenVR/OpenXR",
},
},
},
],
};
let Ok(mut file) = File::create(&manifest_path) else {
bail!("Failed to create manifest file at {:?}", manifest_path);
};
if let Err(e) = manifest.write(&mut file) {
bail!(
"Failed to write manifest file at {:?}: {:?}",
manifest_path,
e
);
}
if let Err(e) = app_mgr.add_application_manifest(&manifest_path, false) {
bail!("Failed to add manifest to OpenVR: {}", e.description());
}
if let Err(e) = app_mgr.set_application_auto_launch(APP_KEY, true) {
bail!("Failed to set auto launch: {}", e.description());
}
Ok(())
}
pub(super) fn uninstall_manifest(app_mgr: &mut ApplicationsManager) -> anyhow::Result<()> {
let manifest_path = config_io::get_config_root().join("wlx-overlay-s.vrmanifest");
if app_mgr.is_application_installed(APP_KEY) == Ok(true) {
if let Err(e) = app_mgr.remove_application_manifest(&manifest_path) {
bail!("Failed to remove manifest from OpenVR: {}", e.description());
}
log::info!("Uninstalled manifest");
}
Ok(())
}

View File

@@ -0,0 +1,389 @@
use std::{
collections::VecDeque,
ops::Add,
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc,
},
time::{Duration, Instant},
};
use anyhow::{anyhow, Result};
use ovr_overlay::{
sys::{ETrackedDeviceProperty, EVRApplicationType, EVREventType},
TrackedDeviceIndex,
};
use vulkano::{device::physical::PhysicalDevice, Handle, VulkanObject};
use crate::{
backend::{
common::{BackendError, OverlayContainer},
input::interact,
notifications::NotificationManager,
openvr::{
helpers::adjust_gain,
input::{set_action_manifest, OpenVrInputSource},
lines::LinePool,
manifest::{install_manifest, uninstall_manifest},
overlay::OpenVrOverlayData,
},
overlay::{OverlayData, ShouldRender},
task::{SystemTask, TaskType},
},
graphics::{init_openvr_graphics, CommandBuffers},
overlays::{
toast::{Toast, ToastTopic},
watch::{watch_fade, WATCH_NAME},
},
state::AppState,
};
#[cfg(feature = "wayvr")]
use crate::{backend::wayvr::WayVRAction, overlays::wayvr::wayvr_action};
pub mod helpers;
pub mod input;
pub mod lines;
pub mod manifest;
pub mod overlay;
pub mod playspace;
static FRAME_COUNTER: AtomicUsize = AtomicUsize::new(0);
pub fn openvr_uninstall() {
let app_type = EVRApplicationType::VRApplication_Overlay;
let Ok(context) = ovr_overlay::Context::init(app_type) else {
log::error!("Uninstall failed: could not reach OpenVR");
return;
};
let mut app_mgr = context.applications_mngr();
let _ = uninstall_manifest(&mut app_mgr);
}
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
pub fn openvr_run(
running: Arc<AtomicBool>,
show_by_default: bool,
headless: bool,
) -> Result<(), BackendError> {
let app_type = EVRApplicationType::VRApplication_Overlay;
let Ok(context) = ovr_overlay::Context::init(app_type) else {
log::warn!("Will not use OpenVR: Context init failed");
return Err(BackendError::NotSupported);
};
log::info!("Using OpenVR runtime");
let mut app_mgr = context.applications_mngr();
let mut input_mgr = context.input_mngr();
let mut system_mgr = context.system_mngr();
let mut overlay_mgr = context.overlay_mngr();
let mut settings_mgr = context.settings_mngr();
let mut chaperone_mgr = context.chaperone_setup_mngr();
let mut compositor_mgr = context.compositor_mngr();
let device_extensions_fn = |device: &PhysicalDevice| {
let names = compositor_mgr.get_vulkan_device_extensions_required(device.handle().as_raw());
names.iter().map(std::string::String::as_str).collect()
};
let mut compositor_mgr = context.compositor_mngr();
let instance_extensions = {
let names = compositor_mgr.get_vulkan_instance_extensions_required();
names.iter().map(std::string::String::as_str).collect()
};
let mut state = {
let (gfx, gfx_extras) = init_openvr_graphics(instance_extensions, device_extensions_fn)?;
AppState::from_graphics(gfx, gfx_extras)?
};
if show_by_default {
state.tasks.enqueue_at(
TaskType::System(SystemTask::ShowHide),
Instant::now().add(Duration::from_secs(1)),
);
}
if let Ok(ipd) = system_mgr.get_tracked_device_property::<f32>(
TrackedDeviceIndex::HMD,
ETrackedDeviceProperty::Prop_UserIpdMeters_Float,
) {
state.input_state.ipd = (ipd * 10000.0).round() * 0.1;
log::info!("IPD: {:.1} mm", state.input_state.ipd);
}
let _ = install_manifest(&mut app_mgr);
let mut overlays = OverlayContainer::<OpenVrOverlayData>::new(&mut state, headless)?;
let mut notifications = NotificationManager::new();
notifications.run_dbus();
notifications.run_udp();
let mut playspace = playspace::PlayspaceMover::new();
playspace.playspace_changed(&mut compositor_mgr, &mut chaperone_mgr);
set_action_manifest(&mut input_mgr)?;
let mut input_source = OpenVrInputSource::new(&mut input_mgr)?;
let Ok(refresh_rate) = system_mgr.get_tracked_device_property::<f32>(
TrackedDeviceIndex::HMD,
ETrackedDeviceProperty::Prop_DisplayFrequency_Float,
) else {
return Err(BackendError::Fatal(anyhow!(
"Failed to get HMD refresh rate"
)));
};
log::info!("HMD running @ {refresh_rate} Hz");
let watch_id = overlays.get_by_name(WATCH_NAME).unwrap().state.id; // want panic
// want at least half refresh rate
let frame_timeout = 2 * (1000.0 / refresh_rate).floor() as u32;
let mut next_device_update = Instant::now();
let mut due_tasks = VecDeque::with_capacity(4);
let mut lines = LinePool::new(state.gfx.clone())?;
let pointer_lines = [lines.allocate(), lines.allocate()];
'main_loop: loop {
let _ = overlay_mgr.wait_frame_sync(frame_timeout);
if !running.load(Ordering::Relaxed) {
log::warn!("Received shutdown signal.");
break 'main_loop;
}
let cur_frame = FRAME_COUNTER.fetch_add(1, Ordering::Relaxed);
while let Some(event) = system_mgr.poll_next_event() {
match event.event_type {
EVREventType::VREvent_Quit => {
log::warn!("Received quit event, shutting down.");
break 'main_loop;
}
EVREventType::VREvent_TrackedDeviceActivated
| EVREventType::VREvent_TrackedDeviceDeactivated
| EVREventType::VREvent_TrackedDeviceUpdated => {
next_device_update = Instant::now();
}
EVREventType::VREvent_SeatedZeroPoseReset
| EVREventType::VREvent_StandingZeroPoseReset
| EVREventType::VREvent_ChaperoneUniverseHasChanged
| EVREventType::VREvent_SceneApplicationChanged => {
playspace.playspace_changed(&mut compositor_mgr, &mut chaperone_mgr);
}
EVREventType::VREvent_IpdChanged => {
if let Ok(ipd) = system_mgr.get_tracked_device_property::<f32>(
TrackedDeviceIndex::HMD,
ETrackedDeviceProperty::Prop_UserIpdMeters_Float,
) {
let ipd = (ipd * 10000.0).round() * 0.1;
if (ipd - state.input_state.ipd).abs() > 0.05 {
log::info!("IPD: {:.1} mm -> {:.1} mm", state.input_state.ipd, ipd);
Toast::new(
ToastTopic::IpdChange,
"IPD".into(),
format!("{ipd:.1} mm").into(),
)
.submit(&mut state);
}
state.input_state.ipd = ipd;
}
}
_ => {}
}
}
if next_device_update <= Instant::now() {
input_source.update_devices(&mut system_mgr, &mut state);
next_device_update = Instant::now() + Duration::from_secs(30);
}
notifications.submit_pending(&mut state);
state.tasks.retrieve_due(&mut due_tasks);
let mut removed_overlays = overlays.update(&mut state)?;
for o in &mut removed_overlays {
o.destroy(&mut overlay_mgr);
}
while let Some(task) = due_tasks.pop_front() {
match task {
TaskType::Overlay(sel, f) => {
if let Some(o) = overlays.mut_by_selector(&sel) {
f(&mut state, &mut o.state);
} else {
log::warn!("Overlay not found for task: {sel:?}");
}
}
TaskType::CreateOverlay(sel, f) => {
let None = overlays.mut_by_selector(&sel) else {
continue;
};
let Some((mut state, backend)) = f(&mut state) else {
continue;
};
state.birthframe = cur_frame;
overlays.add(OverlayData {
state,
backend,
..Default::default()
});
}
TaskType::DropOverlay(sel) => {
if let Some(o) = overlays.mut_by_selector(&sel) {
if o.state.birthframe < cur_frame {
o.destroy(&mut overlay_mgr);
overlays.remove_by_selector(&sel);
}
}
}
TaskType::System(task) => match task {
SystemTask::ColorGain(channel, value) => {
let _ = adjust_gain(&mut settings_mgr, channel, value);
}
SystemTask::FixFloor => {
playspace.fix_floor(&mut chaperone_mgr, &state.input_state);
}
SystemTask::ResetPlayspace => {
playspace.reset_offset(&mut chaperone_mgr, &state.input_state);
}
SystemTask::ShowHide => {
overlays.show_hide(&mut state);
}
},
#[cfg(feature = "wayvr")]
TaskType::WayVR(action) => {
wayvr_action(&mut state, &mut overlays, &action);
}
}
}
let universe = playspace.get_universe();
state.input_state.pre_update();
input_source.update(
universe.clone(),
&mut input_mgr,
&mut system_mgr,
&mut state,
);
state.input_state.post_update(&state.session);
if state
.input_state
.pointers
.iter()
.any(|p| p.now.show_hide && !p.before.show_hide)
{
lines.mark_dirty(); // workaround to prevent lines from not showing
overlays.show_hide(&mut state);
}
#[cfg(feature = "wayvr")]
if state
.input_state
.pointers
.iter()
.any(|p| p.now.toggle_dashboard && !p.before.toggle_dashboard)
{
wayvr_action(&mut state, &mut overlays, &WayVRAction::ToggleDashboard);
}
overlays
.iter_mut()
.for_each(|o| o.state.auto_movement(&mut state));
watch_fade(&mut state, overlays.mut_by_id(watch_id).unwrap()); // want panic
playspace.update(&mut chaperone_mgr, &mut overlays, &state);
let lengths_haptics = interact(&mut overlays, &mut state);
for (idx, (len, haptics)) in lengths_haptics.iter().enumerate() {
lines.draw_from(
pointer_lines[idx],
state.input_state.pointers[idx].pose,
*len,
state.input_state.pointers[idx].interaction.mode as usize + 1,
&state.input_state.hmd,
);
if let Some(haptics) = haptics {
input_source.haptics(&mut input_mgr, idx, haptics);
}
}
state.hid_provider.commit();
let mut buffers = CommandBuffers::default();
lines.update(universe.clone(), &mut overlay_mgr, &mut state)?;
for o in overlays.iter_mut() {
o.after_input(&mut overlay_mgr, &mut state)?;
}
#[cfg(feature = "osc")]
if let Some(ref mut sender) = state.osc_sender {
let _ = sender.send_params(&overlays, &state.input_state.devices);
}
#[cfg(feature = "wayvr")]
if let Err(e) =
crate::overlays::wayvr::tick_events::<OpenVrOverlayData>(&mut state, &mut overlays)
{
log::error!("WayVR tick_events failed: {e:?}");
}
log::trace!("Rendering frame");
for o in overlays.iter_mut() {
if o.state.want_visible {
let ShouldRender::Should = o.should_render(&mut state)? else {
continue;
};
if !o.ensure_image_allocated(&mut state)? {
continue;
}
o.data.image_dirty = o.render(
&mut state,
o.data.image_view.as_ref().unwrap().clone(),
&mut buffers,
1.0, // alpha is instead set using OVR API
)?;
}
}
log::trace!("Rendering overlays");
if let Some(mut future) = buffers.execute_now(state.gfx.queue_gfx.clone())? {
if let Err(e) = future.flush() {
return Err(BackendError::Fatal(e.into()));
}
future.cleanup_finished();
}
overlays
.iter_mut()
.for_each(|o| o.after_render(universe.clone(), &mut overlay_mgr, &state.gfx));
#[cfg(feature = "wayvr")]
if let Some(wayvr) = &state.wayvr {
wayvr.borrow_mut().data.tick_finish()?;
}
// chaperone
// close font handles?
}
log::warn!("OpenVR shutdown");
// context.shutdown() called by Drop
Ok(())
}

View File

@@ -0,0 +1,299 @@
use core::f32;
use std::sync::Arc;
use glam::Vec4;
use ovr_overlay::{
overlay::{OverlayHandle, OverlayManager},
pose::Matrix3x4,
sys::{ETrackingUniverseOrigin, VRVulkanTextureData_t},
};
use vulkano::{
image::{view::ImageView, ImageUsage},
Handle, VulkanObject,
};
use wgui::gfx::WGfx;
use crate::{backend::overlay::OverlayData, state::AppState};
use super::helpers::Affine3AConvert;
#[derive(Default)]
pub(super) struct OpenVrOverlayData {
pub(super) handle: Option<OverlayHandle>,
pub(super) visible: bool,
pub(super) color: Vec4,
pub(crate) width: f32,
pub(super) override_width: bool,
pub(super) image_view: Option<Arc<ImageView>>,
pub(super) image_dirty: bool,
}
impl OverlayData<OpenVrOverlayData> {
pub(super) fn initialize(
&mut self,
overlay: &mut OverlayManager,
app: &mut AppState,
) -> anyhow::Result<OverlayHandle> {
let key = format!("wlx-{}", self.state.name);
log::debug!("Create overlay with key: {}", &key);
let handle = match overlay.create_overlay(&key, &key) {
Ok(handle) => handle,
Err(e) => {
panic!("Failed to create overlay: {e}");
}
};
log::debug!("{}: initialize", self.state.name);
self.data.handle = Some(handle);
self.data.color = Vec4::ONE;
self.init(app)?;
if self.data.width < f32::EPSILON {
self.data.width = 1.0;
}
self.upload_width(overlay);
self.upload_color(overlay);
self.upload_alpha(overlay);
self.upload_curvature(overlay);
self.upload_sort_order(overlay);
Ok(handle)
}
pub(super) fn ensure_image_allocated(&mut self, app: &mut AppState) -> anyhow::Result<bool> {
if self.data.image_view.is_some() {
return Ok(true);
}
let Some(meta) = self.backend.frame_meta() else {
return Ok(false);
};
let image = app.gfx.new_image(
meta.extent[0],
meta.extent[1],
app.gfx.surface_format,
ImageUsage::TRANSFER_SRC | ImageUsage::COLOR_ATTACHMENT | ImageUsage::SAMPLED,
)?;
self.data.image_view = Some(ImageView::new_default(image)?);
Ok(true)
}
pub(super) fn after_input(
&mut self,
overlay: &mut OverlayManager,
app: &mut AppState,
) -> anyhow::Result<()> {
if self.state.want_visible && !self.data.visible {
self.show_internal(overlay, app)?;
} else if !self.state.want_visible && self.data.visible {
self.hide_internal(overlay, app)?;
}
Ok(())
}
pub(super) fn after_render(
&mut self,
universe: ETrackingUniverseOrigin,
overlay: &mut OverlayManager,
graphics: &WGfx,
) {
if self.data.visible {
if self.state.dirty {
self.upload_curvature(overlay);
self.upload_transform(universe, overlay);
self.upload_alpha(overlay);
self.state.dirty = false;
}
self.upload_texture(overlay, graphics);
}
}
fn show_internal(
&mut self,
overlay: &mut OverlayManager,
app: &mut AppState,
) -> anyhow::Result<()> {
let handle = match self.data.handle {
Some(handle) => handle,
None => self.initialize(overlay, app)?,
};
log::debug!("{}: show", self.state.name);
if let Err(e) = overlay.set_visibility(handle, true) {
log::error!("{}: Failed to show overlay: {}", self.state.name, e);
}
self.data.visible = true;
self.backend.resume(app)
}
fn hide_internal(
&mut self,
overlay: &mut OverlayManager,
app: &mut AppState,
) -> anyhow::Result<()> {
let Some(handle) = self.data.handle else {
return Ok(());
};
log::debug!("{}: hide", self.state.name);
if let Err(e) = overlay.set_visibility(handle, false) {
log::error!("{}: Failed to hide overlay: {}", self.state.name, e);
}
self.data.visible = false;
self.backend.pause(app)
}
pub(super) fn upload_alpha(&self, overlay: &mut OverlayManager) {
let Some(handle) = self.data.handle else {
log::debug!("{}: No overlay handle", self.state.name);
return;
};
if let Err(e) = overlay.set_opacity(handle, self.state.alpha) {
log::error!("{}: Failed to set overlay alpha: {}", self.state.name, e);
}
}
pub(super) fn upload_color(&self, overlay: &mut OverlayManager) {
let Some(handle) = self.data.handle else {
log::debug!("{}: No overlay handle", self.state.name);
return;
};
if let Err(e) = overlay.set_tint(
handle,
ovr_overlay::ColorTint {
r: self.data.color.x,
g: self.data.color.y,
b: self.data.color.z,
a: self.data.color.w,
},
) {
log::error!("{}: Failed to set overlay tint: {}", self.state.name, e);
}
}
fn upload_width(&self, overlay: &mut OverlayManager) {
let Some(handle) = self.data.handle else {
log::debug!("{}: No overlay handle", self.state.name);
return;
};
if let Err(e) = overlay.set_width(handle, self.data.width) {
log::error!("{}: Failed to set overlay width: {}", self.state.name, e);
}
}
fn upload_curvature(&self, overlay: &mut OverlayManager) {
let Some(handle) = self.data.handle else {
log::debug!("{}: No overlay handle", self.state.name);
return;
};
if let Err(e) = overlay.set_curvature(handle, self.state.curvature.unwrap_or(0.0)) {
log::error!(
"{}: Failed to set overlay curvature: {}",
self.state.name,
e
);
}
}
fn upload_sort_order(&self, overlay: &mut OverlayManager) {
let Some(handle) = self.data.handle else {
log::debug!("{}: No overlay handle", self.state.name);
return;
};
if let Err(e) = overlay.set_sort_order(handle, self.state.z_order) {
log::error!("{}: Failed to set overlay z order: {}", self.state.name, e);
}
}
pub(super) fn upload_transform(
&mut self,
universe: ETrackingUniverseOrigin,
overlay: &mut OverlayManager,
) {
let Some(handle) = self.data.handle else {
log::debug!("{}: No overlay handle", self.state.name);
return;
};
let effective = self.state.transform
* self
.backend
.frame_meta()
.map(|f| f.transform)
.unwrap_or_default();
let transform = Matrix3x4::from_affine(&effective);
if let Err(e) = overlay.set_transform_absolute(handle, universe, &transform) {
log::error!(
"{}: Failed to set overlay transform: {}",
self.state.name,
e
);
}
}
pub(super) fn upload_texture(&mut self, overlay: &mut OverlayManager, graphics: &WGfx) {
let Some(handle) = self.data.handle else {
log::debug!("{}: No overlay handle", self.state.name);
return;
};
let Some(view) = self.data.image_view.as_ref() else {
log::debug!("{}: Not rendered", self.state.name);
return;
};
if !self.data.image_dirty {
return;
}
self.data.image_dirty = false;
let image = view.image().clone();
let dimensions = image.extent();
if !self.data.override_width {
let new_width = ((dimensions[0] as f32) / (dimensions[1] as f32)).min(1.0);
if (new_width - self.data.width).abs() > f32::EPSILON {
log::info!("{}: New width {}", self.state.name, new_width);
self.data.width = new_width;
self.upload_width(overlay);
}
}
let raw_image = image.handle().as_raw();
let format = image.format();
let mut texture = VRVulkanTextureData_t {
m_nImage: raw_image,
m_nFormat: format as _,
m_nWidth: dimensions[0],
m_nHeight: dimensions[1],
m_nSampleCount: image.samples() as u32,
m_pDevice: graphics.device.handle().as_raw() as *mut _,
m_pPhysicalDevice: graphics.device.physical_device().handle().as_raw() as *mut _,
m_pInstance: graphics.instance.handle().as_raw() as *mut _,
m_pQueue: graphics.queue_gfx.handle().as_raw() as *mut _,
m_nQueueFamilyIndex: graphics.queue_gfx.queue_family_index(),
};
log::trace!(
"{}: UploadTex {:?}, {}x{}, {:?}",
self.state.name,
format,
texture.m_nWidth,
texture.m_nHeight,
image.usage()
);
if let Err(e) = overlay.set_image_vulkan(handle, &mut texture) {
log::error!("{}: Failed to set overlay texture: {}", self.state.name, e);
}
}
pub(super) fn destroy(&mut self, overlay: &mut OverlayManager) {
if let Some(handle) = self.data.handle {
log::debug!("{}: destroy", self.state.name);
if let Err(e) = overlay.destroy_overlay(handle) {
log::error!("{}: Failed to destroy overlay: {}", self.state.name, e);
}
}
}
}

View File

@@ -0,0 +1,302 @@
use glam::{Affine3A, Quat, Vec3, Vec3A};
use ovr_overlay::{
chaperone_setup::ChaperoneSetupManager,
compositor::CompositorManager,
sys::{EChaperoneConfigFile, ETrackingUniverseOrigin, HmdMatrix34_t},
};
use crate::{
backend::{common::OverlayContainer, input::InputState},
state::AppState,
};
use super::{helpers::Affine3AConvert, overlay::OpenVrOverlayData};
struct MoverData<T> {
pose: Affine3A,
hand: usize,
hand_pose: T,
}
pub(super) struct PlayspaceMover {
universe: ETrackingUniverseOrigin,
drag: Option<MoverData<Vec3A>>,
rotate: Option<MoverData<Quat>>,
}
impl PlayspaceMover {
pub const fn new() -> Self {
Self {
universe: ETrackingUniverseOrigin::TrackingUniverseRawAndUncalibrated,
drag: None,
rotate: None,
}
}
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
pub fn update(
&mut self,
chaperone_mgr: &mut ChaperoneSetupManager,
overlays: &mut OverlayContainer<OpenVrOverlayData>,
state: &AppState,
) {
let universe = self.universe.clone();
if let Some(data) = self.rotate.as_mut() {
let pointer = &state.input_state.pointers[data.hand];
if !pointer.now.space_rotate {
self.rotate = None;
log::info!("End space rotate");
return;
}
let new_hand =
Quat::from_affine3(&(data.pose * state.input_state.pointers[data.hand].raw_pose));
let dq = new_hand * data.hand_pose.conjugate();
let rel_y = f32::atan2(
2.0 * dq.y.mul_add(dq.w, dq.x * dq.z),
2.0f32.mul_add(dq.w.mul_add(dq.w, dq.x * dq.x), -1.0),
);
let mut space_transform = Affine3A::from_rotation_y(rel_y);
let offset = (space_transform.transform_vector3a(state.input_state.hmd.translation)
- state.input_state.hmd.translation)
* -1.0;
let mut overlay_transform = Affine3A::from_rotation_y(-rel_y);
overlay_transform.translation = offset;
space_transform.translation = offset;
overlays.iter_mut().for_each(|overlay| {
if overlay.state.grabbable {
overlay.state.dirty = true;
overlay.state.transform.translation =
overlay_transform.transform_point3a(overlay.state.transform.translation);
}
});
data.pose *= space_transform;
data.hand_pose = new_hand;
if self.universe == ETrackingUniverseOrigin::TrackingUniverseStanding {
apply_chaperone_transform(space_transform.inverse(), chaperone_mgr);
}
set_working_copy(&universe, chaperone_mgr, &data.pose);
chaperone_mgr.commit_working_copy(EChaperoneConfigFile::EChaperoneConfigFile_Live);
} else {
for (i, pointer) in state.input_state.pointers.iter().enumerate() {
if pointer.now.space_rotate {
let Some(mat) = get_working_copy(&universe, chaperone_mgr) else {
log::warn!("Can't space rotate - failed to get zero pose");
return;
};
let hand_pose = Quat::from_affine3(&(mat * pointer.raw_pose));
self.rotate = Some(MoverData {
pose: mat,
hand: i,
hand_pose,
});
self.drag = None;
log::info!("Start space rotate");
return;
}
}
}
if let Some(data) = self.drag.as_mut() {
let pointer = &state.input_state.pointers[data.hand];
if !pointer.now.space_drag {
self.drag = None;
log::info!("End space drag");
return;
}
let new_hand = data
.pose
.transform_point3a(state.input_state.pointers[data.hand].raw_pose.translation);
let relative_pos =
(new_hand - data.hand_pose) * state.session.config.space_drag_multiplier;
if relative_pos.length_squared() > 1000.0 {
log::warn!("Space drag too fast, ignoring");
return;
}
let overlay_offset = data.pose.inverse().transform_vector3a(relative_pos) * -1.0;
overlays.iter_mut().for_each(|overlay| {
if overlay.state.grabbable {
overlay.state.dirty = true;
overlay.state.transform.translation += overlay_offset;
}
});
data.pose.translation += relative_pos;
data.hand_pose = new_hand;
if self.universe == ETrackingUniverseOrigin::TrackingUniverseStanding {
apply_chaperone_offset(overlay_offset, chaperone_mgr);
}
set_working_copy(&universe, chaperone_mgr, &data.pose);
chaperone_mgr.commit_working_copy(EChaperoneConfigFile::EChaperoneConfigFile_Live);
} else {
for (i, pointer) in state.input_state.pointers.iter().enumerate() {
if pointer.now.space_drag {
let Some(mat) = get_working_copy(&universe, chaperone_mgr) else {
log::warn!("Can't space drag - failed to get zero pose");
return;
};
let hand_pos = mat.transform_point3a(pointer.raw_pose.translation);
self.drag = Some(MoverData {
pose: mat,
hand: i,
hand_pose: hand_pos,
});
self.rotate = None;
log::info!("Start space drag");
return;
}
}
}
}
pub fn reset_offset(&mut self, chaperone_mgr: &mut ChaperoneSetupManager, input: &InputState) {
let mut height = 1.6;
if let Some(mat) = get_working_copy(&self.universe, chaperone_mgr) {
height = input.hmd.translation.y - mat.translation.y;
if self.universe == ETrackingUniverseOrigin::TrackingUniverseStanding {
apply_chaperone_transform(mat, chaperone_mgr);
}
}
let xform = if self.universe == ETrackingUniverseOrigin::TrackingUniverseSeated {
Affine3A::from_translation(Vec3::NEG_Y * height)
} else {
Affine3A::IDENTITY
};
set_working_copy(&self.universe, chaperone_mgr, &xform);
chaperone_mgr.commit_working_copy(EChaperoneConfigFile::EChaperoneConfigFile_Live);
if self.drag.is_some() {
log::info!("Space drag interrupted by manual reset");
self.drag = None;
}
if self.rotate.is_some() {
log::info!("Space rotate interrupted by manual reset");
self.rotate = None;
}
}
pub fn fix_floor(&mut self, chaperone_mgr: &mut ChaperoneSetupManager, input: &InputState) {
let y1 = input.pointers[0].pose.translation.y;
let y2 = input.pointers[1].pose.translation.y;
let Some(mut mat) = get_working_copy(&self.universe, chaperone_mgr) else {
log::warn!("Can't fix floor - failed to get zero pose");
return;
};
let offset = y1.min(y2) - 0.03;
mat.translation.y += offset;
set_working_copy(&self.universe, chaperone_mgr, &mat);
chaperone_mgr.commit_working_copy(EChaperoneConfigFile::EChaperoneConfigFile_Live);
if self.drag.is_some() {
log::info!("Space drag interrupted by fix floor");
self.drag = None;
}
if self.rotate.is_some() {
log::info!("Space rotate interrupted by fix floor");
self.rotate = None;
}
}
pub fn playspace_changed(
&mut self,
compositor_mgr: &mut CompositorManager,
_chaperone_mgr: &mut ChaperoneSetupManager,
) {
let new_universe = compositor_mgr.get_tracking_space();
if new_universe != self.universe {
log::info!(
"Playspace changed: {} -> {}",
universe_str(&self.universe),
universe_str(&new_universe)
);
self.universe = new_universe;
}
if self.drag.is_some() {
log::info!("Space drag interrupted by external change");
self.drag = None;
}
if self.rotate.is_some() {
log::info!("Space rotate interrupted by external change");
self.rotate = None;
}
}
pub fn get_universe(&self) -> ETrackingUniverseOrigin {
self.universe.clone()
}
}
const fn universe_str(universe: &ETrackingUniverseOrigin) -> &'static str {
match universe {
ETrackingUniverseOrigin::TrackingUniverseSeated => "Seated",
ETrackingUniverseOrigin::TrackingUniverseStanding => "Standing",
ETrackingUniverseOrigin::TrackingUniverseRawAndUncalibrated => "Raw",
}
}
fn get_working_copy(
universe: &ETrackingUniverseOrigin,
chaperone_mgr: &mut ChaperoneSetupManager,
) -> Option<Affine3A> {
chaperone_mgr.revert_working_copy();
let mat = match universe {
ETrackingUniverseOrigin::TrackingUniverseStanding => {
chaperone_mgr.get_working_standing_zero_pose_to_raw_tracking_pose()
}
_ => chaperone_mgr.get_working_seated_zero_pose_to_raw_tracking_pose(),
};
mat.map(|m| m.to_affine())
}
fn set_working_copy(
universe: &ETrackingUniverseOrigin,
chaperone_mgr: &mut ChaperoneSetupManager,
mat: &Affine3A,
) {
let mat = HmdMatrix34_t::from_affine(mat);
match universe {
ETrackingUniverseOrigin::TrackingUniverseStanding => {
chaperone_mgr.set_working_standing_zero_pose_to_raw_tracking_pose(&mat);
}
_ => chaperone_mgr.set_working_seated_zero_pose_to_raw_tracking_pose(&mat),
}
}
fn apply_chaperone_offset(offset: Vec3A, chaperone_mgr: &mut ChaperoneSetupManager) {
let mut quads = chaperone_mgr.get_live_collision_bounds_info();
for quad in &mut quads {
quad.vCorners.iter_mut().for_each(|corner| {
corner.v[0] += offset.x;
corner.v[2] += offset.z;
});
}
chaperone_mgr.set_working_collision_bounds_info(quads.as_mut_slice());
}
fn apply_chaperone_transform(transform: Affine3A, chaperone_mgr: &mut ChaperoneSetupManager) {
let mut quads = chaperone_mgr.get_live_collision_bounds_info();
for quad in &mut quads {
quad.vCorners.iter_mut().for_each(|corner| {
let coord = transform.transform_point3a(Vec3A::from_slice(&corner.v));
corner.v[0] = coord.x;
corner.v[2] = coord.z;
});
}
chaperone_mgr.set_working_collision_bounds_info(quads.as_mut_slice());
}

View File

@@ -0,0 +1,73 @@
use libmonado::{ClientState, Monado};
use log::{info, warn};
use crate::{backend::overlay::OverlayID, state::AppState};
pub(super) struct InputBlocker {
hovered_last_frame: bool,
}
impl InputBlocker {
pub const fn new() -> Self {
Self {
hovered_last_frame: false,
}
}
pub fn update(&mut self, state: &AppState, watch_id: OverlayID, monado: &mut Monado) {
if !state.session.config.block_game_input {
return;
}
let any_hovered = state.input_state.pointers.iter().any(|p| {
p.interaction.hovered_id.is_some_and(|id| {
id != watch_id || !state.session.config.block_game_input_ignore_watch
})
});
match (any_hovered, self.hovered_last_frame) {
(true, false) => {
info!("Blocking input");
set_clients_io_active(monado, false);
}
(false, true) => {
info!("Unblocking input");
set_clients_io_active(monado, true);
}
_ => {}
}
self.hovered_last_frame = any_hovered;
}
}
fn set_clients_io_active(monado: &mut Monado, active: bool) {
match monado.clients() {
Ok(clients) => {
for mut client in clients {
let name = match client.name() {
Ok(n) => n,
Err(e) => {
warn!("Failed to get client name: {e}");
continue;
}
};
let state = match client.state() {
Ok(s) => s,
Err(e) => {
warn!("Failed to get client state: {e}");
continue;
}
};
if name != "wlx-overlay-s" && state.contains(ClientState::ClientSessionVisible) {
if let Err(e) = client.set_io_active(active) {
warn!("Failed to set io active for client: {e}");
}
}
}
}
Err(e) => warn!("Failed to get clients from Monado: {e}"),
}
}

View File

@@ -0,0 +1,190 @@
use anyhow::{bail, ensure};
use glam::{Affine3A, Quat, Vec3, Vec3A};
use openxr::{self as xr, SessionCreateFlags, Version};
use xr::OverlaySessionCreateFlagsEXTX;
pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
let entry = xr::Entry::linked();
let Ok(available_extensions) = entry.enumerate_extensions() else {
bail!("Failed to enumerate OpenXR extensions.");
};
ensure!(
available_extensions.khr_vulkan_enable2,
"Missing KHR_vulkan_enable2 extension."
);
ensure!(
available_extensions.extx_overlay,
"Missing EXTX_overlay extension."
);
let mut enabled_extensions = xr::ExtensionSet::default();
enabled_extensions.khr_vulkan_enable2 = true;
enabled_extensions.extx_overlay = true;
if available_extensions.khr_binding_modification && available_extensions.ext_dpad_binding {
enabled_extensions.khr_binding_modification = true;
enabled_extensions.ext_dpad_binding = true;
} else {
log::warn!("Missing EXT_dpad_binding extension.");
}
if available_extensions.ext_hp_mixed_reality_controller {
enabled_extensions.ext_hp_mixed_reality_controller = true;
} else {
log::warn!("Missing EXT_hp_mixed_reality_controller extension.");
}
if available_extensions.khr_composition_layer_cylinder {
enabled_extensions.khr_composition_layer_cylinder = true;
} else {
log::warn!("Missing EXT_composition_layer_cylinder extension.");
}
if available_extensions.khr_composition_layer_equirect2 {
enabled_extensions.khr_composition_layer_equirect2 = true;
} else {
log::warn!("Missing EXT_composition_layer_equirect2 extension.");
}
if available_extensions
.other
.contains(&"XR_MNDX_system_buttons".to_owned())
{
enabled_extensions
.other
.push("XR_MNDX_system_buttons".to_owned());
}
//#[cfg(not(debug_assertions))]
let layers = [];
//#[cfg(debug_assertions)]
//let layers = [
// "XR_APILAYER_LUNARG_api_dump",
// "XR_APILAYER_LUNARG_standard_validation",
//];
let Ok(xr_instance) = entry.create_instance(
&xr::ApplicationInfo {
api_version: Version::new(1, 1, 37),
application_name: "wlx-overlay-s",
application_version: 0,
engine_name: "wlx-overlay-s",
engine_version: 0,
},
&enabled_extensions,
&layers,
) else {
bail!("Failed to create OpenXR instance.");
};
let Ok(instance_props) = xr_instance.properties() else {
bail!("Failed to query OpenXR instance properties.");
};
log::info!(
"Using OpenXR runtime: {} {}",
instance_props.runtime_name,
instance_props.runtime_version
);
let Ok(system) = xr_instance.system(xr::FormFactor::HEAD_MOUNTED_DISPLAY) else {
bail!("Failed to access OpenXR HMD system.");
};
let vk_target_version_xr = xr::Version::new(1, 1, 0);
let Ok(reqs) = xr_instance.graphics_requirements::<xr::Vulkan>(system) else {
bail!("Failed to query OpenXR Vulkan requirements.");
};
if vk_target_version_xr < reqs.min_api_version_supported
|| vk_target_version_xr.major() > reqs.max_api_version_supported.major()
{
bail!(
"OpenXR runtime requires Vulkan version > {}, < {}.0.0",
reqs.min_api_version_supported,
reqs.max_api_version_supported.major() + 1
);
}
Ok((xr_instance, system))
}
pub(super) unsafe fn create_overlay_session(
instance: &xr::Instance,
system: xr::SystemId,
info: &xr::vulkan::SessionCreateInfo,
) -> Result<xr::sys::Session, xr::sys::Result> {
let overlay = xr::sys::SessionCreateInfoOverlayEXTX {
ty: xr::sys::SessionCreateInfoOverlayEXTX::TYPE,
next: std::ptr::null(),
create_flags: OverlaySessionCreateFlagsEXTX::EMPTY,
session_layers_placement: 5,
};
let binding = xr::sys::GraphicsBindingVulkanKHR {
ty: xr::sys::GraphicsBindingVulkanKHR::TYPE,
next: (&raw const overlay).cast(),
instance: info.instance,
physical_device: info.physical_device,
device: info.device,
queue_family_index: info.queue_family_index,
queue_index: info.queue_index,
};
let info = xr::sys::SessionCreateInfo {
ty: xr::sys::SessionCreateInfo::TYPE,
next: (&raw const binding).cast(),
create_flags: SessionCreateFlags::default(),
system_id: system,
};
let mut out = xr::sys::Session::NULL;
let x = (instance.fp().create_session)(instance.as_raw(), &info, &mut out);
if x.into_raw() >= 0 {
Ok(out)
} else {
Err(x)
}
}
type Vec3M = mint::Vector3<f32>;
type QuatM = mint::Quaternion<f32>;
pub(super) fn ipd_from_views(views: &[xr::View]) -> f32 {
let p0: Vec3 = Vec3M::from(views[0].pose.position).into();
let p1: Vec3 = Vec3M::from(views[1].pose.position).into();
(p0.distance(p1) * 10000.0).round() * 0.1
}
pub(super) fn transform_to_norm_quat(transform: &Affine3A) -> Quat {
let norm_mat3 = transform
.matrix3
.mul_scalar(1.0 / transform.matrix3.x_axis.length());
Quat::from_mat3a(&norm_mat3).normalize()
}
pub(super) fn translation_rotation_to_posef(translation: Vec3A, mut rotation: Quat) -> xr::Posef {
if !rotation.is_finite() {
rotation = Quat::IDENTITY;
}
xr::Posef {
orientation: xr::Quaternionf {
x: rotation.x,
y: rotation.y,
z: rotation.z,
w: rotation.w,
},
position: xr::Vector3f {
x: translation.x,
y: translation.y,
z: translation.z,
},
}
}
pub(super) fn transform_to_posef(transform: &Affine3A) -> xr::Posef {
let translation = transform.translation;
let rotation = transform_to_norm_quat(transform);
translation_rotation_to_posef(translation, rotation)
}
pub(super) fn posef_to_transform(pose: &xr::Posef) -> Affine3A {
let rotation = QuatM::from(pose.orientation).into();
let translation = Vec3M::from(pose.position).into();
Affine3A::from_rotation_translation(rotation, translation)
}

View File

@@ -0,0 +1,692 @@
use std::{
array::from_fn,
mem::transmute,
time::{Duration, Instant},
};
use glam::{bool, Affine3A, Quat, Vec3};
use libmonado as mnd;
use openxr::{self as xr, Quaternionf, Vector2f, Vector3f};
use serde::{Deserialize, Serialize};
use crate::{
backend::input::{Haptics, Pointer, TrackedDevice, TrackedDeviceRole},
config_io,
state::{AppSession, AppState},
};
use super::{helpers::posef_to_transform, XrState};
static CLICK_TIMES: [Duration; 3] = [
Duration::ZERO,
Duration::from_millis(500),
Duration::from_millis(750),
];
pub(super) struct OpenXrInputSource {
action_set: xr::ActionSet,
hands: [OpenXrHand; 2],
}
pub(super) struct OpenXrHand {
source: OpenXrHandSource,
space: xr::Space,
}
pub struct MultiClickHandler<const COUNT: usize> {
name: String,
action_f32: xr::Action<f32>,
action_bool: xr::Action<bool>,
previous: [Instant; COUNT],
held_active: bool,
held_inactive: bool,
}
impl<const COUNT: usize> MultiClickHandler<COUNT> {
fn new(action_set: &xr::ActionSet, action_name: &str, side: &str) -> anyhow::Result<Self> {
let name = format!("{side}_{COUNT}-{action_name}");
let name_f32 = format!("{}_value", &name);
let action_bool = action_set.create_action::<bool>(&name, &name, &[])?;
let action_f32 = action_set.create_action::<f32>(&name_f32, &name_f32, &[])?;
Ok(Self {
name,
action_f32,
action_bool,
previous: from_fn(|_| Instant::now()),
held_active: false,
held_inactive: false,
})
}
fn check<G>(&mut self, session: &xr::Session<G>, threshold: f32) -> anyhow::Result<bool> {
let res = self.action_bool.state(session, xr::Path::NULL)?;
let mut state = res.is_active && res.current_state;
if !state {
let res = self.action_f32.state(session, xr::Path::NULL)?;
state = res.is_active && res.current_state > threshold;
}
if !state {
self.held_active = false;
self.held_inactive = false;
return Ok(false);
}
if self.held_active {
return Ok(true);
}
if self.held_inactive {
return Ok(false);
}
let passed = self
.previous
.iter()
.all(|instant| instant.elapsed() < CLICK_TIMES[COUNT]);
if passed {
log::trace!("{}: passed", self.name);
self.held_active = true;
self.held_inactive = false;
// reset to no prior clicks
let long_ago = Instant::now().checked_sub(Duration::from_secs(10)).unwrap();
self.previous
.iter_mut()
.for_each(|instant| *instant = long_ago);
} else if COUNT > 0 {
log::trace!("{}: rotate", self.name);
self.previous.rotate_right(1);
self.previous[0] = Instant::now();
self.held_inactive = true;
}
Ok(passed)
}
}
pub struct CustomClickAction {
single: MultiClickHandler<0>,
double: MultiClickHandler<1>,
triple: MultiClickHandler<2>,
}
impl CustomClickAction {
pub fn new(action_set: &xr::ActionSet, name: &str, side: &str) -> anyhow::Result<Self> {
let single = MultiClickHandler::new(action_set, name, side)?;
let double = MultiClickHandler::new(action_set, name, side)?;
let triple = MultiClickHandler::new(action_set, name, side)?;
Ok(Self {
single,
double,
triple,
})
}
pub fn state(
&mut self,
before: bool,
state: &XrState,
session: &AppSession,
) -> anyhow::Result<bool> {
let threshold = if before {
session.config.xr_click_sensitivity_release
} else {
session.config.xr_click_sensitivity
};
Ok(self.single.check(&state.session, threshold)?
|| self.double.check(&state.session, threshold)?
|| self.triple.check(&state.session, threshold)?)
}
}
pub(super) struct OpenXrHandSource {
pose: xr::Action<xr::Posef>,
click: CustomClickAction,
grab: CustomClickAction,
alt_click: CustomClickAction,
show_hide: CustomClickAction,
toggle_dashboard: CustomClickAction,
space_drag: CustomClickAction,
space_rotate: CustomClickAction,
space_reset: CustomClickAction,
modifier_right: CustomClickAction,
modifier_middle: CustomClickAction,
move_mouse: CustomClickAction,
scroll: xr::Action<Vector2f>,
haptics: xr::Action<xr::Haptic>,
}
impl OpenXrInputSource {
pub fn new(xr: &XrState) -> anyhow::Result<Self> {
let mut action_set =
xr.session
.instance()
.create_action_set("wlx-overlay-s", "WlxOverlay-S Actions", 0)?;
let left_source = OpenXrHandSource::new(&mut action_set, "left")?;
let right_source = OpenXrHandSource::new(&mut action_set, "right")?;
suggest_bindings(&xr.instance, &[&left_source, &right_source]);
xr.session.attach_action_sets(&[&action_set])?;
Ok(Self {
action_set,
hands: [
OpenXrHand::new(xr, left_source)?,
OpenXrHand::new(xr, right_source)?,
],
})
}
pub fn haptics(&self, xr: &XrState, hand: usize, haptics: &Haptics) {
let action = &self.hands[hand].source.haptics;
let duration_nanos = f64::from(haptics.duration) * 1_000_000_000.0;
let _ = action.apply_feedback(
&xr.session,
xr::Path::NULL,
&xr::HapticVibration::new()
.amplitude(haptics.intensity)
.frequency(haptics.frequency)
.duration(xr::Duration::from_nanos(duration_nanos as _)),
);
}
pub fn update(&mut self, xr: &XrState, state: &mut AppState) -> anyhow::Result<()> {
xr.session.sync_actions(&[(&self.action_set).into()])?;
let loc = xr.view.locate(&xr.stage, xr.predicted_display_time)?;
let hmd = posef_to_transform(&loc.pose);
if loc
.location_flags
.contains(xr::SpaceLocationFlags::ORIENTATION_VALID)
{
state.input_state.hmd.matrix3 = hmd.matrix3;
}
if loc
.location_flags
.contains(xr::SpaceLocationFlags::POSITION_VALID)
{
state.input_state.hmd.translation = hmd.translation;
}
for i in 0..2 {
self.hands[i].update(&mut state.input_state.pointers[i], xr, &state.session)?;
}
Ok(())
}
fn update_device_battery_status(
device: &mut mnd::Device,
role: TrackedDeviceRole,
app: &mut AppState,
) {
if let Ok(status) = device.battery_status() {
if status.present {
app.input_state.devices.push(TrackedDevice {
soc: Some(status.charge),
charging: status.charging,
role,
});
log::debug!(
"Device {} role {:#?}: {:.0}% (charging {})",
device.index,
role,
status.charge * 100.0f32,
status.charging
);
}
}
}
pub fn update_devices(app: &mut AppState, monado: &mut mnd::Monado) {
app.input_state.devices.clear();
let roles = [
(mnd::DeviceRole::Head, TrackedDeviceRole::Hmd),
(mnd::DeviceRole::Eyes, TrackedDeviceRole::None),
(mnd::DeviceRole::Left, TrackedDeviceRole::LeftHand),
(mnd::DeviceRole::Right, TrackedDeviceRole::RightHand),
(mnd::DeviceRole::Gamepad, TrackedDeviceRole::None),
(
mnd::DeviceRole::HandTrackingLeft,
TrackedDeviceRole::LeftHand,
),
(
mnd::DeviceRole::HandTrackingRight,
TrackedDeviceRole::RightHand,
),
];
let mut seen = Vec::<u32>::with_capacity(32);
for (mnd_role, wlx_role) in roles {
let device = monado.device_from_role(mnd_role);
if let Ok(mut device) = device {
if !seen.contains(&device.index) {
seen.push(device.index);
Self::update_device_battery_status(&mut device, wlx_role, app);
}
}
}
if let Ok(devices) = monado.devices() {
for mut device in devices {
if !seen.contains(&device.index) {
let role = if device.name_id >= 4 && device.name_id <= 8 {
TrackedDeviceRole::Tracker
} else {
TrackedDeviceRole::None
};
Self::update_device_battery_status(&mut device, role, app);
}
}
}
app.input_state.devices.sort_by(|a, b| {
u8::from(a.soc.is_none())
.cmp(&u8::from(b.soc.is_none()))
.then((a.role as u8).cmp(&(b.role as u8)))
.then(a.soc.unwrap_or(999.).total_cmp(&b.soc.unwrap_or(999.)))
});
}
}
impl OpenXrHand {
pub(super) fn new(xr: &XrState, source: OpenXrHandSource) -> Result<Self, xr::sys::Result> {
let space = source
.pose
.create_space(&xr.session, xr::Path::NULL, xr::Posef::IDENTITY)?;
Ok(Self { source, space })
}
pub(super) fn update(
&mut self,
pointer: &mut Pointer,
xr: &XrState,
session: &AppSession,
) -> anyhow::Result<()> {
let location = self.space.locate(&xr.stage, xr.predicted_display_time)?;
if location
.location_flags
.contains(xr::SpaceLocationFlags::ORIENTATION_VALID)
{
let (cur_quat, cur_pos) = (Quat::from_affine3(&pointer.pose), pointer.pose.translation);
let (new_quat, new_pos) = unsafe {
(
transmute::<Quaternionf, Quat>(location.pose.orientation),
transmute::<Vector3f, Vec3>(location.pose.position),
)
};
let lerp_factor =
(1.0 / (xr.fps / 100.0) * session.config.pointer_lerp_factor).clamp(0.1, 1.0);
pointer.raw_pose = Affine3A::from_rotation_translation(new_quat, new_pos);
pointer.pose = Affine3A::from_rotation_translation(
cur_quat.lerp(new_quat, lerp_factor),
cur_pos.lerp(new_pos.into(), lerp_factor).into(),
);
}
pointer.now.click = self.source.click.state(pointer.before.click, xr, session)?;
pointer.now.grab = self.source.grab.state(pointer.before.grab, xr, session)?;
let scroll = self
.source
.scroll
.state(&xr.session, xr::Path::NULL)?
.current_state;
pointer.now.scroll_x = scroll.x;
pointer.now.scroll_y = scroll.y;
pointer.now.alt_click =
self.source
.alt_click
.state(pointer.before.alt_click, xr, session)?;
pointer.now.show_hide =
self.source
.show_hide
.state(pointer.before.show_hide, xr, session)?;
pointer.now.click_modifier_right =
self.source
.modifier_right
.state(pointer.before.click_modifier_right, xr, session)?;
pointer.now.toggle_dashboard =
self.source
.toggle_dashboard
.state(pointer.before.toggle_dashboard, xr, session)?;
pointer.now.click_modifier_middle =
self.source
.modifier_middle
.state(pointer.before.click_modifier_middle, xr, session)?;
pointer.now.move_mouse =
self.source
.move_mouse
.state(pointer.before.move_mouse, xr, session)?;
pointer.now.space_drag =
self.source
.space_drag
.state(pointer.before.space_drag, xr, session)?;
pointer.now.space_rotate =
self.source
.space_rotate
.state(pointer.before.space_rotate, xr, session)?;
pointer.now.space_reset =
self.source
.space_reset
.state(pointer.before.space_reset, xr, session)?;
Ok(())
}
}
// supported action types: Haptic, Posef, Vector2f, f32, bool
impl OpenXrHandSource {
pub(super) fn new(action_set: &mut xr::ActionSet, side: &str) -> anyhow::Result<Self> {
let action_pose = action_set.create_action::<xr::Posef>(
&format!("{side}_hand"),
&format!("{side} hand pose"),
&[],
)?;
let action_scroll = action_set.create_action::<Vector2f>(
&format!("{side}_scroll"),
&format!("{side} hand scroll"),
&[],
)?;
let action_haptics = action_set.create_action::<xr::Haptic>(
&format!("{side}_haptics"),
&format!("{side} hand haptics"),
&[],
)?;
Ok(Self {
pose: action_pose,
click: CustomClickAction::new(action_set, "click", side)?,
grab: CustomClickAction::new(action_set, "grab", side)?,
scroll: action_scroll,
alt_click: CustomClickAction::new(action_set, "alt_click", side)?,
show_hide: CustomClickAction::new(action_set, "show_hide", side)?,
toggle_dashboard: CustomClickAction::new(action_set, "toggle_dashboard", side)?,
space_drag: CustomClickAction::new(action_set, "space_drag", side)?,
space_rotate: CustomClickAction::new(action_set, "space_rotate", side)?,
space_reset: CustomClickAction::new(action_set, "space_reset", side)?,
modifier_right: CustomClickAction::new(action_set, "click_modifier_right", side)?,
modifier_middle: CustomClickAction::new(action_set, "click_modifier_middle", side)?,
move_mouse: CustomClickAction::new(action_set, "move_mouse", side)?,
haptics: action_haptics,
})
}
}
fn to_path(maybe_path_str: Option<&String>, instance: &xr::Instance) -> Option<xr::Path> {
maybe_path_str.as_ref().and_then(|s| {
instance
.string_to_path(s)
.inspect_err(|_| {
log::warn!("Invalid binding path: {s}");
})
.ok()
})
}
fn is_bool(maybe_type_str: Option<&String>) -> bool {
maybe_type_str
.as_ref()
.unwrap() // want panic
.split('/')
.next_back()
.is_some_and(|last| matches!(last, "click" | "touch") || last.starts_with("dpad_"))
}
macro_rules! add_custom {
($action:expr, $left:expr, $right:expr, $bindings:expr, $instance:expr) => {
if let Some(action) = $action.as_ref() {
if let Some(p) = to_path(action.left.as_ref(), $instance) {
if is_bool(action.left.as_ref()) {
if action.triple_click.unwrap_or(false) {
$bindings.push(xr::Binding::new(&$left.triple.action_bool, p));
} else if action.double_click.unwrap_or(false) {
$bindings.push(xr::Binding::new(&$left.double.action_bool, p));
} else {
$bindings.push(xr::Binding::new(&$left.single.action_bool, p));
}
} else {
if action.triple_click.unwrap_or(false) {
$bindings.push(xr::Binding::new(&$left.triple.action_f32, p));
} else if action.double_click.unwrap_or(false) {
$bindings.push(xr::Binding::new(&$left.double.action_f32, p));
} else {
$bindings.push(xr::Binding::new(&$left.single.action_f32, p));
}
}
}
if let Some(p) = to_path(action.right.as_ref(), $instance) {
if is_bool(action.right.as_ref()) {
if action.triple_click.unwrap_or(false) {
$bindings.push(xr::Binding::new(&$right.triple.action_bool, p));
} else if action.double_click.unwrap_or(false) {
$bindings.push(xr::Binding::new(&$right.double.action_bool, p));
} else {
$bindings.push(xr::Binding::new(&$right.single.action_bool, p));
}
} else {
if action.triple_click.unwrap_or(false) {
$bindings.push(xr::Binding::new(&$right.triple.action_f32, p));
} else if action.double_click.unwrap_or(false) {
$bindings.push(xr::Binding::new(&$right.double.action_f32, p));
} else {
$bindings.push(xr::Binding::new(&$right.single.action_f32, p));
}
}
}
}
};
}
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
fn suggest_bindings(instance: &xr::Instance, hands: &[&OpenXrHandSource; 2]) {
let profiles = load_action_profiles();
for profile in profiles {
let Ok(profile_path) = instance.string_to_path(&profile.profile) else {
log::debug!("Profile not supported: {}", profile.profile);
continue;
};
let mut bindings: Vec<xr::Binding> = vec![];
if let Some(action) = profile.pose {
if let Some(p) = to_path(action.left.as_ref(), instance) {
bindings.push(xr::Binding::new(&hands[0].pose, p));
}
if let Some(p) = to_path(action.right.as_ref(), instance) {
bindings.push(xr::Binding::new(&hands[1].pose, p));
}
}
if let Some(action) = profile.haptic {
if let Some(p) = to_path(action.left.as_ref(), instance) {
bindings.push(xr::Binding::new(&hands[0].haptics, p));
}
if let Some(p) = to_path(action.right.as_ref(), instance) {
bindings.push(xr::Binding::new(&hands[1].haptics, p));
}
}
if let Some(action) = profile.scroll {
if let Some(p) = to_path(action.left.as_ref(), instance) {
bindings.push(xr::Binding::new(&hands[0].scroll, p));
}
if let Some(p) = to_path(action.right.as_ref(), instance) {
bindings.push(xr::Binding::new(&hands[1].scroll, p));
}
}
add_custom!(
profile.click,
hands[0].click,
hands[1].click,
bindings,
instance
);
add_custom!(
profile.alt_click,
&hands[0].alt_click,
&hands[1].alt_click,
bindings,
instance
);
add_custom!(
profile.grab,
&hands[0].grab,
&hands[1].grab,
bindings,
instance
);
add_custom!(
profile.show_hide,
&hands[0].show_hide,
&hands[1].show_hide,
bindings,
instance
);
add_custom!(
profile.toggle_dashboard,
&hands[0].toggle_dashboard,
&hands[1].toggle_dashboard,
bindings,
instance
);
add_custom!(
profile.space_drag,
&hands[0].space_drag,
&hands[1].space_drag,
bindings,
instance
);
add_custom!(
profile.space_rotate,
&hands[0].space_rotate,
&hands[1].space_rotate,
bindings,
instance
);
add_custom!(
profile.space_reset,
&hands[0].space_reset,
&hands[1].space_reset,
bindings,
instance
);
add_custom!(
profile.click_modifier_right,
&hands[0].modifier_right,
&hands[1].modifier_right,
bindings,
instance
);
add_custom!(
profile.click_modifier_middle,
&hands[0].modifier_middle,
&hands[1].modifier_middle,
bindings,
instance
);
add_custom!(
profile.move_mouse,
&hands[0].move_mouse,
&hands[1].move_mouse,
bindings,
instance
);
if instance
.suggest_interaction_profile_bindings(profile_path, &bindings)
.is_err()
{
log::error!("Bad bindings for {}", &profile.profile[22..]);
log::error!("Verify config: ~/.config/wlxoverlay/openxr_actions.json5");
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct OpenXrActionConfAction {
left: Option<String>,
right: Option<String>,
threshold: Option<[f32; 2]>,
double_click: Option<bool>,
triple_click: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct OpenXrActionConfProfile {
profile: String,
pose: Option<OpenXrActionConfAction>,
click: Option<OpenXrActionConfAction>,
grab: Option<OpenXrActionConfAction>,
alt_click: Option<OpenXrActionConfAction>,
show_hide: Option<OpenXrActionConfAction>,
toggle_dashboard: Option<OpenXrActionConfAction>,
space_drag: Option<OpenXrActionConfAction>,
space_rotate: Option<OpenXrActionConfAction>,
space_reset: Option<OpenXrActionConfAction>,
click_modifier_right: Option<OpenXrActionConfAction>,
click_modifier_middle: Option<OpenXrActionConfAction>,
move_mouse: Option<OpenXrActionConfAction>,
scroll: Option<OpenXrActionConfAction>,
haptic: Option<OpenXrActionConfAction>,
}
const DEFAULT_PROFILES: &str = include_str!("openxr_actions.json5");
fn load_action_profiles() -> Vec<OpenXrActionConfProfile> {
let mut profiles: Vec<OpenXrActionConfProfile> =
serde_json5::from_str(DEFAULT_PROFILES).unwrap(); // want panic
let Some(conf) = config_io::load("openxr_actions.json5") else {
return profiles;
};
match serde_json5::from_str::<Vec<OpenXrActionConfProfile>>(&conf) {
Ok(override_profiles) => {
for new in override_profiles {
if let Some(i) = profiles.iter().position(|old| old.profile == new.profile) {
profiles[i] = new;
}
}
}
Err(e) => {
log::error!("Failed to load openxr_actions.json5: {e}");
}
}
profiles
}

View File

@@ -0,0 +1,201 @@
use glam::{Affine3A, Vec3, Vec3A};
use idmap::IdMap;
use openxr as xr;
use std::{
f32::consts::PI,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
use wgui::gfx::{pipeline::WGfxPipeline, WGfx};
use crate::{
backend::openxr::helpers,
graphics::{CommandBuffers, ExtentExt, Vert2Uv},
state::AppState,
};
use vulkano::{
command_buffer::CommandBufferUsage, pipeline::graphics::input_assembly::PrimitiveTopology,
};
use super::{
swapchain::{create_swapchain, SwapchainOpts, WlxSwapchain},
CompositionLayer, XrState,
};
static LINE_AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(1);
pub(super) const LINE_WIDTH: f32 = 0.002;
// TODO customizable colors
static COLORS: [[f32; 6]; 5] = {
[
[1., 1., 1., 1., 0., 0.],
[0., 0.375, 0.5, 1., 0., 0.],
[0.69, 0.188, 0., 1., 0., 0.],
[0.375, 0., 0.5, 1., 0., 0.],
[1., 0., 0., 1., 0., 0.],
]
};
pub(super) struct LinePool {
lines: IdMap<usize, LineContainer>,
pipeline: Arc<WGfxPipeline<Vert2Uv>>,
}
impl LinePool {
pub(super) fn new(app: &AppState) -> anyhow::Result<Self> {
let pipeline = app.gfx.create_pipeline(
app.gfx_extras.shaders.get("vert_quad").unwrap().clone(), // want panic
app.gfx_extras.shaders.get("frag_color").unwrap().clone(), // want panic
app.gfx.surface_format,
None,
PrimitiveTopology::TriangleStrip,
false,
)?;
Ok(Self {
lines: IdMap::new(),
pipeline,
})
}
pub(super) fn allocate(&mut self, xr: &XrState, gfx: Arc<WGfx>) -> anyhow::Result<usize> {
let id = LINE_AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed);
let srd = create_swapchain(xr, gfx, [1, 1, 1], SwapchainOpts::new())?;
self.lines.insert(
id,
LineContainer {
swapchain: srd,
maybe_line: None,
},
);
Ok(id)
}
pub(super) fn draw_from(
&mut self,
id: usize,
mut from: Affine3A,
len: f32,
color: usize,
hmd: &Affine3A,
) {
if len < 0.01 {
return;
}
debug_assert!(color < COLORS.len());
let Some(line) = self.lines.get_mut(id) else {
log::warn!("Line {id} not found");
return;
};
let rotation = Affine3A::from_axis_angle(Vec3::X, PI * 1.5);
from.translation += from.transform_vector3a(Vec3A::NEG_Z) * (len * 0.5);
let mut transform = from * rotation;
let to_hmd = hmd.translation - from.translation;
let sides = [Vec3A::Z, Vec3A::X, Vec3A::NEG_Z, Vec3A::NEG_X];
let rotations = [
Affine3A::IDENTITY,
Affine3A::from_axis_angle(Vec3::Y, PI * 0.5),
Affine3A::from_axis_angle(Vec3::Y, PI * -1.0),
Affine3A::from_axis_angle(Vec3::Y, PI * 1.5),
];
let mut closest = (0, 0.0);
for (i, &side) in sides.iter().enumerate() {
let dot = to_hmd.dot(transform.transform_vector3a(side));
if i == 0 || dot > closest.1 {
closest = (i, dot);
}
}
transform *= rotations[closest.0];
let posef = helpers::transform_to_posef(&transform);
line.maybe_line = Some(Line {
color,
pose: posef,
length: len,
});
}
pub(super) fn render(
&mut self,
app: &AppState,
buf: &mut CommandBuffers,
) -> anyhow::Result<()> {
for line in self.lines.values_mut() {
if let Some(inner) = line.maybe_line.as_mut() {
let tgt = line.swapchain.acquire_wait_image()?;
let set0 = self
.pipeline
.uniform_buffer_upload(0, COLORS[inner.color].to_vec())?;
let pass = self.pipeline.create_pass(
tgt.extent_f32(),
app.gfx_extras.quad_verts.clone(),
0..4,
0..1,
vec![set0],
)?;
let mut cmd_buffer = app
.gfx
.create_gfx_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
cmd_buffer.begin_rendering(tgt)?;
cmd_buffer.run_ref(&pass)?;
cmd_buffer.end_rendering()?;
buf.push(cmd_buffer.build()?);
}
}
Ok(())
}
pub(super) fn present<'a>(
&'a mut self,
xr: &'a XrState,
) -> anyhow::Result<Vec<CompositionLayer<'a>>> {
let mut quads = Vec::new();
for line in self.lines.values_mut() {
line.swapchain.ensure_image_released()?;
if let Some(inner) = line.maybe_line.take() {
let quad = xr::CompositionLayerQuad::new()
.pose(inner.pose)
.sub_image(line.swapchain.get_subimage())
.eye_visibility(xr::EyeVisibility::BOTH)
.space(&xr.stage)
.size(xr::Extent2Df {
width: LINE_WIDTH,
height: inner.length,
});
quads.push(CompositionLayer::Quad(quad));
}
}
Ok(quads)
}
}
pub(super) struct Line {
pub(super) color: usize,
pub(super) pose: xr::Posef,
pub(super) length: f32,
}
struct LineContainer {
swapchain: WlxSwapchain,
maybe_line: Option<Line>,
}

View File

@@ -0,0 +1,577 @@
use std::{
collections::VecDeque,
ops::Add,
sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc,
},
time::{Duration, Instant},
};
use glam::{Affine3A, Vec3};
use input::OpenXrInputSource;
use libmonado::Monado;
use openxr as xr;
use skybox::create_skybox;
use vulkano::{Handle, VulkanObject};
use crate::{
backend::{
common::{BackendError, OverlayContainer},
input::interact,
notifications::NotificationManager,
openxr::{lines::LinePool, overlay::OpenXrOverlayData},
overlay::{OverlayData, ShouldRender},
task::{SystemTask, TaskType},
},
graphics::{init_openxr_graphics, CommandBuffers},
overlays::{
toast::{Toast, ToastTopic},
watch::{watch_fade, WATCH_NAME},
},
state::AppState,
};
#[cfg(feature = "wayvr")]
use crate::{backend::wayvr::WayVRAction, overlays::wayvr::wayvr_action};
mod blocker;
mod helpers;
mod input;
mod lines;
mod overlay;
mod playspace;
mod skybox;
mod swapchain;
const VIEW_TYPE: xr::ViewConfigurationType = xr::ViewConfigurationType::PRIMARY_STEREO;
static FRAME_COUNTER: AtomicUsize = AtomicUsize::new(0);
struct XrState {
instance: xr::Instance,
session: xr::Session<xr::Vulkan>,
predicted_display_time: xr::Time,
fps: f32,
stage: Arc<xr::Space>,
view: Arc<xr::Space>,
}
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
pub fn openxr_run(
running: Arc<AtomicBool>,
show_by_default: bool,
headless: bool,
) -> Result<(), BackendError> {
let (xr_instance, system) = match helpers::init_xr() {
Ok((xr_instance, system)) => (xr_instance, system),
Err(e) => {
log::warn!("Will not use OpenXR: {e}");
return Err(BackendError::NotSupported);
}
};
let mut app = {
let (gfx, gfx_extras) = init_openxr_graphics(xr_instance.clone(), system)?;
AppState::from_graphics(gfx, gfx_extras)?
};
let environment_blend_mode = {
let modes = xr_instance.enumerate_environment_blend_modes(system, VIEW_TYPE)?;
if modes.contains(&xr::EnvironmentBlendMode::ALPHA_BLEND)
&& app.session.config.use_passthrough
{
xr::EnvironmentBlendMode::ALPHA_BLEND
} else {
modes[0]
}
};
log::info!("Using environment blend mode: {environment_blend_mode:?}");
if show_by_default {
app.tasks.enqueue_at(
TaskType::System(SystemTask::ShowHide),
Instant::now().add(Duration::from_secs(1)),
);
}
let mut overlays = OverlayContainer::<OpenXrOverlayData>::new(&mut app, headless)?;
let mut lines = LinePool::new(&app)?;
let mut notifications = NotificationManager::new();
notifications.run_dbus();
notifications.run_udp();
let mut delete_queue = vec![];
let mut monado = Monado::auto_connect()
.map_err(|e| log::warn!("Will not use libmonado: {e}"))
.ok();
let mut playspace = monado.as_mut().and_then(|m| {
playspace::PlayspaceMover::new(m)
.map_err(|e| log::warn!("Will not use Monado playspace mover: {e}"))
.ok()
});
let mut blocker = monado.is_some().then(blocker::InputBlocker::new);
let (session, mut frame_wait, mut frame_stream) = unsafe {
let raw_session = helpers::create_overlay_session(
&xr_instance,
system,
&xr::vulkan::SessionCreateInfo {
instance: app.gfx.instance.handle().as_raw() as _,
physical_device: app.gfx.device.physical_device().handle().as_raw() as _,
device: app.gfx.device.handle().as_raw() as _,
queue_family_index: app.gfx.queue_gfx.queue_family_index(),
queue_index: 0,
},
)?;
xr::Session::from_raw(xr_instance.clone(), raw_session, Box::new(()))
};
let stage =
session.create_reference_space(xr::ReferenceSpaceType::STAGE, xr::Posef::IDENTITY)?;
let view = session.create_reference_space(xr::ReferenceSpaceType::VIEW, xr::Posef::IDENTITY)?;
let mut xr_state = XrState {
instance: xr_instance,
session,
predicted_display_time: xr::Time::from_nanos(0),
fps: 30.0,
stage: Arc::new(stage),
view: Arc::new(view),
};
let mut skybox = if environment_blend_mode == xr::EnvironmentBlendMode::OPAQUE {
create_skybox(&xr_state, &app)
} else {
None
};
let pointer_lines = [
lines.allocate(&xr_state, app.gfx.clone())?,
lines.allocate(&xr_state, app.gfx.clone())?,
];
let watch_id = overlays.get_by_name(WATCH_NAME).unwrap().state.id; // want panic
let mut input_source = input::OpenXrInputSource::new(&xr_state)?;
let mut session_running = false;
let mut event_storage = xr::EventDataBuffer::new();
let mut next_device_update = Instant::now();
let mut due_tasks = VecDeque::with_capacity(4);
let mut fps_counter: VecDeque<Instant> = VecDeque::new();
let mut main_session_visible = false;
'main_loop: loop {
let cur_frame = FRAME_COUNTER.fetch_add(1, Ordering::Relaxed);
if !running.load(Ordering::Relaxed) {
log::warn!("Received shutdown signal.");
match xr_state.session.request_exit() {
Ok(()) => log::info!("OpenXR session exit requested."),
Err(xr::sys::Result::ERROR_SESSION_NOT_RUNNING) => break 'main_loop,
Err(e) => {
log::error!("Failed to request OpenXR session exit: {e}");
break 'main_loop;
}
}
}
while let Some(event) = xr_state.instance.poll_event(&mut event_storage)? {
match event {
xr::Event::SessionStateChanged(e) => {
// Session state change is where we can begin and end sessions, as well as
// find quit messages!
log::info!("entered state {:?}", e.state());
match e.state() {
xr::SessionState::READY => {
xr_state.session.begin(VIEW_TYPE)?;
session_running = true;
}
xr::SessionState::STOPPING => {
xr_state.session.end()?;
session_running = false;
}
xr::SessionState::EXITING | xr::SessionState::LOSS_PENDING => {
break 'main_loop;
}
_ => {}
}
}
xr::Event::InstanceLossPending(_) => {
break 'main_loop;
}
xr::Event::EventsLost(e) => {
log::warn!("lost {} events", e.lost_event_count());
}
xr::Event::MainSessionVisibilityChangedEXTX(e) => {
if main_session_visible != e.visible() {
main_session_visible = e.visible();
log::info!("Main session visible: {main_session_visible}");
if main_session_visible {
log::debug!("Destroying skybox.");
skybox = None;
} else if environment_blend_mode == xr::EnvironmentBlendMode::OPAQUE {
log::debug!("Allocating skybox.");
skybox = create_skybox(&xr_state, &app);
}
}
}
_ => {}
}
}
if next_device_update <= Instant::now() {
if let Some(monado) = &mut monado {
OpenXrInputSource::update_devices(&mut app, monado);
next_device_update = Instant::now() + Duration::from_secs(30);
}
}
if !session_running {
std::thread::sleep(Duration::from_millis(100));
continue 'main_loop;
}
let xr_frame_state = frame_wait.wait()?;
frame_stream.begin()?;
xr_state.predicted_display_time = xr_frame_state.predicted_display_time;
xr_state.fps = {
fps_counter.push_back(Instant::now());
while let Some(time) = fps_counter.front() {
if time.elapsed().as_secs_f32() > 1. {
fps_counter.pop_front();
} else {
break;
}
}
let total_elapsed = fps_counter
.front()
.map_or(0f32, |time| time.elapsed().as_secs_f32());
fps_counter.len() as f32 / total_elapsed
};
if !xr_frame_state.should_render {
frame_stream.end(
xr_frame_state.predicted_display_time,
environment_blend_mode,
&[],
)?;
continue 'main_loop;
}
app.input_state.pre_update();
input_source.update(&xr_state, &mut app)?;
app.input_state.post_update(&app.session);
if let Some(ref mut blocker) = blocker {
blocker.update(
&app,
watch_id,
monado.as_mut().unwrap(), // safe
);
}
if app
.input_state
.pointers
.iter()
.any(|p| p.now.show_hide && !p.before.show_hide)
{
overlays.show_hide(&mut app);
}
#[cfg(feature = "wayvr")]
if app
.input_state
.pointers
.iter()
.any(|p| p.now.toggle_dashboard && !p.before.toggle_dashboard)
{
wayvr_action(&mut app, &mut overlays, &WayVRAction::ToggleDashboard);
}
watch_fade(&mut app, overlays.mut_by_id(watch_id).unwrap()); // want panic
if let Some(ref mut space_mover) = playspace {
space_mover.update(
&mut overlays,
&app,
monado.as_mut().unwrap(), // safe
);
}
for o in overlays.iter_mut() {
o.after_input(&mut app)?;
}
#[cfg(feature = "osc")]
if let Some(ref mut sender) = app.osc_sender {
let _ = sender.send_params(&overlays, &app.input_state.devices);
}
let (_, views) = xr_state.session.locate_views(
VIEW_TYPE,
xr_frame_state.predicted_display_time,
&xr_state.stage,
)?;
let ipd = helpers::ipd_from_views(&views);
if (app.input_state.ipd - ipd).abs() > 0.01 {
log::info!("IPD changed: {} -> {}", app.input_state.ipd, ipd);
app.input_state.ipd = ipd;
Toast::new(
ToastTopic::IpdChange,
"IPD".into(),
format!("{ipd:.1} mm").into(),
)
.submit(&mut app);
}
overlays
.iter_mut()
.for_each(|o| o.state.auto_movement(&mut app));
let lengths_haptics = interact(&mut overlays, &mut app);
for (idx, (len, haptics)) in lengths_haptics.iter().enumerate() {
lines.draw_from(
pointer_lines[idx],
app.input_state.pointers[idx].pose,
*len,
app.input_state.pointers[idx].interaction.mode as usize + 1,
&app.input_state.hmd,
);
if let Some(haptics) = haptics {
input_source.haptics(&xr_state, idx, haptics);
}
}
app.hid_provider.commit();
let watch = overlays.mut_by_id(watch_id).unwrap(); // want panic
let watch_transform = watch.state.transform;
if !watch.state.want_visible {
watch.state.want_visible = true;
watch.state.transform = Affine3A::from_scale(Vec3 {
x: 0.001,
y: 0.001,
z: 0.001,
});
}
#[cfg(feature = "wayvr")]
if let Err(e) =
crate::overlays::wayvr::tick_events::<OpenXrOverlayData>(&mut app, &mut overlays)
{
log::error!("WayVR tick_events failed: {e:?}");
}
// Begin rendering
let mut buffers = CommandBuffers::default();
if !main_session_visible {
if let Some(skybox) = skybox.as_mut() {
skybox.render(&xr_state, &app, &mut buffers)?;
}
}
for o in overlays.iter_mut() {
o.data.cur_visible = false;
if !o.state.want_visible {
continue;
}
if !o.data.init {
o.init(&mut app)?;
o.data.init = true;
}
let should_render = match o.should_render(&mut app)? {
ShouldRender::Should => true,
ShouldRender::Can => (o.data.last_alpha - o.state.alpha).abs() > f32::EPSILON,
ShouldRender::Unable => false, //try show old image if exists
};
if should_render {
if !o.ensure_swapchain(&app, &xr_state)? {
continue;
}
let tgt = o.data.swapchain.as_mut().unwrap().acquire_wait_image()?; // want
if !o.render(&mut app, tgt, &mut buffers, o.state.alpha)? {
o.data.swapchain.as_mut().unwrap().ensure_image_released()?; // want
continue;
}
o.data.last_alpha = o.state.alpha;
} else if o.data.swapchain.is_none() {
continue;
}
o.data.cur_visible = true;
}
lines.render(&app, &mut buffers)?;
let future = buffers.execute_now(app.gfx.queue_gfx.clone())?;
if let Some(mut future) = future {
if let Err(e) = future.flush() {
return Err(BackendError::Fatal(e.into()));
}
future.cleanup_finished();
}
// End rendering
// Layer composition
let mut layers = vec![];
if !main_session_visible {
if let Some(skybox) = skybox.as_mut() {
for (idx, layer) in skybox.present(&xr_state, &app)?.into_iter().enumerate() {
layers.push(((idx as f32).mul_add(-50.0, 200.0), layer));
}
}
}
for o in overlays.iter_mut() {
if !o.data.cur_visible {
continue;
}
let dist_sq = (app.input_state.hmd.translation - o.state.transform.translation)
.length_squared()
+ (100f32 - o.state.z_order as f32);
if !dist_sq.is_normal() {
o.data.swapchain.as_mut().unwrap().ensure_image_released()?;
continue;
}
let maybe_layer = o.present(&xr_state)?;
if matches!(maybe_layer, CompositionLayer::None) {
continue;
}
layers.push((dist_sq, maybe_layer));
}
for maybe_layer in lines.present(&xr_state)? {
if matches!(maybe_layer, CompositionLayer::None) {
continue;
}
layers.push((0.0, maybe_layer));
}
// End layer composition
#[cfg(feature = "wayvr")]
if let Some(wayvr) = &app.wayvr {
wayvr.borrow_mut().data.tick_finish()?;
}
// Begin layer submit
layers.sort_by(|a, b| b.0.total_cmp(&a.0));
let frame_ref = layers
.iter()
.map(|f| match f.1 {
CompositionLayer::Quad(ref l) => l as &xr::CompositionLayerBase<xr::Vulkan>,
CompositionLayer::Cylinder(ref l) => l as &xr::CompositionLayerBase<xr::Vulkan>,
CompositionLayer::Equirect2(ref l) => l as &xr::CompositionLayerBase<xr::Vulkan>,
CompositionLayer::None => unreachable!(),
})
.collect::<Vec<_>>();
frame_stream.end(
xr_state.predicted_display_time,
environment_blend_mode,
&frame_ref,
)?;
// End layer submit
let removed_overlays = overlays.update(&mut app)?;
for o in removed_overlays {
delete_queue.push((o, cur_frame + 5));
}
notifications.submit_pending(&mut app);
app.tasks.retrieve_due(&mut due_tasks);
while let Some(task) = due_tasks.pop_front() {
match task {
TaskType::Overlay(sel, f) => {
if let Some(o) = overlays.mut_by_selector(&sel) {
f(&mut app, &mut o.state);
} else {
log::warn!("Overlay not found for task: {sel:?}");
}
}
TaskType::CreateOverlay(sel, f) => {
let None = overlays.mut_by_selector(&sel) else {
continue;
};
let Some((mut overlay_state, overlay_backend)) = f(&mut app) else {
continue;
};
overlay_state.birthframe = cur_frame;
overlays.add(OverlayData {
state: overlay_state,
backend: overlay_backend,
..Default::default()
});
}
TaskType::DropOverlay(sel) => {
if let Some(o) = overlays.mut_by_selector(&sel) {
if o.state.birthframe < cur_frame {
log::debug!("{}: destroy", o.state.name);
if let Some(o) = overlays.remove_by_selector(&sel) {
// set for deletion after all images are done showing
delete_queue.push((o, cur_frame + 5));
}
}
}
}
TaskType::System(task) => match task {
SystemTask::FixFloor => {
if let Some(ref mut playspace) = playspace {
playspace.fix_floor(
&app.input_state,
monado.as_mut().unwrap(), // safe
);
}
}
SystemTask::ResetPlayspace => {
if let Some(ref mut playspace) = playspace {
playspace.reset_offset(monado.as_mut().unwrap()); // safe
}
}
SystemTask::ShowHide => {
overlays.show_hide(&mut app);
}
_ => {}
},
#[cfg(feature = "wayvr")]
TaskType::WayVR(action) => {
wayvr_action(&mut app, &mut overlays, &action);
}
}
}
delete_queue.retain(|(_, frame)| *frame > cur_frame);
let watch = overlays.mut_by_id(watch_id).unwrap(); // want panic
watch.state.transform = watch_transform;
}
Ok(())
}
pub(super) enum CompositionLayer<'a> {
None,
Quad(xr::CompositionLayerQuad<'a, xr::Vulkan>),
Cylinder(xr::CompositionLayerCylinderKHR<'a, xr::Vulkan>),
Equirect2(xr::CompositionLayerEquirect2KHR<'a, xr::Vulkan>),
}

View File

@@ -0,0 +1,301 @@
// Available bindings:
//
// -- click --
// primary click to interact with the watch or overlays. required
//
// -- grab --
// used to manipulate position, size, orientation of overlays in 3D space
//
// -- show_hide --
// used to quickly hide and show your last selection of screens + keyboard
//
// -- space_drag --
// move your stage (playspace drag)
//
// -- toggle_dashboard --
// run or toggle visibility of a previously configured WayVR-compatible dashboard
//
// -- space_rotate --
// rotate your stage (playspace rotate, WIP)
//
// -- space_reset --
// reset your stage (reset the offset from playspace drag)
//
// -- click_modifier_right --
// while this is held, your pointer will turn ORANGE and your mouse clicks will be RIGHT clicks
//
// -- click_modifier_middle --
// while this is held, your pointer will turn PURPLE and your mouse clicks will be MIDDLE clicks
//
// -- move_mouse --
// when using `focus_follows_mouse_mode`, you need to hold this for the mouse to move
//
// -- pose, haptic --
// do not mess with these, unless you know what you're doing
[
// Fallback controller, intended for testing
{
profile: "/interaction_profiles/khr/simple_controller",
pose: {
left: "/user/hand/left/input/aim/pose",
right: "/user/hand/right/input/aim/pose"
},
haptic: {
left: "/user/hand/left/output/haptic",
right: "/user/hand/right/output/haptic"
},
click: {
// left trigger is click
left: "/user/hand/left/input/select/click",
},
grab: {
// right trigger is grab
right: "/user/hand/right/input/select/click"
},
show_hide: {
left: "/user/hand/left/input/menu/click"
}
},
// Oculus Touch Controller. Compatible with Quest 2, Quest 3, Quest Pro
{
profile: "/interaction_profiles/oculus/touch_controller",
pose: {
left: "/user/hand/left/input/aim/pose",
right: "/user/hand/right/input/aim/pose"
},
haptic: {
left: "/user/hand/left/output/haptic",
right: "/user/hand/right/output/haptic"
},
click: {
left: "/user/hand/left/input/trigger/value",
right: "/user/hand/right/input/trigger/value"
},
grab: {
left: "/user/hand/left/input/squeeze/value",
right: "/user/hand/right/input/squeeze/value"
},
scroll: {
left: "/user/hand/left/input/thumbstick/y",
right: "/user/hand/right/input/thumbstick/y"
},
scroll_horizontal: {
left: "/user/hand/left/input/thumbstick/x",
right: "/user/hand/right/input/thumbstick/x"
},
show_hide: {
double_click: true,
left: "/user/hand/left/input/y/click",
},
space_drag: {
left: "/user/hand/left/input/menu/click",
},
space_reset: {
double_click: true,
left: "/user/hand/left/input/menu/click",
},
click_modifier_right: {
left: "/user/hand/left/input/y/touch",
right: "/user/hand/right/input/b/touch"
},
click_modifier_middle: {
left: "/user/hand/left/input/x/touch",
right: "/user/hand/right/input/a/touch"
},
move_mouse: {
// used with focus_follows_mouse_mode
left: "/user/hand/left/input/trigger/touch",
right: "/user/hand/right/input/trigger/touch"
}
},
// Index controller
{
profile: "/interaction_profiles/valve/index_controller",
pose: {
left: "/user/hand/left/input/aim/pose",
right: "/user/hand/right/input/aim/pose"
},
haptic: {
left: "/user/hand/left/output/haptic",
right: "/user/hand/right/output/haptic"
},
click: {
left: "/user/hand/left/input/trigger/value",
right: "/user/hand/right/input/trigger/value"
},
alt_click: {
// left trackpad is space_drag
right: "/user/hand/right/input/trackpad/force",
},
grab: {
left: "/user/hand/left/input/squeeze/force",
right: "/user/hand/right/input/squeeze/force"
},
scroll: {
left: "/user/hand/left/input/thumbstick/y",
right: "/user/hand/right/input/thumbstick/y"
},
scroll_horizontal: {
left: "/user/hand/left/input/thumbstick/x",
right: "/user/hand/right/input/thumbstick/x"
},
toggle_dashboard: {
double_click: false,
right: "/user/hand/right/input/system/click",
},
show_hide: {
double_click: true,
left: "/user/hand/left/input/b/click",
},
space_drag: {
left: "/user/hand/left/input/trackpad/force",
// right trackpad is alt_click
},
space_reset: {
left: "/user/hand/left/input/trackpad/force",
double_click: true,
},
click_modifier_right: {
left: "/user/hand/left/input/b/touch",
right: "/user/hand/right/input/b/touch"
},
click_modifier_middle: {
left: "/user/hand/left/input/a/touch",
right: "/user/hand/right/input/a/touch"
},
move_mouse: {
// used with focus_follows_mouse_mode
left: "/user/hand/left/input/trigger/touch",
right: "/user/hand/right/input/trigger/touch"
}
},
// Vive controller
{
profile: "/interaction_profiles/htc/vive_controller",
pose: {
left: "/user/hand/left/input/aim/pose",
right: "/user/hand/right/input/aim/pose"
},
click: {
left: "/user/hand/left/input/trigger/value",
right: "/user/hand/right/input/trigger/value"
},
grab: {
left: "/user/hand/left/input/squeeze/click",
right: "/user/hand/right/input/squeeze/click"
},
scroll: {
left: "/user/hand/left/input/trackpad/y",
right: "/user/hand/right/input/trackpad/y"
},
scroll_horizontal: {
left: "/user/hand/left/input/trackpad/x",
right: "/user/hand/right/input/trackpad/x"
},
show_hide: {
left: "/user/hand/left/input/menu/click",
},
space_drag: {
right: "/user/hand/right/input/menu/click",
},
space_reset: {
double_click: true,
right: "/user/hand/right/input/menu/click",
},
haptic: {
left: "/user/hand/left/output/haptic",
right: "/user/hand/right/output/haptic"
}
},
// Windows Mixed Reality controller
{
profile: "/interaction_profiles/microsoft/motion_controller",
pose: {
left: "/user/hand/left/input/aim/pose",
right: "/user/hand/right/input/aim/pose"
},
haptic: {
left: "/user/hand/left/output/haptic",
right: "/user/hand/right/output/haptic"
},
click: {
left: "/user/hand/left/input/trigger/value",
right: "/user/hand/right/input/trigger/value"
},
grab: {
left: "/user/hand/left/input/squeeze/click",
right: "/user/hand/right/input/squeeze/click"
},
scroll: {
left: "/user/hand/left/input/thumbstick/y",
right: "/user/hand/right/input/thumbstick/y"
},
scroll_horizontal: {
left: "/user/hand/left/input/thumbstick/x",
right: "/user/hand/right/input/thumbstick/x"
},
show_hide: {
left: "/user/hand/left/input/system/click",
},
space_drag: {
right: "/user/hand/right/input/system/click",
},
space_reset: {
double_click: true,
right: "/user/hand/right/input/system/click",
},
click_modifier_right: {
left: "/user/hand/left/input/trackpad/dpad_up",
right: "/user/hand/right/input/trackpad/dpad_up"
},
click_modifier_middle: {
left: "/user/hand/left/input/trackpad/dpad_down",
right: "/user/hand/right/input/trackpad/dpad_down"
},
},
// HP Reverb G2 controller
{
profile: "/interaction_profiles/hp/mixed_reality_controller",
pose: {
left: "/user/hand/left/input/aim/pose",
right: "/user/hand/right/input/aim/pose"
},
haptic: {
left: "/user/hand/left/output/haptic",
right: "/user/hand/right/output/haptic"
},
click: {
left: "/user/hand/left/input/trigger/value",
right: "/user/hand/right/input/trigger/value"
},
grab: {
left: "/user/hand/left/input/squeeze/value",
right: "/user/hand/right/input/squeeze/value"
},
scroll: {
left: "/user/hand/left/input/thumbstick/y",
right: "/user/hand/right/input/thumbstick/y"
},
scroll_horizontal: {
left: "/user/hand/left/input/thumbstick/x",
right: "/user/hand/right/input/thumbstick/x"
},
show_hide: {
left: "/user/hand/left/input/system/click",
},
space_drag: {
right: "/user/hand/right/input/system/click",
},
space_reset: {
double_click: true,
right: "/user/hand/right/input/system/click",
},
},
]

View File

@@ -0,0 +1,123 @@
use glam::Vec3A;
use openxr::{self as xr, CompositionLayerFlags};
use std::f32::consts::PI;
use xr::EyeVisibility;
use super::{helpers, swapchain::WlxSwapchain, CompositionLayer, XrState};
use crate::{
backend::{
openxr::swapchain::{create_swapchain, SwapchainOpts},
overlay::OverlayData,
},
state::AppState,
};
#[derive(Default)]
pub struct OpenXrOverlayData {
last_visible: bool,
pub(super) swapchain: Option<WlxSwapchain>,
pub(super) init: bool,
pub(super) cur_visible: bool,
pub(super) last_alpha: f32,
}
impl OverlayData<OpenXrOverlayData> {
pub(super) fn ensure_swapchain<'a>(
&'a mut self,
app: &AppState,
xr: &'a XrState,
) -> anyhow::Result<bool> {
if self.data.swapchain.is_some() {
return Ok(true);
}
let Some(meta) = self.frame_meta() else {
log::warn!(
"{}: swapchain cannot be created due to missing metadata",
self.state.name
);
return Ok(false);
};
let extent = meta.extent;
self.data.swapchain = Some(create_swapchain(
xr,
app.gfx.clone(),
extent,
SwapchainOpts::new(),
)?);
Ok(true)
}
pub(super) fn present<'a>(
&'a mut self,
xr: &'a XrState,
) -> anyhow::Result<CompositionLayer<'a>> {
let Some(swapchain) = self.data.swapchain.as_mut() else {
log::warn!("{}: swapchain not ready", self.state.name);
return Ok(CompositionLayer::None);
};
if !swapchain.ever_acquired {
log::warn!("{}: swapchain not rendered", self.state.name);
return Ok(CompositionLayer::None);
}
swapchain.ensure_image_released()?;
let sub_image = swapchain.get_subimage();
let transform = self.state.transform * self.backend.frame_meta().unwrap().transform; // contract
let aspect_ratio = swapchain.extent[1] as f32 / swapchain.extent[0] as f32;
let (scale_x, scale_y) = if aspect_ratio < 1.0 {
let major = transform.matrix3.col(0).length();
(major, major * aspect_ratio)
} else {
let major = transform.matrix3.col(1).length();
(major / aspect_ratio, major)
};
if let Some(curvature) = self.state.curvature {
let radius = scale_x / (2.0 * PI * curvature);
let quat = helpers::transform_to_norm_quat(&transform);
let center_point = transform.translation + quat.mul_vec3a(Vec3A::Z * radius);
let posef = helpers::translation_rotation_to_posef(center_point, quat);
let angle = 2.0 * (scale_x / (2.0 * radius));
let cylinder = xr::CompositionLayerCylinderKHR::new()
.layer_flags(CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA)
.pose(posef)
.sub_image(sub_image)
.eye_visibility(EyeVisibility::BOTH)
.space(&xr.stage)
.radius(radius)
.central_angle(angle)
.aspect_ratio(aspect_ratio);
Ok(CompositionLayer::Cylinder(cylinder))
} else {
let posef = helpers::transform_to_posef(&transform);
let quad = xr::CompositionLayerQuad::new()
.layer_flags(CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA)
.pose(posef)
.sub_image(sub_image)
.eye_visibility(EyeVisibility::BOTH)
.space(&xr.stage)
.size(xr::Extent2Df {
width: scale_x,
height: scale_y,
});
Ok(CompositionLayer::Quad(quad))
}
}
pub(super) fn after_input(&mut self, app: &mut AppState) -> anyhow::Result<()> {
if self.data.last_visible != self.state.want_visible {
if self.state.want_visible {
self.backend.resume(app)?;
} else {
self.backend.pause(app)?;
}
}
self.data.last_visible = self.state.want_visible;
Ok(())
}
}

View File

@@ -0,0 +1,199 @@
use glam::{Affine3A, Quat, Vec3A};
use libmonado::{Monado, Pose, ReferenceSpaceType};
use crate::{
backend::{common::OverlayContainer, input::InputState},
state::AppState,
};
use super::overlay::OpenXrOverlayData;
struct MoverData<T> {
pose: Affine3A,
hand: usize,
hand_pose: T,
}
pub(super) struct PlayspaceMover {
last_transform: Affine3A,
drag: Option<MoverData<Vec3A>>,
rotate: Option<MoverData<Quat>>,
}
impl PlayspaceMover {
pub fn new(monado: &mut Monado) -> anyhow::Result<Self> {
log::info!("Monado: using space offset API");
let Ok(stage) = monado.get_reference_space_offset(ReferenceSpaceType::Stage) else {
anyhow::bail!("Space offsets not supported.");
};
log::debug!("STAGE is at {:?}, {:?}", stage.position, stage.orientation);
// initial offset
let last_transform =
Affine3A::from_rotation_translation(stage.orientation.into(), stage.position.into());
Ok(Self {
last_transform,
drag: None,
rotate: None,
})
}
pub fn update(
&mut self,
overlays: &mut OverlayContainer<OpenXrOverlayData>,
state: &AppState,
monado: &mut Monado,
) {
for pointer in &state.input_state.pointers {
if pointer.now.space_reset {
if !pointer.before.space_reset {
log::info!("Space reset");
self.reset_offset(monado);
}
return;
}
}
if let Some(mut data) = self.rotate.take() {
let pointer = &state.input_state.pointers[data.hand];
if !pointer.now.space_rotate {
self.last_transform = data.pose;
log::info!("End space rotate");
return;
}
let new_hand =
Quat::from_affine3(&(data.pose * state.input_state.pointers[data.hand].raw_pose));
let dq = new_hand * data.hand_pose.conjugate();
let mut space_transform = if state.session.config.space_rotate_unlocked {
Affine3A::from_quat(dq)
} else {
let rel_y = f32::atan2(
2.0 * dq.y.mul_add(dq.w, dq.x * dq.z),
2.0f32.mul_add(dq.w.mul_add(dq.w, dq.x * dq.x), -1.0),
);
Affine3A::from_rotation_y(rel_y)
};
let offset = (space_transform.transform_vector3a(state.input_state.hmd.translation)
- state.input_state.hmd.translation)
* -1.0;
space_transform.translation = offset;
data.pose *= space_transform;
data.hand_pose = new_hand;
apply_offset(data.pose, monado);
self.rotate = Some(data);
} else {
for (i, pointer) in state.input_state.pointers.iter().enumerate() {
if pointer.now.space_rotate {
let hand_pose = Quat::from_affine3(&(self.last_transform * pointer.raw_pose));
self.rotate = Some(MoverData {
pose: self.last_transform,
hand: i,
hand_pose,
});
self.drag = None;
log::info!("Start space rotate");
return;
}
}
}
if let Some(mut data) = self.drag.take() {
let pointer = &state.input_state.pointers[data.hand];
if !pointer.now.space_drag {
self.last_transform = data.pose;
log::info!("End space drag");
return;
}
let new_hand = data
.pose
.transform_point3a(state.input_state.pointers[data.hand].raw_pose.translation);
let relative_pos =
(new_hand - data.hand_pose) * state.session.config.space_drag_multiplier;
if relative_pos.length_squared() > 1000.0 {
log::warn!("Space drag too fast, ignoring");
return;
}
let overlay_offset = data.pose.inverse().transform_vector3a(relative_pos) * -1.0;
overlays.iter_mut().for_each(|overlay| {
if overlay.state.grabbable {
overlay.state.dirty = true;
overlay.state.transform.translation += overlay_offset;
}
});
data.pose.translation += relative_pos;
data.hand_pose = new_hand;
apply_offset(data.pose, monado);
self.drag = Some(data);
} else {
for (i, pointer) in state.input_state.pointers.iter().enumerate() {
if pointer.now.space_drag {
let hand_pos = self
.last_transform
.transform_point3a(pointer.raw_pose.translation);
self.drag = Some(MoverData {
pose: self.last_transform,
hand: i,
hand_pose: hand_pos,
});
log::info!("Start space drag");
return;
}
}
}
}
pub fn reset_offset(&mut self, monado: &mut Monado) {
if self.drag.is_some() {
log::info!("Space drag interrupted by manual reset");
self.drag = None;
}
if self.rotate.is_some() {
log::info!("Space rotate interrupted by manual reset");
self.rotate = None;
}
self.last_transform = Affine3A::IDENTITY;
apply_offset(self.last_transform, monado);
}
pub fn fix_floor(&mut self, input: &InputState, monado: &mut Monado) {
if self.drag.is_some() {
log::info!("Space drag interrupted by fix floor");
self.drag = None;
}
if self.rotate.is_some() {
log::info!("Space rotate interrupted by fix floor");
self.rotate = None;
}
let y1 = input.pointers[0].raw_pose.translation.y;
let y2 = input.pointers[1].raw_pose.translation.y;
let delta = y1.min(y2) - 0.03;
self.last_transform.translation.y += delta;
apply_offset(self.last_transform, monado);
}
}
fn apply_offset(transform: Affine3A, monado: &mut Monado) {
let pose = Pose {
position: transform.translation.into(),
orientation: Quat::from_affine3(&transform).into(),
};
let _ = monado.set_reference_space_offset(ReferenceSpaceType::Stage, pose);
}

View File

@@ -0,0 +1,252 @@
use std::{
f32::consts::PI,
fs::File,
sync::{Arc, LazyLock},
};
use glam::{Quat, Vec3A};
use openxr as xr;
use vulkano::{
command_buffer::CommandBufferUsage,
image::view::ImageView,
pipeline::graphics::{color_blend::AttachmentBlend, input_assembly::PrimitiveTopology},
};
use crate::{
backend::openxr::{helpers::translation_rotation_to_posef, swapchain::SwapchainOpts},
config_io,
graphics::{dds::WlxCommandBufferDds, CommandBuffers, ExtentExt},
state::AppState,
};
use super::{
swapchain::{create_swapchain, WlxSwapchain},
CompositionLayer, XrState,
};
pub(super) struct Skybox {
view: Arc<ImageView>,
sky: Option<WlxSwapchain>,
grid: Option<WlxSwapchain>,
}
impl Skybox {
pub fn new(app: &AppState) -> anyhow::Result<Self> {
let mut command_buffer = app
.gfx
.create_xfer_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
let mut maybe_image = None;
'custom_tex: {
if app.session.config.skybox_texture.is_empty() {
break 'custom_tex;
}
let real_path = config_io::get_config_root().join(&*app.session.config.skybox_texture);
let Ok(f) = File::open(real_path) else {
log::warn!(
"Could not open custom skybox texture at: {}",
app.session.config.skybox_texture
);
break 'custom_tex;
};
match command_buffer.upload_image_dds(f) {
Ok(image) => {
maybe_image = Some(image);
}
Err(e) => {
log::warn!(
"Could not use custom skybox texture at: {}",
app.session.config.skybox_texture
);
log::warn!("{e:?}");
}
}
}
if maybe_image.is_none() {
let p = include_bytes!("../../res/table_mountain_2.dds");
maybe_image = Some(command_buffer.upload_image_dds(p.as_slice())?);
}
command_buffer.build_and_execute_now()?;
let view = ImageView::new_default(maybe_image.unwrap())?; // safe unwrap
Ok(Self {
view,
sky: None,
grid: None,
})
}
fn prepare_sky<'a>(
&'a mut self,
xr: &'a XrState,
app: &AppState,
buf: &mut CommandBuffers,
) -> anyhow::Result<()> {
if self.sky.is_some() {
return Ok(());
}
let opts = SwapchainOpts::new().immutable();
let extent = self.view.image().extent();
let mut swapchain = create_swapchain(xr, app.gfx.clone(), extent, opts)?;
let tgt = swapchain.acquire_wait_image()?;
let pipeline = app.gfx.create_pipeline(
app.gfx_extras.shaders.get("vert_quad").unwrap().clone(), // want panic
app.gfx_extras.shaders.get("frag_srgb").unwrap().clone(), // want panic
app.gfx.surface_format,
None,
PrimitiveTopology::TriangleStrip,
false,
)?;
let set0 = pipeline.uniform_sampler(0, self.view.clone(), app.gfx.texture_filter)?;
let set1 = pipeline.uniform_buffer_upload(1, vec![1f32])?;
let pass = pipeline.create_pass(
tgt.extent_f32(),
app.gfx_extras.quad_verts.clone(),
0..4,
0..1,
vec![set0, set1],
)?;
let mut cmd_buffer = app
.gfx
.create_gfx_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
cmd_buffer.begin_rendering(tgt)?;
cmd_buffer.run_ref(&pass)?;
cmd_buffer.end_rendering()?;
buf.push(cmd_buffer.build()?);
self.sky = Some(swapchain);
Ok(())
}
fn prepare_grid<'a>(
&'a mut self,
xr: &'a XrState,
app: &AppState,
buf: &mut CommandBuffers,
) -> anyhow::Result<()> {
if self.grid.is_some() {
return Ok(());
}
let extent = [1024, 1024, 1];
let mut swapchain = create_swapchain(
xr,
app.gfx.clone(),
extent,
SwapchainOpts::new().immutable(),
)?;
let pipeline = app.gfx.create_pipeline(
app.gfx_extras.shaders.get("vert_quad").unwrap().clone(), // want panic
app.gfx_extras.shaders.get("frag_grid").unwrap().clone(), // want panic
app.gfx.surface_format,
Some(AttachmentBlend::alpha()),
PrimitiveTopology::TriangleStrip,
false,
)?;
let tgt = swapchain.acquire_wait_image()?;
let pass = pipeline.create_pass(
tgt.extent_f32(),
app.gfx_extras.quad_verts.clone(),
0..4,
0..1,
vec![],
)?;
let mut cmd_buffer = app
.gfx
.create_gfx_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
cmd_buffer.begin_rendering(tgt)?;
cmd_buffer.run_ref(&pass)?;
cmd_buffer.end_rendering()?;
buf.push(cmd_buffer.build()?);
self.grid = Some(swapchain);
Ok(())
}
pub(super) fn render(
&mut self,
xr: &XrState,
app: &AppState,
buf: &mut CommandBuffers,
) -> anyhow::Result<()> {
self.prepare_sky(xr, app, buf)?;
self.prepare_grid(xr, app, buf)?;
Ok(())
}
pub(super) fn present<'a>(
&'a mut self,
xr: &'a XrState,
app: &AppState,
) -> anyhow::Result<Vec<CompositionLayer<'a>>> {
// cover the entire sphere
const HORIZ_ANGLE: f32 = 2.0 * PI;
const HI_VERT_ANGLE: f32 = 0.5 * PI;
const LO_VERT_ANGLE: f32 = -0.5 * PI;
static GRID_POSE: LazyLock<xr::Posef> = LazyLock::new(|| {
translation_rotation_to_posef(Vec3A::ZERO, Quat::from_rotation_x(PI * -0.5))
});
let pose = xr::Posef {
orientation: xr::Quaternionf::IDENTITY,
position: xr::Vector3f {
x: app.input_state.hmd.translation.x,
y: app.input_state.hmd.translation.y,
z: app.input_state.hmd.translation.z,
},
};
self.sky.as_mut().unwrap().ensure_image_released()?;
let sky = xr::CompositionLayerEquirect2KHR::new()
.layer_flags(xr::CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA)
.pose(pose)
.radius(10.0)
.sub_image(self.sky.as_ref().unwrap().get_subimage())
.eye_visibility(xr::EyeVisibility::BOTH)
.space(&xr.stage)
.central_horizontal_angle(HORIZ_ANGLE)
.upper_vertical_angle(HI_VERT_ANGLE)
.lower_vertical_angle(LO_VERT_ANGLE);
self.grid.as_mut().unwrap().ensure_image_released()?;
let grid = xr::CompositionLayerQuad::new()
.layer_flags(xr::CompositionLayerFlags::BLEND_TEXTURE_SOURCE_ALPHA)
.pose(*GRID_POSE)
.size(xr::Extent2Df {
width: 10.0,
height: 10.0,
})
.sub_image(self.grid.as_ref().unwrap().get_subimage())
.eye_visibility(xr::EyeVisibility::BOTH)
.space(&xr.stage);
Ok(vec![
CompositionLayer::Equirect2(sky),
CompositionLayer::Quad(grid),
])
}
}
pub(super) fn create_skybox(xr: &XrState, app: &AppState) -> Option<Skybox> {
if !app.session.config.use_skybox {
return None;
}
xr.instance
.exts()
.khr_composition_layer_equirect2
.and_then(|_| Skybox::new(app).ok())
}

View File

@@ -0,0 +1,125 @@
use std::sync::Arc;
use ash::vk;
use openxr as xr;
use smallvec::SmallVec;
use vulkano::{
image::{sys::RawImage, view::ImageView, ImageCreateInfo, ImageUsage},
Handle,
};
use wgui::gfx::WGfx;
use super::XrState;
#[derive(Default)]
pub(super) struct SwapchainOpts {
pub immutable: bool,
}
impl SwapchainOpts {
pub fn new() -> Self {
Self::default()
}
pub const fn immutable(mut self) -> Self {
self.immutable = true;
self
}
}
pub(super) fn create_swapchain(
xr: &XrState,
gfx: Arc<WGfx>,
extent: [u32; 3],
opts: SwapchainOpts,
) -> anyhow::Result<WlxSwapchain> {
let create_flags = if opts.immutable {
xr::SwapchainCreateFlags::STATIC_IMAGE
} else {
xr::SwapchainCreateFlags::EMPTY
};
let swapchain = xr.session.create_swapchain(&xr::SwapchainCreateInfo {
create_flags,
usage_flags: xr::SwapchainUsageFlags::COLOR_ATTACHMENT | xr::SwapchainUsageFlags::SAMPLED,
format: gfx.surface_format as _,
sample_count: 1,
width: extent[0],
height: extent[1],
face_count: 1,
array_size: 1,
mip_count: 1,
})?;
let images = swapchain
.enumerate_images()?
.into_iter()
.map(|handle| {
let vk_image = vk::Image::from_raw(handle);
// thanks @yshui
let raw_image = unsafe {
RawImage::from_handle_borrowed(
gfx.device.clone(),
vk_image,
ImageCreateInfo {
format: gfx.surface_format as _,
extent,
usage: ImageUsage::COLOR_ATTACHMENT,
..Default::default()
},
)?
};
// SAFETY: OpenXR guarantees that the image is a swapchain image, thus has memory backing it.
let image = Arc::new(unsafe { raw_image.assume_bound() });
Ok(ImageView::new_default(image)?)
})
.collect::<anyhow::Result<SmallVec<[Arc<ImageView>; 4]>>>()?;
Ok(WlxSwapchain {
acquired: false,
ever_acquired: false,
swapchain,
images,
extent,
})
}
pub(super) struct WlxSwapchain {
acquired: bool,
pub(super) ever_acquired: bool,
pub(super) swapchain: xr::Swapchain<xr::Vulkan>,
pub(super) extent: [u32; 3],
pub(super) images: SmallVec<[Arc<ImageView>; 4]>,
}
impl WlxSwapchain {
pub(super) fn acquire_wait_image(&mut self) -> anyhow::Result<Arc<ImageView>> {
let idx = self.swapchain.acquire_image()? as usize;
self.swapchain.wait_image(xr::Duration::INFINITE)?;
self.ever_acquired = true;
self.acquired = true;
Ok(self.images[idx].clone())
}
pub(super) fn ensure_image_released(&mut self) -> anyhow::Result<()> {
if self.acquired {
self.swapchain.release_image()?;
self.acquired = false;
}
Ok(())
}
pub(super) fn get_subimage(&self) -> xr::SwapchainSubImage<xr::Vulkan> {
debug_assert!(self.ever_acquired, "swapchain was never acquired!");
xr::SwapchainSubImage::new()
.swapchain(&self.swapchain)
.image_rect(xr::Rect2Di {
offset: xr::Offset2Di { x: 0, y: 0 },
extent: xr::Extent2Di {
width: self.extent[0] as _,
height: self.extent[1] as _,
},
})
.image_array_index(0)
}
}

View File

@@ -0,0 +1,183 @@
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket},
time::Instant,
};
use anyhow::bail;
use rosc::{OscMessage, OscPacket, OscType};
use crate::overlays::{keyboard::KEYBOARD_NAME, watch::WATCH_NAME};
use crate::backend::input::TrackedDeviceRole;
use super::{common::OverlayContainer, input::TrackedDevice};
pub struct OscSender {
last_sent_overlay: Instant,
last_sent_battery: Instant,
upstream: UdpSocket,
}
impl OscSender {
pub fn new(send_port: u16) -> anyhow::Result<Self> {
let ip = IpAddr::V4(Ipv4Addr::LOCALHOST);
let Ok(upstream) = UdpSocket::bind("0.0.0.0:0") else {
bail!("Failed to bind UDP socket - OSC will not function.");
};
let Ok(()) = upstream.connect(SocketAddr::new(ip, send_port)) else {
bail!("Failed to connect UDP socket - OSC will not function.");
};
Ok(Self {
upstream,
last_sent_overlay: Instant::now(),
last_sent_battery: Instant::now(),
})
}
pub fn send_message(&self, addr: String, args: Vec<OscType>) -> anyhow::Result<()> {
let packet = OscPacket::Message(OscMessage { addr, args });
let Ok(bytes) = rosc::encoder::encode(&packet) else {
bail!("Could not encode OSC packet.");
};
let Ok(_) = self.upstream.send(&bytes) else {
bail!("Could not send OSC packet.");
};
Ok(())
}
pub fn send_params<D>(
&mut self,
overlays: &OverlayContainer<D>,
devices: &Vec<TrackedDevice>,
) -> anyhow::Result<()>
where
D: Default,
{
// send overlay data every 0.1 seconds
if self.last_sent_overlay.elapsed().as_millis() >= 100 {
self.last_sent_overlay = Instant::now();
let mut num_overlays = 0;
let mut has_keyboard = false;
let mut has_wrist = false;
for o in overlays.iter() {
if !o.state.want_visible {
continue;
}
match o.state.name.as_ref() {
WATCH_NAME => has_wrist = true,
KEYBOARD_NAME => has_keyboard = true,
_ => {
if o.state.interactable {
num_overlays += 1;
}
}
}
}
self.send_message(
"/avatar/parameters/isOverlayOpen".into(),
vec![OscType::Bool(num_overlays > 0)],
)?;
self.send_message(
"/avatar/parameters/isKeyboardOpen".into(),
vec![OscType::Bool(has_keyboard)],
)?;
self.send_message(
"/avatar/parameters/isWristVisible".into(),
vec![OscType::Bool(has_wrist)],
)?;
self.send_message(
"/avatar/parameters/openOverlayCount".into(),
vec![OscType::Int(num_overlays)],
)?;
}
// send battery levels every 10 seconds
if self.last_sent_battery.elapsed().as_millis() >= 10000 {
self.last_sent_battery = Instant::now();
let mut tracker_count: i8 = 0;
let mut controller_count: i8 = 0;
let mut tracker_total_bat = 0.0;
let mut controller_total_bat = 0.0;
for device in devices {
let tracker_param;
// soc is the battery level (set to device status.charge)
let level = device.soc.unwrap_or(-1.0);
let parameter = match device.role {
TrackedDeviceRole::None => continue,
TrackedDeviceRole::Hmd => {
// legacy OVR Toolkit style (int)
// as of 20 Nov 2024 OVR Toolkit uses int 0-100, but this may change in a future update.
//TODO: update this once their implementation matches their docs
self.send_message(
"/avatar/parameters/hmdBattery".into(),
vec![OscType::Int((level * 100.0f32).round() as i32)],
)?;
"headset"
}
TrackedDeviceRole::LeftHand => {
controller_count += 1;
controller_total_bat += level;
"leftController"
}
TrackedDeviceRole::RightHand => {
controller_count += 1;
controller_total_bat += level;
"rightController"
}
TrackedDeviceRole::Tracker => {
tracker_count += 1;
tracker_total_bat += level;
tracker_param = format!("tracker{tracker_count}");
tracker_param.as_str()
}
};
// send device battery parameters
self.send_message(
format!("/avatar/parameters/{parameter}Battery"),
vec![OscType::Float(level)],
)?;
self.send_message(
format!("/avatar/parameters/{parameter}Charging"),
vec![OscType::Bool(device.charging)],
)?;
}
// send average controller and tracker battery parameters
self.send_message(
String::from("/avatar/parameters/averageControllerBattery"),
vec![OscType::Float(
controller_total_bat / f32::from(controller_count),
)],
)?;
self.send_message(
String::from("/avatar/parameters/averageTrackerBattery"),
vec![OscType::Float(tracker_total_bat / f32::from(tracker_count))],
)?;
}
Ok(())
}
pub fn send_single_param(
&mut self,
parameter: String,
values: Vec<OscType>,
) -> anyhow::Result<()> {
self.send_message(parameter, values)?;
Ok(())
}
}

View File

@@ -0,0 +1,465 @@
use std::{
f32::consts::PI,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
use anyhow::Ok;
use glam::{Affine2, Affine3A, Mat3A, Quat, Vec2, Vec3, Vec3A};
use serde::Deserialize;
use vulkano::{format::Format, image::view::ImageView};
use crate::{
config::AStrMapExt,
graphics::CommandBuffers,
state::{AppState, KeyboardFocus},
};
use super::{
common::snap_upright,
input::{DummyInteractionHandler, Haptics, InteractionHandler, PointerHit},
};
static OVERLAY_AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(0);
pub trait OverlayBackend: OverlayRenderer + InteractionHandler {
fn set_renderer(&mut self, renderer: Box<dyn OverlayRenderer>);
fn set_interaction(&mut self, interaction: Box<dyn InteractionHandler>);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Default)]
pub struct OverlayID(pub usize);
pub const Z_ORDER_TOAST: u32 = 70;
pub const Z_ORDER_LINES: u32 = 69;
pub const Z_ORDER_WATCH: u32 = 68;
pub const Z_ORDER_ANCHOR: u32 = 67;
pub const Z_ORDER_DEFAULT: u32 = 0;
pub const Z_ORDER_DASHBOARD: u32 = Z_ORDER_DEFAULT;
pub struct OverlayState {
pub id: OverlayID,
pub name: Arc<str>,
pub want_visible: bool,
pub show_hide: bool,
pub grabbable: bool,
pub interactable: bool,
pub recenter: bool,
pub keyboard_focus: Option<KeyboardFocus>,
pub dirty: bool,
pub alpha: f32,
pub z_order: u32,
pub transform: Affine3A,
pub spawn_scale: f32, // aka width
pub spawn_point: Vec3A,
pub spawn_rotation: Quat,
pub saved_transform: Option<Affine3A>,
pub positioning: Positioning,
pub curvature: Option<f32>,
pub interaction_transform: Affine2,
pub birthframe: usize,
}
impl Default for OverlayState {
fn default() -> Self {
Self {
id: OverlayID(OVERLAY_AUTO_INCREMENT.fetch_add(1, Ordering::Relaxed)),
name: Arc::from(""),
want_visible: false,
show_hide: false,
grabbable: false,
recenter: false,
interactable: false,
keyboard_focus: None,
dirty: true,
alpha: 1.0,
z_order: Z_ORDER_DEFAULT,
positioning: Positioning::Floating,
curvature: None,
spawn_scale: 1.0,
spawn_point: Vec3A::NEG_Z,
spawn_rotation: Quat::IDENTITY,
saved_transform: None,
transform: Affine3A::IDENTITY,
interaction_transform: Affine2::IDENTITY,
birthframe: 0,
}
}
}
pub struct OverlayData<T>
where
T: Default,
{
pub state: OverlayState,
pub backend: Box<dyn OverlayBackend>,
pub primary_pointer: Option<usize>,
pub data: T,
}
impl<T> Default for OverlayData<T>
where
T: Default,
{
fn default() -> Self {
Self {
state: OverlayState::default(),
backend: Box::<SplitOverlayBackend>::default(),
primary_pointer: None,
data: Default::default(),
}
}
}
impl OverlayState {
fn get_transform(&self) -> Affine3A {
self.saved_transform.unwrap_or_else(|| {
Affine3A::from_scale_rotation_translation(
Vec3::ONE * self.spawn_scale,
self.spawn_rotation,
self.spawn_point.into(),
)
})
}
pub fn auto_movement(&mut self, app: &mut AppState) {
let (target_transform, lerp) = match self.positioning {
Positioning::FollowHead { lerp } => (app.input_state.hmd * self.get_transform(), lerp),
Positioning::FollowHand { hand, lerp } => (
app.input_state.pointers[hand].pose * self.get_transform(),
lerp,
),
_ => return,
};
self.transform = match lerp {
1.0 => target_transform,
lerp => {
let scale = target_transform.matrix3.x_axis.length();
let rot_from = Quat::from_mat3a(&self.transform.matrix3.div_scalar(scale));
let rot_to = Quat::from_mat3a(&target_transform.matrix3.div_scalar(scale));
let rotation = rot_from.slerp(rot_to, lerp);
let translation = self
.transform
.translation
.slerp(target_transform.translation, lerp);
Affine3A::from_scale_rotation_translation(
Vec3::ONE * scale,
rotation,
translation.into(),
)
}
};
self.dirty = true;
}
pub fn reset(&mut self, app: &mut AppState, hard_reset: bool) {
let parent_transform = match self.positioning {
Positioning::Floating
| Positioning::FollowHead { .. }
| Positioning::FollowHeadPaused { .. } => app.input_state.hmd,
Positioning::FollowHand { hand, .. } | Positioning::FollowHandPaused { hand, .. } => {
app.input_state.pointers[hand].pose
}
Positioning::Anchored => app.anchor,
Positioning::Static => return,
};
if hard_reset {
self.saved_transform = None;
}
self.transform = parent_transform * self.get_transform();
if self.grabbable && hard_reset {
self.realign(&app.input_state.hmd);
}
self.dirty = true;
}
pub fn save_transform(&mut self, app: &mut AppState) -> bool {
let parent_transform = match self.positioning {
Positioning::Floating => snap_upright(app.input_state.hmd, Vec3A::Y),
Positioning::FollowHead { .. } | Positioning::FollowHeadPaused { .. } => {
app.input_state.hmd
}
Positioning::FollowHand { hand, .. } | Positioning::FollowHandPaused { hand, .. } => {
app.input_state.pointers[hand].pose
}
Positioning::Anchored => snap_upright(app.anchor, Vec3A::Y),
Positioning::Static => return false,
};
self.saved_transform = Some(parent_transform.inverse() * self.transform);
true
}
pub fn realign(&mut self, hmd: &Affine3A) {
let to_hmd = hmd.translation - self.transform.translation;
let up_dir: Vec3A;
if hmd.x_axis.dot(Vec3A::Y).abs() > 0.2 {
// Snap upright
up_dir = hmd.y_axis;
} else {
let dot = to_hmd.normalize().dot(hmd.z_axis);
let z_dist = to_hmd.length();
let y_dist = (self.transform.translation.y - hmd.translation.y).abs();
let x_angle = (y_dist / z_dist).asin();
if dot < -f32::EPSILON {
// facing down
let up_point = hmd.translation + z_dist / x_angle.cos() * Vec3A::Y;
up_dir = (up_point - self.transform.translation).normalize();
} else if dot > f32::EPSILON {
// facing up
let dn_point = hmd.translation + z_dist / x_angle.cos() * Vec3A::NEG_Y;
up_dir = (self.transform.translation - dn_point).normalize();
} else {
// perfectly upright
up_dir = Vec3A::Y;
}
}
let scale = self.transform.x_axis.length();
let col_z = (self.transform.translation - hmd.translation).normalize();
let col_y = up_dir;
let col_x = col_y.cross(col_z);
let col_y = col_z.cross(col_x).normalize();
let col_x = col_x.normalize();
let rot = Mat3A::from_quat(self.spawn_rotation)
* Mat3A::from_quat(Quat::from_axis_angle(Vec3::Y, PI));
self.transform.matrix3 = Mat3A::from_cols(col_x, col_y, col_z).mul_scalar(scale) * rot;
}
}
impl<T> OverlayData<T>
where
T: Default,
{
pub fn init(&mut self, app: &mut AppState) -> anyhow::Result<()> {
if self.state.curvature.is_none() {
self.state.curvature = app
.session
.config
.curve_values
.arc_get(self.state.name.as_ref())
.copied();
}
if matches!(
self.state.positioning,
Positioning::Floating | Positioning::Anchored
) {
let hard_reset;
if let Some(transform) = app
.session
.config
.transform_values
.arc_get(self.state.name.as_ref())
{
self.state.saved_transform = Some(*transform);
hard_reset = false;
} else {
hard_reset = true;
}
self.state.reset(app, hard_reset);
}
self.backend.init(app)
}
pub fn should_render(&mut self, app: &mut AppState) -> anyhow::Result<ShouldRender> {
self.backend.should_render(app)
}
pub fn render(
&mut self,
app: &mut AppState,
tgt: Arc<ImageView>,
buf: &mut CommandBuffers,
alpha: f32,
) -> anyhow::Result<bool> {
self.backend.render(app, tgt, buf, alpha)
}
pub fn frame_meta(&mut self) -> Option<FrameMeta> {
self.backend.frame_meta()
}
}
#[derive(Default, Clone, Copy)]
pub struct FrameMeta {
pub extent: [u32; 3],
pub transform: Affine3A,
pub format: Format,
}
pub enum ShouldRender {
/// The overlay is dirty and needs to be rendered.
Should,
/// The overlay is not dirty but is ready to be rendered.
Can,
/// The overlay is not ready to be rendered.
Unable,
}
pub trait OverlayRenderer {
/// Called once, before the first frame is rendered
fn init(&mut self, app: &mut AppState) -> anyhow::Result<()>;
fn pause(&mut self, app: &mut AppState) -> anyhow::Result<()>;
fn resume(&mut self, app: &mut AppState) -> anyhow::Result<()>;
/// Called when the presentation layer is ready to present a new frame
fn should_render(&mut self, app: &mut AppState) -> anyhow::Result<ShouldRender>;
/// Called when the contents need to be rendered to the swapchain
fn render(
&mut self,
app: &mut AppState,
tgt: Arc<ImageView>,
buf: &mut CommandBuffers,
alpha: f32,
) -> anyhow::Result<bool>;
/// Called to retrieve the effective extent of the image
/// Used for creating swapchains.
///
/// Must be true if should_render was also true on the same frame.
fn frame_meta(&mut self) -> Option<FrameMeta>;
}
pub struct FallbackRenderer;
impl OverlayRenderer for FallbackRenderer {
fn init(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
Ok(())
}
fn pause(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
Ok(())
}
fn resume(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
Ok(())
}
fn should_render(&mut self, _app: &mut AppState) -> anyhow::Result<ShouldRender> {
Ok(ShouldRender::Unable)
}
fn render(
&mut self,
_app: &mut AppState,
_tgt: Arc<ImageView>,
_buf: &mut CommandBuffers,
_alpha: f32,
) -> anyhow::Result<bool> {
Ok(false)
}
fn frame_meta(&mut self) -> Option<FrameMeta> {
None
}
}
// Boilerplate and dummies
#[derive(Clone, Copy, Debug, Default)]
pub enum Positioning {
/// Stays in place unless recentered, recenters relative to HMD
#[default]
Floating,
/// Stays in place unless recentered, recenters relative to anchor
Anchored,
/// Following HMD
FollowHead { lerp: f32 },
/// Normally follows HMD, but paused due to interaction
FollowHeadPaused { lerp: f32 },
/// Following hand
FollowHand { hand: usize, lerp: f32 },
/// Normally follows hand, but paused due to interaction
FollowHandPaused { hand: usize, lerp: f32 },
/// Stays in place, no recentering
Static,
}
pub struct SplitOverlayBackend {
pub renderer: Box<dyn OverlayRenderer>,
pub interaction: Box<dyn InteractionHandler>,
}
impl Default for SplitOverlayBackend {
fn default() -> Self {
Self {
renderer: Box::new(FallbackRenderer),
interaction: Box::new(DummyInteractionHandler),
}
}
}
impl OverlayBackend for SplitOverlayBackend {
fn set_renderer(&mut self, renderer: Box<dyn OverlayRenderer>) {
self.renderer = renderer;
}
fn set_interaction(&mut self, interaction: Box<dyn InteractionHandler>) {
self.interaction = interaction;
}
}
impl OverlayRenderer for SplitOverlayBackend {
fn init(&mut self, app: &mut AppState) -> anyhow::Result<()> {
self.renderer.init(app)
}
fn pause(&mut self, app: &mut AppState) -> anyhow::Result<()> {
self.renderer.pause(app)
}
fn resume(&mut self, app: &mut AppState) -> anyhow::Result<()> {
self.renderer.resume(app)
}
fn should_render(&mut self, app: &mut AppState) -> anyhow::Result<ShouldRender> {
self.renderer.should_render(app)
}
fn render(
&mut self,
app: &mut AppState,
tgt: Arc<ImageView>,
buf: &mut CommandBuffers,
alpha: f32,
) -> anyhow::Result<bool> {
self.renderer.render(app, tgt, buf, alpha)
}
fn frame_meta(&mut self) -> Option<FrameMeta> {
self.renderer.frame_meta()
}
}
impl InteractionHandler for SplitOverlayBackend {
fn on_left(&mut self, app: &mut AppState, pointer: usize) {
self.interaction.on_left(app, pointer);
}
fn on_hover(&mut self, app: &mut AppState, hit: &PointerHit) -> Option<Haptics> {
self.interaction.on_hover(app, hit)
}
fn on_scroll(&mut self, app: &mut AppState, hit: &PointerHit, delta_y: f32, delta_x: f32) {
self.interaction.on_scroll(app, hit, delta_y, delta_x);
}
fn on_pointer(&mut self, app: &mut AppState, hit: &PointerHit, pressed: bool) {
self.interaction.on_pointer(app, hit, pressed);
}
}
pub fn ui_transform(extent: [u32; 2]) -> Affine2 {
let aspect = extent[0] as f32 / extent[1] as f32;
let scale = if aspect < 1.0 {
Vec2 {
x: 1.0 / aspect,
y: -1.0,
}
} else {
Vec2 {
x: 1.0,
y: -1.0 * aspect,
}
};
let center = Vec2 { x: 0.5, y: 0.5 };
Affine2::from_scale_angle_translation(scale, 0.0, center)
}

View File

@@ -0,0 +1,118 @@
use std::{
cmp,
collections::{BinaryHeap, VecDeque},
sync::atomic::{self, AtomicUsize},
time::Instant,
};
use serde::Deserialize;
use crate::state::AppState;
#[cfg(feature = "wayvr")]
use crate::backend::wayvr::WayVRAction;
use super::{
common::OverlaySelector,
overlay::{OverlayBackend, OverlayState},
};
static TASK_AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(0);
struct AppTask {
pub not_before: Instant,
pub id: usize,
pub task: TaskType,
}
impl PartialEq<Self> for AppTask {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == cmp::Ordering::Equal
}
}
impl PartialOrd<Self> for AppTask {
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Eq for AppTask {}
impl Ord for AppTask {
fn cmp(&self, other: &Self) -> cmp::Ordering {
self.not_before
.cmp(&other.not_before)
.then(self.id.cmp(&other.id))
.reverse()
}
}
pub enum SystemTask {
ColorGain(ColorChannel, f32),
ResetPlayspace,
FixFloor,
ShowHide,
}
pub type OverlayTask = dyn FnOnce(&mut AppState, &mut OverlayState) + Send;
pub type CreateOverlayTask =
dyn FnOnce(&mut AppState) -> Option<(OverlayState, Box<dyn OverlayBackend>)> + Send;
pub enum TaskType {
Overlay(OverlaySelector, Box<OverlayTask>),
CreateOverlay(OverlaySelector, Box<CreateOverlayTask>),
DropOverlay(OverlaySelector),
System(SystemTask),
#[cfg(feature = "wayvr")]
WayVR(WayVRAction),
}
#[derive(Deserialize, Clone, Copy)]
pub enum ColorChannel {
R,
G,
B,
All,
}
pub struct TaskContainer {
tasks: BinaryHeap<AppTask>,
}
impl TaskContainer {
pub const fn new() -> Self {
Self {
tasks: BinaryHeap::new(),
}
}
pub fn enqueue(&mut self, task: TaskType) {
self.tasks.push(AppTask {
not_before: Instant::now(),
id: TASK_AUTO_INCREMENT.fetch_add(1, atomic::Ordering::Relaxed),
task,
});
}
/// Enqueue a task to be executed at a specific time.
/// If the time is in the past, the task will be executed immediately.
/// Multiple tasks enqueued for the same instant will be executed in order of submission.
pub fn enqueue_at(&mut self, task: TaskType, not_before: Instant) {
self.tasks.push(AppTask {
not_before,
id: TASK_AUTO_INCREMENT.fetch_add(1, atomic::Ordering::Relaxed),
task,
});
}
pub fn retrieve_due(&mut self, dest_buf: &mut VecDeque<TaskType>) {
let now = Instant::now();
while let Some(task) = self.tasks.peek() {
if task.not_before > now {
break;
}
// Safe unwrap because we peeked.
dest_buf.push_back(self.tasks.pop().unwrap().task);
}
}
}

View File

@@ -0,0 +1,252 @@
use std::{io::Read, os::unix::net::UnixStream, path::PathBuf, sync::Arc};
use smithay::{
backend::input::Keycode,
input::{keyboard::KeyboardHandle, pointer::PointerHandle},
reexports::wayland_server,
utils::SerialCounter,
};
use crate::backend::wayvr::{ExternalProcessRequest, WayVRTask};
use super::{
comp::{self, ClientState},
display, process, ProcessWayVREnv,
};
pub struct WayVRClient {
pub client: wayland_server::Client,
pub display_handle: display::DisplayHandle,
pub pid: u32,
}
pub struct WayVRCompositor {
pub state: comp::Application,
pub seat_keyboard: KeyboardHandle<comp::Application>,
pub seat_pointer: PointerHandle<comp::Application>,
pub serial_counter: SerialCounter,
pub wayland_env: super::WaylandEnv,
display: wayland_server::Display<comp::Application>,
listener: wayland_server::ListeningSocket,
toplevel_surf_count: u32, // for logging purposes
pub clients: Vec<WayVRClient>,
}
fn get_wayvr_env_from_pid(pid: i32) -> anyhow::Result<ProcessWayVREnv> {
let path = format!("/proc/{pid}/environ");
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" {
env.display_auth = Some(String::from(value));
} else if key == "WAYVR_DISPLAY_NAME" {
env.display_name = Some(String::from(value));
}
}
}
Ok(env)
}
impl WayVRCompositor {
pub fn new(
state: comp::Application,
display: wayland_server::Display<comp::Application>,
seat_keyboard: KeyboardHandle<comp::Application>,
seat_pointer: PointerHandle<comp::Application>,
) -> anyhow::Result<Self> {
let (wayland_env, listener) = create_wayland_listener()?;
Ok(Self {
state,
display,
seat_keyboard,
seat_pointer,
listener,
wayland_env,
serial_counter: SerialCounter::new(),
clients: Vec::new(),
toplevel_surf_count: 0,
})
}
pub fn add_client(&mut self, client: WayVRClient) {
self.clients.push(client);
}
pub fn cleanup_clients(&mut self) {
self.clients.retain(|client| {
let Some(data) = client.client.get_data::<ClientState>() else {
return false;
};
if *data.disconnected.lock().unwrap() {
return false;
}
true
});
}
fn accept_connection(
&mut self,
stream: UnixStream,
displays: &mut display::DisplayVec,
processes: &mut process::ProcessVec,
) -> anyhow::Result<()> {
let client = self
.display
.handle()
.insert_client(stream, Arc::new(comp::ClientState::default()))
.unwrap();
let creds = client.get_credentials(&self.display.handle())?;
let process_env = get_wayvr_env_from_pid(creds.pid)?;
// Find suitable auth key from the process list
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.add_client(WayVRClient {
client,
display_handle: process.display_handle,
pid: creds.pid as u32,
});
return Ok(());
}
}
}
}
}
// 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(
&mut self,
displays: &mut display::DisplayVec,
processes: &mut process::ProcessVec,
) -> anyhow::Result<()> {
if let Some(stream) = self.listener.accept()? {
if let Err(e) = self.accept_connection(stream, displays, processes) {
log::error!("Failed to accept connection: {e}");
}
}
Ok(())
}
pub fn tick_wayland(
&mut self,
displays: &mut display::DisplayVec,
processes: &mut process::ProcessVec,
) -> anyhow::Result<()> {
if let Err(e) = self.accept_connections(displays, processes) {
log::error!("accept_connections failed: {e}");
}
self.display.dispatch_clients(&mut self.state)?;
self.display.flush_clients()?;
let surf_count = self.state.xdg_shell.toplevel_surfaces().len() as u32;
if surf_count != self.toplevel_surf_count {
self.toplevel_surf_count = surf_count;
log::info!("Toplevel surface count changed: {surf_count}");
}
Ok(())
}
pub fn send_key(&mut self, virtual_key: u32, down: bool) {
let state = if down {
smithay::backend::input::KeyState::Pressed
} else {
smithay::backend::input::KeyState::Released
};
self.seat_keyboard.input::<(), _>(
&mut self.state,
Keycode::new(virtual_key),
state,
self.serial_counter.next_serial(),
0,
|_, _, _| smithay::input::keyboard::FilterResult::Forward,
);
}
}
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_or_else(|_| PathBuf::from("/tmp"), PathBuf::from);
path.push("wayvr.disp");
std::fs::write(path, format!("{display_num}\n"))?;
Ok(())
}
fn create_wayland_listener() -> anyhow::Result<(super::WaylandEnv, wayland_server::ListeningSocket)>
{
let mut env = super::WaylandEnv {
display_num: STARTING_WAYLAND_ADDR_IDX,
};
let listener = loop {
let display_str = env.display_num_string();
log::debug!("Trying to open socket \"{display_str}\"");
match wayland_server::ListeningSocket::bind(display_str.as_str()) {
Ok(listener) => {
log::debug!("Listening to {display_str}");
break listener;
}
Err(e) => {
log::debug!(
"Failed to open socket \"{display_str}\" (reason: {e}), trying next..."
);
env.display_num += 1;
if env.display_num > STARTING_WAYLAND_ADDR_IDX + 20 {
// Highly unlikely for the user to have 20 Wayland displays enabled at once. Return error instead.
anyhow::bail!("Failed to create wayland-server socket")
}
}
}
};
let _ = export_display_number(env.display_num);
Ok((env, listener))
}

View File

@@ -0,0 +1,236 @@
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::utils::on_commit_buffer_handler;
use smithay::backend::renderer::ImportDma;
use smithay::input::{Seat, SeatHandler, SeatState};
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
use smithay::reexports::wayland_server;
use smithay::reexports::wayland_server::protocol::{wl_buffer, wl_seat, wl_surface};
use smithay::reexports::wayland_server::Resource;
use smithay::wayland::buffer::BufferHandler;
use smithay::wayland::dmabuf::{
DmabufFeedback, DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier,
};
use smithay::wayland::output::OutputHandler;
use smithay::wayland::shm::{ShmHandler, ShmState};
use smithay::{
delegate_compositor, delegate_data_device, delegate_dmabuf, delegate_output, delegate_seat,
delegate_shm, delegate_xdg_shell,
};
use std::collections::HashSet;
use std::os::fd::OwnedFd;
use std::sync::{Arc, Mutex};
use smithay::utils::Serial;
use smithay::wayland::compositor::{
self, with_surface_tree_downward, SurfaceAttributes, TraversalAction,
};
use smithay::wayland::selection::data_device::{
ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler,
};
use smithay::wayland::selection::SelectionHandler;
use smithay::wayland::shell::xdg::{
PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
};
use wayland_server::backend::{ClientData, ClientId, DisconnectReason};
use wayland_server::protocol::wl_surface::WlSurface;
use wayland_server::Client;
use super::event_queue::SyncEventQueue;
use super::WayVRTask;
pub struct Application {
pub gles_renderer: GlesRenderer,
pub dmabuf_state: (DmabufState, DmabufGlobal, Option<DmabufFeedback>),
pub compositor: compositor::CompositorState,
pub xdg_shell: XdgShellState,
pub seat_state: SeatState<Application>,
pub shm: ShmState,
pub data_device: DataDeviceState,
pub wayvr_tasks: SyncEventQueue<WayVRTask>,
pub redraw_requests: HashSet<wayland_server::backend::ObjectId>,
}
impl Application {
pub fn check_redraw(&mut self, surface: &WlSurface) -> bool {
self.redraw_requests.remove(&surface.id())
}
}
impl compositor::CompositorHandler for Application {
fn compositor_state(&mut self) -> &mut compositor::CompositorState {
&mut self.compositor
}
fn client_compositor_state<'a>(
&self,
client: &'a Client,
) -> &'a compositor::CompositorClientState {
&client.get_data::<ClientState>().unwrap().compositor_state
}
fn commit(&mut self, surface: &WlSurface) {
on_commit_buffer_handler::<Self>(surface);
self.redraw_requests.insert(surface.id());
}
}
impl SeatHandler for Application {
type KeyboardFocus = WlSurface;
type PointerFocus = WlSurface;
type TouchFocus = WlSurface;
fn seat_state(&mut self) -> &mut SeatState<Self> {
&mut self.seat_state
}
fn focus_changed(&mut self, _seat: &Seat<Self>, _focused: Option<&WlSurface>) {}
fn cursor_image(
&mut self,
_seat: &Seat<Self>,
_image: smithay::input::pointer::CursorImageStatus,
) {
}
}
impl BufferHandler for Application {
fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
}
impl ClientDndGrabHandler for Application {}
impl ServerDndGrabHandler for Application {
fn send(&mut self, _mime_type: String, _fd: OwnedFd, _seat: Seat<Self>) {}
}
impl DataDeviceHandler for Application {
fn data_device_state(&self) -> &DataDeviceState {
&self.data_device
}
}
impl SelectionHandler for Application {
type SelectionUserData = ();
}
#[derive(Default)]
pub struct ClientState {
compositor_state: compositor::CompositorClientState,
pub disconnected: Arc<Mutex<bool>>,
}
impl ClientData for ClientState {
fn initialized(&self, client_id: ClientId) {
log::debug!("Client ID {client_id:?} connected");
}
fn disconnected(&self, client_id: ClientId, reason: DisconnectReason) {
*self.disconnected.lock().unwrap() = true;
log::debug!("Client ID {client_id:?} disconnected. Reason: {reason:?}");
}
}
impl AsMut<compositor::CompositorState> for Application {
fn as_mut(&mut self) -> &mut compositor::CompositorState {
&mut self.compositor
}
}
impl XdgShellHandler for Application {
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
&mut self.xdg_shell
}
fn new_toplevel(&mut self, surface: ToplevelSurface) {
if let Some(client) = surface.wl_surface().client() {
self.wayvr_tasks
.send(WayVRTask::NewToplevel(client.id(), surface.clone()));
}
surface.with_pending_state(|state| {
state.states.set(xdg_toplevel::State::Activated);
});
surface.send_configure();
}
fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
if let Some(client) = surface.wl_surface().client() {
self.wayvr_tasks
.send(WayVRTask::DropToplevel(client.id(), surface.clone()));
}
}
fn new_popup(&mut self, _surface: PopupSurface, _positioner: PositionerState) {
// Handle popup creation here
}
fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) {
// Handle popup grab here
}
fn reposition_request(
&mut self,
_surface: PopupSurface,
_positioner: PositionerState,
_token: u32,
) {
// Handle popup reposition here
}
}
impl ShmHandler for Application {
fn shm_state(&self) -> &ShmState {
&self.shm
}
}
impl OutputHandler for Application {}
impl DmabufHandler for Application {
fn dmabuf_state(&mut self) -> &mut DmabufState {
&mut self.dmabuf_state.0
}
fn dmabuf_imported(
&mut self,
_global: &DmabufGlobal,
dmabuf: Dmabuf,
notifier: ImportNotifier,
) {
if self.gles_renderer.import_dmabuf(&dmabuf, None).is_ok() {
let _ = notifier.successful::<Self>();
} else {
notifier.failed();
}
}
}
delegate_dmabuf!(Application);
delegate_xdg_shell!(Application);
delegate_compositor!(Application);
delegate_shm!(Application);
delegate_seat!(Application);
delegate_data_device!(Application);
delegate_output!(Application);
pub fn send_frames_surface_tree(surface: &wl_surface::WlSurface, time: u32) {
with_surface_tree_downward(
surface,
(),
|_, _, &()| TraversalAction::DoChildren(()),
|_surf, states, &()| {
// the surface may not have any user_data if it is a subsurface and has not
// yet been commited
for callback in states
.cached_state
.get::<SurfaceAttributes>()
.current()
.frame_callbacks
.drain(..)
{
callback.done(time);
}
},
|_, _, &()| true,
);
}

View File

@@ -0,0 +1,605 @@
use std::{cell::RefCell, rc::Rc, sync::Arc};
use smithay::{
backend::renderer::{
element::{
surface::{render_elements_from_surface_tree, WaylandSurfaceRenderElement},
Kind,
},
gles::{ffi, GlesRenderer, GlesTexture},
utils::draw_render_elements,
Bind, Color32F, Frame, Renderer,
},
input,
utils::{Logical, Point, Rectangle, Size, Transform},
wayland::shell::xdg::ToplevelSurface,
};
use wayvr_ipc::packet_server;
use crate::{
backend::{overlay::OverlayID, wayvr::time::get_millis},
gen_id,
};
use super::{
client::WayVRCompositor, comp::send_frames_surface_tree, egl_data, event_queue::SyncEventQueue,
process, smithay_wrapper, time, window, BlitMethod, WayVRSignal,
};
fn generate_auth_key() -> String {
let uuid = uuid::Uuid::new_v4();
uuid.to_string()
}
#[derive(Debug)]
pub struct DisplayWindow {
pub window_handle: window::WindowHandle,
pub toplevel: ToplevelSurface,
pub process_handle: process::ProcessHandle,
}
pub struct SpawnProcessResult {
pub auth_key: String,
pub child: std::process::Child,
}
#[derive(Debug)]
pub enum DisplayTask {
ProcessCleanup(process::ProcessHandle),
}
const MAX_DISPLAY_SIZE: u16 = 8192;
#[derive(Debug)]
pub struct Display {
// Display info stuff
pub width: u16,
pub height: u16,
pub name: String,
pub visible: bool,
pub layout: packet_server::WvrDisplayWindowLayout,
pub overlay_id: Option<OverlayID>,
pub wants_redraw: bool,
pub rendered_frame_count: u32,
pub primary: bool,
pub wm: Rc<RefCell<window::WindowManager>>,
pub displayed_windows: Vec<DisplayWindow>,
wayland_env: super::WaylandEnv,
last_pressed_time_ms: u64,
pub no_windows_since: Option<u64>,
// Render data stuff
gles_texture: GlesTexture, // TODO: drop texture
egl_image: khronos_egl::Image,
egl_data: Rc<egl_data::EGLData>,
pub render_data: egl_data::RenderData,
pub tasks: SyncEventQueue<DisplayTask>,
}
impl Drop for Display {
fn drop(&mut self) {
let _ = self
.egl_data
.egl
.destroy_image(self.egl_data.display, self.egl_image);
}
}
pub struct DisplayInitParams<'a> {
pub wm: Rc<RefCell<window::WindowManager>>,
pub config: &'a super::Config,
pub renderer: &'a mut GlesRenderer,
pub egl_data: Rc<egl_data::EGLData>,
pub wayland_env: super::WaylandEnv,
pub width: u16,
pub height: u16,
pub name: &'a str,
pub primary: bool,
}
impl Display {
pub fn new(params: DisplayInitParams) -> anyhow::Result<Self> {
if params.width > MAX_DISPLAY_SIZE {
anyhow::bail!(
"display width ({}) is larger than {}",
params.width,
MAX_DISPLAY_SIZE
);
}
if params.height > MAX_DISPLAY_SIZE {
anyhow::bail!(
"display height ({}) is larger than {}",
params.height,
MAX_DISPLAY_SIZE
);
}
let tex_format = ffi::RGBA;
let internal_format = ffi::RGBA8;
let tex_id = params.renderer.with_context(|gl| {
smithay_wrapper::create_framebuffer_texture(
gl,
u32::from(params.width),
u32::from(params.height),
tex_format,
internal_format,
)
})?;
let egl_image = params.egl_data.create_egl_image(tex_id)?;
let render_data = match params.config.blit_method {
BlitMethod::Dmabuf => match params.egl_data.create_dmabuf_data(&egl_image) {
Ok(dmabuf_data) => egl_data::RenderData::Dmabuf(dmabuf_data),
Err(e) => {
log::error!("create_dmabuf_data failed: {e:?}. Using software blitting (This will be slow!)");
egl_data::RenderData::Software(None)
}
},
BlitMethod::Software => egl_data::RenderData::Software(None),
};
let opaque = false;
let size = (i32::from(params.width), i32::from(params.height)).into();
let gles_texture = unsafe {
GlesTexture::from_raw(params.renderer, Some(tex_format), opaque, tex_id, size)
};
Ok(Self {
egl_data: params.egl_data,
width: params.width,
height: params.height,
name: String::from(params.name),
primary: params.primary,
wayland_env: params.wayland_env,
wm: params.wm,
displayed_windows: Vec::new(),
render_data,
egl_image,
gles_texture,
last_pressed_time_ms: 0,
no_windows_since: None,
overlay_id: None,
tasks: SyncEventQueue::new(),
visible: true,
wants_redraw: true,
rendered_frame_count: 0,
layout: packet_server::WvrDisplayWindowLayout::Tiling,
})
}
pub fn as_packet(&self, handle: DisplayHandle) -> packet_server::WvrDisplay {
packet_server::WvrDisplay {
width: self.width,
height: self.height,
name: self.name.clone(),
visible: self.visible,
handle: handle.as_packet(),
}
}
pub fn add_window(
&mut self,
window_handle: window::WindowHandle,
process_handle: process::ProcessHandle,
toplevel: &ToplevelSurface,
) {
log::debug!("Attaching toplevel surface into display");
self.displayed_windows.push(DisplayWindow {
window_handle,
process_handle,
toplevel: toplevel.clone(),
});
self.reposition_windows();
}
pub fn remove_window(&mut self, window_handle: window::WindowHandle) {
self.displayed_windows
.retain(|disp| disp.window_handle != window_handle);
}
pub fn reposition_windows(&mut self) {
let window_count = self.displayed_windows.len();
match &self.layout {
packet_server::WvrDisplayWindowLayout::Tiling => {
let mut i = 0;
for win in &mut self.displayed_windows {
if let Some(window) = self.wm.borrow_mut().windows.get_mut(&win.window_handle) {
if !window.visible {
continue;
}
let d_cur = i as f32 / window_count as f32;
let d_next = (i + 1) as f32 / window_count as f32;
let left = (d_cur * f32::from(self.width)) as i32;
let right = (d_next * f32::from(self.width)) as i32;
window.set_pos(left, 0);
window.set_size((right - left) as u32, u32::from(self.height));
i += 1;
}
}
}
packet_server::WvrDisplayWindowLayout::Stacking(opts) => {
let do_margins = |margins: &packet_server::Margins, window: &mut window::Window| {
let top = i32::from(margins.top);
let bottom = i32::from(self.height) - i32::from(margins.bottom);
let left = i32::from(margins.left);
let right = i32::from(self.width) - i32::from(margins.right);
let width = right - left;
let height = bottom - top;
if width < 0 || height < 0 {
return; // wrong parameters, do nothing!
}
window.set_pos(left, top);
window.set_size(width as u32, height as u32);
};
let mut i = 0;
for win in &mut self.displayed_windows {
if let Some(window) = self.wm.borrow_mut().windows.get_mut(&win.window_handle) {
if !window.visible {
continue;
}
do_margins(
if i == 0 {
&opts.margins_first
} else {
&opts.margins_rest
},
window,
);
i += 1;
}
}
}
}
}
pub fn tick(
&mut self,
config: &super::Config,
handle: &DisplayHandle,
signals: &mut SyncEventQueue<WayVRSignal>,
) {
if self.visible {
if !self.displayed_windows.is_empty() {
self.no_windows_since = None;
} else if let Some(auto_hide_delay) = config.auto_hide_delay {
if let Some(s) = self.no_windows_since {
if s + u64::from(auto_hide_delay) < get_millis() {
// Auto-hide after specific time
signals.send(WayVRSignal::DisplayVisibility(*handle, false));
}
}
}
}
while let Some(task) = self.tasks.read() {
match task {
DisplayTask::ProcessCleanup(process_handle) => {
let count = self.displayed_windows.len();
self.displayed_windows
.retain(|win| win.process_handle != process_handle);
log::info!(
"Cleanup finished for display \"{}\". Current window count: {}",
self.name,
self.displayed_windows.len()
);
self.no_windows_since = Some(get_millis());
if count != self.displayed_windows.len() {
signals.send(WayVRSignal::BroadcastStateChanged(
packet_server::WvrStateChanged::WindowRemoved,
));
}
self.reposition_windows();
}
}
}
}
pub fn tick_render(&mut self, renderer: &mut GlesRenderer, time_ms: u64) -> anyhow::Result<()> {
renderer.bind(self.gles_texture.clone())?;
let size = Size::from((i32::from(self.width), i32::from(self.height)));
let damage: Rectangle<i32, smithay::utils::Physical> = Rectangle::from_size(size);
let elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> = self
.displayed_windows
.iter()
.flat_map(|display_window| {
let wm = self.wm.borrow_mut();
if let Some(window) = wm.windows.get(&display_window.window_handle) {
if !window.visible {
return vec![];
}
render_elements_from_surface_tree(
renderer,
display_window.toplevel.wl_surface(),
(window.pos_x, window.pos_y),
1.0,
1.0,
Kind::Unspecified,
)
} else {
// Failed to fetch window
vec![]
}
})
.collect();
let mut frame = renderer.render(size, Transform::Normal)?;
let clear_color = if self.displayed_windows.is_empty() {
Color32F::new(0.5, 0.5, 0.5, 0.5)
} else {
Color32F::new(0.0, 0.0, 0.0, 0.0)
};
frame.clear(clear_color, &[damage])?;
draw_render_elements(&mut frame, 1.0, &elements, &[damage])?;
let _sync_point = frame.finish()?;
for window in &self.displayed_windows {
send_frames_surface_tree(window.toplevel.wl_surface(), time_ms as u32);
}
if let egl_data::RenderData::Software(_) = &self.render_data {
// Read OpenGL texture into memory. Slow!
let pixel_data = renderer.with_context(|gl| unsafe {
gl.BindTexture(ffi::TEXTURE_2D, self.gles_texture.tex_id());
let len = self.width as usize * self.height as usize * 4;
let mut data: Box<[u8]> = Box::new_uninit_slice(len).assume_init();
gl.ReadPixels(
0,
0,
i32::from(self.width),
i32::from(self.height),
ffi::RGBA,
ffi::UNSIGNED_BYTE,
data.as_mut_ptr().cast(),
);
let data: Arc<[u8]> = Arc::from(data);
data
})?;
self.render_data =
egl_data::RenderData::Software(Some(egl_data::RenderSoftwarePixelsData {
data: pixel_data,
width: self.width,
height: self.height,
}));
}
self.rendered_frame_count += 1;
Ok(())
}
fn get_hovered_window(&self, cursor_x: u32, cursor_y: u32) -> Option<window::WindowHandle> {
let wm = self.wm.borrow();
for cell in self.displayed_windows.iter().rev() {
if let Some(window) = wm.windows.get(&cell.window_handle) {
if !window.visible {
continue;
}
if (cursor_x as i32) >= window.pos_x
&& (cursor_x as i32) < window.pos_x + window.size_x as i32
&& (cursor_y as i32) >= window.pos_y
&& (cursor_y as i32) < window.pos_y + window.size_y as i32
{
return Some(cell.window_handle);
}
}
}
None
}
pub const fn trigger_rerender(&mut self) {
self.wants_redraw = true;
}
pub fn set_visible(&mut self, visible: bool) {
log::info!("Display \"{}\" visible: {}", self.name.as_str(), visible);
if self.visible == visible {
return;
}
self.visible = visible;
if visible {
self.no_windows_since = None;
self.trigger_rerender();
}
}
pub fn set_layout(&mut self, layout: packet_server::WvrDisplayWindowLayout) {
log::info!("Display \"{}\" layout: {:?}", self.name.as_str(), layout);
if self.layout == layout {
return;
}
self.layout = layout;
self.trigger_rerender();
self.reposition_windows();
}
pub fn send_mouse_move(
&self,
config: &super::Config,
manager: &mut WayVRCompositor,
x: u32,
y: u32,
) {
let current_ms = time::get_millis();
if self.last_pressed_time_ms + u64::from(config.click_freeze_time_ms) > current_ms {
return;
}
if let Some(window_handle) = self.get_hovered_window(x, y) {
let wm = self.wm.borrow();
if let Some(window) = wm.windows.get(&window_handle) {
let surf = window.toplevel.wl_surface().clone();
let point = Point::<f64, Logical>::from((
f64::from(x as i32 - window.pos_x),
f64::from(y as i32 - window.pos_y),
));
manager.seat_pointer.motion(
&mut manager.state,
Some((surf, Point::from((0.0, 0.0)))),
&input::pointer::MotionEvent {
serial: manager.serial_counter.next_serial(),
time: 0,
location: point,
},
);
manager.seat_pointer.frame(&mut manager.state);
}
}
}
const fn get_mouse_index_number(index: super::MouseIndex) -> u32 {
match index {
super::MouseIndex::Left => 0x110, /* BTN_LEFT */
super::MouseIndex::Center => 0x112, /* BTN_MIDDLE */
super::MouseIndex::Right => 0x111, /* BTN_RIGHT */
}
}
pub fn send_mouse_down(&mut self, manager: &mut WayVRCompositor, index: super::MouseIndex) {
// Change keyboard focus to pressed window
let loc = manager.seat_pointer.current_location();
self.last_pressed_time_ms = time::get_millis();
if let Some(window_handle) =
self.get_hovered_window(loc.x.max(0.0) as u32, loc.y.max(0.0) as u32)
{
let wm = self.wm.borrow();
if let Some(window) = wm.windows.get(&window_handle) {
let surf = window.toplevel.wl_surface().clone();
manager.seat_keyboard.set_focus(
&mut manager.state,
Some(surf),
manager.serial_counter.next_serial(),
);
}
}
manager.seat_pointer.button(
&mut manager.state,
&input::pointer::ButtonEvent {
button: Self::get_mouse_index_number(index),
serial: manager.serial_counter.next_serial(),
time: 0,
state: smithay::backend::input::ButtonState::Pressed,
},
);
manager.seat_pointer.frame(&mut manager.state);
}
pub fn send_mouse_up(manager: &mut WayVRCompositor, index: super::MouseIndex) {
manager.seat_pointer.button(
&mut manager.state,
&input::pointer::ButtonEvent {
button: Self::get_mouse_index_number(index),
serial: manager.serial_counter.next_serial(),
time: 0,
state: smithay::backend::input::ButtonState::Released,
},
);
manager.seat_pointer.frame(&mut manager.state);
}
pub fn send_mouse_scroll(manager: &mut WayVRCompositor, delta_y: f32, delta_x: f32) {
manager.seat_pointer.axis(
&mut manager.state,
input::pointer::AxisFrame {
source: None,
relative_direction: (
smithay::backend::input::AxisRelativeDirection::Identical,
smithay::backend::input::AxisRelativeDirection::Identical,
),
time: 0,
axis: (f64::from(delta_x), f64::from(-delta_y)),
v120: Some((0, (delta_y * -120.0) as i32)),
stop: (false, false),
},
);
manager.seat_pointer.frame(&mut manager.state);
}
fn configure_env(&self, cmd: &mut std::process::Command, auth_key: &str) {
cmd.env_remove("DISPLAY"); // Goodbye X11
cmd.env("WAYLAND_DISPLAY", self.wayland_env.display_num_string());
cmd.env("WAYVR_DISPLAY_AUTH", auth_key);
}
pub fn spawn_process(
&mut self,
exec_path: &str,
args: &[&str],
env: &[(&str, &str)],
working_dir: Option<&str>,
) -> anyhow::Result<SpawnProcessResult> {
log::info!("Spawning subprocess with exec path \"{exec_path}\"");
let auth_key = generate_auth_key();
let mut cmd = std::process::Command::new(exec_path);
self.configure_env(&mut cmd, auth_key.as_str());
cmd.args(args);
if let Some(working_dir) = working_dir {
cmd.current_dir(working_dir);
}
for e in env {
cmd.env(e.0, e.1);
}
match cmd.spawn() {
Ok(child) => Ok(SpawnProcessResult { auth_key, child }),
Err(e) => {
anyhow::bail!(
"Failed to launch process with path \"{}\": {}. Make sure your exec path exists.",
exec_path,
e
);
}
}
}
}
gen_id!(DisplayVec, Display, DisplayCell, DisplayHandle);
impl DisplayHandle {
pub const fn from_packet(handle: packet_server::WvrDisplayHandle) -> Self {
Self {
generation: handle.generation,
idx: handle.idx,
}
}
pub const fn as_packet(&self) -> packet_server::WvrDisplayHandle {
packet_server::WvrDisplayHandle {
idx: self.idx,
generation: self.generation,
}
}
}

View File

@@ -0,0 +1,315 @@
use std::sync::Arc;
use crate::backend::wayvr::egl_ex::{
PFNEGLGETPLATFORMDISPLAYEXTPROC, PFNEGLQUERYDMABUFFORMATSEXTPROC,
PFNEGLQUERYDMABUFMODIFIERSEXTPROC,
};
use super::egl_ex;
use anyhow::anyhow;
#[derive(Debug)]
pub struct EGLData {
pub egl: khronos_egl::Instance<khronos_egl::Static>,
pub display: khronos_egl::Display,
pub config: khronos_egl::Config,
pub context: khronos_egl::Context,
}
#[macro_export]
macro_rules! bind_egl_function {
($func_type:ident, $func:expr) => {
std::mem::transmute_copy::<_, $func_type>($func).unwrap()
};
}
#[derive(Debug, Clone)]
pub struct DMAbufModifierInfo {
pub modifiers: Vec<u64>,
pub fourcc: u32,
}
#[derive(Debug, Clone)]
pub struct RenderDMAbufData {
pub fd: i32,
pub stride: i32,
pub offset: i32,
pub mod_info: DMAbufModifierInfo,
}
#[derive(Debug, Clone)]
pub struct RenderSoftwarePixelsData {
pub data: Arc<[u8]>,
pub width: u16,
pub height: u16,
}
#[derive(Debug, Clone)]
pub enum RenderData {
Dmabuf(RenderDMAbufData),
Software(Option<RenderSoftwarePixelsData>), // will be set if the next image data is available
}
fn load_egl_func(
egl: &khronos_egl::Instance<khronos_egl::Static>,
func_name: &str,
) -> anyhow::Result<extern "system" fn()> {
let raw_fn = egl
.get_proc_address(func_name)
.ok_or_else(|| anyhow::anyhow!("Required EGL function {} not found", func_name))?;
Ok(raw_fn)
}
fn get_disp(
egl: &khronos_egl::Instance<khronos_egl::Static>,
) -> anyhow::Result<khronos_egl::Display> {
unsafe {
if let Ok(func) = load_egl_func(egl, "eglGetPlatformDisplayEXT") {
let egl_get_platform_display_ext =
bind_egl_function!(PFNEGLGETPLATFORMDISPLAYEXTPROC, &func);
let display_ext = egl_get_platform_display_ext(
egl_ex::EGL_PLATFORM_WAYLAND_EXT, // platform
std::ptr::null_mut(), // void *native_display
std::ptr::null_mut(), // EGLint *attrib_list
);
if display_ext.is_null() {
log::warn!("eglGetPlatformDisplayEXT failed, using eglGetDisplay instead");
} else {
return Ok(khronos_egl::Display::from_ptr(display_ext));
}
}
egl
.get_display(khronos_egl::DEFAULT_DISPLAY)
.ok_or_else(|| anyhow!(
"Both eglGetPlatformDisplayEXT and eglGetDisplay failed. This shouldn't happen unless you don't have any display manager running. Cannot continue, check your EGL installation."
))
}
}
impl EGLData {
pub fn new() -> anyhow::Result<Self> {
let egl = khronos_egl::Instance::new(khronos_egl::Static);
let display = get_disp(&egl)?;
let (major, minor) = egl.initialize(display)?;
log::debug!("EGL version: {major}.{minor}");
let attrib_list = [
khronos_egl::RED_SIZE,
8,
khronos_egl::GREEN_SIZE,
8,
khronos_egl::BLUE_SIZE,
8,
khronos_egl::SURFACE_TYPE,
khronos_egl::WINDOW_BIT,
khronos_egl::RENDERABLE_TYPE,
khronos_egl::OPENGL_BIT,
khronos_egl::NONE,
];
let config = egl
.choose_first_config(display, &attrib_list)?
.ok_or_else(|| anyhow!("Failed to get EGL config"))?;
egl.bind_api(khronos_egl::OPENGL_ES_API)?;
log::debug!("eglCreateContext");
// Require OpenGL ES 3.0
let context_attrib_list = [
khronos_egl::CONTEXT_MAJOR_VERSION,
3,
khronos_egl::CONTEXT_MINOR_VERSION,
0,
khronos_egl::NONE,
];
let context = egl.create_context(display, config, None, &context_attrib_list)?;
log::debug!("eglMakeCurrent");
egl.make_current(display, None, None, Some(context))?;
Ok(Self {
egl,
display,
config,
context,
})
}
fn query_dmabuf_mod_info(&self) -> anyhow::Result<DMAbufModifierInfo> {
let target_fourcc = 0x3432_4258; //XB24
unsafe {
let egl_query_dmabuf_formats_ext = bind_egl_function!(
PFNEGLQUERYDMABUFFORMATSEXTPROC,
&load_egl_func(&self.egl, "eglQueryDmaBufFormatsEXT")?
);
// Query format count
let mut num_formats: khronos_egl::Int = 0;
egl_query_dmabuf_formats_ext(
self.display.as_ptr(),
0,
std::ptr::null_mut(),
&mut num_formats,
);
// Retrieve formt list
let mut formats: Vec<i32> = vec![0; num_formats as usize];
egl_query_dmabuf_formats_ext(
self.display.as_ptr(),
num_formats,
formats.as_mut_ptr(),
&mut num_formats,
);
/*for (idx, format) in formats.iter().enumerate() {
let bytes = format.to_le_bytes();
log::trace!(
"idx {}, format {}{}{}{} (hex {:#x})",
idx,
bytes[0] as char,
bytes[1] as char,
bytes[2] as char,
bytes[3] as char,
format
);
}*/
let egl_query_dmabuf_modifiers_ext = bind_egl_function!(
PFNEGLQUERYDMABUFMODIFIERSEXTPROC,
&load_egl_func(&self.egl, "eglQueryDmaBufModifiersEXT")?
);
let mut num_mods: khronos_egl::Int = 0;
// Query modifier count
egl_query_dmabuf_modifiers_ext(
self.display.as_ptr(),
target_fourcc,
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut num_mods,
);
if num_mods == 0 {
anyhow::bail!("eglQueryDmaBufModifiersEXT modifier count is zero");
}
let mut mods: Vec<u64> = vec![0; num_mods as usize];
egl_query_dmabuf_modifiers_ext(
self.display.as_ptr(),
target_fourcc,
num_mods,
mods.as_mut_ptr(),
std::ptr::null_mut(),
&mut num_mods,
);
if mods[0] == 0xFFFF_FFFF_FFFF_FFFF {
anyhow::bail!("modifier is -1")
}
log::trace!("Modifier list:");
for modifier in &mods {
log::trace!("{modifier:#x}");
}
// We should not change these modifier values. Passing all of them to the Vulkan dmabuf
// texture system causes significant graphical corruption due to invalid memory layout and
// tiling on this specific GPU model (very probably others also have the same issue).
// It is not guaranteed that this modifier will be present in other models.
// If not, the full list of modifiers will be passed. Further testing is required.
// For now, it looks like only NAVI32-based gpus have this problem.
let mod_whitelist: [u64; 2] = [
0x200_0000_2086_bf04, /* AMD RX 7800 XT, Navi32 */
0x200_0000_1866_bf04, /* AMD RX 7600 XT, Navi33 */
];
for modifier in &mod_whitelist {
if mods.contains(modifier) {
log::warn!("Using whitelisted dmabuf tiling modifier: {modifier:#x}");
mods = vec![*modifier, 0x0 /* also important (???) */];
break;
}
}
Ok(DMAbufModifierInfo {
modifiers: mods,
fourcc: target_fourcc as u32,
})
}
}
pub fn create_dmabuf_data(
&self,
egl_image: &khronos_egl::Image,
) -> anyhow::Result<RenderDMAbufData> {
use egl_ex::PFNEGLEXPORTDMABUFIMAGEMESAPROC as FUNC;
unsafe {
let egl_export_dmabuf_image_mesa =
bind_egl_function!(FUNC, &load_egl_func(&self.egl, "eglExportDMABUFImageMESA")?);
let mut fds: [i32; 3] = [0; 3];
let mut strides: [i32; 3] = [0; 3];
let mut offsets: [i32; 3] = [0; 3];
let ret = egl_export_dmabuf_image_mesa(
self.display.as_ptr(),
egl_image.as_ptr(),
fds.as_mut_ptr(),
strides.as_mut_ptr(),
offsets.as_mut_ptr(),
);
if ret != khronos_egl::TRUE {
anyhow::bail!("eglExportDMABUFImageMESA failed with return code {ret}");
}
if fds[0] <= 0 {
anyhow::bail!("fd is <=0 (got {})", fds[0]);
}
// many planes in RGB data?
if fds[1] != 0 || strides[1] != 0 || offsets[1] != 0 {
anyhow::bail!("multi-planar data received, packed RGB expected");
}
if strides[0] < 0 {
anyhow::bail!("strides is < 0");
}
if offsets[0] < 0 {
anyhow::bail!("offsets is < 0");
}
let mod_info = self.query_dmabuf_mod_info()?;
Ok(RenderDMAbufData {
fd: fds[0],
stride: strides[0],
offset: offsets[0],
mod_info,
})
}
}
pub fn create_egl_image(&self, gl_tex_id: u32) -> anyhow::Result<khronos_egl::Image> {
unsafe {
Ok(self.egl.create_image(
self.display,
self.context,
khronos_egl::GL_TEXTURE_2D as std::ffi::c_uint,
khronos_egl::ClientBuffer::from_ptr(gl_tex_id as *mut std::ffi::c_void),
&[khronos_egl::ATTRIB_NONE],
)?)
}
}
}

View File

@@ -0,0 +1,49 @@
#![allow(clippy::all)]
pub const EGL_PLATFORM_WAYLAND_EXT: khronos_egl::Enum = 0x31D8;
// eglGetPlatformDisplayEXT
// https://registry.khronos.org/EGL/extensions/EXT/EGL_EXT_platform_base.txt
pub type PFNEGLGETPLATFORMDISPLAYEXTPROC = Option<
unsafe extern "C" fn(
platform: khronos_egl::Enum,
native_display: *mut std::ffi::c_void,
attrib_list: *mut khronos_egl::Enum,
) -> khronos_egl::EGLDisplay,
>;
// eglExportDMABUFImageMESA
// https://registry.khronos.org/EGL/extensions/MESA/EGL_MESA_image_dma_buf_export.txt
pub type PFNEGLEXPORTDMABUFIMAGEMESAPROC = Option<
unsafe extern "C" fn(
dpy: khronos_egl::EGLDisplay,
image: khronos_egl::EGLImage,
fds: *mut i32,
strides: *mut khronos_egl::Int,
offsets: *mut khronos_egl::Int,
) -> khronos_egl::Boolean,
>;
// eglQueryDmaBufModifiersEXT
// https://registry.khronos.org/EGL/extensions/EXT/EGL_EXT_image_dma_buf_import_modifiers.txt
pub type PFNEGLQUERYDMABUFMODIFIERSEXTPROC = Option<
unsafe extern "C" fn(
dpy: khronos_egl::EGLDisplay,
format: khronos_egl::Int,
max_modifiers: khronos_egl::Int,
modifiers: *mut u64,
external_only: *mut khronos_egl::Boolean,
num_modifiers: *mut khronos_egl::Int,
) -> khronos_egl::Boolean,
>;
// eglQueryDmaBufFormatsEXT
// https://registry.khronos.org/EGL/extensions/EXT/EGL_EXT_image_dma_buf_import_modifiers.txt
pub type PFNEGLQUERYDMABUFFORMATSEXTPROC = Option<
unsafe extern "C" fn(
dpy: khronos_egl::EGLDisplay,
max_formats: khronos_egl::Int,
formats: *mut khronos_egl::Int,
num_formats: *mut khronos_egl::Int,
) -> khronos_egl::Boolean,
>;

View File

@@ -0,0 +1,33 @@
#![allow(dead_code)]
use std::{cell::RefCell, collections::VecDeque, rc::Rc};
#[derive(Debug)]
struct Data<DataType> {
queue: VecDeque<DataType>,
}
#[derive(Debug, Clone)]
pub struct SyncEventQueue<DataType> {
data: Rc<RefCell<Data<DataType>>>,
}
impl<DataType> SyncEventQueue<DataType> {
pub fn new() -> Self {
Self {
data: Rc::new(RefCell::new(Data {
queue: VecDeque::default(),
})),
}
}
pub fn send(&self, message: DataType) {
let mut data = self.data.borrow_mut();
data.queue.push_back(message);
}
pub fn read(&self) -> Option<DataType> {
let mut data = self.data.borrow_mut();
data.queue.pop_front()
}
}

View File

@@ -0,0 +1,176 @@
#[macro_export]
macro_rules! gen_id {
(
$container_name:ident,
$instance_name:ident,
$cell_name:ident,
$handle_name:ident) => {
//ThingCell
#[derive(Debug)]
pub struct $cell_name {
pub obj: $instance_name,
pub generation: u64,
}
//ThingVec
#[derive(Debug)]
pub struct $container_name {
// Vec<Option<ThingCell>>
pub vec: Vec<Option<$cell_name>>,
cur_generation: u64,
}
//ThingHandle
#[derive(Default, Debug, Clone, Copy, PartialEq, Hash, Eq)]
pub struct $handle_name {
idx: u32,
generation: u64,
}
#[allow(dead_code)]
impl $handle_name {
pub const fn reset(&mut self) {
self.generation = 0;
}
pub const fn is_set(&self) -> bool {
self.generation > 0
}
pub const fn id(&self) -> u32 {
self.idx
}
pub const fn new(idx: u32, generation: u64) -> Self {
Self { idx, generation }
}
}
//ThingVec
impl $container_name {
pub const fn new() -> Self {
Self {
vec: Vec::new(),
cur_generation: 0,
}
}
pub fn iter(&self) -> impl Iterator<Item = ($handle_name, &$instance_name)> {
self.vec.iter().enumerate().filter_map(|(idx, opt_cell)| {
opt_cell.as_ref().map(|cell| {
let handle = $container_name::get_handle(&cell, idx);
(handle, &cell.obj)
})
})
}
pub fn iter_mut(
&mut self,
) -> impl Iterator<Item = ($handle_name, &mut $instance_name)> {
self.vec
.iter_mut()
.enumerate()
.filter_map(|(idx, opt_cell)| {
opt_cell.as_mut().map(|cell| {
let handle = $container_name::get_handle(&cell, idx);
(handle, &mut cell.obj)
})
})
}
pub const fn get_handle(cell: &$cell_name, idx: usize) -> $handle_name {
$handle_name {
idx: idx as u32,
generation: cell.generation,
}
}
fn find_unused_idx(&mut self) -> Option<u32> {
for (num, obj) in self.vec.iter().enumerate() {
if obj.is_none() {
return Some(num as u32);
}
}
None
}
pub fn add(&mut self, obj: $instance_name) -> $handle_name {
self.cur_generation += 1;
let generation = self.cur_generation;
let unused_idx = self.find_unused_idx();
let idx = if let Some(idx) = unused_idx {
idx
} else {
self.vec.len() as u32
};
let handle = $handle_name { idx, generation };
let cell = $cell_name { obj, generation };
if let Some(idx) = unused_idx {
self.vec[idx as usize] = Some(cell);
} else {
self.vec.push(Some(cell))
}
handle
}
pub fn remove(&mut self, handle: &$handle_name) {
// Out of bounds, ignore
if handle.idx as usize >= self.vec.len() {
return;
}
// Remove only if the generation matches
if let Some(cell) = &self.vec[handle.idx as usize] {
if cell.generation == handle.generation {
self.vec[handle.idx as usize] = None;
}
}
}
pub fn get(&self, handle: &$handle_name) -> Option<&$instance_name> {
// Out of bounds, ignore
if handle.idx as usize >= self.vec.len() {
return None;
}
if let Some(cell) = &self.vec[handle.idx as usize] {
if cell.generation == handle.generation {
return Some(&cell.obj);
}
}
None
}
pub fn get_mut(&mut self, handle: &$handle_name) -> Option<&mut $instance_name> {
// Out of bounds, ignore
if handle.idx as usize >= self.vec.len() {
return None;
}
if let Some(cell) = &mut self.vec[handle.idx as usize] {
if cell.generation == handle.generation {
return Some(&mut cell.obj);
}
}
None
}
}
};
}
/* Example usage:
gen_id!(ThingVec, ThingInstance, ThingCell, ThingHandle);
struct ThingInstance {}
impl ThingInstance {}
*/

View File

@@ -0,0 +1,740 @@
pub mod client;
mod comp;
pub mod display;
pub mod egl_data;
mod egl_ex;
pub mod event_queue;
mod handle;
mod process;
pub mod server_ipc;
mod smithay_wrapper;
mod time;
mod window;
use comp::Application;
use display::{Display, DisplayInitParams, DisplayVec};
use event_queue::SyncEventQueue;
use process::ProcessVec;
use serde::Deserialize;
use server_ipc::WayVRServer;
use smallvec::SmallVec;
use smithay::{
backend::{
egl,
renderer::{gles::GlesRenderer, ImportDma},
},
input::{keyboard::XkbConfig, SeatState},
output::{Mode, Output},
reexports::wayland_server::{self, backend::ClientId},
wayland::{
compositor,
dmabuf::{DmabufFeedbackBuilder, DmabufState},
selection::data_device::DataDeviceState,
shell::xdg::{ToplevelSurface, XdgShellState},
shm::ShmState,
},
};
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
rc::Rc,
sync::Arc,
};
use time::get_millis;
use wayvr_ipc::{packet_client, packet_server};
use crate::{hid::MODS_TO_KEYS, state::AppState};
const STR_INVALID_HANDLE_DISP: &str = "Invalid display handle";
#[derive(Debug, Clone)]
pub struct WaylandEnv {
pub display_num: u32,
}
impl WaylandEnv {
pub fn display_num_string(&self) -> String {
// e.g. "wayland-20"
format!("wayland-{}", self.display_num)
}
}
#[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),
DropToplevel(ClientId, ToplevelSurface),
NewExternalProcess(ExternalProcessRequest),
ProcessTerminationRequest(process::ProcessHandle),
}
#[derive(Clone)]
pub enum WayVRSignal {
DisplayVisibility(display::DisplayHandle, bool),
DisplayWindowLayout(
display::DisplayHandle,
packet_server::WvrDisplayWindowLayout,
),
BroadcastStateChanged(packet_server::WvrStateChanged),
DropOverlay(super::overlay::OverlayID),
Haptics(super::input::Haptics),
}
pub enum BlitMethod {
Dmabuf,
Software,
}
impl BlitMethod {
pub fn from_string(str: &str) -> Option<Self> {
match str {
"dmabuf" => Some(Self::Dmabuf),
"software" => Some(Self::Software),
_ => None,
}
}
}
pub struct Config {
pub click_freeze_time_ms: u32,
pub keyboard_repeat_delay_ms: u32,
pub keyboard_repeat_rate: u32,
pub auto_hide_delay: Option<u32>, // if None, auto-hide is disabled
pub blit_method: BlitMethod,
}
pub struct WayVRState {
time_start: u64,
pub displays: display::DisplayVec,
pub manager: client::WayVRCompositor,
wm: Rc<RefCell<window::WindowManager>>,
egl_data: Rc<egl_data::EGLData>,
pub processes: process::ProcessVec,
pub config: Config,
dashboard_display: Option<display::DisplayHandle>,
pub tasks: SyncEventQueue<WayVRTask>,
pub signals: SyncEventQueue<WayVRSignal>,
ticks: u64,
cur_modifiers: u8,
}
pub struct WayVR {
pub state: WayVRState,
pub ipc_server: WayVRServer,
}
pub enum MouseIndex {
Left,
Center,
Right,
}
pub enum TickTask {
NewExternalProcess(ExternalProcessRequest), // Call WayVRCompositor::add_client after receiving this message
NewDisplay(
packet_client::WvrDisplayCreateParams,
Option<display::DisplayHandle>, /* existing handle? */
),
}
impl WayVR {
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
pub fn new(config: Config) -> 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);
let xdg_shell = XdgShellState::new::<Application>(&dh);
let mut seat_state = SeatState::new();
let shm = ShmState::new::<Application>(&dh, Vec::new());
let data_device = DataDeviceState::new::<Application>(&dh);
let mut seat = seat_state.new_wl_seat(&dh, "wayvr");
let dummy_width = 1280;
let dummy_height = 720;
let dummy_milli_hz = 60000; /* refresh rate in millihertz */
let output = Output::new(
String::from("wayvr_display"),
smithay::output::PhysicalProperties {
size: (dummy_width, dummy_height).into(),
subpixel: smithay::output::Subpixel::None,
make: String::from("Completely Legit"),
model: String::from("Virtual WayVR Display"),
},
);
let mode = Mode {
refresh: dummy_milli_hz,
size: (dummy_width, dummy_height).into(),
};
let _global = output.create_global::<Application>(&dh);
output.change_current_state(Some(mode), None, None, None);
output.set_preferred(mode);
let egl_data = egl_data::EGLData::new()?;
let smithay_display = smithay_wrapper::get_egl_display(&egl_data)?;
let smithay_context = smithay_wrapper::get_egl_context(&egl_data, &smithay_display)?;
let render_node = egl::EGLDevice::device_for_display(&smithay_display)
.and_then(|device| device.try_get_render_node());
let gles_renderer = unsafe { GlesRenderer::new(smithay_context)? };
let dmabuf_default_feedback = match render_node {
Ok(Some(node)) => {
let dmabuf_formats = gles_renderer.dmabuf_formats();
let dmabuf_default_feedback =
DmabufFeedbackBuilder::new(node.dev_id(), dmabuf_formats)
.build()
.unwrap();
Some(dmabuf_default_feedback)
}
Ok(None) => {
log::warn!("dmabuf: Failed to query render node");
None
}
Err(err) => {
log::warn!("dmabuf: Failed to get egl device for display: {err}");
None
}
};
let dmabuf_state = dmabuf_default_feedback.map_or_else(
|| {
let dmabuf_formats = gles_renderer.dmabuf_formats();
let mut dmabuf_state = DmabufState::new();
let dmabuf_global =
dmabuf_state.create_global::<Application>(&display.handle(), dmabuf_formats);
(dmabuf_state, dmabuf_global, None)
},
|default_feedback| {
let mut dmabuf_state = DmabufState::new();
let dmabuf_global = dmabuf_state
.create_global_with_default_feedback::<Application>(
&display.handle(),
&default_feedback,
);
(dmabuf_state, dmabuf_global, Some(default_feedback))
},
);
let seat_keyboard = seat.add_keyboard(
XkbConfig::default(),
config.keyboard_repeat_delay_ms as i32,
config.keyboard_repeat_rate as i32,
)?;
let seat_pointer = seat.add_pointer();
let tasks = SyncEventQueue::new();
let state = Application {
compositor,
xdg_shell,
seat_state,
shm,
data_device,
wayvr_tasks: tasks.clone(),
redraw_requests: HashSet::new(),
dmabuf_state,
gles_renderer,
};
let time_start = get_millis();
let ipc_server = WayVRServer::new()?;
let state = WayVRState {
time_start,
manager: client::WayVRCompositor::new(state, display, seat_keyboard, seat_pointer)?,
displays: DisplayVec::new(),
processes: ProcessVec::new(),
egl_data: Rc::new(egl_data),
wm: Rc::new(RefCell::new(window::WindowManager::new())),
config,
dashboard_display: None,
ticks: 0,
tasks,
signals: SyncEventQueue::new(),
cur_modifiers: 0,
};
Ok(Self { state, ipc_server })
}
pub fn render_display(&mut self, display: display::DisplayHandle) -> anyhow::Result<bool> {
let display = self
.state
.displays
.get_mut(&display)
.ok_or_else(|| anyhow::anyhow!(STR_INVALID_HANDLE_DISP))?;
/* Buffer warm-up is required, always two first calls of this function are always rendered */
if !display.wants_redraw && display.rendered_frame_count >= 2 {
// Nothing changed, do not render
return Ok(false);
}
if !display.visible {
// Display is invisible, do not render
return Ok(false);
}
// millis since the start of wayvr
let time_ms = get_millis() - self.state.time_start;
display.tick_render(&mut self.state.manager.state.gles_renderer, time_ms)?;
display.wants_redraw = false;
Ok(true)
}
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
pub fn tick_events(&mut self, app: &AppState) -> anyhow::Result<Vec<TickTask>> {
let mut tasks: Vec<TickTask> = Vec::new();
self.ipc_server.tick(&mut server_ipc::TickParams {
state: &mut self.state,
tasks: &mut tasks,
app,
});
// Check for redraw events
for (_, disp) in self.state.displays.iter_mut() {
for disp_window in &disp.displayed_windows {
if self
.state
.manager
.state
.check_redraw(disp_window.toplevel.wl_surface())
{
disp.wants_redraw = true;
}
}
}
// Tick all child processes
let mut to_remove: SmallVec<[(process::ProcessHandle, display::DisplayHandle); 2]> =
SmallVec::new();
for (handle, process) in self.state.processes.iter_mut() {
if !process.is_running() {
to_remove.push((handle, process.display_handle()));
}
}
for (p_handle, disp_handle) in &to_remove {
self.state.processes.remove(p_handle);
if let Some(display) = self.state.displays.get_mut(disp_handle) {
display
.tasks
.send(display::DisplayTask::ProcessCleanup(*p_handle));
display.wants_redraw = true;
}
}
for (handle, display) in self.state.displays.iter_mut() {
display.tick(&self.state.config, &handle, &mut self.state.signals);
}
if !to_remove.is_empty() {
self.state.signals.send(WayVRSignal::BroadcastStateChanged(
packet_server::WvrStateChanged::ProcessRemoved,
));
}
while let Some(task) = self.state.tasks.read() {
match task {
WayVRTask::NewExternalProcess(req) => {
tasks.push(TickTask::NewExternalProcess(req));
}
WayVRTask::NewToplevel(client_id, toplevel) => {
// Attach newly created toplevel surfaces to displays
for client in &self.state.manager.clients {
if client.client.id() != client_id {
continue;
}
let Some(process_handle) =
process::find_by_pid(&self.state.processes, client.pid)
else {
log::error!(
"WayVR window creation failed: Unexpected process ID {}. It wasn't registered before.",
client.pid
);
continue;
};
let window_handle = self
.state
.wm
.borrow_mut()
.create_window(client.display_handle, &toplevel);
let Some(display) = self.state.displays.get_mut(&client.display_handle)
else {
// This shouldn't happen, scream if it does
log::error!("Could not attach window handle into display");
continue;
};
display.add_window(window_handle, process_handle, &toplevel);
self.state.signals.send(WayVRSignal::BroadcastStateChanged(
packet_server::WvrStateChanged::WindowCreated,
));
}
}
WayVRTask::DropToplevel(client_id, toplevel) => {
for client in &self.state.manager.clients {
if client.client.id() != client_id {
continue;
}
let mut wm = self.state.wm.borrow_mut();
let Some(window_handle) = wm.find_window_handle(&toplevel) else {
log::warn!("DropToplevel: Couldn't find matching window handle");
continue;
};
let Some(display) = self.state.displays.get_mut(&client.display_handle)
else {
log::warn!("DropToplevel: Couldn't find matching display");
continue;
};
display.remove_window(window_handle);
wm.remove_window(window_handle);
drop(wm);
display.reposition_windows();
}
}
WayVRTask::ProcessTerminationRequest(process_handle) => {
if let Some(process) = self.state.processes.get_mut(&process_handle) {
process.terminate();
}
}
}
}
self.state
.manager
.tick_wayland(&mut self.state.displays, &mut self.state.processes)?;
if self.state.ticks % 200 == 0 {
self.state.manager.cleanup_clients();
}
self.state.ticks += 1;
Ok(tasks)
}
pub fn tick_finish(&mut self) -> anyhow::Result<()> {
self.state
.manager
.state
.gles_renderer
.with_context(|gl| unsafe {
gl.Flush();
gl.Finish();
})?;
Ok(())
}
#[allow(dead_code)]
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));
}
}
}
None
}
pub fn terminate_process(&mut self, process_handle: process::ProcessHandle) {
self.state
.tasks
.send(WayVRTask::ProcessTerminationRequest(process_handle));
}
}
impl WayVRState {
pub fn send_mouse_move(&mut self, display: display::DisplayHandle, x: u32, y: u32) {
if let Some(display) = self.displays.get(&display) {
display.send_mouse_move(&self.config, &mut self.manager, x, y);
}
}
pub fn send_mouse_down(&mut self, display: display::DisplayHandle, index: MouseIndex) {
if let Some(display) = self.displays.get_mut(&display) {
display.send_mouse_down(&mut self.manager, index);
}
}
pub fn send_mouse_up(&mut self, index: MouseIndex) {
Display::send_mouse_up(&mut self.manager, index);
}
pub fn send_mouse_scroll(&mut self, delta_y: f32, delta_x: f32) {
Display::send_mouse_scroll(&mut self.manager, delta_y, delta_x);
}
pub fn send_key(&mut self, virtual_key: u32, down: bool) {
self.manager.send_key(virtual_key, down);
}
pub fn set_modifiers(&mut self, modifiers: u8) {
let changed = self.cur_modifiers ^ modifiers;
for i in 0..8 {
let m = 1 << i;
if changed & m != 0 {
if let Some(vk) = MODS_TO_KEYS.get(m).into_iter().flatten().next() {
self.send_key(*vk as u32, modifiers & m != 0);
}
}
}
self.cur_modifiers = modifiers;
}
pub fn set_display_visible(&mut self, display: display::DisplayHandle, visible: bool) {
if let Some(display) = self.displays.get_mut(&display) {
display.set_visible(visible);
}
}
pub fn set_display_layout(
&mut self,
display: display::DisplayHandle,
layout: packet_server::WvrDisplayWindowLayout,
) {
if let Some(display) = self.displays.get_mut(&display) {
display.set_layout(layout);
}
}
pub fn get_render_data(
&self,
display: display::DisplayHandle,
) -> Option<&egl_data::RenderData> {
self.displays
.get(&display)
.map(|display| &display.render_data)
}
pub fn create_display(
&mut self,
width: u16,
height: u16,
name: &str,
primary: bool,
) -> anyhow::Result<display::DisplayHandle> {
let display = display::Display::new(DisplayInitParams {
wm: self.wm.clone(),
egl_data: self.egl_data.clone(),
renderer: &mut self.manager.state.gles_renderer,
wayland_env: self.manager.wayland_env.clone(),
config: &self.config,
width,
height,
name,
primary,
})?;
let handle = self.displays.add(display);
self.signals.send(WayVRSignal::BroadcastStateChanged(
packet_server::WvrStateChanged::DisplayCreated,
));
Ok(handle)
}
pub fn destroy_display(&mut self, handle: display::DisplayHandle) -> anyhow::Result<()> {
let Some(display) = self.displays.get(&handle) else {
anyhow::bail!("Display not found");
};
if let Some(overlay_id) = display.overlay_id {
self.signals.send(WayVRSignal::DropOverlay(overlay_id));
} else {
log::warn!("Destroying display without OverlayID set"); // This shouldn't happen, but log it anyways.
}
let mut process_names = Vec::<String>::new();
for (_, process) in self.processes.iter_mut() {
if process.display_handle() == handle {
process_names.push(process.get_name());
}
}
if !display.displayed_windows.is_empty() || !process_names.is_empty() {
anyhow::bail!(
"Display is not empty. Attached processes: {}",
process_names.join(", ")
);
}
self.manager.cleanup_clients();
for client in &self.manager.clients {
if client.display_handle == handle {
// This shouldn't happen, but make sure we are all set to destroy this display
anyhow::bail!("Wayland client still exists");
}
}
self.displays.remove(&handle);
self.signals.send(WayVRSignal::BroadcastStateChanged(
packet_server::WvrStateChanged::DisplayRemoved,
));
Ok(())
}
pub fn get_or_create_dashboard_display(
&mut self,
width: u16,
height: u16,
name: &str,
) -> anyhow::Result<(bool /* newly created? */, display::DisplayHandle)> {
if let Some(handle) = &self.dashboard_display {
// ensure it still exists
if self.displays.get(handle).is_some() {
return Ok((false, *handle));
}
}
let new_disp = self.create_display(width, height, name, false)?;
self.dashboard_display = Some(new_disp);
Ok((true, new_disp))
}
// Check if process with given arguments already exists
pub fn process_query(
&self,
display_handle: display::DisplayHandle,
exec_path: &str,
args: &[&str],
_env: &[(&str, &str)],
) -> Option<process::ProcessHandle> {
for (idx, cell) in self.processes.vec.iter().enumerate() {
if let Some(cell) = &cell {
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
}
pub fn add_external_process(
&mut self,
display_handle: display::DisplayHandle,
pid: u32,
) -> process::ProcessHandle {
self.processes
.add(process::Process::External(process::ExternalProcess {
pid,
display_handle,
}))
}
pub fn spawn_process(
&mut self,
display_handle: display::DisplayHandle,
exec_path: &str,
args: &[&str],
env: &[(&str, &str)],
working_dir: Option<&str>,
userdata: HashMap<String, String>,
) -> anyhow::Result<process::ProcessHandle> {
let display = self
.displays
.get_mut(&display_handle)
.ok_or_else(|| anyhow::anyhow!(STR_INVALID_HANDLE_DISP))?;
let res = display.spawn_process(exec_path, args, env, working_dir)?;
let handle = self
.processes
.add(process::Process::Managed(process::WayVRProcess {
auth_key: res.auth_key,
child: res.child,
display_handle,
exec_path: String::from(exec_path),
userdata,
args: args.iter().map(|x| String::from(*x)).collect(),
working_dir: working_dir.map(String::from),
env: env
.iter()
.map(|(a, b)| (String::from(*a), String::from(*b)))
.collect(),
}));
self.signals.send(WayVRSignal::BroadcastStateChanged(
packet_server::WvrStateChanged::ProcessCreated,
));
Ok(handle)
}
}
#[derive(Deserialize, Clone)]
pub enum WayVRDisplayClickAction {
ToggleVisibility,
Reset,
}
#[derive(Deserialize, Clone)]
pub enum WayVRAction {
AppClick {
catalog_name: Arc<str>,
app_name: Arc<str>,
},
DisplayClick {
display_name: Arc<str>,
action: WayVRDisplayClickAction,
},
ToggleDashboard,
}

View File

@@ -0,0 +1,228 @@
use std::{collections::HashMap, io::Read};
use wayvr_ipc::packet_server;
use crate::gen_id;
use super::display;
#[derive(Debug)]
#[allow(dead_code)]
pub struct WayVRProcess {
pub auth_key: String,
pub child: std::process::Child,
pub display_handle: display::DisplayHandle,
pub exec_path: String,
pub args: Vec<String>,
pub env: Vec<(String, String)>,
pub working_dir: Option<String>,
pub userdata: HashMap<String, String>,
}
#[derive(Debug)]
pub struct ExternalProcess {
pub pid: u32,
pub display_handle: display::DisplayHandle,
}
#[derive(Debug)]
pub enum Process {
Managed(WayVRProcess), // Process spawned by WayVR
External(ExternalProcess), // External process not directly controlled by us
}
impl Process {
pub const fn display_handle(&self) -> display::DisplayHandle {
match self {
Self::Managed(p) => p.display_handle,
Self::External(p) => p.display_handle,
}
}
pub fn is_running(&mut self) -> bool {
match self {
Self::Managed(p) => p.is_running(),
Self::External(p) => p.is_running(),
}
}
pub fn terminate(&mut self) {
match self {
Self::Managed(p) => p.terminate(),
Self::External(p) => p.terminate(),
}
}
pub fn get_name(&self) -> String {
match self {
Self::Managed(p) => p.get_name().unwrap_or_else(|| String::from("unknown")),
Self::External(p) => p.get_name().unwrap_or_else(|| String::from("unknown")),
}
}
pub fn to_packet(&self, handle: ProcessHandle) -> packet_server::WvrProcess {
match self {
Self::Managed(p) => packet_server::WvrProcess {
name: p.get_name().unwrap_or_else(|| String::from("unknown")),
userdata: p.userdata.clone(),
display_handle: p.display_handle.as_packet(),
handle: handle.as_packet(),
},
Self::External(p) => packet_server::WvrProcess {
name: p.get_name().unwrap_or_else(|| String::from("unknown")),
userdata: HashMap::default(),
display_handle: p.display_handle.as_packet(),
handle: handle.as_packet(),
},
}
}
}
impl Drop for WayVRProcess {
fn drop(&mut self) {
log::info!(
"Sending SIGTERM (graceful exit) to process {}",
self.exec_path.as_str()
);
self.terminate();
}
}
fn get_process_env_value(pid: i32, key: &str) -> anyhow::Result<Option<String>> {
let path = format!("/proc/{pid}/environ");
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();
for line in lines {
if let Some(cell) = line.split_once('=') {
if cell.0 == key {
return Ok(Some(String::from(cell.1)));
}
}
}
Ok(None)
}
impl WayVRProcess {
fn is_running(&mut self) -> bool {
match self.child.try_wait() {
Ok(Some(_exit_status)) => false,
Ok(None) => true,
Err(e) => {
// this shouldn't happen
log::error!("Child::try_wait failed: {e}");
false
}
}
}
fn terminate(&mut self) {
unsafe {
// Gracefully stop process
libc::kill(self.child.id() as i32, libc::SIGTERM);
}
}
pub fn get_name(&self) -> Option<String> {
get_exec_name_from_pid(self.child.id())
}
}
fn get_exec_name_from_pid(pid: u32) -> Option<String> {
let path = format!("/proc/{pid}/exe");
match std::fs::read_link(&path) {
Ok(buf) => {
if let Some(process_name) = buf.file_name().and_then(|s| s.to_str()) {
return Some(String::from(process_name));
}
None
}
Err(_) => None,
}
}
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;
}
pub fn get_name(&self) -> Option<String> {
get_exec_name_from_pid(self.pid)
}
}
gen_id!(ProcessVec, Process, ProcessCell, ProcessHandle);
pub fn find_by_pid(processes: &ProcessVec, pid: u32) -> Option<ProcessHandle> {
log::debug!("Finding process with PID {pid}");
for (idx, cell) in processes.vec.iter().enumerate() {
let Some(cell) = cell else {
continue;
};
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));
}
}
}
}
log::debug!("Finding by PID failed, trying WAYVR_DISPLAY_AUTH...");
if let Ok(Some(value)) = get_process_env_value(pid as i32, "WAYVR_DISPLAY_AUTH") {
for (idx, cell) in processes.vec.iter().enumerate() {
let Some(cell) = cell else {
continue;
};
if let Process::Managed(wayvr_process) = &cell.obj {
if wayvr_process.auth_key == value {
return Some(ProcessVec::get_handle(cell, idx));
}
}
}
}
log::debug!("Process find with PID {pid} failed");
None
}
impl ProcessHandle {
pub const fn from_packet(handle: packet_server::WvrProcessHandle) -> Self {
Self {
generation: handle.generation,
idx: handle.idx,
}
}
pub const fn as_packet(&self) -> packet_server::WvrProcessHandle {
packet_server::WvrProcessHandle {
idx: self.idx,
generation: self.generation,
}
}
}

View File

@@ -0,0 +1,663 @@
use crate::state::AppState;
use super::{display, process, window, TickTask, WayVRSignal};
use bytes::BufMut;
use glam::Vec3A;
use interprocess::local_socket::{self, traits::Listener, ToNsName};
use smallvec::SmallVec;
use std::io::{Read, Write};
use wayvr_ipc::{
ipc::{self},
packet_client::{self, PacketClient},
packet_server::{self, PacketServer, WlxInputStatePointer},
};
pub struct AuthInfo {
pub client_name: String,
pub protocol_version: u32, // client protocol version
}
pub struct Connection {
alive: bool,
conn: local_socket::Stream,
next_packet: Option<u32>,
auth: Option<AuthInfo>,
}
pub fn send_packet(conn: &mut local_socket::Stream, data: &[u8]) -> anyhow::Result<()> {
let mut bytes = bytes::BytesMut::new();
// packet size
bytes.put_u32(data.len() as u32);
// packet data
bytes.put_slice(data);
conn.write_all(&bytes)?;
Ok(())
}
fn read_check(expected_size: u32, res: std::io::Result<usize>) -> bool {
match res {
Ok(count) => {
if count == 0 {
return false;
}
if count as u32 == expected_size {
true // read succeeded
} else {
log::error!("count {count} is not {expected_size}");
false
}
}
Err(_e) => {
//log::error!("failed to get packet size: {}", e);
false
}
}
}
type Payload = SmallVec<[u8; 64]>;
fn read_payload(conn: &mut local_socket::Stream, size: u32) -> Option<Payload> {
let mut payload = Payload::new();
payload.resize(size as usize, 0);
if read_check(size, conn.read(&mut payload)) {
Some(payload)
} else {
None
}
}
pub struct TickParams<'a> {
pub state: &'a mut super::WayVRState,
pub tasks: &'a mut Vec<TickTask>,
pub app: &'a AppState,
}
pub fn gen_args_vec(input: &str) -> Vec<&str> {
input.split_whitespace().collect()
}
pub fn gen_env_vec(input: &[String]) -> Vec<(&str, &str)> {
let res = input
.iter()
.filter_map(|e| e.as_str().split_once('='))
.collect();
res
}
impl Connection {
const fn new(conn: local_socket::Stream) -> Self {
Self {
conn,
alive: true,
auth: None,
next_packet: None,
}
}
fn kill(&mut self, reason: &str) {
let _dont_care = send_packet(
&mut self.conn,
&ipc::data_encode(&PacketServer::Disconnect(packet_server::Disconnect {
reason: String::from(reason),
})),
);
self.alive = false;
}
fn process_handshake(&mut self, handshake: &packet_client::Handshake) -> anyhow::Result<()> {
if self.auth.is_some() {
anyhow::bail!("You were already authenticated");
}
if handshake.protocol_version != ipc::PROTOCOL_VERSION {
anyhow::bail!(
"Unsupported protocol version {}",
handshake.protocol_version
);
}
if handshake.magic != ipc::CONNECTION_MAGIC {
anyhow::bail!("Invalid magic");
}
match handshake.client_name.len() {
0 => anyhow::bail!("Client name is empty"),
1..32 => {}
_ => anyhow::bail!("Client name is too long"),
}
log::info!("IPC: Client \"{}\" connected.", handshake.client_name);
self.auth = Some(AuthInfo {
client_name: handshake.client_name.clone(),
protocol_version: handshake.protocol_version,
});
// Send auth response
send_packet(
&mut self.conn,
&ipc::data_encode(&PacketServer::HandshakeSuccess(
packet_server::HandshakeSuccess {
runtime: String::from("wlx-overlay-s"),
},
)),
)?;
Ok(())
}
fn handle_wvr_display_list(
&mut self,
params: &TickParams,
serial: ipc::Serial,
) -> anyhow::Result<()> {
let list: Vec<packet_server::WvrDisplay> = params
.state
.displays
.vec
.iter()
.enumerate()
.filter_map(|(idx, opt_cell)| {
let Some(cell) = opt_cell else {
return None;
};
let display = &cell.obj;
Some(display.as_packet(display::DisplayHandle::new(idx as u32, cell.generation)))
})
.collect();
send_packet(
&mut self.conn,
&ipc::data_encode(&PacketServer::WvrDisplayListResponse(
serial,
packet_server::WvrDisplayList { list },
)),
)?;
Ok(())
}
fn handle_wlx_input_state(
&mut self,
params: &TickParams,
serial: ipc::Serial,
) -> anyhow::Result<()> {
let input_state = &params.app.input_state;
let to_arr = |vec: &Vec3A| -> [f32; 3] { [vec.x, vec.y, vec.z] };
send_packet(
&mut self.conn,
&ipc::data_encode(&PacketServer::WlxInputStateResponse(
serial,
packet_server::WlxInputState {
hmd_pos: to_arr(&input_state.hmd.translation),
left: WlxInputStatePointer {
pos: to_arr(&input_state.pointers[0].raw_pose.translation),
},
right: WlxInputStatePointer {
pos: to_arr(&input_state.pointers[0].raw_pose.translation),
},
},
)),
)?;
Ok(())
}
fn handle_wvr_display_create(
&mut self,
params: &mut TickParams,
serial: ipc::Serial,
packet_params: packet_client::WvrDisplayCreateParams,
) -> anyhow::Result<()> {
let display_handle = params.state.create_display(
packet_params.width,
packet_params.height,
&packet_params.name,
false,
)?;
params
.tasks
.push(TickTask::NewDisplay(packet_params, Some(display_handle)));
send_packet(
&mut self.conn,
&ipc::data_encode(&PacketServer::WvrDisplayCreateResponse(
serial,
display_handle.as_packet(),
)),
)?;
Ok(())
}
fn handle_wvr_display_remove(
&mut self,
params: &mut TickParams,
serial: ipc::Serial,
handle: packet_server::WvrDisplayHandle,
) -> anyhow::Result<()> {
let res = params
.state
.destroy_display(display::DisplayHandle::from_packet(handle))
.map_err(|e| format!("{e:?}"));
send_packet(
&mut self.conn,
&ipc::data_encode(&PacketServer::WvrDisplayRemoveResponse(serial, res)),
)?;
Ok(())
}
fn handle_wvr_display_set_visible(
params: &mut TickParams,
handle: packet_server::WvrDisplayHandle,
visible: bool,
) {
params.state.signals.send(WayVRSignal::DisplayVisibility(
display::DisplayHandle::from_packet(handle),
visible,
));
}
fn handle_wvr_display_set_window_layout(
params: &mut TickParams,
handle: packet_server::WvrDisplayHandle,
layout: packet_server::WvrDisplayWindowLayout,
) {
params.state.signals.send(WayVRSignal::DisplayWindowLayout(
display::DisplayHandle::from_packet(handle),
layout,
));
}
fn handle_wvr_display_window_list(
&mut self,
params: &mut TickParams,
serial: ipc::Serial,
display_handle: packet_server::WvrDisplayHandle,
) -> anyhow::Result<()> {
let mut send = |list: Option<packet_server::WvrWindowList>| -> anyhow::Result<()> {
send_packet(
&mut self.conn,
&ipc::data_encode(&PacketServer::WvrDisplayWindowListResponse(serial, list)),
)
};
let Some(display) = params
.state
.displays
.get(&display::DisplayHandle::from_packet(display_handle.clone()))
else {
return send(None);
};
send(Some(packet_server::WvrWindowList {
list: display
.displayed_windows
.iter()
.filter_map(|disp_win| {
params
.state
.wm
.borrow_mut()
.windows
.get(&disp_win.window_handle)
.map(|win| packet_server::WvrWindow {
handle: window::WindowHandle::as_packet(&disp_win.window_handle),
process_handle: process::ProcessHandle::as_packet(
&disp_win.process_handle,
),
pos_x: win.pos_x,
pos_y: win.pos_y,
size_x: win.size_x,
size_y: win.size_y,
visible: win.visible,
display_handle: display_handle.clone(),
})
})
.collect::<Vec<_>>(),
}))
}
fn handle_wvr_window_set_visible(
params: &mut TickParams,
handle: packet_server::WvrWindowHandle,
visible: bool,
) {
let mut to_resize = None;
if let Some(window) = params
.state
.wm
.borrow_mut()
.windows
.get_mut(&window::WindowHandle::from_packet(handle))
{
window.visible = visible;
to_resize = Some(window.display_handle);
}
if let Some(to_resize) = to_resize {
if let Some(display) = params.state.displays.get_mut(&to_resize) {
display.reposition_windows();
display.trigger_rerender();
}
}
}
fn handle_wvr_process_launch(
&mut self,
params: &mut TickParams,
serial: ipc::Serial,
packet_params: packet_client::WvrProcessLaunchParams,
) -> anyhow::Result<()> {
let args_vec = gen_args_vec(&packet_params.args);
let env_vec = gen_env_vec(&packet_params.env);
let res = params.state.spawn_process(
super::display::DisplayHandle::from_packet(packet_params.target_display),
&packet_params.exec,
&args_vec,
&env_vec,
None,
packet_params.userdata,
);
let res = res.map(|r| r.as_packet()).map_err(|e| e.to_string());
send_packet(
&mut self.conn,
&ipc::data_encode(&PacketServer::WvrProcessLaunchResponse(serial, res)),
)?;
Ok(())
}
fn handle_wvr_display_get(
&mut self,
params: &TickParams,
serial: ipc::Serial,
display_handle: packet_server::WvrDisplayHandle,
) -> anyhow::Result<()> {
let native_handle = &display::DisplayHandle::from_packet(display_handle);
let disp = params
.state
.displays
.get(native_handle)
.map(|disp| disp.as_packet(*native_handle));
send_packet(
&mut self.conn,
&ipc::data_encode(&PacketServer::WvrDisplayGetResponse(serial, disp)),
)?;
Ok(())
}
fn handle_wvr_process_list(
&mut self,
params: &TickParams,
serial: ipc::Serial,
) -> anyhow::Result<()> {
let list: Vec<packet_server::WvrProcess> = params
.state
.processes
.vec
.iter()
.enumerate()
.filter_map(|(idx, opt_cell)| {
let Some(cell) = opt_cell else {
return None;
};
let process = &cell.obj;
Some(process.to_packet(process::ProcessHandle::new(idx as u32, cell.generation)))
})
.collect();
send_packet(
&mut self.conn,
&ipc::data_encode(&PacketServer::WvrProcessListResponse(
serial,
packet_server::WvrProcessList { list },
)),
)?;
Ok(())
}
// This request doesn't return anything to the client
fn handle_wvr_process_terminate(
params: &mut TickParams,
process_handle: packet_server::WvrProcessHandle,
) {
let native_handle = &process::ProcessHandle::from_packet(process_handle);
let process = params.state.processes.get_mut(native_handle);
let Some(process) = process else {
return;
};
process.terminate();
}
fn handle_wvr_process_get(
&mut self,
params: &TickParams,
serial: ipc::Serial,
process_handle: packet_server::WvrProcessHandle,
) -> anyhow::Result<()> {
let native_handle = &process::ProcessHandle::from_packet(process_handle);
let process = params
.state
.processes
.get(native_handle)
.map(|process| process.to_packet(*native_handle));
send_packet(
&mut self.conn,
&ipc::data_encode(&PacketServer::WvrProcessGetResponse(serial, process)),
)?;
Ok(())
}
fn handle_wlx_haptics(
params: &mut TickParams,
haptics_params: packet_client::WlxHapticsParams,
) {
params.state.signals.send(super::WayVRSignal::Haptics(
crate::backend::input::Haptics {
duration: haptics_params.duration,
frequency: haptics_params.frequency,
intensity: haptics_params.intensity,
},
));
}
fn process_payload(&mut self, params: &mut TickParams, payload: Payload) -> anyhow::Result<()> {
let packet: PacketClient = ipc::data_decode(&payload)?;
if let PacketClient::Handshake(handshake) = &packet {
self.process_handshake(handshake)?;
return Ok(());
}
match packet {
PacketClient::Handshake(_) => unreachable!(), // handled previously
PacketClient::WlxInputState(serial) => {
self.handle_wlx_input_state(params, serial)?;
}
PacketClient::WvrDisplayList(serial) => {
self.handle_wvr_display_list(params, serial)?;
}
PacketClient::WvrDisplayGet(serial, display_handle) => {
self.handle_wvr_display_get(params, serial, display_handle)?;
}
PacketClient::WvrDisplayRemove(serial, display_handle) => {
self.handle_wvr_display_remove(params, serial, display_handle)?;
}
PacketClient::WvrDisplaySetVisible(display_handle, visible) => {
Self::handle_wvr_display_set_visible(params, display_handle, visible);
}
PacketClient::WvrDisplaySetWindowLayout(display_handle, layout) => {
Self::handle_wvr_display_set_window_layout(params, display_handle, layout);
}
PacketClient::WvrDisplayWindowList(serial, display_handle) => {
self.handle_wvr_display_window_list(params, serial, display_handle)?;
}
PacketClient::WvrWindowSetVisible(window_handle, visible) => {
Self::handle_wvr_window_set_visible(params, window_handle, visible);
}
PacketClient::WvrProcessGet(serial, process_handle) => {
self.handle_wvr_process_get(params, serial, process_handle)?;
}
PacketClient::WvrProcessList(serial) => {
self.handle_wvr_process_list(params, serial)?;
}
PacketClient::WvrProcessLaunch(serial, packet_params) => {
self.handle_wvr_process_launch(params, serial, packet_params)?;
}
PacketClient::WvrDisplayCreate(serial, packet_params) => {
self.handle_wvr_display_create(params, serial, packet_params)?;
}
PacketClient::WvrProcessTerminate(process_handle) => {
Self::handle_wvr_process_terminate(params, process_handle);
}
PacketClient::WlxHaptics(haptics_params) => {
Self::handle_wlx_haptics(params, haptics_params);
}
}
Ok(())
}
fn process_check_payload(&mut self, params: &mut TickParams, payload: Payload) -> bool {
log::debug!("payload size {}", payload.len());
if let Err(e) = self.process_payload(params, payload) {
log::error!("Invalid payload from the client, closing connection: {e}");
// send also error message directly to the client before disconnecting
self.kill(format!("{e}").as_str());
false
} else {
true
}
}
fn read_packet(&mut self, params: &mut TickParams) -> bool {
if let Some(payload_size) = self.next_packet {
let Some(payload) = read_payload(&mut self.conn, payload_size) else {
// still failed to read payload, try in next tick
return false;
};
if !self.process_check_payload(params, payload) {
return false;
}
self.next_packet = None;
}
let mut buf_packet_header: [u8; 4] = [0; 4];
if !read_check(4, self.conn.read(&mut buf_packet_header)) {
return false;
}
let payload_size = u32::from_be_bytes(buf_packet_header[0..4].try_into().unwrap()); // 0-3 bytes (u32 size)
let size_limit: u32 = 128 * 1024;
if payload_size > size_limit {
// over 128 KiB?
log::error!(
"Client sent a packet header with the size over {size_limit} bytes, closing connection."
);
self.kill("Too big packet received (over 128 KiB)");
return false;
}
let Some(payload) = read_payload(&mut self.conn, payload_size) else {
// failed to read payload, try in next tick
self.next_packet = Some(payload_size);
return false;
};
if !self.process_check_payload(params, payload) {
return false;
}
true
}
fn tick(&mut self, params: &mut TickParams) {
while self.read_packet(params) {}
}
}
impl Drop for Connection {
fn drop(&mut self) {
log::info!("Connection closed");
}
}
pub struct WayVRServer {
listener: local_socket::Listener,
connections: Vec<Connection>,
}
impl WayVRServer {
pub fn new() -> anyhow::Result<Self> {
let printname = "/tmp/wayvr_ipc.sock";
let name = printname.to_ns_name::<local_socket::GenericNamespaced>()?;
let opts = local_socket::ListenerOptions::new()
.name(name)
.nonblocking(local_socket::ListenerNonblockingMode::Both);
let listener = match opts.create_sync() {
Ok(listener) => listener,
Err(e) => anyhow::bail!("Failed to start WayVRServer IPC listener. Reason: {}", e),
};
log::info!("WayVRServer IPC running at {printname}");
Ok(Self {
listener,
connections: Vec::new(),
})
}
fn accept_connections(&mut self) {
let Ok(conn) = self.listener.accept() else {
return; // No new connection or other error
};
self.connections.push(Connection::new(conn));
}
fn tick_connections(&mut self, params: &mut TickParams) {
for c in &mut self.connections {
c.tick(params);
}
// remove killed connections
self.connections.retain(|c| c.alive);
}
pub fn tick(&mut self, params: &mut TickParams) {
self.accept_connections();
self.tick_connections(params);
}
pub fn broadcast(&mut self, packet: packet_server::PacketServer) {
for connection in &mut self.connections {
if let Err(e) = send_packet(&mut connection.conn, &ipc::data_encode(&packet)) {
log::error!("failed to broadcast packet: {e:?}");
}
}
}
}

View File

@@ -0,0 +1,54 @@
use super::egl_data;
use smithay::backend::{egl as smithay_egl, renderer::gles::ffi};
pub fn get_egl_display(data: &egl_data::EGLData) -> anyhow::Result<smithay_egl::EGLDisplay> {
Ok(unsafe { smithay_egl::EGLDisplay::from_raw(data.display.as_ptr(), data.config.as_ptr())? })
}
pub fn get_egl_context(
data: &egl_data::EGLData,
display: &smithay_egl::EGLDisplay,
) -> anyhow::Result<smithay_egl::EGLContext> {
let display_ptr = display.get_display_handle().handle;
debug_assert!(std::ptr::eq(display_ptr, data.display.as_ptr()));
let config_ptr = data.config.as_ptr();
let context_ptr = data.context.as_ptr();
Ok(unsafe { smithay_egl::EGLContext::from_raw(display_ptr, config_ptr, context_ptr)? })
}
pub fn create_framebuffer_texture(
gl: &ffi::Gles2,
width: u32,
height: u32,
tex_format: u32,
internal_format: u32,
) -> u32 {
unsafe {
let mut tex = 0;
gl.GenTextures(1, &mut tex);
gl.BindTexture(ffi::TEXTURE_2D, tex);
gl.TexParameteri(
ffi::TEXTURE_2D,
ffi::TEXTURE_MIN_FILTER,
ffi::NEAREST as i32,
);
gl.TexParameteri(
ffi::TEXTURE_2D,
ffi::TEXTURE_MAG_FILTER,
ffi::NEAREST as i32,
);
gl.TexImage2D(
ffi::TEXTURE_2D,
0,
internal_format as i32,
width as i32,
height as i32,
0,
tex_format,
ffi::UNSIGNED_BYTE,
std::ptr::null(),
);
gl.BindTexture(ffi::TEXTURE_2D, 0);
tex
}
}

View File

@@ -0,0 +1,9 @@
use std::time::{SystemTime, UNIX_EPOCH};
// Returns milliseconds since unix epoch
pub fn get_millis() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64
}

View File

@@ -0,0 +1,102 @@
use smithay::wayland::shell::xdg::ToplevelSurface;
use wayvr_ipc::packet_server;
use crate::gen_id;
use super::display;
#[derive(Debug)]
pub struct Window {
pub pos_x: i32,
pub pos_y: i32,
pub size_x: u32,
pub size_y: u32,
pub visible: bool,
pub toplevel: ToplevelSurface,
pub display_handle: display::DisplayHandle,
}
impl Window {
pub fn new(display_handle: display::DisplayHandle, toplevel: &ToplevelSurface) -> Self {
Self {
pos_x: 0,
pos_y: 0,
size_x: 0,
size_y: 0,
visible: true,
toplevel: toplevel.clone(),
display_handle,
}
}
pub const fn set_pos(&mut self, pos_x: i32, pos_y: i32) {
self.pos_x = pos_x;
self.pos_y = pos_y;
}
pub fn set_size(&mut self, size_x: u32, size_y: u32) {
self.toplevel.with_pending_state(|state| {
//state.bounds = Some((size_x as i32, size_y as i32).into());
state.size = Some((size_x as i32, size_y as i32).into());
});
self.toplevel.send_configure();
self.size_x = size_x;
self.size_y = size_y;
}
}
#[derive(Debug)]
pub struct WindowManager {
pub windows: WindowVec,
}
impl WindowManager {
pub const fn new() -> Self {
Self {
windows: WindowVec::new(),
}
}
pub fn find_window_handle(&self, toplevel: &ToplevelSurface) -> Option<WindowHandle> {
for (idx, cell) in self.windows.vec.iter().enumerate() {
if let Some(cell) = cell {
let window = &cell.obj;
if window.toplevel == *toplevel {
return Some(WindowVec::get_handle(cell, idx));
}
}
}
None
}
pub fn create_window(
&mut self,
display_handle: display::DisplayHandle,
toplevel: &ToplevelSurface,
) -> WindowHandle {
self.windows.add(Window::new(display_handle, toplevel))
}
pub fn remove_window(&mut self, window_handle: WindowHandle) {
self.windows.remove(&window_handle);
}
}
gen_id!(WindowVec, Window, WindowCell, WindowHandle);
impl WindowHandle {
pub const fn from_packet(handle: packet_server::WvrWindowHandle) -> Self {
Self {
generation: handle.generation,
idx: handle.idx,
}
}
pub const fn as_packet(&self) -> packet_server::WvrWindowHandle {
packet_server::WvrWindowHandle {
idx: self.idx,
generation: self.generation,
}
}
}