Bug 1666641 - Build hit-testing structure from clip templates. r=nical

Previously, the hit-testing structure was built from the set of
clip-chains supplied to the frame builder, but this has several
disadvantages. Now, the hit-testing structure is built during
scene building, flattening clip templates into a set of clips
that applies to each hit-testing primitive.

The advantages are:
 - Most importantly, hit-testing doesn't rely on the existing
   clip-chain code, allowing changes and optimizations to that.
 - No need to read all of the clips for the entire scene, only
   clips that are referenced by hit-test items are read (which
   is typically a small fraction).
 - No need to read the clips each time the hit-tester is rebuilt,
   during frame building. Instead, the clips are read only once
   during each scene build.
 - Clips are stored in the hit testing scene, and thus shared
   among all hit-testers created for a given scene.

Differential Revision: https://phabricator.services.mozilla.com/D91067
This commit is contained in:
Glenn Watson 2020-09-23 14:27:41 +00:00
parent bd0e9ab215
commit 87ee371a3b
5 changed files with 154 additions and 235 deletions

View File

@ -155,7 +155,7 @@ pub struct SceneClipInstance {
/// The interned clip + positioning information that is used during frame building.
pub clip: ClipInstance,
/// The definition of the clip, used during scene building to optimize clip-chains.
pub key: ClipItemKeyKind,
pub key: ClipItemKey,
}
/// A clip template defines clips in terms of the public API. Specifically,
@ -704,7 +704,7 @@ pub struct ClipStore {
/// Map of all clip templates defined by the public API to templates
#[ignore_malloc_size_of = "range missing"]
templates: FastHashMap<ClipId, ClipTemplate>,
pub templates: FastHashMap<ClipId, ClipTemplate>,
/// A stack of current clip-chain builders. A new clip-chain builder is
/// typically created each time a clip root (such as an iframe or stacking
@ -1378,7 +1378,7 @@ impl ClipItemKeyKind {
}
}
#[derive(Debug, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
#[derive(Debug, Copy, Clone, Eq, MallocSizeOf, PartialEq, Hash)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct ClipItemKey {

View File

