Skip to main content

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}