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/// Platform-agnostic display backend trait.
100///
101/// Implementations handle the hardware-specific details of transferring a
102/// framebuffer to the display. The API is intentionally ownership-based:
103/// `start_dma_transfer` takes the framebuffer **by value** and returns a
104/// [`DmaTransfer`] token. The buffer is held inside the token and cannot
105/// be accessed again until [`DmaTransfer::wait`] returns it. This
106/// prevents write-after-submit data races at compile time.
107///
108/// # Implementing for real hardware
109///
110/// 1. Define a concrete transfer type that holds the buffer and any
111///    hardware state needed to poll or cancel the DMA.
112/// 2. In `start_dma_transfer`: program the DMA controller, then move the
113///    buffer into your transfer type.
114/// 3. In `DmaTransfer::wait`: spin or sleep until the done flag is set by
115///    the DMA interrupt, then return the buffer.
116/// 4. In `DmaTransfer::drop`: if the transfer is still running, cancel
117///    it. This ensures that dropping a forgotten token never leaves the
118///    DMA controller pointing at freed memory.
119pub trait DisplayBackend<const W: usize, const H: usize, FB>
120where
121    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
122{
123    /// The transfer token type returned by this backend.
124    type Transfer: DmaTransfer<Buffer = FrameBuf<Rgb565, FB>>;
125
126    /// Start a non-blocking DMA transfer of the full framebuffer.
127    ///
128    /// Takes ownership of the framebuffer. The caller cannot access the
129    /// buffer again until [`DmaTransfer::wait`] returns it.
130    ///
131    /// If starting the transfer fails the buffer is returned inside
132    /// [`TransferError`] so it is not lost.
133    fn start_dma_transfer(
134        &mut self,
135        framebuffer: FrameBuf<Rgb565, FB>,
136    ) -> Result<Self::Transfer, TransferError<FB>>;
137
138    /// Start a non-blocking DMA transfer of a framebuffer sub-region.
139    ///
140    /// Backends that do not support partial transfers may ignore `region`
141    /// and fall back to a full-frame transfer.
142    fn start_dma_transfer_region(
143        &mut self,
144        framebuffer: FrameBuf<Rgb565, FB>,
145        _region: DisplayRegion,
146    ) -> Result<Self::Transfer, TransferError<FB>> {
147        self.start_dma_transfer(framebuffer)
148    }
149}
150
151// ── SimulatorBackend ──────────────────────────────────────────────────────────
152
153/// A transfer token that is already complete.
154///
155/// Used by [`SimulatorBackend`]: since there is no real DMA hardware, the
156/// framebuffer is simply held here until `wait` returns it.
157pub struct CompletedTransfer<FB>
158where
159    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
160{
161    framebuffer: Option<FrameBuf<Rgb565, FB>>,
162}
163
164impl<FB> DmaTransfer for CompletedTransfer<FB>
165where
166    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
167{
168    type Buffer = FrameBuf<Rgb565, FB>;
169
170    fn is_done(&self) -> bool {
171        true
172    }
173
174    fn wait(mut self) -> FrameBuf<Rgb565, FB> {
175        self.framebuffer
176            .take()
177            .expect("CompletedTransfer polled after completion")
178    }
179}
180
181/// No-op display backend for simulators and testing.
182///
183/// All transfers complete immediately — there is no real DMA hardware.
184/// Useful for:
185/// - Desktop simulators
186/// - Unit testing swap chain logic
187/// - Development without target hardware
188pub struct SimulatorBackend;
189
190impl SimulatorBackend {
191    pub fn new() -> Self {
192        Self
193    }
194}
195
196impl Default for SimulatorBackend {
197    fn default() -> Self {
198        Self::new()
199    }
200}
201
202impl<const W: usize, const H: usize, FB> DisplayBackend<W, H, FB> for SimulatorBackend
203where
204    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
205{
206    type Transfer = CompletedTransfer<FB>;
207
208    fn start_dma_transfer(
209        &mut self,
210        framebuffer: FrameBuf<Rgb565, FB>,
211    ) -> Result<CompletedTransfer<FB>, TransferError<FB>> {
212        Ok(CompletedTransfer {
213            framebuffer: Some(framebuffer),
214        })
215    }
216}
217
218// ── Tests ─────────────────────────────────────────────────────────────────────
219
220#[cfg(test)]
221mod tests {
222    extern crate std;
223    use super::*;
224    use core::cell::Cell;
225    use embedded_graphics_core::pixelcolor::RgbColor;
226    use embedded_graphics_framebuf::backends::EndianCorrectedBuffer;
227    use std::vec;
228
229    type TestBackend = EndianCorrectedBuffer<'static, Rgb565>;
230
231    fn make_fb<const W: usize, const H: usize>() -> FrameBuf<Rgb565, TestBackend> {
232        use embedded_graphics_framebuf::backends::EndianCorrection;
233        let data: &'static mut [Rgb565] = vec![Rgb565::BLACK; W * H].leak();
234        FrameBuf::new(
235            EndianCorrectedBuffer::new(data, EndianCorrection::ToLittleEndian),
236            W,
237            H,
238        )
239    }
240
241    // ── RegionTrackingBackend ─────────────────────────────────────────────────
242
243    struct RegionTransfer<FB: DMACapableFrameBufferBackend<Color = Rgb565>> {
244        framebuffer: Option<FrameBuf<Rgb565, FB>>,
245    }
246
247    impl<FB: DMACapableFrameBufferBackend<Color = Rgb565>> DmaTransfer for RegionTransfer<FB> {
248        type Buffer = FrameBuf<Rgb565, FB>;
249        fn is_done(&self) -> bool {
250            true
251        }
252        fn wait(mut self) -> FrameBuf<Rgb565, FB> {
253            self.framebuffer.take().unwrap()
254        }
255    }
256
257    struct RegionTrackingBackend {
258        region_calls: Cell<usize>,
259    }
260
261    impl RegionTrackingBackend {
262        fn new() -> Self {
263            Self {
264                region_calls: Cell::new(0),
265            }
266        }
267    }
268
269    impl<const W: usize, const H: usize, FB> DisplayBackend<W, H, FB> for RegionTrackingBackend
270    where
271        FB: DMACapableFrameBufferBackend<Color = Rgb565>,
272    {
273        type Transfer = RegionTransfer<FB>;
274
275        fn start_dma_transfer(
276            &mut self,
277            framebuffer: FrameBuf<Rgb565, FB>,
278        ) -> Result<RegionTransfer<FB>, TransferError<FB>> {
279            Ok(RegionTransfer {
280                framebuffer: Some(framebuffer),
281            })
282        }
283
284        fn start_dma_transfer_region(
285            &mut self,
286            framebuffer: FrameBuf<Rgb565, FB>,
287            _region: DisplayRegion,
288        ) -> Result<RegionTransfer<FB>, TransferError<FB>> {
289            self.region_calls.set(self.region_calls.get() + 1);
290            Ok(RegionTransfer {
291                framebuffer: Some(framebuffer),
292            })
293        }
294    }
295
296    // ── Tests ─────────────────────────────────────────────────────────────────
297
298    #[test]
299    fn test_simulator_backend_transfer_completes_immediately() {
300        let mut backend = SimulatorBackend::new();
301        let fb = make_fb::<2, 2>();
302        let xfer = <SimulatorBackend as DisplayBackend<2, 2, TestBackend>>::start_dma_transfer(
303            &mut backend,
304            fb,
305        )
306        .unwrap();
307        assert!(xfer.is_done());
308        let _fb = xfer.wait();
309    }
310
311    #[test]
312    fn test_simulator_backend_returns_buffer_on_wait() {
313        let mut backend = SimulatorBackend::new();
314        let fb = make_fb::<4, 4>();
315        let xfer = <SimulatorBackend as DisplayBackend<4, 4, TestBackend>>::start_dma_transfer(
316            &mut backend,
317            fb,
318        )
319        .unwrap();
320        // wait() should return the framebuffer
321        let fb_back = xfer.wait();
322        assert_eq!(fb_back.width(), 4);
323        assert_eq!(fb_back.height(), 4);
324    }
325
326    #[test]
327    fn test_region_tracking_backend_counts_region_transfers() {
328        let mut backend = RegionTrackingBackend::new();
329        let fb = make_fb::<2, 2>();
330        let region = DisplayRegion::new(0, 0, 1, 1);
331        let xfer = <RegionTrackingBackend as DisplayBackend<2, 2, TestBackend>>::start_dma_transfer_region(
332            &mut backend,
333            fb,
334            region,
335        )
336        .unwrap();
337        assert_eq!(backend.region_calls.get(), 1);
338        let _ = xfer.wait();
339    }
340
341    #[test]
342    fn test_full_transfer_does_not_increment_region_counter() {
343        let mut backend = RegionTrackingBackend::new();
344        let fb = make_fb::<2, 2>();
345        let xfer =
346            <RegionTrackingBackend as DisplayBackend<2, 2, TestBackend>>::start_dma_transfer(
347                &mut backend,
348                fb,
349            )
350            .unwrap();
351        assert_eq!(backend.region_calls.get(), 0);
352        let _ = xfer.wait();
353    }
354}