new workspace
This commit is contained in:
370
wlx-overlay-s/src/backend/common.rs
Normal file
370
wlx-overlay-s/src/backend/common.rs
Normal 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
|
||||
}
|
||||
}
|
||||
744
wlx-overlay-s/src/backend/input.rs
Normal file
744
wlx-overlay-s/src/backend/input.rs
Normal 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()))
|
||||
}
|
||||
22
wlx-overlay-s/src/backend/mod.rs
Normal file
22
wlx-overlay-s/src/backend/mod.rs
Normal 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;
|
||||
292
wlx-overlay-s/src/backend/notifications.rs
Normal file
292
wlx-overlay-s/src/backend/notifications.rs
Normal 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>,
|
||||
}
|
||||
353
wlx-overlay-s/src/backend/notifications_dbus.rs
Normal file
353
wlx-overlay-s/src/backend/notifications_dbus.rs
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
188
wlx-overlay-s/src/backend/openvr/helpers.rs
Normal file
188
wlx-overlay-s/src/backend/openvr/helpers.rs
Normal 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(())
|
||||
}
|
||||
360
wlx-overlay-s/src/backend/openvr/input.rs
Normal file
360
wlx-overlay-s/src/backend/openvr/input.rs
Normal 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(())
|
||||
}
|
||||
264
wlx-overlay-s/src/backend/openvr/lines.rs
Normal file
264
wlx-overlay-s/src/backend/openvr/lines.rs
Normal 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)
|
||||
}
|
||||
88
wlx-overlay-s/src/backend/openvr/manifest.rs
Normal file
88
wlx-overlay-s/src/backend/openvr/manifest.rs
Normal 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(())
|
||||
}
|
||||
389
wlx-overlay-s/src/backend/openvr/mod.rs
Normal file
389
wlx-overlay-s/src/backend/openvr/mod.rs
Normal 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(())
|
||||
}
|
||||
299
wlx-overlay-s/src/backend/openvr/overlay.rs
Normal file
299
wlx-overlay-s/src/backend/openvr/overlay.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
302
wlx-overlay-s/src/backend/openvr/playspace.rs
Normal file
302
wlx-overlay-s/src/backend/openvr/playspace.rs
Normal 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());
|
||||
}
|
||||
73
wlx-overlay-s/src/backend/openxr/blocker.rs
Normal file
73
wlx-overlay-s/src/backend/openxr/blocker.rs
Normal 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}"),
|
||||
}
|
||||
}
|
||||
190
wlx-overlay-s/src/backend/openxr/helpers.rs
Normal file
190
wlx-overlay-s/src/backend/openxr/helpers.rs
Normal 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)
|
||||
}
|
||||
692
wlx-overlay-s/src/backend/openxr/input.rs
Normal file
692
wlx-overlay-s/src/backend/openxr/input.rs
Normal 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
|
||||
}
|
||||
201
wlx-overlay-s/src/backend/openxr/lines.rs
Normal file
201
wlx-overlay-s/src/backend/openxr/lines.rs
Normal 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>,
|
||||
}
|
||||
577
wlx-overlay-s/src/backend/openxr/mod.rs
Normal file
577
wlx-overlay-s/src/backend/openxr/mod.rs
Normal 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>),
|
||||
}
|
||||
301
wlx-overlay-s/src/backend/openxr/openxr_actions.json5
Normal file
301
wlx-overlay-s/src/backend/openxr/openxr_actions.json5
Normal 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",
|
||||
},
|
||||
},
|
||||
|
||||
]
|
||||
123
wlx-overlay-s/src/backend/openxr/overlay.rs
Normal file
123
wlx-overlay-s/src/backend/openxr/overlay.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
199
wlx-overlay-s/src/backend/openxr/playspace.rs
Normal file
199
wlx-overlay-s/src/backend/openxr/playspace.rs
Normal 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);
|
||||
}
|
||||
252
wlx-overlay-s/src/backend/openxr/skybox.rs
Normal file
252
wlx-overlay-s/src/backend/openxr/skybox.rs
Normal 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())
|
||||
}
|
||||
125
wlx-overlay-s/src/backend/openxr/swapchain.rs
Normal file
125
wlx-overlay-s/src/backend/openxr/swapchain.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
183
wlx-overlay-s/src/backend/osc.rs
Normal file
183
wlx-overlay-s/src/backend/osc.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
465
wlx-overlay-s/src/backend/overlay.rs
Normal file
465
wlx-overlay-s/src/backend/overlay.rs
Normal 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)
|
||||
}
|
||||
118
wlx-overlay-s/src/backend/task.rs
Normal file
118
wlx-overlay-s/src/backend/task.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
252
wlx-overlay-s/src/backend/wayvr/client.rs
Normal file
252
wlx-overlay-s/src/backend/wayvr/client.rs
Normal 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))
|
||||
}
|
||||
236
wlx-overlay-s/src/backend/wayvr/comp.rs
Normal file
236
wlx-overlay-s/src/backend/wayvr/comp.rs
Normal 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,
|
||||
);
|
||||
}
|
||||
605
wlx-overlay-s/src/backend/wayvr/display.rs
Normal file
605
wlx-overlay-s/src/backend/wayvr/display.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
315
wlx-overlay-s/src/backend/wayvr/egl_data.rs
Normal file
315
wlx-overlay-s/src/backend/wayvr/egl_data.rs
Normal 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],
|
||||
)?)
|
||||
}
|
||||
}
|
||||
}
|
||||
49
wlx-overlay-s/src/backend/wayvr/egl_ex.rs
Normal file
49
wlx-overlay-s/src/backend/wayvr/egl_ex.rs
Normal 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,
|
||||
>;
|
||||
33
wlx-overlay-s/src/backend/wayvr/event_queue.rs
Normal file
33
wlx-overlay-s/src/backend/wayvr/event_queue.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
176
wlx-overlay-s/src/backend/wayvr/handle.rs
Normal file
176
wlx-overlay-s/src/backend/wayvr/handle.rs
Normal 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 {}
|
||||
*/
|
||||
740
wlx-overlay-s/src/backend/wayvr/mod.rs
Normal file
740
wlx-overlay-s/src/backend/wayvr/mod.rs
Normal 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,
|
||||
}
|
||||
228
wlx-overlay-s/src/backend/wayvr/process.rs
Normal file
228
wlx-overlay-s/src/backend/wayvr/process.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
663
wlx-overlay-s/src/backend/wayvr/server_ipc.rs
Normal file
663
wlx-overlay-s/src/backend/wayvr/server_ipc.rs
Normal 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 = ¶ms.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:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
54
wlx-overlay-s/src/backend/wayvr/smithay_wrapper.rs
Normal file
54
wlx-overlay-s/src/backend/wayvr/smithay_wrapper.rs
Normal 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
|
||||
}
|
||||
}
|
||||
9
wlx-overlay-s/src/backend/wayvr/time.rs
Normal file
9
wlx-overlay-s/src/backend/wayvr/time.rs
Normal 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
|
||||
}
|
||||
102
wlx-overlay-s/src/backend/wayvr/window.rs
Normal file
102
wlx-overlay-s/src/backend/wayvr/window.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user