Attempt 2 to fix launching permission denied

This commit is contained in:
Stephen Hellings 2026-03-09 09:45:30 -04:00
parent d9fa1323c3
commit 5e154e19f9
2 changed files with 294 additions and 181 deletions

View file

@ -36,7 +36,7 @@ use smithay::{
output::{Mode as OutputMode, Output, PhysicalProperties, Scale, Subpixel}, output::{Mode as OutputMode, Output, PhysicalProperties, Scale, Subpixel},
reexports::{ reexports::{
drm::control::{connector, Device as _, ModeTypeFlags}, drm::control::{connector, Device as _, ModeTypeFlags},
rustix::fs::{Mode, OFlags}, rustix::fs::OFlags,
wayland_server::{Display, ListeningSocket}, wayland_server::{Display, ListeningSocket},
}, },
utils::{Buffer, DeviceFd, Physical, Rectangle, Transform}, utils::{Buffer, DeviceFd, Physical, Rectangle, Transform},
@ -47,7 +47,6 @@ use smithay::{
use std::path::Path; use std::path::Path;
use std::{ use std::{
io::Write, io::Write,
thread,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use tracing::{error, info, trace, warn}; use tracing::{error, info, trace, warn};
@ -83,8 +82,7 @@ struct EventLoopData {
frame_timestamps: std::collections::HashMap<String, Instant>, frame_timestamps: std::collections::HashMap<String, Instant>,
gles_tex_cache: std::collections::HashMap<String, smithay::backend::renderer::gles::GlesTexture>, gles_tex_cache: std::collections::HashMap<String, smithay::backend::renderer::gles::GlesTexture>,
input_events_pending: bool, input_events_pending: bool,
#[allow(dead_code)] session: smithay::backend::session::libseat::LibSeatSession,
session: Option<smithay::backend::session::libseat::LibSeatSession>,
active: bool, active: bool,
vblank_pending: bool, vblank_pending: bool,
consecutive_flip_failures: u32, consecutive_flip_failures: u32,
@ -106,11 +104,33 @@ fn write_flip_failure_report(report: &str) {
} }
} }
fn is_would_block_message(msg: &str) -> bool { fn is_permission_denied_message(msg: &str) -> bool {
let lowered = msg.to_ascii_lowercase(); let lowered = msg.to_ascii_lowercase();
lowered.contains("wouldblock") lowered.contains("permission denied") || lowered.contains("eacces") || lowered.contains("eperm")
|| lowered.contains("resource temporarily unavailable") }
fn is_eagain_message(msg: &str) -> bool {
let lowered = msg.to_ascii_lowercase();
lowered.contains("resource temporarily unavailable")
|| lowered.contains("eagain") || lowered.contains("eagain")
|| lowered.contains("wouldblock")
}
fn enumerate_drm_card_paths() -> Vec<std::path::PathBuf> {
let mut paths: Vec<std::path::PathBuf> = std::fs::read_dir("/dev/dri")
.ok()
.into_iter()
.flat_map(|entries| entries.filter_map(Result::ok))
.map(|entry| entry.path())
.filter(|path| {
path.file_name()
.and_then(|name| name.to_str())
.map(|name| name.starts_with("card"))
.unwrap_or(false)
})
.collect();
paths.sort();
paths
} }
fn is_egl_context_or_surface_lost(err: &str) -> bool { fn is_egl_context_or_surface_lost(err: &str) -> bool {
@ -254,7 +274,7 @@ fn render_frame(data: &mut EventLoopData) {
data.frame_time data.frame_time
}; };
if data.last_render.elapsed() < adaptive_frame_time { if !data.first_render && data.last_render.elapsed() < adaptive_frame_time {
return; return;
} }
data.last_render = std::time::Instant::now(); data.last_render = std::time::Instant::now();
@ -530,122 +550,233 @@ fn render_frame(data: &mut EventLoopData) {
} }
fn run_drm(config: Config) -> Result<(), anyhow::Error> { fn run_drm(config: Config) -> Result<(), anyhow::Error> {
let mut _session_notifier = None; let (session, session_notifier) = LibSeatSession::new().map_err(|err| {
let mut session = match LibSeatSession::new() { anyhow::anyhow!(
Ok((session, notifier)) => { "Failed to create libseat session: {err}. wo requires a seat-managed session to access DRM/input devices safely. Run from a proper login session (logind/seatd)."
)
})?;
info!("LibSeat session created successfully"); info!("LibSeat session created successfully");
_session_notifier = Some(notifier); let mut session = session;
Some(session)
}
Err(err) => {
warn!(
"Failed to create libseat session: {err}. Falling back to direct DRM open; libinput and VT switching will be disabled"
);
None
}
};
let mut event_loop: EventLoop<()> = EventLoop::try_new().context("creating event loop")?; // Create the event loop early—before opening any DRM devices.
// Niri, wlroots, and KWin all register the session notifier on the event
// loop before attempting to open device fds. Without this, calloop never
// polls the libseat fd, seat.dispatch() is never called, the EnableSeat
// message from seatd/logind stays unread, and libseat_open_device()
// returns EAGAIN for every attempt.
let mut event_loop: EventLoop<Option<EventLoopData>> =
EventLoop::try_new().context("creating event loop")?;
let loop_handle = event_loop.handle(); let loop_handle = event_loop.handle();
let loop_signal = event_loop.get_signal();
if let Some(notifier) = _session_notifier.take() {
loop_handle loop_handle
.insert_source(notifier, |_, _, _: &mut ()| { .insert_source(session_notifier, |event, _, data: &mut Option<EventLoopData>| {
trace!("Session event during init"); use smithay::backend::session::Event;
let Some(data) = data.as_mut() else {
// Pre-init phase: ActivateSession received before DRM setup is
// complete. The session is now active; device opening can proceed.
return;
};
match event {
Event::PauseSession => {
info!("Session paused (VT switched away). Releasing DRM master and halting rendering.");
data.active = false;
data.vblank_pending = false;
if let Err(e) = drm::Device::release_master_lock(&data.drm_device_fd) {
warn!("Failed to release DRM master on session pause: {e}");
}
}
Event::ActivateSession => {
info!("Session activated (VT switched back). Re-acquiring DRM master and resuming rendering.");
if let Err(e) = drm::Device::acquire_master_lock(&data.drm_device_fd) {
error!("Failed to re-acquire DRM master on session activation: {e}");
data.active = false;
data.vblank_pending = false;
return;
}
data.active = true;
data.vblank_pending = false;
if let Err(e) = data.display.flush_clients() {
warn!("Failed to flush Wayland clients on activation: {e}");
}
render_frame(data);
}
}
}) })
.map_err(|e| anyhow::anyhow!("Error inserting session notifier: {e:?}"))?; .map_err(|e| anyhow::anyhow!("Error inserting session notifier: {e:?}"))?;
info!("Session notifier registered, dispatching initial events"); info!("Session notifier registered for VT switching");
// Dispatch events to ensure session is activated // Dispatch the event loop until the seat is active and ready for device
// requests. This must run even when session.is_active() is already true:
// LibSeatSession::new() may have set the active flag via an internal
// dispatch(0), but calloop still needs to poll the libseat fd at least
// once *after* the notifier is registered before open_device() will
// succeed. Without this, libseat_open_device() returns EAGAIN immediately.
if !session.is_active() {
info!("Waiting for seat activation (EnableSeat from seat manager)...");
}
let mut pre_init: Option<EventLoopData> = None;
let deadline = Instant::now() + Duration::from_secs(5);
loop {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
break;
}
event_loop event_loop
.dispatch(Some(std::time::Duration::from_millis(100)), &mut ()) .dispatch(
.context("initial event dispatch")?; Some(remaining.min(Duration::from_millis(200))),
&mut pre_init,
)
.context("dispatching event loop for session activation")?;
if session.is_active() {
info!("Session is active, ready to open devices");
break;
}
}
if !session.is_active() {
return Err(anyhow::anyhow!(
"Seat session never became active. \
Run wo from a proper TTY login session managed by logind or seatd. \
If another compositor holds the seat, switch to a free VT first."
));
} }
let seat_name = session // One unconditional drain after the activation break: the dispatch() call
.as_ref() // that set is_active()=true may have left additional buffered libseat events
.map(|s| s.seat()) // unread. A zero-timeout pump clears them so that the first open_device()
.unwrap_or_else(|| "seat0".to_string()); // request sees a clean state and does not get a spurious EAGAIN.
let _ = event_loop.dispatch(Some(Duration::from_millis(0)), &mut pre_init);
let seat_name = session.seat();
info!("Using seat: {}", seat_name); info!("Using seat: {}", seat_name);
let mut display: Display<WoState> = Display::new().context("creating Wayland display")?; let mut display: Display<WoState> = Display::new().context("creating Wayland display")?;
let dh = display.handle(); let dh = display.handle();
let gpu_path = if let Some(p) = &config.compositor.drm_device { let configured_gpu = config
std::path::PathBuf::from(p) .compositor
.drm_device
.as_ref()
.map(std::path::PathBuf::from);
let detected_primary_gpu = primary_gpu(&seat_name).ok().flatten();
let mut gpu_candidates = Vec::new();
if let Some(path) = configured_gpu {
gpu_candidates.push(path);
} else { } else {
primary_gpu(&seat_name) if let Some(path) = detected_primary_gpu {
.ok() gpu_candidates.push(path);
.flatten() }
.context("no primary GPU found")? for path in enumerate_drm_card_paths() {
}; if !gpu_candidates.iter().any(|p| p == &path) {
gpu_candidates.push(path);
}
}
}
info!(gpu = %gpu_path.display(), "using GPU"); if gpu_candidates.is_empty() {
return Err(anyhow::anyhow!(
"no DRM card candidates found (checked primary GPU and /dev/dri/card*)"
));
}
let drm_fd = if let Some(session) = session.as_mut() { info!(
info!("Opening DRM device via libseat session: {:?}", gpu_path); candidates = ?gpu_candidates,
let mut last_err = None; "trying DRM devices in priority order"
let mut opened_fd = None; );
let max_attempts = 30;
let retry_delay = Duration::from_millis(100);
for attempt in 1..=max_attempts { let mut chosen_gpu_path = None;
match session.open( let mut chosen_drm_fd = None;
&gpu_path, let mut open_errors: Vec<String> = Vec::new();
OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK,
) { // Open DRM in blocking mode. Some drivers can return EAGAIN for
// O_NONBLOCK opens even when no other compositor/session owns the device.
// libseat already provides asynchronous behavior and transient EAGAIN
// handling through the dispatch/retry loop below.
let open_flags = OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY;
// Round-robin EAGAIN retry across all GPU candidates.
//
// The old per-GPU retry loop had a structural flaw: EAGAIN from libseat is
// a seat-level condition (the seat manager hasn't finished processing an
// earlier request), not a GPU-level one. If every candidate gets EAGAIN the
// old loop would spend its entire 500 ms budget on the first GPU and then
// immediately abandon all remaining ones without any further dispatching.
//
// The new approach keeps a "pending" list of candidates that returned EAGAIN.
// After each global round it pumps the event loop once (50 ms) and retries
// all pending candidates. Up to MAX_EAGAIN_ROUNDS × 50 ms ≤ 1 s total.
const MAX_EAGAIN_ROUNDS: u32 = 20;
let mut eagain_pending: Vec<std::path::PathBuf> = gpu_candidates;
'outer: for round in 0..=MAX_EAGAIN_ROUNDS {
let mut still_eagain: Vec<std::path::PathBuf> = Vec::new();
for gpu_path in eagain_pending {
if round == 0 {
info!("Opening DRM device: {:?}", gpu_path);
}
match session.open(&gpu_path, open_flags) {
Ok(fd) => { Ok(fd) => {
info!("Successfully opened DRM device via session, fd={:?}", fd); info!(gpu = %gpu_path.display(), "using GPU");
opened_fd = Some(fd); chosen_gpu_path = Some(gpu_path);
break; chosen_drm_fd = Some(fd);
break 'outer;
} }
Err(e) => { Err(e) => {
let err_msg = e.to_string(); let detail = e.to_string();
if is_would_block_message(&err_msg) && attempt < max_attempts { if is_eagain_message(&detail) {
warn!( if round == 0 {
attempt, info!(
max_attempts,
gpu = %gpu_path.display(), gpu = %gpu_path.display(),
"DRM device busy (WouldBlock/EAGAIN), retrying open" "libseat returned EAGAIN; will retry after event loop pump..."
); );
last_err = Some(err_msg);
thread::sleep(retry_delay);
continue;
} }
last_err = Some(err_msg); still_eagain.push(gpu_path);
} else {
let is_perm = is_permission_denied_message(&detail);
warn!(
gpu = %gpu_path.display(),
permission_related = is_perm,
round,
"Failed to open DRM device: {detail}"
);
open_errors.push(format!("{}: {detail}", gpu_path.display()));
}
}
}
}
eagain_pending = still_eagain;
if eagain_pending.is_empty() {
break; break;
} }
} if round < MAX_EAGAIN_ROUNDS {
} let _ = event_loop.dispatch(Some(Duration::from_millis(50)), &mut pre_init);
} else {
match opened_fd { // Final round exhausted; record EAGAIN as a permanent failure for
Some(fd) => fd, // each remaining candidate so the error message is informative.
None => { for gpu_path in &eagain_pending {
let detail = last_err.unwrap_or_else(|| "unknown error".to_string()); open_errors.push(format!(
error!( "{}: libseat returned EAGAIN after {} retries (seat manager unresponsive?)",
gpu = %gpu_path.display(), gpu_path.display(),
"Failed to open DRM device via session after retries: {detail}" MAX_EAGAIN_ROUNDS,
);
return Err(anyhow::anyhow!(
"opening DRM device via session failed after retries: {}. This usually means another active compositor/session still owns the seat DRM node.",
detail
)); ));
} }
} }
}
let gpu_path = chosen_gpu_path.ok_or_else(|| {
let detail = if open_errors.is_empty() {
"no candidates produced a detailed error".to_string()
} else { } else {
info!("Opening DRM device directly (no session): {:?}", gpu_path); open_errors.join("; ")
smithay::reexports::rustix::fs::open(
&gpu_path,
OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK,
Mode::empty(),
)
.context("opening DRM device directly (no session)")?
}; };
anyhow::anyhow!(
"no usable DRM device found. Tried: {detail}. Check for stale holders with: fuser /dev/dri/card*"
)
})?;
let drm_fd = chosen_drm_fd.context("no usable DRM device fd found")?;
let drm_device_fd = DrmDeviceFd::new(DeviceFd::from(drm_fd)); let drm_device_fd = DrmDeviceFd::new(DeviceFd::from(drm_fd));
let (mut drm, _drm_notifier) =
DrmDevice::new(drm_device_fd.clone(), true).context("creating DRM device")?;
if let Err(e) = drm::Device::acquire_master_lock(&drm_device_fd) { if let Err(e) = drm::Device::acquire_master_lock(&drm_device_fd) {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
@ -654,6 +785,25 @@ fn run_drm(config: Config) -> Result<(), anyhow::Error> {
)); ));
} }
// Try to create DRM device with atomic mode first
// If it fails due to invalid state restoration, try without atomic mode
let (mut drm, _drm_notifier) = match DrmDevice::new(drm_device_fd.clone(), true) {
Ok(result) => {
info!("DRM device created in atomic mode");
result
}
Err(e) => {
let err_msg = e.to_string();
if err_msg.contains("Invalid argument") || err_msg.contains("EINVAL") {
warn!("Atomic mode initialization failed with EINVAL (likely stale state on display), trying legacy mode: {e}");
DrmDevice::new(drm_device_fd.clone(), false)
.map_err(|e| anyhow::anyhow!("creating DRM device in legacy mode after atomic failure: {e}"))?
} else {
return Err(anyhow::anyhow!("creating DRM device: {e}"));
}
}
};
let gbm_device = GbmDevice::new(drm_device_fd.clone()).context("creating GBM device")?; let gbm_device = GbmDevice::new(drm_device_fd.clone()).context("creating GBM device")?;
let egl_display = let egl_display =
@ -683,12 +833,20 @@ fn run_drm(config: Config) -> Result<(), anyhow::Error> {
.max_by_key(|m| (m.mode_type().contains(ModeTypeFlags::PREFERRED), m.size())) .max_by_key(|m| (m.mode_type().contains(ModeTypeFlags::PREFERRED), m.size()))
.context("no DRM mode")?; .context("no DRM mode")?;
let crtc_h = resources let crtc_h = connector_info
.crtcs() .encoders()
.iter() .iter()
.filter_map(|&encoder_h| drm.get_encoder(encoder_h).ok())
.find_map(|encoder_info| encoder_info.crtc())
.or_else(|| {
connector_info
.encoders()
.iter()
.filter_map(|&encoder_h| drm.get_encoder(encoder_h).ok())
.flat_map(|encoder_info| resources.filter_crtcs(encoder_info.possible_crtcs()))
.next() .next()
.copied() })
.context("no suitable CRTC")?; .context("no suitable CRTC for connected connector")?;
let drm_surface = drm let drm_surface = drm
.create_surface(crtc_h, mode, &[connector_h]) .create_surface(crtc_h, mode, &[connector_h])
@ -734,7 +892,7 @@ fn run_drm(config: Config) -> Result<(), anyhow::Error> {
DrmNode::from_path(&gpu_path).context("resolving DRM node for dmabuf feedback")?; DrmNode::from_path(&gpu_path).context("resolving DRM node for dmabuf feedback")?;
let mut state = WoState::new(&mut display, config.clone(), render_node, backend); let mut state = WoState::new(&mut display, config.clone(), render_node, backend);
state.can_switch_vt = session.is_some(); state.can_switch_vt = true;
let (ipc, msg_rx) = ElectronIpc::listen(&config.compositor.ipc_socket).context("IPC socket")?; let (ipc, msg_rx) = ElectronIpc::listen(&config.compositor.ipc_socket).context("IPC socket")?;
@ -835,17 +993,12 @@ fn run_drm(config: Config) -> Result<(), anyhow::Error> {
let bg = Color32F::from(config.compositor.background); let bg = Color32F::from(config.compositor.background);
// Initialize libinput for input events (only when session backend is available) // Initialize libinput for input events (only when session backend is available)
let libinput_backend = if let Some(session_ref) = session.as_ref() {
let mut libinput_context = let mut libinput_context =
Libinput::new_with_udev(LibinputSessionInterface::from(session_ref.clone())); Libinput::new_with_udev(LibinputSessionInterface::from(session.clone()));
libinput_context libinput_context
.udev_assign_seat(&session_ref.seat()) .udev_assign_seat(&session.seat())
.map_err(|()| anyhow::anyhow!("Failed to assign seat to libinput"))?; .map_err(|()| anyhow::anyhow!("Failed to assign seat to libinput"))?;
Some(LibinputInputBackend::new(libinput_context)) let libinput_backend = LibinputInputBackend::new(libinput_context);
} else {
warn!("Running without libinput backend because no session backend is available");
None
};
// Prepare frame pacing using the active DRM mode refresh when available. // Prepare frame pacing using the active DRM mode refresh when available.
let refresh_hz = (output_mode.refresh as f64) / 1000.0; let refresh_hz = (output_mode.refresh as f64) / 1000.0;
@ -887,12 +1040,6 @@ fn run_drm(config: Config) -> Result<(), anyhow::Error> {
gles_tex_cache: std::collections::HashMap::new(), gles_tex_cache: std::collections::HashMap::new(),
}; };
// Create calloop event loop
let mut event_loop: EventLoop<EventLoopData> =
EventLoop::try_new().context("creating event loop")?;
let loop_handle = event_loop.handle();
let loop_signal = event_loop.get_signal();
let signal_for_handler = loop_signal.clone(); let signal_for_handler = loop_signal.clone();
ctrlc::set_handler(move || { ctrlc::set_handler(move || {
info!("Received Ctrl+C, initiating shutdown"); info!("Received Ctrl+C, initiating shutdown");
@ -900,34 +1047,9 @@ fn run_drm(config: Config) -> Result<(), anyhow::Error> {
}) })
.expect("Error setting Ctrl-C handler"); .expect("Error setting Ctrl-C handler");
// 1. Hook up the Session Notifier to handle VT switching!
if let Some(notifier) = _session_notifier {
loop_handle loop_handle
.insert_source(notifier, |event, _, data: &mut EventLoopData| { .insert_source(_drm_notifier, |event, _, data: &mut Option<EventLoopData>| {
use smithay::backend::session::Event; let data = data.as_mut().expect("EventLoopData initialized");
match event {
Event::PauseSession => {
info!("Session paused (VT switched away). Halting rendering.");
data.active = false;
data.vblank_pending = false;
}
Event::ActivateSession => {
info!("Session activated (VT switched back). Resuming rendering.");
data.active = true;
data.vblank_pending = false; // Reset to ensure we can render
if let Err(e) = data.display.flush_clients() {
warn!("Failed to flush Wayland clients on activation: {e}");
}
render_frame(data); // Force an immediate redraw
}
}
})
.map_err(|e| anyhow::anyhow!("Error inserting session notifier: {e:?}"))?;
info!("Session notifier registered for VT switching");
}
loop_handle
.insert_source(_drm_notifier, |event, _, data: &mut EventLoopData| {
use smithay::backend::drm::DrmEvent; use smithay::backend::drm::DrmEvent;
if !data.active { if !data.active {
if let DrmEvent::VBlank(_) = event { if let DrmEvent::VBlank(_) = event {
@ -945,40 +1067,37 @@ fn run_drm(config: Config) -> Result<(), anyhow::Error> {
.map_err(|e| anyhow::anyhow!("Error inserting DRM notifier: {e:?}"))?; .map_err(|e| anyhow::anyhow!("Error inserting DRM notifier: {e:?}"))?;
// Insert libinput backend into event loop (operate on `loop_data.state`) // Insert libinput backend into event loop (operate on `loop_data.state`)
if let Some(libinput_backend) = libinput_backend {
loop_handle loop_handle
.insert_source( .insert_source(
libinput_backend, libinput_backend,
move |event, _, data: &mut EventLoopData| { move |event, _, data: &mut Option<EventLoopData>| {
let data = data.as_mut().expect("EventLoopData initialized");
data.state.process_input_event(event); data.state.process_input_event(event);
data.input_events_pending = true; data.input_events_pending = true;
}, },
) )
.map_err(|e| anyhow::anyhow!("Error inserting libinput source: {e:?}"))?; .map_err(|e| anyhow::anyhow!("Error inserting libinput source: {e:?}"))?;
}
// Kickstart the render loop with an initial frame // Kickstart the render loop with an initial frame
info!("Kickstarting VBlank-driven rendering"); info!("Kickstarting VBlank-driven rendering");
render_frame(&mut loop_data); render_frame(&mut loop_data);
// Main event loop - purely event-driven (blocking) // Main event loop - purely event-driven (blocking)
let mut main_data: Option<EventLoopData> = Some(loop_data);
event_loop event_loop
.run(None, &mut loop_data, |data| { .run(None, &mut main_data, |opt_data| {
let data = opt_data.as_mut().expect("EventLoopData initialized");
if !data.state.running { if !data.state.running {
loop_signal.stop(); loop_signal.stop();
return; return;
} }
if let Some(vt) = data.state.pending_vt_switch.take() { if let Some(vt) = data.state.pending_vt_switch.take() {
if let Some(session) = data.session.as_mut() { if let Err(e) = data.session.change_vt(vt as i32) {
if let Err(e) = session.change_vt(vt as i32) {
warn!(vt, "Failed to switch VT: {e}"); warn!(vt, "Failed to switch VT: {e}");
} else { } else {
info!(vt, "Switched to VT"); info!(vt, "Switched to VT");
} }
} else {
warn!(vt, "Ignoring VT switch request: no active session backend");
}
} }
while let Ok(Some(stream)) = data.wayland_socket.accept() { while let Ok(Some(stream)) = data.wayland_socket.accept() {

View file

@ -1,17 +1,11 @@
2026-03-09T06:55:04.057372Z  INFO wo: wo compositor starting 2026-03-09T13:10:06.828981Z  INFO wo: wo compositor starting
2026-03-09T06:55:04.057548Z  INFO wo::config: Loading config from "/home/hellings/.config/wo/config.toml" 2026-03-09T13:10:06.829194Z  INFO wo::config: Loading config from "/home/hellings/.config/wo/config.toml"
2026-03-09T06:55:04.057918Z  INFO wo: Running in DRM/KMS mode 2026-03-09T13:10:06.829640Z  INFO wo: Running in DRM/KMS mode
2026-03-09T06:55:04.059240Z  INFO wo: LibSeat session created successfully 2026-03-09T13:10:06.831907Z  INFO wo: LibSeat session created successfully
2026-03-09T06:55:04.059383Z  INFO wo: Session notifier registered, dispatching initial events 2026-03-09T13:10:06.832112Z  INFO wo: Session notifier registered for VT switching
2026-03-09T06:55:04.059468Z  INFO wo: Using seat: seat0 2026-03-09T13:10:06.832217Z  INFO wo: Session is active, ready to open devices
2026-03-09T06:55:04.059874Z  INFO wo: using GPU gpu=/dev/dri/card1 2026-03-09T13:10:06.832243Z  INFO wo: Using seat: seat0
2026-03-09T06:55:04.059895Z  INFO wo: Opening DRM device via libseat session: "/dev/dri/card1" 2026-03-09T13:10:06.834656Z  INFO wo: trying DRM devices in priority order candidates=["/dev/dri/card1"]
2026-03-09T06:55:04.060310Z  INFO wo: Successfully opened DRM device via session, fd=OwnedFd { fd: 15 } 2026-03-09T13:10:06.834715Z  INFO wo: Opening DRM device: "/dev/dri/card1"
2026-03-09T06:55:04.060356Z  WARN smithay::backend::drm::device::fd: Unable to become drm master, assuming unprivileged mode 2026-03-09T13:10:06.835267Z  INFO wo: libseat returned EAGAIN; pumping event loop and retrying device open... gpu=/dev/dri/card1
2026-03-09T06:55:13.132016Z ERROR smithay::backend::egl::ffi: [EGL] 0x3003 (BAD_ALLOC) eglQueryDevicesEXT: EGL_BAD_ALLOC error: In eglQueryDevicesEXT: Failed to allocate device list. 2026-03-09T13:10:07.351716Z  WARN wo: Failed to open DRM device: Failed to open device: Resource temporarily unavailable (os error 11) gpu=/dev/dri/card1 permission_related=false attempt=10
2026-03-09T06:55:13.132291Z ERROR smithay::backend::egl::ffi: [EGL] 0x3001 (NOT_INITIALIZED) eglInitialize: DRI2: gbm device using incorrect/incompatible backend
2026-03-09T06:55:13.132304Z ERROR smithay::backend::egl::ffi: [EGL] 0x3001 (NOT_INITIALIZED) eglInitialize: DRI2: gbm device using incorrect/incompatible backend
2026-03-09T06:55:13.132311Z ERROR smithay::backend::egl::ffi: [EGL] 0x3001 (NOT_INITIALIZED) eglInitialize: DRI2: gbm device using incorrect/incompatible backend
2026-03-09T06:55:13.132318Z ERROR smithay::backend::egl::ffi: [EGL] 0x3001 (NOT_INITIALIZED) eglInitialize: eglInitialize
2026-03-09T06:55:13.132487Z ERROR smithay::backend::drm::device::atomic: Failed to restore previous state. Error: Invalid argument (os error 22)