Skip to main content

webrender/
picture.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
5//! A picture represents a dynamically rendered image.
6//!
7//! # Overview
8//!
9//! Pictures consists of:
10//!
11//! - A number of primitives that are drawn onto the picture.
12//! - A composite operation describing how to composite this
13//!   picture into its parent.
14//! - A configuration describing how to draw the primitives on
15//!   this picture (e.g. in screen space or local space).
16//!
17//! The tree of pictures are generated during scene building.
18//!
19//! Depending on their composite operations pictures can be rendered into
20//! intermediate targets or folded into their parent picture.
21//!
22//! ## Picture caching
23//!
24//! Pictures can be cached to reduce the amount of rasterization happening per
25//! frame.
26//!
27//! When picture caching is enabled, the scene is cut into a small number of slices,
28//! typically:
29//!
30//! - content slice
31//! - UI slice
32//! - background UI slice which is hidden by the other two slices most of the time.
33//!
34//! Each of these slice is made up of fixed-size large tiles of 2048x512 pixels
35//! (or 128x128 for the UI slice).
36//!
37//! Tiles can be either cached rasterized content into a texture or "clear tiles"
38//! that contain only a solid color rectangle rendered directly during the composite
39//! pass.
40//!
41//! ## Invalidation
42//!
43//! Each tile keeps track of the elements that affect it, which can be:
44//!
45//! - primitives
46//! - clips
47//! - image keys
48//! - opacity bindings
49//! - transforms
50//!
51//! These dependency lists are built each frame and compared to the previous frame to
52//! see if the tile changed.
53//!
54//! The tile's primitive dependency information is organized in a quadtree, each node
55//! storing an index buffer of tile primitive dependencies.
56//!
57//! The union of the invalidated leaves of each quadtree produces a per-tile dirty rect
58//! which defines the scissor rect used when replaying the tile's drawing commands and
59//! can be used for partial present.
60//!
61//! ## Display List shape
62//!
63//! WR will first look for an iframe item in the root stacking context to apply
64//! picture caching to. If that's not found, it will apply to the entire root
65//! stacking context of the display list. Apart from that, the format of the
66//! display list is not important to picture caching. Each time a new scroll root
67//! is encountered, a new picture cache slice will be created. If the display
68//! list contains more than some arbitrary number of slices (currently 8), the
69//! content will all be squashed into a single slice, in order to save GPU memory
70//! and compositing performance.
71//!
72//! ## Compositor Surfaces
73//!
74//! Sometimes, a primitive would prefer to exist as a native compositor surface.
75//! This allows a large and/or regularly changing primitive (such as a video, or
76//! webgl canvas) to be updated each frame without invalidating the content of
77//! tiles, and can provide a significant performance win and battery saving.
78//!
79//! Since drawing a primitive as a compositor surface alters the ordering of
80//! primitives in a tile, we use 'overlay tiles' to ensure correctness. If a
81//! tile has a compositor surface, _and_ that tile has primitives that overlap
82//! the compositor surface rect, the tile switches to be drawn in alpha mode.
83//!
84//! We rely on only promoting compositor surfaces that are opaque primitives.
85//! With this assumption, the tile(s) that intersect the compositor surface get
86//! a 'cutout' in the rectangle where the compositor surface exists (not the
87//! entire tile), allowing that tile to be drawn as an alpha tile after the
88//! compositor surface.
89//!
90//! Tiles are only drawn in overlay mode if there is content that exists on top
91//! of the compositor surface. Otherwise, we can draw the tiles in the normal fast
92//! path before the compositor surface is drawn. Use of the per-tile valid and
93//! dirty rects ensure that we do a minimal amount of per-pixel work here to
94//! blend the overlay tile (this is not always optimal right now, but will be
95//! improved as a follow up).
96
97use api::RasterSpace;
98use api::{DebugFlags, ColorF, PrimitiveFlags, SnapshotInfo};
99use api::units::*;
100use crate::command_buffer::PrimitiveCommand;
101use crate::renderer::GpuBufferBuilderF;
102use crate::box_shadow::BLUR_SAMPLE_SCALE;
103use crate::clip::{ClipNodeId, ClipTreeBuilder};
104use crate::spatial_tree::{SpatialTree, CoordinateSpaceMapping, SpatialNodeIndex, VisibleFace};
105use crate::composite::{tile_kind, CompositeTileSurface, CompositorKind, NativeTileId};
106use crate::composite::{CompositeTileDescriptor, CompositeTile};
107use crate::debug_colors;
108use euclid::{vec3, Scale, Vector2D, Box2D};
109use crate::internal_types::{FastHashMap, PlaneSplitter, Filter};
110use crate::internal_types::{PlaneSplitterIndex, PlaneSplitAnchor, TextureSource};
111use crate::frame_builder::{FrameBuildingContext, FrameBuildingState, PictureState, PictureContext};
112use plane_split::{Clipper, Polygon};
113use crate::prim_store::{PictureIndex, PrimitiveInstance, PrimitiveKind};
114use crate::prim_store::PrimitiveScratchBuffer;
115use crate::prim_store::storage;
116use crate::print_tree::PrintTreePrinter;
117use crate::render_backend::DataStores;
118use crate::render_task_graph::RenderTaskId;
119use crate::render_task::{RenderTask, RenderTaskLocation};
120use crate::render_task::{StaticRenderTaskSurface, RenderTaskKind};
121use crate::renderer::GpuBufferAddress;
122use crate::resource_cache::ResourceCache;
123use crate::space::SpaceMapper;
124use crate::scene::SceneProperties;
125use crate::spatial_tree::CoordinateSystemId;
126use crate::surface::{SurfaceDescriptor, SurfaceTileDescriptor, get_surface_rects};
127pub use crate::surface::{SurfaceIndex, SurfaceInfo, SubpixelMode};
128pub use crate::surface::calculate_screen_uv;
129use smallvec::SmallVec;
130use std::{mem, u8, u32};
131use std::ops::Range;
132use crate::picture_textures::PictureCacheTextureHandle;
133use crate::util::{MaxRect, Recycler, ScaleOffset};
134use crate::tile_cache::{SliceDebugInfo, TileDebugInfo, DirtyTileDebugInfo, CompositorClipDebugInfo};
135use crate::tile_cache::{SliceId, TileCacheInstance, TileSurface, NativeSurface};
136use crate::tile_cache::{BackdropKind, BackdropSurface};
137use crate::tile_cache::{TileKey, SubSliceIndex};
138use crate::invalidation::InvalidationReason;
139use crate::tile_cache::MAX_SURFACE_SIZE;
140
141pub use crate::picture_composite_mode::{PictureCompositeMode, prepare_composite_mode};
142
143// Maximum blur radius for blur filter (different than box-shadow blur).
144// Taken from FilterNodeSoftware.cpp in Gecko.
145pub(crate) const MAX_BLUR_RADIUS: f32 = 100.;
146
147/// Maximum size of a compositor surface.
148pub const MAX_COMPOSITOR_SURFACES_SIZE: f32 = 8192.0;
149
150pub fn clamp(value: i32, low: i32, high: i32) -> i32 {
151    value.max(low).min(high)
152}
153
154pub fn clampf(value: f32, low: f32, high: f32) -> f32 {
155    value.max(low).min(high)
156}
157
158/// A descriptor for the kind of texture that a picture cache tile will
159/// be drawn into.
160#[derive(Debug)]
161pub enum SurfaceTextureDescriptor {
162    /// When using the WR compositor, the tile is drawn into an entry
163    /// in the WR texture cache.
164    TextureCache {
165        handle: Option<PictureCacheTextureHandle>,
166    },
167    /// When using an OS compositor, the tile is drawn into a native
168    /// surface identified by arbitrary id.
169    Native {
170        /// The arbitrary id of this tile.
171        id: Option<NativeTileId>,
172    },
173}
174
175/// This is the same as a `SurfaceTextureDescriptor` but has been resolved
176/// into a texture cache handle (if appropriate) that can be used by the
177/// batching and compositing code in the renderer.
178#[derive(Clone, Debug, Eq, PartialEq, Hash)]
179#[cfg_attr(feature = "capture", derive(Serialize))]
180#[cfg_attr(feature = "replay", derive(Deserialize))]
181pub enum ResolvedSurfaceTexture {
182    TextureCache {
183        /// The texture ID to draw to.
184        texture: TextureSource,
185    },
186    Native {
187        /// The arbitrary id of this tile.
188        id: NativeTileId,
189        /// The size of the tile in device pixels.
190        size: DeviceIntSize,
191    }
192}
193
194impl SurfaceTextureDescriptor {
195    /// Create a resolved surface texture for this descriptor
196    pub fn resolve(
197        &self,
198        resource_cache: &ResourceCache,
199        size: DeviceIntSize,
200    ) -> ResolvedSurfaceTexture {
201        match self {
202            SurfaceTextureDescriptor::TextureCache { handle } => {
203                let texture = resource_cache
204                    .picture_textures
205                    .get_texture_source(handle.as_ref().unwrap());
206
207                ResolvedSurfaceTexture::TextureCache { texture }
208            }
209            SurfaceTextureDescriptor::Native { id } => {
210                ResolvedSurfaceTexture::Native {
211                    id: id.expect("bug: native surface not allocated"),
212                    size,
213                }
214            }
215        }
216    }
217}
218
219pub struct PictureScratchBuffer {
220    surface_stack: Vec<SurfaceIndex>,
221}
222
223impl Default for PictureScratchBuffer {
224    fn default() -> Self {
225        PictureScratchBuffer {
226            surface_stack: Vec::new(),
227        }
228    }
229}
230
231impl PictureScratchBuffer {
232    pub fn begin_frame(&mut self) {
233        self.surface_stack.clear();
234    }
235
236    pub fn recycle(&mut self, recycler: &mut Recycler) {
237        recycler.recycle_vec(&mut self.surface_stack);
238    }
239}
240
241#[derive(Debug)]
242#[cfg_attr(feature = "capture", derive(Serialize))]
243pub struct RasterConfig {
244    /// How this picture should be composited into
245    /// the parent surface.
246    // TODO(gw): We should remove this and just use what is in PictureInstance
247    pub composite_mode: PictureCompositeMode,
248    /// Index to the surface descriptor for this
249    /// picture.
250    pub surface_index: SurfaceIndex,
251}
252
253bitflags! {
254    /// A set of flags describing why a picture may need a backing surface.
255    #[cfg_attr(feature = "capture", derive(Serialize))]
256    #[derive(Debug, Copy, PartialEq, Eq, Clone, PartialOrd, Ord, Hash)]
257    pub struct BlitReason: u32 {
258        /// Mix-blend-mode on a child that requires isolation.
259        const BLEND_MODE = 1 << 0;
260        /// Clip node that _might_ require a surface.
261        const CLIP = 1 << 1;
262        /// Preserve-3D requires a surface for plane-splitting.
263        const PRESERVE3D = 1 << 2;
264        /// A forced isolation request from gecko.
265        const FORCED_ISOLATION = 1 << 3;
266        /// We may need to render the picture into an image and cache it.
267        const SNAPSHOT = 1 << 4;
268    }
269}
270
271/// Enum value describing the place of a picture in a 3D context.
272#[derive(Clone, Debug)]
273#[cfg_attr(feature = "capture", derive(Serialize))]
274pub enum Picture3DContext<C> {
275    /// The picture is not a part of 3D context sub-hierarchy.
276    Out,
277    /// The picture is a part of 3D context.
278    In {
279        /// Additional data per child for the case of this a root of 3D hierarchy.
280        root_data: Option<Vec<C>>,
281        /// The spatial node index of an "ancestor" element, i.e. one
282        /// that establishes the transformed element's containing block.
283        ///
284        /// See CSS spec draft for more details:
285        /// https://drafts.csswg.org/css-transforms-2/#accumulated-3d-transformation-matrix-computation
286        ancestor_index: SpatialNodeIndex,
287        /// Index in the built scene's array of plane splitters.
288        plane_splitter_index: PlaneSplitterIndex,
289    },
290}
291
292/// Information about a preserve-3D hierarchy child that has been plane-split
293/// and ordered according to the view direction.
294#[derive(Clone, Debug)]
295#[cfg_attr(feature = "capture", derive(Serialize))]
296pub struct OrderedPictureChild {
297    pub anchor: PlaneSplitAnchor,
298    pub gpu_address: GpuBufferAddress,
299}
300
301bitflags! {
302    /// A set of flags describing why a picture may need a backing surface.
303    #[cfg_attr(feature = "capture", derive(Serialize))]
304    #[derive(Debug, Copy, PartialEq, Eq, Clone, PartialOrd, Ord, Hash)]
305    pub struct ClusterFlags: u32 {
306        /// Whether this cluster is visible when the position node is a backface.
307        const IS_BACKFACE_VISIBLE = 1;
308        /// This flag is set during the first pass picture traversal, depending on whether
309        /// the cluster is visible or not. It's read during the second pass when primitives
310        /// consult their owning clusters to see if the primitive itself is visible.
311        const IS_VISIBLE = 2;
312    }
313}
314
315/// Descriptor for a cluster of primitives. For now, this is quite basic but will be
316/// extended to handle more spatial clustering of primitives.
317#[cfg_attr(feature = "capture", derive(Serialize))]
318pub struct PrimitiveCluster {
319    /// The positioning node for this cluster.
320    pub spatial_node_index: SpatialNodeIndex,
321    /// The bounding rect of the cluster, in the local space of the spatial node,
322    /// using display-list-authored prim culling rects (not snapped to the device
323    /// pixel grid). This is used to quickly determine the overall bounding rect
324    /// for a picture during the first picture traversal, which is needed for
325    /// local scale determination, and render task size calculations.
326    pub unsnapped_bounding_rect: LayoutRect,
327    /// The bounding rect of the cluster, snapped to the device pixel grid in
328    /// the cluster's own spatial-node space. Refreshed each frame by
329    /// `frame_snap::snap_frame_rects` from `unsnapped_bounding_rect`, before
330    /// any frame-time consumer reads it.
331    pub snapped_bounding_rect: LayoutRect,
332    /// The range of primitive instance indices associated with this cluster.
333    pub prim_range: Range<usize>,
334    /// Various flags / state for this cluster.
335    pub flags: ClusterFlags,
336}
337
338impl PrimitiveCluster {
339    /// Construct a new primitive cluster for a given positioning node.
340    fn new(
341        spatial_node_index: SpatialNodeIndex,
342        flags: ClusterFlags,
343        first_instance_index: usize,
344    ) -> Self {
345        PrimitiveCluster {
346            unsnapped_bounding_rect: LayoutRect::zero(),
347            snapped_bounding_rect: LayoutRect::zero(),
348            spatial_node_index,
349            flags,
350            prim_range: first_instance_index..first_instance_index
351        }
352    }
353
354    /// Return true if this cluster is compatible with the given params
355    pub fn is_compatible(
356        &self,
357        spatial_node_index: SpatialNodeIndex,
358        flags: ClusterFlags,
359        instance_index: usize,
360    ) -> bool {
361        self.flags == flags &&
362        self.spatial_node_index == spatial_node_index &&
363        instance_index == self.prim_range.end
364    }
365
366    pub fn prim_range(&self) -> Range<usize> {
367        self.prim_range.clone()
368    }
369
370    /// Add a primitive instance to this cluster, at the start or end
371    fn add_instance(
372        &mut self,
373        culling_rect: &LayoutRect,
374        instance_index: usize,
375    ) {
376        debug_assert_eq!(instance_index, self.prim_range.end);
377        self.unsnapped_bounding_rect = self.unsnapped_bounding_rect.union(culling_rect);
378        self.prim_range.end += 1;
379    }
380}
381
382/// A list of primitive instances that are added to a picture
383/// This ensures we can keep a list of primitives that
384/// are pictures, for a fast initial traversal of the picture
385/// tree without walking the instance list.
386#[cfg_attr(feature = "capture", derive(Serialize))]
387pub struct PrimitiveList {
388    /// List of primitives grouped into clusters.
389    pub clusters: Vec<PrimitiveCluster>,
390    pub child_pictures: Vec<PictureIndex>,
391    /// The number of Image compositor surfaces that were found when
392    /// adding prims to this list, which might be rendered as overlays.
393    pub image_surface_count: usize,
394    /// The number of YuvImage compositor surfaces that were found when
395    /// adding prims to this list, which might be rendered as overlays.
396    pub yuv_image_surface_count: usize,
397    pub needs_scissor_rect: bool,
398}
399
400impl PrimitiveList {
401    /// Construct an empty primitive list. This is
402    /// just used during the take_context / restore_context
403    /// borrow check dance, which will be removed as the
404    /// picture traversal pass is completed.
405    pub fn empty() -> Self {
406        PrimitiveList {
407            clusters: Vec::new(),
408            child_pictures: Vec::new(),
409            image_surface_count: 0,
410            yuv_image_surface_count: 0,
411            needs_scissor_rect: false,
412        }
413    }
414
415    pub fn merge(&mut self, other: PrimitiveList) {
416        self.clusters.extend(other.clusters);
417        self.child_pictures.extend(other.child_pictures);
418        self.image_surface_count += other.image_surface_count;
419        self.yuv_image_surface_count += other.yuv_image_surface_count;
420        self.needs_scissor_rect |= other.needs_scissor_rect;
421    }
422
423    /// Add a primitive instance to the end of the list
424    pub fn add_prim(
425        &mut self,
426        prim_instance: PrimitiveInstance,
427        prim_rect: LayoutRect,
428        spatial_node_index: SpatialNodeIndex,
429        prim_flags: PrimitiveFlags,
430        prim_instances: &mut Vec<PrimitiveInstance>,
431        clip_tree_builder: &ClipTreeBuilder,
432    ) {
433        let mut flags = ClusterFlags::empty();
434
435        // Pictures are always put into a new cluster, to make it faster to
436        // iterate all pictures in a given primitive list.
437        match prim_instance.kind {
438            PrimitiveKind::Picture { pic_index, .. } => {
439                self.child_pictures.push(pic_index);
440            }
441            PrimitiveKind::TextRun { .. } => {
442                self.needs_scissor_rect = true;
443            }
444            PrimitiveKind::YuvImage { .. } => {
445                // Any YUV image that requests a compositor surface is implicitly
446                // opaque. Though we might treat this prim as an underlay, which
447                // doesn't require an overlay surface, we add to the count anyway
448                // in case we opt to present it as an overlay. This means we may
449                // be allocating more subslices than we actually need, but it
450                // gives us maximum flexibility.
451                if prim_flags.contains(PrimitiveFlags::PREFER_COMPOSITOR_SURFACE) {
452                    self.yuv_image_surface_count += 1;
453                }
454            }
455            PrimitiveKind::Image { .. } => {
456                // For now, we assume that any image that wants a compositor surface
457                // is transparent, and uses the existing overlay compositor surface
458                // infrastructure. In future, we could detect opaque images, however
459                // it's a little bit of work, as scene building doesn't have access
460                // to the opacity state of an image key at this point.
461                if prim_flags.contains(PrimitiveFlags::PREFER_COMPOSITOR_SURFACE) {
462                    self.image_surface_count += 1;
463                }
464            }
465            _ => {}
466        }
467
468        if prim_flags.contains(PrimitiveFlags::IS_BACKFACE_VISIBLE) {
469            flags.insert(ClusterFlags::IS_BACKFACE_VISIBLE);
470        }
471
472        let clip_leaf = clip_tree_builder.get_leaf(prim_instance.clip_leaf_id);
473        // Scene-build feeds the cluster's `unsnapped_bounding_rect` from this
474        // culling rect (clip-leaf rect ∩ prim_rect). Both inputs are pre-snap;
475        // the cluster's per-frame `snapped_bounding_rect` is produced by
476        // re-snapping that bound in `frame_snap::snap_frame_rects`.
477        let culling_rect = clip_leaf.unsnapped_local_clip_rect
478            .intersection(&prim_rect)
479            .unwrap_or_else(LayoutRect::zero);
480
481        let instance_index = prim_instances.len();
482        prim_instances.push(prim_instance);
483
484        if let Some(cluster) = self.clusters.last_mut() {
485            if cluster.is_compatible(spatial_node_index, flags, instance_index) {
486                cluster.add_instance(&culling_rect, instance_index);
487                return;
488            }
489        }
490
491        // Same idea with clusters, using a different distribution.
492        let clusters_len = self.clusters.len();
493        if clusters_len == self.clusters.capacity() {
494            let next_alloc = match clusters_len {
495                1 ..= 15 => 16 - clusters_len,
496                16 ..= 127 => 128 - clusters_len,
497                _ => clusters_len * 2,
498            };
499
500            self.clusters.reserve(next_alloc);
501        }
502
503        let mut cluster = PrimitiveCluster::new(
504            spatial_node_index,
505            flags,
506            instance_index,
507        );
508
509        cluster.add_instance(&culling_rect, instance_index);
510        self.clusters.push(cluster);
511    }
512
513    /// Returns true if there are no clusters (and thus primitives)
514    pub fn is_empty(&self) -> bool {
515        self.clusters.is_empty()
516    }
517}
518
519bitflags! {
520    #[cfg_attr(feature = "capture", derive(Serialize))]
521    /// Flags describing properties for a given PictureInstance
522    #[derive(Debug, Copy, PartialEq, Eq, Clone, PartialOrd, Ord, Hash)]
523    pub struct PictureFlags : u8 {
524        /// This picture is a resolve target (doesn't actually render content itself,
525        /// will have content copied in to it)
526        const IS_RESOLVE_TARGET = 1 << 0;
527        /// This picture establishes a sub-graph, which affects how SurfaceBuilder will
528        /// set up dependencies in the render task graph
529        const IS_SUB_GRAPH = 1 << 1;
530        /// If set, this picture should not apply snapping via changing the raster root
531        const DISABLE_SNAPPING = 1 << 2;
532    }
533}
534
535/// Per-frame scratch data for a Picture primitive. Pushed in `take_context`
536/// and read by both prepare and batch through the `scratch_handle` carried
537/// on `PrimitiveKind::Picture`.
538#[derive(Debug)]
539#[cfg_attr(feature = "capture", derive(Serialize))]
540pub struct PictureScratch {
541    /// All render tasks have 0-2 input tasks.
542    pub primary_render_task_id: Option<RenderTaskId>,
543    /// If a mix-blend-mode, contains the render task for
544    /// the readback of the framebuffer that we use to sample
545    /// from in the mix-blend-mode shader.
546    /// For drop-shadow filter, this will store the original
547    /// picture task which would be rendered on screen after
548    /// blur pass.
549    /// This is also used by SVGFEBlend, SVGFEComposite and
550    /// SVGFEDisplacementMap filters.
551    pub secondary_render_task_id: Option<RenderTaskId>,
552    /// Optional cache handles for storing extra data in the
553    /// GPU cache, depending on the type of picture.
554    pub extra_gpu_data: SmallVec<[GpuBufferAddress; 1]>,
555}
556
557impl PictureScratch {
558    pub fn empty() -> Self {
559        PictureScratch {
560            primary_render_task_id: None,
561            secondary_render_task_id: None,
562            extra_gpu_data: SmallVec::new(),
563        }
564    }
565}
566
567#[cfg_attr(feature = "capture", derive(Serialize))]
568pub struct PictureInstance {
569    /// List of primitives, and associated info for this picture.
570    pub prim_list: PrimitiveList,
571
572    /// If false and transform ends up showing the back of the picture,
573    /// it will be considered invisible.
574    pub is_backface_visible: bool,
575
576    /// How this picture should be composited.
577    /// If None, don't composite - just draw directly on parent surface.
578    pub composite_mode: Option<PictureCompositeMode>,
579
580    pub raster_config: Option<RasterConfig>,
581    pub context_3d: Picture3DContext<OrderedPictureChild>,
582
583    /// The spatial node index of this picture when it is
584    /// composited into the parent picture.
585    pub spatial_node_index: SpatialNodeIndex,
586
587    /// Store the state of the previous local rect
588    /// for this picture. We need this in order to know when
589    /// to invalidate segments / drop-shadow gpu cache handles.
590    pub prev_local_rect: LayoutRect,
591
592    /// If false, this picture needs to (re)build segments
593    /// if it supports segment rendering. This can occur
594    /// if the local rect of the picture changes due to
595    /// transform animation and/or scrolling.
596    pub segments_are_valid: bool,
597
598    /// Requested raster space for this picture
599    pub raster_space: RasterSpace,
600
601    /// Flags for this picture primitive
602    pub flags: PictureFlags,
603
604    /// The lowest common ancestor clip of all of the primitives in this
605    /// picture, to be ignored when clipping those primitives and applied
606    /// later when compositing the picture.
607    pub clip_root: Option<ClipNodeId>,
608
609    /// If provided, cache the content of this picture into an image
610    /// associated with the image key.
611    pub snapshot: Option<SnapshotInfo>,
612}
613
614impl PictureInstance {
615    pub fn print<T: PrintTreePrinter>(
616        &self,
617        pictures: &[Self],
618        self_index: PictureIndex,
619        pt: &mut T,
620    ) {
621        pt.new_level(format!("{:?}", self_index));
622        pt.add_item(format!("cluster_count: {:?}", self.prim_list.clusters.len()));
623        pt.add_item(format!("spatial_node_index: {:?}", self.spatial_node_index));
624        pt.add_item(format!("raster_config: {:?}", self.raster_config));
625        pt.add_item(format!("composite_mode: {:?}", self.composite_mode));
626        pt.add_item(format!("flags: {:?}", self.flags));
627
628        for child_pic_index in &self.prim_list.child_pictures {
629            pictures[child_pic_index.0].print(pictures, *child_pic_index, pt);
630        }
631
632        pt.end_level();
633    }
634
635    pub fn resolve_scene_properties(&mut self, properties: &SceneProperties) {
636        match self.composite_mode {
637            Some(PictureCompositeMode::Filter(ref mut filter)) => {
638                match *filter {
639                    Filter::Opacity(ref binding, ref mut value) => {
640                        *value = properties.resolve_float(binding);
641                    }
642                    _ => {}
643                }
644            }
645            _ => {}
646        }
647    }
648
649    pub fn is_visible(
650        &self,
651        spatial_tree: &SpatialTree,
652    ) -> bool {
653        if let Some(PictureCompositeMode::Filter(ref filter)) = self.composite_mode {
654            if !filter.is_visible() {
655                return false;
656            }
657        }
658
659        // For out-of-preserve-3d pictures, the backface visibility is determined by
660        // the local transform only.
661        // Note: we aren't taking the transform relative to the parent picture,
662        // since picture tree can be more dense than the corresponding spatial tree.
663        if !self.is_backface_visible {
664            if let Picture3DContext::Out = self.context_3d {
665                match spatial_tree.get_local_visible_face(self.spatial_node_index) {
666                    VisibleFace::Front => {}
667                    VisibleFace::Back => return false,
668                }
669            }
670        }
671
672        true
673    }
674
675    pub fn new_image(
676        composite_mode: Option<PictureCompositeMode>,
677        context_3d: Picture3DContext<OrderedPictureChild>,
678        prim_flags: PrimitiveFlags,
679        prim_list: PrimitiveList,
680        spatial_node_index: SpatialNodeIndex,
681        raster_space: RasterSpace,
682        flags: PictureFlags,
683        snapshot: Option<SnapshotInfo>,
684    ) -> Self {
685        PictureInstance {
686            prim_list,
687            composite_mode,
688            raster_config: None,
689            context_3d,
690            is_backface_visible: prim_flags.contains(PrimitiveFlags::IS_BACKFACE_VISIBLE),
691            spatial_node_index,
692            prev_local_rect: LayoutRect::zero(),
693            segments_are_valid: false,
694            raster_space,
695            flags,
696            clip_root: None,
697            snapshot,
698        }
699    }
700
701    pub fn take_context(
702        &mut self,
703        pic_index: PictureIndex,
704        parent_surface_index: Option<SurfaceIndex>,
705        parent_subpixel_mode: SubpixelMode,
706        frame_state: &mut FrameBuildingState,
707        frame_context: &FrameBuildingContext,
708        data_stores: &mut DataStores,
709        scratch: &mut PrimitiveScratchBuffer,
710        tile_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
711    ) -> Option<(PictureContext, PictureState, PrimitiveList, storage::Index<PictureScratch>)> {
712        let mut picture_scratch = PictureScratch::empty();
713
714        let dbg_flags = DebugFlags::PICTURE_CACHING_DBG | DebugFlags::PICTURE_BORDERS;
715        if frame_context.debug_flags.intersects(dbg_flags) {
716            self.draw_debug_overlay(
717                parent_surface_index,
718                frame_state,
719                frame_context,
720                tile_caches,
721                scratch,
722            );
723        }
724
725        if !self.is_visible(frame_context.spatial_tree) {
726            return None;
727        }
728
729        profile_scope!("take_context");
730
731        let surface_index = match self.raster_config {
732            Some(ref raster_config) => raster_config.surface_index,
733            None => parent_surface_index.expect("bug: no parent"),
734        };
735        let surface = &frame_state.surfaces[surface_index.0];
736        let surface_spatial_node_index = surface.surface_spatial_node_index;
737
738        let map_pic_to_world = SpaceMapper::new_with_target(
739            frame_context.root_spatial_node_index,
740            surface_spatial_node_index,
741            frame_context.global_screen_world_rect,
742            frame_context.spatial_tree,
743        );
744
745        let map_pic_to_vis = SpaceMapper::new_with_target(
746            // TODO: switch from root to raster space.
747            frame_context.root_spatial_node_index,
748            surface_spatial_node_index,
749            surface.culling_rect,
750            frame_context.spatial_tree,
751        );
752
753        // TODO: When moving VisRect to raster space, compute the picture
754        // bounds by projecting the parent surface's culling rect into the
755        // current surface's raster space.
756        let pic_bounds = map_pic_to_world
757            .unmap(&map_pic_to_world.bounds)
758            .unwrap_or_else(PictureRect::max_rect);
759
760        let map_local_to_pic = SpaceMapper::new(
761            surface_spatial_node_index,
762            pic_bounds,
763        );
764
765        match self.raster_config {
766            Some(RasterConfig { surface_index, composite_mode: PictureCompositeMode::TileCache { slice_id }, .. }) => {
767                prepare_tiled_picture_surface(
768                    surface_index,
769                    slice_id,
770                    surface_spatial_node_index,
771                    &map_pic_to_world,
772                    frame_context,
773                    frame_state,
774                    tile_caches,
775                );
776            }
777            Some(ref mut raster_config) => {
778                let (pic_rect, force_scissor_rect) = {
779                    let surface = &frame_state.surfaces[raster_config.surface_index.0];
780                    (surface.clipped_local_rect, surface.force_scissor_rect)
781                };
782
783                let parent_surface_index = parent_surface_index.expect("bug: no parent for child surface");
784
785                // Layout space for the picture is picture space from the
786                // perspective of its child primitives.
787                let local_rect = pic_rect * Scale::new(1.0);
788
789                // If the precise rect changed since last frame, we need to invalidate
790                // any segments and gpu cache handles for drop-shadows.
791                // TODO(gw): Requiring storage of the `prev_precise_local_rect` here
792                //           is a total hack. It's required because `prev_precise_local_rect`
793                //           gets written to twice (during initial vis pass and also during
794                //           prepare pass). The proper longer term fix for this is to make
795                //           use of the conservative picture rect for segmenting (which should
796                //           be done during scene building).
797                if local_rect != self.prev_local_rect {
798                    // Invalidate any segments built for this picture, since the local
799                    // rect has changed.
800                    self.segments_are_valid = false;
801                    self.prev_local_rect = local_rect;
802                }
803
804                let max_surface_size = frame_context
805                    .fb_config
806                    .max_surface_override
807                    .unwrap_or(MAX_SURFACE_SIZE) as f32;
808
809                let surface_rects = match get_surface_rects(
810                    raster_config.surface_index,
811                    &raster_config.composite_mode,
812                    parent_surface_index,
813                    &mut frame_state.surfaces,
814                    frame_context.spatial_tree,
815                    max_surface_size,
816                    force_scissor_rect,
817                ) {
818                    Some(rects) => rects,
819                    None => return None,
820                };
821
822                if let PictureCompositeMode::IntermediateSurface = raster_config.composite_mode {
823                    if !scratch.frame.required_sub_graphs.contains(&pic_index) {
824                        return None;
825                    }
826                }
827
828                let can_use_shared_surface = !self.flags.contains(PictureFlags::IS_RESOLVE_TARGET);
829                let (surface_descriptor, render_tasks) = prepare_composite_mode(
830                    &raster_config.composite_mode,
831                    surface_index,
832                    parent_surface_index,
833                    &surface_rects,
834                    &self.snapshot,
835                    can_use_shared_surface,
836                    frame_context,
837                    frame_state,
838                    data_stores,
839                    &mut picture_scratch.extra_gpu_data,
840                );
841
842                picture_scratch.primary_render_task_id = render_tasks[0];
843                picture_scratch.secondary_render_task_id = render_tasks[1];
844
845                let is_sub_graph = self.flags.contains(PictureFlags::IS_SUB_GRAPH);
846
847                frame_state.surface_builder.push_surface(
848                    raster_config.surface_index,
849                    is_sub_graph,
850                    surface_rects.clipped_local,
851                    Some(surface_descriptor),
852                    frame_state.surfaces,
853                    frame_state.rg_builder,
854                );
855            }
856            None => {}
857        };
858
859        let state = PictureState {
860            map_local_to_pic,
861            map_pic_to_vis,
862        };
863
864        let mut dirty_region_count = 0;
865
866        // If this is a picture cache, push the dirty region to ensure any
867        // child primitives are culled and clipped to the dirty rect(s).
868        if let Some(RasterConfig { composite_mode: PictureCompositeMode::TileCache { slice_id }, .. }) = self.raster_config {
869            let dirty_region = tile_caches[&slice_id].dirty_region.clone();
870            frame_state.push_dirty_region(dirty_region);
871
872            dirty_region_count += 1;
873        }
874
875        let subpixel_mode = compute_subpixel_mode(
876            &self.raster_config,
877            tile_caches,
878            parent_subpixel_mode
879        );
880
881        let context = PictureContext {
882            pic_index,
883            raster_spatial_node_index: frame_state.surfaces[surface_index.0].raster_spatial_node_index,
884            // TODO: switch the visibility spatial node from the root to raster space.
885            visibility_spatial_node_index: frame_context.root_spatial_node_index,
886            surface_spatial_node_index,
887            surface_index,
888            dirty_region_count,
889            subpixel_mode,
890        };
891
892        let prim_list = mem::replace(&mut self.prim_list, PrimitiveList::empty());
893
894        let scratch_handle = scratch.frame.pictures.push(picture_scratch);
895        Some((context, state, prim_list, scratch_handle))
896    }
897
898    pub fn restore_context(
899        &mut self,
900        pic_index: PictureIndex,
901        prim_list: PrimitiveList,
902        context: PictureContext,
903        frame_context: &FrameBuildingContext,
904        frame_state: &mut FrameBuildingState,
905        scratch: &PrimitiveScratchBuffer,
906    ) {
907        // Pop any dirty regions this picture set
908        for _ in 0 .. context.dirty_region_count {
909            frame_state.pop_dirty_region();
910        }
911
912        if self.raster_config.is_some() {
913            frame_state.surface_builder.pop_surface(
914                pic_index,
915                frame_state.rg_builder,
916                frame_state.cmd_buffers,
917            );
918        }
919
920        if let Picture3DContext::In { root_data: Some(ref mut list), plane_splitter_index, ancestor_index, .. } = self.context_3d {
921            let splitter = &mut frame_state.plane_splitters[plane_splitter_index.0];
922
923            // Resolve split planes via BSP
924            PictureInstance::resolve_split_planes(
925                splitter,
926                list,
927                &mut frame_state.frame_gpu_data.f32,
928                ancestor_index,
929                &frame_context.spatial_tree,
930            );
931
932            // Add the child prims to the relevant command buffers
933            let mut cmd_buffer_targets = Vec::new();
934            for child in list {
935                if frame_state.surface_builder.get_cmd_buffer_targets_for_prim(
936                    &scratch.frame.draws[child.anchor.instance_index.0 as usize],
937                    &mut cmd_buffer_targets,
938                ) {
939                    let prim_cmd = PrimitiveCommand::complex(
940                        storage::Index::from_u32(child.anchor.instance_index.0),
941                        child.gpu_address
942                    );
943
944                    frame_state.push_prim(
945                        &prim_cmd,
946                        child.anchor.spatial_node_index,
947                        &cmd_buffer_targets,
948                    );
949                }
950            }
951        }
952
953        self.prim_list = prim_list;
954    }
955
956    /// Add a primitive instance to the plane splitter. The function would generate
957    /// an appropriate polygon, clip it against the frustum, and register with the
958    /// given plane splitter.
959    pub fn add_split_plane(
960        splitter: &mut PlaneSplitter,
961        spatial_tree: &SpatialTree,
962        prim_spatial_node_index: SpatialNodeIndex,
963        // Coordinate space of the 3D rendering context's containing block
964        // (`ancestor_index` from Picture3DContext::In). Polygons are projected
965        // into this pre-perspective space so the BSP works on real 3D planes
966        // rather than post-perspective ones.
967        ancestor_spatial_node_index: SpatialNodeIndex,
968        visibility_spatial_node_index: SpatialNodeIndex,
969        original_local_rect: LayoutRect,
970        combined_local_clip_rect: &LayoutRect,
971        dirty_rect: VisRect,
972        plane_split_anchor: PlaneSplitAnchor,
973    ) -> bool {
974        let prim_to_ancestor = spatial_tree.get_relative_transform(
975            prim_spatial_node_index,
976            ancestor_spatial_node_index
977        );
978
979        let ancestor_matrix = prim_to_ancestor.clone().into_transform().cast().to_untyped();
980
981        // Apply the local clip rect here, before splitting. This is
982        // because the local clip rect can't be applied in the vertex
983        // shader for split composites, since we are drawing polygons
984        // rather that rectangles. The interpolation still works correctly
985        // since we determine the UVs by doing a bilerp with a factor
986        // from the original local rect.
987        let local_rect = match original_local_rect
988            .intersection(combined_local_clip_rect)
989        {
990            Some(rect) => rect.cast(),
991            None => return false,
992        };
993
994        match prim_to_ancestor {
995            CoordinateSpaceMapping::Local => {
996                let polygon = Polygon::from_rect(
997                    local_rect.to_rect() * Scale::new(1.0),
998                    plane_split_anchor,
999                );
1000                splitter.add(polygon);
1001            }
1002            CoordinateSpaceMapping::ScaleOffset(scale_offset) if scale_offset.scale == Vector2D::new(1.0, 1.0) => {
1003                let inv_matrix = scale_offset.inverse().to_transform().cast();
1004                let polygon = Polygon::from_transformed_rect_with_inverse(
1005                    local_rect.to_rect().to_untyped(),
1006                    &ancestor_matrix,
1007                    &inv_matrix,
1008                    plane_split_anchor,
1009                ).unwrap();
1010                splitter.add(polygon);
1011            }
1012            CoordinateSpaceMapping::ScaleOffset(_) |
1013            CoordinateSpaceMapping::Transform(_) => {
1014                // Project the visible region back into the 3D context's containing-block
1015                // (ancestor) space.
1016                // This may fail if the dirty rect doesn't have a valid pre-image (e.g. it
1017                // sits behind the projection plane in ancestor space), in which case we
1018                // fall back to no lateral bounds.
1019                // TODO: Instead of trying to map the vis (world) space dirty rect into the
1020                // right space here, we should be able to find the dirty rect in this space
1021                // that was built during the dirty rect propagation at the beginning of the
1022                // frame.
1023                let map_ancestor_to_vis = SpaceMapper::<LayoutPixel, VisPixel>::new_with_target(
1024                    visibility_spatial_node_index,
1025                    ancestor_spatial_node_index,
1026                    VisRect::max_rect(),
1027                    spatial_tree,
1028                );
1029                let ancestor_dirty_rect = map_ancestor_to_vis.unmap(&dirty_rect);
1030
1031                let ancestor_bounds = ancestor_dirty_rect.map(|r| r.cast().to_rect().to_untyped());
1032
1033                let mut clipper = Clipper::<PlaneSplitAnchor>::new();
1034                let planes = match Clipper::<PlaneSplitAnchor>::frustum_planes(&ancestor_matrix, ancestor_bounds) {
1035                    Ok(p) => p,
1036                    Err(_) => return false,
1037                };
1038                for plane in planes {
1039                    clipper.add(plane);
1040                }
1041
1042                let polygon = Polygon::from_rect(
1043                    local_rect.to_rect().to_untyped(),
1044                    plane_split_anchor,
1045                );
1046                let clipped: Vec<_> = clipper.clip(polygon).to_vec();
1047
1048                for poly in clipped {
1049                    if let Some(transformed) = poly.transform(&ancestor_matrix) {
1050                        splitter.add(transformed);
1051                    }
1052                }
1053            }
1054        }
1055
1056        true
1057    }
1058
1059    fn resolve_split_planes(
1060        splitter: &mut PlaneSplitter,
1061        ordered: &mut Vec<OrderedPictureChild>,
1062        gpu_buffer: &mut GpuBufferBuilderF,
1063        ancestor_index: SpatialNodeIndex,
1064        spatial_tree: &SpatialTree,
1065    ) {
1066        ordered.clear();
1067
1068        // Process the accumulated split planes and order them for rendering.
1069        // Z axis is directed at the screen, `sort` is ascending, and we need back-to-front order.
1070        let sorted = splitter.sort(vec3(0.0, 0.0, 1.0));
1071        ordered.reserve(sorted.len());
1072        for poly in sorted {
1073            let transform = match spatial_tree
1074                .get_relative_transform(poly.anchor.spatial_node_index, ancestor_index)
1075                .inverse()
1076            {
1077                Some(transform) => transform.into_transform(),
1078                // logging this would be a bit too verbose
1079                None => continue,
1080            };
1081
1082            let local_points = [
1083                transform.transform_point3d(poly.points[0].cast_unit().to_f32()),
1084                transform.transform_point3d(poly.points[1].cast_unit().to_f32()),
1085                transform.transform_point3d(poly.points[2].cast_unit().to_f32()),
1086                transform.transform_point3d(poly.points[3].cast_unit().to_f32()),
1087            ];
1088
1089            // If any of the points are un-transformable, just drop this
1090            // plane from drawing.
1091            if local_points.iter().any(|p| p.is_none()) {
1092                continue;
1093            }
1094
1095            let p0 = local_points[0].unwrap();
1096            let p1 = local_points[1].unwrap();
1097            let p2 = local_points[2].unwrap();
1098            let p3 = local_points[3].unwrap();
1099
1100            let mut writer = gpu_buffer.write_blocks(2);
1101            writer.push_one([p0.x, p0.y, p1.x, p1.y]);
1102            writer.push_one([p2.x, p2.y, p3.x, p3.y]);
1103            let gpu_address = writer.finish();
1104
1105            ordered.push(OrderedPictureChild {
1106                anchor: poly.anchor,
1107                gpu_address,
1108            });
1109        }
1110    }
1111
1112    /// Called during initial picture traversal, before we know the
1113    /// bounding rect of children. It is possible to determine the
1114    /// surface / raster config now though.
1115    pub fn assign_surface(
1116        &mut self,
1117        frame_context: &FrameBuildingContext,
1118        parent_surface_index: Option<SurfaceIndex>,
1119        tile_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
1120        surfaces: &mut Vec<SurfaceInfo>,
1121    ) -> Option<SurfaceIndex> {
1122        // Reset raster config in case we early out below.
1123        self.raster_config = None;
1124
1125        match self.composite_mode {
1126            Some(ref composite_mode) => {
1127                let surface_spatial_node_index = self.spatial_node_index;
1128
1129                // Currently, we ensure that the scaling factor is >= 1.0 as a smaller scale factor can result in blurry output.
1130                let mut min_scale;
1131                let mut max_scale = 1.0e32;
1132
1133                // If a raster root is established, this surface should be scaled based on the scale factors of the surface raster to parent raster transform.
1134                // This scaling helps ensure that the content in this surface does not become blurry or pixelated when composited in the parent surface.
1135
1136                let world_scale_factors = match parent_surface_index {
1137                    Some(parent_surface_index) => {
1138                        let parent_surface = &surfaces[parent_surface_index.0];
1139
1140                        let local_to_surface = frame_context
1141                            .spatial_tree
1142                            .get_relative_transform(
1143                                surface_spatial_node_index,
1144                                parent_surface.surface_spatial_node_index,
1145                            );
1146
1147                        // Since we can't determine reasonable scale factors for transforms
1148                        // with perspective, just use a scale of (1,1) for now, which is
1149                        // what Gecko does when it choosed to supplies a scale factor anyway.
1150                        // In future, we might be able to improve the quality here by taking
1151                        // into account the screen rect after clipping, but for now this gives
1152                        // better results than just taking the matrix scale factors.
1153                        let scale_factors = if local_to_surface.is_perspective() {
1154                            (1.0, 1.0)
1155                        } else {
1156                            local_to_surface.scale_factors()
1157                        };
1158
1159                        let scale_factors = (
1160                            scale_factors.0 * parent_surface.world_scale_factors.0,
1161                            scale_factors.1 * parent_surface.world_scale_factors.1,
1162                        );
1163
1164                        scale_factors
1165                    }
1166                    None => {
1167                        let local_to_surface_scale_factors = frame_context
1168                            .spatial_tree
1169                            .get_relative_transform(
1170                                surface_spatial_node_index,
1171                                frame_context.spatial_tree.root_reference_frame_index(),
1172                            )
1173                            .scale_factors();
1174
1175                        let scale_factors = (
1176                            local_to_surface_scale_factors.0,
1177                            local_to_surface_scale_factors.1,
1178                        );
1179
1180                        scale_factors
1181                    }
1182                };
1183
1184                // TODO(gw): For now, we disable snapping on any sub-graph, as that implies
1185                //           that the spatial / raster node must be the same as the parent
1186                //           surface. In future, we may be able to support snapping in these
1187                //           cases (if it's even useful?) or perhaps add a ENABLE_SNAPPING
1188                //           picture flag, if the IS_SUB_GRAPH is ever useful in a different
1189                //           context.
1190                let allow_snapping = !self.flags.contains(PictureFlags::DISABLE_SNAPPING);
1191
1192                // For some primitives (e.g. text runs) we can't rely on the bounding rect being
1193                // exactly correct. For these cases, ensure we set a scissor rect when drawing
1194                // this picture to a surface.
1195                // TODO(gw) In future, we may be able to improve how the text run bounding rect is
1196                // calculated so that we don't need to do this. We could either fix Gecko up to
1197                // provide an exact bounds, or we could calculate the bounding rect internally in
1198                // WR, which would be easier to do efficiently once we have retained text runs
1199                // as part of the planned frame-tree interface changes.
1200                let force_scissor_rect = self.prim_list.needs_scissor_rect;
1201
1202                // Check if there is perspective or if an SVG filter is applied, and thus whether a new
1203                // rasterization root should be established.
1204                let (device_pixel_scale, raster_spatial_node_index, local_scale, world_scale_factors) = match composite_mode {
1205                    PictureCompositeMode::TileCache { slice_id } => {
1206                        let tile_cache = tile_caches.get_mut(&slice_id).unwrap();
1207
1208                        // Get the complete scale-offset from local space to device space
1209                        let local_to_device = get_relative_scale_offset(
1210                            tile_cache.spatial_node_index,
1211                            frame_context.root_spatial_node_index,
1212                            frame_context.spatial_tree,
1213                        );
1214                        let local_to_cur_raster_scale = local_to_device.scale.x / tile_cache.current_raster_scale;
1215
1216                        // We only update the raster scale if we're in high quality zoom mode, or there is no
1217                        // pinch-zoom active, or the zoom has doubled or halved since the raster scale was
1218                        // last updated. During a low-quality zoom we therefore typically retain the previous
1219                        // scale factor, which avoids expensive re-rasterizations, except for when the zoom
1220                        // has become too large or too small when we re-rasterize to avoid bluriness or a
1221                        // proliferation of picture cache tiles. When the zoom ends we select a high quality
1222                        // scale factor for the next frame to be drawn.
1223                        if !frame_context.fb_config.low_quality_pinch_zoom
1224                            || !frame_context
1225                                .spatial_tree.get_spatial_node(tile_cache.spatial_node_index)
1226                                .is_ancestor_or_self_zooming
1227                            || local_to_cur_raster_scale <= 0.5
1228                            || local_to_cur_raster_scale >= 2.0
1229                        {
1230                            tile_cache.current_raster_scale = local_to_device.scale.x;
1231                        }
1232
1233                        // We may need to minify when zooming out picture cache tiles
1234                        min_scale = 0.0;
1235
1236                        if frame_context.fb_config.low_quality_pinch_zoom {
1237                            // Force the scale for this tile cache to be the currently selected
1238                            // local raster scale, so we don't need to rasterize tiles during
1239                            // the pinch-zoom.
1240                            min_scale = tile_cache.current_raster_scale;
1241                            max_scale = tile_cache.current_raster_scale;
1242                        }
1243
1244                        // Pick the largest scale factor of the transform for the scaling factor.
1245                        let scaling_factor = world_scale_factors.0.max(world_scale_factors.1).max(min_scale).min(max_scale);
1246
1247                        let device_pixel_scale = Scale::new(scaling_factor);
1248
1249                        (device_pixel_scale, surface_spatial_node_index, (1.0, 1.0), world_scale_factors)
1250                    }
1251                    _ => {
1252                        let surface_spatial_node = frame_context.spatial_tree.get_spatial_node(surface_spatial_node_index);
1253
1254                        let enable_snapping =
1255                            allow_snapping &&
1256                            surface_spatial_node.coordinate_system_id == CoordinateSystemId::root() &&
1257                            surface_spatial_node.snapping_transform.is_some();
1258
1259                        if enable_snapping {
1260                            let raster_spatial_node_index = frame_context.spatial_tree.root_reference_frame_index();
1261
1262                            let local_to_raster_transform = frame_context
1263                                .spatial_tree
1264                                .get_relative_transform(
1265                                    self.spatial_node_index,
1266                                    raster_spatial_node_index,
1267                                );
1268
1269                            let local_scale = local_to_raster_transform.scale_factors();
1270
1271                            (Scale::new(1.0), raster_spatial_node_index, local_scale, (1.0, 1.0))
1272                        } else {
1273                            // If client supplied a specific local scale, use that instead of
1274                            // estimating from parent transform
1275                            let world_scale_factors = match self.raster_space {
1276                                RasterSpace::Screen => world_scale_factors,
1277                                RasterSpace::Local(scale) => (scale, scale),
1278                            };
1279
1280                            let device_pixel_scale = Scale::new(
1281                                world_scale_factors.0.max(world_scale_factors.1).min(max_scale)
1282                            );
1283
1284                            (device_pixel_scale, surface_spatial_node_index, (1.0, 1.0), world_scale_factors)
1285                        }
1286                    }
1287                };
1288
1289                let surface = SurfaceInfo::new(
1290                    surface_spatial_node_index,
1291                    raster_spatial_node_index,
1292                    frame_context.global_screen_world_rect,
1293                    &frame_context.spatial_tree,
1294                    device_pixel_scale,
1295                    world_scale_factors,
1296                    local_scale,
1297                    allow_snapping,
1298                    force_scissor_rect,
1299                );
1300
1301                let surface_index = SurfaceIndex(surfaces.len());
1302
1303                surfaces.push(surface);
1304
1305                self.raster_config = Some(RasterConfig {
1306                    composite_mode: composite_mode.clone(),
1307                    surface_index,
1308                });
1309
1310                Some(surface_index)
1311            }
1312            None => {
1313                None
1314            }
1315        }
1316    }
1317
1318    /// Called after updating child pictures during the initial
1319    /// picture traversal. Bounding rects are propagated from
1320    /// child pictures up to parent picture surfaces, so that the
1321    /// parent bounding rect includes any dynamic picture bounds.
1322    pub fn propagate_bounding_rect(
1323        &mut self,
1324        surface_index: SurfaceIndex,
1325        parent_surface_index: Option<SurfaceIndex>,
1326        surfaces: &mut [SurfaceInfo],
1327        frame_context: &FrameBuildingContext,
1328    ) {
1329        let surface = &mut surfaces[surface_index.0];
1330
1331        for cluster in &mut self.prim_list.clusters {
1332            cluster.flags.remove(ClusterFlags::IS_VISIBLE);
1333
1334            // `cluster.snapped_bounding_rect` was refreshed for this frame by
1335            // `frame_snap::snap_frame_rects` (snap of `unsnapped_bounding_rect`
1336            // in the cluster's spatial-node space). Note that this alone is
1337            // not enough to make `surface.unclipped_local_rect` snap-correct
1338            // — see the SNAPTODO on that field.
1339
1340            // Skip the cluster if backface culled.
1341            if !cluster.flags.contains(ClusterFlags::IS_BACKFACE_VISIBLE) {
1342                // For in-preserve-3d primitives and pictures, the backface visibility is
1343                // evaluated relative to the containing block.
1344                if let Picture3DContext::In { ancestor_index, .. } = self.context_3d {
1345                    let mut face = VisibleFace::Front;
1346                    frame_context.spatial_tree.get_relative_transform_with_face(
1347                        cluster.spatial_node_index,
1348                        ancestor_index,
1349                        Some(&mut face),
1350                    );
1351                    if face == VisibleFace::Back {
1352                        continue
1353                    }
1354                }
1355            }
1356
1357            // No point including this cluster if it can't be transformed
1358            let spatial_node = &frame_context
1359                .spatial_tree
1360                .get_spatial_node(cluster.spatial_node_index);
1361            if !spatial_node.invertible {
1362                continue;
1363            }
1364
1365            // Map the cluster bounding rect into the space of the surface, and
1366            // include it in the surface bounding rect.
1367            surface.map_local_to_picture.set_target_spatial_node(
1368                cluster.spatial_node_index,
1369                frame_context.spatial_tree,
1370            );
1371
1372            // Mark the cluster visible, since it passed the invertible and
1373            // backface checks.
1374            cluster.flags.insert(ClusterFlags::IS_VISIBLE);
1375            if let Some(cluster_rect) = surface.map_local_to_picture.map(&cluster.snapped_bounding_rect) {
1376                surface.unclipped_local_rect = surface.unclipped_local_rect.union(&cluster_rect);
1377            }
1378        }
1379
1380        // If this picture establishes a surface, then map the surface bounding
1381        // rect into the parent surface coordinate space, and propagate that up
1382        // to the parent.
1383        if let Some(ref mut raster_config) = self.raster_config {
1384            // Propagate up to parent surface, now that we know this surface's static rect
1385            if let Some(parent_surface_index) = parent_surface_index {
1386                let surface_rect = raster_config.composite_mode.get_coverage(
1387                    surface,
1388                    Some(surface.unclipped_local_rect.cast_unit()),
1389                );
1390
1391                let parent_surface = &mut surfaces[parent_surface_index.0];
1392                parent_surface.map_local_to_picture.set_target_spatial_node(
1393                    self.spatial_node_index,
1394                    frame_context.spatial_tree,
1395                );
1396
1397                // Drop shadows draw both a content and shadow rect, so need to expand the local
1398                // rect of any surfaces to be composited in parent surfaces correctly.
1399
1400                if let Some(parent_surface_rect) = parent_surface
1401                    .map_local_to_picture
1402                    .map(&surface_rect)
1403                {
1404                    parent_surface.unclipped_local_rect =
1405                        parent_surface.unclipped_local_rect.union(&parent_surface_rect);
1406                }
1407            }
1408        }
1409    }
1410
1411    pub fn write_gpu_blocks(
1412        &mut self,
1413        frame_state: &mut FrameBuildingState,
1414        data_stores: &mut DataStores,
1415        scratch: &mut PictureScratch,
1416    ) {
1417        let raster_config = match self.raster_config {
1418            Some(ref mut raster_config) => raster_config,
1419            None => {
1420                return;
1421            }
1422        };
1423
1424        raster_config.composite_mode.write_gpu_blocks(
1425            &frame_state.surfaces[raster_config.surface_index.0],
1426            &mut frame_state.frame_gpu_data,
1427            data_stores,
1428            &mut scratch.extra_gpu_data,
1429        );
1430    }
1431
1432    #[cold]
1433    fn draw_debug_overlay(
1434        &self,
1435        parent_surface_index: Option<SurfaceIndex>,
1436        frame_state: &FrameBuildingState,
1437        frame_context: &FrameBuildingContext,
1438        tile_caches: &FastHashMap<SliceId, Box<TileCacheInstance>>,
1439        scratch: &mut PrimitiveScratchBuffer,
1440    ) {
1441        fn draw_debug_border(
1442            local_rect: &PictureRect,
1443            thickness: i32,
1444            pic_to_world_mapper: &SpaceMapper<PicturePixel, WorldPixel>,
1445            global_device_pixel_scale: DevicePixelScale,
1446            color: ColorF,
1447            scratch: &mut PrimitiveScratchBuffer,
1448        ) {
1449            if let Some(world_rect) = pic_to_world_mapper.map(&local_rect) {
1450                let device_rect = world_rect * global_device_pixel_scale;
1451                scratch.push_debug_rect(
1452                    device_rect,
1453                    thickness,
1454                    color,
1455                    ColorF::TRANSPARENT,
1456                );
1457            }
1458        }
1459
1460        let flags = frame_context.debug_flags;
1461        let draw_borders = flags.contains(DebugFlags::PICTURE_BORDERS);
1462        let draw_tile_dbg = flags.contains(DebugFlags::PICTURE_CACHING_DBG);
1463
1464        let surface_index = match &self.raster_config {
1465            Some(raster_config) => raster_config.surface_index,
1466            None => parent_surface_index.expect("bug: no parent"),
1467        };
1468        let surface_spatial_node_index = frame_state
1469            .surfaces[surface_index.0]
1470            .surface_spatial_node_index;
1471
1472        let map_pic_to_world = SpaceMapper::new_with_target(
1473            frame_context.root_spatial_node_index,
1474            surface_spatial_node_index,
1475            frame_context.global_screen_world_rect,
1476            frame_context.spatial_tree,
1477        );
1478
1479        let Some(raster_config) = &self.raster_config else {
1480            return;
1481        };
1482
1483        if draw_borders {
1484            let layer_color;
1485            if let PictureCompositeMode::TileCache { slice_id } = &raster_config.composite_mode {
1486                // Tiled picture;
1487                layer_color = ColorF::new(0.0, 1.0, 0.0, 0.8);
1488
1489                let Some(tile_cache) = tile_caches.get(&slice_id) else {
1490                    return;
1491                };
1492
1493                // Draw a rectangle for each tile.
1494                for (_, sub_slice) in tile_cache.sub_slices.iter().enumerate() {
1495                    for tile in sub_slice.tiles.values() {
1496                        if !tile.is_visible {
1497                            continue;
1498                        }
1499                        let rect = tile.cached_surface.local_rect.intersection(&tile_cache.local_rect);
1500                        if let Some(rect) = rect {
1501                            draw_debug_border(
1502                                &rect,
1503                                1,
1504                                &map_pic_to_world,
1505                                frame_context.global_device_pixel_scale,
1506                                ColorF::new(0.0, 1.0, 0.0, 0.2),
1507                                scratch,
1508                            );
1509                        }
1510                    }
1511                }
1512            } else {
1513                // Non-tiled picture
1514                layer_color = ColorF::new(1.0, 0.0, 0.0, 0.5);
1515            }
1516
1517            // Draw a rectangle for the whole picture.
1518            let pic_rect = frame_state
1519                .surfaces[raster_config.surface_index.0]
1520                .unclipped_local_rect;
1521
1522            draw_debug_border(
1523                &pic_rect,
1524                3,
1525                &map_pic_to_world,
1526                frame_context.global_device_pixel_scale,
1527                layer_color,
1528                scratch,
1529            );
1530        }
1531
1532        if draw_tile_dbg && self.is_visible(frame_context.spatial_tree) {
1533            if let PictureCompositeMode::TileCache { slice_id } = &raster_config.composite_mode {
1534                let Some(tile_cache) = tile_caches.get(&slice_id) else {
1535                    return;
1536                };
1537                for (sub_slice_index, sub_slice) in tile_cache.sub_slices.iter().enumerate() {
1538                    for tile in sub_slice.tiles.values() {
1539                        if !tile.is_visible {
1540                            continue;
1541                        }
1542                        tile.cached_surface.root.draw_debug_rects(
1543                            &map_pic_to_world,
1544                            tile.is_opaque,
1545                            tile.cached_surface.current_descriptor.local_valid_rect,
1546                            scratch,
1547                            frame_context.global_device_pixel_scale,
1548                        );
1549
1550                        let label_offset = DeviceVector2D::new(
1551                            20.0 + sub_slice_index as f32 * 20.0,
1552                            30.0 + sub_slice_index as f32 * 20.0,
1553                        );
1554                        let tile_device_rect = tile.world_tile_rect
1555                            * frame_context.global_device_pixel_scale;
1556
1557                        if tile_device_rect.height() >= label_offset.y {
1558                            let surface = tile.surface.as_ref().expect("no tile surface set!");
1559
1560                            scratch.push_debug_string(
1561                                tile_device_rect.min + label_offset,
1562                                debug_colors::RED,
1563                                format!("{:?}: s={} is_opaque={} surface={} sub={}",
1564                                        tile.id,
1565                                        tile_cache.slice,
1566                                        tile.is_opaque,
1567                                        surface.kind(),
1568                                        sub_slice_index,
1569                                ),
1570                            );
1571                        }
1572                    }
1573                }
1574            }
1575        }
1576    }
1577}
1578
1579pub fn get_relative_scale_offset(
1580    child_spatial_node_index: SpatialNodeIndex,
1581    parent_spatial_node_index: SpatialNodeIndex,
1582    spatial_tree: &SpatialTree,
1583) -> ScaleOffset {
1584    let transform = spatial_tree.get_relative_transform(
1585        child_spatial_node_index,
1586        parent_spatial_node_index,
1587    );
1588    let mut scale_offset = match transform {
1589        CoordinateSpaceMapping::Local => ScaleOffset::identity(),
1590        CoordinateSpaceMapping::ScaleOffset(scale_offset) => scale_offset,
1591        CoordinateSpaceMapping::Transform(m) => {
1592            ScaleOffset::from_transform(&m).expect("bug: pictures caches don't support complex transforms")
1593        }
1594    };
1595
1596    // Compositors expect things to be aligned on device pixels. Logic at a higher level ensures that is
1597    // true, but floating point inaccuracy can sometimes result in small differences, so remove
1598    // them here.
1599    scale_offset.offset = scale_offset.offset.round();
1600
1601    scale_offset
1602}
1603
1604/// Update dirty rects, ensure that tiles have backing surfaces and build
1605/// the tile render tasks.
1606fn prepare_tiled_picture_surface(
1607    surface_index: SurfaceIndex,
1608    slice_id: SliceId,
1609    surface_spatial_node_index: SpatialNodeIndex,
1610    map_pic_to_world: &SpaceMapper<PicturePixel, WorldPixel>,
1611    frame_context: &FrameBuildingContext,
1612    frame_state: &mut FrameBuildingState,
1613    tile_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
1614) {
1615    let tile_cache = tile_caches.get_mut(&slice_id).unwrap();
1616    let mut debug_info = SliceDebugInfo::new();
1617    let mut surface_render_tasks = FastHashMap::default();
1618    let mut surface_local_dirty_rect = PictureRect::zero();
1619    let device_pixel_scale = frame_state
1620        .surfaces[surface_index.0]
1621        .device_pixel_scale;
1622    let mut at_least_one_tile_visible = false;
1623
1624    // Get the overall world space rect of the picture cache. Used to clip
1625    // the tile rects below for occlusion testing to the relevant area.
1626    let world_clip_rect = map_pic_to_world
1627        .map(&tile_cache.local_clip_rect)
1628        .expect("bug: unable to map clip rect")
1629        .round();
1630    let device_clip_rect = (world_clip_rect * frame_context.global_device_pixel_scale).round();
1631
1632    for (sub_slice_index, sub_slice) in tile_cache.sub_slices.iter_mut().enumerate() {
1633        for tile in sub_slice.tiles.values_mut() {
1634            // Ensure that the dirty rect doesn't extend outside the local valid rect.
1635            tile.cached_surface.local_dirty_rect = tile.cached_surface.local_dirty_rect
1636                .intersection(&tile.cached_surface.current_descriptor.local_valid_rect)
1637                .unwrap_or_else(|| { tile.cached_surface.is_valid = true; PictureRect::zero() });
1638
1639            let valid_rect = frame_state.composite_state.get_surface_rect(
1640                &tile.cached_surface.current_descriptor.local_valid_rect,
1641                &tile.cached_surface.local_rect,
1642                tile_cache.transform_index,
1643            ).to_i32();
1644
1645            let scissor_rect = frame_state.composite_state.get_surface_rect(
1646                &tile.cached_surface.local_dirty_rect,
1647                &tile.cached_surface.local_rect,
1648                tile_cache.transform_index,
1649            ).to_i32().intersection(&valid_rect).unwrap_or_else(|| { Box2D::zero() });
1650
1651            if tile.is_visible {
1652                // Get the world space rect that this tile will actually occupy on screen
1653                let world_draw_rect = world_clip_rect.intersection(&tile.world_valid_rect);
1654
1655                // If that draw rect is occluded by some set of tiles in front of it,
1656                // then mark it as not visible and skip drawing. When it's not occluded
1657                // it will fail this test, and get rasterized by the render task setup
1658                // code below.
1659                match world_draw_rect {
1660                    Some(world_draw_rect) => {
1661                        let check_occluded_tiles = match frame_state.composite_state.compositor_kind {
1662                            CompositorKind::Layer { .. } => true,
1663                            CompositorKind::Native { .. } | CompositorKind::Draw { .. } => {
1664                                // Only check for occlusion on visible tiles that are fixed position.
1665                                tile_cache.spatial_node_index == frame_context.root_spatial_node_index
1666                            }
1667                        };
1668                        if check_occluded_tiles &&
1669                           frame_state.composite_state.occluders.is_tile_occluded(tile.z_id, world_draw_rect) {
1670                            // If this tile has an allocated native surface, free it, since it's completely
1671                            // occluded. We will need to re-allocate this surface if it becomes visible,
1672                            // but that's likely to be rare (e.g. when there is no content display list
1673                            // for a frame or two during a tab switch).
1674                            let surface = tile.surface.as_mut().expect("no tile surface set!");
1675
1676                            if let TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { id, .. }, .. } = surface {
1677                                if let Some(id) = id.take() {
1678                                    frame_state.resource_cache.destroy_compositor_tile(id);
1679                                }
1680                            }
1681
1682                            tile.is_visible = false;
1683
1684                            if frame_context.fb_config.testing {
1685                                debug_info.tiles.insert(
1686                                    tile.tile_offset,
1687                                    TileDebugInfo::Occluded,
1688                                );
1689                            }
1690
1691                            continue;
1692                        }
1693                    }
1694                    None => {
1695                        tile.is_visible = false;
1696                    }
1697                }
1698
1699                // In extreme zoom/offset cases, we may end up with a local scissor/valid rect
1700                // that becomes empty after transformation to device space (e.g. if the local
1701                // rect height is 0.00001 and the compositor transform has large scale + offset).
1702                // DirectComposition panics if we try to BeginDraw with an empty rect, so catch
1703                // that here and mark the tile non-visible. This is a bit of a hack - we should
1704                // ideally handle these in a more accurate way so we don't end up with an empty
1705                // rect here.
1706                if !tile.cached_surface.is_valid && (scissor_rect.is_empty() || valid_rect.is_empty()) {
1707                    tile.is_visible = false;
1708                }
1709            }
1710
1711            // If we get here, we want to ensure that the surface remains valid in the texture
1712            // cache, _even if_ it's not visible due to clipping or being scrolled off-screen.
1713            // This ensures that we retain valid tiles that are off-screen, but still in the
1714            // display port of this tile cache instance.
1715            if let Some(TileSurface::Texture { descriptor, .. }) = tile.surface.as_ref() {
1716                if let SurfaceTextureDescriptor::TextureCache { handle: Some(handle), .. } = descriptor {
1717                    frame_state.resource_cache
1718                        .picture_textures.request(handle);
1719                }
1720            }
1721
1722            // If the tile has been found to be off-screen / clipped, skip any further processing.
1723            if !tile.is_visible {
1724                if frame_context.fb_config.testing {
1725                    debug_info.tiles.insert(
1726                        tile.tile_offset,
1727                        TileDebugInfo::Culled,
1728                    );
1729                }
1730
1731                continue;
1732            }
1733
1734            at_least_one_tile_visible = true;
1735
1736            if let TileSurface::Texture { descriptor, .. } = tile.surface.as_mut().unwrap() {
1737                match descriptor {
1738                    SurfaceTextureDescriptor::TextureCache { ref handle, .. } => {
1739                        let exists = handle.as_ref().map_or(false,
1740                            |handle| frame_state.resource_cache.picture_textures.entry_exists(handle)
1741                        );
1742                        // Invalidate if the backing texture was evicted.
1743                        if exists {
1744                            // Request the backing texture so it won't get evicted this frame.
1745                            // We specifically want to mark the tile texture as used, even
1746                            // if it's detected not visible below and skipped. This is because
1747                            // we maintain the set of tiles we care about based on visibility
1748                            // during pre_update. If a tile still exists after that, we are
1749                            // assuming that it's either visible or we want to retain it for
1750                            // a while in case it gets scrolled back onto screen soon.
1751                            // TODO(gw): Consider switching to manual eviction policy?
1752                            frame_state.resource_cache
1753                                .picture_textures
1754                                .request(handle.as_ref().unwrap());
1755                        } else {
1756                            // If the texture was evicted on a previous frame, we need to assume
1757                            // that the entire tile rect is dirty.
1758                            tile.invalidate(None, InvalidationReason::NoTexture);
1759                        }
1760                    }
1761                    SurfaceTextureDescriptor::Native { id, .. } => {
1762                        if id.is_none() {
1763                            // There is no current surface allocation, so ensure the entire tile is invalidated
1764                            tile.invalidate(None, InvalidationReason::NoSurface);
1765                        }
1766                    }
1767                }
1768            }
1769
1770            // Ensure - again - that the dirty rect doesn't extend outside the local valid rect,
1771            // as the tile could have been invalidated since the first computation.
1772            tile.cached_surface.local_dirty_rect = tile.cached_surface.local_dirty_rect
1773                .intersection(&tile.cached_surface.current_descriptor.local_valid_rect)
1774                .unwrap_or_else(|| { tile.cached_surface.is_valid = true; PictureRect::zero() });
1775
1776            surface_local_dirty_rect = surface_local_dirty_rect.union(&tile.cached_surface.local_dirty_rect);
1777
1778            // Update the world/device dirty rect
1779            let world_dirty_rect = map_pic_to_world.map(&tile.cached_surface.local_dirty_rect).expect("bug");
1780
1781            let device_rect = (tile.world_tile_rect * frame_context.global_device_pixel_scale).round();
1782            tile.device_dirty_rect = (world_dirty_rect * frame_context.global_device_pixel_scale)
1783                .round_out()
1784                .intersection(&device_rect)
1785                .unwrap_or_else(DeviceRect::zero);
1786
1787            if tile.cached_surface.is_valid {
1788                if frame_context.fb_config.testing {
1789                    debug_info.tiles.insert(
1790                        tile.tile_offset,
1791                        TileDebugInfo::Valid,
1792                    );
1793                }
1794            } else {
1795                // Track that actual tile rasterization is occurring
1796                frame_state.composite_state.did_rasterize_any_tile = true;
1797
1798                // Add this dirty rect to the dirty region tracker. This must be done outside the if statement below,
1799                // so that we include in the dirty region tiles that are handled by a background color only (no
1800                // surface allocation).
1801                tile_cache.dirty_region.add_dirty_region(
1802                    tile.cached_surface.local_dirty_rect,
1803                    frame_context.spatial_tree,
1804                );
1805
1806                // Ensure that this texture is allocated.
1807                if let TileSurface::Texture { ref mut descriptor } = tile.surface.as_mut().unwrap() {
1808                    match descriptor {
1809                        SurfaceTextureDescriptor::TextureCache { ref mut handle } => {
1810
1811                            frame_state.resource_cache.picture_textures.update(
1812                                tile_cache.current_tile_size,
1813                                handle,
1814                                &mut frame_state.resource_cache.texture_cache.next_id,
1815                                &mut frame_state.resource_cache.texture_cache.pending_updates,
1816                            );
1817                        }
1818                        SurfaceTextureDescriptor::Native { id } => {
1819                            if id.is_none() {
1820                                // Allocate a native surface id if we're in native compositing mode,
1821                                // and we don't have a surface yet (due to first frame, or destruction
1822                                // due to tile size changing etc).
1823                                if sub_slice.native_surface.is_none() {
1824                                    let opaque = frame_state
1825                                        .resource_cache
1826                                        .create_compositor_surface(
1827                                            tile_cache.virtual_offset,
1828                                            tile_cache.current_tile_size,
1829                                            true,
1830                                        );
1831
1832                                    let alpha = frame_state
1833                                        .resource_cache
1834                                        .create_compositor_surface(
1835                                            tile_cache.virtual_offset,
1836                                            tile_cache.current_tile_size,
1837                                            false,
1838                                        );
1839
1840                                    sub_slice.native_surface = Some(NativeSurface {
1841                                        opaque,
1842                                        alpha,
1843                                    });
1844                                }
1845
1846                                // Create the tile identifier and allocate it.
1847                                let surface_id = if tile.is_opaque {
1848                                    sub_slice.native_surface.as_ref().unwrap().opaque
1849                                } else {
1850                                    sub_slice.native_surface.as_ref().unwrap().alpha
1851                                };
1852
1853                                let tile_id = NativeTileId {
1854                                    surface_id,
1855                                    x: tile.tile_offset.x,
1856                                    y: tile.tile_offset.y,
1857                                };
1858
1859                                frame_state.resource_cache.create_compositor_tile(tile_id);
1860
1861                                *id = Some(tile_id);
1862                            }
1863                        }
1864                    }
1865
1866                    // The cast_unit() here is because the `content_origin` is expected to be in
1867                    // device pixels, however we're establishing raster roots for picture cache
1868                    // tiles meaning the `content_origin` needs to be in the local space of that root.
1869                    // TODO(gw): `content_origin` should actually be in RasterPixels to be consistent
1870                    //           with both local / screen raster modes, but this involves a lot of
1871                    //           changes to render task and picture code.
1872                    let content_origin_f = tile.cached_surface.local_rect.min.cast_unit() * device_pixel_scale;
1873                    let content_origin = content_origin_f.round();
1874                    // TODO: these asserts used to have a threshold of 0.01 but failed intermittently the
1875                    // gfx/layers/apz/test/mochitest/test_group_double_tap_zoom-2.html test on android.
1876                    // moving the rectangles in space mapping conversion code to the Box2D representaton
1877                    // made the failure happen more often.
1878                    debug_assert!((content_origin_f.x - content_origin.x).abs() < 0.15);
1879                    debug_assert!((content_origin_f.y - content_origin.y).abs() < 0.15);
1880
1881                    let surface = descriptor.resolve(
1882                        frame_state.resource_cache,
1883                        tile_cache.current_tile_size,
1884                    );
1885
1886                    // Recompute the scissor rect as the tile could have been invalidated since the first computation.
1887                    let scissor_rect = frame_state.composite_state.get_surface_rect(
1888                        &tile.cached_surface.local_dirty_rect,
1889                        &tile.cached_surface.local_rect,
1890                        tile_cache.transform_index,
1891                    ).to_i32();
1892
1893                    let composite_task_size = tile_cache.current_tile_size;
1894
1895                    let tile_key = TileKey {
1896                        sub_slice_index: SubSliceIndex::new(sub_slice_index),
1897                        tile_offset: tile.tile_offset,
1898                    };
1899
1900                    let mut clear_color = ColorF::TRANSPARENT;
1901
1902                    if SubSliceIndex::new(sub_slice_index).is_primary() {
1903                        if let Some(background_color) = tile_cache.background_color {
1904                            clear_color = background_color;
1905                        }
1906
1907                        // If this picture cache has a spanning_opaque_color, we will use
1908                        // that as the clear color. The primitive that was detected as a
1909                        // spanning primitive will have been set with IS_BACKDROP, causing
1910                        // it to be skipped and removing everything added prior to it
1911                        // during batching.
1912                        if let Some(color) = tile_cache.backdrop.spanning_opaque_color {
1913                            clear_color = color;
1914                        }
1915                    }
1916
1917                    let cmd_buffer_index = frame_state.cmd_buffers.create_cmd_buffer();
1918
1919                    // TODO(gw): As a performance optimization, we could skip the resolve picture
1920                    //           if the dirty rect is the same as the resolve rect (probably quite
1921                    //           common for effects that scroll underneath a backdrop-filter, for example).
1922                    let use_tile_composite = !tile.cached_surface.sub_graphs.is_empty();
1923
1924                    if use_tile_composite {
1925                        let mut local_content_rect = tile.cached_surface.local_dirty_rect;
1926
1927                        for (sub_graph_rect, surface_stack) in &tile.cached_surface.sub_graphs {
1928                            if let Some(dirty_sub_graph_rect) = sub_graph_rect.intersection(&tile.cached_surface.local_dirty_rect) {
1929                                for (composite_mode, surface_index) in surface_stack {
1930                                    let surface = &frame_state.surfaces[surface_index.0];
1931
1932                                    let rect = composite_mode.get_coverage(
1933                                        surface,
1934                                        Some(dirty_sub_graph_rect.cast_unit()),
1935                                    ).cast_unit();
1936
1937                                    local_content_rect = local_content_rect.union(&rect);
1938                                }
1939                            }
1940                        }
1941
1942                        // We know that we'll never need to sample > 300 device pixels outside the tile
1943                        // for blurring, so clamp the content rect here so that we don't try to allocate
1944                        // a really large surface in the case of a drop-shadow with large offset.
1945                        let max_content_rect = (tile.cached_surface.local_dirty_rect.cast_unit() * device_pixel_scale)
1946                            .inflate(
1947                                MAX_BLUR_RADIUS * BLUR_SAMPLE_SCALE,
1948                                MAX_BLUR_RADIUS * BLUR_SAMPLE_SCALE,
1949                            )
1950                            .round_out()
1951                            .to_i32();
1952
1953                        let content_device_rect = (local_content_rect.cast_unit() * device_pixel_scale)
1954                            .round_out()
1955                            .to_i32();
1956
1957                        let content_device_rect = content_device_rect
1958                            .intersection(&max_content_rect)
1959                            .expect("bug: no intersection with tile dirty rect: {content_device_rect:?} / {max_content_rect:?}");
1960
1961                        let content_task_size = content_device_rect.size();
1962                        let normalized_content_rect = content_task_size.into();
1963
1964                        let inner_offset = content_origin + scissor_rect.min.to_vector().to_f32();
1965                        let outer_offset = content_device_rect.min.to_f32();
1966                        let sub_rect_offset = (inner_offset - outer_offset).round().to_i32();
1967
1968                        let render_task_id = frame_state.rg_builder.add().init(
1969                            RenderTask::new_dynamic(
1970                                content_task_size,
1971                                RenderTaskKind::new_picture(
1972                                    content_task_size,
1973                                    true,
1974                                    content_device_rect.min.to_f32(),
1975                                    surface_spatial_node_index,
1976                                    // raster == surface implicitly for picture cache tiles
1977                                    surface_spatial_node_index,
1978                                    device_pixel_scale,
1979                                    Some(normalized_content_rect),
1980                                    None,
1981                                    Some(clear_color),
1982                                    cmd_buffer_index,
1983                                    false,
1984                                    None,
1985                                )
1986                            ),
1987                        );
1988
1989                        let composite_task_id = frame_state.rg_builder.add().init(
1990                            RenderTask::new(
1991                                RenderTaskLocation::Static {
1992                                    surface: StaticRenderTaskSurface::PictureCache {
1993                                        surface,
1994                                    },
1995                                    rect: composite_task_size.into(),
1996                                },
1997                                RenderTaskKind::new_tile_composite(
1998                                    sub_rect_offset,
1999                                    scissor_rect,
2000                                    valid_rect,
2001                                    clear_color,
2002                                ),
2003                            ),
2004                        );
2005
2006                        surface_render_tasks.insert(
2007                            tile_key,
2008                            SurfaceTileDescriptor {
2009                                current_task_id: render_task_id,
2010                                composite_task_id: Some(composite_task_id),
2011                                dirty_rect: tile.cached_surface.local_dirty_rect,
2012                            },
2013                        );
2014                    } else {
2015                        let render_task_id = frame_state.rg_builder.add().init(
2016                            RenderTask::new(
2017                                RenderTaskLocation::Static {
2018                                    surface: StaticRenderTaskSurface::PictureCache {
2019                                        surface,
2020                                    },
2021                                    rect: composite_task_size.into(),
2022                                },
2023                                RenderTaskKind::new_picture(
2024                                    composite_task_size,
2025                                    true,
2026                                    content_origin,
2027                                    surface_spatial_node_index,
2028                                    // raster == surface implicitly for picture cache tiles
2029                                    surface_spatial_node_index,
2030                                    device_pixel_scale,
2031                                    Some(scissor_rect),
2032                                    Some(valid_rect),
2033                                    Some(clear_color),
2034                                    cmd_buffer_index,
2035                                    false,
2036                                    None,
2037                                )
2038                            ),
2039                        );
2040
2041                        surface_render_tasks.insert(
2042                            tile_key,
2043                            SurfaceTileDescriptor {
2044                                current_task_id: render_task_id,
2045                                composite_task_id: None,
2046                                dirty_rect: tile.cached_surface.local_dirty_rect,
2047                            },
2048                        );
2049                    }
2050                }
2051
2052                if frame_context.fb_config.testing {
2053                    debug_info.tiles.insert(
2054                        tile.tile_offset,
2055                        TileDebugInfo::Dirty(DirtyTileDebugInfo {
2056                            local_valid_rect: tile.cached_surface.current_descriptor.local_valid_rect,
2057                            local_dirty_rect: tile.cached_surface.local_dirty_rect,
2058                        }),
2059                    );
2060                }
2061            }
2062
2063            let surface = tile.surface.as_ref().expect("no tile surface set!");
2064
2065            let descriptor = CompositeTileDescriptor {
2066                surface_kind: surface.into(),
2067                tile_id: tile.id,
2068            };
2069
2070            let (surface, is_opaque) = match surface {
2071                TileSurface::Color { color } => {
2072                    (CompositeTileSurface::Color { color: *color }, true)
2073                }
2074                TileSurface::Texture { descriptor, .. } => {
2075                    let surface = descriptor.resolve(frame_state.resource_cache, tile_cache.current_tile_size);
2076                    (
2077                        CompositeTileSurface::Texture { surface },
2078                        tile.is_opaque
2079                    )
2080                }
2081            };
2082
2083            if is_opaque {
2084                sub_slice.opaque_tile_descriptors.push(descriptor);
2085            } else {
2086                sub_slice.alpha_tile_descriptors.push(descriptor);
2087            }
2088
2089            let composite_tile = CompositeTile {
2090                kind: tile_kind(&surface, is_opaque),
2091                surface,
2092                local_rect: tile.cached_surface.local_rect,
2093                local_valid_rect: tile.cached_surface.current_descriptor.local_valid_rect,
2094                local_dirty_rect: tile.cached_surface.local_dirty_rect,
2095                device_clip_rect,
2096                z_id: tile.z_id,
2097                transform_index: tile_cache.transform_index,
2098                clip_index: tile_cache.compositor_clip,
2099                tile_id: Some(tile.id),
2100            };
2101
2102            sub_slice.composite_tiles.push(composite_tile);
2103
2104            // Now that the tile is valid, reset the dirty rect.
2105            tile.cached_surface.local_dirty_rect = PictureRect::zero();
2106            tile.cached_surface.is_valid = true;
2107        }
2108
2109        // Sort the tile descriptor lists, since iterating values in the tile_cache.tiles
2110        // hashmap doesn't provide any ordering guarantees, but we want to detect the
2111        // composite descriptor as equal if the tiles list is the same, regardless of
2112        // ordering.
2113        sub_slice.opaque_tile_descriptors.sort_by_key(|desc| desc.tile_id);
2114        sub_slice.alpha_tile_descriptors.sort_by_key(|desc| desc.tile_id);
2115    }
2116
2117    // Check to see if we should add backdrops as native surfaces.
2118    let backdrop_rect = tile_cache.backdrop.backdrop_rect
2119        .intersection(&tile_cache.local_rect)
2120        .and_then(|r| {
2121            r.intersection(&tile_cache.local_clip_rect)
2122    });
2123
2124    let mut backdrop_in_use_and_visible = false;
2125    if let Some(backdrop_rect) = backdrop_rect {
2126        let supports_surface_for_backdrop = match frame_state.composite_state.compositor_kind {
2127            CompositorKind::Draw { .. } | CompositorKind::Layer { .. } => {
2128                false
2129            }
2130            CompositorKind::Native { capabilities, .. } => {
2131                capabilities.supports_surface_for_backdrop
2132            }
2133        };
2134        if supports_surface_for_backdrop && !tile_cache.found_prims_after_backdrop && at_least_one_tile_visible {
2135            if let Some(BackdropKind::Color { color }) = tile_cache.backdrop.kind {
2136                backdrop_in_use_and_visible = true;
2137
2138                // We're going to let the compositor handle the backdrop as a native surface.
2139                // Hide all of our sub_slice tiles so they aren't also trying to draw it.
2140                for sub_slice in &mut tile_cache.sub_slices {
2141                    for tile in sub_slice.tiles.values_mut() {
2142                        tile.is_visible = false;
2143                    }
2144                }
2145
2146                // Destroy our backdrop surface if it doesn't match the new color.
2147                // TODO: This is a performance hit for animated color backdrops.
2148                if let Some(backdrop_surface) = &tile_cache.backdrop_surface {
2149                    if backdrop_surface.color != color {
2150                        frame_state.resource_cache.destroy_compositor_surface(backdrop_surface.id);
2151                        tile_cache.backdrop_surface = None;
2152                    }
2153                }
2154
2155                // Calculate the device_rect for the backdrop, which is just the backdrop_rect
2156                // converted into world space and scaled to device pixels.
2157                let world_backdrop_rect = map_pic_to_world.map(&backdrop_rect).expect("bug: unable to map backdrop rect");
2158                let device_rect = (world_backdrop_rect * frame_context.global_device_pixel_scale).round();
2159
2160                // If we already have a backdrop surface, update the device rect. Otherwise, create
2161                // a backdrop surface.
2162                if let Some(backdrop_surface) = &mut tile_cache.backdrop_surface {
2163                    backdrop_surface.device_rect = device_rect;
2164                } else {
2165                    // Create native compositor surface with color for the backdrop and store the id.
2166                    tile_cache.backdrop_surface = Some(BackdropSurface {
2167                        id: frame_state.resource_cache.create_compositor_backdrop_surface(color),
2168                        color,
2169                        device_rect,
2170                    });
2171                }
2172            }
2173        }
2174    }
2175
2176    if !backdrop_in_use_and_visible {
2177        if let Some(backdrop_surface) = &tile_cache.backdrop_surface {
2178            // We've already allocated a backdrop surface, but we're not using it.
2179            // Tell the compositor to get rid of it.
2180            frame_state.resource_cache.destroy_compositor_surface(backdrop_surface.id);
2181            tile_cache.backdrop_surface = None;
2182        }
2183    }
2184
2185    // If invalidation debugging is enabled, dump the picture cache state to a tree printer.
2186    if frame_context.debug_flags.contains(DebugFlags::INVALIDATION_DBG) {
2187        tile_cache.print();
2188    }
2189
2190    // If testing mode is enabled, write some information about the current state
2191    // of this picture cache (made available in RenderResults).
2192    if frame_context.fb_config.testing {
2193        debug_info.compositor_clip = tile_cache.compositor_clip.map(|clip_index| {
2194            let clip = frame_state.composite_state.get_compositor_clip(clip_index);
2195            CompositorClipDebugInfo {
2196                rect: clip.rect,
2197                radius: clip.radius,
2198            }
2199        });
2200
2201        frame_state.composite_state
2202            .picture_cache_debug
2203            .slices
2204            .insert(
2205                tile_cache.slice,
2206                debug_info,
2207            );
2208    }
2209
2210    let descriptor = SurfaceDescriptor::new_tiled(surface_render_tasks);
2211
2212    frame_state.surface_builder.push_surface(
2213        surface_index,
2214        false,
2215        surface_local_dirty_rect,
2216        Some(descriptor),
2217        frame_state.surfaces,
2218        frame_state.rg_builder,
2219    );
2220}
2221
2222fn compute_subpixel_mode(
2223    raster_config: &Option<RasterConfig>,
2224    tile_caches: &FastHashMap<SliceId, Box<TileCacheInstance>>,
2225    parent_subpixel_mode: SubpixelMode,
2226) -> SubpixelMode {
2227
2228    // Disallow subpixel AA if an intermediate surface is needed.
2229    // TODO(lsalzman): allow overriding parent if intermediate surface is opaque
2230    let subpixel_mode = match raster_config {
2231        Some(RasterConfig { ref composite_mode, .. }) => {
2232            let subpixel_mode = match composite_mode {
2233                PictureCompositeMode::TileCache { slice_id } => {
2234                    tile_caches[&slice_id].subpixel_mode
2235                }
2236                PictureCompositeMode::Blit(..) |
2237                PictureCompositeMode::ComponentTransferFilter(..) |
2238                PictureCompositeMode::Filter(..) |
2239                PictureCompositeMode::MixBlend(..) |
2240                PictureCompositeMode::IntermediateSurface |
2241                PictureCompositeMode::SVGFEGraph(..) => {
2242                    // TODO(gw): We can take advantage of the same logic that
2243                    //           exists in the opaque rect detection for tile
2244                    //           caches, to allow subpixel text on other surfaces
2245                    //           that can be detected as opaque.
2246                    SubpixelMode::Deny
2247                }
2248            };
2249
2250            subpixel_mode
2251        }
2252        None => {
2253            SubpixelMode::Allow
2254        }
2255    };
2256
2257    // Still disable subpixel AA if parent forbids it
2258    let subpixel_mode = match (parent_subpixel_mode, subpixel_mode) {
2259        (SubpixelMode::Allow, SubpixelMode::Allow) => {
2260            // Both parent and this surface unconditionally allow subpixel AA
2261            SubpixelMode::Allow
2262        }
2263        (SubpixelMode::Allow, SubpixelMode::Conditional { allowed_rect, prohibited_rect }) => {
2264            // Parent allows, but we are conditional subpixel AA
2265            SubpixelMode::Conditional {
2266                allowed_rect,
2267                prohibited_rect,
2268            }
2269        }
2270        (SubpixelMode::Conditional { allowed_rect, prohibited_rect }, SubpixelMode::Allow) => {
2271            // Propagate conditional subpixel mode to child pictures that allow subpixel AA
2272            SubpixelMode::Conditional {
2273                allowed_rect,
2274                prohibited_rect,
2275            }
2276        }
2277        (SubpixelMode::Conditional { .. }, SubpixelMode::Conditional { ..}) => {
2278            unreachable!("bug: only top level picture caches have conditional subpixel");
2279        }
2280        (SubpixelMode::Deny, _) | (_, SubpixelMode::Deny) => {
2281            // Either parent or this surface explicitly deny subpixel, these take precedence
2282            SubpixelMode::Deny
2283        }
2284    };
2285
2286    subpixel_mode
2287}
2288
2289#[test]
2290fn test_large_surface_scale_1() {
2291    use crate::spatial_tree::{SceneSpatialTree, SpatialTree};
2292
2293    let mut cst = SceneSpatialTree::new();
2294    let root_reference_frame_index = cst.root_reference_frame_index();
2295
2296    let mut spatial_tree = SpatialTree::new();
2297    spatial_tree.apply_updates(cst.end_frame_and_get_pending_updates());
2298    spatial_tree.update_tree(&SceneProperties::new());
2299
2300    let map_local_to_picture = SpaceMapper::new_with_target(
2301        root_reference_frame_index,
2302        root_reference_frame_index,
2303        PictureRect::max_rect(),
2304        &spatial_tree,
2305    );
2306
2307    let mut surfaces = vec![
2308        SurfaceInfo {
2309            unclipped_local_rect: PictureRect::max_rect(),
2310            clipped_local_rect: PictureRect::max_rect(),
2311            is_opaque: true,
2312            clipping_rect: PictureRect::max_rect(),
2313            culling_rect: VisRect::max_rect(),
2314            map_local_to_picture: map_local_to_picture.clone(),
2315            raster_spatial_node_index: root_reference_frame_index,
2316            surface_spatial_node_index: root_reference_frame_index,
2317            visibility_spatial_node_index: root_reference_frame_index,
2318            device_pixel_scale: DevicePixelScale::new(1.0),
2319            world_scale_factors: (1.0, 1.0),
2320            local_scale: (1.0, 1.0),
2321            allow_snapping: true,
2322            force_scissor_rect: false,
2323        },
2324        SurfaceInfo {
2325            unclipped_local_rect: PictureRect::new(
2326                PicturePoint::new(52.76350021362305, 0.0),
2327                PicturePoint::new(159.6738739013672, 35.0),
2328            ),
2329            clipped_local_rect: PictureRect::max_rect(),
2330            is_opaque: true,
2331            clipping_rect: PictureRect::max_rect(),
2332            culling_rect: VisRect::max_rect(),
2333            map_local_to_picture,
2334            raster_spatial_node_index: root_reference_frame_index,
2335            surface_spatial_node_index: root_reference_frame_index,
2336            visibility_spatial_node_index: root_reference_frame_index,
2337            device_pixel_scale: DevicePixelScale::new(43.82798767089844),
2338            world_scale_factors: (1.0, 1.0),
2339            local_scale: (1.0, 1.0),
2340            allow_snapping: true,
2341            force_scissor_rect: false,
2342        },
2343    ];
2344
2345    get_surface_rects(
2346        SurfaceIndex(1),
2347        &PictureCompositeMode::Blit(BlitReason::BLEND_MODE),
2348        SurfaceIndex(0),
2349        &mut surfaces,
2350        &spatial_tree,
2351        MAX_SURFACE_SIZE as f32,
2352        false,
2353    );
2354}
2355
2356#[test]
2357fn test_drop_filter_dirty_region_outside_prim() {
2358    // Ensure that if we have a drop-filter where the content of the
2359    // shadow is outside the dirty rect, but blurred pixels from that
2360    // content will affect the dirty rect, that we correctly calculate
2361    // the required region of the drop-filter input
2362
2363    use api::Shadow;
2364    use crate::spatial_tree::{SceneSpatialTree, SpatialTree};
2365
2366    let mut cst = SceneSpatialTree::new();
2367    let root_reference_frame_index = cst.root_reference_frame_index();
2368
2369    let mut spatial_tree = SpatialTree::new();
2370    spatial_tree.apply_updates(cst.end_frame_and_get_pending_updates());
2371    spatial_tree.update_tree(&SceneProperties::new());
2372
2373    let map_local_to_picture = SpaceMapper::new_with_target(
2374        root_reference_frame_index,
2375        root_reference_frame_index,
2376        PictureRect::max_rect(),
2377        &spatial_tree,
2378    );
2379
2380    let mut surfaces = vec![
2381        SurfaceInfo {
2382            unclipped_local_rect: PictureRect::max_rect(),
2383            clipped_local_rect: PictureRect::max_rect(),
2384            is_opaque: true,
2385            clipping_rect: PictureRect::max_rect(),
2386            map_local_to_picture: map_local_to_picture.clone(),
2387            raster_spatial_node_index: root_reference_frame_index,
2388            surface_spatial_node_index: root_reference_frame_index,
2389            visibility_spatial_node_index: root_reference_frame_index,
2390            device_pixel_scale: DevicePixelScale::new(1.0),
2391            world_scale_factors: (1.0, 1.0),
2392            local_scale: (1.0, 1.0),
2393            allow_snapping: true,
2394            force_scissor_rect: false,
2395            culling_rect: VisRect::max_rect(),
2396        },
2397        SurfaceInfo {
2398            unclipped_local_rect: PictureRect::new(
2399                PicturePoint::new(0.0, 0.0),
2400                PicturePoint::new(750.0, 450.0),
2401            ),
2402            clipped_local_rect: PictureRect::new(
2403                PicturePoint::new(0.0, 0.0),
2404                PicturePoint::new(750.0, 450.0),
2405            ),
2406            is_opaque: true,
2407            clipping_rect: PictureRect::max_rect(),
2408            map_local_to_picture,
2409            raster_spatial_node_index: root_reference_frame_index,
2410            surface_spatial_node_index: root_reference_frame_index,
2411            visibility_spatial_node_index: root_reference_frame_index,
2412            device_pixel_scale: DevicePixelScale::new(1.0),
2413            world_scale_factors: (1.0, 1.0),
2414            local_scale: (1.0, 1.0),
2415            allow_snapping: true,
2416            force_scissor_rect: false,
2417            culling_rect: VisRect::max_rect(),
2418        },
2419    ];
2420
2421    let shadows = smallvec![
2422        Shadow {
2423            offset: LayoutVector2D::zero(),
2424            color: ColorF::BLACK,
2425            blur_radius: 75.0,
2426        },
2427    ];
2428
2429    let composite_mode = PictureCompositeMode::Filter(Filter::DropShadows(shadows));
2430
2431    // Ensure we get a valid and correct render task size when dirty region covers entire screen
2432    let info = get_surface_rects(
2433        SurfaceIndex(1),
2434        &composite_mode,
2435        SurfaceIndex(0),
2436        &mut surfaces,
2437        &spatial_tree,
2438        MAX_SURFACE_SIZE as f32,
2439        false,
2440    ).expect("No surface rect");
2441    assert_eq!(info.task_size, DeviceIntSize::new(1200, 900));
2442
2443    // Ensure we get a valid and correct render task size when dirty region is outside filter content
2444    surfaces[0].clipping_rect = PictureRect::new(
2445        PicturePoint::new(768.0, 128.0),
2446        PicturePoint::new(1024.0, 256.0),
2447    );
2448    let info = get_surface_rects(
2449        SurfaceIndex(1),
2450        &composite_mode,
2451        SurfaceIndex(0),
2452        &mut surfaces,
2453        &spatial_tree,
2454        MAX_SURFACE_SIZE as f32,
2455        false,
2456    ).expect("No surface rect");
2457    assert_eq!(info.task_size, DeviceIntSize::new(432, 578));
2458}
2459
2460#[test]
2461fn test_drop_filter_partial_dirty_content_inflate() {
2462    // Bug 1822189: When the parent's dirty region (clipping_rect here) overlaps
2463    // the drop-shadow's image content but stops short of the picture's full
2464    // unclipped extent, the source-texture allocation must include enough blur
2465    // margin around the image content to keep the picture_task texture's edges
2466    // in transparent space. Otherwise the content quad's blur margin samples
2467    // UVs > 1 and the texture-edge image content bleeds into the visible
2468    // result.
2469
2470    use api::Shadow;
2471    use crate::spatial_tree::{SceneSpatialTree, SpatialTree};
2472
2473    let mut cst = SceneSpatialTree::new();
2474    let root_reference_frame_index = cst.root_reference_frame_index();
2475
2476    let mut spatial_tree = SpatialTree::new();
2477    spatial_tree.apply_updates(cst.end_frame_and_get_pending_updates());
2478    spatial_tree.update_tree(&SceneProperties::new());
2479
2480    let map_local_to_picture = SpaceMapper::new_with_target(
2481        root_reference_frame_index,
2482        root_reference_frame_index,
2483        PictureRect::max_rect(),
2484        &spatial_tree,
2485    );
2486
2487    // 500x500 image content, drop-shadow with non-zero offset.
2488    let mut surfaces = vec![
2489        SurfaceInfo {
2490            unclipped_local_rect: PictureRect::max_rect(),
2491            clipped_local_rect: PictureRect::max_rect(),
2492            is_opaque: true,
2493            // Parent's clipping_rect = dirty region that partially overlaps
2494            // the image but stops short of the full picture extent. This is
2495            // the scenario where the bug used to leave the texture's right
2496            // and bottom edges on image content.
2497            clipping_rect: PictureRect::new(
2498                PicturePoint::new(0.0, 0.0),
2499                PicturePoint::new(683.0, 341.0),
2500            ),
2501            map_local_to_picture: map_local_to_picture.clone(),
2502            raster_spatial_node_index: root_reference_frame_index,
2503            surface_spatial_node_index: root_reference_frame_index,
2504            visibility_spatial_node_index: root_reference_frame_index,
2505            device_pixel_scale: DevicePixelScale::new(1.0),
2506            world_scale_factors: (1.0, 1.0),
2507            local_scale: (1.0, 1.0),
2508            allow_snapping: true,
2509            force_scissor_rect: false,
2510            culling_rect: VisRect::max_rect(),
2511        },
2512        SurfaceInfo {
2513            unclipped_local_rect: PictureRect::new(
2514                PicturePoint::new(0.0, 0.0),
2515                PicturePoint::new(500.0, 500.0),
2516            ),
2517            clipped_local_rect: PictureRect::new(
2518                PicturePoint::new(0.0, 0.0),
2519                PicturePoint::new(500.0, 500.0),
2520            ),
2521            is_opaque: true,
2522            clipping_rect: PictureRect::max_rect(),
2523            map_local_to_picture,
2524            raster_spatial_node_index: root_reference_frame_index,
2525            surface_spatial_node_index: root_reference_frame_index,
2526            visibility_spatial_node_index: root_reference_frame_index,
2527            device_pixel_scale: DevicePixelScale::new(1.0),
2528            world_scale_factors: (1.0, 1.0),
2529            local_scale: (1.0, 1.0),
2530            allow_snapping: true,
2531            force_scissor_rect: false,
2532            culling_rect: VisRect::max_rect(),
2533        },
2534    ];
2535
2536    let shadows = smallvec![
2537        Shadow {
2538            offset: LayoutVector2D::new(400.0, 100.0),
2539            color: ColorF::BLACK,
2540            blur_radius: 20.0,
2541        },
2542    ];
2543
2544    let composite_mode = PictureCompositeMode::Filter(Filter::DropShadows(shadows));
2545
2546    let info = get_surface_rects(
2547        SurfaceIndex(1),
2548        &composite_mode,
2549        SurfaceIndex(0),
2550        &mut surfaces,
2551        &spatial_tree,
2552        MAX_SURFACE_SIZE as f32,
2553        false,
2554    ).expect("No surface rect");
2555
2556    // With the fix, the image-content side of required_local_rect is inflated
2557    // by blur (20 * BLUR_SAMPLE_SCALE = 60) so the texture extends to
2558    // (-60, -60)..(560, 401) — placing the right and bottom edges in the
2559    // transparent blur margin around the image rather than on image content.
2560    // Width=620, height=461. Without the fix this would be (560, 401) i.e.
2561    // 560x401 with the texture's right and bottom edges sitting on the
2562    // 500x500 image content, producing the visible bleed.
2563    assert_eq!(info.task_size, DeviceIntSize::new(620, 461));
2564}