diff --git a/api/rs/slint/tests/partial_renderer.rs b/api/rs/slint/tests/partial_renderer.rs index fac85dc8920..a75cebca2d4 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> { @@ -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/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/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/item_rendering.rs b/internal/core/item_rendering.rs index df5b043799e..8f9c5eeae2b 100644 --- a/internal/core/item_rendering.rs +++ b/internal/core/item_rendering.rs @@ -4,67 +4,26 @@ #![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, }; -use crate::properties::PropertyTracker; -use crate::window::{WindowAdapter, WindowInner}; -use crate::{Brush, Coord, SharedString}; +pub use crate::partial_renderer::CachedRenderingData; +use crate::window::WindowAdapter; +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 +35,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 +75,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() @@ -322,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) { @@ -470,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(), ); @@ -571,696 +502,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/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/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 } 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..a48ecc4f56d --- /dev/null +++ b/internal/core/partial_renderer.rs @@ -0,0 +1,841 @@ +// 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. +//! +//! 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 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. +//! 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, + 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. + 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); + Some(cache.remove(index).data) + } else { + None + } + } + + /// Return the value if it is in the cache + fn get_entry<'a>( + &self, + cache: &'a mut PartialRendererCache, + ) -> Option<&'a mut PartialRenderingCachedData> { + 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, PartialEq)] +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() } + } + } +} + +struct PartialRenderingCachedData { + /// The geometry of the item as it was previously rendered. + pub data: CachedItemBoundingBoxAndTransform, + /// The property tracker that should be used to evaluate whether the item needs to be re-rendered + pub tracker: Option>>, +} +impl PartialRenderingCachedData { + fn new(data: CachedItemBoundingBoxAndTransform) -> Self { + Self { data, tracker: None } + } +} + +/// The cache that needs to be held by the Window for the partial rendering +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 PartialRenderingCachedData> { + self.slab.get_mut(index) + } + + /// Inserts data into the cache and returns the index for retrieval later. + 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) -> PartialRenderingCachedData { + 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)] +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 +/// +/// This is constructed from a [`PartialRenderingState`] +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 + 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 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, + ); + + 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_changed; + } + + 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, + ); + + new_state.adjust_transforms_for_child( + &new_geom.transform(), + &old_geom.transform(), + ); + + *cached_geom = new_geom; + + return ItemVisitorResult::Continue(new_state); + } + + new_state.adjust_transforms_for_child( + &cached_geom.transform(), + &cached_geom.transform(), + ); + + if rendering_dirty { + self.mark_dirty_rect( + cached_geom.bounding_rect(), + state.transform_to_screen, + &state.clipped, + ); + + ItemVisitorResult::Continue(new_state) + } else { + 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, + ); + } else { + if let Some(tr) = &tracker { + tr.as_ref().register_as_dependency_to_current_binding(); + } + } + + 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(); + if new_state.clipped.is_empty() { + return ItemVisitorResult::SkipChildren; + } + } + ItemVisitorResult::Continue(new_state) + } + } + 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(); + } + + self.mark_dirty_rect( + &geom.bounding_rect(), + state.transform_to_screen, + &state.clipped, + ); + if new_state.clipped.is_empty() { + ItemVisitorResult::SkipChildren + } else { + 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, + ) { + #[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) + { + self.dirty_region.add_rect(rect); + } + } + } + + fn do_rendering( + cache: &RefCell, + rendering_data: &CachedRenderingData, + 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 + .tracker + .get_or_insert_with(|| Box::pin(PropertyTracker::default())) + .as_ref() + .evaluate(render_fn); + } else { + 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") + } + } + + /// 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, item_rc, &self.window_adapter, || { + ret = Some(self.actual_renderer.$fn(obj, item_rc, size)); + }); + 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, item_rc, &self.window_adapter, || { + ret = Some(self.actual_renderer.$fn(obj, item_rc, size, &cache)); + }); + 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(); + + // 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, 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); + let draw = clipped_geom.is_some_and(|clipped_geom| { + let clipped_geom = clipped_geom.translate(self.translation()); + self.dirty_region.draw_intersects(clipped_geom) + }); + + (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/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, 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;