diff options
Diffstat (limited to 'gfx/wr/webrender/src/clip.rs')
-rw-r--r-- | gfx/wr/webrender/src/clip.rs | 2310 |
1 files changed, 2310 insertions, 0 deletions
diff --git a/gfx/wr/webrender/src/clip.rs b/gfx/wr/webrender/src/clip.rs new file mode 100644 index 0000000000..f6bf55e967 --- /dev/null +++ b/gfx/wr/webrender/src/clip.rs @@ -0,0 +1,2310 @@ +/* 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/. */ + +//! Internal representation of clips in WebRender. +//! +//! # Data structures +//! +//! There are a number of data structures involved in the clip module: +//! +//! - ClipStore - Main interface used by other modules. +//! +//! - ClipItem - A single clip item (e.g. a rounded rect, or a box shadow). +//! These are an exposed API type, stored inline in a ClipNode. +//! +//! - ClipNode - A ClipItem with an attached GPU handle. The GPU handle is populated +//! when a ClipNodeInstance is built from this node (which happens while +//! preparing primitives for render). +//! +//! ClipNodeInstance - A ClipNode with attached positioning information (a spatial +//! node index). This is stored as a contiguous array of nodes +//! within the ClipStore. +//! +//! ```ascii +//! +-----------------------+-----------------------+-----------------------+ +//! | ClipNodeInstance | ClipNodeInstance | ClipNodeInstance | +//! +-----------------------+-----------------------+-----------------------+ +//! | ClipItem | ClipItem | ClipItem | +//! | Spatial Node Index | Spatial Node Index | Spatial Node Index | +//! | GPU cache handle | GPU cache handle | GPU cache handle | +//! | ... | ... | ... | +//! +-----------------------+-----------------------+-----------------------+ +//! 0 1 2 +//! +----------------+ | | +//! | ClipNodeRange |____| | +//! | index: 1 | | +//! | count: 2 |___________________________________________________| +//! +----------------+ +//! ``` +//! +//! - ClipNodeRange - A clip item range identifies a range of clip nodes instances. +//! It is stored as an (index, count). +//! +//! - ClipChainNode - A clip chain node contains a handle to an interned clip item, +//! positioning information (from where the clip was defined), and +//! an optional parent link to another ClipChainNode. ClipChainId +//! is an index into an array, or ClipChainId::NONE for no parent. +//! +//! ```ascii +//! +----------------+ ____+----------------+ ____+----------------+ /---> ClipChainId::NONE +//! | ClipChainNode | | | ClipChainNode | | | ClipChainNode | | +//! +----------------+ | +----------------+ | +----------------+ | +//! | ClipDataHandle | | | ClipDataHandle | | | ClipDataHandle | | +//! | Spatial index | | | Spatial index | | | Spatial index | | +//! | Parent Id |___| | Parent Id |___| | Parent Id |___| +//! | ... | | ... | | ... | +//! +----------------+ +----------------+ +----------------+ +//! ``` +//! +//! - ClipChainInstance - A ClipChain that has been built for a specific primitive + positioning node. +//! +//! When given a clip chain ID, and a local primitive rect and its spatial node, the clip module +//! creates a clip chain instance. This is a struct with various pieces of useful information +//! (such as a local clip rect). It also contains a (index, count) +//! range specifier into an index buffer of the ClipNodeInstance structures that are actually relevant +//! for this clip chain instance. The index buffer structure allows a single array to be used for +//! all of the clip-chain instances built in a single frame. Each entry in the index buffer +//! also stores some flags relevant to the clip node in this positioning context. +//! +//! ```ascii +//! +----------------------+ +//! | ClipChainInstance | +//! +----------------------+ +//! | ... | +//! | local_clip_rect |________________________________________________________________________ +//! | clips_range |_______________ | +//! +----------------------+ | | +//! | | +//! +------------------+------------------+------------------+------------------+------------------+ +//! | ClipNodeInstance | ClipNodeInstance | ClipNodeInstance | ClipNodeInstance | ClipNodeInstance | +//! +------------------+------------------+------------------+------------------+------------------+ +//! | flags | flags | flags | flags | flags | +//! | ... | ... | ... | ... | ... | +//! +------------------+------------------+------------------+------------------+------------------+ +//! ``` +//! +//! # Rendering clipped primitives +//! +//! See the [`segment` module documentation][segment.rs]. +//! +//! +//! [segment.rs]: ../segment/index.html +//! + +use api::{BorderRadius, ClipMode, ComplexClipRegion, ImageMask, ClipId, ClipChainId}; +use api::{BoxShadowClipMode, FillRule, ImageKey, ImageRendering}; +use api::units::*; +use crate::image_tiling::{self, Repetition}; +use crate::border::{ensure_no_corner_overlap, BorderRadiusAu}; +use crate::box_shadow::{BLUR_SAMPLE_SCALE, BoxShadowClipSource, BoxShadowCacheKey}; +use crate::spatial_tree::{SpatialTree, SpatialNodeIndex}; +use crate::ellipse::Ellipse; +use crate::gpu_cache::GpuCache; +use crate::gpu_types::{BoxShadowStretchMode}; +use crate::intern; +use crate::internal_types::{FastHashMap, FastHashSet, LayoutPrimitiveInfo}; +use crate::prim_store::{VisibleMaskImageTile}; +use crate::prim_store::{PointKey, SizeKey, RectangleKey, PolygonKey}; +use crate::render_task_cache::to_cache_size; +use crate::resource_cache::{ImageRequest, ResourceCache}; +use crate::scene_builder_thread::Interners; +use crate::space::SpaceMapper; +use crate::util::{clamp_to_scale_factor, MaxRect, extract_inner_rect_safe, project_rect, ScaleOffset}; +use euclid::approxeq::ApproxEq; +use std::{iter, ops, u32, mem}; + +/// A (non-leaf) node inside a clip-tree +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +#[derive(MallocSizeOf)] +pub struct ClipTreeNode { + pub handle: ClipDataHandle, + pub parent: ClipNodeId, + + children: Vec<ClipNodeId>, + + // TODO(gw): Consider adding a default leaf for cases when the local_clip_rect is not relevant, + // that can be shared among primitives (to reduce amount of clip-chain building). +} + +/// A leaf node in a clip-tree. Any primitive that is clipped will have a handle to +/// a clip-tree leaf. +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +#[derive(MallocSizeOf)] +pub struct ClipTreeLeaf { + pub node_id: ClipNodeId, + + // TODO(gw): For now, this preserves the ability to build a culling rect + // from the supplied leaf local clip rect on the primitive. In + // future, we'll expand this to be more efficient by combining + // it will compatible clip rects from the `node_id`. + pub local_clip_rect: LayoutRect, +} + +/// ID for a ClipTreeNode +#[derive(Debug, Copy, Clone, PartialEq, MallocSizeOf, Eq, Hash)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct ClipNodeId(u32); + +impl ClipNodeId { + pub const NONE: ClipNodeId = ClipNodeId(0); +} + +/// ID for a ClipTreeLeaf +#[derive(Debug, Copy, Clone, PartialEq, MallocSizeOf, Eq, Hash)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct ClipLeafId(u32); + +/// A clip-tree built during scene building and used during frame-building to apply clips to primitives. +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct ClipTree { + nodes: Vec<ClipTreeNode>, + leaves: Vec<ClipTreeLeaf>, + clip_root_stack: Vec<ClipNodeId>, +} + +impl ClipTree { + pub fn new() -> Self { + ClipTree { + nodes: vec![ + ClipTreeNode { + handle: ClipDataHandle::INVALID, + children: Vec::new(), + parent: ClipNodeId::NONE, + } + ], + leaves: Vec::new(), + clip_root_stack: vec![ + ClipNodeId::NONE, + ], + } + } + + /// Add a set of clips to the provided tree node id, reusing existing + /// nodes in the tree where possible + fn add_impl( + id: ClipNodeId, + clips: &[ClipDataHandle], + nodes: &mut Vec<ClipTreeNode>, + ) -> ClipNodeId { + if clips.is_empty() { + return id; + } + + let handle = clips[0]; + let next_clips = &clips[1..]; + + let node_index = nodes[id.0 as usize] + .children + .iter() + .find(|n| nodes[n.0 as usize].handle == handle) + .cloned(); + + let node_index = match node_index { + Some(node_index) => node_index, + None => { + let node_index = ClipNodeId(nodes.len() as u32); + nodes[id.0 as usize].children.push(node_index); + let node = ClipTreeNode { + handle, + children: Vec::new(), + parent: id, + }; + nodes.push(node); + node_index + } + }; + + ClipTree::add_impl( + node_index, + next_clips, + nodes, + ) + } + + /// Add a set of clips to the provided tree node id, reusing existing + /// nodes in the tree where possible + pub fn add( + &mut self, + root: ClipNodeId, + clips: &[ClipDataHandle], + ) -> ClipNodeId { + ClipTree::add_impl( + root, + clips, + &mut self.nodes, + ) + } + + /// Get the current clip root (the node in the clip-tree where clips can be + /// ignored when building the clip-chain instance for a primitive) + pub fn current_clip_root(&self) -> ClipNodeId { + self.clip_root_stack.last().cloned().unwrap() + } + + /// Push a clip root (e.g. when a surface is encountered) that prevents clips + /// from this node and above being applied to primitives within the root. + pub fn push_clip_root_leaf(&mut self, clip_leaf_id: ClipLeafId) { + let leaf = &self.leaves[clip_leaf_id.0 as usize]; + self.clip_root_stack.push(leaf.node_id); + } + + /// Push a clip root (e.g. when a surface is encountered) that prevents clips + /// from this node and above being applied to primitives within the root. + pub fn push_clip_root_node(&mut self, clip_node_id: ClipNodeId) { + self.clip_root_stack.push(clip_node_id); + } + + /// Pop a clip root, when exiting a surface. + pub fn pop_clip_root(&mut self) { + self.clip_root_stack.pop().unwrap(); + } + + /// Retrieve a clip tree node by id + pub fn get_node(&self, id: ClipNodeId) -> &ClipTreeNode { + assert!(id != ClipNodeId::NONE); + + &self.nodes[id.0 as usize] + } + + /// Retrieve a clip tree leaf by id + pub fn get_leaf(&self, id: ClipLeafId) -> &ClipTreeLeaf { + &self.leaves[id.0 as usize] + } + + /// Debug print the clip-tree + #[allow(unused)] + pub fn print(&self) { + use crate::print_tree::PrintTree; + + fn print_node<T: crate::print_tree::PrintTreePrinter>( + id: ClipNodeId, + nodes: &[ClipTreeNode], + pt: &mut T, + ) { + let node = &nodes[id.0 as usize]; + + pt.new_level(format!("{:?}", id)); + pt.add_item(format!("{:?}", node.handle)); + + for child_id in &node.children { + print_node(*child_id, nodes, pt); + } + + pt.end_level(); + } + + fn print_leaf<T: crate::print_tree::PrintTreePrinter>( + id: ClipLeafId, + leaves: &[ClipTreeLeaf], + pt: &mut T, + ) { + let leaf = &leaves[id.0 as usize]; + + pt.new_level(format!("{:?}", id)); + pt.add_item(format!("node_id: {:?}", leaf.node_id)); + pt.add_item(format!("local_clip_rect: {:?}", leaf.local_clip_rect)); + pt.end_level(); + } + + let mut pt = PrintTree::new("clip tree"); + print_node(ClipNodeId::NONE, &self.nodes, &mut pt); + + for i in 0 .. self.leaves.len() { + print_leaf(ClipLeafId(i as u32), &self.leaves, &mut pt); + } + } + + /// Find the lowest common ancestor of two clip tree nodes. This is useful + /// to identify shared clips between primitives attached to different clip-leaves. + pub fn find_lowest_common_ancestor( + &self, + mut node1: ClipNodeId, + mut node2: ClipNodeId, + ) -> ClipNodeId { + // TODO(gw): Consider caching / storing the depth in the node? + fn get_node_depth( + id: ClipNodeId, + nodes: &[ClipTreeNode], + ) -> usize { + let mut depth = 0; + let mut current = id; + + while current != ClipNodeId::NONE { + let node = &nodes[current.0 as usize]; + depth += 1; + current = node.parent; + } + + depth + } + + let mut depth1 = get_node_depth(node1, &self.nodes); + let mut depth2 = get_node_depth(node2, &self.nodes); + + while depth1 > depth2 { + node1 = self.nodes[node1.0 as usize].parent; + depth1 -= 1; + } + + while depth2 > depth1 { + node2 = self.nodes[node2.0 as usize].parent; + depth2 -= 1; + } + + while node1 != node2 { + node1 = self.nodes[node1.0 as usize].parent; + node2 = self.nodes[node2.0 as usize].parent; + } + + node1 + } +} + +/// Represents a clip-chain as defined by the public API that we decompose in to +/// the clip-tree. In future, we would like to remove this and have Gecko directly +/// build the clip-tree. +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct ClipChain { + parent: Option<usize>, + clips: Vec<ClipDataHandle>, +} + +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct ClipStackEntry { + /// Cache the previous clip-chain build, since this is a common case + last_clip_chain_cache: Option<(ClipChainId, ClipNodeId)>, + + /// Set of clips that were already seen and included in clip_node_id + seen_clips: FastHashSet<ClipDataHandle>, + + /// The build clip_node_id for this level of the stack + clip_node_id: ClipNodeId, +} + +/// Used by the scene builder to build the clip-tree that is part of the built scene. +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct ClipTreeBuilder { + /// Clips defined by the display list + clip_map: FastHashMap<ClipId, ClipDataHandle>, + + /// Clip-chains defined by the display list + clip_chains: Vec<ClipChain>, + clip_chain_map: FastHashMap<ClipChainId, usize>, + + /// List of clips pushed/popped by grouping items, such as stacking contexts and iframes + clip_stack: Vec<ClipStackEntry>, + + /// The tree we are building + tree: ClipTree, + + /// A temporary buffer stored here to avoid constant heap allocs/frees + clip_handles_buffer: Vec<ClipDataHandle>, +} + +impl ClipTreeBuilder { + pub fn new() -> Self { + ClipTreeBuilder { + clip_map: FastHashMap::default(), + clip_chain_map: FastHashMap::default(), + clip_chains: Vec::new(), + clip_stack: vec![ + ClipStackEntry { + clip_node_id: ClipNodeId::NONE, + last_clip_chain_cache: None, + seen_clips: FastHashSet::default(), + }, + ], + tree: ClipTree::new(), + clip_handles_buffer: Vec::new(), + } + } + + /// Define a new rect clip + pub fn define_rect_clip( + &mut self, + id: ClipId, + handle: ClipDataHandle, + ) { + self.clip_map.insert(id, handle); + } + + /// Define a new rounded rect clip + pub fn define_rounded_rect_clip( + &mut self, + id: ClipId, + handle: ClipDataHandle, + ) { + self.clip_map.insert(id, handle); + } + + /// Define a image mask clip + pub fn define_image_mask_clip( + &mut self, + id: ClipId, + handle: ClipDataHandle, + ) { + self.clip_map.insert(id, handle); + } + + /// Define a clip-chain + pub fn define_clip_chain<I: Iterator<Item = ClipId>>( + &mut self, + id: ClipChainId, + parent: Option<ClipChainId>, + clips: I, + ) { + let parent = parent.map(|ref id| self.clip_chain_map[id]); + let index = self.clip_chains.len(); + let clips = clips.map(|clip_id| { + self.clip_map[&clip_id] + }).collect(); + self.clip_chains.push(ClipChain { + parent, + clips, + }); + self.clip_chain_map.insert(id, index); + } + + /// Push a clip-chain that will be applied to any prims built prior to next pop + pub fn push_clip_chain( + &mut self, + clip_chain_id: Option<ClipChainId>, + reset_seen: bool, + ) { + let (mut clip_node_id, mut seen_clips) = { + let prev = self.clip_stack.last().unwrap(); + (prev.clip_node_id, prev.seen_clips.clone()) + }; + + if let Some(clip_chain_id) = clip_chain_id { + if clip_chain_id != ClipChainId::INVALID { + self.clip_handles_buffer.clear(); + + let clip_chain_index = self.clip_chain_map[&clip_chain_id]; + ClipTreeBuilder::add_clips( + clip_chain_index, + &mut seen_clips, + &mut self.clip_handles_buffer, + &self.clip_chains, + ); + + clip_node_id = self.tree.add( + clip_node_id, + &self.clip_handles_buffer, + ); + } + } + + if reset_seen { + seen_clips.clear(); + } + + self.clip_stack.push(ClipStackEntry { + last_clip_chain_cache: None, + clip_node_id, + seen_clips, + }); + } + + /// Push a clip-id that will be applied to any prims built prior to next pop + pub fn push_clip_id( + &mut self, + clip_id: ClipId, + ) { + let (clip_node_id, mut seen_clips) = { + let prev = self.clip_stack.last().unwrap(); + (prev.clip_node_id, prev.seen_clips.clone()) + }; + + self.clip_handles_buffer.clear(); + let clip_index = self.clip_map[&clip_id]; + + if seen_clips.insert(clip_index) { + self.clip_handles_buffer.push(clip_index); + } + + let clip_node_id = self.tree.add( + clip_node_id, + &self.clip_handles_buffer, + ); + + self.clip_stack.push(ClipStackEntry { + last_clip_chain_cache: None, + seen_clips, + clip_node_id, + }); + } + + /// Pop a clip off the clip_stack, when exiting a grouping item + pub fn pop_clip(&mut self) { + self.clip_stack.pop().unwrap(); + } + + /// Add clips from a given clip-chain to the set of clips for a primitive during clip-set building + fn add_clips( + clip_chain_index: usize, + seen_clips: &mut FastHashSet<ClipDataHandle>, + output: &mut Vec<ClipDataHandle>, + clip_chains: &[ClipChain], + ) { + // TODO(gw): It's possible that we may see clip outputs that include identical clips + // (e.g. if there is a clip positioned by two spatial nodes, where one spatial + // node is a child of the other, and has an identity transform). If we ever + // see this in real-world cases, it might be worth checking for that here and + // excluding them, to ensure the shape of the tree matches what we need for + // finding shared_clips for tile caches etc. + + let clip_chain = &clip_chains[clip_chain_index]; + + if let Some(parent) = clip_chain.parent { + ClipTreeBuilder::add_clips( + parent, + seen_clips, + output, + clip_chains, + ); + } + + for clip_index in clip_chain.clips.iter().rev() { + if seen_clips.insert(*clip_index) { + output.push(*clip_index); + } + } + } + + /// Main entry point to build a path in the clip-tree for a given primitive + pub fn build_clip_set( + &mut self, + clip_chain_id: ClipChainId, + ) -> ClipNodeId { + let clip_stack = self.clip_stack.last_mut().unwrap(); + + if clip_chain_id == ClipChainId::INVALID { + clip_stack.clip_node_id + } else { + if let Some((cached_clip_chain, cached_clip_node)) = clip_stack.last_clip_chain_cache { + if cached_clip_chain == clip_chain_id { + return cached_clip_node; + } + } + + let clip_chain_index = self.clip_chain_map[&clip_chain_id]; + + self.clip_handles_buffer.clear(); + + ClipTreeBuilder::add_clips( + clip_chain_index, + &mut clip_stack.seen_clips, + &mut self.clip_handles_buffer, + &self.clip_chains, + ); + + // We mutated the `clip_stack.seen_clips` in order to remove duplicate clips from + // the supplied `clip_chain_id`. Now step through and remove any clips we added + // to the set, so we don't get incorrect results next time `build_clip_set` is + // called for a different clip-chain. Doing it this way rather than cloning means + // we avoid heap allocations for each `build_clip_set` call. + for handle in &self.clip_handles_buffer { + clip_stack.seen_clips.remove(handle); + } + + let clip_node_id = self.tree.add( + clip_stack.clip_node_id, + &self.clip_handles_buffer, + ); + + clip_stack.last_clip_chain_cache = Some((clip_chain_id, clip_node_id)); + + clip_node_id + } + } + + /// Recursive impl to check if a clip-chain has complex (non-rectangular) clips + fn has_complex_clips_impl( + &self, + clip_chain_index: usize, + interners: &Interners, + ) -> bool { + let clip_chain = &self.clip_chains[clip_chain_index]; + + for clip_handle in &clip_chain.clips { + let clip_info = &interners.clip[*clip_handle]; + + if let ClipNodeKind::Complex = clip_info.key.kind.node_kind() { + return true; + } + } + + match clip_chain.parent { + Some(parent) => self.has_complex_clips_impl(parent, interners), + None => false, + } + } + + /// Check if a clip-chain has complex (non-rectangular) clips + pub fn clip_chain_has_complex_clips( + &self, + clip_chain_id: ClipChainId, + interners: &Interners, + ) -> bool { + let clip_chain_index = self.clip_chain_map[&clip_chain_id]; + self.has_complex_clips_impl(clip_chain_index, interners) + } + + /// Check if a clip-node has complex (non-rectangular) clips + pub fn clip_node_has_complex_clips( + &self, + clip_node_id: ClipNodeId, + interners: &Interners, + ) -> bool { + let mut current = clip_node_id; + + while current != ClipNodeId::NONE { + let node = &self.tree.nodes[current.0 as usize]; + let clip_info = &interners.clip[node.handle]; + + if let ClipNodeKind::Complex = clip_info.key.kind.node_kind() { + return true; + } + + current = node.parent; + } + + false + } + + /// Finalize building and return the clip-tree + pub fn finalize(self) -> ClipTree { + self.tree + } + + /// Get a clip node by id + pub fn get_node(&self, id: ClipNodeId) -> &ClipTreeNode { + assert!(id != ClipNodeId::NONE); + + &self.tree.nodes[id.0 as usize] + } + + /// Get a clip leaf by id + pub fn get_leaf(&self, id: ClipLeafId) -> &ClipTreeLeaf { + &self.tree.leaves[id.0 as usize] + } + + /// Build a clip-leaf for a tile-cache + pub fn build_for_tile_cache( + &mut self, + clip_node_id: ClipNodeId, + extra_clips: &[ClipId], + ) -> ClipLeafId { + self.clip_handles_buffer.clear(); + + for clip_id in extra_clips { + let handle = self.clip_map[clip_id]; + self.clip_handles_buffer.push(handle); + } + + let node_id = self.tree.add( + clip_node_id, + &self.clip_handles_buffer, + ); + + let clip_leaf_id = ClipLeafId(self.tree.leaves.len() as u32); + + self.tree.leaves.push(ClipTreeLeaf { + node_id, + local_clip_rect: LayoutRect::max_rect(), + }); + + clip_leaf_id + } + + /// Build a clip-leaf for a picture + pub fn build_for_picture( + &mut self, + clip_node_id: ClipNodeId, + ) -> ClipLeafId { + let node_id = self.tree.add( + clip_node_id, + &[], + ); + + let clip_leaf_id = ClipLeafId(self.tree.leaves.len() as u32); + + self.tree.leaves.push(ClipTreeLeaf { + node_id, + local_clip_rect: LayoutRect::max_rect(), + }); + + clip_leaf_id + } + + /// Build a clip-leaf for a normal primitive + pub fn build_for_prim( + &mut self, + clip_node_id: ClipNodeId, + info: &LayoutPrimitiveInfo, + extra_clips: &[ClipItemKey], + interners: &mut Interners, + ) -> ClipLeafId { + + let node_id = if extra_clips.is_empty() { + clip_node_id + } else { + // TODO(gw): Cache the previous build of clip-node / clip-leaf to handle cases where we get a + // lot of primitives referencing the same clip set (e.g. dl_mutate and similar tests) + self.clip_handles_buffer.clear(); + + for item in extra_clips { + // Intern this clip item, and store the handle + // in the clip chain node. + let handle = interners.clip.intern(item, || { + ClipInternData { + key: item.clone(), + } + }); + + self.clip_handles_buffer.push(handle); + } + + self.tree.add( + clip_node_id, + &self.clip_handles_buffer, + ) + }; + + let clip_leaf_id = ClipLeafId(self.tree.leaves.len() as u32); + + self.tree.leaves.push(ClipTreeLeaf { + node_id, + local_clip_rect: info.clip_rect, + }); + + clip_leaf_id + } + + // Find the LCA for two given clip nodes + pub fn find_lowest_common_ancestor( + &self, + node1: ClipNodeId, + node2: ClipNodeId, + ) -> ClipNodeId { + self.tree.find_lowest_common_ancestor(node1, node2) + } +} + +// Type definitions for interning clip nodes. + +#[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, Eq, Hash)] +#[cfg_attr(any(feature = "serde"), derive(Deserialize, Serialize))] +pub enum ClipIntern {} + +pub type ClipDataStore = intern::DataStore<ClipIntern>; +pub type ClipDataHandle = intern::Handle<ClipIntern>; + +/// Helper to identify simple clips (normal rects) from other kinds of clips, +/// which can often be handled via fast code paths. +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +#[derive(Debug, Copy, Clone, MallocSizeOf)] +pub enum ClipNodeKind { + /// A normal clip rectangle, with Clip mode. + Rectangle, + /// A rectangle with ClipOut, or any other kind of clip. + Complex, +} + +// Result of comparing a clip node instance against a local rect. +#[derive(Debug)] +enum ClipResult { + // The clip does not affect the region at all. + Accept, + // The clip prevents the region from being drawn. + Reject, + // The clip affects part of the region. This may + // require a clip mask, depending on other factors. + Partial, +} + +// A clip node is a single clip source, along with some +// positioning information and implementation details +// that control where the GPU data for this clip source +// can be found. +#[derive(Debug)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +#[derive(MallocSizeOf)] +pub struct ClipNode { + pub item: ClipItem, +} + +// Convert from an interning key for a clip item +// to a clip node, which is cached in the document. +impl From<ClipItemKey> for ClipNode { + fn from(item: ClipItemKey) -> Self { + let kind = match item.kind { + ClipItemKeyKind::Rectangle(rect, mode) => { + ClipItemKind::Rectangle { rect: rect.into(), mode } + } + ClipItemKeyKind::RoundedRectangle(rect, radius, mode) => { + ClipItemKind::RoundedRectangle { + rect: rect.into(), + radius: radius.into(), + mode, + } + } + ClipItemKeyKind::ImageMask(rect, image, polygon_handle) => { + ClipItemKind::Image { + image, + rect: rect.into(), + polygon_handle, + } + } + ClipItemKeyKind::BoxShadow(shadow_rect_fract_offset, shadow_rect_size, shadow_radius, prim_shadow_rect, blur_radius, clip_mode) => { + ClipItemKind::new_box_shadow( + shadow_rect_fract_offset.into(), + shadow_rect_size.into(), + shadow_radius.into(), + prim_shadow_rect.into(), + blur_radius.to_f32_px(), + clip_mode, + ) + } + }; + + ClipNode { + item: ClipItem { + kind, + spatial_node_index: item.spatial_node_index, + }, + } + } +} + +// Flags that are attached to instances of clip nodes. +bitflags! { + #[cfg_attr(feature = "capture", derive(Serialize))] + #[cfg_attr(feature = "replay", derive(Deserialize))] + #[derive(MallocSizeOf)] + pub struct ClipNodeFlags: u8 { + const SAME_SPATIAL_NODE = 0x1; + const SAME_COORD_SYSTEM = 0x2; + const USE_FAST_PATH = 0x4; + } +} + +// When a clip node is found to be valid for a +// clip chain instance, it's stored in an index +// buffer style structure. This struct contains +// an index to the node data itself, as well as +// some flags describing how this clip node instance +// is positioned. +#[derive(Debug, MallocSizeOf)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct ClipNodeInstance { + pub handle: ClipDataHandle, + pub flags: ClipNodeFlags, + pub visible_tiles: Option<ops::Range<usize>>, +} + +impl ClipNodeInstance { + pub fn has_visible_tiles(&self) -> bool { + self.visible_tiles.is_some() + } +} + +// A range of clip node instances that were found by +// building a clip chain instance. +#[derive(Debug, Copy, Clone)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct ClipNodeRange { + pub first: u32, + pub count: u32, +} + +impl ClipNodeRange { + pub fn to_range(&self) -> ops::Range<usize> { + let start = self.first as usize; + let end = start + self.count as usize; + + ops::Range { + start, + end, + } + } +} + +/// A helper struct for converting between coordinate systems +/// of clip sources and primitives. +// todo(gw): optimize: +// separate arrays for matrices +// cache and only build as needed. +//TODO: merge with `CoordinateSpaceMapping`? +#[derive(Debug, MallocSizeOf)] +#[cfg_attr(feature = "capture", derive(Serialize))] +enum ClipSpaceConversion { + Local, + ScaleOffset(ScaleOffset), + Transform(LayoutToWorldTransform), +} + +impl ClipSpaceConversion { + /// Construct a new clip space converter between two spatial nodes. + fn new( + prim_spatial_node_index: SpatialNodeIndex, + clip_spatial_node_index: SpatialNodeIndex, + spatial_tree: &SpatialTree, + ) -> Self { + //Note: this code is different from `get_relative_transform` in a way that we only try + // getting the relative transform if it's Local or ScaleOffset, + // falling back to the world transform otherwise. + let clip_spatial_node = spatial_tree.get_spatial_node(clip_spatial_node_index); + let prim_spatial_node = spatial_tree.get_spatial_node(prim_spatial_node_index); + + if prim_spatial_node_index == clip_spatial_node_index { + ClipSpaceConversion::Local + } else if prim_spatial_node.coordinate_system_id == clip_spatial_node.coordinate_system_id { + let scale_offset = prim_spatial_node.content_transform + .inverse() + .accumulate(&clip_spatial_node.content_transform); + ClipSpaceConversion::ScaleOffset(scale_offset) + } else { + ClipSpaceConversion::Transform( + spatial_tree + .get_world_transform(clip_spatial_node_index) + .into_transform() + ) + } + } + + fn to_flags(&self) -> ClipNodeFlags { + match *self { + ClipSpaceConversion::Local => { + ClipNodeFlags::SAME_SPATIAL_NODE | ClipNodeFlags::SAME_COORD_SYSTEM + } + ClipSpaceConversion::ScaleOffset(..) => { + ClipNodeFlags::SAME_COORD_SYSTEM + } + ClipSpaceConversion::Transform(..) => { + ClipNodeFlags::empty() + } + } + } +} + +// Temporary information that is cached and reused +// during building of a clip chain instance. +#[derive(MallocSizeOf)] +#[cfg_attr(feature = "capture", derive(Serialize))] +struct ClipNodeInfo { + conversion: ClipSpaceConversion, + handle: ClipDataHandle, +} + +impl ClipNodeInfo { + fn create_instance( + &self, + node: &ClipNode, + clipped_rect: &LayoutRect, + gpu_cache: &mut GpuCache, + resource_cache: &mut ResourceCache, + mask_tiles: &mut Vec<VisibleMaskImageTile>, + spatial_tree: &SpatialTree, + request_resources: bool, + ) -> Option<ClipNodeInstance> { + // Calculate some flags that are required for the segment + // building logic. + let mut flags = self.conversion.to_flags(); + + // Some clip shaders support a fast path mode for simple clips. + // TODO(gw): We could also apply fast path when segments are created, since we only write + // the mask for a single corner at a time then, so can always consider radii uniform. + let is_raster_2d = + flags.contains(ClipNodeFlags::SAME_COORD_SYSTEM) || + spatial_tree + .get_world_viewport_transform(node.item.spatial_node_index) + .is_2d_axis_aligned(); + if is_raster_2d && node.item.kind.supports_fast_path_rendering() { + flags |= ClipNodeFlags::USE_FAST_PATH; + } + + let mut visible_tiles = None; + + if let ClipItemKind::Image { rect, image, .. } = node.item.kind { + let request = ImageRequest { + key: image, + rendering: ImageRendering::Auto, + tile: None, + }; + + if let Some(props) = resource_cache.get_image_properties(image) { + if let Some(tile_size) = props.tiling { + let tile_range_start = mask_tiles.len(); + + // Bug 1648323 - It is unclear why on rare occasions we get + // a clipped_rect that does not intersect the clip's mask rect. + // defaulting to clipped_rect here results in zero repetitions + // which clips the primitive entirely. + let visible_rect = + clipped_rect.intersection(&rect).unwrap_or(*clipped_rect); + + let repetitions = image_tiling::repetitions( + &rect, + &visible_rect, + rect.size(), + ); + + for Repetition { origin, .. } in repetitions { + let layout_image_rect = LayoutRect::from_origin_and_size( + origin, + rect.size(), + ); + let tiles = image_tiling::tiles( + &layout_image_rect, + &visible_rect, + &props.visible_rect, + tile_size as i32, + ); + for tile in tiles { + if request_resources { + resource_cache.request_image( + request.with_tile(tile.offset), + gpu_cache, + ); + } + mask_tiles.push(VisibleMaskImageTile { + tile_offset: tile.offset, + tile_rect: tile.rect, + }); + } + } + visible_tiles = Some(tile_range_start..mask_tiles.len()); + } else if request_resources { + resource_cache.request_image(request, gpu_cache); + } + } else { + // If the supplied image key doesn't exist in the resource cache, + // skip the clip node since there is nothing to mask with. + warn!("Clip mask with missing image key {:?}", request.key); + return None; + } + } + + Some(ClipNodeInstance { + handle: self.handle, + flags, + visible_tiles, + }) + } +} + +impl ClipNode { + pub fn update( + &mut self, + device_pixel_scale: DevicePixelScale, + ) { + match self.item.kind { + ClipItemKind::Image { .. } | + ClipItemKind::Rectangle { .. } | + ClipItemKind::RoundedRectangle { .. } => {} + + ClipItemKind::BoxShadow { ref mut source } => { + // Quote from https://drafts.csswg.org/css-backgrounds-3/#shadow-blur + // "the image that would be generated by applying to the shadow a + // Gaussian blur with a standard deviation equal to half the blur radius." + let blur_radius_dp = source.blur_radius * 0.5; + + // Create scaling from requested size to cache size. + let mut content_scale = LayoutToWorldScale::new(1.0) * device_pixel_scale; + content_scale.0 = clamp_to_scale_factor(content_scale.0, false); + + // Create the cache key for this box-shadow render task. + let cache_size = to_cache_size(source.shadow_rect_alloc_size, &mut content_scale); + + let bs_cache_key = BoxShadowCacheKey { + blur_radius_dp: (blur_radius_dp * content_scale.0).round() as i32, + clip_mode: source.clip_mode, + original_alloc_size: (source.original_alloc_size * content_scale).round().to_i32(), + br_top_left: (source.shadow_radius.top_left * content_scale).round().to_i32(), + br_top_right: (source.shadow_radius.top_right * content_scale).round().to_i32(), + br_bottom_right: (source.shadow_radius.bottom_right * content_scale).round().to_i32(), + br_bottom_left: (source.shadow_radius.bottom_left * content_scale).round().to_i32(), + device_pixel_scale: Au::from_f32_px(content_scale.0), + }; + + source.cache_key = Some((cache_size, bs_cache_key)); + } + } + } +} + +#[derive(Default)] +pub struct ClipStoreScratchBuffer { + clip_node_instances: Vec<ClipNodeInstance>, + mask_tiles: Vec<VisibleMaskImageTile>, +} + +/// The main clipping public interface that other modules access. +#[derive(MallocSizeOf)] +#[cfg_attr(feature = "capture", derive(Serialize))] +pub struct ClipStore { + pub clip_node_instances: Vec<ClipNodeInstance>, + mask_tiles: Vec<VisibleMaskImageTile>, + + active_clip_node_info: Vec<ClipNodeInfo>, + active_local_clip_rect: Option<LayoutRect>, + active_pic_coverage_rect: PictureRect, +} + +// A clip chain instance is what gets built for a given clip +// chain id + local primitive region + positioning node. +#[derive(Debug)] +#[cfg_attr(feature = "capture", derive(Serialize))] +pub struct ClipChainInstance { + pub clips_range: ClipNodeRange, + // Combined clip rect for clips that are in the + // same coordinate system as the primitive. + pub local_clip_rect: LayoutRect, + pub has_non_local_clips: bool, + // If true, this clip chain requires allocation + // of a clip mask. + pub needs_mask: bool, + // Combined clip rect in picture space (may + // be more conservative that local_clip_rect). + pub pic_coverage_rect: PictureRect, + // Space, in which the `pic_coverage_rect` is defined. + pub pic_spatial_node_index: SpatialNodeIndex, +} + +impl ClipChainInstance { + pub fn empty() -> Self { + ClipChainInstance { + clips_range: ClipNodeRange { + first: 0, + count: 0, + }, + local_clip_rect: LayoutRect::zero(), + has_non_local_clips: false, + needs_mask: false, + pic_coverage_rect: PictureRect::zero(), + pic_spatial_node_index: SpatialNodeIndex::INVALID, + } + } +} + +impl ClipStore { + pub fn new() -> Self { + ClipStore { + clip_node_instances: Vec::new(), + mask_tiles: Vec::new(), + active_clip_node_info: Vec::new(), + active_local_clip_rect: None, + active_pic_coverage_rect: PictureRect::max_rect(), + } + } + + pub fn get_instance_from_range( + &self, + node_range: &ClipNodeRange, + index: u32, + ) -> &ClipNodeInstance { + &self.clip_node_instances[(node_range.first + index) as usize] + } + + /// Setup the active clip chains for building a clip chain instance. + pub fn set_active_clips( + &mut self, + prim_spatial_node_index: SpatialNodeIndex, + pic_spatial_node_index: SpatialNodeIndex, + clip_leaf_id: ClipLeafId, + spatial_tree: &SpatialTree, + clip_data_store: &ClipDataStore, + clip_tree: &ClipTree, + ) { + self.active_clip_node_info.clear(); + self.active_local_clip_rect = None; + self.active_pic_coverage_rect = PictureRect::max_rect(); + + let clip_root = clip_tree.current_clip_root(); + let clip_leaf = clip_tree.get_leaf(clip_leaf_id); + + let mut local_clip_rect = clip_leaf.local_clip_rect; + let mut current = clip_leaf.node_id; + + while current != clip_root { + let node = clip_tree.get_node(current); + + if !add_clip_node_to_current_chain( + node.handle, + prim_spatial_node_index, + pic_spatial_node_index, + &mut local_clip_rect, + &mut self.active_clip_node_info, + &mut self.active_pic_coverage_rect, + clip_data_store, + spatial_tree, + ) { + return; + } + + current = node.parent; + } + + self.active_local_clip_rect = Some(local_clip_rect); + } + + /// Setup the active clip chains, based on an existing primitive clip chain instance. + pub fn set_active_clips_from_clip_chain( + &mut self, + prim_clip_chain: &ClipChainInstance, + prim_spatial_node_index: SpatialNodeIndex, + spatial_tree: &SpatialTree, + clip_data_store: &ClipDataStore, + ) { + // TODO(gw): Although this does less work than set_active_clips(), it does + // still do some unnecessary work (such as the clip space conversion). + // We could consider optimizing this if it ever shows up in a profile. + + self.active_clip_node_info.clear(); + self.active_local_clip_rect = Some(prim_clip_chain.local_clip_rect); + self.active_pic_coverage_rect = prim_clip_chain.pic_coverage_rect; + + let clip_instances = &self + .clip_node_instances[prim_clip_chain.clips_range.to_range()]; + for clip_instance in clip_instances { + let clip = &clip_data_store[clip_instance.handle]; + let conversion = ClipSpaceConversion::new( + prim_spatial_node_index, + clip.item.spatial_node_index, + spatial_tree, + ); + self.active_clip_node_info.push(ClipNodeInfo { + handle: clip_instance.handle, + conversion, + }); + } + } + + /// Given a clip-chain instance, return a safe rect within the visible region + /// that can be assumed to be unaffected by clip radii. Returns None if it + /// encounters any complex cases, just handling rounded rects in the same + /// coordinate system as the clip-chain for now. + pub fn get_inner_rect_for_clip_chain( + &self, + clip_chain: &ClipChainInstance, + clip_data_store: &ClipDataStore, + spatial_tree: &SpatialTree, + ) -> Option<PictureRect> { + let mut inner_rect = clip_chain.pic_coverage_rect; + let clip_instances = &self + .clip_node_instances[clip_chain.clips_range.to_range()]; + + for clip_instance in clip_instances { + // Don't handle mapping between coord systems for now + if !clip_instance.flags.contains(ClipNodeFlags::SAME_COORD_SYSTEM) { + return None; + } + + let clip_node = &clip_data_store[clip_instance.handle]; + + match clip_node.item.kind { + // Ignore any clips which are complex or impossible to calculate + // inner rects for now + ClipItemKind::Rectangle { mode: ClipMode::ClipOut, .. } | + ClipItemKind::Image { .. } | + ClipItemKind::BoxShadow { .. } | + ClipItemKind::RoundedRectangle { mode: ClipMode::ClipOut, .. } => { + return None; + } + // Normal Clip rects are already handled by the clip-chain pic_coverage_rect, + // no need to do anything here + ClipItemKind::Rectangle { mode: ClipMode::Clip, .. } => {} + ClipItemKind::RoundedRectangle { mode: ClipMode::Clip, rect, radius } => { + // Get an inner rect for the rounded-rect clip + let local_inner_rect = match extract_inner_rect_safe(&rect, &radius) { + Some(rect) => rect, + None => return None, + }; + + // Map it from local -> picture space + let mapper = SpaceMapper::new_with_target( + clip_chain.pic_spatial_node_index, + clip_node.item.spatial_node_index, + PictureRect::max_rect(), + spatial_tree, + ); + + // Accumulate in to the inner_rect, in case there are multiple rounded-rect clips + if let Some(pic_inner_rect) = mapper.map(&local_inner_rect) { + inner_rect = inner_rect.intersection(&pic_inner_rect).unwrap_or(PictureRect::zero()); + } + } + } + } + + Some(inner_rect) + } + + /// The main interface external code uses. Given a local primitive, positioning + /// information, and a clip chain id, build an optimized clip chain instance. + pub fn build_clip_chain_instance( + &mut self, + local_prim_rect: LayoutRect, + prim_to_pic_mapper: &SpaceMapper<LayoutPixel, PicturePixel>, + pic_to_world_mapper: &SpaceMapper<PicturePixel, WorldPixel>, + spatial_tree: &SpatialTree, + gpu_cache: &mut GpuCache, + resource_cache: &mut ResourceCache, + device_pixel_scale: DevicePixelScale, + world_rect: &WorldRect, + clip_data_store: &mut ClipDataStore, + request_resources: bool, + ) -> Option<ClipChainInstance> { + let local_clip_rect = match self.active_local_clip_rect { + Some(rect) => rect, + None => return None, + }; + profile_scope!("build_clip_chain_instance"); + + let local_bounding_rect = local_prim_rect.intersection(&local_clip_rect)?; + let mut pic_coverage_rect = prim_to_pic_mapper.map(&local_bounding_rect)?; + let world_clip_rect = pic_to_world_mapper.map(&pic_coverage_rect)?; + + // Now, we've collected all the clip nodes that *potentially* affect this + // primitive region, and reduced the size of the prim region as much as possible. + + // Run through the clip nodes, and see which ones affect this prim region. + + let first_clip_node_index = self.clip_node_instances.len() as u32; + let mut has_non_local_clips = false; + let mut needs_mask = false; + + // For each potential clip node + for node_info in self.active_clip_node_info.drain(..) { + let node = &mut clip_data_store[node_info.handle]; + + // See how this clip affects the prim region. + let clip_result = match node_info.conversion { + ClipSpaceConversion::Local => { + node.item.kind.get_clip_result(&local_bounding_rect) + } + ClipSpaceConversion::ScaleOffset(ref scale_offset) => { + has_non_local_clips = true; + node.item.kind.get_clip_result(&scale_offset.unmap_rect(&local_bounding_rect)) + } + ClipSpaceConversion::Transform(ref transform) => { + has_non_local_clips = true; + node.item.kind.get_clip_result_complex( + transform, + &world_clip_rect, + world_rect, + ) + } + }; + + match clip_result { + ClipResult::Accept => { + // Doesn't affect the primitive at all, so skip adding to list + } + ClipResult::Reject => { + // Completely clips the supplied prim rect + return None; + } + ClipResult::Partial => { + // Needs a mask -> add to clip node indices + + // TODO(gw): Ensure this only runs once on each node per frame? + node.update(device_pixel_scale); + + // Create the clip node instance for this clip node + if let Some(instance) = node_info.create_instance( + node, + &local_bounding_rect, + gpu_cache, + resource_cache, + &mut self.mask_tiles, + spatial_tree, + request_resources, + ) { + // As a special case, a partial accept of a clip rect that is + // in the same coordinate system as the primitive doesn't need + // a clip mask. Instead, it can be handled by the primitive + // vertex shader as part of the local clip rect. This is an + // important optimization for reducing the number of clip + // masks that are allocated on common pages. + needs_mask |= match node.item.kind { + ClipItemKind::Rectangle { mode: ClipMode::ClipOut, .. } | + ClipItemKind::RoundedRectangle { .. } | + ClipItemKind::Image { .. } | + ClipItemKind::BoxShadow { .. } => { + true + } + + ClipItemKind::Rectangle { mode: ClipMode::Clip, .. } => { + !instance.flags.contains(ClipNodeFlags::SAME_COORD_SYSTEM) + } + }; + + // Store this in the index buffer for this clip chain instance. + self.clip_node_instances.push(instance); + } + } + } + } + + // Get the range identifying the clip nodes in the index buffer. + let clips_range = ClipNodeRange { + first: first_clip_node_index, + count: self.clip_node_instances.len() as u32 - first_clip_node_index, + }; + + // If this clip chain needs a mask, reduce the size of the mask allocation + // by any clips that were in the same space as the picture. This can result + // in much smaller clip mask allocations in some cases. Note that the ordering + // here is important - the reduction must occur *after* the clip item accept + // reject checks above, so that we don't eliminate masks accidentally (since + // we currently only support a local clip rect in the vertex shader). + if needs_mask { + pic_coverage_rect = pic_coverage_rect.intersection(&self.active_pic_coverage_rect)?; + } + + // Return a valid clip chain instance + Some(ClipChainInstance { + clips_range, + has_non_local_clips, + local_clip_rect, + pic_coverage_rect, + pic_spatial_node_index: prim_to_pic_mapper.ref_spatial_node_index, + needs_mask, + }) + } + + pub fn begin_frame(&mut self, scratch: &mut ClipStoreScratchBuffer) { + mem::swap(&mut self.clip_node_instances, &mut scratch.clip_node_instances); + mem::swap(&mut self.mask_tiles, &mut scratch.mask_tiles); + self.clip_node_instances.clear(); + self.mask_tiles.clear(); + } + + pub fn end_frame(&mut self, scratch: &mut ClipStoreScratchBuffer) { + mem::swap(&mut self.clip_node_instances, &mut scratch.clip_node_instances); + mem::swap(&mut self.mask_tiles, &mut scratch.mask_tiles); + } + + pub fn visible_mask_tiles(&self, instance: &ClipNodeInstance) -> &[VisibleMaskImageTile] { + if let Some(range) = &instance.visible_tiles { + &self.mask_tiles[range.clone()] + } else { + &[] + } + } +} + +pub struct ComplexTranslateIter<I> { + source: I, + offset: LayoutVector2D, +} + +impl<I: Iterator<Item = ComplexClipRegion>> Iterator for ComplexTranslateIter<I> { + type Item = ComplexClipRegion; + fn next(&mut self) -> Option<Self::Item> { + self.source + .next() + .map(|mut complex| { + complex.rect = complex.rect.translate(self.offset); + complex + }) + } +} + +// The ClipItemKey is a hashable representation of the contents +// of a clip item. It is used during interning to de-duplicate +// clip nodes between frames and display lists. This allows quick +// comparison of clip node equality by handle, and also allows +// the uploaded GPU cache handle to be retained between display lists. +// TODO(gw): Maybe we should consider constructing these directly +// in the DL builder? +#[derive(Copy, Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub enum ClipItemKeyKind { + Rectangle(RectangleKey, ClipMode), + RoundedRectangle(RectangleKey, BorderRadiusAu, ClipMode), + ImageMask(RectangleKey, ImageKey, Option<PolygonDataHandle>), + BoxShadow(PointKey, SizeKey, BorderRadiusAu, RectangleKey, Au, BoxShadowClipMode), +} + +impl ClipItemKeyKind { + pub fn rectangle(rect: LayoutRect, mode: ClipMode) -> Self { + ClipItemKeyKind::Rectangle(rect.into(), mode) + } + + pub fn rounded_rect(rect: LayoutRect, mut radii: BorderRadius, mode: ClipMode) -> Self { + if radii.is_zero() { + ClipItemKeyKind::rectangle(rect, mode) + } else { + ensure_no_corner_overlap(&mut radii, rect.size()); + ClipItemKeyKind::RoundedRectangle( + rect.into(), + radii.into(), + mode, + ) + } + } + + pub fn image_mask(image_mask: &ImageMask, mask_rect: LayoutRect, + polygon_handle: Option<PolygonDataHandle>) -> Self { + ClipItemKeyKind::ImageMask( + mask_rect.into(), + image_mask.image, + polygon_handle, + ) + } + + pub fn box_shadow( + shadow_rect: LayoutRect, + shadow_radius: BorderRadius, + prim_shadow_rect: LayoutRect, + blur_radius: f32, + clip_mode: BoxShadowClipMode, + ) -> Self { + // Get the fractional offsets required to match the + // source rect with a minimal rect. + let fract_offset = LayoutPoint::new( + shadow_rect.min.x.fract().abs(), + shadow_rect.min.y.fract().abs(), + ); + + ClipItemKeyKind::BoxShadow( + fract_offset.into(), + shadow_rect.size().into(), + shadow_radius.into(), + prim_shadow_rect.into(), + Au::from_f32_px(blur_radius), + clip_mode, + ) + } + + pub fn node_kind(&self) -> ClipNodeKind { + match *self { + ClipItemKeyKind::Rectangle(_, ClipMode::Clip) => ClipNodeKind::Rectangle, + + ClipItemKeyKind::Rectangle(_, ClipMode::ClipOut) | + ClipItemKeyKind::RoundedRectangle(..) | + ClipItemKeyKind::ImageMask(..) | + ClipItemKeyKind::BoxShadow(..) => ClipNodeKind::Complex, + } + } +} + +#[derive(Debug, Copy, Clone, Eq, MallocSizeOf, PartialEq, Hash)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct ClipItemKey { + pub kind: ClipItemKeyKind, + pub spatial_node_index: SpatialNodeIndex, +} + +/// The data available about an interned clip node during scene building +#[derive(Debug, MallocSizeOf)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct ClipInternData { + pub key: ClipItemKey, +} + +impl intern::InternDebug for ClipItemKey {} + +impl intern::Internable for ClipIntern { + type Key = ClipItemKey; + type StoreData = ClipNode; + type InternData = ClipInternData; + const PROFILE_COUNTER: usize = crate::profiler::INTERNED_CLIPS; +} + +#[derive(Debug, MallocSizeOf)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub enum ClipItemKind { + Rectangle { + rect: LayoutRect, + mode: ClipMode, + }, + RoundedRectangle { + rect: LayoutRect, + radius: BorderRadius, + mode: ClipMode, + }, + Image { + image: ImageKey, + rect: LayoutRect, + polygon_handle: Option<PolygonDataHandle>, + }, + BoxShadow { + source: BoxShadowClipSource, + }, +} + +#[derive(Debug, MallocSizeOf)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct ClipItem { + pub kind: ClipItemKind, + pub spatial_node_index: SpatialNodeIndex, +} + +fn compute_box_shadow_parameters( + shadow_rect_fract_offset: LayoutPoint, + shadow_rect_size: LayoutSize, + mut shadow_radius: BorderRadius, + prim_shadow_rect: LayoutRect, + blur_radius: f32, + clip_mode: BoxShadowClipMode, +) -> BoxShadowClipSource { + // Make sure corners don't overlap. + ensure_no_corner_overlap(&mut shadow_radius, shadow_rect_size); + + let fract_size = LayoutSize::new( + shadow_rect_size.width.fract().abs(), + shadow_rect_size.height.fract().abs(), + ); + + // Create a minimal size primitive mask to blur. In this + // case, we ensure the size of each corner is the same, + // to simplify the shader logic that stretches the blurred + // result across the primitive. + let max_corner_width = shadow_radius.top_left.width + .max(shadow_radius.bottom_left.width) + .max(shadow_radius.top_right.width) + .max(shadow_radius.bottom_right.width); + let max_corner_height = shadow_radius.top_left.height + .max(shadow_radius.bottom_left.height) + .max(shadow_radius.top_right.height) + .max(shadow_radius.bottom_right.height); + + // Get maximum distance that can be affected by given blur radius. + let blur_region = (BLUR_SAMPLE_SCALE * blur_radius).ceil(); + + // If the largest corner is smaller than the blur radius, we need to ensure + // that it's big enough that the corners don't affect the middle segments. + let used_corner_width = max_corner_width.max(blur_region); + let used_corner_height = max_corner_height.max(blur_region); + + // Minimal nine-patch size, corner + internal + corner. + let min_shadow_rect_size = LayoutSize::new( + 2.0 * used_corner_width + blur_region, + 2.0 * used_corner_height + blur_region, + ); + + // The minimal rect to blur. + let mut minimal_shadow_rect = LayoutRect::from_origin_and_size( + LayoutPoint::new( + blur_region + shadow_rect_fract_offset.x, + blur_region + shadow_rect_fract_offset.y, + ), + LayoutSize::new( + min_shadow_rect_size.width + fract_size.width, + min_shadow_rect_size.height + fract_size.height, + ), + ); + + // If the width or height ends up being bigger than the original + // primitive shadow rect, just blur the entire rect along that + // axis and draw that as a simple blit. This is necessary for + // correctness, since the blur of one corner may affect the blur + // in another corner. + let mut stretch_mode_x = BoxShadowStretchMode::Stretch; + if shadow_rect_size.width < minimal_shadow_rect.width() { + minimal_shadow_rect.max.x = minimal_shadow_rect.min.x + shadow_rect_size.width; + stretch_mode_x = BoxShadowStretchMode::Simple; + } + + let mut stretch_mode_y = BoxShadowStretchMode::Stretch; + if shadow_rect_size.height < minimal_shadow_rect.height() { + minimal_shadow_rect.max.y = minimal_shadow_rect.min.y + shadow_rect_size.height; + stretch_mode_y = BoxShadowStretchMode::Simple; + } + + // Expand the shadow rect by enough room for the blur to take effect. + let shadow_rect_alloc_size = LayoutSize::new( + 2.0 * blur_region + minimal_shadow_rect.width().ceil(), + 2.0 * blur_region + minimal_shadow_rect.height().ceil(), + ); + + BoxShadowClipSource { + original_alloc_size: shadow_rect_alloc_size, + shadow_rect_alloc_size, + shadow_radius, + prim_shadow_rect, + blur_radius, + clip_mode, + stretch_mode_x, + stretch_mode_y, + render_task: None, + cache_key: None, + minimal_shadow_rect, + } +} + +impl ClipItemKind { + pub fn new_box_shadow( + shadow_rect_fract_offset: LayoutPoint, + shadow_rect_size: LayoutSize, + mut shadow_radius: BorderRadius, + prim_shadow_rect: LayoutRect, + blur_radius: f32, + clip_mode: BoxShadowClipMode, + ) -> Self { + let mut source = compute_box_shadow_parameters( + shadow_rect_fract_offset, + shadow_rect_size, + shadow_radius, + prim_shadow_rect, + blur_radius, + clip_mode, + ); + + fn needed_downscaling(source: &BoxShadowClipSource) -> Option<f32> { + // This size is fairly arbitrary, but it's the same as the size that + // we use to avoid caching big blurred stacking contexts. + // + // If you change it, ensure that the reftests + // box-shadow-large-blur-radius-* still hit the downscaling path, + // and that they render correctly. + const MAX_SIZE: f32 = 2048.; + + let max_dimension = + source.shadow_rect_alloc_size.width.max(source.shadow_rect_alloc_size.height); + + if max_dimension > MAX_SIZE { + Some(MAX_SIZE / max_dimension) + } else { + None + } + } + + if let Some(downscale) = needed_downscaling(&source) { + shadow_radius.bottom_left.height *= downscale; + shadow_radius.bottom_left.width *= downscale; + shadow_radius.bottom_right.height *= downscale; + shadow_radius.bottom_right.width *= downscale; + shadow_radius.top_left.height *= downscale; + shadow_radius.top_left.width *= downscale; + shadow_radius.top_right.height *= downscale; + shadow_radius.top_right.width *= downscale; + + let original_alloc_size = source.shadow_rect_alloc_size; + + source = compute_box_shadow_parameters( + shadow_rect_fract_offset * downscale, + shadow_rect_size * downscale, + shadow_radius, + prim_shadow_rect, + blur_radius * downscale, + clip_mode, + ); + source.original_alloc_size = original_alloc_size; + } + ClipItemKind::BoxShadow { source } + } + + /// Returns true if this clip mask can run through the fast path + /// for the given clip item type. + /// + /// Note: this logic has to match `ClipBatcher::add` behavior. + fn supports_fast_path_rendering(&self) -> bool { + match *self { + ClipItemKind::Rectangle { .. } | + ClipItemKind::Image { .. } | + ClipItemKind::BoxShadow { .. } => { + false + } + ClipItemKind::RoundedRectangle { ref radius, .. } => { + // The rounded clip rect fast path shader can only work + // if the radii are uniform. + radius.is_uniform().is_some() + } + } + } + + // Get an optional clip rect that a clip source can provide to + // reduce the size of a primitive region. This is typically + // used to eliminate redundant clips, and reduce the size of + // any clip mask that eventually gets drawn. + pub fn get_local_clip_rect(&self) -> Option<LayoutRect> { + match *self { + ClipItemKind::Rectangle { rect, mode: ClipMode::Clip } => Some(rect), + ClipItemKind::Rectangle { mode: ClipMode::ClipOut, .. } => None, + ClipItemKind::RoundedRectangle { rect, mode: ClipMode::Clip, .. } => Some(rect), + ClipItemKind::RoundedRectangle { mode: ClipMode::ClipOut, .. } => None, + ClipItemKind::Image { rect, .. } => { + Some(rect) + } + ClipItemKind::BoxShadow { .. } => None, + } + } + + fn get_clip_result_complex( + &self, + transform: &LayoutToWorldTransform, + prim_world_rect: &WorldRect, + world_rect: &WorldRect, + ) -> ClipResult { + let visible_rect = match prim_world_rect.intersection(world_rect) { + Some(rect) => rect, + None => return ClipResult::Reject, + }; + + let (clip_rect, inner_rect, mode) = match *self { + ClipItemKind::Rectangle { rect, mode } => { + (rect, Some(rect), mode) + } + ClipItemKind::RoundedRectangle { rect, ref radius, mode } => { + let inner_clip_rect = extract_inner_rect_safe(&rect, radius); + (rect, inner_clip_rect, mode) + } + ClipItemKind::Image { rect, .. } => { + (rect, None, ClipMode::Clip) + } + ClipItemKind::BoxShadow { .. } => { + return ClipResult::Partial; + } + }; + + if let Some(ref inner_clip_rect) = inner_rect { + if let Some(()) = projected_rect_contains(inner_clip_rect, transform, &visible_rect) { + return match mode { + ClipMode::Clip => ClipResult::Accept, + ClipMode::ClipOut => ClipResult::Reject, + }; + } + } + + match mode { + ClipMode::Clip => { + let outer_clip_rect = match project_rect( + transform, + &clip_rect, + &world_rect, + ) { + Some(outer_clip_rect) => outer_clip_rect, + None => return ClipResult::Partial, + }; + + match outer_clip_rect.intersection(prim_world_rect) { + Some(..) => { + ClipResult::Partial + } + None => { + ClipResult::Reject + } + } + } + ClipMode::ClipOut => ClipResult::Partial, + } + } + + // Check how a given clip source affects a local primitive region. + fn get_clip_result( + &self, + prim_rect: &LayoutRect, + ) -> ClipResult { + match *self { + ClipItemKind::Rectangle { rect, mode: ClipMode::Clip } => { + if rect.contains_box(prim_rect) { + return ClipResult::Accept; + } + + match rect.intersection(prim_rect) { + Some(..) => { + ClipResult::Partial + } + None => { + ClipResult::Reject + } + } + } + ClipItemKind::Rectangle { rect, mode: ClipMode::ClipOut } => { + if rect.contains_box(prim_rect) { + return ClipResult::Reject; + } + + match rect.intersection(prim_rect) { + Some(_) => { + ClipResult::Partial + } + None => { + ClipResult::Accept + } + } + } + ClipItemKind::RoundedRectangle { rect, ref radius, mode: ClipMode::Clip } => { + // TODO(gw): Consider caching this in the ClipNode + // if it ever shows in profiles. + if rounded_rectangle_contains_box_quick(&rect, radius, &prim_rect) { + return ClipResult::Accept; + } + + match rect.intersection(prim_rect) { + Some(..) => { + ClipResult::Partial + } + None => { + ClipResult::Reject + } + } + } + ClipItemKind::RoundedRectangle { rect, ref radius, mode: ClipMode::ClipOut } => { + // TODO(gw): Consider caching this in the ClipNode + // if it ever shows in profiles. + if rounded_rectangle_contains_box_quick(&rect, radius, &prim_rect) { + return ClipResult::Reject; + } + + match rect.intersection(prim_rect) { + Some(_) => { + ClipResult::Partial + } + None => { + ClipResult::Accept + } + } + } + ClipItemKind::Image { rect, .. } => { + match rect.intersection(prim_rect) { + Some(..) => { + ClipResult::Partial + } + None => { + ClipResult::Reject + } + } + } + ClipItemKind::BoxShadow { .. } => { + ClipResult::Partial + } + } + } +} + +/// Represents a local rect and a device space +/// rectangles that are either outside or inside bounds. +#[derive(Clone, Debug, PartialEq)] +pub struct Geometry { + pub local_rect: LayoutRect, + pub device_rect: DeviceIntRect, +} + +impl From<LayoutRect> for Geometry { + fn from(local_rect: LayoutRect) -> Self { + Geometry { + local_rect, + device_rect: DeviceIntRect::zero(), + } + } +} + +pub fn rounded_rectangle_contains_point( + point: &LayoutPoint, + rect: &LayoutRect, + radii: &BorderRadius +) -> bool { + if !rect.contains(*point) { + return false; + } + + let top_left_center = rect.min + radii.top_left.to_vector(); + if top_left_center.x > point.x && top_left_center.y > point.y && + !Ellipse::new(radii.top_left).contains(*point - top_left_center.to_vector()) { + return false; + } + + let bottom_right_center = rect.bottom_right() - radii.bottom_right.to_vector(); + if bottom_right_center.x < point.x && bottom_right_center.y < point.y && + !Ellipse::new(radii.bottom_right).contains(*point - bottom_right_center.to_vector()) { + return false; + } + + let top_right_center = rect.top_right() + + LayoutVector2D::new(-radii.top_right.width, radii.top_right.height); + if top_right_center.x < point.x && top_right_center.y > point.y && + !Ellipse::new(radii.top_right).contains(*point - top_right_center.to_vector()) { + return false; + } + + let bottom_left_center = rect.bottom_left() + + LayoutVector2D::new(radii.bottom_left.width, -radii.bottom_left.height); + if bottom_left_center.x > point.x && bottom_left_center.y < point.y && + !Ellipse::new(radii.bottom_left).contains(*point - bottom_left_center.to_vector()) { + return false; + } + + true +} + +/// Return true if the rounded rectangle described by `container` and `radii` +/// definitely contains `containee`. May return false negatives, but never false +/// positives. +fn rounded_rectangle_contains_box_quick( + container: &LayoutRect, + radii: &BorderRadius, + containee: &LayoutRect, +) -> bool { + if !container.contains_box(containee) { + return false; + } + + /// Return true if `point` falls within `corner`. This only covers the + /// upper-left case; we transform the other corners into that form. + fn foul(point: LayoutPoint, corner: LayoutPoint) -> bool { + point.x < corner.x && point.y < corner.y + } + + /// Flip `pt` about the y axis (i.e. negate `x`). + fn flip_x(pt: LayoutPoint) -> LayoutPoint { + LayoutPoint { x: -pt.x, .. pt } + } + + /// Flip `pt` about the x axis (i.e. negate `y`). + fn flip_y(pt: LayoutPoint) -> LayoutPoint { + LayoutPoint { y: -pt.y, .. pt } + } + + if foul(containee.top_left(), container.top_left() + radii.top_left) || + foul(flip_x(containee.top_right()), flip_x(container.top_right()) + radii.top_right) || + foul(flip_y(containee.bottom_left()), flip_y(container.bottom_left()) + radii.bottom_left) || + foul(-containee.bottom_right(), -container.bottom_right() + radii.bottom_right) + { + return false; + } + + true +} + +/// Test where point p is relative to the infinite line that passes through the segment +/// defined by p0 and p1. Point p is on the "left" of the line if the triangle (p0, p1, p) +/// forms a counter-clockwise triangle. +/// > 0 is left of the line +/// < 0 is right of the line +/// == 0 is on the line +pub fn is_left_of_line( + p_x: f32, + p_y: f32, + p0_x: f32, + p0_y: f32, + p1_x: f32, + p1_y: f32, +) -> f32 { + (p1_x - p0_x) * (p_y - p0_y) - (p_x - p0_x) * (p1_y - p0_y) +} + +pub fn polygon_contains_point( + point: &LayoutPoint, + rect: &LayoutRect, + polygon: &PolygonKey, +) -> bool { + if !rect.contains(*point) { + return false; + } + + // p is a LayoutPoint that we'll be comparing to dimensionless PointKeys, + // which were created from LayoutPoints, so it all works out. + let p = LayoutPoint::new(point.x - rect.min.x, point.y - rect.min.y); + + // Calculate a winding number for this point. + let mut winding_number: i32 = 0; + + let count = polygon.point_count as usize; + + for i in 0..count { + let p0 = polygon.points[i]; + let p1 = polygon.points[(i + 1) % count]; + + if p0.y <= p.y { + if p1.y > p.y { + if is_left_of_line(p.x, p.y, p0.x, p0.y, p1.x, p1.y) > 0.0 { + winding_number = winding_number + 1; + } + } + } else if p1.y <= p.y { + if is_left_of_line(p.x, p.y, p0.x, p0.y, p1.x, p1.y) < 0.0 { + winding_number = winding_number - 1; + } + } + } + + match polygon.fill_rule { + FillRule::Nonzero => winding_number != 0, + FillRule::Evenodd => winding_number.abs() % 2 == 1, + } +} + +pub fn projected_rect_contains( + source_rect: &LayoutRect, + transform: &LayoutToWorldTransform, + target_rect: &WorldRect, +) -> Option<()> { + let points = [ + transform.transform_point2d(source_rect.top_left())?, + transform.transform_point2d(source_rect.top_right())?, + transform.transform_point2d(source_rect.bottom_right())?, + transform.transform_point2d(source_rect.bottom_left())?, + ]; + let target_points = [ + target_rect.top_left(), + target_rect.top_right(), + target_rect.bottom_right(), + target_rect.bottom_left(), + ]; + // iterate the edges of the transformed polygon + for (a, b) in points + .iter() + .cloned() + .zip(points[1..].iter().cloned().chain(iter::once(points[0]))) + { + // If this edge is redundant, it's a weird, case, and we shouldn't go + // length in trying to take the fast path (e.g. when the whole rectangle is a point). + // If any of edges of the target rectangle crosses the edge, it's not completely + // inside our transformed polygon either. + if a.approx_eq(&b) || target_points.iter().any(|&c| (b - a).cross(c - a) < 0.0) { + return None + } + } + + Some(()) +} + + +// Add a clip node into the list of clips to be processed +// for the current clip chain. Returns false if the clip +// results in the entire primitive being culled out. +fn add_clip_node_to_current_chain( + handle: ClipDataHandle, + prim_spatial_node_index: SpatialNodeIndex, + pic_spatial_node_index: SpatialNodeIndex, + local_clip_rect: &mut LayoutRect, + clip_node_info: &mut Vec<ClipNodeInfo>, + pic_coverage_rect: &mut PictureRect, + clip_data_store: &ClipDataStore, + spatial_tree: &SpatialTree, +) -> bool { + let clip_node = &clip_data_store[handle]; + + // Determine the most efficient way to convert between coordinate + // systems of the primitive and clip node. + let conversion = ClipSpaceConversion::new( + prim_spatial_node_index, + clip_node.item.spatial_node_index, + spatial_tree, + ); + + // If we can convert spaces, try to reduce the size of the region + // requested, and cache the conversion information for the next step. + if let Some(clip_rect) = clip_node.item.kind.get_local_clip_rect() { + match conversion { + ClipSpaceConversion::Local => { + *local_clip_rect = match local_clip_rect.intersection(&clip_rect) { + Some(rect) => rect, + None => return false, + }; + } + ClipSpaceConversion::ScaleOffset(ref scale_offset) => { + let clip_rect = scale_offset.map_rect(&clip_rect); + *local_clip_rect = match local_clip_rect.intersection(&clip_rect) { + Some(rect) => rect, + None => return false, + }; + } + ClipSpaceConversion::Transform(..) => { + // Map the local clip rect directly into the same space as the picture + // surface. This will often be the same space as the clip itself, which + // results in a reduction in allocated clip mask size. + + // For simplicity, only apply this optimization if the clip is in the + // same coord system as the picture. There are some 'advanced' perspective + // clip tests in wrench that break without this check. Those cases are + // never used in Gecko, and we aim to remove support in WR for that + // in future to simplify the clipping pipeline. + let pic_coord_system = spatial_tree + .get_spatial_node(pic_spatial_node_index) + .coordinate_system_id; + + let clip_coord_system = spatial_tree + .get_spatial_node(clip_node.item.spatial_node_index) + .coordinate_system_id; + + if pic_coord_system == clip_coord_system { + let mapper = SpaceMapper::new_with_target( + pic_spatial_node_index, + clip_node.item.spatial_node_index, + PictureRect::max_rect(), + spatial_tree, + ); + + if let Some(pic_clip_rect) = mapper.map(&clip_rect) { + *pic_coverage_rect = pic_clip_rect + .intersection(pic_coverage_rect) + .unwrap_or(PictureRect::zero()); + } + } + } + } + } + + clip_node_info.push(ClipNodeInfo { + conversion, + handle, + }); + + true +} + +#[cfg(test)] +mod tests { + use super::projected_rect_contains; + use euclid::{Transform3D, rect}; + + #[test] + fn test_empty_projected_rect() { + assert_eq!( + None, + projected_rect_contains( + &rect(10.0, 10.0, 0.0, 0.0).to_box2d(), + &Transform3D::identity(), + &rect(20.0, 20.0, 10.0, 10.0).to_box2d(), + ), + "Empty rectangle is considered to include a non-empty!" + ); + } +} + +/// PolygonKeys get interned, because it's a convenient way to move the data +/// for the polygons out of the ClipItemKind and ClipItemKeyKind enums. The +/// polygon data is both interned and retrieved by the scene builder, and not +/// accessed at all by the frame builder. Another oddity is that the +/// PolygonKey contains the totality of the information about the polygon, so +/// the InternData and StoreData types are both PolygonKey. +#[derive(Copy, Clone, Debug, Hash, MallocSizeOf, PartialEq, Eq)] +#[cfg_attr(any(feature = "serde"), derive(Deserialize, Serialize))] +pub enum PolygonIntern {} + +pub type PolygonDataHandle = intern::Handle<PolygonIntern>; + +impl intern::InternDebug for PolygonKey {} + +impl intern::Internable for PolygonIntern { + type Key = PolygonKey; + type StoreData = PolygonKey; + type InternData = PolygonKey; + const PROFILE_COUNTER: usize = crate::profiler::INTERNED_POLYGONS; +} |