diff --git a/rutabaga_gfx/src/cross_domain/cross_domain.rs b/rutabaga_gfx/src/cross_domain/cross_domain.rs index 4426f51037..37cfea474f 100644 --- a/rutabaga_gfx/src/cross_domain/cross_domain.rs +++ b/rutabaga_gfx/src/cross_domain/cross_domain.rs @@ -6,9 +6,20 @@ //! boundaries. use std::collections::BTreeMap as Map; +use std::collections::VecDeque; +use std::convert::TryInto; +use std::fs::File; +use std::io::{Seek, SeekFrom}; use std::mem::size_of; use std::os::unix::net::UnixStream; +use std::ptr::copy_nonoverlapping; use std::sync::Arc; +use std::thread; + +use base::{ + error, pipe, AsRawDescriptor, Event, FileFlags, FileReadWriteVolatile, FromRawDescriptor, + PollToken, SafeDescriptor, ScmSocket, WaitContext, +}; use crate::cross_domain::cross_domain_protocol::*; use crate::rutabaga_core::{RutabagaComponent, RutabagaContext, RutabagaResource}; @@ -19,13 +30,79 @@ use crate::{ use data_model::{DataInit, VolatileMemory, VolatileSlice}; -use sync::Mutex; +use sync::{Condvar, Mutex}; + +const CROSS_DOMAIN_DEFAULT_BUFFER_SIZE: usize = 4096; +const CROSS_DOMAIN_MAX_SEND_RECV_SIZE: usize = + CROSS_DOMAIN_DEFAULT_BUFFER_SIZE - size_of::(); + +#[derive(PollToken)] +enum CrossDomainToken { + ContextChannel, + WaylandReadPipe(u32), + Resample, + Kill, +} + +enum CrossDomainItem { + ImageRequirements(ImageMemoryRequirements), + WaylandKeymap(SafeDescriptor), + WaylandReadPipe(File), + WaylandWritePipe(File), +} + +enum CrossDomainJob { + HandleFence(RutabagaFenceData), + AddReadPipe(u32), +} + +enum RingWrite<'a, T> { + Write(T, Option<&'a [u8]>), + WriteFromFile(CrossDomainReadWrite, &'a mut File, bool), +} + +type CrossDomainResources = Arc>>; +type CrossDomainJobs = Mutex>>; +type CrossDomainItemState = Arc>; struct CrossDomainResource { pub handle: Option>, pub backing_iovecs: Option>, } +struct CrossDomainItems { + descriptor_id: u32, + requirements_blob_id: u32, + table: Map, +} + +struct CrossDomainState { + context_resources: CrossDomainResources, + ring_id: u32, + connection: Option, + jobs: CrossDomainJobs, + jobs_cvar: Condvar, +} + +struct CrossDomainWorker { + wait_ctx: WaitContext, + state: Arc, + item_state: CrossDomainItemState, + fence_handler: RutabagaFenceHandler, +} + +struct CrossDomainContext { + channels: Option>, + gralloc: Arc>, + state: Option>, + context_resources: CrossDomainResources, + item_state: CrossDomainItemState, + fence_handler: RutabagaFenceHandler, + worker_thread: Option>>, + resample_evt: Option, + kill_evt: Option, +} + /// The CrossDomain component contains a list of channels that the guest may connect to and the /// ability to allocate memory. pub struct CrossDomain { @@ -33,16 +110,369 @@ pub struct CrossDomain { gralloc: Arc>, } -struct CrossDomainContext { - channels: Option>, - gralloc: Arc>, - connection: Option, - ring_id: u32, - requirements_blobs: Map, - context_resources: Map, - last_fence_data: Option, - fence_handler: RutabagaFenceHandler, - blob_id: u64, +// TODO(gurchetansingh): optimize the item tracker. Each requirements blob is long-lived and can +// be stored in a Slab or vector. Descriptors received from the Wayland socket *seem* to come one +// at a time, and can be stored as options. Need to confirm. +fn add_item(item_state: &CrossDomainItemState, item: CrossDomainItem) -> u32 { + let mut items = item_state.lock(); + + let item_id = match item { + CrossDomainItem::ImageRequirements(_) => { + items.requirements_blob_id += 2; + items.requirements_blob_id + } + _ => { + items.descriptor_id += 2; + items.descriptor_id + } + }; + + items.table.insert(item_id, item); + + item_id +} + +// Determine type of OS-specific descriptor. See `from_file` in wl.rs for explantation on the +// current, Linux-based method. +fn descriptor_analysis( + descriptor: &mut File, + descriptor_type: &mut u32, + size: &mut u32, +) -> RutabagaResult<()> { + match descriptor.seek(SeekFrom::End(0)) { + Ok(seek_size) => { + *descriptor_type = CROSS_DOMAIN_ID_TYPE_VIRTGPU_BLOB; + *size = seek_size.try_into()?; + Ok(()) + } + _ => { + // Expect to receive the write end of a Wayland pipe only. + *descriptor_type = match FileFlags::from_file(descriptor) { + Ok(FileFlags::Write) => CROSS_DOMAIN_ID_TYPE_WRITE_PIPE, + _ => return Err(RutabagaError::SpecViolation), + }; + Ok(()) + } + } +} + +impl Default for CrossDomainItems { + fn default() -> Self { + // Odd for descriptors, and even for requirement blobs. + CrossDomainItems { + descriptor_id: 1, + requirements_blob_id: 2, + table: Default::default(), + } + } +} + +impl CrossDomainState { + fn new( + ring_id: u32, + context_resources: CrossDomainResources, + connection: Option, + ) -> CrossDomainState { + CrossDomainState { + ring_id, + context_resources, + connection, + jobs: Mutex::new(Some(VecDeque::new())), + jobs_cvar: Condvar::new(), + } + } + + fn add_job(&self, job: CrossDomainJob) { + let mut jobs = self.jobs.lock(); + if let Some(queue) = jobs.as_mut() { + queue.push_back(job); + self.jobs_cvar.notify_one(); + } + } + + fn end_jobs(&self) { + let mut jobs = self.jobs.lock(); + *jobs = None; + // Only one worker thread in the current implementation. + self.jobs_cvar.notify_one(); + } + + fn wait_for_job(&self) -> Option { + let mut jobs = self.jobs.lock(); + loop { + match jobs.as_mut()?.pop_front() { + Some(job) => return Some(job), + None => jobs = self.jobs_cvar.wait(jobs), + } + } + } + + fn write_to_ring(&self, mut ring_write: RingWrite) -> RutabagaResult + where + T: DataInit, + { + let mut context_resources = self.context_resources.lock(); + let mut bytes_read: usize = 0; + + let resource = context_resources + .get_mut(&self.ring_id) + .ok_or(RutabagaError::InvalidResourceId)?; + + let iovecs = resource + .backing_iovecs + .as_mut() + .ok_or(RutabagaError::InvalidIovec)?; + + // Safe because we've verified the iovecs are attached and owned only by this context. + let slice = + unsafe { VolatileSlice::from_raw_parts(iovecs[0].base as *mut u8, iovecs[0].len) }; + + match ring_write { + RingWrite::Write(cmd, opaque_data_opt) => { + slice.copy_from(&[cmd]); + if let Some(opaque_data) = opaque_data_opt { + let offset = size_of::(); + let sub_slice = slice.sub_slice(offset, opaque_data.len())?; + let dst_ptr = sub_slice.as_mut_ptr(); + let src_ptr = opaque_data.as_ptr(); + + // Safe because: + // + // (1) The volatile slice has atleast `opaque_data.len()' bytes. + // (2) The both the destination and source are non-overlapping. + unsafe { + copy_nonoverlapping(src_ptr, dst_ptr, opaque_data.len()); + } + } + } + RingWrite::WriteFromFile(mut cmd_read, ref mut file, readable) => { + let offset = size_of::(); + let sub_slice = slice.offset(offset)?; + + if readable { + bytes_read = file.read_volatile(sub_slice)?; + } + + if bytes_read == 0 { + cmd_read.hang_up = 1; + } + + cmd_read.opaque_data_size = bytes_read.try_into()?; + slice.copy_from(&[cmd_read]); + } + } + + Ok(bytes_read) + } + + fn send_msg( + &self, + opaque_data: &[VolatileSlice], + descriptors: &[i32], + ) -> RutabagaResult { + self.connection + .as_ref() + .ok_or(RutabagaError::SpecViolation) + .and_then(|conn| Ok(conn.send_with_fds(opaque_data, descriptors)?)) + } + + fn receive_msg( + &self, + opaque_data: &mut Vec, + descriptors: &mut [i32; CROSS_DOMAIN_MAX_IDENTIFIERS], + ) -> RutabagaResult<(usize, Vec)> { + // If any errors happen, the socket will get dropped, preventing more reading. + if let Some(connection) = &self.connection { + let mut files: Vec = Vec::new(); + let (len, file_count) = connection.recv_with_fds(&mut opaque_data[..], descriptors)?; + + for descriptor in descriptors.iter_mut().take(file_count) { + // Safe since the descriptors from recv_with_fds(..) are owned by us and valid. + let file = unsafe { File::from_raw_descriptor(*descriptor) }; + files.push(file); + } + + Ok((len, files)) + } else { + Err(RutabagaError::SpecViolation) + } + } +} + +impl CrossDomainWorker { + fn new( + wait_ctx: WaitContext, + state: Arc, + item_state: CrossDomainItemState, + fence_handler: RutabagaFenceHandler, + ) -> CrossDomainWorker { + CrossDomainWorker { + wait_ctx, + state, + item_state, + fence_handler, + } + } + + // Handles the fence according the the token according to the event token. On success, a + // boolean value indicating whether the worker thread should be stopped is returned. + fn handle_fence( + &mut self, + fence: RutabagaFenceData, + resample_evt: &Event, + receive_buf: &mut Vec, + ) -> RutabagaResult { + let events = self.wait_ctx.wait()?; + let mut stop_thread = false; + + for event in &events { + match event.token { + CrossDomainToken::ContextChannel => { + let mut descriptors = [0; CROSS_DOMAIN_MAX_IDENTIFIERS]; + + let (len, files) = self.state.receive_msg(receive_buf, &mut descriptors)?; + if len != 0 || files.len() != 0 { + let mut cmd_receive: CrossDomainSendReceive = Default::default(); + + let num_files = files.len(); + cmd_receive.hdr.cmd = CROSS_DOMAIN_CMD_RECEIVE; + cmd_receive.num_identifiers = files.len().try_into()?; + cmd_receive.opaque_data_size = len.try_into()?; + + let iter = cmd_receive + .identifiers + .iter_mut() + .zip(cmd_receive.identifier_types.iter_mut()) + .zip(cmd_receive.identifier_sizes.iter_mut()) + .zip(files.into_iter()) + .take(num_files); + + for (((identifier, mut identifier_type), mut identifier_size), mut file) in + iter + { + // Safe since the descriptors from receive_msg(..) are owned by us and valid. + descriptor_analysis( + &mut file, + &mut identifier_type, + &mut identifier_size, + )?; + + *identifier = match *identifier_type { + CROSS_DOMAIN_ID_TYPE_VIRTGPU_BLOB => add_item( + &self.item_state, + CrossDomainItem::WaylandKeymap(file.into()), + ), + CROSS_DOMAIN_ID_TYPE_WRITE_PIPE => add_item( + &self.item_state, + CrossDomainItem::WaylandWritePipe(file), + ), + _ => return Err(RutabagaError::SpecViolation), + }; + } + + self.state.write_to_ring(RingWrite::Write( + cmd_receive, + Some(&receive_buf[0..len]), + ))?; + self.fence_handler.call(fence); + } + } + CrossDomainToken::Resample => { + // The resample event is triggered when the job queue is in the following state: + // + // [CrossDomain::AddReadPipe(..)] -> END + // + // After this event, the job queue will be the following state: + // + // [CrossDomain::AddReadPipe(..)] -> [CrossDomain::HandleFence(..)] -> END + // + // Fence handling is tied to some new data transfer across a pollable + // descriptor. When we're adding new descriptors, we stop polling. + resample_evt.read()?; + self.state.add_job(CrossDomainJob::HandleFence(fence)); + } + CrossDomainToken::WaylandReadPipe(pipe_id) => { + let mut items = self.item_state.lock(); + let mut cmd_read: CrossDomainReadWrite = Default::default(); + let bytes_read; + + cmd_read.hdr.cmd = CROSS_DOMAIN_CMD_READ; + cmd_read.identifier = pipe_id; + + let item = items + .table + .get_mut(&pipe_id) + .ok_or(RutabagaError::SpecViolation)?; + + match item { + CrossDomainItem::WaylandReadPipe(ref mut file) => { + let ring_write = + RingWrite::WriteFromFile(cmd_read, file, event.is_readable); + bytes_read = self + .state + .write_to_ring::(ring_write)?; + + // Zero bytes read indicates end-of-file on POSIX. + if event.is_hungup && bytes_read == 0 { + self.wait_ctx.delete(file)?; + } + } + _ => return Err(RutabagaError::SpecViolation), + } + + if event.is_hungup && bytes_read == 0 { + items.table.remove(&pipe_id); + } + + self.fence_handler.call(fence); + } + CrossDomainToken::Kill => { + self.fence_handler.call(fence); + stop_thread = true; + } + } + } + + Ok(stop_thread) + } + + fn run(&mut self, kill_evt: Event, resample_evt: Event) -> RutabagaResult<()> { + self.wait_ctx + .add(&resample_evt, CrossDomainToken::Resample)?; + self.wait_ctx.add(&kill_evt, CrossDomainToken::Kill)?; + let mut receive_buf: Vec = vec![0; CROSS_DOMAIN_MAX_SEND_RECV_SIZE]; + + while let Some(job) = self.state.wait_for_job() { + match job { + CrossDomainJob::HandleFence(fence) => { + match self.handle_fence(fence, &resample_evt, &mut receive_buf) { + Ok(true) => return Ok(()), + Ok(false) => (), + Err(e) => { + error!("Worker halting due to: {}", e); + return Err(e); + } + } + } + CrossDomainJob::AddReadPipe(read_pipe_id) => { + let items = self.item_state.lock(); + let item = items + .table + .get(&read_pipe_id) + .ok_or(RutabagaError::SpecViolation)?; + + match item { + CrossDomainItem::WaylandReadPipe(file) => self + .wait_ctx + .add(file, CrossDomainToken::WaylandReadPipe(read_pipe_id))?, + _ => return Err(RutabagaError::SpecViolation), + } + } + } + } + + Ok(()) + } } impl CrossDomain { @@ -61,11 +491,17 @@ impl CrossDomain { impl CrossDomainContext { fn initialize(&mut self, cmd_init: &CrossDomainInit) -> RutabagaResult<()> { - if !self.context_resources.contains_key(&cmd_init.ring_id) { + if !self + .context_resources + .lock() + .contains_key(&cmd_init.ring_id) + { return Err(RutabagaError::InvalidResourceId); } - self.ring_id = cmd_init.ring_id; + let ring_id = cmd_init.ring_id; + let context_resources = self.context_resources.clone(); + // Zero means no requested channel. if cmd_init.channel_type != 0 { let channels = self.channels.take().ok_or(RutabagaError::SpecViolation)?; @@ -75,7 +511,47 @@ impl CrossDomainContext { .ok_or(RutabagaError::SpecViolation)? .base_channel; - self.connection = Some(UnixStream::connect(base_channel)?); + let connection = UnixStream::connect(base_channel)?; + + let (kill_evt, thread_kill_evt) = Event::new().and_then(|e| Ok((e.try_clone()?, e)))?; + let (resample_evt, thread_resample_evt) = + Event::new().and_then(|e| Ok((e.try_clone()?, e)))?; + + let wait_ctx = + WaitContext::build_with(&[(&connection, CrossDomainToken::ContextChannel)])?; + + let state = Arc::new(CrossDomainState::new( + ring_id, + context_resources, + Some(connection), + )); + + let thread_state = state.clone(); + let thread_items = self.item_state.clone(); + let thread_fence_handler = self.fence_handler.clone(); + + let worker_result = thread::Builder::new() + .name("cross domain".to_string()) + .spawn(move || -> RutabagaResult<()> { + CrossDomainWorker::new( + wait_ctx, + thread_state, + thread_items, + thread_fence_handler, + ) + .run(thread_kill_evt, thread_resample_evt) + }); + + self.worker_thread = Some(worker_result.unwrap()); + self.state = Some(state); + self.resample_evt = Some(resample_evt); + self.kill_evt = Some(kill_evt); + } else { + self.state = Some(Arc::new(CrossDomainState::new( + ring_id, + context_resources, + None, + ))); } Ok(()) @@ -92,7 +568,6 @@ impl CrossDomainContext { flags: RutabagaGrallocFlags::new(cmd_get_reqs.flags), }; - self.blob_id += 1; let reqs = self.gralloc.lock().get_image_memory_requirements(info)?; let mut response = CrossDomainImageRequirements { @@ -100,9 +575,8 @@ impl CrossDomainContext { offsets: reqs.offsets, modifier: reqs.modifier, size: reqs.size, - blob_id: self.blob_id, + blob_id: 0, map_info: reqs.map_info, - pad: 0, memory_idx: -1, physical_device_idx: -1, }; @@ -112,26 +586,156 @@ impl CrossDomainContext { response.physical_device_idx = vk_info.physical_device_idx as i32; } - let resource = self - .context_resources - .get_mut(&self.ring_id) - .ok_or(RutabagaError::InvalidResourceId)?; + if let Some(state) = &self.state { + response.blob_id = add_item(&self.item_state, CrossDomainItem::ImageRequirements(reqs)); + state.write_to_ring(RingWrite::Write(response, None))?; + Ok(()) + } else { + Err(RutabagaError::SpecViolation) + } + } - let iovecs = resource - .backing_iovecs - .take() - .ok_or(RutabagaError::InvalidIovec)?; + fn send( + &self, + cmd_send: &CrossDomainSendReceive, + opaque_data: &[VolatileSlice], + ) -> RutabagaResult<()> { + let mut descriptors = [0; CROSS_DOMAIN_MAX_IDENTIFIERS]; - // Safe because we've verified the iovecs are attached and owned only by this context. - let slice = - unsafe { VolatileSlice::from_raw_parts(iovecs[0].base as *mut u8, iovecs[0].len) }; + let mut write_pipe_opt: Option = None; + let mut read_pipe_id_opt: Option = None; + + let num_identifiers = cmd_send.num_identifiers.try_into()?; + + if num_identifiers > CROSS_DOMAIN_MAX_IDENTIFIERS { + return Err(RutabagaError::SpecViolation); + } + + let iter = cmd_send + .identifiers + .iter() + .zip(cmd_send.identifier_types.iter()) + .zip(descriptors.iter_mut()) + .take(num_identifiers); + + for ((identifier, identifier_type), descriptor) in iter { + if *identifier_type == CROSS_DOMAIN_ID_TYPE_VIRTGPU_BLOB { + let context_resources = self.context_resources.lock(); + + let context_resource = context_resources + .get(identifier) + .ok_or(RutabagaError::InvalidResourceId)?; + + if let Some(ref handle) = context_resource.handle { + *descriptor = handle.os_handle.as_raw_descriptor(); + } else { + return Err(RutabagaError::SpecViolation); + } + } else if *identifier_type == CROSS_DOMAIN_ID_TYPE_READ_PIPE { + // In practice, just 1 write or read pipe per send is observed. If we encounter + // more, this can be changed later. + if write_pipe_opt.is_some() { + return Err(RutabagaError::SpecViolation); + } + + let (read_pipe, write_pipe) = pipe(true)?; + + *descriptor = write_pipe.as_raw_descriptor(); + let read_pipe_id: u32 = add_item( + &self.item_state, + CrossDomainItem::WaylandReadPipe(read_pipe), + ); + + // For Wayland read pipes, the guest guesses which identifier the host will use to + // avoid waiting for the host to generate one. Validate guess here. This works + // because of the way Sommelier copy + paste works. If the Sommelier sequence of events + // changes, it's always possible to wait for the host response. + if read_pipe_id != *identifier { + return Err(RutabagaError::SpecViolation); + } + + // The write pipe needs to be dropped after the send_msg(..) call is complete, so the read pipe + // can receive subsequent hang-up events. + write_pipe_opt = Some(write_pipe); + read_pipe_id_opt = Some(read_pipe_id); + } else { + // Don't know how to handle anything else yet. + return Err(RutabagaError::SpecViolation); + } + } + + if let (Some(state), Some(resample_evt)) = (&self.state, &self.resample_evt) { + state.send_msg(opaque_data, &descriptors[..num_identifiers])?; + + if let Some(read_pipe_id) = read_pipe_id_opt { + state.add_job(CrossDomainJob::AddReadPipe(read_pipe_id)); + resample_evt.write(1)?; + } + } else { + return Err(RutabagaError::SpecViolation); + } - // The copy_from(..) method guarantees out of bounds buffer accesses will not occur. - slice.copy_from(&[response]); - resource.backing_iovecs = Some(iovecs); - self.requirements_blobs.insert(self.blob_id, reqs); Ok(()) } + + fn write( + &self, + cmd_write: &CrossDomainReadWrite, + opaque_data: VolatileSlice, + ) -> RutabagaResult<()> { + let mut items = self.item_state.lock(); + + // Most of the time, hang-up and writing will be paired. In lieu of this, remove the + // item rather than getting a reference. In case of an error, there's not much to do + // besides reporting it. + let item = items + .table + .remove(&cmd_write.identifier) + .ok_or(RutabagaError::SpecViolation)?; + + let len: usize = cmd_write.opaque_data_size.try_into()?; + match item { + CrossDomainItem::WaylandWritePipe(mut file) => { + if len != 0 { + file.write_all_volatile(opaque_data)?; + } + + if cmd_write.hang_up == 0 { + items.table.insert( + cmd_write.identifier, + CrossDomainItem::WaylandWritePipe(file), + ); + } + + Ok(()) + } + _ => Err(RutabagaError::SpecViolation), + } + } +} + +impl Drop for CrossDomainContext { + fn drop(&mut self) { + if let Some(state) = &self.state { + state.end_jobs(); + } + + if let Some(kill_evt) = self.kill_evt.take() { + // Don't join the the worker thread unless the write to `kill_evt` is successful. + // Otherwise, this may block indefinitely. + match kill_evt.write(1) { + Ok(_) => (), + Err(e) => { + error!("failed to write cross domain kill event: {}", e); + return; + } + } + + if let Some(worker_thread) = self.worker_thread.take() { + let _ = worker_thread.join(); + } + } + } } impl RutabagaContext for CrossDomainContext { @@ -141,45 +745,88 @@ impl RutabagaContext for CrossDomainContext { resource_create_blob: ResourceCreateBlob, handle: Option, ) -> RutabagaResult { - let reqs = self - .requirements_blobs - .get(&resource_create_blob.blob_id) - .ok_or(RutabagaError::SpecViolation)?; + let item_id = resource_create_blob.blob_id as u32; - if reqs.size != resource_create_blob.size { - return Err(RutabagaError::SpecViolation); + // We don't want to remove requirements blobs, since they can be used for subsequent + // allocations. We do want to remove Wayland keymaps, since they are mapped the guest + // and then never used again. The current protocol encodes this as divisiblity by 2. + if item_id % 2 == 0 { + let items = self.item_state.lock(); + let item = items + .table + .get(&item_id) + .ok_or(RutabagaError::SpecViolation)?; + + match item { + CrossDomainItem::ImageRequirements(reqs) => { + if reqs.size != resource_create_blob.size { + return Err(RutabagaError::SpecViolation); + } + + // Strictly speaking, it's against the virtio-gpu spec to allocate memory in the context + // create blob function, which says "the actual allocation is done via + // VIRTIO_GPU_CMD_SUBMIT_3D." However, atomic resource creation is easiest for the + // cross-domain use case, so whatever. + let hnd = match handle { + Some(handle) => handle, + None => self.gralloc.lock().allocate_memory(*reqs)?, + }; + + let info_3d = Resource3DInfo { + width: reqs.info.width, + height: reqs.info.height, + drm_fourcc: reqs.info.drm_format.into(), + strides: reqs.strides, + offsets: reqs.offsets, + modifier: reqs.modifier, + }; + + Ok(RutabagaResource { + resource_id, + handle: Some(Arc::new(hnd)), + blob: true, + blob_mem: resource_create_blob.blob_mem, + blob_flags: resource_create_blob.blob_flags, + map_info: Some(reqs.map_info), + info_2d: None, + info_3d: Some(info_3d), + vulkan_info: reqs.vulkan_info, + backing_iovecs: None, + }) + } + _ => Err(RutabagaError::SpecViolation), + } + } else { + let item = self + .item_state + .lock() + .table + .remove(&item_id) + .ok_or(RutabagaError::SpecViolation)?; + + match item { + CrossDomainItem::WaylandKeymap(descriptor) => { + let hnd = RutabagaHandle { + os_handle: descriptor, + handle_type: RUTABAGA_MEM_HANDLE_TYPE_OPAQUE_FD, + }; + + Ok(RutabagaResource { + resource_id, + handle: Some(Arc::new(hnd)), + blob: true, + blob_mem: resource_create_blob.blob_mem, + blob_flags: resource_create_blob.blob_flags, + map_info: Some(RUTABAGA_MAP_CACHE_CACHED), + info_2d: None, + info_3d: None, + vulkan_info: None, + backing_iovecs: None, + }) + } + _ => Err(RutabagaError::SpecViolation), + } } - - // Strictly speaking, it's against the virtio-gpu spec to allocate memory in the context - // create blob function, which says "the actual allocation is done via - // VIRTIO_GPU_CMD_SUBMIT_3D." However, atomic resource creation is easiest for the - // cross-domain use case, so whatever. - let hnd = match handle { - Some(handle) => handle, - None => self.gralloc.lock().allocate_memory(*reqs)?, - }; - - let info_3d = Resource3DInfo { - width: reqs.info.width, - height: reqs.info.height, - drm_fourcc: reqs.info.drm_format.into(), - strides: reqs.strides, - offsets: reqs.offsets, - modifier: reqs.modifier, - }; - - Ok(RutabagaResource { - resource_id, - handle: Some(Arc::new(hnd)), - blob: true, - blob_mem: resource_create_blob.blob_mem, - blob_flags: resource_create_blob.blob_flags, - map_info: Some(reqs.map_info), - info_2d: None, - info_3d: Some(info_3d), - vulkan_info: reqs.vulkan_info, - backing_iovecs: None, - }) } fn submit_cmd(&mut self, commands: &mut [u8]) -> RutabagaResult<()> { @@ -202,6 +849,27 @@ impl RutabagaContext for CrossDomainContext { self.get_image_requirements(&cmd_get_reqs)?; } + CROSS_DOMAIN_CMD_SEND => { + let opaque_data_offset = size_of::(); + let cmd_send: CrossDomainSendReceive = slice.get_ref(offset)?.load(); + + let opaque_data = + slice.sub_slice(opaque_data_offset, cmd_send.opaque_data_size as usize)?; + + self.send(&cmd_send, &[opaque_data])?; + } + CROSS_DOMAIN_CMD_POLL => { + // Actual polling is done in the subsequent when creating a fence. + } + CROSS_DOMAIN_CMD_WRITE => { + let opaque_data_offset = size_of::(); + let cmd_write: CrossDomainReadWrite = slice.get_ref(offset)?.load(); + + let opaque_data = + slice.sub_slice(opaque_data_offset, cmd_write.opaque_data_size as usize)?; + + self.write(&cmd_write, opaque_data)?; + } _ => return Err(RutabagaError::Unsupported), } @@ -213,7 +881,7 @@ impl RutabagaContext for CrossDomainContext { fn attach(&mut self, resource: &mut RutabagaResource) { if resource.blob_mem == RUTABAGA_BLOB_MEM_GUEST { - self.context_resources.insert( + self.context_resources.lock().insert( resource.resource_id, CrossDomainResource { handle: None, @@ -221,7 +889,7 @@ impl RutabagaContext for CrossDomainContext { }, ); } else if let Some(ref handle) = resource.handle { - self.context_resources.insert( + self.context_resources.lock().insert( resource.resource_id, CrossDomainResource { handle: Some(handle.clone()), @@ -232,21 +900,21 @@ impl RutabagaContext for CrossDomainContext { } fn detach(&mut self, resource: &RutabagaResource) { - self.context_resources.remove(&resource.resource_id); + self.context_resources.lock().remove(&resource.resource_id); } fn context_create_fence(&mut self, fence_data: RutabagaFenceData) -> RutabagaResult<()> { - self.fence_handler.call(fence_data); - self.last_fence_data = Some(fence_data); - Ok(()) - } - - fn context_poll(&mut self) -> Option> { - let fence_data = self.last_fence_data.take(); - match fence_data { - Some(fence_data) => Some(vec![fence_data]), - None => None, + match fence_data.ring_idx { + CROSS_DOMAIN_QUERY_RING => self.fence_handler.call(fence_data), + CROSS_DOMAIN_CHANNEL_RING => { + if let Some(state) = &self.state { + state.add_job(CrossDomainJob::HandleFence(fence_data)); + } + } + _ => return Err(RutabagaError::SpecViolation), } + + Ok(()) } } @@ -271,7 +939,7 @@ impl RutabagaComponent for CrossDomain { caps.supports_external_gpu_memory = 1; } - // Version 1 supports all commands up to and including CROSS_DOMAIN_CMD_RECEIVE. + // Version 1 supports all commands up to and including CROSS_DOMAIN_CMD_WRITE. caps.version = 1; caps.as_slice().to_vec() } @@ -285,13 +953,13 @@ impl RutabagaComponent for CrossDomain { Ok(Box::new(CrossDomainContext { channels: self.channels.clone(), gralloc: self.gralloc.clone(), - connection: None, - ring_id: 0, - requirements_blobs: Default::default(), - context_resources: Default::default(), - last_fence_data: None, + state: None, + context_resources: Arc::new(Mutex::new(Default::default())), + item_state: Arc::new(Mutex::new(Default::default())), fence_handler, - blob_id: 0, + worker_thread: None, + resample_evt: None, + kill_evt: None, })) } } diff --git a/rutabaga_gfx/src/cross_domain/cross_domain_protocol.rs b/rutabaga_gfx/src/cross_domain/cross_domain_protocol.rs index aaa7a8f55e..d3f6865caf 100644 --- a/rutabaga_gfx/src/cross_domain/cross_domain_protocol.rs +++ b/rutabaga_gfx/src/cross_domain/cross_domain_protocol.rs @@ -12,11 +12,37 @@ use data_model::DataInit; /// Cross-domain commands (only a maximum of 255 supported) pub const CROSS_DOMAIN_CMD_INIT: u8 = 1; pub const CROSS_DOMAIN_CMD_GET_IMAGE_REQUIREMENTS: u8 = 2; +pub const CROSS_DOMAIN_CMD_POLL: u8 = 3; +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; /// 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; +/// The maximum number of identifiers (value inspired by wp_linux_dmabuf) +pub const CROSS_DOMAIN_MAX_IDENTIFIERS: usize = 4; + +/// virtgpu memory resource ID. Also works with non-blob memory resources, despite the name. +pub const CROSS_DOMAIN_ID_TYPE_VIRTGPU_BLOB: u32 = 1; +/// virtgpu synchronization resource id. +pub const CROSS_DOMAIN_ID_TYPE_VIRTGPU_SYNC: u32 = 2; +/// ID for Wayland pipe used for reading. The reading is done by the guest proxy and the host +/// proxy. The host sends the write end of the proxied pipe over the host Wayland socket. +pub const CROSS_DOMAIN_ID_TYPE_READ_PIPE: u32 = 3; +/// ID for Wayland pipe used for writing. The writing is done by the guest and the host proxy. +/// The host receives the write end of the pipe over the host Wayland socket. +pub const CROSS_DOMAIN_ID_TYPE_WRITE_PIPE: u32 = 4; + +/// No ring +pub const CROSS_DOMAIN_RING_NONE: u32 = 0xffffffff; +/// A ring for metadata queries. +pub const CROSS_DOMAIN_QUERY_RING: u32 = 0; +/// A ring based on this particular context's channel. +pub const CROSS_DOMAIN_CHANNEL_RING: u32 = 1; + #[repr(C)] #[derive(Copy, Clone, Default)] pub struct CrossDomainCapabilities { @@ -35,9 +61,8 @@ pub struct CrossDomainImageRequirements { pub offsets: [u32; 4], pub modifier: u64, pub size: u64, - pub blob_id: u64, + pub blob_id: u32, pub map_info: u32, - pub pad: u32, pub memory_idx: i32, pub physical_device_idx: i32, } @@ -76,3 +101,30 @@ pub struct CrossDomainGetImageRequirements { } unsafe impl DataInit for CrossDomainGetImageRequirements {} + +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct CrossDomainSendReceive { + pub hdr: CrossDomainHeader, + pub num_identifiers: u32, + pub opaque_data_size: u32, + pub identifiers: [u32; CROSS_DOMAIN_MAX_IDENTIFIERS], + pub identifier_types: [u32; CROSS_DOMAIN_MAX_IDENTIFIERS], + pub identifier_sizes: [u32; CROSS_DOMAIN_MAX_IDENTIFIERS], + // Data of size "opaque data size follows" +} + +unsafe impl DataInit for CrossDomainSendReceive {} + +#[repr(C)] +#[derive(Copy, Clone, Default)] +pub struct CrossDomainReadWrite { + pub hdr: CrossDomainHeader, + pub identifier: u32, + pub hang_up: u32, + pub opaque_data_size: u32, + pub pad: u32, + // Data of size "opaque data size follows" +} + +unsafe impl DataInit for CrossDomainReadWrite {} diff --git a/rutabaga_gfx/src/rutabaga_utils.rs b/rutabaga_gfx/src/rutabaga_utils.rs index bf92434ef2..a204f04e9e 100644 --- a/rutabaga_gfx/src/rutabaga_utils.rs +++ b/rutabaga_gfx/src/rutabaga_utils.rs @@ -25,12 +25,16 @@ use vulkano::memory::DeviceMemoryAllocError; /// Represents a buffer. `base` contains the address of a buffer, while `len` contains the length /// of the buffer. +#[repr(C)] #[derive(Copy, Clone)] pub struct RutabagaIovec { pub base: *mut c_void, pub len: usize, } +unsafe impl Send for RutabagaIovec {} +unsafe impl Sync for RutabagaIovec {} + /// 3D resource creation parameters. Also used to create 2D resource. Constants based on Mesa's /// (internal) Gallium interface. Not in the virtio-gpu spec, but should be since dumb resources /// can't work with gfxstream/virglrenderer without this.