Compare commits

...
64 Commits
Author SHA1 Message Date
galister 5ab506e192 Release v0.3.2 2024-03-23 21:55:09 +01:00
galister 7ab036024d chore: more clippy 2024-03-23 16:05:42 +01:00
galister e70aaa002e chore: clippy 2024-03-23 15:41:54 +01:00
galister a1d3b9684d fix: end async work on soft-shutdown 2024-03-23 15:35:01 +01:00
galister 9fe9832a8d openxr: separate release threshold for grab, click, altclick 2024-03-21 21:47:08 +01:00
galister 0b813aeb10 docs: wivrn love + update known issues 2024-03-21 21:10:23 +01:00
galister 155f653f32 omg: curved screens 2024-03-21 21:04:20 +01:00
galister 82374a60a0 Release v0.3.1 2024-03-19 18:19:32 +01:00
galister 0d41bf1a3e openvr: correct size for portrait overlays 2024-03-19 18:16:23 +01:00
galister 3907e089de openvr: re-upload line texs on show-hide to prevent disappearing lasers 2024-03-19 17:16:35 +01:00
Aleksander 72bac6465e Fix ui_transform (#17) 2024-03-19 16:14:56 +00:00
Stuart Irwin 271e037692 Save screen scaling changes (#16) 2024-03-17 21:22:18 +00:00
galister bc78450c6a openxr: use squeeze/force for knuckles 2024-03-17 17:11:25 +01:00
galister dce634edd1 openxr: click/grab/altclick sensitivity option 2024-03-17 16:41:32 +01:00
galister 69d14eebe5 fix when desktop origin other than (0,0) 2024-03-17 16:30:16 +01:00
galister 54dfb297ac improve error handling 2024-03-17 16:11:05 +01:00
galister 1d1230b1e7 chore: update wlx-capture & other deps 2024-03-17 13:08:49 +01:00
galister 67b6b90158 fonts: dont panic on load error 2024-03-17 11:25:11 +01:00
galister 31ec035db7 pipewire: log screen capture start failures 2024-03-16 11:27:10 +01:00
galister deb43f0d94 do not log toasts 2024-03-11 09:07:35 +01:00
Stuart Irwin 4434d82981 Add uptime timer module (#15) 2024-03-11 07:51:43 +00:00
libjared 8ba5d68e5a Update keyboard.yaml (#14)
- update virtual key list URL
- remove references to xmodmap
- ensure layouts are consistent with length
- remove alt_layout for now
2024-03-10 22:31:54 +00:00
galister 0c6e319fbe Release v0.3.0 2024-03-10 21:40:14 +01:00
galister 4679961cb2 openvr: smoother rotate 2024-03-10 20:56:13 +01:00
galister cb12357d68 openvr: dont overwrite bindings 2024-03-10 16:55:40 +01:00
galister b401fb28fd openvr: space rotate, recenter height adjust 2024-03-10 16:28:37 +01:00
galister 06478bbfc9 openvr: recenter rework 2024-03-10 14:40:50 +01:00
galister b57b77b06a openvr: reset offsets reloads from disk 2024-03-10 14:08:12 +01:00
galister 5a077e394c openvr: reload playspace on scene app change 2024-03-10 13:23:00 +01:00
galister a08c260898 openvr: chaperone movement fix 2024-03-10 13:19:08 +01:00
galister 45d08cd89b x11: fix cursor size 2024-03-10 00:10:28 +01:00
galister 49b9cc361d x11: fix screen gamma 2024-03-10 00:02:10 +01:00
galister 2f8dcaf110 use linked openxr 2024-03-09 21:21:48 +01:00
galister 473c6e2038 cleanup 2024-03-09 17:15:37 +01:00
galister 4c91470b5c Release v0.2.6 2024-03-09 16:54:52 +01:00
galister 85e8e80538 update ovr_overlay 2024-03-09 01:59:11 +01:00
galister 49e1e8639d Release v0.2.5 2024-03-09 01:26:32 +01:00
galister 88bcd42e56 openvr: seated space drag 2024-03-09 01:24:24 +01:00
galister 38395c9183 openvr: universe-aware overlays 2024-03-08 23:08:30 +01:00
galister 19bc4a5df2 Release v0.2.4 2024-03-08 00:11:03 +01:00
galister ed096fffce fix space drag 2024-03-08 00:00:49 +01:00
galister cd297376a8 openvr: throttle using WaitFrameSync 2024-03-07 17:57:07 +01:00
galister e7456a9b0a WIP: space drag rework 2024-03-07 13:17:12 +01:00
galister 507c0677c8 ShowUi & ShowMirror: no implicit show/hide 2024-03-06 20:43:40 +01:00
galister 1d9b60fc07 update ovr_overlay 2024-03-06 19:09:57 +01:00
galister 85d9ea9127 simplify a thing 2024-03-06 19:03:40 +01:00
galister a55ade6741 modular-ui: use boxes where it makes sense 2024-03-06 18:45:14 +01:00
galister 5800d7d99d refactor: graphics type aliases 2024-03-06 18:41:34 +01:00
galister 27d77ad638 clippy 2024-03-06 18:20:56 +01:00
galister 2aed560950 osc: only interactable in openOverlayCount 2024-03-06 18:20:20 +01:00
galister 1a70ea4fcc settings: notifications section rearrange 2024-03-06 18:18:27 +01:00
galister c73a01fb7f watch: save fade settings 2024-03-06 18:16:34 +01:00
galister 97fd6c1fc0 openvr: support appimage autostart 2024-03-06 18:14:33 +01:00
galister 39ce2a7c00 increase X11 screen width cap to 2560 2024-03-02 03:00:44 +01:00
galister 0030ad43eb add instructions for input group 2024-03-02 03:00:44 +01:00
galister 3006e52795 Update README.md 2024-03-02 00:11:10 +00:00
galister cfb432622e add building from source & aur 2024-03-01 22:51:56 +01:00
galister b49add2e89 reduce warnings when --no-default-features 2024-03-01 22:26:45 +01:00
galister da1936823f fix build 2024-03-01 22:14:37 +01:00
galister f42572d4cb fix more no-default-features builds 2024-03-01 22:13:06 +01:00
galister d562d4b236 fix dependency tree when --no-default-features 2024-03-01 21:43:50 +01:00
galister 75dcfc8096 fix pw required with no wayland feature 2024-03-01 21:39:54 +01:00
galister c49e8b66b8 fix p 2024-03-01 20:19:44 +01:00
galister d3681b79d2 add openvr 2024-03-01 20:05:46 +01:00
40 changed files with 1467 additions and 949 deletions

No files matched your search

+2 -2
View File
@@ -19,7 +19,7 @@ jobs:
sudo add-apt-repository -syn universe
sudo add-apt-repository -syn ppa:pipewire-debian/pipewire-upstream || sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 25088A0359807596
sudo apt-get update
sudo apt-get install fuse cmake pkg-config fontconfig clang git wget curl libasound2-dev libxkbcommon-dev libopenxr-dev libfontconfig-dev libdbus-1-dev libpipewire-0.3-0 libpipewire-0.3-dev libspa-0.2-dev libx11-6 libxext6 libxrandr2 libx11-dev libxext-dev libxrandr-dev
sudo apt-get install fuse cmake pkg-config fontconfig libasound2-dev libxkbcommon-dev libopenxr-dev libfontconfig-dev libdbus-1-dev libpipewire-0.3-0 libpipewire-0.3-dev libspa-0.2-dev libx11-6 libxext6 libxrandr2 libx11-dev libxext-dev libxrandr-dev libopenvr-dev libopenvr-api1
rustup update
test -f linuxdeploy-x86_64.AppImage || wget -q "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage"
@@ -37,7 +37,7 @@ jobs:
- name: Package AppImage
run: |
export VERSION=$GITHUB_REF_NAME
./linuxdeploy-x86_64.AppImage -dAppRun.desktop -iWlxOverlay.png --appdir=${APPDIR} --output appimage --exclude-library '*libpipewire*'
./linuxdeploy-x86_64.AppImage -dwlx-overlay-s.desktop -iwlx-overlay-s.png --appdir=${APPDIR} --output appimage --exclude-library '*libpipewire*'
mv WlxOverlay-S-$VERSION-x86_64.AppImage WlxOverlay-S-x86_64.AppImage
- name: Create Release
Generated
+411 -496
View File
File diff suppressed because it is too large. Load diff
+5 -5
View File
@@ -4,7 +4,7 @@ debug = true
[package]
name = "wlx-overlay-s"
version = "0.2.3"
version = "0.3.2"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -27,9 +27,9 @@ idmap = "0.2.21"
idmap-derive = "0.1.2"
input-linux = "0.6.0"
json = { version = "0.12.4", optional = true }
libc = "0.2.147"
log = "0.4.20"
once_cell = "1.18.0"
libc = "0.2.153"
log = "0.4.21"
once_cell = "1.19.0"
openxr = { version = "0.17.1", features = ["linked"], optional = true }
ovr_overlay = { features = ["ovr_input", "ovr_system"], git = "https://github.com/galister/ovr_overlay_oyasumi", optional = true }
regex = "1.9.5"
@@ -43,7 +43,7 @@ strum = { version = "0.25.0", features = ["derive"] }
thiserror = "1.0.56"
vulkano = { git = "https://github.com/vulkano-rs/vulkano", rev = "94f50f1" }
vulkano-shaders = { git = "https://github.com/vulkano-rs/vulkano", rev = "94f50f1" }
wlx-capture = { git = "https://github.com/galister/wlx-capture" }
wlx-capture = { git = "https://github.com/galister/wlx-capture", tag = "v0.3.1", default-features = false }
xdg = "2.5.2"
[features]
+11 -12
View File
@@ -18,15 +18,18 @@ We are available on either:
Questions/issues specific to WlxOverlay-S will be handled in the `wlxoverlay` chat room.
# First Setup
# Setup
Recommend grabbing [rustup](https://rustup.rs/) if you don't have it yet.
1. Grab the latest AppImage from [Releases](https://github.com/galister/wlx-overlay-s/releases).
1. `chmod +x WlxOverlay-S-*.AppImage`
1. Start Monado, WiVRn or SteamVR.
1. Run the overlay
Start Monado or SteamVR.
AUR package is [wlx-overlay-s-git](https://aur.archlinux.org/packages/wlx-overlay-s-git).
```sh
cargo run --release
```
You may also want to [build from source](https://github.com/galister/wlx-overlay-s/wiki/Building-from-Source).
# First Start
**If you get a screen share pop-up, check the terminal and select the screens in the order it tells you to.**
@@ -89,18 +92,14 @@ We would like to work with you and include additional bindings.
# Known Issues
## SteamVR: laser pointers not visible
## Modifiers get stuck in weird ways
This seems to be a rare issue with SteamVR startup overlays. Restarting the overlay will fix the issue.
This is a rare issue that can make your desktop not react to click or keys due to a modifier being stuck somewhere. Restarting the overlay fixes this.
## Scroll wheel doesn't work
This seems to be an issue specific to Electron apps (Discord, Element, Slack, Spotify) on Wayland. Scrolling will work when using these in your web browser.
## WiVRn support
While WiVRn technically supports EXTX_overlay, I do not recommend using this software with WiVRn at this time, due to WiVRn not being optimized for overlay apps. You will likely get ghosting or stuttering while rotating your head.
## X11 limitations
DPI scaling and upright screens are not supported on X11. These might display incorrectly or mess up your mouse position.
+64 -7
View File
@@ -1,5 +1,6 @@
use std::{
collections::{BinaryHeap, VecDeque},
f32::consts::PI,
sync::Arc,
time::Instant,
};
@@ -7,7 +8,7 @@ use std::{
#[cfg(feature = "openxr")]
use openxr as xr;
use glam::{Affine3A, Vec2, Vec3A};
use glam::{Affine3A, Vec2, Vec3A, Vec3Swizzles};
use idmap::IdMap;
use serde::Deserialize;
use thiserror::Error;
@@ -252,23 +253,79 @@ impl TaskContainer {
}
}
pub fn raycast(
pub fn raycast_plane(
source: &Affine3A,
source_fwd: Vec3A,
plane: &Affine3A,
plane_norm: Vec3A,
) -> Option<(Vec3A, f32)> {
) -> 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);
if dist < 0.0 {
// plane is behind the caster
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 hit_pos = source.translation + ray_dir * dist;
Some((hit_pos, dist))
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()))
}
+54 -14
View File
@@ -9,7 +9,7 @@ use smallvec::{smallvec, SmallVec};
use crate::state::AppState;
use super::{
common::{raycast, OverlayContainer},
common::{raycast_cylinder, raycast_plane, OverlayContainer},
overlay::OverlayData,
};
@@ -71,6 +71,9 @@ impl InputState {
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.click_modifier_right != hand.before.click_modifier_right {
log::debug!(
"Hand {}: click_modifier_right {}",
@@ -159,6 +162,7 @@ impl Default for InteractionState {
pub struct Pointer {
pub idx: usize,
pub pose: Affine3A,
pub raw_pose: Affine3A,
pub now: PointerState,
pub before: PointerState,
pub last_click: Instant,
@@ -171,6 +175,7 @@ impl Pointer {
Self {
idx,
pose: Affine3A::IDENTITY,
raw_pose: Affine3A::IDENTITY,
now: Default::default(),
before: Default::default(),
last_click: Instant::now(),
@@ -187,6 +192,7 @@ pub struct PointerState {
pub alt_click: bool,
pub show_hide: bool,
pub space_drag: bool,
pub space_rotate: bool,
pub click_modifier_right: bool,
pub click_modifier_middle: bool,
}
@@ -228,7 +234,8 @@ impl InteractionHandler for DummyInteractionHandler {
#[derive(Debug, Clone, Copy, Default)]
struct RayHit {
overlay: usize,
hit_pos: Vec3A,
global_pos: Vec3A,
local_pos: Vec2,
dist: f32,
}
@@ -356,7 +363,28 @@ where
if pointer.now.scroll.abs() > 0.1 {
let scroll = pointer.now.scroll;
hovered.backend.on_scroll(app, &hit, scroll);
if app.input_state.pointers[1 - idx]
.interaction
.grabbed
.is_some_and(|x| x.grabbed_id == hit.overlay)
{
let is_portrait = hovered.view().is_some_and(|v| {
let extent = v.image().extent();
extent[0] >= extent[1]
});
if is_portrait {
let cur = hovered.state.curvature.unwrap_or(0.0);
let new = (cur - scroll * 0.01).min(0.35);
if new <= f32::EPSILON {
hovered.state.curvature = None;
} else {
hovered.state.curvature = Some(new);
}
}
} else {
hovered.backend.on_scroll(app, &hit, scroll);
}
pointer = &mut app.input_state.pointers[idx];
}
@@ -387,7 +415,11 @@ impl Pointer {
continue;
}
if let Some(hit) = self.ray_test(overlay.state.id, &overlay.state.transform) {
if let Some(hit) = self.ray_test(
overlay.state.id,
&overlay.state.transform,
&overlay.state.curvature,
) {
if hit.dist.is_infinite() || hit.dist.is_nan() {
continue;
}
@@ -402,12 +434,8 @@ impl Pointer {
let uv = overlay
.state
.transform
.inverse()
.transform_point3a(hit.hit_pos)
.truncate();
let uv = overlay.state.interaction_transform.transform_point2(uv);
.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;
@@ -483,14 +511,26 @@ impl Pointer {
}
}
fn ray_test(&self, overlay: usize, plane: &Affine3A) -> Option<RayHit> {
let Some((hit_pos, dist)) = raycast(&self.pose, Vec3A::NEG_Z, plane, Vec3A::NEG_Z) else {
fn ray_test(
&self,
overlay: usize,
transform: &Affine3A,
curvature: &Option<f32>,
) -> Option<RayHit> {
let (dist, local_pos) = match curvature {
Some(curvature) => raycast_cylinder(&self.pose, Vec3A::NEG_Z, transform, *curvature),
_ => raycast_plane(&self.pose, Vec3A::NEG_Z, transform, Vec3A::NEG_Z),
}?;
if dist < 0.0 {
// hit is behind us
return None;
};
}
Some(RayHit {
overlay,
hit_pos,
global_pos: self.pose.transform_point3a(Vec3A::NEG_Z * dist),
local_pos,
dist,
})
}
+20 -4
View File
@@ -2,7 +2,10 @@ use dbus::{blocking::Connection, channel::MatchingReceiver, message::MatchRule};
use serde::{Deserialize, Serialize};
use std::{
path::PathBuf,
sync::{mpsc, Arc},
sync::{
atomic::{AtomicBool, Ordering},
mpsc, Arc,
},
time::Duration,
};
@@ -12,6 +15,7 @@ pub struct NotificationManager {
rx_toast: mpsc::Receiver<Toast>,
tx_toast: mpsc::SyncSender<Toast>,
dbus_data: Option<Connection>,
running: Arc<AtomicBool>,
}
impl NotificationManager {
@@ -21,6 +25,7 @@ impl NotificationManager {
rx_toast,
tx_toast,
dbus_data: None,
running: Arc::new(AtomicBool::new(true)),
}
}
@@ -96,7 +101,7 @@ impl NotificationManager {
let sender2 = self.tx_toast.clone();
let result = c.add_match(rule_with_eavesdrop, move |_: (), _, msg| {
if let Ok(toast) = parse_dbus(&msg) {
if let Ok(toast) = parse_dbus(msg) {
match sender2.try_send(toast) {
Ok(_) => {}
Err(e) => {
@@ -125,7 +130,7 @@ impl NotificationManager {
pub fn run_udp(&mut self) {
let sender = self.tx_toast.clone();
// NOTE: We're detaching the thread, as there's no simple way to gracefully stop it other than app shutdown.
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) {
@@ -135,9 +140,13 @@ impl NotificationManager {
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
loop {
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,
@@ -170,10 +179,17 @@ impl NotificationManager {
}
}
}
log::info!("Notification listener stopped.");
});
}
}
impl Drop for NotificationManager {
fn drop(&mut self) {
self.running.store(false, Ordering::Relaxed);
}
}
fn parse_dbus(msg: &dbus::Message) -> anyhow::Result<Toast> {
let mut args = msg.iter_init();
let app_name: String = args.read()?;
+4 -4
View File
@@ -7,12 +7,12 @@ use thiserror::Error;
use crate::backend::common::{BackendError, ColorChannel};
pub trait Affine3AConvert {
fn from_affine(affine: Affine3A) -> Self;
fn from_affine(affine: &Affine3A) -> Self;
fn to_affine(&self) -> Affine3A;
}
impl Affine3AConvert for Matrix3x4 {
fn from_affine(affine: Affine3A) -> Self {
fn from_affine(affine: &Affine3A) -> Self {
Matrix3x4([
[
affine.matrix3.x_axis.x,
@@ -46,7 +46,7 @@ impl Affine3AConvert for Matrix3x4 {
}
impl Affine3AConvert for HmdMatrix34_t {
fn from_affine(affine: Affine3A) -> Self {
fn from_affine(affine: &Affine3A) -> Self {
HmdMatrix34_t {
m: [
[
@@ -101,7 +101,7 @@ impl From<OVRError> for BackendError {
use cstr::cstr;
const STEAMVR_SECTION: &CStr = cstr!("steamvr");
const COLOR_GAIN_CSTR: [&'static CStr; 3] = [
const COLOR_GAIN_CSTR: [&CStr; 3] = [
cstr!("hmdDisplayColorGainR"),
cstr!("hmdDisplayColorGainG"),
cstr!("hmdDisplayColorGainB"),
+23 -8
View File
@@ -36,6 +36,7 @@ const PATH_SCROLL: &str = "/actions/default/in/Scroll";
const PATH_ALT_CLICK: &str = "/actions/default/in/AltClick";
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_CLICK_MODIFIER_RIGHT: &str = "/actions/default/in/ClickModifierRight";
const PATH_CLICK_MODIFIER_MIDDLE: &str = "/actions/default/in/ClickModifierMiddle";
@@ -50,12 +51,14 @@ pub(super) struct OpenVrInputSource {
alt_click_hnd: ActionHandle,
show_hide_hnd: ActionHandle,
space_drag_hnd: ActionHandle,
space_rotate_hnd: ActionHandle,
click_modifier_right_hnd: ActionHandle,
click_modifier_middle_hnd: ActionHandle,
}
pub(super) struct OpenVrHandSource {
has_pose: bool,
device: Option<TrackedDeviceIndex>,
input_hnd: InputValueHandle,
pose_hnd: ActionHandle,
haptics_hnd: ActionHandle,
@@ -71,6 +74,7 @@ impl OpenVrInputSource {
let alt_click_hnd = input.get_action_handle(PATH_ALT_CLICK)?;
let show_hide_hnd = input.get_action_handle(PATH_SHOW_HIDE)?;
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)?;
@@ -91,6 +95,7 @@ impl OpenVrInputSource {
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],
@@ -104,6 +109,7 @@ impl OpenVrInputSource {
alt_click_hnd,
show_hide_hnd,
space_drag_hnd,
space_rotate_hnd,
click_modifier_right_hnd,
click_modifier_middle_hnd,
hands,
@@ -124,6 +130,7 @@ impl OpenVrInputSource {
pub fn update(
&mut self,
universe: ETrackingUniverseOrigin,
input: &mut InputManager,
system: &mut SystemManager,
app: &mut AppState,
@@ -138,12 +145,19 @@ impl OpenVrInputSource {
let _ = input.update_actions(&mut [aas]);
let universe = ETrackingUniverseOrigin::TrackingUniverseStanding;
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
@@ -183,6 +197,11 @@ impl OpenVrInputSource {
.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)
@@ -198,14 +217,10 @@ impl OpenVrInputSource {
.map(|x| x.0.y)
.unwrap_or(0.0);
}
let devices = system.get_device_to_absolute_tracking_pose(universe, 0.005);
app.input_state.hmd = devices[0].mDeviceToAbsoluteTracking.to_affine();
}
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) {
@@ -220,9 +235,11 @@ impl OpenVrInputSource {
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,
@@ -294,9 +311,7 @@ fn get_tracked_device(
pub fn set_action_manifest(input: &mut InputManager) -> anyhow::Result<()> {
let action_path = CONFIG_ROOT_PATH.join("actions.json");
if !action_path.is_file() {
File::create(&action_path)?.write_all(include_bytes!("../../res/actions.json"))?;
}
File::create(&action_path)?.write_all(include_bytes!("../../res/actions.json"))?;
let binding_path = CONFIG_ROOT_PATH.join("actions_binding_knuckles.json");
if !binding_path.is_file() {
+10 -1
View File
@@ -5,6 +5,7 @@ use std::sync::Arc;
use glam::{Affine3A, Vec3, Vec3A, Vec4};
use idmap::IdMap;
use ovr_overlay::overlay::OverlayManager;
use ovr_overlay::sys::ETrackingUniverseOrigin;
use vulkano::command_buffer::CommandBufferUsage;
use vulkano::format::Format;
use vulkano::image::view::ImageView;
@@ -74,6 +75,7 @@ impl LinePool {
}),
data: OpenVrOverlayData {
width: 0.002,
override_width: true,
..Default::default()
},
..Default::default()
@@ -141,6 +143,7 @@ impl LinePool {
pub fn update(
&mut self,
universe: ETrackingUniverseOrigin,
overlay: &mut OverlayManager,
app: &mut AppState,
) -> anyhow::Result<()> {
@@ -152,12 +155,18 @@ impl LinePool {
data.state.dirty = false;
}
data.upload_transform(overlay);
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 {
+10 -4
View File
@@ -9,12 +9,18 @@ use crate::config_io::CONFIG_ROOT_PATH;
const APP_KEY: &str = "galister.wlxoverlay-s";
pub(super) fn install_manifest(app_mgr: &mut ApplicationsManager) -> anyhow::Result<()> {
let executable_pathbuf = std::env::current_exe()?;
let executable_path = executable_pathbuf
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid executable path"))?;
let manifest_path = CONFIG_ROOT_PATH.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 let Ok(true) = app_mgr.is_application_installed(APP_KEY) {
if let Ok(mut file) = File::open(&manifest_path) {
let mut buf = String::new();
+53 -35
View File
@@ -1,7 +1,7 @@
use std::{
collections::VecDeque,
sync::{
atomic::{AtomicBool, Ordering},
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc,
},
time::{Duration, Instant},
@@ -46,6 +46,8 @@ 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 {
@@ -67,15 +69,15 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
log::info!("Using OpenVR runtime");
let mut app_mgr = context.applications_mngr();
let mut input_mngr = context.input_mngr();
let mut system_mngr = context.system_mngr();
let mut overlay_mngr = context.overlay_mngr();
let mut settings_mngr = context.settings_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_mngr = context.compositor_mngr();
let mut compositor_mgr = context.compositor_mngr();
let device_extensions_fn = |device: &PhysicalDevice| {
let names = compositor_mngr.get_vulkan_device_extensions_required(device.handle().as_raw());
let names = compositor_mgr.get_vulkan_device_extensions_required(device.handle().as_raw());
let ext = DeviceExtensions::from_iter(names.iter().map(|s| s.as_str()));
ext
};
@@ -98,18 +100,20 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
notifications.run_dbus();
notifications.run_udp();
let mut space_mover = playspace::PlayspaceMover::new();
let mut playspace = playspace::PlayspaceMover::new();
playspace.playspace_changed(&mut compositor_mgr, &mut chaperone_mgr);
#[cfg(feature = "osc")]
let mut osc_sender =
crate::backend::osc::OscSender::new(state.session.config.osc_out_port).ok();
state.hid_provider.set_desktop_extent(overlays.extent);
set_action_manifest(&mut input_mngr)?;
set_action_manifest(&mut input_mgr)?;
let mut input_source = OpenVrInputSource::new(&mut input_mngr)?;
let mut input_source = OpenVrInputSource::new(&mut input_mgr)?;
let Ok(refresh_rate) = system_mngr.get_tracked_device_property::<f32>(
let Ok(refresh_rate) = system_mgr.get_tracked_device_property::<f32>(
TrackedDeviceIndex::HMD,
ETrackedDeviceProperty::Prop_DisplayFrequency_Float,
) else {
@@ -122,7 +126,9 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
let watch_id = overlays.get_by_name(WATCH_NAME).unwrap().state.id; // want panic
let frame_time = (1000.0 / refresh_rate).floor() * 0.001;
// 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);
@@ -130,12 +136,16 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
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;
}
while let Some(event) = system_mngr.poll_next_event() {
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.");
@@ -146,12 +156,18 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
| 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);
}
_ => {}
}
}
if next_device_update <= Instant::now() {
input_source.update_devices(&mut system_mngr, &mut state);
input_source.update_devices(&mut system_mgr, &mut state);
next_device_update = Instant::now() + Duration::from_secs(30);
}
@@ -171,9 +187,10 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
continue;
};
let Some((state, backend)) = f(&mut state) else {
let Some((mut state, backend)) = f(&mut state) else {
continue;
};
state.birthframe = cur_frame;
overlays.add(OverlayData {
state,
@@ -183,26 +200,35 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
}
TaskType::DropOverlay(sel) => {
if let Some(o) = overlays.mut_by_selector(&sel) {
o.destroy(&mut overlay_mngr);
overlays.remove_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_mngr, channel, value);
let _ = adjust_gain(&mut settings_mgr, channel, value);
}
SystemTask::FixFloor => {
space_mover.fix_floor(&mut chaperone_mgr, &state.input_state);
playspace.fix_floor(&mut chaperone_mgr, &state.input_state);
}
SystemTask::ResetPlayspace => {
space_mover.reset_offset(&mut chaperone_mgr);
playspace.reset_offset(&mut chaperone_mgr, &state.input_state);
}
},
}
}
let universe = playspace.get_universe();
state.input_state.pre_update();
input_source.update(&mut input_mngr, &mut system_mngr, &mut state);
input_source.update(
universe.clone(),
&mut input_mgr,
&mut system_mgr,
&mut state,
);
state.input_state.post_update();
if state
@@ -211,6 +237,7 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
.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);
}
@@ -219,7 +246,7 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
.for_each(|o| o.state.auto_movement(&mut state));
watch_fade(&mut state, overlays.mut_by_id(watch_id).unwrap()); // want panic
space_mover.update(&mut chaperone_mgr, &mut overlays, &state);
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() {
@@ -231,14 +258,14 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
&state.input_state.hmd,
);
if let Some(haptics) = haptics {
input_source.haptics(&mut input_mngr, idx, haptics)
input_source.haptics(&mut input_mgr, idx, haptics)
}
}
lines.update(&mut overlay_mngr, &mut state)?;
lines.update(universe.clone(), &mut overlay_mgr, &mut state)?;
for o in overlays.iter_mut() {
o.after_input(&mut overlay_mngr, &mut state)?;
o.after_input(&mut overlay_mgr, &mut state)?;
}
#[cfg(feature = "osc")]
@@ -258,22 +285,13 @@ pub fn openvr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
overlays
.iter_mut()
.for_each(|o| o.after_render(&mut overlay_mngr, &state.graphics));
.for_each(|o| o.after_render(universe.clone(), &mut overlay_mgr, &state.graphics));
// chaperone
// close font handles?
state.hid_provider.on_new_frame();
let mut seconds_since_vsync = 0f32;
std::thread::sleep(Duration::from_secs_f32(
if system_mngr.get_time_since_last_vsync(&mut seconds_since_vsync, &mut 0u64) {
(frame_time - seconds_since_vsync).max(0.0)
} else {
frame_time
},
));
}
log::warn!("OpenVR shutdown");
+29 -11
View File
@@ -1,3 +1,5 @@
use core::f32;
use glam::Vec4;
use ovr_overlay::{
overlay::{OverlayHandle, OverlayManager},
@@ -21,9 +23,9 @@ pub(super) struct OpenVrOverlayData {
pub(super) last_image: Option<u64>,
pub(super) visible: bool,
pub(super) color: Vec4,
pub(super) curvature: f32,
pub(super) sort_order: u32,
pub(crate) width: f32,
pub(super) override_width: bool,
pub(super) relative_to: RelativeTo,
}
@@ -79,10 +81,17 @@ impl OverlayData<OpenVrOverlayData> {
Ok(())
}
pub(super) fn after_render(&mut self, overlay: &mut OverlayManager, graphics: &WlxGraphics) {
pub(super) fn after_render(
&mut self,
universe: ETrackingUniverseOrigin,
overlay: &mut OverlayManager,
graphics: &WlxGraphics,
) {
if self.data.visible {
if self.state.dirty {
self.upload_transform(overlay);
self.upload_curvature(overlay);
self.upload_transform(universe, overlay);
self.upload_alpha(overlay);
self.state.dirty = false;
}
@@ -166,7 +175,7 @@ impl OverlayData<OpenVrOverlayData> {
log::debug!("{}: No overlay handle", self.state.name);
return;
};
if let Err(e) = overlay.set_curvature(handle, self.data.curvature) {
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,
@@ -185,19 +194,19 @@ impl OverlayData<OpenVrOverlayData> {
}
}
pub(super) fn upload_transform(&self, overlay: &mut OverlayManager) {
pub(super) fn upload_transform(
&self,
universe: ETrackingUniverseOrigin,
overlay: &mut OverlayManager,
) {
let Some(handle) = self.data.handle else {
log::debug!("{}: No overlay handle", self.state.name);
return;
};
let transform = Matrix3x4::from_affine(self.state.transform);
let transform = Matrix3x4::from_affine(&self.state.transform);
if let Err(e) = overlay.set_transform_absolute(
handle,
ETrackingUniverseOrigin::TrackingUniverseStanding,
&transform,
) {
if let Err(e) = overlay.set_transform_absolute(handle, universe, &transform) {
log::error!(
"{}: Failed to set overlay transform: {}",
self.state.name,
@@ -228,6 +237,15 @@ impl OverlayData<OpenVrOverlayData> {
}
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 format = image.format();
let mut texture = VRVulkanTextureData_t {
+239 -44
View File
@@ -1,25 +1,35 @@
use glam::Vec3A;
use ovr_overlay::{chaperone_setup::ChaperoneSetupManager, sys::EChaperoneConfigFile};
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::overlay::OpenVrOverlayData;
use super::{helpers::Affine3AConvert, overlay::OpenVrOverlayData};
struct MoverData<T> {
pose: Affine3A,
hand: usize,
hand_pose: T,
}
pub(super) struct PlayspaceMover {
drag_hand: Option<usize>,
offset: Vec3A,
start_position: Vec3A,
universe: ETrackingUniverseOrigin,
drag: Option<MoverData<Vec3A>>,
rotate: Option<MoverData<Quat>>,
}
impl PlayspaceMover {
pub fn new() -> Self {
Self {
drag_hand: None,
offset: Vec3A::ZERO,
start_position: Vec3A::ZERO,
universe: ETrackingUniverseOrigin::TrackingUniverseRawAndUncalibrated,
drag: None,
rotate: None,
}
}
@@ -29,68 +39,253 @@ impl PlayspaceMover {
overlays: &mut OverlayContainer<OpenVrOverlayData>,
state: &AppState,
) {
if let Some(hand) = self.drag_hand {
let pointer = &state.input_state.pointers[hand];
if !pointer.now.space_drag {
self.drag_hand = None;
log::info!("End space drag");
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 hand_pos = state.input_state.pointers[hand].pose.translation;
let relative_pos = hand_pos - self.start_position;
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 * dq.w + dq.x * dq.z),
(2.0 * (dq.w * 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 += relative_pos * -1.0;
overlay.state.transform.translation =
overlay_transform.transform_point3a(overlay.state.transform.translation);
}
});
self.offset += relative_pos;
self.apply_offset(chaperone_mgr);
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;
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 {
self.drag_hand = Some(i);
self.start_position = pointer.pose.translation;
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");
break;
return;
}
}
}
}
pub fn reset_offset(&mut self, chaperone_mgr: &mut ChaperoneSetupManager) {
self.offset = Vec3A::ZERO;
self.apply_offset(chaperone_mgr);
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;
self.offset.y += y1.min(y2) - 0.03;
self.apply_offset(chaperone_mgr);
}
pub fn reset(&mut self) {
self.offset = Vec3A::ZERO;
self.start_position = Vec3A::ZERO;
}
fn apply_offset(&mut self, chaperone_mgr: &mut ChaperoneSetupManager) {
let Some(mut zero_pose) =
chaperone_mgr.get_working_standing_zero_pose_to_raw_tracking_pose()
else {
log::warn!("Can't space drag - failed to get zero pose");
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;
zero_pose.m[0][3] = self.offset.x;
zero_pose.m[1][3] = self.offset.y;
zero_pose.m[2][3] = self.offset.z;
chaperone_mgr.set_working_standing_zero_pose_to_raw_tracking_pose(&zero_pose);
set_working_copy(&self.universe, chaperone_mgr, &mat);
chaperone_mgr.commit_working_copy(EChaperoneConfigFile::EChaperoneConfigFile_Live);
}
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()
}
}
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();
quads.iter_mut().for_each(|quad| {
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();
quads.iter_mut().for_each(|quad| {
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());
}
+13 -7
View File
@@ -1,12 +1,10 @@
use anyhow::{bail, ensure};
use glam::{Affine3A, Quat, Vec3};
use glam::{Affine3A, Quat, Vec3, Vec3A};
use openxr as xr;
use xr::OverlaySessionCreateFlagsEXTX;
pub(super) fn init_xr() -> Result<(xr::Instance, xr::SystemId), anyhow::Error> {
let Ok(entry) = (unsafe { xr::Entry::load() }) else {
bail!("OpenXR Loader not found.");
};
let entry = xr::Entry::linked();
let Ok(available_extensions) = entry.enumerate_extensions() else {
bail!("Failed to enumerate OpenXR extensions.");
@@ -141,12 +139,14 @@ fn quat_lerp(a: Quat, mut b: Quat, t: f32) -> Quat {
.normalize()
}
pub(super) fn transform_to_posef(transform: &Affine3A) -> xr::Posef {
let translation = transform.translation;
pub(super) fn transform_to_norm_quat(transform: &Affine3A) -> Quat {
let norm_mat3 = transform
.matrix3
.mul_scalar(1.0 / transform.matrix3.x_axis.length());
let mut rotation = Quat::from_mat3a(&norm_mat3).normalize();
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;
}
@@ -165,3 +165,9 @@ pub(super) fn transform_to_posef(transform: &Affine3A) -> xr::Posef {
},
}
}
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)
}
+35 -23
View File
@@ -5,7 +5,7 @@ use openxr as xr;
use crate::{
backend::input::{Haptics, Pointer},
state::AppState,
state::{AppSession, AppState},
};
use super::XrState;
@@ -98,12 +98,7 @@ impl OpenXrInputSource {
xr.session.sync_actions(&[(&self.action_set).into()])?;
for i in 0..2 {
self.hands[i].update(
&mut state.input_state.pointers[i],
&xr.stage,
&xr.session,
xr.predicted_display_time,
)?;
self.hands[i].update(&mut state.input_state.pointers[i], xr, &state.session)?;
}
Ok(())
}
@@ -123,11 +118,10 @@ impl OpenXrHand {
pub(super) fn update(
&self,
pointer: &mut Pointer,
stage: &xr::Space,
session: &XrSession,
time: xr::Time,
xr: &XrState,
session: &AppSession,
) -> Result<(), xr::sys::Result> {
let location = self.space.locate(stage, time)?;
let location = self.space.locate(&xr.stage, xr.predicted_display_time)?;
if location
.location_flags
.contains(xr::SpaceLocationFlags::ORIENTATION_VALID)
@@ -137,49 +131,67 @@ impl OpenXrHand {
pointer.pose = Affine3A::from_rotation_translation(quat, pos);
}
let click_sensitivity = if pointer.before.click {
session.config.xr_click_sensitivity_release
} else {
session.config.xr_click_sensitivity
};
pointer.now.click = self
.source
.action_click
.state(session, xr::Path::NULL)?
.state(&xr.session, xr::Path::NULL)?
.current_state
> 0.7;
> click_sensitivity;
let grab_sensitivity = if pointer.before.grab {
session.config.xr_grab_sensitivity_release
} else {
session.config.xr_grab_sensitivity
};
pointer.now.grab = self
.source
.action_grab
.state(session, xr::Path::NULL)?
.state(&xr.session, xr::Path::NULL)?
.current_state
> 0.7;
> grab_sensitivity;
pointer.now.scroll = self
.source
.action_scroll
.state(session, xr::Path::NULL)?
.state(&xr.session, xr::Path::NULL)?
.current_state;
let alt_click_sensitivity = if pointer.before.alt_click {
session.config.xr_alt_click_sensitivity_release
} else {
session.config.xr_alt_click_sensitivity
};
pointer.now.alt_click = self
.source
.action_alt_click
.state(session, xr::Path::NULL)?
.state(&xr.session, xr::Path::NULL)?
.current_state
> 0.7;
> alt_click_sensitivity;
pointer.now.show_hide = self
.source
.action_show_hide
.state(session, xr::Path::NULL)?
.state(&xr.session, xr::Path::NULL)?
.current_state;
pointer.now.click_modifier_right = self
.source
.action_click_modifier_right
.state(session, xr::Path::NULL)?
.state(&xr.session, xr::Path::NULL)?
.current_state;
pointer.now.click_modifier_middle = self
.source
.action_click_modifier_middle
.state(session, xr::Path::NULL)?
.state(&xr.session, xr::Path::NULL)?
.current_state;
Ok(())
@@ -380,11 +392,11 @@ fn suggest_bindings(
),
xr::Binding::new(
&hands[0].action_grab,
instance.string_to_path("/user/hand/left/input/squeeze/value")?,
instance.string_to_path("/user/hand/left/input/squeeze/force")?,
),
xr::Binding::new(
&hands[1].action_grab,
instance.string_to_path("/user/hand/right/input/squeeze/value")?,
instance.string_to_path("/user/hand/right/input/squeeze/force")?,
),
xr::Binding::new(
&hands[0].action_scroll,
+3 -3
View File
@@ -19,7 +19,7 @@ use crate::{
use super::{
swapchain::{create_swapchain_render_data, SwapchainRenderData},
XrState,
CompositionLayer, XrState,
};
static AUTO_INCREMENT: AtomicUsize = AtomicUsize::new(1);
@@ -135,7 +135,7 @@ impl LinePool {
&'a mut self,
xr: &'a XrState,
command_buffer: &mut WlxCommandBuffer,
) -> anyhow::Result<Vec<xr::CompositionLayerQuad<xr::Vulkan>>> {
) -> anyhow::Result<Vec<CompositionLayer>> {
let mut quads = Vec::new();
for line in self.lines.values_mut() {
@@ -155,7 +155,7 @@ impl LinePool {
height: inner.length,
});
quads.push(quad);
quads.push(CompositionLayer::Quad(quad));
}
}
+30 -10
View File
@@ -275,13 +275,18 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
continue;
}
if let Some(quad) = o.present_xr(&xr_state, &mut command_buffer)? {
layers.push((dist_sq, quad));
};
let maybe_layer = o.present_xr(&xr_state, &mut command_buffer)?;
if let CompositionLayer::None = maybe_layer {
continue;
}
layers.push((dist_sq, maybe_layer));
}
for quad in lines.present_xr(&xr_state, &mut command_buffer)? {
layers.push((0.0, quad));
for maybe_layer in lines.present_xr(&xr_state, &mut command_buffer)? {
if let CompositionLayer::None = maybe_layer {
continue;
}
layers.push((0.0, maybe_layer));
}
command_buffer.build_and_execute_now()?;
@@ -290,7 +295,11 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
let frame_ref = layers
.iter()
.map(|f| &f.1 as &xr::CompositionLayerBase<xr::Vulkan>)
.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::None => unreachable!(),
})
.collect::<Vec<_>>();
frame_stream.end(
@@ -315,9 +324,10 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
continue;
};
let Some((state, backend)) = f(&mut app_state) else {
let Some((mut state, backend)) = f(&mut app_state) else {
continue;
};
state.birthframe = cur_frame;
overlays.add(OverlayData {
state,
@@ -326,9 +336,13 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
});
}
TaskType::DropOverlay(sel) => {
let o = overlays.remove_by_selector(&sel);
// set for deletion after all images are done showing
delete_queue.push((o, cur_frame + 5));
if let Some(o) = overlays.mut_by_selector(&sel) {
if o.state.birthframe < cur_frame {
let 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) => {
// Not implemented
@@ -346,3 +360,9 @@ pub fn openxr_run(running: Arc<AtomicBool>) -> Result<(), BackendError> {
Ok(())
}
pub(super) enum CompositionLayer<'a> {
None,
Quad(xr::CompositionLayerQuad<'a, xr::Vulkan>),
Cylinder(xr::CompositionLayerCylinderKHR<'a, xr::Vulkan>),
}
+37 -17
View File
@@ -1,8 +1,9 @@
use glam::Vec3A;
use openxr as xr;
use std::sync::Arc;
use std::{f32::consts::PI, sync::Arc};
use xr::{CompositionLayerFlags, EyeVisibility};
use super::{helpers, swapchain::SwapchainRenderData, XrState};
use super::{helpers, swapchain::SwapchainRenderData, CompositionLayer, XrState};
use crate::{
backend::{openxr::swapchain::create_swapchain_render_data, overlay::OverlayData},
graphics::WlxCommandBuffer,
@@ -23,7 +24,7 @@ impl OverlayData<OpenXrOverlayData> {
&'a mut self,
xr: &'a XrState,
command_buffer: &mut WlxCommandBuffer,
) -> anyhow::Result<Option<xr::CompositionLayerQuad<xr::Vulkan>>> {
) -> anyhow::Result<CompositionLayer> {
if let Some(new_view) = self.view() {
self.data.last_view = Some(new_view);
}
@@ -32,7 +33,7 @@ impl OverlayData<OpenXrOverlayData> {
view.clone()
} else {
log::warn!("{}: Will not show - image not ready", self.state.name);
return Ok(None);
return Ok(CompositionLayer::None);
};
let extent = my_view.image().extent();
@@ -55,10 +56,8 @@ impl OverlayData<OpenXrOverlayData> {
};
let sub_image = data.acquire_present_release(command_buffer, my_view, self.state.alpha)?;
let posef = helpers::transform_to_posef(&self.state.transform);
let aspect_ratio = extent[1] as f32 / extent[0] as f32;
let (scale_x, scale_y) = if aspect_ratio < 1.0 {
let major = self.state.transform.matrix3.col(0).length();
(major, major * aspect_ratio)
@@ -67,17 +66,38 @@ impl OverlayData<OpenXrOverlayData> {
(major / aspect_ratio, major)
};
let quad = xr::CompositionLayerQuad::new()
.pose(posef)
.sub_image(sub_image)
.eye_visibility(EyeVisibility::BOTH)
.layer_flags(CompositionLayerFlags::CORRECT_CHROMATIC_ABERRATION)
.space(&xr.stage)
.size(xr::Extent2Df {
width: scale_x,
height: scale_y,
});
Ok(Some(quad))
if let Some(curvature) = self.state.curvature {
let radius = scale_x / (2.0 * PI * curvature);
let quat = helpers::transform_to_norm_quat(&self.state.transform);
let center_point = self.state.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()
.pose(posef)
.sub_image(sub_image)
.eye_visibility(EyeVisibility::BOTH)
.layer_flags(CompositionLayerFlags::CORRECT_CHROMATIC_ABERRATION)
.space(&xr.stage)
.radius(radius)
.central_angle(angle)
.aspect_ratio(aspect_ratio);
Ok(CompositionLayer::Cylinder(cylinder))
} else {
let posef = helpers::transform_to_posef(&self.state.transform);
let quad = xr::CompositionLayerQuad::new()
.pose(posef)
.sub_image(sub_image)
.eye_visibility(EyeVisibility::BOTH)
.layer_flags(CompositionLayerFlags::CORRECT_CHROMATIC_ABERRATION)
.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<()> {
+5 -1
View File
@@ -65,7 +65,11 @@ impl OscSender {
match o.state.name.as_ref() {
WATCH_NAME => has_wrist = true,
KEYBOARD_NAME => has_keyboard = true,
_ => num_overlays += 1,
_ => {
if o.state.interactable {
num_overlays += 1
}
}
}
}
+18 -5
View File
@@ -35,8 +35,10 @@ pub struct OverlayState {
pub spawn_point: Vec3A,
pub spawn_rotation: Quat,
pub relative_to: RelativeTo,
pub curvature: Option<f32>,
pub primary_pointer: Option<usize>,
pub interaction_transform: Affine2,
pub birthframe: usize,
}
impl Default for OverlayState {
@@ -52,6 +54,7 @@ impl Default for OverlayState {
dirty: true,
alpha: 1.0,
relative_to: RelativeTo::None,
curvature: None,
saved_point: None,
saved_scale: None,
spawn_scale: 1.0,
@@ -60,6 +63,7 @@ impl Default for OverlayState {
transform: Affine3A::IDENTITY,
primary_pointer: None,
interaction_transform: Affine2::IDENTITY,
birthframe: 0,
}
}
}
@@ -159,6 +163,7 @@ impl OverlayState {
}
let scale = self.transform.x_axis.length();
self.saved_scale = Some(scale);
let col_z = (self.transform.translation - hmd.translation).normalize();
let col_y = up_dir;
@@ -285,10 +290,18 @@ impl InteractionHandler for SplitOverlayBackend {
}
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_cols(
Vec2::X,
Vec2::NEG_Y * (extent[0] as f32 / extent[1] as f32),
center,
)
Affine2::from_scale_angle_translation(scale, 0.0, center)
}
+50 -15
View File
@@ -42,11 +42,11 @@ fn def_one() -> f32 {
1.0
}
fn def_half() -> f32 {
pub fn def_half() -> f32 {
0.5
}
fn def_point7() -> f32 {
pub fn def_point7() -> f32 {
0.7
}
@@ -117,6 +117,24 @@ pub struct GeneralConfig {
#[serde(default = "def_auto")]
pub capture_method: Arc<str>,
#[serde(default = "def_point7")]
pub xr_grab_sensitivity: f32,
#[serde(default = "def_point7")]
pub xr_click_sensitivity: f32,
#[serde(default = "def_point7")]
pub xr_alt_click_sensitivity: f32,
#[serde(default = "def_half")]
pub xr_grab_sensitivity_release: f32,
#[serde(default = "def_half")]
pub xr_click_sensitivity_release: f32,
#[serde(default = "def_half")]
pub xr_alt_click_sensitivity_release: f32,
}
impl GeneralConfig {
@@ -136,8 +154,8 @@ impl GeneralConfig {
}
fn post_load(&self) {
GeneralConfig::sanitize_range("keyboard_scale", self.keyboard_scale, 0.0, 5.0);
GeneralConfig::sanitize_range("desktop_view_scale", self.desktop_view_scale, 0.0, 5.0);
GeneralConfig::sanitize_range("keyboard_scale", self.keyboard_scale, 0.05, 5.0);
GeneralConfig::sanitize_range("desktop_view_scale", self.desktop_view_scale, 0.05, 5.0);
}
}
@@ -165,14 +183,12 @@ where
let file_name = FILES[config_type as usize];
let maybe_override = config_io::load(file_name);
for yaml in [maybe_override.as_deref(), Some(fallback)].iter() {
if let Some(yaml_data) = yaml {
match serde_yaml::from_str::<T>(yaml_data) {
Ok(d) => return d,
Err(e) => {
error!("Failed to parse {}, falling back to defaults.", file_name);
error!("{}", e);
}
for yaml in [maybe_override.as_deref(), Some(fallback)].iter().flatten() {
match serde_yaml::from_str::<T>(yaml) {
Ok(d) => return d,
Err(e) => {
error!("Failed to parse {}, falling back to defaults.", file_name);
error!("{}", e);
}
}
}
@@ -194,12 +210,31 @@ pub fn load_general() -> GeneralConfig {
// Add files from conf.d directory
let path_conf_d = get_conf_d_path();
if let Ok(paths_unsorted) = std::fs::read_dir(path_conf_d) {
let mut paths: Vec<_> = paths_unsorted
.filter_map(|r| match r {
Ok(entry) => Some(entry),
Err(e) => {
error!("Failed to read conf.d directory: {}", e);
None
}
})
.collect();
// Sort paths alphabetically
let mut paths: Vec<_> = paths_unsorted.map(|r| r.unwrap()).collect(); // TODO safe unwrap?
paths.sort_by_key(|dir| dir.path());
for path in paths {
if !path.file_type().unwrap().is_file() {
// TODO safe unwrap?
let file_type = match path.file_type() {
Ok(file_type) => file_type,
Err(e) => {
error!(
"Failed to get file type of {}: {}",
path.path().to_string_lossy(),
e
);
continue;
}
};
if !file_type.is_file() {
continue;
}
+38 -35
View File
@@ -10,14 +10,20 @@ use ash::vk::SubmitInfo;
use smallvec::smallvec;
#[cfg(feature = "openvr")]
use vulkano::{
device::physical::PhysicalDeviceType, instance::InstanceCreateFlags,
instance::InstanceExtensions,
};
use vulkano::{device::physical::PhysicalDeviceType, instance::InstanceCreateFlags};
#[cfg(feature = "openxr")]
use {ash::vk, std::os::raw::c_void};
pub type Vert2Buf = Subbuffer<[Vert2Uv]>;
pub type IndexBuf = Subbuffer<[u16]>;
pub type LegacyPipeline = WlxPipeline<WlxPipelineLegacy>;
pub type DynamicPipeline = WlxPipeline<WlxPipelineDynamic>;
pub type LegacyPass = WlxPass<WlxPipelineLegacy>;
pub type DynamicPass = WlxPass<WlxPipelineDynamic>;
use vulkano::{
buffer::{
allocator::{SubbufferAllocator, SubbufferAllocatorCreateInfo},
@@ -47,7 +53,7 @@ use vulkano::{
Image, ImageCreateInfo, ImageLayout, ImageTiling, ImageType, ImageUsage, SampleCount,
SubresourceLayout,
},
instance::{Instance, InstanceCreateInfo},
instance::{Instance, InstanceCreateInfo, InstanceExtensions},
memory::{
allocator::{
AllocationCreateInfo, GenericMemoryAllocatorCreateInfo, MemoryAllocator,
@@ -121,8 +127,8 @@ pub struct WlxGraphics {
pub command_buffer_allocator: Arc<StandardCommandBufferAllocator>,
pub descriptor_set_allocator: Arc<StandardDescriptorSetAllocator>,
pub quad_verts: Subbuffer<[Vert2Uv]>,
pub quad_indices: Subbuffer<[u16]>,
pub quad_verts: Vert2Buf,
pub quad_indices: IndexBuf,
pub shared_shaders: RwLock<HashMap<&'static str, Arc<ShaderModule>>>,
}
@@ -477,7 +483,7 @@ impl WlxGraphics {
fn default_quad(
memory_allocator: Arc<StandardMemoryAllocator>,
) -> anyhow::Result<(Subbuffer<[Vert2Uv]>, Subbuffer<[u16]>)> {
) -> anyhow::Result<(Vert2Buf, IndexBuf)> {
let vertices = [
Vert2Uv {
in_pos: [0., 0.],
@@ -535,7 +541,7 @@ impl WlxGraphics {
y: f32,
w: f32,
h: f32,
) -> anyhow::Result<Subbuffer<[Vert2Uv]>> {
) -> anyhow::Result<Vert2Buf> {
let rw = width;
let rh = height;
@@ -707,8 +713,8 @@ impl WlxGraphics {
frag: Arc<ShaderModule>,
format: Format,
blend: Option<AttachmentBlend>,
) -> anyhow::Result<Arc<WlxPipeline<WlxPipelineLegacy>>> {
Ok(Arc::new(WlxPipeline::<WlxPipelineLegacy>::new(
) -> anyhow::Result<Arc<LegacyPipeline>> {
Ok(Arc::new(LegacyPipeline::new(
render_target,
self.clone(),
vert,
@@ -727,8 +733,8 @@ impl WlxGraphics {
blend: Option<AttachmentBlend>,
initial_layout: ImageLayout,
final_layout: ImageLayout,
) -> anyhow::Result<Arc<WlxPipeline<WlxPipelineLegacy>>> {
Ok(Arc::new(WlxPipeline::<WlxPipelineLegacy>::new_with_layout(
) -> anyhow::Result<Arc<LegacyPipeline>> {
Ok(Arc::new(LegacyPipeline::new_with_layout(
render_target,
self.clone(),
vert,
@@ -747,8 +753,8 @@ impl WlxGraphics {
frag: Arc<ShaderModule>,
format: Format,
blend: Option<AttachmentBlend>,
) -> anyhow::Result<Arc<WlxPipeline<WlxPipelineDynamic>>> {
Ok(Arc::new(WlxPipeline::<WlxPipelineDynamic>::new(
) -> anyhow::Result<Arc<DynamicPipeline>> {
Ok(Arc::new(DynamicPipeline::new(
self.clone(),
vert,
frag,
@@ -844,10 +850,7 @@ pub struct WlxCommandBuffer {
#[allow(dead_code)]
impl WlxCommandBuffer {
pub fn begin_render_pass(
&mut self,
pipeline: &WlxPipeline<WlxPipelineLegacy>,
) -> anyhow::Result<()> {
pub fn begin_render_pass(&mut self, pipeline: &LegacyPipeline) -> anyhow::Result<()> {
self.command_buffer.begin_render_pass(
RenderPassBeginInfo {
clear_values: vec![Some([0.0, 0.0, 0.0, 0.0].into())],
@@ -1034,11 +1037,11 @@ impl WlxPipeline<WlxPipelineDynamic> {
pub fn create_pass(
self: &Arc<Self>,
dimensions: [f32; 2],
vertex_buffer: Subbuffer<[Vert2Uv]>,
index_buffer: Subbuffer<[u16]>,
vertex_buffer: Vert2Buf,
index_buffer: IndexBuf,
descriptor_sets: Vec<Arc<DescriptorSet>>,
) -> anyhow::Result<WlxPass<WlxPipelineDynamic>> {
WlxPass::<WlxPipelineDynamic>::new(
) -> anyhow::Result<DynamicPass> {
DynamicPass::new(
self.clone(),
dimensions,
vertex_buffer,
@@ -1203,11 +1206,11 @@ impl WlxPipeline<WlxPipelineLegacy> {
pub fn create_pass(
self: &Arc<Self>,
dimensions: [f32; 2],
vertex_buffer: Subbuffer<[Vert2Uv]>,
index_buffer: Subbuffer<[u16]>,
vertex_buffer: Vert2Buf,
index_buffer: IndexBuf,
descriptor_sets: Vec<Arc<DescriptorSet>>,
) -> anyhow::Result<WlxPass<WlxPipelineLegacy>> {
WlxPass::<WlxPipelineLegacy>::new(
) -> anyhow::Result<LegacyPass> {
LegacyPass::new(
self.clone(),
dimensions,
vertex_buffer,
@@ -1281,18 +1284,18 @@ impl<D> WlxPipeline<D> {
#[allow(dead_code)]
pub struct WlxPass<D> {
pipeline: Arc<WlxPipeline<D>>,
vertex_buffer: Subbuffer<[Vert2Uv]>,
index_buffer: Subbuffer<[u16]>,
vertex_buffer: Vert2Buf,
index_buffer: IndexBuf,
descriptor_sets: Vec<Arc<DescriptorSet>>,
pub command_buffer: Arc<CommandBuffer>,
}
impl WlxPass<WlxPipelineLegacy> {
fn new(
pipeline: Arc<WlxPipeline<WlxPipelineLegacy>>,
pipeline: Arc<LegacyPipeline>,
dimensions: [f32; 2],
vertex_buffer: Subbuffer<[Vert2Uv]>,
index_buffer: Subbuffer<[u16]>,
vertex_buffer: Vert2Buf,
index_buffer: IndexBuf,
descriptor_sets: Vec<Arc<DescriptorSet>>,
) -> anyhow::Result<Self> {
let viewport = Viewport {
@@ -1349,10 +1352,10 @@ impl WlxPass<WlxPipelineLegacy> {
impl WlxPass<WlxPipelineDynamic> {
fn new(
pipeline: Arc<WlxPipeline<WlxPipelineDynamic>>,
pipeline: Arc<DynamicPipeline>,
dimensions: [f32; 2],
vertex_buffer: Subbuffer<[Vert2Uv]>,
index_buffer: Subbuffer<[u16]>,
vertex_buffer: Vert2Buf,
index_buffer: IndexBuf,
descriptor_sets: Vec<Arc<DescriptorSet>>,
) -> anyhow::Result<Self> {
let viewport = Viewport {
+20 -13
View File
@@ -106,9 +106,7 @@ impl FontCache {
}
let pattern_str = format!("{PRIMARY_FONT}-{size}:style=bold:charset={cp:04x}");
let mut pattern =
OwnedPattern::from_str(&pattern_str).expect("Failed to create fontconfig pattern");
let mut pattern = OwnedPattern::from_str(&pattern_str).unwrap(); // safe because PRIMARY_FONT is const
self.fc
.substitute(&mut pattern, fontconfig::MatchKind::Pattern);
pattern.default_substitute();
@@ -124,12 +122,22 @@ impl FontCache {
let font_idx = pattern.face_index().unwrap_or(0);
let face = self
.ft
.new_face(path, font_idx as _)
.expect("Failed to load font face");
face.set_char_size(size << 6, size << 6, 96, 96)
.expect("Failed to set font size");
let face = match self.ft.new_face(path, font_idx as _) {
Ok(face) => face,
Err(e) => {
log::warn!("Failed to load font at {}: {:?}", path, e);
coll.cp_map.insert(cp, 0);
return 0;
}
};
match face.set_char_size(size << 6, size << 6, 96, 96) {
Ok(_) => {}
Err(e) => {
log::warn!("Failed to set font size: {:?}", e);
coll.cp_map.insert(cp, 0);
return 0;
}
};
let idx = coll.fonts.len();
for cp in 0..0xFFFF {
@@ -156,11 +164,10 @@ impl FontCache {
let font = Font { face, glyphs };
coll.fonts.push(font);
idx
} else {
coll.cp_map.insert(cp, 0);
0
return idx;
}
coll.cp_map.insert(cp, 0);
0
}
fn get_glyph_for_cp(
+8 -38
View File
@@ -1,6 +1,5 @@
use std::{
f32::consts::PI,
io::Cursor,
ops::Add,
process::{self, Child},
sync::Arc,
@@ -8,7 +7,6 @@ use std::{
};
use glam::{Quat, Vec3A};
use rodio::{Decoder, Source};
use serde::Deserialize;
use crate::{
@@ -19,7 +17,6 @@ use crate::{
overlay::RelativeTo,
},
overlays::{
mirror,
toast::Toast,
watch::{save_watch, WATCH_NAME},
},
@@ -173,7 +170,7 @@ pub struct ButtonData {
}
pub fn modular_button_init(button: &mut ModularControl, data: &ButtonData) {
button.state = Some(ModularData::Button(data.clone()));
button.state = Some(ModularData::Button(Box::new(data.clone())));
button.on_press = Some(modular_button_dn);
button.on_release = Some(modular_button_up);
button.on_scroll = Some(modular_button_scroll);
@@ -475,7 +472,7 @@ fn run_watch(data: &WatchAction, app: &mut AppState) {
app.tasks.enqueue(TaskType::Overlay(
OverlaySelector::Name(WATCH_NAME.into()),
Box::new(move |app, o| {
o.spawn_rotation = o.spawn_rotation * rot;
o.spawn_rotation *= rot;
app.session.config.watch_rot = o.spawn_rotation.into();
o.dirty = true;
}),
@@ -577,23 +574,12 @@ fn run_overlay(overlay: &OverlaySelector, action: &OverlayAction, app: &mut AppS
}
}
#[cfg(feature = "wayland")]
fn run_window(window: &Arc<str>, action: &WindowAction, app: &mut AppState) {
use crate::overlays::custom;
let selector = OverlaySelector::Name(window.clone());
match action {
WindowAction::ShowMirror => {
app.tasks.enqueue(TaskType::Overlay(
selector,
Box::new(|app, o| {
o.want_visible = !o.want_visible;
if o.recenter {
o.show_hide = o.want_visible;
o.reset(app, false);
}
}),
));
#[cfg(feature = "wayland")]
app.tasks.enqueue(TaskType::CreateOverlay(
OverlaySelector::Name(window.clone()),
Box::new({
@@ -602,22 +588,14 @@ fn run_window(window: &Arc<str>, action: &WindowAction, app: &mut AppState) {
Toast::new("Check your desktop for popup.".into(), "".into())
.with_sound(true)
.submit(app);
mirror::new_mirror(name.clone(), false, &app.session)
crate::overlays::mirror::new_mirror(name.clone(), false, &app.session)
}
}),
));
#[cfg(not(feature = "wayland"))]
log::warn!("Mirror not available without Wayland feature.");
}
WindowAction::ShowUi => {
app.tasks.enqueue(TaskType::Overlay(
selector,
Box::new(|app, o| {
o.want_visible = !o.want_visible;
if o.recenter {
o.show_hide = o.want_visible;
o.reset(app, false);
}
}),
));
app.tasks.enqueue(TaskType::CreateOverlay(
OverlaySelector::Name(window.clone()),
Box::new({
@@ -633,16 +611,8 @@ fn run_window(window: &Arc<str>, action: &WindowAction, app: &mut AppState) {
}
}
#[cfg(not(feature = "wayland"))]
fn run_action_window(_: Arc<str>, _: &WindowAction, _: &mut AppState) {
log::warn!("Cannot run Window action without Wayland feature.");
}
const THUMP_AUDIO_WAV: &[u8] = include_bytes!("../../res/380885.wav");
fn audio_thump(app: &mut AppState) {
if let Some(handle) = app.audio.get_handle() {
let wav = include_bytes!("../../res/380885.wav");
let cursor = Cursor::new(wav);
let source = Decoder::new_wav(cursor).unwrap();
let _ = handle.play_raw(source.convert_samples());
}
app.audio.play(THUMP_AUDIO_WAV);
}
+22 -3
View File
@@ -29,6 +29,9 @@ pub enum LabelContent {
format: Arc<str>,
timezone: Option<Arc<str>>,
},
Timer {
format: Arc<str>,
},
Battery {
device: usize,
low_threshold: f32,
@@ -49,6 +52,10 @@ pub enum LabelData {
format: Arc<str>,
timezone: Option<Tz>,
},
Timer {
format: Arc<str>,
start: Instant,
},
Exec {
last_exec: Instant,
interval: f32,
@@ -85,6 +92,10 @@ pub fn modular_label_init(label: &mut ModularControl, content: &LabelContent) {
timezone: tz,
})
}
LabelContent::Timer { format } => Some(LabelData::Timer {
format: format.clone(),
start: Instant::now(),
}),
LabelContent::Exec { command, interval } => Some(LabelData::Exec {
last_exec: Instant::now(),
interval: *interval,
@@ -92,13 +103,13 @@ pub fn modular_label_init(label: &mut ModularControl, content: &LabelContent) {
child: None,
}),
LabelContent::Static { text } => {
label.set_text(&text);
label.set_text(text);
None
}
};
if let Some(state) = state {
label.state = Some(ModularData::Label(state));
label.state = Some(ModularData::Label(Box::new(state)));
label.on_update = Some(label_update);
}
}
@@ -107,7 +118,7 @@ pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut A
let ModularData::Label(data) = control.state.as_mut().unwrap() else {
panic!("Label control has no state");
};
match data {
match data.as_mut() {
LabelData::Battery {
device,
low_threshold,
@@ -155,6 +166,14 @@ pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut A
control.set_text(&format!("{}", &date.format(&format)));
}
}
LabelData::Timer { format, start } => {
let mut format = format.clone().to_lowercase();
let duration = start.elapsed().as_secs();
format = format.replace("%s", &format!("{:02}", (duration % 60)));
format = format.replace("%m", &format!("{:02}", ((duration / 60) % 60)));
format = format.replace("%h", &format!("{:02}", ((duration / 60) / 60)));
control.set_text(&format);
}
LabelData::Exec {
last_exec,
interval,
+13 -13
View File
@@ -70,7 +70,7 @@ pub enum ModularElement {
bg_color: Arc<str>,
text: Arc<str>,
#[serde(flatten)]
data: ButtonData,
data: Box<ButtonData>,
},
/// Convenience type to save you from having to create a bunch of labels
BatteryList {
@@ -90,7 +90,7 @@ pub enum ModularElement {
bg_color: Arc<str>,
layout: ListLayout,
#[serde(flatten)]
template: OverlayListTemplate,
template: Box<OverlayListTemplate>,
},
}
@@ -107,8 +107,8 @@ pub enum ListLayout {
}
pub enum ModularData {
Label(LabelData),
Button(ButtonData),
Label(Box<LabelData>),
Button(Box<ButtonData>),
}
pub fn modular_canvas(
@@ -130,7 +130,7 @@ pub fn modular_canvas(
rect: [x, y, w, h],
bg_color,
} => {
canvas.bg_color = color_parse(&bg_color).unwrap_or(FALLBACK_COLOR);
canvas.bg_color = color_parse(bg_color).unwrap_or(FALLBACK_COLOR);
canvas.panel(*x, *y, *w, *h);
}
ModularElement::Label {
@@ -140,9 +140,9 @@ pub fn modular_canvas(
data,
} => {
canvas.font_size = *font_size;
canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR);
let mut label = canvas.label(*x, *y, *w, *h, empty_str.clone());
modular_label_init(&mut label, data);
canvas.fg_color = color_parse(fg_color).unwrap_or(FALLBACK_COLOR);
let label = canvas.label(*x, *y, *w, *h, empty_str.clone());
modular_label_init(label, data);
}
ModularElement::Button {
rect: [x, y, w, h],
@@ -152,8 +152,8 @@ pub fn modular_canvas(
text,
data,
} => {
canvas.bg_color = color_parse(&bg_color).unwrap_or(FALLBACK_COLOR);
canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR);
canvas.bg_color = color_parse(bg_color).unwrap_or(FALLBACK_COLOR);
canvas.fg_color = color_parse(fg_color).unwrap_or(FALLBACK_COLOR);
canvas.font_size = *font_size;
let button = canvas.button(*x, *y, *w, *h, text.clone());
modular_button_init(button, data);
@@ -177,7 +177,7 @@ pub fn modular_canvas(
ListLayout::Vertical => (*w, *h / num_buttons),
};
let fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR);
let fg_color = color_parse(fg_color).unwrap_or(FALLBACK_COLOR);
canvas.font_size = *font_size;
canvas.fg_color = fg_color;
@@ -225,8 +225,8 @@ pub fn modular_canvas(
ListLayout::Vertical => (*w, *h / num_buttons),
};
canvas.bg_color = color_parse(&bg_color).unwrap_or(FALLBACK_COLOR);
canvas.fg_color = color_parse(&fg_color).unwrap_or(FALLBACK_COLOR);
canvas.bg_color = color_parse(bg_color).unwrap_or(FALLBACK_COLOR);
canvas.fg_color = color_parse(fg_color).unwrap_or(FALLBACK_COLOR);
canvas.font_size = *font_size;
for screen in state.screens.iter() {
+8 -1
View File
@@ -17,7 +17,14 @@ pub fn initialize() -> Box<dyn HidProvider> {
return Box::new(uinput);
}
log::error!("Could not create uinput provider. Keyboard/Mouse input will not work!");
log::error!("Check if you're in `input` group: `id -nG`");
log::error!("To check if you're in input group, run: id -nG");
if let Ok(user) = std::env::var("USER") {
log::error!(
"To add yourself to the input group, run: sudo usermod -aG input {}",
user
);
log::error!("After adding yourself to the input group, you will need to reboot.");
}
Box::new(DummyProvider {})
}
+34 -6
View File
@@ -42,9 +42,9 @@ struct Args {
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
if let Some(log_to) = args.log_to {
if let Some(ref log_to) = args.log_to {
flexi_logger::Logger::try_with_env_or_str("info")?
.log_to_file(FileSpec::default().directory(&log_to))
.log_to_file(FileSpec::default().directory(log_to))
.log_to_stdout()
.start()?;
println!("Logging to: {}", &log_to);
@@ -72,16 +72,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
});
auto_run(running, args.openvr, args.openxr);
auto_run(running, args);
Ok(())
}
fn auto_run(running: Arc<AtomicBool>, openvr: bool, openxr: bool) {
fn auto_run(running: Arc<AtomicBool>, args: Args) {
use backend::common::BackendError;
#[cfg(feature = "openxr")]
if !openvr {
if !args_get_openvr(&args) {
use crate::backend::openxr::openxr_run;
match openxr_run(running.clone()) {
Ok(()) => return,
@@ -94,7 +94,7 @@ fn auto_run(running: Arc<AtomicBool>, openvr: bool, openxr: bool) {
}
#[cfg(feature = "openvr")]
if !openxr {
if !args_get_openxr(&args) {
use crate::backend::openvr::openvr_run;
match openvr_run(running.clone()) {
Ok(()) => return,
@@ -107,4 +107,32 @@ fn auto_run(running: Arc<AtomicBool>, openvr: bool, openxr: bool) {
}
log::error!("No more backends to try");
#[cfg(not(any(feature = "openvr", feature = "openxr")))]
compile_error!("No VR support! Enable either openvr or openxr features!");
#[cfg(not(any(feature = "wayland", feature = "x11")))]
compile_error!("No desktop support! Enable either wayland or x11 features!");
}
#[allow(dead_code)]
fn args_get_openvr(_args: &Args) -> bool {
#[cfg(feature = "openvr")]
let ret = _args.openvr;
#[cfg(not(feature = "openvr"))]
let ret = false;
ret
}
#[allow(dead_code)]
fn args_get_openxr(_args: &Args) -> bool {
#[cfg(feature = "openxr")]
let ret = _args.openxr;
#[cfg(not(feature = "openxr"))]
let ret = false;
ret
}
+18 -16
View File
@@ -1,6 +1,5 @@
use std::{
collections::HashMap,
io::Cursor,
process::{Child, Command},
str::FromStr,
};
@@ -18,7 +17,6 @@ use crate::{
use glam::{vec2, vec3a, Affine2, Vec4};
use once_cell::sync::Lazy;
use regex::Regex;
use rodio::{Decoder, Source};
use serde::{Deserialize, Serialize};
const PIXELS_PER_UNIT: f32 = 80.;
@@ -83,10 +81,14 @@ where
verbs: key_events_for_macro(macro_verbs),
});
} else if let Some(exec_args) = LAYOUT.exec_commands.get(key) {
if exec_args.is_empty() {
log::error!("Keyboard: EXEC args empty for {}", key);
continue;
}
maybe_state = Some(KeyButtonData::Exec {
program: exec_args
.first()
.expect("Keyboard: Invalid EXEC args")
.unwrap() // safe because we checked is_empty
.clone(),
args: exec_args.iter().skip(1).cloned().collect(),
});
@@ -227,18 +229,12 @@ struct KeyboardData {
processes: Vec<Child>,
}
const KEY_AUDIO_WAV: &[u8] = include_bytes!("../res/421581.wav");
impl KeyboardData {
fn key_click(&mut self, app: &mut AppState) {
if !app.session.config.keyboard_sound_enabled {
return;
}
if let Some(handle) = app.audio.get_handle() {
// https://freesound.org/people/UberBosser/sounds/421581/
let wav = include_bytes!("../res/421581.wav");
let cursor = Cursor::new(wav);
let source = Decoder::new_wav(cursor).unwrap();
let _ = handle.play_raw(source.convert_samples());
if app.session.config.keyboard_sound_enabled {
app.audio.play(KEY_AUDIO_WAV);
}
}
}
@@ -253,7 +249,7 @@ enum KeyButtonData {
static LAYOUT: Lazy<Layout> = Lazy::new(Layout::load_from_disk);
static MACRO_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^([A-Za-z0-1_-]+)(?: +(UP|DOWN))?$").unwrap());
Lazy::new(|| Regex::new(r"^([A-Za-z0-1_-]+)(?: +(UP|DOWN))?$").unwrap()); // want panic
#[derive(Debug, Deserialize, Serialize)]
pub struct Layout {
@@ -314,11 +310,17 @@ impl Layout {
key = &key[3..];
}
if key.contains('_') {
key = key.split('_').next().unwrap();
key = key.split('_').next().unwrap_or_else(|| {
log::error!(
"keyboard.yaml: Key '{}' must not start or end with '_'!",
key
);
"???"
});
}
vec![format!(
"{}{}",
key.chars().next().unwrap().to_uppercase(),
key.chars().next().unwrap().to_uppercase(), // safe because we checked is_empty
&key[1..].to_lowercase()
)]
}
+22 -13
View File
@@ -1,6 +1,9 @@
use std::{sync::Arc, thread::JoinHandle};
use std::{
sync::Arc,
task::{Context, Poll},
};
use futures::executor;
use futures::{Future, FutureExt};
use glam::vec3a;
use wlx_capture::pipewire::{pipewire_select_screen, PipewireCapture, PipewireSelectScreenResult};
@@ -15,21 +18,23 @@ use crate::{
};
use super::screen::ScreenRenderer;
type PinnedSelectorFuture = core::pin::Pin<
Box<dyn Future<Output = Result<PipewireSelectScreenResult, wlx_capture::pipewire::AshpdError>>>,
>;
pub struct MirrorRenderer {
name: Arc<str>,
renderer: Option<ScreenRenderer>,
selector: Option<JoinHandle<Option<PipewireSelectScreenResult>>>,
selector: Option<PinnedSelectorFuture>,
last_extent: [u32; 3],
}
impl MirrorRenderer {
pub fn new(name: Arc<str>) -> Self {
let selector = Box::pin(pipewire_select_screen(None, false, false, false));
Self {
name,
renderer: None,
selector: Some(std::thread::spawn(|| {
executor::block_on(pipewire_select_screen(None, false, false, false)).ok()
})),
selector: Some(selector),
last_extent: [0; 3],
}
}
@@ -40,14 +45,18 @@ impl OverlayRenderer for MirrorRenderer {
Ok(())
}
fn render(&mut self, app: &mut AppState) -> anyhow::Result<()> {
if let Some(selector) = self.selector.take() {
if !selector.is_finished() {
self.selector = Some(selector);
return Ok(());
}
if let Some(mut selector) = self.selector.take() {
let maybe_pw_result = match selector
.poll_unpin(&mut Context::from_waker(futures::task::noop_waker_ref()))
{
Poll::Ready(result) => result,
Poll::Pending => {
self.selector = Some(selector);
return Ok(());
}
};
// safe unwrap because we know it's finished
if let Some(pw_result) = selector.join().unwrap() {
if let Ok(pw_result) = maybe_pw_result {
log::info!(
"{}: PipeWire node selected: {}",
self.name.clone(),
+80 -24
View File
@@ -16,7 +16,6 @@ use wlx_capture::{
DrmFormat, MouseMeta, WlxFrame, DRM_FORMAT_ABGR8888, DRM_FORMAT_ARGB8888,
DRM_FORMAT_XBGR8888, DRM_FORMAT_XRGB8888,
},
wayland::wayland_client::protocol::wl_output::Transform,
WlxCapture,
};
@@ -27,7 +26,7 @@ use {
std::{collections::HashMap, error::Error, f32::consts::PI, ops::Deref, path::PathBuf},
wlx_capture::{
pipewire::{pipewire_select_screen, PipewireCapture},
wayland::{WlxClient, WlxOutput},
wayland::{wayland_client::protocol::wl_output, WlxClient, WlxOutput},
wlr_dmabuf::WlrDmabufCapture,
wlr_screencopy::WlrScreencopyCapture,
},
@@ -49,6 +48,8 @@ use crate::{
state::{AppSession, AppState},
};
const CURSOR_SIZE: f32 = 16. / 1440.;
static DRM_FORMATS: once_cell::sync::OnceCell<Vec<DrmFormat>> = once_cell::sync::OnceCell::new();
pub struct ScreenInteractionHandler {
@@ -208,18 +209,21 @@ impl ScreenPipeline {
cmd.run_ref(&pass)?;
if let (Some(mouse), Some(mouse_view)) = (mouse, self.mouse.clone()) {
let size = CURSOR_SIZE * self.extentf[1];
let half_size = size * 0.5;
let vertex_buffer = app.graphics.upload_verts(
self.extentf[0],
self.extentf[1],
mouse.x * self.extentf[0] - 2.,
mouse.y * self.extentf[1] - 2.,
4.0,
4.0,
mouse.x * self.extentf[0] - half_size,
mouse.y * self.extentf[1] - half_size,
size,
size,
)?;
let set0 = self
.pipeline
.uniform_sampler(0, mouse_view.clone(), Filter::Linear)?;
.uniform_sampler(0, mouse_view.clone(), Filter::Nearest)?;
let pass = self.pipeline.create_pass(
self.extentf,
@@ -289,19 +293,18 @@ impl ScreenRenderer {
output: &WlxOutput,
token: Option<&str>,
session: &AppSession,
) -> Option<(
) -> anyhow::Result<(
ScreenRenderer,
Option<String>, /* pipewire restore token */
)> {
let name = output.name.clone();
let embed_mouse = !session.config.double_cursor_fix;
let select_screen_result =
futures::executor::block_on(pipewire_select_screen(token, embed_mouse, true, true))
.ok()?;
futures::executor::block_on(pipewire_select_screen(token, embed_mouse, true, true))?;
let capture = PipewireCapture::new(name, select_screen_result.node_id, 60);
Some((
Ok((
ScreenRenderer {
name: output.name.clone(),
capture: Box::new(capture),
@@ -547,17 +550,26 @@ where
);
}
if let Some((renderer, restore_token)) = ScreenRenderer::new_pw(output, token, session) {
capture = Some(renderer);
match ScreenRenderer::new_pw(output, token, session) {
Ok((renderer, restore_token)) => {
capture = Some(renderer);
if let Some(token) = restore_token {
if pw_token_store
.insert(String::from(display_name), token.clone())
.is_none()
{
log::info!("Adding Pipewire token {}", token);
if let Some(token) = restore_token {
if pw_token_store
.insert(String::from(display_name), token.clone())
.is_none()
{
log::info!("Adding Pipewire token {}", token);
}
}
}
Err(e) => {
log::warn!(
"{}: Failed to create Pipewire capture: {:?}",
&output.name,
e
);
}
}
}
if let Some(capture) = capture {
@@ -566,14 +578,16 @@ where
interaction: Box::new(ScreenInteractionHandler::new(
vec2(output.logical_pos.0 as f32, output.logical_pos.1 as f32),
vec2(output.logical_size.0 as f32, output.logical_size.1 as f32),
output.transform,
output.transform.into(),
)),
});
let axis = Vec3::new(0., 0., 1.);
let transform = output.transform.into();
let angle = if session.config.upright_screen_fix {
match output.transform {
match transform {
Transform::_90 | Transform::Flipped90 => PI / 2.,
Transform::_180 | Transform::Flipped180 => PI,
Transform::_270 | Transform::Flipped270 => -PI / 2.,
@@ -584,7 +598,7 @@ where
};
let center = Vec2 { x: 0.5, y: 0.5 };
let interaction_transform = match output.transform {
let interaction_transform = match transform {
Transform::_90 | Transform::Flipped90 => Affine2::from_cols(
Vec2::NEG_Y * (output.size.0 as f32 / output.size.1 as f32),
Vec2::NEG_X,
@@ -688,7 +702,7 @@ where
O: Default,
{
let mut overlays = vec![];
let wl = WlxClient::new().ok_or_else(|| anyhow::anyhow!("Failed to connect to Wayland"))?;
let mut wl = WlxClient::new().ok_or_else(|| anyhow::anyhow!("Failed to connect to Wayland"))?;
// Load existing Pipewire tokens from file
let mut pw_tokens: HashMap<String, String> = if let Ok(conf) = load_pw_token_config() {
@@ -699,6 +713,20 @@ where
let pw_tokens_copy = pw_tokens.clone();
let mut origin = (i32::MAX, i32::MAX);
for output in wl.outputs.values() {
origin.0 = origin.0.min(output.logical_pos.0);
origin.1 = origin.1.min(output.logical_pos.1);
}
log::info!("Desktop origin: {:?}", origin);
// adjust all outputs so that the top-left corner is at (0, 0)
for output in wl.outputs.values_mut() {
output.logical_pos.0 -= origin.0;
output.logical_pos.1 -= origin.1;
}
for id in wl.outputs.keys() {
if let Some(overlay) = try_create_screen(&wl, *id, &mut pw_tokens, session) {
overlays.push(overlay);
@@ -797,11 +825,39 @@ where
Ok((overlays, extent))
}
#[allow(unused)]
pub enum Transform {
Normal,
_90,
_180,
_270,
Flipped90,
Flipped180,
Flipped270,
}
#[cfg(feature = "wayland")]
impl From<wl_output::Transform> for Transform {
fn from(t: wl_output::Transform) -> Transform {
match t {
wl_output::Transform::Normal => Transform::Normal,
wl_output::Transform::_90 => Transform::_90,
wl_output::Transform::_180 => Transform::_180,
wl_output::Transform::_270 => Transform::_270,
wl_output::Transform::Flipped => Transform::Flipped180,
wl_output::Transform::Flipped90 => Transform::Flipped90,
wl_output::Transform::Flipped180 => Transform::Flipped180,
wl_output::Transform::Flipped270 => Transform::Flipped270,
_ => Transform::Normal,
}
}
}
fn extent_from_res(res: (i32, i32)) -> [u32; 3] {
// screens above a certain resolution will have severe aliasing
// TODO make dynamic. maybe don't go above HMD resolution?
let w = res.0.min(1920) as u32;
let w = res.0.min(2560) as u32;
let h = (res.1 as f32 / res.0 as f32 * w as f32) as u32;
[w, h, 1]
}
+2 -11
View File
@@ -1,12 +1,9 @@
use std::{
io::Cursor,
ops::Add,
sync::{atomic::AtomicUsize, Arc},
time::Instant,
};
use rodio::{Decoder, Source};
use glam::vec3a;
use crate::{
@@ -21,6 +18,7 @@ use crate::{
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);
@@ -79,12 +77,7 @@ impl Toast {
.enqueue_at(TaskType::DropOverlay(selector), destroy_at);
if has_sound {
if let Some(handle) = app.audio.get_handle() {
let wav = include_bytes!("../res/557297.wav");
let cursor = Cursor::new(wav);
let source = Decoder::new_wav(cursor).unwrap();
let _ = handle.play_raw(source.convert_samples());
}
app.audio.play(TOAST_AUDIO_WAV);
}
}
}
@@ -136,7 +129,6 @@ fn new_toast(
canvas.panel(0., 0., size.0, size.1);
if toast.body.len() > 0 {
log::info!("{}: {}", title, toast.body);
canvas.label(PADDING.0, 54., og_width, size.1 - 54., toast.body);
canvas.fg_color = color_parse("#101010").unwrap(); // want panic
@@ -144,7 +136,6 @@ fn new_toast(
canvas.panel(0., 0., size.0, 30.);
canvas.label_centered(PADDING.0, 16., og_width, FONT_SIZE as f32 + 2., title);
} else {
log::info!("Toast: {}", title);
canvas.label_centered(PADDING.0, 0., og_width, size.1, title);
}
+13 -3
View File
@@ -4,7 +4,9 @@ use std::path::PathBuf;
use crate::{
backend::overlay::{ui_transform, OverlayData, OverlayState, RelativeTo},
config::{def_left, def_watch_pos, def_watch_rot, load_known_yaml, ConfigType},
config::{
def_half, def_left, def_point7, def_watch_pos, def_watch_rot, load_known_yaml, ConfigType,
},
config_io,
gui::modular::{modular_canvas, ModularUiConfig},
state::{AppState, LeftRight},
@@ -78,6 +80,12 @@ pub struct WatchConf {
#[serde(default = "def_left")]
pub watch_hand: LeftRight,
#[serde(default = "def_half")]
pub watch_view_angle_min: f32,
#[serde(default = "def_point7")]
pub watch_view_angle_max: f32,
}
fn get_config_path() -> PathBuf {
@@ -87,9 +95,11 @@ fn get_config_path() -> PathBuf {
}
pub fn save_watch(app: &mut AppState) -> anyhow::Result<()> {
let conf = WatchConf {
watch_pos: app.session.config.watch_pos.clone(),
watch_rot: app.session.config.watch_rot.clone(),
watch_pos: app.session.config.watch_pos,
watch_rot: app.session.config.watch_rot,
watch_hand: app.session.config.watch_hand,
watch_view_angle_min: app.session.config.watch_view_angle_min,
watch_view_angle_max: app.session.config.watch_view_angle_max,
};
let yaml = serde_yaml::to_string(&conf)?;
+5
View File
@@ -40,6 +40,11 @@
"type": "boolean",
"requirement": "optional"
},
{
"name": "/actions/default/in/SpaceRotate",
"type": "boolean",
"requirement": "optional"
},
{
"name": "/actions/default/in/LeftHand",
"type": "pose",
+12 -32
View File
@@ -22,47 +22,27 @@ row_size: 23
# Specifies the size of each key. The sum of any given row must equal RowSize
key_sizes:
- [1.5,0.5, 1, 1, 1, 1,0.5,1, 1, 1, 1,0.5,1, 1, 1, 1, 0.5, 1, 1, 1, 4.5]
- [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0.5, 1, 1, 1, 0.5, 1, 1, 1, 1]
- [1.5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.5, 0.5, 1, 1, 1, 0.5, 1, 1, 1, 1]
- [1.75, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2.25, 4, 1, 1, 1, 1]
- [2.25, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2.75, 1.5, 1, 1.5, 1, 1, 1, 1]
- [1.25, 1.25, 1.25, 6.25, 1.25, 1.25, 1.25, 1.25, 0.5, 1, 1, 1, 0.5, 2, 1, 1]
- [1.5,0.5, 1, 1, 1, 1,0.5,1, 1, 1, 1,0.5,1, 1, 1, 1, 0.5, 1, 1, 1, 0.5, 1, 1, 1, 1]
- [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0.5, 1, 1, 1, 0.5, 1, 1, 1, 1]
- [1.5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1.5, 0.5, 1, 1, 1, 0.5, 1, 1, 1, 1]
- [1.75, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2.25, 4, 1, 1, 1, 1]
- [2.25, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2.75, 1.5, 1, 1.5, 1, 1, 1, 1]
- [1.25, 1.25, 1.25, 6.25, 1.25, 1.25, 1.25, 1.25, 0.5, 1, 1, 1, 0.5, 2, 1, 1]
# The main (blue) layout of the keyboard.
# Accepted are:
# - virtual keys, for a full list go to https://github.com/galister/X11Overlay/blob/master/Types/VirtualKey.cs
# - ExecCommands (defined below)
# - Macros (defined below)
# - virtual keys. For a full list, look at enum VirtualKey in https://github.com/galister/wlx-overlay-s/blob/main/src/hid.rs
# - exec_commands (defined below)
# - macros (defined below)
# - ~ (null) will leave an empty space with the corresponding size from key_sizes
main_layout:
- ["Escape", ~, "F1", "F2", "F3", "F4", ~, "F5", "F6", "F7", "F8", ~, "F9", "F10", "F11", "F12", ~, "Print", "Scroll", "Pause", ~]
- ["Escape", ~, "F1", "F2", "F3", "F4", ~, "F5", "F6", "F7", "F8", ~, "F9", "F10", "F11", "F12", ~, "Print", "Scroll", "Pause", ~, "COPY", "PASTE", ~, "KILL"]
- ["Oem3", "N1", "N2", "N3", "N4", "N5", "N6", "N7", "N8", "N9", "N0", "Minus", "Plus", "BackSpace", ~, "Insert", "Home", "Prior", ~, "NumLock", "KP_Divide", "KP_Multiply", "KP_Subtract"]
- ["Tab", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "Oem4", "Oem6", "Oem5", ~, "Delete", "End", "Next", ~, "KP_7", "KP_8", "KP_9", "KP_Add"]
- ["XF86Favorites", "A", "S", "D", "F", "G", "H", "J", "K", "L", "Oem1", "Oem7", "Return", ~, "KP_4", "KP_5", "KP_6", ~]
- ["LShift", "Z", "X", "C", "V", "B", "N", "M", "Comma", "Period", "Oem2", "RShift", ~, "Up", ~, "KP_1", "KP_2", "KP_3", "KP_Enter"]
- ["LCtrl", "LSuper", "LAlt", "Space", "Meta", "RSuper", "Menu", "RCtrl", ~, "Left", "Down", "Right", ~, "KP_0", "KP_Decimal", ~]
# When using the purple pointer...
# None - No special functionality when using purple pointer
# Shift - Use same functionality as the orange pointer
# Ctrl - Use Main layout with Ctrl modifier
# Super - Use Main layout with Super (WinKey) modifier
# Meta - Use Main layout with Meta (AltGr) modifier
# Layout - Use the alt_layout defined below
alt_layout_mode: Ctrl
# The alt (fn) layout of the keyboard. Only takes effect when alt_layout_mode: Layout
# Accepted values are the same as for MainLayout.
# The default layout here is a dummy. Change it to fit your liking.
alt_layout:
- ["Escape", ~, "F1", "F2", "F3", "F4", ~, "F5", "F6", "F7", "F8", ~, "F9", "F10", "F11", "F12", ~, "Print", "Scroll", "Pause", ~, "COPY", "PASTE"]
- ["Oem3", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "Minus", "Plus", "BackSpace", ~, "Insert", "Home", "Prior", ~, "NumLock", "KP_Divide", "KP_Multiply", "KP_Subtract"]
- ["Tab", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "Oem4", "Oem6", "Oem5", ~, "Delete", "End", "Next", ~, "KP_7", "KP_8", "KP_9", "KP_Add"]
- ["XF86Favorites", "A", "S", "D", "F", "G", "H", "J", "K", "L", "Oem1", "Oem7", "Return", ~, "KP_4", "KP_5", "KP_6", ~]
- ["LShift", "Oem102", "Z", "X", "C", "V", "B", "N", "M", "Comma", "Period", "Oem2", "RShift", ~, "Up", ~, "KP_1", "KP_2", "KP_3", "KP_Enter"]
- ["LCtrl", "LSuper", "LAlt", "Space", "Meta", "RSuper", "Menu", "RCtrl", ~, "Left", "Down", "Right", ~, "KP_0", "KP_Decimal", ~]
# Shell commands to be used in a layout.
# Value is an array of string arguments.
exec_commands:
@@ -70,7 +50,7 @@ exec_commands:
# Series of keypresses to be used in a layout.
# Format: keyName [DOWN|UP]
# keyName must be a valid key from `xmodmap -pke`
# keyName must be a valid virtual key from the VirtualKey enum (see above)
# DOWN|UP: can be omitted for an implicit "keyName DOWN, keyName UP"
macros:
KILL: [ "LSuper DOWN", "LCtrl DOWN", "Escape", "LCtrl UP", "LSuper UP" ]
@@ -78,7 +58,7 @@ macros:
PASTE: [ "LCtrl DOWN", "V", "LCtrl UP" ]
# Custom labels to use.
# Key: element of MainLayout / AltLayout
# Key: element of main_layout
# Value: Array of strings. 0th element is the upper row, 1st element is lower row.
# For empty labels, use [] (do not use ~)
labels:
+16 -7
View File
@@ -270,10 +270,13 @@ elements:
fg_color: "#ffffff"
bg_color: "#707070"
text: "Show/Hide"
click_down:
click_down: # ToggleVisible if exists, else create
- type: Overlay
target: M1
action: ToggleVisible # only fires if overlay exists
- type: Window
target: M1
action: ShowMirror
action: ShowMirror # only fires if not exists
- type: Button
rect: [185, 270, 60, 30]
@@ -311,6 +314,9 @@ elements:
bg_color: "#707070"
text: "Show/Hide"
click_down:
- type: Overlay
target: M2
action: ToggleVisible
- type: Window
target: M2
action: ShowMirror
@@ -351,6 +357,9 @@ elements:
bg_color: "#707070"
text: "Show/Hide"
click_down:
- type: Overlay
target: M3
action: ToggleVisible
- type: Window
target: M3
action: ShowMirror
@@ -509,14 +518,14 @@ elements:
bg_color: "#c0c0c0"
- type: Label
rect: [325, 480, 90, 24]
rect: [325, 490, 90, 24]
font_size: 18
fg_color: "#ffffff"
source: Static
text: Notifications
- type: Button
rect: [330, 495, 220, 30]
rect: [330, 505, 220, 30]
font_size: 12
fg_color: "#ffffff"
bg_color: "#606020"
@@ -526,7 +535,7 @@ elements:
action: ToggleNotifications
- type: Button
rect: [330, 545, 220, 30]
rect: [330, 555, 220, 30]
font_size: 12
fg_color: "#ffffff"
bg_color: "#606020"
@@ -536,11 +545,11 @@ elements:
action: ToggleNotificationSounds
- type: Panel
rect: [50, 595, 500, 1]
rect: [50, 605, 500, 1]
bg_color: "#c0c0c0"
- type: Button
rect: [330, 615, 220, 30]
rect: [330, 625, 220, 30]
font_size: 12
fg_color: "#ffffff"
bg_color: "#206060"
+5 -2
View File
@@ -18,10 +18,13 @@ elements:
bg_color: "#808040"
fg_color: "#ffffff"
text: "C"
click_up:
click_up: # destroy if exists, otherwise create
- type: Window
target: settings
action: ShowUi
action: ShowUi # only triggers if not exists
- type: Window
target: settings
action: Destroy # only triggers if exists since before current frame
# Keyboard button
- type: Button
+6
View File
@@ -94,6 +94,12 @@ pub mod frag_screen {
void main()
{
out_color = texture(in_texture, in_uv);
// linear to srgb
bvec4 cutoff = lessThan(out_color, vec4(0.0031308));
vec4 higher = (pow(out_color, vec4(0.41666)) * vec4(1.055)) - vec4(0.055);
vec4 lower = out_color*vec4(12.92);
out_color = mix(higher, lower, cutoff);
out_color.a = 1.0;
}
",
+19 -4
View File
@@ -1,8 +1,8 @@
use std::{path::PathBuf, sync::Arc};
use std::{io::Cursor, path::PathBuf, sync::Arc};
use anyhow::bail;
use glam::Vec3;
use rodio::{OutputStream, OutputStreamHandle};
use rodio::{Decoder, OutputStream, OutputStreamHandle, Source};
use serde::{Deserialize, Serialize};
use smallvec::{smallvec, SmallVec};
use vulkano::format::Format;
@@ -126,18 +126,33 @@ impl AudioOutput {
}
}
pub fn get_handle(&mut self) -> Option<&OutputStreamHandle> {
fn get_handle(&mut self) -> Option<&OutputStreamHandle> {
if self.audio_stream.is_none() && self.first_try {
self.first_try = false;
if let Ok((stream, handle)) = OutputStream::try_default() {
self.audio_stream = Some((stream, handle));
} else {
log::error!("Failed to open audio stream");
log::error!("Failed to open audio stream. Audio will not work.");
return None;
}
}
self.audio_stream.as_ref().map(|(_, h)| h)
}
pub fn play(&mut self, wav_bytes: &'static [u8]) {
let Some(handle) = self.get_handle() else {
return;
};
let cursor = Cursor::new(wav_bytes);
let source = match Decoder::new_wav(cursor) {
Ok(source) => source,
Err(e) => {
log::error!("Failed to play sound: {:?}", e);
return;
}
};
let _ = handle.play_raw(source.convert_samples());
}
}
pub struct ScreenMeta {