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
8use embedded_graphics_core::pixelcolor::Rgb565;
9use embedded_graphics_framebuf::{FrameBuf, backends::DMACapableFrameBufferBackend};
10
11/// Rectangle region for partial framebuffer presents.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct DisplayRegion {
14    pub x: usize,
15    pub y: usize,
16    pub width: usize,
17    pub height: usize,
18}
19
20impl DisplayRegion {
21    pub const fn new(x: usize, y: usize, width: usize, height: usize) -> Self {
22        Self {
23            x,
24            y,
25            width,
26            height,
27        }
28    }
29}
30
31/// Error types for display backend operations
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum DisplayError {
34    /// DMA transfer is still in progress
35    Busy,
36    /// Hardware error during transfer
37    HardwareError,
38    /// Invalid buffer configuration
39    InvalidBuffer,
40}
41
42/// Platform-agnostic display backend trait
43///
44/// Implementations of this trait handle the hardware-specific details of
45/// transferring framebuffer data to the display using DMA.
46pub trait DisplayBackend<const W: usize, const H: usize, FB>
47where
48    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
49{
50    /// Start a non-blocking DMA transfer of the framebuffer to the display
51    ///
52    /// # Arguments
53    /// * `framebuffer` - The framebuffer to transfer
54    ///
55    /// # Returns
56    /// `Ok(())` if transfer started successfully, `Err(DisplayError)` otherwise
57    ///
58    /// # Note
59    /// This function should not block. If a transfer is already in progress,
60    /// it should return `Err(DisplayError::Busy)`.
61    fn start_dma_transfer(
62        &mut self,
63        framebuffer: &FrameBuf<Rgb565, FB>,
64    ) -> Result<(), DisplayError>;
65
66    /// Start a non-blocking transfer of a framebuffer sub-region.
67    ///
68    /// Default implementation falls back to full-frame transfer.
69    fn start_dma_transfer_region(
70        &mut self,
71        framebuffer: &FrameBuf<Rgb565, FB>,
72        _region: DisplayRegion,
73    ) -> Result<(), DisplayError> {
74        self.start_dma_transfer(framebuffer)
75    }
76
77    /// Wait for the current DMA transfer to complete
78    ///
79    /// This function blocks until the DMA transfer finishes.
80    fn wait_for_dma(&mut self);
81
82    /// Check if DMA is ready for a new transfer
83    ///
84    /// # Returns
85    /// `true` if no transfer is in progress, `false` otherwise
86    fn is_dma_ready(&self) -> bool;
87
88    /// Present a framebuffer to the display (convenience method)
89    ///
90    /// This is equivalent to calling `wait_for_dma()` followed by `start_dma_transfer()`.
91    ///
92    /// # Arguments
93    /// * `framebuffer` - The framebuffer to display
94    ///
95    /// # Returns
96    /// `Ok(())` if successful, `Err(DisplayError)` otherwise
97    fn present(&mut self, framebuffer: &FrameBuf<Rgb565, FB>) -> Result<(), DisplayError> {
98        self.wait_for_dma();
99        self.start_dma_transfer(framebuffer)
100    }
101
102    /// Present a framebuffer sub-region to the display.
103    ///
104    /// Default implementation falls back to full-frame present.
105    fn present_region(
106        &mut self,
107        framebuffer: &FrameBuf<Rgb565, FB>,
108        region: DisplayRegion,
109    ) -> Result<(), DisplayError> {
110        self.wait_for_dma();
111        self.start_dma_transfer_region(framebuffer, region)
112    }
113}
114
115/// No-op display backend for simulators and testing
116///
117/// This backend immediately "completes" all transfers and is always ready.
118/// It's useful for:
119/// - Desktop simulators that don't have real DMA hardware
120/// - Unit testing swap chain logic
121/// - Development without target hardware
122pub struct SimulatorBackend {
123    // No state needed for no-op backend
124}
125
126impl SimulatorBackend {
127    /// Create a new simulator backend
128    pub fn new() -> Self {
129        Self {}
130    }
131}
132
133impl Default for SimulatorBackend {
134    fn default() -> Self {
135        Self::new()
136    }
137}
138
139impl<const W: usize, const H: usize, FB> DisplayBackend<W, H, FB> for SimulatorBackend
140where
141    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
142{
143    fn start_dma_transfer(
144        &mut self,
145        _framebuffer: &FrameBuf<Rgb565, FB>,
146    ) -> Result<(), DisplayError> {
147        // No-op: simulator doesn't actually transfer data
148        Ok(())
149    }
150
151    fn wait_for_dma(&mut self) {
152        // No-op: no real DMA to wait for
153    }
154
155    fn is_dma_ready(&self) -> bool {
156        // Always ready since there's no real DMA
157        true
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    extern crate std;
164    use super::*;
165    use core::cell::Cell;
166    use embedded_graphics_core::pixelcolor::RgbColor;
167    use embedded_graphics_framebuf::backends::EndianCorrectedBuffer;
168    use std::vec;
169
170    // Type alias for testing
171    type TestBackend = EndianCorrectedBuffer<'static, Rgb565>;
172
173    struct RegionTrackingBackend {
174        region_calls: Cell<usize>,
175    }
176
177    impl RegionTrackingBackend {
178        fn new() -> Self {
179            Self {
180                region_calls: Cell::new(0),
181            }
182        }
183    }
184
185    impl<const W: usize, const H: usize, FB> DisplayBackend<W, H, FB> for RegionTrackingBackend
186    where
187        FB: DMACapableFrameBufferBackend<Color = Rgb565>,
188    {
189        fn start_dma_transfer(
190            &mut self,
191            _framebuffer: &FrameBuf<Rgb565, FB>,
192        ) -> Result<(), DisplayError> {
193            Ok(())
194        }
195
196        fn start_dma_transfer_region(
197            &mut self,
198            _framebuffer: &FrameBuf<Rgb565, FB>,
199            _region: DisplayRegion,
200        ) -> Result<(), DisplayError> {
201            self.region_calls.set(self.region_calls.get() + 1);
202            Ok(())
203        }
204
205        fn wait_for_dma(&mut self) {}
206
207        fn is_dma_ready(&self) -> bool {
208            true
209        }
210    }
211
212    #[test]
213    fn test_simulator_backend_creation() {
214        let backend = SimulatorBackend::new();
215        // Backend should exist and be ready
216        // Use explicit trait method call with types
217        assert!(<SimulatorBackend as DisplayBackend<
218            320,
219            240,
220            TestBackend,
221        >>::is_dma_ready(&backend));
222    }
223
224    #[test]
225    fn test_simulator_backend_always_ready() {
226        let mut backend = SimulatorBackend::new();
227
228        // Should always be ready
229        assert!(<SimulatorBackend as DisplayBackend<
230            320,
231            240,
232            TestBackend,
233        >>::is_dma_ready(&backend));
234
235        // Wait should be no-op
236        <SimulatorBackend as DisplayBackend<320, 240, TestBackend>>::wait_for_dma(&mut backend);
237        assert!(<SimulatorBackend as DisplayBackend<
238            320,
239            240,
240            TestBackend,
241        >>::is_dma_ready(&backend));
242    }
243
244    #[test]
245    fn test_present_region_calls_region_transfer() {
246        let mut backend = RegionTrackingBackend::new();
247        let data = vec![Rgb565::BLACK; 4].leak();
248        let fb = FrameBuf::new(
249            EndianCorrectedBuffer::new(
250                data,
251                embedded_graphics_framebuf::backends::EndianCorrection::ToLittleEndian,
252            ),
253            2,
254            2,
255        );
256        let region = DisplayRegion::new(0, 0, 1, 1);
257        <RegionTrackingBackend as DisplayBackend<2, 2, TestBackend>>::present_region(
258            &mut backend,
259            &fb,
260            region,
261        )
262        .unwrap();
263        assert_eq!(backend.region_calls.get(), 1);
264    }
265}