Skip to main content

webrender/tile_cache/
slice_builder.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use api::{BorderRadius, ClipId, ClipMode, ColorF, DebugFlags, PrimitiveFlags, QualitySettings, RasterSpace};
6use api::units::*;
7use crate::clip::{clamped_radius, ClipItemKeyKind, ClipNodeId, ClipTreeBuilder, intersect_rounded_rects};
8use crate::frame_builder::FrameBuilderConfig;
9use crate::internal_types::FastHashMap;
10use crate::picture::{PrimitiveList, PictureCompositeMode, PictureInstance, Picture3DContext, PictureFlags};
11use crate::tile_cache::{SliceId, TileCacheParams};
12use crate::prim_store::{PrimitiveInstance, PrimitiveStore, PictureIndex};
13use crate::scene_building::SliceFlags;
14use crate::scene_builder_thread::Interners;
15use crate::spatial_tree::{SpatialNodeIndex, SceneSpatialTree};
16use crate::util::VecHelper;
17use std::mem;
18
19/*
20 Types and functionality related to picture caching. In future, we'll
21 move more and more of the existing functionality out of picture.rs
22 and into here.
23 */
24
25// If the page would create too many slices (an arbitrary definition where
26// it's assumed the GPU memory + compositing overhead would be too high)
27// then create a single picture cache for the remaining content. This at
28// least means that we can cache small content changes efficiently when
29// scrolling isn't occurring. Scrolling regions will be handled reasonably
30// efficiently by the dirty rect tracking (since it's likely that if the
31// page has so many slices there isn't a single major scroll region).
32const MAX_CACHE_SLICES: usize = 16;
33
34struct SliceDescriptor {
35    prim_list: PrimitiveList,
36    scroll_root: SpatialNodeIndex,
37}
38
39enum SliceKind {
40    Default {
41        secondary_slices: Vec<SliceDescriptor>,
42    },
43    Atomic {
44        prim_list: PrimitiveList,
45    },
46}
47
48impl SliceKind {
49    fn default() -> Self {
50        SliceKind::Default {
51            secondary_slices: Vec::new(),
52        }
53    }
54}
55
56struct PrimarySlice {
57    /// Whether this slice is atomic or has secondary slice(s)
58    kind: SliceKind,
59    /// Optional background color of this slice
60    background_color: Option<ColorF>,
61    /// Optional root clip for the iframe
62    iframe_clip: Option<ClipId>,
63    /// Information about how to draw and composite this slice
64    slice_flags: SliceFlags,
65}
66
67impl PrimarySlice {
68    fn new(
69        slice_flags: SliceFlags,
70        iframe_clip: Option<ClipId>,
71        background_color: Option<ColorF>,
72    ) -> Self {
73        PrimarySlice {
74            kind: SliceKind::default(),
75            background_color,
76            iframe_clip,
77            slice_flags,
78        }
79    }
80
81    fn has_too_many_slices(&self) -> bool {
82        match self.kind {
83            SliceKind::Atomic { .. } => false,
84            SliceKind::Default { ref secondary_slices } => secondary_slices.len() > MAX_CACHE_SLICES,
85        }
86    }
87
88    fn merge(&mut self) {
89        self.slice_flags |= SliceFlags::IS_ATOMIC;
90
91        let old = mem::replace(
92            &mut self.kind,
93            SliceKind::Default { secondary_slices: Vec::new() },
94        );
95
96        self.kind = match old {
97            SliceKind::Default { mut secondary_slices } => {
98                let mut prim_list = PrimitiveList::empty();
99
100                for descriptor in secondary_slices.drain(..) {
101                    prim_list.merge(descriptor.prim_list);
102                }
103
104                SliceKind::Atomic {
105                    prim_list,
106                }
107            }
108            atomic => atomic,
109        }
110    }
111}
112
113/// Used during scene building to construct the list of pending tile caches.
114pub struct TileCacheBuilder {
115    /// List of tile caches that have been created so far (last in the list is currently active).
116    primary_slices: Vec<PrimarySlice>,
117    /// Cache the previous scroll root search for a spatial node, since they are often the same.
118    prev_scroll_root_cache: (SpatialNodeIndex, SpatialNodeIndex),
119    /// Handle to the root reference frame
120    root_spatial_node_index: SpatialNodeIndex,
121    /// Debug flags to provide to our TileCacheInstances.
122    debug_flags: DebugFlags,
123}
124
125/// The output of a tile cache builder, containing all details needed to construct the
126/// tile cache(s) for the next scene, and retain tiles from the previous frame when sent
127/// send to the frame builder.
128pub struct TileCacheConfig {
129    /// Mapping of slice id to the parameters needed to construct this tile cache.
130    pub tile_caches: FastHashMap<SliceId, TileCacheParams>,
131    /// Number of picture cache slices that were created (for profiler)
132    pub picture_cache_slice_count: usize,
133}
134
135impl TileCacheConfig {
136    pub fn new(picture_cache_slice_count: usize) -> Self {
137        TileCacheConfig {
138            tile_caches: FastHashMap::default(),
139            picture_cache_slice_count,
140        }
141    }
142}
143
144impl TileCacheBuilder {
145    /// Construct a new tile cache builder.
146    pub fn new(
147        root_spatial_node_index: SpatialNodeIndex,
148        background_color: Option<ColorF>,
149        debug_flags: DebugFlags,
150    ) -> Self {
151        TileCacheBuilder {
152            primary_slices: vec![PrimarySlice::new(SliceFlags::empty(), None, background_color)],
153            prev_scroll_root_cache: (SpatialNodeIndex::INVALID, SpatialNodeIndex::INVALID),
154            root_spatial_node_index,
155            debug_flags,
156        }
157    }
158
159    pub fn make_current_slice_atomic(&mut self) {
160        self.primary_slices
161            .last_mut()
162            .unwrap()
163            .merge();
164    }
165
166    /// Returns true if the current slice has no primitives added yet
167    pub fn is_current_slice_empty(&self) -> bool {
168        match self.primary_slices.last() {
169            Some(slice) => {
170                match slice.kind {
171                    SliceKind::Default { ref secondary_slices } => {
172                        secondary_slices.is_empty()
173                    }
174                    SliceKind::Atomic { ref prim_list } => {
175                        prim_list.is_empty()
176                    }
177                }
178            }
179            None => {
180                true
181            }
182        }
183    }
184
185    /// Set a barrier that forces a new tile cache next time a prim is added.
186    pub fn add_tile_cache_barrier(
187        &mut self,
188        slice_flags: SliceFlags,
189        iframe_clip: Option<ClipId>,
190    ) {
191        let new_slice = PrimarySlice::new(
192            slice_flags,
193            iframe_clip,
194            None,
195        );
196
197        self.primary_slices.push(new_slice);
198    }
199
200    /// Create a new tile cache for an existing prim_list
201    fn build_tile_cache(
202        &mut self,
203        prim_list: PrimitiveList,
204        spatial_tree: &SceneSpatialTree,
205    ) -> Option<SliceDescriptor> {
206        if prim_list.is_empty() {
207            return None;
208        }
209
210        // Iterate the clusters and determine which is the most commonly occurring
211        // scroll root. This is a reasonable heuristic to decide which spatial node
212        // should be considered the scroll root of this tile cache, in order to
213        // minimize the invalidations that occur due to scrolling. It's often the
214        // case that a blend container will have only a single scroll root.
215        let mut scroll_root_occurrences = FastHashMap::default();
216
217        for cluster in &prim_list.clusters {
218            // If we encounter a cluster which has an unknown spatial node,
219            // we don't include that in the set of spatial nodes that we
220            // are trying to find scroll roots for. Later on, in finalize_picture,
221            // the cluster spatial node will be updated to the selected scroll root.
222            if cluster.spatial_node_index == SpatialNodeIndex::UNKNOWN {
223                continue;
224            }
225
226            let scroll_root = find_scroll_root(
227                cluster.spatial_node_index,
228                &mut self.prev_scroll_root_cache,
229                spatial_tree,
230                true,
231            );
232
233            *scroll_root_occurrences.entry(scroll_root).or_insert(0) += 1;
234        }
235
236        // We can't just select the most commonly occurring scroll root in this
237        // primitive list. If that is a nested scroll root, there may be
238        // primitives in the list that are outside that scroll root, which
239        // can cause panics when calculating relative transforms. To ensure
240        // this doesn't happen, only retain scroll root candidates that are
241        // also ancestors of every other scroll root candidate.
242        let scroll_roots: Vec<SpatialNodeIndex> = scroll_root_occurrences
243            .keys()
244            .cloned()
245            .collect();
246
247        scroll_root_occurrences.retain(|parent_spatial_node_index, _| {
248            scroll_roots.iter().all(|child_spatial_node_index| {
249                parent_spatial_node_index == child_spatial_node_index ||
250                spatial_tree.is_ancestor(
251                    *parent_spatial_node_index,
252                    *child_spatial_node_index,
253                )
254            })
255        });
256
257        // Select the scroll root by finding the most commonly occurring one
258        let scroll_root = scroll_root_occurrences
259            .iter()
260            .max_by_key(|entry | entry.1)
261            .map(|(spatial_node_index, _)| *spatial_node_index)
262            .unwrap_or(self.root_spatial_node_index);
263
264        Some(SliceDescriptor {
265            scroll_root,
266            prim_list,
267        })
268    }
269
270    /// Add a primitive, either to the current tile cache, or a new one, depending on various conditions.
271    pub fn add_prim(
272        &mut self,
273        prim_instance: PrimitiveInstance,
274        prim_rect: LayoutRect,
275        spatial_node_index: SpatialNodeIndex,
276        prim_flags: PrimitiveFlags,
277        spatial_tree: &SceneSpatialTree,
278        quality_settings: &QualitySettings,
279        prim_instances: &mut Vec<PrimitiveInstance>,
280        clip_tree_builder: &ClipTreeBuilder,
281    ) {
282        let primary_slice = self.primary_slices.last_mut().unwrap();
283
284        match primary_slice.kind {
285            SliceKind::Atomic { ref mut prim_list } => {
286                prim_list.add_prim(
287                    prim_instance,
288                    prim_rect,
289                    spatial_node_index,
290                    prim_flags,
291                    prim_instances,
292                    clip_tree_builder,
293                );
294            }
295            SliceKind::Default { ref mut secondary_slices } => {
296                assert_ne!(spatial_node_index, SpatialNodeIndex::UNKNOWN);
297
298                // Check if we want to create a new slice based on the current / next scroll root
299                let scroll_root = find_scroll_root(
300                    spatial_node_index,
301                    &mut self.prev_scroll_root_cache,
302                    spatial_tree,
303                    // Allow sticky frames as scroll roots, unless our quality settings prefer
304                    // subpixel AA over performance.
305                    !quality_settings.force_subpixel_aa_where_possible,
306                );
307
308                let current_scroll_root = secondary_slices
309                    .last()
310                    .map(|p| p.scroll_root);
311
312                let mut want_new_tile_cache = secondary_slices.is_empty();
313
314                if let Some(current_scroll_root) = current_scroll_root {
315                    want_new_tile_cache |= match (current_scroll_root, scroll_root) {
316                        (_, _) if current_scroll_root == self.root_spatial_node_index && scroll_root == self.root_spatial_node_index => {
317                            // Both current slice and this cluster are fixed position, no need to cut
318                            false
319                        }
320                        (_, _) if current_scroll_root == self.root_spatial_node_index => {
321                            // A real scroll root is being established, so create a cache slice
322                            true
323                        }
324                        (_, _) if scroll_root == self.root_spatial_node_index => {
325                            // If quality settings force subpixel AA over performance, skip creating
326                            // a slice for the fixed position element(s) here.
327                            if quality_settings.force_subpixel_aa_where_possible {
328                                false
329                            } else {
330                                // A fixed position slice is encountered within a scroll root. Only create
331                                // a slice in this case if all the clips referenced by this cluster are also
332                                // fixed position. There's no real point in creating slices for these cases,
333                                // since we'll have to rasterize them as the scrolling clip moves anyway. It
334                                // also allows us to retain subpixel AA in these cases. For these types of
335                                // slices, the intra-slice dirty rect handling typically works quite well
336                                // (a common case is parallax scrolling effects).
337                                let mut create_slice = true;
338
339                                let leaf = clip_tree_builder.get_leaf(prim_instance.clip_leaf_id);
340                                let mut current_node_id = leaf.node_id;
341
342                                while current_node_id != ClipNodeId::NONE {
343                                    let node = clip_tree_builder.get_node(current_node_id);
344
345                                    let spatial_root = find_scroll_root(
346                                        node.spatial_node_index,
347                                        &mut self.prev_scroll_root_cache,
348                                        spatial_tree,
349                                        true,
350                                    );
351
352                                    if spatial_root != self.root_spatial_node_index {
353                                        create_slice = false;
354                                        break;
355                                    }
356
357                                    current_node_id = node.parent;
358                                }
359
360                                create_slice
361                            }
362                        }
363                        (curr_scroll_root, scroll_root) => {
364                            // Two scrolling roots - only need a new slice if they differ
365                            curr_scroll_root != scroll_root
366                        }
367                    };
368                }
369
370                if want_new_tile_cache {
371                    secondary_slices.push(SliceDescriptor {
372                        prim_list: PrimitiveList::empty(),
373                        scroll_root,
374                    });
375                }
376
377                secondary_slices
378                    .last_mut()
379                    .unwrap()
380                    .prim_list
381                    .add_prim(
382                        prim_instance,
383                        prim_rect,
384                        spatial_node_index,
385                        prim_flags,
386                        prim_instances,
387                        clip_tree_builder,
388                    );
389            }
390        }
391    }
392
393    /// Consume this object and build the list of tile cache primitives
394    pub fn build(
395        mut self,
396        config: &FrameBuilderConfig,
397        prim_store: &mut PrimitiveStore,
398        spatial_tree: &SceneSpatialTree,
399        prim_instances: &[PrimitiveInstance],
400        clip_tree_builder: &mut ClipTreeBuilder,
401        interners: &Interners,
402    ) -> (TileCacheConfig, Vec<PictureIndex>) {
403        let mut result = TileCacheConfig::new(self.primary_slices.len());
404        let mut tile_cache_pictures = Vec::new();
405        let primary_slices = std::mem::replace(&mut self.primary_slices, Vec::new());
406
407        // TODO: At the moment, culling, clipping and invalidation are always
408        // done in the root coordinate space. The plan is to move to doing it
409        // (always or mostly) in raster space.
410        let visibility_node = spatial_tree.root_reference_frame_index();
411
412        for mut primary_slice in primary_slices {
413
414            if primary_slice.has_too_many_slices() {
415                primary_slice.merge();
416            }
417
418            match primary_slice.kind {
419                SliceKind::Atomic { prim_list } => {
420                    if let Some(descriptor) = self.build_tile_cache(
421                        prim_list,
422                        spatial_tree,
423                    ) {
424                        create_tile_cache(
425                            self.debug_flags,
426                            primary_slice.slice_flags,
427                            descriptor.scroll_root,
428                            visibility_node,
429                            primary_slice.iframe_clip,
430                            descriptor.prim_list,
431                            primary_slice.background_color,
432                            prim_store,
433                            prim_instances,
434                            config,
435                            &mut result.tile_caches,
436                            &mut tile_cache_pictures,
437                            clip_tree_builder,
438                            interners,
439                            spatial_tree,
440                        );
441                    }
442                }
443                SliceKind::Default { secondary_slices } => {
444                    for descriptor in secondary_slices {
445                        create_tile_cache(
446                            self.debug_flags,
447                            primary_slice.slice_flags,
448                            descriptor.scroll_root,
449                            visibility_node,
450                            primary_slice.iframe_clip,
451                            descriptor.prim_list,
452                            primary_slice.background_color,
453                            prim_store,
454                            prim_instances,
455                            config,
456                            &mut result.tile_caches,
457                            &mut tile_cache_pictures,
458                            clip_tree_builder,
459                            interners,
460                            spatial_tree,
461                        );
462                    }
463                }
464            }
465        }
466
467        (result, tile_cache_pictures)
468    }
469}
470
471/// Find the scroll root for a given spatial node
472fn find_scroll_root(
473    spatial_node_index: SpatialNodeIndex,
474    prev_scroll_root_cache: &mut (SpatialNodeIndex, SpatialNodeIndex),
475    spatial_tree: &SceneSpatialTree,
476    allow_sticky_frames: bool,
477) -> SpatialNodeIndex {
478    if prev_scroll_root_cache.0 == spatial_node_index {
479        return prev_scroll_root_cache.1;
480    }
481
482    let scroll_root = spatial_tree.find_scroll_root(spatial_node_index, allow_sticky_frames);
483    *prev_scroll_root_cache = (spatial_node_index, scroll_root);
484
485    scroll_root
486}
487
488/// Given a PrimitiveList and scroll root, construct a tile cache primitive instance
489/// that wraps the primitive list.
490fn create_tile_cache(
491    debug_flags: DebugFlags,
492    slice_flags: SliceFlags,
493    scroll_root: SpatialNodeIndex,
494    visibility_node: SpatialNodeIndex,
495    iframe_clip: Option<ClipId>,
496    prim_list: PrimitiveList,
497    background_color: Option<ColorF>,
498    prim_store: &mut PrimitiveStore,
499    prim_instances: &[PrimitiveInstance],
500    frame_builder_config: &FrameBuilderConfig,
501    tile_caches: &mut FastHashMap<SliceId, TileCacheParams>,
502    tile_cache_pictures: &mut Vec<PictureIndex>,
503    clip_tree_builder: &mut ClipTreeBuilder,
504    interners: &Interners,
505    spatial_tree: &SceneSpatialTree,
506) {
507    // Accumulate any clip instances from the iframe_clip into the shared clips
508    // that will be applied by this tile cache during compositing.
509    let mut additional_clips = Vec::new();
510
511    if let Some(clip_id) = iframe_clip {
512        additional_clips.push(clip_id);
513    }
514
515    // Find the best shared clip node that we can apply while compositing tiles,
516    // rather than applying to each item individually.
517
518    // Step 1: Walk the primitive list, and find the LCA of the clip-tree that
519    //         matches all primitives. This gives us our "best-case" shared
520    //         clip node that moves as many clips as possible to compositing.
521    let mut shared_clip_node_id = None;
522
523    for cluster in &prim_list.clusters {
524        for prim_instance in &prim_instances[cluster.prim_range()] {
525            let leaf = clip_tree_builder.get_leaf(prim_instance.clip_leaf_id);
526
527            // TODO(gw): Need to cache last clip-node id here?
528            shared_clip_node_id = match shared_clip_node_id {
529                Some(current) => {
530                    Some(clip_tree_builder.find_lowest_common_ancestor(current, leaf.node_id))
531                }
532                None => {
533                    Some(leaf.node_id)
534                }
535            }
536        }
537    }
538
539    // Step 2: Now we need to walk up the shared clip node hierarchy, and remove clips
540    //         that we can't handle during compositing, such as:
541    //         (a) Non axis-aligned clips
542    //         (b) Box-shadow or image-mask clips
543    //         (c) More than one rounded-rect clip (unless they can be intersected
544    //             into a single rounded-rect clip).
545    let mut shared_clip_node_id = shared_clip_node_id.unwrap_or(ClipNodeId::NONE);
546    let mut current_node_id = shared_clip_node_id;
547    let mut rounded_rect_count = 0;
548
549    // Track accumulated rounded rect info so we can attempt to combine
550    // multiple rounded rects into a single compositing clip.
551    let mut accumulated_rounded_rect: Option<(LayoutRect, BorderRadius)> = None;
552
553    // SNAPTODO: Scene-build slice partitioning reads `node.unsnapped_clip_rect`
554    // (and feeds it through `clamped_radius` / `intersect_rounded_rects` /
555    // `accumulated_rounded_rect`) to decide whether clips can be promoted
556    // into a shared compositing clip. Snapping isn't available at scene-build
557    // time, so audit whether pixel-aligned vs. sub-pixel clip rects can flip
558    // the can_use_fast_path / intersect decisions once per-frame snapping
559    // is real.
560    // Walk up the hierarchy to the root of the clip-tree
561    while current_node_id != ClipNodeId::NONE {
562        let node = clip_tree_builder.get_node(current_node_id);
563        let clip_node_data = &interners.clip[node.handle];
564
565        // Check if this clip is in the root coord system (i.e. is axis-aligned with tile-cache)
566        let is_rcs = spatial_tree.is_root_coord_system(node.spatial_node_index);
567
568        let node_valid = if is_rcs {
569            match clip_node_data.key.kind {
570                ClipItemKeyKind::ImageMask(..) |
571                ClipItemKeyKind::Rectangle(ClipMode::ClipOut) |
572                ClipItemKeyKind::RoundedRectangle(_, ClipMode::ClipOut) => {
573                    // Has an image-mask or clip-out clip, we can't handle this as a shared clip
574                    false
575                }
576                ClipItemKeyKind::RoundedRectangle(radius, ClipMode::Clip) => {
577                    // The shader and CoreAnimation rely on certain constraints such
578                    // as uniform radii to be able to apply the clip during compositing.
579                    let br = clamped_radius(&BorderRadius::from(radius), node.unsnapped_clip_rect.size());
580                    if br.can_use_fast_path_in(&node.unsnapped_clip_rect) {
581                        rounded_rect_count += 1;
582
583                        if accumulated_rounded_rect.is_none() {
584                            accumulated_rounded_rect = Some((node.unsnapped_clip_rect, br));
585                        }
586
587                        true
588                    } else {
589                        false
590                    }
591                }
592                ClipItemKeyKind::Rectangle(ClipMode::Clip) => {
593                    // We can apply multiple (via combining) axis-aligned rectangle
594                    // clips to the shared compositing clip.
595                    true
596                }
597            }
598        } else {
599            // Has a complex transform, we can't handle this as a shared clip
600            false
601        };
602
603        if node_valid {
604            // This node was found to be one we can apply during compositing.
605            if rounded_rect_count > 1 {
606                // Check if the two rounded rects can be combined. Both clips are in
607                // the root coordinate system (is_rcs). The actual intersection with
608                // correct spatial transforms is performed in pre_update; here we just
609                // verify the clips are geometrically compatible in their local spaces
610                // to decide whether to keep both in the shared clip chain.
611                let can_combine = match (accumulated_rounded_rect, clip_node_data.key.kind) {
612                    (
613                        Some((acc_rect, acc_radius)),
614                        ClipItemKeyKind::RoundedRectangle(radius, ClipMode::Clip),
615                    ) => {
616                        let radius = clamped_radius(&BorderRadius::from(radius), node.unsnapped_clip_rect.size());
617                        intersect_rounded_rects(
618                            acc_rect, acc_radius,
619                            node.unsnapped_clip_rect, radius,
620                        )
621                    }
622                    _ => None,
623                };
624
625                if let Some((combined_rect, combined_radius)) = can_combine {
626                    // Successfully combined — keep both clips in the shared
627                    // set and update the accumulated state for potential
628                    // further combinations.
629                    rounded_rect_count = 1;
630                    accumulated_rounded_rect = Some((combined_rect, combined_radius));
631                } else {
632                    // Can't combine, drop children and keep only this clip.
633                    shared_clip_node_id = current_node_id;
634                    rounded_rect_count = 1;
635                    if let ClipItemKeyKind::RoundedRectangle(radius, ClipMode::Clip) = clip_node_data.key.kind {
636                        let radius = clamped_radius(&BorderRadius::from(radius), node.unsnapped_clip_rect.size());
637                        accumulated_rounded_rect = Some((node.unsnapped_clip_rect, radius));
638                    }
639                }
640            }
641        } else {
642            // Node was invalid, due to transform / clip type. Drop this clip
643            // and reset the rounded rect count to 0, since we drop children
644            // from here too.
645            shared_clip_node_id = node.parent;
646            rounded_rect_count = 0;
647            accumulated_rounded_rect = None;
648        }
649
650        current_node_id = node.parent;
651    }
652
653    let shared_clip_leaf_id = Some(clip_tree_builder.build_for_tile_cache(
654        shared_clip_node_id,
655        &additional_clips,
656    ));
657
658    // Build a clip-chain for the tile cache, that contains any of the shared clips
659    // we will apply when drawing the tiles. In all cases provided by Gecko, these
660    // are rectangle clips with a scale/offset transform only, and get handled as
661    // a simple local clip rect in the vertex shader. However, this should in theory
662    // also work with any complex clips, such as rounded rects and image masks, by
663    // producing a clip mask that is applied to the picture cache tiles.
664
665    let slice = tile_cache_pictures.len();
666
667    let background_color = if slice == 0 {
668        background_color
669    } else {
670        None
671    };
672
673    let slice_id = SliceId::new(slice);
674
675    // Store some information about the picture cache slice. This is used when we swap the
676    // new scene into the frame builder to either reuse existing slices, or create new ones.
677    tile_caches.insert(slice_id, TileCacheParams {
678        debug_flags,
679        slice,
680        slice_flags,
681        spatial_node_index: scroll_root,
682        visibility_node_index: visibility_node,
683        background_color,
684        shared_clip_node_id,
685        shared_clip_leaf_id,
686        virtual_surface_size: frame_builder_config.compositor_kind.get_virtual_surface_size(),
687        image_surface_count: prim_list.image_surface_count,
688        yuv_image_surface_count: prim_list.yuv_image_surface_count,
689    });
690
691    let pic_index = prim_store.pictures.alloc().init(PictureInstance::new_image(
692        Some(PictureCompositeMode::TileCache { slice_id }),
693        Picture3DContext::Out,
694        PrimitiveFlags::IS_BACKFACE_VISIBLE,
695        prim_list,
696        scroll_root,
697        RasterSpace::Screen,
698        PictureFlags::empty(),
699        None,
700    ));
701
702    tile_cache_pictures.push(PictureIndex(pic_index));
703}
704
705/// Debug information about a set of picture cache slices, exposed via RenderResults
706#[derive(Debug)]
707#[cfg_attr(feature = "capture", derive(Serialize))]
708#[cfg_attr(feature = "replay", derive(Deserialize))]
709pub struct PictureCacheDebugInfo {
710    pub slices: FastHashMap<usize, SliceDebugInfo>,
711}
712
713impl PictureCacheDebugInfo {
714    pub fn new() -> Self {
715        PictureCacheDebugInfo {
716            slices: FastHashMap::default(),
717        }
718    }
719
720    /// Convenience method to retrieve a given slice. Deliberately panics
721    /// if the slice isn't present.
722    pub fn slice(&self, slice: usize) -> &SliceDebugInfo {
723        &self.slices[&slice]
724    }
725}
726
727impl Default for PictureCacheDebugInfo {
728    fn default() -> PictureCacheDebugInfo {
729        PictureCacheDebugInfo::new()
730    }
731}
732
733/// Debug information about the compositor clip applied to a picture cache slice
734#[derive(Debug, Clone, PartialEq)]
735#[cfg_attr(feature = "capture", derive(Serialize))]
736#[cfg_attr(feature = "replay", derive(Deserialize))]
737pub struct CompositorClipDebugInfo {
738    pub rect: DeviceRect,
739    pub radius: BorderRadius,
740}
741
742/// Debug information about a set of picture cache tiles, exposed via RenderResults
743#[derive(Debug)]
744#[cfg_attr(feature = "capture", derive(Serialize))]
745#[cfg_attr(feature = "replay", derive(Deserialize))]
746pub struct SliceDebugInfo {
747    pub tiles: FastHashMap<TileOffset, TileDebugInfo>,
748    pub compositor_clip: Option<CompositorClipDebugInfo>,
749}
750
751impl SliceDebugInfo {
752    pub fn new() -> Self {
753        SliceDebugInfo {
754            tiles: FastHashMap::default(),
755            compositor_clip: None,
756        }
757    }
758
759    /// Convenience method to retrieve a given tile. Deliberately panics
760    /// if the tile isn't present.
761    pub fn tile(&self, x: i32, y: i32) -> &TileDebugInfo {
762        &self.tiles[&TileOffset::new(x, y)]
763    }
764}
765
766/// Debug information about a tile that was dirty and was rasterized
767#[derive(Debug, PartialEq)]
768#[cfg_attr(feature = "capture", derive(Serialize))]
769#[cfg_attr(feature = "replay", derive(Deserialize))]
770pub struct DirtyTileDebugInfo {
771    pub local_valid_rect: PictureRect,
772    pub local_dirty_rect: PictureRect,
773}
774
775/// Debug information about the state of a tile
776#[derive(Debug, PartialEq)]
777#[cfg_attr(feature = "capture", derive(Serialize))]
778#[cfg_attr(feature = "replay", derive(Deserialize))]
779pub enum TileDebugInfo {
780    /// Tile was occluded by a tile in front of it
781    Occluded,
782    /// Tile was culled (not visible in current display port)
783    Culled,
784    /// Tile was valid (no rasterization was done) and visible
785    Valid,
786    /// Tile was dirty, and was updated
787    Dirty(DirtyTileDebugInfo),
788}
789
790impl TileDebugInfo {
791    pub fn is_occluded(&self) -> bool {
792        match self {
793            TileDebugInfo::Occluded => true,
794            TileDebugInfo::Culled |
795            TileDebugInfo::Valid |
796            TileDebugInfo::Dirty(..) => false,
797        }
798    }
799
800    pub fn is_valid(&self) -> bool {
801        match self {
802            TileDebugInfo::Valid => true,
803            TileDebugInfo::Culled |
804            TileDebugInfo::Occluded |
805            TileDebugInfo::Dirty(..) => false,
806        }
807    }
808
809    pub fn is_culled(&self) -> bool {
810        match self {
811            TileDebugInfo::Culled => true,
812            TileDebugInfo::Valid |
813            TileDebugInfo::Occluded |
814            TileDebugInfo::Dirty(..) => false,
815        }
816    }
817
818    pub fn as_dirty(&self) -> &DirtyTileDebugInfo {
819        match self {
820            TileDebugInfo::Occluded |
821            TileDebugInfo::Culled |
822            TileDebugInfo::Valid => {
823                panic!("not a dirty tile!");
824            }
825            TileDebugInfo::Dirty(ref info) => {
826                info
827            }
828        }
829    }
830}