atagen

monolith

public
Star0
Watch0

Commit 6482fe98839d

tarball

atagen <boss@atagen.co>2026-10-16 13:03 UTC

1 file changed+144-44
monolith: render each output just in time for its next vblank

Rendering right after a vblank made every frame a full refresh old by the time it scanned out, and commits landing just after the render waited another refresh. Each output now starts rendering at the last moment that still makes its next vblank and samples animations at that vblank. The lead time is the worst recent CPU render time plus a slack that grows on every missed vblank and decays while frames land, covering GPU time and the commit without measuring them.
Modified file: crates/monolith/src/drm.rs+144 additions−44 deletions
@@ -24,14 +24,14 @@ use smithay::backend::udev::{UdevBackend, UdevEvent, all_gpus, primary_gpu};
2424 use smithay::input::pointer::AxisFrame;
2525 use smithay::output::{Mode, Output, PhysicalProperties, Scale, Subpixel};
2626 use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
27-use smithay::reexports::calloop::{EventLoop, LoopHandle};
27+use smithay::reexports::calloop::{EventLoop, LoopHandle, RegistrationToken};
2828 use smithay::reexports::drm::Device as _;
2929 use smithay::reexports::drm::control::{ModeTypeFlags, connector, crtc};
3030 use smithay::reexports::input::Libinput;
3131 use smithay::reexports::rustix::fs::OFlags;
3232 use smithay::utils::{DeviceFd, Point, Transform};
3333 use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner};
34-use std::collections::HashMap;
34+use std::collections::{HashMap, VecDeque};
3535 use std::path::{Path, PathBuf};
3636 use std::time::Duration;
3737
@@ -62,17 +62,70 @@ struct Surface {
6262 redraw: bool,
6363 last_vblank_ns: Option<u64>,
6464 refresh_ns: u64,
65+ /// The pending render, timed to start just in time for `target_ns`.
66+ scheduled: Option<RegistrationToken>,
67+ /// The vblank the frame being drawn or queued is meant for.
68+ target_ns: Option<u64>,
69+ budget: Budget,
6570 }
6671
6772 impl Surface {
68- /// The vblank after `now` that a frame rendered now will reach.
69- fn predict(&self, now: u64) -> u64 {
70- match self.last_vblank_ns {
71- Some(last) if last <= now && self.refresh_ns > 0 => {
72- last + ((now - last) / self.refresh_ns + 1) * self.refresh_ns
73- }
74- _ => now,
73+ /// The first vblank a render starting at `now` can still make, and the
74+ /// latest moment to start it.
75+ fn next_deadline(&self, now: u64) -> Option<(u64, u64)> {
76+ let last = self.last_vblank_ns?;
77+ let refresh = self.refresh_ns;
78+ if refresh == 0 {
79+ return None;
80+ }
81+ let earliest = now + self.budget.ns();
82+ let periods = earliest.saturating_sub(last).div_ceil(refresh).max(1);
83+ let vblank = last + periods * refresh;
84+ Some((vblank, vblank - self.budget.ns()))
85+ }
86+}
87+
88+/// How long before a vblank rendering must start. The worst recent CPU
89+/// render time covers the CPU side, and a slack that grows on every missed
90+/// vblank and slowly shrinks while frames land covers GPU time and the commit.
91+struct Budget {
92+ renders: VecDeque<u64>,
93+ slack_ns: u64,
94+}
95+
96+impl Budget {
97+ const KEEP: usize = 32;
98+ const MIN_SLACK_NS: u64 = 1_000_000;
99+ const STEP_NS: u64 = 500_000;
100+ const DECAY_NS: u64 = 10_000;
101+
102+ fn new() -> Self {
103+ Self {
104+ renders: VecDeque::with_capacity(Self::KEEP),
105+ slack_ns: 2_000_000,
106+ }
107+ }
108+
109+ fn ns(&self) -> u64 {
110+ self.renders.iter().max().copied().unwrap_or(0) + self.slack_ns
111+ }
112+
113+ fn record(&mut self, render_ns: u64) {
114+ if self.renders.len() == Self::KEEP {
115+ self.renders.pop_front();
75116 }
117+ self.renders.push_back(render_ns);
118+ }
119+
120+ fn missed(&mut self, refresh_ns: u64) {
121+ self.slack_ns = (self.slack_ns + Self::STEP_NS).min(refresh_ns / 2);
122+ }
123+
124+ fn landed(&mut self) {
125+ self.slack_ns = self
126+ .slack_ns
127+ .saturating_sub(Self::DECAY_NS)
128+ .max(Self::MIN_SLACK_NS);
76129 }
77130 }
78131
@@ -304,15 +357,24 @@ fn connect(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
304357 queued: false,
305358 redraw: true,
306359 last_vblank_ns: None,
360+ scheduled: None,
361+ target_ns: None,
362+ budget: Budget::new(),
307363 refresh_ns: 1_000_000_000_000 / u64::from(wl_mode.refresh.max(1) as u32),
308364 },
309365 );
310366 }
311367
312368 fn disconnect(state: &mut State, crtc: crtc::Handle) {
313- let Some(surface) = state.drm.as_mut().and_then(|d| d.surfaces.remove(&crtc)) else {
369+ let Some(drm) = state.drm.as_mut() else {
370+ return;
371+ };
372+ let Some(surface) = drm.surfaces.remove(&crtc) else {
314373 return;
315374 };
375+ if let Some(token) = surface.scheduled {
376+ drm.handle.remove(token);
377+ }
316378 tracing::info!(name = surface.output.name(), "output disconnected");
317379 state.space.unmap_output(&surface.output);
318380 state.layout.remove_output(surface.id);
@@ -336,20 +398,63 @@ fn on_vblank(state: &mut State, crtc: crtc::Handle, meta: &mut Option<DrmEventMe
336398 surface.queued = false;
337399 if let Some(DrmEventTime::Monotonic(t)) = meta.as_ref().map(|m| m.time) {
338400 let t = t.as_nanos() as u64;
339- if let Some(last) = surface.last_vblank_ns
340- && surface.redraw
341- && t - last > surface.refresh_ns * 3 / 2
342- {
343- let missed = (t - last) / surface.refresh_ns - 1;
344- tracing::debug!(
345- output = surface.output.name(),
346- missed,
347- "missed vblanks while animating"
348- );
401+ if let Some(target) = surface.target_ns.take() {
402+ if t > target + surface.refresh_ns / 2 {
403+ surface.budget.missed(surface.refresh_ns);
404+ tracing::debug!(
405+ output = surface.output.name(),
406+ late_ns = t - target,
407+ budget_ns = surface.budget.ns(),
408+ "frame missed its vblank"
409+ );
410+ } else {
411+ surface.budget.landed();
412+ }
349413 }
350414 surface.last_vblank_ns = Some(t);
351415 }
352- render_surface(state, crtc);
416+ if surface.redraw {
417+ schedule(state, crtc);
418+ }
419+}
420+
421+/// Starts a render at the last moment that still makes the next reachable
422+/// vblank, so commits arriving until then make it into the frame.
423+fn schedule(state: &mut State, crtc: crtc::Handle) {
424+ let Some(drm) = state.drm.as_mut() else {
425+ return;
426+ };
427+ let Some(surface) = drm.surfaces.get_mut(&crtc) else {
428+ return;
429+ };
430+ if surface.queued || surface.scheduled.is_some() {
431+ return;
432+ }
433+ let now = now_ns();
434+ let Some((vblank, start)) = surface.next_deadline(now) else {
435+ draw(state, crtc, now);
436+ return;
437+ };
438+ surface.target_ns = Some(vblank);
439+ let wait = Timer::from_duration(Duration::from_nanos(start.saturating_sub(now)));
440+ let token = drm.handle.insert_source(wait, move |_, _, state| {
441+ let target = state.drm.as_mut().and_then(|d| {
442+ let s = d.surfaces.get_mut(&crtc)?;
443+ s.scheduled = None;
444+ s.target_ns
445+ });
446+ if let Some(target) = target {
447+ draw(state, crtc, target);
448+ }
449+ TimeoutAction::Drop
450+ });
451+ surface.scheduled = token.ok();
452+}
453+
454+fn draw(state: &mut State, crtc: crtc::Handle, target: u64) {
455+ if render_surface(state, crtc, target) {
456+ schedule(state, crtc);
457+ }
353458 }
354459
355460 /// Renders every output that has something new and no frame in flight.
@@ -369,11 +474,13 @@ pub fn render(state: &mut State) {
369474 .map(|(c, _)| *c)
370475 .collect();
371476 for crtc in ready {
372- render_surface(state, crtc);
477+ schedule(state, crtc);
373478 }
374479 }
375480
376-fn render_surface(state: &mut State, crtc: crtc::Handle) {
481+/// Draws one output for the vblank at `target`. Returns whether it still
482+/// animates without a frame in flight, and so needs another render.
483+fn render_surface(state: &mut State, crtc: crtc::Handle, target: u64) -> bool {
377484 let State {
378485 drm: Some(drm),
379486 renderer,
@@ -390,21 +497,21 @@ fn render_surface(state: &mut State, crtc: crtc::Handle) {
390497 ..
391498 } = state
392499 else {
393- return;
500+ return false;
394501 };
395502 if !drm.session.is_active() {
396- return;
503+ return false;
397504 }
398505 let Some(surface) = drm.surfaces.get_mut(&crtc) else {
399- return;
506+ return false;
400507 };
401508 if surface.queued {
402- return;
509+ return false;
403510 }
404511 let began = std::time::Instant::now();
405- let now = surface.predict(now_ns());
512+ let now = target;
406513 let Some(area) = layout.output(surface.id).map(|o| o.area) else {
407- return;
514+ return false;
408515 };
409516 let mut elements = Vec::new();
410517 let local = *pointer_pos - Point::from((area.x, area.y));
@@ -434,23 +541,15 @@ fn render_surface(state: &mut State, crtc: crtc::Handle) {
434541 Ok(_) => {}
435542 Err(e) => tracing::warn!("rendering failed: {e}"),
436543 }
437- let took = began.elapsed();
438- if took.as_nanos() as u64 > surface.refresh_ns / 2 {
439- tracing::debug!(output = surface.output.name(), ?took, "slow frame");
544+ let took = began.elapsed().as_nanos() as u64;
545+ surface.budget.record(took);
546+ if took > surface.refresh_ns / 2 {
547+ tracing::debug!(output = surface.output.name(), took_ns = took, "slow frame");
440548 }
441- surface.redraw = animating;
442- if animating && !surface.queued {
443- let wait = Duration::from_nanos(surface.refresh_ns);
444- let _ = drm
445- .handle
446- .insert_source(Timer::from_duration(wait), move |_, _, state| {
447- if let Some(s) = state.drm.as_mut().and_then(|d| d.surfaces.get_mut(&crtc)) {
448- s.redraw = true;
449- }
450- render(state);
451- TimeoutAction::Drop
452- });
549+ if !surface.queued {
550+ surface.target_ns = None;
453551 }
552+ surface.redraw = animating;
454553 let elapsed = start.elapsed();
455554 let output = surface.output.clone();
456555 for window in space.elements_for_output(&output) {
@@ -458,6 +557,7 @@ fn render_surface(state: &mut State, crtc: crtc::Handle) {
458557 Some(output.clone())
459558 });
460559 }
560+ animating && !surface.queued
461561 }
462562
463563 fn on_input(state: &mut State, event: InputEvent<LibinputInputBackend>) {