From 39b27bbfb19e0c7521d8fe3a3861e4d9eff6f4e5 Mon Sep 17 00:00:00 2001 From: Simon Hausmann Date: Wed, 22 Jan 2025 15:14:20 +0100 Subject: [PATCH] Add support for triple buffered partial rendering with Skia --- api/rs/slint/tests/partial_renderer.rs | 4 -- internal/core/item_rendering.rs | 38 ++++++---------- internal/core/software_renderer.rs | 55 ++++++++++++++++++++---- internal/renderers/skia/lib.rs | 37 ++++++++++++---- internal/renderers/skia/metal_surface.rs | 6 --- 5 files changed, 87 insertions(+), 53 deletions(-) diff --git a/api/rs/slint/tests/partial_renderer.rs b/api/rs/slint/tests/partial_renderer.rs index 5ef16a2c25e..69f03df102f 100644 --- a/api/rs/slint/tests/partial_renderer.rs +++ b/api/rs/slint/tests/partial_renderer.rs @@ -439,10 +439,6 @@ fn rotated_image() { window.set_size(slint::PhysicalSize::new(250, 250).into()); ui.show().unwrap(); - assert!(window.draw_if_needed()); - // Redraw once more, to work around set_repaint_buffer_type clearing the rendering cache - // because the buffer age changed from 0 to 1. - window.request_redraw(); assert!(window.draw_if_needed()); assert_eq!( window.last_dirty_region_bounding_box_size(), diff --git a/internal/core/item_rendering.rs b/internal/core/item_rendering.rs index cf9c65c7b4c..205cf0603d6 100644 --- a/internal/core/item_rendering.rs +++ b/internal/core/item_rendering.rs @@ -1038,29 +1038,11 @@ pub struct PartialRenderingState { partial_cache: RefCell, /// This is the area which we are going to redraw in the next frame, no matter if the items are dirty or not force_dirty: RefCell, - repaint_buffer_type: Cell, - /// This is the area which was dirty on the previous frame. - /// Only used if repaint_buffer_type == RepaintBufferType::SwappedBuffers - prev_frame_dirty: Cell, /// Force a redraw in the next frame, no matter what's dirty. Use only as a last resort. force_screen_refresh: Cell, } impl PartialRenderingState { - /// Sets the repaint type of the back buffer used for the next rendering. This helps to compute the partial - /// rendering region correctly, for example when using swapped buffers, the region will include the dirty region - /// of the previous frame. - pub fn set_repaint_buffer_type(&self, repaint_buffer_type: RepaintBufferType) { - if self.repaint_buffer_type.replace(repaint_buffer_type) != repaint_buffer_type { - self.partial_cache.borrow_mut().clear(); - } - } - - /// Returns the current repaint buffer type. - pub fn repaint_buffer_type(&self) -> RepaintBufferType { - self.repaint_buffer_type.get() - } - /// Creates a partial renderer that's initialized with the partial rendering caches maintained in this state structure. /// Call [`Self::apply_dirty_region`] after this function to compute the correct partial rendering region. pub fn create_partial_renderer<'a, T: ItemRenderer + ItemRendererFeatures>( @@ -1071,13 +1053,16 @@ impl PartialRenderingState { } /// Compute the correct partial rendering region based on the components to be drawn, the bounding rectangles of - /// changes items within, and the current repaint buffer type. + /// changes items within, and the current repaint buffer type. Returns the computed dirty region just for this frame. + /// The provided buffer_dirty_region specifies which area of the buffer is known to *additionally* require repainting, + /// where `None` means that buffer is not known to be dirty beyond what applies to this frame (reused buffer). pub fn apply_dirty_region( &self, partial_renderer: &mut PartialRenderer<'_, T>, components: &[(&ItemTreeRc, LogicalPoint)], logical_window_size: LogicalSize, - ) { + dirty_region_of_existing_buffer: Option, + ) -> DirtyRegion { for (component, origin) in components { partial_renderer.compute_dirty_regions(component, *origin, logical_window_size); } @@ -1088,14 +1073,15 @@ impl PartialRenderingState { partial_renderer.dirty_region = screen_region.into(); } - partial_renderer.dirty_region = match self.repaint_buffer_type.get() { - RepaintBufferType::NewBuffer => screen_region.into(), - RepaintBufferType::ReusedBuffer => partial_renderer.dirty_region.clone(), - RepaintBufferType::SwappedBuffers => partial_renderer - .dirty_region - .union(&self.prev_frame_dirty.replace(partial_renderer.dirty_region.clone())), + let region_to_repaint = partial_renderer.dirty_region.clone(); + + partial_renderer.dirty_region = match dirty_region_of_existing_buffer { + Some(dirty_region) => partial_renderer.dirty_region.union(&dirty_region), + None => partial_renderer.dirty_region.clone(), } .intersection(screen_region); + + region_to_repaint } /// Add the specified region to the list of regions to include in the next rendering. diff --git a/internal/core/software_renderer.rs b/internal/core/software_renderer.rs index fb1f96d3b81..5dcc2db99c8 100644 --- a/internal/core/software_renderer.rs +++ b/internal/core/software_renderer.rs @@ -377,6 +377,10 @@ fn region_line_ranges( /// is only useful if the device does not have enough memory to render the whole window /// in one single buffer pub struct SoftwareRenderer { + repaint_buffer_type: Cell, + /// This is the area which was dirty on the previous frame. + /// Only used if repaint_buffer_type == RepaintBufferType::SwappedBuffers + prev_frame_dirty: Cell, partial_rendering_state: PartialRenderingState, maybe_window_adapter: RefCell>>, rotation: Cell, @@ -387,9 +391,11 @@ impl Default for SoftwareRenderer { fn default() -> Self { Self { partial_rendering_state: Default::default(), + prev_frame_dirty: Default::default(), maybe_window_adapter: Default::default(), rotation: Default::default(), rendering_metrics_collector: RenderingMetricsCollector::new("software"), + repaint_buffer_type: Default::default(), } } } @@ -405,7 +411,7 @@ impl SoftwareRenderer { /// The `repaint_buffer_type` parameter specify what kind of buffer are passed to [`Self::render`] pub fn new_with_repaint_buffer_type(repaint_buffer_type: RepaintBufferType) -> Self { let self_ = Self::default(); - self_.partial_rendering_state.set_repaint_buffer_type(repaint_buffer_type); + self_.repaint_buffer_type.set(repaint_buffer_type); self_ } @@ -413,12 +419,14 @@ impl SoftwareRenderer { /// /// This may clear the internal caches pub fn set_repaint_buffer_type(&self, repaint_buffer_type: RepaintBufferType) { - self.partial_rendering_state.set_repaint_buffer_type(repaint_buffer_type); + if self.repaint_buffer_type.replace(repaint_buffer_type) != repaint_buffer_type { + self.partial_rendering_state.clear_cache(); + } } /// Returns the kind of buffer that must be passed to [`Self::render`] pub fn repaint_buffer_type(&self) -> RepaintBufferType { - self.partial_rendering_state.repaint_buffer_type() + self.repaint_buffer_type.get() } /// Set how the window need to be rotated in the buffer. @@ -497,11 +505,26 @@ impl SoftwareRenderer { window_inner .draw_contents(|components| { let logical_size = (size.cast() / factor).cast(); - self.partial_rendering_state.apply_dirty_region( + + let dirty_region_of_existing_buffer = match self.repaint_buffer_type.get() { + RepaintBufferType::NewBuffer => { + Some(LogicalRect::from_size(logical_size).into()) + } + RepaintBufferType::ReusedBuffer => None, + RepaintBufferType::SwappedBuffers => Some(self.prev_frame_dirty.take()), + }; + + let dirty_region_for_this_frame = self.partial_rendering_state.apply_dirty_region( &mut renderer, components, logical_size, + dirty_region_of_existing_buffer, ); + + if self.repaint_buffer_type.get() == RepaintBufferType::SwappedBuffers { + self.prev_frame_dirty.set(dirty_region_for_this_frame); + } + let rotation = RotationInfo { orientation: rotation, screen_size: size }; let screen_rect = PhysicalRect::from_size(size); let mut i = renderer.dirty_region.iter().filter_map(|r| { @@ -973,11 +996,25 @@ fn prepare_scene( let mut dirty_region = PhysicalRegion::default(); window.draw_contents(|components| { let logical_size = (size.cast() / factor).cast(); - software_renderer.partial_rendering_state.apply_dirty_region( - &mut renderer, - components, - logical_size, - ); + + let dirty_region_of_existing_buffer = match software_renderer.repaint_buffer_type.get() { + RepaintBufferType::NewBuffer => Some(LogicalRect::from_size(logical_size).into()), + RepaintBufferType::ReusedBuffer => None, + RepaintBufferType::SwappedBuffers => Some(software_renderer.prev_frame_dirty.take()), + }; + + let dirty_region_for_this_frame = + software_renderer.partial_rendering_state.apply_dirty_region( + &mut renderer, + components, + logical_size, + dirty_region_of_existing_buffer, + ); + + if software_renderer.repaint_buffer_type.get() == RepaintBufferType::SwappedBuffers { + software_renderer.prev_frame_dirty.set(dirty_region_for_this_frame); + } + let rotation = RotationInfo { orientation: software_renderer.rotation.get(), screen_size: size }; let screen_rect = PhysicalRect::from_size(size); diff --git a/internal/renderers/skia/lib.rs b/internal/renderers/skia/lib.rs index de24106eadd..d7fd4a79e11 100644 --- a/internal/renderers/skia/lib.rs +++ b/internal/renderers/skia/lib.rs @@ -14,7 +14,6 @@ use i_slint_core::api::{ use i_slint_core::graphics::euclid::{self, Vector2D}; use i_slint_core::graphics::rendering_metrics_collector::RenderingMetricsCollector; use i_slint_core::graphics::{BorderRadius, FontRequest, RequestedGraphicsAPI, SharedPixelBuffer}; -use i_slint_core::item_rendering::RepaintBufferType; use i_slint_core::item_rendering::{DirtyRegion, ItemCache, ItemRenderer, PartialRenderingState}; use i_slint_core::lengths::{ LogicalLength, LogicalPoint, LogicalRect, LogicalSize, PhysicalPx, ScaleFactor, @@ -120,6 +119,8 @@ pub struct SkiaRenderer { pre_present_callback: RefCell>>, partial_rendering_state: Option, visualize_dirty_region: bool, + /// Tracking dirty regions indexed by buffer age - 1. More than 3 back buffers aren't supported, but also unlikely to happen. + dirty_region_history: RefCell<[DirtyRegion; 3]>, } impl Default for SkiaRenderer { @@ -137,6 +138,7 @@ impl Default for SkiaRenderer { pre_present_callback: Default::default(), partial_rendering_state, visualize_dirty_region, + dirty_region_history: Default::default(), } } } @@ -166,6 +168,7 @@ impl SkiaRenderer { pre_present_callback: Default::default(), partial_rendering_state, visualize_dirty_region, + dirty_region_history: Default::default(), } } @@ -193,6 +196,7 @@ impl SkiaRenderer { pre_present_callback: Default::default(), partial_rendering_state, visualize_dirty_region, + dirty_region_history: Default::default(), } } @@ -220,6 +224,7 @@ impl SkiaRenderer { pre_present_callback: Default::default(), partial_rendering_state, visualize_dirty_region, + dirty_region_history: Default::default(), } } @@ -247,6 +252,7 @@ impl SkiaRenderer { pre_present_callback: Default::default(), partial_rendering_state, visualize_dirty_region, + dirty_region_history: Default::default(), } } @@ -274,6 +280,7 @@ impl SkiaRenderer { pre_present_callback: Default::default(), partial_rendering_state, visualize_dirty_region, + dirty_region_history: Default::default(), } } @@ -308,6 +315,7 @@ impl SkiaRenderer { pre_present_callback: Default::default(), partial_rendering_state, visualize_dirty_region, + dirty_region_history: Default::default(), } } @@ -501,19 +509,29 @@ impl SkiaRenderer { let mut dirty_region_to_visualize = None; if let Some(partial_rendering_state) = self.partial_rendering_state.as_ref() { - partial_rendering_state.set_repaint_buffer_type(match back_buffer_age { - 1 => RepaintBufferType::ReusedBuffer, - 2 => RepaintBufferType::SwappedBuffers, - _ => RepaintBufferType::NewBuffer, - }); - partial_renderer = partial_rendering_state.create_partial_renderer(skia_item_renderer); - partial_rendering_state.apply_dirty_region( + let mut dirty_region_history = self.dirty_region_history.borrow_mut(); + + let buffer_dirty_region = if back_buffer_age > 0 + && back_buffer_age as usize - 1 < dirty_region_history.len() + { + // The dirty region is the union of all the previous dirty regions + Some( + dirty_region_history[0..back_buffer_age as usize - 1] + .iter() + .fold(DirtyRegion::default(), |acc, region| acc.union(®ion)), + ) + } else { + Some(LogicalRect::from_size(logical_window_size).into()) + }; + + let dirty_region_for_this_frame = partial_rendering_state.apply_dirty_region( &mut partial_renderer, components, logical_window_size, + buffer_dirty_region, ); let mut clip_path = skia_safe::Path::new(); @@ -525,6 +543,9 @@ impl SkiaRenderer { dirty_region = partial_renderer.dirty_region.clone().into(); + dirty_region_history.rotate_right(1); + dirty_region_history[0] = dirty_region_for_this_frame; + skia_canvas.clip_path(&clip_path, None, false); if self.visualize_dirty_region { diff --git a/internal/renderers/skia/metal_surface.rs b/internal/renderers/skia/metal_surface.rs index 5837a9b30d9..45399a3ecc6 100644 --- a/internal/renderers/skia/metal_surface.rs +++ b/internal/renderers/skia/metal_surface.rs @@ -67,12 +67,6 @@ impl super::Surface for MetalSurface { ca_layer.setPresentsWithTransaction(false); ca_layer.setDrawableSize(CGSize::new(size.width as f64, size.height as f64)); - - // When partial rendering is enabled, we need to set the maximum drawable count to 2 to avoid triple - // buffering. Triple buffering is not supported by the partial renderer. - if std::env::var("SLINT_SKIA_PARTIAL_RENDERING").is_ok() { - ca_layer.setMaximumDrawableCount(2); - } } let flipped = ca_layer.contentsAreFlipped();