// SPDX-License-Identifier: GPL-3.0-or-later mod drm_util; use std::{ collections::{HashMap, HashSet}, ffi::OsString, path::{Path, PathBuf}, time::Duration, }; use anyhow::{anyhow, ensure, Context}; use pinnacle_api_defs::pinnacle::signal::v0alpha1::OutputConnectResponse; use smithay::{ backend::{ allocator::{ dmabuf::{AnyError, Dmabuf, DmabufAllocator}, gbm::{GbmAllocator, GbmBufferFlags, GbmDevice}, vulkan::{ImageUsageFlags, VulkanAllocator}, Allocator, Fourcc, }, drm::{ compositor::{DrmCompositor, PrimaryPlaneElement, RenderFrameResult}, gbm::GbmFramebuffer, CreateDrmNodeError, DrmDevice, DrmDeviceFd, DrmError, DrmEvent, DrmEventMetadata, DrmNode, NodeType, }, egl::{self, EGLDevice, EGLDisplay}, libinput::{LibinputInputBackend, LibinputSessionInterface}, renderer::{ self, damage, element::{self, texture::TextureBuffer, Element, RenderElement}, gles::{GlesRenderbuffer, GlesRenderer}, multigpu::{gbm::GbmGlesBackend, GpuManager, MultiRenderer, MultiTexture}, utils::CommitCounter, Bind, BufferType, ExportMem, ImportDma, ImportEgl, ImportMemWl, Offscreen, Renderer, TextureFilter, }, session::{ self, libseat::{self, LibSeatSession}, Session, }, udev::{self, UdevBackend, UdevEvent}, vulkan::{self, version::Version, PhysicalDevice}, SwapBuffersError, }, desktop::{ layer_map_for_output, utils::{send_frames_surface_tree, OutputPresentationFeedback}, }, input::pointer::CursorImageStatus, output::{Output, PhysicalProperties, Subpixel}, reexports::{ ash::vk::ExtPhysicalDeviceDrmFn, calloop::{ self, generic::Generic, EventLoop, Idle, Interest, LoopHandle, PostAction, RegistrationToken, }, drm::control::{connector, crtc, ModeTypeFlags}, gbm::BufferObject, input::Libinput, rustix::fs::OFlags, wayland_protocols::wp::{ linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1, presentation_time::server::wp_presentation_feedback, }, wayland_server::{ backend::GlobalId, protocol::{wl_shm, wl_surface::WlSurface}, Display, DisplayHandle, }, }, utils::{DeviceFd, IsAlive, Point, Rectangle, Transform}, wayland::dmabuf::{self, DmabufFeedback, DmabufFeedbackBuilder, DmabufGlobal, DmabufState}, }; use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner}; use tracing::{debug, error, warn}; use crate::{ backend::Backend, config::ConnectorSavedState, output::OutputName, render::{pointer::PointerElement, pointer_render_elements, take_presentation_feedback}, state::{State, SurfaceDmabufFeedback, WithState}, }; use self::drm_util::EdidInfo; use super::BackendData; const SUPPORTED_FORMATS: &[Fourcc] = &[ Fourcc::Abgr2101010, Fourcc::Argb2101010, Fourcc::Abgr8888, Fourcc::Argb8888, ]; const SUPPORTED_FORMATS_8BIT_ONLY: &[Fourcc] = &[Fourcc::Abgr8888, Fourcc::Argb8888]; /// A [`MultiRenderer`] that uses the [`GbmGlesBackend`]. #[allow(dead_code)] // dunno if i'm gonna need this again type UdevRenderer<'a> = MultiRenderer< 'a, 'a, GbmGlesBackend, GbmGlesBackend, >; /// Udev state attached to each [`Output`]. #[derive(Debug, PartialEq)] struct UdevOutputData { /// The GPU node device_id: DrmNode, /// The [Crtc][crtc::Handle] the output is pushing to crtc: crtc::Handle, } // TODO: document desperately pub struct Udev { pub session: LibSeatSession, display_handle: DisplayHandle, pub(super) dmabuf_state: Option<(DmabufState, DmabufGlobal)>, pub(super) primary_gpu: DrmNode, allocator: Option>>, pub(super) gpu_manager: GpuManager>, backends: HashMap, pointer_images: Vec<(xcursor::parser::Image, TextureBuffer)>, pointer_element: PointerElement, pointer_image: crate::cursor::Cursor, pub(super) upscale_filter: TextureFilter, pub(super) downscale_filter: TextureFilter, } impl Backend { fn udev(&self) -> &Udev { let Backend::Udev(udev) = self else { unreachable!() }; udev } fn udev_mut(&mut self) -> &mut Udev { let Backend::Udev(udev) = self else { unreachable!() }; udev } } impl Udev { /// Schedule a new render that will cause the compositor to redraw everything. pub fn schedule_render(&mut self, loop_handle: &LoopHandle, output: &Output) { let Some(surface) = render_surface_for_output(output, &mut self.backends) else { return; }; match &surface.render_state { RenderState::Idle => { let output = output.clone(); let token = loop_handle.insert_idle(move |state| { state.render_surface(&output); }); surface.render_state = RenderState::Scheduled(token); } RenderState::Scheduled(_) => (), RenderState::WaitingForVblank { dirty: _ } => { surface.render_state = RenderState::WaitingForVblank { dirty: true } } } } } impl State { /// Switch the tty. /// /// This will first clear the overlay plane to prevent any lingering artifacts, /// then switch the vt. /// /// Does nothing when called on the winit backend. pub fn switch_vt(&mut self, vt: i32) { if let Backend::Udev(udev) = &mut self.backend { for backend in udev.backends.values_mut() { for surface in backend.surfaces.values_mut() { // Clear the overlay planes on tty switch. // // On my machine, switching a tty would leave the topmost window on the // screen. Smithay will render the topmost window on the overlay plane, // so we clear it here. let planes = surface.compositor.surface().planes().clone(); tracing::debug!("Clearing overlay planes"); for overlay_plane in planes.overlay { if let Err(err) = surface .compositor .surface() .clear_plane(overlay_plane.handle) { warn!("Failed to clear overlay planes: {err}"); } } } } // Wait for the clear to commit before switching self.schedule( |state| { let udev = state.backend.udev(); !udev .backends .values() .flat_map(|backend| backend.surfaces.values()) .map(|surface| surface.compositor.surface()) .any(|drm_surf| drm_surf.commit_pending()) }, move |state| { let udev = state.backend.udev_mut(); if let Err(err) = udev.session.change_vt(vt) { error!("Failed to switch to vt {vt}: {err}"); } }, ); } } /// Resize the output with the given mode. /// /// TODO: This is in udev.rs but is also used in winit.rs. /// | I've got no clue how to make things public without making a mess. pub fn resize_output(&mut self, output: &Output, mode: smithay::output::Mode) { if let Backend::Udev(udev) = &mut self.backend { let drm_mode = udev.backends.iter().find_map(|(_, backend)| { backend .drm_scanner .crtcs() .find(|(_, handle)| { output .user_data() .get::() .is_some_and(|data| &data.crtc == handle) }) .and_then(|(info, _)| { info.modes() .iter() .find(|m| smithay::output::Mode::from(**m) == mode) }) .copied() }); if let Some(drm_mode) = drm_mode { if let Some(render_surface) = render_surface_for_output(output, &mut udev.backends) { match render_surface.compositor.use_mode(drm_mode) { Ok(()) => { output.change_current_state(Some(mode), None, None, None); layer_map_for_output(output).arrange(); } Err(err) => error!("Failed to resize output: {err}"), } } } } else { output.change_current_state(Some(mode), None, None, None); layer_map_for_output(output).arrange(); } self.schedule_render(output); self.request_layout(output); } } impl BackendData for Udev { fn seat_name(&self) -> String { self.session.seat() } fn reset_buffers(&mut self, output: &Output) { if let Some(id) = output.user_data().get::() { if let Some(gpu) = self.backends.get_mut(&id.device_id) { if let Some(surface) = gpu.surfaces.get_mut(&id.crtc) { surface.compositor.reset_buffers(); } } } } fn early_import(&mut self, surface: &WlSurface) { if let Err(err) = self.gpu_manager.early_import(self.primary_gpu, surface) { warn!("early buffer import failed: {}", err); } } } pub fn setup_udev( no_config: bool, config_dir: Option, ) -> anyhow::Result<(State, EventLoop<'static, State>)> { let event_loop = EventLoop::try_new()?; let display = Display::new()?; // Initialize session let (session, notifier) = LibSeatSession::new()?; // Get the primary gpu let primary_gpu = udev::primary_gpu(session.seat()) .context("unable to get primary gpu path")? .and_then(|x| { DrmNode::from_path(x) .ok()? .node_with_type(NodeType::Render)? .ok() }) .unwrap_or_else(|| { udev::all_gpus(session.seat()) .expect("failed to get gpu paths") .into_iter() .find_map(|x| DrmNode::from_path(x).ok()) .expect("No GPU!") }); tracing::info!("Using {} as primary gpu.", primary_gpu); let gpu_manager = GpuManager::new(GbmGlesBackend::default())?; let data = Udev { display_handle: display.handle(), dmabuf_state: None, session, primary_gpu, gpu_manager, allocator: None, backends: HashMap::new(), pointer_image: crate::cursor::Cursor::load(), pointer_images: Vec::new(), pointer_element: PointerElement::default(), upscale_filter: TextureFilter::Linear, downscale_filter: TextureFilter::Linear, }; let display_handle = display.handle(); let mut state = State::init( Backend::Udev(data), display, event_loop.get_signal(), event_loop.handle(), no_config, config_dir, )?; // Initialize the udev backend let udev_backend = UdevBackend::new(state.seat.name())?; // Create DrmNodes from already connected GPUs for (device_id, path) in udev_backend.device_list() { if let Err(err) = DrmNode::from_dev_id(device_id) .map_err(DeviceAddError::DrmNode) .and_then(|node| state.device_added(node, path)) { error!("Skipping device {device_id}: {err}"); } } let udev = state.backend.udev_mut(); event_loop .handle() .insert_source(udev_backend, move |event, _, state| match event { // GPU connected UdevEvent::Added { device_id, path } => { if let Err(err) = DrmNode::from_dev_id(device_id) .map_err(DeviceAddError::DrmNode) .and_then(|node| state.device_added(node, &path)) { error!("Skipping device {device_id}: {err}"); } } UdevEvent::Changed { device_id } => { if let Ok(node) = DrmNode::from_dev_id(device_id) { state.device_changed(node) } } // GPU disconnected UdevEvent::Removed { device_id } => { if let Ok(node) = DrmNode::from_dev_id(device_id) { state.device_removed(node) } } }) .expect("failed to insert udev_backend into event loop"); // Initialize libinput backend let mut libinput_context = Libinput::new_with_udev::>( udev.session.clone().into(), ); libinput_context .udev_assign_seat(state.seat.name()) .expect("failed to assign seat to libinput"); let libinput_backend = LibinputInputBackend::new(libinput_context.clone()); // Bind all our objects that get driven by the event loop let insert_ret = event_loop .handle() .insert_source(libinput_backend, move |event, _, state| { state.apply_libinput_settings(&event); state.process_input_event(event); }); if let Err(err) = insert_ret { anyhow::bail!("Failed to insert libinput_backend into event loop: {err}"); } event_loop .handle() .insert_source(notifier, move |event, _, state| { let udev = state.backend.udev_mut(); match event { session::Event::PauseSession => { libinput_context.suspend(); tracing::info!("pausing session"); for backend in udev.backends.values_mut() { backend.drm.pause(); } } session::Event::ActivateSession => { tracing::info!("resuming session"); if let Err(err) = libinput_context.resume() { error!("Failed to resume libinput context: {:?}", err); } for backend in udev.backends.values_mut() { // TODO: this is false because i'm too lazy to remove the code directly // | below it backend.drm.activate(false).expect("failed to activate drm"); for surface in backend.surfaces.values_mut() { if let Err(err) = surface.compositor.surface().reset_state() { warn!("Failed to reset drm surface state: {}", err); } // reset the buffers after resume to trigger a full redraw // this is important after a vt switch as the primary plane // has no content and damage tracking may prevent a redraw // otherwise surface.compositor.reset_buffers(); } } let connectors = udev .backends .iter() .map(|(node, backend)| { let connectors = backend .drm_scanner .crtcs() .map(|(info, crtc)| (info.clone(), crtc)) .collect::>(); (*node, connectors) }) .collect::>(); for (node, connectors) in connectors { for (connector, crtc) in connectors { state.connector_disconnected(node, connector.clone(), crtc); state.connector_connected(node, connector, crtc); } } for output in state.space.outputs().cloned().collect::>() { state.schedule_render(&output); } } } }) .expect("failed to insert libinput notifier into event loop"); state.shm_state.update_formats( udev.gpu_manager .single_renderer(&primary_gpu)? .shm_formats(), ); // Create the Vulkan allocator if let Ok(instance) = vulkan::Instance::new(Version::VERSION_1_2, None) { if let Some(physical_device) = PhysicalDevice::enumerate(&instance) .ok() .and_then(|devices| { devices .filter(|phd| phd.has_device_extension(ExtPhysicalDeviceDrmFn::name())) .find(|phd| { phd.primary_node() .is_ok_and(|node| node == Some(primary_gpu)) || phd .render_node() .is_ok_and(|node| node == Some(primary_gpu)) }) }) { match VulkanAllocator::new( &physical_device, ImageUsageFlags::COLOR_ATTACHMENT | ImageUsageFlags::SAMPLED, ) { Ok(allocator) => { udev.allocator = Some(Box::new(DmabufAllocator(allocator)) as Box>); } Err(err) => { warn!("Failed to create vulkan allocator: {}", err); } } } } if udev.allocator.is_none() { tracing::info!("No vulkan allocator found, using GBM."); let gbm = udev .backends .get(&primary_gpu) // If the primary_gpu failed to initialize, we likely have a kmsro device .or_else(|| udev.backends.values().next()) // Don't fail, if there is no allocator. There is a chance, that this a single gpu system and we don't need one. .map(|backend| backend.gbm.clone()); udev.allocator = gbm.map(|gbm| { Box::new(DmabufAllocator(GbmAllocator::new( gbm, GbmBufferFlags::RENDERING, ))) as Box<_> }); } let mut renderer = udev.gpu_manager.single_renderer(&primary_gpu)?; tracing::info!( ?primary_gpu, "Trying to initialize EGL Hardware Acceleration", ); match renderer.bind_wl_display(&display_handle) { Ok(_) => tracing::info!("EGL hardware-acceleration enabled"), Err(err) => error!(?err, "Failed to initialize EGL hardware-acceleration"), } // init dmabuf support with format list from our primary gpu let dmabuf_formats = renderer.dmabuf_formats().collect::>(); let default_feedback = DmabufFeedbackBuilder::new(primary_gpu.dev_id(), dmabuf_formats) .build() .expect("failed to create dmabuf feedback"); let mut dmabuf_state = DmabufState::new(); let global = dmabuf_state .create_global_with_default_feedback::(&display_handle, &default_feedback); udev.dmabuf_state = Some((dmabuf_state, global)); let gpu_manager = &mut udev.gpu_manager; udev.backends.values_mut().for_each(|backend_data| { // Update the per drm surface dmabuf feedback backend_data.surfaces.values_mut().for_each(|surface_data| { surface_data.dmabuf_feedback = surface_data.dmabuf_feedback.take().or_else(|| { get_surface_dmabuf_feedback( primary_gpu, surface_data.render_node, gpu_manager, &surface_data.compositor, ) }); }); }); if let Err(err) = state.xwayland.start( state.loop_handle.clone(), None, std::iter::empty::<(OsString, OsString)>(), true, |_| {}, ) { error!("Failed to start XWayland: {err}"); } Ok((state, event_loop)) } // TODO: document desperately struct UdevBackendData { surfaces: HashMap, gbm: GbmDevice, drm: DrmDevice, drm_scanner: DrmScanner, render_node: DrmNode, registration_token: RegistrationToken, } #[derive(Debug, thiserror::Error)] enum DeviceAddError { #[error("Failed to open device using libseat: {0}")] DeviceOpen(libseat::Error), #[error("Failed to initialize drm device: {0}")] DrmDevice(DrmError), #[error("Failed to initialize gbm device: {0}")] GbmDevice(std::io::Error), #[error("Failed to access drm node: {0}")] DrmNode(CreateDrmNodeError), #[error("Failed to add device to GpuManager: {0}")] AddNode(egl::Error), } fn get_surface_dmabuf_feedback( primary_gpu: DrmNode, render_node: DrmNode, gpu_manager: &mut GpuManager>, composition: &GbmDrmCompositor, ) -> Option { let primary_formats = gpu_manager .single_renderer(&primary_gpu) .ok()? .dmabuf_formats() .collect::>(); let render_formats = gpu_manager .single_renderer(&render_node) .ok()? .dmabuf_formats() .collect::>(); let all_render_formats = primary_formats .iter() .chain(render_formats.iter()) .copied() .collect::>(); let surface = composition.surface(); let planes = surface.planes().clone(); // We limit the scan-out trache to formats we can also render from // so that there is always a fallback render path available in case // the supplied buffer can not be scanned out directly let planes_formats = planes .primary .formats .into_iter() .chain(planes.overlay.into_iter().flat_map(|p| p.formats)) .collect::>() .intersection(&all_render_formats) .copied() .collect::>(); let builder = DmabufFeedbackBuilder::new(primary_gpu.dev_id(), primary_formats); let render_feedback = builder .clone() .add_preference_tranche(render_node.dev_id(), None, render_formats.clone()) .build() .ok()?; // INFO: this is an unwrap in Anvil, does it matter? let scanout_feedback = builder .add_preference_tranche( surface.device_fd().dev_id().ok()?, // INFO: this is an unwrap in Anvil, does it matter? Some(zwp_linux_dmabuf_feedback_v1::TrancheFlags::Scanout), planes_formats, ) .add_preference_tranche(render_node.dev_id(), None, render_formats) .build() .ok()?; // INFO: this is an unwrap in Anvil, does it matter? Some(DrmSurfaceDmabufFeedback { render_feedback, scanout_feedback, }) } struct DrmSurfaceDmabufFeedback { render_feedback: DmabufFeedback, scanout_feedback: DmabufFeedback, } /// The state of a [`RenderSurface`]. #[derive(Debug)] enum RenderState { /// No render is scheduled. Idle, // TODO: remove the token on tty switch or output unplug /// A render has been queued. Scheduled( /// The idle token from a render being scheduled. /// This is used to cancel renders if, for example, /// the output being rendered is removed. #[allow(dead_code)] // TODO: Idle<'static>, ), /// A frame was rendered and scheduled and we are waiting for vblank. WaitingForVblank { /// A render was scheduled while waiting for vblank. /// In this case, another render will be scheduled once vblank happens. dirty: bool, }, } /// Render surface for an output. struct RenderSurface { /// The output global id. global: Option, /// A display handle used to remove the global on drop. display_handle: DisplayHandle, /// The node from `connector_connected`. device_id: DrmNode, /// The node rendering to the screen? idk /// /// If this is equal to the primary gpu node then it does the rendering operations. /// If it's not it is the node the composited buffer ends up on. render_node: DrmNode, /// The thing rendering elements and queueing frames. compositor: GbmDrmCompositor, dmabuf_feedback: Option, render_state: RenderState, primary_plane_element_swapchain_commit_counter: CommitCounter, primary_plane_element_element_commit_counter: CommitCounter, } impl Drop for RenderSurface { // Stop advertising this output to clients on drop. fn drop(&mut self) { if let Some(global) = self.global.take() { self.display_handle.remove_global::(global); } } } type GbmDrmCompositor = DrmCompositor< GbmAllocator, GbmDevice, Option, DrmDeviceFd, >; /// Render a frame with the given elements. /// /// This frame needs to be queued for scanout afterwards. fn render_frame<'a, R, E>( compositor: &mut GbmDrmCompositor, renderer: &mut R, elements: &'a [E], clear_color: [f32; 4], ) -> Result, GbmFramebuffer, E>, SwapBuffersError> where R: Renderer + Bind, ::TextureId: 'static, ::Error: Into, E: RenderElement, { use smithay::backend::drm::compositor::RenderFrameError; compositor .render_frame(renderer, elements, clear_color) .map_err(|err| match err { RenderFrameError::PrepareFrame(err) => err.into(), RenderFrameError::RenderFrame(damage::Error::Rendering(err)) => err.into(), _ => unreachable!(), }) } impl State { /// A GPU was plugged in. fn device_added(&mut self, node: DrmNode, path: &Path) -> Result<(), DeviceAddError> { let udev = self.backend.udev_mut(); // Try to open the device let fd = udev .session .open( path, OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK, ) .map_err(DeviceAddError::DeviceOpen)?; let fd = DrmDeviceFd::new(DeviceFd::from(fd)); let (drm, notifier) = DrmDevice::new(fd.clone(), true).map_err(DeviceAddError::DrmDevice)?; let gbm = GbmDevice::new(fd).map_err(DeviceAddError::GbmDevice)?; let registration_token = self .loop_handle .insert_source(notifier, move |event, metadata, state| match event { DrmEvent::VBlank(crtc) => { state.on_vblank(node, crtc, metadata); } DrmEvent::Error(error) => { error!("{:?}", error); } }) .expect("failed to insert drm notifier into event loop"); // SAFETY: no clue lol just copied this from anvil let render_node = EGLDevice::device_for_display(&unsafe { EGLDisplay::new(gbm.clone()).expect("failed to create EGLDisplay") }) .ok() .and_then(|x| x.try_get_render_node().ok().flatten()) .unwrap_or(node); udev.gpu_manager .as_mut() .add_node(render_node, gbm.clone()) .map_err(DeviceAddError::AddNode)?; udev.backends.insert( node, UdevBackendData { registration_token, gbm, drm, drm_scanner: DrmScanner::new(), render_node, surfaces: HashMap::new(), }, ); self.device_changed(node); Ok(()) } /// A display was plugged in. fn connector_connected( &mut self, node: DrmNode, connector: connector::Info, crtc: crtc::Handle, ) { let udev = self.backend.udev_mut(); let device = if let Some(device) = udev.backends.get_mut(&node) { device } else { return; }; let mut renderer = udev .gpu_manager .single_renderer(&device.render_node) .expect("failed to get primary gpu MultiRenderer"); let render_formats = renderer .as_mut() .egl_context() .dmabuf_render_formats() .clone(); tracing::info!( ?crtc, "Trying to setup connector {:?}-{}", connector.interface(), connector.interface_id(), ); let mode_id = connector .modes() .iter() .position(|mode| mode.mode_type().contains(ModeTypeFlags::PREFERRED)) .unwrap_or(0); let drm_mode = connector.modes()[mode_id]; let wl_mode = smithay::output::Mode::from(drm_mode); let modes = connector .modes() .iter() .map(|mode| smithay::output::Mode::from(*mode)); let surface = match device .drm .create_surface(crtc, drm_mode, &[connector.handle()]) { Ok(surface) => surface, Err(err) => { warn!("Failed to create drm surface: {}", err); return; } }; let output_name = format!( "{}-{}", connector.interface().as_str(), connector.interface_id() ); let (make, model) = EdidInfo::try_from_connector(&device.drm, connector.handle()) .map(|info| (info.manufacturer, info.model)) .unwrap_or_else(|err| { warn!("Failed to parse EDID info: {err}"); ("Unknown".into(), "Unknown".into()) }); let (phys_w, phys_h) = connector.size().unwrap_or((0, 0)); if self.space.outputs().any(|op| { op.user_data() .get::() .is_some_and(|op_id| op_id.crtc == crtc) }) { return; } let output = Output::new( output_name, PhysicalProperties { size: (phys_w as i32, phys_h as i32).into(), subpixel: Subpixel::from(connector.subpixel()), make, model, }, ); let global = output.create_global::(&udev.display_handle); for mode in modes { output.add_mode(mode); } self.output_focus_stack.set_focus(output.clone()); let x = self.space.outputs().fold(0, |acc, o| { let Some(geo) = self.space.output_geometry(o) else { unreachable!() }; acc + geo.size.w }); let position = (x, 0).into(); output.set_preferred(wl_mode); output.change_current_state(Some(wl_mode), None, None, Some(position)); self.space.map_output(&output, position); output.user_data().insert_if_missing(|| UdevOutputData { crtc, device_id: node, }); let allocator = GbmAllocator::new( device.gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT, ); // I like how this is still in here let color_formats = if std::env::var("ANVIL_DISABLE_10BIT").is_ok() { SUPPORTED_FORMATS_8BIT_ONLY } else { SUPPORTED_FORMATS }; let compositor = { let mut planes = surface.planes().clone(); // INFO: We are disabling overlay planes because it seems that any elements on // | overlay planes don't get up/downscaled according to the set filter; // | it always defaults to linear. planes.overlay.clear(); match DrmCompositor::new( &output, surface, Some(planes), allocator, device.gbm.clone(), color_formats, render_formats, device.drm.cursor_size(), Some(device.gbm.clone()), ) { Ok(compositor) => compositor, Err(err) => { warn!("Failed to create drm compositor: {}", err); return; } } }; let dmabuf_feedback = get_surface_dmabuf_feedback( udev.primary_gpu, device.render_node, &mut udev.gpu_manager, &compositor, ); let surface = RenderSurface { display_handle: udev.display_handle.clone(), device_id: node, render_node: device.render_node, global: Some(global), compositor, dmabuf_feedback, render_state: RenderState::Idle, primary_plane_element_swapchain_commit_counter: CommitCounter::default(), primary_plane_element_element_commit_counter: CommitCounter::default(), }; device.surfaces.insert(crtc, surface); // If there is saved connector state, the connector was previously plugged in. // In this case, restore its tags and location. // TODO: instead of checking the connector, check the monitor's edid info instead if let Some(saved_state) = self .config .connector_saved_states .get(&OutputName(output.name())) { let ConnectorSavedState { loc, tags, scale } = saved_state; output.change_current_state(None, None, *scale, Some(*loc)); self.space.map_output(&output, *loc); output.with_state_mut(|state| state.tags = tags.clone()); } else { self.signal_state.output_connect.signal(|buffer| { buffer.push_back(OutputConnectResponse { output_name: Some(output.name()), }) }); } } /// A display was unplugged. fn connector_disconnected( &mut self, node: DrmNode, _connector: connector::Info, crtc: crtc::Handle, ) { tracing::debug!(?crtc, "connector_disconnected"); let udev = self.backend.udev_mut(); let device = if let Some(device) = udev.backends.get_mut(&node) { device } else { return; }; device.surfaces.remove(&crtc); let output = self .space .outputs() .find(|o| { o.user_data() .get::() .map(|id| id.device_id == node && id.crtc == crtc) .unwrap_or(false) }) .cloned(); if let Some(output) = output { // Save this output's state. It will be restored if the monitor gets replugged. self.config.connector_saved_states.insert( OutputName(output.name()), ConnectorSavedState { loc: output.current_location(), tags: output.with_state(|state| state.tags.clone()), scale: Some(output.current_scale()), }, ); self.space.unmap_output(&output); } } fn device_changed(&mut self, node: DrmNode) { let udev = self.backend.udev_mut(); let device = if let Some(device) = udev.backends.get_mut(&node) { device } else { return; }; for event in device.drm_scanner.scan_connectors(&device.drm) { match event { DrmScanEvent::Connected { connector, crtc: Some(crtc), } => { self.connector_connected(node, connector, crtc); } DrmScanEvent::Disconnected { connector, crtc: Some(crtc), } => { self.connector_disconnected(node, connector, crtc); } _ => {} } } } /// A GPU was unplugged. fn device_removed(&mut self, node: DrmNode) { let crtcs = { let udev = self.backend.udev(); let Some(device) = udev.backends.get(&node) else { return; }; device .drm_scanner .crtcs() .map(|(info, crtc)| (info.clone(), crtc)) .collect::>() }; for (connector, crtc) in crtcs { self.connector_disconnected(node, connector, crtc); } tracing::debug!("Surfaces dropped"); let udev = self.backend.udev_mut(); // drop the backends on this side if let Some(backend_data) = udev.backends.remove(&node) { udev.gpu_manager .as_mut() .remove_node(&backend_data.render_node); self.loop_handle.remove(backend_data.registration_token); tracing::debug!("Dropping device"); } } /// Mark [`OutputPresentationFeedback`]s as presented and schedule a new render on idle. fn on_vblank( &mut self, dev_id: DrmNode, crtc: crtc::Handle, metadata: &mut Option, ) { let udev = self.backend.udev_mut(); let Some(surface) = udev .backends .get_mut(&dev_id) .and_then(|device| device.surfaces.get_mut(&crtc)) else { return; }; let output = if let Some(output) = self.space.outputs().find(|o| { let udev_op_data = o.user_data().get::(); udev_op_data .is_some_and(|data| data.device_id == surface.device_id && data.crtc == crtc) }) { output.clone() } else { // somehow we got called with an invalid output return; }; match surface .compositor .frame_submitted() .map_err(SwapBuffersError::from) { Ok(user_data) => { if let Some(mut feedback) = user_data.flatten() { let tp = metadata.as_ref().and_then(|metadata| match metadata.time { smithay::backend::drm::DrmEventTime::Monotonic(tp) => Some(tp), smithay::backend::drm::DrmEventTime::Realtime(_) => None, }); let seq = metadata .as_ref() .map(|metadata| metadata.sequence) .unwrap_or(0); let (clock, flags) = if let Some(tp) = tp { ( tp.into(), wp_presentation_feedback::Kind::Vsync | wp_presentation_feedback::Kind::HwClock | wp_presentation_feedback::Kind::HwCompletion, ) } else { (self.clock.now(), wp_presentation_feedback::Kind::Vsync) }; feedback.presented( clock, output .current_mode() .map(|mode| Duration::from_secs_f64(1000f64 / mode.refresh as f64)) .unwrap_or_default(), seq as u64, flags, ); } } Err(err) => { warn!("Error during rendering: {:?}", err); if let SwapBuffersError::ContextLost(err) = err { panic!("Rendering loop lost: {}", err) } } }; let RenderState::WaitingForVblank { dirty } = surface.render_state else { unreachable!(); }; surface.render_state = RenderState::Idle; if dirty { self.schedule_render(&output); } else { for window in self.windows.iter() { window.send_frame(&output, self.clock.now(), Some(Duration::ZERO), |_, _| { Some(output.clone()) }); } } } /// Render to the [`RenderSurface`] associated with the given `output`. #[tracing::instrument(level = "debug", skip(self), fields(output = output.name()))] fn render_surface(&mut self, output: &Output) { let udev = self.backend.udev_mut(); let Some(surface) = render_surface_for_output(output, &mut udev.backends) else { return; }; assert!(matches!(surface.render_state, RenderState::Scheduled(_))); // TODO get scale from the rendersurface when supporting HiDPI let frame = udev.pointer_image.get_image( 1, // output.current_scale().integer_scale() as u32, self.clock.now().into(), ); let render_node = surface.render_node; let primary_gpu = udev.primary_gpu; let mut renderer = if primary_gpu == render_node { udev.gpu_manager.single_renderer(&render_node) } else { let format = surface.compositor.format(); udev.gpu_manager .renderer(&primary_gpu, &render_node, format) } .expect("failed to create MultiRenderer"); let _ = renderer.upscale_filter(udev.upscale_filter); let _ = renderer.downscale_filter(udev.downscale_filter); let pointer_images = &mut udev.pointer_images; let pointer_image = pointer_images .iter() .find_map( |(image, texture)| { if image == &frame { Some(texture.clone()) } else { None } }, ) .unwrap_or_else(|| { let texture = TextureBuffer::from_memory( &mut renderer, &frame.pixels_rgba, Fourcc::Abgr8888, (frame.width as i32, frame.height as i32), false, 1, Transform::Normal, None, ) .expect("Failed to import cursor bitmap"); pointer_images.push((frame, texture.clone())); texture }); let windows = self.space.elements().cloned().collect::>(); let pointer_location = self .seat .get_pointer() .map(|ptr| ptr.current_location()) .unwrap_or((0.0, 0.0).into()); // set cursor udev.pointer_element.set_texture(pointer_image.clone()); // draw the cursor as relevant and // reset the cursor if the surface is no longer alive if let CursorImageStatus::Surface(surface) = &self.cursor_status { if !surface.alive() { self.cursor_status = CursorImageStatus::default_named(); } else { send_frames_surface_tree( surface, output, self.clock.now(), Some(Duration::ZERO), |_, _| None, ); } } udev.pointer_element.set_status(self.cursor_status.clone()); let pending_screencopy_with_cursor = output.with_state(|state| state.screencopy.as_ref().map(|sc| sc.overlay_cursor())); let mut output_render_elements = Vec::new(); // If there isn't a pending screencopy that doesn't want to overlay the cursor, // render it. // // This will cause the cursor to disappear for a frame if there is one though, // but it shouldn't meaningfully affect anything. match pending_screencopy_with_cursor { Some(include_cursor) => { if include_cursor { // HACK: Doing `RenderFrameResult::blit_frame_result` with something on the // | cursor plane causes the cursor to overwrite the pixels underneath it, // | leading to a transparent hole under the cursor. // | To circumvent that, we set the cursor to render on the primary plane instead. udev.pointer_element .set_element_kind(element::Kind::Unspecified); let pointer_render_elements = pointer_render_elements( output, &mut renderer, &self.space, pointer_location, &mut self.cursor_status, self.dnd_icon.as_ref(), &udev.pointer_element, ); udev.pointer_element.set_element_kind(element::Kind::Cursor); output_render_elements.extend(pointer_render_elements); } } None => { let pointer_render_elements = pointer_render_elements( output, &mut renderer, &self.space, pointer_location, &mut self.cursor_status, self.dnd_icon.as_ref(), &udev.pointer_element, ); output_render_elements.extend(pointer_render_elements); } } output_render_elements.extend(crate::render::generate_render_elements( output, &mut renderer, &self.space, &windows, )); let result = (|| -> Result { let render_frame_result = render_frame( &mut surface.compositor, &mut renderer, &output_render_elements, [0.6, 0.6, 0.6, 1.0], )?; if let PrimaryPlaneElement::Swapchain(element) = &render_frame_result.primary_element { element.sync.wait(); } // Compute damage // // Also, I'm pretty sure that `RenderFrameResult` used to give us the damage, didn't // it? But it was removed in favor of the `is_empty` bool let damage = match &render_frame_result.primary_element { PrimaryPlaneElement::Swapchain(element) => { let damage = element .damage .damage_since(Some(surface.primary_plane_element_swapchain_commit_counter)); surface.primary_plane_element_swapchain_commit_counter = element.damage.current_commit(); damage.map(|dmg| { dmg.into_iter() .map(|rect| { rect.to_logical(1, Transform::Normal, &rect.size) .to_physical(1) }) .collect() }) } PrimaryPlaneElement::Element(element) => { let damage = element.damage_since( smithay::utils::Scale::from(output.current_scale().fractional_scale()), Some(surface.primary_plane_element_swapchain_commit_counter), ); surface.primary_plane_element_element_commit_counter = element.current_commit(); Some(damage) } }; let mut damage = damage.unwrap_or_else(|| { vec![Rectangle::from_loc_and_size( Point::from((0, 0)), output.current_mode().unwrap().size, // TODO: transform )] }); // The primary plane had no damage but something got rendered, so it must be the cursor // // We currently have overlay planes disabled, so we don't have to worry about that. if damage.is_empty() && !render_frame_result.is_empty { if let Some(cursor_elem) = render_frame_result.cursor_element { damage.push(cursor_elem.geometry(smithay::utils::Scale::from( output.current_scale().fractional_scale(), ))); } } if let Some(mut screencopy) = output.with_state_mut(|state| state.screencopy.take()) { 'screencopy: { assert!(screencopy.output() == output); if screencopy.with_damage() { tracing::info!("WITH DAMAGE"); if damage.is_empty() { output.with_state_mut(|state| state.screencopy.replace(screencopy)); break 'screencopy; } screencopy.damage(&damage); } let sync_fd = if let Ok(dmabuf) = dmabuf::get_dmabuf(screencopy.buffer()) { debug!("Dmabuf screencopy"); renderer.bind(dmabuf).unwrap(); render_frame_result .blit_frame_result( screencopy.physical_region().size, Transform::Normal, output.current_scale().fractional_scale(), &mut renderer, if screencopy.with_damage() { damage } else { vec![screencopy.physical_region()] }, [], ) .map(|sync| sync.export()) .map_err(|err| anyhow!("{err}")) } else if !matches!( renderer::buffer_type(screencopy.buffer()), Some(BufferType::Shm) ) { Err(anyhow!("not a shm buffer")) } else { debug!("Shm screencopy"); let res = smithay::wayland::shm::with_buffer_contents_mut( &screencopy.buffer().clone(), |shm_ptr, shm_len, buffer_data| { // yoinked from Niri (thanks yall) ensure!( // The buffer prefers pixels in little endian ... buffer_data.format == wl_shm::Format::Argb8888 && buffer_data.stride == screencopy.physical_region().size.w * 4 && buffer_data.height == screencopy.physical_region().size.h && shm_len as i32 == buffer_data.stride * buffer_data.height, "invalid buffer format or size" ); let buffer_rect = screencopy.physical_region().to_logical(1).to_buffer( 1, Transform::Normal, &screencopy.physical_region().size.to_logical(1), ); let offscreen: GlesRenderbuffer = renderer.create_buffer( smithay::backend::allocator::Fourcc::Argb8888, buffer_rect.size, )?; renderer.bind(offscreen)?; let sync_point = render_frame_result.blit_frame_result( screencopy.physical_region().size, Transform::Normal, output.current_scale().fractional_scale(), &mut renderer, if screencopy.with_damage() { damage } else { vec![screencopy.physical_region()] }, [], )?; let mapping = renderer.copy_framebuffer( Rectangle::from_loc_and_size( Point::from((0, 0)), buffer_rect.size, ), smithay::backend::allocator::Fourcc::Argb8888, )?; let bytes = renderer.map_texture(&mapping)?; ensure!(bytes.len() == shm_len, "mapped buffer has wrong length"); // SAFETY: TODO: safety docs unsafe { std::ptr::copy_nonoverlapping(bytes.as_ptr(), shm_ptr, shm_len); } Ok(sync_point.export()) }, ); res.unwrap() }; match sync_fd { Ok(Some(sync_fd)) => { let mut screencopy = Some(screencopy); let source = Generic::new(sync_fd, Interest::READ, calloop::Mode::OneShot); let res = self.loop_handle.insert_source(source, move |_, _, _| { let Some(screencopy) = screencopy.take() else { unreachable!("This source is removed after one run"); }; screencopy.submit(false); Ok(PostAction::Remove) }); if res.is_err() { error!("Failed to schedule screencopy submission"); } } Ok(None) => { screencopy.submit(false); } Err(err) => error!("Failed to submit screencopy: {err}"), } } } let time = self.clock.now(); super::post_repaint( output, &render_frame_result.states, &self.space, surface .dmabuf_feedback .as_ref() .map(|feedback| SurfaceDmabufFeedback { render_feedback: &feedback.render_feedback, scanout_feedback: &feedback.scanout_feedback, }), time.into(), &self.cursor_status, ); let rendered = !render_frame_result.is_empty; if rendered { let output_presentation_feedback = take_presentation_feedback(output, &self.space, &render_frame_result.states); surface .compositor .queue_frame(Some(output_presentation_feedback)) .map_err(SwapBuffersError::from)?; } Ok(rendered) })(); match result { Ok(true) => surface.render_state = RenderState::WaitingForVblank { dirty: false }, Ok(false) | Err(_) => surface.render_state = RenderState::Idle, } } } fn render_surface_for_output<'a>( output: &Output, backends: &'a mut HashMap, ) -> Option<&'a mut RenderSurface> { let UdevOutputData { device_id, crtc } = output.user_data().get()?; backends .get_mut(device_id) .and_then(|device| device.surfaces.get_mut(crtc)) }