diff options
Diffstat (limited to 'gfx/wr/webrender/src/tile_cache.rs')
-rw-r--r-- | gfx/wr/webrender/src/tile_cache.rs | 547 |
1 files changed, 547 insertions, 0 deletions
diff --git a/gfx/wr/webrender/src/tile_cache.rs b/gfx/wr/webrender/src/tile_cache.rs new file mode 100644 index 0000000000..5064097c4f --- /dev/null +++ b/gfx/wr/webrender/src/tile_cache.rs @@ -0,0 +1,547 @@ +/* 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, PrimitiveFlags, QualitySettings}; +use api::units::*; +use crate::clip::{ClipChainId, ClipNodeKind, ClipStore, ClipInstance}; +use crate::frame_builder::FrameBuilderConfig; +use crate::internal_types::{FastHashMap, FastHashSet}; +use crate::picture::{PrimitiveList, PictureCompositeMode, PictureOptions, PicturePrimitive, SliceId}; +use crate::picture::{Picture3DContext, TileCacheParams, TileOffset}; +use crate::prim_store::{PrimitiveInstance, PrimitiveInstanceKind, PrimitiveStore, PictureIndex}; +use crate::scene_building::SliceFlags; +use crate::scene_builder_thread::Interners; +use crate::spatial_tree::{ROOT_SPATIAL_NODE_INDEX, SpatialNodeIndex, SpatialTree}; +use crate::util::VecHelper; + +/* + 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. + */ + +/// Created during scene building, describes how to create a tile cache for a given slice. +pub struct PendingTileCache { + /// List of primitives that are part of this slice + pub prim_list: PrimitiveList, + /// Parameters that define the tile cache (such as background color, shared clips, reference spatial node) + pub params: TileCacheParams, +} + +/// Used during scene building to construct the list of pending tile caches. +pub struct TileCacheBuilder { + /// When true, a new tile cache will be created for the next primitive. + need_new_tile_cache: bool, + /// List of tile caches that have been created so far (last in the list is currently active). + pending_tile_caches: Vec<PendingTileCache>, + + /// Cache the previous scroll root search for a spatial node, since they are often the same. + prev_scroll_root_cache: (SpatialNodeIndex, SpatialNodeIndex), + /// A buffer for collecting clips for a clip-chain. Retained here to avoid memory allocations in add_prim. + prim_clips_buffer: Vec<ClipInstance>, + /// Cache the last clip-chain that was added to the shared clips as it's often the same between prims. + last_checked_clip_chain: ClipChainId, +} + +/// 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<SliceId, TileCacheParams>, + /// A set of any spatial nodes that are attached to either a picture cache + /// root, or a clip node on the picture cache primitive. These are used + /// to detect cases where picture caching must be disabled. This is mostly + /// a temporary workaround for some existing wrench tests. I don't think + /// Gecko ever produces picture cache slices with complex transforms, so + /// in future we should prevent this in the public API and remove this hack. + pub picture_cache_spatial_nodes: FastHashSet<SpatialNodeIndex>, + /// 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_spatial_nodes: FastHashSet::default(), + picture_cache_slice_count, + } + } +} + +impl TileCacheBuilder { + /// Construct a new tile cache builder. + pub fn new() -> Self { + TileCacheBuilder { + need_new_tile_cache: true, + pending_tile_caches: Vec::new(), + prev_scroll_root_cache: (ROOT_SPATIAL_NODE_INDEX, ROOT_SPATIAL_NODE_INDEX), + prim_clips_buffer: Vec::new(), + last_checked_clip_chain: ClipChainId::INVALID, + } + } + + /// Set a barrier that forces a new tile cache next time a prim is added. + pub fn add_tile_cache_barrier(&mut self) { + self.need_new_tile_cache = true; + } + + /// 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: &SpatialTree, + clip_store: &ClipStore, + interners: &Interners, + config: &FrameBuilderConfig, + quality_settings: &QualitySettings, + ) { + // Scrollbars and clear primitives always get their own slice + let is_scrollbar_container = prim_flags.contains(PrimitiveFlags::IS_SCROLLBAR_CONTAINER); + let is_clear_prim = match prim_instance.kind { + PrimitiveInstanceKind::Clear { .. } => true, + _ => false, + }; + let requires_own_slice = is_scrollbar_container || is_clear_prim; + + // Check if we want to create a new slice based on the current / next scroll root + let scroll_root = self.find_scroll_root(spatial_node_index, spatial_tree); + + // Also create a new slice if there was a barrier previously set + let mut want_new_tile_cache = + self.need_new_tile_cache || + requires_own_slice || + self.pending_tile_caches.is_empty(); + + let current_scroll_root = self.pending_tile_caches + .last() + .map(|p| p.params.spatial_node_index); + + if let Some(current_scroll_root) = current_scroll_root { + want_new_tile_cache |= match (current_scroll_root, scroll_root) { + (ROOT_SPATIAL_NODE_INDEX, ROOT_SPATIAL_NODE_INDEX) => { + // Both current slice and this cluster are fixed position, no need to cut + false + } + (ROOT_SPATIAL_NODE_INDEX, _) => { + // A real scroll root is being established, so create a cache slice + true + } + (_, 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 mut current_clip_chain_id = prim_instance.clip_set.clip_chain_id; + + while current_clip_chain_id != ClipChainId::NONE { + let clip_chain_node = &clip_store.clip_chain_nodes[current_clip_chain_id.0 as usize]; + let spatial_root = self.find_scroll_root(clip_chain_node.spatial_node_index, spatial_tree); + if spatial_root != ROOT_SPATIAL_NODE_INDEX { + create_slice = false; + break; + } + current_clip_chain_id = clip_chain_node.parent_clip_chain_id; + } + + create_slice + } + } + (curr_scroll_root, scroll_root) => { + // Two scrolling roots - only need a new slice if they differ + curr_scroll_root != scroll_root + } + }; + + // Update the list of clips that apply to this primitive instance, to track which are the + // shared clips for this tile cache that can be applied during compositing. + if self.last_checked_clip_chain != prim_instance.clip_set.clip_chain_id { + let prim_clips_buffer = &mut self.prim_clips_buffer; + prim_clips_buffer.clear(); + add_clips( + current_scroll_root, + prim_instance.clip_set.clip_chain_id, + prim_clips_buffer, + clip_store, + interners, + spatial_tree, + ); + + let current_shared_clips = &self.pending_tile_caches + .last() + .unwrap() + .params + .shared_clips; + + // If the shared clips are not compatible, create a new slice. + // TODO(gw): Does Gecko ever supply duplicate or out-of-order + // shared clips? It doesn't seem to, but if it does, + // we will need to be more clever here to check if + // the shared clips are compatible. + want_new_tile_cache |= current_shared_clips != prim_clips_buffer; + + self.last_checked_clip_chain = prim_instance.clip_set.clip_chain_id; + } + } + + if want_new_tile_cache { + // 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 = 12; + let slice = self.pending_tile_caches.len(); + + // If we have exceeded the maximum number of slices, skip creating a new + // one and the primitive will be added to the last slice. + if slice < MAX_CACHE_SLICES { + // When we reach the last valid slice that can be created, it is created as + // a fixed slice without shared clips, ensuring that we can safely add any + // subsequent primitives to it. This doesn't seem to occur on any real + // world content (only contrived test cases), where this acts as a fail safe + // to ensure we don't allocate too much GPU memory for surface caches. + // However, if we _do_ ever see this occur on real world content, we could + // probably consider increasing the max cache slices a bit more than the + // current limit. + let params = if slice == MAX_CACHE_SLICES-1 { + TileCacheParams { + slice, + slice_flags: SliceFlags::empty(), + spatial_node_index: ROOT_SPATIAL_NODE_INDEX, + background_color: None, + shared_clips: Vec::new(), + shared_clip_chain: ClipChainId::NONE, + virtual_surface_size: config.compositor_kind.get_virtual_surface_size(), + } + } else { + let slice_flags = if is_scrollbar_container { + SliceFlags::IS_SCROLLBAR + } else { + SliceFlags::empty() + }; + + let background_color = if slice == 0 { + config.background_color + } else { + None + }; + + let mut shared_clips = Vec::new(); + add_clips( + scroll_root, + prim_instance.clip_set.clip_chain_id, + &mut shared_clips, + clip_store, + interners, + spatial_tree, + ); + + self.last_checked_clip_chain = prim_instance.clip_set.clip_chain_id; + + TileCacheParams { + slice, + slice_flags, + spatial_node_index: scroll_root, + background_color, + shared_clips, + shared_clip_chain: ClipChainId::NONE, + virtual_surface_size: config.compositor_kind.get_virtual_surface_size(), + } + }; + + self.pending_tile_caches.push(PendingTileCache { + prim_list: PrimitiveList::empty(), + params, + }); + + self.need_new_tile_cache = requires_own_slice; + } + } + + self.pending_tile_caches + .last_mut() + .unwrap() + .prim_list + .add_prim( + prim_instance, + prim_rect, + spatial_node_index, + prim_flags, + ); + } + + /// Consume this object and build the list of tile cache primitives + pub fn build( + self, + config: &FrameBuilderConfig, + clip_store: &mut ClipStore, + prim_store: &mut PrimitiveStore, + ) -> (TileCacheConfig, Vec<PictureIndex>) { + let mut result = TileCacheConfig::new(self.pending_tile_caches.len()); + let mut tile_cache_pictures = Vec::new(); + + for pending_tile_cache in self.pending_tile_caches { + let pic_index = create_tile_cache( + pending_tile_cache.params.slice, + pending_tile_cache.params.slice_flags, + pending_tile_cache.params.spatial_node_index, + pending_tile_cache.prim_list, + pending_tile_cache.params.background_color, + pending_tile_cache.params.shared_clips, + prim_store, + clip_store, + &mut result.picture_cache_spatial_nodes, + config, + &mut result.tile_caches, + ); + + tile_cache_pictures.push(pic_index); + } + + (result, tile_cache_pictures) + } + + /// Find the scroll root for a given spatial node + fn find_scroll_root( + &mut self, + spatial_node_index: SpatialNodeIndex, + spatial_tree: &SpatialTree, + ) -> SpatialNodeIndex { + if self.prev_scroll_root_cache.0 == spatial_node_index { + return self.prev_scroll_root_cache.1; + } + + let scroll_root = spatial_tree.find_scroll_root(spatial_node_index); + self.prev_scroll_root_cache = (spatial_node_index, scroll_root); + + scroll_root + } +} + +// Helper fn to collect clip handles from a given clip chain. +fn add_clips( + scroll_root: SpatialNodeIndex, + clip_chain_id: ClipChainId, + prim_clips: &mut Vec<ClipInstance>, + clip_store: &ClipStore, + interners: &Interners, + spatial_tree: &SpatialTree, +) { + let mut current_clip_chain_id = clip_chain_id; + + while current_clip_chain_id != ClipChainId::NONE { + let clip_chain_node = &clip_store + .clip_chain_nodes[current_clip_chain_id.0 as usize]; + + let clip_node_data = &interners.clip[clip_chain_node.handle]; + if let ClipNodeKind::Rectangle = clip_node_data.clip_node_kind { + if spatial_tree.is_ancestor( + clip_chain_node.spatial_node_index, + scroll_root, + ) { + prim_clips.push(ClipInstance::new(clip_chain_node.handle, clip_chain_node.spatial_node_index)); + } + } + + current_clip_chain_id = clip_chain_node.parent_clip_chain_id; + } +} + +/// Given a PrimitiveList and scroll root, construct a tile cache primitive instance +/// that wraps the primitive list. +fn create_tile_cache( + slice: usize, + slice_flags: SliceFlags, + scroll_root: SpatialNodeIndex, + prim_list: PrimitiveList, + background_color: Option<ColorF>, + shared_clips: Vec<ClipInstance>, + prim_store: &mut PrimitiveStore, + clip_store: &mut ClipStore, + picture_cache_spatial_nodes: &mut FastHashSet<SpatialNodeIndex>, + frame_builder_config: &FrameBuilderConfig, + tile_caches: &mut FastHashMap<SliceId, TileCacheParams>, +) -> PictureIndex { + // Add this spatial node to the list to check for complex transforms + // at the start of a frame build. + picture_cache_spatial_nodes.insert(scroll_root); + + // 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 mut parent_clip_chain_id = ClipChainId::NONE; + for clip_instance in &shared_clips { + // Add this spatial node to the list to check for complex transforms + // at the start of a frame build. + picture_cache_spatial_nodes.insert(clip_instance.spatial_node_index); + + parent_clip_chain_id = clip_store.add_clip_chain_node( + clip_instance.handle, + clip_instance.spatial_node_index, + parent_clip_chain_id, + ); + } + + 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 { + slice, + slice_flags, + spatial_node_index: scroll_root, + background_color, + shared_clips, + shared_clip_chain: parent_clip_chain_id, + virtual_surface_size: frame_builder_config.compositor_kind.get_virtual_surface_size(), + }); + + let pic_index = prim_store.pictures.alloc().init(PicturePrimitive::new_image( + Some(PictureCompositeMode::TileCache { slice_id }), + Picture3DContext::Out, + true, + PrimitiveFlags::IS_BACKFACE_VISIBLE, + prim_list, + scroll_root, + PictureOptions::default(), + )); + + 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<usize, SliceDebugInfo>, +} + +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<TileOffset, TileDebugInfo>, +} + +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 + } + } + } +} |