Skip to main content

ferroui_base/rendering/
swapchain_base.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3
4use crate::reactive::{Disposable, IDisposable};
5use crate::PixelSize;
6
7/// The state of the last presentation of a swapchain image.
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9pub enum SwapchainImagePresentStatus {
10    /// The presentation has not completed yet.
11    Pending,
12    /// The presentation completed successfully.
13    RanToCompletion,
14    /// The presentation was canceled.
15    Canceled,
16    /// The presentation failed.
17    Faulted,
18}
19
20/// An image of a composition-backed swapchain.
21pub trait ISwapchainImage {
22    fn size(&self) -> PixelSize;
23
24    /// The state of the last presentation; `None` when the image has not
25    /// been presented yet.
26    fn last_present(&self) -> Option<SwapchainImagePresentStatus>;
27
28    fn begin_draw(&self);
29
30    fn present(&self);
31
32    /// Releases the image.
33    fn dispose(&self);
34}
35
36/// The image factory of a swapchain: the member a concrete swapchain has to
37/// provide.
38pub trait SwapchainImageFactory<TImage: ISwapchainImage + ?Sized> {
39    fn create_image(&self, size: PixelSize) -> Rc<TImage>;
40}
41
42/// A helper class for composition-backed swapchains, should not be a public
43/// API yet.
44///
45/// A concrete swapchain embeds this type and provides the image factory.
46pub struct SwapchainBase<TImage: ISwapchainImage + ?Sized + 'static> {
47    pending_images: Rc<RefCell<Vec<Rc<TImage>>>>,
48}
49
50impl<TImage: ISwapchainImage + ?Sized + 'static> Default for SwapchainBase<TImage> {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl<TImage: ISwapchainImage + ?Sized + 'static> SwapchainBase<TImage> {
57    pub fn new() -> Self {
58        Self { pending_images: Rc::new(RefCell::new(Vec::new())) }
59    }
60
61    fn is_broken(image: &TImage) -> bool {
62        image.last_present() == Some(SwapchainImagePresentStatus::Faulted)
63    }
64
65    fn is_ready(image: &TImage) -> bool {
66        matches!(image.last_present(), None | Some(SwapchainImagePresentStatus::RanToCompletion))
67    }
68
69    fn cleanup_and_find_next_image(&self, size: PixelSize) -> Option<Rc<TImage>> {
70        let mut first_found: Option<Rc<TImage>> = None;
71        let mut found_multiple = false;
72
73        let mut c = self.pending_images.borrow().len();
74        while c > 0 {
75            c -= 1;
76            // Cloned out: the image callbacks must not run under the borrow.
77            let image = self.pending_images.borrow()[c].clone();
78            let ready = Self::is_ready(&image);
79            let matches = image.size() == size;
80            if Self::is_broken(&image) || (!matches && ready) {
81                image.dispose();
82                self.pending_images.borrow_mut().remove(c);
83            }
84
85            if matches && ready {
86                if first_found.is_none() {
87                    first_found = Some(image);
88                } else {
89                    found_multiple = true;
90                }
91            }
92        }
93
94        // We are making sure that there was at least one image of the same size in flight
95        // Otherwise we might encounter UI thread lockups
96        if found_multiple {
97            first_found
98        } else {
99            None
100        }
101    }
102
103    /// Starts drawing to an image of the given size, which is either reused
104    /// or created by `factory`. Disposing the returned value presents the
105    /// image.
106    pub fn begin_draw_core(
107        &self,
108        factory: &(impl SwapchainImageFactory<TImage> + ?Sized),
109        size: PixelSize,
110    ) -> (Rc<dyn IDisposable>, Rc<TImage>) {
111        let img = match self.cleanup_and_find_next_image(size) {
112            Some(img) => img,
113            None => factory.create_image(size),
114        };
115
116        img.begin_draw();
117        {
118            let mut pending_images = self.pending_images.borrow_mut();
119            if let Some(index) = pending_images.iter().position(|pending| Rc::ptr_eq(pending, &img)) {
120                pending_images.remove(index);
121            }
122        }
123
124        let image = img.clone();
125        let pending_images = self.pending_images.clone();
126        let session = Disposable::create(move || {
127            img.present();
128            pending_images.borrow_mut().push(img);
129        });
130        (session, image)
131    }
132
133    /// Releases every pending image.
134    pub fn dispose(&self) {
135        let pending_images: Vec<Rc<TImage>> = self.pending_images.borrow().clone();
136        for img in pending_images {
137            img.dispose();
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use std::cell::Cell;
146
147    struct Image {
148        id: i32,
149        size: PixelSize,
150        last_present: Cell<Option<SwapchainImagePresentStatus>>,
151        begin_draws: Cell<i32>,
152        presents: Cell<i32>,
153        disposed: Cell<bool>,
154    }
155
156    impl ISwapchainImage for Image {
157        fn size(&self) -> PixelSize {
158            self.size
159        }
160
161        fn last_present(&self) -> Option<SwapchainImagePresentStatus> {
162            self.last_present.get()
163        }
164
165        fn begin_draw(&self) {
166            self.begin_draws.set(self.begin_draws.get() + 1);
167        }
168
169        fn present(&self) {
170            self.presents.set(self.presents.get() + 1);
171            self.last_present.set(Some(SwapchainImagePresentStatus::Pending));
172        }
173
174        fn dispose(&self) {
175            self.disposed.set(true);
176        }
177    }
178
179    #[derive(Default)]
180    struct Swapchain {
181        base: SwapchainBase<Image>,
182        created: RefCell<Vec<Rc<Image>>>,
183    }
184
185    impl SwapchainImageFactory<Image> for Swapchain {
186        fn create_image(&self, size: PixelSize) -> Rc<Image> {
187            let image = Rc::new(Image {
188                id: self.created.borrow().len() as i32,
189                size,
190                last_present: Cell::new(None),
191                begin_draws: Cell::new(0),
192                presents: Cell::new(0),
193                disposed: Cell::new(false),
194            });
195            self.created.borrow_mut().push(image.clone());
196            image
197        }
198    }
199
200    impl Swapchain {
201        fn begin_draw(&self, size: PixelSize) -> (Rc<dyn IDisposable>, Rc<Image>) {
202            self.base.begin_draw_core(self, size)
203        }
204
205        /// Draws and presents one frame; returns the id of the image used.
206        fn frame(&self, size: PixelSize) -> i32 {
207            let (session, image) = self.begin_draw(size);
208            session.dispose();
209            image.id
210        }
211
212        fn complete_all(&self) {
213            for image in self.created.borrow().iter() {
214                if image.last_present.get() == Some(SwapchainImagePresentStatus::Pending) {
215                    image.last_present.set(Some(SwapchainImagePresentStatus::RanToCompletion));
216                }
217            }
218        }
219
220        fn pending(&self) -> Vec<i32> {
221            self.base.pending_images.borrow().iter().map(|image| image.id).collect()
222        }
223    }
224
225    const SIZE: PixelSize = PixelSize::new(100, 50);
226
227    #[test]
228    fn draw_session_begins_draws_and_presents_on_dispose() {
229        let swapchain = Swapchain::default();
230        let (session, image) = swapchain.begin_draw(SIZE);
231        assert_eq!(image.size, SIZE);
232        assert_eq!((image.begin_draws.get(), image.presents.get()), (1, 0));
233        // The image being drawn to is not pending.
234        assert!(swapchain.pending().is_empty());
235
236        session.dispose();
237        assert_eq!(image.presents.get(), 1);
238        assert_eq!(swapchain.pending(), [0]);
239
240        // Disposing the session again does nothing.
241        session.dispose();
242        assert_eq!(image.presents.get(), 1);
243        assert_eq!(swapchain.pending(), [0]);
244    }
245
246    #[test]
247    fn images_are_not_reused_while_their_presentation_is_pending() {
248        let swapchain = Swapchain::default();
249        assert_eq!(swapchain.frame(SIZE), 0);
250        assert_eq!(swapchain.frame(SIZE), 1);
251        assert_eq!(swapchain.frame(SIZE), 2);
252        assert_eq!(swapchain.pending(), [0, 1, 2]);
253    }
254
255    #[test]
256    fn an_image_is_reused_only_when_another_ready_image_of_the_size_exists() {
257        let swapchain = Swapchain::default();
258        assert_eq!(swapchain.frame(SIZE), 0);
259        swapchain.complete_all();
260        // Only one ready image: a second one is created.
261        assert_eq!(swapchain.frame(SIZE), 1);
262        swapchain.complete_all();
263        // Two ready images: the most recently presented one is reused.
264        assert_eq!(swapchain.frame(SIZE), 1);
265        assert_eq!(swapchain.created.borrow().len(), 2);
266        assert_eq!(swapchain.pending(), [0, 1]);
267        assert_eq!(swapchain.created.borrow()[1].begin_draws.get(), 2);
268
269        // Image 1 is in flight again, so only image 0 is ready.
270        assert_eq!(swapchain.frame(SIZE), 2);
271        assert_eq!(swapchain.pending(), [0, 1, 2]);
272    }
273
274    #[test]
275    fn ready_images_of_another_size_are_disposed() {
276        let swapchain = Swapchain::default();
277        swapchain.frame(SIZE);
278        swapchain.frame(SIZE);
279        swapchain.complete_all();
280        // Still in flight when the size changes.
281        swapchain.frame(SIZE);
282
283        let other = PixelSize::new(200, 50);
284        assert_eq!(swapchain.frame(other), 2);
285        let created = swapchain.created.borrow().clone();
286        // Image 1 was reused for the third frame and is still pending.
287        assert!(created[0].disposed.get());
288        assert!(!created[1].disposed.get());
289        assert_eq!(swapchain.pending(), [1, 2]);
290
291        swapchain.complete_all();
292        assert_eq!(swapchain.frame(other), 3);
293        assert!(created[1].disposed.get());
294        assert_eq!(swapchain.pending(), [2, 3]);
295    }
296
297    #[test]
298    fn broken_images_are_disposed() {
299        let swapchain = Swapchain::default();
300        swapchain.frame(SIZE);
301        swapchain.frame(SIZE);
302        let created = swapchain.created.borrow().clone();
303        created[0].last_present.set(Some(SwapchainImagePresentStatus::Faulted));
304        created[1].last_present.set(Some(SwapchainImagePresentStatus::Canceled));
305
306        assert_eq!(swapchain.frame(SIZE), 2);
307        assert!(created[0].disposed.get());
308        // A canceled presentation is neither broken nor ready.
309        assert!(!created[1].disposed.get());
310        assert_eq!(swapchain.pending(), [1, 2]);
311    }
312
313    #[test]
314    fn dispose_releases_the_pending_images() {
315        let swapchain = Swapchain::default();
316        swapchain.frame(SIZE);
317        let (_session, drawing) = swapchain.begin_draw(SIZE);
318        swapchain.base.dispose();
319        let created = swapchain.created.borrow().clone();
320        assert!(created[0].disposed.get());
321        // The image of the open session is not pending.
322        assert!(!drawing.disposed.get());
323    }
324}