diff --git a/src/devices/src/virtio/gpu/virtio_gpu.rs b/src/devices/src/virtio/gpu/virtio_gpu.rs index a224522f..c1af2398 100644 --- a/src/devices/src/virtio/gpu/virtio_gpu.rs +++ b/src/devices/src/virtio/gpu/virtio_gpu.rs @@ -20,7 +20,7 @@ use rutabaga_gfx::RUTABAGA_MEM_HANDLE_TYPE_SHM; use rutabaga_gfx::{ ResourceCreate3D, ResourceCreateBlob, Rutabaga, RutabagaBuilder, RutabagaChannel, RutabagaFence, RutabagaFenceHandler, RutabagaIovec, Transfer3D, RUTABAGA_CHANNEL_TYPE_WAYLAND, - RUTABAGA_MAP_CACHE_MASK, + RUTABAGA_CHANNEL_TYPE_X11, RUTABAGA_MAP_CACHE_MASK, }; #[cfg(target_os = "linux")] use rutabaga_gfx::{ @@ -192,12 +192,22 @@ impl VirtioGpu { Ok(display) => display, Err(_) => "wayland-0".to_string(), }; - let path = PathBuf::from(format!("{}/{}", xdg_runtime_dir, wayland_display)); - - let rutabaga_channels: Vec = vec![RutabagaChannel { - base_channel: path, - channel_type: RUTABAGA_CHANNEL_TYPE_WAYLAND, - }]; + let wayland_path = PathBuf::from(format!("{}/{}", xdg_runtime_dir, wayland_display)); + let x_display = match env::var("DISPLAY") { + Ok(display) => display, + Err(_) => ":0".to_string(), + }; + let x_path = PathBuf::from(format!("/tmp/.X11-unix/X{}", &x_display[1..])); + let rutabaga_channels: Vec = vec![ + RutabagaChannel { + base_channel: wayland_path, + channel_type: RUTABAGA_CHANNEL_TYPE_WAYLAND, + }, + RutabagaChannel { + base_channel: x_path, + channel_type: RUTABAGA_CHANNEL_TYPE_X11, + }, + ]; let rutabaga_channels_opt = Some(rutabaga_channels); let builder = RutabagaBuilder::new( diff --git a/src/rutabaga_gfx/ffi/src/include/rutabaga_gfx_ffi.h b/src/rutabaga_gfx/ffi/src/include/rutabaga_gfx_ffi.h index 27992a3f..caaa59c4 100644 --- a/src/rutabaga_gfx/ffi/src/include/rutabaga_gfx_ffi.h +++ b/src/rutabaga_gfx/ffi/src/include/rutabaga_gfx_ffi.h @@ -87,6 +87,7 @@ extern "C" { * Rutabaga channel types */ #define RUTABAGA_CHANNEL_TYPE_WAYLAND 1 +#define RUTABAGA_CHANNEL_TYPE_X11 0x11 /** * Rutabaga WSI diff --git a/src/rutabaga_gfx/ffi/src/tests/virtgpu_cross_domain_protocol.h b/src/rutabaga_gfx/ffi/src/tests/virtgpu_cross_domain_protocol.h index ebcd009a..e1bf410a 100644 --- a/src/rutabaga_gfx/ffi/src/tests/virtgpu_cross_domain_protocol.h +++ b/src/rutabaga_gfx/ffi/src/tests/virtgpu_cross_domain_protocol.h @@ -19,6 +19,7 @@ // Channel types (must match rutabaga channel types) #define CROSS_DOMAIN_CHANNEL_TYPE_WAYLAND 0x0001 #define CROSS_DOMAIN_CHANNEL_TYPE_CAMERA 0x0002 +#define CROSS_DOMAIN_CHANNEL_TYPE_X11 0x0011 // The maximum number of identifiers (value based on wp_linux_dmabuf) #define CROSS_DOMAIN_MAX_IDENTIFIERS 4 diff --git a/src/rutabaga_gfx/src/cross_domain/cross_domain_protocol.rs b/src/rutabaga_gfx/src/cross_domain/cross_domain_protocol.rs index 59139107..f96c9222 100644 --- a/src/rutabaga_gfx/src/cross_domain/cross_domain_protocol.rs +++ b/src/rutabaga_gfx/src/cross_domain/cross_domain_protocol.rs @@ -18,10 +18,14 @@ pub const CROSS_DOMAIN_CMD_SEND: u8 = 4; pub const CROSS_DOMAIN_CMD_RECEIVE: u8 = 5; pub const CROSS_DOMAIN_CMD_READ: u8 = 6; pub const CROSS_DOMAIN_CMD_WRITE: u8 = 7; +pub const CROSS_DOMAIN_CMD_FUTEX_NEW: u8 = 8; +pub const CROSS_DOMAIN_CMD_FUTEX_SIGNAL: u8 = 9; +pub const CROSS_DOMAIN_CMD_FUTEX_DESTROY: u8 = 10; /// Channel types (must match rutabaga channel types) pub const CROSS_DOMAIN_CHANNEL_TYPE_WAYLAND: u32 = 0x0001; pub const CROSS_DOMAIN_CHANNEL_TYPE_CAMERA: u32 = 0x0002; +pub const CROSS_DOMAIN_CHANNEL_TYPE_X11: u32 = 0x0011; /// The maximum number of identifiers (value inspired by wp_linux_dmabuf) pub const CROSS_DOMAIN_MAX_IDENTIFIERS: usize = 4; @@ -37,6 +41,8 @@ pub const CROSS_DOMAIN_ID_TYPE_READ_PIPE: u32 = 3; /// The host receives the write end of the pipe over the host Wayland socket. pub const CROSS_DOMAIN_ID_TYPE_WRITE_PIPE: u32 = 4; +pub const CROSS_DOMAIN_ID_TYPE_SHM: u32 = 5; + /// No ring pub const CROSS_DOMAIN_RING_NONE: u32 = 0xffffffff; /// A ring for metadata queries. @@ -119,3 +125,29 @@ pub struct CrossDomainReadWrite { pub pad: u32, // Data of size "opaque data size follows" } + +#[repr(C)] +#[derive(Copy, Clone, Default, AsBytes, FromBytes)] +pub struct CrossDomainFutexNew { + pub hdr: CrossDomainHeader, + pub fs_id: u64, + pub handle: u64, + pub id: u32, + pub pad: u32, +} + +#[repr(C)] +#[derive(Copy, Clone, Default, AsBytes, FromBytes)] +pub struct CrossDomainFutexSignal { + pub hdr: CrossDomainHeader, + pub id: u32, + pub pad: u32, +} + +#[repr(C)] +#[derive(Copy, Clone, Default, AsBytes, FromBytes)] +pub struct CrossDomainFutexDestroy { + pub hdr: CrossDomainHeader, + pub id: u32, + pub pad: u32, +} diff --git a/src/rutabaga_gfx/src/cross_domain/mod.rs b/src/rutabaga_gfx/src/cross_domain/mod.rs index f9fe444d..3c000f85 100644 --- a/src/rutabaga_gfx/src/cross_domain/mod.rs +++ b/src/rutabaga_gfx/src/cross_domain/mod.rs @@ -5,18 +5,22 @@ //! The cross-domain component type, specialized for allocating and sharing resources across domain //! boundaries. +use libc::{c_int, FUTEX_WAKE_BITSET}; +use log::error; +use nix::sys::mman::{mmap, munmap, MapFlags, ProtFlags}; use std::cmp::max; use std::collections::BTreeMap as Map; use std::collections::VecDeque; use std::convert::TryInto; +use std::ffi::c_void; use std::fs::File; use std::mem::size_of; +use std::os::fd::AsRawFd; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::Arc; use std::sync::Condvar; use std::sync::Mutex; -use std::thread; - -use log::error; +use std::{ptr, thread}; use zerocopy::AsBytes; use zerocopy::FromBytes; @@ -61,6 +65,7 @@ pub enum CrossDomainToken { WaylandReadPipe(u32), Resample, Kill, + Futex(u32), } const CROSS_DOMAIN_DEFAULT_BUFFER_SIZE: usize = 4096; @@ -80,6 +85,7 @@ pub(crate) enum CrossDomainJob { #[allow(dead_code)] // `AddReadPipe` is never constructed on Windows. AddReadPipe(u32), Finish, + AddFutex(u32, Arc), } enum RingWrite<'a, T> { @@ -90,6 +96,7 @@ enum RingWrite<'a, T> { pub(crate) type CrossDomainResources = Arc>>; type CrossDomainJobs = Mutex>>; pub(crate) type CrossDomainItemState = Arc>; +pub(crate) type CrossDomainFutexes = Arc>>; pub(crate) struct CrossDomainResource { #[allow(dead_code)] // `handle` is never used on Windows. @@ -106,6 +113,7 @@ pub(crate) struct CrossDomainItems { pub(crate) struct CrossDomainState { context_resources: CrossDomainResources, + futexes: CrossDomainFutexes, query_ring_id: u32, channel_ring_id: u32, #[allow(dead_code)] // `connection` is never used on Windows. @@ -121,6 +129,100 @@ struct CrossDomainWorker { fence_handler: RutabagaFenceHandler, } +struct FutexPtr(*mut c_void); +unsafe impl Send for FutexPtr {} +pub(crate) struct CrossDomainFutex { + address: FutexPtr, + handle: SafeDescriptor, + watcher_thread: Option>, + shutdown: Arc, + evt: Arc, +} + +impl CrossDomainFutex { + fn watcher_thread( + address: FutexPtr, + shutdown: Arc, + sender: Sender, + initial_value: u32, + ) { + let op = libc::FUTEX_WAIT_BITSET; + let timeout = ptr::null::<()>(); + let uaddr2 = ptr::null::<()>(); + let val3 = 1u32; + let address = address.0; + let atomic_val = unsafe { AtomicU32::from_ptr(address as *mut u32) }; + // The goal of this code is to ensure that the other side observes at least + // the latest wake event along with the value that the futex had when that + // wake event was signaled. + + // The initial value is passed in from the futex creation, and therefore + // was loaded synchronously with the cross domain operation that created + // the futex, so it cannot have an associated wake event yet. + let mut val = initial_value; + let _ = channel_signal(&sender); + loop { + // This returns when the futex is woken up OR if the value has changed. + unsafe { + libc::syscall(libc::SYS_futex, address, op, val, timeout, uaddr2, val3); + } + // Load the new value, which the other side is guaranteed to observe. + val = atomic_val.load(Ordering::SeqCst); + // If this wake was triggered by the shutdown code below, just bail. + // If the shutdown command is issued after this point, then it will + // change the futex value, which will disagree with the one we read + // above, so we will still not block in SYS_futex. + if shutdown.load(Ordering::SeqCst) { + // Signal the futex to process the shutdown and remove it from + // the waiter table + let _ = channel_signal(&sender); + break; + } + // Signal the other side after the load. If another change occurs and + // another wake is signaled here, we will miss the wake, but the + // disagreeing value will cause SYS_futex to return early. + if channel_signal(&sender).is_err() { + break; + } + } + } + + fn shutdown(&mut self) { + self.shutdown.store(true, Ordering::SeqCst); + let atomic_val = unsafe { AtomicU32::from_ptr(self.address.0 as *mut u32) }; + let v = atomic_val.load(Ordering::SeqCst); + atomic_val.store(!v, Ordering::SeqCst); + unsafe { + libc::syscall( + libc::SYS_futex, + self.address.0, + libc::FUTEX_WAKE, + i32::MAX, + ptr::null::<()>(), + ptr::null::<()>(), + 0, + ); + } + self.watcher_thread.take().unwrap().join().unwrap(); + } + + fn is_shutdown(&self) -> bool { + self.shutdown.load(Ordering::Relaxed) + } +} + +impl Drop for CrossDomainFutex { + fn drop(&mut self) { + if !self.is_shutdown() { + log::info!("Futex dropped without shutdown"); + self.shutdown(); + } + unsafe { + munmap(self.address.0, 4).unwrap(); + } + } +} + pub(crate) struct CrossDomainContext { #[allow(dead_code)] // `channels` is unused on Windows. pub(crate) channels: Option>, @@ -128,8 +230,8 @@ pub(crate) struct CrossDomainContext { pub(crate) state: Option>, pub(crate) context_resources: CrossDomainResources, pub(crate) item_state: CrossDomainItemState, + pub(crate) futexes: CrossDomainFutexes, fence_handler: RutabagaFenceHandler, - #[allow(dead_code)] export_table: Option, worker_thread: Option>>, pub(crate) resample_evt: Option, @@ -188,12 +290,14 @@ impl CrossDomainState { query_ring_id: u32, channel_ring_id: u32, context_resources: CrossDomainResources, + futexes: CrossDomainFutexes, connection: Option, ) -> CrossDomainState { CrossDomainState { query_ring_id, channel_ring_id, context_resources, + futexes, connection, jobs: Mutex::new(Some(VecDeque::new())), jobs_cvar: Condvar::new(), @@ -408,6 +512,38 @@ impl CrossDomainWorker { self.fence_handler.call(fence); } + CrossDomainToken::Futex(id) => { + let mut futexes = self.state.futexes.lock().unwrap(); + let mut remove = false; + let mut fence = Some(fence); + + if let Some(ftx) = futexes.get(&id) { + if ftx.is_shutdown() { + self.wait_ctx + .delete(CrossDomainToken::Futex(id), &ftx.evt)?; + remove = true; + } else { + channel_wait(&ftx.evt)?; + + let mut cmd_ftx: CrossDomainFutexSignal = Default::default(); + cmd_ftx.hdr.cmd = CROSS_DOMAIN_CMD_FUTEX_SIGNAL; + cmd_ftx.id = id; + self.state.write_to_ring( + RingWrite::Write(cmd_ftx, None), + self.state.channel_ring_id, + )?; + self.fence_handler.call(fence.take().unwrap()); + } + }; + + if let Some(fence) = fence { + self.state.add_job(CrossDomainJob::HandleFence(fence)); + } + + if remove { + futexes.remove(&id); + } + } CrossDomainToken::Kill => { self.fence_handler.call(fence); } @@ -453,6 +589,9 @@ impl CrossDomainWorker { _ => return Err(RutabagaError::InvalidCrossDomainItemType), } } + CrossDomainJob::AddFutex(id, recv) => { + self.wait_ctx.add(CrossDomainToken::Futex(id), recv)?; + } CrossDomainJob::Finish => return Ok(()), } } @@ -493,6 +632,7 @@ impl CrossDomainContext { let query_ring_id = cmd_init.query_ring_id; let channel_ring_id = cmd_init.channel_ring_id; let context_resources = self.context_resources.clone(); + let futexes = self.futexes.clone(); // Zero means no requested channel. if cmd_init.channel_type != 0 { @@ -522,6 +662,7 @@ impl CrossDomainContext { query_ring_id, channel_ring_id, context_resources, + futexes, connection, )); @@ -550,6 +691,7 @@ impl CrossDomainContext { query_ring_id, channel_ring_id, context_resources, + futexes, None, ))); } @@ -601,6 +743,102 @@ impl CrossDomainContext { } } + fn futex_signal(&mut self, cmd_futex_signal: &CrossDomainFutexSignal) -> RutabagaResult<()> { + let futexes = self.futexes.lock().unwrap(); + if let Some(ftx) = futexes.get(&cmd_futex_signal.id) { + let uaddr = ftx.address.0; + let op = FUTEX_WAKE_BITSET; + let val = c_int::MAX; + let timeout = ptr::null::<()>(); + let uaddr2 = ptr::null::<()>(); + let val3 = !1u32; + unsafe { + libc::syscall(libc::SYS_futex, uaddr, op, val, timeout, uaddr2, val3); + } + Ok(()) + } else { + Err(RutabagaError::InvalidCrossDomainItemId) + } + } + + fn futex_destroy(&mut self, cmd_futex_destroy: &CrossDomainFutexDestroy) -> RutabagaResult<()> { + let mut futexes = self.futexes.lock().unwrap(); + futexes + .get_mut(&cmd_futex_destroy.id) + .ok_or(RutabagaError::InvalidCrossDomainItemId)? + .shutdown(); + Ok(()) + } + + fn futex_new(&mut self, cmd_futex_new: &CrossDomainFutexNew) -> RutabagaResult<()> { + let exports = self + .export_table + .as_ref() + .ok_or(RutabagaError::InvalidCrossDomainItemId)? + .lock() + .unwrap(); + + let mut futexes = self.futexes.lock().unwrap(); + if futexes.contains_key(&cmd_futex_new.id) { + return Err(RutabagaError::AlreadyInUse); + } + + let file = exports + .get(&(cmd_futex_new.fs_id, cmd_futex_new.handle)) + .ok_or(RutabagaError::InvalidCrossDomainItemId)? + .try_clone()?; + + let handle = SafeDescriptor::from(file); + let address = unsafe { + mmap( + None, + 4.try_into().unwrap(), + ProtFlags::PROT_WRITE | ProtFlags::PROT_READ, + MapFlags::MAP_SHARED, + handle.as_raw_fd(), + 0, + )? + }; + + let shutdown = Arc::new(AtomicBool::new(false)); + let (ftx_evt, thread_ftx_evt) = channel()?; + + let thread_ftx_evt = Arc::new(thread_ftx_evt); + + let id = cmd_futex_new.id; + + let state = self + .state + .as_ref() + .ok_or(RutabagaError::InvalidCrossDomainState)?; + state.add_job(CrossDomainJob::AddFutex(id, thread_ftx_evt.clone())); + + let shutdown2 = shutdown.clone(); + let fptr = FutexPtr(address); + let initial_value = + unsafe { AtomicU32::from_ptr(address as *mut u32) }.load(Ordering::SeqCst); + let watcher_thread = Some( + thread::Builder::new() + .name(format!("futexw {}", cmd_futex_new.id)) + .spawn(move || { + CrossDomainFutex::watcher_thread(fptr, shutdown2, ftx_evt, initial_value); + })?, + ); + + futexes.insert( + id, + CrossDomainFutex { + handle, + address: FutexPtr(address), + watcher_thread, + shutdown, + evt: thread_ftx_evt, + }, + ); + + Ok(()) + } + fn write(&self, cmd_write: &CrossDomainReadWrite, opaque_data: &[u8]) -> RutabagaResult<()> { let mut items = self.item_state.lock().unwrap(); @@ -838,6 +1076,23 @@ impl RutabagaContext for CrossDomainContext { self.write(&cmd_write, opaque_data)?; } + CROSS_DOMAIN_CMD_FUTEX_NEW => { + let cmd_new_futex = CrossDomainFutexNew::read_from_prefix(commands.as_bytes()) + .ok_or(RutabagaError::InvalidCommandBuffer)?; + self.futex_new(&cmd_new_futex)?; + } + CROSS_DOMAIN_CMD_FUTEX_SIGNAL => { + let cmd_futex_signal = + CrossDomainFutexSignal::read_from_prefix(commands.as_bytes()) + .ok_or(RutabagaError::InvalidCommandBuffer)?; + self.futex_signal(&cmd_futex_signal)?; + } + CROSS_DOMAIN_CMD_FUTEX_DESTROY => { + let cmd_futex_destroy = + CrossDomainFutexDestroy::read_from_prefix(commands.as_bytes()) + .ok_or(RutabagaError::InvalidCommandBuffer)?; + self.futex_destroy(&cmd_futex_destroy)?; + } _ => return Err(RutabagaError::SpecViolation("invalid cross domain command")), } @@ -904,7 +1159,7 @@ impl RutabagaComponent for CrossDomain { let mut caps: CrossDomainCapabilities = Default::default(); if let Some(ref channels) = self.channels { for channel in channels { - caps.supported_channels = 1 << channel.channel_type; + caps.supported_channels |= 1 << channel.channel_type; } } @@ -974,6 +1229,7 @@ impl RutabagaComponent for CrossDomain { resample_evt: None, kill_evt: None, export_table: self.export_table.clone(), + futexes: Arc::new(Mutex::new(Default::default())), })) } diff --git a/src/rutabaga_gfx/src/cross_domain/sys/unix.rs b/src/rutabaga_gfx/src/cross_domain/sys/unix.rs index 9ba9e8c7..5f5a7dcc 100644 --- a/src/rutabaga_gfx/src/cross_domain/sys/unix.rs +++ b/src/rutabaga_gfx/src/cross_domain/sys/unix.rs @@ -47,7 +47,7 @@ use super::super::CrossDomainJob; use super::super::CrossDomainState; use super::epoll_internal::Epoll; use super::epoll_internal::EpollEvent; -use crate::cross_domain::cross_domain_protocol::CrossDomainInit; +use crate::cross_domain::cross_domain_protocol::{CrossDomainInit, CROSS_DOMAIN_ID_TYPE_SHM}; use crate::cross_domain::CrossDomainEvent; use crate::cross_domain::CrossDomainToken; use crate::cross_domain::WAIT_CONTEXT_MAX; @@ -232,6 +232,12 @@ impl CrossDomainContext { // can receive subsequent hang-up events. write_pipe_opt = Some(write_pipe); read_pipe_id_opt = Some(read_pipe_id); + } else if *identifier_type == CROSS_DOMAIN_ID_TYPE_SHM { + if let Some(ftx) = self.futexes.lock().unwrap().get(identifier) { + *descriptor = ftx.handle.as_raw_descriptor(); + } else { + return Err(RutabagaError::InvalidCrossDomainItemId); + } } else { // Don't know how to handle anything else yet. return Err(RutabagaError::InvalidCrossDomainItemType); diff --git a/src/rutabaga_gfx/src/rutabaga_utils.rs b/src/rutabaga_gfx/src/rutabaga_utils.rs index 6d7da269..9ab9621d 100644 --- a/src/rutabaga_gfx/src/rutabaga_utils.rs +++ b/src/rutabaga_gfx/src/rutabaga_utils.rs @@ -599,6 +599,7 @@ impl Transfer3D { /// Rutabaga channel types pub const RUTABAGA_CHANNEL_TYPE_WAYLAND: u32 = 0x0001; pub const RUTABAGA_CHANNEL_TYPE_CAMERA: u32 = 0x0002; +pub const RUTABAGA_CHANNEL_TYPE_X11: u32 = 0x0011; /// Information needed to open an OS-specific RutabagaConnection (TBD). Only Linux hosts are /// considered at the moment.