atagen

monolith

public
Star0
Watch0

Commit 8f974106aae3

tarball

atagen <boss@atagen.co>2026-10-17 10:49 UTC

1 file changed+28-2
monolith: wait a refresh before retrying an animation frame that showed nothing

Under VRR, schedule draws immediately, so an animation behind a fullscreen window produced empty frames that rescheduled themselves through draw and schedule until the stack overflowed and the compositor aborted. Revealing a tag behind a fullscreen window was enough.
Modified file: crates/monolith/src/drm.rs+28 additions−2 deletions
@@ -520,10 +520,36 @@ fn schedule(state: &mut State, crtc: crtc::Handle) {
520520 surface.scheduled = token.ok();
521521 }
522522
523+/// An animation whose frame had nothing to show, like one hidden behind a
524+/// fullscreen window, has no flip to wait for. Under VRR scheduling again
525+/// would draw at once and recurse until the stack overflowed, so the next
526+/// try waits a refresh.
523527 fn draw(state: &mut State, crtc: crtc::Handle, target: u64) {
524- if render_surface(state, crtc, target) {
525- schedule(state, crtc);
528+ if !render_surface(state, crtc, target) {
529+ return;
530+ }
531+ let Some(drm) = state.backend.drm_mut() else {
532+ return;
533+ };
534+ let Some(surface) = drm.surfaces.get_mut(&crtc) else {
535+ return;
536+ };
537+ if surface.scheduled.is_some() {
538+ return;
526539 }
540+ let wait = Timer::from_duration(Duration::from_nanos(surface.refresh_ns.max(1_000_000)));
541+ let token = drm.handle.insert_source(wait, move |_, _, state| {
542+ if let Some(s) = state
543+ .backend
544+ .drm_mut()
545+ .and_then(|d| d.surfaces.get_mut(&crtc))
546+ {
547+ s.scheduled = None;
548+ }
549+ schedule(state, crtc);
550+ TimeoutAction::Drop
551+ });
552+ surface.scheduled = token.ok();
527553 }
528554
529555 /// Renders every output that has something new and no frame in flight.