Skip to main content

embedded_gui/
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///
23/// Alias for [`crate::PresentRegion`] so GUI dirty regions and DMA presents
24/// share the same type.
25pub use crate::present::PresentRegion as DisplayRegion;
26
27/// Error types for display backend operations.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum DisplayError {
30    /// DMA transfer is still in progress.
31    Busy,
32    /// Hardware error during transfer.
33    HardwareError,
34    /// Invalid buffer configuration.
35    InvalidBuffer,
36}
37
38/// Returned when a DMA transfer fails to start.
39///
40/// Carries both the error code and the framebuffer back to the caller so
41/// the buffer is not lost on failure.
42pub struct TransferError<FB>
43where
44    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
45{
46    /// The framebuffer that could not be transferred.
47    pub framebuffer: FrameBuf<Rgb565, FB>,
48    /// The reason the transfer failed.
49    pub error: DisplayError,
50}
51
52impl<FB> core::fmt::Debug for TransferError<FB>
53where
54    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
55{
56    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
57        f.debug_struct("TransferError")
58            .field("error", &self.error)
59            .finish_non_exhaustive()
60    }
61}
62
63/// An in-progress DMA transfer that holds the framebuffer until completion.
64///
65/// The buffer is inaccessible while this token is live — the only way to
66/// get it back is to call [`wait`](DmaTransfer::wait), which blocks until
67/// the hardware has finished reading.
68///
69/// Implementors for real hardware should cancel the DMA in their [`Drop`]
70/// impl so that dropping a token without waiting is always safe.
71pub trait DmaTransfer {
72    /// The buffer type that is returned when the transfer completes.
73    type Buffer;
74
75    /// Returns `true` if the DMA hardware has finished the transfer.
76    fn is_done(&self) -> bool;
77
78    /// Block until the transfer is complete and return the framebuffer.
79    ///
80    /// Consuming `self` ensures the buffer cannot be accessed while DMA
81    /// is still reading it.
82    fn wait(self) -> Self::Buffer;
83}
84
85/// An asynchronous DMA transfer that can be awaited as a future.
86///
87/// This allows integration with async executors (e.g. Embassy, RTOS task executors)
88/// without blocking the CPU while the DMA transfer is in progress.
89pub trait AsyncDmaTransfer: DmaTransfer {
90    /// The future type returned by `wait_async`.
91    type WaitFuture: core::future::Future<Output = Self::Buffer>;
92
93    /// Return a future that resolves to the framebuffer once the DMA transfer completes.
94    fn wait_async(self) -> Self::WaitFuture;
95}
96
97/// Platform-agnostic display backend trait.
98///
99/// Implementations handle the hardware-specific details of transferring a
100/// framebuffer to the display. The API is intentionally ownership-based:
101/// `start_dma_transfer` takes the framebuffer **by value** and returns a
102/// [`DmaTransfer`] token. The buffer is held inside the token and cannot
103/// be accessed again until [`DmaTransfer::wait`] returns it. This
104/// prevents write-after-submit data races at compile time.
105pub trait DisplayBackend<const W: usize, const H: usize, FB>
106where
107    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
108{
109    /// The transfer token type returned by this backend.
110    type Transfer: DmaTransfer<Buffer = FrameBuf<Rgb565, FB>>;
111
112    /// Start a non-blocking DMA transfer of the full framebuffer.
113    fn start_dma_transfer(
114        &mut self,
115        framebuffer: FrameBuf<Rgb565, FB>,
116    ) -> Result<Self::Transfer, TransferError<FB>>;
117
118    /// Start a non-blocking DMA transfer of a framebuffer sub-region.
119    ///
120    /// Backends that do not support partial transfers may ignore `region`
121    /// and fall back to a full-frame transfer.
122    fn start_dma_transfer_region(
123        &mut self,
124        framebuffer: FrameBuf<Rgb565, FB>,
125        _region: DisplayRegion,
126    ) -> Result<Self::Transfer, TransferError<FB>> {
127        self.start_dma_transfer(framebuffer)
128    }
129}
130
131// ── SimulatorBackend ──────────────────────────────────────────────────────────
132
133/// A transfer token that is already complete.
134pub struct CompletedTransfer<FB>
135where
136    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
137{
138    framebuffer: Option<FrameBuf<Rgb565, FB>>,
139}
140
141impl<FB> DmaTransfer for CompletedTransfer<FB>
142where
143    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
144{
145    type Buffer = FrameBuf<Rgb565, FB>;
146
147    fn is_done(&self) -> bool {
148        true
149    }
150
151    fn wait(mut self) -> FrameBuf<Rgb565, FB> {
152        self.framebuffer
153            .take()
154            .expect("CompletedTransfer polled after completion")
155    }
156}
157
158impl<FB> core::future::Future for CompletedTransfer<FB>
159where
160    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
161{
162    type Output = FrameBuf<Rgb565, FB>;
163
164    fn poll(
165        self: core::pin::Pin<&mut Self>,
166        _cx: &mut core::task::Context<'_>,
167    ) -> core::task::Poll<Self::Output> {
168        let buf = unsafe { self.get_unchecked_mut() }
169            .framebuffer
170            .take()
171            .expect("CompletedTransfer polled after completion");
172        core::task::Poll::Ready(buf)
173    }
174}
175
176impl<FB> AsyncDmaTransfer for CompletedTransfer<FB>
177where
178    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
179{
180    type WaitFuture = Self;
181
182    fn wait_async(self) -> Self::WaitFuture {
183        self
184    }
185}
186
187/// No-op display backend for simulators and testing.
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#[cfg(test)]
219mod tests {
220    extern crate std;
221    use super::*;
222    use core::cell::Cell;
223    use embedded_graphics_core::pixelcolor::RgbColor;
224    use embedded_graphics_framebuf::backends::EndianCorrectedBuffer;
225    use std::vec;
226
227    type TestBackend = EndianCorrectedBuffer<'static, Rgb565>;
228
229    fn make_fb<const W: usize, const H: usize>() -> FrameBuf<Rgb565, TestBackend> {
230        use embedded_graphics_framebuf::backends::EndianCorrection;
231        let data: &'static mut [Rgb565] = vec![Rgb565::BLACK; W * H].leak();
232        FrameBuf::new(
233            EndianCorrectedBuffer::new(data, EndianCorrection::ToLittleEndian),
234            W,
235            H,
236        )
237    }
238
239    struct RegionTransfer<FB: DMACapableFrameBufferBackend<Color = Rgb565>> {
240        framebuffer: Option<FrameBuf<Rgb565, FB>>,
241    }
242
243    impl<FB: DMACapableFrameBufferBackend<Color = Rgb565>> DmaTransfer for RegionTransfer<FB> {
244        type Buffer = FrameBuf<Rgb565, FB>;
245        fn is_done(&self) -> bool {
246            true
247        }
248        fn wait(mut self) -> FrameBuf<Rgb565, FB> {
249            self.framebuffer.take().unwrap()
250        }
251    }
252
253    impl<FB: DMACapableFrameBufferBackend<Color = Rgb565>> core::future::Future for RegionTransfer<FB> {
254        type Output = FrameBuf<Rgb565, FB>;
255        fn poll(
256            self: core::pin::Pin<&mut Self>,
257            _cx: &mut core::task::Context<'_>,
258        ) -> core::task::Poll<Self::Output> {
259            core::task::Poll::Ready(
260                unsafe { self.get_unchecked_mut() }
261                    .framebuffer
262                    .take()
263                    .unwrap(),
264            )
265        }
266    }
267
268    impl<FB: DMACapableFrameBufferBackend<Color = Rgb565>> AsyncDmaTransfer for RegionTransfer<FB> {
269        type WaitFuture = Self;
270        fn wait_async(self) -> Self::WaitFuture {
271            self
272        }
273    }
274
275    struct RegionTrackingBackend {
276        region_calls: Cell<usize>,
277    }
278
279    impl RegionTrackingBackend {
280        fn new() -> Self {
281            Self {
282                region_calls: Cell::new(0),
283            }
284        }
285    }
286
287    impl<const W: usize, const H: usize, FB> DisplayBackend<W, H, FB> for RegionTrackingBackend
288    where
289        FB: DMACapableFrameBufferBackend<Color = Rgb565>,
290    {
291        type Transfer = RegionTransfer<FB>;
292
293        fn start_dma_transfer(
294            &mut self,
295            framebuffer: FrameBuf<Rgb565, FB>,
296        ) -> Result<RegionTransfer<FB>, TransferError<FB>> {
297            Ok(RegionTransfer {
298                framebuffer: Some(framebuffer),
299            })
300        }
301
302        fn start_dma_transfer_region(
303            &mut self,
304            framebuffer: FrameBuf<Rgb565, FB>,
305            _region: DisplayRegion,
306        ) -> Result<RegionTransfer<FB>, TransferError<FB>> {
307            self.region_calls.set(self.region_calls.get() + 1);
308            Ok(RegionTransfer {
309                framebuffer: Some(framebuffer),
310            })
311        }
312    }
313
314    #[test]
315    fn test_simulator_backend_transfer_completes_immediately() {
316        let mut backend = SimulatorBackend::new();
317        let fb = make_fb::<2, 2>();
318        let xfer = <SimulatorBackend as DisplayBackend<2, 2, TestBackend>>::start_dma_transfer(
319            &mut backend,
320            fb,
321        )
322        .unwrap();
323        assert!(xfer.is_done());
324        let _fb = xfer.wait();
325    }
326
327    #[test]
328    fn test_region_tracking_backend_counts_region_transfers() {
329        let mut backend = RegionTrackingBackend::new();
330        let fb = make_fb::<2, 2>();
331        let region = DisplayRegion::new(0, 0, 1, 1);
332        let xfer = <RegionTrackingBackend as DisplayBackend<2, 2, TestBackend>>::start_dma_transfer_region(
333            &mut backend,
334            fb,
335            region,
336        )
337        .unwrap();
338        assert_eq!(backend.region_calls.get(), 1);
339        let _ = xfer.wait();
340    }
341}