atagen

monolith

public
Star0
Watch0

Commit f27fa4c2ade9

tarball

atagen <boss@atagen.co>2026-10-16 12:59 UTC

6 files changed+77-21
render-vk, monolith: render on the GPU that drives the outputs

The renderer picked its own device, preferring a discrete GPU, which only matched the scanout device by luck. On bare metal monolith now resolves the seat primary GPU first and builds the renderer on the Vulkan device whose primary or render node is that one, using VK_EXT_physical_device_drm.
Modified file: crates/monolith/src/drm.rs+14 additions−6 deletions
@@ -32,6 +32,7 @@ use smithay::reexports::rustix::fs::OFlags;
3232 use smithay::utils::{DeviceFd, Point, Transform};
3333 use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner};
3434 use std::collections::HashMap;
35+use std::path::{Path, PathBuf};
3536 use std::time::Duration;
3637
3738 const CLEAR: [f32; 4] = [0.07, 0.075, 0.09, 1.0];
@@ -75,20 +76,27 @@ impl Surface {
7576 }
7677 }
7778
79+/// The GPU monolith drives on bare metal, the seat's primary or else its
80+/// first.
81+pub fn primary() -> Result<PathBuf, Box<dyn std::error::Error>> {
82+ let seat = std::env::var("XDG_SEAT").unwrap_or_else(|_| "seat0".into());
83+ Ok(match primary_gpu(&seat)? {
84+ Some(p) => p,
85+ None => all_gpus(&seat)?.into_iter().next().ok_or("no GPU found")?,
86+ })
87+}
88+
7889 pub fn init(
7990 event_loop: &mut EventLoop<'static, State>,
8091 state: &mut State,
92+ path: &Path,
8193 ) -> Result<(), Box<dyn std::error::Error>> {
8294 let (mut session, notifier) = LibSeatSession::new()?;
8395 let seat = session.seat();
84- let path = match primary_gpu(&seat)? {
85- Some(p) => p,
86- None => all_gpus(&seat)?.into_iter().next().ok_or("no GPU found")?,
87- };
88- let node = DrmNode::from_path(&path)?;
96+ let node = DrmNode::from_path(path)?;
8997 tracing::info!(path = %path.display(), "primary GPU");
9098 let fd = session.open(
91- &path,
99+ path,
92100 OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK,
93101 )?;
94102 let fd = DrmDeviceFd::new(DeviceFd::from(fd));
Modified file: crates/monolith/src/main.rs+13 additions−4 deletions
@@ -117,10 +117,19 @@ fn run(
117117 ) -> Result<(), Box<dyn std::error::Error>> {
118118 let mut event_loop: EventLoop<'static, State> = EventLoop::try_new()?;
119119 let display: Display<State> = Display::new()?;
120- let mut state = State::new(&mut event_loop, display, config)?;
121- match backend {
122- Backend::Drm => drm::init(&mut event_loop, &mut state)?,
123- _ => nested::init(&mut event_loop, &mut state)?,
120+ let gpu = match backend {
121+ Backend::Drm => Some(drm::primary()?),
122+ _ => None,
123+ };
124+ let node = gpu
125+ .as_deref()
126+ .map(smithay::backend::drm::DrmNode::from_path)
127+ .transpose()?
128+ .map(|n| n.dev_id());
129+ let mut state = State::new(&mut event_loop, display, config, node)?;
130+ match &gpu {
131+ Some(path) => drm::init(&mut event_loop, &mut state, path)?,
132+ None => nested::init(&mut event_loop, &mut state)?,
124133 }
125134 ipc::init(&event_loop, &mut state)?;
126135 tracing::info!(socket = ?state.socket_name, device = state.renderer.device_name(), "monolith running");
Modified file: crates/monolith/src/state.rs+5 additions−1 deletions
@@ -92,6 +92,7 @@ impl State {
9292 event_loop: &mut EventLoop<'static, State>,
9393 display: Display<State>,
9494 config_path: Option<PathBuf>,
95+ gpu: Option<u64>,
9596 ) -> Result<Self, Box<dyn std::error::Error>> {
9697 let dh = display.handle();
9798 let config_path = config_path.or_else(Config::path);
@@ -99,7 +100,10 @@ impl State {
99100 tracing::error!("config: {e}, using the built-in default");
100101 Config::parse(monolith_config::DEFAULT).expect("the default config parses")
101102 });
102- let renderer = VulkanRenderer::new()?;
103+ let renderer = match gpu {
104+ Some(node) => VulkanRenderer::for_node(node)?,
105+ None => VulkanRenderer::new()?,
106+ };
103107 let mut dmabuf_state = DmabufState::new();
104108 let dmabuf_global = dmabuf_state.create_global::<State>(&dh, renderer.dmabuf_formats());
105109 let mut seat_state = SeatState::new();
Modified file: crates/render-vk/Cargo.toml+1 additions−1 deletions
@@ -11,7 +11,7 @@ workspace = true
1111 [dependencies]
1212 ash = "0.38"
1313 glam = { version = "0.33", default-features = false, features = ["std"] }
14-rustix = { version = "1", features = ["event"] }
14+rustix = { version = "1", features = ["event", "fs"] }
1515 gpu-allocator = { version = "0.27", default-features = false, features = ["vulkan"] }
1616 smithay.workspace = true
1717 thiserror = "2"
Modified file: crates/render-vk/src/device.rs+33 additions−8 deletions
@@ -51,8 +51,9 @@ impl Drop for Device {
5151
5252 impl Shared {
5353 /// Opens the first suitable device, or the one whose name contains
54- /// `MONOLITH_VK_DEVICE` when that is set.
55- pub fn new() -> Result<Self, Error> {
54+ /// `MONOLITH_VK_DEVICE` when that is set. With `node`, only the device
55+ /// behind that DRM device number qualifies.
56+ pub fn new(node: Option<u64>) -> Result<Self, Error> {
5657 let entry = unsafe { ash::Entry::load()? };
5758 let app = vk::ApplicationInfo::default()
5859 .application_name(c"monolith")
@@ -65,7 +66,7 @@ impl Shared {
6566 )?
6667 };
6768 let wanted = std::env::var("MONOLITH_VK_DEVICE").ok();
68- let picked = unsafe { pick_physical(&instance, wanted.as_deref()) };
69+ let picked = unsafe { pick_physical(&instance, wanted.as_deref(), node) };
6970 let Some((physical, queue_family, name)) = picked else {
7071 unsafe { instance.destroy_instance(None) };
7172 return Err(Error::NoSuitableDevice);
@@ -133,9 +134,24 @@ impl Shared {
133134 }
134135 }
135136
137+/// Whether `physical` is the device behind DRM device number `node`, as its
138+/// primary or render node.
139+unsafe fn serves_node(instance: &ash::Instance, physical: vk::PhysicalDevice, node: u64) -> bool {
140+ let mut drm = vk::PhysicalDeviceDrmPropertiesEXT::default();
141+ let mut props = vk::PhysicalDeviceProperties2::default().push_next(&mut drm);
142+ unsafe { instance.get_physical_device_properties2(physical, &mut props) };
143+ let (major, minor) = (
144+ rustix::fs::major(node) as i64,
145+ rustix::fs::minor(node) as i64,
146+ );
147+ (drm.has_primary == vk::TRUE && (drm.primary_major, drm.primary_minor) == (major, minor))
148+ || (drm.has_render == vk::TRUE && (drm.render_major, drm.render_minor) == (major, minor))
149+}
150+
136151 unsafe fn pick_physical(
137152 instance: &ash::Instance,
138153 wanted: Option<&str>,
154+ node: Option<u64>,
139155 ) -> Option<(vk::PhysicalDevice, u32, String)> {
140156 let devices = unsafe { instance.enumerate_physical_devices() }.ok()?;
141157 let mut candidates = Vec::new();
@@ -158,11 +174,20 @@ unsafe fn pick_physical(
158174 if f13.dynamic_rendering == vk::FALSE || f13.synchronization2 == vk::FALSE {
159175 continue;
160176 }
161- let has_push = unsafe { instance.enumerate_device_extension_properties(physical) }
162- .unwrap_or_default()
163- .iter()
164- .any(|e| e.extension_name_as_c_str().ok() == Some(ash::khr::push_descriptor::NAME));
165- if !has_push {
177+ let extensions =
178+ unsafe { instance.enumerate_device_extension_properties(physical) }.unwrap_or_default();
179+ let has = |n: &std::ffi::CStr| {
180+ extensions
181+ .iter()
182+ .any(|e| e.extension_name_as_c_str().ok() == Some(n))
183+ };
184+ if !has(ash::khr::push_descriptor::NAME) {
185+ continue;
186+ }
187+ if let Some(node) = node
188+ && !(has(ash::ext::physical_device_drm::NAME)
189+ && unsafe { serves_node(instance, physical, node) })
190+ {
166191 continue;
167192 }
168193 let families = unsafe { instance.get_physical_device_queue_family_properties(physical) };
Modified file: crates/render-vk/src/renderer.rs+11 additions−1 deletions
@@ -60,7 +60,17 @@ impl fmt::Debug for VulkanRenderer {
6060
6161 impl VulkanRenderer {
6262 pub fn new() -> Result<Self, Error> {
63- let shared = Arc::new(Shared::new()?);
63+ Self::with_node(None)
64+ }
65+
66+ /// A renderer on the GPU behind DRM device number `node`, so it can
67+ /// render into buffers that GPU scans out.
68+ pub fn for_node(node: u64) -> Result<Self, Error> {
69+ Self::with_node(Some(node))
70+ }
71+
72+ fn with_node(node: Option<u64>) -> Result<Self, Error> {
73+ let shared = Arc::new(Shared::new(node)?);
6474 let d = &shared.device.device;
6575 let pool = unsafe {
6676 d.create_command_pool(