diff --git a/mm-server/Cargo.lock b/mm-server/Cargo.lock index 42fbdbf..0bf2e1c 100644 --- a/mm-server/Cargo.lock +++ b/mm-server/Cargo.lock @@ -208,6 +208,20 @@ name = "bytemuck" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] [[package]] name = "byteorder" @@ -568,12 +582,46 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "drm" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f" +dependencies = [ + "bitflags 2.6.0", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "libc", + "rustix", +] + +[[package]] +name = "drm-ffi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e41459d99a9b529845f6d2c909eb9adf3b6d2f82635ae40be8de0601726e8b" +dependencies = [ + "drm-sys", + "rustix", +] + [[package]] name = "drm-fourcc" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" +[[package]] +name = "drm-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bafb66c8dbc944d69e15cfcc661df7e703beffbaec8bd63151368b06c5f9858c" +dependencies = [ + "libc", + "linux-raw-sys 0.6.5", +] + [[package]] name = "either" version = "1.13.0" @@ -989,6 +1037,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "linux-raw-sys" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7" + [[package]] name = "listenfd" version = "1.0.1" @@ -1148,6 +1202,7 @@ dependencies = [ "ctrlc", "cursor-icon", "dasp", + "drm", "drm-fourcc", "either", "fuser 0.14.0 (git+https://github.com/colinmarc/fuser?rev=643facdc1bcc9a3b11d7a88ebfaaaa045c3596c1)", @@ -1790,7 +1845,7 @@ dependencies = [ "bitflags 2.6.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.14", "windows-sys 0.52.0", ] diff --git a/mm-server/Cargo.toml b/mm-server/Cargo.toml index e27b796..222146f 100644 --- a/mm-server/Cargo.toml +++ b/mm-server/Cargo.toml @@ -26,6 +26,7 @@ cstr = "0.2" ctrlc = "3" cursor-icon = "1" dasp = "0.11" +drm = "0.14" drm-fourcc = "2" either = "1" git-version = "0.3" diff --git a/mm-server/src/session/compositor.rs b/mm-server/src/session/compositor.rs index 524b75a..1292e81 100644 --- a/mm-server/src/session/compositor.rs +++ b/mm-server/src/session/compositor.rs @@ -6,14 +6,14 @@ use std::{collections::BTreeMap, sync::Arc}; use protocols::*; use slotmap::SlotMap; -use tracing::trace; +use tracing::{instrument, trace}; use wayland_protocols::{ wp::{ fractional_scale::v1::server::wp_fractional_scale_manager_v1, linux_dmabuf::zv1::server::zwp_linux_dmabuf_v1, linux_drm_syncobj::v1::server::wp_linux_drm_syncobj_manager_v1, pointer_constraints::zv1::server::zwp_pointer_constraints_v1, - presentation_time::server::{wp_presentation, wp_presentation_feedback}, + presentation_time::server::wp_presentation, relative_pointer::zv1::server::zwp_relative_pointer_manager_v1, text_input::zv3::server::zwp_text_input_manager_v3, }, @@ -26,8 +26,12 @@ use wayland_server::{ }; use crate::{ - session::{control::*, video, SessionHandle}, - vulkan::{VkContext, VkTimelinePoint}, + session::{ + control::*, + video::{self, TextureSync}, + SessionHandle, + }, + vulkan::VkContext, }; pub mod buffers; @@ -54,11 +58,9 @@ pub struct Compositor { buffers: SlotMap, shm_pools: SlotMap, cached_dmabuf_feedback: buffers::CachedDmabufFeedback, - imported_buffer_timelines: SlotMap, - pending_presentation_feedback: Vec<( - wp_presentation_feedback::WpPresentationFeedback, - VkTimelinePoint, - )>, + imported_syncobj_timelines: SlotMap, + in_flight_buffers: Vec, + pending_presentation_feedback: Vec, surface_stack: Vec, active_surface: Option, @@ -93,7 +95,8 @@ impl Compositor { buffers: SlotMap::default(), shm_pools: SlotMap::default(), cached_dmabuf_feedback, - imported_buffer_timelines: SlotMap::default(), + imported_syncobj_timelines: SlotMap::default(), + in_flight_buffers: Vec::new(), pending_presentation_feedback: Vec::new(), surface_stack: Vec::new(), @@ -164,6 +167,7 @@ impl Compositor { Ok(()) } + #[instrument(skip_all)] pub fn composite_frame( &mut self, video_pipeline: &mut video::EncodePipeline, @@ -199,27 +203,17 @@ impl Compositor { let buffer = &mut self.buffers[content.buffer]; let sync = match &mut buffer.backing { - buffers::BufferBacking::Dmabuf { .. } if content.explicit_sync.is_some() => { - let (acquire, _) = content.explicit_sync.as_ref().unwrap(); - Some(video::TextureSync::TimelineAcquire(acquire.clone())) - } - buffers::BufferBacking::Dmabuf { - fd, - interop_sema, - interop_sema_tripped, - .. - } if !*interop_sema_tripped => { - // Grab a semaphore for explicit sync interop. - buffers::import_dmabuf_fence_as_semaphore(self.vk.clone(), *interop_sema, fd)?; - - // Make sure we only wait for the semaphore once per commit. - *interop_sema_tripped = true; - Some(video::TextureSync::BinaryAcquire(*interop_sema)) + buffers::BufferBacking::Dmabuf { .. } => { + if let Some((acquire, _)) = content.explicit_sync.as_ref() { + Some(TextureSync::Explicit(acquire.clone())) + } else { + Some(TextureSync::ImplicitInterop) + } } _ => None, }; - unsafe { buffer.release_wait = video_pipeline.composite_surface(buffer, sync, conf)? }; + unsafe { content.tp_done = video_pipeline.composite_surface(buffer, sync, conf)? }; if let Some(callback) = surface.frame_callback.current.take().as_mut() { callback.done(now); } @@ -234,7 +228,7 @@ impl Compositor { let tp_render = unsafe { video_pipeline.end_and_submit()? }; for fb in presentation_feedback.drain(..) { self.pending_presentation_feedback - .push((fb, tp_render.clone())); + .push(surface::PendingPresentationFeedback(fb, tp_render.clone())); } Ok(()) diff --git a/mm-server/src/session/compositor/buffers.rs b/mm-server/src/session/compositor/buffers.rs index be4c962..fe460e9 100644 --- a/mm-server/src/session/compositor/buffers.rs +++ b/mm-server/src/session/compositor/buffers.rs @@ -3,8 +3,10 @@ // SPDX-License-Identifier: BUSL-1.1 mod modifiers; +mod syncobj_timeline; use std::{ + collections::BTreeSet, os::fd::{AsFd, AsRawFd, FromRawFd as _, IntoRawFd as _, OwnedFd}, sync::{Arc, RwLock}, }; @@ -12,18 +14,14 @@ use std::{ use anyhow::bail; use ash::vk; use drm_fourcc::DrmModifier; -use hashbrown::HashSet; pub use modifiers::*; -use tracing::trace; -use wayland_protocols::wp::linux_drm_syncobj::v1::server::wp_linux_drm_syncobj_timeline_v1; +pub use syncobj_timeline::*; +use tracing::{instrument, trace}; use wayland_server::{protocol::wl_buffer, Resource as _}; use crate::{ session::compositor::{shm::Pool, Compositor}, - vulkan::{ - create_image_view, select_memory_type, VkContext, VkHostBuffer, VkImage, VkTimelinePoint, - VkTimelineSemaphore, - }, + vulkan::{create_image_view, select_memory_type, VkContext, VkHostBuffer, VkImage}, }; slotmap::new_key_type! { pub struct BufferKey; } @@ -32,17 +30,6 @@ pub struct Buffer { pub wl_buffer: wl_buffer::WlBuffer, pub backing: BufferBacking, - /// The client is waiting for us to release this buffer. - pub needs_release: bool, - - /// If set, we should wait on this timeline point before releasing the - /// buffer. - pub release_wait: Option, - - /// If set, we should signal this timeline point when we're done with - /// the buffer (instead of using the normal wl_buffer.release signal). - pub release_signal: Option, - /// Next time we release this buffer, we should destroy it as well. pub needs_destruction: bool, } @@ -56,21 +43,6 @@ impl Buffer { } } -impl Drop for Buffer { - fn drop(&mut self) { - if let BufferBacking::Dmabuf { - vk, interop_sema, .. - } = &self.backing - { - // This should be safe, since we would've waited on it before - // releasing the buffer. - unsafe { - vk.device.destroy_semaphore(*interop_sema, None); - } - } - } -} - pub enum BufferBacking { Shm { format: PlaneMetadata, @@ -86,13 +58,6 @@ pub enum BufferBacking { format: PlaneMetadata, fd: OwnedFd, image: VkImage, - - /// Used for implicit-explicit sync interop, where we use an ioctl to - /// get an FD and use that to set a binary semaphore. - interop_sema: vk::Semaphore, - interop_sema_tripped: bool, - - vk: Arc, }, } @@ -106,69 +71,78 @@ pub struct PlaneMetadata { pub offset: u32, } -slotmap::new_key_type! { pub struct BufferTimelineKey; } - -pub struct BufferTimeline { - pub _wp_syncobj_timeline: wp_linux_drm_syncobj_timeline_v1::WpLinuxDrmSyncobjTimelineV1, - pub sema: VkTimelineSemaphore, -} - impl Compositor { + #[instrument(skip_all)] pub fn release_buffers(&mut self) -> anyhow::Result<()> { - let mut used_buffers = HashSet::new(); - used_buffers.extend( - self.surfaces - .iter() - .flat_map(|(_, s)| s.content.as_ref()) - .map(|c| c.buffer), - ); - - let mut to_destroy = HashSet::new(); - for (id, buffer) in self.buffers.iter_mut().filter(|(_, b)| b.needs_release) { - if used_buffers.contains(&id) { - continue; - } - - if let Some(tp) = &buffer.release_wait { + // Check if any content updates have finished. + let mut still_in_flight = Vec::new(); + for content in self.in_flight_buffers.drain(..) { + if let Some(tp) = &content.tp_done { if unsafe { !tp.poll()? } { + // The frame using this content is still in-progress. + still_in_flight.push(content); continue; } } - trace!( - wl_buffer = buffer.wl_buffer.id().protocol_id(), - "releasing buffer" - ); + if content.needs_release { + let buffer = self + .buffers + .get(content.buffer) + .expect("buffer has no entry"); + + trace!( + wl_buffer = buffer.wl_buffer.id().protocol_id(), + "explicitly releasing buffer" + ); - if let Some(tp) = &buffer.release_signal.take() { - unsafe { - tp.signal()?; - } - } else { buffer.wl_buffer.release(); } - buffer.needs_release = false; - buffer.release_wait = None; - if buffer.needs_destruction { - to_destroy.insert(id); + if let Some((_, release)) = content.explicit_sync { + release.signal()?; + } + + // If we didn't move the presentation feedback into a separate queue, + // that means we didn't use the content update and we should relate + // that to the client. + if let Some(feedback) = &content.wp_presentation_feedback { + feedback.discarded(); } } - for id in to_destroy { - let buf = self.buffers.remove(id).unwrap(); - assert!(!buf.wl_buffer.is_alive()); + self.in_flight_buffers = still_in_flight; + + // A buffer is in use if it's either part of an in-flight frame, or if + // we're holding on to it because the client hasn't committed a new one + // yet, and we may need to display it again. + let used_buffers: BTreeSet = self + .surfaces + .values() + .flat_map(|s| &s.content) + .chain(self.in_flight_buffers.iter()) + .map(|c| c.buffer) + .collect(); + + self.buffers.retain(|id, buffer| { + if !buffer.needs_destruction || used_buffers.contains(&id) { + true + } else { + assert!(!buffer.wl_buffer.is_alive()); + trace!( + wl_buffer = buffer.wl_buffer.id().protocol_id(), + "destroying buffer" + ); - trace!( - wl_buffer = buf.wl_buffer.id().protocol_id(), - "destroying buffer" - ); - } + false + } + }); Ok(()) } } +#[instrument(skip_all)] pub fn import_shm_buffer( vk: Arc, wl_buffer: wl_buffer::WlBuffer, @@ -211,13 +185,11 @@ pub fn import_shm_buffer( format, dirty: true, }, - needs_release: false, - release_wait: None, - release_signal: None, needs_destruction: false, }) } +#[instrument(skip_all)] pub fn import_dmabuf_buffer( vk: Arc, wl_buffer: wl_buffer::WlBuffer, @@ -349,25 +321,9 @@ pub fn import_dmabuf_buffer( let view = unsafe { create_image_view(&vk.device, image, vk_format, ignore_alpha)? }; let image = VkImage::wrap(vk.clone(), image, view, memory, vk_format, width, height); - let interop_sema = unsafe { - vk.device - .create_semaphore(&vk::SemaphoreCreateInfo::default(), None)? - }; - Ok(Buffer { wl_buffer, - backing: BufferBacking::Dmabuf { - format, - fd, - image, - interop_sema, - interop_sema_tripped: false, - - vk, - }, - needs_release: false, - release_wait: None, - release_signal: None, + backing: BufferBacking::Dmabuf { format, fd, image }, needs_destruction: false, }) } @@ -414,8 +370,6 @@ mod ioctl { pub(super) const DMA_BUF_SYNC_READ: u32 = 1 << 0; pub(super) const DMA_BUF_SYNC_WRITE: u32 = 1 << 1; - // Opcode::write::(b'b', 3); - #[repr(C)] #[allow(non_camel_case_types)] struct dma_buf_export_sync_file { @@ -497,6 +451,7 @@ mod ioctl { /// Retrieves a dmabuf fence, and uses it to set a semaphore. The semaphore will /// be triggered when the dmabuf texture is safe to read. Note that the spec /// insists that the semaphore must be waited on once set this way. +#[instrument(skip_all)] pub fn import_dmabuf_fence_as_semaphore( vk: Arc, semaphore: vk::Semaphore, @@ -505,16 +460,23 @@ pub fn import_dmabuf_fence_as_semaphore( let fd = fd.as_fd(); let sync_fd = unsafe { export_sync_file(fd, ioctl::DMA_BUF_SYNC_READ)? }; + unsafe { import_sync_file_as_semaphore(vk, sync_fd, semaphore) } +} + +#[instrument(skip_all)] +pub unsafe fn import_sync_file_as_semaphore( + vk: Arc, + fd: OwnedFd, + semaphore: vk::Semaphore, +) -> anyhow::Result<()> { let import_info = vk::ImportSemaphoreFdInfoKHR::default() .semaphore(semaphore) .handle_type(vk::ExternalSemaphoreHandleTypeFlags::SYNC_FD) .flags(vk::SemaphoreImportFlags::TEMPORARY) - .fd(sync_fd.into_raw_fd()); // Vulkan owns the fd now. + .fd(fd.into_raw_fd()); // Vulkan owns the fd now. - unsafe { - vk.external_semaphore_api - .import_semaphore_fd(&import_info)?; - } + vk.external_semaphore_api + .import_semaphore_fd(&import_info)?; Ok(()) } diff --git a/mm-server/src/session/compositor/buffers/syncobj_timeline.rs b/mm-server/src/session/compositor/buffers/syncobj_timeline.rs new file mode 100644 index 0000000..d1ce211 --- /dev/null +++ b/mm-server/src/session/compositor/buffers/syncobj_timeline.rs @@ -0,0 +1,99 @@ +// Copyright 2024 Colin Marc +// +// SPDX-License-Identifier: BUSL-1.1 + +use std::{ + io, + os::fd::{AsFd as _, OwnedFd}, + sync::Arc, +}; + +use ash::vk; +use drm::control::{syncobj, Device as _}; +use tracing::{instrument, trace}; +use wayland_protocols::wp::linux_drm_syncobj::v1::server::wp_linux_drm_syncobj_timeline_v1; + +use crate::vulkan::VkContext; + +slotmap::new_key_type! { pub struct SyncobjTimelineKey; } + +pub struct SyncobjTimeline(Arc); + +struct TimelineHandle { + pub _wp_syncobj_timeline: wp_linux_drm_syncobj_timeline_v1::WpLinuxDrmSyncobjTimelineV1, + handle: syncobj::Handle, + vk: Arc, +} + +impl Drop for TimelineHandle { + fn drop(&mut self) { + let _ = self.vk.drm_device.destroy_syncobj(self.handle); + } +} + +#[derive(Clone)] +pub struct SyncobjTimelinePoint { + pub value: u64, + handle: Arc, +} + +impl SyncobjTimelinePoint { + pub fn signal(&self) -> io::Result<()> { + trace!(handle = ?self.handle.handle, value = self.value, "signaling timeline point"); + + self.handle + .vk + .drm_device + .syncobj_timeline_signal(&[self.handle.handle], &[self.value]) + } + + #[instrument(skip_all)] + pub fn import_as_semaphore(&self, semaphore: vk::Semaphore) -> anyhow::Result<()> { + trace!( + value = self.value, + ?semaphore, + "importing timeline point as semaphore" + ); + + let device = &self.handle.vk.drm_device; + + // First, we export a sync file by creating a new syncobj and copying + // the timeline point to 0 on the new syncobj. + let syncobj = device.create_syncobj(false)?; + scopeguard::defer! { + self.handle.vk + .drm_device + .destroy_syncobj(syncobj) + .expect("failed to destroy syncobj") + }; + + device.syncobj_timeline_transfer(self.handle.handle, syncobj, self.value, 0)?; + let sync_fd = device.syncobj_to_fd(syncobj, true)?; + + // Then we can import it into a vulkan semaphore. + unsafe { super::import_sync_file_as_semaphore(self.handle.vk.clone(), sync_fd, semaphore) } + } +} + +impl SyncobjTimeline { + pub fn import( + vk: Arc, + wp_syncobj_timeline: wp_linux_drm_syncobj_timeline_v1::WpLinuxDrmSyncobjTimelineV1, + fd: OwnedFd, + ) -> io::Result { + let handle = vk.drm_device.fd_to_syncobj(fd.as_fd(), false)?; + + Ok(Self(Arc::new(TimelineHandle { + _wp_syncobj_timeline: wp_syncobj_timeline, + handle, + vk, + }))) + } + + pub fn new_timeline_point(&self, value: u64) -> SyncobjTimelinePoint { + SyncobjTimelinePoint { + value, + handle: self.0.clone(), + } + } +} diff --git a/mm-server/src/session/compositor/dispatch/wp_linux_drm_syncobj.rs b/mm-server/src/session/compositor/dispatch/wp_linux_drm_syncobj.rs index 0b78e05..7fd12bf 100644 --- a/mm-server/src/session/compositor/dispatch/wp_linux_drm_syncobj.rs +++ b/mm-server/src/session/compositor/dispatch/wp_linux_drm_syncobj.rs @@ -9,13 +9,10 @@ use wayland_protocols::wp::linux_drm_syncobj::v1::server::{ }; use wayland_server::Resource as _; -use crate::{ - session::compositor::{ - buffers::{BufferTimeline, BufferTimelineKey}, - surface::SurfaceKey, - Compositor, - }, - vulkan::VkTimelineSemaphore, +use crate::session::compositor::{ + buffers::{SyncobjTimeline, SyncobjTimelineKey}, + surface::SurfaceKey, + Compositor, }; impl wayland_server::GlobalDispatch @@ -67,24 +64,15 @@ impl wayland_server::Dispatch { - let sema = match VkTimelineSemaphore::from_syncobj_fd(state.vk.clone(), fd) { - Ok(t) => t, - Err(e) => { - error!("failed to import syncobj timeline: {e:#}"); - resource.post_error( - wp_linux_drm_syncobj_manager_v1::Error::InvalidTimeline, - "Failed to import timeline.", - ); - return; - } - }; - - state - .imported_buffer_timelines - .insert_with_key(|k| BufferTimeline { - _wp_syncobj_timeline: data_init.init(id, k), - sema, - }); + if let Err(err) = state.imported_syncobj_timelines.try_insert_with_key(|k| { + SyncobjTimeline::import(state.vk.clone(), data_init.init(id, k), fd) + }) { + error!("failed to import syncobj timeline: {err:#}"); + resource.post_error( + wp_linux_drm_syncobj_manager_v1::Error::InvalidTimeline, + "Failed to import timeline.", + ); + } } wp_linux_drm_syncobj_manager_v1::Request::Destroy => (), _ => unreachable!(), @@ -114,8 +102,8 @@ impl point_lo, } => { let timeline = timeline - .data::() - .and_then(|key| state.imported_buffer_timelines.get(*key)) + .data::() + .and_then(|key| state.imported_syncobj_timelines.get(*key)) .expect("timeline has no entry"); let surface = state @@ -124,7 +112,7 @@ impl .expect("surface has no entry"); surface.pending_acquire_point = - Some(timeline.sema.new_point(super::make_u64(point_hi, point_lo))) + Some(timeline.new_timeline_point(super::make_u64(point_hi, point_lo))) } wp_linux_drm_syncobj_surface_v1::Request::SetReleasePoint { timeline, @@ -132,8 +120,8 @@ impl point_lo, } => { let timeline = timeline - .data::() - .and_then(|key| state.imported_buffer_timelines.get(*key)) + .data::() + .and_then(|key| state.imported_syncobj_timelines.get(*key)) .expect("timeline has no entry"); let surface = state @@ -142,7 +130,7 @@ impl .expect("surface has no entry"); surface.pending_release_point = - Some(timeline.sema.new_point(super::make_u64(point_hi, point_lo))) + Some(timeline.new_timeline_point(super::make_u64(point_hi, point_lo))) } wp_linux_drm_syncobj_surface_v1::Request::Destroy => (), _ => unreachable!(), @@ -166,7 +154,7 @@ impl impl wayland_server::Dispatch< wp_linux_drm_syncobj_timeline_v1::WpLinuxDrmSyncobjTimelineV1, - BufferTimelineKey, + SyncobjTimelineKey, > for Compositor { fn request( @@ -174,7 +162,7 @@ impl _client: &wayland_server::Client, _resource: &wp_linux_drm_syncobj_timeline_v1::WpLinuxDrmSyncobjTimelineV1, _request: wp_linux_drm_syncobj_timeline_v1::Request, - _data: &BufferTimelineKey, + _data: &SyncobjTimelineKey, _dhandle: &wayland_server::DisplayHandle, _data_init: &mut wayland_server::DataInit<'_, Self>, ) { diff --git a/mm-server/src/session/compositor/surface.rs b/mm-server/src/session/compositor/surface.rs index dd6fe1f..8fe87e6 100644 --- a/mm-server/src/session/compositor/surface.rs +++ b/mm-server/src/session/compositor/surface.rs @@ -18,6 +18,7 @@ use wayland_server::{ Resource as _, }; +use super::buffers::SyncobjTimelinePoint; use crate::{ pixel_scale::PixelScale, session::compositor::{ @@ -42,8 +43,8 @@ pub struct Surface { pub content: Option, pub wp_syncobj_surface: Option, - pub pending_acquire_point: Option, - pub pending_release_point: Option, + pub pending_acquire_point: Option, + pub pending_release_point: Option, pub role: DoubleBuffered, pub sent_configuration: Option, @@ -278,8 +279,15 @@ pub enum PendingBuffer { pub struct ContentUpdate { pub buffer: BufferKey, + /// Whether the client is waiting on a buffer.release(). + pub needs_release: bool, + /// Used for explicit sync. - pub explicit_sync: Option<(VkTimelinePoint, VkTimelinePoint)>, + pub explicit_sync: Option<(SyncobjTimelinePoint, SyncobjTimelinePoint)>, + + /// If the content update is in use, this timeline point indicates when it + /// will be free. + pub tp_done: Option, /// The real dimensions of the buffer. This is how surface coordinates are /// determined in wayland. @@ -287,13 +295,10 @@ pub struct ContentUpdate { pub wp_presentation_feedback: Option, } -impl Drop for ContentUpdate { - fn drop(&mut self) { - if let Some(feedback) = self.wp_presentation_feedback.take() { - feedback.discarded(); - } - } -} +pub struct PendingPresentationFeedback( + pub wp_presentation_feedback::WpPresentationFeedback, + pub VkTimelinePoint, +); pub struct CommitError(pub xdg_surface::Error, pub String); @@ -323,7 +328,7 @@ impl Compositor { { return Err(CommitError( xdg_surface::Error::UnconfiguredBuffer, - "The buffer must be configured prior to attaching a buffer.".to_string(), + "The surface must be configured prior to attaching a buffer.".to_string(), )); } @@ -339,9 +344,8 @@ impl Compositor { } } - buffer.needs_release = true; - // In the case of shm buffer, we do a copy and immediately release it. + let mut needs_release = true; if let BufferBacking::Shm { staging_buffer, format, @@ -364,7 +368,7 @@ impl Compositor { staging_buffer.copy_from_slice(contents); *dirty = true; - buffer.needs_release = false; + needs_release = false; buffer.wl_buffer.release(); } @@ -393,16 +397,24 @@ impl Compositor { Some((acquire_point, release_point)) }); - if let Some((_, release)) = explicit_sync.as_ref() { - buffer.release_signal = Some(release.clone()); + if needs_release && explicit_sync.is_some() { + // No need for release events if explicit sync is used. + needs_release = false; } - surface.content = Some(ContentUpdate { + let old_content = surface.content.replace(ContentUpdate { buffer: buffer_id, + needs_release, explicit_sync, + tp_done: None, dimensions: buffer.dimensions(), wp_presentation_feedback: feedback, }); + + if let Some(old_content) = old_content { + // Enqueue the buffer for release. + self.in_flight_buffers.push(old_content); + } } None => (), } @@ -584,9 +596,9 @@ impl Compositor { let refresh = time::Duration::from_secs_f64(1.0 / framerate as f64).as_nanos() as u32; let mut still_pending = Vec::with_capacity(self.pending_presentation_feedback.len()); - for (fb, tp) in self.pending_presentation_feedback.drain(..) { + for PendingPresentationFeedback(fb, tp) in self.pending_presentation_feedback.drain(..) { if unsafe { !tp.poll()? } { - still_pending.push((fb, tp)); + still_pending.push(PendingPresentationFeedback(fb, tp)); continue; } diff --git a/mm-server/src/session/video.rs b/mm-server/src/session/video.rs index 54711c7..e117ef1 100644 --- a/mm-server/src/session/video.rs +++ b/mm-server/src/session/video.rs @@ -10,9 +10,12 @@ use ash::vk; mod composite; mod convert; -use tracing::{instrument, trace, warn}; +use tracing::{instrument, trace, trace_span, warn}; -use super::{compositor, DisplayParams, SessionHandle, VideoStreamParams}; +use super::{ + compositor::{self, buffers::SyncobjTimelinePoint}, + DisplayParams, SessionHandle, VideoStreamParams, +}; use crate::{ color::ColorSpace, encoder::{self}, @@ -62,8 +65,8 @@ impl encoder::Sink for Sink { pub struct SwapFrame { convert_ds: vk::DescriptorSet, // Should be dropped first. draws: Vec<(vk::ImageView, glam::Vec2, glam::Vec2)>, - texture_semas: Vec, - texture_acquire_points: Vec, + texture_semas: Vec, // Reused each frame. + texture_semas_used: usize, /// An RGBA image to composite to. blend_image: VkImage, @@ -88,8 +91,8 @@ pub struct SwapFrame { } pub enum TextureSync { - BinaryAcquire(vk::Semaphore), - TimelineAcquire(VkTimelinePoint), + Explicit(SyncobjTimelinePoint), + ImplicitInterop, } pub struct EncodePipeline { @@ -180,9 +183,11 @@ impl EncodePipeline { } // We conditionally create the staging span, below. Rendering always happens. - frame.render_span = Some(ctx.span(tracy_client::span_location!())?); + frame.render_span = Some(ctx.span(tracy_client::span_location!("render"))?); } + frame.texture_semas_used = 0; + // Wait for the frame to no longer be in flight, and then establish new timeline // points. frame.tp_clear.wait()?; @@ -302,9 +307,11 @@ impl EncodePipeline { vk::AccessFlags2::SHADER_READ, ); + assert!(sync.is_none()); + (image.view, None) } - compositor::buffers::BufferBacking::Dmabuf { image, .. } => { + compositor::buffers::BufferBacking::Dmabuf { image, fd, .. } => { // Transition the image to be readable. A special queue, // EXTERNAL, is used in a queue transfer to indicate // acquiring the texture from the wayland client. @@ -335,16 +342,25 @@ impl EncodePipeline { vk::AccessFlags2::NONE, ); - (image.view, Some(frame.tp_render_done.clone())) - } - }; + if let Some(sync) = sync { + let sema = allocate_texture_semaphore(self.vk.clone(), frame)?; + + match sync { + TextureSync::Explicit(syncobj) => { + syncobj.import_as_semaphore(sema)?; + } + TextureSync::ImplicitInterop => { + compositor::buffers::import_dmabuf_fence_as_semaphore( + self.vk.clone(), + sema, + fd, + )?; + } + } + } - match sync { - Some(TextureSync::TimelineAcquire(acquire)) => { - frame.texture_acquire_points.push(acquire) + (image.view, Some(frame.tp_render_done.clone())) } - Some(TextureSync::BinaryAcquire(semaphore)) => frame.texture_semas.push(semaphore), - None => (), }; // Convert the destination rect into clip coordinates. @@ -359,7 +375,7 @@ impl EncodePipeline { Ok(release) } - #[instrument(level = "trace", skip(self))] + #[instrument(skip_all)] pub unsafe fn end_and_submit(&mut self) -> anyhow::Result { let device = &self.vk.device; let frame = &mut self.swap[self.swap_idx]; @@ -509,31 +525,24 @@ impl EncodePipeline { .stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS) .value(frame.tp_render_done.value())]; - for sema in frame.texture_semas.drain(..) { + for sema in &frame.texture_semas[0..frame.texture_semas_used] { render_wait_infos.push( vk::SemaphoreSubmitInfo::default() - .semaphore(sema) + .semaphore(*sema) .stage_mask(vk::PipelineStageFlags2::FRAGMENT_SHADER), ); } - // Wait on explicit sync acquire points before sampling. - for acquire in frame.texture_acquire_points.drain(..) { - render_wait_infos.push( - vk::SemaphoreSubmitInfo::default() - .semaphore(acquire.timeline().as_semaphore()) - .value(acquire.value()) - .stage_mask(vk::PipelineStageFlags2::FRAGMENT_SHADER), - ) - } - let render_submit_info = vk::SubmitInfo2::default() .command_buffer_infos(&render_cb_infos) .wait_semaphore_infos(&render_wait_infos) .signal_semaphore_infos(&render_signal_infos); submits.push(render_submit_info); - device.queue_submit2(self.vk.graphics_queue.queue, &submits, vk::Fence::null())?; + + trace_span!("queue_submit2").in_scope(|| { + device.queue_submit2(self.vk.graphics_queue.queue, &submits, vk::Fence::null()) + })?; // Trigger encode. self.encoder.submit_encode( @@ -544,7 +553,7 @@ impl EncodePipeline { // Wait for uploads to finish before returning, so that writes to the // staging buffers are synchronized. - frame.tp_staging_done.wait()?; + trace_span!("tp_staging_done.wait").in_scope(|| frame.tp_staging_done.wait())?; let tp_clear = frame.tp_clear.clone(); let swap_len = self.swap.len(); @@ -580,6 +589,10 @@ impl Drop for EncodePipeline { device.destroy_image_view(*view, None); } + for sema in &frame.texture_semas { + device.destroy_semaphore(*sema, None); + } + device.destroy_query_pool(frame.render_ts_pool.pool, None); device.destroy_query_pool(frame.staging_ts_pool.pool, None); } @@ -665,7 +678,7 @@ fn new_swapframe( Ok(SwapFrame { convert_ds, texture_semas: Vec::new(), - texture_acquire_points: Vec::new(), + texture_semas_used: 0, draws: Vec::new(), blend_image, encode_image, @@ -685,6 +698,26 @@ fn new_swapframe( }) } +fn allocate_texture_semaphore( + vk: Arc, + frame: &mut SwapFrame, +) -> anyhow::Result { + let idx = frame.texture_semas_used; + frame.texture_semas_used += 1; + + if frame.texture_semas_used <= frame.texture_semas.len() { + return Ok(frame.texture_semas[idx]); + } + + let sema = unsafe { + vk.device + .create_semaphore(&vk::SemaphoreCreateInfo::default(), None)? + }; + + frame.texture_semas.push(sema); + Ok(sema) +} + fn format_is_semiplanar(format: vk::Format) -> bool { // grep for 2PLANE in the vulkan spec. matches!( diff --git a/mm-server/src/vulkan.rs b/mm-server/src/vulkan.rs index 6457b53..b195a17 100644 --- a/mm-server/src/vulkan.rs +++ b/mm-server/src/vulkan.rs @@ -5,6 +5,7 @@ #![allow(clippy::too_many_arguments)] mod chain; +mod drm; mod timeline; pub mod video; @@ -46,6 +47,7 @@ pub struct VkContext { pub debug: Option, pub device: ash::Device, pub device_info: VkDeviceInfo, + pub drm_device: drm::DrmDevice, pub graphics_queue: VkQueue, pub encode_queue: Option, pub descriptor_pool: vk::DescriptorPool, @@ -495,6 +497,8 @@ impl VkContext { let (index, device_info) = devices.remove(0); info!("selected gpu: {:?} ({index})", device_info.device_name); + let drm_device = drm::DrmDevice::new(device_info.drm_node)?; + let device = { let queue_priorities = &[1.0]; let mut queue_indices = Vec::new(); @@ -613,6 +617,7 @@ impl VkContext { instance, device, device_info, + drm_device, graphics_queue, encode_queue, descriptor_pool, diff --git a/mm-server/src/vulkan/drm.rs b/mm-server/src/vulkan/drm.rs new file mode 100644 index 0000000..522c8e0 --- /dev/null +++ b/mm-server/src/vulkan/drm.rs @@ -0,0 +1,36 @@ +// Copyright 2024 Colin Marc +// +// SPDX-License-Identifier: BUSL-1.1 + +use std::{ + fs::{File, OpenOptions}, + os::fd::{AsFd, BorrowedFd}, +}; + +use anyhow::anyhow; +use libc::dev_t; + +pub struct DrmDevice(File); + +impl AsFd for DrmDevice { + fn as_fd(&self) -> BorrowedFd<'_> { + self.0.as_fd() + } +} + +impl drm::Device for DrmDevice {} +impl drm::control::Device for DrmDevice {} + +impl DrmDevice { + pub fn new(dev: dev_t) -> anyhow::Result { + let path = drm::node::DrmNode::from_dev_id(dev)? + .dev_path() + .ok_or(anyhow!("no device file found"))?; + + let mut options = OpenOptions::new(); + options.read(true); + options.write(true); + + Ok(Self(options.open(path)?)) + } +} diff --git a/mm-server/src/vulkan/timeline.rs b/mm-server/src/vulkan/timeline.rs index dfc0fa3..ac2ebb1 100644 --- a/mm-server/src/vulkan/timeline.rs +++ b/mm-server/src/vulkan/timeline.rs @@ -105,7 +105,7 @@ impl VkTimelinePoint { VkTimelineSemaphore(self.0.clone()) } - #[instrument(level = "trace", skip_all)] + #[instrument(skip_all)] pub unsafe fn wait(&self) -> anyhow::Result<()> { let device = &self.0.vk.device; device.wait_semaphores( @@ -118,7 +118,7 @@ impl VkTimelinePoint { Ok(()) } - #[instrument(level = "trace", skip_all)] + #[instrument(skip_all)] pub unsafe fn signal(&self) -> anyhow::Result<()> { let device = &self.0.vk.device; device.signal_semaphore(