// SPDX-License-Identifier: GPL-3.0-or-later mod drm_util; mod gamma; 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, Buffer, 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, surface::WaylandSurfaceRenderElement, texture::TextureBuffer, Element, }, gles::{GlesRenderbuffer, GlesRenderer}, multigpu::{gbm::GbmGlesBackend, GpuManager, MultiRenderer, MultiTexture}, sync::SyncPoint, utils::CommitCounter, Bind, Blit, 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, Dispatcher, 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}, shm::shm_format_to_fourcc, }, }; use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner}; use tracing::{debug, error, info, trace, warn}; use crate::{ backend::Backend, config::ConnectorSavedState, output::OutputName, render::{ pointer::PointerElement, pointer_render_elements, take_presentation_feedback, OutputRenderElement, }, 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`]. type UdevRenderer<'a> = MultiRenderer< 'a, 'a, GbmGlesBackend, GbmGlesBackend, >; type UdevRenderFrameResult<'a> = RenderFrameResult< 'a, BufferObject<()>, GbmFramebuffer, OutputRenderElement, WaylandSurfaceRenderElement>>, >; /// 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, udev_dispatcher: Dispatcher<'static, UdevBackend, State>, 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 { if let Err(err) = udev.session.change_vt(vt) { error!("Failed to switch to vt {vt}: {err}"); } // TODO: uncomment this when `RenderFrameResult::blit_frame_result` is fixed for // | overlay/cursor planes // 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!") }); info!("Using {} as primary gpu.", primary_gpu); let gpu_manager = GpuManager::new(GbmGlesBackend::default())?; // let gpu_manager = GpuManager::new(GbmGlesBackend::with_factory(|egl| { // let ctx = EGLContext::new(egl)?; // let mut supported = unsafe { GlesRenderer::supported_capabilities(&ctx) }?; // supported.retain(|cap| cap != &Capability::ColorTransformations); // Ok(unsafe { GlesRenderer::with_capabilities(ctx, supported) }?) // }))?; // Initialize the udev backend let udev_backend = UdevBackend::new(session.seat())?; let udev_dispatcher = Dispatcher::new( udev_backend, move |event, _, state: &mut 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) } } }, ); event_loop .handle() .register_dispatcher(udev_dispatcher.clone())?; let data = Udev { display_handle: display.handle(), udev_dispatcher, 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, )?; let things = state .backend .udev() .udev_dispatcher .as_source_ref() .device_list() .map(|(id, path)| (id, path.to_path_buf())) .collect::>(); // Create DrmNodes from already connected GPUs for (device_id, path) in things { 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(); // 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| { match event { session::Event::PauseSession => { let udev = state.backend.udev_mut(); libinput_context.suspend(); info!("pausing session"); for backend in udev.backends.values_mut() { backend.drm.pause(); } } session::Event::ActivateSession => { info!("resuming session"); if libinput_context.resume().is_err() { error!("Failed to resume libinput context"); } // TODO: All this dance around borrowing is a consequence of the fact that I haven't // | split the State struct into a main State and a substruct like // | Niri and cosmic-comp have done let (mut device_list, connected_devices, disconnected_devices) = { let udev = state.backend.udev(); let device_list = udev .udev_dispatcher .as_source_ref() .device_list() .flat_map(|(id, path)| { Some((DrmNode::from_dev_id(id).ok()?, path.to_path_buf())) }) .collect::>(); let (connected_devices, disconnected_devices) = udev .backends .keys() .copied() .partition::, _>(|node| device_list.contains_key(node)); (device_list, connected_devices, disconnected_devices) }; for node in disconnected_devices { device_list.remove(&node); state.device_removed(node); } for node in connected_devices { device_list.remove(&node); // TODO: split off the big State struct to avoid this bs { let udev = state.backend.udev_mut(); let Some(backend) = udev.backends.get_mut(&node) else { unreachable!(); }; if let Err(err) = backend.drm.activate(true) { error!("Error activating DRM device: {err}"); } } state.device_changed(node); // Apply pending gammas // // Also welcome to some really doodoo code let udev = state.backend.udev_mut(); let Some(backend) = udev.backends.get_mut(&node) else { unreachable!(); }; for (crtc, surface) in backend.surfaces.iter_mut() { match std::mem::take(&mut surface.pending_gamma_change) { PendingGammaChange::Idle => { debug!("Restoring from previous gamma"); if let Err(err) = Udev::set_gamma_internal( &backend.drm, crtc, surface.previous_gamma.clone(), ) { warn!("Failed to reset gamma: {err}"); surface.previous_gamma = None; } } PendingGammaChange::Restore => { debug!("Restoring to original gamma"); if let Err(err) = Udev::set_gamma_internal( &backend.drm, crtc, None::<[&[u16]; 3]>, ) { warn!("Failed to reset gamma: {err}"); } surface.previous_gamma = None; } PendingGammaChange::Change(gamma) => { debug!("Changing to pending gamma"); match Udev::set_gamma_internal( &backend.drm, crtc, Some([&gamma[0], &gamma[1], &gamma[2]]), ) { Ok(()) => { surface.previous_gamma = Some(gamma); } Err(err) => { warn!("Failed to set pending gamma: {err}"); surface.previous_gamma = None; } } } } } } // Newly connected devices for (node, path) in device_list.into_iter() { if let Err(err) = state.device_added(node, &path) { error!("Error adding device: {err}"); } } 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() { 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)?; info!( ?primary_gpu, "Trying to initialize EGL Hardware Acceleration", ); match renderer.bind_wl_display(&display_handle) { Ok(_) => 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, screencopy_commit_state: ScreencopyCommitState, previous_gamma: Option<[Box<[u16]>; 3]>, pending_gamma_change: PendingGammaChange, } #[derive(Debug, Clone, Default)] enum PendingGammaChange { /// No pending gamma #[default] Idle, /// Restore the original gamma Restore, /// Change the gamma Change([Box<[u16]>; 3]), } #[derive(Default, Debug, Clone, Copy)] struct ScreencopyCommitState { primary_plane_swapchain: CommitCounter, primary_plane_element: CommitCounter, _cursor: 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>( compositor: &mut GbmDrmCompositor, renderer: &mut UdevRenderer<'a>, elements: &'a [OutputRenderElement< UdevRenderer<'a>, WaylandSurfaceRenderElement>, >], clear_color: [f32; 4], ) -> Result, SwapBuffersError> { 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(); 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, screencopy_commit_state: ScreencopyCommitState::default(), previous_gamma: None, pending_gamma_change: PendingGammaChange::Idle, }; 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); self.gamma_control_manager_state.output_removed(&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. 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. // | Unfortunately that means I can't composite the cursor separately from // | the screencopy, meaning if you have an active screencopy recording // | without cursor overlay then the cursor will dim/flicker out/disappear. 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 { if let Err(err) = element.sync.wait() { warn!("Failed to wait for sync point: {err}"); } } handle_pending_screencopy( &mut renderer, output, surface, &render_frame_result, &self.loop_handle, ); 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, }), Duration::from(self.clock.now()), &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)) } fn handle_pending_screencopy<'a>( renderer: &mut UdevRenderer<'a>, output: &Output, surface: &mut RenderSurface, render_frame_result: &UdevRenderFrameResult<'a>, loop_handle: &LoopHandle<'static, State>, ) { let Some(mut screencopy) = output.with_state_mut(|state| state.screencopy.take()) else { return; }; assert!(screencopy.output() == output); let untransformed_output_size = output.current_mode().expect("output no mode").size; let scale = smithay::utils::Scale::from(output.current_scale().fractional_scale()); if screencopy.with_damage() { if render_frame_result.is_empty { output.with_state_mut(|state| state.screencopy.replace(screencopy)); return; } // Compute damage // // I have no idea if the damage event is supposed to send rects local to the output or to the // region. Sway does the former, Hyprland the latter. Also, no one actually seems to be using the // received damage. wf-recorder and wl-mirror have no-op handlers for the damage event. let damage = match &render_frame_result.primary_element { PrimaryPlaneElement::Swapchain(element) => { let swapchain_commit = &mut surface.screencopy_commit_state.primary_plane_swapchain; let damage = element.damage.damage_since(Some(*swapchain_commit)); *swapchain_commit = 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) => { // INFO: Is this element guaranteed to be the same size as the // | output? If not this becomes a // FIXME: offset the damage by the element's location // // also is this even ever reachable? let element_commit = &mut surface.screencopy_commit_state.primary_plane_element; let damage = element.damage_since(scale, Some(*element_commit)); *element_commit = element.current_commit(); Some(damage) } } .unwrap_or_else(|| { // Returning `None` means the previous CommitCounter is too old or damage // was reset, so damage the whole output vec![Rectangle::from_loc_and_size( Point::from((0, 0)), untransformed_output_size, )] }); // INFO: This code is here for if the bug where `blit_frame_result` makes the area around // | the cursor transparent is fixed/a workaround found. // let cursor_damage = render_frame_result // .cursor_element // .map(|cursor| { // let damage = // cursor.damage_since(scale, Some(surface.screencopy_commit_state.cursor)); // new_commit_counters.cursor = cursor.current_commit(); // damage // }) // .unwrap_or_default(); // // damage.extend(cursor_damage); // // // The primary plane and cursor had no damage but something got rendered, // // so it must be the cursor moving. // // // // 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(scale)); // } // } // INFO: Protocol states that `copy_with_damage` should wait until there is // | damage to be copied. // |. // | Now, for region screencopies this currently submits the frame if there is // | *any* damage on the output, not just in the region. I've found that // | wf-recorder blocks until the last frame is submitted, and if I don't // | send a submission because its region isn't damaged it will hang. // | I'm fairly certain Sway is doing a similar thing. if damage.is_empty() { output.with_state_mut(|state| state.screencopy.replace(screencopy)); return; } screencopy.damage(&damage); } let sync_point = if let Ok(dmabuf) = dmabuf::get_dmabuf(screencopy.buffer()) { trace!("Dmabuf screencopy"); let format_correct = Some(dmabuf.format().code) == shm_format_to_fourcc(wl_shm::Format::Argb8888); let width_correct = dmabuf.width() == screencopy.physical_region().size.w as u32; let height_correct = dmabuf.height() == screencopy.physical_region().size.h as u32; if !(format_correct && width_correct && height_correct) { return; } (|| -> anyhow::Result> { if screencopy.physical_region() == Rectangle::from_loc_and_size(Point::from((0, 0)), untransformed_output_size) { // Optimization to not have to do an extra blit; // just blit the whole output renderer.bind(dmabuf)?; Ok(Some(render_frame_result.blit_frame_result( screencopy.physical_region().size, Transform::Normal, output.current_scale().fractional_scale(), renderer, [screencopy.physical_region()], [], )?)) } else { // `RenderFrameResult::blit_frame_result` doesn't expose a way to // blit from a source rectangle, so blit into another buffer // then blit from that into the dmabuf. let output_buffer_size = untransformed_output_size .to_logical(1) .to_buffer(1, Transform::Normal); let offscreen: GlesRenderbuffer = renderer.create_buffer( smithay::backend::allocator::Fourcc::Abgr8888, output_buffer_size, )?; renderer.bind(offscreen.clone())?; let sync_point = render_frame_result.blit_frame_result( untransformed_output_size, Transform::Normal, output.current_scale().fractional_scale(), renderer, [Rectangle::from_loc_and_size( Point::from((0, 0)), untransformed_output_size, )], [], )?; // ayo are we supposed to wait this here (granted it doesn't do anything // because it's always ready but I want to be correct here) // // renderer.wait(&sync_point)?; // no-op // INFO: I have literally no idea why but doing // | a blit_to offscreen -> dmabuf leads to some weird // | artifacting within the first few frames of a wf-recorder // | recording, but doing it with the targets reversed // | is completely fine???? Bruh that essentially runs the same internal // | code and I don't understand why there's different behavior. // |. // | I can see in the code that `blit_to` is missing a `self.unbind()?` // | call, but adding that back in doesn't fix anything. So strange renderer.bind(dmabuf)?; renderer.blit_from( offscreen, screencopy.physical_region(), Rectangle::from_loc_and_size( Point::from((0, 0)), screencopy.physical_region().size, ), TextureFilter::Linear, )?; Ok(Some(sync_point)) } })() } else if !matches!( renderer::buffer_type(screencopy.buffer()), Some(BufferType::Shm) ) { Err(anyhow!("not a shm buffer")) } else { trace!("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 src_buffer_rect = screencopy.physical_region().to_logical(1).to_buffer( 1, Transform::Normal, &screencopy.physical_region().size.to_logical(1), ); let output_buffer_size = untransformed_output_size .to_logical(1) .to_buffer(1, Transform::Normal); let offscreen: GlesRenderbuffer = renderer.create_buffer( smithay::backend::allocator::Fourcc::Abgr8888, output_buffer_size, )?; renderer.bind(offscreen)?; // Blit the entire output to `offscreen`. // Only the needed region will be copied below let sync_point = render_frame_result.blit_frame_result( untransformed_output_size, Transform::Normal, output.current_scale().fractional_scale(), renderer, [Rectangle::from_loc_and_size( Point::from((0, 0)), untransformed_output_size, )], [], )?; // Can someone explain to me why it feels like some things are // arbitrarily `Physical` or `Buffer` let mapping = renderer.copy_framebuffer( src_buffer_rect, 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(Some(sync_point)) }, ); let Ok(res) = res else { unreachable!( "buffer is guaranteed to be shm from above and should be managed by the shm global" ); }; res }; match sync_point { Ok(Some(sync_point)) if !sync_point.is_reached() => { let Some(sync_fd) = sync_point.export() else { screencopy.submit(false); return; }; let mut screencopy = Some(screencopy); let source = Generic::new(sync_fd, Interest::READ, calloop::Mode::OneShot); let res = loop_handle.insert_source(source, move |_, _, _| { let Some(screencopy) = screencopy.take() else { unreachable!("This source is removed after one run"); }; screencopy.submit(false); trace!("Submitted screencopy"); Ok(PostAction::Remove) }); if res.is_err() { error!("Failed to schedule screencopy submission"); } } Ok(_) => screencopy.submit(false), Err(err) => error!("Failed to submit screencopy: {err}"), } }