atagen

monolith

public
Star0
Watch0

Commit 0f99c9f9166b

tarball

atagen <boss@atagen.co>2026-10-19 07:51 UTC

1 file changed+82-7
drm: retry failed frames and recover lost page flips

`render_surface` reset `redraw` to `animating` after a `render_frame` or `queue_frame` error, so the stale image stayed until something unrelated redrew. Failures now keep the request and retry with a capped backoff. A flip watchdog clears `queued` when a vblank never arrives, which `DrmEvent::Error` can cause and which otherwise wedged the output for good.
Modified file: crates/monolith/src/drm.rs+82 additions−7 deletions
@@ -73,6 +73,11 @@ struct Surface {
7373 drm: Out,
7474 /// A frame is on its way to the screen and its vblank has not landed.
7575 queued: bool,
76+ queued_at: u64,
77+ /// Fires when a queued frame's vblank never arrives.
78+ watchdog: Option<RegistrationToken>,
79+ /// Consecutive frames that failed to render or queue.
80+ failures: u32,
7681 redraw: bool,
7782 last_vblank_ns: Option<u64>,
7883 refresh_ns: u64,
@@ -105,6 +110,12 @@ impl Surface {
105110 fn next_deadline(&self, now: u64) -> Option<(u64, u64)> {
106111 crate::pacing::deadline(self.last_vblank_ns?, self.refresh_ns, self.budget.ns(), now)
107112 }
113+
114+ /// How long a queued frame may go without its vblank. Under VRR a flip
115+ /// waits for the panel's longest frame, so the floor stays well above it.
116+ fn flip_timeout(&self) -> u64 {
117+ (self.refresh_ns * 4).max(100_000_000)
118+ }
108119 }
109120
110121 /// The GPU monolith drives on bare metal, the seat's primary or else its
@@ -392,6 +403,9 @@ fn connect(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
392403 id,
393404 drm: out,
394405 queued: false,
406+ queued_at: 0,
407+ watchdog: None,
408+ failures: 0,
395409 redraw: true,
396410 last_vblank_ns: None,
397411 scheduled: None,
@@ -516,7 +530,7 @@ fn disconnect(state: &mut State, crtc: crtc::Handle) {
516530 let Some(surface) = drm.surfaces.remove(&crtc) else {
517531 return;
518532 };
519- if let Some(token) = surface.scheduled {
533+ for token in [surface.scheduled, surface.watchdog].into_iter().flatten() {
520534 drm.handle.remove(token);
521535 }
522536 tracing::info!(name = surface.output.name(), "output disconnected");
@@ -677,6 +691,54 @@ fn arm(
677691 }
678692 }
679693
694+/// Arms the flip watchdog unless it already runs.
695+fn watch(handle: &LoopHandle<'static, State>, surface: &mut Surface, crtc: crtc::Handle) {
696+ if surface.watchdog.is_some() {
697+ return;
698+ }
699+ let after = Duration::from_nanos(surface.flip_timeout());
700+ let token = handle.insert_source(Timer::from_duration(after), move |_, _, state| {
701+ flip_watchdog(state, crtc)
702+ });
703+ match token {
704+ Ok(token) => surface.watchdog = Some(token),
705+ Err(e) => tracing::warn!(
706+ output = surface.output.name(),
707+ "could not arm the flip watchdog: {e}"
708+ ),
709+ }
710+}
711+
712+/// Recovers an output whose page flip event never came, as after a
713+/// `DrmEvent::Error`, so `queued` cannot hold it dark forever.
714+fn flip_watchdog(state: &mut State, crtc: crtc::Handle) -> TimeoutAction {
715+ let Some(drm) = state.backend.drm_mut() else {
716+ return TimeoutAction::Drop;
717+ };
718+ let active = drm.session.is_active();
719+ let Some(surface) = drm.surfaces.get_mut(&crtc) else {
720+ return TimeoutAction::Drop;
721+ };
722+ let age = now_ns().saturating_sub(surface.queued_at);
723+ let limit = surface.flip_timeout();
724+ if active && surface.queued && age < limit {
725+ return TimeoutAction::ToDuration(Duration::from_nanos(limit - age));
726+ }
727+ surface.watchdog = None;
728+ if !active || !surface.queued {
729+ return TimeoutAction::Drop;
730+ }
731+ tracing::warn!(output = surface.output.name(), "page flip never landed");
732+ if let Err(e) = surface.drm.frame_submitted() {
733+ tracing::warn!("frame was not submitted: {e}");
734+ }
735+ surface.queued = false;
736+ surface.target_ns = None;
737+ surface.redraw = true;
738+ schedule(state, crtc);
739+ TimeoutAction::Drop
740+}
741+
680742 /// An animation whose frame had nothing to show, like one hidden behind a
681743 /// fullscreen window, has no flip to wait for. Under VRR scheduling again
682744 /// would draw at once and recurse until the stack overflowed, so the next
@@ -694,8 +756,11 @@ fn draw(state: &mut State, crtc: crtc::Handle, target: u64) {
694756 if surface.scheduled.is_some() {
695757 return;
696758 }
697- let wait = Duration::from_nanos(surface.refresh_ns.max(1_000_000));
698- arm(&drm.handle, surface, crtc, wait, true, move |state| {
759+ let backoff = surface.failures.saturating_sub(1).min(6);
760+ let wait = (surface.refresh_ns.max(1_000_000) << backoff).min(1_000_000_000);
761+ let wait = Duration::from_nanos(wait);
762+ let preemptible = surface.failures == 0;
763+ arm(&drm.handle, surface, crtc, wait, preemptible, move |state| {
699764 schedule(state, crtc);
700765 });
701766 }
@@ -804,6 +869,7 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
804869 let render_began = std::time::Instant::now();
805870 let cpu_began = thread_cpu_ns();
806871 let mut queue_ns = 0;
872+ let mut failed = false;
807873 match surface
808874 .drm
809875 .render_frame(renderer, &elements, CLEAR, drm.frame_flags)
@@ -828,15 +894,23 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
828894 match surface.drm.queue_frame(()) {
829895 Ok(()) => {
830896 surface.queued = true;
897+ surface.queued_at = now_ns();
898+ watch(&drm.handle, surface, crtc);
831899 lock.shown(&surface.output);
832900 captures.output_damaged(&surface.output);
833901 }
834- Err(e) => tracing::warn!("queueing a frame failed: {e}"),
902+ Err(e) => {
903+ failed = true;
904+ tracing::warn!("queueing a frame failed: {e}");
905+ }
835906 }
836907 queue_ns = queue_began.elapsed().as_nanos() as u64;
837908 }
838909 Ok(_) => lock.shown(&surface.output),
839- Err(e) => tracing::warn!("rendering failed: {e}"),
910+ Err(e) => {
911+ failed = true;
912+ tracing::warn!("rendering failed: {e}");
913+ }
840914 }
841915 let render_cpu_ns = thread_cpu_ns() - cpu_began;
842916 let render_ns = render_began.elapsed().as_nanos() as u64 - queue_ns;
@@ -857,10 +931,11 @@ fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
857931 if !surface.queued {
858932 surface.target_ns = None;
859933 }
860- surface.redraw = animating;
934+ surface.failures = if failed { surface.failures + 1 } else { 0 };
935+ surface.redraw = animating || failed;
861936 let elapsed = core.start.elapsed();
862937 crate::frame::send_frame_callbacks(wm, lock, surface.id, &surface.output, now, elapsed);
863- animating && !surface.queued
938+ (animating || failed) && !surface.queued
864939 }
865940
866941 /// CPU time this thread has used, to tell a render that works from one that