Skip to main content

ferroui_skia/
framebuffer_render_target.rs

1use crate::drawing_context_impl::{CreateInfo, DrawingContextImpl};
2use crate::skia_sharp_extensions::{to_sk_alpha_type, to_sk_color_type_or_panic};
3use ferroui_base::platform::surfaces::{IFramebufferPlatformSurface, IFramebufferRenderTarget};
4use ferroui_base::platform::{
5    IDrawingContextImpl, ILockedFramebuffer, IRenderTarget, PlatformRenderTargetState,
6    RenderTargetDrawingContextProperties, RenderTargetProperties, RenderTargetSceneInfo,
7};
8use crate::text_rendering_settings::{TextRenderingSettings, TextRenderingSource};
9use skia_safe::{surfaces, Bitmap, ColorType, ImageInfo, Surface, SurfaceProps};
10use std::cell::{Cell, RefCell};
11use std::rc::Rc;
12
13/// The surface over the locked framebuffer, kept between frames while the
14/// framebuffer stays the same.
15struct SurfaceState {
16    current_image_info: Option<ImageInfo>,
17    current_props: Option<SurfaceProps>,
18    current_framebuffer_address: *mut u8,
19    framebuffer_surface: Option<Surface>,
20    conversion_shim: Option<Rc<PixelFormatConversionShim>>,
21}
22
23/// Skia render target that renders to a framebuffer surface. No GPU
24/// acceleration available.
25pub struct FramebufferRenderTarget {
26    use_scaled_drawing: bool,
27    state: RefCell<SurfaceState>,
28    render_target: RefCell<Option<Rc<dyn IFramebufferRenderTarget>>>,
29    had_conversion_shim: Cell<bool>,
30    text_rendering: TextRenderingSource,
31    /// The text rendering of the frame drawn last, shared with the layers created for it.
32    frame_text_rendering: Rc<Cell<TextRenderingSettings>>,
33}
34
35impl FramebufferRenderTarget {
36    /// Creates a framebuffer render target for a framebuffer surface.
37    pub fn new(platform_surface: &dyn IFramebufferPlatformSurface, use_scaled_drawing: bool) -> Self {
38        Self::from_render_target(platform_surface.create_framebuffer_render_target(), use_scaled_drawing)
39    }
40
41    /// Creates a framebuffer render target over the render target of a
42    /// framebuffer surface.
43    pub fn from_render_target(render_target: Rc<dyn IFramebufferRenderTarget>, use_scaled_drawing: bool) -> Self {
44        Self {
45            use_scaled_drawing,
46            state: RefCell::new(SurfaceState {
47                current_image_info: None,
48                current_props: None,
49                current_framebuffer_address: std::ptr::null_mut(),
50                framebuffer_surface: None,
51                conversion_shim: None,
52            }),
53            render_target: RefCell::new(Some(render_target)),
54            had_conversion_shim: Cell::new(false),
55            text_rendering: TextRenderingSource::default(),
56            frame_text_rendering: Rc::default(),
57        }
58    }
59
60    /// The render target reading the text rendering parameters of the
61    /// platform from `text_rendering` every frame (the default: none, so
62    /// greyscale text; docs/porting/text-rendering.md).
63    pub fn with_text_rendering(mut self, text_rendering: TextRenderingSource) -> Self {
64        self.text_rendering = text_rendering;
65        self
66    }
67
68    /// Checks whether the given infos are compatible: a surface created for
69    /// one can be reused for the other.
70    fn are_image_infos_compatible(current_image_info: &ImageInfo, desired_image_info: &ImageInfo) -> bool {
71        current_image_info.width() == desired_image_info.width()
72            && current_image_info.height() == desired_image_info.height()
73            && current_image_info.color_type() == desired_image_info.color_type()
74    }
75
76    /// Creates the surface over the framebuffer if the current one is not
77    /// compatible or has other surface properties.
78    fn create_surface(&self, desired_image_info: &ImageInfo, props: &SurfaceProps, framebuffer: &dyn ILockedFramebuffer) {
79        let mut state = self.state.borrow_mut();
80        let address = framebuffer.address();
81
82        if state.framebuffer_surface.is_some()
83            && state
84                .current_image_info
85                .as_ref()
86                .is_some_and(|current| Self::are_image_infos_compatible(current, desired_image_info))
87            && state.current_props.as_ref() == Some(props)
88            && state.current_framebuffer_address == address
89        {
90            return;
91        }
92
93        Self::free_surface(&mut state);
94
95        state.current_framebuffer_address = address;
96
97        // A surface with a width/height of 0 is invalid and can't be created.
98        if desired_image_info.width() <= 0 || desired_image_info.height() <= 0 {
99            panic!(
100                "Unable to create a surface with size {}x{}",
101                desired_image_info.width(),
102                desired_image_info.height()
103            );
104        }
105
106        let row_bytes = framebuffer.row_bytes().max(0) as usize;
107        let mut surface = None;
108
109        if !address.is_null() && row_bytes >= desired_image_info.min_row_bytes() {
110            // SAFETY: the framebuffer contract guarantees `row_bytes * height`
111            // bytes of memory at `address` that stay valid until the
112            // framebuffer is disposed. The surface draws into that memory
113            // only through drawing contexts created while the framebuffer is
114            // locked; it is recreated when the address changes.
115            let pixels =
116                unsafe { std::slice::from_raw_parts_mut(address, row_bytes * desired_image_info.height() as usize) };
117
118            surface = surfaces::wrap_pixels(desired_image_info, pixels, row_bytes, Some(props))
119                // SAFETY: releasing the borrow turns the lifetime-bound
120                // surface into an owned one; the validity of the pixels is
121                // upheld as described above.
122                .map(|surface| unsafe { surface.release() });
123        }
124
125        // If the surface cannot be created, try to create a compatibility
126        // shim first.
127        if surface.is_none() {
128            let shim = Rc::new(PixelFormatConversionShim::new(desired_image_info.clone(), address, row_bytes, props));
129            surface = Some(shim.surface.clone());
130            state.conversion_shim = Some(shim);
131        }
132
133        state.framebuffer_surface = surface;
134        state.current_image_info = Some(desired_image_info.clone());
135        state.current_props = Some(*props);
136    }
137
138    /// Frees the surface over the framebuffer.
139    fn free_surface(state: &mut SurfaceState) {
140        state.conversion_shim = None;
141        state.framebuffer_surface = None;
142        state.current_props = None;
143        state.current_framebuffer_address = std::ptr::null_mut();
144    }
145}
146
147impl IRenderTarget for FramebufferRenderTarget {
148    fn properties(&self) -> RenderTargetProperties {
149        RenderTargetProperties {
150            retains_previous_frame_contents: !self.had_conversion_shim.get()
151                && self.render_target.borrow().as_ref().is_some_and(|target| target.retains_frame_contents()),
152            is_suitable_for_direct_rendering: true,
153        }
154    }
155
156    fn create_drawing_context(
157        &self,
158        scene_info: &RenderTargetSceneInfo,
159    ) -> (Box<dyn IDrawingContextImpl>, RenderTargetDrawingContextProperties) {
160        let render_target =
161            self.render_target.borrow().clone().unwrap_or_else(|| panic!("FramebufferRenderTarget has been disposed"));
162
163        let (framebuffer, lock_properties) = render_target.lock(scene_info);
164        let size = framebuffer.size();
165        let framebuffer_image_info = ImageInfo::new(
166            (size.width, size.height),
167            to_sk_color_type_or_panic(framebuffer.format()),
168            to_sk_alpha_type(framebuffer.alpha_format()),
169            None,
170        );
171
172        let text_rendering = self.text_rendering.settings(scene_info.scaling, scene_info.transparency_level);
173        self.frame_text_rendering.set(text_rendering);
174        self.create_surface(&framebuffer_image_info, &text_rendering.surface_props(), &*framebuffer);
175
176        let (mut surface, conversion_shim) = {
177            let state = self.state.borrow();
178            (state.framebuffer_surface.clone().expect("the surface was just created"), state.conversion_shim.clone())
179        };
180
181        self.had_conversion_shim.set(self.had_conversion_shim.get() || conversion_shim.is_some());
182
183        {
184            let canvas = surface.canvas();
185            canvas.restore_to_count(1);
186            canvas.save();
187            canvas.reset_matrix();
188        }
189
190        let create_info = CreateInfo {
191            surface: Some(surface),
192            dpi: framebuffer.dpi(),
193            scale_drawing_to_dpi: self.use_scaled_drawing,
194            text_rendering,
195            frame_text_rendering: Some(self.frame_text_rendering.clone()),
196            ..CreateInfo::default()
197        };
198
199        let properties = RenderTargetDrawingContextProperties {
200            previous_frame_is_retained: !self.had_conversion_shim.get() && lock_properties.previous_frame_is_retained,
201        };
202
203        let mut disposables: Vec<Box<dyn FnOnce()>> = Vec::with_capacity(2);
204        if let Some(shim) = conversion_shim {
205            disposables.push(Box::new(move || shim.copy_surface()));
206        }
207        disposables.push(Box::new(move || framebuffer.dispose()));
208
209        (Box::new(DrawingContextImpl::new(create_info, disposables)), properties)
210    }
211
212    fn platform_render_target_state(&self) -> PlatformRenderTargetState {
213        match &*self.render_target.borrow() {
214            Some(render_target) => render_target.state(),
215            None => PlatformRenderTargetState::DISPOSED,
216        }
217    }
218
219    fn dispose(&self) {
220        if let Some(render_target) = self.render_target.borrow_mut().take() {
221            render_target.dispose();
222        }
223        Self::free_surface(&mut self.state.borrow_mut());
224    }
225}
226
227/// Converts a non-compatible pixel format using an intermediate surface: the
228/// frame is drawn into a surface in the platform's native format and copied
229/// (with conversion) into the framebuffer when the frame is finished.
230struct PixelFormatConversionShim {
231    surface: Surface,
232    destination_info: ImageInfo,
233    framebuffer_address: *mut u8,
234    framebuffer_row_bytes: usize,
235}
236
237impl PixelFormatConversionShim {
238    fn new(
239        destination_info: ImageInfo,
240        framebuffer_address: *mut u8,
241        framebuffer_row_bytes: usize,
242        props: &SurfaceProps,
243    ) -> Self {
244        // Create the surface using the default platform settings.
245        if !Self::can_copy_to(destination_info.color_type()) {
246            panic!(
247                "Unable to create pixel format shim for conversion from {:?} to {:?}",
248                ColorType::N32,
249                destination_info.color_type()
250            );
251        }
252
253        let info = ImageInfo::new_n32_premul(destination_info.dimensions(), None);
254        let surface = surfaces::raster(&info, None, Some(props)).unwrap_or_else(|| {
255            panic!(
256                "Unable to create pixel format shim surface for conversion from {:?} to {:?}",
257                ColorType::N32,
258                destination_info.color_type()
259            )
260        });
261
262        Self { surface, destination_info, framebuffer_address, framebuffer_row_bytes }
263    }
264
265    /// Whether pixels can be converted to the given color type.
266    fn can_copy_to(color_type: ColorType) -> bool {
267        if color_type == ColorType::Unknown {
268            return false;
269        }
270
271        let info = ImageInfo::new_n32_premul((1, 1), None).with_color_type(color_type);
272        Bitmap::new().try_alloc_pixels_info(&info, None)
273    }
274
275    /// Copies the frame into the framebuffer, converting the pixel format.
276    fn copy_surface(&self) {
277        if self.framebuffer_address.is_null() {
278            return;
279        }
280
281        let row_bytes = self.framebuffer_row_bytes.max(self.destination_info.min_row_bytes());
282
283        // SAFETY: called while the framebuffer the shim was created for is
284        // still locked (the copy runs before the framebuffer is disposed),
285        // so `row_bytes * height` bytes at the address are valid.
286        let pixels = unsafe {
287            std::slice::from_raw_parts_mut(
288                self.framebuffer_address,
289                row_bytes * self.destination_info.height() as usize,
290            )
291        };
292
293        self.surface.clone().read_pixels(&self.destination_info, pixels, row_bytes, (0, 0));
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300    use skia_safe::{AlphaType, Color};
301
302    #[test]
303    fn conversion_shim_copies_the_frame_in_the_destination_format() {
304        // The frame is drawn in the native format and converted to
305        // unpremultiplied RGBA when it is copied to the framebuffer.
306        let destination_info = ImageInfo::new((4, 4), ColorType::RGBA8888, AlphaType::Unpremul, None);
307        let row_bytes = 4 * 4 + 4;
308        let mut framebuffer = vec![0u8; row_bytes * 4];
309
310        let shim = PixelFormatConversionShim::new(
311            destination_info,
312            framebuffer.as_mut_ptr(),
313            row_bytes,
314            &SurfaceProps::default(),
315        );
316        shim.surface.clone().canvas().clear(Color::from_argb(128, 255, 0, 0));
317        shim.copy_surface();
318        drop(shim);
319
320        for y in 0..4 {
321            let row = &framebuffer[y * row_bytes..];
322            assert_eq!([255, 0, 0, 128], row[4..8]);
323            // The padding after the row is untouched.
324            assert_eq!([0, 0, 0, 0], row[16..20]);
325        }
326    }
327
328    #[test]
329    fn conversion_shim_rejects_unknown_color_types() {
330        assert!(!PixelFormatConversionShim::can_copy_to(ColorType::Unknown));
331        assert!(PixelFormatConversionShim::can_copy_to(ColorType::RGB565));
332    }
333}