@ -3,13 +3,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use api::{BorderRadius, ClipMode, HitTestItem, HitTestResult, ItemTag, PrimitiveFlags};
use api::{PipelineId, ApiHitTester};
use api::{PipelineId, ApiHitTester, ClipId};
use api::units::*;
use crate::clip::{ClipChainId, ClipDataStore, ClipNode, ClipItemKind, ClipStore};
use crate::clip::{rounded_rectangle_contains_point};
use crate::clip::{ClipItemKind, ClipStore, ClipNode, rounded_rectangle_contains_point};
use crate::spatial_tree::{SpatialNodeIndex, SpatialTree};
use crate::internal_types::{FastHashMap, LayoutPrimitiveInfo};
use std::{ops, u32};
use std::ops;
use std::sync::{Arc, Mutex};
use crate::util::LayoutToWorldFastTransform;
@ -52,7 +51,7 @@ impl ApiHitTester for SharedHitTester {
/// data from the SpatialTree that will persist as a new frame is under construction,
/// allowing hit tests consistent with the currently rendered frame.
#[derive(MallocSizeOf)]
pub struct HitTestSpatialNode {
struct HitTestSpatialNode {
/// The pipeline id of this node.
pipeline_id: PipelineId,
@ -67,14 +66,19 @@ pub struct HitTestSpatialNode {
}
#[derive(MallocSizeOf)]
pub struct HitTestClipNode {
struct HitTestClipNode {
/// A particular point must be inside all of these regions to be considered clipped in
/// for the purposes of a hit test.
region: HitTestRegion,
/// The positioning node for this clip
spatial_node_index: SpatialNodeIndex,
}
impl HitTestClipNode {
fn new(node: &ClipNode) -> Self {
fn new(
node: ClipNode,
spatial_node_index: SpatialNodeIndex,
) -> Self {
let region = match node.item.kind {
ClipItemKind::Rectangle { rect, mode } => {
HitTestRegion::Rectangle(rect, mode)
@ -90,50 +94,28 @@ impl HitTestClipNode {
HitTestClipNode {
region,
spatial_node_index,
}
}
}
#[derive(Debug, Copy, Clone, MallocSizeOf, PartialEq, Eq, Hash)]
pub struct HitTestClipChainId(u32);
impl HitTestClipChainId {
pub const NONE: Self = HitTestClipChainId(u32::MAX);
}
/// A hit testing clip chain node is the same as a
/// normal clip chain node, except that the clip
/// node is embedded inside the clip chain, rather
/// than referenced. This means we don't need to
/// copy the complete interned clip data store for
/// hit testing.
#[derive(MallocSizeOf)]
pub struct HitTestClipChainNode {
pub region: HitTestClipNode,
pub spatial_node_index: SpatialNodeIndex,
pub parent_clip_chain_id: HitTestClipChainId,
}
#[derive(Copy, Clone, Debug, MallocSizeOf)]
pub struct HitTestingClipChainIndex(u32);
#[derive(Clone, MallocSizeOf)]
pub struct HitTestingItem {
struct HitTestingItem {
rect: LayoutRect,
clip_rect: LayoutRect,
tag: ItemTag,
is_backface_visible: bool,
#[ignore_malloc_size_of = "simple"]
clip_chain_range: ops::Range<HitTestingClipChainIndex>,
spatial_node_index: SpatialNodeIndex,
#[ignore_malloc_size_of = "Range"]
clip_nodes_range: ops::Range<ClipNodeIndex>,
}
impl HitTestingItem {
pub fn new(
fn new(
tag: ItemTag,
info: &LayoutPrimitiveInfo,
spatial_node_index: SpatialNodeIndex,
clip_chain_range: ops::Range<HitTestingClipChainIndex>,
clip_nodes_range: ops::Range<ClipNodeIndex>,
) -> HitTestingItem {
HitTestingItem {
rect: info.rect,
@ -141,7 +123,7 @@ impl HitTestingItem {
tag,
is_backface_visible: info.flags.contains(PrimitiveFlags::IS_BACKFACE_VISIBLE),
spatial_node_index,
clip_chain_range,
clip_nodes_range,
}
}
}
@ -149,19 +131,22 @@ impl HitTestingItem {
/// Statistics about allocation sizes of current hit tester,
/// used to pre-allocate size of the next hit tester.
pub struct HitTestingSceneStats {
pub clip_chain_roots_count: usize,
pub clip_nodes_count: usize,
pub items_count: usize,
}
impl HitTestingSceneStats {
pub fn empty() -> Self {
HitTestingSceneStats {
clip_chain_roots_count: 0,
clip_nodes_count: 0,
items_count: 0,
}
}
}
#[derive(MallocSizeOf, Debug, Copy, Clone)]
pub struct ClipNodeIndex(u32);
/// Defines the immutable part of a hit tester for a given scene.
/// The hit tester is recreated each time a frame is built, since
/// it relies on the current values of the spatial tree.
@ -170,11 +155,15 @@ impl HitTestingSceneStats {
/// hit tester instances via Arc.
#[derive(MallocSizeOf)]
pub struct HitTestingScene {
/// The list of variable clip chain roots referenced by the items.
pub clip_chain_roots: Vec<HitTestClipChainId>,
/// Packed array of all hit test clip nodes
clip_nodes: Vec<HitTestClipNode>,
/// List of hit testing primitives.
pub items: Vec<HitTestingItem>,
items: Vec<HitTestingItem>,
/// Current stack of clip ids from stacking context
#[ignore_malloc_size_of = "ClipId"]
clip_id_stack: Vec<ClipId>,
}
impl HitTestingScene {
@ -182,39 +171,75 @@ impl HitTestingScene {
/// provided by previous scene stats.
pub fn new(stats: &HitTestingSceneStats) -> Self {
HitTestingScene {
clip_chain_roots: Vec::with_capacity(stats.clip_chain_roots_count),
clip_nodes: Vec::with_capacity(stats.clip_nodes_count),
items: Vec::with_capacity(stats.items_count),
clip_id_stack: Vec::with_capacity(8),
}
}
/// Get stats about the current scene allocation sizes.
pub fn get_stats(&self) -> HitTestingSceneStats {
HitTestingSceneStats {
clip_chain_roots_count: self.clip_chain_roots.len(),
clip_nodes_count: self.clip_nodes.len(),
items_count: self.items.len(),
}
}
/// Add a hit testing primitive.
pub fn add_item(&mut self, item: HitTestingItem) {
pub fn add_item(
&mut self,
tag: ItemTag,
info: &LayoutPrimitiveInfo,
spatial_node_index: SpatialNodeIndex,
clip_id: ClipId,
clip_store: &ClipStore,
) {
let start = ClipNodeIndex(self.clip_nodes.len() as u32);
// Flatten all clips from the stacking context hierarchy
for clip_id in &self.clip_id_stack {
add_clips(
*clip_id,
clip_store,
&mut self.clip_nodes,
);
}
// Add the primitive clip
add_clips(
clip_id,
clip_store,
&mut self.clip_nodes,
);
let end = ClipNodeIndex(self.clip_nodes.len() as u32);
let item = HitTestingItem::new(
tag,
info,
spatial_node_index,
ops::Range {
start,
end,
},
);
self.items.push(item);
}
/// Add a clip chain to the clip chain roots list.
pub fn add_clip_chain(&mut self, clip_chain_id: ClipChainId) {
if clip_chain_id != ClipChainId::INVALID {
self.clip_chain_roots.push(HitTestClipChainId(clip_chain_id.0));
}
/// Push a clip onto the current stack
pub fn push_clip(
&mut self,
clip_id: ClipId,
) {
self.clip_id_stack.push(clip_id);
}
/// Get the slice of clip chain roots for a given hit test primitive.
fn get_clip_chains_for_item(&self, item: &HitTestingItem) -> &[HitTestClipChainId] {
&self.clip_chain_roots[item.clip_chain_range.start.0 as usize .. item.clip_chain_range.end.0 as usize]
}
/// Get the next index of the clip chain roots list.
pub fn next_clip_chain_index(&self) -> HitTestingClipChainIndex {
HitTestingClipChainIndex(self.clip_chain_roots.len() as u32)
/// Pop a clip from the current stack
pub fn pop_clip(
&mut self,
) {
self.clip_id_stack.pop().unwrap();
}
}
@ -246,7 +271,6 @@ pub struct HitTester {
#[ignore_malloc_size_of = "Arc"]
scene: Arc<HitTestingScene>,
spatial_nodes: Vec<HitTestSpatialNode>,
clip_chains: Vec<HitTestClipChainNode>,
pipeline_root_nodes: FastHashMap<PipelineId, SpatialNodeIndex>,
}
@ -255,7 +279,6 @@ impl HitTester {
HitTester {
scene: Arc::new(HitTestingScene::new(&HitTestingSceneStats::empty())),
spatial_nodes: Vec::new(),
clip_chains: Vec::new(),
pipeline_root_nodes: FastHashMap::default(),
}
}
@ -263,31 +286,21 @@ impl HitTester {
pub fn new(
scene: Arc<HitTestingScene>,
spatial_tree: &SpatialTree,
clip_store: &ClipStore,
clip_data_store: &ClipDataStore,
) -> HitTester {
let mut hit_tester = HitTester {
scene,
spatial_nodes: Vec::new(),
clip_chains: Vec::new(),
pipeline_root_nodes: FastHashMap::default(),
};
hit_tester.read_spatial_tree(
spatial_tree,
clip_store,
clip_data_store,
);
hit_tester.read_spatial_tree(spatial_tree);
hit_tester
}
fn read_spatial_tree(
&mut self,
spatial_tree: &SpatialTree,
clip_store: &ClipStore,
clip_data_store: &ClipDataStore,
) {
self.spatial_nodes.clear();
self.clip_chains.clear();
self.spatial_nodes.reserve(spatial_tree.spatial_nodes.len());
for (index, node) in spatial_tree.spatial_nodes.iter().enumerate() {
@ -312,97 +325,11 @@ impl HitTester {
external_scroll_offset: spatial_tree.external_scroll_offset(index),
});
}
// For each clip chain node, extract the clip node from the clip
// data store, and store it inline with the clip chain node.
self.clip_chains.reserve(clip_store.clip_chain_nodes.len());
for node in &clip_store.clip_chain_nodes {
let clip_node = &clip_data_store[node.handle];
self.clip_chains.push(HitTestClipChainNode {
region: HitTestClipNode::new(clip_node),
spatial_node_index: node.spatial_node_index,
parent_clip_chain_id: HitTestClipChainId(node.parent_clip_chain_id.0),
});
}
}
fn is_point_clipped_in_for_clip_chain(
&self,
point: WorldPoint,
clip_chain_id: HitTestClipChainId,
test: &mut HitTest
) -> bool {
if clip_chain_id == HitTestClipChainId::NONE {
return true;
}
if let Some(result) = test.get_from_clip_chain_cache(clip_chain_id) {
return result == ClippedIn::ClippedIn;
}
let descriptor = &self.clip_chains[clip_chain_id.0 as usize];
let parent_clipped_in = self.is_point_clipped_in_for_clip_chain(
point,
descriptor.parent_clip_chain_id,
test,
);
if !parent_clipped_in {
test.set_in_clip_chain_cache(clip_chain_id, ClippedIn::NotClippedIn);
return false;
}
if !self.is_point_clipped_in_for_clip_node(
point,
clip_chain_id,
descriptor.spatial_node_index,
test,
) {
test.set_in_clip_chain_cache(clip_chain_id, ClippedIn::NotClippedIn);
return false;
}
test.set_in_clip_chain_cache(clip_chain_id, ClippedIn::ClippedIn);
true
}
fn is_point_clipped_in_for_clip_node(
&self,
point: WorldPoint,
clip_chain_node_id: HitTestClipChainId,
spatial_node_index: SpatialNodeIndex,
test: &mut HitTest
) -> bool {
if let Some(clipped_in) = test.node_cache.get(&clip_chain_node_id) {
return *clipped_in == ClippedIn::ClippedIn;
}
let node = &self.clip_chains[clip_chain_node_id.0 as usize].region;
let transform = self
.spatial_nodes[spatial_node_index.0 as usize]
.world_content_transform;
let transformed_point = match transform
.inverse()
.and_then(|inverted| inverted.transform_point2d(point))
{
Some(point) => point,
None => {
test.node_cache.insert(clip_chain_node_id, ClippedIn::NotClippedIn);
return false;
}
};
if !node.region.contains(&transformed_point) {
test.node_cache.insert(clip_chain_node_id, ClippedIn::NotClippedIn);
return false;
}
test.node_cache.insert(clip_chain_node_id, ClippedIn::ClippedIn);
true
}
pub fn hit_test(&self, mut test: HitTest) -> HitTestResult {
pub fn hit_test(&self, test: HitTest) -> HitTestResult {
let mut result = HitTestResult::default();
let mut current_spatial_node_index = SpatialNodeIndex::INVALID;
let mut point_in_layer = None;
let mut current_root_spatial_node_index = SpatialNodeIndex::INVALID;
@ -438,12 +365,23 @@ impl HitTester {
continue;
}
// See if any of the clip chain roots for this primitive
// cull out the item.
let clip_chains = self.scene.get_clip_chains_for_item(item);
// See if any of the clips for this primitive cull out the item.
let mut is_valid = true;
for clip_chain_id in clip_chains {
if !self.is_point_clipped_in_for_clip_chain(test.point, *clip_chain_id, &mut test) {
let clip_nodes = &self.scene.clip_nodes[item.clip_nodes_range.start.0 as usize .. item.clip_nodes_range.end.0 as usize];
for clip_node in clip_nodes {
let transform = self
.spatial_nodes[clip_node.spatial_node_index.0 as usize]
.world_content_transform;
let transformed_point = match transform
.inverse()
.and_then(|inverted| inverted.transform_point2d(test.point))
{
Some(point) => point,
None => {
continue;
}
};
if !clip_node.region.contains(&transformed_point) {
is_valid = false;
break;
}
@ -489,18 +427,10 @@ impl HitTester {
}
}
#[derive(Clone, Copy, MallocSizeOf, PartialEq)]
enum ClippedIn {
ClippedIn,
NotClippedIn,
}
#[derive(MallocSizeOf)]
pub struct HitTest {
pipeline_id: Option<PipelineId>,
point: WorldPoint,
node_cache: FastHashMap<HitTestClipChainId, ClippedIn>,
clip_chain_cache: Vec<Option<ClippedIn>>,
}
impl HitTest {
@ -511,25 +441,34 @@ impl HitTest {
HitTest {
pipeline_id,
point,
node_cache: FastHashMap::default(),
clip_chain_cache: Vec::new(),
}
}
fn get_from_clip_chain_cache(&mut self, index: HitTestClipChainId) -> Option<ClippedIn> {
let index = index.0 as usize;
if index >= self.clip_chain_cache.len() {
None
} else {
self.clip_chain_cache[index]
}
}
fn set_in_clip_chain_cache(&mut self, index: HitTestClipChainId, value: ClippedIn) {
let index = index.0 as usize;
if index >= self.clip_chain_cache.len() {
self.clip_chain_cache.resize(index + 1, None);
}
self.clip_chain_cache[index] = Some(value);
}
}
/// Collect clips for a given ClipId, convert and add them to the hit testing
/// scene, if not already present.
fn add_clips(
clip_id: ClipId,
clip_store: &ClipStore,
clip_nodes: &mut Vec<HitTestClipNode>,
) {
let template = &clip_store.templates[&clip_id];
for clip in &template.clips {
let hit_test_clip_node = HitTestClipNode::new(
clip.key.into(),
clip.clip.spatial_node_index,
);
clip_nodes.push(hit_test_clip_node);
}
// The ClipId parenting is terminated when we reach the root ClipId
if clip_id != template.parent {
add_clips(
template.parent,
clip_store,
clip_nodes,
);
}
}

View File

@ -663,7 +663,7 @@ impl Document {
&self.dynamic_properties,
);
let hit_tester = Arc::new(self.scene.create_hit_tester(&self.data_stores.clip));
let hit_tester = Arc::new(self.scene.create_hit_tester());
self.hit_tester = Some(Arc::clone(&hit_tester));
self.shared_hit_tester.update(hit_tester);
self.hit_tester_is_valid = true;

View File

@ -8,7 +8,7 @@ use api::units::*;
use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
use crate::render_api::MemoryReport;
use crate::composite::CompositorKind;
use crate::clip::{ClipStore, ClipDataStore};
use crate::clip::ClipStore;
use crate::spatial_tree::SpatialTree;
use crate::frame_builder::{ChasePrimitive, FrameBuilderConfig};
use crate::hit_test::{HitTester, HitTestingScene, HitTestingSceneStats};
@ -320,15 +320,10 @@ impl BuiltScene {
}
}
pub fn create_hit_tester(
&mut self,
clip_data_store: &ClipDataStore,
) -> HitTester {
pub fn create_hit_tester(&mut self) -> HitTester {
HitTester::new(
Arc::clone(&self.hit_testing_scene),
&self.spatial_tree,
&self.clip_store,
clip_data_store,
)
}
}

View File

@ -19,7 +19,7 @@ use crate::clip::{ClipInternData, ClipNodeKind, ClipInstance, SceneClipInstance}
use crate::spatial_tree::{ROOT_SPATIAL_NODE_INDEX, SpatialTree, SpatialNodeIndex};
use crate::frame_builder::{ChasePrimitive, FrameBuilderConfig};
use crate::glyph_rasterizer::FontInstance;
use crate::hit_test::{HitTestingItem, HitTestingScene};
use crate::hit_test::HitTestingScene;
use crate::intern::Interner;
use crate::internal_types::{FastHashMap, LayoutPrimitiveInfo, Filter};
use crate::picture::{Picture3DContext, PictureCompositeMode, PicturePrimitive, PictureOptions};
@ -42,7 +42,7 @@ use crate::space::SpaceSnapper;
use crate::spatial_node::{StickyFrameInfo, ScrollFrameKind};
use crate::tile_cache::TileCacheBuilder;
use euclid::approxeq::ApproxEq;
use std::{f32, mem, usize, ops};
use std::{f32, mem, usize};
use std::collections::vec_deque::VecDeque;
use std::sync::Arc;
use crate::util::{MaxRect, VecHelper};
@ -931,7 +931,9 @@ impl<'a> SceneBuilder<'a> {
DisplayItem::HitTest(ref info) => {
profile_scope!("hit_test");
let (layout, _, spatial_node_index, clip_chain_id) = self.process_common_properties(
// TODO(gw): We could skip building the clip-chain here completely, as it's not used by
// hit-test items.
let (layout, _, spatial_node_index, _) = self.process_common_properties(
&info.common,
None,
);
@ -942,7 +944,7 @@ impl<'a> SceneBuilder<'a> {
self.add_primitive_to_hit_testing_list(
&layout,
spatial_node_index,
clip_chain_id,
info.common.clip_id,
info.tag,
);
}
@ -1352,37 +1354,16 @@ impl<'a> SceneBuilder<'a> {
&mut self,
info: &LayoutPrimitiveInfo,
spatial_node_index: SpatialNodeIndex,
clip_chain_id: ClipChainId,
clip_id: ClipId,
tag: ItemTag,
) {
// We want to get a range of clip chain roots that apply to this
// hit testing primitive.
// Get the start index for the clip chain root range for this primitive.
let start = self.hit_testing_scene.next_clip_chain_index();
// Add the clip chain root for the primitive itself.
self.hit_testing_scene.add_clip_chain(clip_chain_id);
// Append any clip chain roots from enclosing stacking contexts.
for sc in &self.sc_stack {
self.hit_testing_scene.add_clip_chain(sc.clip_chain_id);
}
// Construct a clip chain roots range to be stored with the item.
let clip_chain_range = ops::Range {
start,
end: self.hit_testing_scene.next_clip_chain_index(),
};
// Create and store the hit testing primitive itself.
let new_item = HitTestingItem::new(
self.hit_testing_scene.add_item(
tag,
info,
spatial_node_index,
clip_chain_range,
clip_id,
&self.clip_store,
);
self.hit_testing_scene.add_item(new_item);
}
/// Add an already created primitive to the draw lists.
@ -1670,6 +1651,9 @@ impl<'a> SceneBuilder<'a> {
} else {
self.clip_store.push_clip_root(None, false);
}
// Push this clip id into the hit-testing scene for child primitives
self.hit_testing_scene.push_clip(clip_id);
}
// If not redundant, create a stacking context to hold primitive clusters
@ -1710,6 +1694,7 @@ impl<'a> SceneBuilder<'a> {
// If the stacking context established a clip root, pop off the stack
if info.pop_clip_root {
self.clip_store.pop_clip_root();
self.hit_testing_scene.pop_clip();
}
// If the stacking context was otherwise redundant, early exit
@ -2043,7 +2028,7 @@ impl<'a> SceneBuilder<'a> {
});
let instance = SceneClipInstance {
key: item.kind,
key: item,
clip: ClipInstance::new(handle, spatial_node_index),
};
@ -2081,7 +2066,7 @@ impl<'a> SceneBuilder<'a> {
});
let instance = SceneClipInstance {
key: item.kind,
key: item,
clip: ClipInstance::new(handle, spatial_node_index),
};
@ -2123,7 +2108,7 @@ impl<'a> SceneBuilder<'a> {
});
let instance = SceneClipInstance {
key: item.kind,
key: item,
clip: ClipInstance::new(handle, spatial_node_index),
};
@ -2171,7 +2156,7 @@ impl<'a> SceneBuilder<'a> {
});
instances.push(
SceneClipInstance {
key: item.kind,
key: item,
clip: ClipInstance::new(handle, spatial_node_index),
},
);
@ -2197,7 +2182,7 @@ impl<'a> SceneBuilder<'a> {
instances.push(
SceneClipInstance {
key: item.kind,
key: item,
clip: ClipInstance::new(handle, spatial_node_index),
},
);