From 46308bb58cee4849ba5fb4abd98bbb64c740f49e Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Thu, 20 Nov 2025 12:19:43 +0300 Subject: tty: Fall back to primary GPU for display-only devices This enables DisplayLink support. Co-authored-by: Christian Meissl Co-authored-by: Scott McKendry <39483124+scottmckendry@users.noreply.github.com> --- src/backend/tty.rs | 97 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 71 insertions(+), 26 deletions(-) (limited to 'src') diff --git a/src/backend/tty.rs b/src/backend/tty.rs index d0626cd5..693b7d6f 100644 --- a/src/backend/tty.rs +++ b/src/backend/tty.rs @@ -132,7 +132,8 @@ type GbmDrmCompositor = DrmCompositor< pub struct OutputDevice { token: RegistrationToken, - render_node: DrmNode, + // Can be None for display-only devices such as DisplayLink. + render_node: Option, drm_scanner: DrmScanner, surfaces: HashMap, known_crtcs: HashMap, @@ -140,6 +141,7 @@ pub struct OutputDevice { // See https://github.com/Smithay/smithay/issues/1102. drm: DrmDevice, gbm: GbmDevice, + // For display-only devices this will be the allocator from the primary device. allocator: GbmAllocator, pub drm_lease_state: Option, @@ -751,18 +753,39 @@ impl Tty { GbmDevice::new(device_fd) }?; - let display = unsafe { EGLDisplay::new(gbm.clone())? }; - let egl_device = EGLDevice::device_for_display(&display)?; + let mut try_initialize_gpu = || { + 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")?; + // Software EGL devices (e.g., llvmpipe/softpipe) are rejected for now. They have some + // problems (segfault on importing dmabufs from other renderers) and need to be + // excluded from some places like DRM leasing. + ensure!( + !egl_device.is_software(), + "software EGL renderers are skipped" + ); + + let render_node = egl_device + .try_get_render_node() + .ok() + .flatten() + .unwrap_or(node); + self.gpu_manager + .as_mut() + .add_node(render_node, gbm.clone()) + .context("error adding render node to GPU manager")?; + + Ok(render_node) + }; + + let render_node = try_initialize_gpu() + .inspect_err(|err| { + debug!("failed to initialize renderer, falling back to primary gpu: {err:?}"); + }) + .ok(); - if render_node == self.primary_render_node { + if render_node == Some(self.primary_render_node) { + let render_node = self.primary_render_node; debug!("this is the primary render node"); let mut renderer = self @@ -807,13 +830,14 @@ impl Tty { assert!(self.dmabuf_global.replace(dmabuf_global).is_none()); // Update the dmabuf feedbacks for all surfaces. - for device in self.devices.values_mut() { + for (node, device) in self.devices.iter_mut() { for surface in device.surfaces.values_mut() { match surface_dmabuf_feedback( &surface.compositor, primary_formats.clone(), self.primary_render_node, device.render_node, + *node, ) { Ok(feedback) => { surface.dmabuf_feedback = Some(feedback); @@ -826,8 +850,15 @@ impl Tty { } } + let allocator_gbm = if render_node.is_some() { + gbm.clone() + } else if let Some(primary_device) = self.devices.get(&self.primary_node) { + primary_device.gbm.clone() + } else { + bail!("no allocator available for device"); + }; let gbm_flags = GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT; - let allocator = GbmAllocator::new(gbm.clone(), gbm_flags); + let allocator = GbmAllocator::new(allocator_gbm, gbm_flags); let token = niri .event_loop @@ -1065,8 +1096,8 @@ impl Tty { lease_state.disable_global::(); } - if device.render_node == self.primary_render_node { - match self.gpu_manager.single_renderer(&device.render_node) { + if device.render_node == Some(self.primary_render_node) { + match self.gpu_manager.single_renderer(&self.primary_render_node) { Ok(mut renderer) => renderer.unbind_wl_display(), Err(err) => { warn!("error creating renderer during device removal: {err}"); @@ -1101,9 +1132,11 @@ impl Tty { } } - self.gpu_manager.as_mut().remove_node(&device.render_node); - // Trigger re-enumeration in order to remove the device from gpu_manager. - let _ = self.gpu_manager.devices(); + if let Some(render_node) = device.render_node { + self.gpu_manager.as_mut().remove_node(&render_node); + // Trigger re-enumeration in order to remove the device from gpu_manager. + let _ = self.gpu_manager.devices(); + } niri.event_loop.remove(device.token); @@ -1296,13 +1329,16 @@ impl Tty { output.user_data().insert_if_missing(|| PanelOrientation(x)); } - let renderer = self.gpu_manager.single_renderer(&device.render_node)?; + let render_node = device.render_node.unwrap_or(self.primary_render_node); + let renderer = self.gpu_manager.single_renderer(&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. // + // For display only devices, restrict to linear buffers for best compatibility. + // // 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 @@ -1311,6 +1347,10 @@ impl Tty { .iter() .copied() .filter(|format| { + if device.render_node.is_none() { + return format.modifier == Modifier::Linear; + } + let is_ccs = matches!( format.modifier, Modifier::I915_y_tiled_ccs @@ -1391,6 +1431,7 @@ impl Tty { primary_formats, self.primary_render_node, device.render_node, + node, ) { Ok(feedback) => { dmabuf_feedback = Some(feedback); @@ -1747,7 +1788,7 @@ impl Tty { let mut renderer = match self.gpu_manager.renderer( &self.primary_render_node, - &device.render_node, + &device.render_node.unwrap_or(self.primary_render_node), surface.compositor.format(), ) { Ok(renderer) => renderer, @@ -2099,7 +2140,7 @@ impl Tty { let device = self .devices .values() - .find(|d| d.render_node == self.primary_render_node); + .find(|d| d.render_node == Some(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)); @@ -2650,7 +2691,8 @@ fn surface_dmabuf_feedback( compositor: &GbmDrmCompositor, primary_formats: FormatSet, primary_render_node: DrmNode, - surface_render_node: DrmNode, + surface_render_node: Option, + surface_scanout_node: DrmNode, ) -> Result { let surface = compositor.surface(); let planes = surface.planes(); @@ -2675,7 +2717,10 @@ fn surface_dmabuf_feedback( // 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 { + // + // Also limit scan-out formats to Linear if we have a device without a render node (i.e. + // we're rendering on a different device). + if surface_render_node != Some(primary_render_node) { primary_scanout_formats.retain(|f| f.modifier == Modifier::Linear); primary_or_overlay_scanout_formats.retain(|f| f.modifier == Modifier::Linear); } @@ -2694,12 +2739,12 @@ fn surface_dmabuf_feedback( let scanout = builder .clone() .add_preference_tranche( - surface_render_node.dev_id(), + surface_scanout_node.dev_id(), Some(TrancheFlags::Scanout), primary_scanout_formats, ) .add_preference_tranche( - surface_render_node.dev_id(), + surface_scanout_node.dev_id(), Some(TrancheFlags::Scanout), primary_or_overlay_scanout_formats, ) @@ -2707,7 +2752,7 @@ fn surface_dmabuf_feedback( // 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 { + let render = if surface_render_node == Some(primary_render_node) { scanout.clone() } else { builder.build()? -- cgit