use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::fmt::Write; use std::iter::zip; use std::num::NonZeroU64; use std::os::fd::AsFd; use std::path::Path; use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::time::Duration; use std::{io, mem}; use anyhow::{anyhow, bail, ensure, Context}; use bytemuck::cast_slice_mut; use libc::dev_t; use niri_config::{Config, OutputName}; use smithay::backend::allocator::dmabuf::Dmabuf; use smithay::backend::allocator::format::FormatSet; use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice}; use smithay::backend::allocator::Fourcc; use smithay::backend::drm::compositor::{DrmCompositor, FrameFlags, PrimaryPlaneElement}; use smithay::backend::drm::{ DrmDevice, DrmDeviceFd, DrmEvent, DrmEventMetadata, DrmEventTime, DrmNode, NodeType, VrrSupport, }; use smithay::backend::egl::context::ContextPriority; use smithay::backend::egl::{EGLDevice, EGLDisplay}; use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface}; use smithay::backend::renderer::gles::GlesRenderer; use smithay::backend::renderer::multigpu::gbm::GbmGlesBackend; use smithay::backend::renderer::multigpu::{GpuManager, MultiFrame, MultiRenderer}; use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl, Renderer}; use smithay::backend::session::libseat::LibSeatSession; use smithay::backend::session::{Event as SessionEvent, Session}; use smithay::backend::udev::{self, UdevBackend, UdevEvent}; use smithay::desktop::utils::OutputPresentationFeedback; use smithay::output::{Mode, Output, OutputModeSource, PhysicalProperties}; use smithay::reexports::calloop::timer::{TimeoutAction, Timer}; use smithay::reexports::calloop::{Dispatcher, LoopHandle, RegistrationToken}; use smithay::reexports::drm::control::{ self, connector, crtc, property, Device, Mode as DrmMode, ModeFlags, ModeTypeFlags, ResourceHandle, }; use smithay::reexports::gbm::Modifier; use smithay::reexports::input::Libinput; use smithay::reexports::rustix::fs::OFlags; use smithay::reexports::wayland_protocols; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::utils::DeviceFd; use smithay::wayland::dmabuf::{DmabufFeedback, DmabufFeedbackBuilder, DmabufGlobal}; use smithay::wayland::drm_lease::{ DrmLease, DrmLeaseBuilder, DrmLeaseRequest, DrmLeaseState, LeaseRejected, }; use smithay::wayland::presentation::Refresh; use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner}; use wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1::TrancheFlags; use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback; use super::{IpcOutputMap, RenderResult}; use crate::backend::OutputId; use crate::frame_clock::FrameClock; use crate::niri::{Niri, RedrawState, State}; use crate::render_helpers::debug::draw_damage; use crate::render_helpers::renderer::AsGlesRenderer; use crate::render_helpers::{resources, shaders, RenderTarget}; use crate::utils::{get_monotonic_time, is_laptop_panel, logical_output}; const SUPPORTED_COLOR_FORMATS: [Fourcc; 4] = [ Fourcc::Xrgb8888, Fourcc::Xbgr8888, Fourcc::Argb8888, Fourcc::Abgr8888, ]; pub struct Tty { config: Rc>, session: LibSeatSession, udev_dispatcher: Dispatcher<'static, UdevBackend, State>, libinput: Libinput, gpu_manager: GpuManager>, // DRM node corresponding to the primary GPU. May or may not be the same as // primary_render_node. primary_node: DrmNode, // DRM render node corresponding to the primary GPU. primary_render_node: DrmNode, // Devices indexed by DRM node (not necessarily the render node). devices: HashMap, // The dma-buf global corresponds to the output device (the primary GPU). It is only `Some()` // if we have a device corresponding to the primary GPU. dmabuf_global: Option, // The output config had changed, but the session is paused, so we need to update it on resume. update_output_config_on_resume: bool, // Whether the debug tinting is enabled. debug_tint: bool, ipc_outputs: Arc>, } pub type TtyRenderer<'render> = MultiRenderer< 'render, 'render, GbmGlesBackend, GbmGlesBackend, >; pub type TtyFrame<'render, 'frame> = MultiFrame< 'render, 'render, 'frame, GbmGlesBackend, GbmGlesBackend, >; pub type TtyRendererError<'render> = as Renderer>::Error; type GbmDrmCompositor = DrmCompositor< GbmAllocator, GbmDevice, (OutputPresentationFeedback, Duration), DrmDeviceFd, >; pub struct OutputDevice { token: RegistrationToken, render_node: DrmNode, drm_scanner: DrmScanner, surfaces: HashMap, output_ids: HashMap, // SAFETY: drop after all the objects used with them are dropped. // See https://github.com/Smithay/smithay/issues/1102. drm: DrmDevice, gbm: GbmDevice, pub drm_lease_state: Option, non_desktop_connectors: HashSet<(connector::Handle, crtc::Handle)>, active_leases: Vec, } impl OutputDevice { pub fn lease_request( &self, request: DrmLeaseRequest, ) -> Result { let mut builder = DrmLeaseBuilder::new(&self.drm); for connector in request.connectors { let (_, crtc) = self .non_desktop_connectors .iter() .find(|(conn, _)| connector == *conn) .ok_or_else(|| { warn!("Attempted to lease connector that is not non-desktop"); LeaseRejected::default() })?; builder.add_connector(connector); builder.add_crtc(*crtc); let planes = self.drm.planes(crtc).map_err(LeaseRejected::with_cause)?; let (primary_plane, primary_plane_claim) = planes .primary .iter() .find_map(|plane| { self.drm .claim_plane(plane.handle, *crtc) .map(|claim| (plane, claim)) }) .ok_or_else(LeaseRejected::default)?; builder.add_plane(primary_plane.handle, primary_plane_claim); } Ok(builder) } pub fn new_lease(&mut self, lease: DrmLease) { self.active_leases.push(lease); } pub fn remove_lease(&mut self, lease_id: u32) { self.active_leases.retain(|l| l.id() != lease_id); } } #[derive(Debug, Clone, Copy)] struct TtyOutputState { node: DrmNode, crtc: crtc::Handle, } struct Surface { name: OutputName, compositor: GbmDrmCompositor, connector: connector::Handle, dmabuf_feedback: Option, gamma_props: Option, /// Gamma change to apply upon session resume. pending_gamma_change: Option>>, /// Tracy frame that goes from vblank to vblank. vblank_frame: Option, /// Frame name for the VBlank frame. vblank_frame_name: tracy_client::FrameName, /// Plot name for the time since presentation plot. time_since_presentation_plot_name: tracy_client::PlotName, /// Plot name for the presentation misprediction plot. presentation_misprediction_plot_name: tracy_client::PlotName, sequence_delta_plot_name: tracy_client::PlotName, } pub struct SurfaceDmabufFeedback { pub render: DmabufFeedback, pub scanout: DmabufFeedback, } struct GammaProps { crtc: crtc::Handle, gamma_lut: property::Handle, gamma_lut_size: property::Handle, previous_blob: Option, } impl Tty { pub fn new( config: Rc>, event_loop: LoopHandle<'static, State>, ) -> anyhow::Result { let (session, notifier) = LibSeatSession::new().context( "Error creating a session. This might mean that you're trying to run niri on a TTY \ that is already busy, for example if you're running this inside tmux that had been \ originally started on a different TTY", )?; let seat_name = session.seat(); let udev_backend = UdevBackend::new(session.seat()).context("error creating a udev backend")?; let udev_dispatcher = Dispatcher::new(udev_backend, move |event, _, state: &mut State| { state.backend.tty().on_udev_event(&mut state.niri, event); }); event_loop .register_dispatcher(udev_dispatcher.clone()) .unwrap(); let mut libinput = Libinput::new_with_udev(LibinputSessionInterface::from(session.clone())); libinput .udev_assign_seat(&seat_name) .map_err(|()| anyhow!("error assigning the seat to libinput"))?; let input_backend = LibinputInputBackend::new(libinput.clone()); event_loop .insert_source(input_backend, |mut event, _, state| { state.process_libinput_event(&mut event); state.process_input_event(event); }) .unwrap(); event_loop .insert_source(notifier, move |event, _, state| { state.backend.tty().on_session_event(&mut state.niri, event); }) .unwrap(); let api = GbmGlesBackend::with_context_priority(ContextPriority::High); let gpu_manager = GpuManager::new(api).context("error creating the GPU manager")?; let (primary_node, primary_render_node) = primary_node_from_config(&config.borrow()) .ok_or(()) .or_else(|()| { let primary_gpu_path = udev::primary_gpu(&seat_name) .context("error getting the primary GPU")? .context("couldn't find a GPU")?; let primary_node = DrmNode::from_path(primary_gpu_path) .context("error opening the primary GPU DRM node")?; let primary_render_node = primary_node .node_with_type(NodeType::Render) .and_then(Result::ok) .unwrap_or_else(|| { warn!( "error getting the render node for the primary GPU; proceeding anyway" ); primary_node }); Ok::<_, anyhow::Error>((primary_node, primary_render_node)) })?; let mut node_path = String::new(); if let Some(path) = primary_render_node.dev_path() { write!(node_path, "{:?}", path).unwrap(); } else { write!(node_path, "{}", primary_render_node).unwrap(); } info!("using as the render node: {}", node_path); Ok(Self { config, session, udev_dispatcher, libinput, gpu_manager, primary_node, primary_render_node, devices: HashMap::new(), dmabuf_global: None, update_output_config_on_resume: false, debug_tint: false, ipc_outputs: Arc::new(Mutex::new(HashMap::new())), }) } pub fn init(&mut self, niri: &mut Niri) { for (device_id, path) in self.udev_dispatcher.clone().as_source_ref().device_list() { if let Err(err) = self.device_added(device_id, path, niri) { warn!("error adding device: {err:?}"); } } } fn on_udev_event(&mut self, niri: &mut Niri, event: UdevEvent) { let _span = tracy_client::span!("Tty::on_udev_event"); match event { UdevEvent::Added { device_id, path } => { if !self.session.is_active() { debug!("skipping UdevEvent::Added as session is inactive"); return; } if let Err(err) = self.device_added(device_id, &path, niri) { warn!("error adding device: {err:?}"); } } UdevEvent::Changed { device_id } => { if !self.session.is_active() { debug!("skipping UdevEvent::Changed as session is inactive"); return; } self.device_changed(device_id, niri) } UdevEvent::Removed { device_id } => { if !self.session.is_active() { debug!("skipping UdevEvent::Removed as session is inactive"); return; } self.device_removed(device_id, niri) } } } fn on_session_event(&mut self, niri: &mut Niri, event: SessionEvent) { let _span = tracy_client::span!("Tty::on_session_event"); match event { SessionEvent::PauseSession => { debug!("pausing session"); self.libinput.suspend(); for device in self.devices.values_mut() { device.drm.pause(); if let Some(lease_state) = &mut device.drm_lease_state { lease_state.suspend(); } } } SessionEvent::ActivateSession => { debug!("resuming session"); if self.libinput.resume().is_err() { warn!("error resuming libinput"); } let mut device_list = self .udev_dispatcher .as_source_ref() .device_list() .map(|(device_id, path)| (device_id, path.to_owned())) .collect::>(); let removed_devices = self .devices .keys() .filter(|node| !device_list.contains_key(&node.dev_id())) .copied() .collect::>(); let remained_devices = self .devices .keys() .filter(|node| device_list.contains_key(&node.dev_id())) .copied() .collect::>(); // Remove removed devices. for node in removed_devices { device_list.remove(&node.dev_id()); self.device_removed(node.dev_id(), niri); } // Update remained devices. for node in remained_devices { device_list.remove(&node.dev_id()); // It hasn't been removed, update its state as usual. let device = self.devices.get_mut(&node).unwrap(); if let Err(err) = device.drm.activate(true) { warn!("error activating DRM device: {err:?}"); } if let Some(lease_state) = &mut device.drm_lease_state { lease_state.resume::(); } // Refresh the connectors. self.device_changed(node.dev_id(), niri); // Apply pending gamma changes and restore our existing gamma. let device = self.devices.get_mut(&node).unwrap(); for (crtc, surface) in device.surfaces.iter_mut() { if let Some(ramp) = surface.pending_gamma_change.take() { let ramp = ramp.as_deref(); let res = if let Some(gamma_props) = &mut surface.gamma_props { gamma_props.set_gamma(&device.drm, ramp) } else { set_gamma_for_crtc(&device.drm, *crtc, ramp) }; if let Err(err) = res { warn!("error applying pending gamma change: {err:?}"); } } else if let Some(gamma_props) = &surface.gamma_props { if let Err(err) = gamma_props.restore_gamma(&device.drm) { warn!("error restoring gamma: {err:?}"); } } } } // Add new devices. for (device_id, path) in device_list.into_iter() { if let Err(err) = self.device_added(device_id, &path, niri) { warn!("error adding device: {err:?}"); } } if self.update_output_config_on_resume { self.on_output_config_changed(niri); } self.refresh_ipc_outputs(niri); niri.idle_notifier_state.notify_activity(&niri.seat); niri.monitors_active = true; self.set_monitors_active(true); niri.queue_redraw_all(); } } } fn device_added( &mut self, device_id: dev_t, path: &Path, niri: &mut Niri, ) -> anyhow::Result<()> { debug!("device added: {device_id} {path:?}"); let node = DrmNode::from_dev_id(device_id)?; let open_flags = OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK; let fd = self.session.open(path, open_flags)?; let device_fd = DrmDeviceFd::new(DeviceFd::from(fd)); let (drm, drm_notifier) = DrmDevice::new(device_fd.clone(), true)?; let gbm = GbmDevice::new(device_fd)?; let display = unsafe { EGLDisplay::new(gbm.clone())? }; let egl_device = EGLDevice::device_for_display(&display)?; let render_node = egl_device .try_get_render_node()? .context("no render node")?; self.gpu_manager .as_mut() .add_node(render_node, gbm.clone()) .context("error adding render node to GPU manager")?; if node == self.primary_node || render_node == self.primary_render_node { if node == self.primary_node { debug!("this is the primary node"); } if render_node == self.primary_render_node { debug!("this is the primary render node"); } let mut renderer = self .gpu_manager .single_renderer(&render_node) .context("error creating renderer")?; if let Err(err) = renderer.bind_wl_display(&niri.display_handle) { warn!("error binding wl-display in EGL: {err:?}"); } let gles_renderer = renderer.as_gles_renderer(); resources::init(gles_renderer); shaders::init(gles_renderer); let config = self.config.borrow(); if let Some(src) = config.animations.window_resize.custom_shader.as_deref() { shaders::set_custom_resize_program(gles_renderer, Some(src)); } if let Some(src) = config.animations.window_close.custom_shader.as_deref() { shaders::set_custom_close_program(gles_renderer, Some(src)); } if let Some(src) = config.animations.window_open.custom_shader.as_deref() { shaders::set_custom_open_program(gles_renderer, Some(src)); } drop(config); niri.layout.update_shaders(); // Create the dmabuf global. let primary_formats = renderer.dmabuf_formats(); let default_feedback = DmabufFeedbackBuilder::new(render_node.dev_id(), primary_formats.clone()) .build() .context("error building default dmabuf feedback")?; let dmabuf_global = niri .dmabuf_state .create_global_with_default_feedback::( &niri.display_handle, &default_feedback, ); assert!(self.dmabuf_global.replace(dmabuf_global).is_none()); // Update the dmabuf feedbacks for all surfaces. for device in self.devices.values_mut() { for surface in device.surfaces.values_mut() { match surface_dmabuf_feedback( &surface.compositor, primary_formats.clone(), self.primary_render_node, device.render_node, ) { Ok(feedback) => { surface.dmabuf_feedback = Some(feedback); } Err(err) => { warn!("error building dmabuf feedback: {err:?}"); } } } } } let token = niri .event_loop .insert_source(drm_notifier, move |event, meta, state| { let tty = state.backend.tty(); match event { DrmEvent::VBlank(crtc) => { let meta = meta.expect("VBlank events must have metadata"); tty.on_vblank(&mut state.niri, node, crtc, meta); } DrmEvent::Error(error) => warn!("DRM error: {error}"), }; }) .unwrap(); let drm_lease_state = DrmLeaseState::new::(&niri.display_handle, &node) .map_err(|err| warn!("error initializing DRM leasing for {node}: {err:?}")) .ok(); let device = OutputDevice { token, render_node, drm, gbm, drm_scanner: DrmScanner::new(), surfaces: HashMap::new(), output_ids: HashMap::new(), drm_lease_state, active_leases: Vec::new(), non_desktop_connectors: HashSet::new(), }; assert!(self.devices.insert(node, device).is_none()); self.device_changed(device_id, niri); Ok(()) } fn device_changed(&mut self, device_id: dev_t, niri: &mut Niri) { debug!("device changed: {device_id}"); let Ok(node) = DrmNode::from_dev_id(device_id) else { warn!("error creating DrmNode"); return; }; let Some(device) = self.devices.get_mut(&node) else { warn!("no such device"); return; }; let scan_result = match device.drm_scanner.scan_connectors(&device.drm) { Ok(x) => x, Err(err) => { warn!("error scanning connectors: {err:?}"); return; } }; let mut removed = Vec::new(); for event in scan_result { match event { DrmScanEvent::Connected { connector, crtc: Some(crtc), } => { let connector_name = format_connector_name(&connector); let output_name = make_output_name(&device.drm, connector.handle(), connector_name, false); debug!( "new connector: {} \"{}\"", &output_name.connector, output_name.format_make_model_serial(), ); // Assign an id to this crtc. device.output_ids.insert(crtc, OutputId::next()); } DrmScanEvent::Disconnected { crtc: Some(crtc), .. } => { removed.push(crtc); } _ => (), } } for crtc in &removed { self.connector_disconnected(niri, node, *crtc); } let Some(device) = self.devices.get_mut(&node) else { error!("device disappeared"); return; }; for crtc in removed { if device.output_ids.remove(&crtc).is_none() { error!("output ID missing for disconnected crtc: {crtc:?}"); } } // This will connect any new connectors if needed, and apply other changes, such as // connecting back the internal laptop monitor once it becomes the only monitor left. // // It will also call refresh_ipc_outputs(), which will catch the disconnected connectors // above. self.on_output_config_changed(niri); } fn device_removed(&mut self, device_id: dev_t, niri: &mut Niri) { debug!("device removed: {device_id}"); let Ok(node) = DrmNode::from_dev_id(device_id) else { warn!("error creating DrmNode"); return; }; let Some(device) = self.devices.get_mut(&node) else { warn!("no such device"); return; }; let crtcs: Vec<_> = device .drm_scanner .crtcs() .map(|(_info, crtc)| crtc) .collect(); for crtc in crtcs { self.connector_disconnected(niri, node, crtc); } let mut device = self.devices.remove(&node).unwrap(); if let Some(lease_state) = &mut device.drm_lease_state { lease_state.disable_global::(); } if node == self.primary_node || device.render_node == self.primary_render_node { match self.gpu_manager.single_renderer(&device.render_node) { Ok(mut renderer) => renderer.unbind_wl_display(), Err(err) => { warn!("error creating renderer during device removal: {err}"); } } // Disable and destroy the dmabuf global. if let Some(global) = self.dmabuf_global.take() { niri.dmabuf_state .disable_global::(&niri.display_handle, &global); niri.event_loop .insert_source( Timer::from_duration(Duration::from_secs(10)), move |_, _, state| { state .niri .dmabuf_state .destroy_global::(&state.niri.display_handle, global); TimeoutAction::Drop }, ) .unwrap(); // Clear the dmabuf feedbacks for all surfaces. for device in self.devices.values_mut() { for surface in device.surfaces.values_mut() { surface.dmabuf_feedback = None; } } } else { error!("dmabuf global was already missing"); } } self.gpu_manager.as_mut().remove_node(&device.render_node); niri.event_loop.remove(device.token); self.refresh_ipc_outputs(niri); } fn connector_connected( &mut self, niri: &mut Niri, node: DrmNode, connector: connector::Info, crtc: crtc::Handle, ) -> anyhow::Result<()> { let connector_name = format_connector_name(&connector); debug!("connecting connector: {connector_name}"); let device = self.devices.get_mut(&node).context("missing device")?; let output_name = make_output_name( &device.drm, connector.handle(), connector_name.clone(), self.config.borrow().debug.disable_monitor_names, ); let non_desktop = find_drm_property(&device.drm, connector.handle(), "non-desktop") .and_then(|(_, info, value)| info.value_type().convert_value(value).as_boolean()) .unwrap_or(false); if non_desktop { debug!("output is non desktop"); let description = output_name.format_description(); if let Some(lease_state) = &mut device.drm_lease_state { lease_state.add_connector::(connector.handle(), connector_name, description); } device .non_desktop_connectors .insert((connector.handle(), crtc)); return Ok(()); } let config = self .config .borrow() .outputs .find(&output_name) .cloned() .unwrap_or_default(); for m in connector.modes() { trace!("{m:?}"); } let (mode, fallback) = pick_mode(&connector, config.mode).ok_or_else(|| anyhow!("no mode"))?; if fallback { let target = config.mode.unwrap(); warn!( "configured mode {}x{}{} could not be found, falling back to preferred", target.width, target.height, if let Some(refresh) = target.refresh { format!("@{refresh}") } else { String::new() }, ); } debug!("picking mode: {mode:?}"); // We only use 8888 RGB formats, so set max bpc to 8 to allow more types of links to run. match set_max_bpc(&device.drm, connector.handle(), 8) { Ok(bpc) => debug!("set max bpc to {bpc}"), Err(err) => debug!("error setting max bpc: {err:?}"), } let mut gamma_props = GammaProps::new(&device.drm, crtc) .map_err(|err| debug!("error getting gamma properties: {err:?}")) .ok(); // Reset gamma in case it was set before. let res = if let Some(gamma_props) = &mut gamma_props { gamma_props.set_gamma(&device.drm, None) } else { set_gamma_for_crtc(&device.drm, crtc, None) }; if let Err(err) = res { debug!("error resetting gamma: {err:?}"); } let surface = device .drm .create_surface(crtc, mode, &[connector.handle()])?; // Try to enable VRR if requested. match surface.vrr_supported(connector.handle()) { Ok(VrrSupport::Supported | VrrSupport::RequiresModeset) => { // Even if on-demand, we still disable it until later checks. let vrr = config.is_vrr_always_on(); let word = if vrr { "enabling" } else { "disabling" }; if let Err(err) = surface.use_vrr(vrr) { warn!("error {} VRR: {err:?}", word); } } Ok(VrrSupport::NotSupported) => { if !config.is_vrr_always_off() { warn!("cannot enable VRR because connector does not support it"); } // Try to disable it anyway to work around a bug where resetting DRM state causes // vrr_capable to be reset to 0, potentially leaving VRR_ENABLED at 1. let _ = surface.use_vrr(false); } Err(err) => { warn!("error querying for VRR support: {err:?}"); } } // Create GBM allocator. let gbm_flags = GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT; let allocator = GbmAllocator::new(device.gbm.clone(), gbm_flags); // Update the output mode. let (physical_width, physical_height) = connector.size().unwrap_or((0, 0)); let output = Output::new( connector_name.clone(), PhysicalProperties { size: (physical_width as i32, physical_height as i32).into(), subpixel: connector.subpixel().into(), model: output_name.model.as_deref().unwrap_or("Unknown").to_owned(), make: output_name.make.as_deref().unwrap_or("Unknown").to_owned(), }, ); let wl_mode = Mode::from(mode); output.change_current_state(Some(wl_mode), None, None, None); output.set_preferred(wl_mode); output .user_data() .insert_if_missing(|| TtyOutputState { node, crtc }); output.user_data().insert_if_missing(|| output_name.clone()); let renderer = self.gpu_manager.single_renderer(&device.render_node)?; let egl_context = renderer.as_ref().egl_context(); let render_formats = egl_context.dmabuf_render_formats(); // Filter out the CCS modifiers as they have increased bandwidth, causing some monitor // configurations to stop working. // // The invalid modifier attempt below should make this unnecessary in some cases, but it // would still be a bad idea to remove this until Smithay has some kind of full-device // modesetting test that is able to "downgrade" existing connector modifiers to get enough // bandwidth for a newly connected one. let render_formats = render_formats .iter() .copied() .filter(|format| { !matches!( format.modifier, Modifier::I915_y_tiled_ccs // I915_FORMAT_MOD_Yf_TILED_CCS | Modifier::Unrecognized(0x100000000000005) | Modifier::I915_y_tiled_gen12_rc_ccs | Modifier::I915_y_tiled_gen12_mc_ccs // I915_FORMAT_MOD_Y_TILED_GEN12_RC_CCS_CC | Modifier::Unrecognized(0x100000000000008) // I915_FORMAT_MOD_4_TILED_DG2_RC_CCS | Modifier::Unrecognized(0x10000000000000a) // I915_FORMAT_MOD_4_TILED_DG2_MC_CCS | Modifier::Unrecognized(0x10000000000000b) // I915_FORMAT_MOD_4_TILED_DG2_RC_CCS_CC | Modifier::Unrecognized(0x10000000000000c) ) }) .collect::(); // Create the compositor. let res = DrmCompositor::new( OutputModeSource::Auto(output.clone()), surface, None, allocator.clone(), device.gbm.clone(), SUPPORTED_COLOR_FORMATS, // This is only used to pick a good internal format, so it can use the surface's render // formats, even though we only ever render on the primary GPU. render_formats.clone(), device.drm.cursor_size(), Some(device.gbm.clone()), ); let mut compositor = match res { Ok(x) => x, Err(err) => { warn!("error creating DRM compositor, will try with invalid modifier: {err:?}"); let render_formats = render_formats .iter() .copied() .filter(|format| format.modifier == Modifier::Invalid) .collect::(); // DrmCompositor::new() consumed the surface... let surface = device .drm .create_surface(crtc, mode, &[connector.handle()])?; DrmCompositor::new( OutputModeSource::Auto(output.clone()), surface, None, allocator, device.gbm.clone(), SUPPORTED_COLOR_FORMATS, render_formats, device.drm.cursor_size(), Some(device.gbm.clone()), ) .context("error creating DRM compositor")? } }; if self.debug_tint { compositor.set_debug_flags(DebugFlags::TINT); } let mut dmabuf_feedback = None; if let Ok(primary_renderer) = self.gpu_manager.single_renderer(&self.primary_render_node) { let primary_formats = primary_renderer.dmabuf_formats(); match surface_dmabuf_feedback( &compositor, primary_formats, self.primary_render_node, device.render_node, ) { Ok(feedback) => { dmabuf_feedback = Some(feedback); } Err(err) => { warn!("error building dmabuf feedback: {err:?}"); } } } // Some buggy monitors replug upon powering off, so powering on here would prevent such // monitors from powering off. Therefore, we avoid unconditionally powering on. if !niri.monitors_active { if let Err(err) = compositor.clear() { warn!("error clearing drm surface: {err:?}"); } } let vrr_enabled = compositor.vrr_enabled(); let vblank_frame_name = tracy_client::FrameName::new_leak(format!("vblank on {connector_name}")); let time_since_presentation_plot_name = tracy_client::PlotName::new_leak(format!( "{connector_name} time since presentation, ms" )); let presentation_misprediction_plot_name = tracy_client::PlotName::new_leak(format!( "{connector_name} presentation misprediction, ms" )); let sequence_delta_plot_name = tracy_client::PlotName::new_leak(format!("{connector_name} sequence delta")); let surface = Surface { name: output_name, connector: connector.handle(), compositor, dmabuf_feedback, gamma_props, pending_gamma_change: None, vblank_frame: None, vblank_frame_name, time_since_presentation_plot_name, presentation_misprediction_plot_name, sequence_delta_plot_name, }; let res = device.surfaces.insert(crtc, surface); assert!(res.is_none(), "crtc must not have already existed"); niri.add_output(output.clone(), Some(refresh_interval(mode)), vrr_enabled); if niri.monitors_active { // Redraw the new monitor. niri.event_loop.insert_idle(move |state| { // Guard against output disconnecting before the idle has a chance to run. if state.niri.output_state.contains_key(&output) { state.niri.queue_redraw(&output); } }); } Ok(()) } fn connector_disconnected(&mut self, niri: &mut Niri, node: DrmNode, crtc: crtc::Handle) { let Some(device) = self.devices.get_mut(&node) else { debug!("disconnecting connector for crtc: {crtc:?}"); error!("missing device"); return; }; let Some(surface) = device.surfaces.remove(&crtc) else { debug!("disconnecting connector for crtc: {crtc:?}"); if let Some((conn, _)) = device .non_desktop_connectors .iter() .find(|(_, crtc_)| *crtc_ == crtc) { debug!("withdrawing non-desktop connector from DRM leasing"); let conn = *conn; device.non_desktop_connectors.remove(&(conn, crtc)); if let Some(lease_state) = &mut device.drm_lease_state { lease_state.withdraw_connector(conn); } } else { debug!("crtc wasn't enabled"); } return; }; debug!("disconnecting connector: {:?}", surface.name.connector); let output = niri .global_space .outputs() .find(|output| { let tty_state: &TtyOutputState = output.user_data().get().unwrap(); tty_state.node == node && tty_state.crtc == crtc }) .cloned(); if let Some(output) = output { niri.remove_output(&output); } else { error!("missing output for crtc {crtc:?}"); }; } fn on_vblank( &mut self, niri: &mut Niri, node: DrmNode, crtc: crtc::Handle, meta: DrmEventMetadata, ) { let span = tracy_client::span!("Tty::on_vblank"); let now = get_monotonic_time(); let Some(device) = self.devices.get_mut(&node) else { // I've seen it happen. error!("missing device in vblank callback for crtc {crtc:?}"); return; }; let Some(surface) = device.surfaces.get_mut(&crtc) else { error!("missing surface in vblank callback for crtc {crtc:?}"); return; }; // Finish the Tracy frame, if any. drop(surface.vblank_frame.take()); let name = &surface.name.connector; trace!("vblank on {name} {meta:?}"); span.emit_text(name); let presentation_time = match meta.time { DrmEventTime::Monotonic(time) => time, DrmEventTime::Realtime(_) => { // Not supported. // This value will be ignored in the frame clock code. Duration::ZERO } }; let presentation_time = if niri.config.borrow().debug.emulate_zero_presentation_time { Duration::ZERO } else { presentation_time }; let message = if presentation_time.is_zero() { format!("vblank on {name}, presentation time unknown") } else if presentation_time > now { let diff = presentation_time - now; tracy_client::Client::running().unwrap().plot( surface.time_since_presentation_plot_name, -diff.as_secs_f64() * 1000., ); format!("vblank on {name}, presentation is {diff:?} later") } else { let diff = now - presentation_time; tracy_client::Client::running().unwrap().plot( surface.time_since_presentation_plot_name, diff.as_secs_f64() * 1000., ); format!("vblank on {name}, presentation was {diff:?} ago") }; tracy_client::Client::running() .unwrap() .message(&message, 0); let Some(output) = niri .global_space .outputs() .find(|output| { let tty_state: &TtyOutputState = output.user_data().get().unwrap(); tty_state.node == node && tty_state.crtc == crtc }) .cloned() else { error!("missing output in global space for {name}"); return; }; let Some(output_state) = niri.output_state.get_mut(&output) else { error!("missing output state for {name}"); return; }; let redraw_needed = match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) { RedrawState::WaitingForVBlank { redraw_needed } => redraw_needed, state @ (RedrawState::Idle | RedrawState::Queued | RedrawState::WaitingForEstimatedVBlank(_) | RedrawState::WaitingForEstimatedVBlankAndQueued(_)) => { // This is an error!() because it shouldn't happen, but on some systems it somehow // does. Kernel sending rogue vblank events? // // https://github.com/YaLTeR/niri/issues/556 // https://github.com/YaLTeR/niri/issues/615 error!( "unexpected redraw state for output {name} (should be WaitingForVBlank); \ can happen when resuming from sleep or powering on monitors: {state:?}" ); true } }; // Mark the last frame as submitted. match surface.compositor.frame_submitted() { Ok(Some((mut feedback, target_presentation_time))) => { let refresh = match output_state.frame_clock.refresh_interval() { Some(refresh) => { if output_state.frame_clock.vrr() { Refresh::Variable(refresh) } else { Refresh::Fixed(refresh) } } None => Refresh::Unknown, }; // FIXME: ideally should be monotonically increasing for a surface. let seq = meta.sequence as u64; let mut flags = wp_presentation_feedback::Kind::Vsync | wp_presentation_feedback::Kind::HwCompletion; let time = if presentation_time.is_zero() { now } else { flags.insert(wp_presentation_feedback::Kind::HwClock); presentation_time }; feedback.presented::<_, smithay::utils::Monotonic>(time, refresh, seq, flags); if !presentation_time.is_zero() { let misprediction_s = presentation_time.as_secs_f64() - target_presentation_time.as_secs_f64(); tracy_client::Client::running().unwrap().plot( surface.presentation_misprediction_plot_name, misprediction_s * 1000., ); } } Ok(None) => (), Err(err) => { warn!("error marking frame as submitted: {err}"); } } if let Some(last_sequence) = output_state.last_drm_sequence { let delta = meta.sequence as f64 - last_sequence as f64; tracy_client::Client::running() .unwrap() .plot(surface.sequence_delta_plot_name, delta); } output_state.last_drm_sequence = Some(meta.sequence); output_state.frame_clock.presented(presentation_time); if redraw_needed || output_state.unfinished_animations_remain { let vblank_frame = tracy_client::Client::running() .unwrap() .non_continuous_frame(surface.vblank_frame_name); surface.vblank_frame = Some(vblank_frame); niri.queue_redraw(&output); } else { niri.send_frame_callbacks(&output); } } fn on_estimated_vblank_timer(&self, niri: &mut Niri, output: Output) { let span = tracy_client::span!("Tty::on_estimated_vblank_timer"); let name = output.name(); span.emit_text(&name); let Some(output_state) = niri.output_state.get_mut(&output) else { error!("missing output state for {name}"); return; }; // We waited for the timer, now we can send frame callbacks again. output_state.frame_callback_sequence = output_state.frame_callback_sequence.wrapping_add(1); match mem::replace(&mut output_state.redraw_state, RedrawState::Idle) { RedrawState::Idle => unreachable!(), RedrawState::Queued => unreachable!(), RedrawState::WaitingForVBlank { .. } => unreachable!(), RedrawState::WaitingForEstimatedVBlank(_) => (), // The timer fired just in front of a redraw. RedrawState::WaitingForEstimatedVBlankAndQueued(_) => { output_state.redraw_state = RedrawState::Queued; return; } } if output_state.unfinished_animations_remain { niri.queue_redraw(&output); } else { niri.send_frame_callbacks(&output); } } pub fn seat_name(&self) -> String { self.session.seat() } pub fn with_primary_renderer( &mut self, f: impl FnOnce(&mut GlesRenderer) -> T, ) -> Option { let mut renderer = self .gpu_manager .single_renderer(&self.primary_render_node) .ok()?; Some(f(renderer.as_gles_renderer())) } pub fn render( &mut self, niri: &mut Niri, output: &Output, target_presentation_time: Duration, ) -> RenderResult { let span = tracy_client::span!("Tty::render"); let mut rv = RenderResult::Skipped; let tty_state: &TtyOutputState = output.user_data().get().unwrap(); let Some(device) = self.devices.get_mut(&tty_state.node) else { error!("missing output device"); return rv; }; let Some(surface) = device.surfaces.get_mut(&tty_state.crtc) else { error!("missing surface"); return rv; }; span.emit_text(&surface.name.connector); if !device.drm.is_active() { warn!("device is inactive"); return rv; } let mut renderer = match self.gpu_manager.renderer( &self.primary_render_node, &device.render_node, surface.compositor.format(), ) { Ok(renderer) => renderer, Err(err) => { warn!("error creating renderer for primary GPU: {err:?}"); return rv; } }; // Render the elements. let mut elements = niri.render::(&mut renderer, output, true, RenderTarget::Output); // Visualize the damage, if enabled. if niri.debug_draw_damage { let output_state = niri.output_state.get_mut(output).unwrap(); draw_damage(&mut output_state.debug_damage_tracker, &mut elements); } // Overlay planes are disabled by default as they cause weird performance issues on my // system. let flags = { let debug = &self.config.borrow().debug; let primary_scanout_flag = if debug.restrict_primary_scanout_to_matching_format { FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT } else { FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT_ANY }; let mut flags = primary_scanout_flag | FrameFlags::ALLOW_CURSOR_PLANE_SCANOUT; if debug.enable_overlay_planes { flags.insert(FrameFlags::ALLOW_OVERLAY_PLANE_SCANOUT); } if debug.disable_direct_scanout { flags.remove(primary_scanout_flag); flags.remove(FrameFlags::ALLOW_OVERLAY_PLANE_SCANOUT); } if debug.disable_cursor_plane { flags.remove(FrameFlags::ALLOW_CURSOR_PLANE_SCANOUT); } flags }; // Hand them over to the DRM. let drm_compositor = &mut surface.compositor; match drm_compositor.render_frame::<_, _>(&mut renderer, &elements, [0.; 4], flags) { Ok(res) => { let needs_sync = res.needs_sync() || self .config .borrow() .debug .wait_for_frame_completion_before_queueing; if needs_sync { if let PrimaryPlaneElement::Swapchain(element) = res.primary_element { let _span = tracy_client::span!("wait for completion"); if let Err(err) = element.sync.wait() { warn!("error waiting for frame completion: {err:?}"); } } } niri.update_primary_scanout_output(output, &res.states); if let Some(dmabuf_feedback) = surface.dmabuf_feedback.as_ref() { niri.send_dmabuf_feedbacks(output, dmabuf_feedback, &res.states); } if !res.is_empty { let presentation_feedbacks = niri.take_presentation_feedbacks(output, &res.states); let data = (presentation_feedbacks, target_presentation_time); match drm_compositor.queue_frame(data) { Ok(()) => { let output_state = niri.output_state.get_mut(output).unwrap(); let new_state = RedrawState::WaitingForVBlank { redraw_needed: false, }; match mem::replace(&mut output_state.redraw_state, new_state) { RedrawState::Idle => unreachable!(), RedrawState::Queued => (), RedrawState::WaitingForVBlank { .. } => unreachable!(), RedrawState::WaitingForEstimatedVBlank(_) => unreachable!(), RedrawState::WaitingForEstimatedVBlankAndQueued(token) => { niri.event_loop.remove(token); } }; // We queued this frame successfully, so the current client buffers were // latched. We can send frame callbacks now, since a new client commit // will no longer overwrite this frame and will wait for a VBlank. output_state.frame_callback_sequence = output_state.frame_callback_sequence.wrapping_add(1); return RenderResult::Submitted; } Err(err) => { warn!("error queueing frame: {err}"); } } } else { rv = RenderResult::NoDamage; } } Err(err) => { // Can fail if we switched to a different TTY. warn!("error rendering frame: {err}"); } } // We're not expecting a vblank right after this. drop(surface.vblank_frame.take()); // Queue a timer to fire at the predicted vblank time. queue_estimated_vblank_timer(niri, output.clone(), target_presentation_time); rv } pub fn change_vt(&mut self, vt: i32) { if let Err(err) = self.session.change_vt(vt) { warn!("error changing VT: {err}"); } } pub fn suspend(&self) { #[cfg(feature = "dbus")] if let Err(err) = suspend() { warn!("error suspending: {err:?}"); } } pub fn toggle_debug_tint(&mut self) { self.debug_tint = !self.debug_tint; for device in self.devices.values_mut() { for surface in device.surfaces.values_mut() { let compositor = &mut surface.compositor; let mut flags = compositor.debug_flags(); flags.set(DebugFlags::TINT, self.debug_tint); compositor.set_debug_flags(flags); } } } pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> bool { let mut renderer = match self.gpu_manager.single_renderer(&self.primary_render_node) { Ok(renderer) => renderer, Err(err) => { debug!("error creating renderer for primary GPU: {err:?}"); return false; } }; match renderer.import_dmabuf(dmabuf, None) { Ok(_texture) => { dmabuf.set_node(Some(self.primary_render_node)); true } Err(err) => { debug!("error importing dmabuf: {err:?}"); false } } } pub fn early_import(&mut self, surface: &WlSurface) { if let Err(err) = self.gpu_manager.early_import( // We always render on the primary GPU. self.primary_render_node, surface, ) { warn!("error doing early import: {err:?}"); } } pub fn get_gamma_size(&self, output: &Output) -> anyhow::Result { let tty_state = output.user_data().get::().unwrap(); let crtc = tty_state.crtc; let device = self .devices .get(&tty_state.node) .context("missing device")?; let surface = device.surfaces.get(&crtc).context("missing surface")?; if let Some(gamma_props) = &surface.gamma_props { gamma_props.gamma_size(&device.drm) } else { let info = device .drm .get_crtc(crtc) .context("error getting crtc info")?; Ok(info.gamma_length()) } } pub fn set_gamma(&mut self, output: &Output, ramp: Option>) -> anyhow::Result<()> { let tty_state = output.user_data().get::().unwrap(); let crtc = tty_state.crtc; let device = self .devices .get_mut(&tty_state.node) .context("missing device")?; let surface = device.surfaces.get_mut(&crtc).context("missing surface")?; // Cannot change properties while the device is inactive. if !self.session.is_active() { surface.pending_gamma_change = Some(ramp); return Ok(()); } let ramp = ramp.as_deref(); if let Some(gamma_props) = &mut surface.gamma_props { gamma_props.set_gamma(&device.drm, ramp) } else { set_gamma_for_crtc(&device.drm, crtc, ramp) } } fn refresh_ipc_outputs(&self, niri: &mut Niri) { let _span = tracy_client::span!("Tty::refresh_ipc_outputs"); let mut ipc_outputs = HashMap::new(); for (node, device) in &self.devices { for (connector, crtc) in device.drm_scanner.crtcs() { let connector_name = format_connector_name(connector); let physical_size = connector.size(); let output_name = make_output_name( &device.drm, connector.handle(), connector_name.clone(), self.config.borrow().debug.disable_monitor_names, ); let surface = device.surfaces.get(&crtc); let current_crtc_mode = surface.map(|surface| surface.compositor.pending_mode()); let mut current_mode = None; let modes = connector .modes() .iter() .filter(|m| !m.flags().contains(ModeFlags::INTERLACE)) .enumerate() .map(|(idx, m)| { if Some(*m) == current_crtc_mode { current_mode = Some(idx); } niri_ipc::Mode { width: m.size().0, height: m.size().1, refresh_rate: Mode::from(*m).refresh as u32, is_preferred: m.mode_type().contains(ModeTypeFlags::PREFERRED), } }) .collect(); if let Some(crtc_mode) = current_crtc_mode { if current_mode.is_none() { if crtc_mode.flags().contains(ModeFlags::INTERLACE) { warn!("connector mode list missing current mode (interlaced)"); } else { error!("connector mode list missing current mode"); } } } let vrr_supported = surface .map(|surface| { matches!( surface.compositor.vrr_supported(connector.handle()), Ok(VrrSupport::Supported | VrrSupport::RequiresModeset) ) }) .unwrap_or_else(|| { is_vrr_capable(&device.drm, connector.handle()) == Some(true) }); let vrr_enabled = surface.map_or(false, |surface| surface.compositor.vrr_enabled()); let logical = niri .global_space .outputs() .find(|output| { let tty_state: &TtyOutputState = output.user_data().get().unwrap(); tty_state.node == *node && tty_state.crtc == crtc }) .map(logical_output); let ipc_output = niri_ipc::Output { name: connector_name, make: output_name.make.unwrap_or_else(|| "Unknown".into()), model: output_name.model.unwrap_or_else(|| "Unknown".into()), serial: output_name.serial, physical_size, modes, current_mode, vrr_supported, vrr_enabled, logical, }; let id = device.output_ids.get(&crtc).copied().unwrap_or_else(|| { error!("output ID missing for crtc: {crtc:?}"); OutputId::next() }); ipc_outputs.insert(id, ipc_output); } } let mut guard = self.ipc_outputs.lock().unwrap(); *guard = ipc_outputs; niri.ipc_outputs_changed = true; } pub fn ipc_outputs(&self) -> Arc> { self.ipc_outputs.clone() } #[cfg(feature = "xdp-gnome-screencast")] pub fn primary_gbm_device(&self) -> Option> { // Try to find a device corresponding to the primary render node. let device = self .devices .values() .find(|d| d.render_node == self.primary_render_node); // Otherwise, try to get the device corresponding to the primary node. let device = device.or_else(|| self.devices.get(&self.primary_node)); Some(device?.gbm.clone()) } pub fn set_monitors_active(&mut self, active: bool) { // We only disable the CRTC here, this will also reset the // surface state so that the next call to `render_frame` will // always produce a new frame and `queue_frame` will change // the CRTC to active. This makes sure we always enable a CRTC // within an atomic operation. if active { return; } for device in self.devices.values_mut() { for surface in device.surfaces.values_mut() { if let Err(err) = surface.compositor.clear() { warn!("error clearing drm surface: {err:?}"); } } } } pub fn set_output_on_demand_vrr(&mut self, niri: &mut Niri, output: &Output, enable_vrr: bool) { let _span = tracy_client::span!("Tty::set_output_on_demand_vrr"); let output_state = niri.output_state.get_mut(output).unwrap(); output_state.on_demand_vrr_enabled = enable_vrr; if output_state.frame_clock.vrr() == enable_vrr { return; } for (&node, device) in self.devices.iter_mut() { for (&crtc, surface) in device.surfaces.iter_mut() { let tty_state: &TtyOutputState = output.user_data().get().unwrap(); if tty_state.node == node && tty_state.crtc == crtc { let word = if enable_vrr { "enabling" } else { "disabling" }; if let Err(err) = surface.compositor.use_vrr(enable_vrr) { warn!( "output {:?}: error {} VRR: {err:?}", surface.name.connector, word ); } output_state .frame_clock .set_vrr(surface.compositor.vrr_enabled()); self.refresh_ipc_outputs(niri); return; } } } } pub fn on_output_config_changed(&mut self, niri: &mut Niri) { let _span = tracy_client::span!("Tty::on_output_config_changed"); // If we're inactive, we can't do anything, so just set a flag for later. if !self.session.is_active() { self.update_output_config_on_resume = true; return; } self.update_output_config_on_resume = false; // Figure out if we should disable laptop panels. let mut disable_laptop_panels = false; if niri.is_lid_closed { let config = self.config.borrow(); if !config.debug.keep_laptop_panel_on_when_lid_is_closed { // Check if any external monitor is connected. 'outer: for device in self.devices.values() { for (connector, _crtc) in device.drm_scanner.crtcs() { if !is_laptop_panel(&format_connector_name(connector)) { disable_laptop_panels = true; break 'outer; } } } } } let should_disable = |connector: &str| disable_laptop_panels && is_laptop_panel(connector); let mut to_disconnect = vec![]; let mut to_connect = vec![]; for (&node, device) in &mut self.devices { for (&crtc, surface) in device.surfaces.iter_mut() { let config = self .config .borrow() .outputs .find(&surface.name) .cloned() .unwrap_or_default(); if config.off || should_disable(&surface.name.connector) { to_disconnect.push((node, crtc)); continue; } // Check if we need to change the mode. let Some(connector) = device.drm_scanner.connectors().get(&surface.connector) else { error!("missing enabled connector in drm_scanner"); continue; }; let Some((mode, fallback)) = pick_mode(connector, config.mode) else { warn!("couldn't pick mode for enabled connector"); continue; }; let change_mode = surface.compositor.pending_mode() != mode; let vrr_enabled = surface.compositor.vrr_enabled(); let change_always_vrr = vrr_enabled != config.is_vrr_always_on(); let is_on_demand_vrr = config.is_vrr_on_demand(); if !change_mode && !change_always_vrr && !is_on_demand_vrr { continue; } let output = niri .global_space .outputs() .find(|output| { let tty_state: &TtyOutputState = output.user_data().get().unwrap(); tty_state.node == node && tty_state.crtc == crtc }) .cloned(); let Some(output) = output else { error!("missing output for crtc: {crtc:?}"); continue; }; let Some(output_state) = niri.output_state.get_mut(&output) else { error!("missing state for output {:?}", surface.name.connector); continue; }; if (is_on_demand_vrr && vrr_enabled != output_state.on_demand_vrr_enabled) || (!is_on_demand_vrr && change_always_vrr) { let vrr = !vrr_enabled; let word = if vrr { "enabling" } else { "disabling" }; if let Err(err) = surface.compositor.use_vrr(vrr) { warn!( "output {:?}: error {} VRR: {err:?}", surface.name.connector, word ); } output_state .frame_clock .set_vrr(surface.compositor.vrr_enabled()); } if change_mode { if fallback { let target = config.mode.unwrap(); warn!( "output {:?}: configured mode {}x{}{} could not be found, \ falling back to preferred", surface.name.connector, target.width, target.height, if let Some(refresh) = target.refresh { format!("@{refresh}") } else { String::new() }, ); } debug!( "output {:?}: picking mode: {mode:?}", surface.name.connector ); if let Err(err) = surface.compositor.use_mode(mode) { warn!("error changing mode: {err:?}"); continue; } let wl_mode = Mode::from(mode); output.change_current_state(Some(wl_mode), None, None, None); output.set_preferred(wl_mode); output_state.frame_clock = FrameClock::new( Some(refresh_interval(mode)), surface.compositor.vrr_enabled(), ); niri.output_resized(&output); } } for (connector, crtc) in device.drm_scanner.crtcs() { // Check if connected. if connector.state() != connector::State::Connected { continue; } // Check if already enabled. if device.surfaces.contains_key(&crtc) || device .non_desktop_connectors .contains(&(connector.handle(), crtc)) { continue; } let connector_name = format_connector_name(connector); let output_name = make_output_name( &device.drm, connector.handle(), connector_name, self.config.borrow().debug.disable_monitor_names, ); let config = self .config .borrow() .outputs .find(&output_name) .cloned() .unwrap_or_default(); if !(config.off || should_disable(&output_name.connector)) { to_connect.push((node, connector.clone(), crtc, output_name)); } } } for (node, crtc) in to_disconnect { self.connector_disconnected(niri, node, crtc); } // Sort by output name to get more predictable first focused output at initial compositor // startup, when multiple connectors appear at once. to_connect.sort_unstable_by(|a, b| a.3.compare(&b.3)); for (node, connector, crtc, _name) in to_connect { if let Err(err) = self.connector_connected(niri, node, connector, crtc) { warn!("error connecting connector: {err:?}"); } } self.refresh_ipc_outputs(niri); } pub fn get_device_from_node(&mut self, node: DrmNode) -> Option<&mut OutputDevice> { self.devices.get_mut(&node) } pub fn disconnected_connector_name_by_name_match(&self, target: &str) -> Option { for device in self.devices.values() { for (connector, crtc) in device.drm_scanner.crtcs() { // Check if connected. if connector.state() != connector::State::Connected { continue; } // Check if already enabled. if device.surfaces.contains_key(&crtc) || device .non_desktop_connectors .contains(&(connector.handle(), crtc)) { continue; } let connector_name = format_connector_name(connector); let output_name = make_output_name( &device.drm, connector.handle(), connector_name, self.config.borrow().debug.disable_monitor_names, ); if output_name.matches(target) { return Some(output_name); } } } None } } impl GammaProps { fn new(device: &DrmDevice, crtc: crtc::Handle) -> anyhow::Result { let mut gamma_lut = None; let mut gamma_lut_size = None; let props = device .get_properties(crtc) .context("error getting properties")?; for (prop, _) in props { let Ok(info) = device.get_property(prop) else { continue; }; let Ok(name) = info.name().to_str() else { continue; }; match name { "GAMMA_LUT" => { ensure!( matches!(info.value_type(), property::ValueType::Blob), "wrong GAMMA_LUT value type" ); gamma_lut = Some(prop); } "GAMMA_LUT_SIZE" => { ensure!( matches!(info.value_type(), property::ValueType::UnsignedRange(_, _)), "wrong GAMMA_LUT_SIZE value type" ); gamma_lut_size = Some(prop); } _ => (), } } let gamma_lut = gamma_lut.context("missing GAMMA_LUT property")?; let gamma_lut_size = gamma_lut_size.context("missing GAMMA_LUT_SIZE property")?; Ok(Self { crtc, gamma_lut, gamma_lut_size, previous_blob: None, }) } fn gamma_size(&self, device: &DrmDevice) -> anyhow::Result { let value = get_drm_property(device, self.crtc, self.gamma_lut_size) .context("missing GAMMA_LUT_SIZE property")?; Ok(value as u32) } fn set_gamma(&mut self, device: &DrmDevice, gamma: Option<&[u16]>) -> anyhow::Result<()> { let _span = tracy_client::span!("GammaProps::set_gamma"); let blob = if let Some(gamma) = gamma { let gamma_size = self .gamma_size(device) .context("error getting gamma size")? as usize; ensure!(gamma.len() == gamma_size * 3, "wrong gamma length"); #[allow(non_camel_case_types)] #[repr(C)] #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)] pub struct drm_color_lut { pub red: u16, pub green: u16, pub blue: u16, pub reserved: u16, } let (red, rest) = gamma.split_at(gamma_size); let (blue, green) = rest.split_at(gamma_size); let mut data = zip(zip(red, blue), green) .map(|((&red, &green), &blue)| drm_color_lut { red, green, blue, reserved: 0, }) .collect::>(); let data = cast_slice_mut(&mut data); let blob = drm_ffi::mode::create_property_blob(device.as_fd(), data) .context("error creating property blob")?; NonZeroU64::new(u64::from(blob.blob_id)) } else { None }; { let _span = tracy_client::span!("set_property"); let blob = blob.map(NonZeroU64::get).unwrap_or(0); device .set_property( self.crtc, self.gamma_lut, property::Value::Blob(blob).into(), ) .context("error setting GAMMA_LUT") .inspect_err(|_| { if blob != 0 { // Destroy the blob we just allocated. if let Err(err) = device.destroy_property_blob(blob) { warn!("error destroying GAMMA_LUT property blob: {err:?}"); } } })?; } if let Some(blob) = mem::replace(&mut self.previous_blob, blob) { if let Err(err) = device.destroy_property_blob(blob.get()) { warn!("error destroying previous GAMMA_LUT blob: {err:?}"); } } Ok(()) } fn restore_gamma(&self, device: &DrmDevice) -> anyhow::Result<()> { let _span = tracy_client::span!("GammaProps::restore_gamma"); let blob = self.previous_blob.map(NonZeroU64::get).unwrap_or(0); device .set_property( self.crtc, self.gamma_lut, property::Value::Blob(blob).into(), ) .context("error setting GAMMA_LUT")?; Ok(()) } } fn primary_node_from_config(config: &Config) -> Option<(DrmNode, DrmNode)> { let path = config.debug.render_drm_device.as_ref()?; debug!("attempting to use render node from config: {path:?}"); match DrmNode::from_path(path) { Ok(node) => { if node.ty() == NodeType::Render { match node.node_with_type(NodeType::Primary) { Some(Ok(primary_node)) => { return Some((primary_node, node)); } Some(Err(err)) => { warn!("error opening primary node for render node {path:?}: {err:?}"); } None => { warn!("error opening primary node for render node {path:?}"); } } } else { warn!("DRM node {path:?} is not a render node"); // Gracefully handle misconfiguration on regular desktop systems. if let Some(Ok(render_node)) = node.node_with_type(NodeType::Render) { return Some((node, render_node)); } warn!("could not get render node for DRM node {path:?}; proceeding anyway"); return Some((node, node)); } } Err(err) => { warn!("error opening {path:?} as DRM node: {err:?}"); } } None } fn surface_dmabuf_feedback( compositor: &GbmDrmCompositor, primary_formats: FormatSet, primary_render_node: DrmNode, surface_render_node: DrmNode, ) -> Result { let surface = compositor.surface(); let planes = surface.planes(); let primary_plane_formats = surface.plane_info().formats.clone(); let primary_or_overlay_plane_formats = primary_plane_formats .iter() .chain(planes.overlay.iter().flat_map(|p| p.formats.iter())) .copied() .collect::(); // 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 mut primary_scanout_formats = primary_plane_formats .intersection(&primary_formats) .copied() .collect::>(); let mut primary_or_overlay_scanout_formats = primary_or_overlay_plane_formats .intersection(&primary_formats) .copied() .collect::>(); // HACK: AMD iGPU + dGPU systems share some modifiers between the two, and yet cross-device // buffers produce a glitched scanout if the modifier is not Linear... if primary_render_node != surface_render_node { primary_scanout_formats.retain(|f| f.modifier == Modifier::Linear); primary_or_overlay_scanout_formats.retain(|f| f.modifier == Modifier::Linear); } let builder = DmabufFeedbackBuilder::new(primary_render_node.dev_id(), primary_formats); trace!( "primary scanout formats: {}, overlay adds: {}", primary_scanout_formats.len(), primary_or_overlay_scanout_formats.len() - primary_scanout_formats.len(), ); // Prefer the primary-plane-only formats, then primary-or-overlay-plane formats. This will // increase the chance of scanning out a client even with our disabled-by-default overlay // planes. let scanout = builder .clone() .add_preference_tranche( surface_render_node.dev_id(), Some(TrancheFlags::Scanout), primary_scanout_formats, ) .add_preference_tranche( surface_render_node.dev_id(), Some(TrancheFlags::Scanout), primary_or_overlay_scanout_formats, ) .build()?; // If this is the primary node surface, send scanout formats in both tranches to avoid // duplication. let render = if primary_render_node == surface_render_node { scanout.clone() } else { builder.build()? }; Ok(SurfaceDmabufFeedback { render, scanout }) } fn find_drm_property( drm: &DrmDevice, resource: impl ResourceHandle, name: &str, ) -> Option<(property::Handle, property::Info, property::RawValue)> { let props = match drm.get_properties(resource) { Ok(props) => props, Err(err) => { warn!("error getting properties: {err:?}"); return None; } }; props.into_iter().find_map(|(handle, value)| { let info = drm.get_property(handle).ok()?; let n = info.name().to_str().ok()?; (n == name).then_some((handle, info, value)) }) } fn get_drm_property( drm: &DrmDevice, resource: impl ResourceHandle, prop: property::Handle, ) -> Option { let props = match drm.get_properties(resource) { Ok(props) => props, Err(err) => { warn!("error getting properties: {err:?}"); return None; } }; props .into_iter() .find_map(|(handle, value)| (handle == prop).then_some(value)) } fn refresh_interval(mode: DrmMode) -> Duration { let clock = mode.clock() as u64; let htotal = mode.hsync().2 as u64; let vtotal = mode.vsync().2 as u64; let mut numerator = htotal * vtotal * 1_000_000; let mut denominator = clock; if mode.flags().contains(ModeFlags::INTERLACE) { denominator *= 2; } if mode.flags().contains(ModeFlags::DBLSCAN) { numerator *= 2; } if mode.vscan() > 1 { numerator *= mode.vscan() as u64; } let refresh_interval = (numerator + denominator / 2) / denominator; Duration::from_nanos(refresh_interval) } #[cfg(feature = "dbus")] fn suspend() -> anyhow::Result<()> { let conn = zbus::blocking::Connection::system().context("error connecting to system bus")?; conn.call_method( Some("org.freedesktop.login1"), "/org/freedesktop/login1", Some("org.freedesktop.login1.Manager"), "Suspend", &(true), ) .context("error suspending")?; Ok(()) } fn queue_estimated_vblank_timer( niri: &mut Niri, output: Output, target_presentation_time: Duration, ) { let output_state = niri.output_state.get_mut(&output).unwrap(); match mem::take(&mut output_state.redraw_state) { RedrawState::Idle => unreachable!(), RedrawState::Queued => (), RedrawState::WaitingForVBlank { .. } => unreachable!(), RedrawState::WaitingForEstimatedVBlank(token) | RedrawState::WaitingForEstimatedVBlankAndQueued(token) => { output_state.redraw_state = RedrawState::WaitingForEstimatedVBlank(token); return; } } let now = get_monotonic_time(); let mut duration = target_presentation_time.saturating_sub(now); // No use setting a zero timer, since we'll send frame callbacks anyway right after the call to // render(). This can happen for example with unknown presentation time from DRM. if duration.is_zero() { duration += output_state .frame_clock .refresh_interval() // Unknown refresh interval, i.e. winit backend. Would be good to estimate it somehow // but it's not that important for this code path. .unwrap_or(Duration::from_micros(16_667)); } trace!("queueing estimated vblank timer to fire in {duration:?}"); let timer = Timer::from_duration(duration); let token = niri .event_loop .insert_source(timer, move |_, _, data| { data.backend .tty() .on_estimated_vblank_timer(&mut data.niri, output.clone()); TimeoutAction::Drop }) .unwrap(); output_state.redraw_state = RedrawState::WaitingForEstimatedVBlank(token); } fn pick_mode( connector: &connector::Info, target: Option, ) -> Option<(control::Mode, bool)> { let mut mode = None; let mut fallback = false; if let Some(target) = target { let refresh = target.refresh.map(|r| (r * 1000.).round() as i32); for m in connector.modes() { if m.size() != (target.width, target.height) { continue; } // Interlaced modes don't appear to work. if m.flags().contains(ModeFlags::INTERLACE) { continue; } if let Some(refresh) = refresh { // If refresh is set, only pick modes with matching refresh. let wl_mode = Mode::from(*m); if wl_mode.refresh == refresh { mode = Some(m); } } else if let Some(curr) = mode { // If refresh isn't set, pick the mode with the highest refresh. if curr.vrefresh() < m.vrefresh() { mode = Some(m); } } else { mode = Some(m); } } if mode.is_none() { fallback = true; } } if mode.is_none() { // Pick a preferred mode. for m in connector.modes() { if !m.mode_type().contains(ModeTypeFlags::PREFERRED) { continue; } if let Some(curr) = mode { if curr.vrefresh() < m.vrefresh() { mode = Some(m); } } else { mode = Some(m); } } } if mode.is_none() { // Last attempt. mode = connector.modes().first(); } mode.map(|m| (*m, fallback)) } fn get_edid_info( device: &DrmDevice, connector: connector::Handle, ) -> anyhow::Result { let (_, info, value) = find_drm_property(device, connector, "EDID").context("no EDID property")?; let blob = info .value_type() .convert_value(value) .as_blob() .context("EDID was not blob type")?; let data = device .get_property_blob(blob) .context("error getting EDID blob value")?; libdisplay_info::info::Info::parse_edid(&data).context("error parsing EDID") } fn set_max_bpc(device: &DrmDevice, connector: connector::Handle, bpc: u64) -> anyhow::Result { let props = device .get_properties(connector) .context("error getting properties")?; for (prop, value) in props { let info = device .get_property(prop) .context("error getting property")?; if info.name().to_str() != Ok("max bpc") { continue; } let property::ValueType::UnsignedRange(min, max) = info.value_type() else { bail!("wrong property type") }; let bpc = bpc.clamp(min, max); let property::Value::UnsignedRange(value) = info.value_type().convert_value(value) else { bail!("wrong property type") }; if value == bpc { return Ok(bpc); } device .set_property(connector, prop, property::Value::UnsignedRange(bpc).into()) .context("error setting property")?; return Ok(bpc); } Err(anyhow!("couldn't find max bpc property")) } fn is_vrr_capable(device: &DrmDevice, connector: connector::Handle) -> Option { let (_, info, value) = find_drm_property(device, connector, "vrr_capable")?; info.value_type().convert_value(value).as_boolean() } pub fn set_gamma_for_crtc( device: &DrmDevice, crtc: crtc::Handle, ramp: Option<&[u16]>, ) -> anyhow::Result<()> { let _span = tracy_client::span!("set_gamma_for_crtc"); let info = device.get_crtc(crtc).context("error getting crtc info")?; let gamma_length = info.gamma_length() as usize; ensure!(gamma_length != 0, "setting gamma is not supported"); let mut temp; let ramp = if let Some(ramp) = ramp { ensure!(ramp.len() == gamma_length * 3, "wrong gamma length"); ramp } else { let _span = tracy_client::span!("generate linear gamma"); // The legacy API provides no way to reset the gamma, so set a linear one manually. temp = vec![0u16; gamma_length * 3]; let (red, rest) = temp.split_at_mut(gamma_length); let (green, blue) = rest.split_at_mut(gamma_length); let denom = gamma_length as u64 - 1; for (i, ((r, g), b)) in zip(zip(red, green), blue).enumerate() { let value = (0xFFFFu64 * i as u64 / denom) as u16; *r = value; *g = value; *b = value; } &temp }; let (red, ramp) = ramp.split_at(gamma_length); let (green, blue) = ramp.split_at(gamma_length); device .set_gamma(crtc, red, green, blue) .context("error setting gamma")?; Ok(()) } fn format_connector_name(connector: &connector::Info) -> String { format!( "{}-{}", connector.interface().as_str(), connector.interface_id(), ) } fn make_output_name( device: &DrmDevice, connector: connector::Handle, connector_name: String, disable_monitor_names: bool, ) -> OutputName { if disable_monitor_names { return OutputName { connector: connector_name, make: None, model: None, serial: None, }; } let info = get_edid_info(device, connector) .map_err(|err| warn!("error getting EDID info for {connector_name}: {err:?}")) .ok(); OutputName { connector: connector_name, make: info.as_ref().and_then(|info| info.make()), model: info.as_ref().and_then(|info| info.model()), serial: info.as_ref().and_then(|info| info.serial()), } }