commit 800255b55030980cba9b32f08c0e0b3a648fd341
parent d6393e58a45a3a1fc674b2818224d15a861bfce0
Author: Nicolas Silva <nical@fastmail.com>
Date: Mon, 15 Dec 2025 10:53:23 +0000
Bug 1998913 - Part 14 - Extract caching logic from the Tile struct. r=gfx-reviewers,gw
This helps with splitting tile_cache into smaller components. Long term we want to be able to cache child pictures and have non-tiled root pictures and this is a step in that direction.
Differential Revision: https://phabricator.services.mozilla.com/D276337
Diffstat:
4 files changed, 456 insertions(+), 370 deletions(-)
diff --git a/gfx/wr/webrender/src/invalidation/cached_surface.rs b/gfx/wr/webrender/src/invalidation/cached_surface.rs
@@ -0,0 +1,382 @@
+/* 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/. */
+
+use api::ColorF;
+use api::PropertyBindingId;
+use api::units::*;
+use smallvec::SmallVec;
+use crate::ItemUid;
+use crate::composite::CompositeState;
+use crate::internal_types::{FastHashMap, FrameId};
+use crate::invalidation::dependency::ColorBinding;
+use crate::invalidation::dependency::OpacityBinding;
+use crate::invalidation::dependency::OpacityBindingInfo;
+use crate::invalidation::dependency::PrimitiveComparisonKey;
+use crate::invalidation::dependency::SpatialNodeComparer;
+use crate::invalidation::{InvalidationReason, PrimitiveCompareResult, quadtree::TileNode};
+use crate::invalidation::dependency::{PrimitiveComparer, PrimitiveDependency};
+use crate::invalidation::dependency::ColorBindingInfo;
+use crate::picture::ImageDependency;
+use crate::picture::{PictureCompositeMode, SurfaceIndex, clampf};
+use crate::print_tree::PrintTreePrinter;
+use crate::resource_cache::ResourceCache;
+use crate::space::SpaceMapper;
+use crate::spatial_tree::SpatialNodeIndex;
+use crate::tile_cache::{TileDescriptor, PrimitiveDescriptor, PrimitiveDependencyIndex};
+use crate::visibility::FrameVisibilityContext;
+use peek_poke::poke_into_vec;
+use std::mem;
+
+pub struct CachedSurface {
+ pub current_descriptor: TileDescriptor,
+ pub prev_descriptor: TileDescriptor,
+ pub is_valid: bool,
+ pub local_valid_rect: PictureBox2D,
+ pub local_dirty_rect: PictureRect,
+ pub local_rect: PictureRect,
+ pub root: TileNode,
+ pub background_color: Option<ColorF>,
+ pub invalidation_reason: Option<InvalidationReason>,
+ pub sub_graphs: Vec<(PictureRect, Vec<(PictureCompositeMode, SurfaceIndex)>)>,
+}
+
+impl CachedSurface {
+ pub fn new() -> Self {
+ CachedSurface {
+ current_descriptor: TileDescriptor::new(),
+ prev_descriptor: TileDescriptor::new(),
+ is_valid: false,
+ local_valid_rect: PictureBox2D::zero(),
+ local_dirty_rect: PictureRect::zero(),
+ local_rect: PictureRect::zero(),
+ root: TileNode::new_leaf(Vec::new()),
+ background_color: None,
+ invalidation_reason: None,
+ sub_graphs: Vec::new(),
+ }
+ }
+
+ pub fn print(&self, pt: &mut dyn PrintTreePrinter) {
+ pt.add_item(format!("background_color: {:?}", self.background_color));
+ pt.add_item(format!("invalidation_reason: {:?}", self.invalidation_reason));
+ self.current_descriptor.print(pt);
+ }
+
+ /// Setup state before primitive dependency calculation.
+ pub fn pre_update(
+ &mut self,
+ background_color: Option<ColorF>,
+ local_tile_rect: PictureRect,
+ frame_id: FrameId,
+ is_visible: bool,
+ ) {
+ // TODO(gw): This is a hack / fix for Box2D::union in euclid not working with
+ // zero sized rect accumulation. Once that lands, we'll revert this
+ // to be zero.
+ self.local_valid_rect = PictureBox2D::new(
+ PicturePoint::new( 1.0e32, 1.0e32),
+ PicturePoint::new(-1.0e32, -1.0e32),
+ );
+ self.invalidation_reason = None;
+ self.sub_graphs.clear();
+
+ // If the tile isn't visible, early exit, skipping the normal set up to
+ // validate dependencies. Instead, we will only compare the current tile
+ // dependencies the next time it comes into view.
+ if !is_visible {
+ return;
+ }
+
+ if background_color != self.background_color {
+ self.invalidate(None, InvalidationReason::BackgroundColor);
+ self.background_color = background_color;
+ }
+
+ // Clear any dependencies so that when we rebuild them we
+ // can compare if the tile has the same content.
+ mem::swap(
+ &mut self.current_descriptor,
+ &mut self.prev_descriptor,
+ );
+ self.current_descriptor.clear();
+ self.root.clear(local_tile_rect);
+
+ self.current_descriptor.last_updated_frame_id = frame_id;
+ }
+
+ pub fn add_prim_dependency(
+ &mut self,
+ info: &PrimitiveDependencyInfo,
+ local_tile_rect: PictureRect,
+ ) {
+ // Incorporate the bounding rect of the primitive in the local valid rect
+ // for this tile. This is used to minimize the size of the scissor rect
+ // during rasterization and the draw rect during composition of partial tiles.
+ self.local_valid_rect = self.local_valid_rect.union(&info.prim_clip_box);
+
+ // TODO(gw): The prim_clip_rect can be impacted by the clip rect of the display port,
+ // which can cause invalidations when a new display list with changed
+ // display port is received. To work around this, clamp the prim clip rect
+ // to the tile boundaries - if the clip hasn't affected the tile, then the
+ // changed clip can't affect the content of the primitive on this tile.
+ // In future, we could consider supplying the display port clip from Gecko
+ // in a different way (e.g. as a scroll frame clip) which still provides
+ // the desired clip for checkerboarding, but doesn't require this extra
+ // work below.
+
+ // TODO(gw): This is a hot part of the code - we could probably optimize further by:
+ // - Using min/max instead of clamps below (if we guarantee the rects are well formed)
+
+ let pmin = local_tile_rect.min;
+ let pmax = local_tile_rect.max;
+
+ let prim_clip_box = PictureBox2D::new(
+ PicturePoint::new(
+ clampf(info.prim_clip_box.min.x, pmin.x, pmax.x),
+ clampf(info.prim_clip_box.min.y, pmin.y, pmax.y),
+ ),
+ PicturePoint::new(
+ clampf(info.prim_clip_box.max.x, pmin.x, pmax.x),
+ clampf(info.prim_clip_box.max.y, pmin.y, pmax.y),
+ ),
+ );
+
+ // Update the tile descriptor, used for tile comparison during scene swaps.
+ let prim_index = PrimitiveDependencyIndex(self.current_descriptor.prims.len() as u32);
+
+ // Encode the deps for this primitive in the `dep_data` byte buffer.
+ let dep_offset = self.current_descriptor.dep_data.len() as u32;
+ let mut dep_count = 0;
+
+ for clip in &info.clips {
+ dep_count += 1;
+ poke_into_vec(
+ &PrimitiveDependency::Clip {
+ clip: *clip,
+ },
+ &mut self.current_descriptor.dep_data,
+ );
+ }
+
+ for spatial_node_index in &info.spatial_nodes {
+ dep_count += 1;
+ poke_into_vec(
+ &PrimitiveDependency::SpatialNode {
+ index: *spatial_node_index,
+ },
+ &mut self.current_descriptor.dep_data,
+ );
+ }
+
+ for image in &info.images {
+ dep_count += 1;
+ poke_into_vec(
+ &PrimitiveDependency::Image {
+ image: *image,
+ },
+ &mut self.current_descriptor.dep_data,
+ );
+ }
+
+ for binding in &info.opacity_bindings {
+ dep_count += 1;
+ poke_into_vec(
+ &PrimitiveDependency::OpacityBinding {
+ binding: *binding,
+ },
+ &mut self.current_descriptor.dep_data,
+ );
+ }
+
+ if let Some(ref binding) = info.color_binding {
+ dep_count += 1;
+ poke_into_vec(
+ &PrimitiveDependency::ColorBinding {
+ binding: *binding,
+ },
+ &mut self.current_descriptor.dep_data,
+ );
+ }
+
+ self.current_descriptor.prims.push(PrimitiveDescriptor {
+ prim_uid: info.prim_uid,
+ prim_clip_box,
+ dep_offset,
+ dep_count,
+ });
+
+ // Add this primitive to the dirty rect quadtree.
+ self.root.add_prim(prim_index, &info.prim_clip_box);
+ }
+
+ /// Check if the content of the previous and current tile descriptors match
+ fn update_dirty_rects(
+ &mut self,
+ ctx: &TileUpdateDirtyContext,
+ state: &mut TileUpdateDirtyState,
+ invalidation_reason: &mut Option<InvalidationReason>,
+ frame_context: &FrameVisibilityContext,
+ ) -> PictureRect {
+ let mut prim_comparer = PrimitiveComparer::new(
+ &self.prev_descriptor,
+ &self.current_descriptor,
+ state.resource_cache,
+ state.spatial_node_comparer,
+ ctx.opacity_bindings,
+ ctx.color_bindings,
+ );
+
+ let mut dirty_rect = PictureBox2D::zero();
+ self.root.update_dirty_rects(
+ &self.prev_descriptor.prims,
+ &self.current_descriptor.prims,
+ &mut prim_comparer,
+ &mut dirty_rect,
+ state.compare_cache,
+ invalidation_reason,
+ frame_context,
+ );
+
+ dirty_rect
+ }
+
+ /// Invalidate a tile based on change in content. This
+ /// must be called even if the tile is not currently
+ /// visible on screen. We might be able to improve this
+ /// later by changing how ComparableVec is used.
+ pub fn update_content_validity(
+ &mut self,
+ ctx: &TileUpdateDirtyContext,
+ state: &mut TileUpdateDirtyState,
+ frame_context: &FrameVisibilityContext,
+ ) {
+ // Check if the contents of the primitives, clips, and
+ // other dependencies are the same.
+ state.compare_cache.clear();
+ let mut invalidation_reason = None;
+ let dirty_rect = self.update_dirty_rects(
+ ctx,
+ state,
+ &mut invalidation_reason,
+ frame_context,
+ );
+
+ if !dirty_rect.is_empty() {
+ self.invalidate(
+ Some(dirty_rect),
+ invalidation_reason.expect("bug: no invalidation_reason")
+ );
+ }
+ if ctx.invalidate_all {
+ self.invalidate(None, InvalidationReason::ScaleChanged);
+ }
+ // TODO(gw): We can avoid invalidating the whole tile in some cases here,
+ // but it should be a fairly rare invalidation case.
+ if self.current_descriptor.local_valid_rect != self.prev_descriptor.local_valid_rect {
+ self.invalidate(None, InvalidationReason::ValidRectChanged);
+ state.composite_state.dirty_rects_are_valid = false;
+ }
+ }
+
+ /// Invalidate this tile. If `invalidation_rect` is None, the entire
+ /// tile is invalidated.
+ pub fn invalidate(
+ &mut self,
+ invalidation_rect: Option<PictureRect>,
+ reason: InvalidationReason,
+ ) {
+ self.is_valid = false;
+
+ match invalidation_rect {
+ Some(rect) => {
+ self.local_dirty_rect = self.local_dirty_rect.union(&rect);
+ }
+ None => {
+ self.local_dirty_rect = self.local_rect;
+ }
+ }
+
+ if self.invalidation_reason.is_none() {
+ self.invalidation_reason = Some(reason);
+ }
+ }
+}
+
+// Immutable context passed to picture cache tiles during update_dirty_and_valid_rects
+pub struct TileUpdateDirtyContext<'a> {
+ /// Maps from picture cache coords -> world space coords.
+ pub pic_to_world_mapper: SpaceMapper<PicturePixel, WorldPixel>,
+
+ /// Global scale factor from world -> device pixels.
+ pub global_device_pixel_scale: DevicePixelScale,
+
+ /// Information about opacity bindings from the picture cache.
+ pub opacity_bindings: &'a FastHashMap<PropertyBindingId, OpacityBindingInfo>,
+
+ /// Information about color bindings from the picture cache.
+ pub color_bindings: &'a FastHashMap<PropertyBindingId, ColorBindingInfo>,
+
+ /// The local rect of the overall picture cache
+ pub local_rect: PictureRect,
+
+ /// If true, the scale factor of the root transform for this picture
+ /// cache changed, so we need to invalidate the tile and re-render.
+ pub invalidate_all: bool,
+}
+
+// Mutable state passed to picture cache tiles during update_dirty_and_valid_rects
+pub struct TileUpdateDirtyState<'a> {
+ /// Allow access to the texture cache for requesting tiles
+ pub resource_cache: &'a mut ResourceCache,
+
+ /// Current configuration and setup for compositing all the picture cache tiles in renderer.
+ pub composite_state: &'a mut CompositeState,
+
+ /// A cache of comparison results to avoid re-computation during invalidation.
+ pub compare_cache: &'a mut FastHashMap<PrimitiveComparisonKey, PrimitiveCompareResult>,
+
+ /// Information about transform node differences from last frame.
+ pub spatial_node_comparer: &'a mut SpatialNodeComparer,
+}
+
+/// Information about the dependencies of a single primitive instance.
+pub struct PrimitiveDependencyInfo {
+ /// Unique content identifier of the primitive.
+ pub prim_uid: ItemUid,
+
+ /// The (conservative) clipped area in picture space this primitive occupies.
+ pub prim_clip_box: PictureBox2D,
+
+ /// Image keys this primitive depends on.
+ pub images: SmallVec<[ImageDependency; 8]>,
+
+ /// Opacity bindings this primitive depends on.
+ pub opacity_bindings: SmallVec<[OpacityBinding; 4]>,
+
+ /// Color binding this primitive depends on.
+ pub color_binding: Option<ColorBinding>,
+
+ /// Clips that this primitive depends on.
+ pub clips: SmallVec<[ItemUid; 8]>,
+
+ /// Spatial nodes references by the clip dependencies of this primitive.
+ pub spatial_nodes: SmallVec<[SpatialNodeIndex; 4]>,
+}
+
+impl PrimitiveDependencyInfo {
+ pub fn new(
+ prim_uid: crate::intern::ItemUid,
+ prim_clip_box: PictureBox2D,
+ ) -> Self {
+ PrimitiveDependencyInfo {
+ prim_uid,
+ prim_clip_box,
+ images: smallvec::SmallVec::new(),
+ opacity_bindings: smallvec::SmallVec::new(),
+ color_binding: None,
+ clips: smallvec::SmallVec::new(),
+ spatial_nodes: smallvec::SmallVec::new(),
+ }
+ }
+}
diff --git a/gfx/wr/webrender/src/invalidation/mod.rs b/gfx/wr/webrender/src/invalidation/mod.rs
@@ -9,6 +9,7 @@
pub mod dependency;
pub mod quadtree;
+pub mod cached_surface;
use api::units::*;
use crate::spatial_tree::{SpatialTree, SpatialNodeIndex};
diff --git a/gfx/wr/webrender/src/picture.rs b/gfx/wr/webrender/src/picture.rs
@@ -1451,7 +1451,7 @@ impl PicturePrimitive {
if !tile.is_visible {
continue;
}
- let rect = tile.local_tile_rect.intersection(&tile_cache.local_rect);
+ let rect = tile.cached_surface.local_rect.intersection(&tile_cache.local_rect);
if let Some(rect) = rect {
draw_debug_border(
&rect,
@@ -1494,10 +1494,10 @@ impl PicturePrimitive {
if !tile.is_visible {
continue;
}
- tile.root.draw_debug_rects(
+ tile.cached_surface.root.draw_debug_rects(
&map_pic_to_world,
tile.is_opaque,
- tile.current_descriptor.local_valid_rect,
+ tile.cached_surface.current_descriptor.local_valid_rect,
scratch,
frame_context.global_device_pixel_scale,
);
@@ -1587,19 +1587,19 @@ fn prepare_tiled_picture_surface(
for (sub_slice_index, sub_slice) in tile_cache.sub_slices.iter_mut().enumerate() {
for tile in sub_slice.tiles.values_mut() {
// Ensure that the dirty rect doesn't extend outside the local valid rect.
- tile.local_dirty_rect = tile.local_dirty_rect
- .intersection(&tile.current_descriptor.local_valid_rect)
- .unwrap_or_else(|| { tile.is_valid = true; PictureRect::zero() });
+ tile.cached_surface.local_dirty_rect = tile.cached_surface.local_dirty_rect
+ .intersection(&tile.cached_surface.current_descriptor.local_valid_rect)
+ .unwrap_or_else(|| { tile.cached_surface.is_valid = true; PictureRect::zero() });
let valid_rect = frame_state.composite_state.get_surface_rect(
- &tile.current_descriptor.local_valid_rect,
- &tile.local_tile_rect,
+ &tile.cached_surface.current_descriptor.local_valid_rect,
+ &tile.cached_surface.local_rect,
tile_cache.transform_index,
).to_i32();
let scissor_rect = frame_state.composite_state.get_surface_rect(
- &tile.local_dirty_rect,
- &tile.local_tile_rect,
+ &tile.cached_surface.local_dirty_rect,
+ &tile.cached_surface.local_rect,
tile_cache.transform_index,
).to_i32().intersection(&valid_rect).unwrap_or_else(|| { Box2D::zero() });
@@ -1658,7 +1658,7 @@ fn prepare_tiled_picture_surface(
// that here and mark the tile non-visible. This is a bit of a hack - we should
// ideally handle these in a more accurate way so we don't end up with an empty
// rect here.
- if !tile.is_valid && (scissor_rect.is_empty() || valid_rect.is_empty()) {
+ if !tile.cached_surface.is_valid && (scissor_rect.is_empty() || valid_rect.is_empty()) {
tile.is_visible = false;
}
}
@@ -1724,14 +1724,14 @@ fn prepare_tiled_picture_surface(
// Ensure - again - that the dirty rect doesn't extend outside the local valid rect,
// as the tile could have been invalidated since the first computation.
- tile.local_dirty_rect = tile.local_dirty_rect
- .intersection(&tile.current_descriptor.local_valid_rect)
- .unwrap_or_else(|| { tile.is_valid = true; PictureRect::zero() });
+ tile.cached_surface.local_dirty_rect = tile.cached_surface.local_dirty_rect
+ .intersection(&tile.cached_surface.current_descriptor.local_valid_rect)
+ .unwrap_or_else(|| { tile.cached_surface.is_valid = true; PictureRect::zero() });
- surface_local_dirty_rect = surface_local_dirty_rect.union(&tile.local_dirty_rect);
+ surface_local_dirty_rect = surface_local_dirty_rect.union(&tile.cached_surface.local_dirty_rect);
// Update the world/device dirty rect
- let world_dirty_rect = map_pic_to_world.map(&tile.local_dirty_rect).expect("bug");
+ let world_dirty_rect = map_pic_to_world.map(&tile.cached_surface.local_dirty_rect).expect("bug");
let device_rect = (tile.world_tile_rect * frame_context.global_device_pixel_scale).round();
tile.device_dirty_rect = (world_dirty_rect * frame_context.global_device_pixel_scale)
@@ -1739,7 +1739,7 @@ fn prepare_tiled_picture_surface(
.intersection(&device_rect)
.unwrap_or_else(DeviceRect::zero);
- if tile.is_valid {
+ if tile.cached_surface.is_valid {
if frame_context.fb_config.testing {
debug_info.tiles.insert(
tile.tile_offset,
@@ -1751,7 +1751,7 @@ fn prepare_tiled_picture_surface(
// so that we include in the dirty region tiles that are handled by a background color only (no
// surface allocation).
tile_cache.dirty_region.add_dirty_region(
- tile.local_dirty_rect,
+ tile.cached_surface.local_dirty_rect,
frame_context.spatial_tree,
);
@@ -1821,7 +1821,7 @@ fn prepare_tiled_picture_surface(
// TODO(gw): `content_origin` should actually be in RasterPixels to be consistent
// with both local / screen raster modes, but this involves a lot of
// changes to render task and picture code.
- let content_origin_f = tile.local_tile_rect.min.cast_unit() * device_pixel_scale;
+ let content_origin_f = tile.cached_surface.local_rect.min.cast_unit() * device_pixel_scale;
let content_origin = content_origin_f.round();
// TODO: these asserts used to have a threshold of 0.01 but failed intermittently the
// gfx/layers/apz/test/mochitest/test_group_double_tap_zoom-2.html test on android.
@@ -1837,8 +1837,8 @@ fn prepare_tiled_picture_surface(
// Recompute the scissor rect as the tile could have been invalidated since the first computation.
let scissor_rect = frame_state.composite_state.get_surface_rect(
- &tile.local_dirty_rect,
- &tile.local_tile_rect,
+ &tile.cached_surface.local_dirty_rect,
+ &tile.cached_surface.local_rect,
tile_cache.transform_index,
).to_i32();
@@ -1871,13 +1871,13 @@ fn prepare_tiled_picture_surface(
// TODO(gw): As a performance optimization, we could skip the resolve picture
// if the dirty rect is the same as the resolve rect (probably quite
// common for effects that scroll underneath a backdrop-filter, for example).
- let use_tile_composite = !tile.sub_graphs.is_empty();
+ let use_tile_composite = !tile.cached_surface.sub_graphs.is_empty();
if use_tile_composite {
- let mut local_content_rect = tile.local_dirty_rect;
+ let mut local_content_rect = tile.cached_surface.local_dirty_rect;
- for (sub_graph_rect, surface_stack) in &tile.sub_graphs {
- if let Some(dirty_sub_graph_rect) = sub_graph_rect.intersection(&tile.local_dirty_rect) {
+ for (sub_graph_rect, surface_stack) in &tile.cached_surface.sub_graphs {
+ if let Some(dirty_sub_graph_rect) = sub_graph_rect.intersection(&tile.cached_surface.local_dirty_rect) {
for (composite_mode, surface_index) in surface_stack {
let surface = &frame_state.surfaces[surface_index.0];
@@ -1894,7 +1894,7 @@ fn prepare_tiled_picture_surface(
// We know that we'll never need to sample > 300 device pixels outside the tile
// for blurring, so clamp the content rect here so that we don't try to allocate
// a really large surface in the case of a drop-shadow with large offset.
- let max_content_rect = (tile.local_dirty_rect.cast_unit() * device_pixel_scale)
+ let max_content_rect = (tile.cached_surface.local_dirty_rect.cast_unit() * device_pixel_scale)
.inflate(
MAX_BLUR_RADIUS * BLUR_SAMPLE_SCALE,
MAX_BLUR_RADIUS * BLUR_SAMPLE_SCALE,
@@ -1960,7 +1960,7 @@ fn prepare_tiled_picture_surface(
SurfaceTileDescriptor {
current_task_id: render_task_id,
composite_task_id: Some(composite_task_id),
- dirty_rect: tile.local_dirty_rect,
+ dirty_rect: tile.cached_surface.local_dirty_rect,
},
);
} else {
@@ -1995,7 +1995,7 @@ fn prepare_tiled_picture_surface(
SurfaceTileDescriptor {
current_task_id: render_task_id,
composite_task_id: None,
- dirty_rect: tile.local_dirty_rect,
+ dirty_rect: tile.cached_surface.local_dirty_rect,
},
);
}
@@ -2005,8 +2005,8 @@ fn prepare_tiled_picture_surface(
debug_info.tiles.insert(
tile.tile_offset,
TileDebugInfo::Dirty(DirtyTileDebugInfo {
- local_valid_rect: tile.current_descriptor.local_valid_rect,
- local_dirty_rect: tile.local_dirty_rect,
+ local_valid_rect: tile.cached_surface.current_descriptor.local_valid_rect,
+ local_dirty_rect: tile.cached_surface.local_dirty_rect,
}),
);
}
@@ -2041,9 +2041,9 @@ fn prepare_tiled_picture_surface(
let composite_tile = CompositeTile {
kind: tile_kind(&surface, is_opaque),
surface,
- local_rect: tile.local_tile_rect,
- local_valid_rect: tile.current_descriptor.local_valid_rect,
- local_dirty_rect: tile.local_dirty_rect,
+ local_rect: tile.cached_surface.local_rect,
+ local_valid_rect: tile.cached_surface.current_descriptor.local_valid_rect,
+ local_dirty_rect: tile.cached_surface.local_dirty_rect,
device_clip_rect,
z_id: tile.z_id,
transform_index: tile_cache.transform_index,
@@ -2054,8 +2054,8 @@ fn prepare_tiled_picture_surface(
sub_slice.composite_tiles.push(composite_tile);
// Now that the tile is valid, reset the dirty rect.
- tile.local_dirty_rect = PictureRect::zero();
- tile.is_valid = true;
+ tile.cached_surface.local_dirty_rect = PictureRect::zero();
+ tile.cached_surface.is_valid = true;
}
// Sort the tile descriptor lists, since iterating values in the tile_cache.tiles
diff --git a/gfx/wr/webrender/src/tile_cache/mod.rs b/gfx/wr/webrender/src/tile_cache/mod.rs
@@ -23,11 +23,12 @@ use crate::composite::{CompositeTileDescriptor, CompositeTile};
use crate::gpu_types::ZBufferId;
use crate::intern::ItemUid;
use crate::internal_types::{FastHashMap, FrameId, Filter};
-use crate::invalidation::{InvalidationReason, DirtyRegion, PrimitiveCompareResult, quadtree::TileNode};
-use crate::invalidation::dependency::{PrimitiveComparer, PrimitiveDependency, ImageDependency};
+use crate::invalidation::{InvalidationReason, DirtyRegion, PrimitiveCompareResult};
+use crate::invalidation::cached_surface::{CachedSurface, TileUpdateDirtyContext, TileUpdateDirtyState, PrimitiveDependencyInfo};
+use crate::invalidation::dependency::{PrimitiveDependency, ImageDependency};
use crate::invalidation::dependency::{SpatialNodeComparer, PrimitiveComparisonKey};
-use crate::invalidation::dependency::{OpacityBindingInfo, ColorBindingInfo, OpacityBinding, ColorBinding};
-use crate::picture::{SurfaceTextureDescriptor, PictureCompositeMode, SurfaceIndex, clamp, clampf};
+use crate::invalidation::dependency::{OpacityBindingInfo, ColorBindingInfo};
+use crate::picture::{SurfaceTextureDescriptor, PictureCompositeMode, SurfaceIndex, clamp};
use crate::picture::{get_relative_scale_offset, PicturePrimitive};
use crate::picture::MAX_COMPOSITOR_SURFACES_SIZE;
use crate::prim_store::{PrimitiveInstance, PrimitiveInstanceKind, PrimitiveScratchBuffer, PictureIndex};
@@ -45,8 +46,7 @@ use crate::util::{ScaleOffset, MatrixHelpers, MaxRect};
use crate::visibility::{FrameVisibilityContext, FrameVisibilityState, VisibilityState, PrimitiveVisibilityFlags};
use euclid::approxeq::ApproxEq;
use euclid::Box2D;
-use peek_poke::{PeekPoke, poke_into_vec, ensure_red_zone};
-use smallvec::SmallVec;
+use peek_poke::{PeekPoke, ensure_red_zone};
use std::fmt::{Display, Error, Formatter};
use std::{marker, mem, u32};
use std::sync::atomic::{AtomicUsize, Ordering};
@@ -339,10 +339,6 @@ pub struct Tile {
pub tile_offset: TileOffset,
/// The current world rect of this tile.
pub world_tile_rect: WorldRect,
- /// The current local rect of this tile.
- pub local_tile_rect: PictureRect,
- /// The picture space dirty rect for this tile.
- pub local_dirty_rect: PictureRect,
/// The device space dirty rect for this tile.
/// TODO(gw): We have multiple dirty rects available due to the quadtree above. In future,
/// expose these as multiple dirty rects, which will help in some cases.
@@ -351,17 +347,8 @@ pub struct Tile {
pub world_valid_rect: WorldRect,
/// Device space rect that contains valid pixels region of this tile.
pub device_valid_rect: DeviceRect,
- /// Uniquely describes the content of this tile, in a way that can be
- /// (reasonably) efficiently hashed and compared.
- pub current_descriptor: TileDescriptor,
- /// The content descriptor for this tile from the previous frame.
- pub prev_descriptor: TileDescriptor,
/// Handle to the backing surface for this tile.
pub surface: Option<TileSurface>,
- /// If true, this tile is marked valid, and the existing texture
- /// cache handle can be used. Tiles are invalidated during the
- /// build_dirty_regions method.
- pub is_valid: bool,
/// If true, this tile intersects with the currently visible screen
/// rect, and will be drawn.
pub is_visible: bool,
@@ -371,17 +358,10 @@ pub struct Tile {
/// If true, the tile was determined to be opaque, which means blending
/// can be disabled when drawing it.
pub is_opaque: bool,
- /// Root node of the quadtree dirty rect tracker.
- pub root: TileNode,
- /// The last rendered background color on this tile.
- background_color: Option<ColorF>,
- /// The first reason the tile was invalidated this frame.
- invalidation_reason: Option<InvalidationReason>,
- /// The local space valid rect for all primitives that affect this tile.
- pub local_valid_rect: PictureBox2D,
/// z-buffer id for this tile
pub z_id: ZBufferId,
- pub sub_graphs: Vec<(PictureRect, Vec<(PictureCompositeMode, SurfaceIndex)>)>,
+ /// Cached surface state (content tracking, invalidation, dependencies)
+ pub cached_surface: CachedSurface,
}
impl Tile {
@@ -391,69 +371,27 @@ impl Tile {
Tile {
tile_offset,
- local_tile_rect: PictureRect::zero(),
world_tile_rect: WorldRect::zero(),
world_valid_rect: WorldRect::zero(),
device_valid_rect: DeviceRect::zero(),
- local_dirty_rect: PictureRect::zero(),
device_dirty_rect: DeviceRect::zero(),
surface: None,
- current_descriptor: TileDescriptor::new(),
- prev_descriptor: TileDescriptor::new(),
- is_valid: false,
is_visible: false,
id,
is_opaque: false,
- root: TileNode::new_leaf(Vec::new()),
- background_color: None,
- invalidation_reason: None,
- local_valid_rect: PictureBox2D::zero(),
z_id: ZBufferId::invalid(),
- sub_graphs: Vec::new(),
+ cached_surface: CachedSurface::new(),
}
}
/// Print debug information about this tile to a tree printer.
fn print(&self, pt: &mut dyn PrintTreePrinter) {
pt.new_level(format!("Tile {:?}", self.id));
- pt.add_item(format!("local_tile_rect: {:?}", self.local_tile_rect));
- pt.add_item(format!("background_color: {:?}", self.background_color));
- pt.add_item(format!("invalidation_reason: {:?}", self.invalidation_reason));
- self.current_descriptor.print(pt);
+ pt.add_item(format!("local_rect: {:?}", self.cached_surface.local_rect));
+ self.cached_surface.print(pt);
pt.end_level();
}
- /// Check if the content of the previous and current tile descriptors match
- fn update_dirty_rects(
- &mut self,
- ctx: &TileUpdateDirtyContext,
- state: &mut TileUpdateDirtyState,
- invalidation_reason: &mut Option<InvalidationReason>,
- frame_context: &FrameVisibilityContext,
- ) -> PictureRect {
- let mut prim_comparer = PrimitiveComparer::new(
- &self.prev_descriptor,
- &self.current_descriptor,
- state.resource_cache,
- state.spatial_node_comparer,
- ctx.opacity_bindings,
- ctx.color_bindings,
- );
-
- let mut dirty_rect = PictureBox2D::zero();
- self.root.update_dirty_rects(
- &self.prev_descriptor.prims,
- &self.current_descriptor.prims,
- &mut prim_comparer,
- &mut dirty_rect,
- state.compare_cache,
- invalidation_reason,
- frame_context,
- );
-
- dirty_rect
- }
-
/// Invalidate a tile based on change in content. This
/// must be called even if the tile is not currently
/// visible on screen. We might be able to improve this
@@ -464,31 +402,11 @@ impl Tile {
state: &mut TileUpdateDirtyState,
frame_context: &FrameVisibilityContext,
) {
- // Check if the contents of the primitives, clips, and
- // other dependencies are the same.
- state.compare_cache.clear();
- let mut invalidation_reason = None;
- let dirty_rect = self.update_dirty_rects(
+ self.cached_surface.update_content_validity(
ctx,
state,
- &mut invalidation_reason,
frame_context,
);
- if !dirty_rect.is_empty() {
- self.invalidate(
- Some(dirty_rect),
- invalidation_reason.expect("bug: no invalidation_reason"),
- );
- }
- if ctx.invalidate_all {
- self.invalidate(None, InvalidationReason::ScaleChanged);
- }
- // TODO(gw): We can avoid invalidating the whole tile in some cases here,
- // but it should be a fairly rare invalidation case.
- if self.current_descriptor.local_valid_rect != self.prev_descriptor.local_valid_rect {
- self.invalidate(None, InvalidationReason::ValidRectChanged);
- state.composite_state.dirty_rects_are_valid = false;
- }
}
/// Invalidate this tile. If `invalidation_rect` is None, the entire
@@ -498,20 +416,7 @@ impl Tile {
invalidation_rect: Option<PictureRect>,
reason: InvalidationReason,
) {
- self.is_valid = false;
-
- match invalidation_rect {
- Some(rect) => {
- self.local_dirty_rect = self.local_dirty_rect.union(&rect);
- }
- None => {
- self.local_dirty_rect = self.local_tile_rect;
- }
- }
-
- if self.invalidation_reason.is_none() {
- self.invalidation_reason = Some(reason);
- }
+ self.cached_surface.invalidate(invalidation_rect, reason);
}
/// Called during pre_update of a tile cache instance. Allows the
@@ -520,7 +425,7 @@ impl Tile {
&mut self,
ctx: &TilePreUpdateContext,
) {
- self.local_tile_rect = PictureRect::new(
+ self.cached_surface.local_rect = PictureRect::new(
PicturePoint::new(
self.tile_offset.x as f32 * ctx.tile_size.width,
self.tile_offset.y as f32 * ctx.tile_size.height,
@@ -530,47 +435,21 @@ impl Tile {
(self.tile_offset.y + 1) as f32 * ctx.tile_size.height,
),
);
- // TODO(gw): This is a hack / fix for Box2D::union in euclid not working with
- // zero sized rect accumulation. Once that lands, we'll revert this
- // to be zero.
- self.local_valid_rect = PictureBox2D::new(
- PicturePoint::new( 1.0e32, 1.0e32),
- PicturePoint::new(-1.0e32, -1.0e32),
- );
- self.invalidation_reason = None;
- self.sub_graphs.clear();
self.world_tile_rect = ctx.pic_to_world_mapper
- .map(&self.local_tile_rect)
+ .map(&self.cached_surface.local_rect)
.expect("bug: map local tile rect");
// Check if this tile is currently on screen.
self.is_visible = self.world_tile_rect.intersects(&ctx.global_screen_world_rect);
- // If the tile isn't visible, early exit, skipping the normal set up to
- // validate dependencies. Instead, we will only compare the current tile
- // dependencies the next time it comes into view.
- if !self.is_visible {
- return;
- }
-
- if ctx.background_color != self.background_color {
- self.invalidate(None, InvalidationReason::BackgroundColor);
- self.background_color = ctx.background_color;
- }
-
- // Clear any dependencies so that when we rebuild them we
- // can compare if the tile has the same content.
- mem::swap(
- &mut self.current_descriptor,
- &mut self.prev_descriptor,
+ // Delegate to CachedSurface for content tracking setup
+ self.cached_surface.pre_update(
+ ctx.background_color,
+ self.cached_surface.local_rect,
+ ctx.frame_id,
+ self.is_visible,
);
- self.current_descriptor.clear();
- self.root.clear(self.local_tile_rect);
-
- // Since this tile is determined to be visible, it will get updated
- // dependencies, so update the frame id we are storing dependencies for.
- self.current_descriptor.last_updated_frame_id = ctx.frame_id;
}
/// Add dependencies for a given primitive to this tile.
@@ -584,104 +463,7 @@ impl Tile {
return;
}
- // Incorporate the bounding rect of the primitive in the local valid rect
- // for this tile. This is used to minimize the size of the scissor rect
- // during rasterization and the draw rect during composition of partial tiles.
- self.local_valid_rect = self.local_valid_rect.union(&info.prim_clip_box);
-
- // TODO(gw): The prim_clip_rect can be impacted by the clip rect of the display port,
- // which can cause invalidations when a new display list with changed
- // display port is received. To work around this, clamp the prim clip rect
- // to the tile boundaries - if the clip hasn't affected the tile, then the
- // changed clip can't affect the content of the primitive on this tile.
- // In future, we could consider supplying the display port clip from Gecko
- // in a different way (e.g. as a scroll frame clip) which still provides
- // the desired clip for checkerboarding, but doesn't require this extra
- // work below.
-
- // TODO(gw): This is a hot part of the code - we could probably optimize further by:
- // - Using min/max instead of clamps below (if we guarantee the rects are well formed)
-
- let tile_p0 = self.local_tile_rect.min;
- let tile_p1 = self.local_tile_rect.max;
-
- let prim_clip_box = PictureBox2D::new(
- PicturePoint::new(
- clampf(info.prim_clip_box.min.x, tile_p0.x, tile_p1.x),
- clampf(info.prim_clip_box.min.y, tile_p0.y, tile_p1.y),
- ),
- PicturePoint::new(
- clampf(info.prim_clip_box.max.x, tile_p0.x, tile_p1.x),
- clampf(info.prim_clip_box.max.y, tile_p0.y, tile_p1.y),
- ),
- );
-
- // Update the tile descriptor, used for tile comparison during scene swaps.
- let prim_index = PrimitiveDependencyIndex(self.current_descriptor.prims.len() as u32);
-
- // Encode the deps for this primitive in the `dep_data` byte buffer
- let dep_offset = self.current_descriptor.dep_data.len() as u32;
- let mut dep_count = 0;
-
- for clip in &info.clips {
- dep_count += 1;
- poke_into_vec(
- &PrimitiveDependency::Clip {
- clip: *clip,
- },
- &mut self.current_descriptor.dep_data,
- );
- }
-
- for spatial_node_index in &info.spatial_nodes {
- dep_count += 1;
- poke_into_vec(
- &PrimitiveDependency::SpatialNode {
- index: *spatial_node_index,
- },
- &mut self.current_descriptor.dep_data,
- );
- }
-
- for image in &info.images {
- dep_count += 1;
- poke_into_vec(
- &PrimitiveDependency::Image {
- image: *image,
- },
- &mut self.current_descriptor.dep_data,
- );
- }
-
- for binding in &info.opacity_bindings {
- dep_count += 1;
- poke_into_vec(
- &PrimitiveDependency::OpacityBinding {
- binding: *binding,
- },
- &mut self.current_descriptor.dep_data,
- );
- }
-
- if let Some(ref binding) = info.color_binding {
- dep_count += 1;
- poke_into_vec(
- &PrimitiveDependency::ColorBinding {
- binding: *binding,
- },
- &mut self.current_descriptor.dep_data,
- );
- }
-
- self.current_descriptor.prims.push(PrimitiveDescriptor {
- prim_uid: info.prim_uid,
- prim_clip_box,
- dep_offset,
- dep_count,
- });
-
- // Add this primitive to the dirty rect quadtree.
- self.root.add_prim(prim_index, &info.prim_clip_box);
+ self.cached_surface.add_prim_dependency(info, self.cached_surface.local_rect);
}
/// Called during tile cache instance post_update. Allows invalidation and dirty
@@ -693,13 +475,13 @@ impl Tile {
frame_context: &FrameVisibilityContext,
) {
// Ensure peek-poke constraint is met, that `dep_data` is large enough
- ensure_red_zone::<PrimitiveDependency>(&mut self.current_descriptor.dep_data);
+ ensure_red_zone::<PrimitiveDependency>(&mut self.cached_surface.current_descriptor.dep_data);
// Register the frame id of this tile with the spatial node comparer, to ensure
// that it doesn't GC any spatial nodes from the comparer that are referenced
// by this tile. Must be done before we early exit below, so that we retain
// spatial node info even for tiles that are currently not visible.
- state.spatial_node_comparer.retain_for_frame(self.current_descriptor.last_updated_frame_id);
+ state.spatial_node_comparer.retain_for_frame(self.cached_surface.current_descriptor.last_updated_frame_id);
// If tile is not visible, just early out from here - we don't update dependencies
// so don't want to invalidate, merge, split etc. The tile won't need to be drawn
@@ -709,7 +491,7 @@ impl Tile {
}
// Calculate the overall valid rect for this tile.
- self.current_descriptor.local_valid_rect = self.local_valid_rect;
+ self.cached_surface.current_descriptor.local_valid_rect = self.cached_surface.local_valid_rect;
// TODO(gw): In theory, the local tile rect should always have an
// intersection with the overall picture rect. In practice,
@@ -721,15 +503,15 @@ impl Tile {
// accuracy issue above, so that this assumption holds, but
// it shouldn't have any noticeable effect on performance
// or memory usage (textures should never get allocated).
- self.current_descriptor.local_valid_rect = self.local_tile_rect
+ self.cached_surface.current_descriptor.local_valid_rect = self.cached_surface.local_rect
.intersection(&ctx.local_rect)
- .and_then(|r| r.intersection(&self.current_descriptor.local_valid_rect))
+ .and_then(|r| r.intersection(&self.cached_surface.current_descriptor.local_valid_rect))
.unwrap_or_else(PictureRect::zero);
// The device_valid_rect is referenced during `update_content_validity` so it
// must be updated here first.
self.world_valid_rect = ctx.pic_to_world_mapper
- .map(&self.current_descriptor.local_valid_rect)
+ .map(&self.cached_surface.current_descriptor.local_valid_rect)
.expect("bug: map local valid rect");
// The device rect is guaranteed to be aligned on a device pixel - the round
@@ -764,7 +546,7 @@ impl Tile {
// If there are no primitives there is no need to draw or cache it.
// Bug 1719232 - The final device valid rect does not always describe a non-empty
// region. Cull the tile as a workaround.
- if self.current_descriptor.prims.is_empty() || self.device_valid_rect.is_empty() {
+ if self.cached_surface.current_descriptor.prims.is_empty() || self.device_valid_rect.is_empty() {
// If there is a native compositor surface allocated for this (now empty) tile
// it must be freed here, otherwise the stale tile with previous contents will
// be composited. If the tile subsequently gets new primitives added to it, the
@@ -782,11 +564,11 @@ impl Tile {
// Check if this tile can be considered opaque. Opacity state must be updated only
// after all early out checks have been performed. Otherwise, we might miss updating
// the native surface next time this tile becomes visible.
- let clipped_rect = self.current_descriptor.local_valid_rect
+ let clipped_rect = self.cached_surface.current_descriptor.local_valid_rect
.intersection(&ctx.local_clip_rect)
.unwrap_or_else(PictureRect::zero);
- let has_opaque_bg_color = self.background_color.map_or(false, |c| c.a >= 1.0);
+ let has_opaque_bg_color = self.cached_surface.background_color.map_or(false, |c| c.a >= 1.0);
let has_opaque_backdrop = ctx.backdrop.map_or(false, |b| b.opaque_rect.contains_box(&clipped_rect));
let mut is_opaque = has_opaque_bg_color || has_opaque_backdrop;
@@ -842,9 +624,9 @@ impl Tile {
let max_split_level = 3;
// Consider splitting / merging dirty regions
- self.root.maybe_merge_or_split(
+ self.cached_surface.root.maybe_merge_or_split(
0,
- &self.current_descriptor.prims,
+ &self.cached_surface.current_descriptor.prims,
max_split_level,
);
}
@@ -853,8 +635,8 @@ impl Tile {
// The dirty rect will be set correctly by now. If the underlying platform
// doesn't support partial updates, and this tile isn't valid, force the dirty
// rect to be the size of the entire tile.
- if !self.is_valid && !supports_dirty_rects {
- self.local_dirty_rect = self.local_tile_rect;
+ if !self.cached_surface.is_valid && !supports_dirty_rects {
+ self.cached_surface.local_dirty_rect = self.cached_surface.local_rect;
}
// See if this tile is a simple color, in which case we can just draw
@@ -864,7 +646,7 @@ impl Tile {
// should be added as a follow up.
let is_simple_prim =
ctx.backdrop.map_or(false, |b| b.kind.is_some()) &&
- self.current_descriptor.prims.len() == 1 &&
+ self.cached_surface.current_descriptor.prims.len() == 1 &&
self.is_opaque &&
supports_simple_prims;
@@ -2961,7 +2743,7 @@ impl TileCacheInstance {
for x in p0.x .. p1.x {
let key = TileOffset::new(x, y);
let tile = sub_slice.tiles.get_mut(&key).expect("bug: no tile");
- tile.sub_graphs.push((pic_coverage_rect, surface_info.clone()));
+ tile.cached_surface.sub_graphs.push((pic_coverage_rect, surface_info.clone()));
}
}
@@ -3277,7 +3059,7 @@ impl TileCacheInstance {
for x in dirty_test.tile_rect.min.x .. dirty_test.tile_rect.max.x {
let key = TileOffset::new(x, y);
let tile = sub_slice.tiles.get_mut(&key).expect("bug: no tile");
- total_dirty_rect = total_dirty_rect.union(&tile.local_dirty_rect);
+ total_dirty_rect = total_dirty_rect.union(&tile.cached_surface.local_dirty_rect);
}
}
@@ -3527,43 +3309,6 @@ struct TilePreUpdateContext {
frame_id: FrameId,
}
-// Immutable context passed to picture cache tiles during update_dirty_and_valid_rects
-struct TileUpdateDirtyContext<'a> {
- /// Maps from picture cache coords -> world space coords.
- pic_to_world_mapper: SpaceMapper<PicturePixel, WorldPixel>,
-
- /// Global scale factor from world -> device pixels.
- global_device_pixel_scale: DevicePixelScale,
-
- /// Information about opacity bindings from the picture cache.
- opacity_bindings: &'a FastHashMap<PropertyBindingId, OpacityBindingInfo>,
-
- /// Information about color bindings from the picture cache.
- color_bindings: &'a FastHashMap<PropertyBindingId, ColorBindingInfo>,
-
- /// The local rect of the overall picture cache
- local_rect: PictureRect,
-
- /// If true, the scale factor of the root transform for this picture
- /// cache changed, so we need to invalidate the tile and re-render.
- invalidate_all: bool,
-}
-
-// Mutable state passed to picture cache tiles during update_dirty_and_valid_rects
-struct TileUpdateDirtyState<'a> {
- /// Allow access to the texture cache for requesting tiles
- resource_cache: &'a mut ResourceCache,
-
- /// Current configuration and setup for compositing all the picture cache tiles in renderer.
- composite_state: &'a mut CompositeState,
-
- /// A cache of comparison results to avoid re-computation during invalidation.
- compare_cache: &'a mut FastHashMap<PrimitiveComparisonKey, PrimitiveCompareResult>,
-
- /// Information about transform node differences from last frame.
- spatial_node_comparer: &'a mut SpatialNodeComparer,
-}
-
// Immutable context passed to picture cache tiles during post_update
struct TilePostUpdateContext<'a> {
/// The local clip rect (in picture space) of the entire picture cache
@@ -3590,45 +3335,3 @@ struct TilePostUpdateState<'a> {
/// Current configuration and setup for compositing all the picture cache tiles in renderer.
composite_state: &'a mut CompositeState,
}
-
-/// Information about the dependencies of a single primitive instance.
-struct PrimitiveDependencyInfo {
- /// Unique content identifier of the primitive.
- prim_uid: ItemUid,
-
- /// The (conservative) clipped area in picture space this primitive occupies.
- prim_clip_box: PictureBox2D,
-
- /// Image keys this primitive depends on.
- images: SmallVec<[ImageDependency; 8]>,
-
- /// Opacity bindings this primitive depends on.
- opacity_bindings: SmallVec<[OpacityBinding; 4]>,
-
- /// Color binding this primitive depends on.
- color_binding: Option<ColorBinding>,
-
- /// Clips that this primitive depends on.
- clips: SmallVec<[ItemUid; 8]>,
-
- /// Spatial nodes references by the clip dependencies of this primitive.
- spatial_nodes: SmallVec<[SpatialNodeIndex; 4]>,
-}
-
-impl PrimitiveDependencyInfo {
- /// Construct dependency info for a new primitive.
- fn new(
- prim_uid: ItemUid,
- prim_clip_box: PictureBox2D,
- ) -> Self {
- PrimitiveDependencyInfo {
- prim_uid,
- images: SmallVec::new(),
- opacity_bindings: SmallVec::new(),
- color_binding: None,
- prim_clip_box,
- clips: SmallVec::new(),
- spatial_nodes: SmallVec::new(),
- }
- }
-}