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.
This commit is contained in:
Victoria Brekenfeld 2025-05-21 22:07:46 +02:00 committed by Victoria Brekenfeld
parent 4c0c61e94b
commit 8194be30c6
8 changed files with 153 additions and 93 deletions

View file

@ -4,7 +4,7 @@ use crate::{
backend::render::{output_elements, CursorMode, GlMultiRenderer, CLEAR_COLOR}, backend::render::{output_elements, CursorMode, GlMultiRenderer, CLEAR_COLOR},
config::{AdaptiveSync, EdidProduct, OutputConfig, OutputState, ScreenFilter}, config::{AdaptiveSync, EdidProduct, OutputConfig, OutputState, ScreenFilter},
shell::Shell, shell::Shell,
utils::prelude::*, utils::{env::dev_list_var, prelude::*},
wayland::protocols::screencopy::Frame, wayland::protocols::screencopy::Frame,
}; };
@ -21,7 +21,7 @@ use smithay::{
compositor::{FrameError, FrameFlags}, compositor::{FrameError, FrameFlags},
exporter::gbm::GbmFramebufferExporter, exporter::gbm::GbmFramebufferExporter,
output::DrmOutputManager, output::DrmOutputManager,
DrmDevice, DrmDeviceFd, DrmEvent, DrmNode, DrmDevice, DrmDeviceFd, DrmEvent, DrmNode, NodeType,
}, },
egl::{context::ContextPriority, EGLContext, EGLDevice, EGLDisplay}, egl::{context::ContextPriority, EGLContext, EGLDevice, EGLDisplay},
session::Session, session::Session,
@ -312,7 +312,7 @@ impl State {
{ {
for (conn, maybe_crtc) in connectors { for (conn, maybe_crtc) in connectors {
match device.connector_added( match device.connector_added(
self.backend.kms().primary_node.as_ref(), self.backend.kms().primary_node.clone(),
conn, conn,
maybe_crtc, maybe_crtc,
(w, 0), (w, 0),
@ -336,7 +336,14 @@ impl State {
// TODO atomic commit all surfaces together and drop surfaces, if it fails due to bandwidth // 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 self.common
@ -406,7 +413,7 @@ impl State {
for (conn, maybe_crtc) in changes.added { for (conn, maybe_crtc) in changes.added {
match device.connector_added( match device.connector_added(
backend.primary_node.as_ref(), backend.primary_node.clone(),
conn, conn,
maybe_crtc, maybe_crtc,
(w, 0), (w, 0),
@ -466,7 +473,7 @@ impl State {
let drm_node = DrmNode::from_dev_id(dev)?; let drm_node = DrmNode::from_dev_id(dev)?;
let mut outputs_removed = Vec::new(); let mut outputs_removed = Vec::new();
let backend = self.backend.kms(); 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() { if let Some(mut leasing_global) = device.leasing_global.take() {
leasing_global.disable_global::<State>(); leasing_global.disable_global::<State>();
} }
@ -483,6 +490,12 @@ impl State {
.destroy_global::<State>(dh, socket.dmabuf_global); .destroy_global::<State>(dh, socket.dmabuf_global);
dh.remove_global::<State>(socket.drm_global); dh.remove_global::<State>(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 self.common
.output_configuration_state .output_configuration_state
@ -558,7 +571,7 @@ impl Device {
pub fn connector_added( pub fn connector_added(
&mut self, &mut self,
primary_node: Option<&DrmNode>, primary_node: Arc<RwLock<Option<DrmNode>>>,
conn: connector::Handle, conn: connector::Handle,
maybe_crtc: Option<crtc::Handle>, maybe_crtc: Option<crtc::Handle>,
position: (u32, u32), position: (u32, u32),
@ -623,7 +636,7 @@ impl Device {
&output, &output,
crtc, crtc,
conn, conn,
primary_node.copied().unwrap_or(self.render_node), primary_node,
self.dev_node, self.dev_node,
self.render_node, self.render_node,
evlh, evlh,

View file

@ -4,11 +4,12 @@ use crate::{
config::{AdaptiveSync, OutputState, ScreenFilter}, config::{AdaptiveSync, OutputState, ScreenFilter},
shell::Shell, shell::Shell,
state::BackendData, state::BackendData,
utils::prelude::*, utils::{env::dev_var, prelude::*},
}; };
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use calloop::LoopSignal; use calloop::LoopSignal;
use indexmap::IndexMap;
use render::gles::GbmGlowBackend; use render::gles::GbmGlowBackend;
use smithay::{ use smithay::{
backend::{ backend::{
@ -22,13 +23,13 @@ use smithay::{
libinput::{LibinputInputBackend, LibinputSessionInterface}, libinput::{LibinputInputBackend, LibinputSessionInterface},
renderer::{glow::GlowRenderer, multigpu::GpuManager}, renderer::{glow::GlowRenderer, multigpu::GpuManager},
session::{libseat::LibSeatSession, Event as SessionEvent, Session}, session::{libseat::LibSeatSession, Event as SessionEvent, Session},
udev::{all_gpus, primary_gpu, UdevBackend, UdevEvent}, udev::{primary_gpu, UdevBackend, UdevEvent},
}, },
output::Output, output::Output,
reexports::{ reexports::{
calloop::{Dispatcher, EventLoop, LoopHandle}, calloop::{Dispatcher, EventLoop, LoopHandle},
drm::{ drm::{
control::{crtc, Device as _}, control::{connector::Interface, crtc, Device as _},
Device as _, Device as _,
}, },
input::{self, Libinput}, input::{self, Libinput},
@ -65,9 +66,9 @@ use super::render::{init_shaders, output_elements, CursorMode, CLEAR_COLOR};
#[derive(Debug)] #[derive(Debug)]
pub struct KmsState { pub struct KmsState {
pub drm_devices: HashMap<DrmNode, Device>, pub drm_devices: IndexMap<DrmNode, Device>,
pub input_devices: HashMap<String, input::Device>, pub input_devices: HashMap<String, input::Device>,
pub primary_node: Option<DrmNode>, pub primary_node: Arc<RwLock<Option<DrmNode>>>,
// Mesa llvmpipe renderer, if supported and there are no render nodes // Mesa llvmpipe renderer, if supported and there are no render nodes
pub software_renderer: Option<GlowRenderer>, pub software_renderer: Option<GlowRenderer>,
pub api: GpuManager<GbmGlowBackend<DrmDeviceFd>>, pub api: GpuManager<GbmGlowBackend<DrmDeviceFd>>,
@ -90,24 +91,6 @@ pub fn init_backend(
let libinput_context = init_libinput(dh, &session, &event_loop.handle()) let libinput_context = init_libinput(dh, &session, &event_loop.handle())
.context("Failed to initialize libinput backend")?; .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 // watch for gpu events
let udev_dispatcher = init_udev(session.seat(), &event_loop.handle()) let udev_dispatcher = init_udev(session.seat(), &event_loop.handle())
.context("Failed to initialize udev connection")?; .context("Failed to initialize udev connection")?;
@ -134,10 +117,10 @@ pub fn init_backend(
// finish backend initialization // finish backend initialization
state.backend = BackendData::Kms(KmsState { state.backend = BackendData::Kms(KmsState {
drm_devices: HashMap::new(), drm_devices: IndexMap::new(),
input_devices: HashMap::new(), input_devices: HashMap::new(),
primary_node: primary, primary_node: Arc::new(RwLock::new(None)),
software_renderer, software_renderer: None,
api: GpuManager::new(GbmGlowBackend::new()).context("Failed to initialize gpu backend")?, api: GpuManager::new(GbmGlowBackend::new()).context("Failed to initialize gpu backend")?,
session, session,
@ -146,9 +129,6 @@ pub fn init_backend(
syncobj_state: None, syncobj_state: None,
}); });
// start x11
state.launch_xwayland(primary);
// manually add already present gpus // manually add already present gpus
for (dev, path) in udev_dispatcher.as_source_ref().device_list() { for (dev, path) in udev_dispatcher.as_source_ref().device_list() {
if let Err(err) = state.device_added(dev, path.into(), dh) { 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) { // start x11
let kms = match &mut state.backend { let primary = state.backend.kms().primary_node.read().unwrap().clone();
BackendData::Kms(kms) => kms, state.launch_xwayland(primary);
_ => 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::<State>(&dh, import_device);
kms.syncobj_state = Some(syncobj_state);
}
}
}
}
Ok(()) Ok(())
} }
@ -217,32 +182,51 @@ fn init_libinput(
Ok(libinput_context) Ok(libinput_context)
} }
fn determine_primary_gpu(seat: String) -> Option<DrmNode> { fn determine_boot_gpu(seat: String) -> Option<DrmNode> {
if let Some(node) = std::env::var("COSMIC_RENDER_DEVICE") let primary_node = primary_gpu(&seat)
.ok() .ok()
.and_then(|x| DrmNode::from_path(x).ok()) .flatten()
{ .and_then(|x| DrmNode::from_path(x).ok());
Some(node) primary_node.and_then(|x| x.node_with_type(NodeType::Render).and_then(Result::ok))
} 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);
}
}
None fn determine_primary_gpu(
}) drm_devices: &IndexMap<DrmNode, Device>,
seat: String,
) -> Result<Option<DrmNode>> {
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 /// Create `GlowRenderer` for `EGL_MESA_device_software` device, if present
@ -375,6 +359,55 @@ impl State {
} }
impl KmsState { 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::<State>(&dh, import_device);
self.syncobj_state = Some(syncobj_state);
}
return Ok(());
}
}
}
if let Some(old_state) = self.syncobj_state.take() {
dh.remove_global::<State>(old_state.into_global());
}
}
Ok(())
}
pub fn switch_vt(&mut self, num: i32) -> Result<(), anyhow::Error> { pub fn switch_vt(&mut self, num: i32) -> Result<(), anyhow::Error> {
self.session.change_vt(num).map_err(Into::into) self.session.change_vt(num).map_err(Into::into)
} }
@ -459,7 +492,7 @@ impl KmsState {
let mut used_devices = HashSet::new(); let mut used_devices = HashSet::new();
for device in self.drm_devices.values_mut() { 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() { if device.egl.is_none() {
let egl = init_egl(&device.gbm).context("Failed to create EGL context")?; let egl = init_egl(&device.gbm).context("Failed to create EGL context")?;
let mut renderer = unsafe { let mut renderer = unsafe {
@ -495,7 +528,7 @@ impl KmsState {
} }
// trigger re-evaluation... urgh // 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); let _ = self.api.single_renderer(primary_node);
} }
@ -654,7 +687,7 @@ impl KmsState {
if !test_only { if !test_only {
for (conn, crtc) in new_pairings { for (conn, crtc) in new_pairings {
let (output, _) = device.connector_added( let (output, _) = device.connector_added(
self.primary_node.as_ref(), self.primary_node.clone(),
conn, conn,
Some(crtc), Some(crtc),
(w, 0), (w, 0),

View file

@ -119,7 +119,7 @@ pub struct Surface {
pub struct SurfaceThreadState { pub struct SurfaceThreadState {
// rendering // rendering
api: GpuManager<GbmGlowBackend<DrmDeviceFd>>, api: GpuManager<GbmGlowBackend<DrmDeviceFd>>,
primary_node: DrmNode, primary_node: Arc<RwLock<Option<DrmNode>>>,
target_node: DrmNode, target_node: DrmNode,
active: Arc<AtomicBool>, active: Arc<AtomicBool>,
vrr_mode: AdaptiveSync, vrr_mode: AdaptiveSync,
@ -228,7 +228,7 @@ impl Surface {
output: &Output, output: &Output,
crtc: crtc::Handle, crtc: crtc::Handle,
connector: connector::Handle, connector: connector::Handle,
primary_node: DrmNode, primary_node: Arc<RwLock<Option<DrmNode>>>,
dev_node: DrmNode, dev_node: DrmNode,
target_node: DrmNode, target_node: DrmNode,
evlh: &LoopHandle<'static, State>, evlh: &LoopHandle<'static, State>,
@ -469,7 +469,7 @@ impl Drop for Surface {
fn surface_thread( fn surface_thread(
output: Output, output: Output,
primary_node: DrmNode, primary_node: Arc<RwLock<Option<DrmNode>>>,
target_node: DrmNode, target_node: DrmNode,
shell: Arc<RwLock<Shell>>, shell: Arc<RwLock<Shell>>,
active: Arc<AtomicBool>, active: Arc<AtomicBool>,
@ -956,7 +956,11 @@ impl SurfaceThreadState {
let render_node = render_node_for_output( let render_node = render_node_for_output(
self.mirroring.as_ref().unwrap_or(&self.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.target_node,
&*self.shell.read().unwrap(), &*self.shell.read().unwrap(),
); );
@ -1821,6 +1825,8 @@ fn source_node_for_surface(w: &WlSurface) -> Option<DrmNode> {
.flatten() .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( fn render_node_for_output(
output: &Output, output: &Output,
primary_node: &DrmNode, primary_node: &DrmNode,

View file

@ -671,7 +671,7 @@ impl State {
ClientState { ClientState {
compositor_client_state: CompositorClientState::default(), compositor_client_state: CompositorClientState::default(),
advertised_drm_node: match &self.backend { 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, _ => None,
}, },
privileged: !enable_wayland_security(), privileged: !enable_wayland_security(),

View file

@ -104,7 +104,7 @@ pub fn screenshot_window(state: &mut State, surface: &CosmicSurface) {
.backend .backend
.offscreen_renderer(|kms| { .offscreen_renderer(|kms| {
advertised_node_for_surface(&wl_surface, &state.common.display_handle) 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") .with_context(|| "Failed to get renderer for screenshot")
.and_then(|renderer| match renderer { .and_then(|renderer| match renderer {

View file

@ -12,7 +12,7 @@ impl BufferHandler for State {
if let BackendData::Kms(kms_state) = &mut self.backend { if let BackendData::Kms(kms_state) = &mut self.backend {
for device in kms_state.drm_devices.values_mut() { for device in kms_state.drm_devices.values_mut() {
if device.active_buffers.remove(&buffer.downgrade()) { 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() { if let Err(err) = kms_state.refresh_used_devices() {
warn!(?err, "Failed to init devices."); warn!(?err, "Failed to init devices.");
}; };

View file

@ -355,7 +355,10 @@ fn constraints_for_output(output: &Output, backend: &mut BackendData) -> Option<
}; };
let mut renderer = backend 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(); .unwrap();
Some(constraints_for_renderer(mode, renderer.as_mut())) Some(constraints_for_renderer(mode, renderer.as_mut()))
} }
@ -376,7 +379,7 @@ fn constraints_for_toplevel(
}) })
.flatten(); .flatten();
dma_node.or(kms.primary_node) dma_node.or(*kms.primary_node.read().unwrap())
}) })
.unwrap(); .unwrap();

View file

@ -337,7 +337,9 @@ pub fn render_workspace_to_buffer(
let common = &mut state.common; let common = &mut state.common;
let renderer = match state.backend.offscreen_renderer(|kms| { 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) let target_node = get_dmabuf(&buffer)
.ok() .ok()
.and_then(|dma| dma.node()) .and_then(|dma| dma.node())
@ -591,7 +593,7 @@ pub fn render_window_to_buffer(
}) })
.flatten() .flatten()
}) })
.or(kms.primary_node) .or(*kms.primary_node.read().unwrap())
}) { }) {
Ok(renderer) => renderer, Ok(renderer) => renderer,
Err(err) => { Err(err) => {
@ -745,7 +747,10 @@ pub fn render_cursor_to_buffer(
} }
let common = &mut state.common; 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, Ok(renderer) => renderer,
Err(err) => { Err(err) => {
warn!(?err, "Couldn't use node for screencopy"); warn!(?err, "Couldn't use node for screencopy");