From 8194be30c6581cbe74c5249a2e27f1298cb76cdd Mon Sep 17 00:00:00 2001 From: Victoria Brekenfeld Date: Wed, 21 May 2025 22:07:46 +0200 Subject: [PATCH] kms: Allow updating the primary node Add more sophisticated code to handle the primary node disappearing. Also overhaul the selection logic to respect our allow/deny-list and prefer devices with built-in connectors before using the boot gpu. This will also allow triggering a primary node switch at runtime for debugging purposes in the future. --- src/backend/kms/device.rs | 29 +++- src/backend/kms/mod.rs | 179 +++++++++++++--------- src/backend/kms/surface/mod.rs | 14 +- src/state.rs | 2 +- src/utils/screenshot.rs | 2 +- src/wayland/handlers/buffer.rs | 2 +- src/wayland/handlers/screencopy/mod.rs | 7 +- src/wayland/handlers/screencopy/render.rs | 11 +- 8 files changed, 153 insertions(+), 93 deletions(-) diff --git a/src/backend/kms/device.rs b/src/backend/kms/device.rs index 2e26176b..3b9a2964 100644 --- a/src/backend/kms/device.rs +++ b/src/backend/kms/device.rs @@ -4,7 +4,7 @@ use crate::{ backend::render::{output_elements, CursorMode, GlMultiRenderer, CLEAR_COLOR}, config::{AdaptiveSync, EdidProduct, OutputConfig, OutputState, ScreenFilter}, shell::Shell, - utils::prelude::*, + utils::{env::dev_list_var, prelude::*}, wayland::protocols::screencopy::Frame, }; @@ -21,7 +21,7 @@ use smithay::{ compositor::{FrameError, FrameFlags}, exporter::gbm::GbmFramebufferExporter, output::DrmOutputManager, - DrmDevice, DrmDeviceFd, DrmEvent, DrmNode, + DrmDevice, DrmDeviceFd, DrmEvent, DrmNode, NodeType, }, egl::{context::ContextPriority, EGLContext, EGLDevice, EGLDisplay}, session::Session, @@ -312,7 +312,7 @@ impl State { { for (conn, maybe_crtc) in connectors { match device.connector_added( - self.backend.kms().primary_node.as_ref(), + self.backend.kms().primary_node.clone(), conn, maybe_crtc, (w, 0), @@ -336,7 +336,14 @@ impl State { // TODO atomic commit all surfaces together and drop surfaces, if it fails due to bandwidth - self.backend.kms().drm_devices.insert(drm_node, device); + let kms = self.backend.kms(); + let was_empty = kms.drm_devices.is_empty(); + kms.drm_devices.insert(drm_node, device); + if was_empty { + if let Err(err) = kms.select_primary_gpu(dh) { + warn!("Failed to determine a new primary gpu: {}", err); + } + } } self.common @@ -406,7 +413,7 @@ impl State { for (conn, maybe_crtc) in changes.added { match device.connector_added( - backend.primary_node.as_ref(), + backend.primary_node.clone(), conn, maybe_crtc, (w, 0), @@ -466,7 +473,7 @@ impl State { let drm_node = DrmNode::from_dev_id(dev)?; let mut outputs_removed = Vec::new(); let backend = self.backend.kms(); - if let Some(mut device) = backend.drm_devices.remove(&drm_node) { + if let Some(mut device) = backend.drm_devices.shift_remove(&drm_node) { if let Some(mut leasing_global) = device.leasing_global.take() { leasing_global.disable_global::(); } @@ -483,6 +490,12 @@ impl State { .destroy_global::(dh, socket.dmabuf_global); dh.remove_global::(socket.drm_global); } + let was_primary = *backend.primary_node.read().unwrap() == Some(device.render_node); + if was_primary { + if let Err(err) = backend.select_primary_gpu(dh) { + warn!("Failed to determine a new primary gpu: {}", err); + } + } } self.common .output_configuration_state @@ -558,7 +571,7 @@ impl Device { pub fn connector_added( &mut self, - primary_node: Option<&DrmNode>, + primary_node: Arc>>, conn: connector::Handle, maybe_crtc: Option, position: (u32, u32), @@ -623,7 +636,7 @@ impl Device { &output, crtc, conn, - primary_node.copied().unwrap_or(self.render_node), + primary_node, self.dev_node, self.render_node, evlh, diff --git a/src/backend/kms/mod.rs b/src/backend/kms/mod.rs index 624ed077..4991f933 100644 --- a/src/backend/kms/mod.rs +++ b/src/backend/kms/mod.rs @@ -4,11 +4,12 @@ use crate::{ config::{AdaptiveSync, OutputState, ScreenFilter}, shell::Shell, state::BackendData, - utils::prelude::*, + utils::{env::dev_var, prelude::*}, }; use anyhow::{Context, Result}; use calloop::LoopSignal; +use indexmap::IndexMap; use render::gles::GbmGlowBackend; use smithay::{ backend::{ @@ -22,13 +23,13 @@ use smithay::{ libinput::{LibinputInputBackend, LibinputSessionInterface}, renderer::{glow::GlowRenderer, multigpu::GpuManager}, session::{libseat::LibSeatSession, Event as SessionEvent, Session}, - udev::{all_gpus, primary_gpu, UdevBackend, UdevEvent}, + udev::{primary_gpu, UdevBackend, UdevEvent}, }, output::Output, reexports::{ calloop::{Dispatcher, EventLoop, LoopHandle}, drm::{ - control::{crtc, Device as _}, + control::{connector::Interface, crtc, Device as _}, Device as _, }, input::{self, Libinput}, @@ -65,9 +66,9 @@ use super::render::{init_shaders, output_elements, CursorMode, CLEAR_COLOR}; #[derive(Debug)] pub struct KmsState { - pub drm_devices: HashMap, + pub drm_devices: IndexMap, pub input_devices: HashMap, - pub primary_node: Option, + pub primary_node: Arc>>, // Mesa llvmpipe renderer, if supported and there are no render nodes pub software_renderer: Option, pub api: GpuManager>, @@ -90,24 +91,6 @@ pub fn init_backend( let libinput_context = init_libinput(dh, &session, &event_loop.handle()) .context("Failed to initialize libinput backend")?; - // get our primary gpu - let primary = determine_primary_gpu(session.seat()); - if let Some(primary) = primary.as_ref() { - info!("Using {} as primary gpu for rendering.", primary); - } - - let software_renderer = if primary.is_none() { - match software_renderer() { - Ok(renderer) => Some(renderer), - Err(err) => { - error!(?err, "Failed to initialize software EGL renderer."); - None - } - } - } else { - None - }; - // watch for gpu events let udev_dispatcher = init_udev(session.seat(), &event_loop.handle()) .context("Failed to initialize udev connection")?; @@ -134,10 +117,10 @@ pub fn init_backend( // finish backend initialization state.backend = BackendData::Kms(KmsState { - drm_devices: HashMap::new(), + drm_devices: IndexMap::new(), input_devices: HashMap::new(), - primary_node: primary, - software_renderer, + primary_node: Arc::new(RwLock::new(None)), + software_renderer: None, api: GpuManager::new(GbmGlowBackend::new()).context("Failed to initialize gpu backend")?, session, @@ -146,9 +129,6 @@ pub fn init_backend( syncobj_state: None, }); - // start x11 - state.launch_xwayland(primary); - // manually add already present gpus for (dev, path) in udev_dispatcher.as_source_ref().device_list() { if let Err(err) = state.device_added(dev, path.into(), dh) { @@ -156,24 +136,9 @@ pub fn init_backend( } } - if !crate::utils::env::bool_var("COSMIC_DISABLE_SYNCOBJ").unwrap_or(false) { - let kms = match &mut state.backend { - BackendData::Kms(kms) => kms, - _ => unreachable!(), - }; - if let Some(primary_node) = kms - .primary_node - .and_then(|node| node.node_with_type(NodeType::Primary).and_then(|x| x.ok())) - { - if let Some(device) = kms.drm_devices.get(&primary_node) { - let import_device = device.drm.device().device_fd().clone(); - if supports_syncobj_eventfd(&import_device) { - let syncobj_state = DrmSyncobjState::new::(&dh, import_device); - kms.syncobj_state = Some(syncobj_state); - } - } - } - } + // start x11 + let primary = state.backend.kms().primary_node.read().unwrap().clone(); + state.launch_xwayland(primary); Ok(()) } @@ -217,32 +182,51 @@ fn init_libinput( Ok(libinput_context) } -fn determine_primary_gpu(seat: String) -> Option { - if let Some(node) = std::env::var("COSMIC_RENDER_DEVICE") +fn determine_boot_gpu(seat: String) -> Option { + let primary_node = primary_gpu(&seat) .ok() - .and_then(|x| DrmNode::from_path(x).ok()) - { - Some(node) - } else { - let primary_node = primary_gpu(&seat) - .ok() - .flatten() - .and_then(|x| DrmNode::from_path(x).ok()); - primary_node - .and_then(|x| x.node_with_type(NodeType::Render).and_then(Result::ok)) - .or_else(|| { - for dev in all_gpus(&seat).expect("Failed to query gpus") { - if let Some(node) = DrmNode::from_path(dev) - .ok() - .and_then(|x| x.node_with_type(NodeType::Render).and_then(Result::ok)) - { - return Some(node); - } - } + .flatten() + .and_then(|x| DrmNode::from_path(x).ok()); + primary_node.and_then(|x| x.node_with_type(NodeType::Render).and_then(Result::ok)) +} - None - }) +fn determine_primary_gpu( + drm_devices: &IndexMap, + seat: String, +) -> Result> { + if let Some(device) = dev_var("COSMIC_RENDER_DEVICE") { + if let Some(node) = drm_devices + .values() + .find_map(|dev| device.matches(&dev.render_node).then_some(dev.render_node)) + { + return Ok(Some(node)); + } } + + // try to find builtin display + for dev in drm_devices.values() { + if dev.surfaces.values().any(|s| { + if let Some(conn_info) = dev.drm.device().get_connector(s.connector, false).ok() { + let i = conn_info.interface(); + i == Interface::EmbeddedDisplayPort || i == Interface::LVDS || i == Interface::DSI + } else { + false + } + }) { + return Ok(Some(dev.render_node)); + } + } + + // else try to find the boot gpu + let boot = determine_boot_gpu(seat); + if let Some(boot) = boot { + if drm_devices.values().any(|dev| dev.render_node == boot) { + return Ok(Some(boot)); + } + } + + // else just take the first + Ok(drm_devices.values().next().map(|dev| dev.render_node)) } /// Create `GlowRenderer` for `EGL_MESA_device_software` device, if present @@ -375,6 +359,55 @@ impl State { } impl KmsState { + fn select_primary_gpu(&mut self, dh: &DisplayHandle) -> Result<()> { + // We don't have to check the allow/blocklist here, + // as any disallowed devices won't be in `self.drm_devices`. + + let mut primary_node = self.primary_node.write().unwrap(); + let _ = primary_node.take(); // if we error don't leave an old node in place + *primary_node = determine_primary_gpu(&self.drm_devices, self.session.seat())?; + + if let Some(node) = *primary_node { + info!("Using {} as primary gpu for rendering.", node); + self.software_renderer.take(); + } else if self.software_renderer.is_none() { + info!("Failed to find a suitable gpu, using software renderingr"); + self.software_renderer = match software_renderer() { + Ok(renderer) => Some(renderer), + Err(err) => { + error!(?err, "Failed to initialize software EGL renderer."); + None + } + }; + } + + if !crate::utils::env::bool_var("COSMIC_DISABLE_SYNCOBJ").unwrap_or(false) { + if let Some(primary_node) = primary_node + .as_ref() + .and_then(|node| node.node_with_type(NodeType::Primary).and_then(|x| x.ok())) + { + if let Some(device) = self.drm_devices.get(&primary_node) { + let import_device = device.drm.device().device_fd().clone(); + if supports_syncobj_eventfd(&import_device) { + if let Some(state) = self.syncobj_state.as_mut() { + state.update_device(import_device); + } else { + let syncobj_state = DrmSyncobjState::new::(&dh, import_device); + self.syncobj_state = Some(syncobj_state); + } + return Ok(()); + } + } + } + + if let Some(old_state) = self.syncobj_state.take() { + dh.remove_global::(old_state.into_global()); + } + } + + Ok(()) + } + pub fn switch_vt(&mut self, num: i32) -> Result<(), anyhow::Error> { self.session.change_vt(num).map_err(Into::into) } @@ -459,7 +492,7 @@ impl KmsState { let mut used_devices = HashSet::new(); for device in self.drm_devices.values_mut() { - if device.in_use(self.primary_node.as_ref()) { + if device.in_use(self.primary_node.read().unwrap().as_ref()) { if device.egl.is_none() { let egl = init_egl(&device.gbm).context("Failed to create EGL context")?; let mut renderer = unsafe { @@ -495,7 +528,7 @@ impl KmsState { } // trigger re-evaluation... urgh - if let Some(primary_node) = self.primary_node.as_ref() { + if let Some(primary_node) = self.primary_node.read().unwrap().as_ref() { let _ = self.api.single_renderer(primary_node); } @@ -654,7 +687,7 @@ impl KmsState { if !test_only { for (conn, crtc) in new_pairings { let (output, _) = device.connector_added( - self.primary_node.as_ref(), + self.primary_node.clone(), conn, Some(crtc), (w, 0), diff --git a/src/backend/kms/surface/mod.rs b/src/backend/kms/surface/mod.rs index 5122495a..e6802dcc 100644 --- a/src/backend/kms/surface/mod.rs +++ b/src/backend/kms/surface/mod.rs @@ -119,7 +119,7 @@ pub struct Surface { pub struct SurfaceThreadState { // rendering api: GpuManager>, - primary_node: DrmNode, + primary_node: Arc>>, target_node: DrmNode, active: Arc, vrr_mode: AdaptiveSync, @@ -228,7 +228,7 @@ impl Surface { output: &Output, crtc: crtc::Handle, connector: connector::Handle, - primary_node: DrmNode, + primary_node: Arc>>, dev_node: DrmNode, target_node: DrmNode, evlh: &LoopHandle<'static, State>, @@ -469,7 +469,7 @@ impl Drop for Surface { fn surface_thread( output: Output, - primary_node: DrmNode, + primary_node: Arc>>, target_node: DrmNode, shell: Arc>, active: Arc, @@ -956,7 +956,11 @@ impl SurfaceThreadState { let render_node = render_node_for_output( self.mirroring.as_ref().unwrap_or(&self.output), - &self.primary_node, + self.primary_node + .read() + .unwrap() + .as_ref() + .unwrap_or(&self.target_node), &self.target_node, &*self.shell.read().unwrap(), ); @@ -1821,6 +1825,8 @@ fn source_node_for_surface(w: &WlSurface) -> Option { .flatten() } +// TODO: Introduce can_shared_dmabuf_framebuffer for cases where we might select another gpu +// and composite on target if not possible to finally get rid of "primary" fn render_node_for_output( output: &Output, primary_node: &DrmNode, diff --git a/src/state.rs b/src/state.rs index beb606dd..c03c817d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -671,7 +671,7 @@ impl State { ClientState { compositor_client_state: CompositorClientState::default(), advertised_drm_node: match &self.backend { - BackendData::Kms(kms_state) => kms_state.primary_node, + BackendData::Kms(kms_state) => *kms_state.primary_node.read().unwrap(), _ => None, }, privileged: !enable_wayland_security(), diff --git a/src/utils/screenshot.rs b/src/utils/screenshot.rs index 6c063e8d..95c46321 100644 --- a/src/utils/screenshot.rs +++ b/src/utils/screenshot.rs @@ -104,7 +104,7 @@ pub fn screenshot_window(state: &mut State, surface: &CosmicSurface) { .backend .offscreen_renderer(|kms| { advertised_node_for_surface(&wl_surface, &state.common.display_handle) - .or(kms.primary_node) + .or(*kms.primary_node.read().unwrap()) }) .with_context(|| "Failed to get renderer for screenshot") .and_then(|renderer| match renderer { diff --git a/src/wayland/handlers/buffer.rs b/src/wayland/handlers/buffer.rs index b638d1fb..8762c22e 100644 --- a/src/wayland/handlers/buffer.rs +++ b/src/wayland/handlers/buffer.rs @@ -12,7 +12,7 @@ impl BufferHandler for State { if let BackendData::Kms(kms_state) = &mut self.backend { for device in kms_state.drm_devices.values_mut() { if device.active_buffers.remove(&buffer.downgrade()) { - if !device.in_use(kms_state.primary_node.as_ref()) { + if !device.in_use(kms_state.primary_node.read().unwrap().as_ref()) { if let Err(err) = kms_state.refresh_used_devices() { warn!(?err, "Failed to init devices."); }; diff --git a/src/wayland/handlers/screencopy/mod.rs b/src/wayland/handlers/screencopy/mod.rs index 90673f8e..ec6cd6c6 100644 --- a/src/wayland/handlers/screencopy/mod.rs +++ b/src/wayland/handlers/screencopy/mod.rs @@ -355,7 +355,10 @@ fn constraints_for_output(output: &Output, backend: &mut BackendData) -> Option< }; let mut renderer = backend - .offscreen_renderer(|kms| kms.target_node_for_output(&output).or(kms.primary_node)) + .offscreen_renderer(|kms| { + kms.target_node_for_output(&output) + .or(*kms.primary_node.read().unwrap()) + }) .unwrap(); Some(constraints_for_renderer(mode, renderer.as_mut())) } @@ -376,7 +379,7 @@ fn constraints_for_toplevel( }) .flatten(); - dma_node.or(kms.primary_node) + dma_node.or(*kms.primary_node.read().unwrap()) }) .unwrap(); diff --git a/src/wayland/handlers/screencopy/render.rs b/src/wayland/handlers/screencopy/render.rs index c2eb7c2e..d644a54a 100644 --- a/src/wayland/handlers/screencopy/render.rs +++ b/src/wayland/handlers/screencopy/render.rs @@ -337,7 +337,9 @@ pub fn render_workspace_to_buffer( let common = &mut state.common; let renderer = match state.backend.offscreen_renderer(|kms| { - let render_node = kms.target_node_for_output(&output).or(kms.primary_node)?; + let render_node = kms + .target_node_for_output(&output) + .or(*kms.primary_node.read().unwrap())?; let target_node = get_dmabuf(&buffer) .ok() .and_then(|dma| dma.node()) @@ -591,7 +593,7 @@ pub fn render_window_to_buffer( }) .flatten() }) - .or(kms.primary_node) + .or(*kms.primary_node.read().unwrap()) }) { Ok(renderer) => renderer, Err(err) => { @@ -745,7 +747,10 @@ pub fn render_cursor_to_buffer( } let common = &mut state.common; - let renderer = match state.backend.offscreen_renderer(|kms| kms.primary_node) { + let renderer = match state + .backend + .offscreen_renderer(|kms| *kms.primary_node.read().unwrap()) + { Ok(renderer) => renderer, Err(err) => { warn!(?err, "Couldn't use node for screencopy");