diff --git a/src/backend/common.rs b/src/backend/common.rs index 8a6bdf6..4b7c1c6 100644 --- a/src/backend/common.rs +++ b/src/backend/common.rs @@ -1,15 +1,10 @@ -use std::{ - collections::{BinaryHeap, VecDeque}, - f32::consts::PI, - sync::Arc, - time::Instant, -}; +use std::sync::Arc; use once_cell::sync::Lazy; #[cfg(feature = "openxr")] use openxr as xr; -use glam::{Affine3A, Vec2, Vec3, Vec3A, Vec3Swizzles}; +use glam::{Affine3A, Vec3, Vec3A}; use idmap::IdMap; use serde::Deserialize; use thiserror::Error; @@ -25,7 +20,7 @@ use crate::{ state::AppState, }; -use super::overlay::{OverlayBackend, OverlayData, OverlayState}; +use super::overlay::OverlayData; #[derive(Error, Debug)] pub enum BackendError { @@ -351,167 +346,6 @@ pub enum OverlaySelector { Name(Arc), } -struct AppTask { - pub not_before: Instant, - pub task: TaskType, -} - -impl PartialEq for AppTask { - fn eq(&self, other: &Self) -> bool { - self.not_before == other.not_before - } -} -impl PartialOrd for AppTask { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} -impl Eq for AppTask {} -impl Ord for AppTask { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.not_before.cmp(&other.not_before).reverse() - } -} - -pub enum SystemTask { - ColorGain(ColorChannel, f32), - ResetPlayspace, - FixFloor, -} - -pub type OverlayTask = dyn FnOnce(&mut AppState, &mut OverlayState) + Send; -pub type CreateOverlayTask = - dyn FnOnce(&mut AppState) -> Option<(OverlayState, Box)> + Send; - -pub enum TaskType { - Global(Box), - Overlay(OverlaySelector, Box), - CreateOverlay(OverlaySelector, Box), - DropOverlay(OverlaySelector), - System(SystemTask), -} - -#[derive(Deserialize, Clone, Copy)] -pub enum ColorChannel { - R, - G, - B, - All, -} - -pub struct TaskContainer { - tasks: BinaryHeap, -} - -impl TaskContainer { - pub fn new() -> Self { - Self { - tasks: BinaryHeap::new(), - } - } - - pub fn enqueue(&mut self, task: TaskType) { - self.tasks.push(AppTask { - not_before: Instant::now(), - task, - }); - } - - pub fn enqueue_at(&mut self, task: TaskType, not_before: Instant) { - self.tasks.push(AppTask { not_before, task }); - } - - pub fn retrieve_due(&mut self, dest_buf: &mut VecDeque) { - 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); - } - } -} - -pub fn raycast_plane( - source: &Affine3A, - source_fwd: Vec3A, - plane: &Affine3A, - plane_norm: Vec3A, -) -> Option<(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(); - - Some((dist, hit_local)) -} - -pub 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 r = 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 * r; - - let d = ray_dir.xz(); - let s = ray_origin.xz(); - - let a = d.dot(d); - let b = d.dot(s); - let c = s.dot(s) - r * r; - - let d = (b * b) - (a * c); - if d < f32::EPSILON { - return None; - } - - let sqrt_d = d.sqrt(); - - let t1 = (-b - sqrt_d) / a; - let t2 = (-b + sqrt_d) / a; - - 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 * r)); - let x_angle = (hit_local.x / r).asin(); - - hit_local.x = x_angle / max_angle; - hit_local.y /= size; - - Some((t, hit_local.xy())) -} - 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(); diff --git a/src/backend/input.rs b/src/backend/input.rs index 9b76db3..0a99b09 100644 --- a/src/backend/input.rs +++ b/src/backend/input.rs @@ -1,19 +1,17 @@ +use std::f32::consts::PI; use std::{collections::VecDeque, time::Instant}; -use glam::{Affine3A, Vec2, Vec3, Vec3A}; +use glam::{Affine3A, Vec2, Vec3, Vec3A, Vec3Swizzles}; use smallvec::{smallvec, SmallVec}; -use crate::backend::common::{snap_upright, OverlaySelector, TaskType}; +use crate::backend::common::{snap_upright, OverlaySelector}; use crate::config::{save_state, AStrMapExt, GeneralConfig}; use crate::overlays::anchor::ANCHOR_NAME; use crate::state::AppState; -use super::common::TaskContainer; -use super::{ - common::{raycast_cylinder, raycast_plane, OverlayContainer}, - overlay::OverlayData, -}; +use super::task::{TaskContainer, TaskType}; +use super::{common::OverlayContainer, overlay::OverlayData}; pub struct TrackedDevice { pub soc: Option, @@ -590,3 +588,80 @@ impl Pointer { }) } } + +fn raycast_plane( + source: &Affine3A, + source_fwd: Vec3A, + plane: &Affine3A, + plane_norm: Vec3A, +) -> Option<(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(); + + Some((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 r = 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 * r; + + let d = ray_dir.xz(); + let s = ray_origin.xz(); + + let a = d.dot(d); + let b = d.dot(s); + let c = s.dot(s) - r * r; + + let d = (b * b) - (a * c); + if d < f32::EPSILON { + return None; + } + + let sqrt_d = d.sqrt(); + + let t1 = (-b - sqrt_d) / a; + let t2 = (-b + sqrt_d) / a; + + 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 * r)); + let x_angle = (hit_local.x / r).asin(); + + hit_local.x = x_angle / max_angle; + hit_local.y /= size; + + Some((t, hit_local.xy())) +} diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 9d6a682..3c2e86a 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -15,3 +15,5 @@ pub mod uidev; pub mod osc; pub mod overlay; + +pub mod task; diff --git a/src/backend/openvr/helpers.rs b/src/backend/openvr/helpers.rs index 0425c5c..cb30698 100644 --- a/src/backend/openvr/helpers.rs +++ b/src/backend/openvr/helpers.rs @@ -4,7 +4,7 @@ use glam::Affine3A; use ovr_overlay::{pose::Matrix3x4, settings::SettingsManager, sys::HmdMatrix34_t}; use thiserror::Error; -use crate::backend::common::{BackendError, ColorChannel}; +use crate::backend::{common::BackendError, task::ColorChannel}; pub trait Affine3AConvert { fn from_affine(affine: &Affine3A) -> Self; diff --git a/src/backend/openvr/mod.rs b/src/backend/openvr/mod.rs index 8628956..dc1e1e8 100644 --- a/src/backend/openvr/mod.rs +++ b/src/backend/openvr/mod.rs @@ -20,7 +20,7 @@ use vulkano::{ use crate::{ backend::{ - common::SystemTask, + common::{BackendError, OverlayContainer}, input::interact, notifications::NotificationManager, openvr::{ @@ -31,6 +31,7 @@ use crate::{ overlay::OpenVrOverlayData, }, overlay::OverlayData, + task::{SystemTask, TaskType}, }, graphics::WlxGraphics, overlays::{ @@ -40,8 +41,6 @@ use crate::{ state::AppState, }; -use super::common::{BackendError, OverlayContainer, TaskType}; - pub mod helpers; pub mod input; pub mod lines; diff --git a/src/backend/openxr/helpers.rs b/src/backend/openxr/helpers.rs index 8c93b58..aadce8d 100644 --- a/src/backend/openxr/helpers.rs +++ b/src/backend/openxr/helpers.rs @@ -129,29 +129,14 @@ pub(super) fn hmd_pose_from_views(views: &[xr::View]) -> (Affine3A, f32) { (pos0 + pos1) * 0.5 }; let rot = { - let rot0 = unsafe { std::mem::transmute(views[0].pose.orientation) }; - let rot1 = unsafe { std::mem::transmute(views[1].pose.orientation) }; - quat_lerp(rot0, rot1, 0.5) + let rot0: Quat = unsafe { std::mem::transmute(views[0].pose.orientation) }; + let rot1: Quat = unsafe { std::mem::transmute(views[1].pose.orientation) }; + rot0.lerp(rot1, 0.5) }; (Affine3A::from_rotation_translation(rot, pos), ipd) } -fn quat_lerp(a: Quat, mut b: Quat, t: f32) -> Quat { - let l2 = a.dot(b); - if l2 < 0.0 { - b = -b; - } - - Quat::from_xyzw( - a.x - t * (a.x - b.x), - a.y - t * (a.y - b.y), - a.z - t * (a.z - b.z), - a.w - t * (a.w - b.w), - ) - .normalize() -} - pub(super) fn transform_to_norm_quat(transform: &Affine3A) -> Quat { let norm_mat3 = transform .matrix3 diff --git a/src/backend/openxr/mod.rs b/src/backend/openxr/mod.rs index c3bb05e..c409564 100644 --- a/src/backend/openxr/mod.rs +++ b/src/backend/openxr/mod.rs @@ -13,11 +13,12 @@ use vulkano::{command_buffer::CommandBufferUsage, Handle, VulkanObject}; use crate::{ backend::{ - common::{OverlayContainer, TaskType}, + common::{BackendError, OverlayContainer}, input::interact, notifications::NotificationManager, openxr::{input::DoubleClickCounter, lines::LinePool, overlay::OpenXrOverlayData}, overlay::OverlayData, + task::TaskType, }, graphics::WlxGraphics, overlays::{ @@ -27,8 +28,6 @@ use crate::{ state::AppState, }; -use super::common::BackendError; - mod helpers; mod input; mod lines; diff --git a/src/backend/overlay.rs b/src/backend/overlay.rs index 4dfe0cd..b99bdf3 100644 --- a/src/backend/overlay.rs +++ b/src/backend/overlay.rs @@ -124,7 +124,7 @@ impl OverlayState { self.saved_transform = None; } - self.transform = app.anchor * self.get_transform(); + self.transform = self.parent_transform(app).unwrap_or(app.anchor) * self.get_transform(); if self.grabbable && hard_reset { self.realign(&app.input_state.hmd); diff --git a/src/backend/task.rs b/src/backend/task.rs new file mode 100644 index 0000000..2b15251 --- /dev/null +++ b/src/backend/task.rs @@ -0,0 +1,113 @@ +use std::{ + cmp, + collections::{BinaryHeap, VecDeque}, + sync::atomic::{self, AtomicUsize}, + time::Instant, +}; + +use serde::Deserialize; + +use crate::state::AppState; + +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 for AppTask { + fn eq(&self, other: &Self) -> bool { + self.cmp(other) == cmp::Ordering::Equal + } +} +impl PartialOrd for AppTask { + fn partial_cmp(&self, other: &Self) -> Option { + 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, +} + +pub type OverlayTask = dyn FnOnce(&mut AppState, &mut OverlayState) + Send; +pub type CreateOverlayTask = + dyn FnOnce(&mut AppState) -> Option<(OverlayState, Box)> + Send; + +pub enum TaskType { + Global(Box), + Overlay(OverlaySelector, Box), + CreateOverlay(OverlaySelector, Box), + DropOverlay(OverlaySelector), + System(SystemTask), +} + +#[derive(Deserialize, Clone, Copy)] +pub enum ColorChannel { + R, + G, + B, + All, +} + +pub struct TaskContainer { + tasks: BinaryHeap, +} + +impl TaskContainer { + pub 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) { + 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); + } + } +} diff --git a/src/gui/modular/button.rs b/src/gui/modular/button.rs index 9f0ff68..b8978ce 100644 --- a/src/gui/modular/button.rs +++ b/src/gui/modular/button.rs @@ -11,9 +11,10 @@ use serde::Deserialize; use crate::{ backend::{ - common::{ColorChannel, OverlaySelector, SystemTask, TaskType}, + common::OverlaySelector, input::PointerMode, overlay::RelativeTo, + task::{ColorChannel, SystemTask, TaskType}, }, config::{save_settings, save_state, AStrSetExt}, overlays::{ diff --git a/src/overlays/mirror.rs b/src/overlays/mirror.rs index d26fbf3..dc5407f 100644 --- a/src/overlays/mirror.rs +++ b/src/overlays/mirror.rs @@ -9,10 +9,11 @@ use wlx_capture::pipewire::{pipewire_select_screen, PipewireCapture, PipewireSel use crate::{ backend::{ - common::{OverlaySelector, TaskType}, + common::OverlaySelector, overlay::{ ui_transform, OverlayBackend, OverlayRenderer, OverlayState, SplitOverlayBackend, }, + task::TaskType, }, state::{AppSession, AppState}, }; diff --git a/src/overlays/toast.rs b/src/overlays/toast.rs index 6c9e323..1784595 100644 --- a/src/overlays/toast.rs +++ b/src/overlays/toast.rs @@ -1,18 +1,15 @@ -use std::{ - f32::consts::PI, - ops::Add, - sync::{atomic::AtomicUsize, Arc}, - time::Instant, -}; +use std::{f32::consts::PI, ops::Add, sync::Arc, time::Instant}; use glam::{vec3a, Quat, Vec3A}; use idmap_derive::IntegerId; +use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use crate::{ backend::{ - common::{OverlaySelector, TaskType}, + common::OverlaySelector, overlay::{OverlayBackend, OverlayState, RelativeTo}, + task::TaskType, }, gui::{color_parse, CanvasBuilder}, state::{AppState, LeftRight}, @@ -22,8 +19,7 @@ const FONT_SIZE: isize = 16; const PADDING: (f32, f32) = (25., 7.); const PIXELS_TO_METERS: f32 = 1. / 2000.; const TOAST_AUDIO_WAV: &[u8] = include_bytes!("../res/557297.wav"); - -static AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(0); +static TOAST_NAME: Lazy> = Lazy::new(|| "toast".into()); #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub enum DisplayMethod { @@ -77,36 +73,49 @@ impl Toast { self.submit_at(app, Instant::now()); } pub fn submit_at(self, app: &mut AppState, instant: Instant) { - let auto_increment = AUTO_INCREMENT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - let name: Arc = format!("toast-{}", auto_increment).into(); - let selector = OverlaySelector::Name(name.clone()); + let selector = OverlaySelector::Name(TOAST_NAME.clone()); let destroy_at = instant.add(std::time::Duration::from_secs_f32(self.timeout)); let has_sound = self.sound && app.session.config.notifications_sound_enabled; + // drop any toast that was created before us. + // (DropOverlay only drops overlays that were + // created before current frame) + app.tasks + .enqueue_at(TaskType::DropOverlay(selector.clone()), instant); + + // CreateOverlay only creates the overlay if + // the selector doesn't exist yet, so in case + // multiple toasts are submitted for the same + // frame, only the first one gets created app.tasks.enqueue_at( TaskType::CreateOverlay( selector.clone(), - Box::new(move |app| new_toast(self, name, app)), + Box::new(move |app| { + let mut maybe_toast = new_toast(self, app); + if let Some((state, _)) = maybe_toast.as_mut() { + state.auto_movement(app); + app.tasks.enqueue_at( + // at timeout, drop the overlay by ID instead + // in order to avoid dropping any newer toasts + TaskType::DropOverlay(OverlaySelector::Id(state.id)), + destroy_at, + ); + } + maybe_toast + }), ), instant, ); - app.tasks - .enqueue_at(TaskType::DropOverlay(selector), destroy_at); - if has_sound { app.audio.play(TOAST_AUDIO_WAV); } } } -fn new_toast( - toast: Toast, - name: Arc, - app: &mut AppState, -) -> Option<(OverlayState, Box)> { +fn new_toast(toast: Toast, app: &mut AppState) -> Option<(OverlayState, Box)> { let current_method = app .session .toast_topics @@ -186,7 +195,7 @@ fn new_toast( } let state = OverlayState { - name, + name: TOAST_NAME.clone(), want_visible: true, spawn_scale: size.0 * PIXELS_TO_METERS, spawn_rotation, diff --git a/src/state.rs b/src/state.rs index 4d882ba..2c3159e 100644 --- a/src/state.rs +++ b/src/state.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use smallvec::{smallvec, SmallVec}; use crate::{ - backend::{common::TaskContainer, input::InputState}, + backend::{input::InputState, task::TaskContainer}, config::GeneralConfig, config_io, graphics::WlxGraphics,