fix: merge [[root]] entries into config.windows and force initial modeset

Two critical bugs causing a black/off screen:

1. [[root]] window configs were spawned as Electron processes but never
   added to config.windows. The render loop, window_mapped state, and
   respawn logic all iterate config.windows — so root windows were
   invisible. Fix: merge root entries (with fullscreen geometry and low
   z_order) into config.windows before WoState::new(), so all subsystems
   see them uniformly.

2. When the display manager (Ly) leaves the CRTC in a state Smithay
   considers 'already committed' (matching mode/connectors),
   commit_pending() returns false and only page_flip() is used.
   page_flip without a prior modeset can 'succeed' but produce no
   visible output because the CRTC was never properly activated with
   a framebuffer. Fix: force a full modeset commit (ALLOW_MODESET) on
   the very first frame (total_flips==0) regardless of commit_pending.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Stephen Hellings 2026-03-09 13:16:00 -04:00
parent b82a9ca9c3
commit 5123aac2d4

View file

@ -586,16 +586,21 @@ fn render_frame(data: &mut EventLoopData) {
// VT switch), we must use commit() which includes ALLOW_MODESET. // VT switch), we must use commit() which includes ALLOW_MODESET.
// page_flip() only does a non-blocking flip without modesetting and will // page_flip() only does a non-blocking flip without modesetting and will
// fail with EINVAL if the CRTC doesn't have a mode set yet. // fail with EINVAL if the CRTC doesn't have a mode set yet.
//
// IMPORTANT: Even when commit_pending() is false (Ly left the CRTC in a
// matching mode), the first frame still needs a full modeset commit to
// ensure CRTC ACTIVE=1 and a valid framebuffer is bound. page_flip alone
// may "succeed" but produce no output if the CRTC was left without an
// active framebuffer by the display manager.
let commit_pending = data.drm_surface.commit_pending(); let commit_pending = data.drm_surface.commit_pending();
let result = if commit_pending { let force_initial = data.total_flips == 0;
info!("Performing initial modeset commit (textures={})", tex_count); let result = if commit_pending || force_initial {
info!(
"Performing modeset commit (commit_pending={}, first_frame={}, textures={})",
commit_pending, force_initial, tex_count
);
data.drm_surface.commit([plane_state.clone()], true) data.drm_surface.commit([plane_state.clone()], true)
} else { } else {
// On the very first page_flip (no prior flips), log at INFO level
// so we can confirm the rendering pipeline is working.
if data.total_flips == 0 {
info!("First page_flip (commit_pending=false, textures={})", tex_count);
}
data.drm_surface.page_flip([plane_state.clone()], true) data.drm_surface.page_flip([plane_state.clone()], true)
}; };
@ -661,7 +666,7 @@ fn render_frame(data: &mut EventLoopData) {
data.swapchain.submitted(&slot); data.swapchain.submitted(&slot);
} }
fn run_drm(config: Config) -> Result<(), anyhow::Error> { fn run_drm(mut config: Config) -> Result<(), anyhow::Error> {
let (session, session_notifier) = LibSeatSession::new().map_err(|err| { let (session, session_notifier) = LibSeatSession::new().map_err(|err| {
anyhow::anyhow!( anyhow::anyhow!(
"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)." "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)."
@ -1026,6 +1031,18 @@ fn run_drm(config: Config) -> Result<(), anyhow::Error> {
let render_node = let render_node =
DrmNode::from_path(&gpu_path).context("resolving DRM node for dmabuf feedback")?; DrmNode::from_path(&gpu_path).context("resolving DRM node for dmabuf feedback")?;
// Merge [[root]] entries into config.windows BEFORE WoState::new so that
// render_frame, window_mapped, and respawn logic all see them.
for root_cfg in &config.root.clone() {
let mut full = root_cfg.clone();
full.x = 0;
full.y = 0;
full.width = width;
full.height = height;
full.z_order = std::i32::MIN / 2;
config.windows.push(full);
}
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 = true; state.can_switch_vt = true;
@ -1037,29 +1054,8 @@ fn run_drm(config: Config) -> Result<(), anyhow::Error> {
let app_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("electron"); let app_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("electron");
let electron_render_node = render_node_for_card(&gpu_path); let electron_render_node = render_node_for_card(&gpu_path);
// Spawn any root (fullscreen) entries first. Root entries are an
// alternative to [[windows]] and are mapped to the full output size.
eprintln!("Root configs: {:#?}", config.root);
for root_cfg in &config.root {
let mut full = root_cfg.clone();
full.x = 0;
full.y = 0;
full.width = width;
full.height = height;
// put roots behind normal windows by using a very low z_order
full.z_order = std::i32::MIN / 2;
match ElectronProcess::spawn(
&full,
&config.compositor.electron_path,
&app_dir,
&config.compositor.ipc_socket,
&electron_render_node,
) {
Ok(proc) => state.electron_processes.push(proc),
Err(e) => error!(window = %full.name, "failed to spawn Electron root: {e:#}"),
}
}
// All windows (including merged [[root]] entries) are now in config.windows.
for win_cfg in &config.windows { for win_cfg in &config.windows {
match ElectronProcess::spawn( match ElectronProcess::spawn(
win_cfg, win_cfg,