Skip to main content

embedded_gui/
swapchain.rs

1//! Swap chain implementation for double-buffered rendering.
2//!
3//! A swap chain manages two framebuffers (front and back) and coordinates
4//! DMA transfers to eliminate visual tearing and improve performance.
5//!
6//! # Architecture
7//! - **Back buffer**: The CPU renders into this buffer at all times.
8//! - **Front buffer**: Owned by either an in-flight DMA transfer or the
9//!   swap chain itself while idle.
10//! - **Swap**: When rendering completes, `present()` recovers the front
11//!   buffer (waiting for any in-flight DMA), swaps it with the back
12//!   buffer, and hands the new front to the backend to start the next
13//!   transfer.
14//!
15//! # Safety
16//! The front framebuffer is moved into the [`DmaTransfer`] token returned
17//! by the backend. The compiler enforces that nobody can write to it until
18//! [`DmaTransfer::wait`] returns it, eliminating the data race that a
19//! borrow-based API cannot prevent.
20
21use crate::display_backend::{DisplayBackend, DisplayError, DisplayRegion, DmaTransfer};
22use embedded_graphics_core::pixelcolor::Rgb565;
23use embedded_graphics_framebuf::{
24    FrameBuf,
25    backends::{DMACapableFrameBufferBackend, EndianCorrectedBuffer, EndianCorrection},
26};
27
28// ── FrontState ────────────────────────────────────────────────────────────────
29
30/// Tracks whether the front framebuffer is idle (owned by the swap chain)
31/// or in-flight (owned by a DMA transfer token).
32enum FrontState<FB, Xfer> {
33    Idle(FB),
34    InFlight(Xfer),
35}
36
37impl<FB, Xfer: DmaTransfer<Buffer = FB>> FrontState<FB, Xfer> {
38    /// Recover the framebuffer, blocking if a transfer is still running.
39    fn recover(self) -> FB {
40        match self {
41            FrontState::Idle(fb) => fb,
42            FrontState::InFlight(xfer) => xfer.wait(),
43        }
44    }
45
46    /// Returns `true` if there is no in-flight transfer or it has finished.
47    fn is_ready(&self) -> bool {
48        match self {
49            FrontState::Idle(_) => true,
50            FrontState::InFlight(xfer) => xfer.is_done(),
51        }
52    }
53}
54
55impl<FB, Xfer: crate::display_backend::AsyncDmaTransfer<Buffer = FB>> FrontState<FB, Xfer> {
56    /// Recover the framebuffer asynchronously.
57    async fn recover_async(self) -> FB {
58        match self {
59            FrontState::Idle(fb) => fb,
60            FrontState::InFlight(xfer) => xfer.wait_async().await,
61        }
62    }
63}
64
65// ── SwapChain ─────────────────────────────────────────────────────────────────
66
67/// Double-buffered swap chain for tear-free rendering.
68///
69/// The back buffer is always available to the CPU via [`get_back_buffer`].
70/// The front buffer moves into the backend's DMA transfer token on each
71/// [`present`] call and is recovered (blocking) on the next one.
72///
73/// # Type Parameters
74/// - `W`, `H`: Framebuffer dimensions in pixels (const generics).
75/// - `FB`: Framebuffer backend implementing [`DMACapableFrameBufferBackend`].
76/// - `B`: Display backend implementing [`DisplayBackend<W, H, FB>`].
77///
78/// [`get_back_buffer`]: SwapChain::get_back_buffer
79/// [`present`]: SwapChain::present
80pub struct SwapChain<const W: usize, const H: usize, FB, B>
81where
82    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
83    B: DisplayBackend<W, H, FB>,
84{
85    /// Back buffer — always owned by the swap chain.
86    back: FrameBuf<Rgb565, FB>,
87    /// Front buffer — either idle here or inside a DMA transfer token.
88    front: Option<FrontState<FrameBuf<Rgb565, FB>, B::Transfer>>,
89    backend: B,
90    frame_count: u64,
91}
92
93/// Type alias for `SwapChain` backed by [`EndianCorrectedBuffer`].
94///
95/// This is the most common configuration for statically allocated
96/// framebuffer memory.
97pub type StandardSwapChain<const W: usize, const H: usize, B> =
98    SwapChain<W, H, EndianCorrectedBuffer<'static, Rgb565>, B>;
99
100// ── StandardSwapChain constructor ─────────────────────────────────────────────
101
102impl<const W: usize, const H: usize, B> StandardSwapChain<W, H, B>
103where
104    B: DisplayBackend<W, H, EndianCorrectedBuffer<'static, Rgb565>>,
105{
106    /// Create a new swap chain from static slices.
107    ///
108    /// # Arguments
109    /// * `front_data` — Static mutable slice for the front framebuffer.
110    /// * `back_data`  — Static mutable slice for the back framebuffer.
111    /// * `big_endian` — Byte order of pixel data sent to the display.
112    /// * `backend`    — Display backend used for DMA operations.
113    ///
114    /// # Example
115    /// ```ignore
116    /// static mut FB0: [Rgb565; 240 * 135] = [Rgb565::BLACK; 240 * 135];
117    /// static mut FB1: [Rgb565; 240 * 135] = [Rgb565::BLACK; 240 * 135];
118    ///
119    /// let swap_chain = unsafe {
120    ///     StandardSwapChain::<240, 135, _>::from_static_slices(
121    ///         &mut FB0,
122    ///         &mut FB1,
123    ///         false,
124    ///         MyHardwareBackend::new(),
125    ///     )
126    /// };
127    /// ```
128    pub fn from_static_slices(
129        front_data: &'static mut [Rgb565],
130        back_data: &'static mut [Rgb565],
131        big_endian: bool,
132        backend: B,
133    ) -> Self {
134        let mk_buf = |data: &'static mut [Rgb565]| {
135            let correction = if big_endian {
136                EndianCorrection::ToBigEndian
137            } else {
138                EndianCorrection::ToLittleEndian
139            };
140            EndianCorrectedBuffer::new(data, correction)
141        };
142
143        let front_fb = FrameBuf::new(mk_buf(front_data), W, H);
144        let back_fb = FrameBuf::new(mk_buf(back_data), W, H);
145
146        Self {
147            back: back_fb,
148            front: Some(FrontState::Idle(front_fb)),
149            backend,
150            frame_count: 0,
151        }
152    }
153}
154
155// ── SwapChain methods ─────────────────────────────────────────────────────────
156
157impl<const W: usize, const H: usize, FB, B> SwapChain<W, H, FB, B>
158where
159    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
160    B: DisplayBackend<W, H, FB>,
161{
162    /// Get a mutable reference to the back buffer for rendering.
163    ///
164    /// The back buffer is always available — DMA only ever touches the
165    /// front buffer, which is kept separately.
166    pub fn get_back_buffer(&mut self) -> &mut FrameBuf<Rgb565, FB> {
167        &mut self.back
168    }
169
170    /// Get a reference to the front buffer if it is currently idle.
171    ///
172    /// Returns `None` while a DMA transfer is in progress.
173    pub fn get_front_buffer(&self) -> Option<&FrameBuf<Rgb565, FB>> {
174        match &self.front {
175            Some(FrontState::Idle(fb)) => Some(fb),
176            _ => None,
177        }
178    }
179
180    /// Present the back buffer (blocking).
181    ///
182    /// 1. Waits for any in-progress DMA transfer to complete.
183    /// 2. Swaps the front and back buffers.
184    /// 3. Starts a new DMA transfer of the new front buffer.
185    ///
186    /// After this call returns, the CPU may immediately start rendering to
187    /// the new back buffer while DMA reads from the new front buffer.
188    pub fn present(&mut self) -> Result<(), DisplayError> {
189        self.present_impl(|backend, fb| backend.start_dma_transfer(fb))
190    }
191
192    /// Present the back buffer without blocking.
193    ///
194    /// Returns [`DisplayError::Busy`] immediately if a DMA transfer is
195    /// still in progress, leaving both buffers unchanged.
196    pub fn try_present(&mut self) -> Result<(), DisplayError> {
197        if !self.is_ready() {
198            return Err(DisplayError::Busy);
199        }
200        self.present_impl(|backend, fb| backend.start_dma_transfer(fb))
201    }
202
203    /// Present only a sub-region of the back buffer (blocking).
204    ///
205    /// Backends that do not support partial DMA automatically fall back to
206    /// a full-frame transfer.
207    pub fn present_region(&mut self, region: DisplayRegion) -> Result<(), DisplayError> {
208        self.present_impl(|backend, fb| backend.start_dma_transfer_region(fb, region))
209    }
210
211    /// Non-blocking partial present.
212    ///
213    /// Returns [`DisplayError::Busy`] if a transfer is still running.
214    pub fn try_present_region(&mut self, region: DisplayRegion) -> Result<(), DisplayError> {
215        if !self.is_ready() {
216            return Err(DisplayError::Busy);
217        }
218        self.present_impl(|backend, fb| backend.start_dma_transfer_region(fb, region))
219    }
220
221    /// Block until the current DMA transfer completes.
222    ///
223    /// After this call the front buffer is in the idle state and the next
224    /// `present` will not need to wait.
225    pub fn wait_for_vsync(&mut self) {
226        if let Some(state) = self.front.take() {
227            let fb = state.recover();
228            self.front = Some(FrontState::Idle(fb));
229        }
230    }
231
232    /// Wait for the current DMA transfer to complete asynchronously.
233    pub async fn wait_for_vsync_async(&mut self)
234    where
235        B::Transfer: crate::display_backend::AsyncDmaTransfer<Buffer = FrameBuf<Rgb565, FB>>,
236    {
237        if let Some(state) = self.front.take() {
238            let fb = state.recover_async().await;
239            self.front = Some(FrontState::Idle(fb));
240        }
241    }
242
243    /// Present the back buffer asynchronously.
244    pub async fn present_async(&mut self) -> Result<(), DisplayError>
245    where
246        B::Transfer: crate::display_backend::AsyncDmaTransfer<Buffer = FrameBuf<Rgb565, FB>>,
247    {
248        // 1. Recover the front framebuffer asynchronously (yield if DMA was running).
249        let old_front = if let Some(state) = self.front.take() {
250            state.recover_async().await
251        } else {
252            panic!("SwapChain front buffer missing — double present?");
253        };
254
255        // 2. Swap: old_front becomes the new back, current back becomes the new front.
256        let new_front = core::mem::replace(&mut self.back, old_front);
257
258        // 3. Hand the new front to the backend.
259        match self.backend.start_dma_transfer(new_front) {
260            Ok(transfer) => {
261                self.front = Some(FrontState::InFlight(transfer));
262                self.frame_count += 1;
263                Ok(())
264            }
265            Err(e) => {
266                let recovered_front = core::mem::replace(&mut self.back, e.framebuffer);
267                self.front = Some(FrontState::Idle(recovered_front));
268                Err(e.error)
269            }
270        }
271    }
272
273    /// Present only a sub-region of the back buffer asynchronously.
274    pub async fn present_region_async(&mut self, region: DisplayRegion) -> Result<(), DisplayError>
275    where
276        B::Transfer: crate::display_backend::AsyncDmaTransfer<Buffer = FrameBuf<Rgb565, FB>>,
277    {
278        // 1. Recover the front framebuffer asynchronously (yield if DMA was running).
279        let old_front = if let Some(state) = self.front.take() {
280            state.recover_async().await
281        } else {
282            panic!("SwapChain front buffer missing — double present?");
283        };
284
285        // 2. Swap: old_front becomes the new back, current back becomes the new front.
286        let new_front = core::mem::replace(&mut self.back, old_front);
287
288        // 3. Hand the new front to the backend.
289        match self.backend.start_dma_transfer_region(new_front, region) {
290            Ok(transfer) => {
291                self.front = Some(FrontState::InFlight(transfer));
292                self.frame_count += 1;
293                Ok(())
294            }
295            Err(e) => {
296                let recovered_front = core::mem::replace(&mut self.back, e.framebuffer);
297                self.front = Some(FrontState::Idle(recovered_front));
298                Err(e.error)
299            }
300        }
301    }
302
303    /// Returns `true` if no DMA transfer is running (or the hardware has
304    /// signalled completion), so `try_present` would succeed.
305    pub fn is_ready(&self) -> bool {
306        self.front.as_ref().is_none_or(|s| s.is_ready())
307    }
308
309    /// Total number of frames presented since construction (or the last
310    /// [`reset_frame_count`](SwapChain::reset_frame_count) call).
311    pub fn frame_count(&self) -> u64 {
312        self.frame_count
313    }
314
315    /// Reset the frame counter to zero.
316    pub fn reset_frame_count(&mut self) {
317        self.frame_count = 0;
318    }
319
320    /// Framebuffer dimensions `(W, H)`.
321    pub fn dimensions(&self) -> (usize, usize) {
322        (W, H)
323    }
324
325    // ── Private helpers ───────────────────────────────────────────────────────
326
327    /// Shared logic for all present variants.
328    ///
329    /// `start_fn` is called with `(&mut backend, front_framebuffer)` and
330    /// must return a transfer token or a `TransferError`.
331    fn present_impl<F>(&mut self, start_fn: F) -> Result<(), DisplayError>
332    where
333        F: FnOnce(
334            &mut B,
335            FrameBuf<Rgb565, FB>,
336        ) -> Result<B::Transfer, crate::display_backend::TransferError<FB>>,
337    {
338        // 1. Recover the front framebuffer (block if DMA was running).
339        let old_front = self
340            .front
341            .take()
342            .expect("SwapChain front buffer missing — double present?")
343            .recover();
344
345        // 2. Swap: old_front becomes the new back, current back becomes the
346        //    new front.
347        let new_front = core::mem::replace(&mut self.back, old_front);
348
349        // 3. Hand the new front to the backend.
350        match start_fn(&mut self.backend, new_front) {
351            Ok(transfer) => {
352                self.front = Some(FrontState::InFlight(transfer));
353                self.frame_count += 1;
354                Ok(())
355            }
356            Err(e) => {
357                // Transfer failed — put the buffer back so it is not lost.
358                // self.back currently holds old_front; swap back.
359                let recovered_front = core::mem::replace(&mut self.back, e.framebuffer);
360                self.front = Some(FrontState::Idle(recovered_front));
361                Err(e.error)
362            }
363        }
364    }
365}
366
367// ── TripleSwapChain ───────────────────────────────────────────────────────────
368
369/// Triple-buffered swap chain for smoother pacing under bursty frame times.
370///
371/// - `render`: the buffer the CPU is currently writing to.
372/// - `ready`:  the last fully-rendered buffer waiting to be shown.
373/// - `display`: the buffer currently owned by DMA (or idle between frames).
374///
375/// On each `present` call the render and display buffers are rotated so
376/// the CPU can immediately start the next frame without waiting for the
377/// display scan-out to finish.
378#[cfg(feature = "triple-buffering")]
379pub struct TripleSwapChain<const W: usize, const H: usize, FB, B>
380where
381    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
382    B: DisplayBackend<W, H, FB>,
383{
384    display: Option<FrontState<FrameBuf<Rgb565, FB>, B::Transfer>>,
385    ready: FrameBuf<Rgb565, FB>,
386    render: FrameBuf<Rgb565, FB>,
387    backend: B,
388    frame_count: u64,
389}
390
391#[cfg(feature = "triple-buffering")]
392pub type StandardTripleSwapChain<const W: usize, const H: usize, B> =
393    TripleSwapChain<W, H, EndianCorrectedBuffer<'static, Rgb565>, B>;
394
395#[cfg(feature = "triple-buffering")]
396impl<const W: usize, const H: usize, B> StandardTripleSwapChain<W, H, B>
397where
398    B: DisplayBackend<W, H, EndianCorrectedBuffer<'static, Rgb565>>,
399{
400    pub fn from_static_slices(
401        display_data: &'static mut [Rgb565],
402        ready_data: &'static mut [Rgb565],
403        render_data: &'static mut [Rgb565],
404        big_endian: bool,
405        backend: B,
406    ) -> Self {
407        let mk = |data: &'static mut [Rgb565]| {
408            let correction = if big_endian {
409                EndianCorrection::ToBigEndian
410            } else {
411                EndianCorrection::ToLittleEndian
412            };
413            FrameBuf::new(EndianCorrectedBuffer::new(data, correction), W, H)
414        };
415        Self {
416            display: Some(FrontState::Idle(mk(display_data))),
417            ready: mk(ready_data),
418            render: mk(render_data),
419            backend,
420            frame_count: 0,
421        }
422    }
423}
424
425#[cfg(feature = "triple-buffering")]
426impl<const W: usize, const H: usize, FB, B> TripleSwapChain<W, H, FB, B>
427where
428    FB: DMACapableFrameBufferBackend<Color = Rgb565>,
429    B: DisplayBackend<W, H, FB>,
430{
431    /// Get a mutable reference to the render buffer.
432    pub fn get_render_buffer(&mut self) -> &mut FrameBuf<Rgb565, FB> {
433        &mut self.render
434    }
435
436    /// Present the render buffer (blocking).
437    ///
438    /// Waits for the previous display transfer to complete, then:
439    /// 1. Rotates `render → display` and starts DMA.
440    /// 2. Rotates `display (old) → ready` so the CPU has a fresh buffer.
441    pub fn present(&mut self) -> Result<(), DisplayError> {
442        self.present_impl(|backend, fb| backend.start_dma_transfer(fb))
443    }
444
445    /// Present the render buffer asynchronously.
446    pub async fn present_async(&mut self) -> Result<(), DisplayError>
447    where
448        B::Transfer: crate::display_backend::AsyncDmaTransfer<Buffer = FrameBuf<Rgb565, FB>>,
449    {
450        // 1. Recover the display buffer asynchronously (yield if DMA was running).
451        let old_display = if let Some(state) = self.display.take() {
452            state.recover_async().await
453        } else {
454            panic!("TripleSwapChain display buffer missing");
455        };
456
457        // 2. render → new display, old_display → render slot temporarily.
458        let rendered = core::mem::replace(&mut self.render, old_display);
459
460        // 3. Start DMA on the freshly rendered frame.
461        match self.backend.start_dma_transfer(rendered) {
462            Ok(transfer) => {
463                self.display = Some(FrontState::InFlight(transfer));
464                // 4. ready ↔ render: CPU gets the old ready buffer to render into.
465                core::mem::swap(&mut self.ready, &mut self.render);
466                self.frame_count += 1;
467                Ok(())
468            }
469            Err(e) => {
470                let old_display = core::mem::replace(&mut self.render, e.framebuffer);
471                self.display = Some(FrontState::Idle(old_display));
472                Err(e.error)
473            }
474        }
475    }
476
477    /// Non-blocking triple-buffer present.
478    ///
479    /// Returns [`DisplayError::Busy`] if the previous display transfer has
480    /// not finished yet.
481    pub fn try_present(&mut self) -> Result<(), DisplayError> {
482        let ready = self.display.as_ref().is_none_or(|s| s.is_ready());
483        if !ready {
484            return Err(DisplayError::Busy);
485        }
486        self.present_impl(|backend, fb| backend.start_dma_transfer(fb))
487    }
488
489    pub fn frame_count(&self) -> u64 {
490        self.frame_count
491    }
492
493    fn present_impl<F>(&mut self, start_fn: F) -> Result<(), DisplayError>
494    where
495        F: FnOnce(
496            &mut B,
497            FrameBuf<Rgb565, FB>,
498        ) -> Result<B::Transfer, crate::display_backend::TransferError<FB>>,
499    {
500        // 1. Recover the display buffer (block if DMA was running).
501        let old_display = self
502            .display
503            .take()
504            .expect("TripleSwapChain display buffer missing")
505            .recover();
506
507        // 2. render → new display, old_display → render slot temporarily.
508        let rendered = core::mem::replace(&mut self.render, old_display);
509
510        // 3. Start DMA on the freshly rendered frame.
511        match start_fn(&mut self.backend, rendered) {
512            Ok(transfer) => {
513                self.display = Some(FrontState::InFlight(transfer));
514                // 4. ready ↔ render: CPU gets the old ready buffer to render into.
515                core::mem::swap(&mut self.ready, &mut self.render);
516                self.frame_count += 1;
517                Ok(())
518            }
519            Err(e) => {
520                // Undo: put rendered back into render, restore old_display.
521                let old_display = core::mem::replace(&mut self.render, e.framebuffer);
522                self.display = Some(FrontState::Idle(old_display));
523                Err(e.error)
524            }
525        }
526    }
527}
528
529// ── Tests ─────────────────────────────────────────────────────────────────────
530
531#[cfg(test)]
532mod tests {
533    extern crate std;
534    use super::*;
535    use crate::display_backend::{DmaTransfer, SimulatorBackend, TransferError};
536    use core::cell::Cell;
537    use embedded_graphics_core::pixelcolor::RgbColor;
538    use std::vec;
539
540    fn make_static_slice(n: usize) -> &'static mut [Rgb565] {
541        vec![Rgb565::BLACK; n].leak()
542    }
543
544    // ── TrackingBackend ───────────────────────────────────────────────────────
545    //
546    // Counts how many times start_dma_transfer_region was called and
547    // otherwise behaves like SimulatorBackend.
548
549    struct TrackingTransfer<FB: DMACapableFrameBufferBackend<Color = Rgb565>> {
550        framebuffer: Option<FrameBuf<Rgb565, FB>>,
551    }
552
553    impl<FB: DMACapableFrameBufferBackend<Color = Rgb565>> DmaTransfer for TrackingTransfer<FB> {
554        type Buffer = FrameBuf<Rgb565, FB>;
555        fn is_done(&self) -> bool {
556            true
557        }
558        fn wait(mut self) -> FrameBuf<Rgb565, FB> {
559            self.framebuffer.take().unwrap()
560        }
561    }
562
563    impl<FB: DMACapableFrameBufferBackend<Color = Rgb565>> core::future::Future
564        for TrackingTransfer<FB>
565    {
566        type Output = FrameBuf<Rgb565, FB>;
567        fn poll(
568            self: core::pin::Pin<&mut Self>,
569            _cx: &mut core::task::Context<'_>,
570        ) -> core::task::Poll<Self::Output> {
571            core::task::Poll::Ready(
572                unsafe { self.get_unchecked_mut() }
573                    .framebuffer
574                    .take()
575                    .unwrap(),
576            )
577        }
578    }
579
580    impl<FB: DMACapableFrameBufferBackend<Color = Rgb565>> crate::display_backend::AsyncDmaTransfer
581        for TrackingTransfer<FB>
582    {
583        type WaitFuture = Self;
584        fn wait_async(self) -> Self::WaitFuture {
585            self
586        }
587    }
588
589    struct TrackingBackend {
590        region_present_count: Cell<usize>,
591    }
592
593    impl TrackingBackend {
594        fn new() -> Self {
595            Self {
596                region_present_count: Cell::new(0),
597            }
598        }
599    }
600
601    impl<const W: usize, const H: usize, FB> DisplayBackend<W, H, FB> for TrackingBackend
602    where
603        FB: DMACapableFrameBufferBackend<Color = Rgb565>,
604    {
605        type Transfer = TrackingTransfer<FB>;
606
607        fn start_dma_transfer(
608            &mut self,
609            framebuffer: FrameBuf<Rgb565, FB>,
610        ) -> Result<TrackingTransfer<FB>, TransferError<FB>> {
611            Ok(TrackingTransfer {
612                framebuffer: Some(framebuffer),
613            })
614        }
615
616        fn start_dma_transfer_region(
617            &mut self,
618            framebuffer: FrameBuf<Rgb565, FB>,
619            _region: DisplayRegion,
620        ) -> Result<TrackingTransfer<FB>, TransferError<FB>> {
621            self.region_present_count
622                .set(self.region_present_count.get() + 1);
623            Ok(TrackingTransfer {
624                framebuffer: Some(framebuffer),
625            })
626        }
627    }
628
629    // ── Helper ────────────────────────────────────────────────────────────────
630
631    fn make_swap_chain<B>(backend: B) -> StandardSwapChain<320, 240, B>
632    where
633        B: DisplayBackend<320, 240, EndianCorrectedBuffer<'static, Rgb565>>,
634    {
635        StandardSwapChain::<320, 240, _>::from_static_slices(
636            make_static_slice(320 * 240),
637            make_static_slice(320 * 240),
638            false,
639            backend,
640        )
641    }
642
643    // ── SwapChain tests ───────────────────────────────────────────────────────
644
645    #[test]
646    fn test_swapchain_creation() {
647        let sc = make_swap_chain(SimulatorBackend::new());
648        assert_eq!(sc.dimensions(), (320, 240));
649        assert_eq!(sc.frame_count(), 0);
650        assert!(sc.is_ready());
651    }
652
653    #[test]
654    fn test_swapchain_present() {
655        let mut sc = make_swap_chain(SimulatorBackend::new());
656        assert!(sc.present().is_ok());
657        assert_eq!(sc.frame_count(), 1);
658    }
659
660    #[test]
661    fn test_swapchain_multiple_presents() {
662        let mut sc = make_swap_chain(SimulatorBackend::new());
663        for _ in 0..5 {
664            assert!(sc.present().is_ok());
665        }
666        assert_eq!(sc.frame_count(), 5);
667    }
668
669    #[test]
670    fn test_swapchain_try_present() {
671        let mut sc = make_swap_chain(SimulatorBackend::new());
672        assert!(sc.try_present().is_ok());
673        assert_eq!(sc.frame_count(), 1);
674    }
675
676    #[test]
677    fn test_swapchain_frame_counter() {
678        let mut sc = make_swap_chain(SimulatorBackend::new());
679        assert_eq!(sc.frame_count(), 0);
680        sc.present().unwrap();
681        assert_eq!(sc.frame_count(), 1);
682        sc.present().unwrap();
683        assert_eq!(sc.frame_count(), 2);
684        sc.reset_frame_count();
685        assert_eq!(sc.frame_count(), 0);
686    }
687
688    #[test]
689    fn test_swapchain_get_back_buffer_always_available() {
690        let mut sc = make_swap_chain(SimulatorBackend::new());
691        sc.present().unwrap();
692        // Even after present, back buffer must be accessible for rendering
693        let _back = sc.get_back_buffer();
694    }
695
696    #[test]
697    fn test_swapchain_wait_for_vsync() {
698        let mut sc = make_swap_chain(SimulatorBackend::new());
699        sc.present().unwrap();
700        sc.wait_for_vsync();
701        // After vsync, front is idle and is_ready returns true
702        assert!(sc.is_ready());
703    }
704
705    #[test]
706    fn test_swapchain_present_region() {
707        let fb0 = make_static_slice(64 * 64);
708        let fb1 = make_static_slice(64 * 64);
709        let mut sc = StandardSwapChain::<64, 64, _>::from_static_slices(
710            fb0,
711            fb1,
712            false,
713            TrackingBackend::new(),
714        );
715        sc.present_region(DisplayRegion::new(0, 0, 8, 8)).unwrap();
716        assert_eq!(sc.backend.region_present_count.get(), 1);
717    }
718
719    #[test]
720    fn test_swapchain_is_ready_after_simulator_present() {
721        let mut sc = make_swap_chain(SimulatorBackend::new());
722        // SimulatorBackend transfer is always done immediately
723        sc.present().unwrap();
724        assert!(sc.is_ready());
725    }
726
727    // ── TripleSwapChain tests ─────────────────────────────────────────────────
728
729    #[cfg(feature = "triple-buffering")]
730    #[test]
731    fn test_triple_swapchain_present() {
732        let fb0 = make_static_slice(64 * 64);
733        let fb1 = make_static_slice(64 * 64);
734        let fb2 = make_static_slice(64 * 64);
735        let mut sc = StandardTripleSwapChain::<64, 64, _>::from_static_slices(
736            fb0,
737            fb1,
738            fb2,
739            false,
740            SimulatorBackend::new(),
741        );
742        assert_eq!(sc.frame_count(), 0);
743        sc.present().unwrap();
744        assert_eq!(sc.frame_count(), 1);
745    }
746}