From b56abbe23104d13bcc093df43046039a6362f813 Mon Sep 17 00:00:00 2001 From: Olivier Goffart Date: Fri, 19 Sep 2025 14:18:45 +0200 Subject: [PATCH 1/8] core: Refactor the partial renderer in its own module --- api/rs/slint/tests/partial_renderer.rs | 4 +- internal/backends/linuxkms/renderer/skia.rs | 3 +- internal/core/item_rendering.rs | 757 +------------------ internal/core/lib.rs | 1 + internal/core/partial_renderer.rs | 759 ++++++++++++++++++++ internal/core/renderer.rs | 2 +- internal/core/software_renderer.rs | 8 +- internal/renderers/skia/d3d_surface.rs | 2 +- internal/renderers/skia/lib.rs | 5 +- internal/renderers/skia/metal_surface.rs | 2 +- internal/renderers/skia/opengl_surface.rs | 2 +- internal/renderers/skia/software_surface.rs | 2 +- internal/renderers/skia/vulkan_surface.rs | 2 +- internal/renderers/skia/wgpu_26_surface.rs | 2 +- internal/renderers/skia/wgpu_27_surface.rs | 2 +- 15 files changed, 791 insertions(+), 762 deletions(-) create mode 100644 internal/core/partial_renderer.rs diff --git a/api/rs/slint/tests/partial_renderer.rs b/api/rs/slint/tests/partial_renderer.rs index fac85dc8920..5f77bb63129 100644 --- a/api/rs/slint/tests/partial_renderer.rs +++ b/api/rs/slint/tests/partial_renderer.rs @@ -138,7 +138,7 @@ impl WindowAdapter for SkiaTestWindow { #[derive(Default)] struct SkiaTestSoftwareBuffer { pixels: RefCell>>, - last_dirty_region: RefCell>, + last_dirty_region: RefCell>, } impl i_slint_renderer_skia::software_surface::RenderBuffer for SkiaTestSoftwareBuffer { @@ -153,7 +153,7 @@ impl i_slint_renderer_skia::software_surface::RenderBuffer for SkiaTestSoftwareB u8, &mut [u8], ) -> Result< - Option, + Option, i_slint_core::platform::PlatformError, >, ) -> Result<(), i_slint_core::platform::PlatformError> { diff --git a/internal/backends/linuxkms/renderer/skia.rs b/internal/backends/linuxkms/renderer/skia.rs index 728215526d7..816759a7645 100644 --- a/internal/backends/linuxkms/renderer/skia.rs +++ b/internal/backends/linuxkms/renderer/skia.rs @@ -6,7 +6,8 @@ use std::sync::Arc; use crate::display::RenderingRotation; use crate::drmoutput::DrmOutput; use i_slint_core::api::{PhysicalSize as PhysicalWindowSize, Window}; -use i_slint_core::item_rendering::{DirtyRegion, ItemRenderer}; +use i_slint_core::item_rendering::ItemRenderer; +use i_slint_core::partial_renderer::DirtyRegion; use i_slint_core::platform::PlatformError; use i_slint_renderer_skia::SkiaRendererExt; use i_slint_renderer_skia::{skia_safe, SkiaRenderer, SkiaSharedContext}; diff --git a/internal/core/item_rendering.rs b/internal/core/item_rendering.rs index df5b043799e..4b549f2335f 100644 --- a/internal/core/item_rendering.rs +++ b/internal/core/item_rendering.rs @@ -4,67 +4,27 @@ #![warn(missing_docs)] //! module for rendering the tree of items -use super::graphics::RenderingCache; use super::items::*; -use crate::graphics::{CachedGraphicsData, FontRequest, Image, IntRect}; +use crate::graphics::{FontRequest, Image, IntRect}; use crate::item_tree::ItemTreeRc; -use crate::item_tree::{ItemVisitor, ItemVisitorResult, ItemVisitorVTable, VisitChildrenResult}; +use crate::item_tree::{ItemVisitor, ItemVisitorVTable, VisitChildrenResult}; use crate::lengths::{ - ItemTransform, LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalPx, LogicalRect, - LogicalSize, LogicalVector, SizeLengths, + LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, LogicalVector, + SizeLengths, }; -use crate::properties::PropertyTracker; +pub use crate::partial_renderer::CachedRenderingData; use crate::window::{WindowAdapter, WindowInner}; -use crate::{Brush, Coord, SharedString}; +use crate::{Brush, SharedString}; +#[cfg(feature = "std")] use alloc::boxed::Box; use alloc::rc::Rc; -use core::cell::{Cell, RefCell}; +#[cfg(feature = "std")] +use core::cell::RefCell; use core::pin::Pin; #[cfg(feature = "std")] use std::collections::HashMap; use vtable::VRc; -/// This structure must be present in items that are Rendered and contains information. -/// Used by the backend. -#[derive(Default, Debug)] -#[repr(C)] -pub struct CachedRenderingData { - /// Used and modified by the backend, should be initialized to 0 by the user code - pub(crate) cache_index: Cell, - /// Used and modified by the backend, should be initialized to 0 by the user code. - /// The backend compares this generation against the one of the cache to verify - /// the validity of the cache_index field. - pub(crate) cache_generation: Cell, -} - -impl CachedRenderingData { - /// This function can be used to remove an entry from the rendering cache for a given item, if it - /// exists, i.e. if any data was ever cached. This is typically called by the graphics backend's - /// implementation of the release_item_graphics_cache function. - pub fn release(&self, cache: &mut RenderingCache) -> Option { - if self.cache_generation.get() == cache.generation() { - let index = self.cache_index.get(); - self.cache_generation.set(0); - Some(cache.remove(index).data) - } else { - None - } - } - - /// Return the value if it is in the cache - pub fn get_entry<'a, T>( - &self, - cache: &'a mut RenderingCache, - ) -> Option<&'a mut crate::graphics::CachedGraphicsData> { - let index = self.cache_index.get(); - if self.cache_generation.get() == cache.generation() { - cache.get_mut(index) - } else { - None - } - } -} - /// A per-item cache. /// /// Cache rendering information for a given item. @@ -76,9 +36,9 @@ impl CachedRenderingData { #[cfg(feature = "std")] pub struct ItemCache { /// The pointer is a pointer to a component - map: RefCell>>>, + map: RefCell>>>, /// Track if the window scale factor changes; used to clear the cache if necessary. - window_scale_factor_tracker: Pin>, + window_scale_factor_tracker: Pin>, } #[cfg(feature = "std")] @@ -116,7 +76,7 @@ impl ItemCache { } std::collections::hash_map::Entry::Vacant(_) => { drop(borrowed); - let new_entry = CachedGraphicsData::new(update_fn); + let new_entry = crate::graphics::CachedGraphicsData::new(update_fn); let data = new_entry.data.clone(); self.map .borrow_mut() @@ -571,696 +531,3 @@ pub trait ItemRendererFeatures { /// The renderer supports applying 2D transformations to items. const SUPPORTS_TRANSFORMATIONS: bool; } - -/// After rendering an item, we cache the geometry and the transform it applies to -/// children. -#[derive(Clone)] - -pub enum CachedItemBoundingBoxAndTransform { - /// A regular item with a translation - RegularItem { - /// The item's bounding rect relative to its parent. - bounding_rect: LogicalRect, - /// The item's offset relative to its parent. - offset: LogicalVector, - }, - /// An item such as Rotate that defines an additional transformation - ItemWithTransform { - /// The item's bounding rect relative to its parent. - bounding_rect: LogicalRect, - /// The item's transform to apply to children. - transform: Box, - }, - /// A clip item. - ClipItem { - /// The item's geometry relative to its parent. - geometry: LogicalRect, - }, -} - -impl CachedItemBoundingBoxAndTransform { - fn bounding_rect(&self) -> &LogicalRect { - match self { - CachedItemBoundingBoxAndTransform::RegularItem { bounding_rect, .. } => bounding_rect, - CachedItemBoundingBoxAndTransform::ItemWithTransform { bounding_rect, .. } => { - bounding_rect - } - CachedItemBoundingBoxAndTransform::ClipItem { geometry } => geometry, - } - } - - fn transform(&self) -> ItemTransform { - match self { - CachedItemBoundingBoxAndTransform::RegularItem { offset, .. } => { - ItemTransform::translation(offset.x as f32, offset.y as f32) - } - CachedItemBoundingBoxAndTransform::ItemWithTransform { transform, .. } => **transform, - CachedItemBoundingBoxAndTransform::ClipItem { geometry } => { - ItemTransform::translation(geometry.origin.x as f32, geometry.origin.y as f32) - } - } - } - - fn new( - item_rc: &ItemRc, - window_adapter: &Rc, - ) -> Self { - let geometry = item_rc.geometry(); - - if item_rc.borrow().as_ref().clips_children() { - return Self::ClipItem { geometry }; - } - - // Evaluate the bounding rect untracked, as properties that affect the bounding rect are already tracked - // at rendering time. - let bounding_rect = crate::properties::evaluate_no_tracking(|| { - item_rc.bounding_rect(&geometry, window_adapter) - }); - - if let Some(complex_child_transform) = (T::SUPPORTS_TRANSFORMATIONS - && window_adapter.renderer().supports_transformations()) - .then(|| item_rc.children_transform()) - .flatten() - { - Self::ItemWithTransform { - bounding_rect, - transform: complex_child_transform - .then_translate(geometry.origin.to_vector().cast()) - .into(), - } - } else { - Self::RegularItem { bounding_rect, offset: geometry.origin.to_vector() } - } - } -} - -/// The cache that needs to be held by the Window for the partial rendering -pub type PartialRenderingCache = RenderingCache; - -/// A region composed of a few rectangles that need to be redrawn. -#[derive(Default, Clone, Debug)] -pub struct DirtyRegion { - rectangles: [euclid::Box2D; Self::MAX_COUNT], - count: usize, -} - -impl DirtyRegion { - /// The maximum number of rectangles that can be stored in a DirtyRegion - pub(crate) const MAX_COUNT: usize = 3; - - /// An iterator over the part of the region (they can overlap) - pub fn iter(&self) -> impl Iterator> + '_ { - (0..self.count).map(|x| self.rectangles[x]) - } - - /// Add a rectangle to the region. - /// - /// Note that if the region becomes too complex, it might be simplified by being bigger than the actual union. - pub fn add_rect(&mut self, rect: LogicalRect) { - self.add_box(rect.to_box2d()); - } - - /// Add a box to the region - /// - /// Note that if the region becomes too complex, it might be simplified by being bigger than the actual union. - pub fn add_box(&mut self, b: euclid::Box2D) { - if b.is_empty() { - return; - } - let mut i = 0; - while i < self.count { - let r = &self.rectangles[i]; - if r.contains_box(&b) { - // the rectangle is already in the union - return; - } else if b.contains_box(r) { - self.rectangles.swap(i, self.count - 1); - self.count -= 1; - continue; - } - i += 1; - } - - if self.count < Self::MAX_COUNT { - self.rectangles[self.count] = b; - self.count += 1; - } else { - let best_merge = (0..self.count) - .map(|i| (i, self.rectangles[i].union(&b).area() - self.rectangles[i].area())) - .min_by(|a, b| PartialOrd::partial_cmp(&a.1, &b.1).unwrap()) - .expect("There should always be rectangles") - .0; - self.rectangles[best_merge] = self.rectangles[best_merge].union(&b); - } - } - - /// Make an union of two regions. - /// - /// Note that if the region becomes too complex, it might be simplified by being bigger than the actual union - #[must_use] - pub fn union(&self, other: &Self) -> Self { - let mut s = self.clone(); - for o in other.iter() { - s.add_box(o) - } - s - } - - /// Bounding rectangle of the region. - #[must_use] - pub fn bounding_rect(&self) -> LogicalRect { - if self.count == 0 { - return Default::default(); - } - let mut r = self.rectangles[0]; - for i in 1..self.count { - r = r.union(&self.rectangles[i]); - } - r.to_rect() - } - - /// Intersection of a region and a rectangle. - #[must_use] - pub fn intersection(&self, other: LogicalRect) -> DirtyRegion { - let mut ret = self.clone(); - let other = other.to_box2d(); - let mut i = 0; - while i < ret.count { - if let Some(x) = ret.rectangles[i].intersection(&other) { - ret.rectangles[i] = x; - } else { - ret.count -= 1; - ret.rectangles.swap(i, ret.count); - continue; - } - i += 1; - } - ret - } - - fn draw_intersects(&self, clipped_geom: LogicalRect) -> bool { - let b = clipped_geom.to_box2d(); - self.iter().any(|r| r.intersects(&b)) - } -} - -impl From for DirtyRegion { - fn from(value: LogicalRect) -> Self { - let mut s = Self::default(); - s.add_rect(value); - s - } -} - -/// This enum describes which parts of the buffer passed to the [`SoftwareRenderer`](crate::software_renderer::SoftwareRenderer) may be re-used to speed up painting. -// FIXME: #[non_exhaustive] #3023 -#[derive(PartialEq, Eq, Debug, Clone, Default, Copy)] -pub enum RepaintBufferType { - #[default] - /// The full window is always redrawn. No attempt at partial rendering will be made. - NewBuffer, - /// Only redraw the parts that have changed since the previous call to render(). - /// - /// This variant assumes that the same buffer is passed on every call to render() and - /// that it still contains the previously rendered frame. - ReusedBuffer, - - /// Redraw the part that have changed since the last two frames were drawn. - /// - /// This is used when using double buffering and swapping of the buffers. - SwappedBuffers, -} - -/// Put this structure in the renderer to help with partial rendering -pub struct PartialRenderer<'a, T> { - cache: &'a RefCell, - /// The region of the screen which is considered dirty and that should be repainted - pub dirty_region: DirtyRegion, - /// The actual renderer which the drawing call will be forwarded to - pub actual_renderer: T, - /// The window adapter the renderer is rendering into. - pub window_adapter: Rc, -} - -impl<'a, T: ItemRenderer + ItemRendererFeatures> PartialRenderer<'a, T> { - /// Create a new PartialRenderer - pub fn new( - cache: &'a RefCell, - initial_dirty_region: DirtyRegion, - actual_renderer: T, - ) -> Self { - let window_adapter = actual_renderer.window().window_adapter(); - Self { cache, dirty_region: initial_dirty_region, actual_renderer, window_adapter } - } - - /// Visit the tree of item and compute what are the dirty regions - pub fn compute_dirty_regions( - &mut self, - component: &ItemTreeRc, - origin: LogicalPoint, - size: LogicalSize, - ) { - #[derive(Clone, Copy)] - struct ComputeDirtyRegionState { - transform_to_screen: ItemTransform, - old_transform_to_screen: ItemTransform, - clipped: LogicalRect, - must_refresh_children: bool, - } - - impl ComputeDirtyRegionState { - /// Adjust transform_to_screen and old_transform_to_screen to map from item coordinates - /// to the screen when using it on a child, specified by its children transform. - fn adjust_transforms_for_child( - &mut self, - children_transform: &ItemTransform, - old_children_transform: &ItemTransform, - ) { - self.transform_to_screen = children_transform.then(&self.transform_to_screen); - self.old_transform_to_screen = - old_children_transform.then(&self.old_transform_to_screen); - } - } - - crate::item_tree::visit_items( - component, - crate::item_tree::TraversalOrder::BackToFront, - |component, item, index, state| { - let mut new_state = *state; - let mut borrowed = self.cache.borrow_mut(); - let item_rc = ItemRc::new(component.clone(), index); - - match item.cached_rendering_data_offset().get_entry(&mut borrowed) { - Some(CachedGraphicsData { - data: cached_geom, - dependency_tracker: Some(tr), - }) => { - if tr.is_dirty() { - let old_geom = cached_geom.clone(); - drop(borrowed); - let new_geom = crate::properties::evaluate_no_tracking(|| { - CachedItemBoundingBoxAndTransform::new::( - &item_rc, - &self.window_adapter, - ) - }); - - self.mark_dirty_rect( - old_geom.bounding_rect(), - state.old_transform_to_screen, - &state.clipped, - ); - self.mark_dirty_rect( - new_geom.bounding_rect(), - state.transform_to_screen, - &state.clipped, - ); - - new_state.adjust_transforms_for_child( - &new_geom.transform(), - &old_geom.transform(), - ); - - if ItemRef::downcast_pin::(item).is_some() - || ItemRef::downcast_pin::(item).is_some() - { - // When the opacity or the clip change, this will impact all the children, including - // the ones outside the element, regardless if they are themselves dirty or not. - new_state.must_refresh_children = true; - } - - ItemVisitorResult::Continue(new_state) - } else { - tr.as_ref().register_as_dependency_to_current_binding(); - - if state.must_refresh_children - || new_state.transform_to_screen - != new_state.old_transform_to_screen - { - self.mark_dirty_rect( - cached_geom.bounding_rect(), - state.old_transform_to_screen, - &state.clipped, - ); - self.mark_dirty_rect( - cached_geom.bounding_rect(), - state.transform_to_screen, - &state.clipped, - ); - } - - new_state.adjust_transforms_for_child( - &cached_geom.transform(), - &cached_geom.transform(), - ); - - if let CachedItemBoundingBoxAndTransform::ClipItem { geometry } = - &cached_geom - { - new_state.clipped = new_state - .clipped - .intersection( - &state - .transform_to_screen - .outer_transformed_rect(&geometry.cast()) - .cast() - .union( - &state - .old_transform_to_screen - .outer_transformed_rect(&geometry.cast()) - .cast(), - ), - ) - .unwrap_or_default(); - } - ItemVisitorResult::Continue(new_state) - } - } - _ => { - drop(borrowed); - let bounding_rect = crate::properties::evaluate_no_tracking(|| { - let geom = CachedItemBoundingBoxAndTransform::new::( - &item_rc, - &self.window_adapter, - ); - - new_state - .adjust_transforms_for_child(&geom.transform(), &geom.transform()); - - if let CachedItemBoundingBoxAndTransform::ClipItem { geometry } = geom { - new_state.clipped = new_state - .clipped - .intersection( - &state - .transform_to_screen - .outer_transformed_rect(&geometry.cast()) - .cast(), - ) - .unwrap_or_default(); - } - *geom.bounding_rect() - }); - self.mark_dirty_rect( - &bounding_rect, - state.transform_to_screen, - &state.clipped, - ); - ItemVisitorResult::Continue(new_state) - } - } - }, - { - let initial_transform = - euclid::Transform2D::translation(origin.x as f32, origin.y as f32); - ComputeDirtyRegionState { - transform_to_screen: initial_transform, - old_transform_to_screen: initial_transform, - clipped: LogicalRect::from_size(size), - must_refresh_children: false, - } - }, - ); - } - - fn mark_dirty_rect( - &mut self, - rect: &LogicalRect, - transform: ItemTransform, - clip_rect: &LogicalRect, - ) { - if !rect.is_empty() { - if let Some(rect) = - transform.outer_transformed_rect(&rect.cast()).cast().intersection(clip_rect) - { - self.dirty_region.add_rect(rect); - } - } - } - - fn do_rendering( - cache: &RefCell, - rendering_data: &CachedRenderingData, - render_fn: impl FnOnce() -> CachedItemBoundingBoxAndTransform, - ) { - let mut cache = cache.borrow_mut(); - if let Some(entry) = rendering_data.get_entry(&mut cache) { - entry - .dependency_tracker - .get_or_insert_with(|| Box::pin(PropertyTracker::default())) - .as_ref() - .evaluate(render_fn); - } else { - let cache_entry = crate::graphics::CachedGraphicsData::new(render_fn); - rendering_data.cache_index.set(cache.insert(cache_entry)); - rendering_data.cache_generation.set(cache.generation()); - } - } - - /// Move the actual renderer - pub fn into_inner(self) -> T { - self.actual_renderer - } -} - -macro_rules! forward_rendering_call { - (fn $fn:ident($Ty:ty) $(-> $Ret:ty)?) => { - fn $fn(&mut self, obj: Pin<&$Ty>, item_rc: &ItemRc, size: LogicalSize) $(-> $Ret)? { - let mut ret = None; - Self::do_rendering(&self.cache, &obj.cached_rendering_data, || { - ret = Some(self.actual_renderer.$fn(obj, item_rc, size)); - CachedItemBoundingBoxAndTransform::new::(&item_rc, &self.window_adapter) - }); - ret.unwrap_or_default() - } - }; -} - -macro_rules! forward_rendering_call2 { - (fn $fn:ident($Ty:ty) $(-> $Ret:ty)?) => { - fn $fn(&mut self, obj: Pin<&$Ty>, item_rc: &ItemRc, size: LogicalSize, cache: &CachedRenderingData) $(-> $Ret)? { - let mut ret = None; - Self::do_rendering(&self.cache, &cache, || { - ret = Some(self.actual_renderer.$fn(obj, item_rc, size, &cache)); - CachedItemBoundingBoxAndTransform::new::(&item_rc, &self.window_adapter) - }); - ret.unwrap_or_default() - } - }; -} - -impl ItemRenderer for PartialRenderer<'_, T> { - fn filter_item( - &mut self, - item_rc: &ItemRc, - window_adapter: &Rc, - ) -> (bool, LogicalRect) { - let item = item_rc.borrow(); - let eval = || { - // registers dependencies on the geometry and clip properties. - CachedItemBoundingBoxAndTransform::new::(item_rc, window_adapter) - }; - - let rendering_data = item.cached_rendering_data_offset(); - let mut cache = self.cache.borrow_mut(); - let item_bounding_rect = match rendering_data.get_entry(&mut cache) { - Some(CachedGraphicsData { data, dependency_tracker }) => { - dependency_tracker - .get_or_insert_with(|| Box::pin(PropertyTracker::default())) - .as_ref() - .evaluate_if_dirty(|| *data = eval()); - *data.bounding_rect() - } - None => { - let cache_entry = crate::graphics::CachedGraphicsData::new(eval); - let geom = cache_entry.data.clone(); - rendering_data.cache_index.set(cache.insert(cache_entry)); - rendering_data.cache_generation.set(cache.generation()); - *geom.bounding_rect() - } - }; - - let clipped_geom = self.get_current_clip().intersection(&item_bounding_rect); - let draw = clipped_geom.is_some_and(|clipped_geom| { - let clipped_geom = clipped_geom.translate(self.translation()); - self.dirty_region.draw_intersects(clipped_geom) - }); - - // Query untracked, as the bounding rect calculation already registers a dependency on the geometry. - let item_geometry = crate::properties::evaluate_no_tracking(|| item_rc.geometry()); - - (draw, item_geometry) - } - - forward_rendering_call2!(fn draw_rectangle(dyn RenderRectangle)); - forward_rendering_call2!(fn draw_border_rectangle(dyn RenderBorderRectangle)); - forward_rendering_call2!(fn draw_window_background(dyn RenderRectangle)); - forward_rendering_call2!(fn draw_image(dyn RenderImage)); - forward_rendering_call2!(fn draw_text(dyn RenderText)); - forward_rendering_call!(fn draw_text_input(TextInput)); - #[cfg(feature = "std")] - forward_rendering_call!(fn draw_path(Path)); - forward_rendering_call!(fn draw_box_shadow(BoxShadow)); - - forward_rendering_call!(fn visit_clip(Clip) -> RenderingResult); - forward_rendering_call!(fn visit_opacity(Opacity) -> RenderingResult); - - fn combine_clip( - &mut self, - rect: LogicalRect, - radius: LogicalBorderRadius, - border_width: LogicalLength, - ) -> bool { - self.actual_renderer.combine_clip(rect, radius, border_width) - } - - fn get_current_clip(&self) -> LogicalRect { - self.actual_renderer.get_current_clip() - } - - fn translate(&mut self, distance: LogicalVector) { - self.actual_renderer.translate(distance) - } - fn translation(&self) -> LogicalVector { - self.actual_renderer.translation() - } - - fn rotate(&mut self, angle_in_degrees: f32) { - self.actual_renderer.rotate(angle_in_degrees) - } - - fn scale(&mut self, x_factor: f32, y_factor: f32) { - self.actual_renderer.scale(x_factor, y_factor) - } - - fn apply_opacity(&mut self, opacity: f32) { - self.actual_renderer.apply_opacity(opacity) - } - - fn save_state(&mut self) { - self.actual_renderer.save_state() - } - - fn restore_state(&mut self) { - self.actual_renderer.restore_state() - } - - fn scale_factor(&self) -> f32 { - self.actual_renderer.scale_factor() - } - - fn draw_cached_pixmap( - &mut self, - item_rc: &ItemRc, - update_fn: &dyn Fn(&mut dyn FnMut(u32, u32, &[u8])), - ) { - self.actual_renderer.draw_cached_pixmap(item_rc, update_fn) - } - - fn draw_string(&mut self, string: &str, color: crate::Color) { - self.actual_renderer.draw_string(string, color) - } - - fn draw_image_direct(&mut self, image: crate::graphics::image::Image) { - self.actual_renderer.draw_image_direct(image) - } - - fn window(&self) -> &crate::window::WindowInner { - self.actual_renderer.window() - } - - fn as_any(&mut self) -> Option<&mut dyn core::any::Any> { - self.actual_renderer.as_any() - } -} - -/// This struct holds the state of the partial renderer between different frames, in particular the cache of the bounding rect -/// of each item. This permits a more fine-grained computation of the region that needs to be repainted. -#[derive(Default)] -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, - /// Force a redraw in the next frame, no matter what's dirty. Use only as a last resort. - force_screen_refresh: Cell, -} - -impl PartialRenderingState { - /// 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( - &self, - renderer: T, - ) -> PartialRenderer<'_, T> { - PartialRenderer::new(&self.partial_cache, self.force_dirty.take(), renderer) - } - - /// 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. 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); - } - - let screen_region = LogicalRect::from_size(logical_window_size); - - if self.force_screen_refresh.take() { - partial_renderer.dirty_region = screen_region.into(); - } - - 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. - pub fn mark_dirty_region(&self, region: DirtyRegion) { - self.force_dirty.replace_with(|r| r.union(®ion)); - } - - /// Call this from your renderer's `free_graphics_resources` function to ensure that the cached item geometries - /// are cleared for the destroyed items in the item tree. - pub fn free_graphics_resources(&self, items: &mut dyn Iterator>>) { - for item in items { - item.cached_rendering_data_offset().release(&mut self.partial_cache.borrow_mut()); - } - - // We don't have a way to determine the screen region of the delete items, what's in the cache is relative. So - // as a last resort, refresh everything. - self.force_screen_refresh.set(true) - } - - /// Clears the partial rendering cache. Use this for example when the entire undering window surface changes. - pub fn clear_cache(&self) { - self.partial_cache.borrow_mut().clear(); - } - - /// Force re-rendering of the entire window region the next time a partial renderer is created. - pub fn force_screen_refresh(&self) { - self.force_screen_refresh.set(true); - } -} - -#[test] -fn dirty_region_no_intersection() { - let mut region = DirtyRegion::default(); - region.add_rect(LogicalRect::new(LogicalPoint::new(10., 10.), LogicalSize::new(16., 16.))); - region.add_rect(LogicalRect::new(LogicalPoint::new(100., 100.), LogicalSize::new(16., 16.))); - region.add_rect(LogicalRect::new(LogicalPoint::new(200., 100.), LogicalSize::new(16., 16.))); - let i = region - .intersection(LogicalRect::new(LogicalPoint::new(50., 50.), LogicalSize::new(10., 10.))); - assert_eq!(i.iter().count(), 0); -} diff --git a/internal/core/lib.rs b/internal/core/lib.rs index 2d0c906e2c7..bc36541cc9a 100644 --- a/internal/core/lib.rs +++ b/internal/core/lib.rs @@ -42,6 +42,7 @@ pub mod layout; pub mod lengths; pub mod menus; pub mod model; +pub mod partial_renderer; pub mod platform; pub mod properties; pub mod renderer; diff --git a/internal/core/partial_renderer.rs b/internal/core/partial_renderer.rs new file mode 100644 index 00000000000..caad32d6989 --- /dev/null +++ b/internal/core/partial_renderer.rs @@ -0,0 +1,759 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +//! Module for a renderer proxy that tries to render only the parts of the tree that have changed. + +use super::graphics::RenderingCache; +use crate::graphics::CachedGraphicsData; +use crate::item_rendering::{ + ItemRenderer, ItemRendererFeatures, RenderBorderRectangle, RenderImage, RenderRectangle, + RenderText, +}; +use crate::item_tree::{ItemTreeRc, ItemVisitorResult}; +#[cfg(feature = "std")] +use crate::items::Path; +use crate::items::{BoxShadow, Clip, ItemRc, ItemRef, Opacity, RenderingResult, TextInput}; +use crate::lengths::{ + ItemTransform, LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalPx, LogicalRect, + LogicalSize, LogicalVector, +}; +use crate::properties::PropertyTracker; +use crate::window::WindowAdapter; +use crate::Coord; +use alloc::boxed::Box; +use alloc::rc::Rc; +use core::cell::{Cell, RefCell}; +use core::pin::Pin; + +/// This structure must be present in items that are Rendered and contains information. +/// Used by the backend. +#[derive(Default, Debug)] +#[repr(C)] +pub struct CachedRenderingData { + /// Used and modified by the backend, should be initialized to 0 by the user code + pub(crate) cache_index: Cell, + /// Used and modified by the backend, should be initialized to 0 by the user code. + /// The backend compares this generation against the one of the cache to verify + /// the validity of the cache_index field. + pub(crate) cache_generation: Cell, +} + +impl CachedRenderingData { + /// This function can be used to remove an entry from the rendering cache for a given item, if it + /// exists, i.e. if any data was ever cached. This is typically called by the graphics backend's + /// implementation of the release_item_graphics_cache function. + pub fn release(&self, cache: &mut RenderingCache) -> Option { + if self.cache_generation.get() == cache.generation() { + let index = self.cache_index.get(); + self.cache_generation.set(0); + Some(cache.remove(index).data) + } else { + None + } + } + + /// Return the value if it is in the cache + pub fn get_entry<'a, T>( + &self, + cache: &'a mut RenderingCache, + ) -> Option<&'a mut crate::graphics::CachedGraphicsData> { + let index = self.cache_index.get(); + if self.cache_generation.get() == cache.generation() { + cache.get_mut(index) + } else { + None + } + } +} + +/// After rendering an item, we cache the geometry and the transform it applies to +/// children. +#[derive(Clone)] +pub enum CachedItemBoundingBoxAndTransform { + /// A regular item with a translation + RegularItem { + /// The item's bounding rect relative to its parent. + bounding_rect: LogicalRect, + /// The item's offset relative to its parent. + offset: LogicalVector, + }, + /// An item such as Rotate that defines an additional transformation + ItemWithTransform { + /// The item's bounding rect relative to its parent. + bounding_rect: LogicalRect, + /// The item's transform to apply to children. + transform: Box, + }, + /// A clip item. + ClipItem { + /// The item's geometry relative to its parent. + geometry: LogicalRect, + }, +} + +impl CachedItemBoundingBoxAndTransform { + fn bounding_rect(&self) -> &LogicalRect { + match self { + CachedItemBoundingBoxAndTransform::RegularItem { bounding_rect, .. } => bounding_rect, + CachedItemBoundingBoxAndTransform::ItemWithTransform { bounding_rect, .. } => { + bounding_rect + } + CachedItemBoundingBoxAndTransform::ClipItem { geometry } => geometry, + } + } + + fn transform(&self) -> ItemTransform { + match self { + CachedItemBoundingBoxAndTransform::RegularItem { offset, .. } => { + ItemTransform::translation(offset.x as f32, offset.y as f32) + } + CachedItemBoundingBoxAndTransform::ItemWithTransform { transform, .. } => **transform, + CachedItemBoundingBoxAndTransform::ClipItem { geometry } => { + ItemTransform::translation(geometry.origin.x as f32, geometry.origin.y as f32) + } + } + } + + fn new( + item_rc: &ItemRc, + window_adapter: &Rc, + ) -> Self { + let geometry = item_rc.geometry(); + + if item_rc.borrow().as_ref().clips_children() { + return Self::ClipItem { geometry }; + } + + // Evaluate the bounding rect untracked, as properties that affect the bounding rect are already tracked + // at rendering time. + let bounding_rect = crate::properties::evaluate_no_tracking(|| { + item_rc.bounding_rect(&geometry, window_adapter) + }); + + if let Some(complex_child_transform) = (T::SUPPORTS_TRANSFORMATIONS + && window_adapter.renderer().supports_transformations()) + .then(|| item_rc.children_transform()) + .flatten() + { + Self::ItemWithTransform { + bounding_rect, + transform: complex_child_transform + .then_translate(geometry.origin.to_vector().cast()) + .into(), + } + } else { + Self::RegularItem { bounding_rect, offset: geometry.origin.to_vector() } + } + } +} + +/// The cache that needs to be held by the Window for the partial rendering +pub type PartialRenderingCache = RenderingCache; + +/// A region composed of a few rectangles that need to be redrawn. +#[derive(Default, Clone, Debug)] +pub struct DirtyRegion { + rectangles: [euclid::Box2D; Self::MAX_COUNT], + count: usize, +} + +impl DirtyRegion { + /// The maximum number of rectangles that can be stored in a DirtyRegion + pub(crate) const MAX_COUNT: usize = 3; + + /// An iterator over the part of the region (they can overlap) + pub fn iter(&self) -> impl Iterator> + '_ { + (0..self.count).map(|x| self.rectangles[x]) + } + + /// Add a rectangle to the region. + /// + /// Note that if the region becomes too complex, it might be simplified by being bigger than the actual union. + pub fn add_rect(&mut self, rect: LogicalRect) { + self.add_box(rect.to_box2d()); + } + + /// Add a box to the region + /// + /// Note that if the region becomes too complex, it might be simplified by being bigger than the actual union. + pub fn add_box(&mut self, b: euclid::Box2D) { + if b.is_empty() { + return; + } + let mut i = 0; + while i < self.count { + let r = &self.rectangles[i]; + if r.contains_box(&b) { + // the rectangle is already in the union + return; + } else if b.contains_box(r) { + self.rectangles.swap(i, self.count - 1); + self.count -= 1; + continue; + } + i += 1; + } + + if self.count < Self::MAX_COUNT { + self.rectangles[self.count] = b; + self.count += 1; + } else { + let best_merge = (0..self.count) + .map(|i| (i, self.rectangles[i].union(&b).area() - self.rectangles[i].area())) + .min_by(|a, b| PartialOrd::partial_cmp(&a.1, &b.1).unwrap()) + .expect("There should always be rectangles") + .0; + self.rectangles[best_merge] = self.rectangles[best_merge].union(&b); + } + } + + /// Make an union of two regions. + /// + /// Note that if the region becomes too complex, it might be simplified by being bigger than the actual union + #[must_use] + pub fn union(&self, other: &Self) -> Self { + let mut s = self.clone(); + for o in other.iter() { + s.add_box(o) + } + s + } + + /// Bounding rectangle of the region. + #[must_use] + pub fn bounding_rect(&self) -> LogicalRect { + if self.count == 0 { + return Default::default(); + } + let mut r = self.rectangles[0]; + for i in 1..self.count { + r = r.union(&self.rectangles[i]); + } + r.to_rect() + } + + /// Intersection of a region and a rectangle. + #[must_use] + pub fn intersection(&self, other: LogicalRect) -> DirtyRegion { + let mut ret = self.clone(); + let other = other.to_box2d(); + let mut i = 0; + while i < ret.count { + if let Some(x) = ret.rectangles[i].intersection(&other) { + ret.rectangles[i] = x; + } else { + ret.count -= 1; + ret.rectangles.swap(i, ret.count); + continue; + } + i += 1; + } + ret + } + + fn draw_intersects(&self, clipped_geom: LogicalRect) -> bool { + let b = clipped_geom.to_box2d(); + self.iter().any(|r| r.intersects(&b)) + } +} + +impl From for DirtyRegion { + fn from(value: LogicalRect) -> Self { + let mut s = Self::default(); + s.add_rect(value); + s + } +} + +/// This enum describes which parts of the buffer passed to the [`SoftwareRenderer`](crate::software_renderer::SoftwareRenderer) may be re-used to speed up painting. +// FIXME: #[non_exhaustive] #3023 +#[derive(PartialEq, Eq, Debug, Clone, Default, Copy)] +pub enum RepaintBufferType { + #[default] + /// The full window is always redrawn. No attempt at partial rendering will be made. + NewBuffer, + /// Only redraw the parts that have changed since the previous call to render(). + /// + /// This variant assumes that the same buffer is passed on every call to render() and + /// that it still contains the previously rendered frame. + ReusedBuffer, + + /// Redraw the part that have changed since the last two frames were drawn. + /// + /// This is used when using double buffering and swapping of the buffers. + SwappedBuffers, +} + +/// Put this structure in the renderer to help with partial rendering +pub struct PartialRenderer<'a, T> { + cache: &'a RefCell, + /// The region of the screen which is considered dirty and that should be repainted + pub dirty_region: DirtyRegion, + /// The actual renderer which the drawing call will be forwarded to + pub actual_renderer: T, + /// The window adapter the renderer is rendering into. + pub window_adapter: Rc, +} + +impl<'a, T: ItemRenderer + ItemRendererFeatures> PartialRenderer<'a, T> { + /// Create a new PartialRenderer + pub fn new( + cache: &'a RefCell, + initial_dirty_region: DirtyRegion, + actual_renderer: T, + ) -> Self { + let window_adapter = actual_renderer.window().window_adapter(); + Self { cache, dirty_region: initial_dirty_region, actual_renderer, window_adapter } + } + + /// Visit the tree of item and compute what are the dirty regions + pub fn compute_dirty_regions( + &mut self, + component: &ItemTreeRc, + origin: LogicalPoint, + size: LogicalSize, + ) { + #[derive(Clone, Copy)] + struct ComputeDirtyRegionState { + transform_to_screen: ItemTransform, + old_transform_to_screen: ItemTransform, + clipped: LogicalRect, + must_refresh_children: bool, + } + + impl ComputeDirtyRegionState { + /// Adjust transform_to_screen and old_transform_to_screen to map from item coordinates + /// to the screen when using it on a child, specified by its children transform. + fn adjust_transforms_for_child( + &mut self, + children_transform: &ItemTransform, + old_children_transform: &ItemTransform, + ) { + self.transform_to_screen = children_transform.then(&self.transform_to_screen); + self.old_transform_to_screen = + old_children_transform.then(&self.old_transform_to_screen); + } + } + + crate::item_tree::visit_items( + component, + crate::item_tree::TraversalOrder::BackToFront, + |component, item, index, state| { + let mut new_state = *state; + let mut borrowed = self.cache.borrow_mut(); + let item_rc = ItemRc::new(component.clone(), index); + + match item.cached_rendering_data_offset().get_entry(&mut borrowed) { + Some(CachedGraphicsData { + data: cached_geom, + dependency_tracker: Some(tr), + }) => { + if tr.is_dirty() { + let old_geom = cached_geom.clone(); + drop(borrowed); + let new_geom = crate::properties::evaluate_no_tracking(|| { + CachedItemBoundingBoxAndTransform::new::( + &item_rc, + &self.window_adapter, + ) + }); + + self.mark_dirty_rect( + old_geom.bounding_rect(), + state.old_transform_to_screen, + &state.clipped, + ); + self.mark_dirty_rect( + new_geom.bounding_rect(), + state.transform_to_screen, + &state.clipped, + ); + + new_state.adjust_transforms_for_child( + &new_geom.transform(), + &old_geom.transform(), + ); + + if ItemRef::downcast_pin::(item).is_some() + || ItemRef::downcast_pin::(item).is_some() + { + // When the opacity or the clip change, this will impact all the children, including + // the ones outside the element, regardless if they are themselves dirty or not. + new_state.must_refresh_children = true; + } + + ItemVisitorResult::Continue(new_state) + } else { + tr.as_ref().register_as_dependency_to_current_binding(); + + if state.must_refresh_children + || new_state.transform_to_screen + != new_state.old_transform_to_screen + { + self.mark_dirty_rect( + cached_geom.bounding_rect(), + state.old_transform_to_screen, + &state.clipped, + ); + self.mark_dirty_rect( + cached_geom.bounding_rect(), + state.transform_to_screen, + &state.clipped, + ); + } + + new_state.adjust_transforms_for_child( + &cached_geom.transform(), + &cached_geom.transform(), + ); + + if let CachedItemBoundingBoxAndTransform::ClipItem { geometry } = + &cached_geom + { + new_state.clipped = new_state + .clipped + .intersection( + &state + .transform_to_screen + .outer_transformed_rect(&geometry.cast()) + .cast() + .union( + &state + .old_transform_to_screen + .outer_transformed_rect(&geometry.cast()) + .cast(), + ), + ) + .unwrap_or_default(); + } + ItemVisitorResult::Continue(new_state) + } + } + _ => { + drop(borrowed); + let bounding_rect = crate::properties::evaluate_no_tracking(|| { + let geom = CachedItemBoundingBoxAndTransform::new::( + &item_rc, + &self.window_adapter, + ); + + new_state + .adjust_transforms_for_child(&geom.transform(), &geom.transform()); + + if let CachedItemBoundingBoxAndTransform::ClipItem { geometry } = geom { + new_state.clipped = new_state + .clipped + .intersection( + &state + .transform_to_screen + .outer_transformed_rect(&geometry.cast()) + .cast(), + ) + .unwrap_or_default(); + } + *geom.bounding_rect() + }); + self.mark_dirty_rect( + &bounding_rect, + state.transform_to_screen, + &state.clipped, + ); + ItemVisitorResult::Continue(new_state) + } + } + }, + { + let initial_transform = + euclid::Transform2D::translation(origin.x as f32, origin.y as f32); + ComputeDirtyRegionState { + transform_to_screen: initial_transform, + old_transform_to_screen: initial_transform, + clipped: LogicalRect::from_size(size), + must_refresh_children: false, + } + }, + ); + } + + fn mark_dirty_rect( + &mut self, + rect: &LogicalRect, + transform: ItemTransform, + clip_rect: &LogicalRect, + ) { + if !rect.is_empty() { + if let Some(rect) = + transform.outer_transformed_rect(&rect.cast()).cast().intersection(clip_rect) + { + self.dirty_region.add_rect(rect); + } + } + } + + fn do_rendering( + cache: &RefCell, + rendering_data: &CachedRenderingData, + render_fn: impl FnOnce() -> CachedItemBoundingBoxAndTransform, + ) { + let mut cache = cache.borrow_mut(); + if let Some(entry) = rendering_data.get_entry(&mut cache) { + entry + .dependency_tracker + .get_or_insert_with(|| Box::pin(PropertyTracker::default())) + .as_ref() + .evaluate(render_fn); + } else { + let cache_entry = crate::graphics::CachedGraphicsData::new(render_fn); + rendering_data.cache_index.set(cache.insert(cache_entry)); + rendering_data.cache_generation.set(cache.generation()); + } + } + + /// Move the actual renderer + pub fn into_inner(self) -> T { + self.actual_renderer + } +} + +macro_rules! forward_rendering_call { + (fn $fn:ident($Ty:ty) $(-> $Ret:ty)?) => { + fn $fn(&mut self, obj: Pin<&$Ty>, item_rc: &ItemRc, size: LogicalSize) $(-> $Ret)? { + let mut ret = None; + Self::do_rendering(&self.cache, &obj.cached_rendering_data, || { + ret = Some(self.actual_renderer.$fn(obj, item_rc, size)); + CachedItemBoundingBoxAndTransform::new::(&item_rc, &self.window_adapter) + }); + ret.unwrap_or_default() + } + }; +} + +macro_rules! forward_rendering_call2 { + (fn $fn:ident($Ty:ty) $(-> $Ret:ty)?) => { + fn $fn(&mut self, obj: Pin<&$Ty>, item_rc: &ItemRc, size: LogicalSize, cache: &CachedRenderingData) $(-> $Ret)? { + let mut ret = None; + Self::do_rendering(&self.cache, &cache, || { + ret = Some(self.actual_renderer.$fn(obj, item_rc, size, &cache)); + CachedItemBoundingBoxAndTransform::new::(&item_rc, &self.window_adapter) + }); + ret.unwrap_or_default() + } + }; +} + +impl ItemRenderer for PartialRenderer<'_, T> { + fn filter_item( + &mut self, + item_rc: &ItemRc, + window_adapter: &Rc, + ) -> (bool, LogicalRect) { + let item = item_rc.borrow(); + let eval = || { + // registers dependencies on the geometry and clip properties. + CachedItemBoundingBoxAndTransform::new::(item_rc, window_adapter) + }; + + let rendering_data = item.cached_rendering_data_offset(); + let mut cache = self.cache.borrow_mut(); + let item_bounding_rect = match rendering_data.get_entry(&mut cache) { + Some(CachedGraphicsData { data, dependency_tracker }) => { + dependency_tracker + .get_or_insert_with(|| Box::pin(PropertyTracker::default())) + .as_ref() + .evaluate_if_dirty(|| *data = eval()); + *data.bounding_rect() + } + None => { + let cache_entry = crate::graphics::CachedGraphicsData::new(eval); + let geom = cache_entry.data.clone(); + rendering_data.cache_index.set(cache.insert(cache_entry)); + rendering_data.cache_generation.set(cache.generation()); + *geom.bounding_rect() + } + }; + + let clipped_geom = self.get_current_clip().intersection(&item_bounding_rect); + let draw = clipped_geom.is_some_and(|clipped_geom| { + let clipped_geom = clipped_geom.translate(self.translation()); + self.dirty_region.draw_intersects(clipped_geom) + }); + + // Query untracked, as the bounding rect calculation already registers a dependency on the geometry. + let item_geometry = crate::properties::evaluate_no_tracking(|| item_rc.geometry()); + + (draw, item_geometry) + } + + forward_rendering_call2!(fn draw_rectangle(dyn RenderRectangle)); + forward_rendering_call2!(fn draw_border_rectangle(dyn RenderBorderRectangle)); + forward_rendering_call2!(fn draw_window_background(dyn RenderRectangle)); + forward_rendering_call2!(fn draw_image(dyn RenderImage)); + forward_rendering_call2!(fn draw_text(dyn RenderText)); + forward_rendering_call!(fn draw_text_input(TextInput)); + #[cfg(feature = "std")] + forward_rendering_call!(fn draw_path(Path)); + forward_rendering_call!(fn draw_box_shadow(BoxShadow)); + + forward_rendering_call!(fn visit_clip(Clip) -> RenderingResult); + forward_rendering_call!(fn visit_opacity(Opacity) -> RenderingResult); + + fn combine_clip( + &mut self, + rect: LogicalRect, + radius: LogicalBorderRadius, + border_width: LogicalLength, + ) -> bool { + self.actual_renderer.combine_clip(rect, radius, border_width) + } + + fn get_current_clip(&self) -> LogicalRect { + self.actual_renderer.get_current_clip() + } + + fn translate(&mut self, distance: LogicalVector) { + self.actual_renderer.translate(distance) + } + fn translation(&self) -> LogicalVector { + self.actual_renderer.translation() + } + + fn rotate(&mut self, angle_in_degrees: f32) { + self.actual_renderer.rotate(angle_in_degrees) + } + + fn scale(&mut self, x_factor: f32, y_factor: f32) { + self.actual_renderer.scale(x_factor, y_factor) + } + + fn apply_opacity(&mut self, opacity: f32) { + self.actual_renderer.apply_opacity(opacity) + } + + fn save_state(&mut self) { + self.actual_renderer.save_state() + } + + fn restore_state(&mut self) { + self.actual_renderer.restore_state() + } + + fn scale_factor(&self) -> f32 { + self.actual_renderer.scale_factor() + } + + fn draw_cached_pixmap( + &mut self, + item_rc: &ItemRc, + update_fn: &dyn Fn(&mut dyn FnMut(u32, u32, &[u8])), + ) { + self.actual_renderer.draw_cached_pixmap(item_rc, update_fn) + } + + fn draw_string(&mut self, string: &str, color: crate::Color) { + self.actual_renderer.draw_string(string, color) + } + + fn draw_image_direct(&mut self, image: crate::graphics::image::Image) { + self.actual_renderer.draw_image_direct(image) + } + + fn window(&self) -> &crate::window::WindowInner { + self.actual_renderer.window() + } + + fn as_any(&mut self) -> Option<&mut dyn core::any::Any> { + self.actual_renderer.as_any() + } +} + +/// This struct holds the state of the partial renderer between different frames, in particular the cache of the bounding rect +/// of each item. This permits a more fine-grained computation of the region that needs to be repainted. +#[derive(Default)] +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, + /// Force a redraw in the next frame, no matter what's dirty. Use only as a last resort. + force_screen_refresh: Cell, +} + +impl PartialRenderingState { + /// 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( + &self, + renderer: T, + ) -> PartialRenderer<'_, T> { + PartialRenderer::new(&self.partial_cache, self.force_dirty.take(), renderer) + } + + /// 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. 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); + } + + let screen_region = LogicalRect::from_size(logical_window_size); + + if self.force_screen_refresh.take() { + partial_renderer.dirty_region = screen_region.into(); + } + + 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. + pub fn mark_dirty_region(&self, region: DirtyRegion) { + self.force_dirty.replace_with(|r| r.union(®ion)); + } + + /// Call this from your renderer's `free_graphics_resources` function to ensure that the cached item geometries + /// are cleared for the destroyed items in the item tree. + pub fn free_graphics_resources(&self, items: &mut dyn Iterator>>) { + for item in items { + item.cached_rendering_data_offset().release(&mut self.partial_cache.borrow_mut()); + } + + // We don't have a way to determine the screen region of the delete items, what's in the cache is relative. So + // as a last resort, refresh everything. + self.force_screen_refresh.set(true) + } + + /// Clears the partial rendering cache. Use this for example when the entire undering window surface changes. + pub fn clear_cache(&self) { + self.partial_cache.borrow_mut().clear(); + } + + /// Force re-rendering of the entire window region the next time a partial renderer is created. + pub fn force_screen_refresh(&self) { + self.force_screen_refresh.set(true); + } +} + +#[test] +fn dirty_region_no_intersection() { + let mut region = DirtyRegion::default(); + region.add_rect(LogicalRect::new(LogicalPoint::new(10., 10.), LogicalSize::new(16., 16.))); + region.add_rect(LogicalRect::new(LogicalPoint::new(100., 100.), LogicalSize::new(16., 16.))); + region.add_rect(LogicalRect::new(LogicalPoint::new(200., 100.), LogicalSize::new(16., 16.))); + let i = region + .intersection(LogicalRect::new(LogicalPoint::new(50., 50.), LogicalSize::new(10., 10.))); + assert_eq!(i.iter().count(), 0); +} diff --git a/internal/core/renderer.rs b/internal/core/renderer.rs index deb3ae85fad..8eada96a6b9 100644 --- a/internal/core/renderer.rs +++ b/internal/core/renderer.rs @@ -79,7 +79,7 @@ pub trait RendererSealed { /// Mark a given region as dirty regardless whether the items actually are dirty. /// /// Example: when a PopupWindow disappears, the region under the popup needs to be redrawn - fn mark_dirty_region(&self, _region: crate::item_rendering::DirtyRegion) {} + fn mark_dirty_region(&self, _region: crate::partial_renderer::DirtyRegion) {} #[cfg(feature = "std")] // FIXME: just because of the Error /// This function can be used to register a custom TrueType font with Slint, diff --git a/internal/core/software_renderer.rs b/internal/core/software_renderer.rs index f17d8c5c85a..e726c19520a 100644 --- a/internal/core/software_renderer.rs +++ b/internal/core/software_renderer.rs @@ -20,14 +20,14 @@ use crate::api::PlatformError; use crate::graphics::rendering_metrics_collector::{RefreshMode, RenderingMetricsCollector}; use crate::graphics::{BorderRadius, Rgba8Pixel, SharedImageBuffer, SharedPixelBuffer}; use crate::item_rendering::{ - CachedRenderingData, DirtyRegion, PartialRenderingState, RenderBorderRectangle, RenderImage, - RenderRectangle, + CachedRenderingData, RenderBorderRectangle, RenderImage, RenderRectangle, }; use crate::items::{ItemRc, TextOverflow, TextWrap}; use crate::lengths::{ LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, LogicalVector, PhysicalPx, PointLengths, RectLengths, ScaleFactor, SizeLengths, }; +use crate::partial_renderer::{DirtyRegion, PartialRenderingState}; use crate::renderer::RendererSealed; use crate::textlayout::{AbstractFont, FontMetrics, TextParagraphLayout}; use crate::window::{WindowAdapter, WindowInner}; @@ -50,7 +50,7 @@ type PhysicalSize = euclid::Size2D; type PhysicalPoint = euclid::Point2D; type PhysicalBorderRadius = BorderRadius; -pub use crate::item_rendering::RepaintBufferType; +pub use crate::partial_renderer::RepaintBufferType; /// This enum describes the rotation that should be applied to the contents rendered by the software renderer. /// @@ -859,7 +859,7 @@ impl RendererSealed for SoftwareRenderer { Ok(()) } - fn mark_dirty_region(&self, region: crate::item_rendering::DirtyRegion) { + fn mark_dirty_region(&self, region: DirtyRegion) { self.partial_rendering_state.mark_dirty_region(region); } diff --git a/internal/renderers/skia/d3d_surface.rs b/internal/renderers/skia/d3d_surface.rs index c2706fe7c49..8af162a809a 100644 --- a/internal/renderers/skia/d3d_surface.rs +++ b/internal/renderers/skia/d3d_surface.rs @@ -3,7 +3,7 @@ use i_slint_core::api::{PhysicalSize as PhysicalWindowSize, Window}; use i_slint_core::graphics::RequestedGraphicsAPI; -use i_slint_core::item_rendering::DirtyRegion; +use i_slint_core::partial_renderer::DirtyRegion; use i_slint_core::platform::PlatformError; use std::cell::RefCell; use std::sync::Arc; diff --git a/internal/renderers/skia/lib.rs b/internal/renderers/skia/lib.rs index c7778c38ffa..9189a80e3f9 100644 --- a/internal/renderers/skia/lib.rs +++ b/internal/renderers/skia/lib.rs @@ -19,10 +19,11 @@ use i_slint_core::graphics::euclid::{self, Vector2D}; use i_slint_core::graphics::rendering_metrics_collector::RenderingMetricsCollector; use i_slint_core::graphics::RequestedGraphicsAPI; use i_slint_core::graphics::{BorderRadius, FontRequest, SharedPixelBuffer}; -use i_slint_core::item_rendering::{DirtyRegion, ItemCache, ItemRenderer, PartialRenderingState}; +use i_slint_core::item_rendering::{ItemCache, ItemRenderer}; use i_slint_core::lengths::{ LogicalLength, LogicalPoint, LogicalRect, LogicalSize, PhysicalPx, ScaleFactor, }; +use i_slint_core::partial_renderer::{DirtyRegion, PartialRenderingState}; use i_slint_core::platform::PlatformError; use i_slint_core::textlayout::sharedparley; use i_slint_core::window::{WindowAdapter, WindowInner}; @@ -962,7 +963,7 @@ impl i_slint_core::renderer::RendererSealed for SkiaRenderer { Ok(target_buffer) } - fn mark_dirty_region(&self, region: i_slint_core::item_rendering::DirtyRegion) { + fn mark_dirty_region(&self, region: DirtyRegion) { if let Some(partial_rendering_state) = self.partial_rendering_state() { partial_rendering_state.mark_dirty_region(region); } diff --git a/internal/renderers/skia/metal_surface.rs b/internal/renderers/skia/metal_surface.rs index 4709b712fd6..ba99e49cc4c 100644 --- a/internal/renderers/skia/metal_surface.rs +++ b/internal/renderers/skia/metal_surface.rs @@ -3,7 +3,7 @@ use i_slint_core::api::{PhysicalSize as PhysicalWindowSize, Window}; use i_slint_core::graphics::RequestedGraphicsAPI; -use i_slint_core::item_rendering::DirtyRegion; +use i_slint_core::partial_renderer::DirtyRegion; use objc2::rc::autoreleasepool; use objc2::{rc::Retained, runtime::ProtocolObject}; use objc2_core_foundation::CGSize; diff --git a/internal/renderers/skia/opengl_surface.rs b/internal/renderers/skia/opengl_surface.rs index d48f8dd2690..7256179edb5 100644 --- a/internal/renderers/skia/opengl_surface.rs +++ b/internal/renderers/skia/opengl_surface.rs @@ -13,7 +13,7 @@ use glutin::{ }; use i_slint_core::api::{GraphicsAPI, PhysicalSize as PhysicalWindowSize, Window}; use i_slint_core::graphics::{BorrowedOpenGLTexture, RequestedGraphicsAPI, RequestedOpenGLVersion}; -use i_slint_core::item_rendering::DirtyRegion; +use i_slint_core::partial_renderer::DirtyRegion; use i_slint_core::platform::PlatformError; use crate::SkiaSharedContext; diff --git a/internal/renderers/skia/software_surface.rs b/internal/renderers/skia/software_surface.rs index f919b95e8e0..2d0ff78491d 100644 --- a/internal/renderers/skia/software_surface.rs +++ b/internal/renderers/skia/software_surface.rs @@ -3,8 +3,8 @@ use i_slint_core::api::{PhysicalSize as PhysicalWindowSize, Window}; use i_slint_core::graphics::RequestedGraphicsAPI; -use i_slint_core::item_rendering::DirtyRegion; use i_slint_core::lengths::ScaleFactor; +use i_slint_core::partial_renderer::DirtyRegion; use std::cell::RefCell; use std::num::NonZeroU32; diff --git a/internal/renderers/skia/vulkan_surface.rs b/internal/renderers/skia/vulkan_surface.rs index ee89edca07d..656e9ec6db3 100644 --- a/internal/renderers/skia/vulkan_surface.rs +++ b/internal/renderers/skia/vulkan_surface.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use i_slint_core::api::{PhysicalSize as PhysicalWindowSize, Window}; use i_slint_core::graphics::RequestedGraphicsAPI; -use i_slint_core::item_rendering::DirtyRegion; +use i_slint_core::partial_renderer::DirtyRegion; use vulkano::device::physical::{PhysicalDevice, PhysicalDeviceType}; use vulkano::device::{ diff --git a/internal/renderers/skia/wgpu_26_surface.rs b/internal/renderers/skia/wgpu_26_surface.rs index 3fd0621e165..17da7315dc6 100644 --- a/internal/renderers/skia/wgpu_26_surface.rs +++ b/internal/renderers/skia/wgpu_26_surface.rs @@ -3,7 +3,7 @@ use i_slint_core::api::{GraphicsAPI, PhysicalSize as PhysicalWindowSize, Window}; use i_slint_core::graphics::RequestedGraphicsAPI; -use i_slint_core::item_rendering::DirtyRegion; +use i_slint_core::partial_renderer::DirtyRegion; use i_slint_core::platform::PlatformError; use std::cell::RefCell; diff --git a/internal/renderers/skia/wgpu_27_surface.rs b/internal/renderers/skia/wgpu_27_surface.rs index 50410938cce..04b1ba9ad4a 100644 --- a/internal/renderers/skia/wgpu_27_surface.rs +++ b/internal/renderers/skia/wgpu_27_surface.rs @@ -3,7 +3,7 @@ use i_slint_core::api::{GraphicsAPI, PhysicalSize as PhysicalWindowSize, Window}; use i_slint_core::graphics::RequestedGraphicsAPI; -use i_slint_core::item_rendering::DirtyRegion; +use i_slint_core::partial_renderer::DirtyRegion; use i_slint_core::platform::PlatformError; use std::cell::RefCell; From 5c1ebe007559dfaa45ecf34d7f8270f3bcb586d4 Mon Sep 17 00:00:00 2001 From: Olivier Goffart Date: Fri, 19 Sep 2025 14:27:48 +0200 Subject: [PATCH 2/8] refactor: Rename RenderingCache to PartialRenderingCache --- internal/core/graphics.rs | 58 ------------------------- internal/core/partial_renderer.rs | 71 ++++++++++++++++++++++++++----- internal/core/window.rs | 2 +- 3 files changed, 61 insertions(+), 70 deletions(-) diff --git a/internal/core/graphics.rs b/internal/core/graphics.rs index 159a383adc9..0706827e98d 100644 --- a/internal/core/graphics.rs +++ b/internal/core/graphics.rs @@ -6,9 +6,6 @@ Graphics Abstractions. This module contains the abstractions and convenience types used for rendering. - - The run-time library also makes use of [RenderingCache] to store the rendering primitives - created by the backend in a type-erased manner. */ extern crate alloc; use crate::api::PlatformError; @@ -83,61 +80,6 @@ impl CachedGraphicsData { } } -/// The RenderingCache, in combination with CachedGraphicsData, allows back ends to store data that's either -/// intensive to compute or has bad CPU locality. Back ends typically keep a RenderingCache instance and use -/// the item's cached_rendering_data() integer as index in the vec_arena::Arena. -/// -/// This is used only for the [`crate::item_rendering::PartialRenderingCache`] -pub struct RenderingCache { - slab: slab::Slab>, - generation: usize, -} - -impl Default for RenderingCache { - fn default() -> Self { - Self { slab: Default::default(), generation: 1 } - } -} - -impl RenderingCache { - /// Returns the generation of the cache. The generation starts at 1 and is increased - /// whenever the cache is cleared, for example when the GL context is lost. - pub fn generation(&self) -> usize { - self.generation - } - - /// Retrieves a mutable reference to the cached graphics data at index. - pub fn get_mut(&mut self, index: usize) -> Option<&mut CachedGraphicsData> { - self.slab.get_mut(index) - } - - /// Returns true if a cache entry exists for the given index. - pub fn contains(&self, index: usize) -> bool { - self.slab.contains(index) - } - - /// Inserts data into the cache and returns the index for retrieval later. - pub fn insert(&mut self, data: CachedGraphicsData) -> usize { - self.slab.insert(data) - } - - /// Retrieves an immutable reference to the cached graphics data at index. - pub fn get(&self, index: usize) -> Option<&CachedGraphicsData> { - self.slab.get(index) - } - - /// Removes the cached graphics data at the given index. - pub fn remove(&mut self, index: usize) -> CachedGraphicsData { - self.slab.remove(index) - } - - /// Removes all entries from the cache and increases the cache's generation count, so - /// that stale index access can be avoided. - pub fn clear(&mut self) { - self.slab.clear(); - self.generation += 1; - } -} /// FontRequest collects all the developer-configurable properties for fonts, such as family, weight, etc. /// It is submitted as a request to the platform font system (i.e. CoreText on macOS) and in exchange the /// backend returns a `Box`. diff --git a/internal/core/partial_renderer.rs b/internal/core/partial_renderer.rs index caad32d6989..3613d97a25e 100644 --- a/internal/core/partial_renderer.rs +++ b/internal/core/partial_renderer.rs @@ -3,7 +3,6 @@ //! Module for a renderer proxy that tries to render only the parts of the tree that have changed. -use super::graphics::RenderingCache; use crate::graphics::CachedGraphicsData; use crate::item_rendering::{ ItemRenderer, ItemRendererFeatures, RenderBorderRectangle, RenderImage, RenderRectangle, @@ -42,7 +41,10 @@ impl CachedRenderingData { /// This function can be used to remove an entry from the rendering cache for a given item, if it /// exists, i.e. if any data was ever cached. This is typically called by the graphics backend's /// implementation of the release_item_graphics_cache function. - pub fn release(&self, cache: &mut RenderingCache) -> Option { + fn release( + &self, + cache: &mut PartialRendererCache, + ) -> Option { if self.cache_generation.get() == cache.generation() { let index = self.cache_index.get(); self.cache_generation.set(0); @@ -53,10 +55,10 @@ impl CachedRenderingData { } /// Return the value if it is in the cache - pub fn get_entry<'a, T>( + fn get_entry<'a>( &self, - cache: &'a mut RenderingCache, - ) -> Option<&'a mut crate::graphics::CachedGraphicsData> { + cache: &'a mut PartialRendererCache, + ) -> Option<&'a mut CachedGraphicsData> { let index = self.cache_index.get(); if self.cache_generation.get() == cache.generation() { cache.get_mut(index) @@ -148,7 +150,52 @@ impl CachedItemBoundingBoxAndTransform { } /// The cache that needs to be held by the Window for the partial rendering -pub type PartialRenderingCache = RenderingCache; +struct PartialRendererCache { + slab: slab::Slab>, + generation: usize, +} + +impl Default for PartialRendererCache { + fn default() -> Self { + Self { slab: Default::default(), generation: 1 } + } +} + +impl PartialRendererCache { + /// Returns the generation of the cache. The generation starts at 1 and is increased + /// whenever the cache is cleared, for example when the GL context is lost. + pub fn generation(&self) -> usize { + self.generation + } + + /// Retrieves a mutable reference to the cached graphics data at index. + pub fn get_mut( + &mut self, + index: usize, + ) -> Option<&mut CachedGraphicsData> { + self.slab.get_mut(index) + } + + /// Inserts data into the cache and returns the index for retrieval later. + pub fn insert(&mut self, data: CachedGraphicsData) -> usize { + self.slab.insert(data) + } + + /// Removes the cached graphics data at the given index. + pub fn remove( + &mut self, + index: usize, + ) -> CachedGraphicsData { + self.slab.remove(index) + } + + /// Removes all entries from the cache and increases the cache's generation count, so + /// that stale index access can be avoided. + pub fn clear(&mut self) { + self.slab.clear(); + self.generation += 1; + } +} /// A region composed of a few rectangles that need to be redrawn. #[derive(Default, Clone, Debug)] @@ -285,8 +332,10 @@ pub enum RepaintBufferType { } /// Put this structure in the renderer to help with partial rendering +/// +/// This is constructed from a [`PartialRenderingState`] pub struct PartialRenderer<'a, T> { - cache: &'a RefCell, + cache: &'a RefCell, /// The region of the screen which is considered dirty and that should be repainted pub dirty_region: DirtyRegion, /// The actual renderer which the drawing call will be forwarded to @@ -297,8 +346,8 @@ pub struct PartialRenderer<'a, T> { impl<'a, T: ItemRenderer + ItemRendererFeatures> PartialRenderer<'a, T> { /// Create a new PartialRenderer - pub fn new( - cache: &'a RefCell, + fn new( + cache: &'a RefCell, initial_dirty_region: DirtyRegion, actual_renderer: T, ) -> Self { @@ -491,7 +540,7 @@ impl<'a, T: ItemRenderer + ItemRendererFeatures> PartialRenderer<'a, T> { } fn do_rendering( - cache: &RefCell, + cache: &RefCell, rendering_data: &CachedRenderingData, render_fn: impl FnOnce() -> CachedItemBoundingBoxAndTransform, ) { @@ -670,7 +719,7 @@ impl ItemRenderer for PartialRenderer<'_ /// of each item. This permits a more fine-grained computation of the region that needs to be repainted. #[derive(Default)] pub struct PartialRenderingState { - partial_cache: RefCell, + 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, /// Force a redraw in the next frame, no matter what's dirty. Use only as a last resort. diff --git a/internal/core/window.rs b/internal/core/window.rs index 4d1c9dd9616..52dbab91eac 100644 --- a/internal/core/window.rs +++ b/internal/core/window.rs @@ -170,7 +170,7 @@ pub trait WindowAdapterInternal { fn register_item_tree(&self) {} /// This function is called by the generated code when a component and therefore its tree of items are destroyed. The - /// implementation typically uses this to free the underlying graphics resources cached via [`crate::graphics::RenderingCache`]. + /// implementation typically uses this to free the underlying graphics resources. fn unregister_item_tree( &self, _component: ItemTreeRef, From 45f9b40b3ce39d17c0da5edef0adadb94df1c1fb Mon Sep 17 00:00:00 2001 From: Olivier Goffart Date: Fri, 19 Sep 2025 15:47:10 +0200 Subject: [PATCH 3/8] partial_renderer: Separate the geometry tracker from the rendering tracker This way we avoid rendering when only the layout is dirty but didn't actually change --- api/rs/slint/tests/partial_renderer.rs | 39 ++++++ internal/core/partial_renderer.rs | 164 ++++++++++++++++--------- 2 files changed, 147 insertions(+), 56 deletions(-) diff --git a/api/rs/slint/tests/partial_renderer.rs b/api/rs/slint/tests/partial_renderer.rs index 5f77bb63129..a75cebca2d4 100644 --- a/api/rs/slint/tests/partial_renderer.rs +++ b/api/rs/slint/tests/partial_renderer.rs @@ -681,3 +681,42 @@ fn text_alignment() { Some(slint::LogicalPosition { x: 10., y: 10. }) ); } + +#[test] +fn nowrap_text_change_doesnt_change_height() { + slint::slint! { + export component Ui inherits Window { + in property first-text: "First text"; + out property first-label-width: first-label.width; + out property first-label-height: first-label.height; + background: black; + VerticalLayout { + first-label := Text { + text: root.first-text; + } + Text { + text: "Second text"; + } + } + } + } + + slint::platform::set_platform(Box::new(TestPlatform)).ok(); + let ui = Ui::new().unwrap(); + let window = WINDOW.with(|x| x.clone()); + window.set_size(slint::PhysicalSize::new(180, 260)); + ui.show().unwrap(); + assert!(window.draw_if_needed(|renderer| { + do_test_render_region(renderer, 0, 0, 180, 260); + })); + assert!(!window.draw_if_needed(|_| { unreachable!() })); + ui.set_first_text("Hello World longer".into()); + + let expected_width = ui.get_first_label_width().ceil() as _; + let expected_height = ui.get_first_label_height().ceil() as _; + + assert!(window.draw_if_needed(|renderer| { + do_test_render_region(renderer, 0, 0, expected_width, expected_height); + })); + assert!(!window.draw_if_needed(|_| { unreachable!() })); +} diff --git a/internal/core/partial_renderer.rs b/internal/core/partial_renderer.rs index 3613d97a25e..e6ce0dc51f4 100644 --- a/internal/core/partial_renderer.rs +++ b/internal/core/partial_renderer.rs @@ -3,7 +3,6 @@ //! Module for a renderer proxy that tries to render only the parts of the tree that have changed. -use crate::graphics::CachedGraphicsData; use crate::item_rendering::{ ItemRenderer, ItemRendererFeatures, RenderBorderRectangle, RenderImage, RenderRectangle, RenderText, @@ -58,7 +57,7 @@ impl CachedRenderingData { fn get_entry<'a>( &self, cache: &'a mut PartialRendererCache, - ) -> Option<&'a mut CachedGraphicsData> { + ) -> Option<&'a mut PartialRenderingCachedData> { let index = self.cache_index.get(); if self.cache_generation.get() == cache.generation() { cache.get_mut(index) @@ -70,7 +69,7 @@ impl CachedRenderingData { /// After rendering an item, we cache the geometry and the transform it applies to /// children. -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub enum CachedItemBoundingBoxAndTransform { /// A regular item with a translation RegularItem { @@ -149,9 +148,45 @@ impl CachedItemBoundingBoxAndTransform { } } +/// A pair of property trackers that are used to track changes in the geometry and rendering of an item. +#[pin_project::pin_project] +#[derive(Default)] +pub struct PropertyTrackerPair { + /// track only the change in the geometry + #[pin] + pub geometry: PropertyTracker, + /// track only the change in the rendering + #[pin] + pub rendering: PropertyTracker, +} + +struct PartialRenderingCachedData { + /// The backend specific data. + pub data: CachedItemBoundingBoxAndTransform, + /// The property tracker that should be used to evaluate whether the primitive needs to be re-created + /// or not. + pub dependency_tracker: core::pin::Pin>, +} +impl PartialRenderingCachedData { + fn new( + item_rc: &ItemRc, + window_adapter: &Rc, + render_fn: impl FnOnce(), + ) -> Self { + let dependency_tracker = Box::pin(PropertyTrackerPair::default()); + dependency_tracker.as_ref().project_ref().rendering.evaluate(render_fn); + let data = dependency_tracker + .as_ref() + .project_ref() + .geometry + .evaluate(|| CachedItemBoundingBoxAndTransform::new::(item_rc, window_adapter)); + Self { data, dependency_tracker } + } +} + /// The cache that needs to be held by the Window for the partial rendering struct PartialRendererCache { - slab: slab::Slab>, + slab: slab::Slab, generation: usize, } @@ -169,23 +204,17 @@ impl PartialRendererCache { } /// Retrieves a mutable reference to the cached graphics data at index. - pub fn get_mut( - &mut self, - index: usize, - ) -> Option<&mut CachedGraphicsData> { + pub fn get_mut(&mut self, index: usize) -> Option<&mut PartialRenderingCachedData> { self.slab.get_mut(index) } /// Inserts data into the cache and returns the index for retrieval later. - pub fn insert(&mut self, data: CachedGraphicsData) -> usize { + pub fn insert(&mut self, data: PartialRenderingCachedData) -> usize { self.slab.insert(data) } /// Removes the cached graphics data at the given index. - pub fn remove( - &mut self, - index: usize, - ) -> CachedGraphicsData { + pub fn remove(&mut self, index: usize) -> PartialRenderingCachedData { self.slab.remove(index) } @@ -393,13 +422,22 @@ impl<'a, T: ItemRenderer + ItemRendererFeatures> PartialRenderer<'a, T> { let item_rc = ItemRc::new(component.clone(), index); match item.cached_rendering_data_offset().get_entry(&mut borrowed) { - Some(CachedGraphicsData { + Some(PartialRenderingCachedData { data: cached_geom, - dependency_tracker: Some(tr), + dependency_tracker: tr, }) => { - if tr.is_dirty() { + let rendering_dirty = tr.rendering.is_dirty(); + let geometry_dirty = tr.geometry.is_dirty(); + if ItemRef::downcast_pin::(item).is_some() + || ItemRef::downcast_pin::(item).is_some() + { + // When the opacity or the clip change, this will impact all the children, including + // the ones outside the element, regardless if they are themselves dirty or not. + new_state.must_refresh_children |= rendering_dirty | geometry_dirty; + } + + if geometry_dirty { let old_geom = cached_geom.clone(); - drop(borrowed); let new_geom = crate::properties::evaluate_no_tracking(|| { CachedItemBoundingBoxAndTransform::new::( &item_rc, @@ -407,33 +445,49 @@ impl<'a, T: ItemRenderer + ItemRendererFeatures> PartialRenderer<'a, T> { ) }); + if old_geom != new_geom { + self.mark_dirty_rect( + old_geom.bounding_rect(), + state.old_transform_to_screen, + &state.clipped, + ); + self.mark_dirty_rect( + new_geom.bounding_rect(), + state.transform_to_screen, + &state.clipped, + ); + + new_state.adjust_transforms_for_child( + &new_geom.transform(), + &old_geom.transform(), + ); + + return ItemVisitorResult::Continue(new_state); + } + } + + new_state.adjust_transforms_for_child( + &cached_geom.transform(), + &cached_geom.transform(), + ); + + if rendering_dirty { self.mark_dirty_rect( - old_geom.bounding_rect(), - state.old_transform_to_screen, - &state.clipped, - ); - self.mark_dirty_rect( - new_geom.bounding_rect(), + cached_geom.bounding_rect(), state.transform_to_screen, &state.clipped, ); - new_state.adjust_transforms_for_child( - &new_geom.transform(), - &old_geom.transform(), - ); - - if ItemRef::downcast_pin::(item).is_some() - || ItemRef::downcast_pin::(item).is_some() - { - // When the opacity or the clip change, this will impact all the children, including - // the ones outside the element, regardless if they are themselves dirty or not. - new_state.must_refresh_children = true; - } - ItemVisitorResult::Continue(new_state) } else { - tr.as_ref().register_as_dependency_to_current_binding(); + tr.as_ref() + .project_ref() + .geometry + .register_as_dependency_to_current_binding(); + tr.as_ref() + .project_ref() + .rendering + .register_as_dependency_to_current_binding(); if state.must_refresh_children || new_state.transform_to_screen @@ -451,11 +505,6 @@ impl<'a, T: ItemRenderer + ItemRendererFeatures> PartialRenderer<'a, T> { ); } - new_state.adjust_transforms_for_child( - &cached_geom.transform(), - &cached_geom.transform(), - ); - if let CachedItemBoundingBoxAndTransform::ClipItem { geometry } = &cached_geom { @@ -542,17 +591,20 @@ impl<'a, T: ItemRenderer + ItemRendererFeatures> PartialRenderer<'a, T> { fn do_rendering( cache: &RefCell, rendering_data: &CachedRenderingData, - render_fn: impl FnOnce() -> CachedItemBoundingBoxAndTransform, + item_rc: &ItemRc, + window_adapter: &Rc, + render_fn: impl FnOnce(), ) { let mut cache = cache.borrow_mut(); if let Some(entry) = rendering_data.get_entry(&mut cache) { - entry - .dependency_tracker - .get_or_insert_with(|| Box::pin(PropertyTracker::default())) - .as_ref() - .evaluate(render_fn); + let trackers = &entry.dependency_tracker; + trackers.as_ref().project_ref().geometry.evaluate_if_dirty(|| { + CachedItemBoundingBoxAndTransform::new::(&item_rc, window_adapter) + }); + trackers.as_ref().project_ref().rendering.evaluate(render_fn); } else { - let cache_entry = crate::graphics::CachedGraphicsData::new(render_fn); + let cache_entry = + PartialRenderingCachedData::new::(item_rc, window_adapter, render_fn); rendering_data.cache_index.set(cache.insert(cache_entry)); rendering_data.cache_generation.set(cache.generation()); } @@ -568,9 +620,8 @@ macro_rules! forward_rendering_call { (fn $fn:ident($Ty:ty) $(-> $Ret:ty)?) => { fn $fn(&mut self, obj: Pin<&$Ty>, item_rc: &ItemRc, size: LogicalSize) $(-> $Ret)? { let mut ret = None; - Self::do_rendering(&self.cache, &obj.cached_rendering_data, || { + Self::do_rendering(&self.cache, &obj.cached_rendering_data, item_rc, &self.window_adapter, || { ret = Some(self.actual_renderer.$fn(obj, item_rc, size)); - CachedItemBoundingBoxAndTransform::new::(&item_rc, &self.window_adapter) }); ret.unwrap_or_default() } @@ -581,9 +632,8 @@ macro_rules! forward_rendering_call2 { (fn $fn:ident($Ty:ty) $(-> $Ret:ty)?) => { fn $fn(&mut self, obj: Pin<&$Ty>, item_rc: &ItemRc, size: LogicalSize, cache: &CachedRenderingData) $(-> $Ret)? { let mut ret = None; - Self::do_rendering(&self.cache, &cache, || { + Self::do_rendering(&self.cache, &cache, item_rc, &self.window_adapter, || { ret = Some(self.actual_renderer.$fn(obj, item_rc, size, &cache)); - CachedItemBoundingBoxAndTransform::new::(&item_rc, &self.window_adapter) }); ret.unwrap_or_default() } @@ -605,15 +655,17 @@ impl ItemRenderer for PartialRenderer<'_ let rendering_data = item.cached_rendering_data_offset(); let mut cache = self.cache.borrow_mut(); let item_bounding_rect = match rendering_data.get_entry(&mut cache) { - Some(CachedGraphicsData { data, dependency_tracker }) => { + Some(PartialRenderingCachedData { data, dependency_tracker }) => { dependency_tracker - .get_or_insert_with(|| Box::pin(PropertyTracker::default())) .as_ref() + .project_ref() + .geometry .evaluate_if_dirty(|| *data = eval()); *data.bounding_rect() } None => { - let cache_entry = crate::graphics::CachedGraphicsData::new(eval); + let cache_entry = + PartialRenderingCachedData::new::(item_rc, window_adapter, || ()); let geom = cache_entry.data.clone(); rendering_data.cache_index.set(cache.insert(cache_entry)); rendering_data.cache_generation.set(cache.generation()); From 9104626576db7559b88dd32bb8988fc9a0661776 Mon Sep 17 00:00:00 2001 From: Olivier Goffart Date: Mon, 22 Sep 2025 11:30:05 +0200 Subject: [PATCH 4/8] partial_renderer: Save memory by not having a tracker for the geometry We just always query all the geometry anyway --- internal/core/partial_renderer.rs | 222 +++++++++++++----------------- 1 file changed, 95 insertions(+), 127 deletions(-) diff --git a/internal/core/partial_renderer.rs b/internal/core/partial_renderer.rs index e6ce0dc51f4..4eda317372b 100644 --- a/internal/core/partial_renderer.rs +++ b/internal/core/partial_renderer.rs @@ -2,6 +2,18 @@ // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 //! Module for a renderer proxy that tries to render only the parts of the tree that have changed. +//! +//! This is the way the partial renderer work: +//! +//! 1. [`PartialRenderer::compute_dirty_regions`] will go over the items and try to compute the region that needs to be repainted. +//! If either the bounding box has changed, or the PropertyTracker that tracks the rendering properties is dierty, then the +//! region is marked dirty. +//! That pass also register dependencies on every geometry, and on the non-dirty property trackers. +//! 2. The Renderer calls [`PartialRenderer::filter_item`] For most items. +//! This assume that the cached geometry was requested in the previous step. So it will not register new dependencies. +//! 3. Then the renderer calls the rendering function for each item that needs to be rendered. +//! This register dependencies only on the rendering tracker. +//! use crate::item_rendering::{ ItemRenderer, ItemRendererFeatures, RenderBorderRectangle, RenderImage, RenderRectangle, @@ -148,39 +160,15 @@ impl CachedItemBoundingBoxAndTransform { } } -/// A pair of property trackers that are used to track changes in the geometry and rendering of an item. -#[pin_project::pin_project] -#[derive(Default)] -pub struct PropertyTrackerPair { - /// track only the change in the geometry - #[pin] - pub geometry: PropertyTracker, - /// track only the change in the rendering - #[pin] - pub rendering: PropertyTracker, -} - struct PartialRenderingCachedData { - /// The backend specific data. + /// The geometry of the item as it was previously rendered. pub data: CachedItemBoundingBoxAndTransform, - /// The property tracker that should be used to evaluate whether the primitive needs to be re-created - /// or not. - pub dependency_tracker: core::pin::Pin>, + /// The property tracker that should be used to evaluate whether the item needs to be re-rendered + pub tracker: Option>>, } impl PartialRenderingCachedData { - fn new( - item_rc: &ItemRc, - window_adapter: &Rc, - render_fn: impl FnOnce(), - ) -> Self { - let dependency_tracker = Box::pin(PropertyTrackerPair::default()); - dependency_tracker.as_ref().project_ref().rendering.evaluate(render_fn); - let data = dependency_tracker - .as_ref() - .project_ref() - .geometry - .evaluate(|| CachedItemBoundingBoxAndTransform::new::(item_rc, window_adapter)); - Self { data, dependency_tracker } + fn new(data: CachedItemBoundingBoxAndTransform) -> Self { + Self { data, tracker: None } } } @@ -418,52 +406,48 @@ impl<'a, T: ItemRenderer + ItemRendererFeatures> PartialRenderer<'a, T> { crate::item_tree::TraversalOrder::BackToFront, |component, item, index, state| { let mut new_state = *state; - let mut borrowed = self.cache.borrow_mut(); let item_rc = ItemRc::new(component.clone(), index); + let rendering_data = item.cached_rendering_data_offset(); + let mut cache = self.cache.borrow_mut(); + + match rendering_data.get_entry(&mut cache) { + Some(PartialRenderingCachedData { data: cached_geom, tracker }) => { + let rendering_dirty = tracker.as_ref().is_some_and(|tr| tr.is_dirty()); + let old_geom = cached_geom.clone(); + let new_geom = CachedItemBoundingBoxAndTransform::new::( + &item_rc, + &self.window_adapter, + ); - match item.cached_rendering_data_offset().get_entry(&mut borrowed) { - Some(PartialRenderingCachedData { - data: cached_geom, - dependency_tracker: tr, - }) => { - let rendering_dirty = tr.rendering.is_dirty(); - let geometry_dirty = tr.geometry.is_dirty(); + let geometry_changed = old_geom != new_geom; if ItemRef::downcast_pin::(item).is_some() || ItemRef::downcast_pin::(item).is_some() { // When the opacity or the clip change, this will impact all the children, including // the ones outside the element, regardless if they are themselves dirty or not. - new_state.must_refresh_children |= rendering_dirty | geometry_dirty; + new_state.must_refresh_children |= rendering_dirty || geometry_changed; } - if geometry_dirty { - let old_geom = cached_geom.clone(); - let new_geom = crate::properties::evaluate_no_tracking(|| { - CachedItemBoundingBoxAndTransform::new::( - &item_rc, - &self.window_adapter, - ) - }); + if geometry_changed { + self.mark_dirty_rect( + old_geom.bounding_rect(), + state.old_transform_to_screen, + &state.clipped, + ); + self.mark_dirty_rect( + new_geom.bounding_rect(), + state.transform_to_screen, + &state.clipped, + ); - if old_geom != new_geom { - self.mark_dirty_rect( - old_geom.bounding_rect(), - state.old_transform_to_screen, - &state.clipped, - ); - self.mark_dirty_rect( - new_geom.bounding_rect(), - state.transform_to_screen, - &state.clipped, - ); + new_state.adjust_transforms_for_child( + &new_geom.transform(), + &old_geom.transform(), + ); - new_state.adjust_transforms_for_child( - &new_geom.transform(), - &old_geom.transform(), - ); + *cached_geom = new_geom; - return ItemVisitorResult::Continue(new_state); - } + return ItemVisitorResult::Continue(new_state); } new_state.adjust_transforms_for_child( @@ -480,15 +464,6 @@ impl<'a, T: ItemRenderer + ItemRendererFeatures> PartialRenderer<'a, T> { ItemVisitorResult::Continue(new_state) } else { - tr.as_ref() - .project_ref() - .geometry - .register_as_dependency_to_current_binding(); - tr.as_ref() - .project_ref() - .rendering - .register_as_dependency_to_current_binding(); - if state.must_refresh_children || new_state.transform_to_screen != new_state.old_transform_to_screen @@ -503,6 +478,10 @@ impl<'a, T: ItemRenderer + ItemRendererFeatures> PartialRenderer<'a, T> { state.transform_to_screen, &state.clipped, ); + } else { + if let Some(tr) = &tracker { + tr.as_ref().register_as_dependency_to_current_binding(); + } } if let CachedItemBoundingBoxAndTransform::ClipItem { geometry } = @@ -527,32 +506,31 @@ impl<'a, T: ItemRenderer + ItemRendererFeatures> PartialRenderer<'a, T> { ItemVisitorResult::Continue(new_state) } } - _ => { - drop(borrowed); - let bounding_rect = crate::properties::evaluate_no_tracking(|| { - let geom = CachedItemBoundingBoxAndTransform::new::( - &item_rc, - &self.window_adapter, - ); - - new_state - .adjust_transforms_for_child(&geom.transform(), &geom.transform()); + None => { + let geom = CachedItemBoundingBoxAndTransform::new::( + &item_rc, + &self.window_adapter, + ); + let cache_entry = PartialRenderingCachedData::new(geom.clone()); + rendering_data.cache_index.set(cache.insert(cache_entry)); + rendering_data.cache_generation.set(cache.generation()); + + new_state.adjust_transforms_for_child(&geom.transform(), &geom.transform()); + + if let CachedItemBoundingBoxAndTransform::ClipItem { geometry } = geom { + new_state.clipped = new_state + .clipped + .intersection( + &state + .transform_to_screen + .outer_transformed_rect(&geometry.cast()) + .cast(), + ) + .unwrap_or_default(); + } - if let CachedItemBoundingBoxAndTransform::ClipItem { geometry } = geom { - new_state.clipped = new_state - .clipped - .intersection( - &state - .transform_to_screen - .outer_transformed_rect(&geometry.cast()) - .cast(), - ) - .unwrap_or_default(); - } - *geom.bounding_rect() - }); self.mark_dirty_rect( - &bounding_rect, + &geom.bounding_rect(), state.transform_to_screen, &state.clipped, ); @@ -597,16 +575,24 @@ impl<'a, T: ItemRenderer + ItemRendererFeatures> PartialRenderer<'a, T> { ) { let mut cache = cache.borrow_mut(); if let Some(entry) = rendering_data.get_entry(&mut cache) { - let trackers = &entry.dependency_tracker; - trackers.as_ref().project_ref().geometry.evaluate_if_dirty(|| { - CachedItemBoundingBoxAndTransform::new::(&item_rc, window_adapter) - }); - trackers.as_ref().project_ref().rendering.evaluate(render_fn); + entry + .tracker + .get_or_insert_with(|| Box::pin(PropertyTracker::default())) + .as_ref() + .evaluate(render_fn); } else { - let cache_entry = - PartialRenderingCachedData::new::(item_rc, window_adapter, render_fn); + let mut cache_entry = PartialRenderingCachedData::new( + CachedItemBoundingBoxAndTransform::new::(item_rc, window_adapter), + ); + cache_entry + .tracker + .get_or_insert_with(|| Box::pin(PropertyTracker::default())) + .as_ref() + .evaluate(render_fn); rendering_data.cache_index.set(cache.insert(cache_entry)); rendering_data.cache_generation.set(cache.generation()); + + unreachable!("compute_dirty_regions should have already computed the geometry") } } @@ -644,33 +630,18 @@ impl ItemRenderer for PartialRenderer<'_ fn filter_item( &mut self, item_rc: &ItemRc, - window_adapter: &Rc, + _window_adapter: &Rc, ) -> (bool, LogicalRect) { let item = item_rc.borrow(); - let eval = || { - // registers dependencies on the geometry and clip properties. - CachedItemBoundingBoxAndTransform::new::(item_rc, window_adapter) - }; + + // Query untracked, as the bounding rect calculation already registers a dependency on the geometry. + let item_geometry = crate::properties::evaluate_no_tracking(|| item_rc.geometry()); let rendering_data = item.cached_rendering_data_offset(); let mut cache = self.cache.borrow_mut(); let item_bounding_rect = match rendering_data.get_entry(&mut cache) { - Some(PartialRenderingCachedData { data, dependency_tracker }) => { - dependency_tracker - .as_ref() - .project_ref() - .geometry - .evaluate_if_dirty(|| *data = eval()); - *data.bounding_rect() - } - None => { - let cache_entry = - PartialRenderingCachedData::new::(item_rc, window_adapter, || ()); - let geom = cache_entry.data.clone(); - rendering_data.cache_index.set(cache.insert(cache_entry)); - rendering_data.cache_generation.set(cache.generation()); - *geom.bounding_rect() - } + Some(PartialRenderingCachedData { data, tracker: _ }) => *data.bounding_rect(), + None => unreachable!("compute_dirty_regions should have already computed the geometry"), }; let clipped_geom = self.get_current_clip().intersection(&item_bounding_rect); @@ -679,9 +650,6 @@ impl ItemRenderer for PartialRenderer<'_ self.dirty_region.draw_intersects(clipped_geom) }); - // Query untracked, as the bounding rect calculation already registers a dependency on the geometry. - let item_geometry = crate::properties::evaluate_no_tracking(|| item_rc.geometry()); - (draw, item_geometry) } From 6b51cb442d1668290e6c36d008cd74c7fef08f89 Mon Sep 17 00:00:00 2001 From: Olivier Goffart Date: Wed, 8 Oct 2025 11:09:38 +0200 Subject: [PATCH 5/8] Remove Text's bounding rect implementation Since every renderer is now always clipping the text, we don't need to account for out of bound drawing. The other optimization of trying not to draw things if the bounding box is smaller is not really a good one since computing the bounding box is itself too expensive and not cached. --- internal/core/item_rendering.rs | 29 +--------------------------- internal/core/items/text.rs | 34 ++++++++++----------------------- 2 files changed, 11 insertions(+), 52 deletions(-) diff --git a/internal/core/item_rendering.rs b/internal/core/item_rendering.rs index 4b549f2335f..288c52de32f 100644 --- a/internal/core/item_rendering.rs +++ b/internal/core/item_rendering.rs @@ -10,10 +10,9 @@ use crate::item_tree::ItemTreeRc; use crate::item_tree::{ItemVisitor, ItemVisitorVTable, VisitChildrenResult}; use crate::lengths::{ LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, LogicalVector, - SizeLengths, }; pub use crate::partial_renderer::CachedRenderingData; -use crate::window::{WindowAdapter, WindowInner}; +use crate::window::WindowAdapter; use crate::{Brush, SharedString}; #[cfg(feature = "std")] use alloc::boxed::Box; @@ -282,32 +281,6 @@ pub trait RenderText { fn overflow(self: Pin<&Self>) -> TextOverflow; fn letter_spacing(self: Pin<&Self>) -> LogicalLength; fn stroke(self: Pin<&Self>) -> (Brush, LogicalLength, TextStrokeStyle); - - fn text_bounding_rect( - self: Pin<&Self>, - self_rc: &ItemRc, - window_adapter: &Rc, - mut geometry: euclid::Rect, - ) -> euclid::Rect { - let window_inner = WindowInner::from_pub(window_adapter.window()); - let text_string = self.text(); - let font_request = self.font_request(self_rc); - let scale_factor = crate::lengths::ScaleFactor::new(window_inner.scale_factor()); - let max_width = geometry.size.width_length(); - geometry.size = geometry.size.max( - window_adapter - .renderer() - .text_size( - font_request.clone(), - text_string.as_str(), - Some(max_width.cast()), - scale_factor, - self.wrap(), - ) - .cast(), - ); - geometry - } } impl RenderText for (SharedString, Brush) { diff --git a/internal/core/items/text.rs b/internal/core/items/text.rs index f03e02d1c83..e67c7f3ef79 100644 --- a/internal/core/items/text.rs +++ b/internal/core/items/text.rs @@ -20,9 +20,7 @@ use crate::input::{ }; use crate::item_rendering::{CachedRenderingData, ItemRenderer, RenderText}; use crate::layout::{LayoutInfo, Orientation}; -use crate::lengths::{ - LogicalLength, LogicalPoint, LogicalRect, LogicalSize, ScaleFactor, SizeLengths, -}; +use crate::lengths::{LogicalLength, LogicalPoint, LogicalRect, LogicalSize, ScaleFactor}; use crate::platform::Clipboard; #[cfg(feature = "rtti")] use crate::rtti::*; @@ -138,11 +136,11 @@ impl Item for ComplexText { fn bounding_rect( self: core::pin::Pin<&Self>, - window_adapter: &Rc, - self_rc: &ItemRc, + _window_adapter: &Rc, + _self_rc: &ItemRc, geometry: LogicalRect, ) -> LogicalRect { - self.text_bounding_rect(self_rc, window_adapter, geometry.cast()).cast() + geometry } fn clips_children(self: core::pin::Pin<&Self>) -> bool { @@ -309,11 +307,11 @@ impl Item for SimpleText { fn bounding_rect( self: core::pin::Pin<&Self>, - window_adapter: &Rc, - self_rc: &ItemRc, + _window_adapter: &Rc, + _self_rc: &ItemRc, geometry: LogicalRect, ) -> LogicalRect { - self.text_bounding_rect(self_rc, window_adapter, geometry.cast()).cast() + geometry } fn clips_children(self: core::pin::Pin<&Self>) -> bool { @@ -1030,22 +1028,10 @@ impl Item for TextInput { fn bounding_rect( self: core::pin::Pin<&Self>, - window_adapter: &Rc, - self_rc: &ItemRc, - mut geometry: LogicalRect, + _window_adapter: &Rc, + _self_rc: &ItemRc, + geometry: LogicalRect, ) -> LogicalRect { - let window_inner = WindowInner::from_pub(window_adapter.window()); - let text_string = self.text(); - let font_request = self.font_request(self_rc); - let scale_factor = crate::lengths::ScaleFactor::new(window_inner.scale_factor()); - let max_width = geometry.size.width_length(); - geometry.size = geometry.size.max(window_adapter.renderer().text_size( - font_request.clone(), - text_string.as_str(), - Some(max_width), - scale_factor, - self.wrap(), - )); geometry } From 2c4a064b358ec9fdfa900550da7cdd03739d8ebe Mon Sep 17 00:00:00 2001 From: Olivier Goffart Date: Wed, 8 Oct 2025 13:54:07 +0200 Subject: [PATCH 6/8] Do not re-query the geometry for the clip --- internal/core/item_rendering.rs | 8 +++----- internal/core/partial_renderer.rs | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/internal/core/item_rendering.rs b/internal/core/item_rendering.rs index 288c52de32f..8f9c5eeae2b 100644 --- a/internal/core/item_rendering.rs +++ b/internal/core/item_rendering.rs @@ -403,14 +403,12 @@ pub trait ItemRenderer { fn visit_clip( &mut self, clip_item: Pin<&Clip>, - item_rc: &ItemRc, - _size: LogicalSize, + _item_rc: &ItemRc, + size: LogicalSize, ) -> RenderingResult { if clip_item.clip() { - let geometry = item_rc.geometry(); - let clip_region_valid = self.combine_clip( - LogicalRect::new(LogicalPoint::default(), geometry.size), + LogicalRect::new(LogicalPoint::default(), size), clip_item.logical_border_radius(), clip_item.border_width(), ); diff --git a/internal/core/partial_renderer.rs b/internal/core/partial_renderer.rs index 4eda317372b..a9024fcc9c0 100644 --- a/internal/core/partial_renderer.rs +++ b/internal/core/partial_renderer.rs @@ -6,7 +6,7 @@ //! This is the way the partial renderer work: //! //! 1. [`PartialRenderer::compute_dirty_regions`] will go over the items and try to compute the region that needs to be repainted. -//! If either the bounding box has changed, or the PropertyTracker that tracks the rendering properties is dierty, then the +//! If either the bounding box has changed, or the PropertyTracker that tracks the rendering properties is dirty, then the //! region is marked dirty. //! That pass also register dependencies on every geometry, and on the non-dirty property trackers. //! 2. The Renderer calls [`PartialRenderer::filter_item`] For most items. From 53dcecb7c38c5a68a7a088586b782e5c4d1624a6 Mon Sep 17 00:00:00 2001 From: Olivier Goffart Date: Wed, 8 Oct 2025 15:51:42 +0200 Subject: [PATCH 7/8] partial_renderer: don't visit children of clipped item No chance we find something dirty there --- internal/core/item_tree.rs | 3 ++- internal/core/partial_renderer.rs | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/internal/core/item_tree.rs b/internal/core/item_tree.rs index e9cf5f69ea1..b4657d1e521 100644 --- a/internal/core/item_tree.rs +++ b/internal/core/item_tree.rs @@ -1102,6 +1102,7 @@ impl) -> VisitChildrenResult> ItemVisito } pub enum ItemVisitorResult { Continue(State), + SkipChildren, Abort, } @@ -1132,7 +1133,7 @@ fn visit_internal( ItemVisitorResult::Continue(state) => { visit_internal(item_tree, order, visitor, index as isize, &state) } - + ItemVisitorResult::SkipChildren => VisitChildrenResult::CONTINUE, ItemVisitorResult::Abort => VisitChildrenResult::abort(index, 0), } }; diff --git a/internal/core/partial_renderer.rs b/internal/core/partial_renderer.rs index a9024fcc9c0..1bcd8172ef7 100644 --- a/internal/core/partial_renderer.rs +++ b/internal/core/partial_renderer.rs @@ -502,6 +502,9 @@ impl<'a, T: ItemRenderer + ItemRendererFeatures> PartialRenderer<'a, T> { ), ) .unwrap_or_default(); + if new_state.clipped.is_empty() { + return ItemVisitorResult::SkipChildren; + } } ItemVisitorResult::Continue(new_state) } @@ -534,7 +537,11 @@ impl<'a, T: ItemRenderer + ItemRendererFeatures> PartialRenderer<'a, T> { state.transform_to_screen, &state.clipped, ); - ItemVisitorResult::Continue(new_state) + if new_state.clipped.is_empty() { + ItemVisitorResult::SkipChildren + } else { + ItemVisitorResult::Continue(new_state) + } } } }, From 8c5c28fbc019ae663a348df398bb126468f8f5d6 Mon Sep 17 00:00:00 2001 From: Olivier Goffart Date: Wed, 8 Oct 2025 16:34:27 +0200 Subject: [PATCH 8/8] Fix `tests/screenshots/cases/software/basic/text-clipped.slint` It uses NaN in one of the coordinate. It used to be fine because we would rely on the tracker to know if anything had changed. But now that we always compare the rectangle and NaN is always != NaN, we would redraw too much for this test --- internal/core/partial_renderer.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/core/partial_renderer.rs b/internal/core/partial_renderer.rs index 1bcd8172ef7..a48ecc4f56d 100644 --- a/internal/core/partial_renderer.rs +++ b/internal/core/partial_renderer.rs @@ -564,6 +564,12 @@ impl<'a, T: ItemRenderer + ItemRendererFeatures> PartialRenderer<'a, T> { transform: ItemTransform, clip_rect: &LogicalRect, ) { + #[cfg(not(slint_int_coord))] + if !rect.origin.is_finite() { + // Account for NaN + return; + } + if !rect.is_empty() { if let Some(rect) = transform.outer_transformed_rect(&rect.cast()).cast().intersection(clip_rect)