diff options
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/config/src/engine/general.rs | 32 | ||||
| -rw-r--r-- | crates/wayland/src/udev/drm.rs | 1 | ||||
| -rw-r--r-- | crates/wayland/src/udev/mod.rs | 24 | ||||
| -rw-r--r-- | crates/wayland/src/udev/render.rs | 58 | ||||
| -rw-r--r-- | crates/wayland/src/udev/session.rs | 1 | ||||
| -rw-r--r-- | crates/wayland/src/winit/render.rs | 71 |
6 files changed, 161 insertions, 26 deletions
diff --git a/crates/config/src/engine/general.rs b/crates/config/src/engine/general.rs index 94b029b..e5147cd 100644 --- a/crates/config/src/engine/general.rs +++ b/crates/config/src/engine/general.rs @@ -229,14 +229,40 @@ impl Engine { })?) } + /// `srd.load("keybindings")`/`"themes"`/`"rules"`/`"startup"`: each is + /// its own file, its own logical concern, and - deliberately, since + /// this function catches its own execution error rather than letting + /// `?` propagate one - its own failure domain. A `mlua::Error` from + /// one module used to unwind straight out through this function and + /// back into whatever was running `init.lua` itself, aborting every + /// statement after that `srd.load` call, including every *other* + /// `srd.load` - a typo in `rules.lua` silently took `startup.lua` + /// (autostart) down with it, and there was no way from the config + /// author's side to prevent that, short of never making a mistake. + /// Reproduced live in the worse but related case (the error was in + /// `init.lua` itself, above every `srd.load` call, which this + /// function alone can't isolate against): a session that started with + /// nothing but a bare cursor, zero autostart, zero keybindings, no + /// error visible anywhere except one `WARN` line in a multi-hundred- + /// megabyte log file. A module failing now still leaves the user + /// without whatever that module would have set up, logged clearly at + /// `error` level with the module name and the file path - but + /// everything *else* `init.lua` goes on to load still does. pub(super) fn fn_load(&self) -> Result<mlua::Function<'_>> { let state = self.state.clone(); Ok(self.lua.create_function(move |lua, module: String| { let dir = state.borrow().config_dir.clone(); let path = dir.join(format!("{module}.lua")); - let src = std::fs::read_to_string(&path) - .map_err(|e| mlua::Error::RuntimeError(format!("srd.load('{module}'): {e} ({})", path.display())))?; - lua.load(&src).set_name(path.to_string_lossy().as_ref()).exec()?; + let src = match std::fs::read_to_string(&path) { + Ok(src) => src, + Err(e) => { + log::error!("srd.load('{module}'): {e} ({}) - this module did not load, but the rest of init.lua still will", path.display()); + return Ok(()); + } + }; + if let Err(e) = lua.load(&src).set_name(path.to_string_lossy().as_ref()).exec() { + log::error!("srd.load('{module}'): {e} - this module did not finish loading, but the rest of init.lua still will"); + } Ok(()) })?) } diff --git a/crates/wayland/src/udev/drm.rs b/crates/wayland/src/udev/drm.rs index efe53aa..ff25966 100644 --- a/crates/wayland/src/udev/drm.rs +++ b/crates/wayland/src/udev/drm.rs @@ -93,6 +93,7 @@ pub(crate) fn bring_up_head( location, size: (width, height), mode: probe.mode, + flip_retry_after: None, }; Ok((head, crate::state::OutputEntry { output, location })) } diff --git a/crates/wayland/src/udev/mod.rs b/crates/wayland/src/udev/mod.rs index 1490d13..ae0bd7f 100644 --- a/crates/wayland/src/udev/mod.rs +++ b/crates/wayland/src/udev/mod.rs @@ -165,6 +165,30 @@ pub(crate) struct UdevHead { /// further VT switch (either direction) able to recover it, matching a /// CRTC left disabled rather than restored. pub(crate) mode: DrmMode, + /// Set when [`UdevHead::copy_and_flip`] fails; no new flip is attempted + /// for this head again until this deadline passes. + /// + /// Without this, a failed `page_flip` (real and reproduced live: the + /// kernel returns `EBUSY`/"device or resource busy" for a brief window + /// right after a VT-switch resume's `set_crtc` reasserts the mode, + /// before that commit has actually settled) left `flip_pending` still + /// `false` - `copy_and_flip`'s early-return `?` on the failing + /// `page_flip` call skips the line just after it that would have set + /// `flip_pending = true`, so nothing ever marked this head "busy". The + /// next call to `render_udev_frame` (every ~16ms, or sooner -- + /// `event_loop.dispatch`'s timeout is only an upper bound) saw the + /// exact same head still "ready" and every prior damage still pending, + /// tried the exact same flip again, failed the exact same way, forever + /// - a true busy loop with no backoff at all, not merely a missed + /// optimization. Confirmed live from a real session log: tens of + /// thousands of consecutive `page flip failed: Device or resource + /// busy` lines a few *microseconds* apart, the compositor's one thread + /// spinning flat out on nothing else, which is what actually explains + /// the user's report of losing pointer input and the ability to + /// switch VTs at all after switching away and back once - not a + /// separate input bug, this loop simply never yielded the CPU back to + /// anything else, libinput's own event processing included. + pub(crate) flip_retry_after: Option<Instant>, } /// Everything the DRM/udev backend needs that the nested winit backend diff --git a/crates/wayland/src/udev/render.rs b/crates/wayland/src/udev/render.rs index d5d776d..3a62292 100644 --- a/crates/wayland/src/udev/render.rs +++ b/crates/wayland/src/udev/render.rs @@ -136,11 +136,12 @@ impl CompState { head.flip_pending = false; } } + let now = Instant::now(); let ready: Vec<(usize, Output)> = udev .heads .iter() .enumerate() - .filter(|(_, h)| !h.flip_pending) + .filter(|(_, h)| !h.flip_pending && h.flip_retry_after.is_none_or(|t| now >= t)) .map(|(i, h)| (i, h.output.clone())) .collect(); // Kept separately from `presented` below: layer-shell surfaces @@ -597,18 +598,35 @@ impl CompState { // rendered rect either way) but not for the border, // which sits inside the shadow's footprint and needs // the *later* push, not the earlier one, to actually - // end up on top of it. Not fragment-clipped against - // `occluders` like the titlebar/border above: at - // `SHADOW_MAX_ALPHA`'s low opacity, a shadow bleeding - // slightly onto a window stacked in front of this one - // reads as a soft edge, not the hard-line bleed-through - // that made the titlebar/border need it. + // end up on top of it. + // + // Fragment-clipped against `occluders` now, same as the + // titlebar/border above - this used to skip that on + // the reasoning that `SHADOW_MAX_ALPHA`'s low opacity + // would read as a soft edge, not the hard-line bleed- + // through that made the titlebar/border need it. True + // along a shadow's straight edges, false at its + // corners: `shadow_bitmap` falls off by Chebyshev + // (square-ring) distance, not radial, so each corner is + // a hard-edged square block at up to ~35% opacity, not + // a soft vignette - reported live as a small dark + // rectangular patch sitting on top of whatever window a + // floating/cascaded window's own corner happened to + // overlap, most visible exactly where two windows' + // corners nearly meet, which this compositor's default + // cascade placement does constantly. if let Some(shadow) = self.shadow_buffers.get(&id) { let rect = decoration::shadow_rect(frame); - let pos = ((rect.x - origin.x) as f64, (rect.y - origin.y) as f64); - match MemoryRenderBufferRenderElement::from_buffer(&mut udev.renderer, pos, shadow, None, None, None, Kind::Unspecified) { - Ok(elem) => custom_elements.push(crate::elements::OverlayElement::Memory(elem)), - Err(e) => log::warn!("udev: failed to import shadow buffer: {e}"), + for fragment in crate::elements::visible_border_fragments(rect, &occluders) { + let pos = ((fragment.x - origin.x) as f64, (fragment.y - origin.y) as f64); + let src = Rectangle::new( + Point::from(((fragment.x - rect.x) as f64, (fragment.y - rect.y) as f64)), + Size::from((fragment.width as f64, fragment.height as f64)), + ); + match MemoryRenderBufferRenderElement::from_buffer(&mut udev.renderer, pos, shadow, None, Some(src), None, Kind::Unspecified) { + Ok(elem) => custom_elements.push(crate::elements::OverlayElement::Memory(elem)), + Err(e) => log::warn!("udev: failed to import shadow buffer: {e}"), + } } } // The window's own content, at its own `opacity` -- @@ -834,9 +852,25 @@ impl CompState { if has_damage { let head = &mut udev.heads[index]; if let Err(e) = head.copy_and_flip(&udev.card, back, &damage_rects) { - log::error!("udev: page flip failed: {e}"); + // Backed off, not retried on the very next poll tick -- + // see `UdevHead::flip_retry_after`'s own doc comment for + // the real, live-reproduced incident this prevents: a + // failing flip (confirmed live as `EBUSY` right after a + // VT-switch resume, while the kernel's own `set_crtc` + // commit was still settling) used to be retried + // immediately, forever, since nothing else gated + // `ready` on anything but `flip_pending` - which a + // failed `page_flip` call never sets. A fixed, short + // cooldown is enough to ride out that kind of transient + // kernel-side race without needing to distinguish it + // from a real, permanent failure - either way, hammering + // the same doomed `page_flip` call in a tight loop with + // no backoff at all was never the right response. + head.flip_retry_after = Some(Instant::now() + Duration::from_millis(200)); + log::error!("udev: page flip failed: {e} - retrying in 200ms"); continue; } + head.flip_retry_after = None; // This buffer is now fully up to date. It won't be rendered // into again until the *other* slot has also been presented // once (strict two-buffer alternation), so by then it will diff --git a/crates/wayland/src/udev/session.rs b/crates/wayland/src/udev/session.rs index 561ba7a..e39c578 100644 --- a/crates/wayland/src/udev/session.rs +++ b/crates/wayland/src/udev/session.rs @@ -107,6 +107,7 @@ pub(crate) fn register_session_notifier(handle: &LoopHandle<'static, CompState>, // scanned out something else entirely in between). head.flip_pending = false; head.ages = [0, 0]; + head.flip_retry_after = None; } data.render_udev_frame(); } diff --git a/crates/wayland/src/winit/render.rs b/crates/wayland/src/winit/render.rs index 60cccb4..5784520 100644 --- a/crates/wayland/src/winit/render.rs +++ b/crates/wayland/src/winit/render.rs @@ -20,7 +20,41 @@ impl WaylandPlatform { layer_map_for_output(&self.output).arrange(); } - let age = self.backend.buffer_age().unwrap_or(0); + // Always `0` ("contents undefined, damage everything"), not + // `self.backend.buffer_age()` - deliberately never trusted here, + // confirmed live to actually be the source of a real corruption + // bug, not just a missed optimization. `age` tells + // `damage_tracker.render_output` how many past frames' worth of + // damage history it can trust the *current* target buffer to + // already reflect, so it can skip repainting regions nothing has + // touched since. That guarantee depends on the platform's own + // buffer-age report being an honest account of this exact backing + // buffer's real history - which this backend cannot get from a + // nested `winit`/EGL surface hosted inside another Wayland + // session: reproduced live, stable, not a one-frame race -- + // raising one floating window (Firefox) above another (a plain + // terminal) left a rectangular patch of the terminal's own old + // pixels sitting untouched at one edge of Firefox's new, larger, + // fully-opaque topmost window, persisting indefinitely across + // many further frames with no damage anywhere near it to explain + // clearing it. Forcing `age = 0` here (full redraw, every frame) + // made the corruption disappear completely and immediately, and + // nothing else about the scene changed - narrowing the cause to + // exactly this value being wrong, not a geometry, occlusion, or + // z-order bug elsewhere in this file (all independently verified + // correct against the same repro: the window's own border and + // content elements were logged as computed with the right full + // geometry and zero occluders on every single one of the frames + // that still rendered the stale patch). Real hardware's udev + // backend is not affected - it never queries a platform buffer + // age at all, only its own hand-tracked `UdevHead::ages`, advanced + // deterministically from this codebase's own strict two-buffer + // page-flip alternation, not borrowed trust in an outer + // compositor's EGL implementation. This backend exists for nested + // development/testing, not as the real compositor, so paying a + // full software-composited redraw every frame here is the safe + // trade against silently wrong pixels persisting on screen. + let age = 0; let (renderer, mut framebuffer) = self.backend.bind().map_err(err)?; // Locked: srdwm's own native lock UI, or an external locker's @@ -285,18 +319,33 @@ impl WaylandPlatform { // "spacing before the border". Positioned from `geom`, not // `w.geometry`, same reasoning as the border above (a stale- // position shadow during an animated tween looks as detached - // as the border did before that fix). Not fragment-clipped - // against `occluders` like the titlebar/border above: at - // `SHADOW_MAX_ALPHA`'s low opacity, a shadow bleeding slightly - // onto a window stacked in front of this one reads as a soft - // edge, not the hard-line bleed-through that made the - // titlebar/border need it. + // as the border did before that fix). + // + // Fragment-clipped against `occluders` now, same as the + // titlebar/border above - this used to skip that on the + // reasoning that `SHADOW_MAX_ALPHA`'s low opacity would read + // as a soft edge, not the hard-line bleed-through that made + // the titlebar/border need it. True along a shadow's straight + // edges, false at its corners: `shadow_bitmap` falls off by + // Chebyshev (square-ring) distance, not radial, so each corner + // is a hard-edged square block at up to ~35% opacity, not a + // soft vignette - reported live as a small dark rectangular + // patch sitting on top of whatever window a floating/cascaded + // window's own corner happened to overlap, most visible + // exactly where two windows' corners nearly meet, which this + // compositor's default cascade placement does constantly. if let Some(shadow) = self.state.shadow_buffers.get(&id) { let rect = decoration::shadow_rect(frame); - let pos = (rect.x as f64, rect.y as f64); - match MemoryRenderBufferRenderElement::from_buffer(renderer, pos, shadow, None, None, None, Kind::Unspecified) { - Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))), - Err(e) => log::warn!("failed to import shadow buffer for window {id}: {e}"), + for fragment in crate::elements::visible_border_fragments(rect, &occluders) { + let pos = (fragment.x as f64, fragment.y as f64); + let src = Rectangle::new( + Point::from(((fragment.x - rect.x) as f64, (fragment.y - rect.y) as f64)), + Size::from((fragment.width as f64, fragment.height as f64)), + ); + match MemoryRenderBufferRenderElement::from_buffer(renderer, pos, shadow, None, Some(src), None, Kind::Unspecified) { + Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))), + Err(e) => log::warn!("failed to import shadow buffer for window {id}: {e}"), + } } } // The window's own content, at its own `opacity` - see the |