From edfa77e07ce17c266fe358ffae09356f748d46e9 Mon Sep 17 00:00:00 2001 From: Aleksander Date: Fri, 18 Oct 2024 20:21:23 +0200 Subject: [PATCH] Integrate WayVR into wlx directly --- Cargo.lock | 231 +++++++++++++++-- Cargo.toml | 23 +- src/backend/input.rs | 15 +- src/backend/mod.rs | 3 + src/backend/openvr/mod.rs | 10 + src/backend/openxr/mod.rs | 10 + src/backend/overlay.rs | 7 +- src/backend/wayvr/README.md | 37 +++ src/backend/wayvr/client.rs | 178 ++++++++++++++ src/backend/wayvr/comp.rs | 186 ++++++++++++++ src/backend/wayvr/display.rs | 356 +++++++++++++++++++++++++++ src/backend/wayvr/egl_data.rs | 273 ++++++++++++++++++++ src/backend/wayvr/egl_ex.rs | 32 +++ src/backend/wayvr/event_queue.rs | 32 +++ src/backend/wayvr/handle.rs | 157 ++++++++++++ src/backend/wayvr/mod.rs | 213 ++++++++++++++++ src/backend/wayvr/smithay_wrapper.rs | 54 ++++ src/backend/wayvr/time.rs | 9 + src/backend/wayvr/window.rs | 69 ++++++ src/graphics/mod.rs | 54 ++-- src/gui/canvas/builder.rs | 1 + src/gui/canvas/control.rs | 1 + src/overlays/keyboard.rs | 48 +++- src/overlays/mod.rs | 3 + src/overlays/screen.rs | 3 +- src/overlays/wayvr.rs | 272 ++++++++++++++++++++ src/state.rs | 38 ++- 27 files changed, 2256 insertions(+), 59 deletions(-) create mode 100644 src/backend/wayvr/README.md create mode 100644 src/backend/wayvr/client.rs create mode 100644 src/backend/wayvr/comp.rs create mode 100644 src/backend/wayvr/display.rs create mode 100644 src/backend/wayvr/egl_data.rs create mode 100644 src/backend/wayvr/egl_ex.rs create mode 100644 src/backend/wayvr/event_queue.rs create mode 100644 src/backend/wayvr/handle.rs create mode 100644 src/backend/wayvr/mod.rs create mode 100644 src/backend/wayvr/smithay_wrapper.rs create mode 100644 src/backend/wayvr/time.rs create mode 100644 src/backend/wayvr/window.rs create mode 100644 src/overlays/wayvr.rs diff --git a/Cargo.lock b/Cargo.lock index ed12ab5d..82f9fb18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -179,9 +179,24 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" + +[[package]] +name = "appendlist" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e149dc73cd30538307e7ffa2acd3d2221148eaeed4871f246657b1c3eaa1cbd2" + +[[package]] +name = "approx" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278" +dependencies = [ + "num-traits", +] [[package]] name = "approx" @@ -680,13 +695,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "calloop" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ead1e1514bce44c0f40e027899fbc595907fc112635bed21b3b5d975c0a5e7" +dependencies = [ + "bitflags 2.6.0", + "polling", + "rustix", + "slab", + "tracing", +] + [[package]] name = "calloop-wayland-source" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" dependencies = [ - "calloop", + "calloop 0.13.0", "rustix", "wayland-backend", "wayland-client", @@ -745,6 +773,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cgmath" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317" +dependencies = [ + "approx 0.4.0", + "num-traits", +] + [[package]] name = "chrono" version = "0.4.38" @@ -1323,6 +1361,15 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + [[package]] name = "endi" version = "1.1.0" @@ -1679,13 +1726,24 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + [[package]] name = "glam" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "779ae4bf7e8421cf91c0b3b64e7e8b40b862fba4d393f59150042de7c4965a94" dependencies = [ - "approx", + "approx 0.5.1", "mint", "serde", ] @@ -1899,6 +1957,12 @@ dependencies = [ "nix 0.29.0", ] +[[package]] +name = "io-lifetimes" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a611371471e98973dbcab4e0ec66c31a10bc356eeb4d54a0e05eac8158fe38c" + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -2004,6 +2068,22 @@ dependencies = [ "serde", ] +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + [[package]] name = "lazy_static" version = "1.5.0" @@ -3056,6 +3136,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" +dependencies = [ + "quote", + "syn 2.0.72", +] + [[package]] name = "quick-xml" version = "0.30.0" @@ -3067,9 +3166,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.34.0" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f24d770aeca0eacb81ac29dfbc55ebcc09312fdd1f8bbecdc7e4a84e000e3b4" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" dependencies = [ "memchr", ] @@ -3494,6 +3593,41 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "smithay" +version = "0.3.0" +source = "git+https://github.com/Smithay/smithay.git#df79eeba63a8e9c2d33b9be2418aee6a940135e7" +dependencies = [ + "appendlist", + "bitflags 2.6.0", + "calloop 0.14.1", + "cgmath", + "cursor-icon", + "downcast-rs", + "drm-fourcc", + "encoding_rs", + "errno", + "gl_generator", + "indexmap 2.3.0", + "libc", + "libloading 0.8.5", + "once_cell", + "profiling", + "rand", + "rustix", + "scopeguard", + "smallvec", + "tempfile", + "thiserror", + "tracing", + "wayland-protocols", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-server", + "x11rb", + "xkbcommon 0.8.0", +] + [[package]] name = "smithay-client-toolkit" version = "0.19.2" @@ -3502,7 +3636,7 @@ checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ "bitflags 2.6.0", "bytemuck", - "calloop", + "calloop 0.13.0", "calloop-wayland-source", "cursor-icon", "libc", @@ -3777,6 +3911,7 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -3888,6 +4023,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +dependencies = [ + "getrandom", + "rand", +] + [[package]] name = "version-compare" version = "0.2.0" @@ -4047,9 +4192,9 @@ checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wayland-backend" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90e11ce2ca99c97b940ee83edbae9da2d56a08f9ea8158550fd77fa31722993" +checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6" dependencies = [ "cc", "downcast-rs", @@ -4061,9 +4206,9 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.5" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e321577a0a165911bdcfb39cf029302479d7527b517ee58ab0f6ad09edf0943" +checksum = "e3f45d1222915ef1fd2057220c1d9d9624b7654443ea35c3877f7a52bd0a5a2d" dependencies = [ "bitflags 2.6.0", "rustix", @@ -4094,15 +4239,39 @@ dependencies = [ ] [[package]] -name = "wayland-protocols" -version = "0.32.3" +name = "wayland-egl" +version = "0.32.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62989625a776e827cc0f15d41444a3cea5205b963c3a25be48ae1b52d6b4daaa" +checksum = "4e3cb8b84ff95310fe59ce6c61f1fa344ec22f4c240c369a2b20f15caebfede4" +dependencies = [ + "wayland-backend", + "wayland-sys", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b5755d77ae9040bb872a25026555ce4cb0ae75fd923e90d25fba07d81057de0" dependencies = [ "bitflags 2.6.0", "wayland-backend", "wayland-client", "wayland-scanner", + "wayland-server", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40dd9d2f7f2713724d84b920d6f73ff878f6a353712942f75f78f4dadb72886" +dependencies = [ + "bitflags 2.6.0", + "wayland-backend", + "wayland-protocols", + "wayland-scanner", + "wayland-server", ] [[package]] @@ -4129,24 +4298,39 @@ dependencies = [ "wayland-client", "wayland-protocols", "wayland-scanner", + "wayland-server", ] [[package]] name = "wayland-scanner" -version = "0.31.4" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6" +checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" dependencies = [ "proc-macro2", - "quick-xml 0.34.0", + "quick-xml 0.36.2", "quote", ] [[package]] -name = "wayland-sys" -version = "0.31.4" +name = "wayland-server" +version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43676fe2daf68754ecf1d72026e4e6c15483198b5d24e888b74d3f22f887a148" +checksum = "0f18d47038c0b10479e695d99ed073e400ccd9bdbb60e6e503c96f62adcb12b6" +dependencies = [ + "bitflags 2.6.0", + "downcast-rs", + "io-lifetimes", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-sys" +version = "0.31.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09" dependencies = [ "dlib", "log", @@ -4507,7 +4691,7 @@ dependencies = [ "bitflags 2.6.0", "block2", "bytemuck", - "calloop", + "calloop 0.13.0", "cfg_aliases 0.2.1", "concurrent-queue", "core-foundation", @@ -4599,6 +4783,7 @@ dependencies = [ "input-linux", "json", "json5", + "khronos-egl", "libc", "libmonado-rs", "log", @@ -4615,11 +4800,15 @@ dependencies = [ "serde_json5", "serde_yaml", "smallvec", + "smithay", "strum", "sysinfo", "thiserror", + "uuid", "vulkano", "vulkano-shaders", + "wayland-client", + "wayland-egl", "winit", "wlx-capture", "xcb", diff --git a/Cargo.toml b/Cargo.toml index d0690b78..26c80b8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1.0.86" +anyhow = "1.0.89" ash = "^0.37.2" chrono = "0.4.38" chrono-tz = "0.9.0" @@ -70,11 +70,23 @@ image_dds = { version = "0.6.0", default-features = false, features = [ ] } mint = "0.5.9" +# WayVR-only deps +khronos-egl = { version = "6.0.0", features = ["static"], optional = true } +smithay = { git = "https://github.com/Smithay/smithay.git", default-features = false, features = [ + "renderer_gl", + "backend_egl", + "xwayland", + "wayland_frontend", +], optional = true } +uuid = { version = "1.10.0", features = ["v4", "fast-rng"], optional = true } +wayland-client = { version = "0.31.6", optional = true } +wayland-egl = { version = "0.32.4", optional = true } + [build-dependencies] regex = { version = "*" } [features] -default = ["openvr", "openxr", "osc", "x11", "wayland"] +default = ["openxr", "openvr", "osc", "x11", "wayland"] openvr = ["dep:ovr_overlay", "dep:json"] openxr = ["dep:openxr", "dep:libmonado-rs"] osc = ["dep:rosc"] @@ -83,4 +95,11 @@ wayland = ["pipewire", "wlx-capture/wlr", "xkbcommon/wayland"] pipewire = ["wlx-capture/pipewire"] uidev = ["dep:winit"] xcb = ["dep:xcb"] +wayvr = [ + "dep:khronos-egl", + "dep:smithay", + "dep:uuid", + "dep:wayland-client", + "dep:wayland-egl", +] as-raw-xcb-connection = [] diff --git a/src/backend/input.rs b/src/backend/input.rs index 7576e786..649e23b4 100644 --- a/src/backend/input.rs +++ b/src/backend/input.rs @@ -8,9 +8,9 @@ use smallvec::{smallvec, SmallVec}; use crate::backend::common::{snap_upright, OverlaySelector}; use crate::config::{AStrMapExt, GeneralConfig}; use crate::overlays::anchor::ANCHOR_NAME; -use crate::state::AppState; +use crate::state::{AppState, KeyboardFocus}; -use super::overlay::OverlayID; +use super::overlay::{OverlayID, OverlayState}; use super::task::{TaskContainer, TaskType}; use super::{common::OverlayContainer, overlay::OverlayData}; @@ -262,6 +262,15 @@ pub enum PointerMode { Special, } +fn update_focus(focus: &mut KeyboardFocus, state: &OverlayState) { + if let Some(f) = &state.keyboard_focus { + if *focus != *f { + log::info!("Setting keyboard focus to {:?}", *f); + *focus = *f; + } + } +} + pub fn interact( overlays: &mut OverlayContainer, app: &mut AppState, @@ -362,6 +371,7 @@ where log::trace!("Hit: {} {:?}", hovered.state.name, hit); if pointer.now.grab && !pointer.before.grab && hovered.state.grabbable { + update_focus(&mut app.keyboard_focus, &hovered.state); pointer.start_grab(hovered, &mut app.tasks); return ( hit.dist, @@ -407,6 +417,7 @@ where if pointer.now.click && !pointer.before.click { pointer.interaction.clicked_id = Some(hit.overlay); + update_focus(&mut app.keyboard_focus, &hovered.state); hovered.backend.on_pointer(app, &hit, true); } else if !pointer.now.click && pointer.before.click { if let Some(clicked_id) = pointer.interaction.clicked_id.take() { diff --git a/src/backend/mod.rs b/src/backend/mod.rs index aedde510..fa5397ae 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -17,6 +17,9 @@ pub mod uidev; #[cfg(feature = "osc")] pub mod osc; +#[cfg(feature = "wayvr")] +pub mod wayvr; + pub mod overlay; pub mod task; diff --git a/src/backend/openvr/mod.rs b/src/backend/openvr/mod.rs index 6872c95c..f8544803 100644 --- a/src/backend/openvr/mod.rs +++ b/src/backend/openvr/mod.rs @@ -320,6 +320,11 @@ pub fn openvr_run(running: Arc, show_by_default: bool) -> Result<(), let _ = sender.send_params(&overlays); }; + #[cfg(feature = "wayvr")] + if let Some(wayvr) = &state.wayvr { + wayvr.borrow_mut().tick_events()?; + } + log::trace!("Rendering frame"); for o in overlays.iter_mut() { @@ -334,6 +339,11 @@ pub fn openvr_run(running: Arc, show_by_default: bool) -> Result<(), .iter_mut() .for_each(|o| o.after_render(universe.clone(), &mut overlay_mgr, &state.graphics)); + #[cfg(feature = "wayvr")] + if let Some(wayvr) = &state.wayvr { + wayvr.borrow_mut().tick_finish()?; + } + // chaperone // close font handles? diff --git a/src/backend/openxr/mod.rs b/src/backend/openxr/mod.rs index 2e47fba2..b28a071c 100644 --- a/src/backend/openxr/mod.rs +++ b/src/backend/openxr/mod.rs @@ -360,6 +360,11 @@ pub fn openxr_run(running: Arc, show_by_default: bool) -> Result<(), } } + #[cfg(feature = "wayvr")] + if let Some(wayvr) = &app_state.wayvr { + wayvr.borrow_mut().tick_events()?; + } + for o in overlays.iter_mut() { if !o.state.want_visible { continue; @@ -394,6 +399,11 @@ pub fn openxr_run(running: Arc, show_by_default: bool) -> Result<(), layers.push((0.0, maybe_layer)); } + #[cfg(feature = "wayvr")] + if let Some(wayvr) = &app_state.wayvr { + wayvr.borrow_mut().tick_finish()?; + } + command_buffer.build_and_execute_now()?; layers.sort_by(|a, b| b.0.total_cmp(&a.0)); diff --git a/src/backend/overlay.rs b/src/backend/overlay.rs index 94f9a675..04a4e882 100644 --- a/src/backend/overlay.rs +++ b/src/backend/overlay.rs @@ -11,7 +11,10 @@ use glam::{Affine2, Affine3A, Mat3A, Quat, Vec2, Vec3, Vec3A}; use serde::Deserialize; use vulkano::image::view::ImageView; -use crate::{config::AStrMapExt, state::AppState}; +use crate::{ + config::AStrMapExt, + state::{AppState, KeyboardFocus}, +}; use super::input::{DummyInteractionHandler, Haptics, InteractionHandler, PointerHit}; @@ -34,6 +37,7 @@ pub struct OverlayState { pub interactable: bool, pub recenter: bool, pub anchored: bool, + pub keyboard_focus: Option, pub dirty: bool, pub alpha: f32, pub z_order: u32, @@ -60,6 +64,7 @@ impl Default for OverlayState { recenter: false, interactable: false, anchored: false, + keyboard_focus: None, dirty: true, alpha: 1.0, z_order: 0, diff --git a/src/backend/wayvr/README.md b/src/backend/wayvr/README.md new file mode 100644 index 00000000..da5f07b1 --- /dev/null +++ b/src/backend/wayvr/README.md @@ -0,0 +1,37 @@ +**WayVR acts as a bridge between Wayland applications and wlx-overlay-s panels, allowing you to display your applications within a VR environment. Internally, WayVR utilizes Smithay to run a Wayland compositor.** + +# Features + +- Display Wayland applications without GPU overhead (zero-copy via dma-buf) +- Mouse input +- Precision scrolling support +- XWayland "support" via `cage` + +# Supported hardware + +### Confirmed working GPUs + +- Navi 32 family: AMD Radeon RX 7800 XT **\*** +- Navi 23 family: AMD Radeon RX 6600 XT +- Navi 21 family: AMD Radeon Pro W6800, AMD Radeon RX 6800 XT +- Nvidia GTX 16 Series +- _Your GPU here? (Let us know!)_ + +**\*** - With dmabuf modifier mitigation (probably Mesa bug) + +# Supported software + +- Basically all Qt applications (they work out of the box) +- Most XWayland applications via `cage` + +# Known issues + +- Context menus are not functional in most cases yet + +- Due to unknown circumstances, dma-buf textures may display various graphical glitches due to invalid dma-buf tiling modifier. Please report your GPU model when filing an issue. Alternatively, you can run wlx-overlay-s with `LIBGL_ALWAYS_SOFTWARE=1` to mitigate that (only the Smithay compositor will run in software renderer mode, wlx will still be accelerated). + +- Potential data race in the rendering pipeline - A texture could be displayed during the clear-and-blit process in the compositor, causing minor artifacts (no fence sync support yet). + +- Even though some applications support Wayland, some still check for the `DISPLAY` environment variable and an available X11 server, throwing an error. This can be fixed by running `cage`. + +- GNOME still insists on rendering client-side decorations instead of server-side ones. This results in all GTK applications looking odd due to additional window shadows. [Fix here, "Client-side decorations"](https://wiki.archlinux.org/title/GTK) diff --git a/src/backend/wayvr/client.rs b/src/backend/wayvr/client.rs new file mode 100644 index 00000000..84b16edc --- /dev/null +++ b/src/backend/wayvr/client.rs @@ -0,0 +1,178 @@ +use std::{io::Read, os::unix::net::UnixStream, sync::Arc}; + +use smithay::{ + backend::input::Keycode, + input::{keyboard::KeyboardHandle, pointer::PointerHandle}, + reexports::wayland_server, + utils::SerialCounter, +}; + +use super::{ + comp::{self}, + display, +}; + +pub struct WayVRClient { + pub client: wayland_server::Client, + pub display_handle: display::DisplayHandle, + pub pid: i32, +} + +pub struct WayVRManager { + pub state: comp::Application, + pub seat_keyboard: KeyboardHandle, + pub seat_pointer: PointerHandle, + pub serial_counter: SerialCounter, + pub wayland_env: super::WaylandEnv, + + display: wayland_server::Display, + listener: wayland_server::ListeningSocket, + + pub clients: Vec, +} + +fn get_display_auth_from_pid(pid: i32) -> anyhow::Result { + let path = format!("/proc/{}/environ", pid); + let mut env_data = String::new(); + std::fs::File::open(path)?.read_to_string(&mut env_data)?; + + let lines: Vec<&str> = env_data.split('\0').filter(|s| !s.is_empty()).collect(); + + for line in lines { + if let Some((key, value)) = line.split_once('=') { + if key == "WAYVR_DISPLAY_AUTH" { + return Ok(String::from(value)); + } + } + } + + anyhow::bail!("Failed to get display auth from PID {}", pid); +} + +impl WayVRManager { + pub fn new( + state: comp::Application, + display: wayland_server::Display, + seat_keyboard: KeyboardHandle, + seat_pointer: PointerHandle, + ) -> anyhow::Result { + let (wayland_env, listener) = create_wayland_listener()?; + + Ok(Self { + state, + display, + seat_keyboard, + seat_pointer, + listener, + wayland_env, + serial_counter: SerialCounter::new(), + clients: Vec::new(), + }) + } + + fn accept_connection( + &mut self, + stream: UnixStream, + displays: &mut display::DisplayVec, + ) -> anyhow::Result<()> { + let client = self + .display + .handle() + .insert_client(stream, Arc::new(comp::ClientState::default())) + .unwrap(); + + let creds = client.get_credentials(&self.display.handle())?; + let auth_key = get_display_auth_from_pid(creds.pid)?; + + for (idx, cell) in displays.vec.iter().enumerate() { + if let Some(cell) = &cell { + let display = &cell.obj; + if display.auth_key_matches(auth_key.as_str()) { + let display_handle = display::DisplayVec::get_handle(cell, idx); + + self.clients.push(WayVRClient { + client, + display_handle, + pid: creds.pid, + }); + return Ok(()); + } + } + } + + anyhow::bail!("Process auth key is invalid or selected display is non-existent"); + } + + fn accept_connections(&mut self, displays: &mut display::DisplayVec) -> anyhow::Result<()> { + if let Some(stream) = self.listener.accept()? { + if let Err(e) = self.accept_connection(stream, displays) { + log::error!("Failed to accept connection: {}", e); + } + } + + Ok(()) + } + + pub fn tick_wayland(&mut self, displays: &mut display::DisplayVec) -> anyhow::Result<()> { + if let Err(e) = self.accept_connections(displays) { + log::error!("accept_connections failed: {}", e); + } + + self.display.dispatch_clients(&mut self.state)?; + self.display.flush_clients()?; + + Ok(()) + } + + pub fn send_key(&mut self, virtual_key: u32, down: bool) { + let state = if down { + smithay::backend::input::KeyState::Pressed + } else { + smithay::backend::input::KeyState::Released + }; + + self.seat_keyboard.input::<(), _>( + &mut self.state, + Keycode::new(virtual_key), + state, + self.serial_counter.next_serial(), + 0, + |_, _, _| smithay::input::keyboard::FilterResult::Forward, + ); + } +} + +const STARTING_WAYLAND_ADDR_IDX: u32 = 20; + +fn create_wayland_listener() -> anyhow::Result<(super::WaylandEnv, wayland_server::ListeningSocket)> +{ + let mut env = super::WaylandEnv { + display_num: STARTING_WAYLAND_ADDR_IDX, + }; + + let listener = loop { + let display_str = env.display_num_string(); + log::debug!("Trying to open socket \"{}\"", display_str); + match wayland_server::ListeningSocket::bind(display_str.as_str()) { + Ok(listener) => { + log::debug!("Listening to {}", display_str); + break listener; + } + Err(e) => { + log::debug!( + "Failed to open socket \"{}\" (reason: {}), trying next...", + display_str, + e + ); + + env.display_num += 1; + if env.display_num > STARTING_WAYLAND_ADDR_IDX + 20 { + // Highly unlikely for the user to have 20 Wayland displays enabled at once. Return error instead. + anyhow::bail!("Failed to create wayland-server socket") + } + } + } + }; + + Ok((env, listener)) +} diff --git a/src/backend/wayvr/comp.rs b/src/backend/wayvr/comp.rs new file mode 100644 index 00000000..3579b333 --- /dev/null +++ b/src/backend/wayvr/comp.rs @@ -0,0 +1,186 @@ +use smithay::backend::renderer::utils::on_commit_buffer_handler; +use smithay::input::{Seat, SeatHandler, SeatState}; +use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel; +use smithay::reexports::wayland_server::protocol::{wl_buffer, wl_seat, wl_surface}; +use smithay::reexports::wayland_server::{self, Resource}; +use smithay::wayland::buffer::BufferHandler; +use smithay::wayland::shm::{ShmHandler, ShmState}; +use smithay::{ + delegate_compositor, delegate_data_device, delegate_seat, delegate_shm, delegate_xdg_shell, +}; +use std::os::fd::OwnedFd; + +use smithay::utils::Serial; +use smithay::wayland::compositor::{ + self, with_surface_tree_downward, SurfaceAttributes, TraversalAction, +}; + +use smithay::wayland::selection::data_device::{ + ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler, +}; +use smithay::wayland::selection::SelectionHandler; +use smithay::wayland::shell::xdg::{ + PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState, +}; +use wayland_server::backend::{ClientData, ClientId, DisconnectReason}; +use wayland_server::protocol::wl_surface::WlSurface; +use wayland_server::Client; + +use super::event_queue::SyncEventQueue; + +pub struct Application { + pub compositor: compositor::CompositorState, + pub xdg_shell: XdgShellState, + pub seat_state: SeatState, + pub shm: ShmState, + pub data_device: DataDeviceState, + + pub queue_new_toplevel: SyncEventQueue<(ClientId, ToplevelSurface)>, +} + +impl compositor::CompositorHandler for Application { + fn compositor_state(&mut self) -> &mut compositor::CompositorState { + &mut self.compositor + } + + fn client_compositor_state<'a>( + &self, + client: &'a Client, + ) -> &'a compositor::CompositorClientState { + &client.get_data::().unwrap().compositor_state + } + + fn commit(&mut self, surface: &WlSurface) { + on_commit_buffer_handler::(surface); + } +} + +impl SeatHandler for Application { + type KeyboardFocus = WlSurface; + type PointerFocus = WlSurface; + type TouchFocus = WlSurface; + + fn seat_state(&mut self) -> &mut SeatState { + &mut self.seat_state + } + + fn focus_changed(&mut self, _seat: &Seat, _focused: Option<&WlSurface>) {} + fn cursor_image( + &mut self, + _seat: &Seat, + _image: smithay::input::pointer::CursorImageStatus, + ) { + } +} + +impl BufferHandler for Application { + fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {} +} + +impl ClientDndGrabHandler for Application {} + +impl ServerDndGrabHandler for Application { + fn send(&mut self, _mime_type: String, _fd: OwnedFd, _seat: Seat) {} +} + +impl DataDeviceHandler for Application { + fn data_device_state(&self) -> &DataDeviceState { + &self.data_device + } +} + +impl SelectionHandler for Application { + type SelectionUserData = (); +} + +#[derive(Default)] +pub struct ClientState { + compositor_state: compositor::CompositorClientState, +} + +impl ClientData for ClientState { + fn initialized(&self, client_id: ClientId) { + log::debug!("Client ID {:?} connected", client_id); + } + + fn disconnected(&self, client_id: ClientId, reason: DisconnectReason) { + log::debug!( + "Client ID {:?} disconnected. Reason: {:?}", + client_id, + reason + ); + } +} + +impl AsMut for Application { + fn as_mut(&mut self) -> &mut compositor::CompositorState { + &mut self.compositor + } +} + +impl XdgShellHandler for Application { + fn xdg_shell_state(&mut self) -> &mut XdgShellState { + &mut self.xdg_shell + } + + fn new_toplevel(&mut self, surface: ToplevelSurface) { + if let Some(client) = surface.wl_surface().client() { + self.queue_new_toplevel.send((client.id(), surface.clone())); + } + surface.with_pending_state(|state| { + state.states.set(xdg_toplevel::State::Activated); + }); + surface.send_configure(); + } + + fn new_popup(&mut self, _surface: PopupSurface, _positioner: PositionerState) { + // Handle popup creation here + } + + fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) { + // Handle popup grab here + } + + fn reposition_request( + &mut self, + _surface: PopupSurface, + _positioner: PositionerState, + _token: u32, + ) { + // Handle popup reposition here + } +} + +impl ShmHandler for Application { + fn shm_state(&self) -> &ShmState { + &self.shm + } +} + +delegate_xdg_shell!(Application); +delegate_compositor!(Application); +delegate_shm!(Application); +delegate_seat!(Application); +delegate_data_device!(Application); + +pub fn send_frames_surface_tree(surface: &wl_surface::WlSurface, time: u32) { + with_surface_tree_downward( + surface, + (), + |_, _, &()| TraversalAction::DoChildren(()), + |_surf, states, &()| { + // the surface may not have any user_data if it is a subsurface and has not + // yet been commited + for callback in states + .cached_state + .get::() + .current() + .frame_callbacks + .drain(..) + { + callback.done(time); + } + }, + |_, _, &()| true, + ); +} diff --git a/src/backend/wayvr/display.rs b/src/backend/wayvr/display.rs new file mode 100644 index 00000000..b5282a3d --- /dev/null +++ b/src/backend/wayvr/display.rs @@ -0,0 +1,356 @@ +use std::{cell::RefCell, rc::Rc}; + +use smithay::{ + backend::renderer::{ + element::{ + surface::{render_elements_from_surface_tree, WaylandSurfaceRenderElement}, + Kind, + }, + gles::{ffi, GlesRenderer, GlesTexture}, + utils::draw_render_elements, + Bind, Color32F, Frame, Renderer, + }, + input, + utils::{Logical, Point, Rectangle, Size, Transform}, + wayland::shell::xdg::ToplevelSurface, +}; + +use crate::gen_id; + +use super::{ + client::WayVRManager, comp::send_frames_surface_tree, egl_data, smithay_wrapper, window, +}; + +fn generate_auth_key() -> String { + let uuid = uuid::Uuid::new_v4(); + uuid.to_string() +} + +struct Process { + auth_key: String, + child: std::process::Child, +} + +impl Drop for Process { + fn drop(&mut self) { + let _dont_care = self.child.kill(); + } +} + +struct DisplayWindow { + handle: window::WindowHandle, + toplevel: ToplevelSurface, +} + +pub struct Display { + // Display info stuff + pub width: u32, + pub height: u32, + wm: Rc>, + displayed_windows: Vec, + wayland_env: super::WaylandEnv, + + // Render data stuff + gles_texture: GlesTexture, // TODO: drop texture + egl_image: khronos_egl::Image, + egl_data: Rc, + pub dmabuf_data: egl_data::DMAbufData, + + processes: Vec, +} + +impl Drop for Display { + fn drop(&mut self) { + let _ = self + .egl_data + .egl + .destroy_image(self.egl_data.display, self.egl_image); + } +} + +impl Display { + pub fn new( + wm: Rc>, + renderer: &mut GlesRenderer, + egl_data: Rc, + wayland_env: super::WaylandEnv, + width: u32, + height: u32, + ) -> anyhow::Result { + let tex_format = ffi::RGBA; + let internal_format = ffi::RGBA8; + + let tex_id = renderer.with_context(|gl| { + smithay_wrapper::create_framebuffer_texture( + gl, + width, + height, + tex_format, + internal_format, + ) + })?; + + let egl_image = egl_data.create_egl_image(tex_id, width, height)?; + let dmabuf_data = egl_data.create_dmabuf_data(&egl_image)?; + + let opaque = false; + let size = (width as i32, height as i32).into(); + let gles_texture = + unsafe { GlesTexture::from_raw(renderer, Some(tex_format), opaque, tex_id, size) }; + + Ok(Self { + wm, + width, + height, + displayed_windows: Vec::new(), + egl_data, + dmabuf_data, + egl_image, + gles_texture, + wayland_env, + processes: Vec::new(), + }) + } + + pub fn auth_key_matches(&self, auth_key: &str) -> bool { + for process in &self.processes { + if process.auth_key.as_str() == auth_key { + return true; + } + } + false + } + + pub fn add_window(&mut self, window_handle: window::WindowHandle, toplevel: &ToplevelSurface) { + log::debug!("Attaching toplevel surface into display"); + self.displayed_windows.push(DisplayWindow { + handle: window_handle, + toplevel: toplevel.clone(), + }); + self.reposition_windows(); + } + + fn reposition_windows(&mut self) { + let window_count = self.displayed_windows.len(); + + for (i, win) in self.displayed_windows.iter_mut().enumerate() { + if let Some(window) = self.wm.borrow_mut().windows.get_mut(&win.handle) { + let d_cur = i as f32 / window_count as f32; + let d_next = (i + 1) as f32 / window_count as f32; + + let left = (d_cur * self.width as f32) as i32; + let right = (d_next * self.width as f32) as i32; + + window.set_pos(left, 0); + window.set_size((right - left) as u32, self.height); + } + } + } + + pub fn tick_render(&self, renderer: &mut GlesRenderer, time_ms: u64) -> anyhow::Result<()> { + renderer.bind(self.gles_texture.clone())?; + + let size = Size::from((self.width as i32, self.height as i32)); + let damage: Rectangle = + Rectangle::from_loc_and_size((0, 0), size); + + let elements: Vec> = self + .displayed_windows + .iter() + .flat_map(|display_window| { + let wm = self.wm.borrow_mut(); + if let Some(window) = wm.windows.get(&display_window.handle) { + render_elements_from_surface_tree( + renderer, + display_window.toplevel.wl_surface(), + (window.pos_x, window.pos_y), + 1.0, + 1.0, + Kind::Unspecified, + ) + } else { + // Failed to fetch window + vec![] + } + }) + .collect(); + + let mut frame = renderer.render(size, Transform::Normal)?; + + let clear_opacity = if self.displayed_windows.is_empty() { + 0.5 + } else { + 0.0 + }; + + frame.clear(Color32F::new(1.0, 1.0, 1.0, clear_opacity), &[damage])?; + + draw_render_elements(&mut frame, 1.0, &elements, &[damage])?; + + let _sync_point = frame.finish()?; + + for window in &self.displayed_windows { + send_frames_surface_tree(window.toplevel.wl_surface(), time_ms as u32); + } + + Ok(()) + } + + fn get_hovered_window(&self, cursor_x: u32, cursor_y: u32) -> Option { + let wm = self.wm.borrow(); + + for cell in self.displayed_windows.iter() { + if let Some(window) = wm.windows.get(&cell.handle) { + if (cursor_x as i32) >= window.pos_x + && (cursor_x as i32) < window.pos_x + window.size_x as i32 + && (cursor_y as i32) >= window.pos_y + && (cursor_y as i32) < window.pos_y + window.size_y as i32 + { + return Some(cell.handle); + } + } + } + None + } + + pub fn send_mouse_move(&self, manager: &mut WayVRManager, x: u32, y: u32) { + if let Some(window_handle) = self.get_hovered_window(x, y) { + let wm = self.wm.borrow(); + if let Some(window) = wm.windows.get(&window_handle) { + let surf = window.toplevel.wl_surface().clone(); + let point = Point::::from(( + (x as i32 - window.pos_x) as f64, + (y as i32 - window.pos_y) as f64, + )); + + manager.seat_pointer.motion( + &mut manager.state, + Some((surf, Point::from((0.0, 0.0)))), + &input::pointer::MotionEvent { + serial: manager.serial_counter.next_serial(), + time: 0, + location: point, + }, + ); + + manager.seat_pointer.frame(&mut manager.state); + } + } + } + + fn get_mouse_index_number(index: super::MouseIndex) -> u32 { + match index { + super::MouseIndex::Left => 0x110, /* BTN_LEFT */ + super::MouseIndex::Center => 0x112, /* BTN_MIDDLE */ + super::MouseIndex::Right => 0x111, /* BTN_RIGHT */ + } + } + + pub fn send_mouse_down(&self, manager: &mut WayVRManager, index: super::MouseIndex) { + // Change keyboard focus to pressed window + let loc = manager.seat_pointer.current_location(); + + if let Some(window_handle) = + self.get_hovered_window(loc.x.max(0.0) as u32, loc.y.max(0.0) as u32) + { + let wm = self.wm.borrow(); + if let Some(window) = wm.windows.get(&window_handle) { + let surf = window.toplevel.wl_surface().clone(); + + if manager.seat_keyboard.current_focus().is_none() { + manager.seat_keyboard.set_focus( + &mut manager.state, + Some(surf), + manager.serial_counter.next_serial(), + ); + } + } + } + + manager.seat_pointer.button( + &mut manager.state, + &input::pointer::ButtonEvent { + button: Self::get_mouse_index_number(index), + serial: manager.serial_counter.next_serial(), + time: 0, + state: smithay::backend::input::ButtonState::Pressed, + }, + ); + + manager.seat_pointer.frame(&mut manager.state); + } + + pub fn send_mouse_up(&self, manager: &mut WayVRManager, index: super::MouseIndex) { + manager.seat_pointer.button( + &mut manager.state, + &input::pointer::ButtonEvent { + button: Self::get_mouse_index_number(index), + serial: manager.serial_counter.next_serial(), + time: 0, + state: smithay::backend::input::ButtonState::Released, + }, + ); + + manager.seat_pointer.frame(&mut manager.state); + } + + pub fn send_mouse_scroll(&self, manager: &mut WayVRManager, delta: f32) { + manager.seat_pointer.axis( + &mut manager.state, + input::pointer::AxisFrame { + source: None, + relative_direction: ( + smithay::backend::input::AxisRelativeDirection::Identical, + smithay::backend::input::AxisRelativeDirection::Identical, + ), + time: 0, + axis: (0.0, -delta as f64), + v120: Some((0, (delta * -120.0) as i32)), + stop: (false, false), + }, + ); + manager.seat_pointer.frame(&mut manager.state); + } + + fn configure_env(&self, cmd: &mut std::process::Command, auth_key: &str) { + cmd.env_remove("DISPLAY"); // Goodbye X11 + cmd.env("WAYLAND_DISPLAY", self.wayland_env.display_num_string()); + cmd.env("WAYVR_DISPLAY_AUTH", auth_key); + } + + pub fn spawn_process( + &mut self, + exec_path: &str, + args: &[&str], + env: &[(&str, &str)], + ) -> anyhow::Result<()> { + log::info!("Spawning subprocess with exec path \"{}\"", exec_path); + + let auth_key = generate_auth_key(); + + let mut cmd = std::process::Command::new(exec_path); + self.configure_env(&mut cmd, auth_key.as_str()); + cmd.args(args); + + for e in env { + cmd.env(e.0, e.1); + } + + match cmd.spawn() { + Ok(child) => { + self.processes.push(Process { child, auth_key }); + } + Err(e) => { + anyhow::bail!( + "Failed to launch process with path \"{}\": {}. Make sure your exec path exists.", + exec_path, + e + ); + } + } + + Ok(()) + } +} + +gen_id!(DisplayVec, Display, DisplayCell, DisplayHandle); diff --git a/src/backend/wayvr/egl_data.rs b/src/backend/wayvr/egl_data.rs new file mode 100644 index 00000000..34bb3871 --- /dev/null +++ b/src/backend/wayvr/egl_data.rs @@ -0,0 +1,273 @@ +use super::egl_ex; +use anyhow::anyhow; + +pub struct EGLData { + pub egl: khronos_egl::Instance, + pub display: khronos_egl::Display, + pub config: khronos_egl::Config, + pub context: khronos_egl::Context, +} + +#[macro_export] +macro_rules! bind_egl_function { + ($func_type:ident, $func:expr) => { + std::mem::transmute_copy::<_, $func_type>($func).unwrap() + }; +} + +#[derive(Clone)] +pub struct DMAbufModifierInfo { + pub modifiers: Vec, + pub fourcc: u32, +} + +#[derive(Clone)] +pub struct DMAbufData { + pub fd: i32, + pub stride: i32, + pub offset: i32, + pub mod_info: DMAbufModifierInfo, +} + +impl EGLData { + pub fn load_func(&self, func_name: &str) -> anyhow::Result { + let raw_fn = self.egl.get_proc_address(func_name).ok_or(anyhow::anyhow!( + "Required EGL function {} not found", + func_name + ))?; + Ok(raw_fn) + } + + pub fn new() -> anyhow::Result { + unsafe { + let egl = khronos_egl::Instance::new(khronos_egl::Static); + + let display = egl + .get_display(khronos_egl::DEFAULT_DISPLAY) + .ok_or(anyhow!("eglGetDisplay failed"))?; + + let (major, minor) = egl.initialize(display)?; + log::debug!("EGL version: {}.{}", major, minor); + + let attrib_list = [ + khronos_egl::RED_SIZE, + 8, + khronos_egl::GREEN_SIZE, + 8, + khronos_egl::BLUE_SIZE, + 8, + khronos_egl::SURFACE_TYPE, + khronos_egl::WINDOW_BIT, + khronos_egl::RENDERABLE_TYPE, + khronos_egl::OPENGL_BIT, + khronos_egl::NONE, + ]; + + let config = egl + .choose_first_config(display, &attrib_list)? + .ok_or(anyhow!("Failed to get EGL config"))?; + + egl.bind_api(khronos_egl::OPENGL_ES_API)?; + + log::debug!("eglCreateContext"); + + // Require OpenGL ES 3.0 + let context_attrib_list = [ + khronos_egl::CONTEXT_MAJOR_VERSION, + 3, + khronos_egl::CONTEXT_MINOR_VERSION, + 0, + khronos_egl::NONE, + ]; + + let context = egl.create_context(display, config, None, &context_attrib_list)?; + + egl.make_current(display, None, None, Some(context))?; + + Ok(EGLData { + egl, + display, + config, + context, + }) + } + } + + #[allow(dead_code)] + pub fn make_current(&self, surface: &khronos_egl::Surface) -> anyhow::Result<()> { + self.egl.make_current( + self.display, + Some(*surface), + Some(*surface), + Some(self.context), + )?; + + Ok(()) + } + + fn query_dmabuf_mod_info(&self) -> anyhow::Result { + let target_fourcc = 0x34324258; //XB24 + + unsafe { + use egl_ex::PFNEGLQUERYDMABUFFORMATSEXTPROC; + use egl_ex::PFNEGLQUERYDMABUFMODIFIERSEXTPROC; + + let egl_query_dmabuf_formats_ext = bind_egl_function!( + PFNEGLQUERYDMABUFFORMATSEXTPROC, + &self.load_func("eglQueryDmaBufFormatsEXT")? + ); + + // Query format count + let mut num_formats: khronos_egl::Int = 0; + egl_query_dmabuf_formats_ext( + self.display.as_ptr(), + 0, + std::ptr::null_mut(), + &mut num_formats, + ); + + // Retrieve formt list + let mut formats: Vec = vec![0; num_formats as usize]; + egl_query_dmabuf_formats_ext( + self.display.as_ptr(), + num_formats, + formats.as_mut_ptr(), + &mut num_formats, + ); + + /*for (idx, format) in formats.iter().enumerate() { + let bytes = format.to_le_bytes(); + log::trace!( + "idx {}, format {}{}{}{} (hex {:#x})", + idx, + bytes[0] as char, + bytes[1] as char, + bytes[2] as char, + bytes[3] as char, + format + ); + }*/ + + let egl_query_dmabuf_modifiers_ext = bind_egl_function!( + PFNEGLQUERYDMABUFMODIFIERSEXTPROC, + &self.load_func("eglQueryDmaBufModifiersEXT")? + ); + + let mut num_mods: khronos_egl::Int = 0; + + // Query modifier count + egl_query_dmabuf_modifiers_ext( + self.display.as_ptr(), + target_fourcc, + 0, + std::ptr::null_mut(), + std::ptr::null_mut(), + &mut num_mods, + ); + + if num_mods == 0 { + anyhow::bail!("eglQueryDmaBufModifiersEXT modifier count is zero"); + } + + let mut mods: Vec = vec![0; num_mods as usize]; + egl_query_dmabuf_modifiers_ext( + self.display.as_ptr(), + target_fourcc, + num_mods, + mods.as_mut_ptr(), + std::ptr::null_mut(), + &mut num_mods, + ); + + if mods[0] == 0xFFFFFFFFFFFFFFFF { + anyhow::bail!("modifier is -1") + } + + log::trace!("Modifier list:"); + for modifier in &mods { + log::trace!("{:#x}", modifier); + } + + // We should not change these modifier values. Passing all of them to the Vulkan dmabuf + // texture system causes significant graphical corruption due to invalid memory layout and + // tiling on this specific GPU model (very probably others also have the same issue). + // It is not guaranteed that this modifier will be present in other models. + // If not, the full list of modifiers will be passed. Further testing is required. + // For now, it looks like only NAVI32-based gpus have this problem. + let mod_whitelist: [u64; 1] = [0x20000002086bf04 /* AMD RX 7800 XT */]; + + for modifier in &mod_whitelist { + if mods.contains(modifier) { + log::warn!("Using whitelisted dmabuf tiling modifier: {:#x}", modifier); + mods = vec![*modifier, 0x0 /* also important (???) */]; + break; + } + } + + Ok(DMAbufModifierInfo { + modifiers: mods, + fourcc: target_fourcc as u32, + }) + } + } + + pub fn create_dmabuf_data(&self, egl_image: &khronos_egl::Image) -> anyhow::Result { + use egl_ex::PFNEGLEXPORTDMABUFIMAGEMESAPROC as FUNC; + unsafe { + let egl_export_dmabuf_image_mesa = + bind_egl_function!(FUNC, &self.load_func("eglExportDMABUFImageMESA")?); + + let mut fds: [i32; 3] = [0; 3]; + let mut strides: [i32; 3] = [0; 3]; + let mut offsets: [i32; 3] = [0; 3]; + + if egl_export_dmabuf_image_mesa( + self.display.as_ptr(), + egl_image.as_ptr(), + fds.as_mut_ptr(), + strides.as_mut_ptr(), + offsets.as_mut_ptr(), + ) != khronos_egl::TRUE + { + anyhow::bail!("eglExportDMABUFImageMESA failed"); + } + + // many planes in RGB data? + debug_assert!(fds[1] == 0); + debug_assert!(strides[1] == 0); + debug_assert!(offsets[1] == 0); + + let mod_info = self.query_dmabuf_mod_info()?; + + Ok(DMAbufData { + fd: fds[0], + stride: strides[0], + offset: offsets[0], + mod_info, + }) + } + } + + pub fn create_egl_image( + &self, + gl_tex_id: u32, + width: u32, + height: u32, + ) -> anyhow::Result { + unsafe { + Ok(self.egl.create_image( + self.display, + self.context, + khronos_egl::GL_TEXTURE_2D as std::ffi::c_uint, + khronos_egl::ClientBuffer::from_ptr(gl_tex_id as *mut std::ffi::c_void), + &[ + khronos_egl::WIDTH as usize, + width as usize, + khronos_egl::HEIGHT as usize, + height as usize, + khronos_egl::ATTRIB_NONE, + ], + )?) + } + } +} diff --git a/src/backend/wayvr/egl_ex.rs b/src/backend/wayvr/egl_ex.rs new file mode 100644 index 00000000..1f96a9ad --- /dev/null +++ b/src/backend/wayvr/egl_ex.rs @@ -0,0 +1,32 @@ +//eglExportDMABUFImageMESA +pub type PFNEGLEXPORTDMABUFIMAGEMESAPROC = Option< + unsafe extern "C" fn( + dpy: khronos_egl::EGLDisplay, + image: khronos_egl::EGLImage, + fds: *mut i32, + strides: *mut khronos_egl::Int, + offsets: *mut khronos_egl::Int, + ) -> khronos_egl::Boolean, +>; + +//eglQueryDmaBufModifiersEXT +pub type PFNEGLQUERYDMABUFMODIFIERSEXTPROC = Option< + unsafe extern "C" fn( + dpy: khronos_egl::EGLDisplay, + format: khronos_egl::Int, + max_modifiers: khronos_egl::Int, + modifiers: *mut u64, + external_only: *mut khronos_egl::Boolean, + num_modifiers: *mut khronos_egl::Int, + ) -> khronos_egl::Boolean, +>; + +//eglQueryDmaBufFormatsEXT +pub type PFNEGLQUERYDMABUFFORMATSEXTPROC = Option< + unsafe extern "C" fn( + dpy: khronos_egl::EGLDisplay, + max_formats: khronos_egl::Int, + formats: *mut khronos_egl::Int, + num_formats: *mut khronos_egl::Int, + ) -> khronos_egl::Boolean, +>; diff --git a/src/backend/wayvr/event_queue.rs b/src/backend/wayvr/event_queue.rs new file mode 100644 index 00000000..40a940bb --- /dev/null +++ b/src/backend/wayvr/event_queue.rs @@ -0,0 +1,32 @@ +#![allow(dead_code)] + +use std::{cell::RefCell, collections::VecDeque, rc::Rc}; + +struct Data { + queue: VecDeque, +} + +#[derive(Clone)] +pub struct SyncEventQueue { + data: Rc>>, +} + +impl SyncEventQueue { + pub fn new() -> Self { + Self { + data: Rc::new(RefCell::new(Data { + queue: Default::default(), + })), + } + } + + pub fn send(&self, message: DataType) { + let mut data = self.data.borrow_mut(); + data.queue.push_back(message); + } + + pub fn read(&self) -> Option { + let mut data = self.data.borrow_mut(); + data.queue.pop_front() + } +} diff --git a/src/backend/wayvr/handle.rs b/src/backend/wayvr/handle.rs new file mode 100644 index 00000000..e690836a --- /dev/null +++ b/src/backend/wayvr/handle.rs @@ -0,0 +1,157 @@ +#[macro_export] +macro_rules! gen_id { + ( + $container_name:ident, + $instance_name:ident, + $cell_name:ident, + $handle_name:ident) => { + //ThingCell + pub struct $cell_name { + pub obj: $instance_name, + generation: u64, + } + + //ThingVec + pub struct $container_name { + // Vec> + pub vec: Vec>, + + cur_generation: u64, + } + + //ThingHandle + #[derive(Default, Clone, Copy, PartialEq)] + pub struct $handle_name { + idx: u32, + generation: u64, + } + + #[allow(dead_code)] + impl $handle_name { + pub fn reset(&mut self) { + self.generation = 0; + } + + pub fn is_set(&self) -> bool { + self.generation > 0 + } + + pub fn id(&self) -> u32 { + self.idx + } + } + + //ThingVec + #[allow(dead_code)] + impl $container_name { + pub fn new() -> Self { + Self { + vec: Vec::new(), + cur_generation: 0, + } + } + + pub fn iter(&self, callback: &mut dyn FnMut($handle_name, &$instance_name)) { + for (idx, opt_cell) in self.vec.iter().enumerate() { + if let Some(cell) = opt_cell { + let handle = $container_name::get_handle(&cell, idx); + callback(handle, &cell.obj); + } + } + } + + pub fn get_handle(cell: &$cell_name, idx: usize) -> $handle_name { + $handle_name { + idx: idx as u32, + generation: cell.generation, + } + } + + fn find_unused_idx(&mut self) -> Option { + for (num, obj) in self.vec.iter().enumerate() { + if obj.is_none() { + return Some(num as u32); + } + } + None + } + + pub fn add(&mut self, obj: $instance_name) -> $handle_name { + self.cur_generation += 1; + let generation = self.cur_generation; + + let unused_idx = self.find_unused_idx(); + + let idx = if let Some(idx) = unused_idx { + idx + } else { + self.vec.len() as u32 + }; + + let handle = $handle_name { idx, generation }; + + let cell = $cell_name { obj, generation }; + + if let Some(idx) = unused_idx { + self.vec[idx as usize] = Some(cell); + } else { + self.vec.push(Some(cell)) + } + + handle + } + + pub fn remove(&mut self, handle: &$handle_name) { + // Out of bounds, ignore + if handle.idx as usize >= self.vec.len() { + return; + } + + // Remove only if the generation matches + if let Some(cell) = &self.vec[handle.idx as usize] { + if cell.generation == handle.generation { + self.vec[handle.idx as usize] = None; + } + } + } + + pub fn get(&self, handle: &$handle_name) -> Option<&$instance_name> { + // Out of bounds, ignore + if handle.idx as usize >= self.vec.len() { + return None; + } + + if let Some(cell) = &self.vec[handle.idx as usize] { + if cell.generation == handle.generation { + return Some(&cell.obj); + } + } + + None + } + + pub fn get_mut(&mut self, handle: &$handle_name) -> Option<&mut $instance_name> { + // Out of bounds, ignore + if handle.idx as usize >= self.vec.len() { + return None; + } + + if let Some(cell) = &mut self.vec[handle.idx as usize] { + if cell.generation == handle.generation { + return Some(&mut cell.obj); + } + } + + None + } + } + }; +} + +/* Example usage: +gen_id!(ThingVec, ThingInstance, ThingCell, ThingHandle); + +struct ThingInstance {} + +impl ThingInstance {} + */ diff --git a/src/backend/wayvr/mod.rs b/src/backend/wayvr/mod.rs new file mode 100644 index 00000000..7262ce39 --- /dev/null +++ b/src/backend/wayvr/mod.rs @@ -0,0 +1,213 @@ +mod client; +mod comp; +pub mod display; +pub mod egl_data; +mod egl_ex; +mod event_queue; +mod handle; +mod smithay_wrapper; +mod time; +mod window; + +use std::{cell::RefCell, rc::Rc}; + +use comp::Application; +use display::DisplayVec; +use event_queue::SyncEventQueue; +use smithay::{ + backend::renderer::gles::GlesRenderer, + input::SeatState, + reexports::wayland_server::{self, backend::ClientId}, + wayland::{ + compositor, + selection::data_device::DataDeviceState, + shell::xdg::{ToplevelSurface, XdgShellState}, + shm::ShmState, + }, +}; +use time::get_millis; + +#[derive(Clone)] +pub struct WaylandEnv { + pub display_num: u32, +} + +impl WaylandEnv { + pub fn display_num_string(&self) -> String { + // e.g. "wayland-20" + format!("wayland-{}", self.display_num) + } +} + +#[allow(dead_code)] +pub struct WayVR { + time_start: u64, + gles_renderer: GlesRenderer, + displays: display::DisplayVec, + manager: client::WayVRManager, + wm: Rc>, + egl_data: Rc, + + queue_new_toplevel: SyncEventQueue<(ClientId, ToplevelSurface)>, +} + +pub enum MouseIndex { + Left, + Center, + Right, +} + +impl WayVR { + pub fn new() -> anyhow::Result { + let display: wayland_server::Display = wayland_server::Display::new()?; + let dh = display.handle(); + let compositor = compositor::CompositorState::new::(&dh); + let xdg_shell = XdgShellState::new::(&dh); + let mut seat_state = SeatState::new(); + let shm = ShmState::new::(&dh, Vec::new()); + let data_device = DataDeviceState::new::(&dh); + let mut seat = seat_state.new_wl_seat(&dh, "wayvr"); + + // TODO: Keyboard repeat delay and rate? + let seat_keyboard = seat.add_keyboard(Default::default(), 100, 100)?; + let seat_pointer = seat.add_pointer(); + + let queue_new_toplevel = SyncEventQueue::new(); + + let state = Application { + compositor, + xdg_shell, + seat_state, + shm, + data_device, + queue_new_toplevel: queue_new_toplevel.clone(), + }; + + let time_start = get_millis(); + let egl_data = egl_data::EGLData::new()?; + let smithay_display = smithay_wrapper::get_egl_display(&egl_data)?; + let smithay_context = smithay_wrapper::get_egl_context(&egl_data, &smithay_display)?; + let gles_renderer = unsafe { GlesRenderer::new(smithay_context)? }; + + Ok(Self { + gles_renderer, + time_start, + manager: client::WayVRManager::new(state, display, seat_keyboard, seat_pointer)?, + displays: DisplayVec::new(), + egl_data: Rc::new(egl_data), + wm: Rc::new(RefCell::new(window::WindowManager::new())), + queue_new_toplevel, + }) + } + + pub fn tick_display(&mut self, display: display::DisplayHandle) -> anyhow::Result<()> { + // millis since the start of wayvr + let time_ms = get_millis() - self.time_start; + + let display = self + .displays + .get(&display) + .ok_or(anyhow::anyhow!("Invalid display handle"))?; + + display.tick_render(&mut self.gles_renderer, time_ms)?; + + Ok(()) + } + + pub fn tick_events(&mut self) -> anyhow::Result<()> { + // Attach newly created toplevel surfaces to displayes + while let Some((client_id, toplevel)) = self.queue_new_toplevel.read() { + for client in &self.manager.clients { + if client.client.id() == client_id { + let window_handle = self.wm.borrow_mut().create_window(&toplevel); + + if let Some(display) = self.displays.get_mut(&client.display_handle) { + display.add_window(window_handle, &toplevel); + } else { + // This shouldn't happen, scream if it does + log::error!("Could not attach window handle into display"); + } + + break; + } + } + } + + self.manager.tick_wayland(&mut self.displays) + } + + pub fn tick_finish(&mut self) -> anyhow::Result<()> { + self.gles_renderer.with_context(|gl| unsafe { + gl.Flush(); + gl.Finish(); + })?; + Ok(()) + } + + pub fn send_mouse_move(&mut self, display: display::DisplayHandle, x: u32, y: u32) { + if let Some(display) = self.displays.get(&display) { + display.send_mouse_move(&mut self.manager, x, y); + } + } + + pub fn send_mouse_down(&mut self, display: display::DisplayHandle, index: MouseIndex) { + if let Some(display) = self.displays.get(&display) { + display.send_mouse_down(&mut self.manager, index); + } + } + + pub fn send_mouse_up(&mut self, display: display::DisplayHandle, index: MouseIndex) { + if let Some(display) = self.displays.get(&display) { + display.send_mouse_up(&mut self.manager, index); + } + } + + pub fn send_mouse_scroll(&mut self, display: display::DisplayHandle, delta: f32) { + if let Some(display) = self.displays.get(&display) { + display.send_mouse_scroll(&mut self.manager, delta); + } + } + + pub fn send_key(&mut self, virtual_key: u32, down: bool) { + self.manager.send_key(virtual_key, down); + } + + pub fn get_dmabuf_data(&self, display: display::DisplayHandle) -> Option { + self.displays + .get(&display) + .map(|display| display.dmabuf_data.clone()) + } + + pub fn create_display( + &mut self, + width: u32, + height: u32, + ) -> anyhow::Result { + let display = display::Display::new( + self.wm.clone(), + &mut self.gles_renderer, + self.egl_data.clone(), + self.manager.wayland_env.clone(), + width, + height, + )?; + Ok(self.displays.add(display)) + } + + pub fn destroy_display(&mut self, handle: display::DisplayHandle) { + self.displays.remove(&handle); + } + + pub fn spawn_process( + &mut self, + display: display::DisplayHandle, + exec_path: &str, + args: &[&str], + env: &[(&str, &str)], + ) -> anyhow::Result<()> { + if let Some(display) = self.displays.get_mut(&display) { + display.spawn_process(exec_path, args, env)? + } + Ok(()) + } +} diff --git a/src/backend/wayvr/smithay_wrapper.rs b/src/backend/wayvr/smithay_wrapper.rs new file mode 100644 index 00000000..f135d1aa --- /dev/null +++ b/src/backend/wayvr/smithay_wrapper.rs @@ -0,0 +1,54 @@ +use super::egl_data; +use smithay::backend::{egl as smithay_egl, renderer::gles::ffi}; + +pub fn get_egl_display(data: &egl_data::EGLData) -> anyhow::Result { + Ok(unsafe { smithay_egl::EGLDisplay::from_raw(data.display.as_ptr(), data.config.as_ptr())? }) +} + +pub fn get_egl_context( + data: &egl_data::EGLData, + display: &smithay_egl::EGLDisplay, +) -> anyhow::Result { + let display_ptr = display.get_display_handle().handle; + debug_assert!(display_ptr == data.display.as_ptr()); + let config_ptr = data.config.as_ptr(); + let context_ptr = data.context.as_ptr(); + Ok(unsafe { smithay_egl::EGLContext::from_raw(display_ptr, config_ptr, context_ptr)? }) +} + +pub fn create_framebuffer_texture( + gl: &ffi::Gles2, + width: u32, + height: u32, + tex_format: u32, + internal_format: u32, +) -> u32 { + unsafe { + let mut tex = 0; + gl.GenTextures(1, &mut tex); + gl.BindTexture(ffi::TEXTURE_2D, tex); + gl.TexParameteri( + ffi::TEXTURE_2D, + ffi::TEXTURE_MIN_FILTER, + ffi::NEAREST as i32, + ); + gl.TexParameteri( + ffi::TEXTURE_2D, + ffi::TEXTURE_MAG_FILTER, + ffi::NEAREST as i32, + ); + gl.TexImage2D( + ffi::TEXTURE_2D, + 0, + internal_format as i32, + width as i32, + height as i32, + 0, + tex_format, + ffi::UNSIGNED_BYTE, + std::ptr::null(), + ); + gl.BindTexture(ffi::TEXTURE_2D, 0); + tex + } +} diff --git a/src/backend/wayvr/time.rs b/src/backend/wayvr/time.rs new file mode 100644 index 00000000..77d7a213 --- /dev/null +++ b/src/backend/wayvr/time.rs @@ -0,0 +1,9 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +// Returns milliseconds since unix epoch +pub fn get_millis() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as u64 +} diff --git a/src/backend/wayvr/window.rs b/src/backend/wayvr/window.rs new file mode 100644 index 00000000..d49a4a04 --- /dev/null +++ b/src/backend/wayvr/window.rs @@ -0,0 +1,69 @@ +use smithay::wayland::shell::xdg::ToplevelSurface; + +use crate::gen_id; + +pub struct Window { + pub pos_x: i32, + pub pos_y: i32, + pub size_x: u32, + pub size_y: u32, + pub toplevel: ToplevelSurface, +} + +impl Window { + pub fn new(toplevel: &ToplevelSurface) -> Self { + Self { + pos_x: 0, + pos_y: 0, + size_x: 0, + size_y: 0, + toplevel: toplevel.clone(), + } + } + + pub fn set_pos(&mut self, pos_x: i32, pos_y: i32) { + self.pos_x = pos_x; + self.pos_y = pos_y; + } + + pub fn set_size(&mut self, size_x: u32, size_y: u32) { + self.toplevel.with_pending_state(|state| { + //state.bounds = Some((size_x as i32, size_y as i32).into()); + state.size = Some((size_x as i32, size_y as i32).into()); + }); + self.toplevel.send_configure(); + + self.size_x = size_x; + self.size_y = size_y; + } +} + +pub struct WindowManager { + pub windows: WindowVec, +} + +impl WindowManager { + pub fn new() -> Self { + Self { + windows: WindowVec::new(), + } + } + + pub fn find_window_handle(&self, toplevel: &ToplevelSurface) -> Option { + for (idx, cell) in self.windows.vec.iter().enumerate() { + if let Some(cell) = cell { + let window = &cell.obj; + if window.toplevel == *toplevel { + return Some(WindowVec::get_handle(cell, idx)); + } + } + } + None + } + + pub fn create_window(&mut self, toplevel: &ToplevelSurface) -> WindowHandle { + self.windows.add(Window::new(toplevel)) + } +} + +gen_id!(WindowVec, Window, WindowCell, WindowHandle); diff --git a/src/graphics/mod.rs b/src/graphics/mod.rs index 14dba5f9..57be39e2 100644 --- a/src/graphics/mod.rs +++ b/src/graphics/mod.rs @@ -553,7 +553,9 @@ impl WlxGraphics { Arc, Arc, )> { - use vulkano::swapchain::Surface; + use vulkano::{ + device::physical::PhysicalDeviceType, instance::InstanceCreateFlags, swapchain::Surface, + }; use winit::{event_loop::EventLoop, window::Window}; let event_loop = EventLoop::new().unwrap(); // want panic @@ -795,30 +797,17 @@ impl WlxGraphics { )?) } - pub fn dmabuf_texture(&self, frame: DmabufFrame) -> anyhow::Result> { + pub fn dmabuf_texture_ex( + &self, + frame: DmabufFrame, + tiling: ImageTiling, + layouts: Vec, + modifiers: Vec, + ) -> anyhow::Result> { let extent = [frame.format.width, frame.format.height, 1]; let format = fourcc_to_vk(frame.format.fourcc)?; - let mut tiling: ImageTiling = ImageTiling::Optimal; - let mut modifiers: Vec = vec![]; - let mut layouts: Vec = vec![]; - - if frame.format.modifier != DRM_FORMAT_MOD_INVALID { - (0..frame.num_planes).for_each(|i| { - let plane = &frame.planes[i]; - layouts.push(SubresourceLayout { - offset: plane.offset as _, - size: 0, - row_pitch: plane.stride as _, - array_pitch: None, - depth_pitch: None, - }); - modifiers.push(frame.format.modifier); - }); - tiling = ImageTiling::DrmFormatModifier; - }; - let image = unsafe { RawImage::new_unchecked( self.device.clone(), @@ -885,6 +874,29 @@ impl WlxGraphics { } } + pub fn dmabuf_texture(&self, frame: DmabufFrame) -> anyhow::Result> { + let mut modifiers: Vec = vec![]; + let mut tiling: ImageTiling = ImageTiling::Optimal; + let mut layouts: Vec = vec![]; + + if frame.format.modifier != DRM_FORMAT_MOD_INVALID { + (0..frame.num_planes).for_each(|i| { + let plane = &frame.planes[i]; + layouts.push(SubresourceLayout { + offset: plane.offset as _, + size: 0, + row_pitch: plane.stride as _, + array_pitch: None, + depth_pitch: None, + }); + modifiers.push(frame.format.modifier); + }); + tiling = ImageTiling::DrmFormatModifier; + }; + + self.dmabuf_texture_ex(frame, tiling, layouts, modifiers) + } + pub fn render_texture( &self, width: u32, diff --git a/src/gui/canvas/builder.rs b/src/gui/canvas/builder.rs index 8ac0af2d..e349c0cd 100644 --- a/src/gui/canvas/builder.rs +++ b/src/gui/canvas/builder.rs @@ -111,6 +111,7 @@ impl CanvasBuilder { } // Creates a sprite that highlights on pointer hover. Will not draw anything until set_sprite is called. + #[allow(dead_code)] pub fn sprite_interactive(&mut self, x: f32, y: f32, w: f32, h: f32) -> &mut Control { let idx = self.canvas.controls.len(); self.canvas.controls.push(Control { diff --git a/src/gui/canvas/control.rs b/src/gui/canvas/control.rs index d7b56b90..868492d0 100644 --- a/src/gui/canvas/control.rs +++ b/src/gui/canvas/control.rs @@ -336,6 +336,7 @@ impl Control { Ok(()) } + #[allow(dead_code)] pub(super) fn render_sprite_hl( &self, canvas: &CanvasData, diff --git a/src/overlays/keyboard.rs b/src/overlays/keyboard.rs index 93bd299b..33108f9f 100644 --- a/src/overlays/keyboard.rs +++ b/src/overlays/keyboard.rs @@ -18,7 +18,7 @@ use crate::{ get_key_type, KeyModifier, KeyType, VirtualKey, XkbKeymap, ALT, CTRL, KEYS_TO_MODS, META, NUM_LOCK, SHIFT, SUPER, }, - state::AppState, + state::{AppState, KeyboardFocus}, }; use glam::{vec2, vec3a, Affine2, Vec4}; use once_cell::sync::Lazy; @@ -31,6 +31,36 @@ const AUTO_RELEASE_MODS: [KeyModifier; 5] = [SHIFT, CTRL, ALT, SUPER, META]; pub const KEYBOARD_NAME: &str = "kbd"; +fn send_key(app: &mut AppState, key: VirtualKey, down: bool) { + log::info!( + "Sending key {:?} to {:?} (down: {})", + key, + app.keyboard_focus, + down + ); + match app.keyboard_focus { + KeyboardFocus::PhysicalScreen => { + app.hid_provider.send_key(key, down); + } + KeyboardFocus::WayVR => + { + #[cfg(feature = "wayvr")] + if let Some(wayvr) = &app.wayvr { + wayvr.borrow_mut().send_key(key as u32, down); + } + } + } +} + +fn set_modifiers(app: &mut AppState, mods: u8) { + match app.keyboard_focus { + KeyboardFocus::PhysicalScreen => { + app.hid_provider.set_modifiers(mods); + } + KeyboardFocus::WayVR => {} + } +} + pub fn create_keyboard( app: &AppState, keymap: Option, @@ -197,22 +227,22 @@ fn key_press( if let PointerMode::Right = mode { data.modifiers |= SHIFT; - app.hid_provider.set_modifiers(data.modifiers); + set_modifiers(app, data.modifiers); } - app.hid_provider.send_key(*vk, true); + send_key(app, *vk, true); *pressed = true; } Some(KeyButtonData::Modifier { modifier, sticky }) => { *sticky = data.modifiers & *modifier == 0; data.modifiers |= *modifier; data.key_click(app); - app.hid_provider.set_modifiers(data.modifiers); + set_modifiers(app, data.modifiers); } Some(KeyButtonData::Macro { verbs }) => { data.key_click(app); for (vk, press) in verbs { - app.hid_provider.send_key(*vk, *press); + send_key(app, *vk, *press); } } Some(KeyButtonData::Exec { program, args, .. }) => { @@ -236,20 +266,20 @@ fn key_release( ) { match control.state.as_mut() { Some(KeyButtonData::Key { vk, pressed }) => { - app.hid_provider.send_key(*vk, false); + send_key(app, *vk, false); *pressed = false; for m in AUTO_RELEASE_MODS.iter() { if data.modifiers & *m != 0 { data.modifiers &= !*m; - app.hid_provider.set_modifiers(data.modifiers); + set_modifiers(app, data.modifiers); } } } Some(KeyButtonData::Modifier { modifier, sticky }) => { if !*sticky { data.modifiers &= !*modifier; - app.hid_provider.set_modifiers(data.modifiers); + set_modifiers(app, data.modifiers); } } Some(KeyButtonData::Exec { @@ -493,7 +523,7 @@ impl OverlayRenderer for KeyboardBackend { } fn pause(&mut self, app: &mut AppState) -> anyhow::Result<()> { self.canvas.data_mut().modifiers = 0; - app.hid_provider.set_modifiers(0); + set_modifiers(app, 0); self.canvas.pause(app) } fn resume(&mut self, app: &mut AppState) -> anyhow::Result<()> { diff --git a/src/overlays/mod.rs b/src/overlays/mod.rs index 7ddf5243..498cc19f 100644 --- a/src/overlays/mod.rs +++ b/src/overlays/mod.rs @@ -6,3 +6,6 @@ pub mod mirror; pub mod screen; pub mod toast; pub mod watch; + +#[cfg(feature = "wayvr")] +pub mod wayvr; diff --git a/src/overlays/screen.rs b/src/overlays/screen.rs index b6f48ffd..f7a70b5f 100644 --- a/src/overlays/screen.rs +++ b/src/overlays/screen.rs @@ -58,7 +58,7 @@ use crate::{ fourcc_to_vk, WlxCommandBuffer, WlxPipeline, WlxPipelineLegacy, DRM_FORMAT_MOD_INVALID, }, hid::{MOUSE_LEFT, MOUSE_MIDDLE, MOUSE_RIGHT}, - state::{AppSession, AppState, ScreenMeta}, + state::{AppSession, AppState, KeyboardFocus, ScreenMeta}, }; #[cfg(feature = "wayland")] @@ -712,6 +712,7 @@ fn create_screen_state( OverlayState { name: name.clone(), + keyboard_focus: Some(KeyboardFocus::PhysicalScreen), grabbable: true, recenter: true, anchored: true, diff --git a/src/overlays/wayvr.rs b/src/overlays/wayvr.rs new file mode 100644 index 00000000..0b616d92 --- /dev/null +++ b/src/overlays/wayvr.rs @@ -0,0 +1,272 @@ +use glam::{vec3a, Affine2}; +use std::{cell::RefCell, rc::Rc, sync::Arc}; +use vulkano::image::SubresourceLayout; +use wlx_capture::frame::{DmabufFrame, FourCC, FrameFormat, FramePlane}; + +use crate::{ + backend::{ + input::{self, InteractionHandler}, + overlay::{ui_transform, OverlayData, OverlayRenderer, OverlayState, SplitOverlayBackend}, + wayvr, + }, + graphics::WlxGraphics, + state::{self, KeyboardFocus}, +}; + +pub struct WayVRContext { + wayvr: Rc>, + display: wayvr::display::DisplayHandle, + width: u32, + height: u32, +} + +#[derive(Default)] +pub struct WayVRProcess<'a> { + pub exec_path: &'a str, + pub args: &'a [&'a str], + pub env: &'a [(&'a str, &'a str)], +} + +impl WayVRContext { + pub fn new( + wvr: Rc>, + width: u32, + height: u32, + processes: &[WayVRProcess], + ) -> anyhow::Result { + let mut wayvr = wvr.borrow_mut(); + + let display = wayvr.create_display(width, height)?; + + for process in processes { + wayvr.spawn_process(display, process.exec_path, process.args, process.env)?; + } + + Ok(Self { + wayvr: wvr.clone(), + display, + width, + height, + }) + } +} + +pub struct WayVRInteractionHandler { + context: Rc>, + mouse_transform: Affine2, +} + +impl WayVRInteractionHandler { + pub fn new(context: Rc>, mouse_transform: Affine2) -> Self { + Self { + context, + mouse_transform, + } + } +} + +impl InteractionHandler for WayVRInteractionHandler { + fn on_hover( + &mut self, + _app: &mut state::AppState, + hit: &input::PointerHit, + ) -> Option { + let ctx = self.context.borrow(); + + let pos = self.mouse_transform.transform_point2(hit.uv); + let x = ((pos.x * ctx.width as f32) as i32).max(0); + let y = ((pos.y * ctx.height as f32) as i32).max(0); + + let ctx = self.context.borrow(); + ctx.wayvr + .borrow_mut() + .send_mouse_move(ctx.display, x as u32, y as u32); + + None + } + + fn on_left(&mut self, _app: &mut state::AppState, _pointer: usize) { + // Ignore event + } + + fn on_pointer(&mut self, _app: &mut state::AppState, hit: &input::PointerHit, pressed: bool) { + if let Some(index) = match hit.mode { + input::PointerMode::Left => Some(wayvr::MouseIndex::Left), + input::PointerMode::Middle => Some(wayvr::MouseIndex::Center), + input::PointerMode::Right => Some(wayvr::MouseIndex::Right), + _ => { + // Unknown pointer event, ignore + None + } + } { + let ctx = self.context.borrow(); + let mut wayvr = ctx.wayvr.borrow_mut(); + if pressed { + wayvr.send_mouse_down(ctx.display, index); + } else { + wayvr.send_mouse_up(ctx.display, index); + } + } + } + + fn on_scroll(&mut self, _app: &mut state::AppState, _hit: &input::PointerHit, delta: f32) { + let ctx = self.context.borrow(); + ctx.wayvr.borrow_mut().send_mouse_scroll(ctx.display, delta); + } +} + +pub struct WayVRRenderer { + dmabuf_image: Option>, + view: Option>, + context: Rc>, + graphics: Arc, + width: u32, + height: u32, +} + +impl WayVRRenderer { + pub fn new( + app: &mut state::AppState, + wvr: Rc>, + width: u32, + height: u32, + processes: &[WayVRProcess], + ) -> anyhow::Result { + Ok(Self { + context: Rc::new(RefCell::new(WayVRContext::new( + wvr, width, height, processes, + )?)), + width, + height, + dmabuf_image: None, + view: None, + graphics: app.graphics.clone(), + }) + } +} + +impl WayVRRenderer { + fn ensure_dmabuf(&mut self, data: wayvr::egl_data::DMAbufData) -> anyhow::Result<()> { + if self.dmabuf_image.is_none() { + // First init + let mut planes = [FramePlane::default(); 4]; + planes[0].fd = Some(data.fd); + planes[0].offset = data.offset as u32; + planes[0].stride = data.stride; + + let frame = DmabufFrame { + format: FrameFormat { + width: self.width, + height: self.height, + fourcc: FourCC { + value: data.mod_info.fourcc, + }, + modifier: data.mod_info.modifiers[0], /* possibly not proper? */ + }, + num_planes: 1, + planes, + }; + + let layouts: Vec = vec![SubresourceLayout { + offset: data.offset as _, + size: 0, + row_pitch: data.stride as _, + array_pitch: None, + depth_pitch: None, + }]; + + let tex = self.graphics.dmabuf_texture_ex( + frame, + vulkano::image::ImageTiling::DrmFormatModifier, + layouts, + data.mod_info.modifiers, + )?; + self.dmabuf_image = Some(tex.clone()); + self.view = Some(vulkano::image::view::ImageView::new_default(tex).unwrap()); + } + + Ok(()) + } +} + +impl OverlayRenderer for WayVRRenderer { + fn init(&mut self, _app: &mut state::AppState) -> anyhow::Result<()> { + Ok(()) + } + + fn pause(&mut self, _app: &mut state::AppState) -> anyhow::Result<()> { + Ok(()) + } + + fn resume(&mut self, _app: &mut state::AppState) -> anyhow::Result<()> { + Ok(()) + } + + fn render(&mut self, _app: &mut state::AppState) -> anyhow::Result<()> { + let ctx = self.context.borrow(); + let mut wayvr = ctx.wayvr.borrow_mut(); + + wayvr.tick_display(ctx.display)?; + + let dmabuf_data = wayvr + .get_dmabuf_data(ctx.display) + .ok_or(anyhow::anyhow!("Failed to fetch dmabuf data"))? + .clone(); + + drop(wayvr); + drop(ctx); + self.ensure_dmabuf(dmabuf_data.clone())?; + + Ok(()) + } + + fn view(&mut self) -> Option> { + self.view.clone() + } + + fn extent(&mut self) -> Option<[u32; 3]> { + self.view.as_ref().map(|view| view.image().extent()) + } +} + +#[allow(dead_code)] +pub fn create_wayvr( + app: &mut state::AppState, + width: u32, + height: u32, + processes: &[WayVRProcess], +) -> anyhow::Result> +where + O: Default, +{ + let transform = ui_transform(&[width, height]); + + let state = OverlayState { + name: format!("WayVR Screen ({}x{})", width, height).into(), + keyboard_focus: Some(KeyboardFocus::WayVR), + want_visible: true, + interactable: true, + recenter: true, + grabbable: true, + spawn_scale: 1.0, + spawn_point: vec3a(0.0, -0.5, 0.0), + interaction_transform: transform, + ..Default::default() + }; + + let wayvr = app.get_wayvr()?; + + let renderer = WayVRRenderer::new(app, wayvr, width, height, processes)?; + let context = renderer.context.clone(); + + let backend = Box::new(SplitOverlayBackend { + renderer: Box::new(renderer), + interaction: Box::new(WayVRInteractionHandler::new(context, Affine2::IDENTITY)), + }); + + Ok(OverlayData { + state, + backend, + ..Default::default() + }) +} diff --git a/src/state.rs b/src/state.rs index 92da9af9..0c0d43a3 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,13 +1,18 @@ -use std::{io::Cursor, sync::Arc}; - use anyhow::bail; use glam::Affine3A; use idmap::IdMap; use rodio::{Decoder, OutputStream, OutputStreamHandle, Source}; use serde::{Deserialize, Serialize}; use smallvec::{smallvec, SmallVec}; +use std::{io::Cursor, sync::Arc}; use vulkano::image::view::ImageView; +#[cfg(feature = "wayvr")] +use std::{cell::RefCell, rc::Rc}; + +#[cfg(feature = "wayvr")] +use crate::backend::wayvr::WayVR; + use crate::{ backend::{input::InputState, overlay::OverlayID, task::TaskContainer}, config::{AStrMap, GeneralConfig}, @@ -22,6 +27,14 @@ use crate::{ }, }; +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum KeyboardFocus { + PhysicalScreen, + + #[allow(dead_code)] // Not available if "wayvr" feature is disabled + WayVR, // (for now without wayland window id data, it's handled internally), +} + pub struct AppState { pub fc: FontCache, pub session: AppSession, @@ -33,6 +46,10 @@ pub struct AppState { pub screens: SmallVec<[ScreenMeta; 8]>, pub anchor: Affine3A, pub sprites: AStrMap>, + pub keyboard_focus: KeyboardFocus, + + #[cfg(feature = "wayvr")] + pub wayvr: Option>>, // Dynamically created if requested } impl AppState { @@ -84,8 +101,25 @@ impl AppState { screens: smallvec![], anchor: Affine3A::IDENTITY, sprites: AStrMap::new(), + keyboard_focus: KeyboardFocus::PhysicalScreen, + + #[cfg(feature = "wayvr")] + wayvr: None, }) } + + #[cfg(feature = "wayvr")] + #[allow(dead_code)] + pub fn get_wayvr(&mut self) -> anyhow::Result>> { + if let Some(wvr) = &self.wayvr { + Ok(wvr.clone()) + } else { + log::info!("Initializing WayVR"); + let wayvr = Rc::new(RefCell::new(WayVR::new()?)); + self.wayvr = Some(wayvr.clone()); + Ok(wayvr) + } + } } pub struct AppSession {