Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for triple buffered partial rendering with Skia #7440

Merged
merged 1 commit into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions api/rs/slint/tests/partial_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
38 changes: 12 additions & 26 deletions internal/core/item_rendering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1038,29 +1038,11 @@ pub struct PartialRenderingState {
partial_cache: RefCell<PartialRenderingCache>,
/// 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<DirtyRegion>,
repaint_buffer_type: Cell<RepaintBufferType>,
/// This is the area which was dirty on the previous frame.
/// Only used if repaint_buffer_type == RepaintBufferType::SwappedBuffers
prev_frame_dirty: Cell<DirtyRegion>,
/// Force a redraw in the next frame, no matter what's dirty. Use only as a last resort.
force_screen_refresh: Cell<bool>,
}

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>(
Expand All @@ -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<T: ItemRenderer + ItemRendererFeatures>(
&self,
partial_renderer: &mut PartialRenderer<'_, T>,
components: &[(&ItemTreeRc, LogicalPoint)],
logical_window_size: LogicalSize,
) {
dirty_region_of_existing_buffer: Option<DirtyRegion>,
) -> DirtyRegion {
for (component, origin) in components {
partial_renderer.compute_dirty_regions(component, *origin, logical_window_size);
}
Expand All @@ -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.
Expand Down
55 changes: 46 additions & 9 deletions internal/core/software_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RepaintBufferType>,
/// This is the area which was dirty on the previous frame.
/// Only used if repaint_buffer_type == RepaintBufferType::SwappedBuffers
prev_frame_dirty: Cell<DirtyRegion>,
partial_rendering_state: PartialRenderingState,
maybe_window_adapter: RefCell<Option<Weak<dyn crate::window::WindowAdapter>>>,
rotation: Cell<RenderingRotation>,
Expand All @@ -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(),
}
}
}
Expand All @@ -405,20 +411,22 @@ 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_
}

/// Change the what kind of buffer is being passed to [`Self::render`]
///
/// 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.
Expand Down Expand Up @@ -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| {
Expand Down Expand Up @@ -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);
Expand Down
37 changes: 29 additions & 8 deletions internal/renderers/skia/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -120,6 +119,8 @@ pub struct SkiaRenderer {
pre_present_callback: RefCell<Option<Box<dyn FnMut()>>>,
partial_rendering_state: Option<PartialRenderingState>,
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 {
Expand All @@ -137,6 +138,7 @@ impl Default for SkiaRenderer {
pre_present_callback: Default::default(),
partial_rendering_state,
visualize_dirty_region,
dirty_region_history: Default::default(),
}
}
}
Expand Down Expand Up @@ -166,6 +168,7 @@ impl SkiaRenderer {
pre_present_callback: Default::default(),
partial_rendering_state,
visualize_dirty_region,
dirty_region_history: Default::default(),
}
}

Expand Down Expand Up @@ -193,6 +196,7 @@ impl SkiaRenderer {
pre_present_callback: Default::default(),
partial_rendering_state,
visualize_dirty_region,
dirty_region_history: Default::default(),
}
}

Expand Down Expand Up @@ -220,6 +224,7 @@ impl SkiaRenderer {
pre_present_callback: Default::default(),
partial_rendering_state,
visualize_dirty_region,
dirty_region_history: Default::default(),
}
}

Expand Down Expand Up @@ -247,6 +252,7 @@ impl SkiaRenderer {
pre_present_callback: Default::default(),
partial_rendering_state,
visualize_dirty_region,
dirty_region_history: Default::default(),
}
}

Expand Down Expand Up @@ -274,6 +280,7 @@ impl SkiaRenderer {
pre_present_callback: Default::default(),
partial_rendering_state,
visualize_dirty_region,
dirty_region_history: Default::default(),
}
}

Expand Down Expand Up @@ -308,6 +315,7 @@ impl SkiaRenderer {
pre_present_callback: Default::default(),
partial_rendering_state,
visualize_dirty_region,
dirty_region_history: Default::default(),
}
}

Expand Down Expand Up @@ -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(&region)),
)
} 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();
Expand All @@ -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 {
Expand Down
6 changes: 0 additions & 6 deletions internal/renderers/skia/metal_surface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading