Skip to main content

rlvgl_platform/hwcore/
surface.rs

1//! Typed framebuffer ownership.
2//!
3//! Framebuffer state flows as typed handles — not as `u32` addresses — so
4//! the borrow checker enforces the display-pipeline contract at compile
5//! time:
6//!
7//! - [`FrontBuffer<'a>`] is the LTDC scan-out view. CPU writes are impossible
8//!   because the type only exposes `&` methods — `cpu_slice_mut` does not
9//!   exist.
10//! - [`BackBuffer<'a>`] is the renderer/DMA2D target. CPU writes are allowed
11//!   via [`BackBuffer::cpu_slice`] (unsafe). Submitting the buffer to a DMA
12//!   engine consumes a [`BorrowedForDma`] reborrow; while that token is
13//!   alive the `&mut BackBuffer` is held, so `cpu_slice` is rejected by
14//!   rustc as a double-borrow.
15//! - [`InFlight<'dma, T>`] owns a `BorrowedForDma<'dma, T>` for the duration
16//!   of a DMA submission. Dropping / completing the `InFlight` returns the
17//!   buffer's `&mut` borrow to the caller.
18//! - [`Scanout`] owns both [`FrameBuffer`]s and hands out a `FrontBuffer<'_>`
19//!   view plus a `BackBuffer<'_>` mutably-borrowed handle. `back_mut` cannot
20//!   be called twice simultaneously — rustc rejects the second call as
21//!   E0499.
22//!
23//! This module is **additive** in Step 2: no callers migrate yet. The
24//! typed DMA2D API that consumes `BorrowedForDma<'_, BackBuffer<'_>>` lands
25//! in Step 3 (`platform::dma2d::Dma2dBlitter::start_fill_typed`).
26
27use core::marker::PhantomData;
28
29use crate::blit::PixelFmt;
30use crate::hwcore::addr::{DmaAddr, PhysAddr};
31
32// ─── FrameBuffer ────────────────────────────────────────────────────────
33
34/// An owned physical framebuffer region.
35///
36/// Created once per SDRAM allocation at boot. Owns geometry metadata plus
37/// the [`PhysAddr`] — no raw pointer, no `u32` escape hatch in the public
38/// API.
39#[derive(Debug)]
40pub struct FrameBuffer {
41    addr: PhysAddr,
42    width: u32,
43    height: u32,
44    stride_bytes: u32,
45    format: PixelFmt,
46    bank: u8,
47}
48
49/// Sentinel used by [`FrameBuffer::bank`] for addresses outside the SDRAM
50/// Bank 2 window (e.g. SRAM-backed mock buffers during tests).
51const BANK_OUT_OF_RANGE: u8 = u8::MAX;
52
53impl FrameBuffer {
54    /// Construct a [`FrameBuffer`] at a physical address.
55    ///
56    /// # Safety
57    ///
58    /// The caller must ensure that:
59    ///
60    /// - `[addr, addr + stride_bytes * height)` is a mapped, writable RAM
61    ///   region for the `'static` lifetime of the returned value,
62    /// - no other `FrameBuffer`, `&mut [u8]`, or DMA master references
63    ///   overlapping bytes,
64    /// - `addr` is appropriately aligned for `format` (4 bytes for
65    ///   `Argb8888`, 2 bytes for `Rgb565`, 1 byte for `L8`/`A8`/`A4`),
66    /// - `stride_bytes >= width * pixel_size(format)`.
67    pub unsafe fn from_phys(
68        addr: PhysAddr,
69        width: u32,
70        height: u32,
71        stride_bytes: u32,
72        format: PixelFmt,
73    ) -> Self {
74        let bank = addr.sdram_bank().unwrap_or(BANK_OUT_OF_RANGE);
75        Self {
76            addr,
77            width,
78            height,
79            stride_bytes,
80            format,
81            bank,
82        }
83    }
84
85    /// Physical base address.
86    #[inline]
87    pub fn addr(&self) -> PhysAddr {
88        self.addr
89    }
90
91    /// DMA bus address for handoff to a DMA master (DMA2D OMAR, LTDC
92    /// CFBAR, etc.).
93    ///
94    /// The conversion re-asserts pixel-format alignment; a misaligned
95    /// framebuffer address is a construction-time bug but `dma_addr`
96    /// falls back to byte alignment so callers receive a usable value
97    /// rather than a panic. Wire errors are surfaced via the pixel
98    /// validator in the DMA engine itself.
99    pub fn dma_addr(&self) -> DmaAddr {
100        let align = pixel_size(self.format);
101        DmaAddr::from_phys(self.addr, align)
102            .or_else(|_| DmaAddr::from_phys(self.addr, 1))
103            .expect("byte-aligned DmaAddr always valid for a non-zero alignment")
104    }
105
106    /// Width in pixels.
107    #[inline]
108    pub fn width(&self) -> u32 {
109        self.width
110    }
111
112    /// Height in pixels.
113    #[inline]
114    pub fn height(&self) -> u32 {
115        self.height
116    }
117
118    /// Row stride in bytes.
119    #[inline]
120    pub fn stride_bytes(&self) -> u32 {
121        self.stride_bytes
122    }
123
124    /// Pixel format.
125    #[inline]
126    pub fn format(&self) -> PixelFmt {
127        self.format
128    }
129
130    /// SDRAM bank index (0..`SDRAM_BANK_COUNT`), or `None` if this
131    /// framebuffer lives outside the Bank 2 window.
132    #[inline]
133    pub fn bank(&self) -> Option<u8> {
134        if self.bank == BANK_OUT_OF_RANGE {
135            None
136        } else {
137            Some(self.bank)
138        }
139    }
140
141    /// Total framebuffer size in bytes (`stride_bytes * height`).
142    #[inline]
143    pub fn byte_len(&self) -> usize {
144        self.stride_bytes as usize * self.height as usize
145    }
146}
147
148/// Bytes-per-pixel for a given [`PixelFmt`], used for alignment checks.
149#[inline]
150pub const fn pixel_size(fmt: PixelFmt) -> usize {
151    match fmt {
152        PixelFmt::Argb8888 => 4,
153        PixelFmt::Rgb565 => 2,
154        PixelFmt::L8 | PixelFmt::A8 | PixelFmt::A4 => 1,
155    }
156}
157
158// ─── FrontBuffer ────────────────────────────────────────────────────────
159
160/// Shared read-only view of a framebuffer currently handed to the display
161/// scan-out engine (LTDC CFBAR).
162///
163/// Deliberately exposes no `&mut` accessors: the scan-out path reads these
164/// pixels at pixel-clock rate, so CPU writes would tear. Obtaining write
165/// access requires swapping this buffer out via [`Scanout::swap`] first.
166pub struct FrontBuffer<'a> {
167    fb: &'a FrameBuffer,
168}
169
170impl<'a> FrontBuffer<'a> {
171    /// Wrap a shared reference to a [`FrameBuffer`].
172    #[inline]
173    pub fn wrap(fb: &'a FrameBuffer) -> Self {
174        Self { fb }
175    }
176
177    /// Underlying [`FrameBuffer`].
178    #[inline]
179    pub fn framebuffer(&self) -> &FrameBuffer {
180        self.fb
181    }
182
183    /// Physical base address.
184    #[inline]
185    pub fn addr(&self) -> PhysAddr {
186        self.fb.addr()
187    }
188
189    /// DMA bus address (for writing into LTDC CFBAR).
190    #[inline]
191    pub fn dma_addr(&self) -> DmaAddr {
192        self.fb.dma_addr()
193    }
194
195    /// Width in pixels.
196    #[inline]
197    pub fn width(&self) -> u32 {
198        self.fb.width()
199    }
200
201    /// Height in pixels.
202    #[inline]
203    pub fn height(&self) -> u32 {
204        self.fb.height()
205    }
206
207    /// Row stride in bytes.
208    #[inline]
209    pub fn stride_bytes(&self) -> u32 {
210        self.fb.stride_bytes()
211    }
212
213    /// Pixel format.
214    #[inline]
215    pub fn format(&self) -> PixelFmt {
216        self.fb.format()
217    }
218}
219
220// ─── BackBuffer ─────────────────────────────────────────────────────────
221
222/// Mutable view of a framebuffer the renderer currently owns.
223///
224/// CPU writes happen through [`BackBuffer::cpu_slice`]. DMA submissions go
225/// through [`BackBuffer::dma_dst`], which reborrows the buffer and locks
226/// out CPU access for the duration of the transfer.
227pub struct BackBuffer<'a> {
228    fb: &'a mut FrameBuffer,
229}
230
231impl<'a> BackBuffer<'a> {
232    /// Wrap a mutable reference to a [`FrameBuffer`].
233    #[inline]
234    pub fn wrap(fb: &'a mut FrameBuffer) -> Self {
235        Self { fb }
236    }
237
238    /// Underlying [`FrameBuffer`].
239    #[inline]
240    pub fn framebuffer(&self) -> &FrameBuffer {
241        self.fb
242    }
243
244    /// Underlying [`FrameBuffer`] (mutable metadata, not pixels).
245    #[inline]
246    pub fn framebuffer_mut(&mut self) -> &mut FrameBuffer {
247        self.fb
248    }
249
250    /// Physical base address.
251    #[inline]
252    pub fn addr(&self) -> PhysAddr {
253        self.fb.addr()
254    }
255
256    /// DMA bus address (for writing into DMA2D OMAR).
257    #[inline]
258    pub fn dma_addr(&self) -> DmaAddr {
259        self.fb.dma_addr()
260    }
261
262    /// Width in pixels.
263    #[inline]
264    pub fn width(&self) -> u32 {
265        self.fb.width()
266    }
267
268    /// Height in pixels.
269    #[inline]
270    pub fn height(&self) -> u32 {
271        self.fb.height()
272    }
273
274    /// Row stride in bytes.
275    #[inline]
276    pub fn stride_bytes(&self) -> u32 {
277        self.fb.stride_bytes()
278    }
279
280    /// Pixel format.
281    #[inline]
282    pub fn format(&self) -> PixelFmt {
283        self.fb.format()
284    }
285
286    /// Produce a reborrow suitable for DMA-engine submission.
287    ///
288    /// The returned [`BorrowedForDma`] carries a mutable reborrow of this
289    /// back buffer for lifetime `'b`. While it — or any [`InFlight`]
290    /// derived from it — is alive, `self` is borrowed and CPU access via
291    /// [`BackBuffer::cpu_slice`] is rejected by the borrow checker.
292    #[inline]
293    pub fn dma_dst<'b>(&'b mut self) -> BorrowedForDma<'b, BackBuffer<'a>> {
294        BorrowedForDma {
295            inner: self,
296            _phantom: PhantomData,
297        }
298    }
299
300    /// CPU byte access to the back buffer's RAM.
301    ///
302    /// # Safety
303    ///
304    /// The caller must ensure:
305    ///
306    /// - no DMA master is currently reading or writing the buffer (the
307    ///   [`InFlight`] lifecycle already encodes this for DMA2D paths),
308    /// - the RAM region is actually mapped and writable (established by
309    ///   [`FrameBuffer::from_phys`]),
310    /// - no other `&mut [u8]` or `&[u8]` referencing overlapping bytes is
311    ///   alive for the returned slice's lifetime.
312    pub unsafe fn cpu_slice(&mut self) -> &mut [u8] {
313        let len = self.fb.byte_len();
314        // SAFETY: delegated to caller per the contract above; `len` is
315        // computed from the framebuffer geometry owned by this handle.
316        unsafe { self.fb.addr.as_mut_slice(len) }
317    }
318}
319
320// ─── BorrowedForDma + InFlight ─────────────────────────────────────────
321
322/// A framebuffer reborrow tagged as "handed to a DMA master for the
323/// duration of this value's lifetime".
324///
325/// Holds an `&'a mut T`, so while alive it prevents both a second DMA
326/// submission and any CPU access via the source handle. Produced by
327/// [`BackBuffer::dma_dst`]; consumed by DMA-engine submission APIs that
328/// return an [`InFlight`].
329pub struct BorrowedForDma<'a, T: ?Sized + 'a> {
330    inner: &'a mut T,
331    _phantom: PhantomData<&'a mut T>,
332}
333
334impl<'a, 'fb> BorrowedForDma<'a, BackBuffer<'fb>> {
335    /// DMA bus address of the borrowed back buffer.
336    #[inline]
337    pub fn dma_addr(&self) -> DmaAddr {
338        self.inner.dma_addr()
339    }
340
341    /// Width in pixels.
342    #[inline]
343    pub fn width(&self) -> u32 {
344        self.inner.width()
345    }
346
347    /// Height in pixels.
348    #[inline]
349    pub fn height(&self) -> u32 {
350        self.inner.height()
351    }
352
353    /// Row stride in bytes.
354    #[inline]
355    pub fn stride_bytes(&self) -> u32 {
356        self.inner.stride_bytes()
357    }
358
359    /// Pixel format.
360    #[inline]
361    pub fn format(&self) -> PixelFmt {
362        self.inner.format()
363    }
364}
365
366/// Non-blocking DMA transfer token.
367///
368/// Owns a [`BorrowedForDma`] for the transfer duration; the `&mut`
369/// reborrow inside prevents CPU access and re-submission until the
370/// transfer completes. Completion (polling or waiting) consumes the
371/// `InFlight` and returns the [`BorrowedForDma`] (which can then be
372/// dropped to release the `&mut` back to the caller).
373///
374/// The concrete `start_*_typed` methods that produce `InFlight` values
375/// land with the typed DMA2D API in Step 3 of the refactor plan.
376pub struct InFlight<'dma, T: ?Sized + 'dma> {
377    borrow: BorrowedForDma<'dma, T>,
378}
379
380impl<'dma, T: ?Sized + 'dma> InFlight<'dma, T> {
381    /// Wrap a [`BorrowedForDma`] in an `InFlight` — used by DMA engines
382    /// to hand the borrow back to the caller on submission.
383    #[inline]
384    pub fn new(borrow: BorrowedForDma<'dma, T>) -> Self {
385        Self { borrow }
386    }
387
388    /// Release the underlying [`BorrowedForDma`] — called by DMA-engine
389    /// completion handlers when the transfer is done and the CPU may
390    /// touch the buffer again.
391    #[inline]
392    pub fn into_borrow(self) -> BorrowedForDma<'dma, T> {
393        self.borrow
394    }
395}
396
397// ─── Scanout ────────────────────────────────────────────────────────────
398
399/// A pair of framebuffers used as a swap chain by LTDC.
400///
401/// Holds two [`FrameBuffer`]s and hands out `FrontBuffer<'_>` /
402/// `BackBuffer<'_>` views. `back_mut` cannot be called twice
403/// simultaneously — a second call fails rustc borrow check (E0499),
404/// which is the compile-time proof that the renderer never competes
405/// with itself for a single back buffer.
406#[derive(Debug)]
407pub struct Scanout {
408    front: FrameBuffer,
409    back: FrameBuffer,
410}
411
412/// Error returned by [`Scanout::try_new`] when `front` and `back` would
413/// land in the same SDRAM bank — a configuration that causes refresh /
414/// scan-line contention on the STM32H747I-DISCO module.
415#[derive(Debug)]
416pub struct BankCollision {
417    /// The front framebuffer the caller supplied.
418    pub front: FrameBuffer,
419    /// The back framebuffer the caller supplied.
420    pub back: FrameBuffer,
421    /// The common bank index both buffers resolved to.
422    pub bank: u8,
423}
424
425impl Scanout {
426    /// Build a [`Scanout`] from two framebuffers.
427    ///
428    /// Returns [`BankCollision`] if both framebuffers resolve to the same
429    /// SDRAM bank. Framebuffers whose `bank()` returns `None` (non-SDRAM
430    /// RAM, test mocks) are accepted without a collision check.
431    pub fn try_new(front: FrameBuffer, back: FrameBuffer) -> Result<Self, BankCollision> {
432        if let (Some(fb), Some(bb)) = (front.bank(), back.bank())
433            && fb == bb
434        {
435            return Err(BankCollision {
436                front,
437                back,
438                bank: fb,
439            });
440        }
441        Ok(Self { front, back })
442    }
443
444    /// Read-only view of the currently-scanning front buffer.
445    #[inline]
446    pub fn front(&self) -> FrontBuffer<'_> {
447        FrontBuffer::wrap(&self.front)
448    }
449
450    /// Mutable handle on the currently-rendering back buffer.
451    ///
452    /// Calling this twice without dropping the first returned value is a
453    /// compile error (E0499). This is the mechanism that prevents the
454    /// renderer and an overlay path from both holding back-buffer
455    /// borrows simultaneously.
456    #[inline]
457    pub fn back_mut(&mut self) -> BackBuffer<'_> {
458        BackBuffer::wrap(&mut self.back)
459    }
460
461    /// Swap front and back. Typically called at LTDC vertical-sync after
462    /// the back buffer has been fully rendered and any outstanding
463    /// [`InFlight`] has been consumed.
464    #[inline]
465    pub fn swap(&mut self) {
466        core::mem::swap(&mut self.front, &mut self.back);
467    }
468}
469
470#[cfg(test)]
471mod tests {
472    use super::*;
473    use crate::hwcore::addr::{SDRAM_BANK_STRIDE, SDRAM_BANK2_BASE};
474
475    const W: u32 = 480;
476    const H: u32 = 272;
477    const STRIDE: u32 = W * 4;
478
479    fn mk(addr: u32) -> FrameBuffer {
480        // SAFETY: test only; the address is never dereferenced.
481        unsafe { FrameBuffer::from_phys(PhysAddr::new(addr), W, H, STRIDE, PixelFmt::Argb8888) }
482    }
483
484    #[test]
485    fn bank_disjoint_framebuffers_pair_successfully() {
486        let front = mk(SDRAM_BANK2_BASE);
487        let back = mk(SDRAM_BANK2_BASE + SDRAM_BANK_STRIDE);
488        let sc = Scanout::try_new(front, back).expect("disjoint banks");
489        assert_eq!(sc.front().width(), W);
490    }
491
492    #[test]
493    fn same_bank_framebuffers_are_rejected() {
494        let front = mk(SDRAM_BANK2_BASE);
495        let back = mk(SDRAM_BANK2_BASE + 0x100); // same bank
496        let err = Scanout::try_new(front, back).unwrap_err();
497        assert_eq!(err.bank, 0);
498    }
499
500    #[test]
501    fn frontbuffer_reports_geometry() {
502        let fb = mk(SDRAM_BANK2_BASE);
503        let front = FrontBuffer::wrap(&fb);
504        assert_eq!(front.width(), W);
505        assert_eq!(front.height(), H);
506        assert_eq!(front.stride_bytes(), STRIDE);
507        assert_eq!(front.format(), PixelFmt::Argb8888);
508    }
509
510    #[test]
511    fn backbuffer_dma_dst_reborrow_is_well_formed() {
512        let mut fb = mk(SDRAM_BANK2_BASE);
513        let mut back = BackBuffer::wrap(&mut fb);
514        {
515            let dst = back.dma_dst();
516            assert_eq!(dst.width(), W);
517            assert_eq!(dst.stride_bytes(), STRIDE);
518            // `dst` drops here, releasing the reborrow.
519        }
520        // Now `back` is usable again — this only needs to type-check.
521        let _dst2 = back.dma_dst();
522    }
523
524    #[test]
525    fn scanout_swap_exchanges_buffers() {
526        let mut sc = Scanout::try_new(
527            mk(SDRAM_BANK2_BASE),
528            mk(SDRAM_BANK2_BASE + SDRAM_BANK_STRIDE),
529        )
530        .unwrap();
531        let before = sc.front().addr();
532        sc.swap();
533        let after = sc.front().addr();
534        assert_ne!(before, after);
535    }
536
537    #[test]
538    fn inflight_round_trips_borrow() {
539        let mut fb = mk(SDRAM_BANK2_BASE);
540        let mut back = BackBuffer::wrap(&mut fb);
541        let dst = back.dma_dst();
542        let inflight = InFlight::new(dst);
543        let released = inflight.into_borrow();
544        assert_eq!(released.width(), W);
545    }
546
547    #[test]
548    fn pixel_size_matches_format() {
549        assert_eq!(pixel_size(PixelFmt::Argb8888), 4);
550        assert_eq!(pixel_size(PixelFmt::Rgb565), 2);
551        assert_eq!(pixel_size(PixelFmt::L8), 1);
552    }
553}