/* 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::{BorderRadius, ClipId, ClipMode, ColorF, DebugFlags, PrimitiveFlags, QualitySettings, RasterSpace}; use api::units::*; use crate::clip::{ClipItemKeyKind, ClipNodeId, ClipTreeBuilder}; use crate::frame_builder::FrameBuilderConfig; use crate::internal_types::FastHashMap; use crate::picture::{PrimitiveList, PictureCompositeMode, PicturePrimitive, SliceId}; use crate::picture::{Picture3DContext, TileCacheParams, TileOffset, PictureFlags}; use crate::prim_store::{PrimitiveInstance, PrimitiveStore, PictureIndex}; use crate::scene_building::SliceFlags; use crate::scene_builder_thread::Interners; use crate::spatial_tree::{SpatialNodeIndex, SceneSpatialTree}; use crate::util::VecHelper; use std::mem; /* Types and functionality related to picture caching. In future, we'll move more and more of the existing functionality out of picture.rs and into here. */ // If the page would create too many slices (an arbitrary definition where // it's assumed the GPU memory + compositing overhead would be too high) // then create a single picture cache for the remaining content. This at // least means that we can cache small content changes efficiently when // scrolling isn't occurring. Scrolling regions will be handled reasonably // efficiently by the dirty rect tracking (since it's likely that if the // page has so many slices there isn't a single major scroll region). const MAX_CACHE_SLICES: usize = 16; struct SliceDescriptor { prim_list: PrimitiveList, scroll_root: SpatialNodeIndex, } enum SliceKind { Default { secondary_slices: Vec, }, Atomic { prim_list: PrimitiveList, }, } impl SliceKind { fn default() -> Self { SliceKind::Default { secondary_slices: Vec::new(), } } } struct PrimarySlice { /// Whether this slice is atomic or has secondary slice(s) kind: SliceKind, /// Optional background color of this slice background_color: Option, /// Optional root clip for the iframe iframe_clip: Option, /// Information about how to draw and composite this slice slice_flags: SliceFlags, } impl PrimarySlice { fn new( slice_flags: SliceFlags, iframe_clip: Option, background_color: Option, ) -> Self { PrimarySlice { kind: SliceKind::default(), background_color, iframe_clip, slice_flags, } } fn has_too_many_slices(&self) -> bool { match self.kind { SliceKind::Atomic { .. } => false, SliceKind::Default { ref secondary_slices } => secondary_slices.len() > MAX_CACHE_SLICES, } } fn merge(&mut self) { self.slice_flags |= SliceFlags::IS_ATOMIC; let old = mem::replace( &mut self.kind, SliceKind::Default { secondary_slices: Vec::new() }, ); self.kind = match old { SliceKind::Default { mut secondary_slices } => { let mut prim_list = PrimitiveList::empty(); for descriptor in secondary_slices.drain(..) { prim_list.merge(descriptor.prim_list); } SliceKind::Atomic { prim_list, } } atomic => atomic, } } } /// Used during scene building to construct the list of pending tile caches. pub struct TileCacheBuilder { /// List of tile caches that have been created so far (last in the list is currently active). primary_slices: Vec, /// Cache the previous scroll root search for a spatial node, since they are often the same. prev_scroll_root_cache: (SpatialNodeIndex, SpatialNodeIndex), /// Handle to the root reference frame root_spatial_node_index: SpatialNodeIndex, /// Debug flags to provide to our TileCacheInstances. debug_flags: DebugFlags, } /// The output of a tile cache builder, containing all details needed to construct the /// tile cache(s) for the next scene, and retain tiles from the previous frame when sent /// send to the frame builder. pub struct TileCacheConfig { /// Mapping of slice id to the parameters needed to construct this tile cache. pub tile_caches: FastHashMap, /// Number of picture cache slices that were created (for profiler) pub picture_cache_slice_count: usize, } impl TileCacheConfig { pub fn new(picture_cache_slice_count: usize) -> Self { TileCacheConfig { tile_caches: FastHashMap::default(), picture_cache_slice_count, } } } impl TileCacheBuilder { /// Construct a new tile cache builder. pub fn new( root_spatial_node_index: SpatialNodeIndex, background_color: Option, debug_flags: DebugFlags, ) -> Self { TileCacheBuilder { primary_slices: vec![PrimarySlice::new(SliceFlags::empty(), None, background_color)], prev_scroll_root_cache: (SpatialNodeIndex::INVALID, SpatialNodeIndex::INVALID), root_spatial_node_index, debug_flags, } } pub fn make_current_slice_atomic(&mut self) { self.primary_slices .last_mut() .unwrap() .merge(); } /// Returns true if the current slice has no primitives added yet pub fn is_current_slice_empty(&self) -> bool { match self.primary_slices.last() { Some(slice) => { match slice.kind { SliceKind::Default { ref secondary_slices } => { secondary_slices.is_empty() } SliceKind::Atomic { ref prim_list } => { prim_list.is_empty() } } } None => { true } } } /// Set a barrier that forces a new tile cache next time a prim is added. pub fn add_tile_cache_barrier( &mut self, slice_flags: SliceFlags, iframe_clip: Option, ) { let new_slice = PrimarySlice::new( slice_flags, iframe_clip, None, ); self.primary_slices.push(new_slice); } /// Create a new tile cache for an existing prim_list fn build_tile_cache( &mut self, prim_list: PrimitiveList, spatial_tree: &SceneSpatialTree, ) -> Option { if prim_list.is_empty() { return None; } // Iterate the clusters and determine which is the most commonly occurring // scroll root. This is a reasonable heuristic to decide which spatial node // should be considered the scroll root of this tile cache, in order to // minimize the invalidations that occur due to scrolling. It's often the // case that a blend container will have only a single scroll root. let mut scroll_root_occurrences = FastHashMap::default(); for cluster in &prim_list.clusters { // If we encounter a cluster which has an unknown spatial node, // we don't include that in the set of spatial nodes that we // are trying to find scroll roots for. Later on, in finalize_picture, // the cluster spatial node will be updated to the selected scroll root. if cluster.spatial_node_index == SpatialNodeIndex::UNKNOWN { continue; } let scroll_root = find_scroll_root( cluster.spatial_node_index, &mut self.prev_scroll_root_cache, spatial_tree, true, ); *scroll_root_occurrences.entry(scroll_root).or_insert(0) += 1; } // We can't just select the most commonly occurring scroll root in this // primitive list. If that is a nested scroll root, there may be // primitives in the list that are outside that scroll root, which // can cause panics when calculating relative transforms. To ensure // this doesn't happen, only retain scroll root candidates that are // also ancestors of every other scroll root candidate. let scroll_roots: Vec = scroll_root_occurrences .keys() .cloned() .collect(); scroll_root_occurrences.retain(|parent_spatial_node_index, _| { scroll_roots.iter().all(|child_spatial_node_index| { parent_spatial_node_index == child_spatial_node_index || spatial_tree.is_ancestor( *parent_spatial_node_index, *child_spatial_node_index, ) }) }); // Select the scroll root by finding the most commonly occurring one let scroll_root = scroll_root_occurrences .iter() .max_by_key(|entry | entry.1) .map(|(spatial_node_index, _)| *spatial_node_index) .unwrap_or(self.root_spatial_node_index); Some(SliceDescriptor { scroll_root, prim_list, }) } /// Add a primitive, either to the current tile cache, or a new one, depending on various conditions. pub fn add_prim( &mut self, prim_instance: PrimitiveInstance, prim_rect: LayoutRect, spatial_node_index: SpatialNodeIndex, prim_flags: PrimitiveFlags, spatial_tree: &SceneSpatialTree, interners: &Interners, quality_settings: &QualitySettings, prim_instances: &mut Vec, clip_tree_builder: &ClipTreeBuilder, ) { let primary_slice = self.primary_slices.last_mut().unwrap(); match primary_slice.kind { SliceKind::Atomic { ref mut prim_list } => { prim_list.add_prim( prim_instance, prim_rect, spatial_node_index, prim_flags, prim_instances, clip_tree_builder, ); } SliceKind::Default { ref mut secondary_slices } => { assert_ne!(spatial_node_index, SpatialNodeIndex::UNKNOWN); // Check if we want to create a new slice based on the current / next scroll root let scroll_root = find_scroll_root( spatial_node_index, &mut self.prev_scroll_root_cache, spatial_tree, // Allow sticky frames as scroll roots, unless our quality settings prefer // subpixel AA over performance. !quality_settings.force_subpixel_aa_where_possible, ); let current_scroll_root = secondary_slices .last() .map(|p| p.scroll_root); let mut want_new_tile_cache = secondary_slices.is_empty(); if let Some(current_scroll_root) = current_scroll_root { want_new_tile_cache |= match (current_scroll_root, scroll_root) { (_, _) if current_scroll_root == self.root_spatial_node_index && scroll_root == self.root_spatial_node_index => { // Both current slice and this cluster are fixed position, no need to cut false } (_, _) if current_scroll_root == self.root_spatial_node_index => { // A real scroll root is being established, so create a cache slice true } (_, _) if scroll_root == self.root_spatial_node_index => { // If quality settings force subpixel AA over performance, skip creating // a slice for the fixed position element(s) here. if quality_settings.force_subpixel_aa_where_possible { false } else { // A fixed position slice is encountered within a scroll root. Only create // a slice in this case if all the clips referenced by this cluster are also // fixed position. There's no real point in creating slices for these cases, // since we'll have to rasterize them as the scrolling clip moves anyway. It // also allows us to retain subpixel AA in these cases. For these types of // slices, the intra-slice dirty rect handling typically works quite well // (a common case is parallax scrolling effects). let mut create_slice = true; let leaf = clip_tree_builder.get_leaf(prim_instance.clip_leaf_id); let mut current_node_id = leaf.node_id; while current_node_id != ClipNodeId::NONE { let node = clip_tree_builder.get_node(current_node_id); let clip_node_data = &interners.clip[node.handle]; let spatial_root = find_scroll_root( clip_node_data.key.spatial_node_index, &mut self.prev_scroll_root_cache, spatial_tree, true, ); if spatial_root != self.root_spatial_node_index { create_slice = false; break; } current_node_id = node.parent; } create_slice } } (curr_scroll_root, scroll_root) => { // Two scrolling roots - only need a new slice if they differ curr_scroll_root != scroll_root } }; } if want_new_tile_cache { secondary_slices.push(SliceDescriptor { prim_list: PrimitiveList::empty(), scroll_root, }); } secondary_slices .last_mut() .unwrap() .prim_list .add_prim( prim_instance, prim_rect, spatial_node_index, prim_flags, prim_instances, clip_tree_builder, ); } } } /// Consume this object and build the list of tile cache primitives pub fn build( mut self, config: &FrameBuilderConfig, prim_store: &mut PrimitiveStore, spatial_tree: &SceneSpatialTree, prim_instances: &[PrimitiveInstance], clip_tree_builder: &mut ClipTreeBuilder, interners: &Interners, ) -> (TileCacheConfig, Vec) { let mut result = TileCacheConfig::new(self.primary_slices.len()); let mut tile_cache_pictures = Vec::new(); let primary_slices = std::mem::replace(&mut self.primary_slices, Vec::new()); // TODO: At the moment, culling, clipping and invalidation are always // done in the root coordinate space. The plan is to move to doing it // (always or mostly) in raster space. let visibility_node = spatial_tree.root_reference_frame_index(); for mut primary_slice in primary_slices { if primary_slice.has_too_many_slices() { primary_slice.merge(); } match primary_slice.kind { SliceKind::Atomic { prim_list } => { if let Some(descriptor) = self.build_tile_cache( prim_list, spatial_tree, ) { create_tile_cache( self.debug_flags, primary_slice.slice_flags, descriptor.scroll_root, visibility_node, primary_slice.iframe_clip, descriptor.prim_list, primary_slice.background_color, prim_store, prim_instances, config, &mut result.tile_caches, &mut tile_cache_pictures, clip_tree_builder, interners, spatial_tree, ); } } SliceKind::Default { secondary_slices } => { for descriptor in secondary_slices { create_tile_cache( self.debug_flags, primary_slice.slice_flags, descriptor.scroll_root, visibility_node, primary_slice.iframe_clip, descriptor.prim_list, primary_slice.background_color, prim_store, prim_instances, config, &mut result.tile_caches, &mut tile_cache_pictures, clip_tree_builder, interners, spatial_tree, ); } } } } (result, tile_cache_pictures) } } /// Find the scroll root for a given spatial node fn find_scroll_root( spatial_node_index: SpatialNodeIndex, prev_scroll_root_cache: &mut (SpatialNodeIndex, SpatialNodeIndex), spatial_tree: &SceneSpatialTree, allow_sticky_frames: bool, ) -> SpatialNodeIndex { if prev_scroll_root_cache.0 == spatial_node_index { return prev_scroll_root_cache.1; } let scroll_root = spatial_tree.find_scroll_root(spatial_node_index, allow_sticky_frames); *prev_scroll_root_cache = (spatial_node_index, scroll_root); scroll_root } /// Given a PrimitiveList and scroll root, construct a tile cache primitive instance /// that wraps the primitive list. fn create_tile_cache( debug_flags: DebugFlags, slice_flags: SliceFlags, scroll_root: SpatialNodeIndex, visibility_node: SpatialNodeIndex, iframe_clip: Option, prim_list: PrimitiveList, background_color: Option, prim_store: &mut PrimitiveStore, prim_instances: &[PrimitiveInstance], frame_builder_config: &FrameBuilderConfig, tile_caches: &mut FastHashMap, tile_cache_pictures: &mut Vec, clip_tree_builder: &mut ClipTreeBuilder, interners: &Interners, spatial_tree: &SceneSpatialTree, ) { // Accumulate any clip instances from the iframe_clip into the shared clips // that will be applied by this tile cache during compositing. let mut additional_clips = Vec::new(); if let Some(clip_id) = iframe_clip { additional_clips.push(clip_id); } // Find the best shared clip node that we can apply while compositing tiles, // rather than applying to each item individually. // Step 1: Walk the primitive list, and find the LCA of the clip-tree that // matches all primitives. This gives us our "best-case" shared // clip node that moves as many clips as possible to compositing. let mut shared_clip_node_id = None; for cluster in &prim_list.clusters { for prim_instance in &prim_instances[cluster.prim_range()] { let leaf = clip_tree_builder.get_leaf(prim_instance.clip_leaf_id); // TODO(gw): Need to cache last clip-node id here? shared_clip_node_id = match shared_clip_node_id { Some(current) => { Some(clip_tree_builder.find_lowest_common_ancestor(current, leaf.node_id)) } None => { Some(leaf.node_id) } } } } // Step 2: Now we need to walk up the shared clip node hierarchy, and remove clips // that we can't handle during compositing, such as: // (a) Non axis-aligned clips // (b) Box-shadow or image-mask clips // (c) Rounded rect clips. // // A follow up patch to this series will relax the condition on (c) to // allow tile caches to apply a single rounded-rect clip during compositing. let mut shared_clip_node_id = shared_clip_node_id.unwrap_or(ClipNodeId::NONE); let mut current_node_id = shared_clip_node_id; let mut rounded_rect_count = 0; // Walk up the hierarchy to the root of the clip-tree while current_node_id != ClipNodeId::NONE { let node = clip_tree_builder.get_node(current_node_id); let clip_node_data = &interners.clip[node.handle]; // Check if this clip is in the root coord system (i.e. is axis-aligned with tile-cache) let is_rcs = spatial_tree.is_root_coord_system(clip_node_data.key.spatial_node_index); let node_valid = if is_rcs { match clip_node_data.key.kind { ClipItemKeyKind::BoxShadow(..) | ClipItemKeyKind::ImageMask(..) | ClipItemKeyKind::Rectangle(_, ClipMode::ClipOut) | ClipItemKeyKind::RoundedRectangle(_, _, ClipMode::ClipOut) => { // Has a box-shadow / image-mask, we can't handle this as a shared clip false } ClipItemKeyKind::RoundedRectangle(rect, radius, ClipMode::Clip) => { // The shader and CoreAnimation rely on certain constraints such // as uniform radii to be able to apply the clip during compositing. if BorderRadius::from(radius).can_use_fast_path_in(&rect.into()) { rounded_rect_count += 1; true } else { false } } ClipItemKeyKind::Rectangle(_, ClipMode::Clip) => { // We can apply multiple (via combining) axis-aligned rectangle // clips to the shared compositing clip. true } } } else { // Has a complex transform, we can't handle this as a shared clip false }; if node_valid { // This node was found to be one we can apply during compositing. if rounded_rect_count > 1 { // However, we plan to only support one rounded-rect clip. If // we have found > 1 rounded rect, drop children from the shared // clip, and continue looking up the chain. shared_clip_node_id = current_node_id; rounded_rect_count = 1; } } else { // Node was invalid, due to transform / clip type. Drop this clip // and reset the rounded rect count to 0, since we drop children // from here too. shared_clip_node_id = node.parent; rounded_rect_count = 0; } current_node_id = node.parent; } let shared_clip_leaf_id = Some(clip_tree_builder.build_for_tile_cache( shared_clip_node_id, &additional_clips, )); // Build a clip-chain for the tile cache, that contains any of the shared clips // we will apply when drawing the tiles. In all cases provided by Gecko, these // are rectangle clips with a scale/offset transform only, and get handled as // a simple local clip rect in the vertex shader. However, this should in theory // also work with any complex clips, such as rounded rects and image masks, by // producing a clip mask that is applied to the picture cache tiles. let slice = tile_cache_pictures.len(); let background_color = if slice == 0 { background_color } else { None }; let slice_id = SliceId::new(slice); // Store some information about the picture cache slice. This is used when we swap the // new scene into the frame builder to either reuse existing slices, or create new ones. tile_caches.insert(slice_id, TileCacheParams { debug_flags, slice, slice_flags, spatial_node_index: scroll_root, visibility_node_index: visibility_node, background_color, shared_clip_node_id, shared_clip_leaf_id, virtual_surface_size: frame_builder_config.compositor_kind.get_virtual_surface_size(), image_surface_count: prim_list.image_surface_count, yuv_image_surface_count: prim_list.yuv_image_surface_count, }); let pic_index = prim_store.pictures.alloc().init(PicturePrimitive::new_image( Some(PictureCompositeMode::TileCache { slice_id }), Picture3DContext::Out, PrimitiveFlags::IS_BACKFACE_VISIBLE, prim_list, scroll_root, RasterSpace::Screen, PictureFlags::empty(), None, )); tile_cache_pictures.push(PictureIndex(pic_index)); } /// Debug information about a set of picture cache slices, exposed via RenderResults #[derive(Debug)] #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] pub struct PictureCacheDebugInfo { pub slices: FastHashMap, } impl PictureCacheDebugInfo { pub fn new() -> Self { PictureCacheDebugInfo { slices: FastHashMap::default(), } } /// Convenience method to retrieve a given slice. Deliberately panics /// if the slice isn't present. pub fn slice(&self, slice: usize) -> &SliceDebugInfo { &self.slices[&slice] } } impl Default for PictureCacheDebugInfo { fn default() -> PictureCacheDebugInfo { PictureCacheDebugInfo::new() } } /// Debug information about a set of picture cache tiles, exposed via RenderResults #[derive(Debug)] #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] pub struct SliceDebugInfo { pub tiles: FastHashMap, } impl SliceDebugInfo { pub fn new() -> Self { SliceDebugInfo { tiles: FastHashMap::default(), } } /// Convenience method to retrieve a given tile. Deliberately panics /// if the tile isn't present. pub fn tile(&self, x: i32, y: i32) -> &TileDebugInfo { &self.tiles[&TileOffset::new(x, y)] } } /// Debug information about a tile that was dirty and was rasterized #[derive(Debug, PartialEq)] #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] pub struct DirtyTileDebugInfo { pub local_valid_rect: PictureRect, pub local_dirty_rect: PictureRect, } /// Debug information about the state of a tile #[derive(Debug, PartialEq)] #[cfg_attr(feature = "capture", derive(Serialize))] #[cfg_attr(feature = "replay", derive(Deserialize))] pub enum TileDebugInfo { /// Tile was occluded by a tile in front of it Occluded, /// Tile was culled (not visible in current display port) Culled, /// Tile was valid (no rasterization was done) and visible Valid, /// Tile was dirty, and was updated Dirty(DirtyTileDebugInfo), } impl TileDebugInfo { pub fn is_occluded(&self) -> bool { match self { TileDebugInfo::Occluded => true, TileDebugInfo::Culled | TileDebugInfo::Valid | TileDebugInfo::Dirty(..) => false, } } pub fn is_valid(&self) -> bool { match self { TileDebugInfo::Valid => true, TileDebugInfo::Culled | TileDebugInfo::Occluded | TileDebugInfo::Dirty(..) => false, } } pub fn is_culled(&self) -> bool { match self { TileDebugInfo::Culled => true, TileDebugInfo::Valid | TileDebugInfo::Occluded | TileDebugInfo::Dirty(..) => false, } } pub fn as_dirty(&self) -> &DirtyTileDebugInfo { match self { TileDebugInfo::Occluded | TileDebugInfo::Culled | TileDebugInfo::Valid => { panic!("not a dirty tile!"); } TileDebugInfo::Dirty(ref info) => { info } } } }