Commit 6482fe98839d
tarball1 file changed+144-44
@@ -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_ns73
- }74
- _ => now,73
+ /// The first vblank a render starting at `now` can still make, and the74
+ /// 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 CPU89
+/// render time covers the CPU side, and a slack that grows on every missed90
+/// 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_ns111
+ }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 = self126
+ .slack_ns127
+ .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_ns340
- && surface.redraw341
- && t - last > surface.refresh_ns * 3 / 2342
- {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 reachable422
+/// 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_ns445
+ });446
+ if let Some(target) = target {447
+ draw(state, crtc, target);448
+ }449
+ TimeoutAction::Drop450
+ });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 still482
+/// 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
} = state392499
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 _ = drm445
- .handle446
- .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::Drop452
- });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.queued461561
}462562
463563
fn on_input(state: &mut State, event: InputEvent<LibinputInputBackend>) {