tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 448ac4309f904559eed37e781e7a1f92a4a26154
parent ba574911f617033a26b0e10ecc8010db6f2dc398
Author: Nicolas Silva <nical@fastmail.com>
Date:   Fri, 12 Dec 2025 14:44:32 +0000

Bug 1998913 - Part 2 - Extract dependency tracking and quadtree. r=gfx-reviewers,gw

Differential Revision: https://phabricator.services.mozilla.com/D275937

Diffstat:
Agfx/wr/webrender/src/invalidation/dependency.rs | 425+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mgfx/wr/webrender/src/invalidation/mod.rs | 3+++
Agfx/wr/webrender/src/invalidation/quadtree.rs | 440+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mgfx/wr/webrender/src/picture.rs | 844++-----------------------------------------------------------------------------
4 files changed, 884 insertions(+), 828 deletions(-)

diff --git a/gfx/wr/webrender/src/invalidation/dependency.rs b/gfx/wr/webrender/src/invalidation/dependency.rs @@ -0,0 +1,425 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! Dependency tracking for tile invalidation +//! +//! This module contains types and logic for tracking dependencies that affect +//! tile invalidation, including transform comparisons, spatial node tracking, +//! and primitive comparison. + +use api::{ImageKey, PropertyBindingId, ColorU}; +use euclid::approxeq::ApproxEq; +use crate::PrimitiveCompareResult; +use crate::spatial_tree::{SpatialTree, SpatialNodeIndex, CoordinateSpaceMapping}; +use crate::internal_types::{FastHashMap, FastHashSet, FrameId}; +use crate::intern::ItemUid; +use crate::resource_cache::{ResourceCache, ImageGeneration}; +use crate::tile_cache::{TileDescriptor, PrimitiveDescriptor, PrimitiveDependencyIndex}; +use peek_poke::{PeekPoke, peek_from_slice}; +use std::collections::hash_map::Entry; + +/// A comparable transform matrix, that compares with epsilon checks. +#[derive(Debug, Clone)] +pub struct MatrixKey { + pub m: [f32; 16], +} + +impl PartialEq for MatrixKey { + fn eq(&self, other: &Self) -> bool { + const EPSILON: f32 = 0.001; + + // TODO(gw): It's possible that we may need to adjust the epsilon + // to be tighter on most of the matrix, except the + // translation parts? + for (i, j) in self.m.iter().zip(other.m.iter()) { + if !i.approx_eq_eps(j, &EPSILON) { + return false; + } + } + + true + } +} + +/// A comparable scale-offset, that compares with epsilon checks. +#[derive(Debug, Clone)] +pub struct ScaleOffsetKey { + pub sx: f32, + pub sy: f32, + pub tx: f32, + pub ty: f32, +} + +impl PartialEq for ScaleOffsetKey { + fn eq(&self, other: &Self) -> bool { + const EPSILON: f32 = 0.001; + + self.sx.approx_eq_eps(&other.sx, &EPSILON) && + self.sy.approx_eq_eps(&other.sy, &EPSILON) && + self.tx.approx_eq_eps(&other.tx, &EPSILON) && + self.ty.approx_eq_eps(&other.ty, &EPSILON) + } +} + +/// A comparable / hashable version of a coordinate space mapping. Used to determine +/// if a transform dependency for a tile has changed. +#[derive(Debug, PartialEq, Clone)] +pub enum TransformKey { + Local, + ScaleOffset { + so: ScaleOffsetKey, + }, + Transform { + m: MatrixKey, + } +} + +impl<Src, Dst> From<CoordinateSpaceMapping<Src, Dst>> for TransformKey { + fn from(transform: CoordinateSpaceMapping<Src, Dst>) -> TransformKey { + match transform { + CoordinateSpaceMapping::Local => { + TransformKey::Local + } + CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => { + TransformKey::ScaleOffset { + so: ScaleOffsetKey { + sx: scale_offset.scale.x, + sy: scale_offset.scale.y, + tx: scale_offset.offset.x, + ty: scale_offset.offset.y, + } + } + } + CoordinateSpaceMapping::Transform(ref m) => { + TransformKey::Transform { + m: MatrixKey { + m: m.to_array(), + }, + } + } + } + } +} + +/// Get the transform key for a spatial node relative to a cache spatial node. +pub fn get_transform_key( + spatial_node_index: SpatialNodeIndex, + cache_spatial_node_index: SpatialNodeIndex, + spatial_tree: &SpatialTree, +) -> TransformKey { + spatial_tree.get_relative_transform( + spatial_node_index, + cache_spatial_node_index, + ).into() +} + + +/// Information about the state of a binding. +#[derive(Debug)] +pub struct BindingInfo<T> { + /// The current value retrieved from dynamic scene properties. + pub value: T, + /// True if it was changed (or is new) since the last frame build. + pub changed: bool, +} + +/// Information stored in a tile descriptor for a binding. +#[derive(Debug, PartialEq, Clone, Copy, PeekPoke)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub enum Binding<T> { + Value(T), + Binding(PropertyBindingId), +} + +impl<T: Default> Default for Binding<T> { + fn default() -> Self { + Binding::Value(T::default()) + } +} + +impl<T> From<api::PropertyBinding<T>> for Binding<T> { + fn from(binding: api::PropertyBinding<T>) -> Binding<T> { + match binding { + api::PropertyBinding::Binding(key, _) => Binding::Binding(key.id), + api::PropertyBinding::Value(value) => Binding::Value(value), + } + } +} + +pub type OpacityBinding = Binding<f32>; +pub type OpacityBindingInfo = BindingInfo<f32>; + +pub type ColorBinding = Binding<ColorU>; +pub type ColorBindingInfo = BindingInfo<ColorU>; + +/// Types of dependencies that a primitive can have +#[derive(PeekPoke)] +pub enum PrimitiveDependency { + OpacityBinding { + binding: OpacityBinding, + }, + ColorBinding { + binding: ColorBinding, + }, + SpatialNode { + index: SpatialNodeIndex, + }, + Clip { + clip: ItemUid, + }, + Image { + image: ImageDependency, + }, +} + +/// Information stored an image dependency +#[derive(Debug, Copy, Clone, PartialEq, PeekPoke, Default)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct ImageDependency { + pub key: ImageKey, + pub generation: ImageGeneration, +} + +impl ImageDependency { + pub const INVALID: ImageDependency = ImageDependency { + key: ImageKey::DUMMY, + generation: ImageGeneration::INVALID, + }; +} + +/// A dependency for a transform is defined by the spatial node index + frame it was used +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PeekPoke, Default)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct SpatialNodeKey { + pub spatial_node_index: SpatialNodeIndex, + pub frame_id: FrameId, +} + +/// A helper for comparing spatial nodes between frames. The comparisons +/// are done by-value, so that if the shape of the spatial node tree +/// changes, invalidations aren't done simply due to the spatial node +/// index changing between display lists. +pub struct SpatialNodeComparer { + /// The root spatial node index of the tile cache + ref_spatial_node_index: SpatialNodeIndex, + /// Maintains a map of currently active transform keys + spatial_nodes: FastHashMap<SpatialNodeKey, TransformKey>, + /// A cache of recent comparisons between prev and current spatial nodes + compare_cache: FastHashMap<(SpatialNodeKey, SpatialNodeKey), bool>, + /// A set of frames that we need to retain spatial node entries for + referenced_frames: FastHashSet<FrameId>, +} + +impl SpatialNodeComparer { + /// Construct a new comparer + pub fn new() -> Self { + SpatialNodeComparer { + ref_spatial_node_index: SpatialNodeIndex::INVALID, + spatial_nodes: FastHashMap::default(), + compare_cache: FastHashMap::default(), + referenced_frames: FastHashSet::default(), + } + } + + /// Advance to the next frame + pub fn next_frame( + &mut self, + ref_spatial_node_index: SpatialNodeIndex, + ) { + // Drop any node information for unreferenced frames, to ensure that the + // hashmap doesn't grow indefinitely! + let referenced_frames = &self.referenced_frames; + self.spatial_nodes.retain(|key, _| { + referenced_frames.contains(&key.frame_id) + }); + + // Update the root spatial node for this comparer + self.ref_spatial_node_index = ref_spatial_node_index; + self.compare_cache.clear(); + self.referenced_frames.clear(); + } + + /// Register a transform that is used, and build the transform key for it if new. + pub fn register_used_transform( + &mut self, + spatial_node_index: SpatialNodeIndex, + frame_id: FrameId, + spatial_tree: &SpatialTree, + ) { + let key = SpatialNodeKey { + spatial_node_index, + frame_id, + }; + + if let Entry::Vacant(entry) = self.spatial_nodes.entry(key) { + entry.insert( + get_transform_key( + spatial_node_index, + self.ref_spatial_node_index, + spatial_tree, + ) + ); + } + } + + /// Return true if the transforms for two given spatial nodes are considered equivalent + pub fn are_transforms_equivalent( + &mut self, + prev_spatial_node_key: &SpatialNodeKey, + curr_spatial_node_key: &SpatialNodeKey, + ) -> bool { + let key = (*prev_spatial_node_key, *curr_spatial_node_key); + let spatial_nodes = &self.spatial_nodes; + + *self.compare_cache + .entry(key) + .or_insert_with(|| { + let prev = &spatial_nodes[&prev_spatial_node_key]; + let curr = &spatial_nodes[&curr_spatial_node_key]; + curr == prev + }) + } + + /// Ensure that the comparer won't GC any nodes for a given frame id + pub fn retain_for_frame(&mut self, frame_id: FrameId) { + self.referenced_frames.insert(frame_id); + } +} + + +/// A key for storing primitive comparison results during tile dependency tests. +#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)] +pub struct PrimitiveComparisonKey { + pub prev_index: PrimitiveDependencyIndex, + pub curr_index: PrimitiveDependencyIndex, +} + +/// A helper struct to compare a primitive and all its sub-dependencies. +pub struct PrimitiveComparer<'a> { + prev_data: &'a [u8], + curr_data: &'a [u8], + prev_frame_id: FrameId, + curr_frame_id: FrameId, + resource_cache: &'a ResourceCache, + spatial_node_comparer: &'a mut SpatialNodeComparer, + opacity_bindings: &'a FastHashMap<PropertyBindingId, OpacityBindingInfo>, + color_bindings: &'a FastHashMap<PropertyBindingId, ColorBindingInfo>, +} + +impl<'a> PrimitiveComparer<'a> { + pub fn new( + prev: &'a TileDescriptor, + curr: &'a TileDescriptor, + resource_cache: &'a ResourceCache, + spatial_node_comparer: &'a mut SpatialNodeComparer, + opacity_bindings: &'a FastHashMap<PropertyBindingId, OpacityBindingInfo>, + color_bindings: &'a FastHashMap<PropertyBindingId, ColorBindingInfo>, + ) -> Self { + PrimitiveComparer { + prev_data: &prev.dep_data, + curr_data: &curr.dep_data, + prev_frame_id: prev.last_updated_frame_id, + curr_frame_id: curr.last_updated_frame_id, + resource_cache, + spatial_node_comparer, + opacity_bindings, + color_bindings, + } + } + + /// Check if two primitive descriptors are the same. + pub fn compare_prim( + &mut self, + prev_desc: &PrimitiveDescriptor, + curr_desc: &PrimitiveDescriptor, + ) -> PrimitiveCompareResult { + let resource_cache = self.resource_cache; + let spatial_node_comparer = &mut self.spatial_node_comparer; + let opacity_bindings = self.opacity_bindings; + let color_bindings = self.color_bindings; + + // Check equality of the PrimitiveDescriptor + if prev_desc != curr_desc { + return PrimitiveCompareResult::Descriptor; + } + + let mut prev_dep_data = &self.prev_data[prev_desc.dep_offset as usize ..]; + let mut curr_dep_data = &self.curr_data[curr_desc.dep_offset as usize ..]; + + let mut prev_dep = PrimitiveDependency::SpatialNode { index: SpatialNodeIndex::INVALID }; + let mut curr_dep = PrimitiveDependency::SpatialNode { index: SpatialNodeIndex::INVALID }; + + debug_assert_eq!(prev_desc.dep_count, curr_desc.dep_count); + + for _ in 0 .. prev_desc.dep_count { + prev_dep_data = peek_from_slice(prev_dep_data, &mut prev_dep); + curr_dep_data = peek_from_slice(curr_dep_data, &mut curr_dep); + + match (&prev_dep, &curr_dep) { + (PrimitiveDependency::Clip { clip: prev }, PrimitiveDependency::Clip { clip: curr }) => { + if prev != curr { + return PrimitiveCompareResult::Clip; + } + } + (PrimitiveDependency::SpatialNode { index: prev }, PrimitiveDependency::SpatialNode { index: curr }) => { + let prev_key = SpatialNodeKey { + spatial_node_index: *prev, + frame_id: self.prev_frame_id, + }; + let curr_key = SpatialNodeKey { + spatial_node_index: *curr, + frame_id: self.curr_frame_id, + }; + if !spatial_node_comparer.are_transforms_equivalent(&prev_key, &curr_key) { + return PrimitiveCompareResult::Transform; + } + } + (PrimitiveDependency::OpacityBinding { binding: prev }, PrimitiveDependency::OpacityBinding { binding: curr }) => { + if prev != curr { + return PrimitiveCompareResult::OpacityBinding; + } + + if let OpacityBinding::Binding(id) = curr { + if opacity_bindings + .get(id) + .map_or(true, |info| info.changed) { + return PrimitiveCompareResult::OpacityBinding; + } + } + } + (PrimitiveDependency::ColorBinding { binding: prev }, PrimitiveDependency::ColorBinding { binding: curr }) => { + if prev != curr { + return PrimitiveCompareResult::ColorBinding; + } + + if let ColorBinding::Binding(id) = curr { + if color_bindings + .get(id) + .map_or(true, |info| info.changed) { + return PrimitiveCompareResult::ColorBinding; + } + } + } + (PrimitiveDependency::Image { image: prev }, PrimitiveDependency::Image { image: curr }) => { + if prev != curr { + return PrimitiveCompareResult::Image; + } + + if resource_cache.get_image_generation(curr.key) != curr.generation { + return PrimitiveCompareResult::Image; + } + } + _ => { + // There was a mismatch between types of dependencies, so something changed + return PrimitiveCompareResult::Descriptor; + } + } + } + + PrimitiveCompareResult::Equal + } +} diff --git a/gfx/wr/webrender/src/invalidation/mod.rs b/gfx/wr/webrender/src/invalidation/mod.rs @@ -7,6 +7,9 @@ //! This module contains types and logic for tracking dirty regions and //! dependencies used to determine what needs to be redrawn each frame. +pub mod dependency; +pub mod quadtree; + use api::units::*; use crate::spatial_tree::{SpatialTree, SpatialNodeIndex}; use crate::space::SpaceMapper; diff --git a/gfx/wr/webrender/src/invalidation/quadtree.rs b/gfx/wr/webrender/src/invalidation/quadtree.rs @@ -0,0 +1,440 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! Quadtree-based dirty region tracking for tiles +//! +//! This module implements a quadtree data structure that tracks which regions +//! of a tile have been invalidated. The quadtree can dynamically split and merge +//! nodes based on invalidation patterns to optimize tracking. + +use api::units::*; +use crate::debug_colors; +use crate::internal_types::FastHashMap; +use crate::prim_store::PrimitiveScratchBuffer; +use crate::space::SpaceMapper; +use crate::tile_cache::{PrimitiveDescriptor, PrimitiveDependencyIndex}; +use crate::invalidation::{InvalidationReason, PrimitiveCompareResult}; +use crate::invalidation::dependency::PrimitiveComparer; +use crate::visibility::FrameVisibilityContext; +use std::mem; + + +/// Details for a node in a quadtree that tracks dirty rects for a tile. +#[cfg_attr(any(feature="capture",feature="replay"), derive(Clone))] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub enum TileNodeKind { + Leaf { + /// The index buffer of primitives that affected this tile previous frame + #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))] + prev_indices: Vec<PrimitiveDependencyIndex>, + /// The index buffer of primitives that affect this tile on this frame + #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))] + curr_indices: Vec<PrimitiveDependencyIndex>, + /// A bitset of which of the last 64 frames have been dirty for this leaf. + #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))] + dirty_tracker: u64, + /// The number of frames since this node split or merged. + #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))] + frames_since_modified: usize, + }, + Node { + /// The four children of this node + children: Vec<TileNode>, + }, +} + +/// The kind of modification that a tile wants to do +#[derive(Copy, Clone, PartialEq, Debug)] +enum TileModification { + Split, + Merge, +} + +/// A node in the dirty rect tracking quadtree. +#[cfg_attr(any(feature="capture",feature="replay"), derive(Clone))] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct TileNode { + /// Leaf or internal node + pub kind: TileNodeKind, + /// Rect of this node in the same space as the tile cache picture + pub rect: PictureBox2D, +} + +impl TileNode { + /// Construct a new leaf node, with the given primitive dependency index buffer + pub fn new_leaf(curr_indices: Vec<PrimitiveDependencyIndex>) -> Self { + TileNode { + kind: TileNodeKind::Leaf { + prev_indices: Vec::new(), + curr_indices, + dirty_tracker: 0, + frames_since_modified: 0, + }, + rect: PictureBox2D::zero(), + } + } + + /// Draw debug information about this tile node + pub fn draw_debug_rects( + &self, + pic_to_world_mapper: &SpaceMapper<PicturePixel, WorldPixel>, + is_opaque: bool, + local_valid_rect: PictureRect, + scratch: &mut PrimitiveScratchBuffer, + global_device_pixel_scale: DevicePixelScale, + ) { + match self.kind { + TileNodeKind::Leaf { dirty_tracker, .. } => { + let color = if (dirty_tracker & 1) != 0 { + debug_colors::RED + } else if is_opaque { + debug_colors::GREEN + } else { + debug_colors::YELLOW + }; + + if let Some(local_rect) = local_valid_rect.intersection(&self.rect) { + let world_rect = pic_to_world_mapper + .map(&local_rect) + .unwrap(); + let device_rect = world_rect * global_device_pixel_scale; + + let outer_color = color.scale_alpha(0.3); + let inner_color = outer_color.scale_alpha(0.5); + scratch.push_debug_rect( + device_rect.inflate(-3.0, -3.0), + 1, + outer_color, + inner_color + ); + } + } + TileNodeKind::Node { ref children, .. } => { + for child in children.iter() { + child.draw_debug_rects( + pic_to_world_mapper, + is_opaque, + local_valid_rect, + scratch, + global_device_pixel_scale, + ); + } + } + } + } + + /// Calculate the four child rects for a given node + fn get_child_rects( + rect: &PictureBox2D, + result: &mut [PictureBox2D; 4], + ) { + let p0 = rect.min; + let p1 = rect.max; + let pc = p0 + rect.size() * 0.5; + + *result = [ + PictureBox2D::new( + p0, + pc, + ), + PictureBox2D::new( + PicturePoint::new(pc.x, p0.y), + PicturePoint::new(p1.x, pc.y), + ), + PictureBox2D::new( + PicturePoint::new(p0.x, pc.y), + PicturePoint::new(pc.x, p1.y), + ), + PictureBox2D::new( + pc, + p1, + ), + ]; + } + + /// Called during pre_update, to clear the current dependencies + pub fn clear( + &mut self, + rect: PictureBox2D, + ) { + self.rect = rect; + + match self.kind { + TileNodeKind::Leaf { ref mut prev_indices, ref mut curr_indices, ref mut dirty_tracker, ref mut frames_since_modified } => { + // Swap current dependencies to be the previous frame + mem::swap(prev_indices, curr_indices); + curr_indices.clear(); + // Note that another frame has passed in the dirty bit trackers + *dirty_tracker = *dirty_tracker << 1; + *frames_since_modified += 1; + } + TileNodeKind::Node { ref mut children, .. } => { + let mut child_rects = [PictureBox2D::zero(); 4]; + TileNode::get_child_rects(&rect, &mut child_rects); + assert_eq!(child_rects.len(), children.len()); + + for (child, rect) in children.iter_mut().zip(child_rects.iter()) { + child.clear(*rect); + } + } + } + } + + /// Add a primitive dependency to this node + pub fn add_prim( + &mut self, + index: PrimitiveDependencyIndex, + prim_rect: &PictureBox2D, + ) { + match self.kind { + TileNodeKind::Leaf { ref mut curr_indices, .. } => { + curr_indices.push(index); + } + TileNodeKind::Node { ref mut children, .. } => { + for child in children.iter_mut() { + if child.rect.intersects(prim_rect) { + child.add_prim(index, prim_rect); + } + } + } + } + } + + /// Apply a merge or split operation to this tile, if desired + pub fn maybe_merge_or_split( + &mut self, + level: i32, + curr_prims: &[PrimitiveDescriptor], + max_split_levels: i32, + ) { + // Determine if this tile wants to split or merge + let mut tile_mod = None; + + fn get_dirty_frames( + dirty_tracker: u64, + frames_since_modified: usize, + ) -> Option<u32> { + // Only consider splitting or merging at least 64 frames since we last changed + if frames_since_modified > 64 { + // Each bit in the tracker is a frame that was recently invalidated + Some(dirty_tracker.count_ones()) + } else { + None + } + } + + match self.kind { + TileNodeKind::Leaf { dirty_tracker, frames_since_modified, .. } => { + // Only consider splitting if the tree isn't too deep. + if level < max_split_levels { + if let Some(dirty_frames) = get_dirty_frames(dirty_tracker, frames_since_modified) { + // If the tile has invalidated > 50% of the recent number of frames, split. + if dirty_frames > 32 { + tile_mod = Some(TileModification::Split); + } + } + } + } + TileNodeKind::Node { ref children, .. } => { + // There's two conditions that cause a node to merge its children: + // (1) If _all_ the child nodes are constantly invalidating, then we are wasting + // CPU time tracking dependencies for each child, so merge them. + // (2) If _none_ of the child nodes are recently invalid, then the page content + // has probably changed, and we no longer need to track fine grained dependencies here. + + let mut static_count = 0; + let mut changing_count = 0; + + for child in children { + // Only consider merging nodes at the edge of the tree. + if let TileNodeKind::Leaf { dirty_tracker, frames_since_modified, .. } = child.kind { + if let Some(dirty_frames) = get_dirty_frames(dirty_tracker, frames_since_modified) { + if dirty_frames == 0 { + // Hasn't been invalidated for some time + static_count += 1; + } else if dirty_frames == 64 { + // Is constantly being invalidated + changing_count += 1; + } + } + } + + // Only merge if all the child tiles are in agreement. Otherwise, we have some + // that are invalidating / static, and it's worthwhile tracking dependencies for + // them individually. + if static_count == 4 || changing_count == 4 { + tile_mod = Some(TileModification::Merge); + } + } + } + } + + match tile_mod { + Some(TileModification::Split) => { + // To split a node, take the current dependency index buffer for this node, and + // split it into child index buffers. + let curr_indices = match self.kind { + TileNodeKind::Node { .. } => { + unreachable!("bug - only leaves can split"); + } + TileNodeKind::Leaf { ref mut curr_indices, .. } => { + mem::take(curr_indices) + } + }; + + let mut child_rects = [PictureBox2D::zero(); 4]; + TileNode::get_child_rects(&self.rect, &mut child_rects); + + let mut child_indices = [ + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + ]; + + // Step through the index buffer, and add primitives to each of the children + // that they intersect. + for index in curr_indices { + let prim = &curr_prims[index.0 as usize]; + for (child_rect, indices) in child_rects.iter().zip(child_indices.iter_mut()) { + if prim.prim_clip_box.intersects(child_rect) { + indices.push(index); + } + } + } + + // Create the child nodes and switch from leaf -> node. + let children = child_indices + .iter_mut() + .map(|i| TileNode::new_leaf(mem::replace(i, Vec::new()))) + .collect(); + + self.kind = TileNodeKind::Node { + children, + }; + } + Some(TileModification::Merge) => { + // Construct a merged index buffer by collecting the dependency index buffers + // from each child, and merging them into a de-duplicated index buffer. + let merged_indices = match self.kind { + TileNodeKind::Node { ref mut children, .. } => { + let mut merged_indices = Vec::new(); + + for child in children.iter() { + let child_indices = match child.kind { + TileNodeKind::Leaf { ref curr_indices, .. } => { + curr_indices + } + TileNodeKind::Node { .. } => { + unreachable!("bug: child is not a leaf"); + } + }; + merged_indices.extend_from_slice(child_indices); + } + + merged_indices.sort(); + merged_indices.dedup(); + + merged_indices + } + TileNodeKind::Leaf { .. } => { + unreachable!("bug - trying to merge a leaf"); + } + }; + + // Switch from a node to a leaf, with the combined index buffer + self.kind = TileNodeKind::Leaf { + prev_indices: Vec::new(), + curr_indices: merged_indices, + dirty_tracker: 0, + frames_since_modified: 0, + }; + } + None => { + // If this node didn't merge / split, then recurse into children + // to see if they want to split / merge. + if let TileNodeKind::Node { ref mut children, .. } = self.kind { + for child in children.iter_mut() { + child.maybe_merge_or_split( + level+1, + curr_prims, + max_split_levels, + ); + } + } + } + } + } + + /// Update the dirty state of this node, building the overall dirty rect + pub fn update_dirty_rects( + &mut self, + prev_prims: &[PrimitiveDescriptor], + curr_prims: &[PrimitiveDescriptor], + prim_comparer: &mut PrimitiveComparer, + dirty_rect: &mut PictureBox2D, + compare_cache: &mut FastHashMap<crate::invalidation::dependency::PrimitiveComparisonKey, PrimitiveCompareResult>, + invalidation_reason: &mut Option<InvalidationReason>, + frame_context: &FrameVisibilityContext, + ) { + match self.kind { + TileNodeKind::Node { ref mut children, .. } => { + for child in children.iter_mut() { + child.update_dirty_rects( + prev_prims, + curr_prims, + prim_comparer, + dirty_rect, + compare_cache, + invalidation_reason, + frame_context, + ); + } + } + TileNodeKind::Leaf { ref prev_indices, ref curr_indices, ref mut dirty_tracker, .. } => { + // If the index buffers are of different length, they must be different + if prev_indices.len() == curr_indices.len() { + // Walk each index buffer, comparing primitives + for (prev_index, curr_index) in prev_indices.iter().zip(curr_indices.iter()) { + let i0 = prev_index.0 as usize; + let i1 = curr_index.0 as usize; + + // Compare the primitives, caching the result in a hash map + // to save comparisons in other tree nodes. + let key = crate::invalidation::dependency::PrimitiveComparisonKey { + prev_index: *prev_index, + curr_index: *curr_index, + }; + + let prim_compare_result = *compare_cache + .entry(key) + .or_insert_with(|| { + let prev = &prev_prims[i0]; + let curr = &curr_prims[i1]; + prim_comparer.compare_prim(prev, curr) + }); + + // If not the same, mark this node as dirty and update the dirty rect + if prim_compare_result != PrimitiveCompareResult::Equal { + if invalidation_reason.is_none() { + *invalidation_reason = Some(InvalidationReason::Content); + } + *dirty_rect = self.rect.union(dirty_rect); + *dirty_tracker = *dirty_tracker | 1; + break; + } + } + } else { + if invalidation_reason.is_none() { + *invalidation_reason = Some(InvalidationReason::PrimCount); + } + *dirty_rect = self.rect.union(dirty_rect); + *dirty_tracker = *dirty_tracker | 1; + } + } + } + } +} diff --git a/gfx/wr/webrender/src/picture.rs b/gfx/wr/webrender/src/picture.rs @@ -95,8 +95,8 @@ //! improved as a follow up). use api::{BorderRadius, ClipMode, MixBlendMode, PremultipliedColorF, SVGFE_GRAPH_MAX}; -use api::{PropertyBinding, PropertyBindingId, FilterOpGraphPictureBufferId, RasterSpace}; -use api::{DebugFlags, ImageKey, ColorF, ColorU, PrimitiveFlags, SnapshotInfo}; +use api::{PropertyBindingId, FilterOpGraphPictureBufferId, RasterSpace}; +use api::{DebugFlags, ImageKey, ColorF, PrimitiveFlags, SnapshotInfo}; use api::{ImageRendering, ColorDepth, YuvRangedColorSpace, YuvFormat, AlphaType}; use api::units::*; use crate::prim_store::image::AdjustedImageSource; @@ -112,11 +112,11 @@ use crate::debug_colors; use euclid::{vec3, Scale, Vector2D, Box2D}; use euclid::approxeq::ApproxEq; use crate::intern::ItemUid; -use crate::internal_types::{FastHashMap, FastHashSet, PlaneSplitter, FilterGraphOp, FilterGraphNode, Filter, FrameId}; +use crate::internal_types::{FastHashMap, PlaneSplitter, FilterGraphOp, FilterGraphNode, Filter, FrameId}; use crate::internal_types::{PlaneSplitterIndex, PlaneSplitAnchor, TextureSource}; use crate::frame_builder::{FrameBuildingContext, FrameBuildingState, PictureState, PictureContext}; -use crate::gpu_types::{BlurEdgeMode, BrushSegmentGpuData, ImageBrushPrimitiveData, UvRectKind, ZBufferId}; -use peek_poke::{PeekPoke, poke_into_vec, peek_from_slice, ensure_red_zone}; +use crate::gpu_types::{UvRectKind, ZBufferId, BlurEdgeMode, BrushSegmentGpuData, ImageBrushPrimitiveData}; +use peek_poke::{poke_into_vec, ensure_red_zone}; use plane_split::{Clipper, Polygon}; use crate::prim_store::{PrimitiveTemplateKind, PictureIndex, PrimitiveInstance, PrimitiveInstanceKind}; use crate::prim_store::{ColorBindingStorage, ColorBindingIndex, PrimitiveScratchBuffer}; @@ -127,7 +127,7 @@ use crate::render_target::RenderTargetKind; use crate::render_task::{BlurTask, RenderTask, RenderTaskLocation, BlurTaskCache}; use crate::render_task::{StaticRenderTaskSurface, RenderTaskKind}; use crate::renderer::{BlendMode, GpuBufferAddress}; -use crate::resource_cache::{ResourceCache, ImageGeneration, ImageRequest}; +use crate::resource_cache::{ResourceCache, ImageRequest}; use crate::space::SpaceMapper; use crate::scene::SceneProperties; use crate::spatial_tree::CoordinateSystemId; @@ -135,10 +135,9 @@ use crate::surface::{SurfaceDescriptor, SurfaceTileDescriptor}; use smallvec::SmallVec; use std::{mem, u8, u32}; use std::fmt::{Display, Error, Formatter}; -use std::collections::hash_map::Entry; use std::ops::Range; use crate::picture_textures::PictureCacheTextureHandle; -use crate::util::{MaxRect, VecHelper, MatrixHelpers, Recycler, ScaleOffset}; +use crate::util::{MaxRect, MatrixHelpers, Recycler, ScaleOffset}; use crate::filterdata::FilterDataHandle; use crate::tile_cache::{SliceDebugInfo, TileDebugInfo, DirtyTileDebugInfo}; use crate::tile_cache::{TileKey, TileId, TileRect, TileOffset, SubSliceIndex}; @@ -152,6 +151,15 @@ use crate::scene_building::SliceFlags; use core::time::Duration; pub use crate::invalidation::DirtyRegion; +pub use crate::invalidation::dependency::ImageDependency; +pub use crate::invalidation::quadtree::{TileNode, TileNodeKind}; +use crate::invalidation::dependency::{ + OpacityBindingInfo, ColorBindingInfo, + OpacityBinding, ColorBinding, + PrimitiveDependency, + SpatialNodeComparer, + PrimitiveComparisonKey, PrimitiveComparer, +}; use crate::invalidation::PrimitiveCompareResult; @@ -174,89 +182,6 @@ pub enum SubpixelMode { }, } -/// A comparable transform matrix, that compares with epsilon checks. -#[derive(Debug, Clone)] -struct MatrixKey { - m: [f32; 16], -} - -impl PartialEq for MatrixKey { - fn eq(&self, other: &Self) -> bool { - const EPSILON: f32 = 0.001; - - // TODO(gw): It's possible that we may need to adjust the epsilon - // to be tighter on most of the matrix, except the - // translation parts? - for (i, j) in self.m.iter().zip(other.m.iter()) { - if !i.approx_eq_eps(j, &EPSILON) { - return false; - } - } - - true - } -} - -/// A comparable scale-offset, that compares with epsilon checks. -#[derive(Debug, Clone)] -struct ScaleOffsetKey { - sx: f32, - sy: f32, - tx: f32, - ty: f32, -} - -impl PartialEq for ScaleOffsetKey { - fn eq(&self, other: &Self) -> bool { - const EPSILON: f32 = 0.001; - - self.sx.approx_eq_eps(&other.sx, &EPSILON) && - self.sy.approx_eq_eps(&other.sy, &EPSILON) && - self.tx.approx_eq_eps(&other.tx, &EPSILON) && - self.ty.approx_eq_eps(&other.ty, &EPSILON) - } -} - -/// A comparable / hashable version of a coordinate space mapping. Used to determine -/// if a transform dependency for a tile has changed. -#[derive(Debug, PartialEq, Clone)] -enum TransformKey { - Local, - ScaleOffset { - so: ScaleOffsetKey, - }, - Transform { - m: MatrixKey, - } -} - -impl<Src, Dst> From<CoordinateSpaceMapping<Src, Dst>> for TransformKey { - fn from(transform: CoordinateSpaceMapping<Src, Dst>) -> TransformKey { - match transform { - CoordinateSpaceMapping::Local => { - TransformKey::Local - } - CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => { - TransformKey::ScaleOffset { - so: ScaleOffsetKey { - sx: scale_offset.scale.x, - sy: scale_offset.scale.y, - tx: scale_offset.offset.x, - ty: scale_offset.offset.y, - } - } - } - CoordinateSpaceMapping::Transform(ref m) => { - TransformKey::Transform { - m: MatrixKey { - m: m.to_array(), - }, - } - } - } - } -} - /// Maximum size of a compositor surface. const MAX_COMPOSITOR_SURFACES_SIZE: f32 = 8192.0; @@ -268,164 +193,6 @@ fn clampf(value: f32, low: f32, high: f32) -> f32 { value.max(low).min(high) } -/// Information about the state of a binding. -#[derive(Debug)] -pub struct BindingInfo<T> { - /// The current value retrieved from dynamic scene properties. - value: T, - /// True if it was changed (or is new) since the last frame build. - changed: bool, -} - -/// Information stored in a tile descriptor for a binding. -#[derive(Debug, PartialEq, Clone, Copy, PeekPoke)] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub enum Binding<T> { - Value(T), - Binding(PropertyBindingId), -} - -impl<T: Default> Default for Binding<T> { - fn default() -> Self { - Binding::Value(T::default()) - } -} - -impl<T> From<PropertyBinding<T>> for Binding<T> { - fn from(binding: PropertyBinding<T>) -> Binding<T> { - match binding { - PropertyBinding::Binding(key, _) => Binding::Binding(key.id), - PropertyBinding::Value(value) => Binding::Value(value), - } - } -} - -pub type OpacityBinding = Binding<f32>; -pub type OpacityBindingInfo = BindingInfo<f32>; - -pub type ColorBinding = Binding<ColorU>; -pub type ColorBindingInfo = BindingInfo<ColorU>; - -#[derive(PeekPoke)] -enum PrimitiveDependency { - OpacityBinding { - binding: OpacityBinding, - }, - ColorBinding { - binding: ColorBinding, - }, - SpatialNode { - index: SpatialNodeIndex, - }, - Clip { - clip: ItemUid, - }, - Image { - image: ImageDependency, - }, -} - -/// A dependency for a transform is defined by the spatial node index + frame it was used -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PeekPoke, Default)] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub struct SpatialNodeKey { - spatial_node_index: SpatialNodeIndex, - frame_id: FrameId, -} - -/// A helper for comparing spatial nodes between frames. The comparisons -/// are done by-value, so that if the shape of the spatial node tree -/// changes, invalidations aren't done simply due to the spatial node -/// index changing between display lists. -struct SpatialNodeComparer { - /// The root spatial node index of the tile cache - ref_spatial_node_index: SpatialNodeIndex, - /// Maintains a map of currently active transform keys - spatial_nodes: FastHashMap<SpatialNodeKey, TransformKey>, - /// A cache of recent comparisons between prev and current spatial nodes - compare_cache: FastHashMap<(SpatialNodeKey, SpatialNodeKey), bool>, - /// A set of frames that we need to retain spatial node entries for - referenced_frames: FastHashSet<FrameId>, -} - -impl SpatialNodeComparer { - /// Construct a new comparer - fn new() -> Self { - SpatialNodeComparer { - ref_spatial_node_index: SpatialNodeIndex::INVALID, - spatial_nodes: FastHashMap::default(), - compare_cache: FastHashMap::default(), - referenced_frames: FastHashSet::default(), - } - } - - /// Advance to the next frame - fn next_frame( - &mut self, - ref_spatial_node_index: SpatialNodeIndex, - ) { - // Drop any node information for unreferenced frames, to ensure that the - // hashmap doesn't grow indefinitely! - let referenced_frames = &self.referenced_frames; - self.spatial_nodes.retain(|key, _| { - referenced_frames.contains(&key.frame_id) - }); - - // Update the root spatial node for this comparer - self.ref_spatial_node_index = ref_spatial_node_index; - self.compare_cache.clear(); - self.referenced_frames.clear(); - } - - /// Register a transform that is used, and build the transform key for it if new. - fn register_used_transform( - &mut self, - spatial_node_index: SpatialNodeIndex, - frame_id: FrameId, - spatial_tree: &SpatialTree, - ) { - let key = SpatialNodeKey { - spatial_node_index, - frame_id, - }; - - if let Entry::Vacant(entry) = self.spatial_nodes.entry(key) { - entry.insert( - get_transform_key( - spatial_node_index, - self.ref_spatial_node_index, - spatial_tree, - ) - ); - } - } - - /// Return true if the transforms for two given spatial nodes are considered equivalent - fn are_transforms_equivalent( - &mut self, - prev_spatial_node_key: &SpatialNodeKey, - curr_spatial_node_key: &SpatialNodeKey, - ) -> bool { - let key = (*prev_spatial_node_key, *curr_spatial_node_key); - let spatial_nodes = &self.spatial_nodes; - - *self.compare_cache - .entry(key) - .or_insert_with(|| { - let prev = &spatial_nodes[&prev_spatial_node_key]; - let curr = &spatial_nodes[&curr_spatial_node_key]; - curr == prev - }) - } - - /// Ensure that the comparer won't GC any nodes for a given frame id - fn retain_for_frame(&mut self, frame_id: FrameId) { - self.referenced_frames.insert(frame_id); - } -} - // Immutable context passed to picture cache tiles during pre_update struct TilePreUpdateContext { /// Maps from picture cache coords -> world space coords. @@ -6970,40 +6737,6 @@ impl PicturePrimitive { } } -fn get_transform_key( - spatial_node_index: SpatialNodeIndex, - cache_spatial_node_index: SpatialNodeIndex, - spatial_tree: &SpatialTree, -) -> TransformKey { - spatial_tree.get_relative_transform( - spatial_node_index, - cache_spatial_node_index, - ).into() -} - -/// A key for storing primitive comparison results during tile dependency tests. -#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)] -struct PrimitiveComparisonKey { - prev_index: PrimitiveDependencyIndex, - curr_index: PrimitiveDependencyIndex, -} - -/// Information stored an image dependency -#[derive(Debug, Copy, Clone, PartialEq, PeekPoke, Default)] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub struct ImageDependency { - pub key: ImageKey, - pub generation: ImageGeneration, -} - -impl ImageDependency { - pub const INVALID: ImageDependency = ImageDependency { - key: ImageKey::DUMMY, - generation: ImageGeneration::INVALID, - }; -} - /// In some cases, we need to know the dirty rect of all tiles in order /// to correctly invalidate a primitive. #[derive(Debug)] @@ -7014,551 +6747,6 @@ struct DeferredDirtyTest { prim_rect: PictureRect, } -/// A helper struct to compare a primitive and all its sub-dependencies. -struct PrimitiveComparer<'a> { - prev_data: &'a [u8], - curr_data: &'a [u8], - prev_frame_id: FrameId, - curr_frame_id: FrameId, - resource_cache: &'a ResourceCache, - spatial_node_comparer: &'a mut SpatialNodeComparer, - opacity_bindings: &'a FastHashMap<PropertyBindingId, OpacityBindingInfo>, - color_bindings: &'a FastHashMap<PropertyBindingId, ColorBindingInfo>, -} - -impl<'a> PrimitiveComparer<'a> { - fn new( - prev: &'a TileDescriptor, - curr: &'a TileDescriptor, - resource_cache: &'a ResourceCache, - spatial_node_comparer: &'a mut SpatialNodeComparer, - opacity_bindings: &'a FastHashMap<PropertyBindingId, OpacityBindingInfo>, - color_bindings: &'a FastHashMap<PropertyBindingId, ColorBindingInfo>, - ) -> Self { - PrimitiveComparer { - prev_data: &prev.dep_data, - curr_data: &curr.dep_data, - prev_frame_id: prev.last_updated_frame_id, - curr_frame_id: curr.last_updated_frame_id, - resource_cache, - spatial_node_comparer, - opacity_bindings, - color_bindings, - } - } - - /// Check if two primitive descriptors are the same. - fn compare_prim( - &mut self, - prev_desc: &PrimitiveDescriptor, - curr_desc: &PrimitiveDescriptor, - ) -> PrimitiveCompareResult { - let resource_cache = self.resource_cache; - let spatial_node_comparer = &mut self.spatial_node_comparer; - let opacity_bindings = self.opacity_bindings; - let color_bindings = self.color_bindings; - - // Check equality of the PrimitiveDescriptor - if prev_desc != curr_desc { - return PrimitiveCompareResult::Descriptor; - } - - let mut prev_dep_data = &self.prev_data[prev_desc.dep_offset as usize ..]; - let mut curr_dep_data = &self.curr_data[curr_desc.dep_offset as usize ..]; - - let mut prev_dep = PrimitiveDependency::SpatialNode { index: SpatialNodeIndex::INVALID }; - let mut curr_dep = PrimitiveDependency::SpatialNode { index: SpatialNodeIndex::INVALID }; - - debug_assert_eq!(prev_desc.dep_count, curr_desc.dep_count); - - for _ in 0 .. prev_desc.dep_count { - prev_dep_data = peek_from_slice(prev_dep_data, &mut prev_dep); - curr_dep_data = peek_from_slice(curr_dep_data, &mut curr_dep); - - match (&prev_dep, &curr_dep) { - (PrimitiveDependency::Clip { clip: prev }, PrimitiveDependency::Clip { clip: curr }) => { - if prev != curr { - return PrimitiveCompareResult::Clip; - } - } - (PrimitiveDependency::SpatialNode { index: prev }, PrimitiveDependency::SpatialNode { index: curr }) => { - let prev_key = SpatialNodeKey { - spatial_node_index: *prev, - frame_id: self.prev_frame_id, - }; - let curr_key = SpatialNodeKey { - spatial_node_index: *curr, - frame_id: self.curr_frame_id, - }; - if !spatial_node_comparer.are_transforms_equivalent(&prev_key, &curr_key) { - return PrimitiveCompareResult::Transform; - } - } - (PrimitiveDependency::OpacityBinding { binding: prev }, PrimitiveDependency::OpacityBinding { binding: curr }) => { - if prev != curr { - return PrimitiveCompareResult::OpacityBinding; - } - - if let OpacityBinding::Binding(id) = curr { - if opacity_bindings - .get(id) - .map_or(true, |info| info.changed) { - return PrimitiveCompareResult::OpacityBinding; - } - } - } - (PrimitiveDependency::ColorBinding { binding: prev }, PrimitiveDependency::ColorBinding { binding: curr }) => { - if prev != curr { - return PrimitiveCompareResult::ColorBinding; - } - - if let ColorBinding::Binding(id) = curr { - if color_bindings - .get(id) - .map_or(true, |info| info.changed) { - return PrimitiveCompareResult::ColorBinding; - } - } - } - (PrimitiveDependency::Image { image: prev }, PrimitiveDependency::Image { image: curr }) => { - if prev != curr { - return PrimitiveCompareResult::Image; - } - - if resource_cache.get_image_generation(curr.key) != curr.generation { - return PrimitiveCompareResult::Image; - } - } - _ => { - // There was a mismatch between types of dependencies, so something changed - return PrimitiveCompareResult::Descriptor; - } - } - } - - PrimitiveCompareResult::Equal - } -} - -/// Details for a node in a quadtree that tracks dirty rects for a tile. -#[cfg_attr(any(feature="capture",feature="replay"), derive(Clone))] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub enum TileNodeKind { - Leaf { - /// The index buffer of primitives that affected this tile previous frame - #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))] - prev_indices: Vec<PrimitiveDependencyIndex>, - /// The index buffer of primitives that affect this tile on this frame - #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))] - curr_indices: Vec<PrimitiveDependencyIndex>, - /// A bitset of which of the last 64 frames have been dirty for this leaf. - #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))] - dirty_tracker: u64, - /// The number of frames since this node split or merged. - #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))] - frames_since_modified: usize, - }, - Node { - /// The four children of this node - children: Vec<TileNode>, - }, -} - -/// The kind of modification that a tile wants to do -#[derive(Copy, Clone, PartialEq, Debug)] -enum TileModification { - Split, - Merge, -} - -/// A node in the dirty rect tracking quadtree. -#[cfg_attr(any(feature="capture",feature="replay"), derive(Clone))] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub struct TileNode { - /// Leaf or internal node - pub kind: TileNodeKind, - /// Rect of this node in the same space as the tile cache picture - pub rect: PictureBox2D, -} - -impl TileNode { - /// Construct a new leaf node, with the given primitive dependency index buffer - fn new_leaf(curr_indices: Vec<PrimitiveDependencyIndex>) -> Self { - TileNode { - kind: TileNodeKind::Leaf { - prev_indices: Vec::new(), - curr_indices, - dirty_tracker: 0, - frames_since_modified: 0, - }, - rect: PictureBox2D::zero(), - } - } - - /// Draw debug information about this tile node - fn draw_debug_rects( - &self, - pic_to_world_mapper: &SpaceMapper<PicturePixel, WorldPixel>, - is_opaque: bool, - local_valid_rect: PictureRect, - scratch: &mut PrimitiveScratchBuffer, - global_device_pixel_scale: DevicePixelScale, - ) { - match self.kind { - TileNodeKind::Leaf { dirty_tracker, .. } => { - let color = if (dirty_tracker & 1) != 0 { - debug_colors::RED - } else if is_opaque { - debug_colors::GREEN - } else { - debug_colors::YELLOW - }; - - if let Some(local_rect) = local_valid_rect.intersection(&self.rect) { - let world_rect = pic_to_world_mapper - .map(&local_rect) - .unwrap(); - let device_rect = world_rect * global_device_pixel_scale; - - let outer_color = color.scale_alpha(0.3); - let inner_color = outer_color.scale_alpha(0.5); - scratch.push_debug_rect( - device_rect.inflate(-3.0, -3.0), - 1, - outer_color, - inner_color - ); - } - } - TileNodeKind::Node { ref children, .. } => { - for child in children.iter() { - child.draw_debug_rects( - pic_to_world_mapper, - is_opaque, - local_valid_rect, - scratch, - global_device_pixel_scale, - ); - } - } - } - } - - /// Calculate the four child rects for a given node - fn get_child_rects( - rect: &PictureBox2D, - result: &mut [PictureBox2D; 4], - ) { - let p0 = rect.min; - let p1 = rect.max; - let pc = p0 + rect.size() * 0.5; - - *result = [ - PictureBox2D::new( - p0, - pc, - ), - PictureBox2D::new( - PicturePoint::new(pc.x, p0.y), - PicturePoint::new(p1.x, pc.y), - ), - PictureBox2D::new( - PicturePoint::new(p0.x, pc.y), - PicturePoint::new(pc.x, p1.y), - ), - PictureBox2D::new( - pc, - p1, - ), - ]; - } - - /// Called during pre_update, to clear the current dependencies - fn clear( - &mut self, - rect: PictureBox2D, - ) { - self.rect = rect; - - match self.kind { - TileNodeKind::Leaf { ref mut prev_indices, ref mut curr_indices, ref mut dirty_tracker, ref mut frames_since_modified } => { - // Swap current dependencies to be the previous frame - mem::swap(prev_indices, curr_indices); - curr_indices.clear(); - // Note that another frame has passed in the dirty bit trackers - *dirty_tracker = *dirty_tracker << 1; - *frames_since_modified += 1; - } - TileNodeKind::Node { ref mut children, .. } => { - let mut child_rects = [PictureBox2D::zero(); 4]; - TileNode::get_child_rects(&rect, &mut child_rects); - assert_eq!(child_rects.len(), children.len()); - - for (child, rect) in children.iter_mut().zip(child_rects.iter()) { - child.clear(*rect); - } - } - } - } - - /// Add a primitive dependency to this node - fn add_prim( - &mut self, - index: PrimitiveDependencyIndex, - prim_rect: &PictureBox2D, - ) { - match self.kind { - TileNodeKind::Leaf { ref mut curr_indices, .. } => { - curr_indices.push(index); - } - TileNodeKind::Node { ref mut children, .. } => { - for child in children.iter_mut() { - if child.rect.intersects(prim_rect) { - child.add_prim(index, prim_rect); - } - } - } - } - } - - /// Apply a merge or split operation to this tile, if desired - fn maybe_merge_or_split( - &mut self, - level: i32, - curr_prims: &[PrimitiveDescriptor], - max_split_levels: i32, - ) { - // Determine if this tile wants to split or merge - let mut tile_mod = None; - - fn get_dirty_frames( - dirty_tracker: u64, - frames_since_modified: usize, - ) -> Option<u32> { - // Only consider splitting or merging at least 64 frames since we last changed - if frames_since_modified > 64 { - // Each bit in the tracker is a frame that was recently invalidated - Some(dirty_tracker.count_ones()) - } else { - None - } - } - - match self.kind { - TileNodeKind::Leaf { dirty_tracker, frames_since_modified, .. } => { - // Only consider splitting if the tree isn't too deep. - if level < max_split_levels { - if let Some(dirty_frames) = get_dirty_frames(dirty_tracker, frames_since_modified) { - // If the tile has invalidated > 50% of the recent number of frames, split. - if dirty_frames > 32 { - tile_mod = Some(TileModification::Split); - } - } - } - } - TileNodeKind::Node { ref children, .. } => { - // There's two conditions that cause a node to merge its children: - // (1) If _all_ the child nodes are constantly invalidating, then we are wasting - // CPU time tracking dependencies for each child, so merge them. - // (2) If _none_ of the child nodes are recently invalid, then the page content - // has probably changed, and we no longer need to track fine grained dependencies here. - - let mut static_count = 0; - let mut changing_count = 0; - - for child in children { - // Only consider merging nodes at the edge of the tree. - if let TileNodeKind::Leaf { dirty_tracker, frames_since_modified, .. } = child.kind { - if let Some(dirty_frames) = get_dirty_frames(dirty_tracker, frames_since_modified) { - if dirty_frames == 0 { - // Hasn't been invalidated for some time - static_count += 1; - } else if dirty_frames == 64 { - // Is constantly being invalidated - changing_count += 1; - } - } - } - - // Only merge if all the child tiles are in agreement. Otherwise, we have some - // that are invalidating / static, and it's worthwhile tracking dependencies for - // them individually. - if static_count == 4 || changing_count == 4 { - tile_mod = Some(TileModification::Merge); - } - } - } - } - - match tile_mod { - Some(TileModification::Split) => { - // To split a node, take the current dependency index buffer for this node, and - // split it into child index buffers. - let curr_indices = match self.kind { - TileNodeKind::Node { .. } => { - unreachable!("bug - only leaves can split"); - } - TileNodeKind::Leaf { ref mut curr_indices, .. } => { - curr_indices.take() - } - }; - - let mut child_rects = [PictureBox2D::zero(); 4]; - TileNode::get_child_rects(&self.rect, &mut child_rects); - - let mut child_indices = [ - Vec::new(), - Vec::new(), - Vec::new(), - Vec::new(), - ]; - - // Step through the index buffer, and add primitives to each of the children - // that they intersect. - for index in curr_indices { - let prim = &curr_prims[index.0 as usize]; - for (child_rect, indices) in child_rects.iter().zip(child_indices.iter_mut()) { - if prim.prim_clip_box.intersects(child_rect) { - indices.push(index); - } - } - } - - // Create the child nodes and switch from leaf -> node. - let children = child_indices - .iter_mut() - .map(|i| TileNode::new_leaf(mem::replace(i, Vec::new()))) - .collect(); - - self.kind = TileNodeKind::Node { - children, - }; - } - Some(TileModification::Merge) => { - // Construct a merged index buffer by collecting the dependency index buffers - // from each child, and merging them into a de-duplicated index buffer. - let merged_indices = match self.kind { - TileNodeKind::Node { ref mut children, .. } => { - let mut merged_indices = Vec::new(); - - for child in children.iter() { - let child_indices = match child.kind { - TileNodeKind::Leaf { ref curr_indices, .. } => { - curr_indices - } - TileNodeKind::Node { .. } => { - unreachable!("bug: child is not a leaf"); - } - }; - merged_indices.extend_from_slice(child_indices); - } - - merged_indices.sort(); - merged_indices.dedup(); - - merged_indices - } - TileNodeKind::Leaf { .. } => { - unreachable!("bug - trying to merge a leaf"); - } - }; - - // Switch from a node to a leaf, with the combined index buffer - self.kind = TileNodeKind::Leaf { - prev_indices: Vec::new(), - curr_indices: merged_indices, - dirty_tracker: 0, - frames_since_modified: 0, - }; - } - None => { - // If this node didn't merge / split, then recurse into children - // to see if they want to split / merge. - if let TileNodeKind::Node { ref mut children, .. } = self.kind { - for child in children.iter_mut() { - child.maybe_merge_or_split( - level+1, - curr_prims, - max_split_levels, - ); - } - } - } - } - } - - /// Update the dirty state of this node, building the overall dirty rect - fn update_dirty_rects( - &mut self, - prev_prims: &[PrimitiveDescriptor], - curr_prims: &[PrimitiveDescriptor], - prim_comparer: &mut PrimitiveComparer, - dirty_rect: &mut PictureBox2D, - compare_cache: &mut FastHashMap<PrimitiveComparisonKey, PrimitiveCompareResult>, - invalidation_reason: &mut Option<InvalidationReason>, - frame_context: &FrameVisibilityContext, - ) { - match self.kind { - TileNodeKind::Node { ref mut children, .. } => { - for child in children.iter_mut() { - child.update_dirty_rects( - prev_prims, - curr_prims, - prim_comparer, - dirty_rect, - compare_cache, - invalidation_reason, - frame_context, - ); - } - } - TileNodeKind::Leaf { ref prev_indices, ref curr_indices, ref mut dirty_tracker, .. } => { - // If the index buffers are of different length, they must be different - if prev_indices.len() == curr_indices.len() { - // Walk each index buffer, comparing primitives - for (prev_index, curr_index) in prev_indices.iter().zip(curr_indices.iter()) { - let i0 = prev_index.0 as usize; - let i1 = curr_index.0 as usize; - - // Compare the primitives, caching the result in a hash map - // to save comparisons in other tree nodes. - let key = PrimitiveComparisonKey { - prev_index: *prev_index, - curr_index: *curr_index, - }; - - let prim_compare_result = *compare_cache - .entry(key) - .or_insert_with(|| { - let prev = &prev_prims[i0]; - let curr = &curr_prims[i1]; - prim_comparer.compare_prim(prev, curr) - }); - - // If not the same, mark this node as dirty and update the dirty rect - if prim_compare_result != PrimitiveCompareResult::Equal { - if invalidation_reason.is_none() { - *invalidation_reason = Some(InvalidationReason::Content); - } - *dirty_rect = self.rect.union(dirty_rect); - *dirty_tracker = *dirty_tracker | 1; - break; - } - } - } else { - if invalidation_reason.is_none() { - *invalidation_reason = Some(InvalidationReason::PrimCount); - } - *dirty_rect = self.rect.union(dirty_rect); - *dirty_tracker = *dirty_tracker | 1; - } - } - } - } -} - impl CompositeState { // A helper function to destroy all native surfaces for a given list of tiles pub fn destroy_native_tiles<'a, I: Iterator<Item = &'a mut Box<Tile>>>(