From 5cd31e5730160f9162502d2388dc00400c7a87ae Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Wed, 3 Jan 2024 18:16:20 +0400 Subject: Implement multi-GPU support Rendering always happens on the primary GPU. --- src/backend/mod.rs | 10 +- src/backend/tty.rs | 627 +++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 444 insertions(+), 193 deletions(-) (limited to 'src/backend') diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 54b504ca..66b05f77 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -5,6 +5,7 @@ use std::time::Duration; use smithay::backend::allocator::dmabuf::Dmabuf; use smithay::backend::renderer::gles::GlesRenderer; use smithay::output::Output; +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use crate::input::CompositorMod; use crate::Niri; @@ -102,6 +103,13 @@ impl Backend { } } + pub fn early_import(&mut self, surface: &WlSurface) { + match self { + Backend::Tty(tty) => tty.early_import(surface), + Backend::Winit(_) => (), + } + } + #[cfg_attr(not(feature = "dbus"), allow(unused))] pub fn connectors(&self) -> Arc>> { match self { @@ -116,7 +124,7 @@ impl Backend { ) -> Option> { match self { - Backend::Tty(tty) => tty.gbm_device(), + Backend::Tty(tty) => tty.primary_gbm_device(), Backend::Winit(_) => None, } } diff --git a/src/backend/tty.rs b/src/backend/tty.rs index 34798008..067b3e3e 100644 --- a/src/backend/tty.rs +++ b/src/backend/tty.rs @@ -1,23 +1,27 @@ use std::cell::RefCell; use std::collections::{HashMap, HashSet}; -use std::mem; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::time::Duration; +use std::{io, mem}; use anyhow::{anyhow, Context}; use libc::dev_t; -use smithay::backend::allocator::dmabuf::Dmabuf; +use smithay::backend::allocator::dmabuf::{Dmabuf, DmabufAllocator}; use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice}; -use smithay::backend::allocator::Fourcc; +use smithay::backend::allocator::{Format, Fourcc}; use smithay::backend::drm::compositor::{DrmCompositor, PrimaryPlaneElement}; -use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventMetadata, DrmEventTime}; +use smithay::backend::drm::{ + DrmDevice, DrmDeviceFd, DrmEvent, DrmEventMetadata, DrmEventTime, DrmNode, NodeType, +}; use smithay::backend::egl::context::ContextPriority; -use smithay::backend::egl::{EGLContext, EGLDisplay}; +use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay}; use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface}; use smithay::backend::renderer::gles::{Capability, GlesRenderer, GlesTexture}; -use smithay::backend::renderer::{DebugFlags, ImportDma, ImportEgl}; +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}; @@ -31,6 +35,7 @@ use smithay::reexports::drm::control::{ 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_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner}; @@ -41,6 +46,7 @@ use wayland_protocols::wp::presentation_time::server::wp_presentation_feedback; use super::RenderResult; use crate::config::Config; use crate::niri::{RedrawState, State}; +use crate::render_helpers::AsGlesRenderer; use crate::utils::get_monotonic_time; use crate::Niri; @@ -51,14 +57,42 @@ pub struct Tty { session: LibSeatSession, udev_dispatcher: Dispatcher<'static, UdevBackend, State>, libinput: Libinput, - primary_gpu_path: PathBuf, + 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, - output_device: Option, + // The allocator for the primary GPU. It is only `Some()` if we have a device corresponding to + // the primary GPU. + primary_allocator: Option>>, connectors: Arc>>, } +pub type TtyRenderer<'render, 'alloc> = MultiRenderer< + 'render, + 'render, + 'alloc, + GbmGlesBackend, + GbmGlesBackend, +>; + +pub type TtyFrame<'render, 'alloc, 'frame> = MultiFrame< + 'render, + 'render, + 'alloc, + 'frame, + GbmGlesBackend, + GbmGlesBackend, +>; + +pub type TtyRendererError<'render, 'alloc> = as Renderer>::Error; + type GbmDrmCompositor = DrmCompositor< GbmAllocator, GbmDevice, @@ -67,9 +101,8 @@ type GbmDrmCompositor = DrmCompositor< >; struct OutputDevice { - id: dev_t, token: RegistrationToken, - gles: GlesRenderer, + render_node: DrmNode, drm_scanner: DrmScanner, surfaces: HashMap, // SAFETY: drop after all the objects used with them are dropped. @@ -80,14 +113,14 @@ struct OutputDevice { #[derive(Debug, Clone, Copy)] struct TtyOutputState { - device_id: dev_t, + node: DrmNode, crtc: crtc::Handle, } struct Surface { name: String, compositor: GbmDrmCompositor, - dmabuf_feedback: DmabufFeedback, + dmabuf_feedback: Option, /// Tracy frame that goes from vblank to vblank. vblank_frame: Option, /// Frame name for the VBlank frame. @@ -99,6 +132,11 @@ struct Surface { sequence_delta_plot_name: tracy_client::PlotName, } +pub struct SurfaceDmabufFeedback { + pub render: DmabufFeedback, + pub scanout: DmabufFeedback, +} + impl Tty { pub fn new(config: Rc>, event_loop: LoopHandle<'static, State>) -> Self { let (session, notifier) = LibSeatSession::new().unwrap(); @@ -129,16 +167,46 @@ impl Tty { }) .unwrap(); + let config_ = config.clone(); + let create_renderer = move |display: &EGLDisplay| { + let color_transforms = config_ + .borrow() + .debug + .enable_color_transformations_capability; + + let egl_context = EGLContext::new_with_priority(display, ContextPriority::High)?; + let gles = if color_transforms { + unsafe { GlesRenderer::new(egl_context)? } + } else { + let capabilities = unsafe { GlesRenderer::supported_capabilities(&egl_context) }? + .into_iter() + .filter(|c| *c != Capability::ColorTransformations); + unsafe { GlesRenderer::with_capabilities(egl_context, capabilities)? } + }; + Ok(gles) + }; + let api = GbmGlesBackend::with_factory(Box::new(create_renderer)); + let gpu_manager = GpuManager::new(api).unwrap(); + let primary_gpu_path = udev::primary_gpu(&seat_name).unwrap().unwrap(); + let primary_node = DrmNode::from_path(primary_gpu_path).unwrap(); + let primary_render_node = primary_node + .node_with_type(NodeType::Render) + .unwrap() + .unwrap(); + info!("using {} as the primary GPU", primary_render_node); Self { config, session, udev_dispatcher, libinput, - primary_gpu_path, + gpu_manager, + primary_node, + primary_render_node, + devices: HashMap::new(), dmabuf_global: None, - output_device: None, + primary_allocator: None, connectors: Arc::new(Mutex::new(HashMap::new())), } } @@ -193,8 +261,8 @@ impl Tty { self.libinput.suspend(); - if let Some(output_device) = &self.output_device { - output_device.drm.pause(); + for device in self.devices.values() { + device.drm.pause(); } } SessionEvent::ActivateSession => { @@ -204,69 +272,85 @@ impl Tty { error!("error resuming libinput"); } - if let Some(output_device) = &mut self.output_device { - // We had an output device, check if it's been removed. - let output_device_id = output_device.id; - if !self - .udev_dispatcher - .as_source_ref() - .device_list() - .any(|(device_id, _)| device_id == output_device_id) - { - // The output device, if we had any, has been removed. - self.device_removed(output_device_id, niri); - } else { - // It hasn't been removed, update its state as usual. - output_device.drm.activate(); - - // HACK: force reset the connectors to make resuming work across - // sleep. - let output_device = self.output_device.as_mut().unwrap(); - let crtcs: Vec<_> = output_device - .drm_scanner - .crtcs() - .map(|(conn, crtc)| (conn.clone(), crtc)) - .collect(); - for (conn, crtc) in crtcs { - self.connector_disconnected(niri, conn, crtc); - } + 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); + } - let output_device = self.output_device.as_mut().unwrap(); - let _ = output_device - .drm_scanner - .scan_connectors(&output_device.drm); - let crtcs: Vec<_> = output_device - .drm_scanner - .crtcs() - .map(|(conn, crtc)| (conn.clone(), crtc)) - .collect(); - for (conn, crtc) in crtcs { - if let Err(err) = self.connector_connected(niri, conn, crtc) { - warn!("error connecting connector: {err:?}"); - } + // 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[&node]; + device.drm.activate(); + + // HACK: force reset the connectors to make resuming work across sleep. + let device = &self.devices[&node]; + let crtcs: Vec<_> = device + .drm_scanner + .crtcs() + .map(|(conn, crtc)| (conn.clone(), crtc)) + .collect(); + for (conn, crtc) in crtcs { + self.connector_disconnected(niri, node, conn, crtc); + } + + let device = self.devices.get_mut(&node).unwrap(); + let _ = device.drm_scanner.scan_connectors(&device.drm); + let crtcs: Vec<_> = device + .drm_scanner + .crtcs() + .map(|(conn, crtc)| (conn.clone(), crtc)) + .collect(); + for (conn, crtc) in crtcs { + if let Err(err) = self.connector_connected(niri, node, conn, crtc) { + warn!("error connecting connector: {err:?}"); } + } - // // Refresh the connectors. - // self.device_changed(output_device_id, niri); + // // Refresh the connectors. + // self.device_changed(node.dev_id(), niri); - // // Refresh the state on unchanged connectors. - // let output_device = self.output_device.as_mut().unwrap(); - // for drm_compositor in output_device.surfaces.values_mut() { - // if let Err(err) = drm_compositor.surface().reset_state() { - // warn!("error resetting DRM surface state: {err}"); - // } - // drm_compositor.reset_buffers(); - // } + // // Refresh the state on unchanged connectors. + // let device = self.devices.get_mut(&node).unwrap(); + // for surface in device.surfaces.values_mut() { + // let compositor = &mut surface.compositor; + // if let Err(err) = compositor.surface().reset_state() { + // warn!("error resetting DRM surface state: {err}"); + // } + // compositor.reset_buffers(); + // } - // niri.queue_redraw_all(); - } - } else { - // We didn't have an output device, check if it's been added. - let udev_dispatcher = self.udev_dispatcher.clone(); - for (device_id, path) in udev_dispatcher.as_source_ref().device_list() { - if let Err(err) = self.device_added(device_id, path, niri) { - warn!("error adding device: {err:?}"); - } + // niri.queue_redraw_all(); + } + + // 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:?}"); } } } @@ -279,13 +363,9 @@ impl Tty { path: &Path, niri: &mut Niri, ) -> anyhow::Result<()> { - if path != self.primary_gpu_path { - debug!("skipping non-primary device {path:?}"); - return Ok(()); - } + debug!("device added: {device_id} {path:?}"); - debug!("adding device {path:?}"); - assert!(self.output_device.is_none()); + 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)?; @@ -295,24 +375,70 @@ impl Tty { let gbm = GbmDevice::new(device_fd)?; let display = EGLDisplay::new(gbm.clone())?; - let egl_context = EGLContext::new_with_priority(&display, ContextPriority::High)?; - - // ColorTransformations is disabled by default as it makes rendering slightly slower. - let mut gles = if self - .config - .borrow() - .debug - .enable_color_transformations_capability - { - unsafe { GlesRenderer::new(egl_context)? } - } else { - let capabilities = unsafe { GlesRenderer::supported_capabilities(&egl_context) }? - .into_iter() - .filter(|c| *c != Capability::ColorTransformations); - unsafe { GlesRenderer::with_capabilities(egl_context, capabilities)? } - }; - - gles.bind_wl_display(&niri.display_handle)?; + let egl_device = EGLDevice::device_for_display(&display)?; + + // HACK: There's an issue in Smithay where the display created by GpuManager will be the + // same as the one we just created here, so when ours is dropped at the end of the scope, + // it will also close the long-lived display in GpuManager. Thus, we need to drop ours + // beforehand. + drop(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 { + debug!("this is the primary node"); + + let mut renderer = self + .gpu_manager + .single_renderer(&render_node) + .context("error creating renderer")?; + + renderer.bind_wl_display(&niri.display_handle)?; + + // Create the dmabuf global. + let primary_formats = renderer.dmabuf_formats().collect::>(); + 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()); + + // Create the primary allocator. + let primary_allocator = + DmabufAllocator(GbmAllocator::new(gbm.clone(), GbmBufferFlags::RENDERING)); + assert!(self.primary_allocator.replace(primary_allocator).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 @@ -321,30 +447,22 @@ impl Tty { match event { DrmEvent::VBlank(crtc) => { let meta = meta.expect("VBlank events must have metadata"); - tty.on_vblank(&mut state.niri, crtc, meta); + tty.on_vblank(&mut state.niri, node, crtc, meta); } DrmEvent::Error(error) => error!("DRM error: {error}"), }; }) .unwrap(); - let default_feedback = DmabufFeedbackBuilder::new(device_id, gles.dmabuf_formats()) - .build() - .context("error building default dmabuf feedback")?; - let dmabuf_global = niri - .dmabuf_state - .create_global_with_default_feedback::(&niri.display_handle, &default_feedback); - - self.output_device = Some(OutputDevice { - id: device_id, + let device = OutputDevice { token, + render_node, drm, gbm, - gles, drm_scanner: DrmScanner::new(), surfaces: HashMap::new(), - }); - self.dmabuf_global = Some(dmabuf_global); + }; + assert!(self.devices.insert(node, device).is_none()); self.device_changed(device_id, niri); @@ -352,13 +470,17 @@ impl Tty { } fn device_changed(&mut self, device_id: dev_t, niri: &mut Niri) { - let Some(device) = &mut self.output_device else { + debug!("device changed: {device_id}"); + + let Ok(node) = DrmNode::from_dev_id(device_id) else { + warn!("error creating DrmNode"); return; }; - if device.id != device_id { + + let Some(device) = self.devices.get_mut(&node) else { + warn!("no such device"); return; - } - debug!("output device changed"); + }; for event in device.drm_scanner.scan_connectors(&device.drm) { match event { @@ -366,26 +488,31 @@ impl Tty { connector, crtc: Some(crtc), } => { - if let Err(err) = self.connector_connected(niri, connector, crtc) { + if let Err(err) = self.connector_connected(niri, node, connector, crtc) { warn!("error connecting connector: {err:?}"); } } DrmScanEvent::Disconnected { connector, crtc: Some(crtc), - } => self.connector_disconnected(niri, connector, crtc), + } => self.connector_disconnected(niri, node, connector, crtc), _ => (), } } } fn device_removed(&mut self, device_id: dev_t, niri: &mut Niri) { - let Some(device) = &mut self.output_device else { + debug!("device removed: {device_id}"); + + let Ok(node) = DrmNode::from_dev_id(device_id) else { + warn!("error creating DrmNode"); return; }; - if device_id != device.id { + + let Some(device) = self.devices.get_mut(&node) else { + warn!("no such device"); return; - } + }; let crtcs: Vec<_> = device .drm_scanner @@ -394,34 +521,54 @@ impl Tty { .collect(); for (connector, crtc) in crtcs { - self.connector_disconnected(niri, connector, crtc); + self.connector_disconnected(niri, node, connector, crtc); } - let mut device = self.output_device.take().unwrap(); - device.gles.unbind_wl_display(); - - let global = self.dmabuf_global.take().unwrap(); - 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(); + let device = self.devices.remove(&node).unwrap(); + + if node == self.primary_node { + match self.gpu_manager.single_renderer(&device.render_node) { + Ok(mut renderer) => renderer.unbind_wl_display(), + Err(err) => { + error!("error creating renderer during device removal: {err}"); + } + } + + // Disable and destroy the dmabuf global. + let global = self.dmabuf_global.take().unwrap(); + 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(); + + self.primary_allocator = None; + + // 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; + } + } + } + self.gpu_manager.as_mut().remove_node(&device.render_node); niri.event_loop.remove(device.token); } fn connector_connected( &mut self, niri: &mut Niri, + node: DrmNode, connector: connector::Info, crtc: crtc::Handle, ) -> anyhow::Result<()> { @@ -446,10 +593,7 @@ impl Tty { return Ok(()); } - let device = self - .output_device - .as_mut() - .context("missing output device")?; + let device = self.devices.get_mut(&node).context("missing device")?; // FIXME: print modes here until we have a better way to list all modes. for m in connector.modes() { @@ -559,10 +703,9 @@ impl Tty { output.change_current_state(Some(wl_mode), None, Some(Scale::Integer(scale)), None); output.set_preferred(wl_mode); - output.user_data().insert_if_missing(|| TtyOutputState { - device_id: device.id, - crtc, - }); + output + .user_data() + .insert_if_missing(|| TtyOutputState { node, crtc }); let mut planes = surface.planes().clone(); @@ -581,19 +724,10 @@ impl Tty { Some(device.gbm.clone()) }; - let egl_context = device.gles.egl_context(); - let texture_formats = egl_context.dmabuf_texture_formats(); + 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(); - let scanout_formats = planes - .primary - .formats - .iter() - .chain(planes.overlay.iter().flat_map(|p| &p.formats)) - .copied() - .collect::>(); - let scanout_formats = scanout_formats.intersection(texture_formats).copied(); - // Create the compositor. let compositor = DrmCompositor::new( OutputModeSource::Auto(output.clone()), @@ -602,15 +736,31 @@ impl Tty { allocator, 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(), cursor_plane_gbm, )?; - let dmabuf_feedback = DmabufFeedbackBuilder::new(device.id, texture_formats.clone()) - .add_preference_tranche(device.id, Some(TrancheFlags::Scanout), scanout_formats) - .build() - .context("error building dmabuf feedback")?; + 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().collect::>(); + + 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:?}"); + } + } + } let vblank_frame_name = tracy_client::FrameName::new_leak(format!("vblank on {output_name}")); @@ -654,12 +804,14 @@ impl Tty { fn connector_disconnected( &mut self, niri: &mut Niri, + node: DrmNode, connector: connector::Info, crtc: crtc::Handle, ) { debug!("disconnecting connector: {connector:?}"); - let Some(device) = self.output_device.as_mut() else { - error!("missing output device"); + + let Some(device) = self.devices.get_mut(&node) else { + error!("missing device"); return; }; @@ -673,7 +825,7 @@ impl Tty { .outputs() .find(|output| { let tty_state: &TtyOutputState = output.user_data().get().unwrap(); - tty_state.device_id == device.id && tty_state.crtc == crtc + tty_state.node == node && tty_state.crtc == crtc }) .cloned(); if let Some(output) = output { @@ -685,14 +837,20 @@ impl Tty { self.connectors.lock().unwrap().remove(&surface.name); } - fn on_vblank(&mut self, niri: &mut Niri, crtc: crtc::Handle, meta: DrmEventMetadata) { + 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.output_device.as_mut() else { + let Some(device) = self.devices.get_mut(&node) else { // I've seen it happen. - error!("missing output device in vblank callback for crtc {crtc:?}"); + error!("missing device in vblank callback for crtc {crtc:?}"); return; }; @@ -744,7 +902,7 @@ impl Tty { .outputs() .find(|output| { let tty_state: &TtyOutputState = output.user_data().get().unwrap(); - tty_state.device_id == device.id && tty_state.crtc == crtc + tty_state.node == node && tty_state.crtc == crtc }) .cloned() else { @@ -864,7 +1022,11 @@ impl Tty { &mut self, f: impl FnOnce(&mut GlesRenderer) -> T, ) -> Option { - self.output_device.as_mut().map(|d| f(&mut d.gles)) + let mut renderer = self + .gpu_manager + .single_renderer(&self.primary_render_node) + .ok()?; + Some(f(renderer.as_gles_renderer())) } pub fn render( @@ -877,32 +1039,48 @@ impl Tty { let mut rv = RenderResult::Skipped; - let Some(device) = self.output_device.as_mut() else { + 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); + if !device.drm.is_active() { warn!("device is inactive"); return rv; } - let tty_state: &TtyOutputState = output.user_data().get().unwrap(); - let Some(surface) = device.surfaces.get_mut(&tty_state.crtc) else { - error!("missing surface"); + let Some(allocator) = self.primary_allocator.as_mut() else { + warn!("no primary allocator"); return rv; }; - span.emit_text(&surface.name); - - let renderer = &mut device.gles; + let mut renderer = match self.gpu_manager.renderer( + &self.primary_render_node, + &device.render_node, + allocator, + surface.compositor.format(), + ) { + Ok(renderer) => renderer, + Err(err) => { + error!("error creating renderer for primary GPU: {err:?}"); + return rv; + } + }; // Render the elements. - let elements = niri.render::(renderer, output, true); + let elements = niri.render::(&mut renderer, output, true); // Hand them over to the DRM. let drm_compositor = &mut surface.compositor; - match drm_compositor.render_frame::<_, _, GlesTexture>(renderer, &elements, [0.; 4]) { + match drm_compositor.render_frame::<_, _, GlesTexture>(&mut renderer, &elements, [0.; 4]) { Ok(res) => { if self .config @@ -917,7 +1095,9 @@ impl Tty { } niri.update_primary_scanout_output(output, &res.states); - niri.send_dmabuf_feedbacks(output, &surface.dmabuf_feedback); + if let Some(dmabuf_feedback) = surface.dmabuf_feedback.as_ref() { + niri.send_dmabuf_feedbacks(output, dmabuf_feedback, &res.states); + } if res.damage.is_some() { let presentation_feedbacks = @@ -979,7 +1159,7 @@ impl Tty { } pub fn toggle_debug_tint(&mut self) { - if let Some(device) = self.output_device.as_mut() { + for device in self.devices.values_mut() { for surface in device.surfaces.values_mut() { let compositor = &mut surface.compositor; compositor.set_debug_flags(compositor.debug_flags() ^ DebugFlags::TINT); @@ -988,8 +1168,15 @@ impl Tty { } pub fn import_dmabuf(&mut self, dmabuf: &Dmabuf) -> Result<(), ()> { - let device = self.output_device.as_mut().ok_or(())?; - match device.gles.import_dmabuf(dmabuf, None) { + 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 Err(()); + } + }; + + match renderer.import_dmabuf(dmabuf, None) { Ok(_texture) => Ok(()), Err(err) => { debug!("error importing dmabuf: {err:?}"); @@ -998,26 +1185,82 @@ impl Tty { } } + pub fn early_import(&mut self, surface: &WlSurface) { + if let Err(err) = self.gpu_manager.early_import( + // We always advertise the primary GPU in dmabuf feedback. + Some(self.primary_render_node), + // We always render on the primary GPU. + self.primary_render_node, + surface, + ) { + warn!("error doing early import: {err:?}"); + } + } + pub fn connectors(&self) -> Arc>> { self.connectors.clone() } #[cfg(feature = "xdp-gnome-screencast")] - pub fn gbm_device(&self) -> Option> { - self.output_device.as_ref().map(|d| d.gbm.clone()) + pub fn primary_gbm_device(&self) -> Option> { + self.devices.get(&self.primary_node).map(|d| d.gbm.clone()) } pub fn set_monitors_active(&self, active: bool) { - let Some(device) = &self.output_device else { - return; - }; - - for crtc in device.surfaces.keys() { - set_crtc_active(&device.drm, *crtc, active); + for device in self.devices.values() { + for crtc in device.surfaces.keys() { + set_crtc_active(&device.drm, *crtc, active); + } } } } +fn surface_dmabuf_feedback( + compositor: &GbmDrmCompositor, + primary_formats: HashSet, + primary_render_node: DrmNode, + surface_render_node: DrmNode, +) -> Result { + let surface = compositor.surface(); + let planes = surface.planes(); + + let plane_formats = planes + .primary + .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 scanout_formats = plane_formats + .intersection(&primary_formats) + .copied() + .collect::>(); + + let builder = DmabufFeedbackBuilder::new(primary_render_node.dev_id(), primary_formats); + + let scanout = builder + .clone() + .add_preference_tranche( + surface_render_node.dev_id(), + Some(TrancheFlags::Scanout), + 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, crtc: crtc::Handle, name: &str) -> Option { let props = match drm.get_properties(crtc) { Ok(props) => props, -- cgit