├── wayland-display-core ├── src │ ├── wayland │ │ ├── protocols │ │ │ ├── mod.rs │ │ │ └── wl_drm.rs │ │ ├── mod.rs │ │ └── handlers │ │ │ ├── output.rs │ │ │ ├── viewporter.rs │ │ │ ├── presentation.rs │ │ │ ├── relative_pointer.rs │ │ │ ├── mod.rs │ │ │ ├── shm.rs │ │ │ ├── data_device.rs │ │ │ ├── dmabuf.rs │ │ │ ├── wl_drm.rs │ │ │ ├── seat.rs │ │ │ ├── pointer_constraints.rs │ │ │ ├── xdg.rs │ │ │ └── compositor.rs │ ├── tests │ │ ├── mod.rs │ │ ├── device.rs │ │ ├── fixture.rs │ │ ├── test_pointer.rs │ │ └── client.rs │ ├── utils │ │ ├── mod.rs │ │ ├── device │ │ │ ├── mod.rs │ │ │ └── gpu.rs │ │ ├── video_info.rs │ │ ├── renderer │ │ │ └── mod.rs │ │ ├── target.rs │ │ └── allocator │ │ │ └── cuda │ │ │ ├── mod.rs │ │ │ └── ffi.rs │ ├── comp │ │ ├── rendering.rs │ │ ├── focus.rs │ │ └── input.rs │ └── lib.rs ├── resources │ ├── cursor.rgba │ └── protocols │ │ └── wayland-drm.xml ├── build.rs └── Cargo.toml ├── gst-plugin-wayland-display ├── build.rs ├── src │ ├── waylandsrc │ │ └── mod.rs │ ├── lib.rs │ └── utils.rs └── Cargo.toml ├── Cargo.toml ├── .github └── workflows │ └── rust-format.yml ├── .gitignore ├── .devcontainer ├── devcontainer.json └── Dockerfile ├── c-bindings ├── Cargo.toml ├── cbindgen.toml └── src │ └── capi.rs ├── LICENSE └── README.md /wayland-display-core/src/wayland/protocols/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod wl_drm; 2 | -------------------------------------------------------------------------------- /wayland-display-core/src/wayland/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod handlers; 2 | pub mod protocols; 3 | -------------------------------------------------------------------------------- /gst-plugin-wayland-display/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | gst_plugin_version_helper::info() 3 | } 4 | -------------------------------------------------------------------------------- /wayland-display-core/src/tests/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod client; 2 | mod device; 3 | mod fixture; 4 | mod test_pointer; 5 | -------------------------------------------------------------------------------- /wayland-display-core/src/wayland/handlers/output.rs: -------------------------------------------------------------------------------- 1 | use smithay::delegate_output; 2 | 3 | use crate::comp::State; 4 | 5 | delegate_output!(State); 6 | -------------------------------------------------------------------------------- /wayland-display-core/resources/cursor.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/games-on-whales/gst-wayland-display/HEAD/wayland-display-core/resources/cursor.rgba -------------------------------------------------------------------------------- /wayland-display-core/src/wayland/handlers/viewporter.rs: -------------------------------------------------------------------------------- 1 | use smithay::delegate_viewporter; 2 | 3 | use crate::comp::State; 4 | 5 | delegate_viewporter!(State); 6 | -------------------------------------------------------------------------------- /wayland-display-core/src/wayland/handlers/presentation.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | 3 | use crate::comp::State; 4 | use smithay::delegate_presentation; 5 | 6 | delegate_presentation!(State); 7 | -------------------------------------------------------------------------------- /wayland-display-core/src/wayland/handlers/relative_pointer.rs: -------------------------------------------------------------------------------- 1 | use smithay::delegate_relative_pointer; 2 | use smithay::wayland::output::OutputHandler; 3 | 4 | use crate::comp::State; 5 | 6 | impl OutputHandler for State {} 7 | 8 | delegate_relative_pointer!(State); 9 | -------------------------------------------------------------------------------- /wayland-display-core/src/wayland/handlers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod compositor; 2 | pub mod data_device; 3 | pub mod dmabuf; 4 | pub mod output; 5 | pub mod pointer_constraints; 6 | pub mod presentation; 7 | pub mod relative_pointer; 8 | pub mod seat; 9 | pub mod shm; 10 | pub mod viewporter; 11 | pub mod wl_drm; 12 | pub mod xdg; 13 | -------------------------------------------------------------------------------- /wayland-display-core/src/wayland/handlers/shm.rs: -------------------------------------------------------------------------------- 1 | use smithay::{ 2 | delegate_shm, 3 | wayland::shm::{ShmHandler, ShmState}, 4 | }; 5 | 6 | use crate::comp::State; 7 | 8 | impl ShmHandler for State { 9 | fn shm_state(&self) -> &ShmState { 10 | &self.shm_state 11 | } 12 | } 13 | 14 | delegate_shm!(State); 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | ".", 5 | "c-bindings", 6 | "wayland-display-core", 7 | "gst-plugin-wayland-display" 8 | ] 9 | 10 | [workspace.dependencies] 11 | gst = { version = "0.23.2", package = "gstreamer", features = ["v1_24"] } 12 | gst-video = { version = "0.23.2", package = "gstreamer-video", features = ["v1_24"] } 13 | tracing = "0.1.37" 14 | once_cell = "1.17.0" 15 | -------------------------------------------------------------------------------- /.github/workflows/rust-format.yml: -------------------------------------------------------------------------------- 1 | name: "Rust format" 2 | on: 3 | push: 4 | pull_request: 5 | 6 | jobs: 7 | formatting: 8 | name: cargo fmt 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | # Ensure rustfmt is installed and setup problem matcher 14 | - uses: actions-rust-lang/setup-rust-toolchain@v1 15 | with: 16 | components: rustfmt 17 | 18 | - name: Rustfmt Check 19 | uses: actions-rust-lang/rustfmt@v1 -------------------------------------------------------------------------------- /wayland-display-core/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod allocator; 2 | pub mod device; 3 | pub mod renderer; 4 | pub mod video_info; 5 | 6 | mod target; 7 | 8 | pub use self::target::*; 9 | 10 | pub mod tests { 11 | use std::sync::Once; 12 | pub static INIT: Once = Once::new(); 13 | 14 | #[cfg(test)] 15 | pub fn test_init() -> () { 16 | INIT.call_once(|| { 17 | tracing_subscriber::fmt::try_init().ok(); 18 | gst::init().expect("Failed to initialize GStreamer"); 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /gst-plugin-wayland-display/src/waylandsrc/mod.rs: -------------------------------------------------------------------------------- 1 | use gst::glib; 2 | use gst::prelude::*; 3 | 4 | mod imp; 5 | 6 | glib::wrapper! { 7 | pub struct WaylandDisplaySrc(ObjectSubclass) @extends gst_base::PushSrc, gst_base::BaseSrc, gst::Element, gst::Object; 8 | } 9 | 10 | pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { 11 | gst::Element::register( 12 | Some(plugin), 13 | "waylanddisplaysrc", 14 | gst::Rank::MARGINAL, 15 | WaylandDisplaySrc::static_type(), 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | 12 | # IntelliJ project files 13 | .idea 14 | *.iml 15 | out 16 | gen 17 | 18 | # VSCode files 19 | .vscode/* 20 | !.vscode/settings.json 21 | !.vscode/tasks.json 22 | !.vscode/launch.json 23 | !.vscode/extensions.json 24 | *.code-workspace 25 | 26 | # Local History for Visual Studio Code 27 | .history/ 28 | *.app 29 | .snapshots/* 30 | 31 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gst-wayland-comp-dev", 3 | "build": { 4 | "context": "..", 5 | "dockerfile": "Dockerfile" 6 | }, 7 | "features": {}, 8 | "runArgs": ["--runtime=nvidia"], 9 | "hostRequirements": { 10 | "gpu": true 11 | }, 12 | "privileged": true, 13 | "containerEnv": { 14 | "XDG_RUNTIME_DIR": "/home/retro/" 15 | }, 16 | "remoteEnv": { 17 | "XDG_RUNTIME_DIR": "/tmp/sockets", 18 | "RUST_BACKTRACE": "full", 19 | "RUST_LOG": "INFO" 20 | }, 21 | "customizations": { 22 | "vscode": { 23 | "extensions": ["rust-lang.rust-analyzer"] 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /wayland-display-core/build.rs: -------------------------------------------------------------------------------- 1 | use pkg_config; 2 | 3 | fn main() { 4 | // Check if the cuda feature is enabled 5 | #[cfg(feature = "cuda")] 6 | { 7 | // Link GStreamer CUDA library 8 | if let Err(e) = pkg_config::Config::new() 9 | .atleast_version("1.24") 10 | .probe("gstreamer-cuda-1.0") 11 | { 12 | eprintln!( 13 | "Warning: gstreamer-cuda-1.0 not found via pkg-config: {}", 14 | e 15 | ); 16 | } 17 | } 18 | 19 | // Rerun if build script changes 20 | println!("cargo:rerun-if-changed=build.rs"); 21 | } 22 | -------------------------------------------------------------------------------- /wayland-display-core/src/wayland/handlers/data_device.rs: -------------------------------------------------------------------------------- 1 | use smithay::wayland::selection::SelectionHandler; 2 | use smithay::{ 3 | delegate_data_device, 4 | wayland::selection::data_device::{ 5 | ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler, 6 | }, 7 | }; 8 | 9 | use crate::comp::State; 10 | 11 | impl ServerDndGrabHandler for State {} 12 | 13 | impl ClientDndGrabHandler for State {} 14 | 15 | impl SelectionHandler for State { 16 | type SelectionUserData = (); 17 | } 18 | 19 | impl DataDeviceHandler for State { 20 | fn data_device_state(&self) -> &DataDeviceState { 21 | &self.data_device_state 22 | } 23 | } 24 | 25 | delegate_data_device!(State); 26 | -------------------------------------------------------------------------------- /wayland-display-core/src/wayland/handlers/dmabuf.rs: -------------------------------------------------------------------------------- 1 | use smithay::{ 2 | backend::{allocator::dmabuf::Dmabuf, renderer::ImportDma}, 3 | delegate_dmabuf, 4 | wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier}, 5 | }; 6 | 7 | use crate::comp::State; 8 | 9 | impl DmabufHandler for State { 10 | fn dmabuf_state(&mut self) -> &mut DmabufState { 11 | &mut self.dmabuf_state 12 | } 13 | 14 | fn dmabuf_imported( 15 | &mut self, 16 | _global: &DmabufGlobal, 17 | dmabuf: Dmabuf, 18 | notifier: ImportNotifier, 19 | ) { 20 | if self.renderer.import_dmabuf(&dmabuf, None).is_ok() { 21 | let _ = notifier.successful::(); 22 | } else { 23 | notifier.failed(); 24 | } 25 | } 26 | } 27 | 28 | delegate_dmabuf!(State); 29 | -------------------------------------------------------------------------------- /wayland-display-core/src/wayland/handlers/wl_drm.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | comp::State, 3 | wayland::protocols::wl_drm::{DrmHandler, ImportError, delegate_wl_drm}, 4 | }; 5 | use smithay::backend::renderer::ImportDma; 6 | use smithay::{ 7 | backend::allocator::dmabuf::Dmabuf, reexports::wayland_server::protocol::wl_buffer::WlBuffer, 8 | wayland::dmabuf::DmabufGlobal, 9 | }; 10 | 11 | impl DrmHandler<()> for State { 12 | fn dmabuf_imported( 13 | &mut self, 14 | _global: &DmabufGlobal, 15 | dmabuf: Dmabuf, 16 | ) -> Result<(), ImportError> { 17 | self.renderer 18 | .import_dmabuf(&dmabuf, None) 19 | .map(|_| ()) 20 | .map_err(|_| ImportError::Failed) 21 | } 22 | 23 | fn buffer_created(&mut self, _buffer: WlBuffer, _result: ()) {} 24 | } 25 | 26 | delegate_wl_drm!(State); 27 | -------------------------------------------------------------------------------- /c-bindings/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "c-bindings" 3 | authors = ["Victoria Brekenfeld ", "Alessandro Beltramo "] 4 | version = "0.4.0" 5 | edition = "2021" 6 | license = "MIT" 7 | description = "Wayland Compositor producing GStreamer buffers" 8 | rust-version = "1.72" 9 | 10 | [lib] 11 | name = "libgstwaylanddisplay" 12 | path = "src/capi.rs" 13 | 14 | [features] 15 | capi = [] 16 | 17 | [dependencies] 18 | gst.workspace = true 19 | gst-video.workspace = true 20 | tracing.workspace = true 21 | wayland-display-core = { path = "../wayland-display-core" } 22 | tracing-subscriber = "0.3" 23 | 24 | [package.metadata.capi] 25 | min_version = "0.9.21" 26 | 27 | [package.metadata.capi.pkg_config] 28 | requires = """ 29 | gstreamer-1.0, gstreamer-base-1.0, wayland-server 30 | """ 31 | 32 | [package.metadata.capi.header] 33 | enabled = true 34 | -------------------------------------------------------------------------------- /wayland-display-core/src/utils/device/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod gpu; 2 | 3 | #[derive(Debug, Clone, Eq, PartialEq, Hash)] 4 | pub enum PCIVendor { 5 | Unknown = 0x0000, 6 | Intel = 0x8086, 7 | NVIDIA = 0x10de, 8 | AMD = 0x1002, 9 | } 10 | impl PCIVendor { 11 | pub fn as_str(&self) -> &'static str { 12 | match self { 13 | PCIVendor::Intel => "Intel", 14 | PCIVendor::NVIDIA => "NVIDIA", 15 | PCIVendor::AMD => "AMD", 16 | PCIVendor::Unknown => "Unknown", 17 | } 18 | } 19 | } 20 | impl From for PCIVendor { 21 | fn from(vendor_id: u32) -> Self { 22 | match vendor_id { 23 | 0x8086 => PCIVendor::Intel, 24 | 0x10de => PCIVendor::NVIDIA, 25 | 0x1002 => PCIVendor::AMD, 26 | _ => PCIVendor::Unknown, 27 | } 28 | } 29 | } 30 | impl std::fmt::Display for PCIVendor { 31 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 32 | write!(f, "{}", self.as_str()) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /gst-plugin-wayland-display/src/lib.rs: -------------------------------------------------------------------------------- 1 | use gst::glib; 2 | #[cfg(feature = "cuda")] 3 | use waylanddisplaycore::utils::allocator::cuda; 4 | 5 | pub mod utils; 6 | mod waylandsrc; 7 | 8 | fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { 9 | waylandsrc::register(plugin)?; 10 | tracing_subscriber::fmt::try_init().ok(); 11 | #[cfg(feature = "cuda")] 12 | match cuda::init_cuda() { 13 | Ok(_) => { 14 | tracing::info!("CUDA initialization successful"); 15 | } 16 | Err(e) => { 17 | tracing::info!("CUDA initialization failed: {}", e); 18 | } 19 | } 20 | Ok(()) 21 | } 22 | 23 | gst::plugin_define!( 24 | waylanddisplaysrc, 25 | env!("CARGO_PKG_DESCRIPTION"), 26 | plugin_init, 27 | concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")), 28 | "MIT/X11", // https://gitlab.freedesktop.org/gstreamer/gstreamer/-/blob/master/gst/gstplugin.c#L95 29 | env!("CARGO_PKG_NAME"), 30 | env!("CARGO_PKG_NAME"), 31 | env!("CARGO_PKG_REPOSITORY"), 32 | env!("BUILD_REL_DATE") 33 | ); 34 | -------------------------------------------------------------------------------- /wayland-display-core/src/tests/device.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::device::PCIVendor; 2 | use crate::utils::device::gpu::get_gpu_device; 3 | use test_log::test; 4 | 5 | #[test] 6 | fn test_get_gpu_device() { 7 | for path in vec![ 8 | "/dev/dri/card0", 9 | "/dev/dri/renderD128", 10 | "/dev/dri/by-path/pci-0000:2d:00.0-render", 11 | "/dev/dri/card1", 12 | "/dev/dri/renderD129", 13 | "/dev/dri/by-path/pci-0000:04:00.0-render", 14 | ] { 15 | match get_gpu_device(path) { 16 | Ok(device) => { 17 | tracing::info!("Found GPU: {}", device); 18 | assert!(!device.device_name().is_empty(), "Device name is empty"); 19 | assert_ne!( 20 | *device.pci_vendor(), 21 | PCIVendor::Unknown, 22 | "Unknown PCI vendor" 23 | ); 24 | } 25 | Err(e) => { 26 | tracing::error!("Failed to get GPU device for path {}: {}", path, e); 27 | continue; 28 | } 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Victoria Brekenfeld 4 | Copyright (c) 2023 Games on Whales https://github.com/games-on-whales/ 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/games-on-whales/gstreamer:1.26.7 2 | 3 | ARG HOME="/home/$UNAME" 4 | ENV HOME=$HOME 5 | 6 | # Setup user 7 | RUN <<_SETUP_USER 8 | #!/bin/bash 9 | set -e 10 | 11 | userdel -r ubuntu 12 | groupadd --gid $PGID $UNAME 13 | useradd --uid $PUID --gid $PGID $UNAME 14 | mkdir -p $HOME 15 | chown -R $PUID:$PGID $HOME 16 | 17 | apt-get update -y 18 | apt-get install -y sudo 19 | echo $UNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$UNAME 20 | chmod 0440 /etc/sudoers.d/$UNAME 21 | _SETUP_USER 22 | 23 | # Dependencies 24 | RUN apt-get update -y \ 25 | && apt-get install -y --no-install-recommends \ 26 | curl git ca-certificates build-essential pkg-config libssl-dev libicu76 \ 27 | libwayland-dev libwayland-server0 libinput-dev libxkbcommon-dev libgbm-dev \ 28 | librust-gstreamer-allocators-dev 29 | 30 | ARG RUST_VERSION=1.89.0 31 | ENV RUST_VERSION=$RUST_VERSION 32 | ENV PATH="$HOME/.cargo/bin:${PATH}" 33 | 34 | USER $UNAME 35 | 36 | RUN <<_INSTALL_RUST 37 | #!/bin/bash 38 | set -e 39 | 40 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 41 | rustup install $RUST_VERSION 42 | rustup default $RUST_VERSION 43 | 44 | cargo install cargo-c 45 | _INSTALL_RUST 46 | -------------------------------------------------------------------------------- /wayland-display-core/src/wayland/handlers/seat.rs: -------------------------------------------------------------------------------- 1 | use smithay::{ 2 | delegate_seat, 3 | input::{Seat, SeatHandler, SeatState, pointer::CursorImageStatus}, 4 | reexports::wayland_server::Resource, 5 | wayland::selection::data_device::set_data_device_focus, 6 | }; 7 | 8 | use crate::comp::{FocusTarget, State}; 9 | 10 | impl SeatHandler for State { 11 | type KeyboardFocus = FocusTarget; 12 | type PointerFocus = FocusTarget; 13 | type TouchFocus = FocusTarget; 14 | 15 | fn seat_state(&mut self) -> &mut SeatState { 16 | &mut self.seat_state 17 | } 18 | 19 | fn focus_changed(&mut self, seat: &Seat, focus: Option<&Self::KeyboardFocus>) { 20 | if let Some(surface) = focus { 21 | let client = match surface { 22 | FocusTarget::Wayland(w) => w.toplevel().unwrap().wl_surface().client(), 23 | FocusTarget::Popup(p) => p.wl_surface().client(), 24 | }; 25 | set_data_device_focus(&self.dh, seat, client); 26 | } else { 27 | set_data_device_focus(&self.dh, seat, None); 28 | } 29 | } 30 | 31 | fn cursor_image(&mut self, _seat: &Seat, image: CursorImageStatus) { 32 | self.cursor_state = image; 33 | } 34 | } 35 | 36 | delegate_seat!(State); 37 | -------------------------------------------------------------------------------- /gst-plugin-wayland-display/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gst-plugin-wayland-display" 3 | authors = ["Victoria Brekenfeld ", "Alessandro Beltramo "] 4 | version = "0.4.0" 5 | edition = "2024" 6 | license = "MIT" 7 | description = "GStreamer Wayland Compositor Src" 8 | repository = "https://github.com/games-on-whales/wolf" 9 | rust-version = "1.88" 10 | 11 | [lib] 12 | name = "gstwaylanddisplaysrc" 13 | crate-type = ["cdylib", "rlib"] 14 | path = "src/lib.rs" 15 | 16 | [features] 17 | default = [] 18 | static = [] 19 | capi = [] 20 | doc = [] 21 | cuda = [] 22 | 23 | [dependencies] 24 | gst.workspace = true 25 | gst-base = { version = "0.23.2", package = "gstreamer-base", features = ["v1_24"] } 26 | gstreamer-allocators = { version = "0.23.2", package = "gstreamer-allocators", features = ["v1_24"] } 27 | 28 | gst-video.workspace = true 29 | wayland-display-core = { path = "../wayland-display-core" } 30 | tracing.workspace = true 31 | once_cell.workspace = true 32 | tracing-subscriber = "0.3.16" 33 | 34 | [build-dependencies] 35 | gst-plugin-version-helper = "0.7.5" 36 | 37 | [package.metadata.capi] 38 | min_version = "0.9.21" 39 | 40 | [package.metadata.capi.header] 41 | enabled = false 42 | 43 | [package.metadata.capi.library] 44 | install_subdir = "gstreamer-1.0" 45 | versioning = false 46 | import_library = false 47 | 48 | [package.metadata.capi.pkg_config] 49 | requires_private = "gstreamer-1.0, gstreamer-base-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0" 50 | -------------------------------------------------------------------------------- /wayland-display-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wayland-display-core" 3 | authors = ["Victoria Brekenfeld ", "Alessandro Beltramo "] 4 | version = "0.2.0" 5 | edition = "2024" 6 | license = "MIT" 7 | description = "Wayland Compositor producing GStreamer buffers" 8 | rust-version = "1.88" 9 | 10 | [lib] 11 | name = "waylanddisplaycore" 12 | crate-type = ["cdylib", "staticlib", "rlib"] 13 | path = "src/lib.rs" 14 | 15 | [features] 16 | default = [] 17 | cuda = ["dep:libloading"] 18 | 19 | [dependencies] 20 | gst.workspace = true 21 | gst-video.workspace = true 22 | tracing.workspace = true 23 | once_cell.workspace = true 24 | wayland-backend = "0.3.10" 25 | wayland-scanner = "0.31.6" 26 | gstreamer-allocators = { version = "0.23.2", package = "gstreamer-allocators", features = ["v1_24", "v1_16"] } 27 | libloading = { version = "0.8.9", optional = true } 28 | 29 | [build-dependencies] 30 | pkg-config = "0.3" 31 | 32 | [dev-dependencies] 33 | tracing-subscriber = "0.3.18" 34 | wayland-client = "0.31.10" 35 | wayland-protocols = { version = "0.32.8", features = ["client"] } 36 | calloop = { version = "0.14.2", features = ["executor", "futures-io", "signals"] } 37 | tempfile = "3.20.0" 38 | test-log = { version = "0.2.18", features = ["trace"] } 39 | 40 | [dependencies.smithay] 41 | git = "https://github.com/games-on-whales/smithay" 42 | rev = "a166cf4c94b5aedc332a65aa1dd753e8148829c3" 43 | default-features = false 44 | features = [ 45 | "backend_drm", 46 | "backend_egl", 47 | "backend_gbm", 48 | "backend_libinput", 49 | "backend_udev", 50 | "renderer_gl", 51 | "use_system_lib", 52 | "desktop", 53 | "wayland_frontend" 54 | ] 55 | -------------------------------------------------------------------------------- /wayland-display-core/src/wayland/handlers/pointer_constraints.rs: -------------------------------------------------------------------------------- 1 | use crate::comp::State; 2 | use smithay::delegate_pointer_constraints; 3 | use smithay::input::pointer::PointerHandle; 4 | use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; 5 | use smithay::utils::{Logical, Point}; 6 | use smithay::wayland::pointer_constraints::{PointerConstraintsHandler, with_pointer_constraint}; 7 | use smithay::wayland::seat::WaylandFocus; 8 | 9 | impl PointerConstraintsHandler for State { 10 | fn new_constraint(&mut self, surface: &WlSurface, pointer: &PointerHandle) { 11 | if pointer 12 | .current_focus() 13 | .map(|x| &*(x.wl_surface().unwrap()) == surface) 14 | .unwrap_or(false) 15 | { 16 | let under = self 17 | .space 18 | .element_under(self.pointer_location) 19 | .map(|(w, pos)| (w.clone().into(), pos.to_f64())); 20 | self.maybe_activate_pointer_constraint(&under, self.pointer_location); 21 | } 22 | } 23 | 24 | fn cursor_position_hint( 25 | &mut self, 26 | surface: &WlSurface, 27 | pointer: &PointerHandle, 28 | location: Point, 29 | ) { 30 | if with_pointer_constraint(surface, pointer, |constraint| { 31 | constraint.is_some_and(|c| c.is_active()) 32 | }) { 33 | let origin = self 34 | .space 35 | .elements() 36 | .find_map(|window| { 37 | (window.wl_surface().as_deref() == Some(surface)).then(|| window.geometry()) 38 | }) 39 | .unwrap_or_default() 40 | .loc 41 | .to_f64(); 42 | 43 | pointer.set_location(origin + location); 44 | } 45 | } 46 | } 47 | 48 | delegate_pointer_constraints!(State); // Needed by SDL in order to lock the pointer to the window 49 | -------------------------------------------------------------------------------- /c-bindings/cbindgen.toml: -------------------------------------------------------------------------------- 1 | # The language to output bindings in 2 | # 3 | # possible values: "C", "C++", "Cython" 4 | # 5 | # default: "C++" 6 | language = "C" 7 | 8 | 9 | # Whether to include a comment with the version of cbindgen used to generate the file 10 | # default: false 11 | include_version = true 12 | 13 | # A list of headers to #include (with quotes) 14 | # default: [] 15 | sys_includes = ["gstreamer-1.0/gst/gstbuffer.h", "gstreamer-1.0/gst/video/video-info.h"] 16 | 17 | no_includes = true 18 | 19 | include_guard = "GST_WAYLAND_DISPLAY_H" 20 | 21 | # A list of lines to add verbatim after the includes block 22 | after_includes = "\ntypedef void WaylandDisplay;" 23 | 24 | header = """ 25 | /** 26 | MIT License 27 | 28 | Copyright (c) 2023 Games on Whales https://github.com/games-on-whales/ 29 | 30 | Permission is hereby granted, free of charge, to any person obtaining a copy 31 | of this software and associated documentation files (the "Software"), to deal 32 | in the Software without restriction, including without limitation the rights 33 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 34 | copies of the Software, and to permit persons to whom the Software is 35 | furnished to do so, subject to the following conditions: 36 | 37 | The above copyright notice and this permission notice shall be included in all 38 | copies or substantial portions of the Software. 39 | 40 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 41 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 42 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 43 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 44 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 45 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 46 | SOFTWARE. 47 | **/ 48 | """ -------------------------------------------------------------------------------- /gst-plugin-wayland-display/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Write}; 2 | 3 | use gst::glib; 4 | use once_cell::sync::Lazy; 5 | use tracing::field::{Field, Visit}; 6 | 7 | pub static CAT: Lazy = Lazy::new(|| { 8 | gst::DebugCategory::new( 9 | "waylanddisplaysrc", 10 | gst::DebugColorFlags::empty(), 11 | Some("Wayland Display Source Bin"), 12 | ) 13 | }); 14 | 15 | #[derive(Debug, Clone)] 16 | pub struct GstLayer; 17 | 18 | pub struct StringVisitor<'a> { 19 | string: &'a mut String, 20 | } 21 | 22 | impl<'a> Visit for StringVisitor<'a> { 23 | fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) { 24 | write!(self.string, "{} = {:?}; ", field.name(), value).unwrap(); 25 | } 26 | } 27 | 28 | impl tracing_subscriber::Layer for GstLayer 29 | where 30 | S: tracing::Subscriber, 31 | { 32 | fn on_event( 33 | &self, 34 | event: &tracing::Event<'_>, 35 | _ctx: tracing_subscriber::layer::Context<'_, S>, 36 | ) { 37 | let mut message = String::new(); 38 | event.record(&mut StringVisitor { 39 | string: &mut message, 40 | }); 41 | 42 | CAT.log( 43 | Option::<&crate::waylandsrc::WaylandDisplaySrc>::None, 44 | match event.metadata().level() { 45 | &tracing::Level::ERROR => gst::DebugLevel::Error, 46 | &tracing::Level::WARN => gst::DebugLevel::Warning, 47 | &tracing::Level::INFO => gst::DebugLevel::Info, 48 | &tracing::Level::DEBUG => gst::DebugLevel::Debug, 49 | &tracing::Level::TRACE => gst::DebugLevel::Trace, 50 | }, 51 | glib::GString::from(event.metadata().file().unwrap_or("")).as_gstr(), 52 | event.metadata().module_path().unwrap_or(""), 53 | event.metadata().line().unwrap_or(0), 54 | format_args!("{}", message), 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /wayland-display-core/src/utils/video_info.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "cuda")] 2 | use crate::utils::allocator::cuda; 3 | use gst_video::{VideoInfo, VideoInfoDmaDrm}; 4 | use std::sync::{Arc, Mutex}; 5 | 6 | #[cfg(feature = "cuda")] 7 | #[derive(Debug, Clone)] 8 | pub struct CUDAParams { 9 | pub video_info: VideoInfoDmaDrm, 10 | pub cuda_context: Arc>, 11 | } 12 | 13 | #[derive(Debug, Clone)] 14 | pub enum GstVideoInfo { 15 | RAW(VideoInfo), 16 | DMA(VideoInfoDmaDrm), 17 | #[cfg(feature = "cuda")] 18 | CUDA(CUDAParams), 19 | } 20 | 21 | impl From for GstVideoInfo { 22 | fn from(info: VideoInfo) -> Self { 23 | GstVideoInfo::RAW(info) 24 | } 25 | } 26 | 27 | impl From for GstVideoInfo { 28 | fn from(info: VideoInfoDmaDrm) -> Self { 29 | GstVideoInfo::DMA(info) 30 | } 31 | } 32 | 33 | impl From for VideoInfo { 34 | fn from(info: GstVideoInfo) -> Self { 35 | match info { 36 | GstVideoInfo::RAW(info) => info, 37 | GstVideoInfo::DMA(info) => match info.to_video_info() { 38 | Ok(info) => info, 39 | Err(_) => VideoInfo::builder(info.format(), info.width(), info.height()) 40 | .fps(info.fps()) 41 | .build() 42 | .expect("Failed to build VideoInfo from VideoInfoDmaDrm"), 43 | }, 44 | #[cfg(feature = "cuda")] 45 | GstVideoInfo::CUDA(params) => match params.video_info.to_video_info() { 46 | Ok(info) => info, 47 | Err(_) => VideoInfo::builder( 48 | params.video_info.format(), 49 | params.video_info.width(), 50 | params.video_info.height(), 51 | ) 52 | .fps(params.video_info.fps()) 53 | .build() 54 | .expect("Failed to build VideoInfo from VideoInfoDmaDrm"), 55 | }, 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /wayland-display-core/src/utils/renderer/mod.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | use smithay::backend::drm::{DrmNode, NodeType}; 3 | use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay}; 4 | use smithay::backend::renderer::gles::GlesRenderer; 5 | use std::collections::HashMap; 6 | use std::sync::{Arc, Mutex, Weak}; 7 | 8 | static EGL_DISPLAYS: Lazy, Weak>>> = 9 | Lazy::new(|| Mutex::new(HashMap::new())); 10 | 11 | pub fn get_egl_device_for_node(drm_node: &DrmNode) -> EGLDevice { 12 | let drm_node = drm_node 13 | .node_with_type(NodeType::Render) 14 | .and_then(Result::ok) 15 | .unwrap_or(drm_node.clone()); 16 | EGLDevice::enumerate() 17 | .expect("Failed to enumerate EGLDevices") 18 | .find(|d| d.try_get_render_node().unwrap_or_default() == Some(drm_node)) 19 | .expect("Unable to find EGLDevice for drm-node") 20 | } 21 | 22 | pub fn setup_renderer(render_node: Option) -> GlesRenderer { 23 | let mut displays = EGL_DISPLAYS.lock().unwrap(); 24 | let maybe_display = displays 25 | .get(&render_node) 26 | .and_then(|weak_display| weak_display.upgrade()); 27 | 28 | let egl = match maybe_display { 29 | Some(display) => display, 30 | None => { 31 | let device = match render_node.as_ref() { 32 | Some(render_node) => get_egl_device_for_node(render_node), 33 | None => EGLDevice::enumerate() 34 | .expect("Failed to enumerate EGLDevices") 35 | .find(|device| { 36 | device 37 | .extensions() 38 | .iter() 39 | .any(|e| e == "EGL_MESA_device_software") 40 | }) 41 | .expect("Failed to find software device"), 42 | }; 43 | let egl = unsafe { EGLDisplay::new(device).expect("Failed to create EGLDisplay") }; 44 | let display = Arc::new(egl); 45 | displays.insert(render_node, Arc::downgrade(&display)); 46 | display 47 | } 48 | }; 49 | let context = EGLContext::new(&egl).expect("Failed to initialize EGL context"); 50 | let renderer = unsafe { GlesRenderer::new(context) }.expect("Failed to initialize renderer"); 51 | renderer 52 | } 53 | -------------------------------------------------------------------------------- /wayland-display-core/src/utils/target.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::CString, os::unix::fs::MetadataExt, str::FromStr}; 2 | 3 | use smithay::backend::{ 4 | drm::{CreateDrmNodeError, DrmNode, NodeType}, 5 | udev, 6 | }; 7 | use smithay::reexports::rustix::fs::major; 8 | 9 | #[derive(Debug, Clone, PartialEq)] 10 | pub enum RenderTarget { 11 | Hardware(DrmNode), 12 | Software, 13 | } 14 | 15 | impl FromStr for RenderTarget { 16 | type Err = CreateDrmNodeError; 17 | fn from_str(s: &str) -> Result { 18 | Ok(match s { 19 | "software" => RenderTarget::Software, 20 | path => RenderTarget::Hardware(DrmNode::from_path(path)?), 21 | }) 22 | } 23 | } 24 | 25 | impl Into> for RenderTarget { 26 | fn into(self) -> Option { 27 | match self { 28 | RenderTarget::Hardware(node) => Some(node), 29 | RenderTarget::Software => None, 30 | } 31 | } 32 | } 33 | 34 | impl Into for DrmNode { 35 | fn into(self) -> RenderTarget { 36 | RenderTarget::Hardware(self) 37 | } 38 | } 39 | 40 | #[cfg(target_os = "linux")] 41 | const NVIDIA_MAJOR: u32 = 195; 42 | 43 | // no clue how this number is on BSDs, feel free to contribute 44 | 45 | impl RenderTarget { 46 | pub fn as_devices(&self) -> Vec { 47 | match self { 48 | RenderTarget::Hardware(node) => { 49 | let mut devices = Vec::new(); 50 | if let Some(primary) = node.dev_path_with_type(NodeType::Primary) { 51 | devices.push(primary); 52 | } 53 | if let Some(render) = node.dev_path_with_type(NodeType::Render) { 54 | devices.push(render); 55 | } 56 | if udev::driver(node.dev_id()) 57 | .ok() 58 | .flatten() 59 | .map(|s| s.to_str() == Some("nvidia")) 60 | .unwrap_or(false) 61 | { 62 | // no idea how match nvidia device nodes to kms/dri-nodes, so lets map all nvidia-nodes to be sure 63 | for entry in std::fs::read_dir("/dev").expect("Unable to access /dev") { 64 | if let Ok(entry) = entry { 65 | if let Ok(metadata) = entry.metadata() { 66 | if metadata.is_file() && major(metadata.dev()) == NVIDIA_MAJOR { 67 | devices.push(entry.path()); 68 | } 69 | } 70 | } 71 | } 72 | } 73 | 74 | devices 75 | .into_iter() 76 | .flat_map(|path| { 77 | path.to_str() 78 | .map(String::from) 79 | .and_then(|string| CString::new(string).ok()) 80 | }) 81 | .collect() 82 | } 83 | RenderTarget::Software => Vec::new(), 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /wayland-display-core/src/comp/rendering.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, Instant}; 2 | 3 | use super::State; 4 | use crate::utils::allocator::GsBuffer; 5 | use smithay::backend::renderer::gles::GlesError; 6 | use smithay::{ 7 | backend::renderer::{ 8 | ImportAll, ImportMem, Renderer, 9 | damage::{Error as OutputDamageTrackerError, RenderOutputResult}, 10 | element::{ 11 | Kind, memory::MemoryRenderBufferRenderElement, surface::WaylandSurfaceRenderElement, 12 | }, 13 | }, 14 | desktop::space::render_output, 15 | input::pointer::CursorImageStatus, 16 | render_elements, 17 | }; 18 | 19 | pub const CURSOR_DATA_BYTES: &[u8] = include_bytes!("../../resources/cursor.rgba"); 20 | 21 | render_elements! { 22 | CursorElement where R: Renderer + ImportAll + ImportMem; 23 | Surface=WaylandSurfaceRenderElement, 24 | Memory=MemoryRenderBufferRenderElement 25 | } 26 | 27 | impl State { 28 | pub fn create_frame( 29 | &mut self, 30 | ) -> Result<(gst::Buffer, RenderOutputResult), OutputDamageTrackerError> { 31 | assert!(self.output.is_some()); 32 | assert!(self.dtr.is_some()); 33 | assert!(self.video_info.is_some()); 34 | assert!(self.output_buffer.is_some()); 35 | 36 | let elements = 37 | if Instant::now().duration_since(self.last_pointer_movement) < Duration::from_secs(5) { 38 | match &self.cursor_state { 39 | CursorImageStatus::Named(_cursor_icon) => vec![CursorElement::Memory( 40 | // TODO: icon? 41 | MemoryRenderBufferRenderElement::from_buffer( 42 | &mut self.renderer, 43 | self.pointer_location.to_physical_precise_round(1), 44 | &self.cursor_element, 45 | None, 46 | None, 47 | None, 48 | Kind::Cursor, 49 | ) 50 | .map_err(OutputDamageTrackerError::Rendering)?, 51 | )], 52 | CursorImageStatus::Surface(wl_surface) => { 53 | smithay::backend::renderer::element::surface::render_elements_from_surface_tree( 54 | &mut self.renderer, 55 | wl_surface, 56 | self.pointer_location.to_physical_precise_round(1), 57 | 1., 58 | 1., 59 | Kind::Cursor, 60 | ) 61 | } 62 | CursorImageStatus::Hidden => vec![], 63 | } 64 | } else { 65 | vec![] 66 | }; 67 | 68 | let mut output_buffer = self.output_buffer.clone().expect("Output buffer not set"); 69 | 70 | let mut target = output_buffer 71 | .bind(&mut self.renderer) 72 | .map_err(OutputDamageTrackerError::Rendering)?; 73 | 74 | let render_output_result = render_output( 75 | self.output.as_ref().unwrap(), 76 | &mut self.renderer, 77 | &mut target, 78 | 1.0, 79 | 0, 80 | [&self.space], 81 | &*elements, 82 | self.dtr.as_mut().unwrap(), 83 | [0.0, 0.0, 0.0, 1.0], 84 | )?; 85 | 86 | match self 87 | .output_buffer 88 | .clone() 89 | .unwrap() 90 | .to_gs_buffer(&mut target, &mut self.renderer) 91 | { 92 | Ok(buffer) => Ok((buffer, render_output_result)), 93 | Err(e) => { 94 | tracing::warn!("Failed to convert buffer to gst buffer: {:?}", e); 95 | Err(OutputDamageTrackerError::Rendering(GlesError::MappingError)) 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /wayland-display-core/src/wayland/handlers/xdg.rs: -------------------------------------------------------------------------------- 1 | use smithay::{ 2 | delegate_xdg_shell, 3 | desktop::{ 4 | PopupKeyboardGrab, PopupKind, PopupPointerGrab, PopupUngrabStrategy, Window, 5 | find_popup_root_surface, get_popup_toplevel_coords, 6 | }, 7 | input::{Seat, pointer::Focus}, 8 | reexports::wayland_server::protocol::wl_seat::WlSeat, 9 | utils::Serial, 10 | wayland::{ 11 | seat::WaylandFocus, 12 | shell::xdg::{ 13 | PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState, 14 | }, 15 | }, 16 | }; 17 | 18 | use crate::comp::{FocusTarget, State}; 19 | 20 | impl XdgShellHandler for State { 21 | fn xdg_shell_state(&mut self) -> &mut XdgShellState { 22 | &mut self.shell_state 23 | } 24 | 25 | fn new_toplevel(&mut self, surface: ToplevelSurface) { 26 | let window = Window::new_wayland_window(surface); 27 | self.pending_windows.push(window); 28 | } 29 | 30 | fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) { 31 | self.unconstrain_popup(&surface); 32 | if let Err(err) = self.popups.track_popup(PopupKind::from(surface)) { 33 | tracing::warn!(?err, "Failed to track popup."); 34 | } 35 | } 36 | 37 | fn grab(&mut self, surface: PopupSurface, seat: WlSeat, serial: Serial) { 38 | let seat: Seat = Seat::from_resource(&seat).unwrap(); 39 | let kind = PopupKind::Xdg(surface.clone()); 40 | if let Some(root) = find_popup_root_surface(&kind).ok().and_then(|root| { 41 | self.space 42 | .elements() 43 | .find(|w| w.wl_surface().map(|s| *s == root).unwrap_or(false)) 44 | .cloned() 45 | .map(FocusTarget::from) 46 | }) { 47 | let ret = self.popups.grab_popup(root, surface.into(), &seat, serial); 48 | if let Ok(mut grab) = ret { 49 | if let Some(keyboard) = seat.get_keyboard() { 50 | if keyboard.is_grabbed() 51 | && !(keyboard.has_grab(serial) 52 | || keyboard.has_grab(grab.previous_serial().unwrap_or(serial))) 53 | { 54 | grab.ungrab(PopupUngrabStrategy::All); 55 | return; 56 | } 57 | keyboard.set_focus(self, grab.current_grab(), serial); 58 | keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial); 59 | } 60 | if let Some(pointer) = seat.get_pointer() { 61 | if pointer.is_grabbed() 62 | && !(pointer.has_grab(serial) 63 | || pointer 64 | .has_grab(grab.previous_serial().unwrap_or_else(|| grab.serial()))) 65 | { 66 | grab.ungrab(PopupUngrabStrategy::All); 67 | return; 68 | } 69 | pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Clear); 70 | } 71 | } 72 | } 73 | } 74 | 75 | fn reposition_request( 76 | &mut self, 77 | surface: PopupSurface, 78 | positioner: PositionerState, 79 | token: u32, 80 | ) { 81 | surface.with_pending_state(|state| { 82 | let geometry = positioner.get_geometry(); 83 | state.geometry = geometry; 84 | state.positioner = positioner; 85 | }); 86 | self.unconstrain_popup(&surface); 87 | surface.send_repositioned(token); 88 | } 89 | } 90 | 91 | impl State { 92 | fn unconstrain_popup(&self, popup: &PopupSurface) { 93 | let Ok(root) = find_popup_root_surface(&PopupKind::Xdg(popup.clone())) else { 94 | return; 95 | }; 96 | let Some(window) = self 97 | .space 98 | .elements() 99 | .find(|w| w.toplevel().unwrap().wl_surface() == &root) 100 | else { 101 | return; 102 | }; 103 | 104 | let output = self.space.outputs().next().unwrap(); 105 | let output_geo = self.space.output_geometry(output).unwrap(); 106 | let window_geo = self.space.element_geometry(window).unwrap(); 107 | 108 | // The target geometry for the positioner should be relative to its parent's geometry, so 109 | // we will compute that here. 110 | let mut target = output_geo; 111 | target.loc -= get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone())); 112 | target.loc -= window_geo.loc; 113 | 114 | popup.with_pending_state(|state| { 115 | state.geometry = state.positioner.get_unconstrained_geometry(target); 116 | }); 117 | } 118 | } 119 | 120 | delegate_xdg_shell!(State); 121 | -------------------------------------------------------------------------------- /wayland-display-core/src/utils/device/gpu.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::device::PCIVendor; 2 | use smithay::backend::drm::DrmNode; 3 | use std::error::Error; 4 | use std::fs; 5 | use std::os::unix::fs::MetadataExt; 6 | use std::path::PathBuf; 7 | 8 | #[derive(Debug, Clone, PartialEq)] 9 | pub struct GPUDevice { 10 | pci_vendor: PCIVendor, 11 | device_name: String, 12 | } 13 | impl GPUDevice { 14 | pub fn pci_vendor(&self) -> &PCIVendor { 15 | &self.pci_vendor 16 | } 17 | 18 | pub fn device_name(&self) -> &str { 19 | &self.device_name 20 | } 21 | } 22 | impl TryFrom for GPUDevice { 23 | type Error = Box; 24 | fn try_from(drm_node: DrmNode) -> Result { 25 | get_gpu_device(drm_node.dev_path().unwrap().to_str().unwrap()) 26 | } 27 | } 28 | impl std::fmt::Display for GPUDevice { 29 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 30 | write!( 31 | f, 32 | "GPUDevice {{ pci_vendor: {}, device_name: {} }}", 33 | self.pci_vendor, self.device_name 34 | ) 35 | } 36 | } 37 | 38 | pub fn get_gpu_device(path: &str) -> Result> { 39 | let card = get_card_from_render_node(path)?; 40 | let vendor_str = fs::read_to_string(format!("/sys/class/drm/{}/device/vendor", card))?; 41 | let vendor_str = vendor_str.trim_start_matches("0x").trim_end_matches('\n'); 42 | let vendor = u32::from_str_radix(&vendor_str, 16)?; 43 | 44 | let device_id = fs::read_to_string(format!("/sys/class/drm/{}/device/device", card))?; 45 | let device_id = device_id.trim_start_matches("0x").trim_end_matches('\n'); 46 | 47 | // Look up in hwdata PCI database 48 | let device_name = match fs::read_to_string("/usr/share/hwdata/pci.ids") { 49 | Ok(pci_ids) => parse_pci_ids(&pci_ids, vendor_str, device_id).unwrap_or("".to_owned()), 50 | Err(e) => { 51 | tracing::warn!("Failed to read /usr/share/hwdata/pci.ids: {}", e); 52 | "".to_owned() 53 | } 54 | }; 55 | 56 | Ok(GPUDevice { 57 | pci_vendor: PCIVendor::try_from(vendor)?, 58 | device_name, 59 | }) 60 | } 61 | 62 | fn parse_pci_ids(pci_data: &str, vendor_id: &str, device_id: &str) -> Option { 63 | let mut current_vendor = String::new(); 64 | let vendor_id = vendor_id.to_lowercase(); 65 | let device_id = device_id.to_lowercase(); 66 | 67 | for line in pci_data.lines() { 68 | // Skip comments and empty lines 69 | if line.starts_with('#') || line.is_empty() { 70 | continue; 71 | } 72 | 73 | // Check for vendor lines (no leading whitespace) 74 | if !line.starts_with(['\t', ' ']) { 75 | let mut parts = line.splitn(2, ' '); 76 | if let (Some(vendor), Some(_)) = (parts.next(), parts.next()) { 77 | current_vendor = vendor.to_lowercase(); 78 | } 79 | continue; 80 | } 81 | 82 | // Check for device lines (leading whitespace) 83 | let line = line.trim_start(); 84 | let mut parts = line.splitn(2, ' '); 85 | if let (Some(dev_id), Some(desc)) = (parts.next(), parts.next()) { 86 | if dev_id.to_lowercase() == device_id && current_vendor == vendor_id { 87 | return Some(desc.trim().to_owned()); 88 | } 89 | } 90 | } 91 | 92 | None 93 | } 94 | 95 | fn get_card_from_render_node(render_path: &str) -> std::io::Result { 96 | // Get the device's sysfs path 97 | let metadata = fs::metadata(render_path)?; 98 | let rdev = metadata.rdev(); 99 | let major = gnu_dev_major(rdev); 100 | let minor = gnu_dev_minor(rdev); 101 | 102 | // The sysfs path for the device 103 | let sys_path = format!("/sys/dev/char/{}:{}", major, minor); 104 | 105 | // Read the device symlink to get the actual device 106 | let device_link = PathBuf::from(&sys_path).join("device"); 107 | let device_real = fs::canonicalize(device_link)?; 108 | 109 | // Now find card* entries under the same device 110 | let drm_path = device_real.join("drm"); 111 | for entry in fs::read_dir(drm_path)? { 112 | let entry = entry?; 113 | let name = entry.file_name().to_string_lossy().to_string(); 114 | if name.starts_with("card") && !name.contains('-') { 115 | return Ok(name); 116 | } 117 | } 118 | 119 | Err(std::io::Error::new( 120 | std::io::ErrorKind::NotFound, 121 | "No card found", 122 | )) 123 | } 124 | 125 | fn gnu_dev_major(dev: u64) -> u32 { 126 | let mut major = 0; 127 | major |= ((dev >> 8) & 0xfff) as u32; 128 | major |= ((dev >> 32) & 0xfffff000) as u32; 129 | major 130 | } 131 | 132 | fn gnu_dev_minor(dev: u64) -> u32 { 133 | let mut minor = 0; 134 | minor |= (dev & 0xff) as u32; 135 | minor |= ((dev >> 12) & 0xffffff00) as u32; 136 | minor 137 | } 138 | -------------------------------------------------------------------------------- /wayland-display-core/src/wayland/handlers/compositor.rs: -------------------------------------------------------------------------------- 1 | use smithay::{ 2 | backend::renderer::utils::on_commit_buffer_handler, 3 | delegate_compositor, delegate_single_pixel_buffer, 4 | desktop::PopupKind, 5 | reexports::{ 6 | wayland_protocols::xdg::shell::server::xdg_toplevel::State as XdgState, 7 | wayland_server::{ 8 | Client, 9 | protocol::{wl_buffer::WlBuffer, wl_surface::WlSurface}, 10 | }, 11 | }, 12 | utils::SERIAL_COUNTER, 13 | wayland::{ 14 | buffer::BufferHandler, 15 | compositor::{CompositorClientState, CompositorHandler, CompositorState, with_states}, 16 | seat::WaylandFocus, 17 | shell::xdg::{SurfaceCachedState, XdgPopupSurfaceData, XdgToplevelSurfaceData}, 18 | }, 19 | }; 20 | 21 | use crate::comp::{ClientState, FocusTarget, State}; 22 | 23 | impl BufferHandler for State { 24 | fn buffer_destroyed(&mut self, _buffer: &WlBuffer) {} 25 | } 26 | 27 | impl CompositorHandler for State { 28 | fn compositor_state(&mut self) -> &mut CompositorState { 29 | &mut self.compositor_state 30 | } 31 | 32 | fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState { 33 | &client.get_data::().unwrap().compositor_state 34 | } 35 | 36 | fn commit(&mut self, surface: &WlSurface) { 37 | on_commit_buffer_handler::(surface); 38 | 39 | if let Some(window) = self 40 | .space 41 | .elements() 42 | .find(|w| w.wl_surface().map(|s| &*s == surface).unwrap_or(false)) 43 | { 44 | window.on_commit(); 45 | } 46 | self.popups.commit(surface); 47 | 48 | // send the initial configure if relevant 49 | if let Some(idx) = self 50 | .pending_windows 51 | .iter_mut() 52 | .position(|w| w.wl_surface().map(|s| &*s == surface).unwrap_or(false)) 53 | { 54 | let window = self.pending_windows.swap_remove(idx); 55 | 56 | let toplevel = window.toplevel().unwrap(); 57 | let (initial_configure_sent, max_size) = with_states(surface, |states| { 58 | let attributes = states.data_map.get::().unwrap(); 59 | let attributes_guard = attributes.lock().unwrap(); 60 | 61 | ( 62 | attributes_guard.initial_configure_sent, 63 | states 64 | .cached_state 65 | .get::() 66 | .current() 67 | .max_size, 68 | ) 69 | }); 70 | 71 | if self.output.is_none() { 72 | return; 73 | } 74 | 75 | if !initial_configure_sent { 76 | if max_size.w == 0 && max_size.h == 0 { 77 | toplevel.with_pending_state(|state| { 78 | state.size = Some( 79 | self.output 80 | .as_ref() 81 | .unwrap() 82 | .current_mode() 83 | .unwrap() 84 | .size 85 | .to_f64() 86 | .to_logical( 87 | self.output 88 | .as_ref() 89 | .unwrap() 90 | .current_scale() 91 | .fractional_scale(), 92 | ) 93 | .to_i32_round(), 94 | ); 95 | state.states.set(XdgState::Fullscreen); 96 | }); 97 | } 98 | toplevel.with_pending_state(|state| { 99 | state.states.set(XdgState::Activated); 100 | }); 101 | toplevel.send_configure(); 102 | self.pending_windows.push(window); 103 | } else { 104 | let loc = (0, 0); 105 | self.space.map_element(window.clone(), loc, true); 106 | self.seat.get_keyboard().unwrap().set_focus( 107 | self, 108 | Some(FocusTarget::from(window)), 109 | SERIAL_COUNTER.next_serial(), 110 | ); 111 | } 112 | 113 | return; 114 | } 115 | 116 | if let Some(popup) = self.popups.find_popup(surface) { 117 | let PopupKind::Xdg(ref popup) = popup else { 118 | // Our compositor doesn't do input handling in the popup code 119 | unreachable!() 120 | }; 121 | let initial_configure_sent = with_states(surface, |states| { 122 | states 123 | .data_map 124 | .get::() 125 | .unwrap() 126 | .lock() 127 | .unwrap() 128 | .initial_configure_sent 129 | }); 130 | if !initial_configure_sent { 131 | // NOTE: This should never fail as the initial configure is always 132 | // allowed. 133 | popup.send_configure().expect("initial configure failed"); 134 | } 135 | 136 | return; 137 | }; 138 | } 139 | } 140 | 141 | delegate_compositor!(State); 142 | delegate_single_pixel_buffer!(State); 143 | -------------------------------------------------------------------------------- /c-bindings/src/capi.rs: -------------------------------------------------------------------------------- 1 | use gst::ffi::GstBuffer; 2 | use gst::glib::translate::FromGlibPtrNone; 3 | use gst_video::ffi::GstVideoInfo; 4 | use gst_video::VideoInfo; 5 | use std::ffi::{c_char, c_uint, c_void, CStr}; 6 | use std::ptr; 7 | use tracing_subscriber; 8 | use waylanddisplaycore::{Tracer, WaylandDisplay}; 9 | 10 | #[no_mangle] 11 | pub extern "C" fn display_init(render_node: *const c_char) -> *mut WaylandDisplay { 12 | let render_node = if !render_node.is_null() { 13 | Some( 14 | unsafe { CStr::from_ptr(render_node) } 15 | .to_string_lossy() 16 | .into_owned(), 17 | ) 18 | } else { 19 | None 20 | }; 21 | 22 | tracing_subscriber::fmt::try_init().ok(); 23 | 24 | match WaylandDisplay::new(render_node) { 25 | Ok(dpy) => Box::into_raw(Box::new(dpy)), 26 | Err(err) => { 27 | tracing::error!(?err, "Failed to create wayland display."); 28 | ptr::null_mut() 29 | } 30 | } 31 | } 32 | 33 | #[no_mangle] 34 | pub extern "C" fn display_set_trace_fn( 35 | wd: *mut WaylandDisplay, 36 | trace_start: extern "C" fn(*const c_char) -> *mut c_void, 37 | trace_end: extern "C" fn(*mut c_void), 38 | ) { 39 | let display = unsafe { &mut *wd }; 40 | display.tracer = Some(Tracer::new(trace_start, trace_end)); 41 | } 42 | 43 | #[no_mangle] 44 | pub extern "C" fn display_finish(dpy: *mut WaylandDisplay) { 45 | std::mem::drop(unsafe { Box::from_raw(dpy) }) 46 | } 47 | 48 | #[no_mangle] 49 | pub extern "C" fn display_get_devices_len(dpy: *mut WaylandDisplay) -> c_uint { 50 | let display = unsafe { &mut *dpy }; 51 | display.devices.get().len() as c_uint 52 | } 53 | 54 | #[no_mangle] 55 | pub extern "C" fn display_get_devices( 56 | dpy: *mut WaylandDisplay, 57 | devices: *mut *const c_char, 58 | max_len: c_uint, 59 | ) -> c_uint { 60 | let display = unsafe { &mut *dpy }; 61 | let client_devices = unsafe { std::slice::from_raw_parts_mut(devices, max_len as usize) }; 62 | let devices = display.devices.get(); 63 | 64 | for (i, string) in devices.iter().take(max_len as usize).enumerate() { 65 | client_devices[i] = string.as_ptr() as *const _; 66 | } 67 | 68 | std::cmp::max(max_len, devices.len() as c_uint) 69 | } 70 | 71 | #[no_mangle] 72 | pub extern "C" fn display_get_envvars_len(dpy: *mut WaylandDisplay) -> c_uint { 73 | let display = unsafe { &mut *dpy }; 74 | display.envs.get().len() as c_uint 75 | } 76 | 77 | #[no_mangle] 78 | pub extern "C" fn display_get_envvars( 79 | dpy: *mut WaylandDisplay, 80 | env_vars: *mut *const c_char, 81 | max_len: c_uint, 82 | ) -> c_uint { 83 | let display = unsafe { &mut *dpy }; 84 | let client_env_vars = unsafe { std::slice::from_raw_parts_mut(env_vars, max_len as usize) }; 85 | let env_vars = display.envs.get(); 86 | 87 | for (i, string) in env_vars.iter().take(max_len as usize).enumerate() { 88 | client_env_vars[i] = string.as_ptr() as *const _; 89 | } 90 | 91 | std::cmp::max(max_len, env_vars.len() as c_uint) 92 | } 93 | 94 | #[no_mangle] 95 | pub extern "C" fn display_add_input_device(dpy: *mut WaylandDisplay, path: *const c_char) { 96 | let display = unsafe { &mut *dpy }; 97 | let path = unsafe { CStr::from_ptr(path) } 98 | .to_string_lossy() 99 | .into_owned(); 100 | 101 | display.add_input_device(path); 102 | } 103 | 104 | #[no_mangle] 105 | pub extern "C" fn display_set_video_info(dpy: *mut WaylandDisplay, info: *const GstVideoInfo) { 106 | let display = unsafe { &mut *dpy }; 107 | if info.is_null() { 108 | tracing::error!("Video Info is null"); 109 | } 110 | let video_info = unsafe { VideoInfo::from_glib_none(info) }; 111 | 112 | display.set_video_info(video_info.into()); 113 | } 114 | 115 | #[no_mangle] 116 | pub extern "C" fn display_keyboard_input(dpy: *mut WaylandDisplay, key: c_uint, pressed: bool) { 117 | let display = unsafe { &mut *dpy }; 118 | display.keyboard_input(key, pressed); 119 | } 120 | 121 | #[no_mangle] 122 | pub extern "C" fn display_pointer_motion(dpy: *mut WaylandDisplay, x: f64, y: f64) { 123 | let display = unsafe { &mut *dpy }; 124 | display.pointer_motion(x, y); 125 | } 126 | 127 | #[no_mangle] 128 | pub extern "C" fn display_pointer_motion_absolute(dpy: *mut WaylandDisplay, x: f64, y: f64) { 129 | let display = unsafe { &mut *dpy }; 130 | display.pointer_motion_absolute(x, y); 131 | } 132 | 133 | #[no_mangle] 134 | pub extern "C" fn display_pointer_button(dpy: *mut WaylandDisplay, button: c_uint, pressed: bool) { 135 | let display = unsafe { &mut *dpy }; 136 | display.pointer_button(button, pressed); 137 | } 138 | 139 | #[no_mangle] 140 | pub extern "C" fn display_pointer_axis(dpy: *mut WaylandDisplay, x: f64, y: f64) { 141 | let display = unsafe { &mut *dpy }; 142 | display.pointer_axis(x, y); 143 | } 144 | 145 | #[no_mangle] 146 | pub extern "C" fn display_touch_down(dpy: *mut WaylandDisplay, id: c_uint, rel_x: f64, rel_y: f64) { 147 | let display = unsafe { &mut *dpy }; 148 | display.touch_down(id, rel_x, rel_y); 149 | } 150 | 151 | #[no_mangle] 152 | pub extern "C" fn display_touch_up(dpy: *mut WaylandDisplay, id: c_uint) { 153 | let display = unsafe { &mut *dpy }; 154 | display.touch_up(id); 155 | } 156 | 157 | #[no_mangle] 158 | pub extern "C" fn display_touch_motion( 159 | dpy: *mut WaylandDisplay, 160 | id: c_uint, 161 | rel_x: f64, 162 | rel_y: f64, 163 | ) { 164 | let display = unsafe { &mut *dpy }; 165 | display.touch_motion(id, rel_x, rel_y); 166 | } 167 | 168 | #[no_mangle] 169 | pub extern "C" fn display_touch_cancel(dpy: *mut WaylandDisplay) { 170 | let display = unsafe { &mut *dpy }; 171 | display.touch_cancel(); 172 | } 173 | 174 | #[no_mangle] 175 | pub extern "C" fn display_touch_frame(dpy: *mut WaylandDisplay) { 176 | let display = unsafe { &mut *dpy }; 177 | display.touch_frame(); 178 | } 179 | 180 | #[no_mangle] 181 | pub extern "C" fn display_get_frame(dpy: *mut WaylandDisplay) -> *mut GstBuffer { 182 | let display = unsafe { &mut *dpy }; 183 | let _span = match display.tracer.as_ref() { 184 | Some(tracer) => Some(tracer.trace("display_get_frame")), 185 | None => None, 186 | }; 187 | match display.frame() { 188 | Ok(mut frame) => { 189 | let ptr = frame.make_mut().as_mut_ptr(); 190 | std::mem::forget(frame); 191 | ptr 192 | } 193 | Err(err) => { 194 | tracing::error!("Rendering error: {}", err); 195 | ptr::null_mut() 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /wayland-display-core/src/tests/fixture.rs: -------------------------------------------------------------------------------- 1 | use crate::comp::{ClientState, NixInterface, State}; 2 | use crate::tests::client::WaylandClient; 3 | use crate::utils::RenderTarget; 4 | use crate::utils::allocator::{GsBufferType, GsGlesbuffer}; 5 | use crate::utils::tests::INIT; 6 | use calloop::generic::Generic; 7 | use calloop::{EventLoop, Interest, Mode, PostAction}; 8 | use gst_video::VideoInfo; 9 | use smithay::backend::renderer::damage::OutputDamageTracker; 10 | use smithay::output; 11 | use smithay::output::{Output, PhysicalProperties, Subpixel}; 12 | use smithay::reexports::input::Libinput; 13 | use smithay::reexports::wayland_server::Display; 14 | use smithay::wayland::socket::ListeningSocketSource; 15 | use std::os::unix::net::UnixStream; 16 | use std::sync::Arc; 17 | use std::sync::atomic::Ordering; 18 | use std::time::Duration; 19 | 20 | pub struct Fixture { 21 | pub client: WaylandClient, 22 | pub server: State, 23 | server_event_loop: EventLoop<'static, State>, 24 | } 25 | 26 | impl Fixture { 27 | pub fn new() -> Self { 28 | INIT.call_once(|| { 29 | gst::init().expect("Failed to initialize GStreamer"); 30 | }); 31 | 32 | let libinput_context = Libinput::new_from_path(NixInterface); 33 | let event_loop = EventLoop::::try_new().expect("Unable to create event_loop"); 34 | let display = Display::::new().unwrap(); 35 | let dh = display.handle(); 36 | 37 | let server_state = State::new( 38 | &RenderTarget::Software, 39 | &dh, 40 | &libinput_context, 41 | event_loop.handle(), 42 | ); 43 | 44 | let source = ListeningSocketSource::new_auto().unwrap(); 45 | let socket_name = source.socket_name().to_string_lossy().into_owned(); 46 | tracing::info!(?socket_name, "Listening on wayland socket."); 47 | server_state 48 | .handle 49 | .insert_source(source, |client_stream, _, state| { 50 | if let Err(err) = state 51 | .dh 52 | .insert_client(client_stream, Arc::new(ClientState::default())) 53 | { 54 | tracing::error!(?err, "Error adding wayland client."); 55 | }; 56 | }) 57 | .expect("Failed to init wayland socket source"); 58 | 59 | server_state 60 | .handle 61 | .insert_source( 62 | Generic::new(display, Interest::READ, Mode::Level), 63 | |_, display, state| { 64 | // Safety: we don't drop the display 65 | unsafe { 66 | display.get_mut().dispatch_clients(state).unwrap(); 67 | } 68 | Ok(PostAction::Continue) 69 | }, 70 | ) 71 | .unwrap(); 72 | 73 | // Setup Wayland client 74 | let runtime_dir = std::env::var("XDG_RUNTIME_DIR").unwrap(); 75 | let wclient = WaylandClient::new( 76 | UnixStream::connect(format!("{}/{}", runtime_dir, socket_name)).unwrap(), 77 | ); 78 | 79 | let mut f = Fixture { 80 | client: wclient, 81 | server: server_state, 82 | server_event_loop: event_loop, 83 | }; 84 | 85 | f.create_server_output(); 86 | f.round_trip(); 87 | 88 | f 89 | } 90 | 91 | fn create_server_output(&mut self) { 92 | let output = self.server.output.get_or_insert_with(|| { 93 | let output = Output::new( 94 | "HEADLESS-1".into(), 95 | PhysicalProperties { 96 | make: "Virtual".into(), 97 | model: "Wolf".into(), 98 | size: (0, 0).into(), 99 | subpixel: Subpixel::Unknown, 100 | }, 101 | ); 102 | output.create_global::(&self.server.dh); 103 | output 104 | }); 105 | let mode = output::Mode { 106 | size: (320, 240).into(), 107 | refresh: 1000, // 1 FPS 108 | }; 109 | output.change_current_state(Some(mode), None, None, None); 110 | output.set_preferred(mode); 111 | let dtr = OutputDamageTracker::from_output(&output); 112 | 113 | self.server.space.map_output(&output, (0, 0)); 114 | self.server.dtr = Some(dtr); 115 | self.server 116 | .set_pointer_location((mode.size.w as f64 / 2.0, mode.size.h as f64 / 2.0).into()); 117 | 118 | let video_info = VideoInfo::builder( 119 | gst_video::VideoFormat::Rgba, 120 | mode.size.w as u32, 121 | mode.size.h as u32, 122 | ) 123 | .build() 124 | .unwrap(); 125 | let allocator = GsGlesbuffer::new(&mut self.server.renderer, video_info.clone()) 126 | .expect("Failed to create GsGlesbuffer"); 127 | self.server.output_buffer = Some(GsBufferType::RAW(allocator)); 128 | self.server.video_info = Some(video_info); 129 | } 130 | 131 | pub fn round_trip(&mut self) { 132 | let data = self.client.send_sync(); 133 | let mut run_times = 100; 134 | while !data.done.load(Ordering::Relaxed) && run_times > 0 { 135 | self.update_server(); 136 | self.update_client(); 137 | 138 | std::thread::sleep(Duration::from_millis(10)); 139 | run_times -= 1; 140 | } 141 | if run_times == 0 { 142 | panic!("Timeout establishing connection to wayland server!"); 143 | } 144 | } 145 | 146 | pub fn update_server(&mut self) { 147 | self.server 148 | .dh 149 | .flush_clients() 150 | .expect("Failed to flush clients"); 151 | self.server.space.refresh(); 152 | self.server_event_loop 153 | .dispatch(Duration::ZERO, &mut self.server) 154 | .unwrap(); 155 | } 156 | 157 | pub fn update_client(&mut self) { 158 | self.client.dispatch(); 159 | } 160 | 161 | pub fn create_window(&mut self, width: u16, height: u16) { 162 | self.client.create_window(); 163 | self.round_trip(); 164 | 165 | self.client.setup_window(width, height); 166 | self.round_trip(); 167 | self.round_trip(); 168 | 169 | // I don't know why this isn't triggered, but I've spent hours trying to debug why the 170 | // window size was kept at (0,0). Without this `under` in input.rs would never work 171 | for window in self.server.space.elements() { 172 | window.on_commit(); 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gst-wayland-display 2 | 3 | A micro Wayland compositor that can be used as a Gstreamer plugin. Based 4 | on [smithay](https://github.com/Smithay/smithay) 5 | 6 | ```mermaid 7 | flowchart LR 8 | WaylandComp["GST
Wayland
Comp"] 9 | AppLayer["App, Game,
Desktop, ..."] 10 | Pipeline["GStreamer
Pipeline"] 11 | 12 | WaylandComp -->|Wayland Socket| AppLayer 13 | WaylandComp -->|Raw
Framebuffer| Pipeline 14 | Pipeline -->|Mouse, Keyboard
Events| WaylandComp 15 | ``` 16 | 17 | ## Install 18 | 19 | see [cargo-c](https://github.com/lu-zero/cargo-c) 20 | 21 | ```bash 22 | git clone https://github.com/games-on-whales/gst-wayland-display.git 23 | cd gst-wayland-display 24 | # Install cargo-c if you don't have it already 25 | cargo install cargo-c 26 | # Build and install the plugin, by default under 27 | cargo cinstall --prefix=/usr/local 28 | ``` 29 | 30 | ## GStreamer plugin 31 | 32 | By default it'll install the plugin in `/usr/local/lib/gstreamer-1.0/libgstwaylanddisplaysrc.so`. 33 | 34 | You can check if the plugin is picked up by calling: 35 | 36 | ```bash 37 | GST_PLUGIN_PATH=/usr/local/lib/gstreamer-1.0 gst-inspect-1.0 waylanddisplaysrc 38 | ``` 39 | 40 | Example pipeline: 41 | 42 | ```bash 43 | GST_PLUGIN_PATH=/usr/local/lib/gstreamer-1.0 gst-launch-1.0 waylanddisplaysrc ! 'video/x-raw,width=1280,height=720,format=RGBx,framerate=60/1' ! autovideosink 44 | ``` 45 | 46 | If this starts you should have a wayland socket under `$XDG_RUNTIME_DIR` 47 | 48 | ``` 49 | ls $XDG_RUNTIME_DIR 50 | wayland-1 51 | wayland-1.lock 52 | ``` 53 | 54 | You should then be able to start any wayland process and use that socket 55 | 56 | ```bash 57 | WAYLAND_DISPLAY=wayland-1 weston-simple-egl 58 | ``` 59 | 60 | ## Zero copy pipeline support 61 | 62 | This plugin supports outputting **DMA buffers** in order to achieve a proper **zero-copy pipeline**. 63 | It'll negotiate the proper caps with downstream elements using Gstreamer, you can read more about 64 | it [in the official docs](https://gstreamer.freedesktop.org/documentation/additional/design/dmabuf.html?gi-language=c). 65 | 66 | Example pipelines: 67 | 68 | - AMD/Intel 69 | 70 | ```bash 71 | gst-launch-1.0 waylanddisplaysrc ! 'video/x-raw(memory:DMABuf),width=1920,height=1080,framerate=60/1' ! vapostproc ! 'video/x-raw(memory:VAMemory), format=NV12' ! vah265enc ! vah265dec ! autovideosink 72 | ``` 73 | 74 | - Nvidia (or see below for an even better pipeline that uses CUDAMemory) 75 | 76 | ```bash 77 | gst-launch-1.0 waylanddisplaysrc ! 'video/x-raw(memory:DMABuf),width=1920,height=1080,framerate=60/1' ! glupload ! glcolorconvert ! 'video/x-raw(memory:GLMemory), format=NV12' ! nvh265enc ! nvh265dec ! autovideosink 78 | ``` 79 | 80 | ### Support for Nvidia CUDAMemory 81 | 82 | > [!IMPORTANT] 83 | > This is gated behind the `cuda` feature flag. To enable it, you'll need to install `gst-cuda-1.0` and have access to 84 | > `libcuda.so` at runtime. 85 | 86 | This plugin supports outputting **CUDA buffers** for low-latency Nvidia pipelines. By not using `glupload` and 87 | `glcolorconvert` we can not only be way more efficient, but it'll also allow us to re-use the GL context that we already 88 | have in Smithay. 89 | 90 | It can negotiate the CUDA Context with other elements in the pipeline, or it can be a source for other elements when 91 | setting the property`cuda-device-id`. This is particularly useful when running in a multi-GPU environment as it gives 92 | you full control over which GPU is used. 93 | 94 | Example pipeline: 95 | 96 | ```bash 97 | gst-launch-1.0 waylanddisplaysrc cuda-device-id=0 ! 'video/x-raw(memory:CUDAMemory),width=1920,height=1080,framerate=60/1' ! nvh265enc ! nvh265dec ! autovideosink 98 | ``` 99 | 100 | In order to support this we leverage `gst-cuda-1.0` which adds a single build dependency to this project. 101 | At runtime, you'll need to have access to `libcuda.so` but only to access and use `CUDAMemory`; you can still use 102 | `DMABuf` when running this plugin on a platform that doesn't support it. 103 | 104 | ## Run without a GPU 105 | 106 | If you don't have a GPU, you can still run this plugin without it; just use the option `render_node=software` to enable it. Example pipeline: 107 | ```bash 108 | gst-launch-1.0 waylanddisplaysrc render_node=software ! 'video/x-raw,width=1280,height=720,format=RGBx,framerate=60/1' ! videoconvert ! autovideosink 109 | ``` 110 | 111 | ## Mouse and Keyboard 112 | 113 | To pass mouse and keyboard events to the Wayland compositor you have two options: 114 | - Pass two valid input devices to the plugin using the `keyboard` or `mouse` options (ex: `waylanddisplaysrc keyboard=/dev/input/event20 mouse=/dev/input/event21`) 115 | - Send the inputs as raw events to the Gstreamer pipeline 116 | 117 | For example, here's [how Wolf sends the mouse movements](https://github.com/games-on-whales/wolf/blob/b4c4571b061cd243a4606bd16969c235767d6ec2/src/core/src/platforms/linux/virtual-display/gst-wayland-display.cpp#L45-L53) to gst-wayland-comp: 118 | ```c++ 119 | auto msg = gst_structure_new("MouseMoveRelative", 120 | "pointer_x", G_TYPE_DOUBLE, static_cast(delta_x), 121 | "pointer_y", G_TYPE_DOUBLE, static_cast(delta_y), 122 | NULL); 123 | gstreamer::send_message(w_state->wayland_plugin.get(), msg); 124 | ``` 125 | 126 | Mouse, Keyboard and Touch events are supported by the plugin. 127 | 128 | 129 | ## C Bindings 130 | 131 | CmakeLists.txt 132 | 133 | ```cmake 134 | pkg_check_modules(libgstwaylanddisplay REQUIRED IMPORTED_TARGET libgstwaylanddisplay) 135 | target_link_libraries( PUBLIC PkgConfig::libgstwaylanddisplay) 136 | ``` 137 | 138 | Include in your code: 139 | 140 | ```c 141 | #include 142 | ``` 143 | 144 | Example usage: 145 | 146 | ```c++ 147 | auto w_state = display_init("/dev/dri/renderD128"); // Pass a render node 148 | 149 | display_add_input_device(w_state, "/dev/input/event20"); // Mouse 150 | display_add_input_device(w_state, "/dev/input/event21"); // Keyboard 151 | 152 | // Setting video as 1920x1080@60 153 | auto video_info = gst_caps_new_simple("video/x-raw", 154 | "width", G_TYPE_INT, 1920, 155 | "height", G_TYPE_INT, 1080, 156 | "framerate", GST_TYPE_FRACTION, 60, 1, 157 | "format", G_TYPE_STRING, "RGBx", 158 | NULL); 159 | display_set_video_info(w_state, video_info); 160 | 161 | // Get a list of the devices needed, ex: ["/dev/dri/renderD128", "/dev/dri/card0"] 162 | auto n_devices = display_get_devices_len(w_state); 163 | const char *devs[n_devices]; 164 | display_get_devices(w_state, devs, n_devices); 165 | 166 | // Get a list of the env vars needed, notably the wayland socket 167 | // ex: ["WAYLAND_DISPLAY=wayland-1"] 168 | auto n_envs = display_get_envvars_len(w_state); 169 | const char *envs[n_envs]; 170 | display_get_envvars(w_state, envs, n_envs); 171 | 172 | // Example of polling for new video data 173 | GstBuffer * v_buffer; 174 | while(true){ 175 | v_buffer = display_get_frame(w_state); 176 | // TODO: do something with the video data 177 | } 178 | 179 | display_finish(w_state); // Cleanup 180 | ``` -------------------------------------------------------------------------------- /wayland-display-core/resources/protocols/wayland-drm.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Copyright © 2008-2011 Kristian Høgsberg 6 | Copyright © 2010-2011 Intel Corporation 7 | 8 | Permission to use, copy, modify, distribute, and sell this 9 | software and its documentation for any purpose is hereby granted 10 | without fee, provided that\n the above copyright notice appear in 11 | all copies and that both that copyright notice and this permission 12 | notice appear in supporting documentation, and that the name of 13 | the copyright holders not be used in advertising or publicity 14 | pertaining to distribution of the software without specific, 15 | written prior permission. The copyright holders make no 16 | representations about the suitability of this software for any 17 | purpose. It is provided "as is" without express or implied 18 | warranty. 19 | 20 | THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS 21 | SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 22 | FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 23 | SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 24 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 25 | AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 26 | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 27 | THIS SOFTWARE. 28 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 111 | 112 | 113 | 114 | 115 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | Bitmask of capabilities. 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /wayland-display-core/src/wayland/protocols/wl_drm.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | 3 | // Re-export only the actual code, and then only use this re-export 4 | // The `generated` module below is just some boilerplate to properly isolate stuff 5 | // and avoid exposing internal details. 6 | // 7 | // You can use all the types from my_protocol as if they went from `wayland_client::protocol`. 8 | pub use generated::wl_drm; 9 | 10 | mod generated { 11 | use smithay::reexports::wayland_server::{self, protocol::*}; 12 | 13 | pub mod __interfaces { 14 | use smithay::reexports::wayland_server::protocol::__interfaces::*; 15 | use wayland_backend; 16 | wayland_scanner::generate_interfaces!("resources/protocols/wayland-drm.xml"); 17 | } 18 | 19 | use self::__interfaces::*; 20 | wayland_scanner::generate_server_code!("resources/protocols/wayland-drm.xml"); 21 | } 22 | 23 | use smithay::{ 24 | backend::allocator::{ 25 | Format, Fourcc, Modifier, 26 | dmabuf::{Dmabuf, DmabufFlags}, 27 | }, 28 | reexports::wayland_server::{ 29 | Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, 30 | backend::GlobalId, protocol::wl_buffer::WlBuffer, 31 | }, 32 | wayland::{ 33 | buffer::BufferHandler, 34 | dmabuf::{DmabufGlobal, DmabufHandler}, 35 | }, 36 | }; 37 | 38 | use std::{convert::TryFrom, path::PathBuf, sync::Arc}; 39 | 40 | pub struct WlDrmState; 41 | 42 | /// Data associated with a drm global. 43 | pub struct DrmGlobalData { 44 | filter: Box Fn(&'a Client) -> bool + Send + Sync>, 45 | formats: Arc>, 46 | device_path: PathBuf, 47 | dmabuf_global: DmabufGlobal, 48 | } 49 | 50 | pub struct DrmInstanceData { 51 | formats: Arc>, 52 | dmabuf_global: DmabufGlobal, 53 | } 54 | 55 | pub enum ImportError { 56 | Failed, 57 | } 58 | 59 | #[allow(dead_code)] 60 | pub trait DrmHandler { 61 | fn dmabuf_imported(&mut self, global: &DmabufGlobal, dmabuf: Dmabuf) -> Result; 62 | fn buffer_created(&mut self, buffer: WlBuffer, result: R) { 63 | let _ = (buffer, result); 64 | } 65 | } 66 | 67 | impl GlobalDispatch for WlDrmState 68 | where 69 | D: GlobalDispatch 70 | + Dispatch 71 | + BufferHandler 72 | + DmabufHandler 73 | + 'static, 74 | { 75 | fn bind( 76 | _state: &mut D, 77 | _dh: &DisplayHandle, 78 | _client: &Client, 79 | resource: New, 80 | global_data: &DrmGlobalData, 81 | data_init: &mut DataInit<'_, D>, 82 | ) { 83 | let data = DrmInstanceData { 84 | formats: global_data.formats.clone(), 85 | dmabuf_global: global_data.dmabuf_global.clone(), 86 | }; 87 | let drm_instance = data_init.init(resource, data); 88 | 89 | drm_instance.device(global_data.device_path.to_string_lossy().into_owned()); 90 | if drm_instance.version() >= 2 { 91 | drm_instance.capabilities(wl_drm::Capability::Prime as u32); 92 | } 93 | for format in global_data.formats.iter() { 94 | if let Ok(converted) = wl_drm::Format::try_from(*format as u32) { 95 | drm_instance.format(converted as u32); 96 | } 97 | } 98 | } 99 | 100 | fn can_view(client: Client, global_data: &DrmGlobalData) -> bool { 101 | (global_data.filter)(&client) 102 | } 103 | } 104 | 105 | impl Dispatch for WlDrmState 106 | where 107 | D: GlobalDispatch 108 | + Dispatch 109 | + Dispatch 110 | + BufferHandler 111 | + DmabufHandler 112 | + DrmHandler<()> 113 | + 'static, 114 | { 115 | fn request( 116 | state: &mut D, 117 | _client: &Client, 118 | drm: &wl_drm::WlDrm, 119 | request: wl_drm::Request, 120 | data: &DrmInstanceData, 121 | _dh: &DisplayHandle, 122 | data_init: &mut DataInit<'_, D>, 123 | ) { 124 | match request { 125 | wl_drm::Request::Authenticate { .. } => drm.authenticated(), 126 | wl_drm::Request::CreateBuffer { .. } => drm.post_error( 127 | wl_drm::Error::InvalidName, 128 | String::from("Flink handles are unsupported, use PRIME"), 129 | ), 130 | wl_drm::Request::CreatePlanarBuffer { .. } => drm.post_error( 131 | wl_drm::Error::InvalidName, 132 | String::from("Flink handles are unsupported, use PRIME"), 133 | ), 134 | wl_drm::Request::CreatePrimeBuffer { 135 | id, 136 | name, 137 | width, 138 | height, 139 | format, 140 | offset0, 141 | stride0, 142 | .. 143 | } => { 144 | let format = match Fourcc::try_from(format) { 145 | Ok(format) => { 146 | if !data.formats.contains(&format) { 147 | drm.post_error( 148 | wl_drm::Error::InvalidFormat, 149 | String::from("Format not advertised by wl_drm"), 150 | ); 151 | return; 152 | } 153 | format 154 | } 155 | Err(_) => { 156 | drm.post_error( 157 | wl_drm::Error::InvalidFormat, 158 | String::from("Format unknown / not advertised by wl_drm"), 159 | ); 160 | return; 161 | } 162 | }; 163 | 164 | if width < 1 || height < 1 { 165 | drm.post_error( 166 | wl_drm::Error::InvalidFormat, 167 | String::from("width or height not positive"), 168 | ); 169 | return; 170 | } 171 | 172 | let mut dma = Dmabuf::builder( 173 | (width, height), 174 | format, 175 | Modifier::Invalid, 176 | DmabufFlags::empty(), 177 | ); 178 | dma.add_plane(name, 0, offset0 as u32, stride0 as u32); 179 | match dma.build() { 180 | Some(dmabuf) => { 181 | match DrmHandler::dmabuf_imported( 182 | state, 183 | &data.dmabuf_global, 184 | dmabuf.clone(), 185 | ) { 186 | Ok(_) => { 187 | // import was successful 188 | data_init.init(id, dmabuf); 189 | } 190 | 191 | Err(ImportError::Failed) => { 192 | // Buffer import failed. The protocol documentation heavily implies killing the 193 | // client is the right thing to do here. 194 | drm.post_error(wl_drm::Error::InvalidName, "buffer import failed"); 195 | } 196 | } 197 | } 198 | None => { 199 | // Buffer import failed. The protocol documentation heavily implies killing the 200 | // client is the right thing to do here. 201 | drm.post_error( 202 | wl_drm::Error::InvalidName, 203 | "dmabuf global was destroyed on server", 204 | ); 205 | } 206 | } 207 | } 208 | } 209 | } 210 | } 211 | 212 | pub fn create_drm_global( 213 | display: &DisplayHandle, 214 | device_path: PathBuf, 215 | formats: Vec, 216 | dmabuf_global: &DmabufGlobal, 217 | ) -> GlobalId 218 | where 219 | D: GlobalDispatch 220 | + Dispatch 221 | + BufferHandler 222 | + DmabufHandler 223 | + 'static, 224 | { 225 | create_drm_global_with_filter::(display, device_path, formats, dmabuf_global, |_| true) 226 | } 227 | 228 | pub fn create_drm_global_with_filter( 229 | display: &DisplayHandle, 230 | device_path: PathBuf, 231 | formats: Vec, 232 | dmabuf_global: &DmabufGlobal, 233 | client_filter: F, 234 | ) -> GlobalId 235 | where 236 | D: GlobalDispatch 237 | + Dispatch 238 | + BufferHandler 239 | + DmabufHandler 240 | + 'static, 241 | F: for<'a> Fn(&'a Client) -> bool + Send + Sync + 'static, 242 | { 243 | let formats = Arc::new( 244 | formats 245 | .into_iter() 246 | .filter(|f| f.modifier == Modifier::Invalid) 247 | .map(|f| f.code) 248 | .collect(), 249 | ); 250 | let data = DrmGlobalData { 251 | filter: Box::new(client_filter), 252 | formats, 253 | device_path, 254 | dmabuf_global: dmabuf_global.clone(), 255 | }; 256 | 257 | display.create_global::(2, data) 258 | } 259 | 260 | macro_rules! delegate_wl_drm { 261 | ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { 262 | smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ 263 | $crate::wayland::protocols::wl_drm::wl_drm::WlDrm: $crate::wayland::protocols::wl_drm::DrmGlobalData 264 | ] => $crate::wayland::protocols::wl_drm::WlDrmState); 265 | smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ 266 | $crate::wayland::protocols::wl_drm::wl_drm::WlDrm: $crate::wayland::protocols::wl_drm::DrmInstanceData 267 | ] => $crate::wayland::protocols::wl_drm::WlDrmState); 268 | }; 269 | } 270 | pub(crate) use delegate_wl_drm; 271 | -------------------------------------------------------------------------------- /wayland-display-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | use smithay::backend::SwapBuffersError; 2 | use smithay::backend::drm::CreateDrmNodeError; 3 | pub use smithay::reexports::calloop::channel::{Channel, Sender, channel}; 4 | 5 | #[cfg(feature = "cuda")] 6 | use crate::utils::allocator::cuda::CUDABufferPool; 7 | use crate::utils::device::gpu::GPUDevice; 8 | pub use smithay::backend::allocator::{ 9 | Format as DrmFormat, Fourcc, Modifier as DrmModifier, Vendor as DrmVendor, format::FormatSet, 10 | }; 11 | pub use smithay::backend::input::{ButtonState, KeyState}; 12 | use smithay::utils::{Logical, Point}; 13 | use std::ffi::{CString, c_char, c_void}; 14 | use std::str::FromStr; 15 | use std::sync::mpsc::{self, Receiver, SyncSender}; 16 | use std::sync::{Arc, Mutex}; 17 | use std::thread::JoinHandle; 18 | use utils::RenderTarget; 19 | 20 | pub(crate) mod comp; 21 | #[cfg(test)] 22 | mod tests; 23 | pub mod utils; 24 | pub(crate) mod wayland; 25 | 26 | pub use crate::utils::video_info::GstVideoInfo; 27 | 28 | pub enum Command { 29 | InputDevice(String), 30 | VideoInfo(GstVideoInfo), 31 | Buffer( 32 | SyncSender>, 33 | Option, 34 | ), 35 | #[cfg(feature = "cuda")] 36 | UpdateCUDABufferPool(Arc>>), 37 | KeyboardInput(u32, KeyState), 38 | PointerMotion(Point), 39 | PointerMotionAbsolute(Point), 40 | PointerButton(u32, ButtonState), 41 | PointerAxis(f64, f64), 42 | GetSupportedDmaFormats(SyncSender), 43 | GetRenderDevice(SyncSender>), 44 | TouchDown(u32, Point), 45 | TouchUp(u32), 46 | TouchMotion(u32, Point), 47 | TouchCancel, 48 | TouchFrame, 49 | Quit, 50 | } 51 | 52 | #[derive(Clone)] 53 | pub struct Tracer { 54 | start_fn: extern "C" fn(*const c_char) -> *mut c_void, 55 | end_fn: extern "C" fn(*mut c_void), 56 | } 57 | 58 | pub struct Trace { 59 | ctx: *mut c_void, 60 | end_fn: extern "C" fn(*mut c_void), 61 | } 62 | 63 | impl Tracer { 64 | pub fn new( 65 | start_fn: extern "C" fn(*const c_char) -> *mut c_void, 66 | end_fn: extern "C" fn(*mut c_void), 67 | ) -> Self { 68 | Tracer { start_fn, end_fn } 69 | } 70 | 71 | pub fn trace(&self, name: &str) -> Trace { 72 | let trace_name = CString::new(name).unwrap(); 73 | let ctx = (self.start_fn)(trace_name.as_ptr()); 74 | Trace::new(ctx, self.end_fn) 75 | } 76 | } 77 | 78 | impl Trace { 79 | pub fn new(ctx: *mut c_void, end_fn: extern "C" fn(*mut c_void)) -> Self { 80 | Trace { ctx, end_fn } 81 | } 82 | } 83 | 84 | impl Drop for Trace { 85 | fn drop(&mut self) { 86 | (self.end_fn)(self.ctx); 87 | } 88 | } 89 | 90 | pub struct WaylandDisplay { 91 | thread_handle: Option>, 92 | command_tx: Sender, 93 | 94 | pub tracer: Option, 95 | pub devices: MaybeRecv>, 96 | pub envs: MaybeRecv>, 97 | } 98 | 99 | pub enum MaybeRecv { 100 | Rx(Receiver), 101 | Value(T), 102 | } 103 | 104 | impl MaybeRecv { 105 | pub fn get(&mut self) -> &T { 106 | match self { 107 | MaybeRecv::Rx(recv) => { 108 | let value = recv.recv().unwrap(); 109 | *self = MaybeRecv::Value(value.clone()); 110 | self.get() 111 | } 112 | MaybeRecv::Value(val) => val, 113 | } 114 | } 115 | } 116 | 117 | impl WaylandDisplay { 118 | pub fn new(render_node: Option) -> Result { 119 | let (channel_tx, channel_rx) = std::sync::mpsc::sync_channel(0); 120 | let (devices_tx, devices_rx) = std::sync::mpsc::channel(); 121 | let (envs_tx, envs_rx) = std::sync::mpsc::channel(); 122 | let render_target = RenderTarget::from_str( 123 | &render_node.unwrap_or_else(|| String::from("/dev/dri/renderD128")), 124 | )?; 125 | 126 | let thread_handle = std::thread::spawn(move || { 127 | if let Err(err) = std::panic::catch_unwind(|| { 128 | // calloops channel is not "UnwindSafe", but the std channel is... *sigh* lets workaround it creatively 129 | let (command_tx, command_src) = smithay::reexports::calloop::channel::channel(); 130 | channel_tx.send(command_tx).unwrap(); 131 | comp::init(command_src, render_target, devices_tx, envs_tx); 132 | }) { 133 | tracing::error!(?err, "Compositor thread panic'ed!"); 134 | } 135 | }); 136 | let command_tx = channel_rx.recv().unwrap(); 137 | 138 | Ok(WaylandDisplay { 139 | thread_handle: Some(thread_handle), 140 | command_tx, 141 | tracer: None, 142 | devices: MaybeRecv::Rx(devices_rx), 143 | envs: MaybeRecv::Rx(envs_rx), 144 | }) 145 | } 146 | 147 | pub fn new_with_channel( 148 | render_node: Option, 149 | command_tx: Sender, 150 | commands_rx: Channel, 151 | ) -> Result { 152 | let (devices_tx, devices_rx) = std::sync::mpsc::channel(); 153 | let (envs_tx, envs_rx) = std::sync::mpsc::channel(); 154 | let render_target = RenderTarget::from_str( 155 | &render_node.unwrap_or_else(|| String::from("/dev/dri/renderD128")), 156 | )?; 157 | 158 | let thread_handle = std::thread::spawn(move || { 159 | comp::init(commands_rx, render_target, devices_tx, envs_tx); 160 | }); 161 | 162 | Ok(WaylandDisplay { 163 | thread_handle: Some(thread_handle), 164 | command_tx, 165 | tracer: None, 166 | devices: MaybeRecv::Rx(devices_rx), 167 | envs: MaybeRecv::Rx(envs_rx), 168 | }) 169 | } 170 | 171 | pub fn devices(&mut self) -> impl Iterator { 172 | self.devices 173 | .get() 174 | .iter() 175 | .map(|string| string.to_str().unwrap()) 176 | } 177 | 178 | pub fn env_vars(&mut self) -> impl Iterator { 179 | self.envs 180 | .get() 181 | .iter() 182 | .map(|string| string.to_str().unwrap()) 183 | } 184 | 185 | pub fn add_input_device(&self, path: impl Into) { 186 | let _ = self.command_tx.send(Command::InputDevice(path.into())); 187 | } 188 | 189 | pub fn set_video_info(&self, info: GstVideoInfo) { 190 | let _ = self.command_tx.send(Command::VideoInfo(info)); 191 | } 192 | 193 | pub fn keyboard_input(&self, key: u32, pressed: bool) { 194 | let state = if pressed { 195 | KeyState::Pressed 196 | } else { 197 | KeyState::Released 198 | }; 199 | let _ = self.command_tx.send(Command::KeyboardInput(key, state)); 200 | } 201 | 202 | pub fn pointer_motion(&self, x: f64, y: f64) { 203 | let _ = self.command_tx.send(Command::PointerMotion((x, y).into())); 204 | } 205 | 206 | pub fn pointer_motion_absolute(&self, x: f64, y: f64) { 207 | let _ = self 208 | .command_tx 209 | .send(Command::PointerMotionAbsolute((x, y).into())); 210 | } 211 | 212 | pub fn pointer_button(&self, button: u32, pressed: bool) { 213 | let state = if pressed { 214 | ButtonState::Pressed 215 | } else { 216 | ButtonState::Released 217 | }; 218 | let _ = self.command_tx.send(Command::PointerButton(button, state)); 219 | } 220 | 221 | pub fn pointer_axis(&self, x: f64, y: f64) { 222 | let _ = self.command_tx.send(Command::PointerAxis(x, y)); 223 | } 224 | 225 | pub fn touch_down(&self, id: u32, rel_x: f64, rel_y: f64) { 226 | let _ = self 227 | .command_tx 228 | .send(Command::TouchDown(id, (rel_x, rel_y).into())); 229 | } 230 | 231 | pub fn touch_up(&self, id: u32) { 232 | let _ = self.command_tx.send(Command::TouchUp(id)); 233 | } 234 | 235 | pub fn touch_motion(&self, id: u32, rel_x: f64, rel_y: f64) { 236 | let _ = self 237 | .command_tx 238 | .send(Command::TouchMotion(id, (rel_x, rel_y).into())); 239 | } 240 | 241 | pub fn touch_cancel(&self) { 242 | let _ = self.command_tx.send(Command::TouchCancel); 243 | } 244 | 245 | pub fn touch_frame(&self) { 246 | let _ = self.command_tx.send(Command::TouchFrame); 247 | } 248 | 249 | pub fn frame(&self) -> Result { 250 | let (buffer_tx, buffer_rx) = mpsc::sync_channel(0); 251 | if let Err(err) = self 252 | .command_tx 253 | .send(Command::Buffer(buffer_tx, self.tracer.clone())) 254 | { 255 | tracing::warn!(?err, "Failed to send buffer command."); 256 | return Err(gst::FlowError::Eos); 257 | } 258 | 259 | match buffer_rx.recv() { 260 | Ok(Ok(buffer)) => Ok(buffer), 261 | Ok(Err(err)) => match err { 262 | SwapBuffersError::AlreadySwapped => unreachable!(), 263 | SwapBuffersError::ContextLost(_) => Err(gst::FlowError::Eos), 264 | SwapBuffersError::TemporaryFailure(_) => Err(gst::FlowError::Error), 265 | }, 266 | Err(err) => { 267 | tracing::warn!(?err, "Failed to recv buffer ack."); 268 | Err(gst::FlowError::Error) 269 | } 270 | } 271 | } 272 | 273 | pub fn get_supported_dma_formats(&self) -> FormatSet { 274 | let (buffer_tx, buffer_rx) = mpsc::sync_channel(0); 275 | let _ = self 276 | .command_tx 277 | .send(Command::GetSupportedDmaFormats(buffer_tx)); 278 | buffer_rx.recv().unwrap() 279 | } 280 | 281 | pub fn get_render_device(&self) -> Option { 282 | let (buffer_tx, buffer_rx) = mpsc::sync_channel(0); 283 | let _ = self.command_tx.send(Command::GetRenderDevice(buffer_tx)); 284 | buffer_rx.recv().unwrap() 285 | } 286 | } 287 | 288 | impl Drop for WaylandDisplay { 289 | fn drop(&mut self) { 290 | if let Err(err) = self.command_tx.send(Command::Quit) { 291 | tracing::warn!("Failed to send stop command: {}", err); 292 | return; 293 | }; 294 | if self.thread_handle.take().unwrap().join().is_err() { 295 | tracing::warn!("Failed to join compositor thread"); 296 | }; 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /wayland-display-core/src/tests/test_pointer.rs: -------------------------------------------------------------------------------- 1 | use crate::tests::client::MouseEvents; 2 | use crate::tests::fixture::Fixture; 3 | use smithay::utils::Point; 4 | use test_log::test; 5 | use wayland_client::protocol::wl_pointer; 6 | use wayland_protocols::wp::relative_pointer::zv1::client::zwp_relative_pointer_v1; 7 | 8 | fn clean_events(events: &mut Vec) { 9 | while let Some(_event) = events.pop() {} 10 | } 11 | 12 | #[test] 13 | fn move_mouse() { 14 | let mut f = Fixture::new(); 15 | f.round_trip(); 16 | f.create_window(320, 240); 17 | 18 | let expected_location = Point::from((0.0, 0.0)); 19 | f.server.pointer_motion_absolute(0, expected_location); 20 | f.round_trip(); 21 | 22 | { 23 | // Server logic test 24 | assert_eq!(f.server.pointer_location, expected_location); 25 | 26 | // Client logic test 27 | let client_events = f.client.get_client_events(); 28 | assert!(client_events.len() >= 1); 29 | let MouseEvents::Pointer(client_event) = client_events.remove(0) else { 30 | panic!("Unexpected event: {:?}", client_events); 31 | }; 32 | let wl_pointer::Event::Enter { 33 | // First time, we are entering the window 34 | surface_x, 35 | surface_y, 36 | .. 37 | } = client_event 38 | else { 39 | panic!("Unexpected event: {:?}", client_event); 40 | }; 41 | assert_eq!(surface_x, expected_location.x); 42 | assert_eq!(surface_y, expected_location.y); 43 | 44 | clean_events(client_events); 45 | } 46 | 47 | let delta = Point::from((10.0, 15.0)); 48 | f.server.pointer_motion(0, 0, delta, delta); 49 | f.round_trip(); 50 | 51 | { 52 | // Server logic test 53 | assert_eq!(f.server.pointer_location, expected_location + delta); 54 | 55 | // Client logic test 56 | let client_events = f.client.get_client_events(); 57 | assert!(client_events.len() >= 1); 58 | let MouseEvents::Pointer(client_event) = client_events.remove(0) else { 59 | panic!("Unexpected event: {:?}", client_events); 60 | }; 61 | let wl_pointer::Event::Motion { 62 | // Second time, we are moving thru it 63 | surface_x, 64 | surface_y, 65 | .. 66 | } = client_event 67 | else { 68 | panic!("Unexpected event: {:?}", client_event); 69 | }; 70 | assert_eq!(surface_x, delta.x); 71 | assert_eq!(surface_y, delta.y); 72 | 73 | clean_events(client_events); 74 | } 75 | } 76 | 77 | #[test] 78 | fn lock_mouse() { 79 | let mut f = Fixture::new(); 80 | f.round_trip(); 81 | f.create_window(320, 240); 82 | 83 | let expected_location = Point::from((15.0, 45.0)); 84 | f.server.pointer_motion_absolute(0, expected_location); 85 | f.round_trip(); 86 | { 87 | let client_events = f.client.get_client_events(); 88 | assert!(client_events.len() >= 1); 89 | clean_events(client_events); 90 | } 91 | 92 | let _lock = f.client.lock_pointer(); 93 | let _relative_pointer = f.client.get_relative_pointer(); 94 | f.round_trip(); 95 | 96 | // Test pointer_motion() 97 | let delta = Point::from((10.0, 15.0)); 98 | f.server.pointer_motion(0, 0, delta, delta); 99 | f.round_trip(); 100 | { 101 | // Mouse shouldn't be moved! 102 | assert_eq!(f.server.pointer_location, expected_location); 103 | 104 | // But we should still get Relative mouse events 105 | let client_events = f.client.get_client_events(); 106 | assert!(client_events.len() >= 2); 107 | let MouseEvents::Relative(client_event) = client_events.remove(0) else { 108 | panic!("Unexpected event: {:?}", client_events); 109 | }; 110 | let zwp_relative_pointer_v1::Event::RelativeMotion { dx, dy, .. } = client_event else { 111 | panic!("Unexpected event: {:?}", client_event); 112 | }; 113 | assert_eq!(dx, delta.x); 114 | assert_eq!(dy, delta.y); 115 | 116 | // And no Pointer Motion events 117 | while let Some(event) = client_events.pop() { 118 | match event { 119 | MouseEvents::Pointer(p_event) => match p_event { 120 | wl_pointer::Event::Motion { .. } => panic!("Unexpected event: {:?}", p_event), 121 | _ => {} 122 | }, 123 | _ => {} 124 | } 125 | } 126 | } 127 | 128 | // Test pointer_motion_absolute() 129 | let absolute_position = Point::from((100.0, 150.0)); 130 | f.server.pointer_motion_absolute(0, absolute_position); 131 | f.round_trip(); 132 | 133 | { 134 | // Mouse shouldn't be moved! 135 | assert_eq!(f.server.pointer_location, expected_location); 136 | 137 | // But we should still get Relative mouse events 138 | let client_events = f.client.get_client_events(); 139 | assert!(client_events.len() >= 2); 140 | let MouseEvents::Relative(client_event) = client_events.remove(0) else { 141 | panic!("Unexpected event: {:?}", client_events); 142 | }; 143 | let zwp_relative_pointer_v1::Event::RelativeMotion { dx, dy, .. } = client_event else { 144 | panic!("Unexpected event: {:?}", client_event); 145 | }; 146 | assert_eq!(dx, absolute_position.x - f.server.pointer_location.x); 147 | assert_eq!(dy, absolute_position.y - f.server.pointer_location.y); 148 | 149 | // And no Pointer Motion events 150 | while let Some(event) = client_events.pop() { 151 | match event { 152 | MouseEvents::Pointer(p_event) => match p_event { 153 | wl_pointer::Event::Motion { .. } => panic!("Unexpected event: {:?}", p_event), 154 | _ => {} 155 | }, 156 | _ => {} 157 | } 158 | } 159 | } 160 | } 161 | 162 | #[test] 163 | fn confine_mouse_absolute_movement() { 164 | let mut f = Fixture::new(); 165 | f.round_trip(); 166 | f.create_window(320, 240); 167 | 168 | // We start with the pointer at (0,0) 169 | let expected_location = Point::from((0.0, 0.0)); 170 | f.server.pointer_motion_absolute(0, expected_location); 171 | f.round_trip(); 172 | 173 | { 174 | let client_events = f.client.get_client_events(); 175 | assert!(client_events.len() >= 1); 176 | clean_events(client_events); 177 | } 178 | 179 | // We create a confine region in the south right corner 180 | let _confine = f.client.confine_pointer(200, 100, 120, 140); 181 | let _relative_pointer = f.client.get_relative_pointer(); 182 | f.round_trip(); 183 | 184 | let outside_position = Point::from((50.0, 50.0)); 185 | f.server.pointer_motion_absolute(0, outside_position); 186 | f.round_trip(); 187 | { 188 | // The confinement shouldn't be active, we aren't in the region 189 | assert!(!f.client.is_confined()); 190 | 191 | // The pointer has been moved correctly 192 | assert_eq!(f.server.pointer_location, outside_position); 193 | 194 | // Pointer Motion and Relative Motion events have been triggered 195 | let client_events = f.client.get_client_events(); 196 | assert_eq!(client_events.len(), 3); // motion, relative_motion, frame 197 | while let Some(event) = client_events.pop() { 198 | match event { 199 | MouseEvents::Pointer(p_event) => match p_event { 200 | wl_pointer::Event::Motion { 201 | surface_x, 202 | surface_y, 203 | .. 204 | } => { 205 | assert_eq!(surface_x, outside_position.x); 206 | assert_eq!(surface_y, outside_position.y); 207 | } 208 | _ => {} 209 | }, 210 | MouseEvents::Relative(r_event) => match r_event { 211 | zwp_relative_pointer_v1::Event::RelativeMotion { dx, dy, .. } => { 212 | assert_eq!(dx, outside_position.x); 213 | assert_eq!(dy, outside_position.y); 214 | } 215 | _ => {} 216 | }, 217 | } 218 | } 219 | } 220 | 221 | // Let's now move to the confined region 222 | let inside_position = Point::from((250.0, 150.0)); 223 | f.server.pointer_motion_absolute(0, inside_position); 224 | f.round_trip(); 225 | { 226 | // The confinement should have been activated now 227 | assert!(f.client.is_confined()); 228 | 229 | // The pointer has been moved correctly 230 | assert_eq!(f.server.pointer_location, inside_position); 231 | 232 | // Pointer Motion and Relative Motion events have been triggered 233 | let client_events = f.client.get_client_events(); 234 | assert_eq!(client_events.len(), 3); // motion, relative_motion, frame 235 | while let Some(event) = client_events.pop() { 236 | match event { 237 | MouseEvents::Pointer(p_event) => match p_event { 238 | wl_pointer::Event::Motion { 239 | surface_x, 240 | surface_y, 241 | .. 242 | } => { 243 | assert_eq!(surface_x, inside_position.x); 244 | assert_eq!(surface_y, inside_position.y); 245 | } 246 | _ => {} 247 | }, 248 | MouseEvents::Relative(r_event) => match r_event { 249 | zwp_relative_pointer_v1::Event::RelativeMotion { dx, dy, .. } => { 250 | assert_eq!(dx, inside_position.x - outside_position.x); 251 | assert_eq!(dy, inside_position.y - outside_position.y); 252 | } 253 | _ => {} 254 | }, 255 | } 256 | } 257 | } 258 | 259 | // Now, we shouldn't be able to move back out to the confined region 260 | f.server.pointer_motion_absolute(0, outside_position); 261 | f.round_trip(); 262 | { 263 | // The confinement should still be active 264 | assert!(f.client.is_confined()); 265 | 266 | // The pointer is still where it was 267 | assert_eq!(f.server.pointer_location, inside_position); 268 | 269 | // We'll only get a relative motion event in this case 270 | // Pointer Motion and Relative Motion events have been triggered 271 | let client_events = f.client.get_client_events(); 272 | assert_eq!(client_events.len(), 2); // relative_motion, frame 273 | while let Some(event) = client_events.pop() { 274 | match event { 275 | MouseEvents::Pointer(p_event) => match p_event { 276 | wl_pointer::Event::Frame { .. } => {} 277 | _ => { 278 | panic!("Unexpected event: {:?}", p_event) 279 | } 280 | }, 281 | MouseEvents::Relative(r_event) => match r_event { 282 | zwp_relative_pointer_v1::Event::RelativeMotion { dx, dy, .. } => { 283 | assert_eq!(dx, -(inside_position.x - outside_position.x)); 284 | assert_eq!(dy, -(inside_position.y - outside_position.y)); 285 | } 286 | _ => {} 287 | }, 288 | } 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /wayland-display-core/src/comp/focus.rs: -------------------------------------------------------------------------------- 1 | use crate::comp::State; 2 | use smithay::{ 3 | backend::input::KeyState, 4 | desktop::{PopupKind, Window, WindowSurface}, 5 | input::{ 6 | Seat, 7 | keyboard::{KeyboardTarget, KeysymHandle, ModifiersState}, 8 | pointer::{ 9 | AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, 10 | GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, 11 | GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, MotionEvent, 12 | PointerTarget, RelativeMotionEvent, 13 | }, 14 | touch::{DownEvent, OrientationEvent, ShapeEvent, TouchTarget, UpEvent}, 15 | }, 16 | reexports::wayland_server::{backend::ObjectId, protocol::wl_surface::WlSurface}, 17 | utils::{IsAlive, Serial}, 18 | wayland::seat::WaylandFocus, 19 | }; 20 | use std::borrow::Cow; 21 | 22 | #[derive(Debug, Clone, PartialEq)] 23 | pub enum FocusTarget { 24 | Wayland(Window), 25 | Popup(PopupKind), 26 | } 27 | 28 | impl IsAlive for FocusTarget { 29 | fn alive(&self) -> bool { 30 | match self { 31 | FocusTarget::Wayland(w) => w.alive(), 32 | FocusTarget::Popup(p) => p.alive(), 33 | } 34 | } 35 | } 36 | 37 | impl From for FocusTarget { 38 | fn from(w: Window) -> Self { 39 | FocusTarget::Wayland(w) 40 | } 41 | } 42 | 43 | impl From for FocusTarget { 44 | fn from(p: PopupKind) -> Self { 45 | FocusTarget::Popup(p) 46 | } 47 | } 48 | 49 | impl KeyboardTarget for FocusTarget { 50 | fn enter( 51 | &self, 52 | seat: &Seat, 53 | data: &mut State, 54 | keys: Vec>, 55 | serial: Serial, 56 | ) { 57 | match self { 58 | FocusTarget::Wayland(w) => match w.underlying_surface() { 59 | WindowSurface::Wayland(w) => { 60 | KeyboardTarget::enter(w.wl_surface(), seat, data, keys, serial) 61 | } 62 | }, 63 | FocusTarget::Popup(p) => { 64 | KeyboardTarget::enter(p.wl_surface(), seat, data, keys, serial) 65 | } 66 | } 67 | } 68 | 69 | fn leave(&self, seat: &Seat, data: &mut State, serial: Serial) { 70 | match self { 71 | FocusTarget::Wayland(w) => match w.underlying_surface() { 72 | WindowSurface::Wayland(w) => { 73 | KeyboardTarget::leave(w.wl_surface(), seat, data, serial) 74 | } 75 | }, 76 | FocusTarget::Popup(p) => KeyboardTarget::leave(p.wl_surface(), seat, data, serial), 77 | } 78 | } 79 | 80 | fn key( 81 | &self, 82 | seat: &Seat, 83 | data: &mut State, 84 | key: KeysymHandle<'_>, 85 | state: KeyState, 86 | serial: Serial, 87 | time: u32, 88 | ) { 89 | match self { 90 | FocusTarget::Wayland(w) => match w.underlying_surface() { 91 | WindowSurface::Wayland(w) => { 92 | KeyboardTarget::key(w.wl_surface(), seat, data, key, state, serial, time) 93 | } 94 | }, 95 | FocusTarget::Popup(p) => { 96 | KeyboardTarget::key(p.wl_surface(), seat, data, key, state, serial, time) 97 | } 98 | } 99 | } 100 | 101 | fn modifiers( 102 | &self, 103 | seat: &Seat, 104 | data: &mut State, 105 | modifiers: ModifiersState, 106 | serial: Serial, 107 | ) { 108 | match self { 109 | FocusTarget::Wayland(w) => match w.underlying_surface() { 110 | WindowSurface::Wayland(w) => { 111 | KeyboardTarget::modifiers(w.wl_surface(), seat, data, modifiers, serial) 112 | } 113 | }, 114 | FocusTarget::Popup(p) => p.wl_surface().modifiers(seat, data, modifiers, serial), 115 | } 116 | } 117 | } 118 | 119 | impl PointerTarget for FocusTarget { 120 | fn enter(&self, seat: &Seat, data: &mut State, event: &MotionEvent) { 121 | match self { 122 | FocusTarget::Wayland(w) => match w.underlying_surface() { 123 | WindowSurface::Wayland(w) => { 124 | PointerTarget::enter(w.wl_surface(), seat, data, event) 125 | } 126 | }, 127 | FocusTarget::Popup(p) => PointerTarget::enter(p.wl_surface(), seat, data, event), 128 | } 129 | } 130 | 131 | fn motion(&self, seat: &Seat, data: &mut State, event: &MotionEvent) { 132 | match self { 133 | FocusTarget::Wayland(w) => match w.underlying_surface() { 134 | WindowSurface::Wayland(w) => { 135 | PointerTarget::motion(w.wl_surface(), seat, data, event) 136 | } 137 | }, 138 | FocusTarget::Popup(p) => PointerTarget::motion(p.wl_surface(), seat, data, event), 139 | } 140 | } 141 | 142 | fn relative_motion(&self, seat: &Seat, data: &mut State, event: &RelativeMotionEvent) { 143 | match self { 144 | FocusTarget::Wayland(w) => match w.underlying_surface() { 145 | WindowSurface::Wayland(w) => { 146 | PointerTarget::relative_motion(w.wl_surface(), seat, data, event) 147 | } 148 | }, 149 | FocusTarget::Popup(p) => { 150 | PointerTarget::relative_motion(p.wl_surface(), seat, data, event) 151 | } 152 | } 153 | } 154 | 155 | fn button(&self, seat: &Seat, data: &mut State, event: &ButtonEvent) { 156 | match self { 157 | FocusTarget::Wayland(w) => match w.underlying_surface() { 158 | WindowSurface::Wayland(w) => { 159 | PointerTarget::button(w.wl_surface(), seat, data, event) 160 | } 161 | }, 162 | FocusTarget::Popup(p) => PointerTarget::button(p.wl_surface(), seat, data, event), 163 | } 164 | } 165 | 166 | fn axis(&self, seat: &Seat, data: &mut State, frame: AxisFrame) { 167 | match self { 168 | FocusTarget::Wayland(w) => match w.underlying_surface() { 169 | WindowSurface::Wayland(w) => PointerTarget::axis(w.wl_surface(), seat, data, frame), 170 | }, 171 | FocusTarget::Popup(p) => PointerTarget::axis(p.wl_surface(), seat, data, frame), 172 | } 173 | } 174 | 175 | fn frame(&self, seat: &Seat, data: &mut State) { 176 | match self { 177 | FocusTarget::Wayland(w) => match w.underlying_surface() { 178 | WindowSurface::Wayland(w) => PointerTarget::frame(w.wl_surface(), seat, data), 179 | }, 180 | FocusTarget::Popup(p) => PointerTarget::frame(p.wl_surface(), seat, data), 181 | } 182 | } 183 | 184 | fn gesture_swipe_begin( 185 | &self, 186 | seat: &Seat, 187 | data: &mut State, 188 | event: &GestureSwipeBeginEvent, 189 | ) { 190 | match self { 191 | FocusTarget::Wayland(w) => match w.underlying_surface() { 192 | WindowSurface::Wayland(w) => { 193 | PointerTarget::gesture_swipe_begin(w.wl_surface(), seat, data, event) 194 | } 195 | }, 196 | FocusTarget::Popup(p) => { 197 | PointerTarget::gesture_swipe_begin(p.wl_surface(), seat, data, event) 198 | } 199 | } 200 | } 201 | 202 | fn gesture_swipe_update( 203 | &self, 204 | seat: &Seat, 205 | data: &mut State, 206 | event: &GestureSwipeUpdateEvent, 207 | ) { 208 | match self { 209 | FocusTarget::Wayland(w) => match w.underlying_surface() { 210 | WindowSurface::Wayland(w) => { 211 | PointerTarget::gesture_swipe_update(w.wl_surface(), seat, data, event) 212 | } 213 | }, 214 | FocusTarget::Popup(p) => { 215 | PointerTarget::gesture_swipe_update(p.wl_surface(), seat, data, event) 216 | } 217 | } 218 | } 219 | 220 | fn gesture_swipe_end( 221 | &self, 222 | seat: &Seat, 223 | data: &mut State, 224 | event: &GestureSwipeEndEvent, 225 | ) { 226 | match self { 227 | FocusTarget::Wayland(w) => match w.underlying_surface() { 228 | WindowSurface::Wayland(w) => { 229 | PointerTarget::gesture_swipe_end(w.wl_surface(), seat, data, event) 230 | } 231 | }, 232 | FocusTarget::Popup(p) => { 233 | PointerTarget::gesture_swipe_end(p.wl_surface(), seat, data, event) 234 | } 235 | } 236 | } 237 | 238 | fn gesture_pinch_begin( 239 | &self, 240 | seat: &Seat, 241 | data: &mut State, 242 | event: &GesturePinchBeginEvent, 243 | ) { 244 | match self { 245 | FocusTarget::Wayland(w) => match w.underlying_surface() { 246 | WindowSurface::Wayland(w) => { 247 | PointerTarget::gesture_pinch_begin(w.wl_surface(), seat, data, event) 248 | } 249 | }, 250 | FocusTarget::Popup(p) => { 251 | PointerTarget::gesture_pinch_begin(p.wl_surface(), seat, data, event) 252 | } 253 | } 254 | } 255 | 256 | fn gesture_pinch_update( 257 | &self, 258 | seat: &Seat, 259 | data: &mut State, 260 | event: &GesturePinchUpdateEvent, 261 | ) { 262 | match self { 263 | FocusTarget::Wayland(w) => match w.underlying_surface() { 264 | WindowSurface::Wayland(w) => { 265 | PointerTarget::gesture_pinch_update(w.wl_surface(), seat, data, event) 266 | } 267 | }, 268 | FocusTarget::Popup(p) => { 269 | PointerTarget::gesture_pinch_update(p.wl_surface(), seat, data, event) 270 | } 271 | } 272 | } 273 | 274 | fn gesture_pinch_end( 275 | &self, 276 | seat: &Seat, 277 | data: &mut State, 278 | event: &GesturePinchEndEvent, 279 | ) { 280 | match self { 281 | FocusTarget::Wayland(w) => match w.underlying_surface() { 282 | WindowSurface::Wayland(w) => { 283 | PointerTarget::gesture_pinch_end(w.wl_surface(), seat, data, event) 284 | } 285 | }, 286 | FocusTarget::Popup(p) => { 287 | PointerTarget::gesture_pinch_end(p.wl_surface(), seat, data, event) 288 | } 289 | } 290 | } 291 | 292 | fn gesture_hold_begin( 293 | &self, 294 | seat: &Seat, 295 | data: &mut State, 296 | event: &GestureHoldBeginEvent, 297 | ) { 298 | match self { 299 | FocusTarget::Wayland(w) => match w.underlying_surface() { 300 | WindowSurface::Wayland(w) => { 301 | PointerTarget::gesture_hold_begin(w.wl_surface(), seat, data, event) 302 | } 303 | }, 304 | FocusTarget::Popup(p) => { 305 | PointerTarget::gesture_hold_begin(p.wl_surface(), seat, data, event) 306 | } 307 | } 308 | } 309 | 310 | fn gesture_hold_end(&self, seat: &Seat, data: &mut State, event: &GestureHoldEndEvent) { 311 | match self { 312 | FocusTarget::Wayland(w) => match w.underlying_surface() { 313 | WindowSurface::Wayland(w) => { 314 | PointerTarget::gesture_hold_end(w.wl_surface(), seat, data, event) 315 | } 316 | }, 317 | FocusTarget::Popup(p) => { 318 | PointerTarget::gesture_hold_end(p.wl_surface(), seat, data, event) 319 | } 320 | } 321 | } 322 | 323 | fn leave(&self, seat: &Seat, data: &mut State, serial: Serial, time: u32) { 324 | match self { 325 | FocusTarget::Wayland(w) => match w.underlying_surface() { 326 | WindowSurface::Wayland(w) => { 327 | PointerTarget::leave(w.wl_surface(), seat, data, serial, time) 328 | } 329 | }, 330 | FocusTarget::Popup(p) => PointerTarget::leave(p.wl_surface(), seat, data, serial, time), 331 | } 332 | } 333 | } 334 | 335 | impl WaylandFocus for FocusTarget { 336 | fn wl_surface(&self) -> Option> { 337 | match self { 338 | FocusTarget::Wayland(w) => w.wl_surface(), 339 | FocusTarget::Popup(p) => Some(Cow::Borrowed(p.wl_surface())), 340 | } 341 | } 342 | 343 | fn same_client_as(&self, object_id: &ObjectId) -> bool { 344 | match self { 345 | FocusTarget::Wayland(w) => w.same_client_as(object_id), 346 | FocusTarget::Popup(p) => p.wl_surface().same_client_as(object_id), 347 | } 348 | } 349 | } 350 | 351 | impl TouchTarget for FocusTarget { 352 | fn down(&self, seat: &Seat, data: &mut State, event: &DownEvent, seq: Serial) { 353 | match self { 354 | FocusTarget::Wayland(w) => match w.underlying_surface() { 355 | WindowSurface::Wayland(w) => { 356 | TouchTarget::down(w.wl_surface(), seat, data, event, seq) 357 | } 358 | }, 359 | FocusTarget::Popup(p) => TouchTarget::down(p.wl_surface(), seat, data, event, seq), 360 | } 361 | } 362 | 363 | fn up(&self, seat: &Seat, data: &mut State, event: &UpEvent, seq: Serial) { 364 | match self { 365 | FocusTarget::Wayland(w) => match w.underlying_surface() { 366 | WindowSurface::Wayland(w) => { 367 | TouchTarget::up(w.wl_surface(), seat, data, event, seq) 368 | } 369 | }, 370 | FocusTarget::Popup(p) => TouchTarget::up(p.wl_surface(), seat, data, event, seq), 371 | } 372 | } 373 | 374 | fn motion( 375 | &self, 376 | seat: &Seat, 377 | data: &mut State, 378 | event: &smithay::input::touch::MotionEvent, 379 | seq: Serial, 380 | ) { 381 | match self { 382 | FocusTarget::Wayland(w) => match w.underlying_surface() { 383 | WindowSurface::Wayland(w) => { 384 | TouchTarget::motion(w.wl_surface(), seat, data, event, seq) 385 | } 386 | }, 387 | FocusTarget::Popup(p) => TouchTarget::motion(p.wl_surface(), seat, data, event, seq), 388 | } 389 | } 390 | 391 | fn frame(&self, seat: &Seat, data: &mut State, seq: Serial) { 392 | match self { 393 | FocusTarget::Wayland(w) => match w.underlying_surface() { 394 | WindowSurface::Wayland(w) => TouchTarget::frame(w.wl_surface(), seat, data, seq), 395 | }, 396 | FocusTarget::Popup(p) => TouchTarget::frame(p.wl_surface(), seat, data, seq), 397 | } 398 | } 399 | 400 | fn cancel(&self, seat: &Seat, data: &mut State, seq: Serial) { 401 | match self { 402 | FocusTarget::Wayland(w) => match w.underlying_surface() { 403 | WindowSurface::Wayland(w) => TouchTarget::cancel(w.wl_surface(), seat, data, seq), 404 | }, 405 | FocusTarget::Popup(p) => TouchTarget::cancel(p.wl_surface(), seat, data, seq), 406 | } 407 | } 408 | 409 | fn shape(&self, seat: &Seat, data: &mut State, event: &ShapeEvent, seq: Serial) { 410 | match self { 411 | FocusTarget::Wayland(w) => match w.underlying_surface() { 412 | WindowSurface::Wayland(w) => { 413 | TouchTarget::shape(w.wl_surface(), seat, data, event, seq) 414 | } 415 | }, 416 | FocusTarget::Popup(p) => TouchTarget::shape(p.wl_surface(), seat, data, event, seq), 417 | } 418 | } 419 | 420 | fn orientation( 421 | &self, 422 | seat: &Seat, 423 | data: &mut State, 424 | event: &OrientationEvent, 425 | seq: Serial, 426 | ) { 427 | match self { 428 | FocusTarget::Wayland(w) => match w.underlying_surface() { 429 | WindowSurface::Wayland(w) => { 430 | TouchTarget::orientation(w.wl_surface(), seat, data, event, seq) 431 | } 432 | }, 433 | FocusTarget::Popup(p) => { 434 | TouchTarget::orientation(p.wl_surface(), seat, data, event, seq) 435 | } 436 | } 437 | } 438 | } 439 | -------------------------------------------------------------------------------- /wayland-display-core/src/utils/allocator/cuda/mod.rs: -------------------------------------------------------------------------------- 1 | pub use ffi::GstCudaContext; 2 | use ffi::{CUgraphicsResource, PFN_eglDestroyImageKHR}; 3 | use gst::glib::ffi as glib_ffi; 4 | use gst::glib::translate::ToGlibPtr; 5 | use gst::query::Allocation; 6 | use gst::{Buffer as GstBuffer, Context, Element, QueryRef}; 7 | use gst_video::{VideoInfoDmaDrm, VideoMeta}; 8 | use smithay::backend::allocator::Buffer; 9 | use smithay::backend::allocator::dmabuf::Dmabuf; 10 | use smithay::backend::egl; 11 | use smithay::backend::egl::ffi::egl::types::{EGLDisplay, EGLImageKHR, EGLint}; 12 | use std::ffi::c_int; 13 | use std::os::fd::AsRawFd; 14 | use std::os::raw::c_char; 15 | use std::ptr; 16 | use std::sync::Arc; 17 | 18 | mod ffi; 19 | 20 | // Helper to load EGL extension functions 21 | #[derive(Debug, Clone)] 22 | pub struct EglExtensions { 23 | pub create_image: ffi::PFN_eglCreateImageKHR, 24 | pub destroy_image: PFN_eglDestroyImageKHR, 25 | } 26 | 27 | impl EglExtensions { 28 | unsafe fn load() -> Option { 29 | let create_image_ptr = unsafe { egl::get_proc_address("eglCreateImageKHR") }; 30 | let destroy_image_ptr = unsafe { egl::get_proc_address("eglDestroyImageKHR") }; 31 | 32 | if create_image_ptr.is_null() || destroy_image_ptr.is_null() { 33 | return None; 34 | } 35 | 36 | Some(EglExtensions { 37 | create_image: unsafe { std::mem::transmute(create_image_ptr) }, 38 | destroy_image: unsafe { std::mem::transmute(destroy_image_ptr) }, 39 | }) 40 | } 41 | 42 | pub fn new() -> Option { 43 | unsafe { EglExtensions::load() } 44 | } 45 | } 46 | 47 | #[derive(Debug)] 48 | pub struct EGLImage { 49 | image: EGLImageKHR, 50 | egl_display: Arc, 51 | egl_extensions: EglExtensions, 52 | } 53 | 54 | impl EGLImage { 55 | pub fn from(dmabuf: &Dmabuf, egl_display: &EGLDisplay) -> Result { 56 | // Get dmabuf properties 57 | let width = dmabuf.width(); 58 | let height = dmabuf.height(); 59 | let fourcc = dmabuf.format().code as u32; 60 | 61 | // Get modifier if available 62 | let modifier: u64 = dmabuf.format().modifier.into(); 63 | let modifier_lo = (modifier & 0xFFFFFFFF) as EGLint; 64 | let modifier_hi = ((modifier >> 32) & 0xFFFFFFFF) as EGLint; 65 | 66 | // Build EGL attribute list for DMA-BUF import 67 | let mut attribs = [ 68 | ffi::EGL_WIDTH, 69 | width as EGLint, 70 | ffi::EGL_HEIGHT, 71 | height as EGLint, 72 | ffi::EGL_LINUX_DRM_FOURCC_EXT, 73 | fourcc as EGLint, 74 | ] 75 | .to_vec(); 76 | 77 | let offsets = dmabuf.offsets().map(|o| o as usize).collect::>(); 78 | 79 | let strides = dmabuf.strides().map(|s| s as i32).collect::>(); 80 | 81 | for (idx, handle) in dmabuf.handles().enumerate() { 82 | let fd = handle.as_raw_fd(); 83 | // Add to attribs the current plane data 84 | if idx == 0 { 85 | attribs.extend_from_slice(&[ 86 | ffi::EGL_DMA_BUF_PLANE0_FD_EXT, 87 | fd, 88 | ffi::EGL_DMA_BUF_PLANE0_OFFSET_EXT, 89 | offsets[idx] as EGLint, 90 | ffi::EGL_DMA_BUF_PLANE0_PITCH_EXT, 91 | strides[idx], 92 | ffi::EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, 93 | modifier_lo, 94 | ffi::EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, 95 | modifier_hi, 96 | ]); 97 | } else if idx == 1 { 98 | attribs.extend_from_slice(&[ 99 | ffi::EGL_DMA_BUF_PLANE1_FD_EXT, 100 | fd, 101 | ffi::EGL_DMA_BUF_PLANE1_OFFSET_EXT, 102 | offsets[idx] as EGLint, 103 | ffi::EGL_DMA_BUF_PLANE1_PITCH_EXT, 104 | strides[idx], 105 | ffi::EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT, 106 | modifier_lo, 107 | ffi::EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT, 108 | modifier_hi, 109 | ]); 110 | } 111 | } 112 | 113 | attribs.push(ffi::EGL_NONE); 114 | let egl_extensions = EglExtensions::new().ok_or("Failed to load EGL extensions")?; 115 | 116 | let egl_image = unsafe { 117 | (egl_extensions.create_image)( 118 | *egl_display, 119 | ptr::null_mut(), 120 | ffi::EGL_LINUX_DMA_BUF_EXT, 121 | ptr::null_mut(), 122 | attribs.as_ptr(), 123 | ) 124 | }; 125 | if egl_image != ffi::EGL_NO_IMAGE_KHR { 126 | Ok(EGLImage { 127 | image: egl_image, 128 | egl_display: Arc::new(egl_display.clone()), 129 | egl_extensions, 130 | }) 131 | } else { 132 | Err("Failed to create EGLImage".into()) 133 | } 134 | } 135 | } 136 | 137 | impl Drop for EGLImage { 138 | fn drop(&mut self) { 139 | unsafe { 140 | (self.egl_extensions.destroy_image)(*self.egl_display, self.image); 141 | } 142 | } 143 | } 144 | 145 | pub const CAPS_FEATURE_MEMORY_CUDA_MEMORY: &str = "memory:CUDAMemory"; // TODO: get it from FFI from gstcudamemory.h (https://github.com/GStreamer/gstreamer/blob/9d6abcc18cc9a60a212966a2daaf4a1af243f5da/subprojects/gst-plugins-bad/gst-libs/gst/cuda/gstcudamemory.h#L113-L121) 146 | 147 | pub fn init_cuda() -> Result<(), String> { 148 | static mut INITIALIZED: bool = false; 149 | if !unsafe { INITIALIZED } { 150 | unsafe { 151 | if ffi::gst_cuda_load_library() == glib_ffi::GFALSE { 152 | return Err("Failed to load CUDA library".into()); 153 | } 154 | ffi::gst_cuda_memory_init_once(); 155 | ffi::init_cuda_egl()?; 156 | 157 | INITIALIZED = true; 158 | Ok(()) 159 | } 160 | } else { 161 | Ok(()) 162 | } 163 | } 164 | 165 | #[derive(Debug)] 166 | pub struct CUDAContext { 167 | ptr: *mut GstCudaContext, 168 | stream: Option, 169 | } 170 | 171 | impl Drop for CUDAContext { 172 | fn drop(&mut self) { 173 | unsafe { 174 | gst::ffi::gst_object_unref(self.ptr as *mut gst::ffi::GstObject); 175 | } 176 | } 177 | } 178 | 179 | #[derive(Debug)] 180 | pub struct StreamHandle { 181 | stream: ffi::GstCudaStreamHandle, 182 | } 183 | 184 | impl Drop for StreamHandle { 185 | fn drop(&mut self) { 186 | unsafe { 187 | ffi::gst_cuda_stream_unref(self.stream); 188 | } 189 | } 190 | } 191 | 192 | #[derive(Debug)] 193 | pub struct CUDABufferPool { 194 | pool: ffi::GstBufferPool, 195 | } 196 | 197 | impl CUDABufferPool { 198 | pub fn new(cudacontext: &CUDAContext) -> Result { 199 | let pool = unsafe { ffi::gst_cuda_buffer_pool_new(cudacontext.ptr) }; 200 | if pool.is_null() { 201 | Err("Failed to create CUDA buffer pool".into()) 202 | } else { 203 | Ok(CUDABufferPool { pool }) 204 | } 205 | } 206 | 207 | pub fn from(pool: *mut gst::ffi::GstBufferPool) -> Result { 208 | if ffi::gst_is_cuda_buffer_pool(pool) { 209 | unsafe { gst::ffi::gst_object_ref(pool as *mut gst::ffi::GstObject) }; 210 | Ok(CUDABufferPool { 211 | pool: pool as ffi::GstBufferPool, 212 | }) 213 | } else { 214 | Err("Input buffer pool isn't a CUDABufferPool".into()) 215 | } 216 | } 217 | 218 | pub fn configure( 219 | &self, 220 | caps: &gst::Caps, 221 | stream_handle: &StreamHandle, 222 | size: u32, 223 | min_buffers: u32, 224 | max_buffers: u32, 225 | ) -> Result<(), String> { 226 | let config = unsafe { 227 | gst::ffi::gst_buffer_pool_get_config(self.pool as *mut gst::ffi::GstBufferPool) 228 | }; 229 | if config.is_null() { 230 | return Err("Failed to get buffer pool config".into()); 231 | } 232 | 233 | // TODO: support getting the stream handler too here 234 | // https://github.com/GStreamer/gstreamer/blob/c5a470e5164ce7fa8fd5fa80650d9ee35ce214d8/subprojects/gst-plugins-bad/sys/nvcodec/gstcudaconvertscale.c#L1293-L1300 235 | // ultimately it doesn't matter for us because we are creating a new pool every time 236 | 237 | // Configure the pool 238 | unsafe { 239 | // Set the CUDA stream in the config 240 | ffi::gst_buffer_pool_config_set_cuda_stream(config, stream_handle.stream); 241 | 242 | gst::ffi::gst_buffer_pool_config_add_option( 243 | config, 244 | ffi::GST_BUFFER_POOL_OPTION_VIDEO_META.as_ptr() as *const c_char, 245 | ); 246 | gst::ffi::gst_buffer_pool_config_set_params( 247 | config, 248 | caps.to_glib_none().0, 249 | size, 250 | min_buffers, 251 | max_buffers, 252 | ); 253 | } 254 | 255 | // Set the configuration 256 | let result = unsafe { 257 | gst::ffi::gst_buffer_pool_set_config(self.pool as *mut gst::ffi::GstBufferPool, config) 258 | }; 259 | if result == glib_ffi::GFALSE { 260 | Err("Failed to set buffer pool config".into()) 261 | } else { 262 | Ok(()) 263 | } 264 | } 265 | 266 | pub fn get_updated_size(&self) -> Result { 267 | let config = unsafe { 268 | gst::ffi::gst_buffer_pool_get_config(self.pool as *mut gst::ffi::GstBufferPool) 269 | }; 270 | if config.is_null() { 271 | return Err("Failed to get buffer pool config".into()); 272 | } 273 | 274 | let mut size = 0; 275 | unsafe { 276 | gst::ffi::gst_buffer_pool_config_get_params( 277 | config, 278 | ptr::null_mut(), 279 | &mut size, 280 | ptr::null_mut(), 281 | ptr::null_mut(), 282 | ); 283 | } 284 | Ok(size) 285 | } 286 | 287 | pub fn activate(&self) -> Result<(), String> { 288 | let result = unsafe { 289 | gst::ffi::gst_buffer_pool_set_active( 290 | self.pool as *mut gst::ffi::GstBufferPool, 291 | glib_ffi::GTRUE, 292 | ) 293 | }; 294 | if result == glib_ffi::GFALSE { 295 | Err("Failed to activate buffer pool".into()) 296 | } else { 297 | Ok(()) 298 | } 299 | } 300 | 301 | pub fn set_nth_allocation_pool( 302 | &self, 303 | query: &mut Allocation, 304 | idx: u32, 305 | size: u32, 306 | min_buffers: u32, 307 | max_buffers: u32, 308 | ) { 309 | unsafe { 310 | gst::ffi::gst_query_set_nth_allocation_pool( 311 | query.as_mut_ptr(), 312 | idx, 313 | self.pool as *mut gst::ffi::GstBufferPool, 314 | size, 315 | min_buffers, 316 | max_buffers, 317 | ); 318 | } 319 | } 320 | 321 | pub fn add_allocation_pool( 322 | &self, 323 | query: &mut Allocation, 324 | size: u32, 325 | min_buffers: u32, 326 | max_buffers: u32, 327 | ) { 328 | unsafe { 329 | gst::ffi::gst_query_add_allocation_pool( 330 | query.as_mut_ptr(), 331 | self.pool as *mut gst::ffi::GstBufferPool, 332 | size, 333 | min_buffers, 334 | max_buffers, 335 | ) 336 | } 337 | } 338 | } 339 | 340 | impl Drop for CUDABufferPool { 341 | fn drop(&mut self) { 342 | unsafe { 343 | gst::glib::gobject_ffi::g_object_unref( 344 | self.pool as *mut gst::glib::gobject_ffi::GObject, 345 | ); 346 | } 347 | } 348 | } 349 | 350 | unsafe impl Send for CUDAContext {} 351 | unsafe impl Send for CUDABufferPool {} 352 | 353 | impl CUDAContext { 354 | pub fn new(device_id: c_int) -> Result { 355 | let ptr = unsafe { ffi::gst_cuda_context_new(device_id) }; 356 | if ptr.is_null() { 357 | return Err("Failed to create CUDA context".into()); 358 | } 359 | 360 | // Create a CUDA stream 361 | let stream = unsafe { ffi::gst_cuda_stream_new(ptr) }; 362 | 363 | Ok(CUDAContext { 364 | ptr, 365 | stream: if stream.is_null() { 366 | None 367 | } else { 368 | Some(StreamHandle { stream }) 369 | }, 370 | }) 371 | } 372 | 373 | pub fn new_from_gstreamer( 374 | element: &Element, 375 | default_device_id: c_int, 376 | cuda_raw_ptr: *mut *mut GstCudaContext, 377 | ) -> Result { 378 | let result = unsafe { 379 | ffi::gst_cuda_ensure_element_context( 380 | element.to_glib_none().0, 381 | default_device_id, 382 | cuda_raw_ptr, 383 | ) 384 | }; 385 | 386 | if result == glib_ffi::GFALSE { 387 | Err("Failed to create CUDA context".into()) 388 | } else { 389 | let stream = unsafe { ffi::gst_cuda_stream_new(*cuda_raw_ptr) }; 390 | Ok(CUDAContext { 391 | ptr: unsafe { *cuda_raw_ptr }, 392 | stream: if stream.is_null() { 393 | None 394 | } else { 395 | Some(StreamHandle { stream }) 396 | }, 397 | }) 398 | } 399 | } 400 | 401 | pub fn new_from_set_context( 402 | element: &Element, 403 | context: &Context, 404 | default_device_id: c_int, 405 | cuda_raw_ptr: *mut *mut GstCudaContext, 406 | ) -> Result { 407 | let result = unsafe { 408 | ffi::gst_cuda_handle_set_context( 409 | element.to_glib_none().0, 410 | context.to_glib_none().0, 411 | default_device_id, 412 | cuda_raw_ptr, 413 | ) 414 | }; 415 | 416 | if result == glib_ffi::GFALSE { 417 | Err("Failed to create CUDA context".into()) 418 | } else { 419 | let stream = unsafe { ffi::gst_cuda_stream_new(*cuda_raw_ptr) }; 420 | Ok(CUDAContext { 421 | ptr: unsafe { *cuda_raw_ptr }, 422 | stream: if stream.is_null() { 423 | None 424 | } else { 425 | Some(StreamHandle { stream }) 426 | }, 427 | }) 428 | } 429 | } 430 | 431 | pub fn as_ptr(&self) -> *mut GstCudaContext { 432 | self.ptr 433 | } 434 | 435 | pub fn stream(&self) -> Option<&StreamHandle> { 436 | self.stream.as_ref() 437 | } 438 | } 439 | 440 | #[derive(Debug)] 441 | pub struct CUDAImage { 442 | #[allow(dead_code)] 443 | egl_image: EGLImage, 444 | cuda_graphic_resource: CUgraphicsResource, 445 | } 446 | 447 | impl CUDAImage { 448 | pub fn from(egl_image: EGLImage, cuda_context: &CUDAContext) -> Result { 449 | let _cuda_context_guard = ffi::CudaContextGuard::new(cuda_context)?; 450 | let cuda_egl_fn = ffi::get_cuda_egl_functions()?; 451 | // Let's import the EGLImage into CUDA 452 | let mut cuda_resource: CUgraphicsResource = ptr::null_mut(); 453 | unsafe { 454 | cuda_egl_fn.register_egl_image( 455 | &mut cuda_resource, 456 | egl_image.image, 457 | 0, // flags (0 = read/write) 458 | )?; 459 | } 460 | Ok(CUDAImage { 461 | egl_image, 462 | cuda_graphic_resource: cuda_resource, 463 | }) 464 | } 465 | 466 | pub fn to_gst_buffer( 467 | &self, 468 | dma_video_info: VideoInfoDmaDrm, 469 | cuda_context: &CUDAContext, 470 | buffer_pool: Option<&CUDABufferPool>, 471 | ) -> Result> { 472 | let _cuda_context_guard = ffi::CudaContextGuard::new(cuda_context)?; 473 | let cuda_egl_fn = ffi::get_cuda_egl_functions()?; 474 | 475 | let egl_frame = 476 | unsafe { cuda_egl_fn.get_mapped_egl_frame(self.cuda_graphic_resource, 0, 0)? }; 477 | 478 | // Acquire buffer from pool or allocate directly 479 | let mut buffer = ffi::acquire_or_alloc_buffer( 480 | buffer_pool.as_ref().map(|p| p.pool), 481 | cuda_context, 482 | &dma_video_info, 483 | )?; 484 | 485 | // Copy data to the buffer 486 | let video_info = ffi::copy_to_gst_buffer(egl_frame, &mut buffer, cuda_context)?; 487 | 488 | let buffer_ref = buffer.get_mut().unwrap(); 489 | VideoMeta::add_full( 490 | buffer_ref, 491 | gst_video::VideoFrameFlags::empty(), 492 | video_info.format(), 493 | video_info.width(), 494 | video_info.height(), 495 | video_info.offset(), 496 | video_info.stride(), 497 | )?; 498 | 499 | Ok(buffer) 500 | } 501 | } 502 | 503 | impl Drop for CUDAImage { 504 | fn drop(&mut self) { 505 | let cuda_egl_fn = ffi::get_cuda_egl_functions().expect("Failed to get CUDA EGL functions"); 506 | unsafe { 507 | cuda_egl_fn 508 | .unregister_resource(self.cuda_graphic_resource) 509 | .expect("Failed to unregister resource"); 510 | } 511 | } 512 | } 513 | 514 | pub fn gst_cuda_handle_context_query_wrapped( 515 | element: &Element, 516 | query: &mut QueryRef, 517 | cuda_context: &CUDAContext, 518 | ) -> bool { 519 | let result = unsafe { 520 | ffi::gst_cuda_handle_context_query( 521 | element.to_glib_none().0, 522 | query.as_mut_ptr(), 523 | cuda_context.ptr, 524 | ) 525 | }; 526 | result == glib_ffi::GTRUE 527 | } 528 | -------------------------------------------------------------------------------- /wayland-display-core/src/tests/client.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::os::fd::AsFd; 3 | use std::os::unix::net::UnixStream; 4 | use std::sync::Arc; 5 | use std::sync::atomic::{AtomicBool, Ordering}; 6 | use wayland_backend::client::Backend; 7 | use wayland_client::protocol::wl_callback::WlCallback; 8 | use wayland_client::protocol::wl_display::WlDisplay; 9 | use wayland_client::protocol::{wl_callback, wl_pointer, wl_region}; 10 | use wayland_client::{ 11 | Connection, Dispatch, EventQueue, QueueHandle, WEnum, delegate_noop, 12 | protocol::{ 13 | wl_buffer, wl_compositor, wl_keyboard, wl_registry, wl_seat, wl_shm, wl_shm_pool, 14 | wl_surface, 15 | }, 16 | }; 17 | use wayland_protocols::{ 18 | wp::{ 19 | pointer_constraints::zv1::{ 20 | client::zwp_confined_pointer_v1, client::zwp_locked_pointer_v1::ZwpLockedPointerV1, 21 | client::zwp_pointer_constraints_v1, 22 | client::zwp_pointer_constraints_v1::ZwpPointerConstraintsV1, 23 | }, 24 | relative_pointer::zv1::client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, 25 | relative_pointer::zv1::client::zwp_relative_pointer_v1, 26 | relative_pointer::zv1::client::zwp_relative_pointer_v1::ZwpRelativePointerV1, 27 | viewporter::client::wp_viewport::WpViewport, 28 | viewporter::client::wp_viewporter::WpViewporter, 29 | }, 30 | xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base}, 31 | }; 32 | 33 | pub struct WaylandClient { 34 | conn: Connection, 35 | display: WlDisplay, 36 | queue: EventQueue, 37 | qh: QueueHandle, 38 | state: State, 39 | } 40 | 41 | #[derive(Debug)] 42 | pub enum MouseEvents { 43 | Pointer(wl_pointer::Event), 44 | Relative(zwp_relative_pointer_v1::Event), 45 | } 46 | 47 | struct State { 48 | qh: QueueHandle, 49 | 50 | compositor: Option, 51 | buffer: Option, 52 | wm_base: Option, 53 | viewporter: Option, 54 | seat: Option, 55 | pointer_constraints: Option, 56 | relative_pointer_manager: Option, 57 | 58 | pointer: Option, 59 | pointer_confined: bool, 60 | keyboard: Option, 61 | windows: Vec, 62 | pub mouse_events: Vec, 63 | } 64 | 65 | #[derive(Debug, Clone, Default)] 66 | pub struct Configure { 67 | pub size: (i32, i32), 68 | pub bounds: Option<(i32, i32)>, 69 | pub states: Vec, 70 | } 71 | 72 | #[derive(Default)] 73 | pub struct SyncData { 74 | pub done: AtomicBool, 75 | } 76 | 77 | impl Dispatch for State { 78 | fn event( 79 | state: &mut Self, 80 | registry: &wl_registry::WlRegistry, 81 | event: wl_registry::Event, 82 | _: &(), 83 | _: &Connection, 84 | qh: &QueueHandle, 85 | ) { 86 | if let wl_registry::Event::Global { 87 | name, 88 | interface, 89 | version, 90 | } = event 91 | { 92 | tracing::trace!("{:?} {:?}", name, interface); 93 | match &interface[..] { 94 | "wl_compositor" => { 95 | let compositor = 96 | registry.bind::(name, version, qh, ()); 97 | state.compositor = Some(compositor); 98 | } 99 | "wl_shm" => { 100 | let shm = registry.bind::(name, version, qh, ()); 101 | 102 | let (init_w, init_h) = (320, 240); 103 | 104 | let mut file = tempfile::tempfile().unwrap(); 105 | draw(&mut file, (init_w, init_h)); 106 | let pool = shm.create_pool(file.as_fd(), (init_w * init_h * 4) as i32, qh, ()); 107 | let buffer = pool.create_buffer( 108 | 0, 109 | init_w as i32, 110 | init_h as i32, 111 | (init_w * 4) as i32, 112 | wl_shm::Format::Argb8888, 113 | qh, 114 | (), 115 | ); 116 | state.buffer = Some(buffer.clone()); 117 | } 118 | "wl_seat" => { 119 | state.seat = 120 | Some(registry.bind::(name, version, qh, ())); 121 | } 122 | "xdg_wm_base" => { 123 | let wm_base = 124 | registry.bind::(name, version, qh, ()); 125 | state.wm_base = Some(wm_base); 126 | } 127 | "wp_viewporter" => { 128 | state.viewporter = 129 | Some(registry.bind::(name, version, qh, ())); 130 | } 131 | "zwp_pointer_constraints_v1" => { 132 | state.pointer_constraints = 133 | Some(registry.bind::(name, version, qh, ())) 134 | } 135 | "zwp_relative_pointer_manager_v1" => { 136 | state.relative_pointer_manager = Some( 137 | registry.bind::(name, version, qh, ()), 138 | ); 139 | } 140 | _ => {} 141 | } 142 | } 143 | } 144 | } 145 | 146 | delegate_noop!(State: ignore wl_compositor::WlCompositor); 147 | delegate_noop!(State: ignore wl_surface::WlSurface); 148 | delegate_noop!(State: ignore wl_shm::WlShm); 149 | delegate_noop!(State: ignore wl_shm_pool::WlShmPool); 150 | delegate_noop!(State: ignore wl_buffer::WlBuffer); 151 | delegate_noop!(State: ignore wl_region::WlRegion); 152 | delegate_noop!(State: ignore WpViewporter); 153 | delegate_noop!(State: ignore WpViewport); 154 | delegate_noop!(State: ignore ZwpPointerConstraintsV1); 155 | delegate_noop!(State: ignore ZwpLockedPointerV1); 156 | delegate_noop!(State: ignore ZwpRelativePointerManagerV1); 157 | 158 | impl WaylandClient { 159 | pub fn new(w_socket: UnixStream) -> Self { 160 | let backend = Backend::connect(w_socket).unwrap(); 161 | let connection = Connection::from_backend(backend); 162 | let queue = connection.new_event_queue(); 163 | let qh = queue.handle(); 164 | 165 | let display = connection.display(); 166 | let _registry = display.get_registry(&qh, ()); 167 | 168 | let state = State { 169 | qh: qh.clone(), 170 | 171 | compositor: None, 172 | buffer: None, 173 | wm_base: None, 174 | viewporter: None, 175 | seat: None, 176 | pointer_constraints: None, 177 | relative_pointer_manager: None, 178 | 179 | pointer: None, 180 | pointer_confined: false, 181 | keyboard: None, 182 | windows: Vec::new(), 183 | mouse_events: Vec::new(), 184 | }; 185 | 186 | WaylandClient { 187 | conn: connection, 188 | display, 189 | queue, 190 | qh, 191 | state, 192 | } 193 | } 194 | 195 | pub fn dispatch(&mut self) { 196 | self.conn.flush().expect("conn.flush()"); 197 | self.queue.dispatch_pending(&mut self.state).unwrap(); 198 | let _e = self 199 | .conn 200 | .prepare_read() 201 | .map(|guard| guard.read()) 202 | .unwrap_or(Ok(0)); 203 | // even if read_events returns an error, some messages may need dispatching 204 | self.queue.dispatch_pending(&mut self.state).unwrap(); 205 | } 206 | 207 | pub fn send_sync(&self) -> Arc { 208 | let data = Arc::new(SyncData::default()); 209 | self.display.sync(&self.qh, data.clone()); 210 | data 211 | } 212 | 213 | pub fn create_window(&mut self) { 214 | self.state.create_window(); 215 | } 216 | 217 | pub fn setup_window(&mut self, width: u16, height: u16) { 218 | let window = self.state.windows.last_mut().unwrap(); 219 | window.set_title("Hello World!"); 220 | window.attach_new_buffer(self.state.buffer.as_ref().unwrap()); 221 | window.set_size(width, height); 222 | window.ack_last_and_commit(); 223 | } 224 | 225 | pub fn get_client_events(&mut self) -> &mut Vec { 226 | self.state.mouse_events.as_mut() 227 | } 228 | 229 | /// Call this to start receiving Relative events in `get_client_events()` 230 | pub fn get_relative_pointer(&mut self) -> ZwpRelativePointerV1 { 231 | let qh = self.qh.clone(); 232 | let pointer = self.state.pointer.as_ref().unwrap(); 233 | self.state 234 | .relative_pointer_manager 235 | .as_ref() 236 | .unwrap() 237 | .get_relative_pointer(pointer, &qh, ()) 238 | } 239 | 240 | /// Requests and acquire a [pointer lock](https://wayland.app/protocols/pointer-constraints-unstable-v1#zwp_pointer_constraints_v1:request:lock_pointer) 241 | /// 242 | /// Note that while a pointer is locked, the wl_pointer objects of the corresponding seat 243 | /// will not emit any wl_pointer.motion events, but relative motion events will still be emitted 244 | /// via wp_relative_pointer objects of the same seat. Use `get_relative_pointer()` to receive them 245 | pub fn lock_pointer(&mut self) -> ZwpLockedPointerV1 { 246 | let qh = self.qh.clone(); 247 | let pointer = self.state.pointer.as_ref().unwrap().clone(); 248 | let window = self.state.windows.last_mut().unwrap(); 249 | 250 | self.state 251 | .pointer_constraints 252 | .as_ref() 253 | .unwrap() 254 | .lock_pointer( 255 | &window.surface, 256 | &pointer, 257 | None, 258 | zwp_pointer_constraints_v1::Lifetime::Oneshot, 259 | &qh, 260 | (), 261 | ) 262 | } 263 | 264 | /// Request and acquire a [pointer confinement region](https://wayland.app/protocols/pointer-constraints-unstable-v1#zwp_pointer_constraints_v1:request:confine_pointer) 265 | pub fn confine_pointer( 266 | &mut self, 267 | x: i32, 268 | y: i32, 269 | width: i32, 270 | height: i32, 271 | ) -> zwp_confined_pointer_v1::ZwpConfinedPointerV1 { 272 | let qh = self.qh.clone(); 273 | let pointer = self.state.pointer.as_ref().unwrap().clone(); 274 | let window = self.state.windows.last_mut().unwrap(); 275 | let region = self 276 | .state 277 | .compositor 278 | .as_ref() 279 | .unwrap() 280 | .create_region(&qh, ()); 281 | region.add(x, y, width, height); 282 | 283 | self.state 284 | .pointer_constraints 285 | .as_ref() 286 | .unwrap() 287 | .confine_pointer( 288 | &window.surface, 289 | &pointer, 290 | Some(®ion), 291 | zwp_pointer_constraints_v1::Lifetime::Persistent, 292 | &qh, 293 | (), 294 | ) 295 | } 296 | 297 | pub fn is_confined(&self) -> bool { 298 | self.state.pointer_confined 299 | } 300 | } 301 | 302 | impl State { 303 | pub fn create_window(&mut self) { 304 | let compositor = self.compositor.as_ref().unwrap(); 305 | let xdg_wm_base = self.wm_base.as_ref().unwrap(); 306 | let viewporter = self.viewporter.as_ref().unwrap(); 307 | 308 | let surface = compositor.create_surface(&self.qh, ()); 309 | let xdg_surface = xdg_wm_base.get_xdg_surface(&surface, &self.qh, ()); 310 | let xdg_toplevel = xdg_surface.get_toplevel(&self.qh, ()); 311 | let viewport = viewporter.get_viewport(&surface, &self.qh, ()); 312 | 313 | let window = Window { 314 | surface, 315 | xdg_surface, 316 | xdg_toplevel, 317 | viewport, 318 | pending_configure: Configure::default(), 319 | configures_received: Vec::new(), 320 | close_requested: false, 321 | }; 322 | 323 | window.commit(); 324 | 325 | self.windows.push(window); 326 | } 327 | } 328 | 329 | fn draw(tmp: &mut File, (buf_x, buf_y): (u32, u32)) { 330 | use std::{cmp::min, io::Write}; 331 | let mut buf = std::io::BufWriter::new(tmp); 332 | for y in 0..buf_y { 333 | for x in 0..buf_x { 334 | let a = 0xFF; 335 | let r = min(((buf_x - x) * 0xFF) / buf_x, ((buf_y - y) * 0xFF) / buf_y); 336 | let g = min((x * 0xFF) / buf_x, ((buf_y - y) * 0xFF) / buf_y); 337 | let b = min(((buf_x - x) * 0xFF) / buf_x, (y * 0xFF) / buf_y); 338 | buf.write_all(&[b as u8, g as u8, r as u8, a as u8]) 339 | .unwrap(); 340 | } 341 | } 342 | buf.flush().unwrap(); 343 | } 344 | 345 | impl State {} 346 | 347 | pub struct Window { 348 | pub surface: wl_surface::WlSurface, 349 | pub xdg_surface: xdg_surface::XdgSurface, 350 | pub xdg_toplevel: xdg_toplevel::XdgToplevel, 351 | pub viewport: WpViewport, 352 | pub pending_configure: Configure, 353 | pub configures_received: Vec<(u32, Configure)>, 354 | pub close_requested: bool, 355 | } 356 | 357 | impl Window { 358 | pub fn commit(&self) { 359 | self.surface.commit(); 360 | } 361 | 362 | pub fn ack_last(&self) { 363 | let serial = self.configures_received.last().unwrap().0; 364 | self.xdg_surface.ack_configure(serial); 365 | } 366 | 367 | pub fn ack_last_and_commit(&self) { 368 | self.ack_last(); 369 | self.commit(); 370 | } 371 | 372 | pub fn attach_new_buffer(&self, buffer: &wl_buffer::WlBuffer) { 373 | self.surface.attach(Some(buffer), 0, 0); 374 | } 375 | 376 | pub fn set_size(&self, w: u16, h: u16) { 377 | self.viewport.set_destination(i32::from(w), i32::from(h)); 378 | } 379 | 380 | pub fn set_title(&self, title: &str) { 381 | self.xdg_toplevel.set_title(title.to_owned()); 382 | } 383 | } 384 | 385 | impl Dispatch> for State { 386 | fn event( 387 | _state: &mut Self, 388 | _proxy: &WlCallback, 389 | event: ::Event, 390 | data: &Arc, 391 | _conn: &Connection, 392 | _qhandle: &QueueHandle, 393 | ) { 394 | match event { 395 | wl_callback::Event::Done { .. } => data.done.store(true, Ordering::Relaxed), 396 | _ => unreachable!(), 397 | } 398 | } 399 | } 400 | 401 | impl Dispatch for State { 402 | fn event( 403 | _: &mut Self, 404 | wm_base: &xdg_wm_base::XdgWmBase, 405 | event: xdg_wm_base::Event, 406 | _: &(), 407 | _: &Connection, 408 | _: &QueueHandle, 409 | ) { 410 | if let xdg_wm_base::Event::Ping { serial } = event { 411 | wm_base.pong(serial); 412 | } 413 | } 414 | } 415 | 416 | impl Dispatch for State { 417 | fn event( 418 | state: &mut Self, 419 | xdg_surface: &xdg_surface::XdgSurface, 420 | event: xdg_surface::Event, 421 | _: &(), 422 | _: &Connection, 423 | _qh: &QueueHandle, 424 | ) { 425 | match event { 426 | xdg_surface::Event::Configure { serial } => { 427 | let window = state 428 | .windows 429 | .iter_mut() 430 | .find(|w| w.xdg_surface == *xdg_surface) 431 | .unwrap(); 432 | let configure = window.pending_configure.clone(); 433 | window.configures_received.push((serial, configure)); 434 | } 435 | _ => unreachable!(), 436 | } 437 | } 438 | } 439 | 440 | impl Dispatch for State { 441 | fn event( 442 | state: &mut Self, 443 | xdg_toplevel: &xdg_toplevel::XdgToplevel, 444 | event: xdg_toplevel::Event, 445 | _: &(), 446 | _: &Connection, 447 | _: &QueueHandle, 448 | ) { 449 | let window = state 450 | .windows 451 | .iter_mut() 452 | .find(|w| w.xdg_toplevel == *xdg_toplevel) 453 | .unwrap(); 454 | 455 | match event { 456 | xdg_toplevel::Event::Configure { 457 | width, 458 | height, 459 | states, 460 | } => { 461 | let configure = &mut window.pending_configure; 462 | configure.size = (width, height); 463 | configure.states = states 464 | .chunks_exact(4) 465 | .flat_map(TryInto::<[u8; 4]>::try_into) 466 | .map(u32::from_ne_bytes) 467 | .flat_map(xdg_toplevel::State::try_from) 468 | .collect(); 469 | } 470 | xdg_toplevel::Event::Close => { 471 | window.close_requested = true; 472 | } 473 | xdg_toplevel::Event::ConfigureBounds { width, height } => { 474 | window.pending_configure.bounds = Some((width, height)); 475 | } 476 | xdg_toplevel::Event::WmCapabilities { .. } => (), 477 | _ => unreachable!(), 478 | } 479 | } 480 | } 481 | 482 | impl Dispatch for State { 483 | fn event( 484 | state: &mut Self, 485 | seat: &wl_seat::WlSeat, 486 | event: wl_seat::Event, 487 | _: &(), 488 | _: &Connection, 489 | qh: &QueueHandle, 490 | ) { 491 | if let wl_seat::Event::Capabilities { 492 | capabilities: WEnum::Value(capabilities), 493 | } = event 494 | { 495 | if capabilities.contains(wl_seat::Capability::Keyboard) { 496 | state.keyboard = Some(seat.get_keyboard(qh, ())); 497 | } 498 | if capabilities.contains(wl_seat::Capability::Pointer) { 499 | state.pointer = Some(seat.get_pointer(qh, ())); 500 | } 501 | } 502 | } 503 | } 504 | 505 | impl Dispatch for State { 506 | fn event( 507 | _state: &mut Self, 508 | _: &wl_keyboard::WlKeyboard, 509 | event: wl_keyboard::Event, 510 | _: &(), 511 | _: &Connection, 512 | _: &QueueHandle, 513 | ) { 514 | tracing::debug!("{:?}", event); 515 | } 516 | } 517 | 518 | impl Dispatch for State { 519 | fn event( 520 | state: &mut Self, 521 | _: &wl_pointer::WlPointer, 522 | event: wl_pointer::Event, 523 | _: &(), 524 | _: &Connection, 525 | _: &QueueHandle, 526 | ) { 527 | tracing::debug!("{:?}", event); 528 | state.mouse_events.push(MouseEvents::Pointer(event)); 529 | } 530 | } 531 | 532 | impl Dispatch for State { 533 | fn event( 534 | state: &mut Self, 535 | _: &ZwpRelativePointerV1, 536 | event: zwp_relative_pointer_v1::Event, 537 | _: &(), 538 | _: &Connection, 539 | _: &QueueHandle, 540 | ) { 541 | tracing::debug!("{:?}", event); 542 | state.mouse_events.push(MouseEvents::Relative(event)); 543 | } 544 | } 545 | 546 | impl Dispatch for State { 547 | fn event( 548 | state: &mut Self, 549 | _: &zwp_confined_pointer_v1::ZwpConfinedPointerV1, 550 | event: zwp_confined_pointer_v1::Event, 551 | _: &(), 552 | _: &Connection, 553 | _: &QueueHandle, 554 | ) { 555 | tracing::debug!("{:?}", event); 556 | match event { 557 | zwp_confined_pointer_v1::Event::Confined => { 558 | state.pointer_confined = true; 559 | } 560 | zwp_confined_pointer_v1::Event::Unconfined => { 561 | state.pointer_confined = false; 562 | } 563 | _ => {} 564 | } 565 | } 566 | } 567 | -------------------------------------------------------------------------------- /wayland-display-core/src/utils/allocator/cuda/ffi.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![allow(non_camel_case_types)] 3 | #![allow(non_snake_case)] 4 | 5 | use crate::utils::allocator::cuda::CUDAContext; 6 | use gst::ffi as gst_ffi; 7 | use gst::ffi::{GstContext, GstElement, GstQuery}; 8 | use gst::glib::ffi as glib_ffi; 9 | use gst_video::VideoInfoDmaDrm; 10 | use gst_video::ffi::GstVideoInfo; 11 | use gst_video::glib::gobject_ffi; 12 | use gst_video::glib::translate::{FromGlibPtrNone, ToGlibPtr}; 13 | use libloading::{Library, Symbol}; 14 | use smithay::backend::egl::ffi::egl::types::{EGLDisplay, EGLImageKHR, EGLint}; 15 | use std::ffi::c_void; 16 | use std::os::raw::{c_int, c_uint}; 17 | use std::ptr; 18 | use std::sync::{Arc, OnceLock}; 19 | 20 | pub type GstCudaContext = *mut c_void; 21 | 22 | #[macro_export] 23 | macro_rules! cuda_call { 24 | ($expression:expr) => {{ 25 | let result = unsafe { $expression }; 26 | if result != CUDA_SUCCESS { 27 | Err(format!("CUDA error: {}", cuda_result_to_string(result))) 28 | } else { 29 | Ok(()) 30 | } 31 | }}; 32 | } 33 | 34 | type GstCudaStream = *mut c_void; 35 | pub(crate) type GstBufferPool = *mut c_void; 36 | pub(crate) type GstCudaStreamHandle = *mut c_void; 37 | #[repr(C)] 38 | struct GstCudaMemory { 39 | mem: gst_ffi::GstMemory, 40 | context: GstCudaContext, 41 | info: GstVideoInfo, 42 | priv_: *mut c_void, // GstCudaMemoryPrivate pointer 43 | _gst_reserved: [*mut c_void; 4], // GST_PADDING (see gstconfig.h) 44 | } 45 | 46 | impl GstCudaMemory { 47 | /// Safely borrow the GstVideoInfo from a GstMemory pointer 48 | fn from_gst_memory<'a>(mem: *const gst_ffi::GstMemory) -> Option<&'a Self> { 49 | if mem.is_null() { 50 | return None; 51 | } 52 | Some(unsafe { &*(mem as *const GstCudaMemory) }) 53 | } 54 | 55 | /// Get a safe VideoInfo wrapper 56 | pub fn video_info_safe(&self) -> gst_video::VideoInfo { 57 | unsafe { gst_video::VideoInfo::from_glib_none(&self.info as *const _) } 58 | } 59 | } 60 | 61 | #[repr(C)] 62 | pub(crate) struct CUeglFrame { 63 | pub(crate) frame: CUeglFrameUnion, 64 | pub(crate) width: c_uint, 65 | pub(crate) height: c_uint, 66 | pub(crate) depth: c_uint, 67 | pub(crate) pitch: c_uint, 68 | pub(crate) plane_count: c_uint, 69 | pub(crate) num_channels: c_uint, 70 | // Followings are ENUMS 71 | pub(crate) frame_type: c_uint, 72 | pub(crate) egl_color_format: c_uint, 73 | pub(crate) cu_format: c_uint, 74 | } 75 | 76 | #[repr(C)] 77 | pub(crate) union CUeglFrameUnion { 78 | pub p_array: [CUarray; MAX_PLANES], 79 | pub p_pitch: [*mut c_void; MAX_PLANES], 80 | } 81 | 82 | const MAX_PLANES: usize = 3; 83 | 84 | // CUDA driver API types 85 | type CUcontext = *mut c_void; 86 | type CUstream = *mut c_void; 87 | type CUdeviceptr = u64; 88 | type CUarray = *mut c_void; 89 | pub(crate) type CUgraphicsResource = *mut c_void; 90 | type CUresult = c_uint; 91 | 92 | // CUDA constants 93 | pub(crate) const CUDA_SUCCESS: CUresult = 0; 94 | 95 | // EGL constants 96 | pub(crate) const EGL_NO_IMAGE_KHR: EGLImageKHR = ptr::null_mut(); 97 | pub(crate) const EGL_LINUX_DMA_BUF_EXT: u32 = 0x3270; 98 | pub(crate) const EGL_DMA_BUF_PLANE0_FD_EXT: EGLint = 0x3272; 99 | pub(crate) const EGL_DMA_BUF_PLANE0_OFFSET_EXT: EGLint = 0x3273; 100 | pub(crate) const EGL_DMA_BUF_PLANE0_PITCH_EXT: EGLint = 0x3274; 101 | pub(crate) const EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT: EGLint = 0x3443; 102 | pub(crate) const EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT: EGLint = 0x3444; 103 | pub(crate) const EGL_DMA_BUF_PLANE1_FD_EXT: EGLint = 0x3275; 104 | pub(crate) const EGL_DMA_BUF_PLANE1_OFFSET_EXT: EGLint = 0x3276; 105 | pub(crate) const EGL_DMA_BUF_PLANE1_PITCH_EXT: EGLint = 0x3277; 106 | pub(crate) const EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT: EGLint = 0x3445; 107 | pub(crate) const EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT: EGLint = 0x3446; 108 | pub(crate) const EGL_WIDTH: EGLint = 0x3057; 109 | pub(crate) const EGL_HEIGHT: EGLint = 0x3056; 110 | pub(crate) const EGL_LINUX_DRM_FOURCC_EXT: EGLint = 0x3271; 111 | pub(crate) const EGL_NONE: EGLint = 0x3038; 112 | 113 | unsafe extern "C" { 114 | // CUDA Driver API 115 | fn CuMemcpy2DAsync(pCopy: *const CUDA_MEMCPY2D, stream: CUstream) -> CUresult; 116 | } 117 | 118 | // Add dynamic function pointer types for CUDA-EGL interop 119 | pub(crate) type CuGraphicsEGLRegisterImageFn = unsafe extern "C" fn( 120 | pCudaResource: *mut CUgraphicsResource, 121 | image: EGLImageKHR, 122 | flags: c_uint, 123 | ) -> CUresult; 124 | 125 | pub(crate) type CuGraphicsUnregisterResourceFn = 126 | unsafe extern "C" fn(resource: CUgraphicsResource) -> CUresult; 127 | 128 | pub(crate) type CuGraphicsResourceGetMappedEglFrameFn = unsafe extern "C" fn( 129 | pEglFrame: *mut CUeglFrame, 130 | resource: CUgraphicsResource, 131 | index: c_uint, 132 | mipLevel: c_uint, 133 | ) -> CUresult; 134 | 135 | // Structure to hold dynamically loaded EGL interop functions 136 | pub(crate) struct CudaEglFunctions { 137 | // Keep the library handle alive so the symbols remain valid 138 | _lib: Arc, 139 | pub register_image: CuGraphicsEGLRegisterImageFn, 140 | pub unregister_resource: CuGraphicsUnregisterResourceFn, 141 | pub get_mapped_frame: CuGraphicsResourceGetMappedEglFrameFn, 142 | } 143 | 144 | impl CudaEglFunctions { 145 | /// Load CUDA-EGL interop functions dynamically from the CUDA library 146 | pub fn load() -> Result { 147 | unsafe { 148 | // Try to open the CUDA library 149 | let lib = Library::new("libcuda.so.1") 150 | .or_else(|_| Library::new("libcuda.so")) 151 | .map_err(|e| format!("Failed to open CUDA library: {}", e))?; 152 | 153 | // Load cuGraphicsEGLRegisterImage 154 | let register_image: Symbol = lib 155 | .get(b"cuGraphicsEGLRegisterImage") 156 | .map_err(|e| format!("Failed to load cuGraphicsEGLRegisterImage: {}", e))?; 157 | let register_image = *register_image; 158 | 159 | // Load cuGraphicsUnregisterResource 160 | let unregister_resource: Symbol = lib 161 | .get(b"cuGraphicsUnregisterResource") 162 | .map_err(|e| format!("Failed to load cuGraphicsUnregisterResource: {}", e))?; 163 | let unregister_resource = *unregister_resource; 164 | 165 | // Load cuGraphicsResourceGetMappedEglFrame 166 | let get_mapped_frame: Symbol = lib 167 | .get(b"cuGraphicsResourceGetMappedEglFrame") 168 | .map_err(|e| { 169 | format!("Failed to load cuGraphicsResourceGetMappedEglFrame: {}", e) 170 | })?; 171 | let get_mapped_frame = *get_mapped_frame; 172 | 173 | Ok(CudaEglFunctions { 174 | _lib: Arc::new(lib), 175 | register_image, 176 | unregister_resource, 177 | get_mapped_frame, 178 | }) 179 | } 180 | } 181 | 182 | /// Convenience method to register an EGL image 183 | pub unsafe fn register_egl_image( 184 | &self, 185 | resource: *mut CUgraphicsResource, 186 | image: EGLImageKHR, 187 | flags: c_uint, 188 | ) -> Result<(), String> { 189 | let result = unsafe { (self.register_image)(resource, image, flags) }; 190 | if result == CUDA_SUCCESS { 191 | Ok(()) 192 | } else { 193 | Err(cuda_result_to_string(result).into()) 194 | } 195 | } 196 | 197 | /// Convenience method to unregister a resource 198 | pub unsafe fn unregister_resource(&self, resource: CUgraphicsResource) -> Result<(), String> { 199 | let result = unsafe { (self.unregister_resource)(resource) }; 200 | if result == CUDA_SUCCESS { 201 | Ok(()) 202 | } else { 203 | Err(cuda_result_to_string(result).into()) 204 | } 205 | } 206 | 207 | /// Convenience method to get mapped EGL frame 208 | pub unsafe fn get_mapped_egl_frame( 209 | &self, 210 | resource: CUgraphicsResource, 211 | index: c_uint, 212 | mip_level: c_uint, 213 | ) -> Result { 214 | let mut frame: CUeglFrame = unsafe { std::mem::zeroed() }; 215 | let result = unsafe { (self.get_mapped_frame)(&mut frame, resource, index, mip_level) }; 216 | if result == CUDA_SUCCESS { 217 | Ok(frame) 218 | } else { 219 | Err(cuda_result_to_string(result).into()) 220 | } 221 | } 222 | } 223 | 224 | static CUDA_EGL_FUNCTIONS: OnceLock = OnceLock::new(); 225 | 226 | pub fn init_cuda_egl() -> Result<(), String> { 227 | CUDA_EGL_FUNCTIONS 228 | .get_or_init(|| CudaEglFunctions::load().expect("Failed to load CUDA-EGL functions")); 229 | Ok(()) 230 | } 231 | 232 | pub fn get_cuda_egl_functions() -> Result<&'static CudaEglFunctions, String> { 233 | CUDA_EGL_FUNCTIONS.get().ok_or_else(|| { 234 | "CUDA EGL functions not initialized. Call init_cuda_egl() first.".to_string() 235 | }) 236 | } 237 | 238 | fn gst_dma_video_info_to_video_info( 239 | dma_video_info: &VideoInfoDmaDrm, 240 | ) -> Result { 241 | let mut video_info: GstVideoInfo = unsafe { std::mem::zeroed() }; 242 | unsafe { gst_video::ffi::gst_video_info_init(&mut video_info) }; 243 | 244 | let result = unsafe { 245 | gst_video::ffi::gst_video_info_dma_drm_to_video_info( 246 | dma_video_info.to_glib_none().0, 247 | &mut video_info, 248 | ) 249 | }; 250 | if result == glib_ffi::GFALSE { 251 | return Err("Failed to convert DMA-BUF video info to GStreamer video info".into()); 252 | } 253 | 254 | Ok(video_info) 255 | } 256 | 257 | // EGLImage extension function pointers 258 | pub(crate) type PFN_eglCreateImageKHR = unsafe extern "C" fn( 259 | dpy: EGLDisplay, 260 | ctx: *mut c_void, 261 | target: u32, 262 | buffer: *mut c_void, 263 | attrib_list: *const EGLint, 264 | ) -> EGLImageKHR; 265 | 266 | pub(crate) type PFN_eglDestroyImageKHR = 267 | unsafe extern "C" fn(dpy: EGLDisplay, image: EGLImageKHR) -> c_int; 268 | 269 | // CUDA memcpy2D structure 270 | #[repr(C)] 271 | struct CUDA_MEMCPY2D { 272 | pub srcXInBytes: usize, 273 | pub srcY: usize, 274 | pub srcMemoryType: c_uint, 275 | pub srcHost: *const c_void, 276 | pub srcDevice: CUdeviceptr, 277 | pub srcArray: CUarray, 278 | pub srcPitch: usize, 279 | pub dstXInBytes: usize, 280 | pub dstY: usize, 281 | pub dstMemoryType: c_uint, 282 | pub dstHost: *mut c_void, 283 | pub dstDevice: CUdeviceptr, 284 | pub dstArray: CUarray, 285 | pub dstPitch: usize, 286 | pub WidthInBytes: usize, 287 | pub Height: usize, 288 | } 289 | 290 | #[allow(dead_code)] 291 | const CU_MEMORYTYPE_HOST: c_uint = 1; 292 | #[allow(dead_code)] 293 | const CU_MEMORYTYPE_DEVICE: c_uint = 2; 294 | #[allow(dead_code)] 295 | const CU_MEMORYTYPE_ARRAY: c_uint = 3; 296 | #[allow(dead_code)] 297 | const CU_MEMORYTYPE_UNIFIED: c_uint = 4; 298 | 299 | unsafe extern "C" { 300 | // gstcudaloader 301 | pub(crate) fn gst_cuda_load_library() -> glib_ffi::gboolean; 302 | 303 | // GstCudaContext functions 304 | pub(crate) fn gst_cuda_context_new(device_id: c_int) -> *mut GstCudaContext; 305 | fn gst_cuda_context_get_handle(context: *mut GstCudaContext) -> CUcontext; 306 | fn gst_cuda_context_push(context: *mut GstCudaContext) -> glib_ffi::gboolean; 307 | fn gst_cuda_context_pop(pctx: *mut CUcontext) -> glib_ffi::gboolean; 308 | pub(crate) fn gst_cuda_ensure_element_context( 309 | element: *mut GstElement, 310 | device_id: c_int, 311 | cuda_ctx: *mut *mut GstCudaContext, 312 | ) -> glib_ffi::gboolean; 313 | 314 | pub(crate) fn gst_cuda_handle_set_context( 315 | element: *mut GstElement, 316 | context: *mut GstContext, 317 | device_id: c_int, 318 | cuda_ctx: *mut *mut GstCudaContext, 319 | ) -> glib_ffi::gboolean; 320 | 321 | // GstCudaStream functions 322 | pub(crate) fn gst_cuda_stream_new(context: *mut GstCudaContext) -> GstCudaStreamHandle; 323 | pub(crate) fn gst_cuda_stream_ref(stream: GstCudaStreamHandle); 324 | pub(crate) fn gst_cuda_stream_unref(stream: GstCudaStreamHandle); 325 | pub(crate) fn gst_clear_cuda_stream(stream: GstCudaStreamHandle); 326 | 327 | // GstCudaMemory functions 328 | fn gst_cuda_allocator_alloc( 329 | allocator: *mut gst_ffi::GstAllocator, 330 | context: *mut GstCudaContext, 331 | stream: GstCudaStream, 332 | info: *const gst_video::ffi::GstVideoInfo, 333 | ) -> *mut gst_ffi::GstMemory; 334 | 335 | fn gst_cuda_allocator_alloc_wrapped( 336 | allocator: *mut gst_ffi::GstAllocator, 337 | context: *mut GstCudaContext, 338 | stream: GstCudaStream, 339 | info: *const gst_video::ffi::GstVideoInfo, 340 | dev_ptr: *mut CUdeviceptr, 341 | user_data: *mut c_void, 342 | notify: Option, 343 | ) -> *mut gst_ffi::GstMemory; 344 | 345 | fn gst_is_cuda_memory(mem: *mut gst_ffi::GstMemory) -> glib_ffi::gboolean; 346 | 347 | pub(crate) fn gst_cuda_memory_init_once() -> c_void; 348 | 349 | pub(crate) fn gst_cuda_buffer_pool_new(context: *mut GstCudaContext) -> GstBufferPool; 350 | pub(crate) fn gst_cuda_buffer_pool_get_type() -> glib_ffi::GType; 351 | pub(crate) fn gst_buffer_pool_config_set_cuda_stream( 352 | config: *mut gst_ffi::GstStructure, 353 | stream: GstCudaStreamHandle, 354 | ); 355 | 356 | fn gst_cuda_stream_get_handle(stream: GstCudaStream) -> CUstream; 357 | 358 | fn gst_cuda_memory_get_stream(mem: *mut gst_ffi::GstMemory) -> GstCudaStream; 359 | 360 | pub(crate) fn gst_cuda_handle_context_query( 361 | element: *mut GstElement, 362 | query: *mut GstQuery, 363 | gst_cuda_context: *mut GstCudaContext, 364 | ) -> glib_ffi::gboolean; 365 | } 366 | 367 | pub(crate) struct CudaContextGuard; 368 | 369 | impl CudaContextGuard { 370 | pub fn new(cuda_context: &CUDAContext) -> Result { 371 | if unsafe { gst_cuda_context_push(cuda_context.ptr) } == glib_ffi::GFALSE { 372 | return Err("Failed to push CUDA context".into()); 373 | } 374 | Ok(CudaContextGuard) 375 | } 376 | } 377 | 378 | impl Drop for CudaContextGuard { 379 | fn drop(&mut self) { 380 | unsafe { 381 | gst_cuda_context_pop(ptr::null_mut()); 382 | } 383 | } 384 | } 385 | 386 | pub(crate) const GST_BUFFER_POOL_OPTION_VIDEO_META: &[u8] = b"GstBufferPoolOptionVideoMeta\0"; 387 | const GST_MAP_CUDA: u32 = gst_ffi::GST_MAP_FLAG_LAST << 1; 388 | 389 | pub(crate) fn gst_is_cuda_buffer_pool(obj: *mut gst::ffi::GstBufferPool) -> bool { 390 | unsafe { 391 | gobject_ffi::g_type_check_instance_is_a( 392 | obj as *mut gobject_ffi::GTypeInstance, 393 | gst_cuda_buffer_pool_get_type(), 394 | ) != 0 395 | } 396 | } 397 | 398 | fn alloc_cuda_buffer( 399 | cuda_context: &CUDAContext, 400 | video_info: &VideoInfoDmaDrm, 401 | ) -> Result> { 402 | let mut gst_video_info = gst_dma_video_info_to_video_info(video_info)?; 403 | 404 | // Use the stream from the context if available 405 | let stream = cuda_context 406 | .stream() 407 | .as_ref() 408 | .map(|s| s.stream) 409 | .unwrap_or(unsafe { std::mem::zeroed() }); 410 | 411 | let gst_memory = unsafe { 412 | gst_cuda_allocator_alloc( 413 | ptr::null_mut(), 414 | cuda_context.ptr, 415 | stream, 416 | &mut gst_video_info, 417 | ) 418 | }; 419 | if gst_memory.is_null() { 420 | return Err("Failed to allocate GST CUDA memory".into()); 421 | } 422 | 423 | let mut buffer = gst::Buffer::new(); 424 | let buffer_ref = buffer.get_mut().unwrap(); 425 | buffer_ref.append_memory(unsafe { gst::Memory::from_glib_full(gst_memory) }); 426 | 427 | Ok(buffer) 428 | } 429 | 430 | pub(crate) fn acquire_or_alloc_buffer( 431 | buffer_pool: Option, 432 | cuda_context: &CUDAContext, 433 | video_info: &VideoInfoDmaDrm, 434 | ) -> Result> { 435 | if let Some(pool) = buffer_pool { 436 | // Use the pool if available 437 | let mut gst_buffer: *mut gst_ffi::GstBuffer = ptr::null_mut(); 438 | let result = unsafe { 439 | gst::ffi::gst_buffer_pool_acquire_buffer( 440 | pool as *mut gst::ffi::GstBufferPool, 441 | &mut gst_buffer, 442 | ptr::null_mut(), 443 | ) 444 | }; 445 | 446 | if result != gst_ffi::GST_FLOW_OK { 447 | tracing::info!("Failed to acquire buffer from pool: {:?}", result); 448 | return alloc_cuda_buffer(cuda_context, video_info); 449 | } 450 | 451 | if gst_buffer.is_null() { 452 | return Err("Acquired buffer is null".into()); 453 | } 454 | 455 | Ok(unsafe { gst::Buffer::from_glib_full(gst_buffer) }) 456 | } else { 457 | // Fallback to direct allocation 458 | tracing::info!("No buffer pool available, allocating directly"); 459 | alloc_cuda_buffer(cuda_context, video_info) 460 | } 461 | } 462 | 463 | pub(crate) fn copy_to_gst_buffer( 464 | egl_frame: CUeglFrame, 465 | gst_buffer: &mut gst::Buffer, 466 | cuda_context: &CUDAContext, 467 | ) -> Result> { 468 | let stream_handle = cuda_context 469 | .stream() 470 | .as_ref() 471 | .map(|s| unsafe { gst_cuda_stream_get_handle(s.stream) }) 472 | .unwrap_or(unsafe { std::mem::zeroed() }); 473 | 474 | // Get memory from the buffer 475 | let gst_memory = unsafe { gst_ffi::gst_buffer_peek_memory(gst_buffer.as_mut_ptr(), 0) }; 476 | 477 | if gst_memory.is_null() { 478 | return Err("Failed to get memory from buffer".into()); 479 | } 480 | 481 | let video_info = GstCudaMemory::from_gst_memory(gst_memory) 482 | .ok_or("Failed to get GstCudaMemory")? 483 | .video_info_safe(); 484 | 485 | // Map the GStreamer memory 486 | let mut map_info: gst_ffi::GstMapInfo = unsafe { std::mem::zeroed() }; 487 | let map_success = unsafe { 488 | gst_ffi::gst_memory_map( 489 | gst_memory, 490 | &mut map_info, 491 | gst_ffi::GST_MAP_WRITE | GST_MAP_CUDA, 492 | ) 493 | }; 494 | 495 | if map_success == glib_ffi::GFALSE { 496 | return Err("Failed to map GStreamer CUDA memory".into()); 497 | } 498 | 499 | let dst_device_ptr = map_info.data as CUdeviceptr; 500 | 501 | let _cuda_context_guard = CudaContextGuard::new(cuda_context)?; 502 | 503 | // Copy from EGL frame to GStreamer memory for each plane 504 | for plane in 0..egl_frame.plane_count as usize { 505 | let mut copy_params: CUDA_MEMCPY2D = unsafe { std::mem::zeroed() }; 506 | 507 | // Set up source (from EGL frame) 508 | match egl_frame.frame_type { 509 | // Array type 510 | 0 => { 511 | copy_params.srcMemoryType = CU_MEMORYTYPE_ARRAY; 512 | copy_params.srcArray = unsafe { egl_frame.frame.p_array[plane] }; 513 | } 514 | // Pitched pointer type 515 | 1 => { 516 | copy_params.srcMemoryType = CU_MEMORYTYPE_DEVICE; 517 | copy_params.srcDevice = unsafe { egl_frame.frame.p_pitch[plane] as CUdeviceptr }; 518 | copy_params.srcPitch = egl_frame.pitch as usize; 519 | } 520 | _ => { 521 | return Err("Unsupported EGL frame type".into()); 522 | } 523 | } 524 | 525 | copy_params.dstMemoryType = CU_MEMORYTYPE_DEVICE; 526 | copy_params.dstDevice = dst_device_ptr + video_info.offset()[plane] as u64; 527 | copy_params.dstPitch = video_info.stride()[plane] as usize; 528 | copy_params.WidthInBytes = video_info.comp_width(plane as u8) as usize 529 | * video_info.comp_pstride(plane as u8) as usize; 530 | copy_params.Height = video_info.comp_height(plane as u8) as usize; 531 | 532 | cuda_call!(CuMemcpy2DAsync(©_params, stream_handle))?; 533 | } 534 | 535 | // Safe to unmap without synchronization as the copy is async 536 | unsafe { gst_ffi::gst_memory_unmap(gst_memory, &mut map_info) }; 537 | Ok(video_info) 538 | } 539 | 540 | pub(crate) fn cuda_result_to_string(result: CUresult) -> &'static str { 541 | match result { 542 | CUDA_SUCCESS => "CUDA_SUCCESS", 543 | 1 => "CUDA_ERROR_INVALID_VALUE", 544 | 2 => "CUDA_ERROR_OUT_OF_MEMORY", 545 | 3 => "CUDA_ERROR_NOT_INITIALIZED", 546 | 4 => "CUDA_ERROR_DEINITIALIZED", 547 | 100 => "CUDA_ERROR_NO_DEVICE", 548 | 101 => "CUDA_ERROR_INVALID_DEVICE", 549 | 200 => "CUDA_ERROR_INVALID_IMAGE", 550 | 201 => "CUDA_ERROR_INVALID_CONTEXT", 551 | _ => "CUDA_ERROR_UNKNOWN", 552 | } 553 | } 554 | -------------------------------------------------------------------------------- /wayland-display-core/src/comp/input.rs: -------------------------------------------------------------------------------- 1 | use super::{State, focus::FocusTarget}; 2 | use smithay::backend::input::Keycode; 3 | use smithay::backend::libinput::LibinputInputBackend; 4 | use smithay::input::keyboard::Keysym; 5 | use smithay::reexports::input::event::pointer::PointerEventTrait; 6 | use smithay::wayland::seat::WaylandFocus; 7 | use smithay::{ 8 | backend::input::{ 9 | AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputEvent, KeyState, 10 | KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent, TouchEvent, 11 | TouchSlot, 12 | }, 13 | input::{ 14 | keyboard::{FilterResult, keysyms}, 15 | pointer::{AxisFrame, ButtonEvent, MotionEvent, RelativeMotionEvent}, 16 | touch::{DownEvent, MotionEvent as TouchMotionEvent, UpEvent}, 17 | }, 18 | reexports::{ 19 | input::LibinputInterface, 20 | rustix::fs::{Mode, OFlags, open}, 21 | }, 22 | utils::{Logical, Point, SERIAL_COUNTER, Serial}, 23 | wayland::pointer_constraints::{PointerConstraint, with_pointer_constraint}, 24 | }; 25 | use std::{os::unix::io::OwnedFd, path::Path, time::Instant}; 26 | 27 | pub struct NixInterface; 28 | 29 | impl LibinputInterface for NixInterface { 30 | fn open_restricted(&mut self, path: &Path, flags: i32) -> Result { 31 | open( 32 | path, 33 | OFlags::from_bits_truncate(flags as u32), 34 | Mode::empty(), 35 | ) 36 | .map_err(|err| err.raw_os_error()) 37 | } 38 | fn close_restricted(&mut self, fd: OwnedFd) { 39 | let _ = fd; 40 | } 41 | } 42 | 43 | impl State { 44 | pub fn scancode_to_keycode(&self, scancode: u32) -> Keycode { 45 | // see: https://github.com/rust-x-bindings/xkbcommon-rs/blob/cb449998d8a3de375d492fb7ec015b2925f38ddb/src/xkb/mod.rs#L50-L58 46 | Keycode::new(scancode + 8) 47 | } 48 | 49 | pub fn keyboard_input(&mut self, event_time_msec: u32, keycode: Keycode, state: KeyState) { 50 | let serial = SERIAL_COUNTER.next_serial(); 51 | let keyboard = self.seat.get_keyboard().unwrap(); 52 | 53 | keyboard.input::<(), _>( 54 | self, 55 | keycode, 56 | state, 57 | serial, 58 | event_time_msec, 59 | |data, modifiers, handle| { 60 | if state == KeyState::Pressed { 61 | if modifiers.ctrl && modifiers.shift && !modifiers.alt && !modifiers.logo { 62 | match handle.modified_sym() { 63 | Keysym::Tab => { 64 | if let Some(element) = data.space.elements().last().cloned() { 65 | data.surpressed_keys.insert(keysyms::KEY_Tab); 66 | let location = data.space.element_location(&element).unwrap(); 67 | data.space.map_element(element.clone(), location, true); 68 | data.seat.get_keyboard().unwrap().set_focus( 69 | data, 70 | Some(FocusTarget::from(element)), 71 | serial, 72 | ); 73 | return FilterResult::Intercept(()); 74 | } 75 | } 76 | Keysym::Q => { 77 | if let Some(target) = 78 | data.seat.get_keyboard().unwrap().current_focus() 79 | { 80 | match target { 81 | FocusTarget::Wayland(window) => { 82 | window.toplevel().unwrap().send_close(); 83 | } 84 | _ => return FilterResult::Forward, 85 | }; 86 | data.surpressed_keys.insert(keysyms::KEY_Q); 87 | return FilterResult::Intercept(()); 88 | } 89 | } 90 | _ => {} 91 | } 92 | } 93 | } else { 94 | if data.surpressed_keys.remove(&handle.modified_sym().raw()) { 95 | return FilterResult::Intercept(()); 96 | } 97 | } 98 | 99 | FilterResult::Forward 100 | }, 101 | ); 102 | } 103 | 104 | pub(crate) fn maybe_activate_pointer_constraint( 105 | &self, 106 | new_under: &Option<(FocusTarget, Point)>, 107 | new_location: Point, 108 | ) { 109 | let pointer = self.seat.get_pointer().unwrap(); 110 | 111 | if let Some((under, surface_location)) = new_under 112 | .as_ref() 113 | .and_then(|(target, loc)| Some((target.wl_surface()?, loc))) 114 | { 115 | with_pointer_constraint(&under, &pointer, |constraint| match constraint { 116 | Some(constraint) if !constraint.is_active() => { 117 | let point = new_location - *surface_location; 118 | if constraint 119 | .region() 120 | .map_or(true, |region| region.contains(point.to_i32_round())) 121 | { 122 | constraint.activate(); 123 | } 124 | } 125 | _ => {} 126 | }); 127 | } 128 | } 129 | 130 | fn can_pointer_move( 131 | &self, 132 | under: &Option<(FocusTarget, Point)>, 133 | target_position: Point, 134 | ) -> bool { 135 | let pointer = self.seat.get_pointer().unwrap(); 136 | let mut should_motion = true; 137 | 138 | let new_under = self 139 | .space 140 | .element_under(target_position) 141 | .map(|(w, pos)| (w.clone().into(), pos.to_f64())); 142 | 143 | if let Some((surface, surface_loc)) = 144 | under 145 | .as_ref() 146 | .and_then(|(target, l): &(FocusTarget, Point)| { 147 | Some((target.wl_surface()?, l)) 148 | }) 149 | { 150 | with_pointer_constraint(&surface, &pointer, |constraint| match constraint { 151 | Some(constraint) if constraint.is_active() => { 152 | // Constraint does not apply if not within region 153 | if !constraint.region().map_or(true, |x| { 154 | x.contains((pointer.current_location() - *surface_loc).to_i32_round()) 155 | }) { 156 | return; 157 | } 158 | match &*constraint { 159 | PointerConstraint::Locked(_locked) => { 160 | should_motion = false; 161 | } 162 | PointerConstraint::Confined(confine) => { 163 | // If confined, don't move pointer if it would go outside surface or region 164 | if let Some((surface, surface_loc)) = &under { 165 | if new_under.as_ref().and_then( 166 | |(under, _): &(FocusTarget, Point)| { 167 | under.wl_surface() 168 | }, 169 | ) != surface.wl_surface() 170 | { 171 | should_motion = false; 172 | } 173 | if let Some(region) = confine.region() { 174 | if !region 175 | .contains((target_position - *surface_loc).to_i32_round()) 176 | { 177 | should_motion = false; 178 | } 179 | } 180 | } 181 | } 182 | } 183 | } 184 | _ => {} 185 | }); 186 | } 187 | 188 | // If pointer is now in a constraint region, activate it 189 | self.maybe_activate_pointer_constraint(&new_under, target_position); 190 | 191 | should_motion 192 | } 193 | 194 | pub fn pointer_motion( 195 | &mut self, 196 | event_time_msec: u32, 197 | event_time_usec: u64, 198 | delta: Point, 199 | delta_unaccelerated: Point, 200 | ) { 201 | self.last_pointer_movement = Instant::now(); 202 | let serial = SERIAL_COUNTER.next_serial(); 203 | let pointer = self.seat.get_pointer().unwrap(); 204 | let under = self 205 | .space 206 | .element_under(self.pointer_location) 207 | .map(|(w, pos)| (w.clone().into(), pos.to_f64())); 208 | 209 | let possible_pos = self.clamp_coords(self.pointer_location + delta); 210 | 211 | // Pointer should only move if it's not locked or confined (and going out of bounds) 212 | if self.can_pointer_move(&under, possible_pos) { 213 | self.set_pointer_location(possible_pos); 214 | 215 | // Not sure why, but order here matters (at least when using Sway nested)!!! 216 | // If we send the motion event after the relative_motion it'll behave oddly 217 | pointer.motion( 218 | self, 219 | under.clone(), 220 | &MotionEvent { 221 | location: self.pointer_location, 222 | serial, 223 | time: event_time_msec, 224 | }, 225 | ); 226 | } 227 | 228 | // Relative motion is always applied 229 | pointer.relative_motion( 230 | self, 231 | under.map(|(w, pos)| (w, pos.to_f64())), 232 | &RelativeMotionEvent { 233 | delta, 234 | delta_unaccel: delta_unaccelerated, 235 | utime: event_time_usec, 236 | }, 237 | ); 238 | 239 | pointer.frame(self); 240 | } 241 | 242 | pub fn set_pointer_location(&mut self, location: Point) { 243 | self.pointer_location = location; 244 | self.pointer_absolute_location = location; 245 | } 246 | 247 | pub fn pointer_motion_absolute(&mut self, event_time_msec: u32, position: Point) { 248 | let relative_movement = ( 249 | position.x - self.pointer_absolute_location.x, 250 | position.y - self.pointer_absolute_location.y, 251 | ) 252 | .into(); 253 | 254 | self.pointer_motion( 255 | event_time_msec, 256 | event_time_msec as u64 * 1000, 257 | relative_movement, 258 | relative_movement, 259 | ); 260 | // pointer_absolute_location should always point to the unclamped position sent by Moonlight 261 | self.pointer_absolute_location = position; 262 | } 263 | 264 | pub fn pointer_button(&mut self, event_time_msec: u32, button_code: u32, state: ButtonState) { 265 | self.last_pointer_movement = Instant::now(); 266 | let serial = SERIAL_COUNTER.next_serial(); 267 | 268 | if ButtonState::Pressed == state { 269 | self.update_keyboard_focus(serial); 270 | }; 271 | let pointer = self.seat.get_pointer().unwrap(); 272 | pointer.button( 273 | self, 274 | &ButtonEvent { 275 | button: button_code, 276 | state: state.try_into().unwrap(), 277 | serial, 278 | time: event_time_msec, 279 | }, 280 | ); 281 | pointer.frame(self); 282 | } 283 | 284 | pub fn pointer_axis( 285 | &mut self, 286 | event_time_msec: u32, 287 | source: AxisSource, 288 | horizontal_amount: f64, 289 | vertical_amount: f64, 290 | horizontal_amount_discrete: Option, 291 | vertical_amount_discrete: Option, 292 | ) { 293 | let mut frame = AxisFrame::new(event_time_msec).source(source); 294 | if horizontal_amount != 0.0 { 295 | frame = frame.value(Axis::Horizontal, horizontal_amount); 296 | if let Some(discrete) = horizontal_amount_discrete { 297 | frame = frame.v120(Axis::Horizontal, discrete as i32); 298 | } 299 | } else if source == AxisSource::Finger { 300 | frame = frame.stop(Axis::Horizontal); 301 | } 302 | if vertical_amount != 0.0 { 303 | frame = frame.value(Axis::Vertical, vertical_amount); 304 | if let Some(discrete) = vertical_amount_discrete { 305 | frame = frame.v120(Axis::Vertical, discrete as i32); 306 | } 307 | } else if source == AxisSource::Finger { 308 | frame = frame.stop(Axis::Vertical); 309 | } 310 | let pointer = self.seat.get_pointer().unwrap(); 311 | pointer.axis(self, frame); 312 | pointer.frame(self); 313 | } 314 | 315 | fn touch_location_transformed< 316 | B: smithay::backend::input::InputBackend, 317 | E: AbsolutePositionEvent, 318 | >( 319 | &self, 320 | evt: &E, 321 | ) -> Option> { 322 | let output = self 323 | .space 324 | .outputs() 325 | .find(|output| output.name().starts_with("eDP")) 326 | .or_else(|| self.space.outputs().next())?; 327 | 328 | let output_geometry = self.space.output_geometry(output)?; 329 | let transform = output.current_transform(); 330 | let size = transform.invert().transform_size(output_geometry.size); 331 | 332 | Some( 333 | transform.transform_point_in(evt.position_transformed(size), &size.to_f64()) 334 | + output_geometry.loc.to_f64(), 335 | ) 336 | } 337 | 338 | pub fn relative_touch_to_logical( 339 | &mut self, 340 | relative_pos: Point, // 0.0 to 1.0 341 | ) -> Option> { 342 | let output = self 343 | .space 344 | .outputs() 345 | .find(|output| output.name().starts_with("eDP")) 346 | .or_else(|| self.space.outputs().next())?; 347 | 348 | let output_geometry = self.space.output_geometry(output)?; 349 | let transform = output.current_transform(); 350 | 351 | // Size before transform 352 | let untransformed_size = transform.invert().transform_size(output_geometry.size); 353 | let size_f64 = untransformed_size.to_f64(); 354 | 355 | // Scaled raw position in untransformed space 356 | let pos = Point::from((relative_pos.x * size_f64.w, relative_pos.y * size_f64.h)); 357 | 358 | // Now apply the output transform 359 | let transformed_pos = transform.transform_point_in(pos, &size_f64); 360 | 361 | // Map to global logical coordinates 362 | Some(transformed_pos + output_geometry.loc.to_f64()) 363 | } 364 | 365 | pub fn touch_down( 366 | &mut self, 367 | event_time_msec: u32, 368 | slot: TouchSlot, 369 | location: Point, 370 | ) { 371 | let serial = SERIAL_COUNTER.next_serial(); 372 | let touch = self.seat.get_touch().unwrap(); 373 | let under = self 374 | .space 375 | .element_under(location) 376 | .map(|(w, pos)| (w.clone().into(), pos.to_f64())); 377 | 378 | touch.down( 379 | self, 380 | under, 381 | &DownEvent { 382 | slot: slot, 383 | location: location, 384 | serial, 385 | time: event_time_msec, 386 | }, 387 | ); 388 | touch.frame(self); 389 | } 390 | 391 | pub fn touch_up(&mut self, event_time_msec: u32, slot: TouchSlot) { 392 | let serial = SERIAL_COUNTER.next_serial(); 393 | let touch = self.seat.get_touch().unwrap(); 394 | 395 | touch.up( 396 | self, 397 | &UpEvent { 398 | slot: slot, 399 | serial, 400 | time: event_time_msec, 401 | }, 402 | ); 403 | touch.frame(self); 404 | } 405 | 406 | pub fn touch_motion( 407 | &mut self, 408 | event_time_msec: u32, 409 | slot: TouchSlot, 410 | location: Point, 411 | ) { 412 | let touch = self.seat.get_touch().unwrap(); 413 | let under = self 414 | .space 415 | .element_under(location) 416 | .map(|(w, pos)| (w.clone().into(), pos.to_f64())); 417 | 418 | touch.motion( 419 | self, 420 | under, 421 | &TouchMotionEvent { 422 | slot, 423 | location, 424 | time: event_time_msec, 425 | }, 426 | ); 427 | touch.frame(self); 428 | } 429 | 430 | pub fn touch_cancel(&mut self) { 431 | let touch = self.seat.get_touch().unwrap(); 432 | touch.cancel(self); 433 | } 434 | 435 | pub fn touch_frame(&mut self) { 436 | let touch = self.seat.get_touch().unwrap(); 437 | touch.frame(self); 438 | } 439 | 440 | pub fn process_input_event(&mut self, event: InputEvent) { 441 | match event { 442 | InputEvent::Keyboard { event, .. } => { 443 | self.keyboard_input(event.time_msec(), event.key_code(), event.state()); 444 | } 445 | InputEvent::PointerMotion { event, .. } => { 446 | self.pointer_motion( 447 | event.time_msec(), 448 | event.time_usec(), 449 | event.delta(), 450 | event.delta_unaccel(), 451 | ); 452 | } 453 | InputEvent::PointerMotionAbsolute { event } => { 454 | if let Some(output) = self.output.as_ref() { 455 | let output_size = output 456 | .current_mode() 457 | .unwrap() 458 | .size 459 | .to_f64() 460 | .to_logical(output.current_scale().fractional_scale()) 461 | .to_i32_round(); 462 | 463 | let new_x = event.x_transformed(output_size.w); 464 | let new_y = event.y_transformed(output_size.h); 465 | 466 | self.pointer_motion_absolute(event.time_msec(), (new_x, new_y).into()); 467 | } 468 | } 469 | InputEvent::PointerButton { event, .. } => { 470 | self.pointer_button(event.time_msec(), event.button(), event.state()); 471 | } 472 | InputEvent::PointerAxis { event, .. } => { 473 | self.last_pointer_movement = Instant::now(); 474 | let horizontal_amount = event 475 | .amount(Axis::Horizontal) 476 | .or_else(|| event.amount_v120(Axis::Horizontal).map(|x| x * 3.0 / 120.0)) 477 | .unwrap_or(0.0); 478 | let vertical_amount = event 479 | .amount(Axis::Vertical) 480 | .or_else(|| event.amount_v120(Axis::Vertical).map(|y| y * 3.0 / 120.0)) 481 | .unwrap_or(0.0); 482 | let horizontal_amount_discrete = event.amount_v120(Axis::Horizontal); 483 | let vertical_amount_discrete = event.amount_v120(Axis::Vertical); 484 | 485 | self.pointer_axis( 486 | event.time_msec(), 487 | event.source(), 488 | horizontal_amount, 489 | vertical_amount, 490 | horizontal_amount_discrete, 491 | vertical_amount_discrete, 492 | ); 493 | } 494 | InputEvent::TouchDown { event, .. } => { 495 | if let Some(location) = self.touch_location_transformed(&event) { 496 | self.touch_down(event.time_msec(), event.slot(), location); 497 | } 498 | } 499 | InputEvent::TouchUp { event, .. } => { 500 | self.touch_up(event.time_msec(), event.slot()); 501 | } 502 | InputEvent::TouchMotion { event, .. } => { 503 | if let Some(location) = self.touch_location_transformed(&event) { 504 | self.touch_motion(event.time_msec(), event.slot(), location); 505 | } 506 | } 507 | InputEvent::TouchCancel { .. } => { 508 | self.touch_cancel(); 509 | } 510 | InputEvent::TouchFrame { .. } => { 511 | self.touch_frame(); 512 | } 513 | _ => {} 514 | } 515 | } 516 | 517 | fn clamp_coords(&self, pos: Point) -> Point { 518 | if let Some(output) = self.output.as_ref() { 519 | if let Some(mode) = output.current_mode() { 520 | return ( 521 | pos.x.max(0.0).min((mode.size.w - 2) as f64), 522 | pos.y.max(0.0).min((mode.size.h - 2) as f64), 523 | ) 524 | .into(); 525 | } 526 | } 527 | pos 528 | } 529 | 530 | fn update_keyboard_focus(&mut self, serial: Serial) { 531 | let pointer = self.seat.get_pointer().unwrap(); 532 | let keyboard = self.seat.get_keyboard().unwrap(); 533 | // change the keyboard focus unless the pointer or keyboard is grabbed 534 | // We test for any matching surface type here but always use the root 535 | // (in case of a window the toplevel) surface for the focus. 536 | // So for example if a user clicks on a subsurface or popup the toplevel 537 | // will receive the keyboard focus. Directly assigning the focus to the 538 | // matching surface leads to issues with clients dismissing popups and 539 | // subsurface menus (for example firefox-wayland). 540 | // see here for a discussion about that issue: 541 | // https://gitlab.freedesktop.org/wayland/wayland/-/issues/294 542 | if !pointer.is_grabbed() && !keyboard.is_grabbed() { 543 | if let Some((window, _)) = self 544 | .space 545 | .element_under(self.pointer_location) 546 | .map(|(w, p)| (w.clone(), p)) 547 | { 548 | self.space.raise_element(&window, true); 549 | keyboard.set_focus(self, Some(FocusTarget::from(window)), serial); 550 | return; 551 | } 552 | } 553 | } 554 | } 555 | 556 | #[cfg(test)] 557 | mod tests { 558 | use super::*; 559 | use crate::comp::State; 560 | use crate::utils::RenderTarget; 561 | use smithay::input::keyboard::xkb; 562 | use smithay::output::{Output, PhysicalProperties, Subpixel}; 563 | use smithay::reexports::calloop::EventLoop; 564 | use smithay::reexports::input::Libinput; 565 | use smithay::{reexports::wayland_server::Display, utils::Point}; 566 | 567 | struct TestState { 568 | state: State, 569 | } 570 | 571 | impl TestState { 572 | fn new() -> Self { 573 | let libinput_context = Libinput::new_from_path(NixInterface); 574 | let event_loop = EventLoop::::try_new().expect("Unable to create event_loop"); 575 | let display = Display::::new().unwrap(); 576 | let dh = display.handle(); 577 | 578 | let state = State::new( 579 | &RenderTarget::Software, 580 | &dh, 581 | &libinput_context, 582 | event_loop.handle(), 583 | ); 584 | 585 | TestState { state } 586 | } 587 | 588 | fn state(&mut self) -> &mut State { 589 | &mut self.state 590 | } 591 | } 592 | 593 | #[test] 594 | fn keyboard_scancode_conversion() { 595 | let mut harness = TestState::new(); 596 | let state = harness.state(); 597 | 598 | let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); 599 | let keymap = 600 | xkb::Keymap::new_from_names(&context, "", "", "us", "", None, xkb::COMPILE_NO_FLAGS) 601 | .unwrap(); 602 | let xkb_state = xkb::State::new(&keymap); 603 | 604 | // Evdev keycode, from `input-event-codes.h` 605 | // Linux evdev keycode (30) 606 | const KEY_A: u32 = 30; 607 | // ↓ +8 608 | // X11 keycode (38) 609 | let x11_key_code = state.scancode_to_keycode(KEY_A); 610 | // ↓ keymap lookup 611 | // X11 keysym (0x61 = 'a') 612 | let x11_keysym = xkb_state.key_get_one_sym(x11_key_code); 613 | 614 | assert_eq!(Keysym::a, x11_keysym); 615 | } 616 | 617 | #[test] 618 | fn keyboard_input() { 619 | let mut harness = TestState::new(); 620 | let state = harness.state(); 621 | let kb = state.seat.get_keyboard().unwrap(); 622 | 623 | const KEY_A: u32 = 30; 624 | let test_key_code = state.scancode_to_keycode(KEY_A); 625 | state.keyboard_input(0, test_key_code, KeyState::Pressed); 626 | assert!(kb.pressed_keys().contains(&test_key_code)); 627 | 628 | state.keyboard_input(0, test_key_code, KeyState::Released); 629 | assert!(kb.pressed_keys().is_empty()); 630 | } 631 | 632 | #[test] 633 | fn pointer_motion_moves_pointer_location() { 634 | let mut harness = TestState::new(); 635 | let state = harness.state(); 636 | 637 | state.pointer_location = Point::from((50.0, 50.0)); 638 | let delta = Point::from((15.5, -5.0)); 639 | let expected_location = Point::from((65.5, 45.0)); 640 | 641 | // Call the method to test 642 | state.pointer_motion(0, 0, delta, delta); 643 | 644 | // Check that the internal pointer location is updated 645 | assert_eq!(state.pointer_location, expected_location); 646 | 647 | // Check that the pointer's location is also updated 648 | let pointer = state.seat.get_pointer().unwrap(); 649 | assert_eq!(pointer.current_location(), expected_location); 650 | } 651 | 652 | #[test] 653 | fn pointer_motion_absolute_moves_pointer_location() { 654 | let mut harness = TestState::new(); 655 | let state = harness.state(); 656 | 657 | state.set_pointer_location(Point::from((50.0, 50.0))); 658 | let expected_location = Point::from((65.5, 45.0)); 659 | 660 | state.pointer_motion_absolute(0, expected_location); 661 | 662 | assert_eq!(state.pointer_location, expected_location); 663 | let pointer = state.seat.get_pointer().unwrap(); 664 | assert_eq!(pointer.current_location(), expected_location); 665 | } 666 | 667 | #[test] 668 | fn clamp_coords_keeps_within_bounds() { 669 | let mut harness = TestState::new(); 670 | let state = harness.state(); 671 | let output = Output::new( 672 | "HEADLESS-1".into(), 673 | PhysicalProperties { 674 | make: "Virtual".into(), 675 | model: "Wolf".into(), 676 | size: (0, 0).into(), 677 | subpixel: Subpixel::Unknown, 678 | }, 679 | ); 680 | output.create_global::(&state.dh); 681 | output.change_current_state( 682 | Some(smithay::output::Mode { 683 | size: (10, 10).into(), 684 | refresh: 1000, 685 | }), 686 | None, 687 | None, 688 | None, 689 | ); 690 | state.output = Some(output); 691 | 692 | let extreme_pos = Point::from((-100.0, 5000.0)); 693 | let clamped = state.clamp_coords(extreme_pos); 694 | 695 | // Should clamp negative x to 0 and large y to 10 696 | assert!(clamped.x >= 0.0); 697 | assert!(clamped.y <= 10.0); 698 | } 699 | } 700 | --------------------------------------------------------------------------------