Skip to main content

embedded_3dgfx/
display_backend.rs

1//! Display backend abstraction for DMA-based rendering
2//!
3//! This module provides a platform-agnostic interface for asynchronous
4//! framebuffer transfers using DMA (Direct Memory Access). This enables
5//! double-buffered rendering where the CPU can render to one buffer while
6//! the display hardware transfers another buffer.
7//!
8//! # Safety
9//!
10//! The key safety property of this API is that `start_dma_transfer` takes
11//! ownership of the framebuffer and returns a [`DmaTransfer`] token. The
12//! buffer is locked inside the token for the duration of the transfer —
13//! the compiler prevents any access to it until [`DmaTransfer::wait`]
14//! returns it. This eliminates the data race that arises from the
15//! previous borrow-based API, where DMA could be reading from memory that
16//! the CPU was free to overwrite.
17
18use embedded_graphics_core::pixelcolor::Rgb565;
19use embedded_graphics_framebuf::{FrameBuf, backends::DMACapableFrameBufferBackend};
20
21/// Rectangle region for partial framebuffer presents.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct DisplayRegion {
24    pub x: usize,
25    pub y: usize,
26    pub width: usize,
27    pub height: usize,
28}
29
30impl DisplayRegion {
31    pub const fn new(x: usize, y: usize, width: usize, height: usize) -> Self {
32        Self {
33            x,
34            y,
35            width,
36            height,
37        }
38    }
39}
40
41/// Trait for offloading 2D/3D fill, blit, and clear operations to silicon acceleration units (e.g. DMA2D / Chrom-ART).
42pub trait HardwareAccelerator {
43    /// Accelerated fill of a rectangular area with a solid color.
44    fn fill_rect(&mut self, x: u16, y: u16, w: u16, h: u16, color: Rgb565) -> bool;
45
46    /// Accelerated memory copy / blit from source to destination buffer.
47    fn blit(
48        &mut self,
49        src: &[Rgb565],
50        src_stride: usize,
51        dst_x: u16,
52        dst_y: u16,
53        w: u16,
54        h: u16,
55    ) -> bool;
56}
57
58/// Default CPU-fallback implementation of `HardwareAccelerator`.
59#[derive(Debug, Clone, Copy, Default)]
60pub struct CpuAccelerator;
61
62impl HardwareAccelerator for CpuAccelerator {
63    #[inline(always)]
64    fn fill_rect(&mut self, _x: u16, _y: u16, _w: u16, _h: u16, _color: Rgb565) -> bool {
65        false // Fallback to software rasterizer
66    }
67
68    #[inline(always)]
69    fn blit(
70        &mut self,
71        _src: &[Rgb565],
72        _src_stride: usize,
73        _dst_x: u16,
74        _dst_y: u16,
75        _w: u16,
76        _h: u16,
77    ) -> bool {
78        false // Fallback to software rasterizer
79    }
80}
81
82/// Error types for display backend operations.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum DisplayError {
85    /// DMA transfer is still in progress.
86    Busy,
87    /// Hardware error during transfer.
88    HardwareError,
89    /// Invalid buffer configuration.
90    InvalidBuffer,
91}
92
93/// Returned when a DMA transfer fails to start.
94///
95/// Carries both the error code and the framebuffer back to the caller so
96/// the buffer is not lost on failure.
97pub struct TransferError<FB>
98where
99    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
100{
101    /// The framebuffer that could not be transferred.
102    pub framebuffer: FrameBuf<Rgb565, FB>,
103    /// The reason the transfer failed.
104    pub error: DisplayError,
105}
106
107impl<FB> core::fmt::Debug for TransferError<FB>
108where
109    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
110{
111    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
112        f.debug_struct("TransferError")
113            .field("error", &self.error)
114            .finish_non_exhaustive()
115    }
116}
117
118/// An in-progress DMA transfer that holds the framebuffer until completion.
119///
120/// The buffer is inaccessible while this token is live — the only way to
121/// get it back is to call [`wait`](DmaTransfer::wait), which blocks until
122/// the hardware has finished reading.
123///
124/// Implementors for real hardware should cancel the DMA in their [`Drop`]
125/// impl so that dropping a token without waiting is always safe.
126pub trait DmaTransfer {
127    /// The buffer type that is returned when the transfer completes.
128    type Buffer;
129
130    /// Returns `true` if the DMA hardware has finished the transfer.
131    fn is_done(&self) -> bool;
132
133    /// Block until the transfer is complete and return the framebuffer.
134    ///
135    /// Consuming `self` ensures the buffer cannot be accessed while DMA
136    /// is still reading it.
137    fn wait(self) -> Self::Buffer;
138}
139
140/// An asynchronous DMA transfer that can be awaited as a future.
141///
142/// This allows integration with async executors (e.g. Embassy, RTOS task executors)
143/// without blocking the CPU while the DMA transfer is in progress.
144pub trait AsyncDmaTransfer: DmaTransfer {
145    /// The future type returned by `wait_async`.
146    type WaitFuture: core::future::Future<Output = Self::Buffer>;
147
148    /// Return a future that resolves to the framebuffer once the DMA transfer completes.
149    fn wait_async(self) -> Self::WaitFuture;
150}
151
152/// Platform-agnostic display backend trait.
153///
154/// Implementations handle the hardware-specific details of transferring a
155/// framebuffer to the display. The API is intentionally ownership-based:
156/// `start_dma_transfer` takes the framebuffer **by value** and returns a
157/// [`DmaTransfer`] token. The buffer is held inside the token and cannot
158/// be accessed again until [`DmaTransfer::wait`] returns it. This
159/// prevents write-after-submit data races at compile time.
160///
161/// # Implementing for real hardware
162///
163/// 1. Define a concrete transfer type that holds the buffer and any
164///    hardware state needed to poll or cancel the DMA.
165/// 2. In `start_dma_transfer`: program the DMA controller, then move the
166///    buffer into your transfer type.
167/// 3. In `DmaTransfer::wait`: spin or sleep until the done flag is set by
168///    the DMA interrupt, then return the buffer.
169/// 4. In `DmaTransfer::drop`: if the transfer is still running, cancel
170///    it. This ensures that dropping a forgotten token never leaves the
171///    DMA controller pointing at freed memory.
172pub trait DisplayBackend<const W: usize, const H: usize, FB>
173where
174    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
175{
176    /// The transfer token type returned by this backend.
177    type Transfer: DmaTransfer<Buffer = FrameBuf<Rgb565, FB>>;
178
179    /// Start a non-blocking DMA transfer of the full framebuffer.
180    ///
181    /// Takes ownership of the framebuffer. The caller cannot access the
182    /// buffer again until [`DmaTransfer::wait`] returns it.
183    ///
184    /// If starting the transfer fails the buffer is returned inside
185    /// [`TransferError`] so it is not lost.
186    fn start_dma_transfer(
187        &mut self,
188        framebuffer: FrameBuf<Rgb565, FB>,
189    ) -> Result<Self::Transfer, TransferError<FB>>;
190
191    /// Start a non-blocking DMA transfer of a framebuffer sub-region.
192    ///
193    /// Backends that do not support partial transfers may ignore `region`
194    /// and fall back to a full-frame transfer.
195    fn start_dma_transfer_region(
196        &mut self,
197        framebuffer: FrameBuf<Rgb565, FB>,
198        _region: DisplayRegion,
199    ) -> Result<Self::Transfer, TransferError<FB>> {
200        self.start_dma_transfer(framebuffer)
201    }
202
203    /// Optional: Clear the depth buffer using hardware acceleration (e.g. DMA2D / Chrom-ART).
204    /// Returns `true` if the hardware successfully cleared the depth buffer,
205    /// or `false` to let the engine fall back to CPU slice filling.
206    #[cfg(feature = "dma2d")]
207    fn hardware_clear_depth(&mut self, _zbuffer: &mut [crate::ZDepth]) -> bool {
208        false
209    }
210}
211
212// ── SimulatorBackend ──────────────────────────────────────────────────────────
213
214/// A transfer token that is already complete.
215///
216/// Used by [`SimulatorBackend`]: since there is no real DMA hardware, the
217/// framebuffer is simply held here until `wait` returns it.
218pub struct CompletedTransfer<FB>
219where
220    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
221{
222    framebuffer: Option<FrameBuf<Rgb565, FB>>,
223}
224
225impl<FB> DmaTransfer for CompletedTransfer<FB>
226where
227    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
228{
229    type Buffer = FrameBuf<Rgb565, FB>;
230
231    fn is_done(&self) -> bool {
232        true
233    }
234
235    fn wait(mut self) -> FrameBuf<Rgb565, FB> {
236        self.framebuffer
237            .take()
238            .expect("CompletedTransfer polled after completion")
239    }
240}
241
242impl<FB> core::future::Future for CompletedTransfer<FB>
243where
244    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
245{
246    type Output = FrameBuf<Rgb565, FB>;
247
248    fn poll(
249        self: core::pin::Pin<&mut Self>,
250        _cx: &mut core::task::Context<'_>,
251    ) -> core::task::Poll<Self::Output> {
252        let buf = unsafe { self.get_unchecked_mut() }
253            .framebuffer
254            .take()
255            .expect("CompletedTransfer polled after completion");
256        core::task::Poll::Ready(buf)
257    }
258}
259
260impl<FB> AsyncDmaTransfer for CompletedTransfer<FB>
261where
262    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
263{
264    type WaitFuture = Self;
265
266    fn wait_async(self) -> Self::WaitFuture {
267        self
268    }
269}
270
271/// No-op display backend for simulators and testing.
272///
273/// All transfers complete immediately — there is no real DMA hardware.
274/// Useful for:
275/// - Desktop simulators
276/// - Unit testing swap chain logic
277/// - Development without target hardware
278pub struct SimulatorBackend;
279
280impl SimulatorBackend {
281    pub fn new() -> Self {
282        Self
283    }
284}
285
286impl Default for SimulatorBackend {
287    fn default() -> Self {
288        Self::new()
289    }
290}
291
292impl<const W: usize, const H: usize, FB> DisplayBackend<W, H, FB> for SimulatorBackend
293where
294    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
295{
296    type Transfer = CompletedTransfer<FB>;
297
298    fn start_dma_transfer(
299        &mut self,
300        framebuffer: FrameBuf<Rgb565, FB>,
301    ) -> Result<CompletedTransfer<FB>, TransferError<FB>> {
302        Ok(CompletedTransfer {
303            framebuffer: Some(framebuffer),
304        })
305    }
306}
307
308// ── Tests ─────────────────────────────────────────────────────────────────────
309
310#[cfg(test)]
311mod tests {
312    extern crate std;
313    use super::*;
314    use core::cell::Cell;
315    use embedded_graphics_core::pixelcolor::RgbColor;
316    use embedded_graphics_framebuf::backends::EndianCorrectedBuffer;
317    use std::vec;
318
319    type TestBackend = EndianCorrectedBuffer<'static, Rgb565>;
320
321    fn make_fb<const W: usize, const H: usize>() -> FrameBuf<Rgb565, TestBackend> {
322        use embedded_graphics_framebuf::backends::EndianCorrection;
323        let data: &'static mut [Rgb565] = vec![Rgb565::BLACK; W * H].leak();
324        FrameBuf::new(
325            EndianCorrectedBuffer::new(data, EndianCorrection::ToLittleEndian),
326            W,
327            H,
328        )
329    }
330
331    // ── RegionTrackingBackend ─────────────────────────────────────────────────
332
333    struct RegionTransfer<FB: DMACapableFrameBufferBackend<Color = Rgb565>> {
334        framebuffer: Option<FrameBuf<Rgb565, FB>>,
335    }
336
337    impl<FB: DMACapableFrameBufferBackend<Color = Rgb565>> DmaTransfer for RegionTransfer<FB> {
338        type Buffer = FrameBuf<Rgb565, FB>;
339        fn is_done(&self) -> bool {
340            true
341        }
342        fn wait(mut self) -> FrameBuf<Rgb565, FB> {
343            self.framebuffer.take().unwrap()
344        }
345    }
346
347    impl<FB: DMACapableFrameBufferBackend<Color = Rgb565>> core::future::Future for RegionTransfer<FB> {
348        type Output = FrameBuf<Rgb565, FB>;
349        fn poll(
350            self: core::pin::Pin<&mut Self>,
351            _cx: &mut core::task::Context<'_>,
352        ) -> core::task::Poll<Self::Output> {
353            core::task::Poll::Ready(
354                unsafe { self.get_unchecked_mut() }
355                    .framebuffer
356                    .take()
357                    .unwrap(),
358            )
359        }
360    }
361
362    impl<FB: DMACapableFrameBufferBackend<Color = Rgb565>> AsyncDmaTransfer for RegionTransfer<FB> {
363        type WaitFuture = Self;
364        fn wait_async(self) -> Self::WaitFuture {
365            self
366        }
367    }
368
369    struct RegionTrackingBackend {
370        region_calls: Cell<usize>,
371    }
372
373    impl RegionTrackingBackend {
374        fn new() -> Self {
375            Self {
376                region_calls: Cell::new(0),
377            }
378        }
379    }
380
381    impl<const W: usize, const H: usize, FB> DisplayBackend<W, H, FB> for RegionTrackingBackend
382    where
383        FB: DMACapableFrameBufferBackend<Color = Rgb565>,
384    {
385        type Transfer = RegionTransfer<FB>;
386
387        fn start_dma_transfer(
388            &mut self,
389            framebuffer: FrameBuf<Rgb565, FB>,
390        ) -> Result<RegionTransfer<FB>, TransferError<FB>> {
391            Ok(RegionTransfer {
392                framebuffer: Some(framebuffer),
393            })
394        }
395
396        fn start_dma_transfer_region(
397            &mut self,
398            framebuffer: FrameBuf<Rgb565, FB>,
399            _region: DisplayRegion,
400        ) -> Result<RegionTransfer<FB>, TransferError<FB>> {
401            self.region_calls.set(self.region_calls.get() + 1);
402            Ok(RegionTransfer {
403                framebuffer: Some(framebuffer),
404            })
405        }
406    }
407
408    // ── Tests ─────────────────────────────────────────────────────────────────
409
410    #[test]
411    fn test_simulator_backend_transfer_completes_immediately() {
412        let mut backend = SimulatorBackend::new();
413        let fb = make_fb::<2, 2>();
414        let xfer = <SimulatorBackend as DisplayBackend<2, 2, TestBackend>>::start_dma_transfer(
415            &mut backend,
416            fb,
417        )
418        .unwrap();
419        assert!(xfer.is_done());
420        let _fb = xfer.wait();
421    }
422
423    #[test]
424    fn test_simulator_backend_returns_buffer_on_wait() {
425        let mut backend = SimulatorBackend::new();
426        let fb = make_fb::<4, 4>();
427        let xfer = <SimulatorBackend as DisplayBackend<4, 4, TestBackend>>::start_dma_transfer(
428            &mut backend,
429            fb,
430        )
431        .unwrap();
432        // wait() should return the framebuffer
433        let fb_back = xfer.wait();
434        assert_eq!(fb_back.width(), 4);
435        assert_eq!(fb_back.height(), 4);
436    }
437
438    #[test]
439    fn test_region_tracking_backend_counts_region_transfers() {
440        let mut backend = RegionTrackingBackend::new();
441        let fb = make_fb::<2, 2>();
442        let region = DisplayRegion::new(0, 0, 1, 1);
443        let xfer = <RegionTrackingBackend as DisplayBackend<2, 2, TestBackend>>::start_dma_transfer_region(
444            &mut backend,
445            fb,
446            region,
447        )
448        .unwrap();
449        assert_eq!(backend.region_calls.get(), 1);
450        let _ = xfer.wait();
451    }
452
453    #[test]
454    fn test_full_transfer_does_not_increment_region_counter() {
455        let mut backend = RegionTrackingBackend::new();
456        let fb = make_fb::<2, 2>();
457        let xfer =
458            <RegionTrackingBackend as DisplayBackend<2, 2, TestBackend>>::start_dma_transfer(
459                &mut backend,
460                fb,
461            )
462            .unwrap();
463        assert_eq!(backend.region_calls.get(), 0);
464        let _ = xfer.wait();
465    }
466}