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