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}