denise_drm/surface.rs
1//! The scanout surface: dumb buffers, modeset, and page flips.
2
3use denise::{Frame, MAX_DAMAGE_RECTS, PixelFormat, Rect, Size, Surface, SurfaceError};
4use drm::Device as _;
5use drm::DriverCapability;
6use drm::buffer::Buffer as _;
7use drm::control::{
8 Device as ControlDevice, Event, Mode, PageFlipFlags, connector, crtc, dumbbuffer::DumbBuffer,
9 framebuffer,
10};
11use drm_fourcc::DrmFourcc;
12
13use crate::device::Card;
14use crate::error::DrmError;
15use crate::mode::{self, ModePreference, OutputPreference};
16use crate::swapchain::Swapchain;
17
18/// Bits per pixel of the scanout format.
19const BPP: u32 = 32;
20/// Colour depth, excluding the ignored high byte.
21const DEPTH: u32 = 24;
22
23/// When a queued flip actually reaches the panel.
24///
25/// A real trade, not a quality setting. Which way it should go depends on what is
26/// on the screen, and the default here is chosen for the kind of thing Denise is
27/// built for rather than for the kind of thing a compositor is built for.
28#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
29pub enum PresentMode {
30 /// Flip immediately, part-way through a scan-out if necessary. Tears.
31 ///
32 /// The default, and measured on a Pi 3 A+ driving 1920x1080 the reason is
33 /// plain: waiting for vblank costs about 17 ms of latency and was described by
34 /// the person operating it as lagging several milliseconds behind the whole
35 /// time. Flipping immediately removes that wait entirely.
36 ///
37 /// The cost is a horizontal seam where the panel switched buffers mid-frame.
38 /// With damage tracking most updates are a few thousand pixels, so the seam is
39 /// small, brief and in practice invisible — a control panel redrawing a button
40 /// is nothing like a compositor scrolling a window.
41 ///
42 /// This used to say "reconsider for signage or anything with large fast-moving
43 /// content, where a tear crosses something worth looking at". A scrolling
44 /// viewport turned out to be exactly that, and the flicker was reported from a
45 /// Pi within a day of the gallery gaining one. So the mode is no longer a
46 /// promise about every frame: **a frame whose damage covers a quarter of the
47 /// screen's rows or more flips at vblank anyway**, and this asks for async
48 /// flips on the frames where the seam is short and the latency is felt. See
49 /// `flip_flags_for`, including why it counts rows rather than pixels, and
50 /// why it counts the rows covered rather than the rows spanned.
51 ///
52 /// Requires `DRM_CAP_ASYNC_PAGE_FLIP`; drivers without it fall back to
53 /// [`PresentMode::Vsync`], and [`DrmSurface::present_mode`] reports what was
54 /// actually obtained.
55 ///
56 /// **This mode paces the caller only on its large frames.** See
57 /// [`DrmSurface`].
58 #[default]
59 Immediate,
60
61 /// Flip at the next vblank. Never tears.
62 ///
63 /// A flip queued just after a vblank cannot land before the next one, so this
64 /// costs on the order of one refresh period — about 17 ms at 60 Hz. Every
65 /// tear-free system pays it.
66 ///
67 /// In exchange, [`Surface::acquire`] blocks until the flip retires, so the
68 /// display paces the render loop for free and an application needs no frame
69 /// timing of its own.
70 Vsync,
71}
72
73/// How to bring the display up.
74#[derive(Clone, Copy, Debug)]
75pub struct SurfaceConfig {
76 /// Which output to drive.
77 pub output: OutputPreference,
78 /// Which mode to set on it.
79 pub mode: ModePreference,
80 /// How many scanout buffers to rotate through.
81 ///
82 /// Two by default. Three trades latency for smoothness, which is the wrong
83 /// trade for a panel someone is touching.
84 pub buffers: usize,
85
86 /// Whether to wait for vblank before showing a frame.
87 pub present_mode: PresentMode,
88}
89
90impl Default for SurfaceConfig {
91 fn default() -> Self {
92 Self {
93 output: OutputPreference::Auto,
94 mode: ModePreference::Preferred,
95 buffers: 2,
96 present_mode: PresentMode::Vsync,
97 }
98 }
99}
100
101/// One scanout buffer: the allocation, its framebuffer id, and its CPU mapping.
102#[derive(Debug)]
103struct Scanout {
104 dumb: DumbBuffer,
105 fb: framebuffer::Handle,
106 /// Start of the mapping, as `u32` words.
107 ptr: *mut u32,
108 /// Length of the mapping in words.
109 words: usize,
110 /// Length of the mapping in bytes, for `munmap`.
111 bytes: usize,
112}
113
114impl Scanout {
115 fn new(card: &Card, size: Size) -> Result<Self, DrmError> {
116 let mut dumb = card
117 .create_dumb_buffer((size.width, size.height), DrmFourcc::Xrgb8888, BPP)
118 .map_err(|source| DrmError::Allocate {
119 width: size.width,
120 height: size.height,
121 source,
122 })?;
123
124 let fb = card
125 .add_framebuffer(&dumb, DEPTH, BPP)
126 .map_err(DrmError::AddFramebuffer)?;
127
128 // The `drm` crate's mapping unmaps itself on drop, which cannot work here:
129 // the mapping has to outlive the call that made it, and a `Frame` handed to
130 // the renderer borrows from it. So take the pointer and forget the guard,
131 // making this code responsible for the `munmap` in `DrmSurface::drop`.
132 // Mapping once at start-up also saves an mmap/munmap pair every frame.
133 let (ptr, bytes) = {
134 let mut mapping = card.map_dumb_buffer(&mut dumb).map_err(DrmError::Map)?;
135 let slice: &mut [u8] = &mut mapping;
136 let ptr = slice.as_mut_ptr();
137 let bytes = slice.len();
138 core::mem::forget(mapping);
139 (ptr, bytes)
140 };
141
142 let mut scanout = Self {
143 dumb,
144 fb,
145 // SAFETY: `mmap` returns page-aligned memory, which satisfies `u32`
146 // alignment. The cast does not change the region's extent; `words`
147 // below accounts for the narrower element type.
148 ptr: ptr.cast::<u32>(),
149 words: bytes / 4,
150 bytes,
151 };
152
153 // A freshly allocated dumb buffer holds whatever was in that memory.
154 // Without this, the modeset shows one frame of garbage before the first
155 // repaint lands.
156 scanout.pixels_mut().fill(0);
157
158 Ok(scanout)
159 }
160
161 fn pixels_mut(&mut self) -> &mut [u32] {
162 // SAFETY: `ptr` and `words` come from a single successful mapping of this
163 // buffer, which stays mapped until `DrmSurface::drop` unmaps it. `&mut
164 // self` rules out any other live reference to the same region.
165 unsafe { core::slice::from_raw_parts_mut(self.ptr, self.words) }
166 }
167}
168
169/// Above this share of the surface's **rows**, a frame flips at vblank even
170/// under [`PresentMode::Immediate`].
171///
172/// A quarter, as a numerator over [`TEAR_FREE_DENOMINATOR`] so the comparison
173/// stays in integers. The number is not delicate: real damage either spans a
174/// control, which is a few dozen rows, or something that moved a whole column,
175/// which is nearly all of them. There is very little in between.
176const TEAR_FREE_NUMERATOR: u32 = 1;
177const TEAR_FREE_DENOMINATOR: u32 = 4;
178
179/// Which page flip this frame gets: async, or paced by vblank.
180///
181/// **The tear is not the whole cost of tearing.** An async flip lands wherever
182/// the beam happens to be, which for a button redrawing itself puts a seam a few
183/// pixels tall somewhere nobody is looking. For a frame that moved everything —
184/// a scrolling viewport — the seam crosses the thing being read, which is
185/// exactly the case [`PresentMode::Immediate`]'s own documentation says to
186/// reconsider. It reads as flicker, and it was reported as flicker.
187///
188/// The second half is pacing. An async flip never blocks, so a loop that redraws
189/// while input keeps arriving runs as fast as the CPU allows: a Pi 3 A+ paints a
190/// scrolled 1920x1080 viewport in about 14.5 ms, so it will spend a whole core
191/// producing frames that tear, one after another. A vblank-paced flip makes
192/// [`Surface::acquire`] wait for the retire, which caps the loop at the refresh
193/// rate for free. The frame that most needs not to tear is the same frame that
194/// most needs the brakes.
195///
196/// So the mode follows the damage rather than being set once for everything: the
197/// low latency [`PresentMode::Immediate`] exists for is kept where it is felt —
198/// a press lighting a button — and given up on the frames where it is neither
199/// felt nor affordable.
200///
201/// # Rows, not area
202///
203/// The first version of this compared the damaged *area* against the surface,
204/// and a Pi still flashed occasionally. The gallery's sidebar is 300 by 1016 on
205/// a 1920x1080 panel: **14.7% of the pixels, and 94% of the scanlines.** It went
206/// out async and tore across almost the whole height of the screen.
207///
208/// A tear is a horizontal seam, and it appears when the buffer changes under the
209/// beam part-way down. What decides whether it is visible is therefore how many
210/// **rows** the damage spans, not how much of the surface it covers. A full-width
211/// toolbar forty rows tall can tear freely — the seam is a thin band that is gone
212/// next frame. A narrow column down the whole screen cannot.
213///
214/// # Covered, not spanned
215///
216/// Which leaves how to count the rows when the damage is in several pieces. The
217/// first answer here was the bounding box, on the reasoning that one flip
218/// changes the buffer for every rectangle at once, so the beam can seam anywhere
219/// between them. True, and it measures the wrong thing: the beam can seam there,
220/// but nobody can *see* it there. Outside the damage both buffers hold the same
221/// pixels — that is what repainting to the buffer's age guarantees — and a seam
222/// between two identical images is not a seam.
223///
224/// So it counts the rows the damage actually covers, which is what the earlier
225/// reasoning was reaching for anyway: the sidebar covers 1016 rows whichever way
226/// it is counted, and still waits. What changes is the frame this backend was
227/// never meant to catch. The gallery keeps a spinner turning at the top of the
228/// screen, so every frame while a pointer is somewhere in the lower two thirds
229/// carried a 48-row spinner, a 24-row cursor, and a bounding box spanning the
230/// eight hundred untouched rows between them — vblank-paced, all of it, from a
231/// rule written for scrolling. Counting coverage puts that frame back at 72 rows
232/// and back on the async path it was on in 0.13.0.
233///
234/// An empty damage list means the caller presented without saying what changed,
235/// which cannot be assumed to be small.
236fn flip_flags_for(mode: PresentMode, damage: &[Rect], surface: Size) -> PageFlipFlags {
237 let synced = PageFlipFlags::EVENT;
238 let immediate = PageFlipFlags::EVENT | PageFlipFlags::ASYNC;
239
240 if mode == PresentMode::Vsync {
241 return synced;
242 }
243 if damage.is_empty() {
244 return synced;
245 }
246
247 let rows = damaged_rows(damage, surface);
248
249 if rows * TEAR_FREE_DENOMINATOR >= surface.height * TEAR_FREE_NUMERATOR {
250 synced
251 } else {
252 immediate
253 }
254}
255
256/// How many of the surface's scanlines the damage covers, counting an overlap
257/// once.
258///
259/// The vertical extents, merged. Rectangles arrive in no particular order and
260/// may overlap, so this sorts them by top edge — an insertion sort over at most
261/// [`MAX_DAMAGE_RECTS`] items, in a fixed array, because this runs once per
262/// frame on a Pi and must not allocate — and then sweeps, extending the run
263/// while the next span starts before the current one ends.
264///
265/// Rows outside the surface cannot tear, so each span is clipped first. A list
266/// longer than the tracker's own capacity is something this backend has no
267/// business guessing about: it reports the full height, and the caller gets a
268/// vblank.
269fn damaged_rows(damage: &[Rect], surface: Size) -> u32 {
270 if damage.len() > MAX_DAMAGE_RECTS {
271 return surface.height;
272 }
273
274 let bottom_edge = surface.height as i32;
275 let mut spans = [(0i32, 0i32); MAX_DAMAGE_RECTS];
276 let mut len = 0;
277
278 for rect in damage {
279 let top = rect.y.clamp(0, bottom_edge);
280 let bottom = rect.bottom().clamp(0, bottom_edge);
281 if bottom <= top {
282 continue;
283 }
284 let mut i = len;
285 while i > 0 && spans[i - 1].0 > top {
286 spans[i] = spans[i - 1];
287 i -= 1;
288 }
289 spans[i] = (top, bottom);
290 len += 1;
291 }
292
293 let mut rows: u32 = 0;
294 let mut i = 0;
295 while i < len {
296 let (start, mut end) = spans[i];
297 i += 1;
298 // Sorted by top edge, so anything that starts at or before this run's
299 // current end belongs to the same run — and may extend it.
300 while i < len && spans[i].0 <= end {
301 end = end.max(spans[i].1);
302 i += 1;
303 }
304 rows += (end - start) as u32;
305 }
306
307 rows
308}
309
310/// A display brought up under our control, scanning out CPU-rendered buffers.
311///
312/// Takes DRM master on construction and gives it back on drop, restoring whatever
313/// the CRTC was showing before. A clean exit and a panic both hand the console
314/// back, rather than leaving a black screen that needs a power cycle.
315///
316/// # Pacing is the caller's job under [`PresentMode::Immediate`]
317///
318/// Under [`PresentMode::Vsync`], [`acquire`](Surface::acquire) blocks until the
319/// previous flip retires, so a bare `loop { acquire; draw; present }` runs at
320/// exactly the refresh rate and costs nothing extra.
321///
322/// Under [`PresentMode::Immediate`] — the default — a small frame does not wait.
323/// The same loop runs as fast as the CPU allows and will happily use a whole core
324/// drawing frames no one will ever see. An application must either draw only when
325/// something changed, which damage tracking makes natural, or keep a frame
326/// deadline of its own. `examples/kiosk` does both.
327///
328/// This is not a flaw in async flips; it is what removing the wait means. It is
329/// also why a *large* frame gives the wait back: see `flip_flags_for`, where
330/// the same decision that keeps a seam off a scrolling viewport is what stops the
331/// loop repainting it a hundred times a second.
332#[derive(Debug)]
333pub struct DrmSurface {
334 // `pub(crate)` for the cursor plane, which lives in its own module and needs
335 // the card and the CRTC to talk to.
336 pub(crate) card: Card,
337 pub(crate) crtc: crtc::Handle,
338 connector: connector::Handle,
339 buffers: Vec<Scanout>,
340 swapchain: Swapchain,
341 size: Size,
342 /// Row stride in pixels, from the driver's pitch. Rarely equals the width.
343 stride: u32,
344 /// A flip has been queued and its completion event not yet read.
345 flip_pending: bool,
346 /// The mode actually in force, after checking what the driver supports.
347 present_mode: PresentMode,
348 saved_crtc: Option<crtc::Info>,
349 mode_name: String,
350 /// The hardware cursor plane's buffer, allocated on first use. `None` until
351 /// an application asks for a sprite, because a panel driven by touch never
352 /// wants one and should not pay for the allocation.
353 pub(crate) cursor: Option<crate::cursor::CursorBuffer>,
354}
355
356impl DrmSurface {
357 /// Brings up the display.
358 pub fn new(card: Card, config: SurfaceConfig) -> Result<Self, DrmError> {
359 card.become_master()?;
360
361 let (handles, infos) = card.connectors()?;
362 let selection = mode::select(&infos, config.output, config.mode)?;
363 let connector = handles[selection.connector];
364 let crtc = card.crtc_for(connector)?;
365
366 // Re-read the connector for the driver's own `Mode`, since the selection
367 // policy works on a copy that deliberately drops the timing details.
368 let info = card
369 .get_connector(connector, false)
370 .map_err(DrmError::Resources)?;
371 let mode: Mode = info.modes()[selection.mode];
372 let (width, height) = mode.size();
373 let size = Size::new(u32::from(width), u32::from(height));
374
375 let saved_crtc = card.get_crtc(crtc).ok();
376
377 let mut buffers = Vec::with_capacity(config.buffers);
378 for _ in 0..Swapchain::new(config.buffers).count() {
379 buffers.push(Scanout::new(&card, size)?);
380 }
381
382 // The pitch is the driver's, not ours: it is padded for alignment and is
383 // routinely wider than the visible row. Everything downstream addresses
384 // rows through this, never through the width.
385 let pitch = buffers[0].dumb.pitch();
386 if !pitch.is_multiple_of(4) {
387 return Err(DrmError::UnalignedPitch { pitch });
388 }
389
390 // Ask the driver rather than assume. Requesting an async flip on hardware
391 // that cannot do one fails the ioctl every frame, which would turn a
392 // latency preference into a display that never updates.
393 let async_capable = card
394 .get_driver_capability(DriverCapability::ASyncPageFlip)
395 .is_ok_and(|supported| supported != 0);
396
397 let present_mode = match config.present_mode {
398 PresentMode::Immediate if async_capable => PresentMode::Immediate,
399 _ => PresentMode::Vsync,
400 };
401
402 card.set_crtc(crtc, Some(buffers[0].fb), (0, 0), &[connector], Some(mode))
403 .map_err(|source| DrmError::SetMode {
404 mode: format!("{width}x{height}"),
405 crtc: u32::from(crtc),
406 source,
407 })?;
408
409 // Buffer 0 is now being scanned out, so the next frame must not draw into
410 // it. Recording the modeset as a presentation advances past it.
411 let mut swapchain = Swapchain::new(config.buffers);
412 swapchain.presented();
413
414 Ok(Self {
415 card,
416 crtc,
417 connector,
418 buffers,
419 swapchain,
420 size,
421 stride: pitch / 4,
422 flip_pending: false,
423 present_mode,
424 saved_crtc,
425 mode_name: format!("{width}x{height}@{}", mode.vrefresh()),
426 cursor: None,
427 })
428 }
429
430 /// Opens the first display-capable device and brings it up.
431 pub fn open(config: SurfaceConfig) -> Result<Self, DrmError> {
432 Self::new(Card::open_first()?, config)
433 }
434
435 /// The open card, for driving DRM objects the surface does not own —
436 /// video planes above all. One process is DRM master, so anything else
437 /// touching the display **must** go through this card rather than a
438 /// second open, which would either fail or fight. `denise-video` is the
439 /// consumer this seam exists for.
440 pub fn card(&self) -> &Card {
441 &self.card
442 }
443
444 /// The CRTC being driven, for placing planes on it.
445 pub fn crtc(&self) -> drm::control::crtc::Handle {
446 self.crtc
447 }
448
449 /// The mode in force, for logging.
450 pub fn mode_name(&self) -> &str {
451 &self.mode_name
452 }
453
454 /// Row stride in pixels.
455 pub fn stride(&self) -> u32 {
456 self.stride
457 }
458
459 /// Number of buffers in rotation.
460 pub fn buffer_count(&self) -> usize {
461 self.buffers.len()
462 }
463
464 /// The presentation mode actually in force.
465 ///
466 /// May be [`PresentMode::Vsync`] even when [`PresentMode::Immediate`] was
467 /// asked for, if the driver does not advertise `DRM_CAP_ASYNC_PAGE_FLIP`.
468 pub fn present_mode(&self) -> PresentMode {
469 self.present_mode
470 }
471
472 /// Blocks until any queued flip has actually happened.
473 ///
474 /// This is the vsync wait, and it is where the frame loop should spend its
475 /// idle time: the process sleeps in the kernel until the scanout engine is
476 /// done, instead of spinning to guess when that was.
477 ///
478 /// How long that sleep lasts is the driver's business, not ours, and not every
479 /// driver makes it last. `virtio-gpu` under a hypervisor completes the flip as
480 /// soon as the host acknowledges it, so this returns immediately and the loop
481 /// runs at thousands of frames a second on a 75 Hz mode. Real scanout hardware
482 /// — vc4 on a Pi, for one — retires the flip at vblank and this blocks for the
483 /// rest of the frame.
484 ///
485 /// A caller that must not spin when the driver declines to pace it needs its
486 /// own frame deadline on top. That belongs in the event loop, with input, and
487 /// arrives with it.
488 fn wait_for_flip(&mut self) -> Result<(), DrmError> {
489 while self.flip_pending {
490 let events = self.card.receive_events().map_err(DrmError::WaitVblank)?;
491 for event in events {
492 if matches!(event, Event::PageFlip(_)) {
493 self.flip_pending = false;
494 }
495 }
496 }
497 Ok(())
498 }
499}
500
501impl Surface for DrmSurface {
502 fn size(&self) -> Size {
503 self.size
504 }
505
506 fn scale_factor(&self) -> f32 {
507 // DRM has no notion of a scale factor. A panel's physical size is known,
508 // but turning that into a UI scale is policy, and policy does not belong
509 // in the backend.
510 1.0
511 }
512
513 fn format(&self) -> PixelFormat {
514 PixelFormat::Xrgb8888
515 }
516
517 fn acquire(&mut self) -> Result<Frame<'_>, SurfaceError> {
518 // The buffer we are about to hand out may still be on screen until the
519 // previous flip retires. Drawing into it before then is what tearing is.
520 self.wait_for_flip()?;
521
522 let index = self.swapchain.current();
523 let age = self.swapchain.age();
524 let size = self.size;
525 let stride = self.stride;
526
527 Frame::new(
528 self.buffers[index].pixels_mut(),
529 size,
530 stride,
531 PixelFormat::Xrgb8888,
532 age,
533 )
534 }
535
536 fn present(&mut self, damage: &[Rect]) -> Result<(), SurfaceError> {
537 // Damage cannot restrict the *upload* — a page flip swaps whole buffers,
538 // and wiring partial updates in would need atomic modesetting and
539 // `FB_DAMAGE_CLIPS`, which most drivers ignore. It can decide something
540 // else, though: whether this particular frame is one a tear would show
541 // on. See `flip_flags_for`.
542 let index = self.swapchain.current();
543 let fb = self.buffers[index].fb;
544 let flags = flip_flags_for(self.present_mode, damage, self.size);
545
546 self.card
547 .page_flip(self.crtc, fb, flags, None)
548 .map_err(DrmError::PageFlip)?;
549
550 self.flip_pending = true;
551 self.swapchain.presented();
552 Ok(())
553 }
554}
555
556impl Drop for DrmSurface {
557 fn drop(&mut self) {
558 // Let the last flip retire before pulling the buffers out from under the
559 // scanout engine.
560 let _ = self.wait_for_flip();
561
562 if let Some(saved) = self.saved_crtc.as_ref() {
563 let _ = self.card.set_crtc(
564 self.crtc,
565 saved.framebuffer(),
566 saved.position(),
567 &[self.connector],
568 saved.mode(),
569 );
570 }
571
572 if let Some(cursor) = self.cursor.take() {
573 // Off the CRTC before the memory goes, or the scanout engine keeps
574 // compositing a freed buffer.
575 #[allow(deprecated)]
576 let _ = self
577 .card
578 .set_cursor(self.crtc, None::<&drm::control::dumbbuffer::DumbBuffer>);
579 cursor.release(&self.card);
580 }
581
582 for buffer in self.buffers.drain(..) {
583 // SAFETY: `ptr`/`bytes` describe exactly the mapping made in
584 // `Scanout::new`, whose guard was forgotten so that this code owns it.
585 // Nothing else can reference the region: the buffer has been moved out
586 // of `self.buffers` and any `Frame` borrowing it is long dropped.
587 unsafe {
588 let _ = rustix::mm::munmap(buffer.ptr.cast::<core::ffi::c_void>(), buffer.bytes);
589 }
590 let _ = self.card.destroy_framebuffer(buffer.fb);
591 let _ = self.card.destroy_dumb_buffer(buffer.dumb);
592 }
593
594 self.card.release_master();
595 }
596}
597
598#[cfg(test)]
599mod tests {
600 use super::*;
601
602 const SCREEN: Size = Size::new(1920, 1080);
603
604 /// A button lighting up is the case async flips exist for: the seam is a few
605 /// pixels tall, in one place, and gone next frame — and the press that
606 /// caused it is what the latency is measured against.
607 #[test]
608 fn a_small_frame_still_flips_immediately() {
609 let button = [Rect::new(40, 700, 220, 48)];
610 assert_eq!(
611 flip_flags_for(PresentMode::Immediate, &button, SCREEN),
612 PageFlipFlags::EVENT | PageFlipFlags::ASYNC
613 );
614 }
615
616 /// A scrolled viewport is the case it does not: the seam crosses the text
617 /// being read. This is the frame that was reported as flicker from a Pi.
618 #[test]
619 fn a_scrolled_viewport_waits_for_vblank() {
620 let viewport = [Rect::new(320, 60, 1560, 1000)];
621 assert_eq!(
622 flip_flags_for(PresentMode::Immediate, &viewport, SCREEN),
623 PageFlipFlags::EVENT,
624 "a frame that moved everything must not tear"
625 );
626 }
627
628 /// The gallery's sidebar, exactly: 300 by 1016 on a 1920x1080 panel. It is
629 /// under 15% of the pixels and over 90% of the scanlines, and judging it by
630 /// area sent it out async — which is the flash that was still being seen
631 /// after the first version of this shipped.
632 #[test]
633 fn a_narrow_column_down_the_screen_is_not_a_small_frame() {
634 let sidebar = [Rect::new(12, 52, 300, 1016)];
635 assert_eq!(
636 flip_flags_for(PresentMode::Immediate, &sidebar, SCREEN),
637 PageFlipFlags::EVENT,
638 "14.7% of the pixels, 94% of the rows: a tear crosses the lot"
639 );
640 }
641
642 /// And the other way round, which is why this is rows and not "any big
643 /// dimension": a band across the whole width can seam without anybody
644 /// noticing, because the seam is as short as the band.
645 #[test]
646 fn a_wide_shallow_band_may_still_tear() {
647 let toolbar = [Rect::new(0, 0, 1920, 40)];
648 assert_eq!(
649 flip_flags_for(PresentMode::Immediate, &toolbar, SCREEN),
650 PageFlipFlags::EVENT | PageFlipFlags::ASYNC
651 );
652 }
653
654 /// What the damage covers, not what it spans. Two specks far apart leave
655 /// the rows between them untouched, and untouched rows are identical in
656 /// both buffers, so the seam the beam can put there shows nothing.
657 #[test]
658 fn scattered_damage_is_judged_by_what_it_covers() {
659 let corners = [Rect::new(0, 0, 60, 40), Rect::new(1860, 1040, 60, 40)];
660 assert_eq!(
661 flip_flags_for(PresentMode::Immediate, &corners, SCREEN),
662 PageFlipFlags::EVENT | PageFlipFlags::ASYNC,
663 "eighty rows in two places, not the thousand between them"
664 );
665
666 let neighbours = [Rect::new(40, 700, 220, 48), Rect::new(280, 700, 220, 48)];
667 assert_eq!(
668 flip_flags_for(PresentMode::Immediate, &neighbours, SCREEN),
669 PageFlipFlags::EVENT | PageFlipFlags::ASYNC,
670 "two buttons side by side are still two buttons"
671 );
672 }
673
674 /// The frame this rule was costing, and the reason it was reported: the
675 /// gallery's spinner sits at the top and re-damages itself every motion
676 /// tick, so hovering anything below it produced a bounding box most of the
677 /// screen tall. Nothing about that frame is worth a vblank.
678 #[test]
679 fn a_spinner_and_a_pointer_far_apart_are_two_small_things() {
680 let spinner = Rect::new(736, 46, 48, 48);
681 let cursor = Rect::new(910, 812, 16, 24);
682 let hovered = Rect::new(820, 780, 220, 48);
683 assert_eq!(
684 flip_flags_for(PresentMode::Immediate, &[spinner, cursor, hovered], SCREEN),
685 PageFlipFlags::EVENT | PageFlipFlags::ASYNC,
686 "a spinner, a cursor and a highlight cover well under a quarter"
687 );
688 }
689
690 /// A scroll damages one tall rectangle, and the whole point is that it is
691 /// still caught once the count stops being a bounding box.
692 #[test]
693 fn coverage_still_catches_the_frames_bounds_caught() {
694 let sidebar = [Rect::new(12, 52, 300, 1016)];
695 assert_eq!(damaged_rows(&sidebar, SCREEN), 1016);
696
697 let viewport = [Rect::new(320, 60, 1560, 1000)];
698 assert_eq!(damaged_rows(&viewport, SCREEN), 1000);
699 }
700
701 /// Rectangles arrive in no order and may overlap. A row under two of them
702 /// is still one row.
703 #[test]
704 fn overlapping_and_unsorted_rows_are_counted_once() {
705 let stacked = [
706 Rect::new(0, 300, 100, 100),
707 Rect::new(0, 100, 100, 100),
708 Rect::new(0, 350, 100, 100),
709 ];
710 assert_eq!(
711 damaged_rows(&stacked, SCREEN),
712 250,
713 "100 at 100..200, then 150 at 300..450"
714 );
715
716 let abutting = [Rect::new(0, 100, 100, 50), Rect::new(0, 150, 100, 50)];
717 assert_eq!(damaged_rows(&abutting, SCREEN), 100, "one run, not two");
718 }
719
720 /// Rows off the bottom of the panel are never scanned out, so they cannot
721 /// tear and do not count.
722 #[test]
723 fn rows_outside_the_surface_do_not_count() {
724 let overhang = [Rect::new(0, 1000, 100, 400)];
725 assert_eq!(damaged_rows(&overhang, SCREEN), 80);
726
727 let above = [Rect::new(0, -50, 100, 60)];
728 assert_eq!(damaged_rows(&above, SCREEN), 10);
729
730 let offscreen = [Rect::new(0, 1080, 100, 40)];
731 assert_eq!(damaged_rows(&offscreen, SCREEN), 0);
732 }
733
734 /// More rectangles than the tracker can hold is not something this backend
735 /// can reason about, and it is not going to guess in the direction that
736 /// tears.
737 #[test]
738 fn an_oversized_list_is_treated_as_everything() {
739 let many = [Rect::new(0, 0, 8, 8); MAX_DAMAGE_RECTS + 1];
740 assert_eq!(damaged_rows(&many, SCREEN), SCREEN.height);
741 assert_eq!(
742 flip_flags_for(PresentMode::Immediate, &many, SCREEN),
743 PageFlipFlags::EVENT
744 );
745 }
746
747 /// A present that did not say what changed cannot be assumed to be small.
748 #[test]
749 fn damage_nobody_declared_is_treated_as_everything() {
750 assert_eq!(
751 flip_flags_for(PresentMode::Immediate, &[], SCREEN),
752 PageFlipFlags::EVENT
753 );
754 }
755
756 /// Asking for vsync gets vsync, whatever the damage. The mode is still a
757 /// promise; it is only `Immediate` that became a preference.
758 #[test]
759 fn vsync_is_never_overridden() {
760 for damage in [&[][..], &[Rect::new(0, 0, 4, 4)][..]] {
761 assert_eq!(
762 flip_flags_for(PresentMode::Vsync, damage, SCREEN),
763 PageFlipFlags::EVENT
764 );
765 }
766 }
767
768 /// The threshold itself, from both sides, on a screen where a quarter is a
769 /// round number of rows.
770 #[test]
771 fn the_threshold_is_a_quarter_of_the_rows() {
772 let screen = Size::new(1000, 1000);
773 let just_under = [Rect::new(0, 0, 8, 249)];
774 let just_over = [Rect::new(0, 0, 8, 250)];
775 assert_eq!(
776 flip_flags_for(PresentMode::Immediate, &just_under, screen),
777 PageFlipFlags::EVENT | PageFlipFlags::ASYNC
778 );
779 assert_eq!(
780 flip_flags_for(PresentMode::Immediate, &just_over, screen),
781 PageFlipFlags::EVENT
782 );
783 }
784}