diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d138a228..2644dce6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,6 +4,9 @@ # Apple platforms /src/cg.rs @madsmtm +# DRM/KMS (no maintainer) +/src/kms.rs + # Redox /src/orbital.rs @jackpot51 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8250ceac..1be96a25 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,9 +43,10 @@ jobs: - { target: x86_64-unknown-linux-gnu, os: ubuntu-latest, } - { target: x86_64-unknown-linux-gnu, os: ubuntu-latest, options: --no-default-features, features: "x11,x11-dlopen" } - { target: x86_64-unknown-linux-gnu, os: ubuntu-latest, options: --no-default-features, features: "wayland,wayland-dlopen" } + - { target: x86_64-unknown-linux-gnu, os: ubuntu-latest, options: --no-default-features, features: "kms" } - { target: x86_64-unknown-redox, os: ubuntu-latest, } - { target: x86_64-unknown-freebsd, os: ubuntu-latest, } - - { target: x86_64-unknown-netbsd, os: ubuntu-latest, } + - { target: x86_64-unknown-netbsd, os: ubuntu-latest, options: --no-default-features, features: "x11,x11-dlopen,wayland,wayland-dlopen" } - { target: x86_64-apple-darwin, os: macos-latest, } - { target: wasm32-unknown-unknown, os: ubuntu-latest, } include: diff --git a/Cargo.toml b/Cargo.toml index cf9d8093..ac7c7330 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,8 @@ name = "buffer_mut" harness = false [features] -default = ["x11", "x11-dlopen", "wayland", "wayland-dlopen"] +default = ["kms", "x11", "x11-dlopen", "wayland", "wayland-dlopen"] +kms = ["bytemuck", "drm", "drm-sys", "nix"] wayland = ["wayland-backend", "wayland-client", "memmap2", "nix", "fastrand"] wayland-dlopen = ["wayland-sys/dlopen"] x11 = ["as-raw-xcb-connection", "bytemuck", "nix", "tiny-xlib", "x11rb"] @@ -30,6 +31,8 @@ raw-window-handle = "0.5.0" [target.'cfg(all(unix, not(any(target_vendor = "apple", target_os = "android", target_os = "redox"))))'.dependencies] as-raw-xcb-connection = { version = "1.0.0", optional = true } bytemuck = { version = "1.12.3", optional = true } +drm = { version = "0.9.0", default-features = false, optional = true } +drm-sys = { version = "0.4.0", default-features = false, optional = true } memmap2 = { version = "0.7.1", optional = true } nix = { version = "0.26.1", optional = true } tiny-xlib = { version = "0.2.1", optional = true } @@ -76,6 +79,7 @@ redox_syscall = "0.3" cfg_aliases = "0.1.1" [dev-dependencies] +colorous = "1.0.12" criterion = { version = "0.4.0", default-features = false, features = ["cargo_bench_support"] } instant = "0.1.12" winit = "0.28.1" @@ -95,6 +99,9 @@ rayon = "1.5.1" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wasm-bindgen-test = "0.3" +[target.'cfg(all(unix, not(any(target_vendor = "apple", target_os = "android", target_os = "redox"))))'.dev-dependencies] +rustix = { version = "0.38.8", features = ["event"] } + [workspace] members = [ "run-wasm", diff --git a/build.rs b/build.rs index 5113eeb6..63c5f02d 100644 --- a/build.rs +++ b/build.rs @@ -1,6 +1,7 @@ fn main() { cfg_aliases::cfg_aliases! { free_unix: { all(unix, not(any(target_vendor = "apple", target_os = "android", target_os = "redox"))) }, + kms_platform: { all(feature = "kms", free_unix, not(target_arch = "wasm32")) }, x11_platform: { all(feature = "x11", free_unix, not(target_arch = "wasm32")) }, wayland_platform: { all(feature = "wayland", free_unix, not(target_arch = "wasm32")) }, } diff --git a/examples/drm.rs b/examples/drm.rs new file mode 100644 index 00000000..caf6d617 --- /dev/null +++ b/examples/drm.rs @@ -0,0 +1,220 @@ +//! Example of using softbuffer with drm-rs. + +#[cfg(kms_platform)] +mod imple { + use drm::control::{connector, Device as CtrlDevice, Event, ModeTypeFlags, PlaneType}; + use drm::Device; + + use raw_window_handle::{DrmDisplayHandle, DrmWindowHandle}; + use softbuffer::{Context, Surface}; + + use std::num::NonZeroU32; + use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd}; + use std::path::Path; + use std::time::{Duration, Instant}; + + pub(super) fn entry() -> Result<(), Box> { + // Open a new device. + let device = Card::find()?; + + // Create the softbuffer context. + let context = unsafe { + Context::from_raw({ + let mut handle = DrmDisplayHandle::empty(); + handle.fd = device.as_fd().as_raw_fd(); + handle.into() + }) + }?; + + // Get the DRM handles. + let handles = device.resource_handles()?; + + // Get the list of connectors and CRTCs. + let connectors = handles + .connectors() + .iter() + .map(|&con| device.get_connector(con, true)) + .collect::, _>>()?; + let crtcs = handles + .crtcs() + .iter() + .map(|&crtc| device.get_crtc(crtc)) + .collect::, _>>()?; + + // Find a connected crtc. + let con = connectors + .iter() + .find(|con| con.state() == connector::State::Connected) + .ok_or("No connected connectors")?; + + // Get the first CRTC. + let crtc = crtcs.first().ok_or("No CRTCs")?; + + // Find a mode to use. + let mode = con + .modes() + .iter() + .find(|mode| mode.mode_type().contains(ModeTypeFlags::PREFERRED)) + .or_else(|| con.modes().first()) + .ok_or("No modes")?; + + // Look for a primary plane compatible with our CRTC. + let planes = device.plane_handles()?; + let planes = planes + .iter() + .filter(|&&plane| { + device.get_plane(plane).map_or(false, |plane| { + let crtcs = handles.filter_crtcs(plane.possible_crtcs()); + crtcs.contains(&crtc.handle()) + }) + }) + .collect::>(); + + // Find the first primary plane or take the first one period. + let plane = planes + .iter() + .find(|&&&plane| { + if let Ok(props) = device.get_properties(plane) { + let (ids, vals) = props.as_props_and_values(); + for (&id, &val) in ids.iter().zip(vals.iter()) { + if let Ok(info) = device.get_property(id) { + if info.name().to_str().map_or(false, |x| x == "type") { + return val == PlaneType::Primary as u32 as u64; + } + } + } + } + + false + }) + .or(planes.first()) + .ok_or("No planes")?; + + // Create the surface on top of this plane. + // Note: This requires root on DRM/KMS. + let mut surface = unsafe { + Surface::from_raw(&context, { + let mut handle = DrmWindowHandle::empty(); + handle.plane = (**plane).into(); + handle.into() + }) + }?; + + // Resize the surface. + let (width, height) = mode.size(); + surface.resize( + NonZeroU32::new(width as u32).unwrap(), + NonZeroU32::new(height as u32).unwrap(), + )?; + + // Start drawing to it. + let start = Instant::now(); + let mut tick = 0; + while Instant::now().duration_since(start) < Duration::from_secs(2) { + tick += 1; + println!("Drawing tick {tick}"); + + // Start drawing. + let mut buffer = surface.buffer_mut()?; + draw_to_buffer(&mut buffer, tick); + buffer.present()?; + + // Wait for the page flip to happen. + rustix::event::poll( + &mut [rustix::event::PollFd::new( + &device, + rustix::event::PollFlags::IN, + )], + -1, + )?; + + // Receive the events. + let events = device.receive_events()?; + println!("Got some events..."); + for event in events { + match event { + Event::PageFlip(_) => { + println!("Page flip event."); + } + Event::Vblank(_) => { + println!("Vblank event."); + } + _ => { + println!("Unknown event."); + } + } + } + } + + Ok(()) + } + + fn draw_to_buffer(buf: &mut [u32], tick: usize) { + let scale = colorous::SINEBOW; + let mut i = (tick as f64) / 20.0; + while i > 1.0 { + i -= 1.0; + } + + let color = scale.eval_continuous(i); + let pixel = (color.r as u32) << 16 | (color.g as u32) << 8 | (color.b as u32); + buf.fill(pixel); + } + + struct Card(std::fs::File); + + impl Card { + fn find() -> Result> { + for i in 0..10 { + let path = format!("/dev/dri/card{i}"); + let device = Card::open(path)?; + + // Only use it if it has connectors. + let handles = match device.resource_handles() { + Ok(handles) => handles, + Err(_) => continue, + }; + + if handles + .connectors + .iter() + .filter_map(|c| device.get_connector(*c, false).ok()) + .any(|c| c.state() == connector::State::Connected) + { + return Ok(device); + } + } + + Err("No DRM device found".into()) + } + + fn open(path: impl AsRef) -> Result> { + let file = std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(path)?; + Ok(Card(file)) + } + } + + impl AsFd for Card { + fn as_fd(&self) -> BorrowedFd<'_> { + self.0.as_fd() + } + } + + impl Device for Card {} + impl CtrlDevice for Card {} +} + +#[cfg(not(kms_platform))] +mod imple { + pub(super) fn entry() -> Result<(), Box> { + eprintln!("This example requires the `kms` feature."); + Ok(()) + } +} + +fn main() -> Result<(), Box> { + imple::entry() +} diff --git a/src/kms.rs b/src/kms.rs new file mode 100644 index 00000000..bc1638c5 --- /dev/null +++ b/src/kms.rs @@ -0,0 +1,409 @@ +//! Backend for DRM/KMS for raw rendering directly to the screen. +//! +//! This strategy uses dumb buffers for rendering. + +use drm::buffer::{Buffer, DrmFourcc}; +use drm::control::dumbbuffer::{DumbBuffer, DumbMapping}; +use drm::control::{connector, crtc, framebuffer, plane, Device as CtrlDevice, PageFlipFlags}; +use drm::Device; + +use raw_window_handle::{DrmDisplayHandle, DrmWindowHandle}; + +use std::collections::HashSet; +use std::num::NonZeroU32; +use std::os::unix::io::{AsFd, BorrowedFd}; +use std::rc::Rc; + +use crate::error::{SoftBufferError, SwResultExt}; + +#[derive(Debug)] +pub(crate) struct KmsDisplayImpl { + /// The underlying raw device file descriptor. + /// + /// Once rwh v0.6 support is merged, this an be made safe. Until then, + /// we use this hacky workaround, since this FD's validity is guaranteed by + /// the unsafe constructor. + fd: BorrowedFd<'static>, +} + +impl AsFd for KmsDisplayImpl { + fn as_fd(&self) -> BorrowedFd<'_> { + self.fd + } +} + +impl Device for KmsDisplayImpl {} +impl CtrlDevice for KmsDisplayImpl {} + +impl KmsDisplayImpl { + /// SAFETY: The underlying fd must not outlive the display. + pub(crate) unsafe fn new(handle: DrmDisplayHandle) -> Result { + let fd = handle.fd; + if fd == -1 { + return Err(SoftBufferError::IncompleteDisplayHandle); + } + + // SAFETY: Invariants guaranteed by the user. + let fd = unsafe { BorrowedFd::borrow_raw(fd) }; + + Ok(KmsDisplayImpl { fd }) + } +} + +/// All the necessary types for the Drm/Kms backend. +#[derive(Debug)] +pub(crate) struct KmsImpl { + /// The display implementation. + display: Rc, + + /// The connectors to use. + connectors: Vec, + + /// The CRTC to render to. + crtc: crtc::Info, + + /// The dumb buffer we're using as a buffer. + buffer: Option, +} + +#[derive(Debug)] +struct Buffers { + /// The involved set of buffers. + buffers: [SharedBuffer; 2], + + /// Whether to use the first buffer or the second buffer as the front buffer. + first_is_front: bool, + + /// A buffer full of zeroes. + zeroes: Box<[u32]>, +} + +/// The buffer implementation. +pub(crate) struct BufferImpl<'a> { + /// The mapping of the dump buffer. + mapping: DumbMapping<'a>, + + /// The framebuffer object of the current front buffer. + front_fb: framebuffer::Handle, + + /// The CRTC handle. + crtc_handle: crtc::Handle, + + /// This is used to change the front buffer. + first_is_front: &'a mut bool, + + /// Buffer full of zeroes. + zeroes: &'a [u32], + + /// The current size. + size: (NonZeroU32, NonZeroU32), + + /// The display implementation. + display: &'a KmsDisplayImpl, + + /// Age of the front buffer. + front_age: &'a mut u8, + + /// Age of the back buffer. + back_age: &'a mut u8, +} + +/// The combined frame buffer and dumb buffer. +#[derive(Debug)] +struct SharedBuffer { + /// The frame buffer. + fb: framebuffer::Handle, + + /// The dumb buffer. + db: DumbBuffer, + + /// The age of this buffer. + age: u8, +} + +impl KmsImpl { + /// Create a new KMS backend. + /// + /// # Safety + /// + /// The plane must be valid for the lifetime of the backend. + pub(crate) unsafe fn new( + window_handle: DrmWindowHandle, + display: Rc, + ) -> Result { + log::trace!("new: window_handle={:X}", window_handle.plane); + + // Make sure that the window handle is valid. + let plane_handle = match NonZeroU32::new(window_handle.plane) { + Some(handle) => plane::Handle::from(handle), + None => return Err(SoftBufferError::IncompleteWindowHandle), + }; + + let plane_info = display + .get_plane(plane_handle) + .swbuf_err("failed to get plane info")?; + let handles = display + .resource_handles() + .swbuf_err("failed to get resource handles")?; + + // Use either the attached CRTC or the primary CRTC. + let crtc = { + let handle = match plane_info.crtc() { + Some(crtc) => crtc, + None => { + log::warn!("no CRTC attached to plane, falling back to primary CRTC"); + handles + .filter_crtcs(plane_info.possible_crtcs()) + .first() + .copied() + .swbuf_err("failed to find a primary CRTC")? + } + }; + + // Get info about the CRTC. + display + .get_crtc(handle) + .swbuf_err("failed to get CRTC info")? + }; + + // Figure out all of the encoders that are attached to this CRTC. + let encoders = handles + .encoders + .iter() + .flat_map(|handle| display.get_encoder(*handle)) + .filter(|encoder| encoder.crtc() == Some(crtc.handle())) + .map(|encoder| encoder.handle()) + .collect::>(); + + // Get a list of every connector that the CRTC is connected to via encoders. + let connectors = handles + .connectors + .iter() + .flat_map(|handle| display.get_connector(*handle, false)) + .filter(|connector| { + connector + .current_encoder() + .map_or(false, |encoder| encoders.contains(&encoder)) + }) + .map(|info| info.handle()) + .collect::>(); + + Ok(Self { + crtc, + connectors, + display, + buffer: None, + }) + } + + /// Resize the internal buffer to the given size. + pub(crate) fn resize( + &mut self, + width: NonZeroU32, + height: NonZeroU32, + ) -> Result<(), SoftBufferError> { + // Don't resize if we don't have to. + if let Some(buffer) = &self.buffer { + let (buffer_width, buffer_height) = buffer.size(); + if buffer_width == width && buffer_height == height { + return Ok(()); + } + } + + // Create a new buffer set. + let front_buffer = SharedBuffer::new(&self.display, width, height)?; + let back_buffer = SharedBuffer::new(&self.display, width, height)?; + + self.buffer = Some(Buffers { + first_is_front: true, + buffers: [front_buffer, back_buffer], + zeroes: vec![0; width.get() as usize * height.get() as usize].into_boxed_slice(), + }); + + Ok(()) + } + + /// Fetch the buffer from the window. + pub(crate) fn fetch(&mut self) -> Result, SoftBufferError> { + // TODO: Implement this! + Err(SoftBufferError::Unimplemented) + } + + /// Get a mutable reference to the buffer. + pub(crate) fn buffer_mut(&mut self) -> Result, SoftBufferError> { + // Map the dumb buffer. + let set = self + .buffer + .as_mut() + .expect("Must set size of surface before calling `buffer_mut()`"); + + let size = set.size(); + + let [first_buffer, second_buffer] = &mut set.buffers; + let (front_buffer, back_buffer) = if set.first_is_front { + (first_buffer, second_buffer) + } else { + (second_buffer, first_buffer) + }; + + let front_fb = front_buffer.fb; + let front_age = &mut front_buffer.age; + let back_age = &mut back_buffer.age; + + let mapping = self + .display + .map_dumb_buffer(&mut front_buffer.db) + .swbuf_err("failed to map dumb buffer")?; + + Ok(BufferImpl { + mapping, + size, + first_is_front: &mut set.first_is_front, + front_fb, + crtc_handle: self.crtc.handle(), + display: &self.display, + zeroes: &set.zeroes, + front_age, + back_age, + }) + } +} + +impl Drop for KmsImpl { + fn drop(&mut self) { + // Map the CRTC to the information that was there before. + self.display + .set_crtc( + self.crtc.handle(), + self.crtc.framebuffer(), + self.crtc.position(), + &self.connectors, + self.crtc.mode(), + ) + .ok(); + } +} + +impl BufferImpl<'_> { + #[inline] + pub fn pixels(&self) -> &[u32] { + // drm-rs doesn't let us have the immutable reference... so just use a bunch of zeroes. + // TODO: There has to be a better way of doing this! + self.zeroes + } + + #[inline] + pub fn pixels_mut(&mut self) -> &mut [u32] { + bytemuck::cast_slice_mut(self.mapping.as_mut()) + } + + #[inline] + pub fn age(&self) -> u8 { + *self.front_age + } + + #[inline] + pub fn present_with_damage(self, damage: &[crate::Rect]) -> Result<(), SoftBufferError> { + let rectangles = damage + .iter() + .map(|&rect| { + let err = || SoftBufferError::DamageOutOfRange { rect }; + Ok(drm_sys::drm_clip_rect { + x1: rect.x.try_into().map_err(|_| err())?, + y1: rect.y.try_into().map_err(|_| err())?, + x2: rect + .x + .checked_add(rect.width.get()) + .and_then(|x| x.try_into().ok()) + .ok_or_else(err)?, + y2: rect + .y + .checked_add(rect.height.get()) + .and_then(|y| y.try_into().ok()) + .ok_or_else(err)?, + }) + }) + .collect::, _>>()?; + + // Dirty the framebuffer with out damage rectangles. + // + // Some drivers don't support this, so we just ignore the `ENOSYS` error. + // TODO: It would be nice to not have to heap-allocate the above rectangles if we know that + // this is going to fail. Low hanging fruit PR: add a flag that's set to false if this + // returns `ENOSYS` and check that before allocating the above and running this. + match self.display.dirty_framebuffer(self.front_fb, &rectangles) { + Ok(()) + | Err(drm::SystemError::Unknown { + errno: nix::errno::Errno::ENOSYS, + }) => {} + Err(e) => { + return Err(SoftBufferError::PlatformError( + Some("failed to dirty framebuffer".into()), + Some(e.into()), + )); + } + } + + // Swap the buffers. + // TODO: Use atomic commits here! + self.display + .page_flip(self.crtc_handle, self.front_fb, PageFlipFlags::EVENT, None) + .swbuf_err("failed to page flip")?; + + // Flip the front and back buffers. + *self.first_is_front = !*self.first_is_front; + + // Set the ages. + *self.front_age = 1; + if *self.back_age != 0 { + *self.back_age += 1; + } + + Ok(()) + } + + #[inline] + pub fn present(self) -> Result<(), SoftBufferError> { + let (width, height) = self.size; + self.present_with_damage(&[crate::Rect { + x: 0, + y: 0, + width, + height, + }]) + } +} + +impl SharedBuffer { + /// Create a new buffer set. + pub(crate) fn new( + display: &KmsDisplayImpl, + width: NonZeroU32, + height: NonZeroU32, + ) -> Result { + let db = display + .create_dumb_buffer((width.get(), height.get()), DrmFourcc::Xrgb8888, 32) + .swbuf_err("failed to create dumb buffer")?; + let fb = display + .add_framebuffer(&db, 24, 32) + .swbuf_err("failed to add framebuffer")?; + + Ok(SharedBuffer { fb, db, age: 0 }) + } + + /// Get the size of this buffer. + pub(crate) fn size(&self) -> (NonZeroU32, NonZeroU32) { + let (width, height) = self.db.size(); + + NonZeroU32::new(width) + .and_then(|width| NonZeroU32::new(height).map(|height| (width, height))) + .expect("buffer size is zero") + } +} + +impl Buffers { + /// Get the size of this buffer. + pub(crate) fn size(&self) -> (NonZeroU32, NonZeroU32) { + self.buffers[0].size() + } +} diff --git a/src/lib.rs b/src/lib.rs index 0c39dd1a..3f9b919c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,8 @@ extern crate core; #[cfg(target_os = "macos")] mod cg; +#[cfg(kms_platform)] +mod kms; #[cfg(target_os = "redox")] mod orbital; #[cfg(wayland_platform)] @@ -27,7 +29,7 @@ mod util; use std::marker::PhantomData; use std::num::NonZeroU32; use std::ops; -#[cfg(any(wayland_platform, x11_platform))] +#[cfg(any(wayland_platform, x11_platform, kms_platform))] use std::rc::Rc; pub use error::SoftBufferError; @@ -73,6 +75,7 @@ macro_rules! make_dispatch { } } + #[allow(clippy::large_enum_variant)] // it's boxed anyways enum SurfaceDispatch { $( $(#[$attr])* @@ -174,6 +177,8 @@ make_dispatch! { X11(Rc, x11::X11Impl, x11::BufferImpl<'a>), #[cfg(wayland_platform)] Wayland(Rc, wayland::WaylandImpl, wayland::BufferImpl<'a>), + #[cfg(kms_platform)] + Kms(Rc, kms::KmsImpl, kms::BufferImpl<'a>), #[cfg(target_os = "windows")] Win32((), win32::Win32Impl, win32::BufferImpl<'a>), #[cfg(target_os = "macos")] @@ -213,6 +218,10 @@ impl Context { RawDisplayHandle::Wayland(wayland_handle) => unsafe { ContextDispatch::Wayland(Rc::new(wayland::WaylandDisplayImpl::new(wayland_handle)?)) }, + #[cfg(kms_platform)] + RawDisplayHandle::Drm(drm_handle) => unsafe { + ContextDispatch::Kms(Rc::new(kms::KmsDisplayImpl::new(drm_handle)?)) + }, #[cfg(target_os = "windows")] RawDisplayHandle::Windows(_) => ContextDispatch::Win32(()), #[cfg(target_os = "macos")] @@ -303,6 +312,12 @@ impl Surface { ) => SurfaceDispatch::Wayland(unsafe { wayland::WaylandImpl::new(wayland_window_handle, wayland_display_impl.clone())? }), + #[cfg(kms_platform)] + (ContextDispatch::Kms(kms_display_impl), RawWindowHandle::Drm(drm_window_handle)) => { + SurfaceDispatch::Kms(unsafe { + kms::KmsImpl::new(drm_window_handle, kms_display_impl.clone())? + }) + } #[cfg(target_os = "windows")] (ContextDispatch::Win32(()), RawWindowHandle::Win32(win32_handle)) => { SurfaceDispatch::Win32(unsafe { win32::Win32Impl::new(&win32_handle)? }) @@ -361,6 +376,12 @@ impl Surface { /// Return a [`Buffer`] that the next frame should be rendered into. The size must /// be set with [`Surface::resize`] first. The initial contents of the buffer may be zeroed, or /// may contain a previous frame. Call [`Buffer::age`] to determine this. + /// + /// ## Platform Dependent Behavior + /// + /// - On DRM/KMS, there is no reliable and sound way to wait for the page flip to happen from within + /// `softbuffer`. Therefore it is the responsibility of the user to wait for the page flip before + /// sending another frame. pub fn buffer_mut(&mut self) -> Result { Ok(Buffer { buffer_impl: self.surface_impl.buffer_mut()?,