From e5e68f1d24e2323e1f783b9630c3a1eef18c48b2 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Sat, 25 Jul 2026 14:26:36 +0200 Subject: [PATCH] feat(presenter): DRM card selection + a usable kmsdrm swapchain error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both come straight out of the first compositor-less bring-up (P3), on a two-GPU box: PUNKTFUNK_DRM_CARD= pins SDL's KMSDRM device index. SDL enumerates /dev/dri/card* and takes the first it can open, which is regularly the wrong one: it chose the card a live compositor already held DRM master on and died at swapchain creation, while the idle card with the connected display sat unused. Kept an explicit operator choice rather than auto-detection — deciding "is this card already mastered" needs the very ioctl that taking master IS, so any in-process guess would be fragile. The swapchain error now carries what to actually check. "vkCreateSwapchainKHR: Initialization of an object has failed" is useless to someone bringing up a kiosk; on the kmsdrm backend it now names the pinned card and lists the three real causes in order (no connected connector / another DRM master / NVIDIA). Empty on every other backend, where it would be noise. The NVIDIA note is measured, not guessed: on NVIDIA proprietary + kmsdrm, Vulkan enumerates the GPU AND the display (VK_KHR_display reports the connected HDMI connector) and still fails — as root, with nvidia_drm.modeset=Y, on a card no compositor was using. Not permissions, not DRM master; their direct-display path wants the display leased via vkAcquireDrmDisplayEXT and SDL's kmsdrm surface path does not do that. Plan: punktfunk-planning design/embedded-arm64-client.md §P3 Co-Authored-By: Claude Opus 5 (1M context) --- crates/pf-presenter/src/run.rs | 22 ++++++++++++++++++ crates/pf-presenter/src/vk/reconfig.rs | 31 +++++++++++++++++++++++--- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/crates/pf-presenter/src/run.rs b/crates/pf-presenter/src/run.rs index de1f4d4f..9d961bbd 100644 --- a/crates/pf-presenter/src/run.rs +++ b/crates/pf-presenter/src/run.rs @@ -360,6 +360,28 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result // identity and the session window gets the default-Wayland icon (the Linux analog of // the AppUserModelID adoption above). sdl3::hint::set("SDL_APP_ID", "io.unom.Punktfunk"); + // `PUNKTFUNK_DRM_CARD=` → SDL's KMSDRM device index, for a compositor-less (kiosk/embedded) + // run. SDL enumerates /dev/dri/card* and takes the first one it can open, which on a + // multi-GPU box is regularly the WRONG one: measured on a two-card machine it chose the card + // a live compositor already held DRM master on and failed at swapchain creation, while the + // idle card with the connected display sat unused. There is no reliable way to pick from + // inside the process (detecting "already mastered" needs the ioctl that taking master IS), so + // this stays an explicit operator choice rather than fragile auto-detection. + // Ignored unless the kmsdrm backend is actually in use. + if let Ok(card) = std::env::var("PUNKTFUNK_DRM_CARD") { + if card.chars().all(|c| c.is_ascii_digit()) && !card.is_empty() { + tracing::info!( + card, + "PUNKTFUNK_DRM_CARD: pinning SDL's KMSDRM device index" + ); + sdl3::hint::set("SDL_KMSDRM_DEVICE_INDEX", &card); + } else { + tracing::warn!( + card, + "PUNKTFUNK_DRM_CARD must be a card NUMBER (e.g. 0) — ignoring" + ); + } + } let sdl = sdl3::init().context("SDL init")?; let video = sdl.video().context("SDL video")?; let events = sdl.event().context("SDL events")?; diff --git a/crates/pf-presenter/src/vk/reconfig.rs b/crates/pf-presenter/src/vk/reconfig.rs index 6fe96ed3..2cc22aae 100644 --- a/crates/pf-presenter/src/vk/reconfig.rs +++ b/crates/pf-presenter/src/vk/reconfig.rs @@ -3,9 +3,34 @@ use super::setup::pick_formats; use super::{OverlayPipe, Presenter}; use crate::csc::CscPass; -use anyhow::{Context as _, Result}; +use anyhow::{anyhow, Context as _, Result}; use ash::vk; +/// Extra guidance appended to a swapchain-creation failure when SDL is on the KMSDRM backend — +/// i.e. a compositor-less kiosk/embedded run, where the bare Vulkan error is close to useless. +/// +/// Measured on an NVIDIA proprietary + KMSDRM box: SDL opens the card, Vulkan enumerates the GPU +/// *and* the display (`VK_KHR_display` reports the connected HDMI connector), then +/// `vkCreateSwapchainKHR` returns ERROR_INITIALIZATION_FAILED — as root, with `nvidia_drm.modeset` +/// on, on a card no compositor was using. So it is neither permissions nor DRM master: NVIDIA's +/// direct-to-display path wants the display leased (`vkAcquireDrmDisplayEXT`) and SDL's KMSDRM +/// surface path does not do that for it. Empty on every other backend, where the message would be +/// noise. +fn kmsdrm_swapchain_hint() -> String { + let kmsdrm = std::env::var("SDL_VIDEODRIVER").is_ok_and(|v| v.eq_ignore_ascii_case("kmsdrm")); + if !kmsdrm { + return String::new(); + } + let card = std::env::var("PUNKTFUNK_DRM_CARD").unwrap_or_else(|_| "unset".into()); + format!( + " — under SDL_VIDEODRIVER=kmsdrm (PUNKTFUNK_DRM_CARD={card}). Check, in order: the card \ + has a CONNECTED connector (`cat /sys/class/drm/card*-*/status`); nothing else holds DRM \ + master on it (a running compositor does — pin another card with PUNKTFUNK_DRM_CARD=); \ + and the driver is Mesa. NVIDIA's proprietary direct-display path is known to fail here \ + even as root with a display Vulkan can enumerate." + ) +} + impl Presenter { /// (Re)build the swapchain for the window's current pixel size. Also the resize path. pub fn recreate_swapchain(&mut self, window: &sdl3::video::Window) -> Result<()> { @@ -66,8 +91,8 @@ impl Presenter { .present_mode(self.present_mode) .clipped(true) .old_swapchain(old); - let swapchain = - unsafe { self.swap_d.create_swapchain(&info, None) }.context("vkCreateSwapchainKHR")?; + let swapchain = unsafe { self.swap_d.create_swapchain(&info, None) } + .map_err(|e| anyhow!("vkCreateSwapchainKHR: {e}{}", kmsdrm_swapchain_hint()))?; // The old swapchain and everything tied to its images dies NOW: the fence // quiesce covered our own command buffers, the queue drain above covered the // presentation engine's semaphore waits — nothing can still reference them.