feat(presenter): DRM card selection + a usable kmsdrm swapchain error
ci / web (push) Successful in 1m2s
ci / docs-site (push) Successful in 1m15s
apple / swift (push) Successful in 5m25s
ci / bench (push) Successful in 7m44s
deb / build-publish-host (push) Successful in 10m30s
decky / build-publish (push) Successful in 30s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
android / android (push) Successful in 12m52s
deb / build-publish (push) Successful in 11m46s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 11s
arch / build-publish (push) Successful in 13m6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 45s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 12s
ci / rust-arm64 (push) Successful in 13m32s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m1s
deb / build-publish-client-arm64 (push) Successful in 9m12s
windows-host / package (push) Successful in 18m4s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7m1s
flatpak / build-publish (push) Successful in 7m25s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m46s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 9m7s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 9m24s
ci / rust (push) Successful in 23m48s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m28s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m46s
docker / deploy-docs (push) Successful in 22s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m42s
release / apple (push) Successful in 31m30s
docker / build-push-arm64cross (push) Successful in 7m6s
apple / screenshots (push) Successful in 23m4s

Both come straight out of the first compositor-less bring-up (P3), on a
two-GPU box:

PUNKTFUNK_DRM_CARD=<n> 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) <noreply@anthropic.com>
This commit is contained in:
2026-07-25 14:32:45 +02:00
co-authored by Claude Opus 5
parent aff169e131
commit e5e68f1d24
2 changed files with 50 additions and 3 deletions
+22
View File
@@ -360,6 +360,28 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result<Option<Outcome>
// identity and the session window gets the default-Wayland icon (the Linux analog of // identity and the session window gets the default-Wayland icon (the Linux analog of
// the AppUserModelID adoption above). // the AppUserModelID adoption above).
sdl3::hint::set("SDL_APP_ID", "io.unom.Punktfunk"); sdl3::hint::set("SDL_APP_ID", "io.unom.Punktfunk");
// `PUNKTFUNK_DRM_CARD=<n>` → 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 sdl = sdl3::init().context("SDL init")?;
let video = sdl.video().context("SDL video")?; let video = sdl.video().context("SDL video")?;
let events = sdl.event().context("SDL events")?; let events = sdl.event().context("SDL events")?;
+28 -3
View File
@@ -3,9 +3,34 @@
use super::setup::pick_formats; use super::setup::pick_formats;
use super::{OverlayPipe, Presenter}; use super::{OverlayPipe, Presenter};
use crate::csc::CscPass; use crate::csc::CscPass;
use anyhow::{Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use ash::vk; 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=<n>); \
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 { impl Presenter {
/// (Re)build the swapchain for the window's current pixel size. Also the resize path. /// (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<()> { pub fn recreate_swapchain(&mut self, window: &sdl3::video::Window) -> Result<()> {
@@ -66,8 +91,8 @@ impl Presenter {
.present_mode(self.present_mode) .present_mode(self.present_mode)
.clipped(true) .clipped(true)
.old_swapchain(old); .old_swapchain(old);
let swapchain = let swapchain = unsafe { self.swap_d.create_swapchain(&info, None) }
unsafe { self.swap_d.create_swapchain(&info, None) }.context("vkCreateSwapchainKHR")?; .map_err(|e| anyhow!("vkCreateSwapchainKHR: {e}{}", kmsdrm_swapchain_hint()))?;
// The old swapchain and everything tied to its images dies NOW: the fence // 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 // quiesce covered our own command buffers, the queue drain above covered the
// presentation engine's semaphore waits — nothing can still reference them. // presentation engine's semaphore waits — nothing can still reference them.