Skip to main content

rlvgl_platform/
present.rs

1//! LPAR-03 display presenter: resolves a [`PresentPlan`] into
2//! [`DisplayDriver::flush`] calls (LPAR-03 §9).
3//!
4//! # API summary
5//!
6//! - [`present_plan`] — execute a [`PresentPlan`] against a [`DisplayDriver`],
7//!   extracting sub-rect pixel slices from a full logical-screen frame buffer.
8//! - [`ingest_blit_planner`] — bridge a [`BlitPlanner`]'s backend-observed
9//!   write regions into an [`InvalidationList`], promoting to full frame on
10//!   overflow (LPAR-03 §7 "Backend draw planner").
11//!
12//! Both items are pure safe Rust; no `unsafe` is needed.
13
14use alloc::vec::Vec;
15use rlvgl_core::invalidation::{InvalidationList, InvalidationSource, PresentPlan};
16use rlvgl_core::widget::{Color, Rect};
17
18use crate::blit::BlitPlanner;
19use crate::display::DisplayDriver;
20
21/// Execute a [`PresentPlan`] against `driver` using `frame` as the full
22/// logical-screen pixel buffer (LPAR-03 §9.1–§9.5).
23///
24/// `frame` must be in **row-major** order and contain exactly
25/// `screen.width * screen.height` pixels in logical coordinates.
26///
27/// # Behaviour per plan variant
28///
29/// - [`PresentPlan::None`] — no flush, no vsync (LPAR-03 §5.3).
30/// - [`PresentPlan::FullFrame`] — one flush of the full logical screen rect
31///   followed by one [`DisplayDriver::vsync`] call.
32/// - [`PresentPlan::Rects(rs)`] — one flush per rect in order; each rect's
33///   pixel data is extracted from `frame` into a temporary `Vec` and passed
34///   to `flush`. Rects are expected to be screen-clipped by the planner. If
35///   a rect extends outside `frame` bounds (e.g. due to mismatched screen
36///   sizes), it is skipped with a `debug_assert` rather than panicking.
37///   Followed by one `vsync` call.
38///
39/// After all rect flushes (but not for `None`), [`DisplayDriver::vsync`] is
40/// called exactly once (LPAR-03 §9.5).
41///
42/// # Return value
43///
44/// The number of `flush` calls issued (useful for telemetry and tests).
45pub fn present_plan(
46    driver: &mut dyn DisplayDriver,
47    plan: PresentPlan<'_>,
48    frame: &[Color],
49) -> usize {
50    let screen = driver.screen();
51    let (lw, lh) = screen.logical_size();
52
53    match plan {
54        PresentPlan::None => 0,
55
56        PresentPlan::FullFrame => {
57            let full = Rect {
58                x: 0,
59                y: 0,
60                width: lw as i32,
61                height: lh as i32,
62            };
63            driver.flush(full, frame);
64            driver.vsync();
65            1
66        }
67
68        PresentPlan::Rects(rects) => {
69            let stride = lw as usize;
70            let mut count = 0usize;
71            let mut buf: Vec<Color> = Vec::new();
72
73            for &rect in rects {
74                // Defensive bounds check: the planner should always produce
75                // screen-clipped rects, but guard against mismatched frame
76                // dimensions rather than panicking.
77                let rx_end = rect.x.saturating_add(rect.width);
78                let ry_end = rect.y.saturating_add(rect.height);
79                let valid = rect.x >= 0
80                    && rect.y >= 0
81                    && rect.width > 0
82                    && rect.height > 0
83                    && rx_end <= lw as i32
84                    && ry_end <= lh as i32;
85
86                debug_assert!(
87                    valid,
88                    "present_plan: rect {:?} is outside frame {}x{}",
89                    rect, lw, lh
90                );
91
92                if !valid {
93                    continue;
94                }
95
96                let w = rect.width as usize;
97                let h = rect.height as usize;
98                buf.resize(w * h, Color(0, 0, 0, 0));
99
100                for row in 0..h {
101                    let src_start = (rect.y as usize + row) * stride + rect.x as usize;
102                    let dst_start = row * w;
103                    buf[dst_start..dst_start + w].copy_from_slice(&frame[src_start..src_start + w]);
104                }
105
106                driver.flush(rect, &buf);
107                count += 1;
108            }
109
110            if count > 0 {
111                driver.vsync();
112            }
113            count
114        }
115    }
116}
117
118/// Ingest a [`BlitPlanner`]'s backend-observed write regions into an
119/// [`InvalidationList`] (LPAR-03 §7, "Backend draw planner").
120///
121/// Each rect stored in `planner` is converted from [`blit::Rect`]
122/// (unsigned width/height) to [`Rect`] (signed) and pushed into `list`.
123/// If `planner.overflowed()` is `true`, the list is immediately promoted to
124/// a full-frame plan via [`InvalidationList::mark_full_frame`] — dropped
125/// backend rects must never silently suppress a required repaint.
126///
127/// This is a free function rather than an `InvalidationSource` impl because
128/// [`BlitPlanner`] stores `blit::Rect`s (unsigned `w`/`h`) while
129/// [`InvalidationSource::collect_dirty`] requires [`Rect`]s (signed
130/// `width`/`height`), and [`BlitPlanner`] is defined in this crate rather
131/// than `rlvgl_core`, making a cross-crate trait impl both awkward and
132/// architecturally inappropriate.
133pub fn ingest_blit_planner<const N: usize, const M: usize>(
134    planner: &BlitPlanner<N>,
135    list: &mut InvalidationList<M>,
136) {
137    // Overflow check first: if the backend already lost rects, promote now
138    // before any partial list is ingested and misleads the planner.
139    if planner.overflowed() {
140        list.mark_full_frame();
141        return;
142    }
143
144    for br in planner.rects() {
145        // Convert blit::Rect {x, y, w, h} → widget::Rect {x, y, width, height}.
146        // w and h are u32; cast to i32 is safe for any practical display size.
147        let rect = Rect {
148            x: br.x,
149            y: br.y,
150            width: br.w as i32,
151            height: br.h as i32,
152        };
153        list.push(rect);
154    }
155}
156
157/// Adapter that wraps a `&[Rect]` slice as an [`InvalidationSource`].
158///
159/// Useful for feeding a snapshot of dirty rects — e.g. from
160/// [`rlvgl_core::anim::Animations::dirty_rects`] — through a
161/// [`InvalidationList::gather`] call without requiring an external
162/// `Vec` or intermediate struct.
163pub struct SliceSource<'a> {
164    rects: &'a [Rect],
165}
166
167impl<'a> SliceSource<'a> {
168    /// Wrap a rect slice as an invalidation source.
169    pub fn new(rects: &'a [Rect]) -> Self {
170        Self { rects }
171    }
172}
173
174impl InvalidationSource for SliceSource<'_> {
175    fn collect_dirty(&mut self, sink: &mut dyn FnMut(Rect)) {
176        for &r in self.rects {
177            sink(r);
178        }
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use rlvgl_core::invalidation::{BufferedInvalidation, InvalidationList, PresentPlan};
185    use rlvgl_core::widget::{Color, Rect};
186
187    use super::*;
188    use crate::blit::BlitPlanner;
189    use crate::display::{BufferDisplay, DisplayDriver};
190
191    // ──────────────────────────────────────────────────────
192    // Helpers
193    // ──────────────────────────────────────────────────────
194
195    fn r(x: i32, y: i32, w: i32, h: i32) -> Rect {
196        Rect {
197            x,
198            y,
199            width: w,
200            height: h,
201        }
202    }
203
204    fn red() -> Color {
205        Color(255, 0, 0, 255)
206    }
207    fn green() -> Color {
208        Color(0, 255, 0, 255)
209    }
210    fn blue() -> Color {
211        Color(0, 0, 255, 255)
212    }
213    fn black() -> Color {
214        Color(0, 0, 0, 255)
215    }
216
217    /// Build a flat row-major Color frame of `w×h` pixels where every
218    /// pixel at logical (x, y) is `fill`.
219    fn solid_frame(w: usize, h: usize, fill: Color) -> alloc::vec::Vec<Color> {
220        alloc::vec![fill; w * h]
221    }
222
223    // ──────────────────────────────────────────────────────
224    // Vsync-counting wrapper
225    // ──────────────────────────────────────────────────────
226
227    struct VsyncCounter<D: DisplayDriver> {
228        inner: D,
229        /// Incremented on each `vsync()` call.
230        pub vsync_count: usize,
231    }
232
233    impl<D: DisplayDriver> VsyncCounter<D> {
234        fn new(inner: D) -> Self {
235            Self {
236                inner,
237                vsync_count: 0,
238            }
239        }
240    }
241
242    impl<D: DisplayDriver> DisplayDriver for VsyncCounter<D> {
243        fn screen(&self) -> crate::screen::Screen {
244            self.inner.screen()
245        }
246        fn flush(&mut self, area: Rect, colors: &[Color]) {
247            self.inner.flush(area, colors);
248        }
249        fn vsync(&mut self) {
250            self.vsync_count += 1;
251        }
252    }
253
254    // ──────────────────────────────────────────────────────
255    // present_plan tests
256    // ──────────────────────────────────────────────────────
257
258    #[test]
259    fn none_plan_issues_zero_flushes() {
260        let mut disp = BufferDisplay::new(10, 10);
261        let frame = solid_frame(10, 10, red());
262        let count = present_plan(&mut disp, PresentPlan::None, &frame);
263        assert_eq!(count, 0);
264        // Buffer should be untouched (all black, as BufferDisplay::new initialises).
265        assert!(disp.buffer.iter().all(|&c| c == black()));
266    }
267
268    #[test]
269    fn none_plan_does_not_call_vsync() {
270        let mut disp = VsyncCounter::new(BufferDisplay::new(10, 10));
271        let frame = solid_frame(10, 10, red());
272        present_plan(&mut disp, PresentPlan::None, &frame);
273        assert_eq!(disp.vsync_count, 0);
274    }
275
276    #[test]
277    fn full_frame_plan_copies_entire_frame() {
278        let (w, h) = (8, 6);
279        let mut disp = BufferDisplay::new(w, h);
280        let frame = solid_frame(w, h, green());
281        let count = present_plan(&mut disp, PresentPlan::FullFrame, &frame);
282        assert_eq!(count, 1);
283        assert!(disp.buffer.iter().all(|&c| c == green()));
284    }
285
286    #[test]
287    fn full_frame_plan_calls_vsync_once() {
288        let mut disp = VsyncCounter::new(BufferDisplay::new(4, 4));
289        let frame = solid_frame(4, 4, blue());
290        present_plan(&mut disp, PresentPlan::FullFrame, &frame);
291        assert_eq!(disp.vsync_count, 1);
292    }
293
294    #[test]
295    fn multi_rect_pixels_land_at_correct_coordinates() {
296        // 8×8 frame; two 2×2 red patches in corners (0,0) and (6,6),
297        // everything else green.
298        let (w, h) = (8usize, 8usize);
299        let mut frame = solid_frame(w, h, green());
300        // Patch top-left 2×2 with red.
301        for py in 0..2usize {
302            for px in 0..2usize {
303                frame[py * w + px] = red();
304            }
305        }
306        // Patch bottom-right 2×2 with blue.
307        for py in 6..8usize {
308            for px in 6..8usize {
309                frame[py * w + px] = blue();
310            }
311        }
312
313        let mut disp = BufferDisplay::new(w, h);
314        let rects = [r(0, 0, 2, 2), r(6, 6, 2, 2)];
315        let count = present_plan(&mut disp, PresentPlan::Rects(&rects), &frame);
316        assert_eq!(count, 2);
317
318        // Flushed corners should have the right colours.
319        assert_eq!(disp.buffer[0], red());
320        assert_eq!(disp.buffer[1], red());
321        assert_eq!(disp.buffer[w], red());
322        assert_eq!(disp.buffer[w + 1], red());
323
324        assert_eq!(disp.buffer[6 * w + 6], blue());
325        assert_eq!(disp.buffer[6 * w + 7], blue());
326        assert_eq!(disp.buffer[7 * w + 6], blue());
327        assert_eq!(disp.buffer[7 * w + 7], blue());
328
329        // Untouched pixels remain at BufferDisplay's initial black.
330        assert_eq!(disp.buffer[3 * w + 3], black());
331    }
332
333    #[test]
334    fn untouched_pixels_keep_initial_color() {
335        let (w, h) = (6usize, 6usize);
336        let frame = solid_frame(w, h, red());
337        let mut disp = BufferDisplay::new(w, h);
338        // Flush only a 1×1 rect in the centre.
339        let rects = [r(3, 3, 1, 1)];
340        present_plan(&mut disp, PresentPlan::Rects(&rects), &frame);
341        // Pixel at (3,3) should now be red.
342        assert_eq!(disp.buffer[3 * w + 3], red());
343        // Pixel at (0,0) should remain the initial black.
344        assert_eq!(disp.buffer[0], black());
345    }
346
347    #[test]
348    fn flush_count_matches_rect_count() {
349        let (w, h) = (20usize, 10usize);
350        let frame = solid_frame(w, h, green());
351        let mut disp = BufferDisplay::new(w, h);
352        let rects = [r(0, 0, 4, 4), r(5, 0, 4, 4), r(10, 0, 4, 4)];
353        let count = present_plan(&mut disp, PresentPlan::Rects(&rects), &frame);
354        assert_eq!(count, 3);
355    }
356
357    #[test]
358    fn multi_rect_present_calls_vsync_exactly_once() {
359        let (w, h) = (10usize, 10usize);
360        let frame = solid_frame(w, h, blue());
361        let mut disp = VsyncCounter::new(BufferDisplay::new(w, h));
362        let rects = [r(0, 0, 3, 3), r(5, 5, 3, 3)];
363        present_plan(&mut disp, PresentPlan::Rects(&rects), &frame);
364        assert_eq!(disp.vsync_count, 1);
365    }
366
367    #[test]
368    fn empty_rects_slice_yields_zero_flushes_and_no_vsync() {
369        let (w, h) = (8usize, 8usize);
370        let frame = solid_frame(w, h, green());
371        let mut disp = VsyncCounter::new(BufferDisplay::new(w, h));
372        let count = present_plan(&mut disp, PresentPlan::Rects(&[]), &frame);
373        assert_eq!(count, 0);
374        assert_eq!(disp.vsync_count, 0);
375    }
376
377    // ──────────────────────────────────────────────────────
378    // ingest_blit_planner tests
379    // ──────────────────────────────────────────────────────
380
381    fn blit_rect(x: i32, y: i32, w: u32, h: u32) -> crate::blit::Rect {
382        crate::blit::Rect { x, y, w, h }
383    }
384
385    #[test]
386    fn blit_planner_rects_transfer_into_invalidation_list() {
387        let mut planner: BlitPlanner<8> = BlitPlanner::new();
388        planner.add(blit_rect(0, 0, 10, 10));
389        planner.add(blit_rect(20, 0, 5, 5));
390
391        let mut list = InvalidationList::<8>::with_size(100, 100);
392        ingest_blit_planner(&planner, &mut list);
393
394        assert!(!list.is_full_frame());
395        assert_eq!(list.len(), 2);
396        assert_eq!(
397            list.plan(),
398            PresentPlan::Rects(&[r(0, 0, 10, 10), r(20, 0, 5, 5)])
399        );
400    }
401
402    #[test]
403    fn overflowed_blit_planner_promotes_to_full_frame() {
404        // Capacity 1 → adding a second rect causes overflow.
405        let mut planner: BlitPlanner<1> = BlitPlanner::new();
406        planner.add(blit_rect(0, 0, 10, 10));
407        planner.add(blit_rect(50, 50, 10, 10)); // overflows
408
409        assert!(planner.overflowed());
410
411        let mut list = InvalidationList::<8>::with_size(100, 100);
412        ingest_blit_planner(&planner, &mut list);
413
414        assert_eq!(list.plan(), PresentPlan::FullFrame);
415    }
416
417    #[test]
418    fn non_overflowed_planner_does_not_promote_to_full_frame() {
419        let mut planner: BlitPlanner<4> = BlitPlanner::new();
420        planner.add(blit_rect(1, 1, 3, 3));
421
422        let mut list = InvalidationList::<8>::with_size(100, 100);
423        ingest_blit_planner(&planner, &mut list);
424
425        assert!(!list.is_full_frame());
426    }
427
428    // ──────────────────────────────────────────────────────
429    // End-to-end shape test:
430    // InvalidationList → plan() → present_plan() → BufferDisplay
431    // ──────────────────────────────────────────────────────
432
433    #[test]
434    fn end_to_end_invalidation_list_to_buffer_display() {
435        let (w, h) = (20usize, 20usize);
436        // Frame: top-left quadrant red, rest green.
437        let mut frame = solid_frame(w, h, green());
438        for py in 0..10usize {
439            for px in 0..10usize {
440                frame[py * w + px] = red();
441            }
442        }
443
444        // Record the top-left 10×10 as dirty.
445        let mut list = InvalidationList::<8>::with_size(w as i32, h as i32);
446        list.push(r(0, 0, 10, 10));
447        let plan = list.plan();
448
449        let mut disp = BufferDisplay::new(w, h);
450        let count = present_plan(&mut disp, plan, &frame);
451
452        assert_eq!(count, 1);
453        // The 10×10 patch should now be red in the display buffer.
454        for py in 0..10usize {
455            for px in 0..10usize {
456                assert_eq!(disp.buffer[py * w + px], red(), "({px},{py}) should be red");
457            }
458        }
459        // The rest should remain the initial black (never flushed).
460        assert_eq!(disp.buffer[10 * w + 10], black());
461    }
462
463    #[test]
464    fn end_to_end_with_buffered_invalidation() {
465        let (w, h) = (16usize, 16usize);
466        let frame = solid_frame(w, h, blue());
467
468        // K=2: rect persists for 2 presents.
469        let mut buf = BufferedInvalidation::<8, 2>::with_size(w as i32, h as i32);
470        buf.push(r(0, 0, 4, 4));
471
472        // First present.
473        let plan = buf.plan();
474        let mut disp = BufferDisplay::new(w, h);
475        let count = present_plan(&mut disp, plan, &frame);
476        assert_eq!(count, 1);
477        buf.finish_present();
478
479        // Second present (rect should still be live — K=2 retention).
480        let plan2 = buf.plan();
481        assert!(matches!(plan2, PresentPlan::Rects(_)));
482        present_plan(&mut disp, plan2, &frame);
483        buf.finish_present();
484
485        // Third present — rect has expired.
486        assert_eq!(buf.plan(), PresentPlan::None);
487    }
488
489    #[test]
490    fn full_frame_plan_on_overflowed_list_flushes_whole_screen() {
491        let (w, h) = (8usize, 8usize);
492        let frame = solid_frame(w, h, green());
493        let mut disp = BufferDisplay::new(w, h);
494
495        // Force overflow on a capacity-1 list.
496        let mut list = InvalidationList::<1>::with_size(w as i32, h as i32);
497        list.push(r(0, 0, 2, 2));
498        list.push(r(4, 4, 2, 2)); // overflows → FullFrame
499
500        assert_eq!(list.plan(), PresentPlan::FullFrame);
501        let count = present_plan(&mut disp, list.plan(), &frame);
502        assert_eq!(count, 1);
503        assert!(disp.buffer.iter().all(|&c| c == green()));
504    }
505}