tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit a3ba0470371be7fc18d6f922c20c790d0d6fd459
parent dfbf5f194d3ffd414f4e983c33268ab12eeee44d
Author: agoloman <agoloman@mozilla.com>
Date:   Fri, 12 Dec 2025 10:54:29 +0200

Revert "Bug 1998913 - Part 4 - Move SVG filter code into its own module. r=gfx-reviewers,gw" for causing WR bustages.

This reverts commit dfbf5f194d3ffd414f4e983c33268ab12eeee44d.

Revert "Bug 1998913 - Part 3 - Extract surface-related code from picture.rs. r=gfx-reviewers,gw"

This reverts commit ae7e71f9d748c47898dbc244a5758809ef87fe4b.

Revert "Bug 1998913 - Part 2 - Extract dependency tracking and quadtree. r=gfx-reviewers,gw"

This reverts commit 531e824b59231ebd450e8ba25ac5206be710edaf.

Revert "Bug 1998913 - Part 1 - Extract independent data structures. r=gfx-reviewers,gw"

This reverts commit 456ecdfabef1693737fce2795682bb1ca4dce3c4.

Diffstat:
Mgfx/wr/webrender/src/command_buffer.rs | 2+-
Mgfx/wr/webrender/src/composite.rs | 4+---
Mgfx/wr/webrender/src/frame_builder.rs | 3+--
Mgfx/wr/webrender/src/internal_types.rs | 553++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Dgfx/wr/webrender/src/invalidation/dependency.rs | 425-------------------------------------------------------------------------------
Dgfx/wr/webrender/src/invalidation/mod.rs | 158-------------------------------------------------------------------------------
Dgfx/wr/webrender/src/invalidation/quadtree.rs | 440-------------------------------------------------------------------------------
Mgfx/wr/webrender/src/lib.rs | 10+++-------
Mgfx/wr/webrender/src/picture.rs | 5444+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mgfx/wr/webrender/src/picture_graph.rs | 2+-
Mgfx/wr/webrender/src/prepare.rs | 5++---
Mgfx/wr/webrender/src/prim_store/picture.rs | 4++--
Mgfx/wr/webrender/src/render_backend.rs | 3+--
Mgfx/wr/webrender/src/render_target.rs | 6++----
Mgfx/wr/webrender/src/render_task.rs | 6++----
Mgfx/wr/webrender/src/renderer/init.rs | 3++-
Mgfx/wr/webrender/src/renderer/mod.rs | 3+--
Mgfx/wr/webrender/src/scene_building.rs | 3+--
Mgfx/wr/webrender/src/surface.rs | 614++-----------------------------------------------------------------------------
Dgfx/wr/webrender/src/svg_filter.rs | 839-------------------------------------------------------------------------------
Agfx/wr/webrender/src/tile_cache.rs | 779+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dgfx/wr/webrender/src/tile_cache/mod.rs | 3509-------------------------------------------------------------------------------
Dgfx/wr/webrender/src/tile_cache/slice_builder.rs | 779-------------------------------------------------------------------------------
Mgfx/wr/webrender/src/visibility.rs | 6++----
24 files changed, 6715 insertions(+), 6885 deletions(-)

diff --git a/gfx/wr/webrender/src/command_buffer.rs b/gfx/wr/webrender/src/command_buffer.rs @@ -4,7 +4,7 @@ use api::units::PictureRect; use crate::pattern::{PatternKind, PatternShaderInput}; -use crate::{spatial_tree::SpatialNodeIndex, render_task_graph::RenderTaskId, surface::SurfaceTileDescriptor, tile_cache::TileKey, renderer::GpuBufferAddress, FastHashMap, prim_store::PrimitiveInstanceIndex}; +use crate::{spatial_tree::SpatialNodeIndex, render_task_graph::RenderTaskId, surface::SurfaceTileDescriptor, picture::TileKey, renderer::GpuBufferAddress, FastHashMap, prim_store::PrimitiveInstanceIndex}; use crate::gpu_types::{QuadSegment, TransformPaletteId}; use crate::segment::EdgeAaSegmentMask; diff --git a/gfx/wr/webrender/src/composite.rs b/gfx/wr/webrender/src/composite.rs @@ -10,9 +10,7 @@ use crate::renderer::GpuBufferBuilderF; use euclid::Box2D; use crate::gpu_types::{ZBufferId, ZBufferIdGenerator}; use crate::internal_types::{FrameAllocator, FrameMemory, FrameVec, TextureSource}; -use crate::picture::{ImageDependency, ResolvedSurfaceTexture}; -use crate::tile_cache::{TileCacheInstance, TileSurface}; -use crate::tile_cache::TileId; +use crate::picture::{ImageDependency, ResolvedSurfaceTexture, TileCacheInstance, TileId, TileSurface}; use crate::prim_store::DeferredResolve; use crate::resource_cache::{ImageRequest, ResourceCache}; use crate::util::{extract_inner_rect_safe, Preallocator, ScaleOffset}; diff --git a/gfx/wr/webrender/src/frame_builder.rs b/gfx/wr/webrender/src/frame_builder.rs @@ -16,8 +16,7 @@ use crate::debug_item::DebugItem; use crate::gpu_types::{ImageBrushPrimitiveData, PrimitiveHeaders, TransformPalette, ZBufferIdGenerator}; use crate::gpu_types::{QuadSegment, TransformData}; use crate::internal_types::{FastHashMap, PlaneSplitter, FrameStamp}; -use crate::picture::{DirtyRegion}; -use crate::tile_cache::{SliceId, TileCacheInstance}; +use crate::picture::{DirtyRegion, SliceId, TileCacheInstance}; use crate::picture::{SurfaceInfo, SurfaceIndex, ResolvedSurfaceTexture}; use crate::picture::{SubpixelMode, RasterConfig, PictureCompositeMode}; use crate::prepare::prepare_picture; diff --git a/gfx/wr/webrender/src/internal_types.rs b/gfx/wr/webrender/src/internal_types.rs @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use api::{ColorF, DocumentId, ExternalImageId, PrimitiveFlags, Parameter, RenderReasons}; -use api::{ImageFormat, NotificationRequest, Shadow, FilterOp, ImageBufferKind}; +use api::{ImageFormat, NotificationRequest, Shadow, FilterOpGraphPictureBufferId, FilterOpGraphPictureReference, FilterOpGraphNode, FilterOp, ImageBufferKind}; use api::{FramePublishId, TextureCacheCategory}; use api::units::*; use crate::render_api::DebugCommand; @@ -15,7 +15,7 @@ use crate::frame_builder::Frame; use crate::profiler::TransactionProfile; use crate::spatial_tree::SpatialNodeIndex; use crate::prim_store::PrimitiveInstanceIndex; -use crate::svg_filter::{FilterGraphNode, FilterGraphOp, FilterGraphPictureReference}; +use crate::filterdata::FilterDataHandle; use rustc_hash::FxHasher; use plane_split::BspSplitter; use smallvec::SmallVec; @@ -215,6 +215,555 @@ pub struct PlaneSplitterIndex(pub usize); /// An arbitrary number which we assume opacity is invisible below. const OPACITY_EPSILON: f32 = 0.001; +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct FilterGraphPictureReference { + /// Id of the picture in question in a namespace unique to this filter DAG, + /// some are special values like + /// FilterPrimitiveDescription::kPrimitiveIndexSourceGraphic. + pub buffer_id: FilterOpGraphPictureBufferId, + /// Set by wrap_prim_with_filters to the subregion of the input node, may + /// also have been offset for feDropShadow or feOffset + pub subregion: LayoutRect, + /// During scene build this is the offset to apply to the input subregion + /// for feOffset, which can be optimized away by pushing its offset and + /// subregion crop to downstream nodes. This is always zero in render tasks + /// where it has already been applied to subregion by that point. Not used + /// in get_coverage_svgfe because source_padding/target_padding represent + /// the offset there. + pub offset: LayoutVector2D, + /// Equal to the inflate value of the referenced buffer, or 0 + pub inflate: i16, + /// Padding on each side to represent how this input is read relative to the + /// node's output subregion, this represents what the operation needs to + /// read from ths input, which may be blurred or offset. + pub source_padding: LayoutRect, + /// Padding on each side to represent how this input affects the node's + /// subregion, this can be used to calculate target subregion based on + /// SourceGraphic subregion. This is usually equal to source_padding except + /// offset in the opposite direction, inflates typically do the same thing + /// to both types of padding. + pub target_padding: LayoutRect, +} + +impl From<FilterOpGraphPictureReference> for FilterGraphPictureReference { + fn from(pic: FilterOpGraphPictureReference) -> Self { + FilterGraphPictureReference{ + buffer_id: pic.buffer_id, + // All of these are set by wrap_prim_with_filters + subregion: LayoutRect::zero(), + offset: LayoutVector2D::zero(), + inflate: 0, + source_padding: LayoutRect::zero(), + target_padding: LayoutRect::zero(), + } + } +} + +pub const SVGFE_CONVOLVE_DIAMETER_LIMIT: usize = 5; +pub const SVGFE_CONVOLVE_VALUES_LIMIT: usize = SVGFE_CONVOLVE_DIAMETER_LIMIT * + SVGFE_CONVOLVE_DIAMETER_LIMIT; + +#[derive(Clone, Debug)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub enum FilterGraphOp { + /// Filter that copies the SourceGraphic image into the specified subregion, + /// This is intentionally the only way to get SourceGraphic into the graph, + /// as the filter region must be applied before it is used. + /// parameters: FilterOpGraphNode + /// SVG filter semantics - no inputs, no linear + SVGFESourceGraphic, + /// Filter that copies the SourceAlpha image into the specified subregion, + /// This is intentionally the only way to get SourceAlpha into the graph, + /// as the filter region must be applied before it is used. + /// parameters: FilterOpGraphNode + /// SVG filter semantics - no inputs, no linear + SVGFESourceAlpha, + /// Filter that does no transformation of the colors, used to implement a + /// few things like SVGFEOffset, and this is the default value in + /// impl_default_for_enums. + /// parameters: FilterGraphNode + /// SVG filter semantics - selectable input with offset + SVGFEIdentity, + /// represents CSS opacity property as a graph node like the rest of the + /// SVGFE* filters + /// parameters: FilterGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + SVGFEOpacity{valuebinding: api::PropertyBinding<f32>, value: f32}, + /// convert a color image to an alpha channel - internal use; generated by + /// SVGFilterInstance::GetOrCreateSourceAlphaIndex(). + SVGFEToAlpha, + /// combine 2 images with SVG_FEBLEND_MODE_DARKEN + /// parameters: FilterGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#feBlendElement + SVGFEBlendDarken, + /// combine 2 images with SVG_FEBLEND_MODE_LIGHTEN + /// parameters: FilterGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#feBlendElement + SVGFEBlendLighten, + /// combine 2 images with SVG_FEBLEND_MODE_MULTIPLY + /// parameters: FilterGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#feBlendElement + SVGFEBlendMultiply, + /// combine 2 images with SVG_FEBLEND_MODE_NORMAL + /// parameters: FilterGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#feBlendElement + SVGFEBlendNormal, + /// combine 2 images with SVG_FEBLEND_MODE_SCREEN + /// parameters: FilterGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#feBlendElement + SVGFEBlendScreen, + /// combine 2 images with SVG_FEBLEND_MODE_OVERLAY + /// parameters: FilterOpGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Source: https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode + SVGFEBlendOverlay, + /// combine 2 images with SVG_FEBLEND_MODE_COLOR_DODGE + /// parameters: FilterOpGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Source: https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode + SVGFEBlendColorDodge, + /// combine 2 images with SVG_FEBLEND_MODE_COLOR_BURN + /// parameters: FilterOpGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Source: https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode + SVGFEBlendColorBurn, + /// combine 2 images with SVG_FEBLEND_MODE_HARD_LIGHT + /// parameters: FilterOpGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Source: https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode + SVGFEBlendHardLight, + /// combine 2 images with SVG_FEBLEND_MODE_SOFT_LIGHT + /// parameters: FilterOpGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Source: https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode + SVGFEBlendSoftLight, + /// combine 2 images with SVG_FEBLEND_MODE_DIFFERENCE + /// parameters: FilterOpGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Source: https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode + SVGFEBlendDifference, + /// combine 2 images with SVG_FEBLEND_MODE_EXCLUSION + /// parameters: FilterOpGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Source: https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode + SVGFEBlendExclusion, + /// combine 2 images with SVG_FEBLEND_MODE_HUE + /// parameters: FilterOpGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Source: https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode + SVGFEBlendHue, + /// combine 2 images with SVG_FEBLEND_MODE_SATURATION + /// parameters: FilterOpGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Source: https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode + SVGFEBlendSaturation, + /// combine 2 images with SVG_FEBLEND_MODE_COLOR + /// parameters: FilterOpGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Source: https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode + SVGFEBlendColor, + /// combine 2 images with SVG_FEBLEND_MODE_LUMINOSITY + /// parameters: FilterOpGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Source: https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode + SVGFEBlendLuminosity, + /// transform colors of image through 5x4 color matrix (transposed for + /// efficiency) + /// parameters: FilterGraphNode, matrix[5][4] + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#feColorMatrixElement + SVGFEColorMatrix{values: [f32; 20]}, + /// transform colors of image through configurable gradients with component + /// swizzle + /// parameters: FilterGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#feComponentTransferElement + SVGFEComponentTransfer, + /// Processed version of SVGFEComponentTransfer with the FilterData + /// replaced by an interned handle, this is made in wrap_prim_with_filters. + /// Aside from the interned handle, creates_pixels indicates if the transfer + /// parameters will probably fill the entire subregion with non-zero alpha. + SVGFEComponentTransferInterned{handle: FilterDataHandle, creates_pixels: bool}, + /// composite 2 images with chosen composite mode with parameters for that + /// mode + /// parameters: FilterGraphNode, k1, k2, k3, k4 + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#feCompositeElement + SVGFECompositeArithmetic{k1: f32, k2: f32, k3: f32, k4: f32}, + /// composite 2 images with chosen composite mode with parameters for that + /// mode + /// parameters: FilterGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#feCompositeElement + SVGFECompositeATop, + /// composite 2 images with chosen composite mode with parameters for that + /// mode + /// parameters: FilterGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#feCompositeElement + SVGFECompositeIn, + /// composite 2 images with chosen composite mode with parameters for that + /// mode + /// parameters: FilterOpGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Docs: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feComposite + SVGFECompositeLighter, + /// composite 2 images with chosen composite mode with parameters for that + /// mode + /// parameters: FilterGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#feCompositeElement + SVGFECompositeOut, + /// composite 2 images with chosen composite mode with parameters for that + /// mode + /// parameters: FilterGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#feCompositeElement + SVGFECompositeOver, + /// composite 2 images with chosen composite mode with parameters for that + /// mode + /// parameters: FilterGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#feCompositeElement + SVGFECompositeXOR, + /// transform image through convolution matrix of up to 25 values (spec + /// allows more but for performance reasons we do not) + /// parameters: FilterGraphNode, orderX, orderY, kernelValues[25], divisor, + /// bias, targetX, targetY, kernelUnitLengthX, kernelUnitLengthY, + /// preserveAlpha + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#feConvolveMatrixElement + SVGFEConvolveMatrixEdgeModeDuplicate{order_x: i32, order_y: i32, + kernel: [f32; SVGFE_CONVOLVE_VALUES_LIMIT], divisor: f32, bias: f32, + target_x: i32, target_y: i32, kernel_unit_length_x: f32, + kernel_unit_length_y: f32, preserve_alpha: i32}, + /// transform image through convolution matrix of up to 25 values (spec + /// allows more but for performance reasons we do not) + /// parameters: FilterGraphNode, orderX, orderY, kernelValues[25], divisor, + /// bias, targetX, targetY, kernelUnitLengthX, kernelUnitLengthY, + /// preserveAlpha + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#feConvolveMatrixElement + SVGFEConvolveMatrixEdgeModeNone{order_x: i32, order_y: i32, + kernel: [f32; SVGFE_CONVOLVE_VALUES_LIMIT], divisor: f32, bias: f32, + target_x: i32, target_y: i32, kernel_unit_length_x: f32, + kernel_unit_length_y: f32, preserve_alpha: i32}, + /// transform image through convolution matrix of up to 25 values (spec + /// allows more but for performance reasons we do not) + /// parameters: FilterGraphNode, orderX, orderY, kernelValues[25], divisor, + /// bias, targetX, targetY, kernelUnitLengthX, kernelUnitLengthY, + /// preserveAlpha + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#feConvolveMatrixElement + SVGFEConvolveMatrixEdgeModeWrap{order_x: i32, order_y: i32, + kernel: [f32; SVGFE_CONVOLVE_VALUES_LIMIT], divisor: f32, bias: f32, + target_x: i32, target_y: i32, kernel_unit_length_x: f32, + kernel_unit_length_y: f32, preserve_alpha: i32}, + /// calculate lighting based on heightmap image with provided values for a + /// distant light source with specified direction + /// parameters: FilterGraphNode, surfaceScale, diffuseConstant, + /// kernelUnitLengthX, kernelUnitLengthY, azimuth, elevation + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEDiffuseLightingElement + /// https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEDistantLightElement + SVGFEDiffuseLightingDistant{surface_scale: f32, diffuse_constant: f32, + kernel_unit_length_x: f32, kernel_unit_length_y: f32, azimuth: f32, + elevation: f32}, + /// calculate lighting based on heightmap image with provided values for a + /// point light source at specified location + /// parameters: FilterGraphNode, surfaceScale, diffuseConstant, + /// kernelUnitLengthX, kernelUnitLengthY, x, y, z + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEDiffuseLightingElement + /// https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEPointLightElement + SVGFEDiffuseLightingPoint{surface_scale: f32, diffuse_constant: f32, + kernel_unit_length_x: f32, kernel_unit_length_y: f32, x: f32, y: f32, + z: f32}, + /// calculate lighting based on heightmap image with provided values for a + /// spot light source at specified location pointing at specified target + /// location with specified hotspot sharpness and cone angle + /// parameters: FilterGraphNode, surfaceScale, diffuseConstant, + /// kernelUnitLengthX, kernelUnitLengthY, x, y, z, pointsAtX, pointsAtY, + /// pointsAtZ, specularExponent, limitingConeAngle + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEDiffuseLightingElement + /// https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFESpotLightElement + SVGFEDiffuseLightingSpot{surface_scale: f32, diffuse_constant: f32, + kernel_unit_length_x: f32, kernel_unit_length_y: f32, x: f32, y: f32, + z: f32, points_at_x: f32, points_at_y: f32, points_at_z: f32, + cone_exponent: f32, limiting_cone_angle: f32}, + /// calculate a distorted version of first input image using offset values + /// from second input image at specified intensity + /// parameters: FilterGraphNode, scale, xChannelSelector, yChannelSelector + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEDisplacementMapElement + SVGFEDisplacementMap{scale: f32, x_channel_selector: u32, + y_channel_selector: u32}, + /// create and merge a dropshadow version of the specified image's alpha + /// channel with specified offset and blur radius + /// parameters: FilterGraphNode, flood_color, flood_opacity, dx, dy, + /// stdDeviationX, stdDeviationY + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEDropShadowElement + SVGFEDropShadow{color: ColorF, dx: f32, dy: f32, std_deviation_x: f32, + std_deviation_y: f32}, + /// synthesize a new image of specified size containing a solid color + /// parameters: FilterGraphNode, color + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEFloodElement + SVGFEFlood{color: ColorF}, + /// create a blurred version of the input image + /// parameters: FilterGraphNode, stdDeviationX, stdDeviationY + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEGaussianBlurElement + SVGFEGaussianBlur{std_deviation_x: f32, std_deviation_y: f32}, + /// synthesize a new image based on a url (i.e. blob image source) + /// parameters: FilterGraphNode, + /// samplingFilter (see SamplingFilter in Types.h), transform + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEImageElement + SVGFEImage{sampling_filter: u32, matrix: [f32; 6]}, + /// create a new image based on the input image with the contour stretched + /// outward (dilate operator) + /// parameters: FilterGraphNode, radiusX, radiusY + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEMorphologyElement + SVGFEMorphologyDilate{radius_x: f32, radius_y: f32}, + /// create a new image based on the input image with the contour shrunken + /// inward (erode operator) + /// parameters: FilterGraphNode, radiusX, radiusY + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEMorphologyElement + SVGFEMorphologyErode{radius_x: f32, radius_y: f32}, + /// calculate lighting based on heightmap image with provided values for a + /// distant light source with specified direction + /// parameters: FilerData, surfaceScale, specularConstant, specularExponent, + /// kernelUnitLengthX, kernelUnitLengthY, azimuth, elevation + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFESpecularLightingElement + /// https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEDistantLightElement + SVGFESpecularLightingDistant{surface_scale: f32, specular_constant: f32, + specular_exponent: f32, kernel_unit_length_x: f32, + kernel_unit_length_y: f32, azimuth: f32, elevation: f32}, + /// calculate lighting based on heightmap image with provided values for a + /// point light source at specified location + /// parameters: FilterGraphNode, surfaceScale, specularConstant, + /// specularExponent, kernelUnitLengthX, kernelUnitLengthY, x, y, z + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFESpecularLightingElement + /// https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEPointLightElement + SVGFESpecularLightingPoint{surface_scale: f32, specular_constant: f32, + specular_exponent: f32, kernel_unit_length_x: f32, + kernel_unit_length_y: f32, x: f32, y: f32, z: f32}, + /// calculate lighting based on heightmap image with provided values for a + /// spot light source at specified location pointing at specified target + /// location with specified hotspot sharpness and cone angle + /// parameters: FilterGraphNode, surfaceScale, specularConstant, + /// specularExponent, kernelUnitLengthX, kernelUnitLengthY, x, y, z, + /// pointsAtX, pointsAtY, pointsAtZ, specularExponent, limitingConeAngle + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFESpecularLightingElement + /// https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFESpotLightElement + SVGFESpecularLightingSpot{surface_scale: f32, specular_constant: f32, + specular_exponent: f32, kernel_unit_length_x: f32, + kernel_unit_length_y: f32, x: f32, y: f32, z: f32, points_at_x: f32, + points_at_y: f32, points_at_z: f32, cone_exponent: f32, + limiting_cone_angle: f32}, + /// create a new image based on the input image, repeated throughout the + /// output rectangle + /// parameters: FilterGraphNode + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFETileElement + SVGFETile, + /// synthesize a new image based on Fractal Noise (Perlin) with the chosen + /// stitching mode + /// parameters: FilterGraphNode, baseFrequencyX, baseFrequencyY, numOctaves, + /// seed + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFETurbulenceElement + SVGFETurbulenceWithFractalNoiseWithNoStitching{base_frequency_x: f32, + base_frequency_y: f32, num_octaves: u32, seed: u32}, + /// synthesize a new image based on Fractal Noise (Perlin) with the chosen + /// stitching mode + /// parameters: FilterGraphNode, baseFrequencyX, baseFrequencyY, numOctaves, + /// seed + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFETurbulenceElement + SVGFETurbulenceWithFractalNoiseWithStitching{base_frequency_x: f32, + base_frequency_y: f32, num_octaves: u32, seed: u32}, + /// synthesize a new image based on Turbulence Noise (offset vectors) + /// parameters: FilterGraphNode, baseFrequencyX, baseFrequencyY, numOctaves, + /// seed + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFETurbulenceElement + SVGFETurbulenceWithTurbulenceNoiseWithNoStitching{base_frequency_x: f32, + base_frequency_y: f32, num_octaves: u32, seed: u32}, + /// synthesize a new image based on Turbulence Noise (offset vectors) + /// parameters: FilterGraphNode, baseFrequencyX, baseFrequencyY, numOctaves, + /// seed + /// SVG filter semantics - selectable input(s), selectable between linear + /// (default) and sRGB color space for calculations + /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFETurbulenceElement + SVGFETurbulenceWithTurbulenceNoiseWithStitching{base_frequency_x: f32, + base_frequency_y: f32, num_octaves: u32, seed: u32}, +} + +impl FilterGraphOp { + pub fn kind(&self) -> &'static str { + match *self { + FilterGraphOp::SVGFEBlendColor => "SVGFEBlendColor", + FilterGraphOp::SVGFEBlendColorBurn => "SVGFEBlendColorBurn", + FilterGraphOp::SVGFEBlendColorDodge => "SVGFEBlendColorDodge", + FilterGraphOp::SVGFEBlendDarken => "SVGFEBlendDarken", + FilterGraphOp::SVGFEBlendDifference => "SVGFEBlendDifference", + FilterGraphOp::SVGFEBlendExclusion => "SVGFEBlendExclusion", + FilterGraphOp::SVGFEBlendHardLight => "SVGFEBlendHardLight", + FilterGraphOp::SVGFEBlendHue => "SVGFEBlendHue", + FilterGraphOp::SVGFEBlendLighten => "SVGFEBlendLighten", + FilterGraphOp::SVGFEBlendLuminosity => "SVGFEBlendLuminosity", + FilterGraphOp::SVGFEBlendMultiply => "SVGFEBlendMultiply", + FilterGraphOp::SVGFEBlendNormal => "SVGFEBlendNormal", + FilterGraphOp::SVGFEBlendOverlay => "SVGFEBlendOverlay", + FilterGraphOp::SVGFEBlendSaturation => "SVGFEBlendSaturation", + FilterGraphOp::SVGFEBlendScreen => "SVGFEBlendScreen", + FilterGraphOp::SVGFEBlendSoftLight => "SVGFEBlendSoftLight", + FilterGraphOp::SVGFEColorMatrix{..} => "SVGFEColorMatrix", + FilterGraphOp::SVGFEComponentTransfer => "SVGFEComponentTransfer", + FilterGraphOp::SVGFEComponentTransferInterned{..} => "SVGFEComponentTransferInterned", + FilterGraphOp::SVGFECompositeArithmetic{..} => "SVGFECompositeArithmetic", + FilterGraphOp::SVGFECompositeATop => "SVGFECompositeATop", + FilterGraphOp::SVGFECompositeIn => "SVGFECompositeIn", + FilterGraphOp::SVGFECompositeLighter => "SVGFECompositeLighter", + FilterGraphOp::SVGFECompositeOut => "SVGFECompositeOut", + FilterGraphOp::SVGFECompositeOver => "SVGFECompositeOver", + FilterGraphOp::SVGFECompositeXOR => "SVGFECompositeXOR", + FilterGraphOp::SVGFEConvolveMatrixEdgeModeDuplicate{..} => "SVGFEConvolveMatrixEdgeModeDuplicate", + FilterGraphOp::SVGFEConvolveMatrixEdgeModeNone{..} => "SVGFEConvolveMatrixEdgeModeNone", + FilterGraphOp::SVGFEConvolveMatrixEdgeModeWrap{..} => "SVGFEConvolveMatrixEdgeModeWrap", + FilterGraphOp::SVGFEDiffuseLightingDistant{..} => "SVGFEDiffuseLightingDistant", + FilterGraphOp::SVGFEDiffuseLightingPoint{..} => "SVGFEDiffuseLightingPoint", + FilterGraphOp::SVGFEDiffuseLightingSpot{..} => "SVGFEDiffuseLightingSpot", + FilterGraphOp::SVGFEDisplacementMap{..} => "SVGFEDisplacementMap", + FilterGraphOp::SVGFEDropShadow{..} => "SVGFEDropShadow", + FilterGraphOp::SVGFEFlood{..} => "SVGFEFlood", + FilterGraphOp::SVGFEGaussianBlur{..} => "SVGFEGaussianBlur", + FilterGraphOp::SVGFEIdentity => "SVGFEIdentity", + FilterGraphOp::SVGFEImage{..} => "SVGFEImage", + FilterGraphOp::SVGFEMorphologyDilate{..} => "SVGFEMorphologyDilate", + FilterGraphOp::SVGFEMorphologyErode{..} => "SVGFEMorphologyErode", + FilterGraphOp::SVGFEOpacity{..} => "SVGFEOpacity", + FilterGraphOp::SVGFESourceAlpha => "SVGFESourceAlpha", + FilterGraphOp::SVGFESourceGraphic => "SVGFESourceGraphic", + FilterGraphOp::SVGFESpecularLightingDistant{..} => "SVGFESpecularLightingDistant", + FilterGraphOp::SVGFESpecularLightingPoint{..} => "SVGFESpecularLightingPoint", + FilterGraphOp::SVGFESpecularLightingSpot{..} => "SVGFESpecularLightingSpot", + FilterGraphOp::SVGFETile => "SVGFETile", + FilterGraphOp::SVGFEToAlpha => "SVGFEToAlpha", + FilterGraphOp::SVGFETurbulenceWithFractalNoiseWithNoStitching{..} => "SVGFETurbulenceWithFractalNoiseWithNoStitching", + FilterGraphOp::SVGFETurbulenceWithFractalNoiseWithStitching{..} => "SVGFETurbulenceWithFractalNoiseWithStitching", + FilterGraphOp::SVGFETurbulenceWithTurbulenceNoiseWithNoStitching{..} => "SVGFETurbulenceWithTurbulenceNoiseWithNoStitching", + FilterGraphOp::SVGFETurbulenceWithTurbulenceNoiseWithStitching{..} => "SVGFETurbulenceWithTurbulenceNoiseWithStitching", + } + } +} + +#[derive(Clone, Debug)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct FilterGraphNode { + /// Indicates this graph node was marked as necessary by the DAG optimizer + pub kept_by_optimizer: bool, + /// true if color_interpolation_filter == LinearRgb; shader will convert + /// sRGB texture pixel colors on load and convert back on store, for correct + /// interpolation + pub linear: bool, + /// padding for output rect if we need a border to get correct clamping, or + /// to account for larger final subregion than source rect (see bug 1869672) + pub inflate: i16, + /// virtualized picture input bindings, these refer to other filter outputs + /// by number within the graph, usually there is one element + pub inputs: Vec<FilterGraphPictureReference>, + /// clipping rect for filter node output + pub subregion: LayoutRect, +} + +impl From<FilterOpGraphNode> for FilterGraphNode { + fn from(node: FilterOpGraphNode) -> Self { + let mut inputs: Vec<FilterGraphPictureReference> = Vec::new(); + if node.input.buffer_id != FilterOpGraphPictureBufferId::None { + inputs.push(node.input.into()); + } + if node.input2.buffer_id != FilterOpGraphPictureBufferId::None { + inputs.push(node.input2.into()); + } + // If the op used by this node is a feMerge, it will add more inputs + // after this invocation. + FilterGraphNode{ + linear: node.linear, + inputs, + subregion: node.subregion, + // These are computed later in scene_building + kept_by_optimizer: true, + inflate: 0, + } + } +} + + /// Equivalent to api::FilterOp with added internal information #[derive(Clone, Debug)] #[cfg_attr(feature = "capture", derive(Serialize))] diff --git a/gfx/wr/webrender/src/invalidation/dependency.rs b/gfx/wr/webrender/src/invalidation/dependency.rs @@ -1,425 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -//! Dependency tracking for tile invalidation -//! -//! This module contains types and logic for tracking dependencies that affect -//! tile invalidation, including transform comparisons, spatial node tracking, -//! and primitive comparison. - -use api::{ImageKey, PropertyBindingId, ColorU}; -use euclid::approxeq::ApproxEq; -use crate::PrimitiveCompareResult; -use crate::spatial_tree::{SpatialTree, SpatialNodeIndex, CoordinateSpaceMapping}; -use crate::internal_types::{FastHashMap, FastHashSet, FrameId}; -use crate::intern::ItemUid; -use crate::resource_cache::{ResourceCache, ImageGeneration}; -use crate::tile_cache::{TileDescriptor, PrimitiveDescriptor, PrimitiveDependencyIndex}; -use peek_poke::{PeekPoke, peek_from_slice}; -use std::collections::hash_map::Entry; - -/// A comparable transform matrix, that compares with epsilon checks. -#[derive(Debug, Clone)] -pub struct MatrixKey { - pub m: [f32; 16], -} - -impl PartialEq for MatrixKey { - fn eq(&self, other: &Self) -> bool { - const EPSILON: f32 = 0.001; - - // TODO(gw): It's possible that we may need to adjust the epsilon - // to be tighter on most of the matrix, except the - // translation parts? - for (i, j) in self.m.iter().zip(other.m.iter()) { - if !i.approx_eq_eps(j, &EPSILON) { - return false; - } - } - - true - } -} - -/// A comparable scale-offset, that compares with epsilon checks. -#[derive(Debug, Clone)] -pub struct ScaleOffsetKey { - pub sx: f32, - pub sy: f32, - pub tx: f32, - pub ty: f32, -} - -impl PartialEq for ScaleOffsetKey { - fn eq(&self, other: &Self) -> bool { - const EPSILON: f32 = 0.001; - - self.sx.approx_eq_eps(&other.sx, &EPSILON) && - self.sy.approx_eq_eps(&other.sy, &EPSILON) && - self.tx.approx_eq_eps(&other.tx, &EPSILON) && - self.ty.approx_eq_eps(&other.ty, &EPSILON) - } -} - -/// A comparable / hashable version of a coordinate space mapping. Used to determine -/// if a transform dependency for a tile has changed. -#[derive(Debug, PartialEq, Clone)] -pub enum TransformKey { - Local, - ScaleOffset { - so: ScaleOffsetKey, - }, - Transform { - m: MatrixKey, - } -} - -impl<Src, Dst> From<CoordinateSpaceMapping<Src, Dst>> for TransformKey { - fn from(transform: CoordinateSpaceMapping<Src, Dst>) -> TransformKey { - match transform { - CoordinateSpaceMapping::Local => { - TransformKey::Local - } - CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => { - TransformKey::ScaleOffset { - so: ScaleOffsetKey { - sx: scale_offset.scale.x, - sy: scale_offset.scale.y, - tx: scale_offset.offset.x, - ty: scale_offset.offset.y, - } - } - } - CoordinateSpaceMapping::Transform(ref m) => { - TransformKey::Transform { - m: MatrixKey { - m: m.to_array(), - }, - } - } - } - } -} - -/// Get the transform key for a spatial node relative to a cache spatial node. -pub fn get_transform_key( - spatial_node_index: SpatialNodeIndex, - cache_spatial_node_index: SpatialNodeIndex, - spatial_tree: &SpatialTree, -) -> TransformKey { - spatial_tree.get_relative_transform( - spatial_node_index, - cache_spatial_node_index, - ).into() -} - - -/// Information about the state of a binding. -#[derive(Debug)] -pub struct BindingInfo<T> { - /// The current value retrieved from dynamic scene properties. - pub value: T, - /// True if it was changed (or is new) since the last frame build. - pub changed: bool, -} - -/// Information stored in a tile descriptor for a binding. -#[derive(Debug, PartialEq, Clone, Copy, PeekPoke)] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub enum Binding<T> { - Value(T), - Binding(PropertyBindingId), -} - -impl<T: Default> Default for Binding<T> { - fn default() -> Self { - Binding::Value(T::default()) - } -} - -impl<T> From<api::PropertyBinding<T>> for Binding<T> { - fn from(binding: api::PropertyBinding<T>) -> Binding<T> { - match binding { - api::PropertyBinding::Binding(key, _) => Binding::Binding(key.id), - api::PropertyBinding::Value(value) => Binding::Value(value), - } - } -} - -pub type OpacityBinding = Binding<f32>; -pub type OpacityBindingInfo = BindingInfo<f32>; - -pub type ColorBinding = Binding<ColorU>; -pub type ColorBindingInfo = BindingInfo<ColorU>; - -/// Types of dependencies that a primitive can have -#[derive(PeekPoke)] -pub enum PrimitiveDependency { - OpacityBinding { - binding: OpacityBinding, - }, - ColorBinding { - binding: ColorBinding, - }, - SpatialNode { - index: SpatialNodeIndex, - }, - Clip { - clip: ItemUid, - }, - Image { - image: ImageDependency, - }, -} - -/// Information stored an image dependency -#[derive(Debug, Copy, Clone, PartialEq, PeekPoke, Default)] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub struct ImageDependency { - pub key: ImageKey, - pub generation: ImageGeneration, -} - -impl ImageDependency { - pub const INVALID: ImageDependency = ImageDependency { - key: ImageKey::DUMMY, - generation: ImageGeneration::INVALID, - }; -} - -/// A dependency for a transform is defined by the spatial node index + frame it was used -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PeekPoke, Default)] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub struct SpatialNodeKey { - pub spatial_node_index: SpatialNodeIndex, - pub frame_id: FrameId, -} - -/// A helper for comparing spatial nodes between frames. The comparisons -/// are done by-value, so that if the shape of the spatial node tree -/// changes, invalidations aren't done simply due to the spatial node -/// index changing between display lists. -pub struct SpatialNodeComparer { - /// The root spatial node index of the tile cache - ref_spatial_node_index: SpatialNodeIndex, - /// Maintains a map of currently active transform keys - spatial_nodes: FastHashMap<SpatialNodeKey, TransformKey>, - /// A cache of recent comparisons between prev and current spatial nodes - compare_cache: FastHashMap<(SpatialNodeKey, SpatialNodeKey), bool>, - /// A set of frames that we need to retain spatial node entries for - referenced_frames: FastHashSet<FrameId>, -} - -impl SpatialNodeComparer { - /// Construct a new comparer - pub fn new() -> Self { - SpatialNodeComparer { - ref_spatial_node_index: SpatialNodeIndex::INVALID, - spatial_nodes: FastHashMap::default(), - compare_cache: FastHashMap::default(), - referenced_frames: FastHashSet::default(), - } - } - - /// Advance to the next frame - pub fn next_frame( - &mut self, - ref_spatial_node_index: SpatialNodeIndex, - ) { - // Drop any node information for unreferenced frames, to ensure that the - // hashmap doesn't grow indefinitely! - let referenced_frames = &self.referenced_frames; - self.spatial_nodes.retain(|key, _| { - referenced_frames.contains(&key.frame_id) - }); - - // Update the root spatial node for this comparer - self.ref_spatial_node_index = ref_spatial_node_index; - self.compare_cache.clear(); - self.referenced_frames.clear(); - } - - /// Register a transform that is used, and build the transform key for it if new. - pub fn register_used_transform( - &mut self, - spatial_node_index: SpatialNodeIndex, - frame_id: FrameId, - spatial_tree: &SpatialTree, - ) { - let key = SpatialNodeKey { - spatial_node_index, - frame_id, - }; - - if let Entry::Vacant(entry) = self.spatial_nodes.entry(key) { - entry.insert( - get_transform_key( - spatial_node_index, - self.ref_spatial_node_index, - spatial_tree, - ) - ); - } - } - - /// Return true if the transforms for two given spatial nodes are considered equivalent - pub fn are_transforms_equivalent( - &mut self, - prev_spatial_node_key: &SpatialNodeKey, - curr_spatial_node_key: &SpatialNodeKey, - ) -> bool { - let key = (*prev_spatial_node_key, *curr_spatial_node_key); - let spatial_nodes = &self.spatial_nodes; - - *self.compare_cache - .entry(key) - .or_insert_with(|| { - let prev = &spatial_nodes[&prev_spatial_node_key]; - let curr = &spatial_nodes[&curr_spatial_node_key]; - curr == prev - }) - } - - /// Ensure that the comparer won't GC any nodes for a given frame id - pub fn retain_for_frame(&mut self, frame_id: FrameId) { - self.referenced_frames.insert(frame_id); - } -} - - -/// A key for storing primitive comparison results during tile dependency tests. -#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)] -pub struct PrimitiveComparisonKey { - pub prev_index: PrimitiveDependencyIndex, - pub curr_index: PrimitiveDependencyIndex, -} - -/// A helper struct to compare a primitive and all its sub-dependencies. -pub struct PrimitiveComparer<'a> { - prev_data: &'a [u8], - curr_data: &'a [u8], - prev_frame_id: FrameId, - curr_frame_id: FrameId, - resource_cache: &'a ResourceCache, - spatial_node_comparer: &'a mut SpatialNodeComparer, - opacity_bindings: &'a FastHashMap<PropertyBindingId, OpacityBindingInfo>, - color_bindings: &'a FastHashMap<PropertyBindingId, ColorBindingInfo>, -} - -impl<'a> PrimitiveComparer<'a> { - pub fn new( - prev: &'a TileDescriptor, - curr: &'a TileDescriptor, - resource_cache: &'a ResourceCache, - spatial_node_comparer: &'a mut SpatialNodeComparer, - opacity_bindings: &'a FastHashMap<PropertyBindingId, OpacityBindingInfo>, - color_bindings: &'a FastHashMap<PropertyBindingId, ColorBindingInfo>, - ) -> Self { - PrimitiveComparer { - prev_data: &prev.dep_data, - curr_data: &curr.dep_data, - prev_frame_id: prev.last_updated_frame_id, - curr_frame_id: curr.last_updated_frame_id, - resource_cache, - spatial_node_comparer, - opacity_bindings, - color_bindings, - } - } - - /// Check if two primitive descriptors are the same. - pub fn compare_prim( - &mut self, - prev_desc: &PrimitiveDescriptor, - curr_desc: &PrimitiveDescriptor, - ) -> PrimitiveCompareResult { - let resource_cache = self.resource_cache; - let spatial_node_comparer = &mut self.spatial_node_comparer; - let opacity_bindings = self.opacity_bindings; - let color_bindings = self.color_bindings; - - // Check equality of the PrimitiveDescriptor - if prev_desc != curr_desc { - return PrimitiveCompareResult::Descriptor; - } - - let mut prev_dep_data = &self.prev_data[prev_desc.dep_offset as usize ..]; - let mut curr_dep_data = &self.curr_data[curr_desc.dep_offset as usize ..]; - - let mut prev_dep = PrimitiveDependency::SpatialNode { index: SpatialNodeIndex::INVALID }; - let mut curr_dep = PrimitiveDependency::SpatialNode { index: SpatialNodeIndex::INVALID }; - - debug_assert_eq!(prev_desc.dep_count, curr_desc.dep_count); - - for _ in 0 .. prev_desc.dep_count { - prev_dep_data = peek_from_slice(prev_dep_data, &mut prev_dep); - curr_dep_data = peek_from_slice(curr_dep_data, &mut curr_dep); - - match (&prev_dep, &curr_dep) { - (PrimitiveDependency::Clip { clip: prev }, PrimitiveDependency::Clip { clip: curr }) => { - if prev != curr { - return PrimitiveCompareResult::Clip; - } - } - (PrimitiveDependency::SpatialNode { index: prev }, PrimitiveDependency::SpatialNode { index: curr }) => { - let prev_key = SpatialNodeKey { - spatial_node_index: *prev, - frame_id: self.prev_frame_id, - }; - let curr_key = SpatialNodeKey { - spatial_node_index: *curr, - frame_id: self.curr_frame_id, - }; - if !spatial_node_comparer.are_transforms_equivalent(&prev_key, &curr_key) { - return PrimitiveCompareResult::Transform; - } - } - (PrimitiveDependency::OpacityBinding { binding: prev }, PrimitiveDependency::OpacityBinding { binding: curr }) => { - if prev != curr { - return PrimitiveCompareResult::OpacityBinding; - } - - if let OpacityBinding::Binding(id) = curr { - if opacity_bindings - .get(id) - .map_or(true, |info| info.changed) { - return PrimitiveCompareResult::OpacityBinding; - } - } - } - (PrimitiveDependency::ColorBinding { binding: prev }, PrimitiveDependency::ColorBinding { binding: curr }) => { - if prev != curr { - return PrimitiveCompareResult::ColorBinding; - } - - if let ColorBinding::Binding(id) = curr { - if color_bindings - .get(id) - .map_or(true, |info| info.changed) { - return PrimitiveCompareResult::ColorBinding; - } - } - } - (PrimitiveDependency::Image { image: prev }, PrimitiveDependency::Image { image: curr }) => { - if prev != curr { - return PrimitiveCompareResult::Image; - } - - if resource_cache.get_image_generation(curr.key) != curr.generation { - return PrimitiveCompareResult::Image; - } - } - _ => { - // There was a mismatch between types of dependencies, so something changed - return PrimitiveCompareResult::Descriptor; - } - } - } - - PrimitiveCompareResult::Equal - } -} diff --git a/gfx/wr/webrender/src/invalidation/mod.rs b/gfx/wr/webrender/src/invalidation/mod.rs @@ -1,158 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -//! Invalidation tracking and dirty region management -//! -//! This module contains types and logic for tracking dirty regions and -//! dependencies used to determine what needs to be redrawn each frame. - -pub mod dependency; -pub mod quadtree; - -use api::units::*; -use crate::spatial_tree::{SpatialTree, SpatialNodeIndex}; -use crate::space::SpaceMapper; -use crate::util::MaxRect; - -/// Represents the dirty region of a tile cache picture, relative to a -/// "visibility" spatial node. At the moment the visibility node is -/// world space, but the plan is to switch to raster space. -/// -/// The plan is to move away from these world space representation and -/// compute dirty regions in raster space instead. -#[derive(Clone)] -pub struct DirtyRegion { - /// The overall dirty rect, a combination of dirty_rects - pub combined: VisRect, - - /// The corrdinate space used to do clipping, visibility, and - /// dirty rect calculations. - pub visibility_spatial_node: SpatialNodeIndex, - /// Spatial node of the picture this region represents. - local_spatial_node: SpatialNodeIndex, -} - -impl DirtyRegion { - /// Construct a new dirty region tracker. - pub fn new( - visibility_spatial_node: SpatialNodeIndex, - local_spatial_node: SpatialNodeIndex, - ) -> Self { - DirtyRegion { - combined: VisRect::zero(), - visibility_spatial_node, - local_spatial_node, - } - } - - /// Reset the dirty regions back to empty - pub fn reset( - &mut self, - visibility_spatial_node: SpatialNodeIndex, - local_spatial_node: SpatialNodeIndex, - ) { - self.combined = VisRect::zero(); - self.visibility_spatial_node = visibility_spatial_node; - self.local_spatial_node = local_spatial_node; - } - - /// Add a dirty region to the tracker. Returns the visibility mask that corresponds to - /// this region in the tracker. - pub fn add_dirty_region( - &mut self, - rect_in_pic_space: PictureRect, - spatial_tree: &SpatialTree, - ) { - let map_pic_to_raster = SpaceMapper::new_with_target( - self.visibility_spatial_node, - self.local_spatial_node, - VisRect::max_rect(), - spatial_tree, - ); - - let raster_rect = map_pic_to_raster - .map(&rect_in_pic_space) - .expect("bug"); - - // Include this in the overall dirty rect - self.combined = self.combined.union(&raster_rect); - } -} - -/// Debugging information about why a tile was invalidated -#[derive(Debug,Clone)] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub enum InvalidationReason { - /// The background color changed - BackgroundColor, - /// The opaque state of the backing native surface changed - SurfaceOpacityChanged, - /// There was no backing texture (evicted or never rendered) - NoTexture, - /// There was no backing native surface (never rendered, or recreated) - NoSurface, - /// The primitive count in the dependency list was different - PrimCount, - /// The content of one of the primitives was different - Content, - // The compositor type changed - CompositorKindChanged, - // The valid region of the tile changed - ValidRectChanged, - // The overall scale of the picture cache changed - ScaleChanged, - // The content of the sampling surface changed - SurfaceContentChanged, -} - -/// The result of a primitive dependency comparison. Size is a u8 -/// since this is a hot path in the code, and keeping the data small -/// is a performance win. -#[derive(Debug, Copy, Clone, PartialEq)] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -#[repr(u8)] -pub enum PrimitiveCompareResult { - /// Primitives match - Equal, - /// Something in the PrimitiveDescriptor was different - Descriptor, - /// The clip node content or spatial node changed - Clip, - /// The value of the transform changed - Transform, - /// An image dependency was dirty - Image, - /// The value of an opacity binding changed - OpacityBinding, - /// The value of a color binding changed - ColorBinding, -} - -/// Optional extra information returned by is_same when -/// logging is enabled. -#[derive(Debug, Copy, Clone, PartialEq)] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub enum CompareHelperResult<T> { - /// Primitives match - Equal, - /// Counts differ - Count { - prev_count: u8, - curr_count: u8, - }, - /// Sentinel - Sentinel, - /// Two items are not equal - NotEqual { - prev: T, - curr: T, - }, - /// User callback returned true on item - PredicateTrue { - curr: T - }, -} diff --git a/gfx/wr/webrender/src/invalidation/quadtree.rs b/gfx/wr/webrender/src/invalidation/quadtree.rs @@ -1,440 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -//! Quadtree-based dirty region tracking for tiles -//! -//! This module implements a quadtree data structure that tracks which regions -//! of a tile have been invalidated. The quadtree can dynamically split and merge -//! nodes based on invalidation patterns to optimize tracking. - -use api::units::*; -use crate::debug_colors; -use crate::internal_types::FastHashMap; -use crate::prim_store::PrimitiveScratchBuffer; -use crate::space::SpaceMapper; -use crate::tile_cache::{PrimitiveDescriptor, PrimitiveDependencyIndex}; -use crate::invalidation::{InvalidationReason, PrimitiveCompareResult}; -use crate::invalidation::dependency::PrimitiveComparer; -use crate::visibility::FrameVisibilityContext; -use std::mem; - - -/// Details for a node in a quadtree that tracks dirty rects for a tile. -#[cfg_attr(any(feature="capture",feature="replay"), derive(Clone))] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub enum TileNodeKind { - Leaf { - /// The index buffer of primitives that affected this tile previous frame - #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))] - prev_indices: Vec<PrimitiveDependencyIndex>, - /// The index buffer of primitives that affect this tile on this frame - #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))] - curr_indices: Vec<PrimitiveDependencyIndex>, - /// A bitset of which of the last 64 frames have been dirty for this leaf. - #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))] - dirty_tracker: u64, - /// The number of frames since this node split or merged. - #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))] - frames_since_modified: usize, - }, - Node { - /// The four children of this node - children: Vec<TileNode>, - }, -} - -/// The kind of modification that a tile wants to do -#[derive(Copy, Clone, PartialEq, Debug)] -enum TileModification { - Split, - Merge, -} - -/// A node in the dirty rect tracking quadtree. -#[cfg_attr(any(feature="capture",feature="replay"), derive(Clone))] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub struct TileNode { - /// Leaf or internal node - pub kind: TileNodeKind, - /// Rect of this node in the same space as the tile cache picture - pub rect: PictureBox2D, -} - -impl TileNode { - /// Construct a new leaf node, with the given primitive dependency index buffer - pub fn new_leaf(curr_indices: Vec<PrimitiveDependencyIndex>) -> Self { - TileNode { - kind: TileNodeKind::Leaf { - prev_indices: Vec::new(), - curr_indices, - dirty_tracker: 0, - frames_since_modified: 0, - }, - rect: PictureBox2D::zero(), - } - } - - /// Draw debug information about this tile node - pub fn draw_debug_rects( - &self, - pic_to_world_mapper: &SpaceMapper<PicturePixel, WorldPixel>, - is_opaque: bool, - local_valid_rect: PictureRect, - scratch: &mut PrimitiveScratchBuffer, - global_device_pixel_scale: DevicePixelScale, - ) { - match self.kind { - TileNodeKind::Leaf { dirty_tracker, .. } => { - let color = if (dirty_tracker & 1) != 0 { - debug_colors::RED - } else if is_opaque { - debug_colors::GREEN - } else { - debug_colors::YELLOW - }; - - if let Some(local_rect) = local_valid_rect.intersection(&self.rect) { - let world_rect = pic_to_world_mapper - .map(&local_rect) - .unwrap(); - let device_rect = world_rect * global_device_pixel_scale; - - let outer_color = color.scale_alpha(0.3); - let inner_color = outer_color.scale_alpha(0.5); - scratch.push_debug_rect( - device_rect.inflate(-3.0, -3.0), - 1, - outer_color, - inner_color - ); - } - } - TileNodeKind::Node { ref children, .. } => { - for child in children.iter() { - child.draw_debug_rects( - pic_to_world_mapper, - is_opaque, - local_valid_rect, - scratch, - global_device_pixel_scale, - ); - } - } - } - } - - /// Calculate the four child rects for a given node - fn get_child_rects( - rect: &PictureBox2D, - result: &mut [PictureBox2D; 4], - ) { - let p0 = rect.min; - let p1 = rect.max; - let pc = p0 + rect.size() * 0.5; - - *result = [ - PictureBox2D::new( - p0, - pc, - ), - PictureBox2D::new( - PicturePoint::new(pc.x, p0.y), - PicturePoint::new(p1.x, pc.y), - ), - PictureBox2D::new( - PicturePoint::new(p0.x, pc.y), - PicturePoint::new(pc.x, p1.y), - ), - PictureBox2D::new( - pc, - p1, - ), - ]; - } - - /// Called during pre_update, to clear the current dependencies - pub fn clear( - &mut self, - rect: PictureBox2D, - ) { - self.rect = rect; - - match self.kind { - TileNodeKind::Leaf { ref mut prev_indices, ref mut curr_indices, ref mut dirty_tracker, ref mut frames_since_modified } => { - // Swap current dependencies to be the previous frame - mem::swap(prev_indices, curr_indices); - curr_indices.clear(); - // Note that another frame has passed in the dirty bit trackers - *dirty_tracker = *dirty_tracker << 1; - *frames_since_modified += 1; - } - TileNodeKind::Node { ref mut children, .. } => { - let mut child_rects = [PictureBox2D::zero(); 4]; - TileNode::get_child_rects(&rect, &mut child_rects); - assert_eq!(child_rects.len(), children.len()); - - for (child, rect) in children.iter_mut().zip(child_rects.iter()) { - child.clear(*rect); - } - } - } - } - - /// Add a primitive dependency to this node - pub fn add_prim( - &mut self, - index: PrimitiveDependencyIndex, - prim_rect: &PictureBox2D, - ) { - match self.kind { - TileNodeKind::Leaf { ref mut curr_indices, .. } => { - curr_indices.push(index); - } - TileNodeKind::Node { ref mut children, .. } => { - for child in children.iter_mut() { - if child.rect.intersects(prim_rect) { - child.add_prim(index, prim_rect); - } - } - } - } - } - - /// Apply a merge or split operation to this tile, if desired - pub fn maybe_merge_or_split( - &mut self, - level: i32, - curr_prims: &[PrimitiveDescriptor], - max_split_levels: i32, - ) { - // Determine if this tile wants to split or merge - let mut tile_mod = None; - - fn get_dirty_frames( - dirty_tracker: u64, - frames_since_modified: usize, - ) -> Option<u32> { - // Only consider splitting or merging at least 64 frames since we last changed - if frames_since_modified > 64 { - // Each bit in the tracker is a frame that was recently invalidated - Some(dirty_tracker.count_ones()) - } else { - None - } - } - - match self.kind { - TileNodeKind::Leaf { dirty_tracker, frames_since_modified, .. } => { - // Only consider splitting if the tree isn't too deep. - if level < max_split_levels { - if let Some(dirty_frames) = get_dirty_frames(dirty_tracker, frames_since_modified) { - // If the tile has invalidated > 50% of the recent number of frames, split. - if dirty_frames > 32 { - tile_mod = Some(TileModification::Split); - } - } - } - } - TileNodeKind::Node { ref children, .. } => { - // There's two conditions that cause a node to merge its children: - // (1) If _all_ the child nodes are constantly invalidating, then we are wasting - // CPU time tracking dependencies for each child, so merge them. - // (2) If _none_ of the child nodes are recently invalid, then the page content - // has probably changed, and we no longer need to track fine grained dependencies here. - - let mut static_count = 0; - let mut changing_count = 0; - - for child in children { - // Only consider merging nodes at the edge of the tree. - if let TileNodeKind::Leaf { dirty_tracker, frames_since_modified, .. } = child.kind { - if let Some(dirty_frames) = get_dirty_frames(dirty_tracker, frames_since_modified) { - if dirty_frames == 0 { - // Hasn't been invalidated for some time - static_count += 1; - } else if dirty_frames == 64 { - // Is constantly being invalidated - changing_count += 1; - } - } - } - - // Only merge if all the child tiles are in agreement. Otherwise, we have some - // that are invalidating / static, and it's worthwhile tracking dependencies for - // them individually. - if static_count == 4 || changing_count == 4 { - tile_mod = Some(TileModification::Merge); - } - } - } - } - - match tile_mod { - Some(TileModification::Split) => { - // To split a node, take the current dependency index buffer for this node, and - // split it into child index buffers. - let curr_indices = match self.kind { - TileNodeKind::Node { .. } => { - unreachable!("bug - only leaves can split"); - } - TileNodeKind::Leaf { ref mut curr_indices, .. } => { - mem::take(curr_indices) - } - }; - - let mut child_rects = [PictureBox2D::zero(); 4]; - TileNode::get_child_rects(&self.rect, &mut child_rects); - - let mut child_indices = [ - Vec::new(), - Vec::new(), - Vec::new(), - Vec::new(), - ]; - - // Step through the index buffer, and add primitives to each of the children - // that they intersect. - for index in curr_indices { - let prim = &curr_prims[index.0 as usize]; - for (child_rect, indices) in child_rects.iter().zip(child_indices.iter_mut()) { - if prim.prim_clip_box.intersects(child_rect) { - indices.push(index); - } - } - } - - // Create the child nodes and switch from leaf -> node. - let children = child_indices - .iter_mut() - .map(|i| TileNode::new_leaf(mem::replace(i, Vec::new()))) - .collect(); - - self.kind = TileNodeKind::Node { - children, - }; - } - Some(TileModification::Merge) => { - // Construct a merged index buffer by collecting the dependency index buffers - // from each child, and merging them into a de-duplicated index buffer. - let merged_indices = match self.kind { - TileNodeKind::Node { ref mut children, .. } => { - let mut merged_indices = Vec::new(); - - for child in children.iter() { - let child_indices = match child.kind { - TileNodeKind::Leaf { ref curr_indices, .. } => { - curr_indices - } - TileNodeKind::Node { .. } => { - unreachable!("bug: child is not a leaf"); - } - }; - merged_indices.extend_from_slice(child_indices); - } - - merged_indices.sort(); - merged_indices.dedup(); - - merged_indices - } - TileNodeKind::Leaf { .. } => { - unreachable!("bug - trying to merge a leaf"); - } - }; - - // Switch from a node to a leaf, with the combined index buffer - self.kind = TileNodeKind::Leaf { - prev_indices: Vec::new(), - curr_indices: merged_indices, - dirty_tracker: 0, - frames_since_modified: 0, - }; - } - None => { - // If this node didn't merge / split, then recurse into children - // to see if they want to split / merge. - if let TileNodeKind::Node { ref mut children, .. } = self.kind { - for child in children.iter_mut() { - child.maybe_merge_or_split( - level+1, - curr_prims, - max_split_levels, - ); - } - } - } - } - } - - /// Update the dirty state of this node, building the overall dirty rect - pub fn update_dirty_rects( - &mut self, - prev_prims: &[PrimitiveDescriptor], - curr_prims: &[PrimitiveDescriptor], - prim_comparer: &mut PrimitiveComparer, - dirty_rect: &mut PictureBox2D, - compare_cache: &mut FastHashMap<crate::invalidation::dependency::PrimitiveComparisonKey, PrimitiveCompareResult>, - invalidation_reason: &mut Option<InvalidationReason>, - frame_context: &FrameVisibilityContext, - ) { - match self.kind { - TileNodeKind::Node { ref mut children, .. } => { - for child in children.iter_mut() { - child.update_dirty_rects( - prev_prims, - curr_prims, - prim_comparer, - dirty_rect, - compare_cache, - invalidation_reason, - frame_context, - ); - } - } - TileNodeKind::Leaf { ref prev_indices, ref curr_indices, ref mut dirty_tracker, .. } => { - // If the index buffers are of different length, they must be different - if prev_indices.len() == curr_indices.len() { - // Walk each index buffer, comparing primitives - for (prev_index, curr_index) in prev_indices.iter().zip(curr_indices.iter()) { - let i0 = prev_index.0 as usize; - let i1 = curr_index.0 as usize; - - // Compare the primitives, caching the result in a hash map - // to save comparisons in other tree nodes. - let key = crate::invalidation::dependency::PrimitiveComparisonKey { - prev_index: *prev_index, - curr_index: *curr_index, - }; - - let prim_compare_result = *compare_cache - .entry(key) - .or_insert_with(|| { - let prev = &prev_prims[i0]; - let curr = &curr_prims[i1]; - prim_comparer.compare_prim(prev, curr) - }); - - // If not the same, mark this node as dirty and update the dirty rect - if prim_compare_result != PrimitiveCompareResult::Equal { - if invalidation_reason.is_none() { - *invalidation_reason = Some(InvalidationReason::Content); - } - *dirty_rect = self.rect.union(dirty_rect); - *dirty_tracker = *dirty_tracker | 1; - break; - } - } - } else { - if invalidation_reason.is_none() { - *invalidation_reason = Some(InvalidationReason::PrimCount); - } - *dirty_rect = self.rect.union(dirty_rect); - *dirty_tracker = *dirty_tracker | 1; - } - } - } - } -} diff --git a/gfx/wr/webrender/src/lib.rs b/gfx/wr/webrender/src/lib.rs @@ -109,7 +109,6 @@ mod lru_cache; mod pattern; mod picture; mod picture_graph; -mod invalidation; mod prepare; mod prim_store; mod print_tree; @@ -140,7 +139,6 @@ mod rectangle_occlusion; mod picture_textures; mod frame_allocator; mod bump_allocator; -mod svg_filter; /// pub mod intern; @@ -193,11 +191,9 @@ pub use crate::screen_capture::{AsyncScreenshotHandle, RecordedFrameHandle}; pub use crate::texture_cache::TextureCacheConfig; pub use api as webrender_api; pub use webrender_build::shader::{ProgramSourceDigest, ShaderKind}; -pub use crate::tile_cache::{TileDescriptor, TileId}; -pub use crate::invalidation::InvalidationReason; -pub use crate::invalidation::{PrimitiveCompareResult, CompareHelperResult}; -pub use crate::picture::{TileNode, TileNodeKind}; -pub use crate::tile_cache::TileOffset; +pub use crate::picture::{TileDescriptor, TileId, InvalidationReason}; +pub use crate::picture::{PrimitiveCompareResult, CompareHelperResult}; +pub use crate::picture::{TileNode, TileNodeKind, TileOffset}; pub use crate::intern::ItemUid; pub use crate::render_api::*; pub use crate::tile_cache::{PictureCacheDebugInfo, DirtyTileDebugInfo, TileDebugInfo, SliceDebugInfo}; diff --git a/gfx/wr/webrender/src/picture.rs b/gfx/wr/webrender/src/picture.rs @@ -94,130 +94,3955 @@ //! blend the overlay tile (this is not always optimal right now, but will be //! improved as a follow up). -use api::{MixBlendMode, PremultipliedColorF, RasterSpace}; -use api::{DebugFlags, ColorF, PrimitiveFlags, SnapshotInfo}; +use api::{BorderRadius, ClipMode, MixBlendMode, PremultipliedColorF, SVGFE_GRAPH_MAX}; +use api::{PropertyBinding, PropertyBindingId, FilterOpGraphPictureBufferId, RasterSpace}; +use api::{DebugFlags, ImageKey, ColorF, ColorU, PrimitiveFlags, SnapshotInfo}; +use api::{ImageRendering, ColorDepth, YuvRangedColorSpace, YuvFormat, AlphaType}; use api::units::*; use crate::prim_store::image::AdjustedImageSource; use crate::{command_buffer::PrimitiveCommand, render_task_graph::RenderTaskGraphBuilder, renderer::GpuBufferBuilderF}; use crate::box_shadow::BLUR_SAMPLE_SCALE; -use crate::clip::{ClipNodeId, ClipTreeBuilder}; +use crate::clip::{ClipChainInstance, ClipItemKind, ClipLeafId, ClipNodeId, ClipSpaceConversion, ClipStore, ClipTreeBuilder}; +use crate::profiler::{self, add_text_marker, TransactionProfile}; use crate::spatial_tree::{SpatialTree, CoordinateSpaceMapping, SpatialNodeIndex, VisibleFace}; -use crate::composite::{tile_kind, CompositeState, CompositeTileSurface, CompositorKind, NativeTileId}; -use crate::composite::{CompositeTileDescriptor, CompositeTile}; +use crate::composite::{tile_kind, CompositeState, CompositeTileSurface, CompositorClipIndex, CompositorKind, NativeSurfaceId, NativeTileId}; +use crate::composite::{ExternalSurfaceDescriptor, ExternalSurfaceDependency, CompositeTileDescriptor, CompositeTile}; +use crate::composite::{CompositorTransformIndex, CompositorSurfaceKind}; use crate::debug_colors; -use euclid::{vec3, Scale, Vector2D, Box2D}; -use crate::internal_types::{FastHashMap, PlaneSplitter, Filter}; +use euclid::{vec3, Point2D, Scale, Vector2D, Box2D}; +use euclid::approxeq::ApproxEq; +use crate::intern::ItemUid; +use crate::internal_types::{FastHashMap, FastHashSet, PlaneSplitter, FilterGraphOp, FilterGraphNode, Filter, FrameId}; use crate::internal_types::{PlaneSplitterIndex, PlaneSplitAnchor, TextureSource}; use crate::frame_builder::{FrameBuildingContext, FrameBuildingState, PictureState, PictureContext}; -use crate::gpu_types::{BlurEdgeMode, BrushSegmentGpuData, ImageBrushPrimitiveData}; -use crate::svg_filter::{FilterGraphOp, FilterGraphNode, get_coverage_source_svgfe, get_coverage_target_svgfe}; +use crate::gpu_types::{BlurEdgeMode, BrushSegmentGpuData, ImageBrushPrimitiveData, UvRectKind, ZBufferId}; +use peek_poke::{PeekPoke, poke_into_vec, peek_from_slice, ensure_red_zone}; use plane_split::{Clipper, Polygon}; -use crate::prim_store::{PictureIndex, PrimitiveInstance, PrimitiveInstanceKind}; -use crate::prim_store::PrimitiveScratchBuffer; -use crate::print_tree::PrintTreePrinter; +use crate::prim_store::{PrimitiveTemplateKind, PictureIndex, PrimitiveInstance, PrimitiveInstanceKind}; +use crate::prim_store::{ColorBindingStorage, ColorBindingIndex, PrimitiveScratchBuffer}; +use crate::print_tree::{PrintTree, PrintTreePrinter}; use crate::render_backend::DataStores; use crate::render_task_graph::RenderTaskId; use crate::render_target::RenderTargetKind; use crate::render_task::{BlurTask, RenderTask, RenderTaskLocation, BlurTaskCache}; use crate::render_task::{StaticRenderTaskSurface, RenderTaskKind}; use crate::renderer::{BlendMode, GpuBufferAddress}; -use crate::resource_cache::ResourceCache; +use crate::resource_cache::{ResourceCache, ImageGeneration, ImageRequest}; use crate::space::SpaceMapper; use crate::scene::SceneProperties; use crate::spatial_tree::CoordinateSystemId; -use crate::surface::{SurfaceDescriptor, SurfaceTileDescriptor, get_surface_rects}; -pub use crate::surface::{SurfaceIndex, SurfaceInfo, SubpixelMode, SurfaceAllocInfo}; -pub use crate::surface::{calculate_screen_uv, calculate_uv_rect_kind}; +use crate::surface::{SurfaceDescriptor, SurfaceTileDescriptor}; use smallvec::SmallVec; -use std::{mem, u8, u32}; +use std::{mem, u8, marker, u32}; +use std::fmt::{Display, Error, Formatter}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::collections::hash_map::Entry; use std::ops::Range; use crate::picture_textures::PictureCacheTextureHandle; -use crate::util::{MaxRect, Recycler, ScaleOffset}; +use crate::util::{MaxRect, VecHelper, MatrixHelpers, Recycler, ScaleOffset}; use crate::filterdata::FilterDataHandle; use crate::tile_cache::{SliceDebugInfo, TileDebugInfo, DirtyTileDebugInfo}; -use crate::tile_cache::{SliceId, TileCacheInstance, TileSurface, NativeSurface}; -use crate::tile_cache::{BackdropKind, BackdropSurface, Tile}; -use crate::tile_cache::{TileKey, SubSliceIndex}; -use crate::invalidation::InvalidationReason; -use crate::tile_cache::MAX_SURFACE_SIZE; +use crate::visibility::{PrimitiveVisibilityFlags, FrameVisibilityContext}; +use crate::visibility::{VisibilityState, FrameVisibilityState}; +use crate::scene_building::SliceFlags; +use core::time::Duration; -pub use crate::invalidation::DirtyRegion; -pub use crate::invalidation::dependency::ImageDependency; -pub use crate::invalidation::quadtree::{TileNode, TileNodeKind}; +// Maximum blur radius for blur filter (different than box-shadow blur). +// Taken from FilterNodeSoftware.cpp in Gecko. +const MAX_BLUR_RADIUS: f32 = 100.; + +/// Specify whether a surface allows subpixel AA text rendering. +#[derive(Debug, Copy, Clone)] +pub enum SubpixelMode { + /// This surface allows subpixel AA text + Allow, + /// Subpixel AA text cannot be drawn on this surface + Deny, + /// Subpixel AA can be drawn on this surface, if not intersecting + /// with the excluded regions, and inside the allowed rect. + Conditional { + allowed_rect: PictureRect, + prohibited_rect: PictureRect, + }, +} + +/// A comparable transform matrix, that compares with epsilon checks. +#[derive(Debug, Clone)] +struct MatrixKey { + m: [f32; 16], +} + +impl PartialEq for MatrixKey { + fn eq(&self, other: &Self) -> bool { + const EPSILON: f32 = 0.001; + + // TODO(gw): It's possible that we may need to adjust the epsilon + // to be tighter on most of the matrix, except the + // translation parts? + for (i, j) in self.m.iter().zip(other.m.iter()) { + if !i.approx_eq_eps(j, &EPSILON) { + return false; + } + } + + true + } +} + +/// A comparable scale-offset, that compares with epsilon checks. +#[derive(Debug, Clone)] +struct ScaleOffsetKey { + sx: f32, + sy: f32, + tx: f32, + ty: f32, +} + +impl PartialEq for ScaleOffsetKey { + fn eq(&self, other: &Self) -> bool { + const EPSILON: f32 = 0.001; + + self.sx.approx_eq_eps(&other.sx, &EPSILON) && + self.sy.approx_eq_eps(&other.sy, &EPSILON) && + self.tx.approx_eq_eps(&other.tx, &EPSILON) && + self.ty.approx_eq_eps(&other.ty, &EPSILON) + } +} + +/// A comparable / hashable version of a coordinate space mapping. Used to determine +/// if a transform dependency for a tile has changed. +#[derive(Debug, PartialEq, Clone)] +enum TransformKey { + Local, + ScaleOffset { + so: ScaleOffsetKey, + }, + Transform { + m: MatrixKey, + } +} + +impl<Src, Dst> From<CoordinateSpaceMapping<Src, Dst>> for TransformKey { + fn from(transform: CoordinateSpaceMapping<Src, Dst>) -> TransformKey { + match transform { + CoordinateSpaceMapping::Local => { + TransformKey::Local + } + CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => { + TransformKey::ScaleOffset { + so: ScaleOffsetKey { + sx: scale_offset.scale.x, + sy: scale_offset.scale.y, + tx: scale_offset.offset.x, + ty: scale_offset.offset.y, + } + } + } + CoordinateSpaceMapping::Transform(ref m) => { + TransformKey::Transform { + m: MatrixKey { + m: m.to_array(), + }, + } + } + } + } +} + +/// Unit for tile coordinates. +#[derive(Hash, Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub struct TileCoordinate; + +// Geometry types for tile coordinates. +pub type TileOffset = Point2D<i32, TileCoordinate>; +pub type TileRect = Box2D<i32, TileCoordinate>; + +/// The maximum number of compositor surfaces that are allowed per picture cache. This +/// is an arbitrary number that should be enough for common cases, but low enough to +/// prevent performance and memory usage drastically degrading in pathological cases. +pub const MAX_COMPOSITOR_SURFACES: usize = 4; + +/// The size in device pixels of a normal cached tile. +pub const TILE_SIZE_DEFAULT: DeviceIntSize = DeviceIntSize { + width: 1024, + height: 512, + _unit: marker::PhantomData, +}; + +/// The size in device pixels of a tile for horizontal scroll bars +pub const TILE_SIZE_SCROLLBAR_HORIZONTAL: DeviceIntSize = DeviceIntSize { + width: 1024, + height: 32, + _unit: marker::PhantomData, +}; + +/// The size in device pixels of a tile for vertical scroll bars +pub const TILE_SIZE_SCROLLBAR_VERTICAL: DeviceIntSize = DeviceIntSize { + width: 32, + height: 1024, + _unit: marker::PhantomData, +}; + +/// The maximum size per axis of a surface, in DevicePixel coordinates. +/// Render tasks larger than this size are scaled down to fit, which may cause +/// some blurriness. +pub const MAX_SURFACE_SIZE: usize = 4096; +/// Maximum size of a compositor surface. +const MAX_COMPOSITOR_SURFACES_SIZE: f32 = 8192.0; + +/// Used to get unique tile IDs, even when the tile cache is +/// destroyed between display lists / scenes. +static NEXT_TILE_ID: AtomicUsize = AtomicUsize::new(0); + +fn clamp(value: i32, low: i32, high: i32) -> i32 { + value.max(low).min(high) +} + +fn clampf(value: f32, low: f32, high: f32) -> f32 { + value.max(low).min(high) +} + +/// An index into the prims array in a TileDescriptor. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct PrimitiveDependencyIndex(pub u32); + +/// Information about the state of a binding. +#[derive(Debug)] +pub struct BindingInfo<T> { + /// The current value retrieved from dynamic scene properties. + value: T, + /// True if it was changed (or is new) since the last frame build. + changed: bool, +} + +/// Information stored in a tile descriptor for a binding. +#[derive(Debug, PartialEq, Clone, Copy, PeekPoke)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub enum Binding<T> { + Value(T), + Binding(PropertyBindingId), +} + +impl<T: Default> Default for Binding<T> { + fn default() -> Self { + Binding::Value(T::default()) + } +} + +impl<T> From<PropertyBinding<T>> for Binding<T> { + fn from(binding: PropertyBinding<T>) -> Binding<T> { + match binding { + PropertyBinding::Binding(key, _) => Binding::Binding(key.id), + PropertyBinding::Value(value) => Binding::Value(value), + } + } +} + +pub type OpacityBinding = Binding<f32>; +pub type OpacityBindingInfo = BindingInfo<f32>; + +pub type ColorBinding = Binding<ColorU>; +pub type ColorBindingInfo = BindingInfo<ColorU>; + +#[derive(PeekPoke)] +enum PrimitiveDependency { + OpacityBinding { + binding: OpacityBinding, + }, + ColorBinding { + binding: ColorBinding, + }, + SpatialNode { + index: SpatialNodeIndex, + }, + Clip { + clip: ItemUid, + }, + Image { + image: ImageDependency, + }, +} + +/// A dependency for a transform is defined by the spatial node index + frame it was used +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PeekPoke, Default)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct SpatialNodeKey { + spatial_node_index: SpatialNodeIndex, + frame_id: FrameId, +} + +/// A helper for comparing spatial nodes between frames. The comparisons +/// are done by-value, so that if the shape of the spatial node tree +/// changes, invalidations aren't done simply due to the spatial node +/// index changing between display lists. +struct SpatialNodeComparer { + /// The root spatial node index of the tile cache + ref_spatial_node_index: SpatialNodeIndex, + /// Maintains a map of currently active transform keys + spatial_nodes: FastHashMap<SpatialNodeKey, TransformKey>, + /// A cache of recent comparisons between prev and current spatial nodes + compare_cache: FastHashMap<(SpatialNodeKey, SpatialNodeKey), bool>, + /// A set of frames that we need to retain spatial node entries for + referenced_frames: FastHashSet<FrameId>, +} + +impl SpatialNodeComparer { + /// Construct a new comparer + fn new() -> Self { + SpatialNodeComparer { + ref_spatial_node_index: SpatialNodeIndex::INVALID, + spatial_nodes: FastHashMap::default(), + compare_cache: FastHashMap::default(), + referenced_frames: FastHashSet::default(), + } + } + + /// Advance to the next frame + fn next_frame( + &mut self, + ref_spatial_node_index: SpatialNodeIndex, + ) { + // Drop any node information for unreferenced frames, to ensure that the + // hashmap doesn't grow indefinitely! + let referenced_frames = &self.referenced_frames; + self.spatial_nodes.retain(|key, _| { + referenced_frames.contains(&key.frame_id) + }); + + // Update the root spatial node for this comparer + self.ref_spatial_node_index = ref_spatial_node_index; + self.compare_cache.clear(); + self.referenced_frames.clear(); + } + + /// Register a transform that is used, and build the transform key for it if new. + fn register_used_transform( + &mut self, + spatial_node_index: SpatialNodeIndex, + frame_id: FrameId, + spatial_tree: &SpatialTree, + ) { + let key = SpatialNodeKey { + spatial_node_index, + frame_id, + }; + + if let Entry::Vacant(entry) = self.spatial_nodes.entry(key) { + entry.insert( + get_transform_key( + spatial_node_index, + self.ref_spatial_node_index, + spatial_tree, + ) + ); + } + } + + /// Return true if the transforms for two given spatial nodes are considered equivalent + fn are_transforms_equivalent( + &mut self, + prev_spatial_node_key: &SpatialNodeKey, + curr_spatial_node_key: &SpatialNodeKey, + ) -> bool { + let key = (*prev_spatial_node_key, *curr_spatial_node_key); + let spatial_nodes = &self.spatial_nodes; + + *self.compare_cache + .entry(key) + .or_insert_with(|| { + let prev = &spatial_nodes[&prev_spatial_node_key]; + let curr = &spatial_nodes[&curr_spatial_node_key]; + curr == prev + }) + } + + /// Ensure that the comparer won't GC any nodes for a given frame id + fn retain_for_frame(&mut self, frame_id: FrameId) { + self.referenced_frames.insert(frame_id); + } +} + +// Immutable context passed to picture cache tiles during pre_update +struct TilePreUpdateContext { + /// Maps from picture cache coords -> world space coords. + pic_to_world_mapper: SpaceMapper<PicturePixel, WorldPixel>, + + /// The optional background color of the picture cache instance + background_color: Option<ColorF>, + + /// The visible part of the screen in world coords. + global_screen_world_rect: WorldRect, + + /// Current size of tiles in picture units. + tile_size: PictureSize, + + /// The current frame id for this picture cache + frame_id: FrameId, +} + +// Immutable context passed to picture cache tiles during update_dirty_and_valid_rects +struct TileUpdateDirtyContext<'a> { + /// Maps from picture cache coords -> world space coords. + pic_to_world_mapper: SpaceMapper<PicturePixel, WorldPixel>, + + /// Global scale factor from world -> device pixels. + global_device_pixel_scale: DevicePixelScale, + + /// Information about opacity bindings from the picture cache. + opacity_bindings: &'a FastHashMap<PropertyBindingId, OpacityBindingInfo>, + + /// Information about color bindings from the picture cache. + color_bindings: &'a FastHashMap<PropertyBindingId, ColorBindingInfo>, + + /// The local rect of the overall picture cache + local_rect: PictureRect, + + /// If true, the scale factor of the root transform for this picture + /// cache changed, so we need to invalidate the tile and re-render. + invalidate_all: bool, +} + +// Mutable state passed to picture cache tiles during update_dirty_and_valid_rects +struct TileUpdateDirtyState<'a> { + /// Allow access to the texture cache for requesting tiles + resource_cache: &'a mut ResourceCache, + + /// Current configuration and setup for compositing all the picture cache tiles in renderer. + composite_state: &'a mut CompositeState, + + /// A cache of comparison results to avoid re-computation during invalidation. + compare_cache: &'a mut FastHashMap<PrimitiveComparisonKey, PrimitiveCompareResult>, + + /// Information about transform node differences from last frame. + spatial_node_comparer: &'a mut SpatialNodeComparer, +} + +// Immutable context passed to picture cache tiles during post_update +struct TilePostUpdateContext<'a> { + /// The local clip rect (in picture space) of the entire picture cache + local_clip_rect: PictureRect, + + /// The calculated backdrop information for this cache instance. + backdrop: Option<BackdropInfo>, + + /// Current size in device pixels of tiles for this cache + current_tile_size: DeviceIntSize, + + /// Pre-allocated z-id to assign to tiles during post_update. + z_id: ZBufferId, + + /// The list of compositor underlays for this picture cache + underlays: &'a [ExternalSurfaceDescriptor], +} + +// Mutable state passed to picture cache tiles during post_update +struct TilePostUpdateState<'a> { + /// Allow access to the texture cache for requesting tiles + resource_cache: &'a mut ResourceCache, + + /// Current configuration and setup for compositing all the picture cache tiles in renderer. + composite_state: &'a mut CompositeState, +} + +/// Information about the dependencies of a single primitive instance. +struct PrimitiveDependencyInfo { + /// Unique content identifier of the primitive. + prim_uid: ItemUid, + + /// The (conservative) clipped area in picture space this primitive occupies. + prim_clip_box: PictureBox2D, + + /// Image keys this primitive depends on. + images: SmallVec<[ImageDependency; 8]>, + + /// Opacity bindings this primitive depends on. + opacity_bindings: SmallVec<[OpacityBinding; 4]>, + + /// Color binding this primitive depends on. + color_binding: Option<ColorBinding>, + + /// Clips that this primitive depends on. + clips: SmallVec<[ItemUid; 8]>, + + /// Spatial nodes references by the clip dependencies of this primitive. + spatial_nodes: SmallVec<[SpatialNodeIndex; 4]>, +} + +impl PrimitiveDependencyInfo { + /// Construct dependency info for a new primitive. + fn new( + prim_uid: ItemUid, + prim_clip_box: PictureBox2D, + ) -> Self { + PrimitiveDependencyInfo { + prim_uid, + images: SmallVec::new(), + opacity_bindings: SmallVec::new(), + color_binding: None, + prim_clip_box, + clips: SmallVec::new(), + spatial_nodes: SmallVec::new(), + } + } +} + +/// A stable ID for a given tile, to help debugging. These are also used +/// as unique identifiers for tile surfaces when using a native compositor. +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +#[derive(Hash)] +pub struct TileId(pub usize); + +/// Uniquely identifies a tile within a picture cache slice +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +#[derive(Debug, Copy, Clone, PartialEq, Hash, Eq)] +pub struct TileKey { + // Tile index (x,y) + pub tile_offset: TileOffset, + // Sub-slice (z) + pub sub_slice_index: SubSliceIndex, +} + +/// A descriptor for the kind of texture that a picture cache tile will +/// be drawn into. +#[derive(Debug)] +pub enum SurfaceTextureDescriptor { + /// When using the WR compositor, the tile is drawn into an entry + /// in the WR texture cache. + TextureCache { + handle: Option<PictureCacheTextureHandle>, + }, + /// When using an OS compositor, the tile is drawn into a native + /// surface identified by arbitrary id. + Native { + /// The arbitrary id of this tile. + id: Option<NativeTileId>, + }, +} + +/// This is the same as a `SurfaceTextureDescriptor` but has been resolved +/// into a texture cache handle (if appropriate) that can be used by the +/// batching and compositing code in the renderer. +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub enum ResolvedSurfaceTexture { + TextureCache { + /// The texture ID to draw to. + texture: TextureSource, + }, + Native { + /// The arbitrary id of this tile. + id: NativeTileId, + /// The size of the tile in device pixels. + size: DeviceIntSize, + } +} + +impl SurfaceTextureDescriptor { + /// Create a resolved surface texture for this descriptor + pub fn resolve( + &self, + resource_cache: &ResourceCache, + size: DeviceIntSize, + ) -> ResolvedSurfaceTexture { + match self { + SurfaceTextureDescriptor::TextureCache { handle } => { + let texture = resource_cache + .picture_textures + .get_texture_source(handle.as_ref().unwrap()); + + ResolvedSurfaceTexture::TextureCache { texture } + } + SurfaceTextureDescriptor::Native { id } => { + ResolvedSurfaceTexture::Native { + id: id.expect("bug: native surface not allocated"), + size, + } + } + } + } +} + +/// The backing surface for this tile. +#[derive(Debug)] +pub enum TileSurface { + Texture { + /// Descriptor for the surface that this tile draws into. + descriptor: SurfaceTextureDescriptor, + }, + Color { + color: ColorF, + }, +} + +impl TileSurface { + fn kind(&self) -> &'static str { + match *self { + TileSurface::Color { .. } => "Color", + TileSurface::Texture { .. } => "Texture", + } + } +} + +/// Optional extra information returned by is_same when +/// logging is enabled. +#[derive(Debug, Copy, Clone, PartialEq)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub enum CompareHelperResult<T> { + /// Primitives match + Equal, + /// Counts differ + Count { + prev_count: u8, + curr_count: u8, + }, + /// Sentinel + Sentinel, + /// Two items are not equal + NotEqual { + prev: T, + curr: T, + }, + /// User callback returned true on item + PredicateTrue { + curr: T + }, +} + +/// The result of a primitive dependency comparison. Size is a u8 +/// since this is a hot path in the code, and keeping the data small +/// is a performance win. +#[derive(Debug, Copy, Clone, PartialEq)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +#[repr(u8)] +pub enum PrimitiveCompareResult { + /// Primitives match + Equal, + /// Something in the PrimitiveDescriptor was different + Descriptor, + /// The clip node content or spatial node changed + Clip, + /// The value of the transform changed + Transform, + /// An image dependency was dirty + Image, + /// The value of an opacity binding changed + OpacityBinding, + /// The value of a color binding changed + ColorBinding, +} + +/// Debugging information about why a tile was invalidated +#[derive(Debug,Clone)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub enum InvalidationReason { + /// The background color changed + BackgroundColor, + /// The opaque state of the backing native surface changed + SurfaceOpacityChanged, + /// There was no backing texture (evicted or never rendered) + NoTexture, + /// There was no backing native surface (never rendered, or recreated) + NoSurface, + /// The primitive count in the dependency list was different + PrimCount, + /// The content of one of the primitives was different + Content, + // The compositor type changed + CompositorKindChanged, + // The valid region of the tile changed + ValidRectChanged, + // The overall scale of the picture cache changed + ScaleChanged, + // The content of the sampling surface changed + SurfaceContentChanged, +} + +/// Information about a cached tile. +pub struct Tile { + /// The grid position of this tile within the picture cache + pub tile_offset: TileOffset, + /// The current world rect of this tile. + pub world_tile_rect: WorldRect, + /// The current local rect of this tile. + pub local_tile_rect: PictureRect, + /// The picture space dirty rect for this tile. + pub local_dirty_rect: PictureRect, + /// The device space dirty rect for this tile. + /// TODO(gw): We have multiple dirty rects available due to the quadtree above. In future, + /// expose these as multiple dirty rects, which will help in some cases. + pub device_dirty_rect: DeviceRect, + /// World space rect that contains valid pixels region of this tile. + pub world_valid_rect: WorldRect, + /// Device space rect that contains valid pixels region of this tile. + pub device_valid_rect: DeviceRect, + /// Uniquely describes the content of this tile, in a way that can be + /// (reasonably) efficiently hashed and compared. + pub current_descriptor: TileDescriptor, + /// The content descriptor for this tile from the previous frame. + pub prev_descriptor: TileDescriptor, + /// Handle to the backing surface for this tile. + pub surface: Option<TileSurface>, + /// If true, this tile is marked valid, and the existing texture + /// cache handle can be used. Tiles are invalidated during the + /// build_dirty_regions method. + pub is_valid: bool, + /// If true, this tile intersects with the currently visible screen + /// rect, and will be drawn. + pub is_visible: bool, + /// The tile id is stable between display lists and / or frames, + /// if the tile is retained. Useful for debugging tile evictions. + pub id: TileId, + /// If true, the tile was determined to be opaque, which means blending + /// can be disabled when drawing it. + pub is_opaque: bool, + /// Root node of the quadtree dirty rect tracker. + root: TileNode, + /// The last rendered background color on this tile. + background_color: Option<ColorF>, + /// The first reason the tile was invalidated this frame. + invalidation_reason: Option<InvalidationReason>, + /// The local space valid rect for all primitives that affect this tile. + pub local_valid_rect: PictureBox2D, + /// z-buffer id for this tile + pub z_id: ZBufferId, + pub sub_graphs: Vec<(PictureRect, Vec<(PictureCompositeMode, SurfaceIndex)>)>, +} + +impl Tile { + /// Construct a new, invalid tile. + fn new(tile_offset: TileOffset) -> Self { + let id = TileId(NEXT_TILE_ID.fetch_add(1, Ordering::Relaxed)); + + Tile { + tile_offset, + local_tile_rect: PictureRect::zero(), + world_tile_rect: WorldRect::zero(), + world_valid_rect: WorldRect::zero(), + device_valid_rect: DeviceRect::zero(), + local_dirty_rect: PictureRect::zero(), + device_dirty_rect: DeviceRect::zero(), + surface: None, + current_descriptor: TileDescriptor::new(), + prev_descriptor: TileDescriptor::new(), + is_valid: false, + is_visible: false, + id, + is_opaque: false, + root: TileNode::new_leaf(Vec::new()), + background_color: None, + invalidation_reason: None, + local_valid_rect: PictureBox2D::zero(), + z_id: ZBufferId::invalid(), + sub_graphs: Vec::new(), + } + } + + /// Print debug information about this tile to a tree printer. + fn print(&self, pt: &mut dyn PrintTreePrinter) { + pt.new_level(format!("Tile {:?}", self.id)); + pt.add_item(format!("local_tile_rect: {:?}", self.local_tile_rect)); + pt.add_item(format!("background_color: {:?}", self.background_color)); + pt.add_item(format!("invalidation_reason: {:?}", self.invalidation_reason)); + self.current_descriptor.print(pt); + pt.end_level(); + } + + /// Check if the content of the previous and current tile descriptors match + fn update_dirty_rects( + &mut self, + ctx: &TileUpdateDirtyContext, + state: &mut TileUpdateDirtyState, + invalidation_reason: &mut Option<InvalidationReason>, + frame_context: &FrameVisibilityContext, + ) -> PictureRect { + let mut prim_comparer = PrimitiveComparer::new( + &self.prev_descriptor, + &self.current_descriptor, + state.resource_cache, + state.spatial_node_comparer, + ctx.opacity_bindings, + ctx.color_bindings, + ); + + let mut dirty_rect = PictureBox2D::zero(); + self.root.update_dirty_rects( + &self.prev_descriptor.prims, + &self.current_descriptor.prims, + &mut prim_comparer, + &mut dirty_rect, + state.compare_cache, + invalidation_reason, + frame_context, + ); + + dirty_rect + } + + /// Invalidate a tile based on change in content. This + /// must be called even if the tile is not currently + /// visible on screen. We might be able to improve this + /// later by changing how ComparableVec is used. + fn update_content_validity( + &mut self, + ctx: &TileUpdateDirtyContext, + state: &mut TileUpdateDirtyState, + frame_context: &FrameVisibilityContext, + ) { + // Check if the contents of the primitives, clips, and + // other dependencies are the same. + state.compare_cache.clear(); + let mut invalidation_reason = None; + let dirty_rect = self.update_dirty_rects( + ctx, + state, + &mut invalidation_reason, + frame_context, + ); + if !dirty_rect.is_empty() { + self.invalidate( + Some(dirty_rect), + invalidation_reason.expect("bug: no invalidation_reason"), + ); + } + if ctx.invalidate_all { + self.invalidate(None, InvalidationReason::ScaleChanged); + } + // TODO(gw): We can avoid invalidating the whole tile in some cases here, + // but it should be a fairly rare invalidation case. + if self.current_descriptor.local_valid_rect != self.prev_descriptor.local_valid_rect { + self.invalidate(None, InvalidationReason::ValidRectChanged); + state.composite_state.dirty_rects_are_valid = false; + } + } + + /// Invalidate this tile. If `invalidation_rect` is None, the entire + /// tile is invalidated. + fn invalidate( + &mut self, + invalidation_rect: Option<PictureRect>, + reason: InvalidationReason, + ) { + self.is_valid = false; + + match invalidation_rect { + Some(rect) => { + self.local_dirty_rect = self.local_dirty_rect.union(&rect); + } + None => { + self.local_dirty_rect = self.local_tile_rect; + } + } + + if self.invalidation_reason.is_none() { + self.invalidation_reason = Some(reason); + } + } + + /// Called during pre_update of a tile cache instance. Allows the + /// tile to setup state before primitive dependency calculations. + fn pre_update( + &mut self, + ctx: &TilePreUpdateContext, + ) { + self.local_tile_rect = PictureRect::new( + PicturePoint::new( + self.tile_offset.x as f32 * ctx.tile_size.width, + self.tile_offset.y as f32 * ctx.tile_size.height, + ), + PicturePoint::new( + (self.tile_offset.x + 1) as f32 * ctx.tile_size.width, + (self.tile_offset.y + 1) as f32 * ctx.tile_size.height, + ), + ); + // TODO(gw): This is a hack / fix for Box2D::union in euclid not working with + // zero sized rect accumulation. Once that lands, we'll revert this + // to be zero. + self.local_valid_rect = PictureBox2D::new( + PicturePoint::new( 1.0e32, 1.0e32), + PicturePoint::new(-1.0e32, -1.0e32), + ); + self.invalidation_reason = None; + self.sub_graphs.clear(); + + self.world_tile_rect = ctx.pic_to_world_mapper + .map(&self.local_tile_rect) + .expect("bug: map local tile rect"); + + // Check if this tile is currently on screen. + self.is_visible = self.world_tile_rect.intersects(&ctx.global_screen_world_rect); + + // If the tile isn't visible, early exit, skipping the normal set up to + // validate dependencies. Instead, we will only compare the current tile + // dependencies the next time it comes into view. + if !self.is_visible { + return; + } + + if ctx.background_color != self.background_color { + self.invalidate(None, InvalidationReason::BackgroundColor); + self.background_color = ctx.background_color; + } + + // Clear any dependencies so that when we rebuild them we + // can compare if the tile has the same content. + mem::swap( + &mut self.current_descriptor, + &mut self.prev_descriptor, + ); + self.current_descriptor.clear(); + self.root.clear(self.local_tile_rect); + + // Since this tile is determined to be visible, it will get updated + // dependencies, so update the frame id we are storing dependencies for. + self.current_descriptor.last_updated_frame_id = ctx.frame_id; + } + + /// Add dependencies for a given primitive to this tile. + fn add_prim_dependency( + &mut self, + info: &PrimitiveDependencyInfo, + ) { + // If this tile isn't currently visible, we don't want to update the dependencies + // for this tile, as an optimization, since it won't be drawn anyway. + if !self.is_visible { + return; + } + + // Incorporate the bounding rect of the primitive in the local valid rect + // for this tile. This is used to minimize the size of the scissor rect + // during rasterization and the draw rect during composition of partial tiles. + self.local_valid_rect = self.local_valid_rect.union(&info.prim_clip_box); + + // TODO(gw): The prim_clip_rect can be impacted by the clip rect of the display port, + // which can cause invalidations when a new display list with changed + // display port is received. To work around this, clamp the prim clip rect + // to the tile boundaries - if the clip hasn't affected the tile, then the + // changed clip can't affect the content of the primitive on this tile. + // In future, we could consider supplying the display port clip from Gecko + // in a different way (e.g. as a scroll frame clip) which still provides + // the desired clip for checkerboarding, but doesn't require this extra + // work below. + + // TODO(gw): This is a hot part of the code - we could probably optimize further by: + // - Using min/max instead of clamps below (if we guarantee the rects are well formed) + + let tile_p0 = self.local_tile_rect.min; + let tile_p1 = self.local_tile_rect.max; + + let prim_clip_box = PictureBox2D::new( + PicturePoint::new( + clampf(info.prim_clip_box.min.x, tile_p0.x, tile_p1.x), + clampf(info.prim_clip_box.min.y, tile_p0.y, tile_p1.y), + ), + PicturePoint::new( + clampf(info.prim_clip_box.max.x, tile_p0.x, tile_p1.x), + clampf(info.prim_clip_box.max.y, tile_p0.y, tile_p1.y), + ), + ); + + // Update the tile descriptor, used for tile comparison during scene swaps. + let prim_index = PrimitiveDependencyIndex(self.current_descriptor.prims.len() as u32); + + // Encode the deps for this primitive in the `dep_data` byte buffer + let dep_offset = self.current_descriptor.dep_data.len() as u32; + let mut dep_count = 0; + + for clip in &info.clips { + dep_count += 1; + poke_into_vec( + &PrimitiveDependency::Clip { + clip: *clip, + }, + &mut self.current_descriptor.dep_data, + ); + } + + for spatial_node_index in &info.spatial_nodes { + dep_count += 1; + poke_into_vec( + &PrimitiveDependency::SpatialNode { + index: *spatial_node_index, + }, + &mut self.current_descriptor.dep_data, + ); + } + + for image in &info.images { + dep_count += 1; + poke_into_vec( + &PrimitiveDependency::Image { + image: *image, + }, + &mut self.current_descriptor.dep_data, + ); + } + + for binding in &info.opacity_bindings { + dep_count += 1; + poke_into_vec( + &PrimitiveDependency::OpacityBinding { + binding: *binding, + }, + &mut self.current_descriptor.dep_data, + ); + } + + if let Some(ref binding) = info.color_binding { + dep_count += 1; + poke_into_vec( + &PrimitiveDependency::ColorBinding { + binding: *binding, + }, + &mut self.current_descriptor.dep_data, + ); + } + + self.current_descriptor.prims.push(PrimitiveDescriptor { + prim_uid: info.prim_uid, + prim_clip_box, + dep_offset, + dep_count, + }); + + // Add this primitive to the dirty rect quadtree. + self.root.add_prim(prim_index, &info.prim_clip_box); + } + + /// Called during tile cache instance post_update. Allows invalidation and dirty + /// rect calculation after primitive dependencies have been updated. + fn update_dirty_and_valid_rects( + &mut self, + ctx: &TileUpdateDirtyContext, + state: &mut TileUpdateDirtyState, + frame_context: &FrameVisibilityContext, + ) { + // Ensure peek-poke constraint is met, that `dep_data` is large enough + ensure_red_zone::<PrimitiveDependency>(&mut self.current_descriptor.dep_data); + + // Register the frame id of this tile with the spatial node comparer, to ensure + // that it doesn't GC any spatial nodes from the comparer that are referenced + // by this tile. Must be done before we early exit below, so that we retain + // spatial node info even for tiles that are currently not visible. + state.spatial_node_comparer.retain_for_frame(self.current_descriptor.last_updated_frame_id); + + // If tile is not visible, just early out from here - we don't update dependencies + // so don't want to invalidate, merge, split etc. The tile won't need to be drawn + // (and thus updated / invalidated) until it is on screen again. + if !self.is_visible { + return; + } + + // Calculate the overall valid rect for this tile. + self.current_descriptor.local_valid_rect = self.local_valid_rect; + + // TODO(gw): In theory, the local tile rect should always have an + // intersection with the overall picture rect. In practice, + // due to some accuracy issues with how fract_offset (and + // fp accuracy) are used in the calling method, this isn't + // always true. In this case, it's safe to set the local + // valid rect to zero, which means it will be clipped out + // and not affect the scene. In future, we should fix the + // accuracy issue above, so that this assumption holds, but + // it shouldn't have any noticeable effect on performance + // or memory usage (textures should never get allocated). + self.current_descriptor.local_valid_rect = self.local_tile_rect + .intersection(&ctx.local_rect) + .and_then(|r| r.intersection(&self.current_descriptor.local_valid_rect)) + .unwrap_or_else(PictureRect::zero); + + // The device_valid_rect is referenced during `update_content_validity` so it + // must be updated here first. + self.world_valid_rect = ctx.pic_to_world_mapper + .map(&self.current_descriptor.local_valid_rect) + .expect("bug: map local valid rect"); + + // The device rect is guaranteed to be aligned on a device pixel - the round + // is just to deal with float accuracy. However, the valid rect is not + // always aligned to a device pixel. To handle this, round out to get all + // required pixels, and intersect with the tile device rect. + let device_rect = (self.world_tile_rect * ctx.global_device_pixel_scale).round(); + self.device_valid_rect = (self.world_valid_rect * ctx.global_device_pixel_scale) + .round_out() + .intersection(&device_rect) + .unwrap_or_else(DeviceRect::zero); + + // Invalidate the tile based on the content changing. + self.update_content_validity(ctx, state, frame_context); + } + + /// Called during tile cache instance post_update. Allows invalidation and dirty + /// rect calculation after primitive dependencies have been updated. + fn post_update( + &mut self, + ctx: &TilePostUpdateContext, + state: &mut TilePostUpdateState, + frame_context: &FrameVisibilityContext, + ) { + // If tile is not visible, just early out from here - we don't update dependencies + // so don't want to invalidate, merge, split etc. The tile won't need to be drawn + // (and thus updated / invalidated) until it is on screen again. + if !self.is_visible { + return; + } + + // If there are no primitives there is no need to draw or cache it. + // Bug 1719232 - The final device valid rect does not always describe a non-empty + // region. Cull the tile as a workaround. + if self.current_descriptor.prims.is_empty() || self.device_valid_rect.is_empty() { + // If there is a native compositor surface allocated for this (now empty) tile + // it must be freed here, otherwise the stale tile with previous contents will + // be composited. If the tile subsequently gets new primitives added to it, the + // surface will be re-allocated when it's added to the composite draw list. + if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { mut id, .. }, .. }) = self.surface.take() { + if let Some(id) = id.take() { + state.resource_cache.destroy_compositor_tile(id); + } + } + + self.is_visible = false; + return; + } + + // Check if this tile can be considered opaque. Opacity state must be updated only + // after all early out checks have been performed. Otherwise, we might miss updating + // the native surface next time this tile becomes visible. + let clipped_rect = self.current_descriptor.local_valid_rect + .intersection(&ctx.local_clip_rect) + .unwrap_or_else(PictureRect::zero); + + let has_opaque_bg_color = self.background_color.map_or(false, |c| c.a >= 1.0); + let has_opaque_backdrop = ctx.backdrop.map_or(false, |b| b.opaque_rect.contains_box(&clipped_rect)); + let mut is_opaque = has_opaque_bg_color || has_opaque_backdrop; + + // If this tile intersects with any underlay surfaces, we need to consider it + // translucent, since it will contain an alpha cutout + for underlay in ctx.underlays { + if clipped_rect.intersects(&underlay.local_rect) { + is_opaque = false; + break; + } + } + + // Set the correct z_id for this tile + self.z_id = ctx.z_id; + + if is_opaque != self.is_opaque { + // If opacity changed, the native compositor surface and all tiles get invalidated. + // (this does nothing if not using native compositor mode). + // TODO(gw): This property probably changes very rarely, so it is OK to invalidate + // everything in this case. If it turns out that this isn't true, we could + // consider other options, such as per-tile opacity (natively supported + // on CoreAnimation, and supported if backed by non-virtual surfaces in + // DirectComposition). + if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = self.surface { + if let Some(id) = id.take() { + state.resource_cache.destroy_compositor_tile(id); + } + } + + // Invalidate the entire tile to force a redraw. + self.invalidate(None, InvalidationReason::SurfaceOpacityChanged); + self.is_opaque = is_opaque; + } + + // Check if the selected composite mode supports dirty rect updates. For Draw composite + // mode, we can always update the content with smaller dirty rects, unless there is a + // driver bug to workaround. For native composite mode, we can only use dirty rects if + // the compositor supports partial surface updates. + let (supports_dirty_rects, supports_simple_prims) = match state.composite_state.compositor_kind { + CompositorKind::Draw { .. } | CompositorKind::Layer { .. } => { + (frame_context.config.gpu_supports_render_target_partial_update, true) + } + CompositorKind::Native { capabilities, .. } => { + (capabilities.max_update_rects > 0, false) + } + }; + + // TODO(gw): Consider using smaller tiles and/or tile splits for + // native compositors that don't support dirty rects. + if supports_dirty_rects { + // Only allow splitting for normal content sized tiles + if ctx.current_tile_size == state.resource_cache.picture_textures.default_tile_size() { + let max_split_level = 3; + + // Consider splitting / merging dirty regions + self.root.maybe_merge_or_split( + 0, + &self.current_descriptor.prims, + max_split_level, + ); + } + } + + // The dirty rect will be set correctly by now. If the underlying platform + // doesn't support partial updates, and this tile isn't valid, force the dirty + // rect to be the size of the entire tile. + if !self.is_valid && !supports_dirty_rects { + self.local_dirty_rect = self.local_tile_rect; + } + + // See if this tile is a simple color, in which case we can just draw + // it as a rect, and avoid allocating a texture surface and drawing it. + // TODO(gw): Initial native compositor interface doesn't support simple + // color tiles. We can definitely support this in DC, so this + // should be added as a follow up. + let is_simple_prim = + ctx.backdrop.map_or(false, |b| b.kind.is_some()) && + self.current_descriptor.prims.len() == 1 && + self.is_opaque && + supports_simple_prims; + + // Set up the backing surface for this tile. + let surface = if is_simple_prim { + // If we determine the tile can be represented by a color, set the + // surface unconditionally (this will drop any previously used + // texture cache backing surface). + match ctx.backdrop.unwrap().kind { + Some(BackdropKind::Color { color }) => { + TileSurface::Color { + color, + } + } + None => { + // This should be prevented by the is_simple_prim check above. + unreachable!(); + } + } + } else { + // If this tile will be backed by a surface, we want to retain + // the texture handle from the previous frame, if possible. If + // the tile was previously a color, or not set, then just set + // up a new texture cache handle. + match self.surface.take() { + Some(TileSurface::Texture { descriptor }) => { + // Reuse the existing descriptor and vis mask + TileSurface::Texture { + descriptor, + } + } + Some(TileSurface::Color { .. }) | None => { + // This is the case where we are constructing a tile surface that + // involves drawing to a texture. Create the correct surface + // descriptor depending on the compositing mode that will read + // the output. + let descriptor = match state.composite_state.compositor_kind { + CompositorKind::Draw { .. } | CompositorKind::Layer { .. } => { + // For a texture cache entry, create an invalid handle that + // will be allocated when update_picture_cache is called. + SurfaceTextureDescriptor::TextureCache { + handle: None, + } + } + CompositorKind::Native { .. } => { + // Create a native surface surface descriptor, but don't allocate + // a surface yet. The surface is allocated *after* occlusion + // culling occurs, so that only visible tiles allocate GPU memory. + SurfaceTextureDescriptor::Native { + id: None, + } + } + }; + + TileSurface::Texture { + descriptor, + } + } + } + }; + + // Store the current surface backing info for use during batching. + self.surface = Some(surface); + } +} + +/// Defines a key that uniquely identifies a primitive instance. +#[derive(Debug, Clone)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct PrimitiveDescriptor { + pub prim_uid: ItemUid, + pub prim_clip_box: PictureBox2D, + // TODO(gw): These two fields could be packed as a u24/u8 + pub dep_offset: u32, + pub dep_count: u32, +} + +impl PartialEq for PrimitiveDescriptor { + fn eq(&self, other: &Self) -> bool { + const EPSILON: f32 = 0.001; + + if self.prim_uid != other.prim_uid { + return false; + } + + if !self.prim_clip_box.min.x.approx_eq_eps(&other.prim_clip_box.min.x, &EPSILON) { + return false; + } + if !self.prim_clip_box.min.y.approx_eq_eps(&other.prim_clip_box.min.y, &EPSILON) { + return false; + } + if !self.prim_clip_box.max.x.approx_eq_eps(&other.prim_clip_box.max.x, &EPSILON) { + return false; + } + if !self.prim_clip_box.max.y.approx_eq_eps(&other.prim_clip_box.max.y, &EPSILON) { + return false; + } + + if self.dep_count != other.dep_count { + return false; + } + + true + } +} + +/// Uniquely describes the content of this tile, in a way that can be +/// (reasonably) efficiently hashed and compared. +#[cfg_attr(any(feature="capture",feature="replay"), derive(Clone))] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct TileDescriptor { + /// List of primitive instance unique identifiers. The uid is guaranteed + /// to uniquely describe the content of the primitive template, while + /// the other parameters describe the clip chain and instance params. + prims: Vec<PrimitiveDescriptor>, + + /// Picture space rect that contains valid pixels region of this tile. + pub local_valid_rect: PictureRect, + + /// The last frame this tile had its dependencies updated (dependency updating is + /// skipped if a tile is off-screen). + last_updated_frame_id: FrameId, + + /// Packed per-prim dependency information + dep_data: Vec<u8>, +} + +impl TileDescriptor { + fn new() -> Self { + TileDescriptor { + local_valid_rect: PictureRect::zero(), + dep_data: Vec::new(), + prims: Vec::new(), + last_updated_frame_id: FrameId::INVALID, + } + } + + /// Print debug information about this tile descriptor to a tree printer. + fn print(&self, pt: &mut dyn PrintTreePrinter) { + pt.new_level("current_descriptor".to_string()); + + pt.new_level("prims".to_string()); + for prim in &self.prims { + pt.new_level(format!("prim uid={}", prim.prim_uid.get_uid())); + pt.add_item(format!("clip: p0={},{} p1={},{}", + prim.prim_clip_box.min.x, + prim.prim_clip_box.min.y, + prim.prim_clip_box.max.x, + prim.prim_clip_box.max.y, + )); + pt.end_level(); + } + pt.end_level(); + + pt.end_level(); + } + + /// Clear the dependency information for a tile, when the dependencies + /// are being rebuilt. + fn clear(&mut self) { + self.local_valid_rect = PictureRect::zero(); + self.prims.clear(); + self.dep_data.clear(); + } +} + +/// Represents the dirty region of a tile cache picture, relative to a +/// "visibility" spatial node. At the moment the visibility node is +/// world space, but the plan is to switch to raster space. +/// +/// The plan is to move away from these world space representation and +/// compute dirty regions in raster space instead. +#[derive(Clone)] +pub struct DirtyRegion { + /// The overall dirty rect, a combination of dirty_rects + pub combined: VisRect, + + /// The corrdinate space used to do clipping, visibility, and + /// dirty rect calculations. + pub visibility_spatial_node: SpatialNodeIndex, + /// Spatial node of the picture this region represents. + local_spatial_node: SpatialNodeIndex, +} + +impl DirtyRegion { + /// Construct a new dirty region tracker. + pub fn new( + visibility_spatial_node: SpatialNodeIndex, + local_spatial_node: SpatialNodeIndex, + ) -> Self { + DirtyRegion { + combined: VisRect::zero(), + visibility_spatial_node, + local_spatial_node, + } + } + + /// Reset the dirty regions back to empty + pub fn reset( + &mut self, + visibility_spatial_node: SpatialNodeIndex, + local_spatial_node: SpatialNodeIndex, + ) { + self.combined = VisRect::zero(); + self.visibility_spatial_node = visibility_spatial_node; + self.local_spatial_node = local_spatial_node; + } + + /// Add a dirty region to the tracker. Returns the visibility mask that corresponds to + /// this region in the tracker. + pub fn add_dirty_region( + &mut self, + rect_in_pic_space: PictureRect, + spatial_tree: &SpatialTree, + ) { + let map_pic_to_raster = SpaceMapper::new_with_target( + self.visibility_spatial_node, + self.local_spatial_node, + VisRect::max_rect(), + spatial_tree, + ); + + let raster_rect = map_pic_to_raster + .map(&rect_in_pic_space) + .expect("bug"); + + // Include this in the overall dirty rect + self.combined = self.combined.union(&raster_rect); + } +} + +// TODO(gw): Tidy this up by: +// - Add an Other variant for things like opaque gradient backdrops +#[derive(Debug, Copy, Clone)] +pub enum BackdropKind { + Color { + color: ColorF, + }, +} + +/// Stores information about the calculated opaque backdrop of this slice. +#[derive(Debug, Copy, Clone)] +pub struct BackdropInfo { + /// The picture space rectangle that is known to be opaque. This is used + /// to determine where subpixel AA can be used, and where alpha blending + /// can be disabled. + pub opaque_rect: PictureRect, + /// If the backdrop covers the entire slice with an opaque color, this + /// will be set and can be used as a clear color for the slice's tiles. + pub spanning_opaque_color: Option<ColorF>, + /// Kind of the backdrop + pub kind: Option<BackdropKind>, + /// The picture space rectangle of the backdrop, if kind is set. + pub backdrop_rect: PictureRect, +} + +impl BackdropInfo { + fn empty() -> Self { + BackdropInfo { + opaque_rect: PictureRect::zero(), + spanning_opaque_color: None, + kind: None, + backdrop_rect: PictureRect::zero(), + } + } +} + +/// Represents the native surfaces created for a picture cache, if using +/// a native compositor. An opaque and alpha surface is always created, +/// but tiles are added to a surface based on current opacity. If the +/// calculated opacity of a tile changes, the tile is invalidated and +/// attached to a different native surface. This means that we don't +/// need to invalidate the entire surface if only some tiles are changing +/// opacity. It also means we can take advantage of opaque tiles on cache +/// slices where only some of the tiles are opaque. There is an assumption +/// that creating a native surface is cheap, and only when a tile is added +/// to a surface is there a significant cost. This assumption holds true +/// for the current native compositor implementations on Windows and Mac. +pub struct NativeSurface { + /// Native surface for opaque tiles + pub opaque: NativeSurfaceId, + /// Native surface for alpha tiles + pub alpha: NativeSurfaceId, +} + +/// Hash key for an external native compositor surface +#[derive(PartialEq, Eq, Hash)] +pub struct ExternalNativeSurfaceKey { + /// The YUV/RGB image keys that are used to draw this surface. + pub image_keys: [ImageKey; 3], + /// If this is not an 'external' compositor surface created via + /// Compositor::create_external_surface, this is set to the + /// current device size of the surface. + pub size: Option<DeviceIntSize>, +} + +/// Information about a native compositor surface cached between frames. +pub struct ExternalNativeSurface { + /// If true, the surface was used this frame. Used for a simple form + /// of GC to remove old surfaces. + pub used_this_frame: bool, + /// The native compositor surface handle + pub native_surface_id: NativeSurfaceId, + /// List of image keys, and current image generations, that are drawn in this surface. + /// The image generations are used to check if the compositor surface is dirty and + /// needs to be updated. + pub image_dependencies: [ImageDependency; 3], +} + +/// The key that identifies a tile cache instance. For now, it's simple the index of +/// the slice as it was created during scene building. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct SliceId(usize); + +impl SliceId { + pub fn new(index: usize) -> Self { + SliceId(index) + } +} + +/// Information that is required to reuse or create a new tile cache. Created +/// during scene building and passed to the render backend / frame builder. +pub struct TileCacheParams { + // The current debug flags for the system. + pub debug_flags: DebugFlags, + // Index of the slice (also effectively the key of the tile cache, though we use SliceId where that matters) + pub slice: usize, + // Flags describing content of this cache (e.g. scrollbars) + pub slice_flags: SliceFlags, + // The anchoring spatial node / scroll root + pub spatial_node_index: SpatialNodeIndex, + // The space in which visibility/invalidation/clipping computations are done. + pub visibility_node_index: SpatialNodeIndex, + // Optional background color of this tilecache. If present, can be used as an optimization + // to enable opaque blending and/or subpixel AA in more places. + pub background_color: Option<ColorF>, + // Node in the clip-tree that defines where we exclude clips from child prims + pub shared_clip_node_id: ClipNodeId, + // Clip leaf that is used to build the clip-chain for this tile cache. + pub shared_clip_leaf_id: Option<ClipLeafId>, + // Virtual surface sizes are always square, so this represents both the width and height + pub virtual_surface_size: i32, + // The number of Image surfaces that are being requested for this tile cache. + // This is only a suggestion - the tile cache will clamp this as a reasonable number + // and only promote a limited number of surfaces. + pub image_surface_count: usize, + // The number of YuvImage surfaces that are being requested for this tile cache. + // This is only a suggestion - the tile cache will clamp this as a reasonable number + // and only promote a limited number of surfaces. + pub yuv_image_surface_count: usize, +} + +/// Defines which sub-slice (effectively a z-index) a primitive exists on within +/// a picture cache instance. +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct SubSliceIndex(u8); + +impl SubSliceIndex { + pub const DEFAULT: SubSliceIndex = SubSliceIndex(0); + + pub fn new(index: usize) -> Self { + SubSliceIndex(index as u8) + } + + /// Return true if this sub-slice is the primary sub-slice (for now, we assume + /// that only the primary sub-slice may be opaque and support subpixel AA, for example). + pub fn is_primary(&self) -> bool { + self.0 == 0 + } + + /// Get an array index for this sub-slice + pub fn as_usize(&self) -> usize { + self.0 as usize + } +} + +/// Wrapper struct around an external surface descriptor with a little more information +/// that the picture caching code needs. +pub struct CompositorSurface { + // External surface descriptor used by compositing logic + pub descriptor: ExternalSurfaceDescriptor, + // The compositor surface rect + any intersecting prims. Later prims that intersect + // with this must be added to the next sub-slice. + prohibited_rect: PictureRect, + // If the compositor surface content is opaque. + pub is_opaque: bool, +} + +/// A SubSlice represents a potentially overlapping set of tiles within a picture cache. Most +/// picture cache instances will have only a single sub-slice. The exception to this is when +/// a picture cache has compositor surfaces, in which case sub slices are used to interleave +/// content under or order the compositor surface(s). +pub struct SubSlice { + /// Hash of tiles present in this picture. + pub tiles: FastHashMap<TileOffset, Box<Tile>>, + /// The allocated compositor surfaces for this picture cache. May be None if + /// not using native compositor, or if the surface was destroyed and needs + /// to be reallocated next time this surface contains valid tiles. + pub native_surface: Option<NativeSurface>, + /// List of compositor surfaces that have been promoted from primitives + /// in this tile cache. + pub compositor_surfaces: Vec<CompositorSurface>, + /// List of visible tiles to be composited for this subslice + pub composite_tiles: Vec<CompositeTile>, + /// Compositor descriptors of visible, opaque tiles (used by composite_state.push_surface) + pub opaque_tile_descriptors: Vec<CompositeTileDescriptor>, + /// Compositor descriptors of visible, alpha tiles (used by composite_state.push_surface) + pub alpha_tile_descriptors: Vec<CompositeTileDescriptor>, +} + +impl SubSlice { + /// Construct a new sub-slice + fn new() -> Self { + SubSlice { + tiles: FastHashMap::default(), + native_surface: None, + compositor_surfaces: Vec::new(), + composite_tiles: Vec::new(), + opaque_tile_descriptors: Vec::new(), + alpha_tile_descriptors: Vec::new(), + } + } + + /// Reset the list of compositor surfaces that follow this sub-slice. + /// Built per-frame, since APZ may change whether an image is suitable to be a compositor surface. + fn reset(&mut self) { + self.compositor_surfaces.clear(); + self.composite_tiles.clear(); + self.opaque_tile_descriptors.clear(); + self.alpha_tile_descriptors.clear(); + } + + /// Resize the tile grid to match a new tile bounds + fn resize(&mut self, new_tile_rect: TileRect) -> FastHashMap<TileOffset, Box<Tile>> { + let mut old_tiles = mem::replace(&mut self.tiles, FastHashMap::default()); + self.tiles.reserve(new_tile_rect.area() as usize); + + for y in new_tile_rect.min.y .. new_tile_rect.max.y { + for x in new_tile_rect.min.x .. new_tile_rect.max.x { + let key = TileOffset::new(x, y); + let tile = old_tiles + .remove(&key) + .unwrap_or_else(|| { + Box::new(Tile::new(key)) + }); + self.tiles.insert(key, tile); + } + } + + old_tiles + } +} + +pub struct BackdropSurface { + pub id: NativeSurfaceId, + color: ColorF, + pub device_rect: DeviceRect, +} + +/// Represents a cache of tiles that make up a picture primitives. +pub struct TileCacheInstance { + // The current debug flags for the system. + pub debug_flags: DebugFlags, + /// Index of the tile cache / slice for this frame builder. It's determined + /// by the setup_picture_caching method during flattening, which splits the + /// picture tree into multiple slices. It's used as a simple input to the tile + /// keys. It does mean we invalidate tiles if a new layer gets inserted / removed + /// between display lists - this seems very unlikely to occur on most pages, but + /// can be revisited if we ever notice that. + pub slice: usize, + /// Propagated information about the slice + pub slice_flags: SliceFlags, + /// The currently selected tile size to use for this cache + pub current_tile_size: DeviceIntSize, + /// The list of sub-slices in this tile cache + pub sub_slices: Vec<SubSlice>, + /// The positioning node for this tile cache. + pub spatial_node_index: SpatialNodeIndex, + /// The coordinate space to do visibility/clipping/invalidation in. + pub visibility_node_index: SpatialNodeIndex, + /// List of opacity bindings, with some extra information + /// about whether they changed since last frame. + opacity_bindings: FastHashMap<PropertyBindingId, OpacityBindingInfo>, + /// Switch back and forth between old and new bindings hashmaps to avoid re-allocating. + old_opacity_bindings: FastHashMap<PropertyBindingId, OpacityBindingInfo>, + /// A helper to compare transforms between previous and current frame. + spatial_node_comparer: SpatialNodeComparer, + /// List of color bindings, with some extra information + /// about whether they changed since last frame. + color_bindings: FastHashMap<PropertyBindingId, ColorBindingInfo>, + /// Switch back and forth between old and new bindings hashmaps to avoid re-allocating. + old_color_bindings: FastHashMap<PropertyBindingId, ColorBindingInfo>, + /// The current dirty region tracker for this picture. + pub dirty_region: DirtyRegion, + /// Current size of tiles in picture units. + tile_size: PictureSize, + /// Tile coords of the currently allocated grid. + tile_rect: TileRect, + /// Pre-calculated versions of the tile_rect above, used to speed up the + /// calculations in get_tile_coords_for_rect. + tile_bounds_p0: TileOffset, + tile_bounds_p1: TileOffset, + /// Local rect (unclipped) of the picture this cache covers. + pub local_rect: PictureRect, + /// The local clip rect, from the shared clips of this picture. + pub local_clip_rect: PictureRect, + /// Registered clip in CompositeState for this picture cache + pub compositor_clip: Option<CompositorClipIndex>, + /// The screen rect, transformed to local picture space. + pub screen_rect_in_pic_space: PictureRect, + /// The surface index that this tile cache will be drawn into. + surface_index: SurfaceIndex, + /// The background color from the renderer. If this is set opaque, we know it's + /// fine to clear the tiles to this and allow subpixel text on the first slice. + pub background_color: Option<ColorF>, + /// Information about the calculated backdrop content of this cache. + pub backdrop: BackdropInfo, + /// The allowed subpixel mode for this surface, which depends on the detected + /// opacity of the background. + pub subpixel_mode: SubpixelMode, + // Node in the clip-tree that defines where we exclude clips from child prims + pub shared_clip_node_id: ClipNodeId, + // Clip leaf that is used to build the clip-chain for this tile cache. + pub shared_clip_leaf_id: Option<ClipLeafId>, + /// The number of frames until this cache next evaluates what tile size to use. + /// If a picture rect size is regularly changing just around a size threshold, + /// we don't want to constantly invalidate and reallocate different tile size + /// configuration each frame. + frames_until_size_eval: usize, + /// For DirectComposition, virtual surfaces don't support negative coordinates. However, + /// picture cache tile coordinates can be negative. To handle this, we apply an offset + /// to each tile in DirectComposition. We want to change this as little as possible, + /// to avoid invalidating tiles. However, if we have a picture cache tile coordinate + /// which is outside the virtual surface bounds, we must change this to allow + /// correct remapping of the coordinates passed to BeginDraw in DC. + virtual_offset: DeviceIntPoint, + /// keep around the hash map used as compare_cache to avoid reallocating it each + /// frame. + compare_cache: FastHashMap<PrimitiveComparisonKey, PrimitiveCompareResult>, + /// The currently considered tile size override. Used to check if we should + /// re-evaluate tile size, even if the frame timer hasn't expired. + tile_size_override: Option<DeviceIntSize>, + /// A cache of compositor surfaces that are retained between frames + pub external_native_surface_cache: FastHashMap<ExternalNativeSurfaceKey, ExternalNativeSurface>, + /// Current frame ID of this tile cache instance. Used for book-keeping / garbage collecting + frame_id: FrameId, + /// Registered transform in CompositeState for this picture cache + pub transform_index: CompositorTransformIndex, + /// Current transform mapping local picture space to compositor surface raster space + local_to_raster: ScaleOffset, + /// Current transform mapping compositor surface raster space to final device space + raster_to_device: ScaleOffset, + /// If true, we need to invalidate all tiles during `post_update` + invalidate_all_tiles: bool, + /// The current raster scale for tiles in this cache + current_raster_scale: f32, + /// Depth of off-screen surfaces that are currently pushed during dependency updates + current_surface_traversal_depth: usize, + /// A list of extra dirty invalidation tests that can only be checked once we + /// know the dirty rect of all tiles + deferred_dirty_tests: Vec<DeferredDirtyTest>, + /// Is there a backdrop associated with this cache + found_prims_after_backdrop: bool, + pub backdrop_surface: Option<BackdropSurface>, + /// List of underlay compositor surfaces that exist in this picture cache + pub underlays: Vec<ExternalSurfaceDescriptor>, + /// "Region" (actually a spanning rect) containing all overlay promoted surfaces + pub overlay_region: PictureRect, + /// The number YuvImage prims in this cache, provided in our TileCacheParams. + pub yuv_images_count: usize, + /// The remaining number of YuvImage prims we will see this frame. We prioritize + /// promoting these before promoting any Image prims. + pub yuv_images_remaining: usize, +} + +#[derive(Clone, Copy)] +enum SurfacePromotionFailure { + ImageWaitingOnYuvImage, + NotPremultipliedAlpha, + OverlaySurfaceLimit, + OverlayNeedsMask, + UnderlayAlphaBackdrop, + UnderlaySurfaceLimit, + UnderlayIntersectsOverlay, + UnderlayLowQualityZoom, + NotRootTileCache, + ComplexTransform, + SliceAtomic, + SizeTooLarge, +} + +impl Display for SurfacePromotionFailure { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!( + f, + "{}", + match *self { + SurfacePromotionFailure::ImageWaitingOnYuvImage => "Image prim waiting for all YuvImage prims to be considered for promotion", + SurfacePromotionFailure::NotPremultipliedAlpha => "does not use premultiplied alpha", + SurfacePromotionFailure::OverlaySurfaceLimit => "hit the overlay surface limit", + SurfacePromotionFailure::OverlayNeedsMask => "overlay not allowed for prim with mask", + SurfacePromotionFailure::UnderlayAlphaBackdrop => "underlay requires an opaque backdrop", + SurfacePromotionFailure::UnderlaySurfaceLimit => "hit the underlay surface limit", + SurfacePromotionFailure::UnderlayIntersectsOverlay => "underlay intersects already-promoted overlay", + SurfacePromotionFailure::UnderlayLowQualityZoom => "underlay not allowed during low-quality pinch zoom", + SurfacePromotionFailure::NotRootTileCache => "is not on a root tile cache", + SurfacePromotionFailure::ComplexTransform => "has a complex transform", + SurfacePromotionFailure::SliceAtomic => "slice is atomic", + SurfacePromotionFailure::SizeTooLarge => "surface is too large for compositor", + }.to_owned() + ) + } +} + +impl TileCacheInstance { + pub fn new(params: TileCacheParams) -> Self { + // Determine how many sub-slices we need. Clamp to an arbitrary limit to ensure + // we don't create a huge number of OS compositor tiles and sub-slices. + let sub_slice_count = (params.image_surface_count + params.yuv_image_surface_count).min(MAX_COMPOSITOR_SURFACES) + 1; + + let mut sub_slices = Vec::with_capacity(sub_slice_count); + for _ in 0 .. sub_slice_count { + sub_slices.push(SubSlice::new()); + } + + TileCacheInstance { + debug_flags: params.debug_flags, + slice: params.slice, + slice_flags: params.slice_flags, + spatial_node_index: params.spatial_node_index, + visibility_node_index: params.visibility_node_index, + sub_slices, + opacity_bindings: FastHashMap::default(), + old_opacity_bindings: FastHashMap::default(), + spatial_node_comparer: SpatialNodeComparer::new(), + color_bindings: FastHashMap::default(), + old_color_bindings: FastHashMap::default(), + dirty_region: DirtyRegion::new(params.visibility_node_index, params.spatial_node_index), + tile_size: PictureSize::zero(), + tile_rect: TileRect::zero(), + tile_bounds_p0: TileOffset::zero(), + tile_bounds_p1: TileOffset::zero(), + local_rect: PictureRect::zero(), + local_clip_rect: PictureRect::zero(), + compositor_clip: None, + screen_rect_in_pic_space: PictureRect::zero(), + surface_index: SurfaceIndex(0), + background_color: params.background_color, + backdrop: BackdropInfo::empty(), + subpixel_mode: SubpixelMode::Allow, + shared_clip_node_id: params.shared_clip_node_id, + shared_clip_leaf_id: params.shared_clip_leaf_id, + current_tile_size: DeviceIntSize::zero(), + frames_until_size_eval: 0, + // Default to centering the virtual offset in the middle of the DC virtual surface + virtual_offset: DeviceIntPoint::new( + params.virtual_surface_size / 2, + params.virtual_surface_size / 2, + ), + compare_cache: FastHashMap::default(), + tile_size_override: None, + external_native_surface_cache: FastHashMap::default(), + frame_id: FrameId::INVALID, + transform_index: CompositorTransformIndex::INVALID, + raster_to_device: ScaleOffset::identity(), + local_to_raster: ScaleOffset::identity(), + invalidate_all_tiles: true, + current_raster_scale: 1.0, + current_surface_traversal_depth: 0, + deferred_dirty_tests: Vec::new(), + found_prims_after_backdrop: false, + backdrop_surface: None, + underlays: Vec::new(), + overlay_region: PictureRect::zero(), + yuv_images_count: params.yuv_image_surface_count, + yuv_images_remaining: 0, + } + } + + /// Return the total number of tiles allocated by this tile cache + pub fn tile_count(&self) -> usize { + self.tile_rect.area() as usize * self.sub_slices.len() + } + + /// Trims memory held by the tile cache, such as native surfaces. + pub fn memory_pressure(&mut self, resource_cache: &mut ResourceCache) { + for sub_slice in &mut self.sub_slices { + for tile in sub_slice.tiles.values_mut() { + if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = tile.surface { + // Reseting the id to None with take() ensures that a new + // tile will be allocated during the next frame build. + if let Some(id) = id.take() { + resource_cache.destroy_compositor_tile(id); + } + } + } + if let Some(native_surface) = sub_slice.native_surface.take() { + resource_cache.destroy_compositor_surface(native_surface.opaque); + resource_cache.destroy_compositor_surface(native_surface.alpha); + } + } + } + + /// Reset this tile cache with the updated parameters from a new scene + /// that has arrived. This allows the tile cache to be retained across + /// new scenes. + pub fn prepare_for_new_scene( + &mut self, + params: TileCacheParams, + resource_cache: &mut ResourceCache, + ) { + // We should only receive updated state for matching slice key + assert_eq!(self.slice, params.slice); + + // Determine how many sub-slices we need, based on how many compositor surface prims are + // in the supplied primitive list. + let required_sub_slice_count = (params.image_surface_count + params.yuv_image_surface_count).min(MAX_COMPOSITOR_SURFACES) + 1; + + if self.sub_slices.len() != required_sub_slice_count { + self.tile_rect = TileRect::zero(); + + if self.sub_slices.len() > required_sub_slice_count { + let old_sub_slices = self.sub_slices.split_off(required_sub_slice_count); + + for mut sub_slice in old_sub_slices { + for tile in sub_slice.tiles.values_mut() { + if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = tile.surface { + if let Some(id) = id.take() { + resource_cache.destroy_compositor_tile(id); + } + } + } + + if let Some(native_surface) = sub_slice.native_surface { + resource_cache.destroy_compositor_surface(native_surface.opaque); + resource_cache.destroy_compositor_surface(native_surface.alpha); + } + } + } else { + while self.sub_slices.len() < required_sub_slice_count { + self.sub_slices.push(SubSlice::new()); + } + } + } + + // Store the parameters from the scene builder for this slice. Other + // params in the tile cache are retained and reused, or are always + // updated during pre/post_update. + self.slice_flags = params.slice_flags; + self.spatial_node_index = params.spatial_node_index; + self.background_color = params.background_color; + self.shared_clip_leaf_id = params.shared_clip_leaf_id; + self.shared_clip_node_id = params.shared_clip_node_id; + + // Since the slice flags may have changed, ensure we re-evaluate the + // appropriate tile size for this cache next update. + self.frames_until_size_eval = 0; + + // Update the number of YuvImage prims we have in the scene. + self.yuv_images_count = params.yuv_image_surface_count; + } + + /// Destroy any manually managed resources before this picture cache is + /// destroyed, such as native compositor surfaces. + pub fn destroy( + self, + resource_cache: &mut ResourceCache, + ) { + for sub_slice in self.sub_slices { + if let Some(native_surface) = sub_slice.native_surface { + resource_cache.destroy_compositor_surface(native_surface.opaque); + resource_cache.destroy_compositor_surface(native_surface.alpha); + } + } + + for (_, external_surface) in self.external_native_surface_cache { + resource_cache.destroy_compositor_surface(external_surface.native_surface_id) + } + + if let Some(backdrop_surface) = &self.backdrop_surface { + resource_cache.destroy_compositor_surface(backdrop_surface.id); + } + } + + /// Get the tile coordinates for a given rectangle. + fn get_tile_coords_for_rect( + &self, + rect: &PictureRect, + ) -> (TileOffset, TileOffset) { + // Get the tile coordinates in the picture space. + let mut p0 = TileOffset::new( + (rect.min.x / self.tile_size.width).floor() as i32, + (rect.min.y / self.tile_size.height).floor() as i32, + ); + + let mut p1 = TileOffset::new( + (rect.max.x / self.tile_size.width).ceil() as i32, + (rect.max.y / self.tile_size.height).ceil() as i32, + ); + + // Clamp the tile coordinates here to avoid looping over irrelevant tiles later on. + p0.x = clamp(p0.x, self.tile_bounds_p0.x, self.tile_bounds_p1.x); + p0.y = clamp(p0.y, self.tile_bounds_p0.y, self.tile_bounds_p1.y); + p1.x = clamp(p1.x, self.tile_bounds_p0.x, self.tile_bounds_p1.x); + p1.y = clamp(p1.y, self.tile_bounds_p0.y, self.tile_bounds_p1.y); + + (p0, p1) + } + + /// Update transforms, opacity, color bindings and tile rects. + pub fn pre_update( + &mut self, + surface_index: SurfaceIndex, + frame_context: &FrameVisibilityContext, + frame_state: &mut FrameVisibilityState, + ) -> WorldRect { + let surface = &frame_state.surfaces[surface_index.0]; + let pic_rect = surface.unclipped_local_rect; + + self.surface_index = surface_index; + self.local_rect = pic_rect; + self.local_clip_rect = PictureRect::max_rect(); + self.deferred_dirty_tests.clear(); + self.underlays.clear(); + self.overlay_region = PictureRect::zero(); + self.yuv_images_remaining = self.yuv_images_count; + + for sub_slice in &mut self.sub_slices { + sub_slice.reset(); + } + + // Reset the opaque rect + subpixel mode, as they are calculated + // during the prim dependency checks. + self.backdrop = BackdropInfo::empty(); + + // Calculate the screen rect in picture space, for later comparison against + // backdrops, and prims potentially covering backdrops. + let pic_to_world_mapper = SpaceMapper::new_with_target( + frame_context.root_spatial_node_index, + self.spatial_node_index, + frame_context.global_screen_world_rect, + frame_context.spatial_tree, + ); + self.screen_rect_in_pic_space = pic_to_world_mapper + .unmap(&frame_context.global_screen_world_rect) + .expect("unable to unmap screen rect"); + + let pic_to_vis_mapper = SpaceMapper::new_with_target( + // TODO: use the raster node instead of the root node. + frame_context.root_spatial_node_index, + self.spatial_node_index, + surface.culling_rect, + frame_context.spatial_tree, + ); + + // If there is a valid set of shared clips, build a clip chain instance for this, + // which will provide a local clip rect. This is useful for establishing things + // like whether the backdrop rect supplied by Gecko can be considered opaque. + if let Some(shared_clip_leaf_id) = self.shared_clip_leaf_id { + let map_local_to_picture = SpaceMapper::new( + self.spatial_node_index, + pic_rect, + ); + + frame_state.clip_store.set_active_clips( + self.spatial_node_index, + map_local_to_picture.ref_spatial_node_index, + surface.visibility_spatial_node_index, + shared_clip_leaf_id, + frame_context.spatial_tree, + &mut frame_state.data_stores.clip, + &frame_state.clip_tree, + ); + + let clip_chain_instance = frame_state.clip_store.build_clip_chain_instance( + pic_rect.cast_unit(), + &map_local_to_picture, + &pic_to_vis_mapper, + frame_context.spatial_tree, + &mut frame_state.frame_gpu_data.f32, + frame_state.resource_cache, + frame_context.global_device_pixel_scale, + &surface.culling_rect, + &mut frame_state.data_stores.clip, + frame_state.rg_builder, + true, + ); + + // Ensure that if the entire picture cache is clipped out, the local + // clip rect is zero. This makes sure we don't register any occluders + // that are actually off-screen. + self.local_clip_rect = PictureRect::zero(); + self.compositor_clip = None; + + if let Some(clip_chain) = clip_chain_instance { + self.local_clip_rect = clip_chain.pic_coverage_rect; + self.compositor_clip = None; + + if clip_chain.needs_mask { + for i in 0 .. clip_chain.clips_range.count { + let clip_instance = frame_state + .clip_store + .get_instance_from_range(&clip_chain.clips_range, i); + let clip_node = &frame_state.data_stores.clip[clip_instance.handle]; + + match clip_node.item.kind { + ClipItemKind::RoundedRectangle { rect, radius, mode } => { + assert_eq!(mode, ClipMode::Clip); + + // Map the clip in to device space. We know from the shared + // clip creation logic it's in root coord system, so only a + // 2d axis-aligned transform can apply. For example, in the + // case of a pinch-zoom effect. + let map = ClipSpaceConversion::new( + frame_context.root_spatial_node_index, + clip_node.item.spatial_node_index, + frame_context.root_spatial_node_index, + frame_context.spatial_tree, + ); + + let (rect, radius) = match map { + ClipSpaceConversion::Local => { + (rect.cast_unit(), radius) + } + ClipSpaceConversion::ScaleOffset(scale_offset) => { + ( + scale_offset.map_rect(&rect), + BorderRadius { + top_left: scale_offset.map_size(&radius.top_left), + top_right: scale_offset.map_size(&radius.top_right), + bottom_left: scale_offset.map_size(&radius.bottom_left), + bottom_right: scale_offset.map_size(&radius.bottom_right), + }, + ) + } + ClipSpaceConversion::Transform(..) => { + unreachable!(); + } + }; + + self.compositor_clip = Some(frame_state.composite_state.register_clip( + rect, + radius, + )); + + break; + } + _ => { + // The logic to check for shared clips excludes other mask + // clip types (box-shadow, image-mask) and ensures that the + // clip is in the root coord system (so rect clips can't + // produce a mask). + } + } + } + } + } + } + + // Advance the current frame ID counter for this picture cache (must be done + // after any retained prev state is taken above). + self.frame_id.advance(); + + // Notify the spatial node comparer that a new frame has started, and the + // current reference spatial node for this tile cache. + self.spatial_node_comparer.next_frame(self.spatial_node_index); + + // At the start of the frame, step through each current compositor surface + // and mark it as unused. Later, this is used to free old compositor surfaces. + // TODO(gw): In future, we might make this more sophisticated - for example, + // retaining them for >1 frame if unused, or retaining them in some + // kind of pool to reduce future allocations. + for external_native_surface in self.external_native_surface_cache.values_mut() { + external_native_surface.used_this_frame = false; + } + + // Only evaluate what tile size to use fairly infrequently, so that we don't end + // up constantly invalidating and reallocating tiles if the picture rect size is + // changing near a threshold value. + if self.frames_until_size_eval == 0 || + self.tile_size_override != frame_context.config.tile_size_override { + + // Work out what size tile is appropriate for this picture cache. + let desired_tile_size = match frame_context.config.tile_size_override { + Some(tile_size_override) => { + tile_size_override + } + None => { + if self.slice_flags.contains(SliceFlags::IS_SCROLLBAR) { + if pic_rect.width() <= pic_rect.height() { + TILE_SIZE_SCROLLBAR_VERTICAL + } else { + TILE_SIZE_SCROLLBAR_HORIZONTAL + } + } else { + frame_state.resource_cache.picture_textures.default_tile_size() + } + } + }; + + // If the desired tile size has changed, then invalidate and drop any + // existing tiles. + if desired_tile_size != self.current_tile_size { + for sub_slice in &mut self.sub_slices { + // Destroy any native surfaces on the tiles that will be dropped due + // to resizing. + if let Some(native_surface) = sub_slice.native_surface.take() { + frame_state.resource_cache.destroy_compositor_surface(native_surface.opaque); + frame_state.resource_cache.destroy_compositor_surface(native_surface.alpha); + } + sub_slice.tiles.clear(); + } + self.tile_rect = TileRect::zero(); + self.current_tile_size = desired_tile_size; + } + + // Reset counter until next evaluating the desired tile size. This is an + // arbitrary value. + self.frames_until_size_eval = 120; + self.tile_size_override = frame_context.config.tile_size_override; + } + + // Get the complete scale-offset from local space to device space + let local_to_device = get_relative_scale_offset( + self.spatial_node_index, + frame_context.root_spatial_node_index, + frame_context.spatial_tree, + ); + + // Get the compositor transform, which depends on pinch-zoom mode + let mut raster_to_device = local_to_device; + + if frame_context.config.low_quality_pinch_zoom { + raster_to_device.scale.x /= self.current_raster_scale; + raster_to_device.scale.y /= self.current_raster_scale; + } else { + raster_to_device.scale.x = 1.0; + raster_to_device.scale.y = 1.0; + } + + // Use that compositor transform to calculate a relative local to surface + let local_to_raster = local_to_device.then(&raster_to_device.inverse()); + + const EPSILON: f32 = 0.001; + let compositor_translation_changed = + !raster_to_device.offset.x.approx_eq_eps(&self.raster_to_device.offset.x, &EPSILON) || + !raster_to_device.offset.y.approx_eq_eps(&self.raster_to_device.offset.y, &EPSILON); + let compositor_scale_changed = + !raster_to_device.scale.x.approx_eq_eps(&self.raster_to_device.scale.x, &EPSILON) || + !raster_to_device.scale.y.approx_eq_eps(&self.raster_to_device.scale.y, &EPSILON); + let surface_scale_changed = + !local_to_raster.scale.x.approx_eq_eps(&self.local_to_raster.scale.x, &EPSILON) || + !local_to_raster.scale.y.approx_eq_eps(&self.local_to_raster.scale.y, &EPSILON); + + if compositor_translation_changed || + compositor_scale_changed || + surface_scale_changed || + frame_context.config.force_invalidation { + frame_state.composite_state.dirty_rects_are_valid = false; + } + + self.raster_to_device = raster_to_device; + self.local_to_raster = local_to_raster; + self.invalidate_all_tiles = surface_scale_changed || frame_context.config.force_invalidation; + + // Do a hacky diff of opacity binding values from the last frame. This is + // used later on during tile invalidation tests. + let current_properties = frame_context.scene_properties.float_properties(); + mem::swap(&mut self.opacity_bindings, &mut self.old_opacity_bindings); + + self.opacity_bindings.clear(); + for (id, value) in current_properties { + let changed = match self.old_opacity_bindings.get(id) { + Some(old_property) => !old_property.value.approx_eq(value), + None => true, + }; + self.opacity_bindings.insert(*id, OpacityBindingInfo { + value: *value, + changed, + }); + } + + // Do a hacky diff of color binding values from the last frame. This is + // used later on during tile invalidation tests. + let current_properties = frame_context.scene_properties.color_properties(); + mem::swap(&mut self.color_bindings, &mut self.old_color_bindings); + + self.color_bindings.clear(); + for (id, value) in current_properties { + let changed = match self.old_color_bindings.get(id) { + Some(old_property) => old_property.value != (*value).into(), + None => true, + }; + self.color_bindings.insert(*id, ColorBindingInfo { + value: (*value).into(), + changed, + }); + } + + let world_tile_size = WorldSize::new( + self.current_tile_size.width as f32 / frame_context.global_device_pixel_scale.0, + self.current_tile_size.height as f32 / frame_context.global_device_pixel_scale.0, + ); + + self.tile_size = PictureSize::new( + world_tile_size.width / self.local_to_raster.scale.x, + world_tile_size.height / self.local_to_raster.scale.y, + ); + + // Inflate the needed rect a bit, so that we retain tiles that we have drawn + // but have just recently gone off-screen. This means that we avoid re-drawing + // tiles if the user is scrolling up and down small amounts, at the cost of + // a bit of extra texture memory. + let desired_rect_in_pic_space = self.screen_rect_in_pic_space + .inflate(0.0, 1.0 * self.tile_size.height); + + let needed_rect_in_pic_space = desired_rect_in_pic_space + .intersection(&pic_rect) + .unwrap_or_else(Box2D::zero); + + let p0 = needed_rect_in_pic_space.min; + let p1 = needed_rect_in_pic_space.max; + + let x0 = (p0.x / self.tile_size.width).floor() as i32; + let x1 = (p1.x / self.tile_size.width).ceil() as i32; + + let y0 = (p0.y / self.tile_size.height).floor() as i32; + let y1 = (p1.y / self.tile_size.height).ceil() as i32; + + let new_tile_rect = TileRect { + min: TileOffset::new(x0, y0), + max: TileOffset::new(x1, y1), + }; + + // Determine whether the current bounds of the tile grid will exceed the + // bounds of the DC virtual surface, taking into account the current + // virtual offset. If so, we need to invalidate all tiles, and set up + // a new virtual offset, centered around the current tile grid. + + let virtual_surface_size = frame_context.config.compositor_kind.get_virtual_surface_size(); + // We only need to invalidate in this case if the underlying platform + // uses virtual surfaces. + if virtual_surface_size > 0 { + // Get the extremities of the tile grid after virtual offset is applied + let tx0 = self.virtual_offset.x + x0 * self.current_tile_size.width; + let ty0 = self.virtual_offset.y + y0 * self.current_tile_size.height; + let tx1 = self.virtual_offset.x + (x1+1) * self.current_tile_size.width; + let ty1 = self.virtual_offset.y + (y1+1) * self.current_tile_size.height; + + let need_new_virtual_offset = tx0 < 0 || + ty0 < 0 || + tx1 >= virtual_surface_size || + ty1 >= virtual_surface_size; + + if need_new_virtual_offset { + // Calculate a new virtual offset, centered around the middle of the + // current tile grid. This means we won't need to invalidate and get + // a new offset for a long time! + self.virtual_offset = DeviceIntPoint::new( + (virtual_surface_size/2) - ((x0 + x1) / 2) * self.current_tile_size.width, + (virtual_surface_size/2) - ((y0 + y1) / 2) * self.current_tile_size.height, + ); + + // Invalidate all native tile surfaces. They will be re-allocated next time + // they are scheduled to be rasterized. + for sub_slice in &mut self.sub_slices { + for tile in sub_slice.tiles.values_mut() { + if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = tile.surface { + if let Some(id) = id.take() { + frame_state.resource_cache.destroy_compositor_tile(id); + tile.surface = None; + // Invalidate the entire tile to force a redraw. + // TODO(gw): Add a new invalidation reason for virtual offset changing + tile.invalidate(None, InvalidationReason::CompositorKindChanged); + } + } + } + + // Destroy the native virtual surfaces. They will be re-allocated next time a tile + // that references them is scheduled to draw. + if let Some(native_surface) = sub_slice.native_surface.take() { + frame_state.resource_cache.destroy_compositor_surface(native_surface.opaque); + frame_state.resource_cache.destroy_compositor_surface(native_surface.alpha); + } + } + } + } + + // Rebuild the tile grid if the picture cache rect has changed. + if new_tile_rect != self.tile_rect { + for sub_slice in &mut self.sub_slices { + let mut old_tiles = sub_slice.resize(new_tile_rect); + + // When old tiles that remain after the loop, dirty rects are not valid. + if !old_tiles.is_empty() { + frame_state.composite_state.dirty_rects_are_valid = false; + } + + // Any old tiles that remain after the loop above are going to be dropped. For + // simple composite mode, the texture cache handle will expire and be collected + // by the texture cache. For native compositor mode, we need to explicitly + // invoke a callback to the client to destroy that surface. + frame_state.composite_state.destroy_native_tiles( + old_tiles.values_mut(), + frame_state.resource_cache, + ); + } + } + + // This is duplicated information from tile_rect, but cached here to avoid + // redundant calculations during get_tile_coords_for_rect + self.tile_bounds_p0 = TileOffset::new(x0, y0); + self.tile_bounds_p1 = TileOffset::new(x1, y1); + self.tile_rect = new_tile_rect; + + let mut world_culling_rect = WorldRect::zero(); + + let mut ctx = TilePreUpdateContext { + pic_to_world_mapper, + background_color: self.background_color, + global_screen_world_rect: frame_context.global_screen_world_rect, + tile_size: self.tile_size, + frame_id: self.frame_id, + }; + + // Pre-update each tile + for sub_slice in &mut self.sub_slices { + for tile in sub_slice.tiles.values_mut() { + tile.pre_update(&ctx); + + // Only include the tiles that are currently in view into the world culling + // rect. This is a very important optimization for a couple of reasons: + // (1) Primitives that intersect with tiles in the grid that are not currently + // visible can be skipped from primitive preparation, clip chain building + // and tile dependency updates. + // (2) When we need to allocate an off-screen surface for a child picture (for + // example a CSS filter) we clip the size of the GPU surface to the world + // culling rect below (to ensure we draw enough of it to be sampled by any + // tiles that reference it). Making the world culling rect only affected + // by visible tiles (rather than the entire virtual tile display port) can + // result in allocating _much_ smaller GPU surfaces for cases where the + // true off-screen surface size is very large. + if tile.is_visible { + world_culling_rect = world_culling_rect.union(&tile.world_tile_rect); + } + } + + // The background color can only be applied to the first sub-slice. + ctx.background_color = None; + } + + // If compositor mode is changed, need to drop all incompatible tiles. + match frame_context.config.compositor_kind { + CompositorKind::Draw { .. } | CompositorKind::Layer { .. } => { + for sub_slice in &mut self.sub_slices { + for tile in sub_slice.tiles.values_mut() { + if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = tile.surface { + if let Some(id) = id.take() { + frame_state.resource_cache.destroy_compositor_tile(id); + } + tile.surface = None; + // Invalidate the entire tile to force a redraw. + tile.invalidate(None, InvalidationReason::CompositorKindChanged); + } + } + + if let Some(native_surface) = sub_slice.native_surface.take() { + frame_state.resource_cache.destroy_compositor_surface(native_surface.opaque); + frame_state.resource_cache.destroy_compositor_surface(native_surface.alpha); + } + } + + for (_, external_surface) in self.external_native_surface_cache.drain() { + frame_state.resource_cache.destroy_compositor_surface(external_surface.native_surface_id) + } + } + CompositorKind::Native { .. } => { + // This could hit even when compositor mode is not changed, + // then we need to check if there are incompatible tiles. + for sub_slice in &mut self.sub_slices { + for tile in sub_slice.tiles.values_mut() { + if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::TextureCache { .. }, .. }) = tile.surface { + tile.surface = None; + // Invalidate the entire tile to force a redraw. + tile.invalidate(None, InvalidationReason::CompositorKindChanged); + } + } + } + } + } + + world_culling_rect + } + + fn can_promote_to_surface( + &mut self, + prim_clip_chain: &ClipChainInstance, + prim_spatial_node_index: SpatialNodeIndex, + is_root_tile_cache: bool, + sub_slice_index: usize, + surface_kind: CompositorSurfaceKind, + pic_coverage_rect: PictureRect, + frame_context: &FrameVisibilityContext, + force: bool, + ) -> Result<CompositorSurfaceKind, SurfacePromotionFailure> { + use crate::picture::SurfacePromotionFailure::*; + + // Each strategy has different restrictions on whether we can promote + match surface_kind { + CompositorSurfaceKind::Overlay => { + // For now, only support a small (arbitrary) number of compositor surfaces. + // Non-opaque compositor surfaces require sub-slices, as they are drawn + // as overlays. + if sub_slice_index == self.sub_slices.len() - 1 { + return Err(OverlaySurfaceLimit); + } + + // If a complex clip is being applied to this primitive, it can't be + // promoted directly to a compositor surface. + if prim_clip_chain.needs_mask { + return Err(OverlayNeedsMask); + } + } + CompositorSurfaceKind::Underlay => { + // If a mask is needed, there are some restrictions. + if prim_clip_chain.needs_mask { + // Need an opaque region behind this prim. The opaque region doesn't + // need to span the entire visible region of the TileCacheInstance, + // which would set self.backdrop.kind, but that also qualifies. + if !self.backdrop.opaque_rect.contains_box(&pic_coverage_rect) { + let result = Err(UnderlayAlphaBackdrop); + // If we aren't forcing, give up and return Err. + if !force { + return result; + } + + // Log this but don't return an error. + self.report_promotion_failure(result, pic_coverage_rect, true); + } + + // Only one masked underlay allowed. + if !self.underlays.is_empty() { + return Err(UnderlaySurfaceLimit); + } + } + + // Underlays can't appear on top of overlays, because they can't punch + // through the existing overlay. + if self.overlay_region.intersects(&pic_coverage_rect) { + let result = Err(UnderlayIntersectsOverlay); + // If we aren't forcing, give up and return Err. + if !force { + return result; + } + + // Log this but don't return an error. + self.report_promotion_failure(result, pic_coverage_rect, true); + } + + // Underlay cutouts are difficult to align with compositor surfaces when + // compositing during low-quality zoom, and the required invalidation + // whilst zooming would prevent low-quality zoom from working efficiently. + if frame_context.config.low_quality_pinch_zoom && + frame_context.spatial_tree.get_spatial_node(prim_spatial_node_index).is_ancestor_or_self_zooming + { + return Err(UnderlayLowQualityZoom); + } + } + CompositorSurfaceKind::Blit => unreachable!(), + } + + // If not on the root picture cache, it has some kind of + // complex effect (such as a filter, mix-blend-mode or 3d transform). + if !is_root_tile_cache { + return Err(NotRootTileCache); + } + + let mapper : SpaceMapper<PicturePixel, WorldPixel> = SpaceMapper::new_with_target( + frame_context.root_spatial_node_index, + prim_spatial_node_index, + frame_context.global_screen_world_rect, + &frame_context.spatial_tree); + let transform = mapper.get_transform(); + if !transform.is_2d_scale_translation() { + let result = Err(ComplexTransform); + // Unfortunately, ComplexTransform absolutely prevents proper + // functioning of surface promotion. Treating this as a warning + // instead of an error will cause a crash in get_relative_scale_offset. + return result; + } + + if self.slice_flags.contains(SliceFlags::IS_ATOMIC) { + return Err(SliceAtomic); + } + + Ok(surface_kind) + } + + fn setup_compositor_surfaces_yuv( + &mut self, + sub_slice_index: usize, + prim_info: &mut PrimitiveDependencyInfo, + flags: PrimitiveFlags, + local_prim_rect: LayoutRect, + prim_spatial_node_index: SpatialNodeIndex, + pic_coverage_rect: PictureRect, + frame_context: &FrameVisibilityContext, + image_dependencies: &[ImageDependency;3], + api_keys: &[ImageKey; 3], + resource_cache: &mut ResourceCache, + composite_state: &mut CompositeState, + gpu_buffer: &mut GpuBufferBuilderF, + image_rendering: ImageRendering, + color_depth: ColorDepth, + color_space: YuvRangedColorSpace, + format: YuvFormat, + surface_kind: CompositorSurfaceKind, + ) -> Result<CompositorSurfaceKind, SurfacePromotionFailure> { + for &key in api_keys { + if key != ImageKey::DUMMY { + // TODO: See comment in setup_compositor_surfaces_rgb. + resource_cache.request_image(ImageRequest { + key, + rendering: image_rendering, + tile: None, + }, + gpu_buffer, + ); + } + } + + self.setup_compositor_surfaces_impl( + sub_slice_index, + prim_info, + flags, + local_prim_rect, + prim_spatial_node_index, + pic_coverage_rect, + frame_context, + ExternalSurfaceDependency::Yuv { + image_dependencies: *image_dependencies, + color_space, + format, + channel_bit_depth: color_depth.bit_depth(), + }, + api_keys, + resource_cache, + composite_state, + image_rendering, + true, + surface_kind, + ) + } + + fn setup_compositor_surfaces_rgb( + &mut self, + sub_slice_index: usize, + prim_info: &mut PrimitiveDependencyInfo, + flags: PrimitiveFlags, + local_prim_rect: LayoutRect, + prim_spatial_node_index: SpatialNodeIndex, + pic_coverage_rect: PictureRect, + frame_context: &FrameVisibilityContext, + image_dependency: ImageDependency, + api_key: ImageKey, + resource_cache: &mut ResourceCache, + composite_state: &mut CompositeState, + gpu_buffer: &mut GpuBufferBuilderF, + image_rendering: ImageRendering, + is_opaque: bool, + surface_kind: CompositorSurfaceKind, + ) -> Result<CompositorSurfaceKind, SurfacePromotionFailure> { + let mut api_keys = [ImageKey::DUMMY; 3]; + api_keys[0] = api_key; + + // TODO: The picture compositing code requires images promoted + // into their own picture cache slices to be requested every + // frame even if they are not visible. However the image updates + // are only reached on the prepare pass for visible primitives. + // So we make sure to trigger an image request when promoting + // the image here. + resource_cache.request_image(ImageRequest { + key: api_key, + rendering: image_rendering, + tile: None, + }, + gpu_buffer, + ); + + self.setup_compositor_surfaces_impl( + sub_slice_index, + prim_info, + flags, + local_prim_rect, + prim_spatial_node_index, + pic_coverage_rect, + frame_context, + ExternalSurfaceDependency::Rgb { + image_dependency, + }, + &api_keys, + resource_cache, + composite_state, + image_rendering, + is_opaque, + surface_kind, + ) + } + + // returns false if composition is not available for this surface, + // and the non-compositor path should be used to draw it instead. + fn setup_compositor_surfaces_impl( + &mut self, + sub_slice_index: usize, + prim_info: &mut PrimitiveDependencyInfo, + flags: PrimitiveFlags, + local_prim_rect: LayoutRect, + prim_spatial_node_index: SpatialNodeIndex, + pic_coverage_rect: PictureRect, + frame_context: &FrameVisibilityContext, + dependency: ExternalSurfaceDependency, + api_keys: &[ImageKey; 3], + resource_cache: &mut ResourceCache, + composite_state: &mut CompositeState, + image_rendering: ImageRendering, + is_opaque: bool, + surface_kind: CompositorSurfaceKind, + ) -> Result<CompositorSurfaceKind, SurfacePromotionFailure> { + use crate::picture::SurfacePromotionFailure::*; + + let map_local_to_picture = SpaceMapper::new_with_target( + self.spatial_node_index, + prim_spatial_node_index, + self.local_rect, + frame_context.spatial_tree, + ); + + // Map the primitive local rect into picture space. + let prim_rect = match map_local_to_picture.map(&local_prim_rect) { + Some(rect) => rect, + None => return Ok(surface_kind), + }; + + // If the rect is invalid, no need to create dependencies. + if prim_rect.is_empty() { + return Ok(surface_kind); + } + + let pic_to_world_mapper = SpaceMapper::new_with_target( + frame_context.root_spatial_node_index, + self.spatial_node_index, + frame_context.global_screen_world_rect, + frame_context.spatial_tree, + ); + + let world_clip_rect = pic_to_world_mapper + .map(&prim_info.prim_clip_box) + .expect("bug: unable to map clip to world space"); + + let is_visible = world_clip_rect.intersects(&frame_context.global_screen_world_rect); + if !is_visible { + return Ok(surface_kind); + } + + let prim_offset = ScaleOffset::from_offset(local_prim_rect.min.to_vector().cast_unit()); + + let local_prim_to_device = get_relative_scale_offset( + prim_spatial_node_index, + frame_context.root_spatial_node_index, + frame_context.spatial_tree, + ); + + let normalized_prim_to_device = prim_offset.then(&local_prim_to_device); + + let local_to_raster = ScaleOffset::identity(); + let raster_to_device = normalized_prim_to_device; + + // If this primitive is an external image, and supports being used + // directly by a native compositor, then lookup the external image id + // so we can pass that through. + let mut external_image_id = if flags.contains(PrimitiveFlags::SUPPORTS_EXTERNAL_COMPOSITOR_SURFACE) + && image_rendering == ImageRendering::Auto { + resource_cache.get_image_properties(api_keys[0]) + .and_then(|properties| properties.external_image) + .and_then(|image| Some(image.id)) + } else { + None + }; + + + if let CompositorKind::Native { capabilities, .. } = composite_state.compositor_kind { + if external_image_id.is_some() && + !capabilities.supports_external_compositor_surface_negative_scaling && + (raster_to_device.scale.x < 0.0 || raster_to_device.scale.y < 0.0) { + external_image_id = None; + } + } + + let compositor_transform_index = composite_state.register_transform( + local_to_raster, + raster_to_device, + ); + + let surface_size = composite_state.get_surface_rect( + &local_prim_rect, + &local_prim_rect, + compositor_transform_index, + ).size(); + + let clip_rect = (world_clip_rect * frame_context.global_device_pixel_scale).round(); + + if surface_size.width >= MAX_COMPOSITOR_SURFACES_SIZE || + surface_size.height >= MAX_COMPOSITOR_SURFACES_SIZE { + return Err(SizeTooLarge); + } + + // When using native compositing, we need to find an existing native surface + // handle to use, or allocate a new one. For existing native surfaces, we can + // also determine whether this needs to be updated, depending on whether the + // image generation(s) of the planes have changed since last composite. + let (native_surface_id, update_params) = match composite_state.compositor_kind { + CompositorKind::Draw { .. } | CompositorKind::Layer { .. } => { + (None, None) + } + CompositorKind::Native { .. } => { + let native_surface_size = surface_size.to_i32(); + + let key = ExternalNativeSurfaceKey { + image_keys: *api_keys, + size: if external_image_id.is_some() { None } else { Some(native_surface_size) }, + }; + + let native_surface = self.external_native_surface_cache + .entry(key) + .or_insert_with(|| { + // No existing surface, so allocate a new compositor surface. + let native_surface_id = match external_image_id { + Some(_external_image) => { + // If we have a suitable external image, then create an external + // surface to attach to. + resource_cache.create_compositor_external_surface(is_opaque) + } + None => { + // Otherwise create a normal compositor surface and a single + // compositor tile that covers the entire surface. + let native_surface_id = + resource_cache.create_compositor_surface( + DeviceIntPoint::zero(), + native_surface_size, + is_opaque, + ); + + let tile_id = NativeTileId { + surface_id: native_surface_id, + x: 0, + y: 0, + }; + resource_cache.create_compositor_tile(tile_id); + + native_surface_id + } + }; + + ExternalNativeSurface { + used_this_frame: true, + native_surface_id, + image_dependencies: [ImageDependency::INVALID; 3], + } + }); + + // Mark that the surface is referenced this frame so that the + // backing native surface handle isn't freed. + native_surface.used_this_frame = true; + + let update_params = match external_image_id { + Some(external_image) => { + // If this is an external image surface, then there's no update + // to be done. Just attach the current external image to the surface + // and we're done. + resource_cache.attach_compositor_external_image( + native_surface.native_surface_id, + external_image, + ); + None + } + None => { + // If the image dependencies match, there is no need to update + // the backing native surface. + match dependency { + ExternalSurfaceDependency::Yuv{ image_dependencies, .. } => { + if image_dependencies == native_surface.image_dependencies { + None + } else { + Some(native_surface_size) + } + }, + ExternalSurfaceDependency::Rgb{ image_dependency, .. } => { + if image_dependency == native_surface.image_dependencies[0] { + None + } else { + Some(native_surface_size) + } + }, + } + } + }; + + (Some(native_surface.native_surface_id), update_params) + } + }; + + let descriptor = ExternalSurfaceDescriptor { + local_surface_size: local_prim_rect.size(), + local_rect: prim_rect, + local_clip_rect: prim_info.prim_clip_box, + dependency, + image_rendering, + clip_rect, + transform_index: compositor_transform_index, + z_id: ZBufferId::invalid(), + native_surface_id, + update_params, + external_image_id, + }; + + // If the surface is opaque, we can draw it an an underlay (which avoids + // additional sub-slice surfaces, and supports clip masks) + match surface_kind { + CompositorSurfaceKind::Underlay => { + self.underlays.push(descriptor); + } + CompositorSurfaceKind::Overlay => { + // For compositor surfaces, if we didn't find an earlier sub-slice to add to, + // we know we can append to the current slice. + assert!(sub_slice_index < self.sub_slices.len() - 1); + let sub_slice = &mut self.sub_slices[sub_slice_index]; + + // Each compositor surface allocates a unique z-id + sub_slice.compositor_surfaces.push(CompositorSurface { + prohibited_rect: pic_coverage_rect, + is_opaque, + descriptor, + }); + + // Add the pic_coverage_rect to the overlay region. This prevents + // future promoted surfaces from becoming underlays if they would + // intersect with the overlay region. + self.overlay_region = self.overlay_region.union(&pic_coverage_rect); + } + CompositorSurfaceKind::Blit => unreachable!(), + } + + Ok(surface_kind) + } + + /// Push an estimated rect for an off-screen surface during dependency updates. This is + /// a workaround / hack that allows the picture cache code to know when it should be + /// processing primitive dependencies as a single atomic unit. In future, we aim to remove + /// this hack by having the primitive dependencies stored _within_ each owning picture. + /// This is part of the work required to support child picture caching anyway! + pub fn push_surface( + &mut self, + estimated_local_rect: LayoutRect, + surface_spatial_node_index: SpatialNodeIndex, + spatial_tree: &SpatialTree, + ) { + // Only need to evaluate sub-slice regions if we have compositor surfaces present + if self.current_surface_traversal_depth == 0 && self.sub_slices.len() > 1 { + let map_local_to_picture = SpaceMapper::new_with_target( + self.spatial_node_index, + surface_spatial_node_index, + self.local_rect, + spatial_tree, + ); + + if let Some(pic_rect) = map_local_to_picture.map(&estimated_local_rect) { + // Find the first sub-slice we can add this primitive to (we want to add + // prims to the primary surface if possible, so they get subpixel AA). + for sub_slice in &mut self.sub_slices { + let mut intersects_prohibited_region = false; + + for surface in &mut sub_slice.compositor_surfaces { + if pic_rect.intersects(&surface.prohibited_rect) { + surface.prohibited_rect = surface.prohibited_rect.union(&pic_rect); + + intersects_prohibited_region = true; + } + } + + if !intersects_prohibited_region { + break; + } + } + } + } + + self.current_surface_traversal_depth += 1; + } + + /// Pop an off-screen surface off the stack during dependency updates + pub fn pop_surface(&mut self) { + self.current_surface_traversal_depth -= 1; + } + + fn report_promotion_failure(&self, + result: Result<CompositorSurfaceKind, SurfacePromotionFailure>, + rect: PictureRect, + ignored: bool) { + if !self.debug_flags.contains(DebugFlags::SURFACE_PROMOTION_LOGGING) || result.is_ok() { + return; + } + + // Report this as a warning. + // TODO: Find a way to expose this to web authors. + let outcome = if ignored { "failure ignored" } else { "failed" }; + warn!("Surface promotion of prim at {:?} {outcome} with: {}.", rect, result.unwrap_err()); + } + + /// Update the dependencies for each tile for a given primitive instance. + pub fn update_prim_dependencies( + &mut self, + prim_instance: &mut PrimitiveInstance, + prim_spatial_node_index: SpatialNodeIndex, + local_prim_rect: LayoutRect, + frame_context: &FrameVisibilityContext, + data_stores: &DataStores, + clip_store: &ClipStore, + pictures: &[PicturePrimitive], + resource_cache: &mut ResourceCache, + color_bindings: &ColorBindingStorage, + surface_stack: &[(PictureIndex, SurfaceIndex)], + composite_state: &mut CompositeState, + gpu_buffer: &mut GpuBufferBuilderF, + scratch: &mut PrimitiveScratchBuffer, + is_root_tile_cache: bool, + surfaces: &mut [SurfaceInfo], + profile: &mut TransactionProfile, + ) -> VisibilityState { + use crate::picture::SurfacePromotionFailure::*; + + // This primitive exists on the last element on the current surface stack. + profile_scope!("update_prim_dependencies"); + let prim_surface_index = surface_stack.last().unwrap().1; + let prim_clip_chain = &prim_instance.vis.clip_chain; + + // If the primitive is directly drawn onto this picture cache surface, then + // the pic_coverage_rect is in the same space. If not, we need to map it from + // the intermediate picture space into the picture cache space. + let on_picture_surface = prim_surface_index == self.surface_index; + let pic_coverage_rect = if on_picture_surface { + prim_clip_chain.pic_coverage_rect + } else { + // We want to get the rect in the tile cache picture space that this primitive + // occupies, in order to enable correct invalidation regions. Each surface + // that exists in the chain between this primitive and the tile cache surface + // may have an arbitrary inflation factor (for example, in the case of a series + // of nested blur elements). To account for this, step through the current + // surface stack, mapping the primitive rect into each picture space, including + // the inflation factor from each intermediate surface. + let mut current_pic_coverage_rect = prim_clip_chain.pic_coverage_rect; + let mut current_spatial_node_index = surfaces[prim_surface_index.0] + .surface_spatial_node_index; + + for (pic_index, surface_index) in surface_stack.iter().rev() { + let surface = &surfaces[surface_index.0]; + let pic = &pictures[pic_index.0]; + + let map_local_to_parent = SpaceMapper::new_with_target( + surface.surface_spatial_node_index, + current_spatial_node_index, + surface.unclipped_local_rect, + frame_context.spatial_tree, + ); + + // Map the rect into the parent surface, and inflate if this surface requires + // it. If the rect can't be mapping (e.g. due to an invalid transform) then + // just bail out from the dependencies and cull this primitive. + current_pic_coverage_rect = match map_local_to_parent.map(&current_pic_coverage_rect) { + Some(rect) => { + // TODO(gw): The casts here are a hack. We have some interface inconsistencies + // between layout/picture rects which don't really work with the + // current unit system, since sometimes the local rect of a picture + // is a LayoutRect, and sometimes it's a PictureRect. Consider how + // we can improve this? + pic.composite_mode.as_ref().unwrap().get_coverage( + surface, + Some(rect.cast_unit()), + ).cast_unit() + } + None => { + return VisibilityState::Culled; + } + }; + + current_spatial_node_index = surface.surface_spatial_node_index; + } + + current_pic_coverage_rect + }; + + // Get the tile coordinates in the picture space. + let (p0, p1) = self.get_tile_coords_for_rect(&pic_coverage_rect); + + // If the primitive is outside the tiling rects, it's known to not + // be visible. + if p0.x == p1.x || p0.y == p1.y { + return VisibilityState::Culled; + } + + // Build the list of resources that this primitive has dependencies on. + let mut prim_info = PrimitiveDependencyInfo::new( + prim_instance.uid(), + pic_coverage_rect, + ); + + let mut sub_slice_index = self.sub_slices.len() - 1; + + // Only need to evaluate sub-slice regions if we have compositor surfaces present + if sub_slice_index > 0 { + // Find the first sub-slice we can add this primitive to (we want to add + // prims to the primary surface if possible, so they get subpixel AA). + for (i, sub_slice) in self.sub_slices.iter_mut().enumerate() { + let mut intersects_prohibited_region = false; + + for surface in &mut sub_slice.compositor_surfaces { + if pic_coverage_rect.intersects(&surface.prohibited_rect) { + surface.prohibited_rect = surface.prohibited_rect.union(&pic_coverage_rect); + + intersects_prohibited_region = true; + } + } + + if !intersects_prohibited_region { + sub_slice_index = i; + break; + } + } + } + + // Include the prim spatial node, if differs relative to cache root. + if prim_spatial_node_index != self.spatial_node_index { + prim_info.spatial_nodes.push(prim_spatial_node_index); + } + + // If there was a clip chain, add any clip dependencies to the list for this tile. + let clip_instances = &clip_store + .clip_node_instances[prim_clip_chain.clips_range.to_range()]; + for clip_instance in clip_instances { + let clip = &data_stores.clip[clip_instance.handle]; + + prim_info.clips.push(clip_instance.handle.uid()); + + // If the clip has the same spatial node, the relative transform + // will always be the same, so there's no need to depend on it. + if clip.item.spatial_node_index != self.spatial_node_index + && !prim_info.spatial_nodes.contains(&clip.item.spatial_node_index) { + prim_info.spatial_nodes.push(clip.item.spatial_node_index); + } + } + + // Certain primitives may select themselves to be a backdrop candidate, which is + // then applied below. + let mut backdrop_candidate = None; + + // For pictures, we don't (yet) know the valid clip rect, so we can't correctly + // use it to calculate the local bounding rect for the tiles. If we include them + // then we may calculate a bounding rect that is too large, since it won't include + // the clip bounds of the picture. Excluding them from the bounding rect here + // fixes any correctness issues (the clips themselves are considered when we + // consider the bounds of the primitives that are *children* of the picture), + // however it does potentially result in some un-necessary invalidations of a + // tile (in cases where the picture local rect affects the tile, but the clip + // rect eventually means it doesn't affect that tile). + // TODO(gw): Get picture clips earlier (during the initial picture traversal + // pass) so that we can calculate these correctly. + match prim_instance.kind { + PrimitiveInstanceKind::Picture { pic_index,.. } => { + // Pictures can depend on animated opacity bindings. + let pic = &pictures[pic_index.0]; + if let Some(PictureCompositeMode::Filter(Filter::Opacity(binding, _))) = pic.composite_mode { + prim_info.opacity_bindings.push(binding.into()); + } + } + PrimitiveInstanceKind::Rectangle { data_handle, color_binding_index, .. } => { + // Rectangles can only form a backdrop candidate if they are known opaque. + // TODO(gw): We could resolve the opacity binding here, but the common + // case for background rects is that they don't have animated opacity. + let PrimitiveTemplateKind::Rectangle { color, .. } = data_stores.prim[data_handle].kind; + let color = frame_context.scene_properties.resolve_color(&color); + if color.a >= 1.0 { + backdrop_candidate = Some(BackdropInfo { + opaque_rect: pic_coverage_rect, + spanning_opaque_color: None, + kind: Some(BackdropKind::Color { color }), + backdrop_rect: pic_coverage_rect, + }); + } + + if color_binding_index != ColorBindingIndex::INVALID { + prim_info.color_binding = Some(color_bindings[color_binding_index].into()); + } + } + PrimitiveInstanceKind::Image { data_handle, ref mut compositor_surface_kind, .. } => { + let image_key = &data_stores.image[data_handle]; + let image_data = &image_key.kind; + + // For now, assume that for compositor surface purposes, any RGBA image may be + // translucent. See the comment in `add_prim` in this source file for more + // details. We'll leave the `is_opaque` code branches here, but disabled, as + // in future we will want to support this case correctly. + let mut is_opaque = false; + + if let Some(image_properties) = resource_cache.get_image_properties(image_data.key) { + // For an image to be a possible opaque backdrop, it must: + // - Have a valid, opaque image descriptor + // - Not use tiling (since they can fail to draw) + // - Not having any spacing / padding + // - Have opaque alpha in the instance (flattened) color + if image_properties.descriptor.is_opaque() && + image_properties.tiling.is_none() && + image_data.tile_spacing == LayoutSize::zero() && + image_data.color.a >= 1.0 { + backdrop_candidate = Some(BackdropInfo { + opaque_rect: pic_coverage_rect, + spanning_opaque_color: None, + kind: None, + backdrop_rect: PictureRect::zero(), + }); + } + + is_opaque = image_properties.descriptor.is_opaque(); + } + + let mut promotion_result: Result<CompositorSurfaceKind, SurfacePromotionFailure> = Ok(CompositorSurfaceKind::Blit); + if image_key.common.flags.contains(PrimitiveFlags::PREFER_COMPOSITOR_SURFACE) { + // Only consider promoting Images if all of our YuvImages have been + // processed (whether they were promoted or not). + if self.yuv_images_remaining > 0 { + promotion_result = Err(ImageWaitingOnYuvImage); + } else { + promotion_result = self.can_promote_to_surface(prim_clip_chain, + prim_spatial_node_index, + is_root_tile_cache, + sub_slice_index, + CompositorSurfaceKind::Overlay, + pic_coverage_rect, + frame_context, + false); + } + + // Native OS compositors (DC and CA, at least) support premultiplied alpha + // only. If we have an image that's not pre-multiplied alpha, we can't promote it. + if image_data.alpha_type == AlphaType::Alpha { + promotion_result = Err(NotPremultipliedAlpha); + } + + if let Ok(kind) = promotion_result { + promotion_result = self.setup_compositor_surfaces_rgb( + sub_slice_index, + &mut prim_info, + image_key.common.flags, + local_prim_rect, + prim_spatial_node_index, + pic_coverage_rect, + frame_context, + ImageDependency { + key: image_data.key, + generation: resource_cache.get_image_generation(image_data.key), + }, + image_data.key, + resource_cache, + composite_state, + gpu_buffer, + image_data.image_rendering, + is_opaque, + kind, + ); + } + } + + if let Ok(kind) = promotion_result { + *compositor_surface_kind = kind; + + if kind == CompositorSurfaceKind::Overlay { + profile.inc(profiler::COMPOSITOR_SURFACE_OVERLAYS); + return VisibilityState::Culled; + } + + assert!(kind == CompositorSurfaceKind::Blit, "Image prims should either be overlays or blits."); + } else { + // In Err case, we handle as a blit, and proceed. + self.report_promotion_failure(promotion_result, pic_coverage_rect, false); + *compositor_surface_kind = CompositorSurfaceKind::Blit; + } + + if image_key.common.flags.contains(PrimitiveFlags::PREFER_COMPOSITOR_SURFACE) { + profile.inc(profiler::COMPOSITOR_SURFACE_BLITS); + } + + prim_info.images.push(ImageDependency { + key: image_data.key, + generation: resource_cache.get_image_generation(image_data.key), + }); + } + PrimitiveInstanceKind::YuvImage { data_handle, ref mut compositor_surface_kind, .. } => { + let prim_data = &data_stores.yuv_image[data_handle]; + + let mut promotion_result: Result<CompositorSurfaceKind, SurfacePromotionFailure> = Ok(CompositorSurfaceKind::Blit); + if prim_data.common.flags.contains(PrimitiveFlags::PREFER_COMPOSITOR_SURFACE) { + // Note if this is one of the YuvImages we were considering for + // surface promotion. We only care for primitives that were added + // to us, indicated by is_root_tile_cache. Those are the only ones + // that were added to the TileCacheParams that configured the + // current scene. + if is_root_tile_cache { + self.yuv_images_remaining -= 1; + } + + // Should we force the promotion of this surface? We'll force it if promotion + // is necessary for correct color display. + let force = prim_data.kind.color_depth.bit_depth() > 8; + + let promotion_attempts = + [CompositorSurfaceKind::Overlay, CompositorSurfaceKind::Underlay]; + + for kind in promotion_attempts { + // Since this might be an attempt after an earlier error, clear the flag + // so that we are allowed to report another error. + promotion_result = self.can_promote_to_surface( + prim_clip_chain, + prim_spatial_node_index, + is_root_tile_cache, + sub_slice_index, + kind, + pic_coverage_rect, + frame_context, + force); + if promotion_result.is_ok() { + break; + } + + // We couldn't promote, but did we give up because the slice is marked + // atomic? If that was the reason, and the YuvImage is wide color, + // failing to promote will flatten the colors and look terrible. Let's + // ignore the atomic slice restriction in such a case. + if let Err(SliceAtomic) = promotion_result { + if prim_data.kind. color_depth != ColorDepth::Color8 { + // Let's promote with the attempted kind. + promotion_result = Ok(kind); + break; + } + } + } + + // TODO(gw): When we support RGBA images for external surfaces, we also + // need to check if opaque (YUV images are implicitly opaque). + + // If this primitive is being promoted to a surface, construct an external + // surface descriptor for use later during batching and compositing. We only + // add the image keys for this primitive as a dependency if this is _not_ + // a promoted surface, since we don't want the tiles to invalidate when the + // video content changes, if it's a compositor surface! + if let Ok(kind) = promotion_result { + // Build dependency for each YUV plane, with current image generation for + // later detection of when the composited surface has changed. + let mut image_dependencies = [ImageDependency::INVALID; 3]; + for (key, dep) in prim_data.kind.yuv_key.iter().cloned().zip(image_dependencies.iter_mut()) { + *dep = ImageDependency { + key, + generation: resource_cache.get_image_generation(key), + } + } + + promotion_result = self.setup_compositor_surfaces_yuv( + sub_slice_index, + &mut prim_info, + prim_data.common.flags, + local_prim_rect, + prim_spatial_node_index, + pic_coverage_rect, + frame_context, + &image_dependencies, + &prim_data.kind.yuv_key, + resource_cache, + composite_state, + gpu_buffer, + prim_data.kind.image_rendering, + prim_data.kind.color_depth, + prim_data.kind.color_space.with_range(prim_data.kind.color_range), + prim_data.kind.format, + kind, + ); + } + } + + // Store on the YUV primitive instance whether this is a promoted surface. + // This is used by the batching code to determine whether to draw the + // image to the content tiles, or just a transparent z-write. + if let Ok(kind) = promotion_result { + *compositor_surface_kind = kind; + if kind == CompositorSurfaceKind::Overlay { + profile.inc(profiler::COMPOSITOR_SURFACE_OVERLAYS); + return VisibilityState::Culled; + } + + profile.inc(profiler::COMPOSITOR_SURFACE_UNDERLAYS); + } else { + // In Err case, we handle as a blit, and proceed. + self.report_promotion_failure(promotion_result, pic_coverage_rect, false); + *compositor_surface_kind = CompositorSurfaceKind::Blit; + if prim_data.common.flags.contains(PrimitiveFlags::PREFER_COMPOSITOR_SURFACE) { + profile.inc(profiler::COMPOSITOR_SURFACE_BLITS); + } + } + + if *compositor_surface_kind == CompositorSurfaceKind::Blit { + prim_info.images.extend( + prim_data.kind.yuv_key.iter().map(|key| { + ImageDependency { + key: *key, + generation: resource_cache.get_image_generation(*key), + } + }) + ); + } + } + PrimitiveInstanceKind::ImageBorder { data_handle, .. } => { + let border_data = &data_stores.image_border[data_handle].kind; + prim_info.images.push(ImageDependency { + key: border_data.request.key, + generation: resource_cache.get_image_generation(border_data.request.key), + }); + } + PrimitiveInstanceKind::LinearGradient { data_handle, .. } + | PrimitiveInstanceKind::CachedLinearGradient { data_handle, .. } => { + let gradient_data = &data_stores.linear_grad[data_handle]; + if gradient_data.stops_opacity.is_opaque + && gradient_data.tile_spacing == LayoutSize::zero() + { + backdrop_candidate = Some(BackdropInfo { + opaque_rect: pic_coverage_rect, + spanning_opaque_color: None, + kind: None, + backdrop_rect: PictureRect::zero(), + }); + } + } + PrimitiveInstanceKind::ConicGradient { data_handle, .. } => { + let gradient_data = &data_stores.conic_grad[data_handle]; + if gradient_data.stops_opacity.is_opaque + && gradient_data.tile_spacing == LayoutSize::zero() + { + backdrop_candidate = Some(BackdropInfo { + opaque_rect: pic_coverage_rect, + spanning_opaque_color: None, + kind: None, + backdrop_rect: PictureRect::zero(), + }); + } + } + PrimitiveInstanceKind::RadialGradient { data_handle, .. } => { + let gradient_data = &data_stores.radial_grad[data_handle]; + if gradient_data.stops_opacity.is_opaque + && gradient_data.tile_spacing == LayoutSize::zero() + { + backdrop_candidate = Some(BackdropInfo { + opaque_rect: pic_coverage_rect, + spanning_opaque_color: None, + kind: None, + backdrop_rect: PictureRect::zero(), + }); + } + } + PrimitiveInstanceKind::BackdropCapture { .. } => {} + PrimitiveInstanceKind::BackdropRender { pic_index, .. } => { + // If the area that the backdrop covers in the space of the surface it draws on + // is empty, skip any sub-graph processing. This is not just a performance win, + // it also ensures that we don't do a deferred dirty test that invalidates a tile + // even if the tile isn't actually dirty, which can cause panics later in the + // WR pipeline. + if !pic_coverage_rect.is_empty() { + // Mark that we need the sub-graph this render depends on so that + // we don't skip it during the prepare pass + scratch.required_sub_graphs.insert(pic_index); + + // If this is a sub-graph, register the bounds on any affected tiles + // so we know how much to expand the content tile by. + let sub_slice = &mut self.sub_slices[sub_slice_index]; + + let mut surface_info = Vec::new(); + for (pic_index, surface_index) in surface_stack.iter().rev() { + let pic = &pictures[pic_index.0]; + surface_info.push((pic.composite_mode.as_ref().unwrap().clone(), *surface_index)); + } + + for y in p0.y .. p1.y { + for x in p0.x .. p1.x { + let key = TileOffset::new(x, y); + let tile = sub_slice.tiles.get_mut(&key).expect("bug: no tile"); + tile.sub_graphs.push((pic_coverage_rect, surface_info.clone())); + } + } + + // For backdrop-filter, we need to check if any of the dirty rects + // in tiles that are affected by the filter primitive are dirty. + self.deferred_dirty_tests.push(DeferredDirtyTest { + tile_rect: TileRect::new(p0, p1), + prim_rect: pic_coverage_rect, + }); + } + } + PrimitiveInstanceKind::LineDecoration { .. } | + PrimitiveInstanceKind::NormalBorder { .. } | + PrimitiveInstanceKind::BoxShadow { .. } | + PrimitiveInstanceKind::TextRun { .. } => { + // These don't contribute dependencies + } + }; + + // Calculate the screen rect in local space. When we calculate backdrops, we + // care only that they cover the visible rect (based off the local clip), and + // don't have any overlapping prims in the visible rect. + let visible_local_clip_rect = self.local_clip_rect.intersection(&self.screen_rect_in_pic_space).unwrap_or_default(); + if pic_coverage_rect.intersects(&visible_local_clip_rect) { + self.found_prims_after_backdrop = true; + } + + // If this primitive considers itself a backdrop candidate, apply further + // checks to see if it matches all conditions to be a backdrop. + let mut vis_flags = PrimitiveVisibilityFlags::empty(); + let sub_slice = &mut self.sub_slices[sub_slice_index]; + if let Some(mut backdrop_candidate) = backdrop_candidate { + // Update whether the surface that this primitive exists on + // can be considered opaque. Any backdrop kind other than + // a clear primitive (e.g. color, gradient, image) can be + // considered. + match backdrop_candidate.kind { + Some(BackdropKind::Color { .. }) | None => { + let surface = &mut surfaces[prim_surface_index.0]; + + let is_same_coord_system = frame_context.spatial_tree.is_matching_coord_system( + prim_spatial_node_index, + surface.surface_spatial_node_index, + ); + + // To be an opaque backdrop, it must: + // - Be the same coordinate system (axis-aligned) + // - Have no clip mask + // - Have a rect that covers the surface local rect + if is_same_coord_system && + !prim_clip_chain.needs_mask && + prim_clip_chain.pic_coverage_rect.contains_box(&surface.unclipped_local_rect) + { + // Note that we use `prim_clip_chain.pic_clip_rect` here rather + // than `backdrop_candidate.opaque_rect`. The former is in the + // local space of the surface, the latter is in the local space + // of the top level tile-cache. + surface.is_opaque = true; + } + } + } + + // Check a number of conditions to see if we can consider this + // primitive as an opaque backdrop rect. Several of these are conservative + // checks and could be relaxed in future. However, these checks + // are quick and capture the common cases of background rects and images. + // Specifically, we currently require: + // - The primitive is on the main picture cache surface. + // - Same coord system as picture cache (ensures rects are axis-aligned). + // - No clip masks exist. + let same_coord_system = frame_context.spatial_tree.is_matching_coord_system( + prim_spatial_node_index, + self.spatial_node_index, + ); + + let is_suitable_backdrop = same_coord_system && on_picture_surface; + + if sub_slice_index == 0 && + is_suitable_backdrop && + sub_slice.compositor_surfaces.is_empty() { + + // If the backdrop candidate has a clip-mask, try to extract an opaque inner + // rect that is safe to use for subpixel rendering + if prim_clip_chain.needs_mask { + backdrop_candidate.opaque_rect = clip_store + .get_inner_rect_for_clip_chain( + prim_clip_chain, + &data_stores.clip, + frame_context.spatial_tree, + ) + .unwrap_or(PictureRect::zero()); + } + + // We set the backdrop opaque_rect here, indicating the coverage area, which + // is useful for calculate_subpixel_mode. We will only set the backdrop kind + // if it covers the visible rect. + if backdrop_candidate.opaque_rect.contains_box(&self.backdrop.opaque_rect) { + self.backdrop.opaque_rect = backdrop_candidate.opaque_rect; + } + + if let Some(kind) = backdrop_candidate.kind { + if backdrop_candidate.opaque_rect.contains_box(&visible_local_clip_rect) { + self.found_prims_after_backdrop = false; + self.backdrop.kind = Some(kind); + self.backdrop.backdrop_rect = backdrop_candidate.opaque_rect; + + // If we have a color backdrop that spans the entire local rect, mark + // the visibility flags of the primitive so it is skipped during batching + // (and also clears any previous primitives). Additionally, update our + // background color to match the backdrop color, which will ensure that + // our tiles are cleared to this color. + let BackdropKind::Color { color } = kind; + if backdrop_candidate.opaque_rect.contains_box(&self.local_rect) { + vis_flags |= PrimitiveVisibilityFlags::IS_BACKDROP; + self.backdrop.spanning_opaque_color = Some(color); + } + } + } + } + } -// Maximum blur radius for blur filter (different than box-shadow blur). -// Taken from FilterNodeSoftware.cpp in Gecko. -const MAX_BLUR_RADIUS: f32 = 100.; + // Record any new spatial nodes in the used list. + for spatial_node_index in &prim_info.spatial_nodes { + self.spatial_node_comparer.register_used_transform( + *spatial_node_index, + self.frame_id, + frame_context.spatial_tree, + ); + } -/// Maximum size of a compositor surface. -pub const MAX_COMPOSITOR_SURFACES_SIZE: f32 = 8192.0; + // Normalize the tile coordinates before adding to tile dependencies. + // For each affected tile, mark any of the primitive dependencies. + for y in p0.y .. p1.y { + for x in p0.x .. p1.x { + // TODO(gw): Convert to 2d array temporarily to avoid hash lookups per-tile? + let key = TileOffset::new(x, y); + let tile = sub_slice.tiles.get_mut(&key).expect("bug: no tile"); -pub fn clamp(value: i32, low: i32, high: i32) -> i32 { - value.max(low).min(high) -} + tile.add_prim_dependency(&prim_info); + } + } -pub fn clampf(value: f32, low: f32, high: f32) -> f32 { - value.max(low).min(high) -} + VisibilityState::Visible { + vis_flags, + sub_slice_index: SubSliceIndex::new(sub_slice_index), + } + } -/// A descriptor for the kind of texture that a picture cache tile will -/// be drawn into. -#[derive(Debug)] -pub enum SurfaceTextureDescriptor { - /// When using the WR compositor, the tile is drawn into an entry - /// in the WR texture cache. - TextureCache { - handle: Option<PictureCacheTextureHandle>, - }, - /// When using an OS compositor, the tile is drawn into a native - /// surface identified by arbitrary id. - Native { - /// The arbitrary id of this tile. - id: Option<NativeTileId>, - }, -} + /// Print debug information about this picture cache to a tree printer. + fn print(&self) { + // TODO(gw): This initial implementation is very basic - just printing + // the picture cache state to stdout. In future, we can + // make this dump each frame to a file, and produce a report + // stating which frames had invalidations. This will allow + // diff'ing the invalidation states in a visual tool. + let mut pt = PrintTree::new("Picture Cache"); -/// This is the same as a `SurfaceTextureDescriptor` but has been resolved -/// into a texture cache handle (if appropriate) that can be used by the -/// batching and compositing code in the renderer. -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub enum ResolvedSurfaceTexture { - TextureCache { - /// The texture ID to draw to. - texture: TextureSource, - }, - Native { - /// The arbitrary id of this tile. - id: NativeTileId, - /// The size of the tile in device pixels. - size: DeviceIntSize, + pt.new_level(format!("Slice {:?}", self.slice)); + + pt.add_item(format!("background_color: {:?}", self.background_color)); + + for (sub_slice_index, sub_slice) in self.sub_slices.iter().enumerate() { + pt.new_level(format!("SubSlice {:?}", sub_slice_index)); + + for y in self.tile_bounds_p0.y .. self.tile_bounds_p1.y { + for x in self.tile_bounds_p0.x .. self.tile_bounds_p1.x { + let key = TileOffset::new(x, y); + let tile = &sub_slice.tiles[&key]; + tile.print(&mut pt); + } + } + + pt.end_level(); + } + + pt.end_level(); } -} -impl SurfaceTextureDescriptor { - /// Create a resolved surface texture for this descriptor - pub fn resolve( - &self, - resource_cache: &ResourceCache, - size: DeviceIntSize, - ) -> ResolvedSurfaceTexture { - match self { - SurfaceTextureDescriptor::TextureCache { handle } => { - let texture = resource_cache - .picture_textures - .get_texture_source(handle.as_ref().unwrap()); + fn calculate_subpixel_mode(&self) -> SubpixelMode { + // We can only consider the full opaque cases if there's no underlays + if self.underlays.is_empty() { + let has_opaque_bg_color = self.background_color.map_or(false, |c| c.a >= 1.0); - ResolvedSurfaceTexture::TextureCache { texture } + // If the overall tile cache is known opaque, subpixel AA is allowed everywhere + if has_opaque_bg_color { + return SubpixelMode::Allow; } - SurfaceTextureDescriptor::Native { id } => { - ResolvedSurfaceTexture::Native { - id: id.expect("bug: native surface not allocated"), - size, + + // If the opaque backdrop rect covers the entire tile cache surface, + // we can allow subpixel AA anywhere, skipping the per-text-run tests + // later on during primitive preparation. + if self.backdrop.opaque_rect.contains_box(&self.local_rect) { + return SubpixelMode::Allow; + } + } + + // If we didn't find any valid opaque backdrop, no subpixel AA allowed + if self.backdrop.opaque_rect.is_empty() { + return SubpixelMode::Deny; + } + + // Calculate a prohibited rect where we won't allow subpixel AA. + // TODO(gw): This is conservative - it will disallow subpixel AA if there + // are two underlay surfaces with text placed in between them. That's + // probably unlikely to be an issue in practice, but maybe we should support + // an array of prohibted rects? + let prohibited_rect = self + .underlays + .iter() + .fold( + PictureRect::zero(), + |acc, underlay| { + acc.union(&underlay.local_rect) + } + ); + + // If none of the simple cases above match, we need test where we can support subpixel AA. + // TODO(gw): In future, it may make sense to have > 1 inclusion rect, + // but this handles the common cases. + // TODO(gw): If a text run gets animated such that it's moving in a way that is + // sometimes intersecting with the video rect, this can result in subpixel + // AA flicking on/off for that text run. It's probably very rare, but + // something we should handle in future. + SubpixelMode::Conditional { + allowed_rect: self.backdrop.opaque_rect, + prohibited_rect, + } + } + + /// Apply any updates after prim dependency updates. This applies + /// any late tile invalidations, and sets up the dirty rect and + /// set of tile blits. + pub fn post_update( + &mut self, + frame_context: &FrameVisibilityContext, + composite_state: &mut CompositeState, + resource_cache: &mut ResourceCache, + ) { + assert!(self.current_surface_traversal_depth == 0); + + // TODO: Switch from the root node ot raster space. + let visibility_node = frame_context.spatial_tree.root_reference_frame_index(); + + self.dirty_region.reset(visibility_node, self.spatial_node_index); + self.subpixel_mode = self.calculate_subpixel_mode(); + + self.transform_index = composite_state.register_transform( + self.local_to_raster, + // TODO(gw): Once we support scaling of picture cache tiles during compositing, + // that transform gets plugged in here! + self.raster_to_device, + ); + + let map_pic_to_world = SpaceMapper::new_with_target( + frame_context.root_spatial_node_index, + self.spatial_node_index, + frame_context.global_screen_world_rect, + frame_context.spatial_tree, + ); + + // A simple GC of the native external surface cache, to remove and free any + // surfaces that were not referenced during the update_prim_dependencies pass. + self.external_native_surface_cache.retain(|_, surface| { + if !surface.used_this_frame { + // If we removed an external surface, we need to mark the dirty rects as + // invalid so a full composite occurs on the next frame. + composite_state.dirty_rects_are_valid = false; + + resource_cache.destroy_compositor_surface(surface.native_surface_id); + } + + surface.used_this_frame + }); + + let pic_to_world_mapper = SpaceMapper::new_with_target( + frame_context.root_spatial_node_index, + self.spatial_node_index, + frame_context.global_screen_world_rect, + frame_context.spatial_tree, + ); + + let ctx = TileUpdateDirtyContext { + pic_to_world_mapper, + global_device_pixel_scale: frame_context.global_device_pixel_scale, + opacity_bindings: &self.opacity_bindings, + color_bindings: &self.color_bindings, + local_rect: self.local_rect, + invalidate_all: self.invalidate_all_tiles, + }; + + let mut state = TileUpdateDirtyState { + resource_cache, + composite_state, + compare_cache: &mut self.compare_cache, + spatial_node_comparer: &mut self.spatial_node_comparer, + }; + + // Step through each tile and invalidate if the dependencies have changed. Determine + // the current opacity setting and whether it's changed. + for sub_slice in &mut self.sub_slices { + for tile in sub_slice.tiles.values_mut() { + tile.update_dirty_and_valid_rects(&ctx, &mut state, frame_context); + } + } + + // Process any deferred dirty checks + for sub_slice in &mut self.sub_slices { + for dirty_test in self.deferred_dirty_tests.drain(..) { + // Calculate the total dirty rect from all tiles that this primitive affects + let mut total_dirty_rect = PictureRect::zero(); + + for y in dirty_test.tile_rect.min.y .. dirty_test.tile_rect.max.y { + for x in dirty_test.tile_rect.min.x .. dirty_test.tile_rect.max.x { + let key = TileOffset::new(x, y); + let tile = sub_slice.tiles.get_mut(&key).expect("bug: no tile"); + total_dirty_rect = total_dirty_rect.union(&tile.local_dirty_rect); + } + } + + // If that dirty rect intersects with the local rect of the primitive + // being checked, invalidate that region in all of the affected tiles. + // TODO(gw): This is somewhat conservative, we could be more clever + // here and avoid invalidating every tile when this changes. + // We could also store the dirty rect only when the prim + // is encountered, so that we don't invalidate if something + // *after* the query in the rendering order affects invalidation. + if total_dirty_rect.intersects(&dirty_test.prim_rect) { + for y in dirty_test.tile_rect.min.y .. dirty_test.tile_rect.max.y { + for x in dirty_test.tile_rect.min.x .. dirty_test.tile_rect.max.x { + let key = TileOffset::new(x, y); + let tile = sub_slice.tiles.get_mut(&key).expect("bug: no tile"); + tile.invalidate( + Some(dirty_test.prim_rect), + InvalidationReason::SurfaceContentChanged, + ); + } + } + } + } + } + + let mut ctx = TilePostUpdateContext { + local_clip_rect: self.local_clip_rect, + backdrop: None, + current_tile_size: self.current_tile_size, + z_id: ZBufferId::invalid(), + underlays: &self.underlays, + }; + + let mut state = TilePostUpdateState { + resource_cache, + composite_state, + }; + + for (i, sub_slice) in self.sub_slices.iter_mut().enumerate().rev() { + // The backdrop is only relevant for the first sub-slice + if i == 0 { + ctx.backdrop = Some(self.backdrop); + } + + for compositor_surface in sub_slice.compositor_surfaces.iter_mut().rev() { + compositor_surface.descriptor.z_id = state.composite_state.z_generator.next(); + } + + ctx.z_id = state.composite_state.z_generator.next(); + + for tile in sub_slice.tiles.values_mut() { + tile.post_update(&ctx, &mut state, frame_context); + } + } + + // Assign z-order for each underlay + for underlay in self.underlays.iter_mut().rev() { + underlay.z_id = state.composite_state.z_generator.next(); + } + + // Register any opaque external compositor surfaces as potential occluders. This + // is especially useful when viewing video in full-screen mode, as it is + // able to occlude every background tile (avoiding allocation, rasterizion + // and compositing). + + // Register any underlays as occluders where possible + for underlay in &self.underlays { + if let Some(world_surface_rect) = underlay.get_occluder_rect( + &self.local_clip_rect, + &map_pic_to_world, + ) { + composite_state.register_occluder( + underlay.z_id, + world_surface_rect, + self.compositor_clip, + ); + } + } + + for sub_slice in &self.sub_slices { + for compositor_surface in &sub_slice.compositor_surfaces { + if compositor_surface.is_opaque { + if let Some(world_surface_rect) = compositor_surface.descriptor.get_occluder_rect( + &self.local_clip_rect, + &map_pic_to_world, + ) { + composite_state.register_occluder( + compositor_surface.descriptor.z_id, + world_surface_rect, + self.compositor_clip, + ); + } } } } + + // Register the opaque region of this tile cache as an occluder, which + // is used later in the frame to occlude other tiles. + if !self.backdrop.opaque_rect.is_empty() { + let z_id_backdrop = composite_state.z_generator.next(); + + let backdrop_rect = self.backdrop.opaque_rect + .intersection(&self.local_rect) + .and_then(|r| { + r.intersection(&self.local_clip_rect) + }); + + if let Some(backdrop_rect) = backdrop_rect { + let world_backdrop_rect = map_pic_to_world + .map(&backdrop_rect) + .expect("bug: unable to map backdrop to world space"); + + // Since we register the entire backdrop rect, use the opaque z-id for the + // picture cache slice. + composite_state.register_occluder( + z_id_backdrop, + world_backdrop_rect, + self.compositor_clip, + ); + } + } } } @@ -243,6 +4068,259 @@ impl PictureScratchBuffer { } } +#[derive(Debug, Copy, Clone, PartialEq)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct SurfaceIndex(pub usize); + +/// Information about an offscreen surface. For now, +/// it contains information about the size and coordinate +/// system of the surface. In the future, it will contain +/// information about the contents of the surface, which +/// will allow surfaces to be cached / retained between +/// frames and display lists. +pub struct SurfaceInfo { + /// A local rect defining the size of this surface, in the + /// coordinate system of the parent surface. This contains + /// the unclipped bounding rect of child primitives. + pub unclipped_local_rect: PictureRect, + /// The local space coverage of child primitives after they are + /// are clipped to their owning clip-chain. + pub clipped_local_rect: PictureRect, + /// The (conservative) valid part of this surface rect. Used + /// to reduce the size of render target allocation. + pub clipping_rect: PictureRect, + /// The rectangle to use for culling and clipping. + pub culling_rect: VisRect, + /// Helper structs for mapping local rects in different + /// coordinate systems into the picture coordinates. + pub map_local_to_picture: SpaceMapper<LayoutPixel, PicturePixel>, + /// The positioning node for the surface itself, + pub surface_spatial_node_index: SpatialNodeIndex, + /// The rasterization root for this surface. + pub raster_spatial_node_index: SpatialNodeIndex, + /// The spatial node for culling and clipping (anything using VisPixel). + /// TODO: Replace with the raster spatial node. + pub visibility_spatial_node_index: SpatialNodeIndex, + /// The device pixel ratio specific to this surface. + pub device_pixel_scale: DevicePixelScale, + /// The scale factors of the surface to world transform. + pub world_scale_factors: (f32, f32), + /// Local scale factors surface to raster transform + pub local_scale: (f32, f32), + /// If true, we know this surface is completely opaque. + pub is_opaque: bool, + /// If true, allow snapping on this and child surfaces + pub allow_snapping: bool, + /// If true, the scissor rect must be set when drawing this surface + pub force_scissor_rect: bool, +} + +impl SurfaceInfo { + pub fn new( + surface_spatial_node_index: SpatialNodeIndex, + raster_spatial_node_index: SpatialNodeIndex, + world_rect: WorldRect, + spatial_tree: &SpatialTree, + device_pixel_scale: DevicePixelScale, + world_scale_factors: (f32, f32), + local_scale: (f32, f32), + allow_snapping: bool, + force_scissor_rect: bool, + ) -> Self { + let map_surface_to_world = SpaceMapper::new_with_target( + spatial_tree.root_reference_frame_index(), + surface_spatial_node_index, + world_rect, + spatial_tree, + ); + + let pic_bounds = map_surface_to_world + .unmap(&map_surface_to_world.bounds) + .unwrap_or_else(PictureRect::max_rect); + + let map_local_to_picture = SpaceMapper::new( + surface_spatial_node_index, + pic_bounds, + ); + + // TODO: replace the root with raster space. + let visibility_spatial_node_index = spatial_tree.root_reference_frame_index(); + + SurfaceInfo { + unclipped_local_rect: PictureRect::zero(), + clipped_local_rect: PictureRect::zero(), + is_opaque: false, + clipping_rect: PictureRect::zero(), + map_local_to_picture, + raster_spatial_node_index, + surface_spatial_node_index, + visibility_spatial_node_index, + device_pixel_scale, + world_scale_factors, + local_scale, + allow_snapping, + force_scissor_rect, + // TODO: At the moment all culling is done in world space but + // but the plan is to move it to raster space. + culling_rect: world_rect.cast_unit(), + } + } + + /// Clamps the blur radius depending on scale factors. + pub fn clamp_blur_radius( + &self, + x_blur_radius: f32, + y_blur_radius: f32, + ) -> (f32, f32) { + // Clamping must occur after scale factors are applied, but scale factors are not applied + // until later on. To clamp the blur radius, we first apply the scale factors and then clamp + // and finally revert the scale factors. + + let sx_blur_radius = x_blur_radius * self.local_scale.0; + let sy_blur_radius = y_blur_radius * self.local_scale.1; + + let largest_scaled_blur_radius = f32::max( + sx_blur_radius * self.world_scale_factors.0, + sy_blur_radius * self.world_scale_factors.1, + ); + + if largest_scaled_blur_radius > MAX_BLUR_RADIUS { + let sf = MAX_BLUR_RADIUS / largest_scaled_blur_radius; + (x_blur_radius * sf, y_blur_radius * sf) + } else { + // Return the original blur radius to avoid any rounding errors + (x_blur_radius, y_blur_radius) + } + } + + pub fn update_culling_rect( + &mut self, + parent_culling_rect: VisRect, + composite_mode: &PictureCompositeMode, + frame_context: &FrameVisibilityContext, + ) { + // Set the default culling rect to be the parent, in case we fail + // any mappings below due to weird perspective or invalid transforms. + self.culling_rect = parent_culling_rect; + + if let PictureCompositeMode::Filter(Filter::Blur { width, height, should_inflate, .. }) = composite_mode { + if *should_inflate { + // Space mapping vis <-> picture space + let map_surface_to_vis = SpaceMapper::new_with_target( + // TODO: switch from root to raster space. + frame_context.root_spatial_node_index, + self.surface_spatial_node_index, + parent_culling_rect, + frame_context.spatial_tree, + ); + + // Unmap the parent culling rect to surface space. Note that this may be + // quite conservative in the case of a complex transform, especially perspective. + if let Some(local_parent_culling_rect) = map_surface_to_vis.unmap(&parent_culling_rect) { + let (width_factor, height_factor) = self.clamp_blur_radius(*width, *height); + + // Inflate by the local-space amount this surface extends. + let expanded_rect: PictureBox2D = local_parent_culling_rect.inflate( + width_factor.ceil() * BLUR_SAMPLE_SCALE, + height_factor.ceil() * BLUR_SAMPLE_SCALE, + ); + + // Map back to the expected vis-space culling rect + if let Some(rect) = map_surface_to_vis.map(&expanded_rect) { + self.culling_rect = rect; + } + } + } + } + } + + pub fn map_to_device_rect( + &self, + picture_rect: &PictureRect, + spatial_tree: &SpatialTree, + ) -> DeviceRect { + let raster_rect = if self.raster_spatial_node_index != self.surface_spatial_node_index { + // Currently, the surface's spatial node can be different from its raster node only + // for surfaces in the root coordinate system for snapping reasons. + // See `PicturePrimitive::assign_surface`. + assert_eq!(self.device_pixel_scale.0, 1.0); + assert_eq!(self.raster_spatial_node_index, spatial_tree.root_reference_frame_index()); + + let pic_to_raster = SpaceMapper::new_with_target( + self.raster_spatial_node_index, + self.surface_spatial_node_index, + WorldRect::max_rect(), + spatial_tree, + ); + + pic_to_raster.map(&picture_rect).unwrap() + } else { + picture_rect.cast_unit() + }; + + raster_rect * self.device_pixel_scale + } + + /// Clip and transform a local rect to a device rect suitable for allocating + /// a child off-screen surface of this surface (e.g. for clip-masks) + pub fn get_surface_rect( + &self, + local_rect: &PictureRect, + spatial_tree: &SpatialTree, + ) -> Option<DeviceIntRect> { + let local_rect = match local_rect.intersection(&self.clipping_rect) { + Some(rect) => rect, + None => return None, + }; + + let raster_rect = if self.raster_spatial_node_index != self.surface_spatial_node_index { + assert_eq!(self.device_pixel_scale.0, 1.0); + + let local_to_world = SpaceMapper::new_with_target( + spatial_tree.root_reference_frame_index(), + self.surface_spatial_node_index, + WorldRect::max_rect(), + spatial_tree, + ); + + local_to_world.map(&local_rect).unwrap() + } else { + // The content should have been culled out earlier. + assert!(self.device_pixel_scale.0 > 0.0); + + local_rect.cast_unit() + }; + + let surface_rect = (raster_rect * self.device_pixel_scale).round_out().to_i32(); + if surface_rect.is_empty() { + // The local_rect computed above may have non-empty size that is very + // close to zero. Due to limited arithmetic precision, the SpaceMapper + // might transform the near-zero-sized rect into a zero-sized one. + return None; + } + + Some(surface_rect) + } +} + +/// Information from `get_surface_rects` about the allocated size, UV sampling +/// parameters etc for an off-screen surface +#[derive(Debug)] +struct SurfaceAllocInfo { + task_size: DeviceIntSize, + needs_scissor_rect: bool, + clipped: DeviceRect, + unclipped: DeviceRect, + // Only used for SVGFEGraph currently, this is the source pixels needed to + // render the pixels in clipped. + source: DeviceRect, + // Only used for SVGFEGraph, this is the same as clipped before rounding. + clipped_notsnapped: DeviceRect, + clipped_local: PictureRect, + uv_rect_kind: UvRectKind, +} + #[derive(Debug)] #[cfg_attr(feature = "capture", derive(Serialize))] pub struct RasterConfig { @@ -341,7 +4419,7 @@ impl PictureCompositeMode { // Return prim_subregion for use in get_local_prim_rect, which // is the polygon size. // This must match surface_rects.unclipped_local. - get_coverage_target_svgfe(filters, surface_rect.cast_unit()) + self.get_coverage_target_svgfe(filters, surface_rect.cast_unit()) } _ => { surface_rect @@ -394,8 +4472,8 @@ impl PictureCompositeMode { PictureCompositeMode::SVGFEGraph(ref filters) => { // surface_rect may be for source or target, so invalidate based // on both interpretations - let target_subregion = get_coverage_source_svgfe(filters, surface_rect.cast()); - let source_subregion = get_coverage_target_svgfe(filters, surface_rect.cast()); + let target_subregion = self.get_coverage_source_svgfe(filters, surface_rect.cast()); + let source_subregion = self.get_coverage_target_svgfe(filters, surface_rect.cast()); target_subregion.union(&source_subregion) } _ => { @@ -433,6 +4511,288 @@ impl PictureCompositeMode { PictureCompositeMode::Filter(Filter::SVGGraphNode(..)) => "Filter::SVGGraphNode", } } + + /// Here we transform source rect to target rect for SVGFEGraph by walking + /// the whole graph and propagating subregions based on the provided + /// invalidation rect, and we want it to be a tight fit so we don't waste + /// time applying multiple filters to pixels that do not contribute to the + /// invalidated rect. + /// + /// The interesting parts of the handling of SVG filters are: + /// * scene_building.rs : wrap_prim_with_filters + /// * picture.rs : get_coverage_target_svgfe (you are here) + /// * picture.rs : get_coverage_source_svgfe + /// * render_task.rs : new_svg_filter_graph + /// * render_target.rs : add_svg_filter_node_instances + pub fn get_coverage_target_svgfe( + &self, + filters: &[(FilterGraphNode, FilterGraphOp)], + surface_rect: LayoutRect, + ) -> LayoutRect { + + // The value of BUFFER_LIMIT here must be the same as in + // scene_building.rs, or we'll hit asserts here. + const BUFFER_LIMIT: usize = SVGFE_GRAPH_MAX; + + // We need to evaluate the subregions based on the proposed + // SourceGraphic rect as it isn't known at scene build time. + let mut subregion_by_buffer_id: [LayoutRect; BUFFER_LIMIT] = [LayoutRect::zero(); BUFFER_LIMIT]; + for (id, (node, op)) in filters.iter().enumerate() { + let full_subregion = node.subregion; + let mut used_subregion = LayoutRect::zero(); + for input in &node.inputs { + match input.buffer_id { + FilterOpGraphPictureBufferId::BufferId(id) => { + assert!((id as usize) < BUFFER_LIMIT, "BUFFER_LIMIT must be the same in frame building and scene building"); + // This id lookup should always succeed. + let input_subregion = subregion_by_buffer_id[id as usize]; + // Now add the padding that transforms from + // source to target, this was determined during + // scene build based on the operation. + let input_subregion = + LayoutRect::new( + LayoutPoint::new( + input_subregion.min.x + input.target_padding.min.x, + input_subregion.min.y + input.target_padding.min.y, + ), + LayoutPoint::new( + input_subregion.max.x + input.target_padding.max.x, + input_subregion.max.y + input.target_padding.max.y, + ), + ); + used_subregion = used_subregion + .union(&input_subregion); + } + FilterOpGraphPictureBufferId::None => { + panic!("Unsupported BufferId type"); + } + } + } + // We can clip the used subregion to the node subregion + used_subregion = used_subregion + .intersection(&full_subregion) + .unwrap_or(LayoutRect::zero()); + match op { + FilterGraphOp::SVGFEBlendColor => {} + FilterGraphOp::SVGFEBlendColorBurn => {} + FilterGraphOp::SVGFEBlendColorDodge => {} + FilterGraphOp::SVGFEBlendDarken => {} + FilterGraphOp::SVGFEBlendDifference => {} + FilterGraphOp::SVGFEBlendExclusion => {} + FilterGraphOp::SVGFEBlendHardLight => {} + FilterGraphOp::SVGFEBlendHue => {} + FilterGraphOp::SVGFEBlendLighten => {} + FilterGraphOp::SVGFEBlendLuminosity => {} + FilterGraphOp::SVGFEBlendMultiply => {} + FilterGraphOp::SVGFEBlendNormal => {} + FilterGraphOp::SVGFEBlendOverlay => {} + FilterGraphOp::SVGFEBlendSaturation => {} + FilterGraphOp::SVGFEBlendScreen => {} + FilterGraphOp::SVGFEBlendSoftLight => {} + FilterGraphOp::SVGFEColorMatrix { values } => { + if values[19] > 0.0 { + // Manipulating alpha offset can easily create new + // pixels outside of input subregions + used_subregion = full_subregion; + add_text_marker( + "SVGFEColorMatrix", + "SVGFEColorMatrix with non-zero alpha offset, using full subregion", + Duration::from_millis(1)); + } + } + FilterGraphOp::SVGFEComponentTransfer => unreachable!(), + FilterGraphOp::SVGFEComponentTransferInterned{handle: _, creates_pixels} => { + // Check if the value of alpha[0] is modified, if so + // the whole subregion is used because it will be + // creating new pixels outside of input subregions + if *creates_pixels { + used_subregion = full_subregion; + add_text_marker( + "SVGFEComponentTransfer", + "SVGFEComponentTransfer with non-zero minimum alpha, using full subregion", + Duration::from_millis(1)); + } + } + FilterGraphOp::SVGFECompositeArithmetic { k1, k2, k3, k4 } => { + // Optimization opportunity - some inputs may be + // smaller subregions due to the way the math works, + // k1 is the intersection of the two inputs, k2 is + // the first input only, k3 is the second input + // only, and k4 changes the whole subregion. + // + // See logic for SVG_FECOMPOSITE_OPERATOR_ARITHMETIC + // in FilterSupport.cpp + // + // We can at least ignore the entire node if + // everything is zero. + if *k1 <= 0.0 && + *k2 <= 0.0 && + *k3 <= 0.0 { + used_subregion = LayoutRect::zero(); + } + // Check if alpha is added to pixels as it means it + // can fill pixels outside input subregions + if *k4 > 0.0 { + used_subregion = full_subregion; + add_text_marker( + "SVGFECompositeArithmetic", + "SVGFECompositeArithmetic with non-zero offset, using full subregion", + Duration::from_millis(1)); + } + } + FilterGraphOp::SVGFECompositeATop => {} + FilterGraphOp::SVGFECompositeIn => {} + FilterGraphOp::SVGFECompositeLighter => {} + FilterGraphOp::SVGFECompositeOut => {} + FilterGraphOp::SVGFECompositeOver => {} + FilterGraphOp::SVGFECompositeXOR => {} + FilterGraphOp::SVGFEConvolveMatrixEdgeModeDuplicate{..} => {} + FilterGraphOp::SVGFEConvolveMatrixEdgeModeNone{..} => {} + FilterGraphOp::SVGFEConvolveMatrixEdgeModeWrap{..} => {} + FilterGraphOp::SVGFEDiffuseLightingDistant{..} => {} + FilterGraphOp::SVGFEDiffuseLightingPoint{..} => {} + FilterGraphOp::SVGFEDiffuseLightingSpot{..} => {} + FilterGraphOp::SVGFEDisplacementMap{..} => {} + FilterGraphOp::SVGFEDropShadow{..} => {} + FilterGraphOp::SVGFEFlood { color } => { + // Subregion needs to be set to the full node + // subregion for fills (unless the fill is a no-op) + if color.a > 0.0 { + used_subregion = full_subregion; + } + } + FilterGraphOp::SVGFEGaussianBlur{..} => {} + FilterGraphOp::SVGFEIdentity => {} + FilterGraphOp::SVGFEImage { sampling_filter: _sampling_filter, matrix: _matrix } => { + // TODO: calculate the actual subregion + used_subregion = full_subregion; + } + FilterGraphOp::SVGFEMorphologyDilate{..} => {} + FilterGraphOp::SVGFEMorphologyErode{..} => {} + FilterGraphOp::SVGFEOpacity { valuebinding: _valuebinding, value } => { + // If fully transparent, we can ignore this node + if *value <= 0.0 { + used_subregion = LayoutRect::zero(); + } + } + FilterGraphOp::SVGFESourceAlpha | + FilterGraphOp::SVGFESourceGraphic => { + used_subregion = surface_rect; + } + FilterGraphOp::SVGFESpecularLightingDistant{..} => {} + FilterGraphOp::SVGFESpecularLightingPoint{..} => {} + FilterGraphOp::SVGFESpecularLightingSpot{..} => {} + FilterGraphOp::SVGFETile => { + // feTile fills the entire output with + // source pixels, so it's effectively a flood. + used_subregion = full_subregion; + } + FilterGraphOp::SVGFEToAlpha => {} + FilterGraphOp::SVGFETurbulenceWithFractalNoiseWithNoStitching{..} | + FilterGraphOp::SVGFETurbulenceWithFractalNoiseWithStitching{..} | + FilterGraphOp::SVGFETurbulenceWithTurbulenceNoiseWithNoStitching{..} | + FilterGraphOp::SVGFETurbulenceWithTurbulenceNoiseWithStitching{..} => { + // Turbulence produces pixel values throughout the + // node subregion. + used_subregion = full_subregion; + } + } + // Store the subregion so later nodes can refer back + // to this and propagate rects properly + assert!((id as usize) < BUFFER_LIMIT, "BUFFER_LIMIT must be the same in frame building and scene building"); + subregion_by_buffer_id[id] = used_subregion; + } + subregion_by_buffer_id[filters.len() - 1] + } + + /// Here we transform target rect to source rect for SVGFEGraph by walking + /// the whole graph and propagating subregions based on the provided + /// invalidation rect, and we want it to be a tight fit so we don't waste + /// time applying multiple filters to pixels that do not contribute to the + /// invalidated rect. + /// + /// The interesting parts of the handling of SVG filters are: + /// * scene_building.rs : wrap_prim_with_filters + /// * picture.rs : get_coverage_target_svgfe + /// * picture.rs : get_coverage_source_svgfe (you are here) + /// * render_task.rs : new_svg_filter_graph + /// * render_target.rs : add_svg_filter_node_instances + pub fn get_coverage_source_svgfe( + &self, + filters: &[(FilterGraphNode, FilterGraphOp)], + surface_rect: LayoutRect, + ) -> LayoutRect { + + // The value of BUFFER_LIMIT here must be the same as in + // scene_building.rs, or we'll hit asserts here. + const BUFFER_LIMIT: usize = SVGFE_GRAPH_MAX; + + // We're solving the source rect from target rect (e.g. due + // to invalidation of a region, we need to know how much of + // SourceGraphic is needed to draw that region accurately), + // so we need to walk the DAG in reverse and accumulate the source + // subregion for each input onto the referenced node, which can then + // propagate that to its inputs when it is iterated. + let mut source_subregion = LayoutRect::zero(); + let mut subregion_by_buffer_id: [LayoutRect; BUFFER_LIMIT] = + [LayoutRect::zero(); BUFFER_LIMIT]; + let final_buffer_id = filters.len() - 1; + assert!(final_buffer_id < BUFFER_LIMIT, "BUFFER_LIMIT must be the same in frame building and scene building"); + subregion_by_buffer_id[final_buffer_id] = surface_rect; + for (node_buffer_id, (node, op)) in filters.iter().enumerate().rev() { + // This is the subregion this node outputs, we can clip + // the inputs based on source_padding relative to this, + // and accumulate a new subregion for them. + assert!(node_buffer_id < BUFFER_LIMIT, "BUFFER_LIMIT must be the same in frame building and scene building"); + let full_subregion = node.subregion; + let mut used_subregion = + subregion_by_buffer_id[node_buffer_id]; + // We can clip the propagated subregion to the node subregion before + // we add source_padding for each input and propogate to them + used_subregion = used_subregion + .intersection(&full_subregion) + .unwrap_or(LayoutRect::zero()); + if !used_subregion.is_empty() { + for input in &node.inputs { + let input_subregion = LayoutRect::new( + LayoutPoint::new( + used_subregion.min.x + input.source_padding.min.x, + used_subregion.min.y + input.source_padding.min.y, + ), + LayoutPoint::new( + used_subregion.max.x + input.source_padding.max.x, + used_subregion.max.y + input.source_padding.max.y, + ), + ); + match input.buffer_id { + FilterOpGraphPictureBufferId::BufferId(id) => { + // Add the used area to the input, later when + // the referneced node is iterated as a node it + // will propagate the used bounds. + subregion_by_buffer_id[id as usize] = + subregion_by_buffer_id[id as usize] + .union(&input_subregion); + } + FilterOpGraphPictureBufferId::None => {} + } + } + } + // If this is the SourceGraphic or SourceAlpha, we now have the + // source subregion we're looking for. If both exist in the + // same graph, we need to combine them, so don't merely replace. + match op { + FilterGraphOp::SVGFESourceAlpha | + FilterGraphOp::SVGFESourceGraphic => { + source_subregion = source_subregion.union(&used_subregion); + } + _ => {} + } + } + + // Note that this can be zero if SourceGraphic/SourceAlpha is not used + // in this graph. + source_subregion + } } /// Enum value describing the place of a picture in a 3D context. @@ -2939,6 +7299,595 @@ impl PicturePrimitive { } } +fn get_transform_key( + spatial_node_index: SpatialNodeIndex, + cache_spatial_node_index: SpatialNodeIndex, + spatial_tree: &SpatialTree, +) -> TransformKey { + spatial_tree.get_relative_transform( + spatial_node_index, + cache_spatial_node_index, + ).into() +} + +/// A key for storing primitive comparison results during tile dependency tests. +#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)] +struct PrimitiveComparisonKey { + prev_index: PrimitiveDependencyIndex, + curr_index: PrimitiveDependencyIndex, +} + +/// Information stored an image dependency +#[derive(Debug, Copy, Clone, PartialEq, PeekPoke, Default)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct ImageDependency { + pub key: ImageKey, + pub generation: ImageGeneration, +} + +impl ImageDependency { + pub const INVALID: ImageDependency = ImageDependency { + key: ImageKey::DUMMY, + generation: ImageGeneration::INVALID, + }; +} + +/// In some cases, we need to know the dirty rect of all tiles in order +/// to correctly invalidate a primitive. +#[derive(Debug)] +struct DeferredDirtyTest { + /// The tile rect that the primitive being checked affects + tile_rect: TileRect, + /// The picture-cache local rect of the primitive being checked + prim_rect: PictureRect, +} + +/// A helper struct to compare a primitive and all its sub-dependencies. +struct PrimitiveComparer<'a> { + prev_data: &'a [u8], + curr_data: &'a [u8], + prev_frame_id: FrameId, + curr_frame_id: FrameId, + resource_cache: &'a ResourceCache, + spatial_node_comparer: &'a mut SpatialNodeComparer, + opacity_bindings: &'a FastHashMap<PropertyBindingId, OpacityBindingInfo>, + color_bindings: &'a FastHashMap<PropertyBindingId, ColorBindingInfo>, +} + +impl<'a> PrimitiveComparer<'a> { + fn new( + prev: &'a TileDescriptor, + curr: &'a TileDescriptor, + resource_cache: &'a ResourceCache, + spatial_node_comparer: &'a mut SpatialNodeComparer, + opacity_bindings: &'a FastHashMap<PropertyBindingId, OpacityBindingInfo>, + color_bindings: &'a FastHashMap<PropertyBindingId, ColorBindingInfo>, + ) -> Self { + PrimitiveComparer { + prev_data: &prev.dep_data, + curr_data: &curr.dep_data, + prev_frame_id: prev.last_updated_frame_id, + curr_frame_id: curr.last_updated_frame_id, + resource_cache, + spatial_node_comparer, + opacity_bindings, + color_bindings, + } + } + + /// Check if two primitive descriptors are the same. + fn compare_prim( + &mut self, + prev_desc: &PrimitiveDescriptor, + curr_desc: &PrimitiveDescriptor, + ) -> PrimitiveCompareResult { + let resource_cache = self.resource_cache; + let spatial_node_comparer = &mut self.spatial_node_comparer; + let opacity_bindings = self.opacity_bindings; + let color_bindings = self.color_bindings; + + // Check equality of the PrimitiveDescriptor + if prev_desc != curr_desc { + return PrimitiveCompareResult::Descriptor; + } + + let mut prev_dep_data = &self.prev_data[prev_desc.dep_offset as usize ..]; + let mut curr_dep_data = &self.curr_data[curr_desc.dep_offset as usize ..]; + + let mut prev_dep = PrimitiveDependency::SpatialNode { index: SpatialNodeIndex::INVALID }; + let mut curr_dep = PrimitiveDependency::SpatialNode { index: SpatialNodeIndex::INVALID }; + + debug_assert_eq!(prev_desc.dep_count, curr_desc.dep_count); + + for _ in 0 .. prev_desc.dep_count { + prev_dep_data = peek_from_slice(prev_dep_data, &mut prev_dep); + curr_dep_data = peek_from_slice(curr_dep_data, &mut curr_dep); + + match (&prev_dep, &curr_dep) { + (PrimitiveDependency::Clip { clip: prev }, PrimitiveDependency::Clip { clip: curr }) => { + if prev != curr { + return PrimitiveCompareResult::Clip; + } + } + (PrimitiveDependency::SpatialNode { index: prev }, PrimitiveDependency::SpatialNode { index: curr }) => { + let prev_key = SpatialNodeKey { + spatial_node_index: *prev, + frame_id: self.prev_frame_id, + }; + let curr_key = SpatialNodeKey { + spatial_node_index: *curr, + frame_id: self.curr_frame_id, + }; + if !spatial_node_comparer.are_transforms_equivalent(&prev_key, &curr_key) { + return PrimitiveCompareResult::Transform; + } + } + (PrimitiveDependency::OpacityBinding { binding: prev }, PrimitiveDependency::OpacityBinding { binding: curr }) => { + if prev != curr { + return PrimitiveCompareResult::OpacityBinding; + } + + if let OpacityBinding::Binding(id) = curr { + if opacity_bindings + .get(id) + .map_or(true, |info| info.changed) { + return PrimitiveCompareResult::OpacityBinding; + } + } + } + (PrimitiveDependency::ColorBinding { binding: prev }, PrimitiveDependency::ColorBinding { binding: curr }) => { + if prev != curr { + return PrimitiveCompareResult::ColorBinding; + } + + if let ColorBinding::Binding(id) = curr { + if color_bindings + .get(id) + .map_or(true, |info| info.changed) { + return PrimitiveCompareResult::ColorBinding; + } + } + } + (PrimitiveDependency::Image { image: prev }, PrimitiveDependency::Image { image: curr }) => { + if prev != curr { + return PrimitiveCompareResult::Image; + } + + if resource_cache.get_image_generation(curr.key) != curr.generation { + return PrimitiveCompareResult::Image; + } + } + _ => { + // There was a mismatch between types of dependencies, so something changed + return PrimitiveCompareResult::Descriptor; + } + } + } + + PrimitiveCompareResult::Equal + } +} + +/// Details for a node in a quadtree that tracks dirty rects for a tile. +#[cfg_attr(any(feature="capture",feature="replay"), derive(Clone))] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub enum TileNodeKind { + Leaf { + /// The index buffer of primitives that affected this tile previous frame + #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))] + prev_indices: Vec<PrimitiveDependencyIndex>, + /// The index buffer of primitives that affect this tile on this frame + #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))] + curr_indices: Vec<PrimitiveDependencyIndex>, + /// A bitset of which of the last 64 frames have been dirty for this leaf. + #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))] + dirty_tracker: u64, + /// The number of frames since this node split or merged. + #[cfg_attr(any(feature = "capture", feature = "replay"), serde(skip))] + frames_since_modified: usize, + }, + Node { + /// The four children of this node + children: Vec<TileNode>, + }, +} + +/// The kind of modification that a tile wants to do +#[derive(Copy, Clone, PartialEq, Debug)] +enum TileModification { + Split, + Merge, +} + +/// A node in the dirty rect tracking quadtree. +#[cfg_attr(any(feature="capture",feature="replay"), derive(Clone))] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct TileNode { + /// Leaf or internal node + pub kind: TileNodeKind, + /// Rect of this node in the same space as the tile cache picture + pub rect: PictureBox2D, +} + +impl TileNode { + /// Construct a new leaf node, with the given primitive dependency index buffer + fn new_leaf(curr_indices: Vec<PrimitiveDependencyIndex>) -> Self { + TileNode { + kind: TileNodeKind::Leaf { + prev_indices: Vec::new(), + curr_indices, + dirty_tracker: 0, + frames_since_modified: 0, + }, + rect: PictureBox2D::zero(), + } + } + + /// Draw debug information about this tile node + fn draw_debug_rects( + &self, + pic_to_world_mapper: &SpaceMapper<PicturePixel, WorldPixel>, + is_opaque: bool, + local_valid_rect: PictureRect, + scratch: &mut PrimitiveScratchBuffer, + global_device_pixel_scale: DevicePixelScale, + ) { + match self.kind { + TileNodeKind::Leaf { dirty_tracker, .. } => { + let color = if (dirty_tracker & 1) != 0 { + debug_colors::RED + } else if is_opaque { + debug_colors::GREEN + } else { + debug_colors::YELLOW + }; + + if let Some(local_rect) = local_valid_rect.intersection(&self.rect) { + let world_rect = pic_to_world_mapper + .map(&local_rect) + .unwrap(); + let device_rect = world_rect * global_device_pixel_scale; + + let outer_color = color.scale_alpha(0.3); + let inner_color = outer_color.scale_alpha(0.5); + scratch.push_debug_rect( + device_rect.inflate(-3.0, -3.0), + 1, + outer_color, + inner_color + ); + } + } + TileNodeKind::Node { ref children, .. } => { + for child in children.iter() { + child.draw_debug_rects( + pic_to_world_mapper, + is_opaque, + local_valid_rect, + scratch, + global_device_pixel_scale, + ); + } + } + } + } + + /// Calculate the four child rects for a given node + fn get_child_rects( + rect: &PictureBox2D, + result: &mut [PictureBox2D; 4], + ) { + let p0 = rect.min; + let p1 = rect.max; + let pc = p0 + rect.size() * 0.5; + + *result = [ + PictureBox2D::new( + p0, + pc, + ), + PictureBox2D::new( + PicturePoint::new(pc.x, p0.y), + PicturePoint::new(p1.x, pc.y), + ), + PictureBox2D::new( + PicturePoint::new(p0.x, pc.y), + PicturePoint::new(pc.x, p1.y), + ), + PictureBox2D::new( + pc, + p1, + ), + ]; + } + + /// Called during pre_update, to clear the current dependencies + fn clear( + &mut self, + rect: PictureBox2D, + ) { + self.rect = rect; + + match self.kind { + TileNodeKind::Leaf { ref mut prev_indices, ref mut curr_indices, ref mut dirty_tracker, ref mut frames_since_modified } => { + // Swap current dependencies to be the previous frame + mem::swap(prev_indices, curr_indices); + curr_indices.clear(); + // Note that another frame has passed in the dirty bit trackers + *dirty_tracker = *dirty_tracker << 1; + *frames_since_modified += 1; + } + TileNodeKind::Node { ref mut children, .. } => { + let mut child_rects = [PictureBox2D::zero(); 4]; + TileNode::get_child_rects(&rect, &mut child_rects); + assert_eq!(child_rects.len(), children.len()); + + for (child, rect) in children.iter_mut().zip(child_rects.iter()) { + child.clear(*rect); + } + } + } + } + + /// Add a primitive dependency to this node + fn add_prim( + &mut self, + index: PrimitiveDependencyIndex, + prim_rect: &PictureBox2D, + ) { + match self.kind { + TileNodeKind::Leaf { ref mut curr_indices, .. } => { + curr_indices.push(index); + } + TileNodeKind::Node { ref mut children, .. } => { + for child in children.iter_mut() { + if child.rect.intersects(prim_rect) { + child.add_prim(index, prim_rect); + } + } + } + } + } + + /// Apply a merge or split operation to this tile, if desired + fn maybe_merge_or_split( + &mut self, + level: i32, + curr_prims: &[PrimitiveDescriptor], + max_split_levels: i32, + ) { + // Determine if this tile wants to split or merge + let mut tile_mod = None; + + fn get_dirty_frames( + dirty_tracker: u64, + frames_since_modified: usize, + ) -> Option<u32> { + // Only consider splitting or merging at least 64 frames since we last changed + if frames_since_modified > 64 { + // Each bit in the tracker is a frame that was recently invalidated + Some(dirty_tracker.count_ones()) + } else { + None + } + } + + match self.kind { + TileNodeKind::Leaf { dirty_tracker, frames_since_modified, .. } => { + // Only consider splitting if the tree isn't too deep. + if level < max_split_levels { + if let Some(dirty_frames) = get_dirty_frames(dirty_tracker, frames_since_modified) { + // If the tile has invalidated > 50% of the recent number of frames, split. + if dirty_frames > 32 { + tile_mod = Some(TileModification::Split); + } + } + } + } + TileNodeKind::Node { ref children, .. } => { + // There's two conditions that cause a node to merge its children: + // (1) If _all_ the child nodes are constantly invalidating, then we are wasting + // CPU time tracking dependencies for each child, so merge them. + // (2) If _none_ of the child nodes are recently invalid, then the page content + // has probably changed, and we no longer need to track fine grained dependencies here. + + let mut static_count = 0; + let mut changing_count = 0; + + for child in children { + // Only consider merging nodes at the edge of the tree. + if let TileNodeKind::Leaf { dirty_tracker, frames_since_modified, .. } = child.kind { + if let Some(dirty_frames) = get_dirty_frames(dirty_tracker, frames_since_modified) { + if dirty_frames == 0 { + // Hasn't been invalidated for some time + static_count += 1; + } else if dirty_frames == 64 { + // Is constantly being invalidated + changing_count += 1; + } + } + } + + // Only merge if all the child tiles are in agreement. Otherwise, we have some + // that are invalidating / static, and it's worthwhile tracking dependencies for + // them individually. + if static_count == 4 || changing_count == 4 { + tile_mod = Some(TileModification::Merge); + } + } + } + } + + match tile_mod { + Some(TileModification::Split) => { + // To split a node, take the current dependency index buffer for this node, and + // split it into child index buffers. + let curr_indices = match self.kind { + TileNodeKind::Node { .. } => { + unreachable!("bug - only leaves can split"); + } + TileNodeKind::Leaf { ref mut curr_indices, .. } => { + curr_indices.take() + } + }; + + let mut child_rects = [PictureBox2D::zero(); 4]; + TileNode::get_child_rects(&self.rect, &mut child_rects); + + let mut child_indices = [ + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + ]; + + // Step through the index buffer, and add primitives to each of the children + // that they intersect. + for index in curr_indices { + let prim = &curr_prims[index.0 as usize]; + for (child_rect, indices) in child_rects.iter().zip(child_indices.iter_mut()) { + if prim.prim_clip_box.intersects(child_rect) { + indices.push(index); + } + } + } + + // Create the child nodes and switch from leaf -> node. + let children = child_indices + .iter_mut() + .map(|i| TileNode::new_leaf(mem::replace(i, Vec::new()))) + .collect(); + + self.kind = TileNodeKind::Node { + children, + }; + } + Some(TileModification::Merge) => { + // Construct a merged index buffer by collecting the dependency index buffers + // from each child, and merging them into a de-duplicated index buffer. + let merged_indices = match self.kind { + TileNodeKind::Node { ref mut children, .. } => { + let mut merged_indices = Vec::new(); + + for child in children.iter() { + let child_indices = match child.kind { + TileNodeKind::Leaf { ref curr_indices, .. } => { + curr_indices + } + TileNodeKind::Node { .. } => { + unreachable!("bug: child is not a leaf"); + } + }; + merged_indices.extend_from_slice(child_indices); + } + + merged_indices.sort(); + merged_indices.dedup(); + + merged_indices + } + TileNodeKind::Leaf { .. } => { + unreachable!("bug - trying to merge a leaf"); + } + }; + + // Switch from a node to a leaf, with the combined index buffer + self.kind = TileNodeKind::Leaf { + prev_indices: Vec::new(), + curr_indices: merged_indices, + dirty_tracker: 0, + frames_since_modified: 0, + }; + } + None => { + // If this node didn't merge / split, then recurse into children + // to see if they want to split / merge. + if let TileNodeKind::Node { ref mut children, .. } = self.kind { + for child in children.iter_mut() { + child.maybe_merge_or_split( + level+1, + curr_prims, + max_split_levels, + ); + } + } + } + } + } + + /// Update the dirty state of this node, building the overall dirty rect + fn update_dirty_rects( + &mut self, + prev_prims: &[PrimitiveDescriptor], + curr_prims: &[PrimitiveDescriptor], + prim_comparer: &mut PrimitiveComparer, + dirty_rect: &mut PictureBox2D, + compare_cache: &mut FastHashMap<PrimitiveComparisonKey, PrimitiveCompareResult>, + invalidation_reason: &mut Option<InvalidationReason>, + frame_context: &FrameVisibilityContext, + ) { + match self.kind { + TileNodeKind::Node { ref mut children, .. } => { + for child in children.iter_mut() { + child.update_dirty_rects( + prev_prims, + curr_prims, + prim_comparer, + dirty_rect, + compare_cache, + invalidation_reason, + frame_context, + ); + } + } + TileNodeKind::Leaf { ref prev_indices, ref curr_indices, ref mut dirty_tracker, .. } => { + // If the index buffers are of different length, they must be different + if prev_indices.len() == curr_indices.len() { + // Walk each index buffer, comparing primitives + for (prev_index, curr_index) in prev_indices.iter().zip(curr_indices.iter()) { + let i0 = prev_index.0 as usize; + let i1 = curr_index.0 as usize; + + // Compare the primitives, caching the result in a hash map + // to save comparisons in other tree nodes. + let key = PrimitiveComparisonKey { + prev_index: *prev_index, + curr_index: *curr_index, + }; + + let prim_compare_result = *compare_cache + .entry(key) + .or_insert_with(|| { + let prev = &prev_prims[i0]; + let curr = &curr_prims[i1]; + prim_comparer.compare_prim(prev, curr) + }); + + // If not the same, mark this node as dirty and update the dirty rect + if prim_compare_result != PrimitiveCompareResult::Equal { + if invalidation_reason.is_none() { + *invalidation_reason = Some(InvalidationReason::Content); + } + *dirty_rect = self.rect.union(dirty_rect); + *dirty_tracker = *dirty_tracker | 1; + break; + } + } + } else { + if invalidation_reason.is_none() { + *invalidation_reason = Some(InvalidationReason::PrimCount); + } + *dirty_rect = self.rect.union(dirty_rect); + *dirty_tracker = *dirty_tracker | 1; + } + } + } + } +} + impl CompositeState { // A helper function to destroy all native surfaces for a given list of tiles pub fn destroy_native_tiles<'a, I: Iterator<Item = &'a mut Box<Tile>>>( @@ -2965,7 +7914,7 @@ impl CompositeState { } } -pub fn get_relative_scale_offset( +fn get_relative_scale_offset( child_spatial_node_index: SpatialNodeIndex, parent_spatial_node_index: SpatialNodeIndex, spatial_tree: &SpatialTree, @@ -2990,6 +7939,319 @@ pub fn get_relative_scale_offset( scale_offset } +pub fn calculate_screen_uv( + p: DevicePoint, + clipped: DeviceRect, +) -> DeviceHomogeneousVector { + // TODO(gw): Switch to a simple mix, no bilerp / homogeneous vec needed anymore + DeviceHomogeneousVector::new( + (p.x - clipped.min.x) / (clipped.max.x - clipped.min.x), + (p.y - clipped.min.y) / (clipped.max.y - clipped.min.y), + 0.0, + 1.0, + ) +} + +fn get_surface_rects( + surface_index: SurfaceIndex, + composite_mode: &PictureCompositeMode, + parent_surface_index: SurfaceIndex, + surfaces: &mut [SurfaceInfo], + spatial_tree: &SpatialTree, + max_surface_size: f32, + force_scissor_rect: bool, +) -> Option<SurfaceAllocInfo> { + let parent_surface = &surfaces[parent_surface_index.0]; + + let local_to_parent = SpaceMapper::new_with_target( + parent_surface.surface_spatial_node_index, + surfaces[surface_index.0].surface_spatial_node_index, + parent_surface.clipping_rect, + spatial_tree, + ); + + let local_clip_rect = local_to_parent + .unmap(&parent_surface.clipping_rect) + .unwrap_or(PictureRect::max_rect()) + .cast_unit(); + + let surface = &mut surfaces[surface_index.0]; + + let (clipped_local, unclipped_local, source_local) = match composite_mode { + PictureCompositeMode::SVGFEGraph(ref filters) => { + // We need to get the primitive rect, and get_coverage_target_svgfe + // requires the provided rect is in user space (defined in SVG spec) + // for subregion calculations to work properly + // + // Calculate the target rect from source rect, note that this can + // produce a valid target rect even with an empty source rect in the + // case of filters like feFlood, feComponentTransfer, feColorMatrix, + // feImage and feTurbulence which can fill their whole subregion + // even if given empty SourceGraphic. It can also produce a smaller + // rect than source if subregions or filter region apply clipping to + // the intermediate pictures or the final picture. + let prim_subregion = composite_mode.get_rect(surface, None); + + // Clip the prim_subregion by the clip_rect, this will be put into + // surface_rects.clipped. + let visible_subregion: LayoutRect = + prim_subregion.cast_unit() + .intersection(&local_clip_rect) + .unwrap_or(PictureRect::zero()) + .cast_unit(); + + // If the visible_subregion was empty to begin with, or clipped away + // entirely, then there is nothing to do here, this is the hot path + // for culling of composited pictures. + if visible_subregion.is_empty() { + return None; + } + + // Calculate the subregion for how much of SourceGraphic we may need + // to produce to satisfy the invalidation rect, then clip it by the + // original primitive rect because we have no reason to produce any + // out of bounds pixels; they would just be blank anyway. + let source_potential_subregion = composite_mode.get_coverage_source_svgfe( + filters, visible_subregion.cast_unit()); + let source_subregion = + source_potential_subregion + .intersection(&surface.unclipped_local_rect.cast_unit()) + .unwrap_or(LayoutRect::zero()); + + // For some reason, code assumes that the clipped_local rect we make + // here will enclose the source_subregion, and also be a valid + // prim_subregion, so we have to union the two rects to meet those + // expectations. This is an optimization opportunity - figure out + // how to make just the visible_subregion work here. + let coverage_subregion = source_subregion.union(&visible_subregion); + + (coverage_subregion.cast_unit(), prim_subregion.cast_unit(), source_subregion.cast_unit()) + } + PictureCompositeMode::Filter(Filter::DropShadows(ref shadows)) => { + let local_prim_rect = surface.clipped_local_rect; + + let mut required_local_rect = local_prim_rect + .intersection(&local_clip_rect) + .unwrap_or(PictureRect::zero()); + + for shadow in shadows { + let (blur_radius_x, blur_radius_y) = surface.clamp_blur_radius( + shadow.blur_radius, + shadow.blur_radius, + ); + let blur_inflation_x = blur_radius_x * BLUR_SAMPLE_SCALE; + let blur_inflation_y = blur_radius_y * BLUR_SAMPLE_SCALE; + + let local_shadow_rect = local_prim_rect + .translate(shadow.offset.cast_unit()) + .inflate(blur_inflation_x, blur_inflation_y); + + if let Some(clipped_shadow_rect) = local_clip_rect.intersection(&local_shadow_rect) { + let required_shadow_rect = clipped_shadow_rect.inflate(blur_inflation_x, blur_inflation_y); + + let local_clipped_shadow_rect = required_shadow_rect.translate(-shadow.offset.cast_unit()); + + required_local_rect = required_local_rect.union(&local_clipped_shadow_rect); + } + } + + let unclipped = composite_mode.get_rect(surface, None); + let clipped = required_local_rect; + + let clipped = match clipped.intersection(&unclipped.cast_unit()) { + Some(rect) => rect, + None => return None, + }; + + (clipped, unclipped, clipped) + } + _ => { + let surface_origin = surface.clipped_local_rect.min.to_vector().cast_unit(); + + let normalized_prim_rect = composite_mode + .get_rect(surface, None) + .translate(-surface_origin); + + let normalized_clip_rect = local_clip_rect + .cast_unit() + .translate(-surface_origin); + + let norm_clipped_rect = match normalized_prim_rect.intersection(&normalized_clip_rect) { + Some(rect) => rect, + None => return None, + }; + + let norm_clipped_rect = composite_mode.get_rect(surface, Some(norm_clipped_rect)); + + let norm_clipped_rect = match norm_clipped_rect.intersection(&normalized_prim_rect) { + Some(rect) => rect, + None => return None, + }; + + let unclipped = normalized_prim_rect.translate(surface_origin); + let clipped = norm_clipped_rect.translate(surface_origin); + + (clipped.cast_unit(), unclipped.cast_unit(), clipped.cast_unit()) + } + }; + + // We need to put the clipped, unclipped and source rects in the chosen + // raster spatial node if possible, so that it will be rendered at the + // proper pixel scale with antialiasing, otherwise it would be blurry. + let (mut clipped, mut unclipped, mut source) = if surface.raster_spatial_node_index != surface.surface_spatial_node_index { + // Transform surface into the chosen raster spatial node + assert_eq!(surface.device_pixel_scale.0, 1.0); + + let local_to_world = SpaceMapper::new_with_target( + spatial_tree.root_reference_frame_index(), + surface.surface_spatial_node_index, + WorldRect::max_rect(), + spatial_tree, + ); + + let clipped = local_to_world.map(&clipped_local.cast_unit()).unwrap() * surface.device_pixel_scale; + let unclipped = local_to_world.map(&unclipped_local).unwrap() * surface.device_pixel_scale; + let source = local_to_world.map(&source_local.cast_unit()).unwrap() * surface.device_pixel_scale; + + (clipped, unclipped, source) + } else { + // Surface is already in the chosen raster spatial node + let clipped = clipped_local.cast_unit() * surface.device_pixel_scale; + let unclipped = unclipped_local.cast_unit() * surface.device_pixel_scale; + let source = source_local.cast_unit() * surface.device_pixel_scale; + + (clipped, unclipped, source) + }; + let mut clipped_snapped = clipped.round_out(); + let mut source_snapped = source.round_out(); + + // We need to make sure the surface size does not exceed max_surface_size, + // if it would exceed it we actually want to keep the surface in its local + // space and stop worrying about it being a little blurry. + // + // Since both clipped and source are subject to the same limit, we can just + // pick the largest axis from all rects involved. + // + // Importantly, surfaces that are exactly at max_surface_size are relatively + // common for some reason, so we don't want to use a conservative limit. + // + // If you change this, test with: + // ./mach crashtest layout/svg/crashtests/387290-1.svg + let max_dimension = + clipped_snapped.width().max( + clipped_snapped.height().max( + source_snapped.width().max( + source_snapped.height() + ))).ceil(); + if max_dimension > max_surface_size { + // We have to recalculate max_dimension for the local space we'll be + // using as we're no longer rasterizing in the parent space + let max_dimension = + clipped_local.width().max( + clipped_local.height().max( + source_local.width().max( + source_local.height() + ))).ceil(); + surface.raster_spatial_node_index = surface.surface_spatial_node_index; + surface.device_pixel_scale = Scale::new(max_surface_size / max_dimension); + surface.local_scale = (1.0, 1.0); + + let add_markers = profiler::thread_is_being_profiled(); + if add_markers { + let new_clipped = (clipped_local.cast_unit() * surface.device_pixel_scale).round(); + let new_source = (source_local.cast_unit() * surface.device_pixel_scale).round(); + profiler::add_text_marker("SurfaceSizeLimited", + format!("Surface for {:?} reduced from raster {:?} (source {:?}) to local {:?} (source {:?})", + composite_mode.kind(), + clipped.size(), source.size(), + new_clipped, new_source).as_str(), + Duration::from_secs_f32(new_clipped.width() * new_clipped.height() / 1000000000.0)); + } + + clipped = clipped_local.cast_unit() * surface.device_pixel_scale; + unclipped = unclipped_local.cast_unit() * surface.device_pixel_scale; + source = source_local.cast_unit() * surface.device_pixel_scale; + clipped_snapped = clipped.round(); + source_snapped = source.round(); + } + + let task_size = clipped_snapped.size().to_i32(); + // We must avoid hitting the assert here at all costs because panics here + // will repeatedly crash the GPU Process, making the whole app unusable, + // so make sure task_size <= max_surface_size, it's possible that we lose a + // pixel here if the max_dimension threshold was not optimal. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1948939 for more info. + let task_size = task_size.min(DeviceIntSize::new(max_surface_size as i32, max_surface_size as i32)); + debug_assert!( + task_size.width <= max_surface_size as i32 && + task_size.height <= max_surface_size as i32, + "task_size {:?} for {:?} must be within max_surface_size {}", + task_size, + composite_mode.kind(), + max_surface_size); + + let uv_rect_kind = calculate_uv_rect_kind( + clipped_snapped, + unclipped, + ); + + // If the task size is zero sized, skip creation and drawing of it + if task_size.width == 0 || task_size.height == 0 { + return None; + } + + // If the final clipped surface rect is not the same or larger as the unclipped + // local rect of the surface, we need to enable scissor rect (which disables + // merging batches between this and other render tasks allocated to the same + // render target). This is conservative - we could do better in future by + // distinguishing between clips that affect the surface itself vs. clips on + // child primitives that don't affect this. + let needs_scissor_rect = force_scissor_rect || !clipped_local.contains_box(&surface.unclipped_local_rect); + + Some(SurfaceAllocInfo { + task_size, + needs_scissor_rect, + clipped: clipped_snapped, + unclipped, + source: source_snapped, + clipped_notsnapped: clipped, + clipped_local, + uv_rect_kind, + }) +} + +pub fn calculate_uv_rect_kind( + clipped: DeviceRect, + unclipped: DeviceRect, +) -> UvRectKind { + let top_left = calculate_screen_uv( + unclipped.top_left().cast_unit(), + clipped, + ); + + let top_right = calculate_screen_uv( + unclipped.top_right().cast_unit(), + clipped, + ); + + let bottom_left = calculate_screen_uv( + unclipped.bottom_left().cast_unit(), + clipped, + ); + + let bottom_right = calculate_screen_uv( + unclipped.bottom_right().cast_unit(), + clipped, + ); + + UvRectKind::Quad { + top_left, + top_right, + bottom_left, + bottom_right, + } +} + #[test] fn test_large_surface_scale_1() { use crate::spatial_tree::{SceneSpatialTree, SpatialTree}; diff --git a/gfx/wr/webrender/src/picture_graph.rs b/gfx/wr/webrender/src/picture_graph.rs @@ -6,7 +6,7 @@ use crate::frame_builder::FrameBuildingContext; use crate::internal_types::FastHashMap; use crate::prim_store::PictureIndex; use crate::picture::{PicturePrimitive, SurfaceIndex, SurfaceInfo}; -use crate::tile_cache::{TileCacheInstance, SliceId}; +use crate::picture::{TileCacheInstance, SliceId}; use smallvec::SmallVec; #[derive(Debug)] diff --git a/gfx/wr/webrender/src/prepare.rs b/gfx/wr/webrender/src/prepare.rs @@ -23,9 +23,8 @@ use crate::clip::{ClipDataStore, ClipNodeFlags, ClipChainInstance, ClipItemKind} use crate::frame_builder::{FrameBuildingContext, FrameBuildingState, PictureContext, PictureState}; use crate::gpu_types::{BrushFlags, LinearGradientBrushData}; use crate::internal_types::{FastHashMap, PlaneSplitAnchor, Filter}; -use crate::picture::{ClusterFlags, PictureCompositeMode, PicturePrimitive}; -use crate::picture::{PrimitiveList, PrimitiveCluster, SurfaceIndex, SubpixelMode, Picture3DContext}; -use crate::tile_cache::{SliceId, TileCacheInstance}; +use crate::picture::{ClusterFlags, PictureCompositeMode, PicturePrimitive, SliceId}; +use crate::picture::{PrimitiveList, PrimitiveCluster, SurfaceIndex, TileCacheInstance, SubpixelMode, Picture3DContext}; use crate::prim_store::line_dec::MAX_LINE_DECORATION_RESOLUTION; use crate::prim_store::*; use crate::quad; diff --git a/gfx/wr/webrender/src/prim_store/picture.rs b/gfx/wr/webrender/src/prim_store/picture.rs @@ -11,8 +11,8 @@ use crate::scene_building::IsVisible; use crate::gpu_types::BlurEdgeMode; use crate::intern::ItemUid; use crate::intern::{Internable, InternDebug, Handle as InternHandle}; -use crate::internal_types::{LayoutPrimitiveInfo, Filter}; -use crate::svg_filter::{FilterGraphPictureReference, FilterGraphOp, FilterGraphNode, SVGFE_CONVOLVE_VALUES_LIMIT}; +use crate::internal_types::{LayoutPrimitiveInfo, FilterGraphPictureReference, + FilterGraphOp, FilterGraphNode, SVGFE_CONVOLVE_VALUES_LIMIT, Filter}; use crate::picture::PictureCompositeMode; use crate::prim_store::{ PrimitiveInstanceKind, PrimitiveStore, VectorKey, diff --git a/gfx/wr/webrender/src/render_backend.rs b/gfx/wr/webrender/src/render_backend.rs @@ -36,8 +36,7 @@ use crate::intern::DataStore; use crate::internal_types::DebugOutput; use crate::internal_types::{FastHashMap, FrameId, FrameStamp, RenderedDocument, ResultMsg}; use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; -use crate::picture::{PictureScratchBuffer, SurfaceInfo, RasterConfig}; -use crate::tile_cache::{SliceId, TileCacheInstance, TileCacheParams}; +use crate::picture::{PictureScratchBuffer, SliceId, TileCacheInstance, TileCacheParams, SurfaceInfo, RasterConfig}; use crate::picture::PicturePrimitive; use crate::prim_store::{PrimitiveScratchBuffer, PrimitiveInstance}; use crate::prim_store::{PrimitiveInstanceKind, PrimTemplateCommonData}; diff --git a/gfx/wr/webrender/src/render_target.rs b/gfx/wr/webrender/src/render_target.rs @@ -16,10 +16,8 @@ use crate::frame_builder::FrameGlobalResources; use crate::gpu_types::{BorderInstance, SVGFEFilterInstance, BlurDirection, BlurInstance, PrimitiveHeaders, ScalingInstance}; use crate::gpu_types::{TransformPalette, ZBufferIdGenerator, MaskInstance, ClipSpace, BlurEdgeMode}; use crate::gpu_types::{ZBufferId, QuadSegment, PrimitiveInstanceData, TransformPaletteId}; -use crate::internal_types::{CacheTextureId, FastHashMap, FrameAllocator, FrameMemory, FrameVec, TextureSource}; -use crate::svg_filter::FilterGraphOp; -use crate::picture::{SurfaceInfo, ResolvedSurfaceTexture}; -use crate::tile_cache::{SliceId, TileCacheInstance}; +use crate::internal_types::{CacheTextureId, FastHashMap, FilterGraphOp, FrameAllocator, FrameMemory, FrameVec, TextureSource}; +use crate::picture::{SliceId, SurfaceInfo, ResolvedSurfaceTexture, TileCacheInstance}; use crate::quad; use crate::prim_store::{PrimitiveInstance, PrimitiveStore, PrimitiveScratchBuffer}; use crate::prim_store::gradient::{ diff --git a/gfx/wr/webrender/src/render_task.rs b/gfx/wr/webrender/src/render_task.rs @@ -14,10 +14,8 @@ use crate::profiler::{add_text_marker}; use crate::spatial_tree::SpatialNodeIndex; use crate::frame_builder::FrameBuilderConfig; use crate::gpu_types::{BorderInstance, UvRectKind, TransformPaletteId, BlurEdgeMode}; -use crate::internal_types::{CacheTextureId, FastHashMap, TextureSource, Swizzle}; -use crate::svg_filter::{FilterGraphNode, FilterGraphOp, FilterGraphPictureReference, SVGFE_CONVOLVE_VALUES_LIMIT}; -use crate::picture::ResolvedSurfaceTexture; -use crate::tile_cache::MAX_SURFACE_SIZE; +use crate::internal_types::{CacheTextureId, FastHashMap, FilterGraphNode, FilterGraphOp, FilterGraphPictureReference, SVGFE_CONVOLVE_VALUES_LIMIT, TextureSource, Swizzle}; +use crate::picture::{ResolvedSurfaceTexture, MAX_SURFACE_SIZE}; use crate::prim_store::ClipData; use crate::prim_store::gradient::{ FastLinearGradientTask, RadialGradientTask, diff --git a/gfx/wr/webrender/src/renderer/init.rs b/gfx/wr/webrender/src/renderer/init.rs @@ -20,6 +20,7 @@ use crate::glyph_cache::GlyphCache; use glyph_rasterizer::{GlyphRasterThread, GlyphRasterizer, SharedFontResources}; use crate::gpu_types::PrimitiveInstanceData; use crate::internal_types::{FastHashMap, FastHashSet}; +use crate::picture; use crate::profiler::{self, Profiler, TransactionProfile}; use crate::device::query::{GpuProfiler, GpuDebugMethod}; use crate::render_backend::RenderBackend; @@ -666,7 +667,7 @@ pub fn create_webrender_instance( .map(|handler| handler.create_similar()); let texture_cache_config = options.texture_cache_config.clone(); - let mut picture_tile_size = options.picture_tile_size.unwrap_or(crate::tile_cache::TILE_SIZE_DEFAULT); + let mut picture_tile_size = options.picture_tile_size.unwrap_or(picture::TILE_SIZE_DEFAULT); // Clamp the picture tile size to reasonable values. picture_tile_size.width = picture_tile_size.width.max(128).min(4096); picture_tile_size.height = picture_tile_size.height.max(128).min(4096); diff --git a/gfx/wr/webrender/src/renderer/mod.rs b/gfx/wr/webrender/src/renderer/mod.rs @@ -78,8 +78,7 @@ use crate::internal_types::DebugOutput; use crate::internal_types::{CacheTextureId, FastHashMap, FastHashSet, RenderedDocument, ResultMsg}; use crate::internal_types::{TextureCacheAllocInfo, TextureCacheAllocationKind, TextureUpdateList}; use crate::internal_types::{RenderTargetInfo, Swizzle, DeferredResolveIndex}; -use crate::picture::ResolvedSurfaceTexture; -use crate::tile_cache::TileId; +use crate::picture::{ResolvedSurfaceTexture, TileId}; use crate::prim_store::DeferredResolve; use crate::profiler::{self, RenderCommandLog, GpuProfileTag, TransactionProfile}; use crate::profiler::{Profiler, add_event_marker, add_text_marker, thread_is_being_profiled}; diff --git a/gfx/wr/webrender/src/scene_building.rs b/gfx/wr/webrender/src/scene_building.rs @@ -60,8 +60,7 @@ use crate::frame_builder::FrameBuilderConfig; use glyph_rasterizer::{FontInstance, SharedFontResources}; use crate::hit_test::HitTestingScene; use crate::intern::Interner; -use crate::internal_types::{FastHashMap, LayoutPrimitiveInfo, Filter, PlaneSplitterIndex, PipelineInstanceId}; -use crate::svg_filter::{FilterGraphNode, FilterGraphOp, FilterGraphPictureReference}; +use crate::internal_types::{FastHashMap, LayoutPrimitiveInfo, Filter, FilterGraphNode, FilterGraphOp, FilterGraphPictureReference, PlaneSplitterIndex, PipelineInstanceId}; use crate::picture::{Picture3DContext, PictureCompositeMode, PicturePrimitive}; use crate::picture::{BlitReason, OrderedPictureChild, PrimitiveList, SurfaceInfo, PictureFlags}; use crate::picture_graph::PictureGraph; diff --git a/gfx/wr/webrender/src/surface.rs b/gfx/wr/webrender/src/surface.rs @@ -2,303 +2,23 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -//! Contains functionality to help building the render task graph from a series of off-screen -//! surfaces that are created during the prepare pass, and other surface related types and -//! helpers. - use api::units::*; -use crate::box_shadow::BLUR_SAMPLE_SCALE; use crate::command_buffer::{CommandBufferBuilderKind, CommandBufferList, CommandBufferBuilder, CommandBufferIndex}; -use crate::gpu_types::UvRectKind; -use crate::internal_types::{FastHashMap, Filter}; -use crate::picture::PictureCompositeMode; -use crate::svg_filter::get_coverage_source_svgfe; -use crate::tile_cache::{TileKey, SubSliceIndex, MAX_COMPOSITOR_SURFACES}; +use crate::internal_types::FastHashMap; +use crate::picture::{SurfaceInfo, SurfaceIndex, TileKey, SubSliceIndex, MAX_COMPOSITOR_SURFACES}; use crate::prim_store::PictureIndex; -use crate::profiler; use crate::render_task_graph::{RenderTaskId, RenderTaskGraphBuilder}; use crate::render_target::ResolveOp; use crate::render_task::{RenderTask, RenderTaskKind, RenderTaskLocation}; -use crate::space::SpaceMapper; -use crate::spatial_tree::{SpatialTree, SpatialNodeIndex}; -use crate::util::MaxRect; -use crate::visibility::{VisibilityState, PrimitiveVisibility, FrameVisibilityContext}; -use core::time::Duration; -use euclid::Scale; - - -/// Maximum blur radius for blur filter -const MAX_BLUR_RADIUS: f32 = 100.; - -/// An index into the surface array -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub struct SurfaceIndex(pub usize); - -/// Specify whether a surface allows subpixel AA text rendering. -#[derive(Debug, Copy, Clone)] -pub enum SubpixelMode { - /// This surface allows subpixel AA text - Allow, - /// Subpixel AA text cannot be drawn on this surface - Deny, - /// Subpixel AA can be drawn on this surface, if not intersecting - /// with the excluded regions, and inside the allowed rect. - Conditional { - allowed_rect: PictureRect, - prohibited_rect: PictureRect, - }, -} - -/// Information about an offscreen surface. For now, -/// it contains information about the size and coordinate -/// system of the surface. In the future, it will contain -/// information about the contents of the surface, which -/// will allow surfaces to be cached / retained between -/// frames and display lists. -pub struct SurfaceInfo { - /// A local rect defining the size of this surface, in the - /// coordinate system of the parent surface. This contains - /// the unclipped bounding rect of child primitives. - pub unclipped_local_rect: PictureRect, - /// The local space coverage of child primitives after they are - /// are clipped to their owning clip-chain. - pub clipped_local_rect: PictureRect, - /// The (conservative) valid part of this surface rect. Used - /// to reduce the size of render target allocation. - pub clipping_rect: PictureRect, - /// The rectangle to use for culling and clipping. - pub culling_rect: VisRect, - /// Helper structs for mapping local rects in different - /// coordinate systems into the picture coordinates. - pub map_local_to_picture: SpaceMapper<LayoutPixel, PicturePixel>, - /// The positioning node for the surface itself, - pub surface_spatial_node_index: SpatialNodeIndex, - /// The rasterization root for this surface. - pub raster_spatial_node_index: SpatialNodeIndex, - /// The spatial node for culling and clipping (anything using VisPixel). - /// TODO: Replace with the raster spatial node. - pub visibility_spatial_node_index: SpatialNodeIndex, - /// The device pixel ratio specific to this surface. - pub device_pixel_scale: DevicePixelScale, - /// The scale factors of the surface to world transform. - pub world_scale_factors: (f32, f32), - /// Local scale factors surface to raster transform - pub local_scale: (f32, f32), - /// If true, we know this surface is completely opaque. - pub is_opaque: bool, - /// If true, allow snapping on this and child surfaces - pub allow_snapping: bool, - /// If true, the scissor rect must be set when drawing this surface - pub force_scissor_rect: bool, -} - -impl SurfaceInfo { - pub fn new( - surface_spatial_node_index: SpatialNodeIndex, - raster_spatial_node_index: SpatialNodeIndex, - world_rect: WorldRect, - spatial_tree: &SpatialTree, - device_pixel_scale: DevicePixelScale, - world_scale_factors: (f32, f32), - local_scale: (f32, f32), - allow_snapping: bool, - force_scissor_rect: bool, - ) -> Self { - let map_surface_to_world = SpaceMapper::new_with_target( - spatial_tree.root_reference_frame_index(), - surface_spatial_node_index, - world_rect, - spatial_tree, - ); - - let pic_bounds = map_surface_to_world - .unmap(&map_surface_to_world.bounds) - .unwrap_or_else(PictureRect::max_rect); - - let map_local_to_picture = SpaceMapper::new( - surface_spatial_node_index, - pic_bounds, - ); - - // TODO: replace the root with raster space. - let visibility_spatial_node_index = spatial_tree.root_reference_frame_index(); - - SurfaceInfo { - unclipped_local_rect: PictureRect::zero(), - clipped_local_rect: PictureRect::zero(), - is_opaque: false, - clipping_rect: PictureRect::zero(), - map_local_to_picture, - raster_spatial_node_index, - surface_spatial_node_index, - visibility_spatial_node_index, - device_pixel_scale, - world_scale_factors, - local_scale, - allow_snapping, - force_scissor_rect, - // TODO: At the moment all culling is done in world space but - // but the plan is to move it to raster space. - culling_rect: world_rect.cast_unit(), - } - } - - /// Clamps the blur radius depending on scale factors. - pub fn clamp_blur_radius( - &self, - x_blur_radius: f32, - y_blur_radius: f32, - ) -> (f32, f32) { - // Clamping must occur after scale factors are applied, but scale factors are not applied - // until later on. To clamp the blur radius, we first apply the scale factors and then clamp - // and finally revert the scale factors. - - let sx_blur_radius = x_blur_radius * self.local_scale.0; - let sy_blur_radius = y_blur_radius * self.local_scale.1; - - let largest_scaled_blur_radius = f32::max( - sx_blur_radius * self.world_scale_factors.0, - sy_blur_radius * self.world_scale_factors.1, - ); - - if largest_scaled_blur_radius > MAX_BLUR_RADIUS { - let sf = MAX_BLUR_RADIUS / largest_scaled_blur_radius; - (x_blur_radius * sf, y_blur_radius * sf) - } else { - // Return the original blur radius to avoid any rounding errors - (x_blur_radius, y_blur_radius) - } - } - - pub fn update_culling_rect( - &mut self, - parent_culling_rect: VisRect, - composite_mode: &PictureCompositeMode, - frame_context: &FrameVisibilityContext, - ) { - // Set the default culling rect to be the parent, in case we fail - // any mappings below due to weird perspective or invalid transforms. - self.culling_rect = parent_culling_rect; - - if let PictureCompositeMode::Filter(Filter::Blur { width, height, should_inflate, .. }) = composite_mode { - if *should_inflate { - // Space mapping vis <-> picture space - let map_surface_to_vis = SpaceMapper::new_with_target( - // TODO: switch from root to raster space. - frame_context.root_spatial_node_index, - self.surface_spatial_node_index, - parent_culling_rect, - frame_context.spatial_tree, - ); - - // Unmap the parent culling rect to surface space. Note that this may be - // quite conservative in the case of a complex transform, especially perspective. - if let Some(local_parent_culling_rect) = map_surface_to_vis.unmap(&parent_culling_rect) { - let (width_factor, height_factor) = self.clamp_blur_radius(*width, *height); - - // Inflate by the local-space amount this surface extends. - let expanded_rect: PictureBox2D = local_parent_culling_rect.inflate( - width_factor.ceil() * BLUR_SAMPLE_SCALE, - height_factor.ceil() * BLUR_SAMPLE_SCALE, - ); - - // Map back to the expected vis-space culling rect - if let Some(rect) = map_surface_to_vis.map(&expanded_rect) { - self.culling_rect = rect; - } - } - } - } - } - - pub fn map_to_device_rect( - &self, - picture_rect: &PictureRect, - spatial_tree: &SpatialTree, - ) -> DeviceRect { - let raster_rect = if self.raster_spatial_node_index != self.surface_spatial_node_index { - // Currently, the surface's spatial node can be different from its raster node only - // for surfaces in the root coordinate system for snapping reasons. - // See `PicturePrimitive::assign_surface`. - assert_eq!(self.device_pixel_scale.0, 1.0); - assert_eq!(self.raster_spatial_node_index, spatial_tree.root_reference_frame_index()); - - let pic_to_raster = SpaceMapper::new_with_target( - self.raster_spatial_node_index, - self.surface_spatial_node_index, - WorldRect::max_rect(), - spatial_tree, - ); - - pic_to_raster.map(&picture_rect).unwrap() - } else { - picture_rect.cast_unit() - }; - - raster_rect * self.device_pixel_scale - } - - /// Clip and transform a local rect to a device rect suitable for allocating - /// a child off-screen surface of this surface (e.g. for clip-masks) - pub fn get_surface_rect( - &self, - local_rect: &PictureRect, - spatial_tree: &SpatialTree, - ) -> Option<DeviceIntRect> { - let local_rect = match local_rect.intersection(&self.clipping_rect) { - Some(rect) => rect, - None => return None, - }; - - let raster_rect = if self.raster_spatial_node_index != self.surface_spatial_node_index { - assert_eq!(self.device_pixel_scale.0, 1.0); - - let local_to_world = SpaceMapper::new_with_target( - spatial_tree.root_reference_frame_index(), - self.surface_spatial_node_index, - WorldRect::max_rect(), - spatial_tree, - ); - - local_to_world.map(&local_rect).unwrap() - } else { - // The content should have been culled out earlier. - assert!(self.device_pixel_scale.0 > 0.0); - - local_rect.cast_unit() - }; - - let surface_rect = (raster_rect * self.device_pixel_scale).round_out().to_i32(); - if surface_rect.is_empty() { - // The local_rect computed above may have non-empty size that is very - // close to zero. Due to limited arithmetic precision, the SpaceMapper - // might transform the near-zero-sized rect into a zero-sized one. - return None; - } - - Some(surface_rect) - } -} - -/// Information from `get_surface_rects` about the allocated size, UV sampling -/// parameters etc for an off-screen surface -#[derive(Debug)] -pub struct SurfaceAllocInfo { - pub task_size: DeviceIntSize, - pub needs_scissor_rect: bool, - pub clipped: DeviceRect, - pub unclipped: DeviceRect, - // Only used for SVGFEGraph currently, this is the source pixels needed to - // render the pixels in clipped. - pub source: DeviceRect, - // Only used for SVGFEGraph, this is the same as clipped before rounding. - pub clipped_notsnapped: DeviceRect, - pub clipped_local: PictureRect, - pub uv_rect_kind: UvRectKind, -} +use crate::visibility::{VisibilityState, PrimitiveVisibility}; +/* + Contains functionality to help building the render task graph from a series of off-screen + surfaces that are created during the prepare pass. For now, it maintains existing behavior. + A future patch will add support for surface sub-graphs, while ensuring the render task + graph itself is built correctly with dependencies regardless of the surface kind (chained, + tiled, simple). + */ // Information about the render task(s) for a given tile #[cfg_attr(feature = "capture", derive(Serialize))] @@ -897,317 +617,3 @@ impl SurfaceBuilder { assert!(self.builder_stack.is_empty()); } } - - -pub fn calculate_screen_uv( - p: DevicePoint, - clipped: DeviceRect, -) -> DeviceHomogeneousVector { - // TODO(gw): Switch to a simple mix, no bilerp / homogeneous vec needed anymore - DeviceHomogeneousVector::new( - (p.x - clipped.min.x) / (clipped.max.x - clipped.min.x), - (p.y - clipped.min.y) / (clipped.max.y - clipped.min.y), - 0.0, - 1.0, - ) -} - -pub fn get_surface_rects( - surface_index: SurfaceIndex, - composite_mode: &PictureCompositeMode, - parent_surface_index: SurfaceIndex, - surfaces: &mut [SurfaceInfo], - spatial_tree: &SpatialTree, - max_surface_size: f32, - force_scissor_rect: bool, -) -> Option<SurfaceAllocInfo> { - let parent_surface = &surfaces[parent_surface_index.0]; - - let local_to_parent = SpaceMapper::new_with_target( - parent_surface.surface_spatial_node_index, - surfaces[surface_index.0].surface_spatial_node_index, - parent_surface.clipping_rect, - spatial_tree, - ); - - let local_clip_rect = local_to_parent - .unmap(&parent_surface.clipping_rect) - .unwrap_or(PictureRect::max_rect()) - .cast_unit(); - - let surface = &mut surfaces[surface_index.0]; - - let (clipped_local, unclipped_local, source_local) = match composite_mode { - PictureCompositeMode::SVGFEGraph(ref filters) => { - // We need to get the primitive rect, and get_coverage_target_svgfe - // requires the provided rect is in user space (defined in SVG spec) - // for subregion calculations to work properly - // - // Calculate the target rect from source rect, note that this can - // produce a valid target rect even with an empty source rect in the - // case of filters like feFlood, feComponentTransfer, feColorMatrix, - // feImage and feTurbulence which can fill their whole subregion - // even if given empty SourceGraphic. It can also produce a smaller - // rect than source if subregions or filter region apply clipping to - // the intermediate pictures or the final picture. - let prim_subregion = composite_mode.get_rect(surface, None); - - // Clip the prim_subregion by the clip_rect, this will be put into - // surface_rects.clipped. - let visible_subregion: LayoutRect = - prim_subregion.cast_unit() - .intersection(&local_clip_rect) - .unwrap_or(PictureRect::zero()) - .cast_unit(); - - // If the visible_subregion was empty to begin with, or clipped away - // entirely, then there is nothing to do here, this is the hot path - // for culling of composited pictures. - if visible_subregion.is_empty() { - return None; - } - - // Calculate the subregion for how much of SourceGraphic we may need - // to produce to satisfy the invalidation rect, then clip it by the - // original primitive rect because we have no reason to produce any - // out of bounds pixels; they would just be blank anyway. - let source_potential_subregion = get_coverage_source_svgfe( - filters, visible_subregion.cast_unit()); - let source_subregion = - source_potential_subregion - .intersection(&surface.unclipped_local_rect.cast_unit()) - .unwrap_or(LayoutRect::zero()); - - // For some reason, code assumes that the clipped_local rect we make - // here will enclose the source_subregion, and also be a valid - // prim_subregion, so we have to union the two rects to meet those - // expectations. This is an optimization opportunity - figure out - // how to make just the visible_subregion work here. - let coverage_subregion = source_subregion.union(&visible_subregion); - - (coverage_subregion.cast_unit(), prim_subregion.cast_unit(), source_subregion.cast_unit()) - } - PictureCompositeMode::Filter(Filter::DropShadows(ref shadows)) => { - let local_prim_rect = surface.clipped_local_rect; - - let mut required_local_rect = local_prim_rect - .intersection(&local_clip_rect) - .unwrap_or(PictureRect::zero()); - - for shadow in shadows { - let (blur_radius_x, blur_radius_y) = surface.clamp_blur_radius( - shadow.blur_radius, - shadow.blur_radius, - ); - let blur_inflation_x = blur_radius_x * BLUR_SAMPLE_SCALE; - let blur_inflation_y = blur_radius_y * BLUR_SAMPLE_SCALE; - - let local_shadow_rect = local_prim_rect - .translate(shadow.offset.cast_unit()) - .inflate(blur_inflation_x, blur_inflation_y); - - if let Some(clipped_shadow_rect) = local_clip_rect.intersection(&local_shadow_rect) { - let required_shadow_rect = clipped_shadow_rect.inflate(blur_inflation_x, blur_inflation_y); - - let local_clipped_shadow_rect = required_shadow_rect.translate(-shadow.offset.cast_unit()); - - required_local_rect = required_local_rect.union(&local_clipped_shadow_rect); - } - } - - let unclipped = composite_mode.get_rect(surface, None); - let clipped = required_local_rect; - - let clipped = match clipped.intersection(&unclipped.cast_unit()) { - Some(rect) => rect, - None => return None, - }; - - (clipped, unclipped, clipped) - } - _ => { - let surface_origin = surface.clipped_local_rect.min.to_vector().cast_unit(); - - let normalized_prim_rect = composite_mode - .get_rect(surface, None) - .translate(-surface_origin); - - let normalized_clip_rect = local_clip_rect - .cast_unit() - .translate(-surface_origin); - - let norm_clipped_rect = match normalized_prim_rect.intersection(&normalized_clip_rect) { - Some(rect) => rect, - None => return None, - }; - - let norm_clipped_rect = composite_mode.get_rect(surface, Some(norm_clipped_rect)); - - let norm_clipped_rect = match norm_clipped_rect.intersection(&normalized_prim_rect) { - Some(rect) => rect, - None => return None, - }; - - let unclipped = normalized_prim_rect.translate(surface_origin); - let clipped = norm_clipped_rect.translate(surface_origin); - - (clipped.cast_unit(), unclipped.cast_unit(), clipped.cast_unit()) - } - }; - - // We need to put the clipped, unclipped and source rects in the chosen - // raster spatial node if possible, so that it will be rendered at the - // proper pixel scale with antialiasing, otherwise it would be blurry. - let (mut clipped, mut unclipped, mut source) = if surface.raster_spatial_node_index != surface.surface_spatial_node_index { - // Transform surface into the chosen raster spatial node - assert_eq!(surface.device_pixel_scale.0, 1.0); - - let local_to_world = SpaceMapper::new_with_target( - spatial_tree.root_reference_frame_index(), - surface.surface_spatial_node_index, - WorldRect::max_rect(), - spatial_tree, - ); - - let clipped = local_to_world.map(&clipped_local.cast_unit()).unwrap() * surface.device_pixel_scale; - let unclipped = local_to_world.map(&unclipped_local).unwrap() * surface.device_pixel_scale; - let source = local_to_world.map(&source_local.cast_unit()).unwrap() * surface.device_pixel_scale; - - (clipped, unclipped, source) - } else { - // Surface is already in the chosen raster spatial node - let clipped = clipped_local.cast_unit() * surface.device_pixel_scale; - let unclipped = unclipped_local.cast_unit() * surface.device_pixel_scale; - let source = source_local.cast_unit() * surface.device_pixel_scale; - - (clipped, unclipped, source) - }; - let mut clipped_snapped = clipped.round_out(); - let mut source_snapped = source.round_out(); - - // We need to make sure the surface size does not exceed max_surface_size, - // if it would exceed it we actually want to keep the surface in its local - // space and stop worrying about it being a little blurry. - // - // Since both clipped and source are subject to the same limit, we can just - // pick the largest axis from all rects involved. - // - // Importantly, surfaces that are exactly at max_surface_size are relatively - // common for some reason, so we don't want to use a conservative limit. - // - // If you change this, test with: - // ./mach crashtest layout/svg/crashtests/387290-1.svg - let max_dimension = - clipped_snapped.width().max( - clipped_snapped.height().max( - source_snapped.width().max( - source_snapped.height() - ))).ceil(); - if max_dimension > max_surface_size { - // We have to recalculate max_dimension for the local space we'll be - // using as we're no longer rasterizing in the parent space - let max_dimension = - clipped_local.width().max( - clipped_local.height().max( - source_local.width().max( - source_local.height() - ))).ceil(); - surface.raster_spatial_node_index = surface.surface_spatial_node_index; - surface.device_pixel_scale = Scale::new(max_surface_size / max_dimension); - surface.local_scale = (1.0, 1.0); - - let add_markers = profiler::thread_is_being_profiled(); - if add_markers { - let new_clipped = (clipped_local.cast_unit() * surface.device_pixel_scale).round(); - let new_source = (source_local.cast_unit() * surface.device_pixel_scale).round(); - profiler::add_text_marker("SurfaceSizeLimited", - format!("Surface for {:?} reduced from raster {:?} (source {:?}) to local {:?} (source {:?})", - composite_mode.kind(), - clipped.size(), source.size(), - new_clipped, new_source).as_str(), - Duration::from_secs_f32(new_clipped.width() * new_clipped.height() / 1000000000.0)); - } - - clipped = clipped_local.cast_unit() * surface.device_pixel_scale; - unclipped = unclipped_local.cast_unit() * surface.device_pixel_scale; - source = source_local.cast_unit() * surface.device_pixel_scale; - clipped_snapped = clipped.round(); - source_snapped = source.round(); - } - - let task_size = clipped_snapped.size().to_i32(); - // We must avoid hitting the assert here at all costs because panics here - // will repeatedly crash the GPU Process, making the whole app unusable, - // so make sure task_size <= max_surface_size, it's possible that we lose a - // pixel here if the max_dimension threshold was not optimal. - // See https://bugzilla.mozilla.org/show_bug.cgi?id=1948939 for more info. - let task_size = task_size.min(DeviceIntSize::new(max_surface_size as i32, max_surface_size as i32)); - debug_assert!( - task_size.width <= max_surface_size as i32 && - task_size.height <= max_surface_size as i32, - "task_size {:?} for {:?} must be within max_surface_size {}", - task_size, - composite_mode.kind(), - max_surface_size); - - let uv_rect_kind = calculate_uv_rect_kind( - clipped_snapped, - unclipped, - ); - - // If the task size is zero sized, skip creation and drawing of it - if task_size.width == 0 || task_size.height == 0 { - return None; - } - - // If the final clipped surface rect is not the same or larger as the unclipped - // local rect of the surface, we need to enable scissor rect (which disables - // merging batches between this and other render tasks allocated to the same - // render target). This is conservative - we could do better in future by - // distinguishing between clips that affect the surface itself vs. clips on - // child primitives that don't affect this. - let needs_scissor_rect = force_scissor_rect || !clipped_local.contains_box(&surface.unclipped_local_rect); - - Some(SurfaceAllocInfo { - task_size, - needs_scissor_rect, - clipped: clipped_snapped, - unclipped, - source: source_snapped, - clipped_notsnapped: clipped, - clipped_local, - uv_rect_kind, - }) -} - -pub fn calculate_uv_rect_kind( - clipped: DeviceRect, - unclipped: DeviceRect, -) -> UvRectKind { - let top_left = calculate_screen_uv( - unclipped.top_left().cast_unit(), - clipped, - ); - - let top_right = calculate_screen_uv( - unclipped.top_right().cast_unit(), - clipped, - ); - - let bottom_left = calculate_screen_uv( - unclipped.bottom_left().cast_unit(), - clipped, - ); - - let bottom_right = calculate_screen_uv( - unclipped.bottom_right().cast_unit(), - clipped, - ); - - UvRectKind::Quad { - top_left, - top_right, - bottom_left, - bottom_right, - } -} diff --git a/gfx/wr/webrender/src/svg_filter.rs b/gfx/wr/webrender/src/svg_filter.rs @@ -1,839 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use api::{FilterOpGraphPictureReference, FilterOpGraphNode, ColorF}; -use api::SVGFE_GRAPH_MAX; -use api::units::*; -use api::FilterOpGraphPictureBufferId; -use crate::profiler::add_text_marker; -use crate::filterdata::FilterDataHandle; -use core::time::Duration; - -#[derive(Clone, Copy, Debug)] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub struct FilterGraphPictureReference { - /// Id of the picture in question in a namespace unique to this filter DAG, - /// some are special values like - /// FilterPrimitiveDescription::kPrimitiveIndexSourceGraphic. - pub buffer_id: FilterOpGraphPictureBufferId, - /// Set by wrap_prim_with_filters to the subregion of the input node, may - /// also have been offset for feDropShadow or feOffset - pub subregion: LayoutRect, - /// During scene build this is the offset to apply to the input subregion - /// for feOffset, which can be optimized away by pushing its offset and - /// subregion crop to downstream nodes. This is always zero in render tasks - /// where it has already been applied to subregion by that point. Not used - /// in get_coverage_svgfe because source_padding/target_padding represent - /// the offset there. - pub offset: LayoutVector2D, - /// Equal to the inflate value of the referenced buffer, or 0 - pub inflate: i16, - /// Padding on each side to represent how this input is read relative to the - /// node's output subregion, this represents what the operation needs to - /// read from ths input, which may be blurred or offset. - pub source_padding: LayoutRect, - /// Padding on each side to represent how this input affects the node's - /// subregion, this can be used to calculate target subregion based on - /// SourceGraphic subregion. This is usually equal to source_padding except - /// offset in the opposite direction, inflates typically do the same thing - /// to both types of padding. - pub target_padding: LayoutRect, -} - -impl From<FilterOpGraphPictureReference> for FilterGraphPictureReference { - fn from(pic: FilterOpGraphPictureReference) -> Self { - FilterGraphPictureReference{ - buffer_id: pic.buffer_id, - // All of these are set by wrap_prim_with_filters - subregion: LayoutRect::zero(), - offset: LayoutVector2D::zero(), - inflate: 0, - source_padding: LayoutRect::zero(), - target_padding: LayoutRect::zero(), - } - } -} - -pub const SVGFE_CONVOLVE_DIAMETER_LIMIT: usize = 5; -pub const SVGFE_CONVOLVE_VALUES_LIMIT: usize = SVGFE_CONVOLVE_DIAMETER_LIMIT * - SVGFE_CONVOLVE_DIAMETER_LIMIT; - -#[derive(Clone, Debug)] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub enum FilterGraphOp { - /// Filter that copies the SourceGraphic image into the specified subregion, - /// This is intentionally the only way to get SourceGraphic into the graph, - /// as the filter region must be applied before it is used. - /// parameters: FilterOpGraphNode - /// SVG filter semantics - no inputs, no linear - SVGFESourceGraphic, - /// Filter that copies the SourceAlpha image into the specified subregion, - /// This is intentionally the only way to get SourceAlpha into the graph, - /// as the filter region must be applied before it is used. - /// parameters: FilterOpGraphNode - /// SVG filter semantics - no inputs, no linear - SVGFESourceAlpha, - /// Filter that does no transformation of the colors, used to implement a - /// few things like SVGFEOffset, and this is the default value in - /// impl_default_for_enums. - /// parameters: FilterGraphNode - /// SVG filter semantics - selectable input with offset - SVGFEIdentity, - /// represents CSS opacity property as a graph node like the rest of the - /// SVGFE* filters - /// parameters: FilterGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - SVGFEOpacity{valuebinding: api::PropertyBinding<f32>, value: f32}, - /// convert a color image to an alpha channel - internal use; generated by - /// SVGFilterInstance::GetOrCreateSourceAlphaIndex(). - SVGFEToAlpha, - /// combine 2 images with SVG_FEBLEND_MODE_DARKEN - /// parameters: FilterGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#feBlendElement - SVGFEBlendDarken, - /// combine 2 images with SVG_FEBLEND_MODE_LIGHTEN - /// parameters: FilterGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#feBlendElement - SVGFEBlendLighten, - /// combine 2 images with SVG_FEBLEND_MODE_MULTIPLY - /// parameters: FilterGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#feBlendElement - SVGFEBlendMultiply, - /// combine 2 images with SVG_FEBLEND_MODE_NORMAL - /// parameters: FilterGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#feBlendElement - SVGFEBlendNormal, - /// combine 2 images with SVG_FEBLEND_MODE_SCREEN - /// parameters: FilterGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#feBlendElement - SVGFEBlendScreen, - /// combine 2 images with SVG_FEBLEND_MODE_OVERLAY - /// parameters: FilterOpGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Source: https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode - SVGFEBlendOverlay, - /// combine 2 images with SVG_FEBLEND_MODE_COLOR_DODGE - /// parameters: FilterOpGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Source: https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode - SVGFEBlendColorDodge, - /// combine 2 images with SVG_FEBLEND_MODE_COLOR_BURN - /// parameters: FilterOpGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Source: https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode - SVGFEBlendColorBurn, - /// combine 2 images with SVG_FEBLEND_MODE_HARD_LIGHT - /// parameters: FilterOpGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Source: https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode - SVGFEBlendHardLight, - /// combine 2 images with SVG_FEBLEND_MODE_SOFT_LIGHT - /// parameters: FilterOpGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Source: https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode - SVGFEBlendSoftLight, - /// combine 2 images with SVG_FEBLEND_MODE_DIFFERENCE - /// parameters: FilterOpGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Source: https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode - SVGFEBlendDifference, - /// combine 2 images with SVG_FEBLEND_MODE_EXCLUSION - /// parameters: FilterOpGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Source: https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode - SVGFEBlendExclusion, - /// combine 2 images with SVG_FEBLEND_MODE_HUE - /// parameters: FilterOpGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Source: https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode - SVGFEBlendHue, - /// combine 2 images with SVG_FEBLEND_MODE_SATURATION - /// parameters: FilterOpGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Source: https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode - SVGFEBlendSaturation, - /// combine 2 images with SVG_FEBLEND_MODE_COLOR - /// parameters: FilterOpGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Source: https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode - SVGFEBlendColor, - /// combine 2 images with SVG_FEBLEND_MODE_LUMINOSITY - /// parameters: FilterOpGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Source: https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode - SVGFEBlendLuminosity, - /// transform colors of image through 5x4 color matrix (transposed for - /// efficiency) - /// parameters: FilterGraphNode, matrix[5][4] - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#feColorMatrixElement - SVGFEColorMatrix{values: [f32; 20]}, - /// transform colors of image through configurable gradients with component - /// swizzle - /// parameters: FilterGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#feComponentTransferElement - SVGFEComponentTransfer, - /// Processed version of SVGFEComponentTransfer with the FilterData - /// replaced by an interned handle, this is made in wrap_prim_with_filters. - /// Aside from the interned handle, creates_pixels indicates if the transfer - /// parameters will probably fill the entire subregion with non-zero alpha. - SVGFEComponentTransferInterned{handle: FilterDataHandle, creates_pixels: bool}, - /// composite 2 images with chosen composite mode with parameters for that - /// mode - /// parameters: FilterGraphNode, k1, k2, k3, k4 - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#feCompositeElement - SVGFECompositeArithmetic{k1: f32, k2: f32, k3: f32, k4: f32}, - /// composite 2 images with chosen composite mode with parameters for that - /// mode - /// parameters: FilterGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#feCompositeElement - SVGFECompositeATop, - /// composite 2 images with chosen composite mode with parameters for that - /// mode - /// parameters: FilterGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#feCompositeElement - SVGFECompositeIn, - /// composite 2 images with chosen composite mode with parameters for that - /// mode - /// parameters: FilterOpGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Docs: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feComposite - SVGFECompositeLighter, - /// composite 2 images with chosen composite mode with parameters for that - /// mode - /// parameters: FilterGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#feCompositeElement - SVGFECompositeOut, - /// composite 2 images with chosen composite mode with parameters for that - /// mode - /// parameters: FilterGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#feCompositeElement - SVGFECompositeOver, - /// composite 2 images with chosen composite mode with parameters for that - /// mode - /// parameters: FilterGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#feCompositeElement - SVGFECompositeXOR, - /// transform image through convolution matrix of up to 25 values (spec - /// allows more but for performance reasons we do not) - /// parameters: FilterGraphNode, orderX, orderY, kernelValues[25], divisor, - /// bias, targetX, targetY, kernelUnitLengthX, kernelUnitLengthY, - /// preserveAlpha - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#feConvolveMatrixElement - SVGFEConvolveMatrixEdgeModeDuplicate{order_x: i32, order_y: i32, - kernel: [f32; SVGFE_CONVOLVE_VALUES_LIMIT], divisor: f32, bias: f32, - target_x: i32, target_y: i32, kernel_unit_length_x: f32, - kernel_unit_length_y: f32, preserve_alpha: i32}, - /// transform image through convolution matrix of up to 25 values (spec - /// allows more but for performance reasons we do not) - /// parameters: FilterGraphNode, orderX, orderY, kernelValues[25], divisor, - /// bias, targetX, targetY, kernelUnitLengthX, kernelUnitLengthY, - /// preserveAlpha - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#feConvolveMatrixElement - SVGFEConvolveMatrixEdgeModeNone{order_x: i32, order_y: i32, - kernel: [f32; SVGFE_CONVOLVE_VALUES_LIMIT], divisor: f32, bias: f32, - target_x: i32, target_y: i32, kernel_unit_length_x: f32, - kernel_unit_length_y: f32, preserve_alpha: i32}, - /// transform image through convolution matrix of up to 25 values (spec - /// allows more but for performance reasons we do not) - /// parameters: FilterGraphNode, orderX, orderY, kernelValues[25], divisor, - /// bias, targetX, targetY, kernelUnitLengthX, kernelUnitLengthY, - /// preserveAlpha - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#feConvolveMatrixElement - SVGFEConvolveMatrixEdgeModeWrap{order_x: i32, order_y: i32, - kernel: [f32; SVGFE_CONVOLVE_VALUES_LIMIT], divisor: f32, bias: f32, - target_x: i32, target_y: i32, kernel_unit_length_x: f32, - kernel_unit_length_y: f32, preserve_alpha: i32}, - /// calculate lighting based on heightmap image with provided values for a - /// distant light source with specified direction - /// parameters: FilterGraphNode, surfaceScale, diffuseConstant, - /// kernelUnitLengthX, kernelUnitLengthY, azimuth, elevation - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEDiffuseLightingElement - /// https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEDistantLightElement - SVGFEDiffuseLightingDistant{surface_scale: f32, diffuse_constant: f32, - kernel_unit_length_x: f32, kernel_unit_length_y: f32, azimuth: f32, - elevation: f32}, - /// calculate lighting based on heightmap image with provided values for a - /// point light source at specified location - /// parameters: FilterGraphNode, surfaceScale, diffuseConstant, - /// kernelUnitLengthX, kernelUnitLengthY, x, y, z - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEDiffuseLightingElement - /// https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEPointLightElement - SVGFEDiffuseLightingPoint{surface_scale: f32, diffuse_constant: f32, - kernel_unit_length_x: f32, kernel_unit_length_y: f32, x: f32, y: f32, - z: f32}, - /// calculate lighting based on heightmap image with provided values for a - /// spot light source at specified location pointing at specified target - /// location with specified hotspot sharpness and cone angle - /// parameters: FilterGraphNode, surfaceScale, diffuseConstant, - /// kernelUnitLengthX, kernelUnitLengthY, x, y, z, pointsAtX, pointsAtY, - /// pointsAtZ, specularExponent, limitingConeAngle - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEDiffuseLightingElement - /// https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFESpotLightElement - SVGFEDiffuseLightingSpot{surface_scale: f32, diffuse_constant: f32, - kernel_unit_length_x: f32, kernel_unit_length_y: f32, x: f32, y: f32, - z: f32, points_at_x: f32, points_at_y: f32, points_at_z: f32, - cone_exponent: f32, limiting_cone_angle: f32}, - /// calculate a distorted version of first input image using offset values - /// from second input image at specified intensity - /// parameters: FilterGraphNode, scale, xChannelSelector, yChannelSelector - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEDisplacementMapElement - SVGFEDisplacementMap{scale: f32, x_channel_selector: u32, - y_channel_selector: u32}, - /// create and merge a dropshadow version of the specified image's alpha - /// channel with specified offset and blur radius - /// parameters: FilterGraphNode, flood_color, flood_opacity, dx, dy, - /// stdDeviationX, stdDeviationY - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEDropShadowElement - SVGFEDropShadow{color: ColorF, dx: f32, dy: f32, std_deviation_x: f32, - std_deviation_y: f32}, - /// synthesize a new image of specified size containing a solid color - /// parameters: FilterGraphNode, color - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEFloodElement - SVGFEFlood{color: ColorF}, - /// create a blurred version of the input image - /// parameters: FilterGraphNode, stdDeviationX, stdDeviationY - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEGaussianBlurElement - SVGFEGaussianBlur{std_deviation_x: f32, std_deviation_y: f32}, - /// synthesize a new image based on a url (i.e. blob image source) - /// parameters: FilterGraphNode, - /// samplingFilter (see SamplingFilter in Types.h), transform - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEImageElement - SVGFEImage{sampling_filter: u32, matrix: [f32; 6]}, - /// create a new image based on the input image with the contour stretched - /// outward (dilate operator) - /// parameters: FilterGraphNode, radiusX, radiusY - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEMorphologyElement - SVGFEMorphologyDilate{radius_x: f32, radius_y: f32}, - /// create a new image based on the input image with the contour shrunken - /// inward (erode operator) - /// parameters: FilterGraphNode, radiusX, radiusY - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEMorphologyElement - SVGFEMorphologyErode{radius_x: f32, radius_y: f32}, - /// calculate lighting based on heightmap image with provided values for a - /// distant light source with specified direction - /// parameters: FilerData, surfaceScale, specularConstant, specularExponent, - /// kernelUnitLengthX, kernelUnitLengthY, azimuth, elevation - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFESpecularLightingElement - /// https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEDistantLightElement - SVGFESpecularLightingDistant{surface_scale: f32, specular_constant: f32, - specular_exponent: f32, kernel_unit_length_x: f32, - kernel_unit_length_y: f32, azimuth: f32, elevation: f32}, - /// calculate lighting based on heightmap image with provided values for a - /// point light source at specified location - /// parameters: FilterGraphNode, surfaceScale, specularConstant, - /// specularExponent, kernelUnitLengthX, kernelUnitLengthY, x, y, z - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFESpecularLightingElement - /// https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEPointLightElement - SVGFESpecularLightingPoint{surface_scale: f32, specular_constant: f32, - specular_exponent: f32, kernel_unit_length_x: f32, - kernel_unit_length_y: f32, x: f32, y: f32, z: f32}, - /// calculate lighting based on heightmap image with provided values for a - /// spot light source at specified location pointing at specified target - /// location with specified hotspot sharpness and cone angle - /// parameters: FilterGraphNode, surfaceScale, specularConstant, - /// specularExponent, kernelUnitLengthX, kernelUnitLengthY, x, y, z, - /// pointsAtX, pointsAtY, pointsAtZ, specularExponent, limitingConeAngle - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFESpecularLightingElement - /// https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFESpotLightElement - SVGFESpecularLightingSpot{surface_scale: f32, specular_constant: f32, - specular_exponent: f32, kernel_unit_length_x: f32, - kernel_unit_length_y: f32, x: f32, y: f32, z: f32, points_at_x: f32, - points_at_y: f32, points_at_z: f32, cone_exponent: f32, - limiting_cone_angle: f32}, - /// create a new image based on the input image, repeated throughout the - /// output rectangle - /// parameters: FilterGraphNode - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFETileElement - SVGFETile, - /// synthesize a new image based on Fractal Noise (Perlin) with the chosen - /// stitching mode - /// parameters: FilterGraphNode, baseFrequencyX, baseFrequencyY, numOctaves, - /// seed - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFETurbulenceElement - SVGFETurbulenceWithFractalNoiseWithNoStitching{base_frequency_x: f32, - base_frequency_y: f32, num_octaves: u32, seed: u32}, - /// synthesize a new image based on Fractal Noise (Perlin) with the chosen - /// stitching mode - /// parameters: FilterGraphNode, baseFrequencyX, baseFrequencyY, numOctaves, - /// seed - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFETurbulenceElement - SVGFETurbulenceWithFractalNoiseWithStitching{base_frequency_x: f32, - base_frequency_y: f32, num_octaves: u32, seed: u32}, - /// synthesize a new image based on Turbulence Noise (offset vectors) - /// parameters: FilterGraphNode, baseFrequencyX, baseFrequencyY, numOctaves, - /// seed - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFETurbulenceElement - SVGFETurbulenceWithTurbulenceNoiseWithNoStitching{base_frequency_x: f32, - base_frequency_y: f32, num_octaves: u32, seed: u32}, - /// synthesize a new image based on Turbulence Noise (offset vectors) - /// parameters: FilterGraphNode, baseFrequencyX, baseFrequencyY, numOctaves, - /// seed - /// SVG filter semantics - selectable input(s), selectable between linear - /// (default) and sRGB color space for calculations - /// Spec: https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFETurbulenceElement - SVGFETurbulenceWithTurbulenceNoiseWithStitching{base_frequency_x: f32, - base_frequency_y: f32, num_octaves: u32, seed: u32}, -} - -impl FilterGraphOp { - pub fn kind(&self) -> &'static str { - match *self { - FilterGraphOp::SVGFEBlendColor => "SVGFEBlendColor", - FilterGraphOp::SVGFEBlendColorBurn => "SVGFEBlendColorBurn", - FilterGraphOp::SVGFEBlendColorDodge => "SVGFEBlendColorDodge", - FilterGraphOp::SVGFEBlendDarken => "SVGFEBlendDarken", - FilterGraphOp::SVGFEBlendDifference => "SVGFEBlendDifference", - FilterGraphOp::SVGFEBlendExclusion => "SVGFEBlendExclusion", - FilterGraphOp::SVGFEBlendHardLight => "SVGFEBlendHardLight", - FilterGraphOp::SVGFEBlendHue => "SVGFEBlendHue", - FilterGraphOp::SVGFEBlendLighten => "SVGFEBlendLighten", - FilterGraphOp::SVGFEBlendLuminosity => "SVGFEBlendLuminosity", - FilterGraphOp::SVGFEBlendMultiply => "SVGFEBlendMultiply", - FilterGraphOp::SVGFEBlendNormal => "SVGFEBlendNormal", - FilterGraphOp::SVGFEBlendOverlay => "SVGFEBlendOverlay", - FilterGraphOp::SVGFEBlendSaturation => "SVGFEBlendSaturation", - FilterGraphOp::SVGFEBlendScreen => "SVGFEBlendScreen", - FilterGraphOp::SVGFEBlendSoftLight => "SVGFEBlendSoftLight", - FilterGraphOp::SVGFEColorMatrix{..} => "SVGFEColorMatrix", - FilterGraphOp::SVGFEComponentTransfer => "SVGFEComponentTransfer", - FilterGraphOp::SVGFEComponentTransferInterned{..} => "SVGFEComponentTransferInterned", - FilterGraphOp::SVGFECompositeArithmetic{..} => "SVGFECompositeArithmetic", - FilterGraphOp::SVGFECompositeATop => "SVGFECompositeATop", - FilterGraphOp::SVGFECompositeIn => "SVGFECompositeIn", - FilterGraphOp::SVGFECompositeLighter => "SVGFECompositeLighter", - FilterGraphOp::SVGFECompositeOut => "SVGFECompositeOut", - FilterGraphOp::SVGFECompositeOver => "SVGFECompositeOver", - FilterGraphOp::SVGFECompositeXOR => "SVGFECompositeXOR", - FilterGraphOp::SVGFEConvolveMatrixEdgeModeDuplicate{..} => "SVGFEConvolveMatrixEdgeModeDuplicate", - FilterGraphOp::SVGFEConvolveMatrixEdgeModeNone{..} => "SVGFEConvolveMatrixEdgeModeNone", - FilterGraphOp::SVGFEConvolveMatrixEdgeModeWrap{..} => "SVGFEConvolveMatrixEdgeModeWrap", - FilterGraphOp::SVGFEDiffuseLightingDistant{..} => "SVGFEDiffuseLightingDistant", - FilterGraphOp::SVGFEDiffuseLightingPoint{..} => "SVGFEDiffuseLightingPoint", - FilterGraphOp::SVGFEDiffuseLightingSpot{..} => "SVGFEDiffuseLightingSpot", - FilterGraphOp::SVGFEDisplacementMap{..} => "SVGFEDisplacementMap", - FilterGraphOp::SVGFEDropShadow{..} => "SVGFEDropShadow", - FilterGraphOp::SVGFEFlood{..} => "SVGFEFlood", - FilterGraphOp::SVGFEGaussianBlur{..} => "SVGFEGaussianBlur", - FilterGraphOp::SVGFEIdentity => "SVGFEIdentity", - FilterGraphOp::SVGFEImage{..} => "SVGFEImage", - FilterGraphOp::SVGFEMorphologyDilate{..} => "SVGFEMorphologyDilate", - FilterGraphOp::SVGFEMorphologyErode{..} => "SVGFEMorphologyErode", - FilterGraphOp::SVGFEOpacity{..} => "SVGFEOpacity", - FilterGraphOp::SVGFESourceAlpha => "SVGFESourceAlpha", - FilterGraphOp::SVGFESourceGraphic => "SVGFESourceGraphic", - FilterGraphOp::SVGFESpecularLightingDistant{..} => "SVGFESpecularLightingDistant", - FilterGraphOp::SVGFESpecularLightingPoint{..} => "SVGFESpecularLightingPoint", - FilterGraphOp::SVGFESpecularLightingSpot{..} => "SVGFESpecularLightingSpot", - FilterGraphOp::SVGFETile => "SVGFETile", - FilterGraphOp::SVGFEToAlpha => "SVGFEToAlpha", - FilterGraphOp::SVGFETurbulenceWithFractalNoiseWithNoStitching{..} => "SVGFETurbulenceWithFractalNoiseWithNoStitching", - FilterGraphOp::SVGFETurbulenceWithFractalNoiseWithStitching{..} => "SVGFETurbulenceWithFractalNoiseWithStitching", - FilterGraphOp::SVGFETurbulenceWithTurbulenceNoiseWithNoStitching{..} => "SVGFETurbulenceWithTurbulenceNoiseWithNoStitching", - FilterGraphOp::SVGFETurbulenceWithTurbulenceNoiseWithStitching{..} => "SVGFETurbulenceWithTurbulenceNoiseWithStitching", - } - } -} - -#[derive(Clone, Debug)] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub struct FilterGraphNode { - /// Indicates this graph node was marked as necessary by the DAG optimizer - pub kept_by_optimizer: bool, - /// true if color_interpolation_filter == LinearRgb; shader will convert - /// sRGB texture pixel colors on load and convert back on store, for correct - /// interpolation - pub linear: bool, - /// padding for output rect if we need a border to get correct clamping, or - /// to account for larger final subregion than source rect (see bug 1869672) - pub inflate: i16, - /// virtualized picture input bindings, these refer to other filter outputs - /// by number within the graph, usually there is one element - pub inputs: Vec<FilterGraphPictureReference>, - /// clipping rect for filter node output - pub subregion: LayoutRect, -} - -impl From<FilterOpGraphNode> for FilterGraphNode { - fn from(node: FilterOpGraphNode) -> Self { - let mut inputs: Vec<FilterGraphPictureReference> = Vec::new(); - if node.input.buffer_id != FilterOpGraphPictureBufferId::None { - inputs.push(node.input.into()); - } - if node.input2.buffer_id != FilterOpGraphPictureBufferId::None { - inputs.push(node.input2.into()); - } - // If the op used by this node is a feMerge, it will add more inputs - // after this invocation. - FilterGraphNode{ - linear: node.linear, - inputs, - subregion: node.subregion, - // These are computed later in scene_building - kept_by_optimizer: true, - inflate: 0, - } - } -} - -/// Here we transform source rect to target rect for SVGFEGraph by walking -/// the whole graph and propagating subregions based on the provided -/// invalidation rect, and we want it to be a tight fit so we don't waste -/// time applying multiple filters to pixels that do not contribute to the -/// invalidated rect. -/// -/// The interesting parts of the handling of SVG filters are: -/// * scene_building.rs : wrap_prim_with_filters -/// * picture.rs : get_coverage_target_svgfe (you are here) -/// * picture.rs : get_coverage_source_svgfe -/// * render_task.rs : new_svg_filter_graph -/// * render_target.rs : add_svg_filter_node_instances -pub fn get_coverage_target_svgfe( - filters: &[(FilterGraphNode, FilterGraphOp)], - surface_rect: LayoutRect, -) -> LayoutRect { - - // The value of BUFFER_LIMIT here must be the same as in - // scene_building.rs, or we'll hit asserts here. - const BUFFER_LIMIT: usize = SVGFE_GRAPH_MAX; - - // We need to evaluate the subregions based on the proposed - // SourceGraphic rect as it isn't known at scene build time. - let mut subregion_by_buffer_id: [LayoutRect; BUFFER_LIMIT] = [LayoutRect::zero(); BUFFER_LIMIT]; - for (id, (node, op)) in filters.iter().enumerate() { - let full_subregion = node.subregion; - let mut used_subregion = LayoutRect::zero(); - for input in &node.inputs { - match input.buffer_id { - FilterOpGraphPictureBufferId::BufferId(id) => { - assert!((id as usize) < BUFFER_LIMIT, "BUFFER_LIMIT must be the same in frame building and scene building"); - // This id lookup should always succeed. - let input_subregion = subregion_by_buffer_id[id as usize]; - // Now add the padding that transforms from - // source to target, this was determined during - // scene build based on the operation. - let input_subregion = - LayoutRect::new( - LayoutPoint::new( - input_subregion.min.x + input.target_padding.min.x, - input_subregion.min.y + input.target_padding.min.y, - ), - LayoutPoint::new( - input_subregion.max.x + input.target_padding.max.x, - input_subregion.max.y + input.target_padding.max.y, - ), - ); - used_subregion = used_subregion - .union(&input_subregion); - } - FilterOpGraphPictureBufferId::None => { - panic!("Unsupported BufferId type"); - } - } - } - // We can clip the used subregion to the node subregion - used_subregion = used_subregion - .intersection(&full_subregion) - .unwrap_or(LayoutRect::zero()); - match op { - FilterGraphOp::SVGFEBlendColor => {} - FilterGraphOp::SVGFEBlendColorBurn => {} - FilterGraphOp::SVGFEBlendColorDodge => {} - FilterGraphOp::SVGFEBlendDarken => {} - FilterGraphOp::SVGFEBlendDifference => {} - FilterGraphOp::SVGFEBlendExclusion => {} - FilterGraphOp::SVGFEBlendHardLight => {} - FilterGraphOp::SVGFEBlendHue => {} - FilterGraphOp::SVGFEBlendLighten => {} - FilterGraphOp::SVGFEBlendLuminosity => {} - FilterGraphOp::SVGFEBlendMultiply => {} - FilterGraphOp::SVGFEBlendNormal => {} - FilterGraphOp::SVGFEBlendOverlay => {} - FilterGraphOp::SVGFEBlendSaturation => {} - FilterGraphOp::SVGFEBlendScreen => {} - FilterGraphOp::SVGFEBlendSoftLight => {} - FilterGraphOp::SVGFEColorMatrix { values } => { - if values[19] > 0.0 { - // Manipulating alpha offset can easily create new - // pixels outside of input subregions - used_subregion = full_subregion; - add_text_marker( - "SVGFEColorMatrix", - "SVGFEColorMatrix with non-zero alpha offset, using full subregion", - Duration::from_millis(1)); - } - } - FilterGraphOp::SVGFEComponentTransfer => unreachable!(), - FilterGraphOp::SVGFEComponentTransferInterned{handle: _, creates_pixels} => { - // Check if the value of alpha[0] is modified, if so - // the whole subregion is used because it will be - // creating new pixels outside of input subregions - if *creates_pixels { - used_subregion = full_subregion; - add_text_marker( - "SVGFEComponentTransfer", - "SVGFEComponentTransfer with non-zero minimum alpha, using full subregion", - Duration::from_millis(1)); - } - } - FilterGraphOp::SVGFECompositeArithmetic { k1, k2, k3, k4 } => { - // Optimization opportunity - some inputs may be - // smaller subregions due to the way the math works, - // k1 is the intersection of the two inputs, k2 is - // the first input only, k3 is the second input - // only, and k4 changes the whole subregion. - // - // See logic for SVG_FECOMPOSITE_OPERATOR_ARITHMETIC - // in FilterSupport.cpp - // - // We can at least ignore the entire node if - // everything is zero. - if *k1 <= 0.0 && - *k2 <= 0.0 && - *k3 <= 0.0 { - used_subregion = LayoutRect::zero(); - } - // Check if alpha is added to pixels as it means it - // can fill pixels outside input subregions - if *k4 > 0.0 { - used_subregion = full_subregion; - add_text_marker( - "SVGFECompositeArithmetic", - "SVGFECompositeArithmetic with non-zero offset, using full subregion", - Duration::from_millis(1)); - } - } - FilterGraphOp::SVGFECompositeATop => {} - FilterGraphOp::SVGFECompositeIn => {} - FilterGraphOp::SVGFECompositeLighter => {} - FilterGraphOp::SVGFECompositeOut => {} - FilterGraphOp::SVGFECompositeOver => {} - FilterGraphOp::SVGFECompositeXOR => {} - FilterGraphOp::SVGFEConvolveMatrixEdgeModeDuplicate{..} => {} - FilterGraphOp::SVGFEConvolveMatrixEdgeModeNone{..} => {} - FilterGraphOp::SVGFEConvolveMatrixEdgeModeWrap{..} => {} - FilterGraphOp::SVGFEDiffuseLightingDistant{..} => {} - FilterGraphOp::SVGFEDiffuseLightingPoint{..} => {} - FilterGraphOp::SVGFEDiffuseLightingSpot{..} => {} - FilterGraphOp::SVGFEDisplacementMap{..} => {} - FilterGraphOp::SVGFEDropShadow{..} => {} - FilterGraphOp::SVGFEFlood { color } => { - // Subregion needs to be set to the full node - // subregion for fills (unless the fill is a no-op) - if color.a > 0.0 { - used_subregion = full_subregion; - } - } - FilterGraphOp::SVGFEGaussianBlur{..} => {} - FilterGraphOp::SVGFEIdentity => {} - FilterGraphOp::SVGFEImage { sampling_filter: _sampling_filter, matrix: _matrix } => { - // TODO: calculate the actual subregion - used_subregion = full_subregion; - } - FilterGraphOp::SVGFEMorphologyDilate{..} => {} - FilterGraphOp::SVGFEMorphologyErode{..} => {} - FilterGraphOp::SVGFEOpacity { valuebinding: _valuebinding, value } => { - // If fully transparent, we can ignore this node - if *value <= 0.0 { - used_subregion = LayoutRect::zero(); - } - } - FilterGraphOp::SVGFESourceAlpha | - FilterGraphOp::SVGFESourceGraphic => { - used_subregion = surface_rect; - } - FilterGraphOp::SVGFESpecularLightingDistant{..} => {} - FilterGraphOp::SVGFESpecularLightingPoint{..} => {} - FilterGraphOp::SVGFESpecularLightingSpot{..} => {} - FilterGraphOp::SVGFETile => { - // feTile fills the entire output with - // source pixels, so it's effectively a flood. - used_subregion = full_subregion; - } - FilterGraphOp::SVGFEToAlpha => {} - FilterGraphOp::SVGFETurbulenceWithFractalNoiseWithNoStitching{..} | - FilterGraphOp::SVGFETurbulenceWithFractalNoiseWithStitching{..} | - FilterGraphOp::SVGFETurbulenceWithTurbulenceNoiseWithNoStitching{..} | - FilterGraphOp::SVGFETurbulenceWithTurbulenceNoiseWithStitching{..} => { - // Turbulence produces pixel values throughout the - // node subregion. - used_subregion = full_subregion; - } - } - // Store the subregion so later nodes can refer back - // to this and propagate rects properly - assert!((id as usize) < BUFFER_LIMIT, "BUFFER_LIMIT must be the same in frame building and scene building"); - subregion_by_buffer_id[id] = used_subregion; - } - subregion_by_buffer_id[filters.len() - 1] -} - -/// Here we transform target rect to source rect for SVGFEGraph by walking -/// the whole graph and propagating subregions based on the provided -/// invalidation rect, and we want it to be a tight fit so we don't waste -/// time applying multiple filters to pixels that do not contribute to the -/// invalidated rect. -/// -/// The interesting parts of the handling of SVG filters are: -/// * scene_building.rs : wrap_prim_with_filters -/// * picture.rs : get_coverage_target_svgfe -/// * picture.rs : get_coverage_source_svgfe (you are here) -/// * render_task.rs : new_svg_filter_graph -/// * render_target.rs : add_svg_filter_node_instances -pub fn get_coverage_source_svgfe( - filters: &[(FilterGraphNode, FilterGraphOp)], - surface_rect: LayoutRect, -) -> LayoutRect { - - // The value of BUFFER_LIMIT here must be the same as in - // scene_building.rs, or we'll hit asserts here. - const BUFFER_LIMIT: usize = SVGFE_GRAPH_MAX; - - // We're solving the source rect from target rect (e.g. due - // to invalidation of a region, we need to know how much of - // SourceGraphic is needed to draw that region accurately), - // so we need to walk the DAG in reverse and accumulate the source - // subregion for each input onto the referenced node, which can then - // propagate that to its inputs when it is iterated. - let mut source_subregion = LayoutRect::zero(); - let mut subregion_by_buffer_id: [LayoutRect; BUFFER_LIMIT] = - [LayoutRect::zero(); BUFFER_LIMIT]; - let final_buffer_id = filters.len() - 1; - assert!(final_buffer_id < BUFFER_LIMIT, "BUFFER_LIMIT must be the same in frame building and scene building"); - subregion_by_buffer_id[final_buffer_id] = surface_rect; - for (node_buffer_id, (node, op)) in filters.iter().enumerate().rev() { - // This is the subregion this node outputs, we can clip - // the inputs based on source_padding relative to this, - // and accumulate a new subregion for them. - assert!(node_buffer_id < BUFFER_LIMIT, "BUFFER_LIMIT must be the same in frame building and scene building"); - let full_subregion = node.subregion; - let mut used_subregion = - subregion_by_buffer_id[node_buffer_id]; - // We can clip the propagated subregion to the node subregion before - // we add source_padding for each input and propogate to them - used_subregion = used_subregion - .intersection(&full_subregion) - .unwrap_or(LayoutRect::zero()); - if !used_subregion.is_empty() { - for input in &node.inputs { - let input_subregion = LayoutRect::new( - LayoutPoint::new( - used_subregion.min.x + input.source_padding.min.x, - used_subregion.min.y + input.source_padding.min.y, - ), - LayoutPoint::new( - used_subregion.max.x + input.source_padding.max.x, - used_subregion.max.y + input.source_padding.max.y, - ), - ); - match input.buffer_id { - FilterOpGraphPictureBufferId::BufferId(id) => { - // Add the used area to the input, later when - // the referneced node is iterated as a node it - // will propagate the used bounds. - subregion_by_buffer_id[id as usize] = - subregion_by_buffer_id[id as usize] - .union(&input_subregion); - } - FilterOpGraphPictureBufferId::None => {} - } - } - } - // If this is the SourceGraphic or SourceAlpha, we now have the - // source subregion we're looking for. If both exist in the - // same graph, we need to combine them, so don't merely replace. - match op { - FilterGraphOp::SVGFESourceAlpha | - FilterGraphOp::SVGFESourceGraphic => { - source_subregion = source_subregion.union(&used_subregion); - } - _ => {} - } - } - - // Note that this can be zero if SourceGraphic/SourceAlpha is not used - // in this graph. - source_subregion -} diff --git a/gfx/wr/webrender/src/tile_cache.rs b/gfx/wr/webrender/src/tile_cache.rs @@ -0,0 +1,779 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use api::{BorderRadius, ClipId, ClipMode, ColorF, DebugFlags, PrimitiveFlags, QualitySettings, RasterSpace}; +use api::units::*; +use crate::clip::{ClipItemKeyKind, ClipNodeId, ClipTreeBuilder}; +use crate::frame_builder::FrameBuilderConfig; +use crate::internal_types::FastHashMap; +use crate::picture::{PrimitiveList, PictureCompositeMode, PicturePrimitive, SliceId}; +use crate::picture::{Picture3DContext, TileCacheParams, TileOffset, PictureFlags}; +use crate::prim_store::{PrimitiveInstance, PrimitiveStore, PictureIndex}; +use crate::scene_building::SliceFlags; +use crate::scene_builder_thread::Interners; +use crate::spatial_tree::{SpatialNodeIndex, SceneSpatialTree}; +use crate::util::VecHelper; +use std::mem; + +/* + Types and functionality related to picture caching. In future, we'll + move more and more of the existing functionality out of picture.rs + and into here. + */ + +// If the page would create too many slices (an arbitrary definition where +// it's assumed the GPU memory + compositing overhead would be too high) +// then create a single picture cache for the remaining content. This at +// least means that we can cache small content changes efficiently when +// scrolling isn't occurring. Scrolling regions will be handled reasonably +// efficiently by the dirty rect tracking (since it's likely that if the +// page has so many slices there isn't a single major scroll region). +const MAX_CACHE_SLICES: usize = 16; + +struct SliceDescriptor { + prim_list: PrimitiveList, + scroll_root: SpatialNodeIndex, +} + +enum SliceKind { + Default { + secondary_slices: Vec<SliceDescriptor>, + }, + Atomic { + prim_list: PrimitiveList, + }, +} + +impl SliceKind { + fn default() -> Self { + SliceKind::Default { + secondary_slices: Vec::new(), + } + } +} + +struct PrimarySlice { + /// Whether this slice is atomic or has secondary slice(s) + kind: SliceKind, + /// Optional background color of this slice + background_color: Option<ColorF>, + /// Optional root clip for the iframe + iframe_clip: Option<ClipId>, + /// Information about how to draw and composite this slice + slice_flags: SliceFlags, +} + +impl PrimarySlice { + fn new( + slice_flags: SliceFlags, + iframe_clip: Option<ClipId>, + background_color: Option<ColorF>, + ) -> Self { + PrimarySlice { + kind: SliceKind::default(), + background_color, + iframe_clip, + slice_flags, + } + } + + fn has_too_many_slices(&self) -> bool { + match self.kind { + SliceKind::Atomic { .. } => false, + SliceKind::Default { ref secondary_slices } => secondary_slices.len() > MAX_CACHE_SLICES, + } + } + + fn merge(&mut self) { + self.slice_flags |= SliceFlags::IS_ATOMIC; + + let old = mem::replace( + &mut self.kind, + SliceKind::Default { secondary_slices: Vec::new() }, + ); + + self.kind = match old { + SliceKind::Default { mut secondary_slices } => { + let mut prim_list = PrimitiveList::empty(); + + for descriptor in secondary_slices.drain(..) { + prim_list.merge(descriptor.prim_list); + } + + SliceKind::Atomic { + prim_list, + } + } + atomic => atomic, + } + } +} + +/// Used during scene building to construct the list of pending tile caches. +pub struct TileCacheBuilder { + /// List of tile caches that have been created so far (last in the list is currently active). + primary_slices: Vec<PrimarySlice>, + /// Cache the previous scroll root search for a spatial node, since they are often the same. + prev_scroll_root_cache: (SpatialNodeIndex, SpatialNodeIndex), + /// Handle to the root reference frame + root_spatial_node_index: SpatialNodeIndex, + /// Debug flags to provide to our TileCacheInstances. + debug_flags: DebugFlags, +} + +/// The output of a tile cache builder, containing all details needed to construct the +/// tile cache(s) for the next scene, and retain tiles from the previous frame when sent +/// send to the frame builder. +pub struct TileCacheConfig { + /// Mapping of slice id to the parameters needed to construct this tile cache. + pub tile_caches: FastHashMap<SliceId, TileCacheParams>, + /// Number of picture cache slices that were created (for profiler) + pub picture_cache_slice_count: usize, +} + +impl TileCacheConfig { + pub fn new(picture_cache_slice_count: usize) -> Self { + TileCacheConfig { + tile_caches: FastHashMap::default(), + picture_cache_slice_count, + } + } +} + +impl TileCacheBuilder { + /// Construct a new tile cache builder. + pub fn new( + root_spatial_node_index: SpatialNodeIndex, + background_color: Option<ColorF>, + debug_flags: DebugFlags, + ) -> Self { + TileCacheBuilder { + primary_slices: vec![PrimarySlice::new(SliceFlags::empty(), None, background_color)], + prev_scroll_root_cache: (SpatialNodeIndex::INVALID, SpatialNodeIndex::INVALID), + root_spatial_node_index, + debug_flags, + } + } + + pub fn make_current_slice_atomic(&mut self) { + self.primary_slices + .last_mut() + .unwrap() + .merge(); + } + + /// Returns true if the current slice has no primitives added yet + pub fn is_current_slice_empty(&self) -> bool { + match self.primary_slices.last() { + Some(slice) => { + match slice.kind { + SliceKind::Default { ref secondary_slices } => { + secondary_slices.is_empty() + } + SliceKind::Atomic { ref prim_list } => { + prim_list.is_empty() + } + } + } + None => { + true + } + } + } + + /// Set a barrier that forces a new tile cache next time a prim is added. + pub fn add_tile_cache_barrier( + &mut self, + slice_flags: SliceFlags, + iframe_clip: Option<ClipId>, + ) { + let new_slice = PrimarySlice::new( + slice_flags, + iframe_clip, + None, + ); + + self.primary_slices.push(new_slice); + } + + /// Create a new tile cache for an existing prim_list + fn build_tile_cache( + &mut self, + prim_list: PrimitiveList, + spatial_tree: &SceneSpatialTree, + ) -> Option<SliceDescriptor> { + if prim_list.is_empty() { + return None; + } + + // Iterate the clusters and determine which is the most commonly occurring + // scroll root. This is a reasonable heuristic to decide which spatial node + // should be considered the scroll root of this tile cache, in order to + // minimize the invalidations that occur due to scrolling. It's often the + // case that a blend container will have only a single scroll root. + let mut scroll_root_occurrences = FastHashMap::default(); + + for cluster in &prim_list.clusters { + // If we encounter a cluster which has an unknown spatial node, + // we don't include that in the set of spatial nodes that we + // are trying to find scroll roots for. Later on, in finalize_picture, + // the cluster spatial node will be updated to the selected scroll root. + if cluster.spatial_node_index == SpatialNodeIndex::UNKNOWN { + continue; + } + + let scroll_root = find_scroll_root( + cluster.spatial_node_index, + &mut self.prev_scroll_root_cache, + spatial_tree, + true, + ); + + *scroll_root_occurrences.entry(scroll_root).or_insert(0) += 1; + } + + // We can't just select the most commonly occurring scroll root in this + // primitive list. If that is a nested scroll root, there may be + // primitives in the list that are outside that scroll root, which + // can cause panics when calculating relative transforms. To ensure + // this doesn't happen, only retain scroll root candidates that are + // also ancestors of every other scroll root candidate. + let scroll_roots: Vec<SpatialNodeIndex> = scroll_root_occurrences + .keys() + .cloned() + .collect(); + + scroll_root_occurrences.retain(|parent_spatial_node_index, _| { + scroll_roots.iter().all(|child_spatial_node_index| { + parent_spatial_node_index == child_spatial_node_index || + spatial_tree.is_ancestor( + *parent_spatial_node_index, + *child_spatial_node_index, + ) + }) + }); + + // Select the scroll root by finding the most commonly occurring one + let scroll_root = scroll_root_occurrences + .iter() + .max_by_key(|entry | entry.1) + .map(|(spatial_node_index, _)| *spatial_node_index) + .unwrap_or(self.root_spatial_node_index); + + Some(SliceDescriptor { + scroll_root, + prim_list, + }) + } + + /// Add a primitive, either to the current tile cache, or a new one, depending on various conditions. + pub fn add_prim( + &mut self, + prim_instance: PrimitiveInstance, + prim_rect: LayoutRect, + spatial_node_index: SpatialNodeIndex, + prim_flags: PrimitiveFlags, + spatial_tree: &SceneSpatialTree, + interners: &Interners, + quality_settings: &QualitySettings, + prim_instances: &mut Vec<PrimitiveInstance>, + clip_tree_builder: &ClipTreeBuilder, + ) { + let primary_slice = self.primary_slices.last_mut().unwrap(); + + match primary_slice.kind { + SliceKind::Atomic { ref mut prim_list } => { + prim_list.add_prim( + prim_instance, + prim_rect, + spatial_node_index, + prim_flags, + prim_instances, + clip_tree_builder, + ); + } + SliceKind::Default { ref mut secondary_slices } => { + assert_ne!(spatial_node_index, SpatialNodeIndex::UNKNOWN); + + // Check if we want to create a new slice based on the current / next scroll root + let scroll_root = find_scroll_root( + spatial_node_index, + &mut self.prev_scroll_root_cache, + spatial_tree, + // Allow sticky frames as scroll roots, unless our quality settings prefer + // subpixel AA over performance. + !quality_settings.force_subpixel_aa_where_possible, + ); + + let current_scroll_root = secondary_slices + .last() + .map(|p| p.scroll_root); + + let mut want_new_tile_cache = secondary_slices.is_empty(); + + if let Some(current_scroll_root) = current_scroll_root { + want_new_tile_cache |= match (current_scroll_root, scroll_root) { + (_, _) if current_scroll_root == self.root_spatial_node_index && scroll_root == self.root_spatial_node_index => { + // Both current slice and this cluster are fixed position, no need to cut + false + } + (_, _) if current_scroll_root == self.root_spatial_node_index => { + // A real scroll root is being established, so create a cache slice + true + } + (_, _) if scroll_root == self.root_spatial_node_index => { + // If quality settings force subpixel AA over performance, skip creating + // a slice for the fixed position element(s) here. + if quality_settings.force_subpixel_aa_where_possible { + false + } else { + // A fixed position slice is encountered within a scroll root. Only create + // a slice in this case if all the clips referenced by this cluster are also + // fixed position. There's no real point in creating slices for these cases, + // since we'll have to rasterize them as the scrolling clip moves anyway. It + // also allows us to retain subpixel AA in these cases. For these types of + // slices, the intra-slice dirty rect handling typically works quite well + // (a common case is parallax scrolling effects). + let mut create_slice = true; + + let leaf = clip_tree_builder.get_leaf(prim_instance.clip_leaf_id); + let mut current_node_id = leaf.node_id; + + while current_node_id != ClipNodeId::NONE { + let node = clip_tree_builder.get_node(current_node_id); + + let clip_node_data = &interners.clip[node.handle]; + + let spatial_root = find_scroll_root( + clip_node_data.key.spatial_node_index, + &mut self.prev_scroll_root_cache, + spatial_tree, + true, + ); + + if spatial_root != self.root_spatial_node_index { + create_slice = false; + break; + } + + current_node_id = node.parent; + } + + create_slice + } + } + (curr_scroll_root, scroll_root) => { + // Two scrolling roots - only need a new slice if they differ + curr_scroll_root != scroll_root + } + }; + } + + if want_new_tile_cache { + secondary_slices.push(SliceDescriptor { + prim_list: PrimitiveList::empty(), + scroll_root, + }); + } + + secondary_slices + .last_mut() + .unwrap() + .prim_list + .add_prim( + prim_instance, + prim_rect, + spatial_node_index, + prim_flags, + prim_instances, + clip_tree_builder, + ); + } + } + } + + /// Consume this object and build the list of tile cache primitives + pub fn build( + mut self, + config: &FrameBuilderConfig, + prim_store: &mut PrimitiveStore, + spatial_tree: &SceneSpatialTree, + prim_instances: &[PrimitiveInstance], + clip_tree_builder: &mut ClipTreeBuilder, + interners: &Interners, + ) -> (TileCacheConfig, Vec<PictureIndex>) { + let mut result = TileCacheConfig::new(self.primary_slices.len()); + let mut tile_cache_pictures = Vec::new(); + let primary_slices = std::mem::replace(&mut self.primary_slices, Vec::new()); + + // TODO: At the moment, culling, clipping and invalidation are always + // done in the root coordinate space. The plan is to move to doing it + // (always or mostly) in raster space. + let visibility_node = spatial_tree.root_reference_frame_index(); + + for mut primary_slice in primary_slices { + + if primary_slice.has_too_many_slices() { + primary_slice.merge(); + } + + match primary_slice.kind { + SliceKind::Atomic { prim_list } => { + if let Some(descriptor) = self.build_tile_cache( + prim_list, + spatial_tree, + ) { + create_tile_cache( + self.debug_flags, + primary_slice.slice_flags, + descriptor.scroll_root, + visibility_node, + primary_slice.iframe_clip, + descriptor.prim_list, + primary_slice.background_color, + prim_store, + prim_instances, + config, + &mut result.tile_caches, + &mut tile_cache_pictures, + clip_tree_builder, + interners, + spatial_tree, + ); + } + } + SliceKind::Default { secondary_slices } => { + for descriptor in secondary_slices { + create_tile_cache( + self.debug_flags, + primary_slice.slice_flags, + descriptor.scroll_root, + visibility_node, + primary_slice.iframe_clip, + descriptor.prim_list, + primary_slice.background_color, + prim_store, + prim_instances, + config, + &mut result.tile_caches, + &mut tile_cache_pictures, + clip_tree_builder, + interners, + spatial_tree, + ); + } + } + } + } + + (result, tile_cache_pictures) + } +} + +/// Find the scroll root for a given spatial node +fn find_scroll_root( + spatial_node_index: SpatialNodeIndex, + prev_scroll_root_cache: &mut (SpatialNodeIndex, SpatialNodeIndex), + spatial_tree: &SceneSpatialTree, + allow_sticky_frames: bool, +) -> SpatialNodeIndex { + if prev_scroll_root_cache.0 == spatial_node_index { + return prev_scroll_root_cache.1; + } + + let scroll_root = spatial_tree.find_scroll_root(spatial_node_index, allow_sticky_frames); + *prev_scroll_root_cache = (spatial_node_index, scroll_root); + + scroll_root +} + +/// Given a PrimitiveList and scroll root, construct a tile cache primitive instance +/// that wraps the primitive list. +fn create_tile_cache( + debug_flags: DebugFlags, + slice_flags: SliceFlags, + scroll_root: SpatialNodeIndex, + visibility_node: SpatialNodeIndex, + iframe_clip: Option<ClipId>, + prim_list: PrimitiveList, + background_color: Option<ColorF>, + prim_store: &mut PrimitiveStore, + prim_instances: &[PrimitiveInstance], + frame_builder_config: &FrameBuilderConfig, + tile_caches: &mut FastHashMap<SliceId, TileCacheParams>, + tile_cache_pictures: &mut Vec<PictureIndex>, + clip_tree_builder: &mut ClipTreeBuilder, + interners: &Interners, + spatial_tree: &SceneSpatialTree, +) { + // Accumulate any clip instances from the iframe_clip into the shared clips + // that will be applied by this tile cache during compositing. + let mut additional_clips = Vec::new(); + + if let Some(clip_id) = iframe_clip { + additional_clips.push(clip_id); + } + + // Find the best shared clip node that we can apply while compositing tiles, + // rather than applying to each item individually. + + // Step 1: Walk the primitive list, and find the LCA of the clip-tree that + // matches all primitives. This gives us our "best-case" shared + // clip node that moves as many clips as possible to compositing. + let mut shared_clip_node_id = None; + + for cluster in &prim_list.clusters { + for prim_instance in &prim_instances[cluster.prim_range()] { + let leaf = clip_tree_builder.get_leaf(prim_instance.clip_leaf_id); + + // TODO(gw): Need to cache last clip-node id here? + shared_clip_node_id = match shared_clip_node_id { + Some(current) => { + Some(clip_tree_builder.find_lowest_common_ancestor(current, leaf.node_id)) + } + None => { + Some(leaf.node_id) + } + } + } + } + + // Step 2: Now we need to walk up the shared clip node hierarchy, and remove clips + // that we can't handle during compositing, such as: + // (a) Non axis-aligned clips + // (b) Box-shadow or image-mask clips + // (c) Rounded rect clips. + // + // A follow up patch to this series will relax the condition on (c) to + // allow tile caches to apply a single rounded-rect clip during compositing. + let mut shared_clip_node_id = shared_clip_node_id.unwrap_or(ClipNodeId::NONE); + let mut current_node_id = shared_clip_node_id; + let mut rounded_rect_count = 0; + + // Walk up the hierarchy to the root of the clip-tree + while current_node_id != ClipNodeId::NONE { + let node = clip_tree_builder.get_node(current_node_id); + let clip_node_data = &interners.clip[node.handle]; + + // Check if this clip is in the root coord system (i.e. is axis-aligned with tile-cache) + let is_rcs = spatial_tree.is_root_coord_system(clip_node_data.key.spatial_node_index); + + let node_valid = if is_rcs { + match clip_node_data.key.kind { + ClipItemKeyKind::BoxShadow(..) | + ClipItemKeyKind::ImageMask(..) | + ClipItemKeyKind::Rectangle(_, ClipMode::ClipOut) | + ClipItemKeyKind::RoundedRectangle(_, _, ClipMode::ClipOut) => { + // Has a box-shadow / image-mask, we can't handle this as a shared clip + false + } + ClipItemKeyKind::RoundedRectangle(rect, radius, ClipMode::Clip) => { + // The shader and CoreAnimation rely on certain constraints such + // as uniform radii to be able to apply the clip during compositing. + if BorderRadius::from(radius).can_use_fast_path_in(&rect.into()) { + rounded_rect_count += 1; + + true + } else { + false + } + } + ClipItemKeyKind::Rectangle(_, ClipMode::Clip) => { + // We can apply multiple (via combining) axis-aligned rectangle + // clips to the shared compositing clip. + true + } + } + } else { + // Has a complex transform, we can't handle this as a shared clip + false + }; + + if node_valid { + // This node was found to be one we can apply during compositing. + if rounded_rect_count > 1 { + // However, we plan to only support one rounded-rect clip. If + // we have found > 1 rounded rect, drop children from the shared + // clip, and continue looking up the chain. + shared_clip_node_id = current_node_id; + rounded_rect_count = 1; + } + } else { + // Node was invalid, due to transform / clip type. Drop this clip + // and reset the rounded rect count to 0, since we drop children + // from here too. + shared_clip_node_id = node.parent; + rounded_rect_count = 0; + } + + current_node_id = node.parent; + } + + let shared_clip_leaf_id = Some(clip_tree_builder.build_for_tile_cache( + shared_clip_node_id, + &additional_clips, + )); + + // Build a clip-chain for the tile cache, that contains any of the shared clips + // we will apply when drawing the tiles. In all cases provided by Gecko, these + // are rectangle clips with a scale/offset transform only, and get handled as + // a simple local clip rect in the vertex shader. However, this should in theory + // also work with any complex clips, such as rounded rects and image masks, by + // producing a clip mask that is applied to the picture cache tiles. + + let slice = tile_cache_pictures.len(); + + let background_color = if slice == 0 { + background_color + } else { + None + }; + + let slice_id = SliceId::new(slice); + + // Store some information about the picture cache slice. This is used when we swap the + // new scene into the frame builder to either reuse existing slices, or create new ones. + tile_caches.insert(slice_id, TileCacheParams { + debug_flags, + slice, + slice_flags, + spatial_node_index: scroll_root, + visibility_node_index: visibility_node, + background_color, + shared_clip_node_id, + shared_clip_leaf_id, + virtual_surface_size: frame_builder_config.compositor_kind.get_virtual_surface_size(), + image_surface_count: prim_list.image_surface_count, + yuv_image_surface_count: prim_list.yuv_image_surface_count, + }); + + let pic_index = prim_store.pictures.alloc().init(PicturePrimitive::new_image( + Some(PictureCompositeMode::TileCache { slice_id }), + Picture3DContext::Out, + PrimitiveFlags::IS_BACKFACE_VISIBLE, + prim_list, + scroll_root, + RasterSpace::Screen, + PictureFlags::empty(), + None, + )); + + tile_cache_pictures.push(PictureIndex(pic_index)); +} + +/// Debug information about a set of picture cache slices, exposed via RenderResults +#[derive(Debug)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct PictureCacheDebugInfo { + pub slices: FastHashMap<usize, SliceDebugInfo>, +} + +impl PictureCacheDebugInfo { + pub fn new() -> Self { + PictureCacheDebugInfo { + slices: FastHashMap::default(), + } + } + + /// Convenience method to retrieve a given slice. Deliberately panics + /// if the slice isn't present. + pub fn slice(&self, slice: usize) -> &SliceDebugInfo { + &self.slices[&slice] + } +} + +impl Default for PictureCacheDebugInfo { + fn default() -> PictureCacheDebugInfo { + PictureCacheDebugInfo::new() + } +} + +/// Debug information about a set of picture cache tiles, exposed via RenderResults +#[derive(Debug)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct SliceDebugInfo { + pub tiles: FastHashMap<TileOffset, TileDebugInfo>, +} + +impl SliceDebugInfo { + pub fn new() -> Self { + SliceDebugInfo { + tiles: FastHashMap::default(), + } + } + + /// Convenience method to retrieve a given tile. Deliberately panics + /// if the tile isn't present. + pub fn tile(&self, x: i32, y: i32) -> &TileDebugInfo { + &self.tiles[&TileOffset::new(x, y)] + } +} + +/// Debug information about a tile that was dirty and was rasterized +#[derive(Debug, PartialEq)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub struct DirtyTileDebugInfo { + pub local_valid_rect: PictureRect, + pub local_dirty_rect: PictureRect, +} + +/// Debug information about the state of a tile +#[derive(Debug, PartialEq)] +#[cfg_attr(feature = "capture", derive(Serialize))] +#[cfg_attr(feature = "replay", derive(Deserialize))] +pub enum TileDebugInfo { + /// Tile was occluded by a tile in front of it + Occluded, + /// Tile was culled (not visible in current display port) + Culled, + /// Tile was valid (no rasterization was done) and visible + Valid, + /// Tile was dirty, and was updated + Dirty(DirtyTileDebugInfo), +} + +impl TileDebugInfo { + pub fn is_occluded(&self) -> bool { + match self { + TileDebugInfo::Occluded => true, + TileDebugInfo::Culled | + TileDebugInfo::Valid | + TileDebugInfo::Dirty(..) => false, + } + } + + pub fn is_valid(&self) -> bool { + match self { + TileDebugInfo::Valid => true, + TileDebugInfo::Culled | + TileDebugInfo::Occluded | + TileDebugInfo::Dirty(..) => false, + } + } + + pub fn is_culled(&self) -> bool { + match self { + TileDebugInfo::Culled => true, + TileDebugInfo::Valid | + TileDebugInfo::Occluded | + TileDebugInfo::Dirty(..) => false, + } + } + + pub fn as_dirty(&self) -> &DirtyTileDebugInfo { + match self { + TileDebugInfo::Occluded | + TileDebugInfo::Culled | + TileDebugInfo::Valid => { + panic!("not a dirty tile!"); + } + TileDebugInfo::Dirty(ref info) => { + info + } + } + } +} diff --git a/gfx/wr/webrender/src/tile_cache/mod.rs b/gfx/wr/webrender/src/tile_cache/mod.rs @@ -1,3509 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -//! Tile cache types and descriptors -//! -//! This module contains the core tile caching infrastructure including: -//! - Tile identification and coordinate types -//! - Tile descriptors that track primitive dependencies -//! - Comparison results for invalidation tracking - -// Existing tile cache slice builder (was previously tile_cache.rs) -pub mod slice_builder; - -use api::{AlphaType, BorderRadius, ClipMode, ColorF, ColorDepth, DebugFlags, ImageKey, ImageRendering}; -use api::{PropertyBindingId, PrimitiveFlags, YuvFormat, YuvRangedColorSpace}; -use api::units::*; -use crate::clip::{ClipNodeId, ClipLeafId, ClipItemKind, ClipSpaceConversion, ClipChainInstance, ClipStore}; -use crate::composite::{CompositorKind, CompositeState, CompositorSurfaceKind, ExternalSurfaceDescriptor}; -use crate::composite::{ExternalSurfaceDependency, NativeSurfaceId, NativeTileId}; -use crate::composite::{CompositorClipIndex, CompositorTransformIndex}; -use crate::composite::{CompositeTileDescriptor, CompositeTile}; -use crate::gpu_types::ZBufferId; -use crate::intern::ItemUid; -use crate::internal_types::{FastHashMap, FrameId, Filter}; -use crate::invalidation::{InvalidationReason, DirtyRegion, PrimitiveCompareResult, quadtree::TileNode}; -use crate::invalidation::dependency::{PrimitiveComparer, PrimitiveDependency, ImageDependency}; -use crate::invalidation::dependency::{SpatialNodeComparer, PrimitiveComparisonKey}; -use crate::invalidation::dependency::{OpacityBindingInfo, ColorBindingInfo, OpacityBinding, ColorBinding}; -use crate::picture::{SurfaceTextureDescriptor, PictureCompositeMode, SurfaceIndex, clamp, clampf}; -use crate::picture::{get_relative_scale_offset, PicturePrimitive}; -use crate::picture::MAX_COMPOSITOR_SURFACES_SIZE; -use crate::prim_store::{PrimitiveInstance, PrimitiveInstanceKind, PrimitiveScratchBuffer, PictureIndex}; -use crate::prim_store::{ColorBindingStorage, ColorBindingIndex, PrimitiveTemplateKind}; -use crate::print_tree::{PrintTreePrinter, PrintTree}; -use crate::{profiler, render_backend::DataStores}; -use crate::profiler::TransactionProfile; -use crate::renderer::GpuBufferBuilderF; -use crate::resource_cache::{ResourceCache, ImageRequest}; -use crate::scene_building::SliceFlags; -use crate::space::SpaceMapper; -use crate::spatial_tree::{SpatialNodeIndex, SpatialTree}; -use crate::surface::{SubpixelMode, SurfaceInfo}; -use crate::util::{ScaleOffset, MatrixHelpers, MaxRect}; -use crate::visibility::{FrameVisibilityContext, FrameVisibilityState, VisibilityState, PrimitiveVisibilityFlags}; -use euclid::approxeq::ApproxEq; -use euclid::Box2D; -use peek_poke::{PeekPoke, poke_into_vec, ensure_red_zone}; -use smallvec::SmallVec; -use std::fmt::{Display, Error, Formatter}; -use std::{marker, mem, u32}; -use std::sync::atomic::{AtomicUsize, Ordering}; - -pub use self::slice_builder::{ - TileCacheBuilder, TileCacheConfig, - PictureCacheDebugInfo, SliceDebugInfo, DirtyTileDebugInfo, TileDebugInfo, -}; - -pub use api::units::TileOffset; -pub use api::units::TileRange as TileRect; - -/// The maximum number of compositor surfaces that are allowed per picture cache. This -/// is an arbitrary number that should be enough for common cases, but low enough to -/// prevent performance and memory usage drastically degrading in pathological cases. -pub const MAX_COMPOSITOR_SURFACES: usize = 4; - -/// The size in device pixels of a normal cached tile. -pub const TILE_SIZE_DEFAULT: DeviceIntSize = DeviceIntSize { - width: 1024, - height: 512, - _unit: marker::PhantomData, -}; - -/// The size in device pixels of a tile for horizontal scroll bars -pub const TILE_SIZE_SCROLLBAR_HORIZONTAL: DeviceIntSize = DeviceIntSize { - width: 1024, - height: 32, - _unit: marker::PhantomData, -}; - -/// The size in device pixels of a tile for vertical scroll bars -pub const TILE_SIZE_SCROLLBAR_VERTICAL: DeviceIntSize = DeviceIntSize { - width: 32, - height: 1024, - _unit: marker::PhantomData, -}; - -/// The maximum size per axis of a surface, in DevicePixel coordinates. -/// Render tasks larger than this size are scaled down to fit, which may cause -/// some blurriness. -pub const MAX_SURFACE_SIZE: usize = 4096; - -/// Used to get unique tile IDs, even when the tile cache is -/// destroyed between display lists / scenes. -static NEXT_TILE_ID: AtomicUsize = AtomicUsize::new(0); - -/// A unique identifier for a tile. These are stable across display lists and -/// scenes. -#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Hash)] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub struct TileId(pub usize); - -impl TileId { - pub fn new() -> TileId { - TileId(NEXT_TILE_ID.fetch_add(1, Ordering::Relaxed)) - } -} - -// Internal function used by picture.rs for creating TileIds -#[doc(hidden)] -pub fn next_tile_id() -> usize { - NEXT_TILE_ID.fetch_add(1, Ordering::Relaxed) -} - -/// Uniquely identifies a tile within a picture cache slice -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -#[derive(Debug, Copy, Clone, PartialEq, Hash, Eq)] -pub struct TileKey { - // Tile index (x,y) - pub tile_offset: TileOffset, - // Sub-slice (z) - pub sub_slice_index: SubSliceIndex, -} - -/// An index into the prims array in a TileDescriptor. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub struct PrimitiveDependencyIndex(pub u32); - -/// Defines which sub-slice (effectively a z-index) a primitive exists on within -/// a picture cache instance. -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PeekPoke)] -pub struct SubSliceIndex(pub u8); - -impl SubSliceIndex { - pub const DEFAULT: SubSliceIndex = SubSliceIndex(0); - - pub fn new(index: usize) -> Self { - SubSliceIndex(index as u8) - } - - /// Return true if this sub-slice is the primary sub-slice (for now, we assume - /// that only the primary sub-slice may be opaque and support subpixel AA, for example). - pub fn is_primary(&self) -> bool { - self.0 == 0 - } - - /// Get an array index for this sub-slice - pub fn as_usize(&self) -> usize { - self.0 as usize - } -} - - -/// Information about a primitive that is a dependency for a tile. -#[derive(Debug, Clone)] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub struct PrimitiveDescriptor { - pub prim_uid: ItemUid, - pub prim_clip_box: PictureBox2D, - // TODO(gw): These two fields could be packed as a u24/u8 - pub dep_offset: u32, - pub dep_count: u32, -} - -impl PartialEq for PrimitiveDescriptor { - fn eq(&self, other: &Self) -> bool { - const EPSILON: f32 = 0.001; - - if self.prim_uid != other.prim_uid { - return false; - } - - use euclid::approxeq::ApproxEq; - if !self.prim_clip_box.min.x.approx_eq_eps(&other.prim_clip_box.min.x, &EPSILON) { - return false; - } - if !self.prim_clip_box.min.y.approx_eq_eps(&other.prim_clip_box.min.y, &EPSILON) { - return false; - } - if !self.prim_clip_box.max.x.approx_eq_eps(&other.prim_clip_box.max.x, &EPSILON) { - return false; - } - if !self.prim_clip_box.max.y.approx_eq_eps(&other.prim_clip_box.max.y, &EPSILON) { - return false; - } - - if self.dep_count != other.dep_count { - return false; - } - - true - } -} - -impl PartialEq<PrimitiveDescriptor> for (&ItemUid, &PictureBox2D) { - fn eq(&self, other: &PrimitiveDescriptor) -> bool { - self.0 == &other.prim_uid && self.1 == &other.prim_clip_box - } -} - -/// Uniquely describes the content of this tile, in a way that can be -/// (reasonably) efficiently hashed and compared. -#[cfg_attr(any(feature="capture",feature="replay"), derive(Clone))] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub struct TileDescriptor { - /// List of primitive instance unique identifiers. The uid is guaranteed - /// to uniquely describe the content of the primitive template, while - /// the other parameters describe the clip chain and instance params. - pub prims: Vec<PrimitiveDescriptor>, - - /// Picture space rect that contains valid pixels region of this tile. - pub local_valid_rect: PictureRect, - - /// The last frame this tile had its dependencies updated (dependency updating is - /// skipped if a tile is off-screen). - pub last_updated_frame_id: FrameId, - - /// Packed per-prim dependency information - pub dep_data: Vec<u8>, -} - -impl TileDescriptor { - pub fn new() -> Self { - TileDescriptor { - local_valid_rect: PictureRect::zero(), - dep_data: Vec::new(), - prims: Vec::new(), - last_updated_frame_id: FrameId::INVALID, - } - } - - /// Print debug information about this tile descriptor to a tree printer. - pub fn print(&self, pt: &mut dyn crate::print_tree::PrintTreePrinter) { - pt.new_level("current_descriptor".to_string()); - - pt.new_level("prims".to_string()); - for prim in &self.prims { - pt.new_level(format!("prim uid={}", prim.prim_uid.get_uid())); - pt.add_item(format!("clip: p0={},{} p1={},{}", - prim.prim_clip_box.min.x, - prim.prim_clip_box.min.y, - prim.prim_clip_box.max.x, - prim.prim_clip_box.max.y, - )); - pt.end_level(); - } - pt.end_level(); - - pt.end_level(); - } - - /// Clear the dependency information for a tile, when the dependencies - /// are being rebuilt. - pub fn clear(&mut self) { - self.local_valid_rect = PictureRect::zero(); - self.prims.clear(); - self.dep_data.clear(); - } -} - - -/// The key that identifies a tile cache instance. For now, it's simple the index of -/// the slice as it was created during scene building. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub struct SliceId(usize); - -impl SliceId { - pub fn new(index: usize) -> Self { - SliceId(index) - } -} - -/// Information that is required to reuse or create a new tile cache. Created -/// during scene building and passed to the render backend / frame builder. -pub struct TileCacheParams { - // The current debug flags for the system. - pub debug_flags: DebugFlags, - // Index of the slice (also effectively the key of the tile cache, though we use SliceId where that matters) - pub slice: usize, - // Flags describing content of this cache (e.g. scrollbars) - pub slice_flags: SliceFlags, - // The anchoring spatial node / scroll root - pub spatial_node_index: SpatialNodeIndex, - // The space in which visibility/invalidation/clipping computations are done. - pub visibility_node_index: SpatialNodeIndex, - // Optional background color of this tilecache. If present, can be used as an optimization - // to enable opaque blending and/or subpixel AA in more places. - pub background_color: Option<ColorF>, - // Node in the clip-tree that defines where we exclude clips from child prims - pub shared_clip_node_id: ClipNodeId, - // Clip leaf that is used to build the clip-chain for this tile cache. - pub shared_clip_leaf_id: Option<ClipLeafId>, - // Virtual surface sizes are always square, so this represents both the width and height - pub virtual_surface_size: i32, - // The number of Image surfaces that are being requested for this tile cache. - // This is only a suggestion - the tile cache will clamp this as a reasonable number - // and only promote a limited number of surfaces. - pub image_surface_count: usize, - // The number of YuvImage surfaces that are being requested for this tile cache. - // This is only a suggestion - the tile cache will clamp this as a reasonable number - // and only promote a limited number of surfaces. - pub yuv_image_surface_count: usize, -} - -/// The backing surface for this tile. -#[derive(Debug)] -pub enum TileSurface { - Texture { - /// Descriptor for the surface that this tile draws into. - descriptor: SurfaceTextureDescriptor, - }, - Color { - color: ColorF, - }, -} - -impl TileSurface { - pub fn kind(&self) -> &'static str { - match *self { - TileSurface::Color { .. } => "Color", - TileSurface::Texture { .. } => "Texture", - } - } -} - -/// Information about a cached tile. -pub struct Tile { - /// The grid position of this tile within the picture cache - pub tile_offset: TileOffset, - /// The current world rect of this tile. - pub world_tile_rect: WorldRect, - /// The current local rect of this tile. - pub local_tile_rect: PictureRect, - /// The picture space dirty rect for this tile. - pub local_dirty_rect: PictureRect, - /// The device space dirty rect for this tile. - /// TODO(gw): We have multiple dirty rects available due to the quadtree above. In future, - /// expose these as multiple dirty rects, which will help in some cases. - pub device_dirty_rect: DeviceRect, - /// World space rect that contains valid pixels region of this tile. - pub world_valid_rect: WorldRect, - /// Device space rect that contains valid pixels region of this tile. - pub device_valid_rect: DeviceRect, - /// Uniquely describes the content of this tile, in a way that can be - /// (reasonably) efficiently hashed and compared. - pub current_descriptor: TileDescriptor, - /// The content descriptor for this tile from the previous frame. - pub prev_descriptor: TileDescriptor, - /// Handle to the backing surface for this tile. - pub surface: Option<TileSurface>, - /// If true, this tile is marked valid, and the existing texture - /// cache handle can be used. Tiles are invalidated during the - /// build_dirty_regions method. - pub is_valid: bool, - /// If true, this tile intersects with the currently visible screen - /// rect, and will be drawn. - pub is_visible: bool, - /// The tile id is stable between display lists and / or frames, - /// if the tile is retained. Useful for debugging tile evictions. - pub id: TileId, - /// If true, the tile was determined to be opaque, which means blending - /// can be disabled when drawing it. - pub is_opaque: bool, - /// Root node of the quadtree dirty rect tracker. - pub root: TileNode, - /// The last rendered background color on this tile. - background_color: Option<ColorF>, - /// The first reason the tile was invalidated this frame. - invalidation_reason: Option<InvalidationReason>, - /// The local space valid rect for all primitives that affect this tile. - pub local_valid_rect: PictureBox2D, - /// z-buffer id for this tile - pub z_id: ZBufferId, - pub sub_graphs: Vec<(PictureRect, Vec<(PictureCompositeMode, SurfaceIndex)>)>, -} - -impl Tile { - /// Construct a new, invalid tile. - fn new(tile_offset: TileOffset) -> Self { - let id = TileId(crate::tile_cache::next_tile_id()); - - Tile { - tile_offset, - local_tile_rect: PictureRect::zero(), - world_tile_rect: WorldRect::zero(), - world_valid_rect: WorldRect::zero(), - device_valid_rect: DeviceRect::zero(), - local_dirty_rect: PictureRect::zero(), - device_dirty_rect: DeviceRect::zero(), - surface: None, - current_descriptor: TileDescriptor::new(), - prev_descriptor: TileDescriptor::new(), - is_valid: false, - is_visible: false, - id, - is_opaque: false, - root: TileNode::new_leaf(Vec::new()), - background_color: None, - invalidation_reason: None, - local_valid_rect: PictureBox2D::zero(), - z_id: ZBufferId::invalid(), - sub_graphs: Vec::new(), - } - } - - /// Print debug information about this tile to a tree printer. - fn print(&self, pt: &mut dyn PrintTreePrinter) { - pt.new_level(format!("Tile {:?}", self.id)); - pt.add_item(format!("local_tile_rect: {:?}", self.local_tile_rect)); - pt.add_item(format!("background_color: {:?}", self.background_color)); - pt.add_item(format!("invalidation_reason: {:?}", self.invalidation_reason)); - self.current_descriptor.print(pt); - pt.end_level(); - } - - /// Check if the content of the previous and current tile descriptors match - fn update_dirty_rects( - &mut self, - ctx: &TileUpdateDirtyContext, - state: &mut TileUpdateDirtyState, - invalidation_reason: &mut Option<InvalidationReason>, - frame_context: &FrameVisibilityContext, - ) -> PictureRect { - let mut prim_comparer = PrimitiveComparer::new( - &self.prev_descriptor, - &self.current_descriptor, - state.resource_cache, - state.spatial_node_comparer, - ctx.opacity_bindings, - ctx.color_bindings, - ); - - let mut dirty_rect = PictureBox2D::zero(); - self.root.update_dirty_rects( - &self.prev_descriptor.prims, - &self.current_descriptor.prims, - &mut prim_comparer, - &mut dirty_rect, - state.compare_cache, - invalidation_reason, - frame_context, - ); - - dirty_rect - } - - /// Invalidate a tile based on change in content. This - /// must be called even if the tile is not currently - /// visible on screen. We might be able to improve this - /// later by changing how ComparableVec is used. - fn update_content_validity( - &mut self, - ctx: &TileUpdateDirtyContext, - state: &mut TileUpdateDirtyState, - frame_context: &FrameVisibilityContext, - ) { - // Check if the contents of the primitives, clips, and - // other dependencies are the same. - state.compare_cache.clear(); - let mut invalidation_reason = None; - let dirty_rect = self.update_dirty_rects( - ctx, - state, - &mut invalidation_reason, - frame_context, - ); - if !dirty_rect.is_empty() { - self.invalidate( - Some(dirty_rect), - invalidation_reason.expect("bug: no invalidation_reason"), - ); - } - if ctx.invalidate_all { - self.invalidate(None, InvalidationReason::ScaleChanged); - } - // TODO(gw): We can avoid invalidating the whole tile in some cases here, - // but it should be a fairly rare invalidation case. - if self.current_descriptor.local_valid_rect != self.prev_descriptor.local_valid_rect { - self.invalidate(None, InvalidationReason::ValidRectChanged); - state.composite_state.dirty_rects_are_valid = false; - } - } - - /// Invalidate this tile. If `invalidation_rect` is None, the entire - /// tile is invalidated. - pub fn invalidate( - &mut self, - invalidation_rect: Option<PictureRect>, - reason: InvalidationReason, - ) { - self.is_valid = false; - - match invalidation_rect { - Some(rect) => { - self.local_dirty_rect = self.local_dirty_rect.union(&rect); - } - None => { - self.local_dirty_rect = self.local_tile_rect; - } - } - - if self.invalidation_reason.is_none() { - self.invalidation_reason = Some(reason); - } - } - - /// Called during pre_update of a tile cache instance. Allows the - /// tile to setup state before primitive dependency calculations. - fn pre_update( - &mut self, - ctx: &TilePreUpdateContext, - ) { - self.local_tile_rect = PictureRect::new( - PicturePoint::new( - self.tile_offset.x as f32 * ctx.tile_size.width, - self.tile_offset.y as f32 * ctx.tile_size.height, - ), - PicturePoint::new( - (self.tile_offset.x + 1) as f32 * ctx.tile_size.width, - (self.tile_offset.y + 1) as f32 * ctx.tile_size.height, - ), - ); - // TODO(gw): This is a hack / fix for Box2D::union in euclid not working with - // zero sized rect accumulation. Once that lands, we'll revert this - // to be zero. - self.local_valid_rect = PictureBox2D::new( - PicturePoint::new( 1.0e32, 1.0e32), - PicturePoint::new(-1.0e32, -1.0e32), - ); - self.invalidation_reason = None; - self.sub_graphs.clear(); - - self.world_tile_rect = ctx.pic_to_world_mapper - .map(&self.local_tile_rect) - .expect("bug: map local tile rect"); - - // Check if this tile is currently on screen. - self.is_visible = self.world_tile_rect.intersects(&ctx.global_screen_world_rect); - - // If the tile isn't visible, early exit, skipping the normal set up to - // validate dependencies. Instead, we will only compare the current tile - // dependencies the next time it comes into view. - if !self.is_visible { - return; - } - - if ctx.background_color != self.background_color { - self.invalidate(None, InvalidationReason::BackgroundColor); - self.background_color = ctx.background_color; - } - - // Clear any dependencies so that when we rebuild them we - // can compare if the tile has the same content. - mem::swap( - &mut self.current_descriptor, - &mut self.prev_descriptor, - ); - self.current_descriptor.clear(); - self.root.clear(self.local_tile_rect); - - // Since this tile is determined to be visible, it will get updated - // dependencies, so update the frame id we are storing dependencies for. - self.current_descriptor.last_updated_frame_id = ctx.frame_id; - } - - /// Add dependencies for a given primitive to this tile. - fn add_prim_dependency( - &mut self, - info: &PrimitiveDependencyInfo, - ) { - // If this tile isn't currently visible, we don't want to update the dependencies - // for this tile, as an optimization, since it won't be drawn anyway. - if !self.is_visible { - return; - } - - // Incorporate the bounding rect of the primitive in the local valid rect - // for this tile. This is used to minimize the size of the scissor rect - // during rasterization and the draw rect during composition of partial tiles. - self.local_valid_rect = self.local_valid_rect.union(&info.prim_clip_box); - - // TODO(gw): The prim_clip_rect can be impacted by the clip rect of the display port, - // which can cause invalidations when a new display list with changed - // display port is received. To work around this, clamp the prim clip rect - // to the tile boundaries - if the clip hasn't affected the tile, then the - // changed clip can't affect the content of the primitive on this tile. - // In future, we could consider supplying the display port clip from Gecko - // in a different way (e.g. as a scroll frame clip) which still provides - // the desired clip for checkerboarding, but doesn't require this extra - // work below. - - // TODO(gw): This is a hot part of the code - we could probably optimize further by: - // - Using min/max instead of clamps below (if we guarantee the rects are well formed) - - let tile_p0 = self.local_tile_rect.min; - let tile_p1 = self.local_tile_rect.max; - - let prim_clip_box = PictureBox2D::new( - PicturePoint::new( - clampf(info.prim_clip_box.min.x, tile_p0.x, tile_p1.x), - clampf(info.prim_clip_box.min.y, tile_p0.y, tile_p1.y), - ), - PicturePoint::new( - clampf(info.prim_clip_box.max.x, tile_p0.x, tile_p1.x), - clampf(info.prim_clip_box.max.y, tile_p0.y, tile_p1.y), - ), - ); - - // Update the tile descriptor, used for tile comparison during scene swaps. - let prim_index = PrimitiveDependencyIndex(self.current_descriptor.prims.len() as u32); - - // Encode the deps for this primitive in the `dep_data` byte buffer - let dep_offset = self.current_descriptor.dep_data.len() as u32; - let mut dep_count = 0; - - for clip in &info.clips { - dep_count += 1; - poke_into_vec( - &PrimitiveDependency::Clip { - clip: *clip, - }, - &mut self.current_descriptor.dep_data, - ); - } - - for spatial_node_index in &info.spatial_nodes { - dep_count += 1; - poke_into_vec( - &PrimitiveDependency::SpatialNode { - index: *spatial_node_index, - }, - &mut self.current_descriptor.dep_data, - ); - } - - for image in &info.images { - dep_count += 1; - poke_into_vec( - &PrimitiveDependency::Image { - image: *image, - }, - &mut self.current_descriptor.dep_data, - ); - } - - for binding in &info.opacity_bindings { - dep_count += 1; - poke_into_vec( - &PrimitiveDependency::OpacityBinding { - binding: *binding, - }, - &mut self.current_descriptor.dep_data, - ); - } - - if let Some(ref binding) = info.color_binding { - dep_count += 1; - poke_into_vec( - &PrimitiveDependency::ColorBinding { - binding: *binding, - }, - &mut self.current_descriptor.dep_data, - ); - } - - self.current_descriptor.prims.push(PrimitiveDescriptor { - prim_uid: info.prim_uid, - prim_clip_box, - dep_offset, - dep_count, - }); - - // Add this primitive to the dirty rect quadtree. - self.root.add_prim(prim_index, &info.prim_clip_box); - } - - /// Called during tile cache instance post_update. Allows invalidation and dirty - /// rect calculation after primitive dependencies have been updated. - fn update_dirty_and_valid_rects( - &mut self, - ctx: &TileUpdateDirtyContext, - state: &mut TileUpdateDirtyState, - frame_context: &FrameVisibilityContext, - ) { - // Ensure peek-poke constraint is met, that `dep_data` is large enough - ensure_red_zone::<PrimitiveDependency>(&mut self.current_descriptor.dep_data); - - // Register the frame id of this tile with the spatial node comparer, to ensure - // that it doesn't GC any spatial nodes from the comparer that are referenced - // by this tile. Must be done before we early exit below, so that we retain - // spatial node info even for tiles that are currently not visible. - state.spatial_node_comparer.retain_for_frame(self.current_descriptor.last_updated_frame_id); - - // If tile is not visible, just early out from here - we don't update dependencies - // so don't want to invalidate, merge, split etc. The tile won't need to be drawn - // (and thus updated / invalidated) until it is on screen again. - if !self.is_visible { - return; - } - - // Calculate the overall valid rect for this tile. - self.current_descriptor.local_valid_rect = self.local_valid_rect; - - // TODO(gw): In theory, the local tile rect should always have an - // intersection with the overall picture rect. In practice, - // due to some accuracy issues with how fract_offset (and - // fp accuracy) are used in the calling method, this isn't - // always true. In this case, it's safe to set the local - // valid rect to zero, which means it will be clipped out - // and not affect the scene. In future, we should fix the - // accuracy issue above, so that this assumption holds, but - // it shouldn't have any noticeable effect on performance - // or memory usage (textures should never get allocated). - self.current_descriptor.local_valid_rect = self.local_tile_rect - .intersection(&ctx.local_rect) - .and_then(|r| r.intersection(&self.current_descriptor.local_valid_rect)) - .unwrap_or_else(PictureRect::zero); - - // The device_valid_rect is referenced during `update_content_validity` so it - // must be updated here first. - self.world_valid_rect = ctx.pic_to_world_mapper - .map(&self.current_descriptor.local_valid_rect) - .expect("bug: map local valid rect"); - - // The device rect is guaranteed to be aligned on a device pixel - the round - // is just to deal with float accuracy. However, the valid rect is not - // always aligned to a device pixel. To handle this, round out to get all - // required pixels, and intersect with the tile device rect. - let device_rect = (self.world_tile_rect * ctx.global_device_pixel_scale).round(); - self.device_valid_rect = (self.world_valid_rect * ctx.global_device_pixel_scale) - .round_out() - .intersection(&device_rect) - .unwrap_or_else(DeviceRect::zero); - - // Invalidate the tile based on the content changing. - self.update_content_validity(ctx, state, frame_context); - } - - /// Called during tile cache instance post_update. Allows invalidation and dirty - /// rect calculation after primitive dependencies have been updated. - fn post_update( - &mut self, - ctx: &TilePostUpdateContext, - state: &mut TilePostUpdateState, - frame_context: &FrameVisibilityContext, - ) { - // If tile is not visible, just early out from here - we don't update dependencies - // so don't want to invalidate, merge, split etc. The tile won't need to be drawn - // (and thus updated / invalidated) until it is on screen again. - if !self.is_visible { - return; - } - - // If there are no primitives there is no need to draw or cache it. - // Bug 1719232 - The final device valid rect does not always describe a non-empty - // region. Cull the tile as a workaround. - if self.current_descriptor.prims.is_empty() || self.device_valid_rect.is_empty() { - // If there is a native compositor surface allocated for this (now empty) tile - // it must be freed here, otherwise the stale tile with previous contents will - // be composited. If the tile subsequently gets new primitives added to it, the - // surface will be re-allocated when it's added to the composite draw list. - if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { mut id, .. }, .. }) = self.surface.take() { - if let Some(id) = id.take() { - state.resource_cache.destroy_compositor_tile(id); - } - } - - self.is_visible = false; - return; - } - - // Check if this tile can be considered opaque. Opacity state must be updated only - // after all early out checks have been performed. Otherwise, we might miss updating - // the native surface next time this tile becomes visible. - let clipped_rect = self.current_descriptor.local_valid_rect - .intersection(&ctx.local_clip_rect) - .unwrap_or_else(PictureRect::zero); - - let has_opaque_bg_color = self.background_color.map_or(false, |c| c.a >= 1.0); - let has_opaque_backdrop = ctx.backdrop.map_or(false, |b| b.opaque_rect.contains_box(&clipped_rect)); - let mut is_opaque = has_opaque_bg_color || has_opaque_backdrop; - - // If this tile intersects with any underlay surfaces, we need to consider it - // translucent, since it will contain an alpha cutout - for underlay in ctx.underlays { - if clipped_rect.intersects(&underlay.local_rect) { - is_opaque = false; - break; - } - } - - // Set the correct z_id for this tile - self.z_id = ctx.z_id; - - if is_opaque != self.is_opaque { - // If opacity changed, the native compositor surface and all tiles get invalidated. - // (this does nothing if not using native compositor mode). - // TODO(gw): This property probably changes very rarely, so it is OK to invalidate - // everything in this case. If it turns out that this isn't true, we could - // consider other options, such as per-tile opacity (natively supported - // on CoreAnimation, and supported if backed by non-virtual surfaces in - // DirectComposition). - if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = self.surface { - if let Some(id) = id.take() { - state.resource_cache.destroy_compositor_tile(id); - } - } - - // Invalidate the entire tile to force a redraw. - self.invalidate(None, InvalidationReason::SurfaceOpacityChanged); - self.is_opaque = is_opaque; - } - - // Check if the selected composite mode supports dirty rect updates. For Draw composite - // mode, we can always update the content with smaller dirty rects, unless there is a - // driver bug to workaround. For native composite mode, we can only use dirty rects if - // the compositor supports partial surface updates. - let (supports_dirty_rects, supports_simple_prims) = match state.composite_state.compositor_kind { - CompositorKind::Draw { .. } | CompositorKind::Layer { .. } => { - (frame_context.config.gpu_supports_render_target_partial_update, true) - } - CompositorKind::Native { capabilities, .. } => { - (capabilities.max_update_rects > 0, false) - } - }; - - // TODO(gw): Consider using smaller tiles and/or tile splits for - // native compositors that don't support dirty rects. - if supports_dirty_rects { - // Only allow splitting for normal content sized tiles - if ctx.current_tile_size == state.resource_cache.picture_textures.default_tile_size() { - let max_split_level = 3; - - // Consider splitting / merging dirty regions - self.root.maybe_merge_or_split( - 0, - &self.current_descriptor.prims, - max_split_level, - ); - } - } - - // The dirty rect will be set correctly by now. If the underlying platform - // doesn't support partial updates, and this tile isn't valid, force the dirty - // rect to be the size of the entire tile. - if !self.is_valid && !supports_dirty_rects { - self.local_dirty_rect = self.local_tile_rect; - } - - // See if this tile is a simple color, in which case we can just draw - // it as a rect, and avoid allocating a texture surface and drawing it. - // TODO(gw): Initial native compositor interface doesn't support simple - // color tiles. We can definitely support this in DC, so this - // should be added as a follow up. - let is_simple_prim = - ctx.backdrop.map_or(false, |b| b.kind.is_some()) && - self.current_descriptor.prims.len() == 1 && - self.is_opaque && - supports_simple_prims; - - // Set up the backing surface for this tile. - let surface = if is_simple_prim { - // If we determine the tile can be represented by a color, set the - // surface unconditionally (this will drop any previously used - // texture cache backing surface). - match ctx.backdrop.unwrap().kind { - Some(BackdropKind::Color { color }) => { - TileSurface::Color { - color, - } - } - None => { - // This should be prevented by the is_simple_prim check above. - unreachable!(); - } - } - } else { - // If this tile will be backed by a surface, we want to retain - // the texture handle from the previous frame, if possible. If - // the tile was previously a color, or not set, then just set - // up a new texture cache handle. - match self.surface.take() { - Some(TileSurface::Texture { descriptor }) => { - // Reuse the existing descriptor and vis mask - TileSurface::Texture { - descriptor, - } - } - Some(TileSurface::Color { .. }) | None => { - // This is the case where we are constructing a tile surface that - // involves drawing to a texture. Create the correct surface - // descriptor depending on the compositing mode that will read - // the output. - let descriptor = match state.composite_state.compositor_kind { - CompositorKind::Draw { .. } | CompositorKind::Layer { .. } => { - // For a texture cache entry, create an invalid handle that - // will be allocated when update_picture_cache is called. - SurfaceTextureDescriptor::TextureCache { - handle: None, - } - } - CompositorKind::Native { .. } => { - // Create a native surface surface descriptor, but don't allocate - // a surface yet. The surface is allocated *after* occlusion - // culling occurs, so that only visible tiles allocate GPU memory. - SurfaceTextureDescriptor::Native { - id: None, - } - } - }; - - TileSurface::Texture { - descriptor, - } - } - } - }; - - // Store the current surface backing info for use during batching. - self.surface = Some(surface); - } -} - -// TODO(gw): Tidy this up by: -// - Add an Other variant for things like opaque gradient backdrops -#[derive(Debug, Copy, Clone)] -pub enum BackdropKind { - Color { - color: ColorF, - }, -} - -/// Stores information about the calculated opaque backdrop of this slice. -#[derive(Debug, Copy, Clone)] -pub struct BackdropInfo { - /// The picture space rectangle that is known to be opaque. This is used - /// to determine where subpixel AA can be used, and where alpha blending - /// can be disabled. - pub opaque_rect: PictureRect, - /// If the backdrop covers the entire slice with an opaque color, this - /// will be set and can be used as a clear color for the slice's tiles. - pub spanning_opaque_color: Option<ColorF>, - /// Kind of the backdrop - pub kind: Option<BackdropKind>, - /// The picture space rectangle of the backdrop, if kind is set. - pub backdrop_rect: PictureRect, -} - -impl BackdropInfo { - fn empty() -> Self { - BackdropInfo { - opaque_rect: PictureRect::zero(), - spanning_opaque_color: None, - kind: None, - backdrop_rect: PictureRect::zero(), - } - } -} - -/// Represents the native surfaces created for a picture cache, if using -/// a native compositor. An opaque and alpha surface is always created, -/// but tiles are added to a surface based on current opacity. If the -/// calculated opacity of a tile changes, the tile is invalidated and -/// attached to a different native surface. This means that we don't -/// need to invalidate the entire surface if only some tiles are changing -/// opacity. It also means we can take advantage of opaque tiles on cache -/// slices where only some of the tiles are opaque. There is an assumption -/// that creating a native surface is cheap, and only when a tile is added -/// to a surface is there a significant cost. This assumption holds true -/// for the current native compositor implementations on Windows and Mac. -pub struct NativeSurface { - /// Native surface for opaque tiles - pub opaque: NativeSurfaceId, - /// Native surface for alpha tiles - pub alpha: NativeSurfaceId, -} - -/// Hash key for an external native compositor surface -#[derive(PartialEq, Eq, Hash)] -pub struct ExternalNativeSurfaceKey { - /// The YUV/RGB image keys that are used to draw this surface. - pub image_keys: [ImageKey; 3], - /// If this is not an 'external' compositor surface created via - /// Compositor::create_external_surface, this is set to the - /// current device size of the surface. - pub size: Option<DeviceIntSize>, -} - -/// Information about a native compositor surface cached between frames. -pub struct ExternalNativeSurface { - /// If true, the surface was used this frame. Used for a simple form - /// of GC to remove old surfaces. - pub used_this_frame: bool, - /// The native compositor surface handle - pub native_surface_id: NativeSurfaceId, - /// List of image keys, and current image generations, that are drawn in this surface. - /// The image generations are used to check if the compositor surface is dirty and - /// needs to be updated. - pub image_dependencies: [ImageDependency; 3], -} - -/// Wrapper struct around an external surface descriptor with a little more information -/// that the picture caching code needs. -pub struct CompositorSurface { - // External surface descriptor used by compositing logic - pub descriptor: ExternalSurfaceDescriptor, - // The compositor surface rect + any intersecting prims. Later prims that intersect - // with this must be added to the next sub-slice. - prohibited_rect: PictureRect, - // If the compositor surface content is opaque. - pub is_opaque: bool, -} - -pub struct BackdropSurface { - pub id: NativeSurfaceId, - pub color: ColorF, - pub device_rect: DeviceRect, -} - -/// In some cases, we need to know the dirty rect of all tiles in order -/// to correctly invalidate a primitive. -#[derive(Debug)] -pub struct DeferredDirtyTest { - /// The tile rect that the primitive being checked affects - pub tile_rect: TileRect, - /// The picture-cache local rect of the primitive being checked - pub prim_rect: PictureRect, -} - -/// Represents a cache of tiles that make up a picture primitives. -pub struct TileCacheInstance { - // The current debug flags for the system. - pub debug_flags: DebugFlags, - /// Index of the tile cache / slice for this frame builder. It's determined - /// by the setup_picture_caching method during flattening, which splits the - /// picture tree into multiple slices. It's used as a simple input to the tile - /// keys. It does mean we invalidate tiles if a new layer gets inserted / removed - /// between display lists - this seems very unlikely to occur on most pages, but - /// can be revisited if we ever notice that. - pub slice: usize, - /// Propagated information about the slice - pub slice_flags: SliceFlags, - /// The currently selected tile size to use for this cache - pub current_tile_size: DeviceIntSize, - /// The list of sub-slices in this tile cache - pub sub_slices: Vec<SubSlice>, - /// The positioning node for this tile cache. - pub spatial_node_index: SpatialNodeIndex, - /// The coordinate space to do visibility/clipping/invalidation in. - pub visibility_node_index: SpatialNodeIndex, - /// List of opacity bindings, with some extra information - /// about whether they changed since last frame. - opacity_bindings: FastHashMap<PropertyBindingId, OpacityBindingInfo>, - /// Switch back and forth between old and new bindings hashmaps to avoid re-allocating. - old_opacity_bindings: FastHashMap<PropertyBindingId, OpacityBindingInfo>, - /// A helper to compare transforms between previous and current frame. - spatial_node_comparer: SpatialNodeComparer, - /// List of color bindings, with some extra information - /// about whether they changed since last frame. - color_bindings: FastHashMap<PropertyBindingId, ColorBindingInfo>, - /// Switch back and forth between old and new bindings hashmaps to avoid re-allocating. - old_color_bindings: FastHashMap<PropertyBindingId, ColorBindingInfo>, - /// The current dirty region tracker for this picture. - pub dirty_region: DirtyRegion, - /// Current size of tiles in picture units. - tile_size: PictureSize, - /// Tile coords of the currently allocated grid. - tile_rect: TileRect, - /// Pre-calculated versions of the tile_rect above, used to speed up the - /// calculations in get_tile_coords_for_rect. - tile_bounds_p0: TileOffset, - tile_bounds_p1: TileOffset, - /// Local rect (unclipped) of the picture this cache covers. - pub local_rect: PictureRect, - /// The local clip rect, from the shared clips of this picture. - pub local_clip_rect: PictureRect, - /// Registered clip in CompositeState for this picture cache - pub compositor_clip: Option<CompositorClipIndex>, - /// The screen rect, transformed to local picture space. - pub screen_rect_in_pic_space: PictureRect, - /// The surface index that this tile cache will be drawn into. - surface_index: SurfaceIndex, - /// The background color from the renderer. If this is set opaque, we know it's - /// fine to clear the tiles to this and allow subpixel text on the first slice. - pub background_color: Option<ColorF>, - /// Information about the calculated backdrop content of this cache. - pub backdrop: BackdropInfo, - /// The allowed subpixel mode for this surface, which depends on the detected - /// opacity of the background. - pub subpixel_mode: SubpixelMode, - // Node in the clip-tree that defines where we exclude clips from child prims - pub shared_clip_node_id: ClipNodeId, - // Clip leaf that is used to build the clip-chain for this tile cache. - pub shared_clip_leaf_id: Option<ClipLeafId>, - /// The number of frames until this cache next evaluates what tile size to use. - /// If a picture rect size is regularly changing just around a size threshold, - /// we don't want to constantly invalidate and reallocate different tile size - /// configuration each frame. - frames_until_size_eval: usize, - /// For DirectComposition, virtual surfaces don't support negative coordinates. However, - /// picture cache tile coordinates can be negative. To handle this, we apply an offset - /// to each tile in DirectComposition. We want to change this as little as possible, - /// to avoid invalidating tiles. However, if we have a picture cache tile coordinate - /// which is outside the virtual surface bounds, we must change this to allow - /// correct remapping of the coordinates passed to BeginDraw in DC. - pub virtual_offset: DeviceIntPoint, - /// keep around the hash map used as compare_cache to avoid reallocating it each - /// frame. - compare_cache: FastHashMap<PrimitiveComparisonKey, PrimitiveCompareResult>, - /// The currently considered tile size override. Used to check if we should - /// re-evaluate tile size, even if the frame timer hasn't expired. - tile_size_override: Option<DeviceIntSize>, - /// A cache of compositor surfaces that are retained between frames - pub external_native_surface_cache: FastHashMap<ExternalNativeSurfaceKey, ExternalNativeSurface>, - /// Current frame ID of this tile cache instance. Used for book-keeping / garbage collecting - frame_id: FrameId, - /// Registered transform in CompositeState for this picture cache - pub transform_index: CompositorTransformIndex, - /// Current transform mapping local picture space to compositor surface raster space - local_to_raster: ScaleOffset, - /// Current transform mapping compositor surface raster space to final device space - raster_to_device: ScaleOffset, - /// If true, we need to invalidate all tiles during `post_update` - invalidate_all_tiles: bool, - /// The current raster scale for tiles in this cache - pub current_raster_scale: f32, - /// Depth of off-screen surfaces that are currently pushed during dependency updates - current_surface_traversal_depth: usize, - /// A list of extra dirty invalidation tests that can only be checked once we - /// know the dirty rect of all tiles - deferred_dirty_tests: Vec<DeferredDirtyTest>, - /// Is there a backdrop associated with this cache - pub found_prims_after_backdrop: bool, - pub backdrop_surface: Option<BackdropSurface>, - /// List of underlay compositor surfaces that exist in this picture cache - pub underlays: Vec<ExternalSurfaceDescriptor>, - /// "Region" (actually a spanning rect) containing all overlay promoted surfaces - pub overlay_region: PictureRect, - /// The number YuvImage prims in this cache, provided in our TileCacheParams. - pub yuv_images_count: usize, - /// The remaining number of YuvImage prims we will see this frame. We prioritize - /// promoting these before promoting any Image prims. - pub yuv_images_remaining: usize, -} - -impl TileCacheInstance { - pub fn new(params: TileCacheParams) -> Self { - // Determine how many sub-slices we need. Clamp to an arbitrary limit to ensure - // we don't create a huge number of OS compositor tiles and sub-slices. - let sub_slice_count = (params.image_surface_count + params.yuv_image_surface_count).min(MAX_COMPOSITOR_SURFACES) + 1; - - let mut sub_slices = Vec::with_capacity(sub_slice_count); - for _ in 0 .. sub_slice_count { - sub_slices.push(SubSlice::new()); - } - - TileCacheInstance { - debug_flags: params.debug_flags, - slice: params.slice, - slice_flags: params.slice_flags, - spatial_node_index: params.spatial_node_index, - visibility_node_index: params.visibility_node_index, - sub_slices, - opacity_bindings: FastHashMap::default(), - old_opacity_bindings: FastHashMap::default(), - spatial_node_comparer: SpatialNodeComparer::new(), - color_bindings: FastHashMap::default(), - old_color_bindings: FastHashMap::default(), - dirty_region: DirtyRegion::new(params.visibility_node_index, params.spatial_node_index), - tile_size: PictureSize::zero(), - tile_rect: TileRect::zero(), - tile_bounds_p0: TileOffset::zero(), - tile_bounds_p1: TileOffset::zero(), - local_rect: PictureRect::zero(), - local_clip_rect: PictureRect::zero(), - compositor_clip: None, - screen_rect_in_pic_space: PictureRect::zero(), - surface_index: SurfaceIndex(0), - background_color: params.background_color, - backdrop: BackdropInfo::empty(), - subpixel_mode: SubpixelMode::Allow, - shared_clip_node_id: params.shared_clip_node_id, - shared_clip_leaf_id: params.shared_clip_leaf_id, - current_tile_size: DeviceIntSize::zero(), - frames_until_size_eval: 0, - // Default to centering the virtual offset in the middle of the DC virtual surface - virtual_offset: DeviceIntPoint::new( - params.virtual_surface_size / 2, - params.virtual_surface_size / 2, - ), - compare_cache: FastHashMap::default(), - tile_size_override: None, - external_native_surface_cache: FastHashMap::default(), - frame_id: FrameId::INVALID, - transform_index: CompositorTransformIndex::INVALID, - raster_to_device: ScaleOffset::identity(), - local_to_raster: ScaleOffset::identity(), - invalidate_all_tiles: true, - current_raster_scale: 1.0, - current_surface_traversal_depth: 0, - deferred_dirty_tests: Vec::new(), - found_prims_after_backdrop: false, - backdrop_surface: None, - underlays: Vec::new(), - overlay_region: PictureRect::zero(), - yuv_images_count: params.yuv_image_surface_count, - yuv_images_remaining: 0, - } - } - - /// Return the total number of tiles allocated by this tile cache - pub fn tile_count(&self) -> usize { - self.tile_rect.area() as usize * self.sub_slices.len() - } - - /// Trims memory held by the tile cache, such as native surfaces. - pub fn memory_pressure(&mut self, resource_cache: &mut ResourceCache) { - for sub_slice in &mut self.sub_slices { - for tile in sub_slice.tiles.values_mut() { - if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = tile.surface { - // Reseting the id to None with take() ensures that a new - // tile will be allocated during the next frame build. - if let Some(id) = id.take() { - resource_cache.destroy_compositor_tile(id); - } - } - } - if let Some(native_surface) = sub_slice.native_surface.take() { - resource_cache.destroy_compositor_surface(native_surface.opaque); - resource_cache.destroy_compositor_surface(native_surface.alpha); - } - } - } - - /// Reset this tile cache with the updated parameters from a new scene - /// that has arrived. This allows the tile cache to be retained across - /// new scenes. - pub fn prepare_for_new_scene( - &mut self, - params: TileCacheParams, - resource_cache: &mut ResourceCache, - ) { - // We should only receive updated state for matching slice key - assert_eq!(self.slice, params.slice); - - // Determine how many sub-slices we need, based on how many compositor surface prims are - // in the supplied primitive list. - let required_sub_slice_count = (params.image_surface_count + params.yuv_image_surface_count).min(MAX_COMPOSITOR_SURFACES) + 1; - - if self.sub_slices.len() != required_sub_slice_count { - self.tile_rect = TileRect::zero(); - - if self.sub_slices.len() > required_sub_slice_count { - let old_sub_slices = self.sub_slices.split_off(required_sub_slice_count); - - for mut sub_slice in old_sub_slices { - for tile in sub_slice.tiles.values_mut() { - if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = tile.surface { - if let Some(id) = id.take() { - resource_cache.destroy_compositor_tile(id); - } - } - } - - if let Some(native_surface) = sub_slice.native_surface { - resource_cache.destroy_compositor_surface(native_surface.opaque); - resource_cache.destroy_compositor_surface(native_surface.alpha); - } - } - } else { - while self.sub_slices.len() < required_sub_slice_count { - self.sub_slices.push(SubSlice::new()); - } - } - } - - // Store the parameters from the scene builder for this slice. Other - // params in the tile cache are retained and reused, or are always - // updated during pre/post_update. - self.slice_flags = params.slice_flags; - self.spatial_node_index = params.spatial_node_index; - self.background_color = params.background_color; - self.shared_clip_leaf_id = params.shared_clip_leaf_id; - self.shared_clip_node_id = params.shared_clip_node_id; - - // Since the slice flags may have changed, ensure we re-evaluate the - // appropriate tile size for this cache next update. - self.frames_until_size_eval = 0; - - // Update the number of YuvImage prims we have in the scene. - self.yuv_images_count = params.yuv_image_surface_count; - } - - /// Destroy any manually managed resources before this picture cache is - /// destroyed, such as native compositor surfaces. - pub fn destroy( - self, - resource_cache: &mut ResourceCache, - ) { - for sub_slice in self.sub_slices { - if let Some(native_surface) = sub_slice.native_surface { - resource_cache.destroy_compositor_surface(native_surface.opaque); - resource_cache.destroy_compositor_surface(native_surface.alpha); - } - } - - for (_, external_surface) in self.external_native_surface_cache { - resource_cache.destroy_compositor_surface(external_surface.native_surface_id) - } - - if let Some(backdrop_surface) = &self.backdrop_surface { - resource_cache.destroy_compositor_surface(backdrop_surface.id); - } - } - - /// Get the tile coordinates for a given rectangle. - fn get_tile_coords_for_rect( - &self, - rect: &PictureRect, - ) -> (TileOffset, TileOffset) { - // Get the tile coordinates in the picture space. - let mut p0 = TileOffset::new( - (rect.min.x / self.tile_size.width).floor() as i32, - (rect.min.y / self.tile_size.height).floor() as i32, - ); - - let mut p1 = TileOffset::new( - (rect.max.x / self.tile_size.width).ceil() as i32, - (rect.max.y / self.tile_size.height).ceil() as i32, - ); - - // Clamp the tile coordinates here to avoid looping over irrelevant tiles later on. - p0.x = clamp(p0.x, self.tile_bounds_p0.x, self.tile_bounds_p1.x); - p0.y = clamp(p0.y, self.tile_bounds_p0.y, self.tile_bounds_p1.y); - p1.x = clamp(p1.x, self.tile_bounds_p0.x, self.tile_bounds_p1.x); - p1.y = clamp(p1.y, self.tile_bounds_p0.y, self.tile_bounds_p1.y); - - (p0, p1) - } - - /// Update transforms, opacity, color bindings and tile rects. - pub fn pre_update( - &mut self, - surface_index: SurfaceIndex, - frame_context: &FrameVisibilityContext, - frame_state: &mut FrameVisibilityState, - ) -> WorldRect { - let surface = &frame_state.surfaces[surface_index.0]; - let pic_rect = surface.unclipped_local_rect; - - self.surface_index = surface_index; - self.local_rect = pic_rect; - self.local_clip_rect = PictureRect::max_rect(); - self.deferred_dirty_tests.clear(); - self.underlays.clear(); - self.overlay_region = PictureRect::zero(); - self.yuv_images_remaining = self.yuv_images_count; - - for sub_slice in &mut self.sub_slices { - sub_slice.reset(); - } - - // Reset the opaque rect + subpixel mode, as they are calculated - // during the prim dependency checks. - self.backdrop = BackdropInfo::empty(); - - // Calculate the screen rect in picture space, for later comparison against - // backdrops, and prims potentially covering backdrops. - let pic_to_world_mapper = SpaceMapper::new_with_target( - frame_context.root_spatial_node_index, - self.spatial_node_index, - frame_context.global_screen_world_rect, - frame_context.spatial_tree, - ); - self.screen_rect_in_pic_space = pic_to_world_mapper - .unmap(&frame_context.global_screen_world_rect) - .expect("unable to unmap screen rect"); - - let pic_to_vis_mapper = SpaceMapper::new_with_target( - // TODO: use the raster node instead of the root node. - frame_context.root_spatial_node_index, - self.spatial_node_index, - surface.culling_rect, - frame_context.spatial_tree, - ); - - // If there is a valid set of shared clips, build a clip chain instance for this, - // which will provide a local clip rect. This is useful for establishing things - // like whether the backdrop rect supplied by Gecko can be considered opaque. - if let Some(shared_clip_leaf_id) = self.shared_clip_leaf_id { - let map_local_to_picture = SpaceMapper::new( - self.spatial_node_index, - pic_rect, - ); - - frame_state.clip_store.set_active_clips( - self.spatial_node_index, - map_local_to_picture.ref_spatial_node_index, - surface.visibility_spatial_node_index, - shared_clip_leaf_id, - frame_context.spatial_tree, - &mut frame_state.data_stores.clip, - &frame_state.clip_tree, - ); - - let clip_chain_instance = frame_state.clip_store.build_clip_chain_instance( - pic_rect.cast_unit(), - &map_local_to_picture, - &pic_to_vis_mapper, - frame_context.spatial_tree, - &mut frame_state.frame_gpu_data.f32, - frame_state.resource_cache, - frame_context.global_device_pixel_scale, - &surface.culling_rect, - &mut frame_state.data_stores.clip, - frame_state.rg_builder, - true, - ); - - // Ensure that if the entire picture cache is clipped out, the local - // clip rect is zero. This makes sure we don't register any occluders - // that are actually off-screen. - self.local_clip_rect = PictureRect::zero(); - self.compositor_clip = None; - - if let Some(clip_chain) = clip_chain_instance { - self.local_clip_rect = clip_chain.pic_coverage_rect; - self.compositor_clip = None; - - if clip_chain.needs_mask { - for i in 0 .. clip_chain.clips_range.count { - let clip_instance = frame_state - .clip_store - .get_instance_from_range(&clip_chain.clips_range, i); - let clip_node = &frame_state.data_stores.clip[clip_instance.handle]; - - match clip_node.item.kind { - ClipItemKind::RoundedRectangle { rect, radius, mode } => { - assert_eq!(mode, ClipMode::Clip); - - // Map the clip in to device space. We know from the shared - // clip creation logic it's in root coord system, so only a - // 2d axis-aligned transform can apply. For example, in the - // case of a pinch-zoom effect. - let map = ClipSpaceConversion::new( - frame_context.root_spatial_node_index, - clip_node.item.spatial_node_index, - frame_context.root_spatial_node_index, - frame_context.spatial_tree, - ); - - let (rect, radius) = match map { - ClipSpaceConversion::Local => { - (rect.cast_unit(), radius) - } - ClipSpaceConversion::ScaleOffset(scale_offset) => { - ( - scale_offset.map_rect(&rect), - BorderRadius { - top_left: scale_offset.map_size(&radius.top_left), - top_right: scale_offset.map_size(&radius.top_right), - bottom_left: scale_offset.map_size(&radius.bottom_left), - bottom_right: scale_offset.map_size(&radius.bottom_right), - }, - ) - } - ClipSpaceConversion::Transform(..) => { - unreachable!(); - } - }; - - self.compositor_clip = Some(frame_state.composite_state.register_clip( - rect, - radius, - )); - - break; - } - _ => { - // The logic to check for shared clips excludes other mask - // clip types (box-shadow, image-mask) and ensures that the - // clip is in the root coord system (so rect clips can't - // produce a mask). - } - } - } - } - } - } - - // Advance the current frame ID counter for this picture cache (must be done - // after any retained prev state is taken above). - self.frame_id.advance(); - - // Notify the spatial node comparer that a new frame has started, and the - // current reference spatial node for this tile cache. - self.spatial_node_comparer.next_frame(self.spatial_node_index); - - // At the start of the frame, step through each current compositor surface - // and mark it as unused. Later, this is used to free old compositor surfaces. - // TODO(gw): In future, we might make this more sophisticated - for example, - // retaining them for >1 frame if unused, or retaining them in some - // kind of pool to reduce future allocations. - for external_native_surface in self.external_native_surface_cache.values_mut() { - external_native_surface.used_this_frame = false; - } - - // Only evaluate what tile size to use fairly infrequently, so that we don't end - // up constantly invalidating and reallocating tiles if the picture rect size is - // changing near a threshold value. - if self.frames_until_size_eval == 0 || - self.tile_size_override != frame_context.config.tile_size_override { - - // Work out what size tile is appropriate for this picture cache. - let desired_tile_size = match frame_context.config.tile_size_override { - Some(tile_size_override) => { - tile_size_override - } - None => { - if self.slice_flags.contains(SliceFlags::IS_SCROLLBAR) { - if pic_rect.width() <= pic_rect.height() { - TILE_SIZE_SCROLLBAR_VERTICAL - } else { - TILE_SIZE_SCROLLBAR_HORIZONTAL - } - } else { - frame_state.resource_cache.picture_textures.default_tile_size() - } - } - }; - - // If the desired tile size has changed, then invalidate and drop any - // existing tiles. - if desired_tile_size != self.current_tile_size { - for sub_slice in &mut self.sub_slices { - // Destroy any native surfaces on the tiles that will be dropped due - // to resizing. - if let Some(native_surface) = sub_slice.native_surface.take() { - frame_state.resource_cache.destroy_compositor_surface(native_surface.opaque); - frame_state.resource_cache.destroy_compositor_surface(native_surface.alpha); - } - sub_slice.tiles.clear(); - } - self.tile_rect = TileRect::zero(); - self.current_tile_size = desired_tile_size; - } - - // Reset counter until next evaluating the desired tile size. This is an - // arbitrary value. - self.frames_until_size_eval = 120; - self.tile_size_override = frame_context.config.tile_size_override; - } - - // Get the complete scale-offset from local space to device space - let local_to_device = get_relative_scale_offset( - self.spatial_node_index, - frame_context.root_spatial_node_index, - frame_context.spatial_tree, - ); - - // Get the compositor transform, which depends on pinch-zoom mode - let mut raster_to_device = local_to_device; - - if frame_context.config.low_quality_pinch_zoom { - raster_to_device.scale.x /= self.current_raster_scale; - raster_to_device.scale.y /= self.current_raster_scale; - } else { - raster_to_device.scale.x = 1.0; - raster_to_device.scale.y = 1.0; - } - - // Use that compositor transform to calculate a relative local to surface - let local_to_raster = local_to_device.then(&raster_to_device.inverse()); - - const EPSILON: f32 = 0.001; - let compositor_translation_changed = - !raster_to_device.offset.x.approx_eq_eps(&self.raster_to_device.offset.x, &EPSILON) || - !raster_to_device.offset.y.approx_eq_eps(&self.raster_to_device.offset.y, &EPSILON); - let compositor_scale_changed = - !raster_to_device.scale.x.approx_eq_eps(&self.raster_to_device.scale.x, &EPSILON) || - !raster_to_device.scale.y.approx_eq_eps(&self.raster_to_device.scale.y, &EPSILON); - let surface_scale_changed = - !local_to_raster.scale.x.approx_eq_eps(&self.local_to_raster.scale.x, &EPSILON) || - !local_to_raster.scale.y.approx_eq_eps(&self.local_to_raster.scale.y, &EPSILON); - - if compositor_translation_changed || - compositor_scale_changed || - surface_scale_changed || - frame_context.config.force_invalidation { - frame_state.composite_state.dirty_rects_are_valid = false; - } - - self.raster_to_device = raster_to_device; - self.local_to_raster = local_to_raster; - self.invalidate_all_tiles = surface_scale_changed || frame_context.config.force_invalidation; - - // Do a hacky diff of opacity binding values from the last frame. This is - // used later on during tile invalidation tests. - let current_properties = frame_context.scene_properties.float_properties(); - mem::swap(&mut self.opacity_bindings, &mut self.old_opacity_bindings); - - self.opacity_bindings.clear(); - for (id, value) in current_properties { - let changed = match self.old_opacity_bindings.get(id) { - Some(old_property) => !old_property.value.approx_eq(value), - None => true, - }; - self.opacity_bindings.insert(*id, OpacityBindingInfo { - value: *value, - changed, - }); - } - - // Do a hacky diff of color binding values from the last frame. This is - // used later on during tile invalidation tests. - let current_properties = frame_context.scene_properties.color_properties(); - mem::swap(&mut self.color_bindings, &mut self.old_color_bindings); - - self.color_bindings.clear(); - for (id, value) in current_properties { - let changed = match self.old_color_bindings.get(id) { - Some(old_property) => old_property.value != (*value).into(), - None => true, - }; - self.color_bindings.insert(*id, ColorBindingInfo { - value: (*value).into(), - changed, - }); - } - - let world_tile_size = WorldSize::new( - self.current_tile_size.width as f32 / frame_context.global_device_pixel_scale.0, - self.current_tile_size.height as f32 / frame_context.global_device_pixel_scale.0, - ); - - self.tile_size = PictureSize::new( - world_tile_size.width / self.local_to_raster.scale.x, - world_tile_size.height / self.local_to_raster.scale.y, - ); - - // Inflate the needed rect a bit, so that we retain tiles that we have drawn - // but have just recently gone off-screen. This means that we avoid re-drawing - // tiles if the user is scrolling up and down small amounts, at the cost of - // a bit of extra texture memory. - let desired_rect_in_pic_space = self.screen_rect_in_pic_space - .inflate(0.0, 1.0 * self.tile_size.height); - - let needed_rect_in_pic_space = desired_rect_in_pic_space - .intersection(&pic_rect) - .unwrap_or_else(Box2D::zero); - - let p0 = needed_rect_in_pic_space.min; - let p1 = needed_rect_in_pic_space.max; - - let x0 = (p0.x / self.tile_size.width).floor() as i32; - let x1 = (p1.x / self.tile_size.width).ceil() as i32; - - let y0 = (p0.y / self.tile_size.height).floor() as i32; - let y1 = (p1.y / self.tile_size.height).ceil() as i32; - - let new_tile_rect = TileRect { - min: TileOffset::new(x0, y0), - max: TileOffset::new(x1, y1), - }; - - // Determine whether the current bounds of the tile grid will exceed the - // bounds of the DC virtual surface, taking into account the current - // virtual offset. If so, we need to invalidate all tiles, and set up - // a new virtual offset, centered around the current tile grid. - - let virtual_surface_size = frame_context.config.compositor_kind.get_virtual_surface_size(); - // We only need to invalidate in this case if the underlying platform - // uses virtual surfaces. - if virtual_surface_size > 0 { - // Get the extremities of the tile grid after virtual offset is applied - let tx0 = self.virtual_offset.x + x0 * self.current_tile_size.width; - let ty0 = self.virtual_offset.y + y0 * self.current_tile_size.height; - let tx1 = self.virtual_offset.x + (x1+1) * self.current_tile_size.width; - let ty1 = self.virtual_offset.y + (y1+1) * self.current_tile_size.height; - - let need_new_virtual_offset = tx0 < 0 || - ty0 < 0 || - tx1 >= virtual_surface_size || - ty1 >= virtual_surface_size; - - if need_new_virtual_offset { - // Calculate a new virtual offset, centered around the middle of the - // current tile grid. This means we won't need to invalidate and get - // a new offset for a long time! - self.virtual_offset = DeviceIntPoint::new( - (virtual_surface_size/2) - ((x0 + x1) / 2) * self.current_tile_size.width, - (virtual_surface_size/2) - ((y0 + y1) / 2) * self.current_tile_size.height, - ); - - // Invalidate all native tile surfaces. They will be re-allocated next time - // they are scheduled to be rasterized. - for sub_slice in &mut self.sub_slices { - for tile in sub_slice.tiles.values_mut() { - if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = tile.surface { - if let Some(id) = id.take() { - frame_state.resource_cache.destroy_compositor_tile(id); - tile.surface = None; - // Invalidate the entire tile to force a redraw. - // TODO(gw): Add a new invalidation reason for virtual offset changing - tile.invalidate(None, InvalidationReason::CompositorKindChanged); - } - } - } - - // Destroy the native virtual surfaces. They will be re-allocated next time a tile - // that references them is scheduled to draw. - if let Some(native_surface) = sub_slice.native_surface.take() { - frame_state.resource_cache.destroy_compositor_surface(native_surface.opaque); - frame_state.resource_cache.destroy_compositor_surface(native_surface.alpha); - } - } - } - } - - // Rebuild the tile grid if the picture cache rect has changed. - if new_tile_rect != self.tile_rect { - for sub_slice in &mut self.sub_slices { - let mut old_tiles = sub_slice.resize(new_tile_rect); - - // When old tiles that remain after the loop, dirty rects are not valid. - if !old_tiles.is_empty() { - frame_state.composite_state.dirty_rects_are_valid = false; - } - - // Any old tiles that remain after the loop above are going to be dropped. For - // simple composite mode, the texture cache handle will expire and be collected - // by the texture cache. For native compositor mode, we need to explicitly - // invoke a callback to the client to destroy that surface. - frame_state.composite_state.destroy_native_tiles( - old_tiles.values_mut(), - frame_state.resource_cache, - ); - } - } - - // This is duplicated information from tile_rect, but cached here to avoid - // redundant calculations during get_tile_coords_for_rect - self.tile_bounds_p0 = TileOffset::new(x0, y0); - self.tile_bounds_p1 = TileOffset::new(x1, y1); - self.tile_rect = new_tile_rect; - - let mut world_culling_rect = WorldRect::zero(); - - let mut ctx = TilePreUpdateContext { - pic_to_world_mapper, - background_color: self.background_color, - global_screen_world_rect: frame_context.global_screen_world_rect, - tile_size: self.tile_size, - frame_id: self.frame_id, - }; - - // Pre-update each tile - for sub_slice in &mut self.sub_slices { - for tile in sub_slice.tiles.values_mut() { - tile.pre_update(&ctx); - - // Only include the tiles that are currently in view into the world culling - // rect. This is a very important optimization for a couple of reasons: - // (1) Primitives that intersect with tiles in the grid that are not currently - // visible can be skipped from primitive preparation, clip chain building - // and tile dependency updates. - // (2) When we need to allocate an off-screen surface for a child picture (for - // example a CSS filter) we clip the size of the GPU surface to the world - // culling rect below (to ensure we draw enough of it to be sampled by any - // tiles that reference it). Making the world culling rect only affected - // by visible tiles (rather than the entire virtual tile display port) can - // result in allocating _much_ smaller GPU surfaces for cases where the - // true off-screen surface size is very large. - if tile.is_visible { - world_culling_rect = world_culling_rect.union(&tile.world_tile_rect); - } - } - - // The background color can only be applied to the first sub-slice. - ctx.background_color = None; - } - - // If compositor mode is changed, need to drop all incompatible tiles. - match frame_context.config.compositor_kind { - CompositorKind::Draw { .. } | CompositorKind::Layer { .. } => { - for sub_slice in &mut self.sub_slices { - for tile in sub_slice.tiles.values_mut() { - if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::Native { ref mut id, .. }, .. }) = tile.surface { - if let Some(id) = id.take() { - frame_state.resource_cache.destroy_compositor_tile(id); - } - tile.surface = None; - // Invalidate the entire tile to force a redraw. - tile.invalidate(None, InvalidationReason::CompositorKindChanged); - } - } - - if let Some(native_surface) = sub_slice.native_surface.take() { - frame_state.resource_cache.destroy_compositor_surface(native_surface.opaque); - frame_state.resource_cache.destroy_compositor_surface(native_surface.alpha); - } - } - - for (_, external_surface) in self.external_native_surface_cache.drain() { - frame_state.resource_cache.destroy_compositor_surface(external_surface.native_surface_id) - } - } - CompositorKind::Native { .. } => { - // This could hit even when compositor mode is not changed, - // then we need to check if there are incompatible tiles. - for sub_slice in &mut self.sub_slices { - for tile in sub_slice.tiles.values_mut() { - if let Some(TileSurface::Texture { descriptor: SurfaceTextureDescriptor::TextureCache { .. }, .. }) = tile.surface { - tile.surface = None; - // Invalidate the entire tile to force a redraw. - tile.invalidate(None, InvalidationReason::CompositorKindChanged); - } - } - } - } - } - - world_culling_rect - } - - fn can_promote_to_surface( - &mut self, - prim_clip_chain: &ClipChainInstance, - prim_spatial_node_index: SpatialNodeIndex, - is_root_tile_cache: bool, - sub_slice_index: usize, - surface_kind: CompositorSurfaceKind, - pic_coverage_rect: PictureRect, - frame_context: &FrameVisibilityContext, - force: bool, - ) -> Result<CompositorSurfaceKind, SurfacePromotionFailure> { - use SurfacePromotionFailure::*; - - // Each strategy has different restrictions on whether we can promote - match surface_kind { - CompositorSurfaceKind::Overlay => { - // For now, only support a small (arbitrary) number of compositor surfaces. - // Non-opaque compositor surfaces require sub-slices, as they are drawn - // as overlays. - if sub_slice_index == self.sub_slices.len() - 1 { - return Err(OverlaySurfaceLimit); - } - - // If a complex clip is being applied to this primitive, it can't be - // promoted directly to a compositor surface. - if prim_clip_chain.needs_mask { - return Err(OverlayNeedsMask); - } - } - CompositorSurfaceKind::Underlay => { - // If a mask is needed, there are some restrictions. - if prim_clip_chain.needs_mask { - // Need an opaque region behind this prim. The opaque region doesn't - // need to span the entire visible region of the TileCacheInstance, - // which would set self.backdrop.kind, but that also qualifies. - if !self.backdrop.opaque_rect.contains_box(&pic_coverage_rect) { - let result = Err(UnderlayAlphaBackdrop); - // If we aren't forcing, give up and return Err. - if !force { - return result; - } - - // Log this but don't return an error. - self.report_promotion_failure(result, pic_coverage_rect, true); - } - - // Only one masked underlay allowed. - if !self.underlays.is_empty() { - return Err(UnderlaySurfaceLimit); - } - } - - // Underlays can't appear on top of overlays, because they can't punch - // through the existing overlay. - if self.overlay_region.intersects(&pic_coverage_rect) { - let result = Err(UnderlayIntersectsOverlay); - // If we aren't forcing, give up and return Err. - if !force { - return result; - } - - // Log this but don't return an error. - self.report_promotion_failure(result, pic_coverage_rect, true); - } - - // Underlay cutouts are difficult to align with compositor surfaces when - // compositing during low-quality zoom, and the required invalidation - // whilst zooming would prevent low-quality zoom from working efficiently. - if frame_context.config.low_quality_pinch_zoom && - frame_context.spatial_tree.get_spatial_node(prim_spatial_node_index).is_ancestor_or_self_zooming - { - return Err(UnderlayLowQualityZoom); - } - } - CompositorSurfaceKind::Blit => unreachable!(), - } - - // If not on the root picture cache, it has some kind of - // complex effect (such as a filter, mix-blend-mode or 3d transform). - if !is_root_tile_cache { - return Err(NotRootTileCache); - } - - let mapper : SpaceMapper<PicturePixel, WorldPixel> = SpaceMapper::new_with_target( - frame_context.root_spatial_node_index, - prim_spatial_node_index, - frame_context.global_screen_world_rect, - &frame_context.spatial_tree); - let transform = mapper.get_transform(); - if !transform.is_2d_scale_translation() { - let result = Err(ComplexTransform); - // Unfortunately, ComplexTransform absolutely prevents proper - // functioning of surface promotion. Treating this as a warning - // instead of an error will cause a crash in get_relative_scale_offset. - return result; - } - - if self.slice_flags.contains(SliceFlags::IS_ATOMIC) { - return Err(SliceAtomic); - } - - Ok(surface_kind) - } - - fn setup_compositor_surfaces_yuv( - &mut self, - sub_slice_index: usize, - prim_info: &mut PrimitiveDependencyInfo, - flags: PrimitiveFlags, - local_prim_rect: LayoutRect, - prim_spatial_node_index: SpatialNodeIndex, - pic_coverage_rect: PictureRect, - frame_context: &FrameVisibilityContext, - image_dependencies: &[ImageDependency;3], - api_keys: &[ImageKey; 3], - resource_cache: &mut ResourceCache, - composite_state: &mut CompositeState, - gpu_buffer: &mut GpuBufferBuilderF, - image_rendering: ImageRendering, - color_depth: ColorDepth, - color_space: YuvRangedColorSpace, - format: YuvFormat, - surface_kind: CompositorSurfaceKind, - ) -> Result<CompositorSurfaceKind, SurfacePromotionFailure> { - for &key in api_keys { - if key != ImageKey::DUMMY { - // TODO: See comment in setup_compositor_surfaces_rgb. - resource_cache.request_image(ImageRequest { - key, - rendering: image_rendering, - tile: None, - }, - gpu_buffer, - ); - } - } - - self.setup_compositor_surfaces_impl( - sub_slice_index, - prim_info, - flags, - local_prim_rect, - prim_spatial_node_index, - pic_coverage_rect, - frame_context, - ExternalSurfaceDependency::Yuv { - image_dependencies: *image_dependencies, - color_space, - format, - channel_bit_depth: color_depth.bit_depth(), - }, - api_keys, - resource_cache, - composite_state, - image_rendering, - true, - surface_kind, - ) - } - - fn setup_compositor_surfaces_rgb( - &mut self, - sub_slice_index: usize, - prim_info: &mut PrimitiveDependencyInfo, - flags: PrimitiveFlags, - local_prim_rect: LayoutRect, - prim_spatial_node_index: SpatialNodeIndex, - pic_coverage_rect: PictureRect, - frame_context: &FrameVisibilityContext, - image_dependency: ImageDependency, - api_key: ImageKey, - resource_cache: &mut ResourceCache, - composite_state: &mut CompositeState, - gpu_buffer: &mut GpuBufferBuilderF, - image_rendering: ImageRendering, - is_opaque: bool, - surface_kind: CompositorSurfaceKind, - ) -> Result<CompositorSurfaceKind, SurfacePromotionFailure> { - let mut api_keys = [ImageKey::DUMMY; 3]; - api_keys[0] = api_key; - - // TODO: The picture compositing code requires images promoted - // into their own picture cache slices to be requested every - // frame even if they are not visible. However the image updates - // are only reached on the prepare pass for visible primitives. - // So we make sure to trigger an image request when promoting - // the image here. - resource_cache.request_image(ImageRequest { - key: api_key, - rendering: image_rendering, - tile: None, - }, - gpu_buffer, - ); - - self.setup_compositor_surfaces_impl( - sub_slice_index, - prim_info, - flags, - local_prim_rect, - prim_spatial_node_index, - pic_coverage_rect, - frame_context, - ExternalSurfaceDependency::Rgb { - image_dependency, - }, - &api_keys, - resource_cache, - composite_state, - image_rendering, - is_opaque, - surface_kind, - ) - } - - // returns false if composition is not available for this surface, - // and the non-compositor path should be used to draw it instead. - fn setup_compositor_surfaces_impl( - &mut self, - sub_slice_index: usize, - prim_info: &mut PrimitiveDependencyInfo, - flags: PrimitiveFlags, - local_prim_rect: LayoutRect, - prim_spatial_node_index: SpatialNodeIndex, - pic_coverage_rect: PictureRect, - frame_context: &FrameVisibilityContext, - dependency: ExternalSurfaceDependency, - api_keys: &[ImageKey; 3], - resource_cache: &mut ResourceCache, - composite_state: &mut CompositeState, - image_rendering: ImageRendering, - is_opaque: bool, - surface_kind: CompositorSurfaceKind, - ) -> Result<CompositorSurfaceKind, SurfacePromotionFailure> { - use SurfacePromotionFailure::*; - - let map_local_to_picture = SpaceMapper::new_with_target( - self.spatial_node_index, - prim_spatial_node_index, - self.local_rect, - frame_context.spatial_tree, - ); - - // Map the primitive local rect into picture space. - let prim_rect = match map_local_to_picture.map(&local_prim_rect) { - Some(rect) => rect, - None => return Ok(surface_kind), - }; - - // If the rect is invalid, no need to create dependencies. - if prim_rect.is_empty() { - return Ok(surface_kind); - } - - let pic_to_world_mapper = SpaceMapper::new_with_target( - frame_context.root_spatial_node_index, - self.spatial_node_index, - frame_context.global_screen_world_rect, - frame_context.spatial_tree, - ); - - let world_clip_rect = pic_to_world_mapper - .map(&prim_info.prim_clip_box) - .expect("bug: unable to map clip to world space"); - - let is_visible = world_clip_rect.intersects(&frame_context.global_screen_world_rect); - if !is_visible { - return Ok(surface_kind); - } - - let prim_offset = ScaleOffset::from_offset(local_prim_rect.min.to_vector().cast_unit()); - - let local_prim_to_device = get_relative_scale_offset( - prim_spatial_node_index, - frame_context.root_spatial_node_index, - frame_context.spatial_tree, - ); - - let normalized_prim_to_device = prim_offset.then(&local_prim_to_device); - - let local_to_raster = ScaleOffset::identity(); - let raster_to_device = normalized_prim_to_device; - - // If this primitive is an external image, and supports being used - // directly by a native compositor, then lookup the external image id - // so we can pass that through. - let mut external_image_id = if flags.contains(PrimitiveFlags::SUPPORTS_EXTERNAL_COMPOSITOR_SURFACE) - && image_rendering == ImageRendering::Auto { - resource_cache.get_image_properties(api_keys[0]) - .and_then(|properties| properties.external_image) - .and_then(|image| Some(image.id)) - } else { - None - }; - - - if let CompositorKind::Native { capabilities, .. } = composite_state.compositor_kind { - if external_image_id.is_some() && - !capabilities.supports_external_compositor_surface_negative_scaling && - (raster_to_device.scale.x < 0.0 || raster_to_device.scale.y < 0.0) { - external_image_id = None; - } - } - - let compositor_transform_index = composite_state.register_transform( - local_to_raster, - raster_to_device, - ); - - let surface_size = composite_state.get_surface_rect( - &local_prim_rect, - &local_prim_rect, - compositor_transform_index, - ).size(); - - let clip_rect = (world_clip_rect * frame_context.global_device_pixel_scale).round(); - - if surface_size.width >= MAX_COMPOSITOR_SURFACES_SIZE || - surface_size.height >= MAX_COMPOSITOR_SURFACES_SIZE { - return Err(SizeTooLarge); - } - - // When using native compositing, we need to find an existing native surface - // handle to use, or allocate a new one. For existing native surfaces, we can - // also determine whether this needs to be updated, depending on whether the - // image generation(s) of the planes have changed since last composite. - let (native_surface_id, update_params) = match composite_state.compositor_kind { - CompositorKind::Draw { .. } | CompositorKind::Layer { .. } => { - (None, None) - } - CompositorKind::Native { .. } => { - let native_surface_size = surface_size.to_i32(); - - let key = ExternalNativeSurfaceKey { - image_keys: *api_keys, - size: if external_image_id.is_some() { None } else { Some(native_surface_size) }, - }; - - let native_surface = self.external_native_surface_cache - .entry(key) - .or_insert_with(|| { - // No existing surface, so allocate a new compositor surface. - let native_surface_id = match external_image_id { - Some(_external_image) => { - // If we have a suitable external image, then create an external - // surface to attach to. - resource_cache.create_compositor_external_surface(is_opaque) - } - None => { - // Otherwise create a normal compositor surface and a single - // compositor tile that covers the entire surface. - let native_surface_id = - resource_cache.create_compositor_surface( - DeviceIntPoint::zero(), - native_surface_size, - is_opaque, - ); - - let tile_id = NativeTileId { - surface_id: native_surface_id, - x: 0, - y: 0, - }; - resource_cache.create_compositor_tile(tile_id); - - native_surface_id - } - }; - - ExternalNativeSurface { - used_this_frame: true, - native_surface_id, - image_dependencies: [ImageDependency::INVALID; 3], - } - }); - - // Mark that the surface is referenced this frame so that the - // backing native surface handle isn't freed. - native_surface.used_this_frame = true; - - let update_params = match external_image_id { - Some(external_image) => { - // If this is an external image surface, then there's no update - // to be done. Just attach the current external image to the surface - // and we're done. - resource_cache.attach_compositor_external_image( - native_surface.native_surface_id, - external_image, - ); - None - } - None => { - // If the image dependencies match, there is no need to update - // the backing native surface. - match dependency { - ExternalSurfaceDependency::Yuv{ image_dependencies, .. } => { - if image_dependencies == native_surface.image_dependencies { - None - } else { - Some(native_surface_size) - } - }, - ExternalSurfaceDependency::Rgb{ image_dependency, .. } => { - if image_dependency == native_surface.image_dependencies[0] { - None - } else { - Some(native_surface_size) - } - }, - } - } - }; - - (Some(native_surface.native_surface_id), update_params) - } - }; - - let descriptor = ExternalSurfaceDescriptor { - local_surface_size: local_prim_rect.size(), - local_rect: prim_rect, - local_clip_rect: prim_info.prim_clip_box, - dependency, - image_rendering, - clip_rect, - transform_index: compositor_transform_index, - z_id: ZBufferId::invalid(), - native_surface_id, - update_params, - external_image_id, - }; - - // If the surface is opaque, we can draw it an an underlay (which avoids - // additional sub-slice surfaces, and supports clip masks) - match surface_kind { - CompositorSurfaceKind::Underlay => { - self.underlays.push(descriptor); - } - CompositorSurfaceKind::Overlay => { - // For compositor surfaces, if we didn't find an earlier sub-slice to add to, - // we know we can append to the current slice. - assert!(sub_slice_index < self.sub_slices.len() - 1); - let sub_slice = &mut self.sub_slices[sub_slice_index]; - - // Each compositor surface allocates a unique z-id - sub_slice.compositor_surfaces.push(CompositorSurface { - prohibited_rect: pic_coverage_rect, - is_opaque, - descriptor, - }); - - // Add the pic_coverage_rect to the overlay region. This prevents - // future promoted surfaces from becoming underlays if they would - // intersect with the overlay region. - self.overlay_region = self.overlay_region.union(&pic_coverage_rect); - } - CompositorSurfaceKind::Blit => unreachable!(), - } - - Ok(surface_kind) - } - - /// Push an estimated rect for an off-screen surface during dependency updates. This is - /// a workaround / hack that allows the picture cache code to know when it should be - /// processing primitive dependencies as a single atomic unit. In future, we aim to remove - /// this hack by having the primitive dependencies stored _within_ each owning picture. - /// This is part of the work required to support child picture caching anyway! - pub fn push_surface( - &mut self, - estimated_local_rect: LayoutRect, - surface_spatial_node_index: SpatialNodeIndex, - spatial_tree: &SpatialTree, - ) { - // Only need to evaluate sub-slice regions if we have compositor surfaces present - if self.current_surface_traversal_depth == 0 && self.sub_slices.len() > 1 { - let map_local_to_picture = SpaceMapper::new_with_target( - self.spatial_node_index, - surface_spatial_node_index, - self.local_rect, - spatial_tree, - ); - - if let Some(pic_rect) = map_local_to_picture.map(&estimated_local_rect) { - // Find the first sub-slice we can add this primitive to (we want to add - // prims to the primary surface if possible, so they get subpixel AA). - for sub_slice in &mut self.sub_slices { - let mut intersects_prohibited_region = false; - - for surface in &mut sub_slice.compositor_surfaces { - if pic_rect.intersects(&surface.prohibited_rect) { - surface.prohibited_rect = surface.prohibited_rect.union(&pic_rect); - - intersects_prohibited_region = true; - } - } - - if !intersects_prohibited_region { - break; - } - } - } - } - - self.current_surface_traversal_depth += 1; - } - - /// Pop an off-screen surface off the stack during dependency updates - pub fn pop_surface(&mut self) { - self.current_surface_traversal_depth -= 1; - } - - fn report_promotion_failure(&self, - result: Result<CompositorSurfaceKind, SurfacePromotionFailure>, - rect: PictureRect, - ignored: bool) { - if !self.debug_flags.contains(DebugFlags::SURFACE_PROMOTION_LOGGING) || result.is_ok() { - return; - } - - // Report this as a warning. - // TODO: Find a way to expose this to web authors. - let outcome = if ignored { "failure ignored" } else { "failed" }; - warn!("Surface promotion of prim at {:?} {outcome} with: {}.", rect, result.unwrap_err()); - } - - /// Update the dependencies for each tile for a given primitive instance. - pub fn update_prim_dependencies( - &mut self, - prim_instance: &mut PrimitiveInstance, - prim_spatial_node_index: SpatialNodeIndex, - local_prim_rect: LayoutRect, - frame_context: &FrameVisibilityContext, - data_stores: &DataStores, - clip_store: &ClipStore, - pictures: &[PicturePrimitive], - resource_cache: &mut ResourceCache, - color_bindings: &ColorBindingStorage, - surface_stack: &[(PictureIndex, SurfaceIndex)], - composite_state: &mut CompositeState, - gpu_buffer: &mut GpuBufferBuilderF, - scratch: &mut PrimitiveScratchBuffer, - is_root_tile_cache: bool, - surfaces: &mut [SurfaceInfo], - profile: &mut TransactionProfile, - ) -> VisibilityState { - use SurfacePromotionFailure::*; - - // This primitive exists on the last element on the current surface stack. - profile_scope!("update_prim_dependencies"); - let prim_surface_index = surface_stack.last().unwrap().1; - let prim_clip_chain = &prim_instance.vis.clip_chain; - - // If the primitive is directly drawn onto this picture cache surface, then - // the pic_coverage_rect is in the same space. If not, we need to map it from - // the intermediate picture space into the picture cache space. - let on_picture_surface = prim_surface_index == self.surface_index; - let pic_coverage_rect = if on_picture_surface { - prim_clip_chain.pic_coverage_rect - } else { - // We want to get the rect in the tile cache picture space that this primitive - // occupies, in order to enable correct invalidation regions. Each surface - // that exists in the chain between this primitive and the tile cache surface - // may have an arbitrary inflation factor (for example, in the case of a series - // of nested blur elements). To account for this, step through the current - // surface stack, mapping the primitive rect into each picture space, including - // the inflation factor from each intermediate surface. - let mut current_pic_coverage_rect = prim_clip_chain.pic_coverage_rect; - let mut current_spatial_node_index = surfaces[prim_surface_index.0] - .surface_spatial_node_index; - - for (pic_index, surface_index) in surface_stack.iter().rev() { - let surface = &surfaces[surface_index.0]; - let pic = &pictures[pic_index.0]; - - let map_local_to_parent = SpaceMapper::new_with_target( - surface.surface_spatial_node_index, - current_spatial_node_index, - surface.unclipped_local_rect, - frame_context.spatial_tree, - ); - - // Map the rect into the parent surface, and inflate if this surface requires - // it. If the rect can't be mapping (e.g. due to an invalid transform) then - // just bail out from the dependencies and cull this primitive. - current_pic_coverage_rect = match map_local_to_parent.map(&current_pic_coverage_rect) { - Some(rect) => { - // TODO(gw): The casts here are a hack. We have some interface inconsistencies - // between layout/picture rects which don't really work with the - // current unit system, since sometimes the local rect of a picture - // is a LayoutRect, and sometimes it's a PictureRect. Consider how - // we can improve this? - pic.composite_mode.as_ref().unwrap().get_coverage( - surface, - Some(rect.cast_unit()), - ).cast_unit() - } - None => { - return VisibilityState::Culled; - } - }; - - current_spatial_node_index = surface.surface_spatial_node_index; - } - - current_pic_coverage_rect - }; - - // Get the tile coordinates in the picture space. - let (p0, p1) = self.get_tile_coords_for_rect(&pic_coverage_rect); - - // If the primitive is outside the tiling rects, it's known to not - // be visible. - if p0.x == p1.x || p0.y == p1.y { - return VisibilityState::Culled; - } - - // Build the list of resources that this primitive has dependencies on. - let mut prim_info = PrimitiveDependencyInfo::new( - prim_instance.uid(), - pic_coverage_rect, - ); - - let mut sub_slice_index = self.sub_slices.len() - 1; - - // Only need to evaluate sub-slice regions if we have compositor surfaces present - if sub_slice_index > 0 { - // Find the first sub-slice we can add this primitive to (we want to add - // prims to the primary surface if possible, so they get subpixel AA). - for (i, sub_slice) in self.sub_slices.iter_mut().enumerate() { - let mut intersects_prohibited_region = false; - - for surface in &mut sub_slice.compositor_surfaces { - if pic_coverage_rect.intersects(&surface.prohibited_rect) { - surface.prohibited_rect = surface.prohibited_rect.union(&pic_coverage_rect); - - intersects_prohibited_region = true; - } - } - - if !intersects_prohibited_region { - sub_slice_index = i; - break; - } - } - } - - // Include the prim spatial node, if differs relative to cache root. - if prim_spatial_node_index != self.spatial_node_index { - prim_info.spatial_nodes.push(prim_spatial_node_index); - } - - // If there was a clip chain, add any clip dependencies to the list for this tile. - let clip_instances = &clip_store - .clip_node_instances[prim_clip_chain.clips_range.to_range()]; - for clip_instance in clip_instances { - let clip = &data_stores.clip[clip_instance.handle]; - - prim_info.clips.push(clip_instance.handle.uid()); - - // If the clip has the same spatial node, the relative transform - // will always be the same, so there's no need to depend on it. - if clip.item.spatial_node_index != self.spatial_node_index - && !prim_info.spatial_nodes.contains(&clip.item.spatial_node_index) { - prim_info.spatial_nodes.push(clip.item.spatial_node_index); - } - } - - // Certain primitives may select themselves to be a backdrop candidate, which is - // then applied below. - let mut backdrop_candidate = None; - - // For pictures, we don't (yet) know the valid clip rect, so we can't correctly - // use it to calculate the local bounding rect for the tiles. If we include them - // then we may calculate a bounding rect that is too large, since it won't include - // the clip bounds of the picture. Excluding them from the bounding rect here - // fixes any correctness issues (the clips themselves are considered when we - // consider the bounds of the primitives that are *children* of the picture), - // however it does potentially result in some un-necessary invalidations of a - // tile (in cases where the picture local rect affects the tile, but the clip - // rect eventually means it doesn't affect that tile). - // TODO(gw): Get picture clips earlier (during the initial picture traversal - // pass) so that we can calculate these correctly. - match prim_instance.kind { - PrimitiveInstanceKind::Picture { pic_index,.. } => { - // Pictures can depend on animated opacity bindings. - let pic = &pictures[pic_index.0]; - if let Some(PictureCompositeMode::Filter(Filter::Opacity(binding, _))) = pic.composite_mode { - prim_info.opacity_bindings.push(binding.into()); - } - } - PrimitiveInstanceKind::Rectangle { data_handle, color_binding_index, .. } => { - // Rectangles can only form a backdrop candidate if they are known opaque. - // TODO(gw): We could resolve the opacity binding here, but the common - // case for background rects is that they don't have animated opacity. - let PrimitiveTemplateKind::Rectangle { color, .. } = data_stores.prim[data_handle].kind; - let color = frame_context.scene_properties.resolve_color(&color); - if color.a >= 1.0 { - backdrop_candidate = Some(BackdropInfo { - opaque_rect: pic_coverage_rect, - spanning_opaque_color: None, - kind: Some(BackdropKind::Color { color }), - backdrop_rect: pic_coverage_rect, - }); - } - - if color_binding_index != ColorBindingIndex::INVALID { - prim_info.color_binding = Some(color_bindings[color_binding_index].into()); - } - } - PrimitiveInstanceKind::Image { data_handle, ref mut compositor_surface_kind, .. } => { - let image_key = &data_stores.image[data_handle]; - let image_data = &image_key.kind; - - // For now, assume that for compositor surface purposes, any RGBA image may be - // translucent. See the comment in `add_prim` in this source file for more - // details. We'll leave the `is_opaque` code branches here, but disabled, as - // in future we will want to support this case correctly. - let mut is_opaque = false; - - if let Some(image_properties) = resource_cache.get_image_properties(image_data.key) { - // For an image to be a possible opaque backdrop, it must: - // - Have a valid, opaque image descriptor - // - Not use tiling (since they can fail to draw) - // - Not having any spacing / padding - // - Have opaque alpha in the instance (flattened) color - if image_properties.descriptor.is_opaque() && - image_properties.tiling.is_none() && - image_data.tile_spacing == LayoutSize::zero() && - image_data.color.a >= 1.0 { - backdrop_candidate = Some(BackdropInfo { - opaque_rect: pic_coverage_rect, - spanning_opaque_color: None, - kind: None, - backdrop_rect: PictureRect::zero(), - }); - } - - is_opaque = image_properties.descriptor.is_opaque(); - } - - let mut promotion_result: Result<CompositorSurfaceKind, SurfacePromotionFailure> = Ok(CompositorSurfaceKind::Blit); - if image_key.common.flags.contains(PrimitiveFlags::PREFER_COMPOSITOR_SURFACE) { - // Only consider promoting Images if all of our YuvImages have been - // processed (whether they were promoted or not). - if self.yuv_images_remaining > 0 { - promotion_result = Err(ImageWaitingOnYuvImage); - } else { - promotion_result = self.can_promote_to_surface(prim_clip_chain, - prim_spatial_node_index, - is_root_tile_cache, - sub_slice_index, - CompositorSurfaceKind::Overlay, - pic_coverage_rect, - frame_context, - false); - } - - // Native OS compositors (DC and CA, at least) support premultiplied alpha - // only. If we have an image that's not pre-multiplied alpha, we can't promote it. - if image_data.alpha_type == AlphaType::Alpha { - promotion_result = Err(NotPremultipliedAlpha); - } - - if let Ok(kind) = promotion_result { - promotion_result = self.setup_compositor_surfaces_rgb( - sub_slice_index, - &mut prim_info, - image_key.common.flags, - local_prim_rect, - prim_spatial_node_index, - pic_coverage_rect, - frame_context, - ImageDependency { - key: image_data.key, - generation: resource_cache.get_image_generation(image_data.key), - }, - image_data.key, - resource_cache, - composite_state, - gpu_buffer, - image_data.image_rendering, - is_opaque, - kind, - ); - } - } - - if let Ok(kind) = promotion_result { - *compositor_surface_kind = kind; - - if kind == CompositorSurfaceKind::Overlay { - profile.inc(profiler::COMPOSITOR_SURFACE_OVERLAYS); - return VisibilityState::Culled; - } - - assert!(kind == CompositorSurfaceKind::Blit, "Image prims should either be overlays or blits."); - } else { - // In Err case, we handle as a blit, and proceed. - self.report_promotion_failure(promotion_result, pic_coverage_rect, false); - *compositor_surface_kind = CompositorSurfaceKind::Blit; - } - - if image_key.common.flags.contains(PrimitiveFlags::PREFER_COMPOSITOR_SURFACE) { - profile.inc(profiler::COMPOSITOR_SURFACE_BLITS); - } - - prim_info.images.push(ImageDependency { - key: image_data.key, - generation: resource_cache.get_image_generation(image_data.key), - }); - } - PrimitiveInstanceKind::YuvImage { data_handle, ref mut compositor_surface_kind, .. } => { - let prim_data = &data_stores.yuv_image[data_handle]; - - let mut promotion_result: Result<CompositorSurfaceKind, SurfacePromotionFailure> = Ok(CompositorSurfaceKind::Blit); - if prim_data.common.flags.contains(PrimitiveFlags::PREFER_COMPOSITOR_SURFACE) { - // Note if this is one of the YuvImages we were considering for - // surface promotion. We only care for primitives that were added - // to us, indicated by is_root_tile_cache. Those are the only ones - // that were added to the TileCacheParams that configured the - // current scene. - if is_root_tile_cache { - self.yuv_images_remaining -= 1; - } - - // Should we force the promotion of this surface? We'll force it if promotion - // is necessary for correct color display. - let force = prim_data.kind.color_depth.bit_depth() > 8; - - let promotion_attempts = - [CompositorSurfaceKind::Overlay, CompositorSurfaceKind::Underlay]; - - for kind in promotion_attempts { - // Since this might be an attempt after an earlier error, clear the flag - // so that we are allowed to report another error. - promotion_result = self.can_promote_to_surface( - prim_clip_chain, - prim_spatial_node_index, - is_root_tile_cache, - sub_slice_index, - kind, - pic_coverage_rect, - frame_context, - force); - if promotion_result.is_ok() { - break; - } - - // We couldn't promote, but did we give up because the slice is marked - // atomic? If that was the reason, and the YuvImage is wide color, - // failing to promote will flatten the colors and look terrible. Let's - // ignore the atomic slice restriction in such a case. - if let Err(SliceAtomic) = promotion_result { - if prim_data.kind. color_depth != ColorDepth::Color8 { - // Let's promote with the attempted kind. - promotion_result = Ok(kind); - break; - } - } - } - - // TODO(gw): When we support RGBA images for external surfaces, we also - // need to check if opaque (YUV images are implicitly opaque). - - // If this primitive is being promoted to a surface, construct an external - // surface descriptor for use later during batching and compositing. We only - // add the image keys for this primitive as a dependency if this is _not_ - // a promoted surface, since we don't want the tiles to invalidate when the - // video content changes, if it's a compositor surface! - if let Ok(kind) = promotion_result { - // Build dependency for each YUV plane, with current image generation for - // later detection of when the composited surface has changed. - let mut image_dependencies = [ImageDependency::INVALID; 3]; - for (key, dep) in prim_data.kind.yuv_key.iter().cloned().zip(image_dependencies.iter_mut()) { - *dep = ImageDependency { - key, - generation: resource_cache.get_image_generation(key), - } - } - - promotion_result = self.setup_compositor_surfaces_yuv( - sub_slice_index, - &mut prim_info, - prim_data.common.flags, - local_prim_rect, - prim_spatial_node_index, - pic_coverage_rect, - frame_context, - &image_dependencies, - &prim_data.kind.yuv_key, - resource_cache, - composite_state, - gpu_buffer, - prim_data.kind.image_rendering, - prim_data.kind.color_depth, - prim_data.kind.color_space.with_range(prim_data.kind.color_range), - prim_data.kind.format, - kind, - ); - } - } - - // Store on the YUV primitive instance whether this is a promoted surface. - // This is used by the batching code to determine whether to draw the - // image to the content tiles, or just a transparent z-write. - if let Ok(kind) = promotion_result { - *compositor_surface_kind = kind; - if kind == CompositorSurfaceKind::Overlay { - profile.inc(profiler::COMPOSITOR_SURFACE_OVERLAYS); - return VisibilityState::Culled; - } - - profile.inc(profiler::COMPOSITOR_SURFACE_UNDERLAYS); - } else { - // In Err case, we handle as a blit, and proceed. - self.report_promotion_failure(promotion_result, pic_coverage_rect, false); - *compositor_surface_kind = CompositorSurfaceKind::Blit; - if prim_data.common.flags.contains(PrimitiveFlags::PREFER_COMPOSITOR_SURFACE) { - profile.inc(profiler::COMPOSITOR_SURFACE_BLITS); - } - } - - if *compositor_surface_kind == CompositorSurfaceKind::Blit { - prim_info.images.extend( - prim_data.kind.yuv_key.iter().map(|key| { - ImageDependency { - key: *key, - generation: resource_cache.get_image_generation(*key), - } - }) - ); - } - } - PrimitiveInstanceKind::ImageBorder { data_handle, .. } => { - let border_data = &data_stores.image_border[data_handle].kind; - prim_info.images.push(ImageDependency { - key: border_data.request.key, - generation: resource_cache.get_image_generation(border_data.request.key), - }); - } - PrimitiveInstanceKind::LinearGradient { data_handle, .. } - | PrimitiveInstanceKind::CachedLinearGradient { data_handle, .. } => { - let gradient_data = &data_stores.linear_grad[data_handle]; - if gradient_data.stops_opacity.is_opaque - && gradient_data.tile_spacing == LayoutSize::zero() - { - backdrop_candidate = Some(BackdropInfo { - opaque_rect: pic_coverage_rect, - spanning_opaque_color: None, - kind: None, - backdrop_rect: PictureRect::zero(), - }); - } - } - PrimitiveInstanceKind::ConicGradient { data_handle, .. } => { - let gradient_data = &data_stores.conic_grad[data_handle]; - if gradient_data.stops_opacity.is_opaque - && gradient_data.tile_spacing == LayoutSize::zero() - { - backdrop_candidate = Some(BackdropInfo { - opaque_rect: pic_coverage_rect, - spanning_opaque_color: None, - kind: None, - backdrop_rect: PictureRect::zero(), - }); - } - } - PrimitiveInstanceKind::RadialGradient { data_handle, .. } => { - let gradient_data = &data_stores.radial_grad[data_handle]; - if gradient_data.stops_opacity.is_opaque - && gradient_data.tile_spacing == LayoutSize::zero() - { - backdrop_candidate = Some(BackdropInfo { - opaque_rect: pic_coverage_rect, - spanning_opaque_color: None, - kind: None, - backdrop_rect: PictureRect::zero(), - }); - } - } - PrimitiveInstanceKind::BackdropCapture { .. } => {} - PrimitiveInstanceKind::BackdropRender { pic_index, .. } => { - // If the area that the backdrop covers in the space of the surface it draws on - // is empty, skip any sub-graph processing. This is not just a performance win, - // it also ensures that we don't do a deferred dirty test that invalidates a tile - // even if the tile isn't actually dirty, which can cause panics later in the - // WR pipeline. - if !pic_coverage_rect.is_empty() { - // Mark that we need the sub-graph this render depends on so that - // we don't skip it during the prepare pass - scratch.required_sub_graphs.insert(pic_index); - - // If this is a sub-graph, register the bounds on any affected tiles - // so we know how much to expand the content tile by. - let sub_slice = &mut self.sub_slices[sub_slice_index]; - - let mut surface_info = Vec::new(); - for (pic_index, surface_index) in surface_stack.iter().rev() { - let pic = &pictures[pic_index.0]; - surface_info.push((pic.composite_mode.as_ref().unwrap().clone(), *surface_index)); - } - - for y in p0.y .. p1.y { - for x in p0.x .. p1.x { - let key = TileOffset::new(x, y); - let tile = sub_slice.tiles.get_mut(&key).expect("bug: no tile"); - tile.sub_graphs.push((pic_coverage_rect, surface_info.clone())); - } - } - - // For backdrop-filter, we need to check if any of the dirty rects - // in tiles that are affected by the filter primitive are dirty. - self.deferred_dirty_tests.push(DeferredDirtyTest { - tile_rect: TileRect::new(p0, p1), - prim_rect: pic_coverage_rect, - }); - } - } - PrimitiveInstanceKind::LineDecoration { .. } | - PrimitiveInstanceKind::NormalBorder { .. } | - PrimitiveInstanceKind::BoxShadow { .. } | - PrimitiveInstanceKind::TextRun { .. } => { - // These don't contribute dependencies - } - }; - - // Calculate the screen rect in local space. When we calculate backdrops, we - // care only that they cover the visible rect (based off the local clip), and - // don't have any overlapping prims in the visible rect. - let visible_local_clip_rect = self.local_clip_rect.intersection(&self.screen_rect_in_pic_space).unwrap_or_default(); - if pic_coverage_rect.intersects(&visible_local_clip_rect) { - self.found_prims_after_backdrop = true; - } - - // If this primitive considers itself a backdrop candidate, apply further - // checks to see if it matches all conditions to be a backdrop. - let mut vis_flags = PrimitiveVisibilityFlags::empty(); - let sub_slice = &mut self.sub_slices[sub_slice_index]; - if let Some(mut backdrop_candidate) = backdrop_candidate { - // Update whether the surface that this primitive exists on - // can be considered opaque. Any backdrop kind other than - // a clear primitive (e.g. color, gradient, image) can be - // considered. - match backdrop_candidate.kind { - Some(BackdropKind::Color { .. }) | None => { - let surface = &mut surfaces[prim_surface_index.0]; - - let is_same_coord_system = frame_context.spatial_tree.is_matching_coord_system( - prim_spatial_node_index, - surface.surface_spatial_node_index, - ); - - // To be an opaque backdrop, it must: - // - Be the same coordinate system (axis-aligned) - // - Have no clip mask - // - Have a rect that covers the surface local rect - if is_same_coord_system && - !prim_clip_chain.needs_mask && - prim_clip_chain.pic_coverage_rect.contains_box(&surface.unclipped_local_rect) - { - // Note that we use `prim_clip_chain.pic_clip_rect` here rather - // than `backdrop_candidate.opaque_rect`. The former is in the - // local space of the surface, the latter is in the local space - // of the top level tile-cache. - surface.is_opaque = true; - } - } - } - - // Check a number of conditions to see if we can consider this - // primitive as an opaque backdrop rect. Several of these are conservative - // checks and could be relaxed in future. However, these checks - // are quick and capture the common cases of background rects and images. - // Specifically, we currently require: - // - The primitive is on the main picture cache surface. - // - Same coord system as picture cache (ensures rects are axis-aligned). - // - No clip masks exist. - let same_coord_system = frame_context.spatial_tree.is_matching_coord_system( - prim_spatial_node_index, - self.spatial_node_index, - ); - - let is_suitable_backdrop = same_coord_system && on_picture_surface; - - if sub_slice_index == 0 && - is_suitable_backdrop && - sub_slice.compositor_surfaces.is_empty() { - - // If the backdrop candidate has a clip-mask, try to extract an opaque inner - // rect that is safe to use for subpixel rendering - if prim_clip_chain.needs_mask { - backdrop_candidate.opaque_rect = clip_store - .get_inner_rect_for_clip_chain( - prim_clip_chain, - &data_stores.clip, - frame_context.spatial_tree, - ) - .unwrap_or(PictureRect::zero()); - } - - // We set the backdrop opaque_rect here, indicating the coverage area, which - // is useful for calculate_subpixel_mode. We will only set the backdrop kind - // if it covers the visible rect. - if backdrop_candidate.opaque_rect.contains_box(&self.backdrop.opaque_rect) { - self.backdrop.opaque_rect = backdrop_candidate.opaque_rect; - } - - if let Some(kind) = backdrop_candidate.kind { - if backdrop_candidate.opaque_rect.contains_box(&visible_local_clip_rect) { - self.found_prims_after_backdrop = false; - self.backdrop.kind = Some(kind); - self.backdrop.backdrop_rect = backdrop_candidate.opaque_rect; - - // If we have a color backdrop that spans the entire local rect, mark - // the visibility flags of the primitive so it is skipped during batching - // (and also clears any previous primitives). Additionally, update our - // background color to match the backdrop color, which will ensure that - // our tiles are cleared to this color. - let BackdropKind::Color { color } = kind; - if backdrop_candidate.opaque_rect.contains_box(&self.local_rect) { - vis_flags |= PrimitiveVisibilityFlags::IS_BACKDROP; - self.backdrop.spanning_opaque_color = Some(color); - } - } - } - } - } - - // Record any new spatial nodes in the used list. - for spatial_node_index in &prim_info.spatial_nodes { - self.spatial_node_comparer.register_used_transform( - *spatial_node_index, - self.frame_id, - frame_context.spatial_tree, - ); - } - - // Normalize the tile coordinates before adding to tile dependencies. - // For each affected tile, mark any of the primitive dependencies. - for y in p0.y .. p1.y { - for x in p0.x .. p1.x { - // TODO(gw): Convert to 2d array temporarily to avoid hash lookups per-tile? - let key = TileOffset::new(x, y); - let tile = sub_slice.tiles.get_mut(&key).expect("bug: no tile"); - - tile.add_prim_dependency(&prim_info); - } - } - - VisibilityState::Visible { - vis_flags, - sub_slice_index: SubSliceIndex::new(sub_slice_index), - } - } - - /// Print debug information about this picture cache to a tree printer. - pub fn print(&self) { - // TODO(gw): This initial implementation is very basic - just printing - // the picture cache state to stdout. In future, we can - // make this dump each frame to a file, and produce a report - // stating which frames had invalidations. This will allow - // diff'ing the invalidation states in a visual tool. - let mut pt = PrintTree::new("Picture Cache"); - - pt.new_level(format!("Slice {:?}", self.slice)); - - pt.add_item(format!("background_color: {:?}", self.background_color)); - - for (sub_slice_index, sub_slice) in self.sub_slices.iter().enumerate() { - pt.new_level(format!("SubSlice {:?}", sub_slice_index)); - - for y in self.tile_bounds_p0.y .. self.tile_bounds_p1.y { - for x in self.tile_bounds_p0.x .. self.tile_bounds_p1.x { - let key = TileOffset::new(x, y); - let tile = &sub_slice.tiles[&key]; - tile.print(&mut pt); - } - } - - pt.end_level(); - } - - pt.end_level(); - } - - fn calculate_subpixel_mode(&self) -> SubpixelMode { - // We can only consider the full opaque cases if there's no underlays - if self.underlays.is_empty() { - let has_opaque_bg_color = self.background_color.map_or(false, |c| c.a >= 1.0); - - // If the overall tile cache is known opaque, subpixel AA is allowed everywhere - if has_opaque_bg_color { - return SubpixelMode::Allow; - } - - // If the opaque backdrop rect covers the entire tile cache surface, - // we can allow subpixel AA anywhere, skipping the per-text-run tests - // later on during primitive preparation. - if self.backdrop.opaque_rect.contains_box(&self.local_rect) { - return SubpixelMode::Allow; - } - } - - // If we didn't find any valid opaque backdrop, no subpixel AA allowed - if self.backdrop.opaque_rect.is_empty() { - return SubpixelMode::Deny; - } - - // Calculate a prohibited rect where we won't allow subpixel AA. - // TODO(gw): This is conservative - it will disallow subpixel AA if there - // are two underlay surfaces with text placed in between them. That's - // probably unlikely to be an issue in practice, but maybe we should support - // an array of prohibted rects? - let prohibited_rect = self - .underlays - .iter() - .fold( - PictureRect::zero(), - |acc, underlay| { - acc.union(&underlay.local_rect) - } - ); - - // If none of the simple cases above match, we need test where we can support subpixel AA. - // TODO(gw): In future, it may make sense to have > 1 inclusion rect, - // but this handles the common cases. - // TODO(gw): If a text run gets animated such that it's moving in a way that is - // sometimes intersecting with the video rect, this can result in subpixel - // AA flicking on/off for that text run. It's probably very rare, but - // something we should handle in future. - SubpixelMode::Conditional { - allowed_rect: self.backdrop.opaque_rect, - prohibited_rect, - } - } - - /// Apply any updates after prim dependency updates. This applies - /// any late tile invalidations, and sets up the dirty rect and - /// set of tile blits. - pub fn post_update( - &mut self, - frame_context: &FrameVisibilityContext, - composite_state: &mut CompositeState, - resource_cache: &mut ResourceCache, - ) { - assert!(self.current_surface_traversal_depth == 0); - - // TODO: Switch from the root node ot raster space. - let visibility_node = frame_context.spatial_tree.root_reference_frame_index(); - - self.dirty_region.reset(visibility_node, self.spatial_node_index); - self.subpixel_mode = self.calculate_subpixel_mode(); - - self.transform_index = composite_state.register_transform( - self.local_to_raster, - // TODO(gw): Once we support scaling of picture cache tiles during compositing, - // that transform gets plugged in here! - self.raster_to_device, - ); - - let map_pic_to_world = SpaceMapper::new_with_target( - frame_context.root_spatial_node_index, - self.spatial_node_index, - frame_context.global_screen_world_rect, - frame_context.spatial_tree, - ); - - // A simple GC of the native external surface cache, to remove and free any - // surfaces that were not referenced during the update_prim_dependencies pass. - self.external_native_surface_cache.retain(|_, surface| { - if !surface.used_this_frame { - // If we removed an external surface, we need to mark the dirty rects as - // invalid so a full composite occurs on the next frame. - composite_state.dirty_rects_are_valid = false; - - resource_cache.destroy_compositor_surface(surface.native_surface_id); - } - - surface.used_this_frame - }); - - let pic_to_world_mapper = SpaceMapper::new_with_target( - frame_context.root_spatial_node_index, - self.spatial_node_index, - frame_context.global_screen_world_rect, - frame_context.spatial_tree, - ); - - let ctx = TileUpdateDirtyContext { - pic_to_world_mapper, - global_device_pixel_scale: frame_context.global_device_pixel_scale, - opacity_bindings: &self.opacity_bindings, - color_bindings: &self.color_bindings, - local_rect: self.local_rect, - invalidate_all: self.invalidate_all_tiles, - }; - - let mut state = TileUpdateDirtyState { - resource_cache, - composite_state, - compare_cache: &mut self.compare_cache, - spatial_node_comparer: &mut self.spatial_node_comparer, - }; - - // Step through each tile and invalidate if the dependencies have changed. Determine - // the current opacity setting and whether it's changed. - for sub_slice in &mut self.sub_slices { - for tile in sub_slice.tiles.values_mut() { - tile.update_dirty_and_valid_rects(&ctx, &mut state, frame_context); - } - } - - // Process any deferred dirty checks - for sub_slice in &mut self.sub_slices { - for dirty_test in self.deferred_dirty_tests.drain(..) { - // Calculate the total dirty rect from all tiles that this primitive affects - let mut total_dirty_rect = PictureRect::zero(); - - for y in dirty_test.tile_rect.min.y .. dirty_test.tile_rect.max.y { - for x in dirty_test.tile_rect.min.x .. dirty_test.tile_rect.max.x { - let key = TileOffset::new(x, y); - let tile = sub_slice.tiles.get_mut(&key).expect("bug: no tile"); - total_dirty_rect = total_dirty_rect.union(&tile.local_dirty_rect); - } - } - - // If that dirty rect intersects with the local rect of the primitive - // being checked, invalidate that region in all of the affected tiles. - // TODO(gw): This is somewhat conservative, we could be more clever - // here and avoid invalidating every tile when this changes. - // We could also store the dirty rect only when the prim - // is encountered, so that we don't invalidate if something - // *after* the query in the rendering order affects invalidation. - if total_dirty_rect.intersects(&dirty_test.prim_rect) { - for y in dirty_test.tile_rect.min.y .. dirty_test.tile_rect.max.y { - for x in dirty_test.tile_rect.min.x .. dirty_test.tile_rect.max.x { - let key = TileOffset::new(x, y); - let tile = sub_slice.tiles.get_mut(&key).expect("bug: no tile"); - tile.invalidate( - Some(dirty_test.prim_rect), - InvalidationReason::SurfaceContentChanged, - ); - } - } - } - } - } - - let mut ctx = TilePostUpdateContext { - local_clip_rect: self.local_clip_rect, - backdrop: None, - current_tile_size: self.current_tile_size, - z_id: ZBufferId::invalid(), - underlays: &self.underlays, - }; - - let mut state = TilePostUpdateState { - resource_cache, - composite_state, - }; - - for (i, sub_slice) in self.sub_slices.iter_mut().enumerate().rev() { - // The backdrop is only relevant for the first sub-slice - if i == 0 { - ctx.backdrop = Some(self.backdrop); - } - - for compositor_surface in sub_slice.compositor_surfaces.iter_mut().rev() { - compositor_surface.descriptor.z_id = state.composite_state.z_generator.next(); - } - - ctx.z_id = state.composite_state.z_generator.next(); - - for tile in sub_slice.tiles.values_mut() { - tile.post_update(&ctx, &mut state, frame_context); - } - } - - // Assign z-order for each underlay - for underlay in self.underlays.iter_mut().rev() { - underlay.z_id = state.composite_state.z_generator.next(); - } - - // Register any opaque external compositor surfaces as potential occluders. This - // is especially useful when viewing video in full-screen mode, as it is - // able to occlude every background tile (avoiding allocation, rasterizion - // and compositing). - - // Register any underlays as occluders where possible - for underlay in &self.underlays { - if let Some(world_surface_rect) = underlay.get_occluder_rect( - &self.local_clip_rect, - &map_pic_to_world, - ) { - composite_state.register_occluder( - underlay.z_id, - world_surface_rect, - self.compositor_clip, - ); - } - } - - for sub_slice in &self.sub_slices { - for compositor_surface in &sub_slice.compositor_surfaces { - if compositor_surface.is_opaque { - if let Some(world_surface_rect) = compositor_surface.descriptor.get_occluder_rect( - &self.local_clip_rect, - &map_pic_to_world, - ) { - composite_state.register_occluder( - compositor_surface.descriptor.z_id, - world_surface_rect, - self.compositor_clip, - ); - } - } - } - } - - // Register the opaque region of this tile cache as an occluder, which - // is used later in the frame to occlude other tiles. - if !self.backdrop.opaque_rect.is_empty() { - let z_id_backdrop = composite_state.z_generator.next(); - - let backdrop_rect = self.backdrop.opaque_rect - .intersection(&self.local_rect) - .and_then(|r| { - r.intersection(&self.local_clip_rect) - }); - - if let Some(backdrop_rect) = backdrop_rect { - let world_backdrop_rect = map_pic_to_world - .map(&backdrop_rect) - .expect("bug: unable to map backdrop to world space"); - - // Since we register the entire backdrop rect, use the opaque z-id for the - // picture cache slice. - composite_state.register_occluder( - z_id_backdrop, - world_backdrop_rect, - self.compositor_clip, - ); - } - } - } -} - - -/// A SubSlice represents a potentially overlapping set of tiles within a picture cache. Most -/// picture cache instances will have only a single sub-slice. The exception to this is when -/// a picture cache has compositor surfaces, in which case sub slices are used to interleave -/// content under or order the compositor surface(s). -pub struct SubSlice { - /// Hash of tiles present in this picture. - pub tiles: FastHashMap<TileOffset, Box<Tile>>, - /// The allocated compositor surfaces for this picture cache. May be None if - /// not using native compositor, or if the surface was destroyed and needs - /// to be reallocated next time this surface contains valid tiles. - pub native_surface: Option<NativeSurface>, - /// List of compositor surfaces that have been promoted from primitives - /// in this tile cache. - pub compositor_surfaces: Vec<CompositorSurface>, - /// List of visible tiles to be composited for this subslice - pub composite_tiles: Vec<CompositeTile>, - /// Compositor descriptors of visible, opaque tiles (used by composite_state.push_surface) - pub opaque_tile_descriptors: Vec<CompositeTileDescriptor>, - /// Compositor descriptors of visible, alpha tiles (used by composite_state.push_surface) - pub alpha_tile_descriptors: Vec<CompositeTileDescriptor>, -} - -impl SubSlice { - /// Construct a new sub-slice - fn new() -> Self { - SubSlice { - tiles: FastHashMap::default(), - native_surface: None, - compositor_surfaces: Vec::new(), - composite_tiles: Vec::new(), - opaque_tile_descriptors: Vec::new(), - alpha_tile_descriptors: Vec::new(), - } - } - - /// Reset the list of compositor surfaces that follow this sub-slice. - /// Built per-frame, since APZ may change whether an image is suitable to be a compositor surface. - fn reset(&mut self) { - self.compositor_surfaces.clear(); - self.composite_tiles.clear(); - self.opaque_tile_descriptors.clear(); - self.alpha_tile_descriptors.clear(); - } - - /// Resize the tile grid to match a new tile bounds - fn resize(&mut self, new_tile_rect: TileRect) -> FastHashMap<TileOffset, Box<Tile>> { - let mut old_tiles = mem::replace(&mut self.tiles, FastHashMap::default()); - self.tiles.reserve(new_tile_rect.area() as usize); - - for y in new_tile_rect.min.y .. new_tile_rect.max.y { - for x in new_tile_rect.min.x .. new_tile_rect.max.x { - let key = TileOffset::new(x, y); - let tile = old_tiles - .remove(&key) - .unwrap_or_else(|| { - Box::new(Tile::new(key)) - }); - self.tiles.insert(key, tile); - } - } - - old_tiles - } -} - -#[derive(Clone, Copy)] -enum SurfacePromotionFailure { - ImageWaitingOnYuvImage, - NotPremultipliedAlpha, - OverlaySurfaceLimit, - OverlayNeedsMask, - UnderlayAlphaBackdrop, - UnderlaySurfaceLimit, - UnderlayIntersectsOverlay, - UnderlayLowQualityZoom, - NotRootTileCache, - ComplexTransform, - SliceAtomic, - SizeTooLarge, -} - -impl Display for SurfacePromotionFailure { - fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { - write!( - f, - "{}", - match *self { - SurfacePromotionFailure::ImageWaitingOnYuvImage => "Image prim waiting for all YuvImage prims to be considered for promotion", - SurfacePromotionFailure::NotPremultipliedAlpha => "does not use premultiplied alpha", - SurfacePromotionFailure::OverlaySurfaceLimit => "hit the overlay surface limit", - SurfacePromotionFailure::OverlayNeedsMask => "overlay not allowed for prim with mask", - SurfacePromotionFailure::UnderlayAlphaBackdrop => "underlay requires an opaque backdrop", - SurfacePromotionFailure::UnderlaySurfaceLimit => "hit the underlay surface limit", - SurfacePromotionFailure::UnderlayIntersectsOverlay => "underlay intersects already-promoted overlay", - SurfacePromotionFailure::UnderlayLowQualityZoom => "underlay not allowed during low-quality pinch zoom", - SurfacePromotionFailure::NotRootTileCache => "is not on a root tile cache", - SurfacePromotionFailure::ComplexTransform => "has a complex transform", - SurfacePromotionFailure::SliceAtomic => "slice is atomic", - SurfacePromotionFailure::SizeTooLarge => "surface is too large for compositor", - }.to_owned() - ) - } -} - -// Immutable context passed to picture cache tiles during pre_update -struct TilePreUpdateContext { - /// Maps from picture cache coords -> world space coords. - pic_to_world_mapper: SpaceMapper<PicturePixel, WorldPixel>, - - /// The optional background color of the picture cache instance - background_color: Option<ColorF>, - - /// The visible part of the screen in world coords. - global_screen_world_rect: WorldRect, - - /// Current size of tiles in picture units. - tile_size: PictureSize, - - /// The current frame id for this picture cache - frame_id: FrameId, -} - -// Immutable context passed to picture cache tiles during update_dirty_and_valid_rects -struct TileUpdateDirtyContext<'a> { - /// Maps from picture cache coords -> world space coords. - pic_to_world_mapper: SpaceMapper<PicturePixel, WorldPixel>, - - /// Global scale factor from world -> device pixels. - global_device_pixel_scale: DevicePixelScale, - - /// Information about opacity bindings from the picture cache. - opacity_bindings: &'a FastHashMap<PropertyBindingId, OpacityBindingInfo>, - - /// Information about color bindings from the picture cache. - color_bindings: &'a FastHashMap<PropertyBindingId, ColorBindingInfo>, - - /// The local rect of the overall picture cache - local_rect: PictureRect, - - /// If true, the scale factor of the root transform for this picture - /// cache changed, so we need to invalidate the tile and re-render. - invalidate_all: bool, -} - -// Mutable state passed to picture cache tiles during update_dirty_and_valid_rects -struct TileUpdateDirtyState<'a> { - /// Allow access to the texture cache for requesting tiles - resource_cache: &'a mut ResourceCache, - - /// Current configuration and setup for compositing all the picture cache tiles in renderer. - composite_state: &'a mut CompositeState, - - /// A cache of comparison results to avoid re-computation during invalidation. - compare_cache: &'a mut FastHashMap<PrimitiveComparisonKey, PrimitiveCompareResult>, - - /// Information about transform node differences from last frame. - spatial_node_comparer: &'a mut SpatialNodeComparer, -} - -// Immutable context passed to picture cache tiles during post_update -struct TilePostUpdateContext<'a> { - /// The local clip rect (in picture space) of the entire picture cache - local_clip_rect: PictureRect, - - /// The calculated backdrop information for this cache instance. - backdrop: Option<BackdropInfo>, - - /// Current size in device pixels of tiles for this cache - current_tile_size: DeviceIntSize, - - /// Pre-allocated z-id to assign to tiles during post_update. - z_id: ZBufferId, - - /// The list of compositor underlays for this picture cache - underlays: &'a [ExternalSurfaceDescriptor], -} - -// Mutable state passed to picture cache tiles during post_update -struct TilePostUpdateState<'a> { - /// Allow access to the texture cache for requesting tiles - resource_cache: &'a mut ResourceCache, - - /// Current configuration and setup for compositing all the picture cache tiles in renderer. - composite_state: &'a mut CompositeState, -} - -/// Information about the dependencies of a single primitive instance. -struct PrimitiveDependencyInfo { - /// Unique content identifier of the primitive. - prim_uid: ItemUid, - - /// The (conservative) clipped area in picture space this primitive occupies. - prim_clip_box: PictureBox2D, - - /// Image keys this primitive depends on. - images: SmallVec<[ImageDependency; 8]>, - - /// Opacity bindings this primitive depends on. - opacity_bindings: SmallVec<[OpacityBinding; 4]>, - - /// Color binding this primitive depends on. - color_binding: Option<ColorBinding>, - - /// Clips that this primitive depends on. - clips: SmallVec<[ItemUid; 8]>, - - /// Spatial nodes references by the clip dependencies of this primitive. - spatial_nodes: SmallVec<[SpatialNodeIndex; 4]>, -} - -impl PrimitiveDependencyInfo { - /// Construct dependency info for a new primitive. - fn new( - prim_uid: ItemUid, - prim_clip_box: PictureBox2D, - ) -> Self { - PrimitiveDependencyInfo { - prim_uid, - images: SmallVec::new(), - opacity_bindings: SmallVec::new(), - color_binding: None, - prim_clip_box, - clips: SmallVec::new(), - spatial_nodes: SmallVec::new(), - } - } -} diff --git a/gfx/wr/webrender/src/tile_cache/slice_builder.rs b/gfx/wr/webrender/src/tile_cache/slice_builder.rs @@ -1,779 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -use api::{BorderRadius, ClipId, ClipMode, ColorF, DebugFlags, PrimitiveFlags, QualitySettings, RasterSpace}; -use api::units::*; -use crate::clip::{ClipItemKeyKind, ClipNodeId, ClipTreeBuilder}; -use crate::frame_builder::FrameBuilderConfig; -use crate::internal_types::FastHashMap; -use crate::picture::{PrimitiveList, PictureCompositeMode, PicturePrimitive, Picture3DContext, PictureFlags}; -use crate::tile_cache::{SliceId, TileCacheParams}; -use crate::prim_store::{PrimitiveInstance, PrimitiveStore, PictureIndex}; -use crate::scene_building::SliceFlags; -use crate::scene_builder_thread::Interners; -use crate::spatial_tree::{SpatialNodeIndex, SceneSpatialTree}; -use crate::util::VecHelper; -use std::mem; - -/* - Types and functionality related to picture caching. In future, we'll - move more and more of the existing functionality out of picture.rs - and into here. - */ - -// If the page would create too many slices (an arbitrary definition where -// it's assumed the GPU memory + compositing overhead would be too high) -// then create a single picture cache for the remaining content. This at -// least means that we can cache small content changes efficiently when -// scrolling isn't occurring. Scrolling regions will be handled reasonably -// efficiently by the dirty rect tracking (since it's likely that if the -// page has so many slices there isn't a single major scroll region). -const MAX_CACHE_SLICES: usize = 16; - -struct SliceDescriptor { - prim_list: PrimitiveList, - scroll_root: SpatialNodeIndex, -} - -enum SliceKind { - Default { - secondary_slices: Vec<SliceDescriptor>, - }, - Atomic { - prim_list: PrimitiveList, - }, -} - -impl SliceKind { - fn default() -> Self { - SliceKind::Default { - secondary_slices: Vec::new(), - } - } -} - -struct PrimarySlice { - /// Whether this slice is atomic or has secondary slice(s) - kind: SliceKind, - /// Optional background color of this slice - background_color: Option<ColorF>, - /// Optional root clip for the iframe - iframe_clip: Option<ClipId>, - /// Information about how to draw and composite this slice - slice_flags: SliceFlags, -} - -impl PrimarySlice { - fn new( - slice_flags: SliceFlags, - iframe_clip: Option<ClipId>, - background_color: Option<ColorF>, - ) -> Self { - PrimarySlice { - kind: SliceKind::default(), - background_color, - iframe_clip, - slice_flags, - } - } - - fn has_too_many_slices(&self) -> bool { - match self.kind { - SliceKind::Atomic { .. } => false, - SliceKind::Default { ref secondary_slices } => secondary_slices.len() > MAX_CACHE_SLICES, - } - } - - fn merge(&mut self) { - self.slice_flags |= SliceFlags::IS_ATOMIC; - - let old = mem::replace( - &mut self.kind, - SliceKind::Default { secondary_slices: Vec::new() }, - ); - - self.kind = match old { - SliceKind::Default { mut secondary_slices } => { - let mut prim_list = PrimitiveList::empty(); - - for descriptor in secondary_slices.drain(..) { - prim_list.merge(descriptor.prim_list); - } - - SliceKind::Atomic { - prim_list, - } - } - atomic => atomic, - } - } -} - -/// Used during scene building to construct the list of pending tile caches. -pub struct TileCacheBuilder { - /// List of tile caches that have been created so far (last in the list is currently active). - primary_slices: Vec<PrimarySlice>, - /// Cache the previous scroll root search for a spatial node, since they are often the same. - prev_scroll_root_cache: (SpatialNodeIndex, SpatialNodeIndex), - /// Handle to the root reference frame - root_spatial_node_index: SpatialNodeIndex, - /// Debug flags to provide to our TileCacheInstances. - debug_flags: DebugFlags, -} - -/// The output of a tile cache builder, containing all details needed to construct the -/// tile cache(s) for the next scene, and retain tiles from the previous frame when sent -/// send to the frame builder. -pub struct TileCacheConfig { - /// Mapping of slice id to the parameters needed to construct this tile cache. - pub tile_caches: FastHashMap<SliceId, TileCacheParams>, - /// Number of picture cache slices that were created (for profiler) - pub picture_cache_slice_count: usize, -} - -impl TileCacheConfig { - pub fn new(picture_cache_slice_count: usize) -> Self { - TileCacheConfig { - tile_caches: FastHashMap::default(), - picture_cache_slice_count, - } - } -} - -impl TileCacheBuilder { - /// Construct a new tile cache builder. - pub fn new( - root_spatial_node_index: SpatialNodeIndex, - background_color: Option<ColorF>, - debug_flags: DebugFlags, - ) -> Self { - TileCacheBuilder { - primary_slices: vec![PrimarySlice::new(SliceFlags::empty(), None, background_color)], - prev_scroll_root_cache: (SpatialNodeIndex::INVALID, SpatialNodeIndex::INVALID), - root_spatial_node_index, - debug_flags, - } - } - - pub fn make_current_slice_atomic(&mut self) { - self.primary_slices - .last_mut() - .unwrap() - .merge(); - } - - /// Returns true if the current slice has no primitives added yet - pub fn is_current_slice_empty(&self) -> bool { - match self.primary_slices.last() { - Some(slice) => { - match slice.kind { - SliceKind::Default { ref secondary_slices } => { - secondary_slices.is_empty() - } - SliceKind::Atomic { ref prim_list } => { - prim_list.is_empty() - } - } - } - None => { - true - } - } - } - - /// Set a barrier that forces a new tile cache next time a prim is added. - pub fn add_tile_cache_barrier( - &mut self, - slice_flags: SliceFlags, - iframe_clip: Option<ClipId>, - ) { - let new_slice = PrimarySlice::new( - slice_flags, - iframe_clip, - None, - ); - - self.primary_slices.push(new_slice); - } - - /// Create a new tile cache for an existing prim_list - fn build_tile_cache( - &mut self, - prim_list: PrimitiveList, - spatial_tree: &SceneSpatialTree, - ) -> Option<SliceDescriptor> { - if prim_list.is_empty() { - return None; - } - - // Iterate the clusters and determine which is the most commonly occurring - // scroll root. This is a reasonable heuristic to decide which spatial node - // should be considered the scroll root of this tile cache, in order to - // minimize the invalidations that occur due to scrolling. It's often the - // case that a blend container will have only a single scroll root. - let mut scroll_root_occurrences = FastHashMap::default(); - - for cluster in &prim_list.clusters { - // If we encounter a cluster which has an unknown spatial node, - // we don't include that in the set of spatial nodes that we - // are trying to find scroll roots for. Later on, in finalize_picture, - // the cluster spatial node will be updated to the selected scroll root. - if cluster.spatial_node_index == SpatialNodeIndex::UNKNOWN { - continue; - } - - let scroll_root = find_scroll_root( - cluster.spatial_node_index, - &mut self.prev_scroll_root_cache, - spatial_tree, - true, - ); - - *scroll_root_occurrences.entry(scroll_root).or_insert(0) += 1; - } - - // We can't just select the most commonly occurring scroll root in this - // primitive list. If that is a nested scroll root, there may be - // primitives in the list that are outside that scroll root, which - // can cause panics when calculating relative transforms. To ensure - // this doesn't happen, only retain scroll root candidates that are - // also ancestors of every other scroll root candidate. - let scroll_roots: Vec<SpatialNodeIndex> = scroll_root_occurrences - .keys() - .cloned() - .collect(); - - scroll_root_occurrences.retain(|parent_spatial_node_index, _| { - scroll_roots.iter().all(|child_spatial_node_index| { - parent_spatial_node_index == child_spatial_node_index || - spatial_tree.is_ancestor( - *parent_spatial_node_index, - *child_spatial_node_index, - ) - }) - }); - - // Select the scroll root by finding the most commonly occurring one - let scroll_root = scroll_root_occurrences - .iter() - .max_by_key(|entry | entry.1) - .map(|(spatial_node_index, _)| *spatial_node_index) - .unwrap_or(self.root_spatial_node_index); - - Some(SliceDescriptor { - scroll_root, - prim_list, - }) - } - - /// Add a primitive, either to the current tile cache, or a new one, depending on various conditions. - pub fn add_prim( - &mut self, - prim_instance: PrimitiveInstance, - prim_rect: LayoutRect, - spatial_node_index: SpatialNodeIndex, - prim_flags: PrimitiveFlags, - spatial_tree: &SceneSpatialTree, - interners: &Interners, - quality_settings: &QualitySettings, - prim_instances: &mut Vec<PrimitiveInstance>, - clip_tree_builder: &ClipTreeBuilder, - ) { - let primary_slice = self.primary_slices.last_mut().unwrap(); - - match primary_slice.kind { - SliceKind::Atomic { ref mut prim_list } => { - prim_list.add_prim( - prim_instance, - prim_rect, - spatial_node_index, - prim_flags, - prim_instances, - clip_tree_builder, - ); - } - SliceKind::Default { ref mut secondary_slices } => { - assert_ne!(spatial_node_index, SpatialNodeIndex::UNKNOWN); - - // Check if we want to create a new slice based on the current / next scroll root - let scroll_root = find_scroll_root( - spatial_node_index, - &mut self.prev_scroll_root_cache, - spatial_tree, - // Allow sticky frames as scroll roots, unless our quality settings prefer - // subpixel AA over performance. - !quality_settings.force_subpixel_aa_where_possible, - ); - - let current_scroll_root = secondary_slices - .last() - .map(|p| p.scroll_root); - - let mut want_new_tile_cache = secondary_slices.is_empty(); - - if let Some(current_scroll_root) = current_scroll_root { - want_new_tile_cache |= match (current_scroll_root, scroll_root) { - (_, _) if current_scroll_root == self.root_spatial_node_index && scroll_root == self.root_spatial_node_index => { - // Both current slice and this cluster are fixed position, no need to cut - false - } - (_, _) if current_scroll_root == self.root_spatial_node_index => { - // A real scroll root is being established, so create a cache slice - true - } - (_, _) if scroll_root == self.root_spatial_node_index => { - // If quality settings force subpixel AA over performance, skip creating - // a slice for the fixed position element(s) here. - if quality_settings.force_subpixel_aa_where_possible { - false - } else { - // A fixed position slice is encountered within a scroll root. Only create - // a slice in this case if all the clips referenced by this cluster are also - // fixed position. There's no real point in creating slices for these cases, - // since we'll have to rasterize them as the scrolling clip moves anyway. It - // also allows us to retain subpixel AA in these cases. For these types of - // slices, the intra-slice dirty rect handling typically works quite well - // (a common case is parallax scrolling effects). - let mut create_slice = true; - - let leaf = clip_tree_builder.get_leaf(prim_instance.clip_leaf_id); - let mut current_node_id = leaf.node_id; - - while current_node_id != ClipNodeId::NONE { - let node = clip_tree_builder.get_node(current_node_id); - - let clip_node_data = &interners.clip[node.handle]; - - let spatial_root = find_scroll_root( - clip_node_data.key.spatial_node_index, - &mut self.prev_scroll_root_cache, - spatial_tree, - true, - ); - - if spatial_root != self.root_spatial_node_index { - create_slice = false; - break; - } - - current_node_id = node.parent; - } - - create_slice - } - } - (curr_scroll_root, scroll_root) => { - // Two scrolling roots - only need a new slice if they differ - curr_scroll_root != scroll_root - } - }; - } - - if want_new_tile_cache { - secondary_slices.push(SliceDescriptor { - prim_list: PrimitiveList::empty(), - scroll_root, - }); - } - - secondary_slices - .last_mut() - .unwrap() - .prim_list - .add_prim( - prim_instance, - prim_rect, - spatial_node_index, - prim_flags, - prim_instances, - clip_tree_builder, - ); - } - } - } - - /// Consume this object and build the list of tile cache primitives - pub fn build( - mut self, - config: &FrameBuilderConfig, - prim_store: &mut PrimitiveStore, - spatial_tree: &SceneSpatialTree, - prim_instances: &[PrimitiveInstance], - clip_tree_builder: &mut ClipTreeBuilder, - interners: &Interners, - ) -> (TileCacheConfig, Vec<PictureIndex>) { - let mut result = TileCacheConfig::new(self.primary_slices.len()); - let mut tile_cache_pictures = Vec::new(); - let primary_slices = std::mem::replace(&mut self.primary_slices, Vec::new()); - - // TODO: At the moment, culling, clipping and invalidation are always - // done in the root coordinate space. The plan is to move to doing it - // (always or mostly) in raster space. - let visibility_node = spatial_tree.root_reference_frame_index(); - - for mut primary_slice in primary_slices { - - if primary_slice.has_too_many_slices() { - primary_slice.merge(); - } - - match primary_slice.kind { - SliceKind::Atomic { prim_list } => { - if let Some(descriptor) = self.build_tile_cache( - prim_list, - spatial_tree, - ) { - create_tile_cache( - self.debug_flags, - primary_slice.slice_flags, - descriptor.scroll_root, - visibility_node, - primary_slice.iframe_clip, - descriptor.prim_list, - primary_slice.background_color, - prim_store, - prim_instances, - config, - &mut result.tile_caches, - &mut tile_cache_pictures, - clip_tree_builder, - interners, - spatial_tree, - ); - } - } - SliceKind::Default { secondary_slices } => { - for descriptor in secondary_slices { - create_tile_cache( - self.debug_flags, - primary_slice.slice_flags, - descriptor.scroll_root, - visibility_node, - primary_slice.iframe_clip, - descriptor.prim_list, - primary_slice.background_color, - prim_store, - prim_instances, - config, - &mut result.tile_caches, - &mut tile_cache_pictures, - clip_tree_builder, - interners, - spatial_tree, - ); - } - } - } - } - - (result, tile_cache_pictures) - } -} - -/// Find the scroll root for a given spatial node -fn find_scroll_root( - spatial_node_index: SpatialNodeIndex, - prev_scroll_root_cache: &mut (SpatialNodeIndex, SpatialNodeIndex), - spatial_tree: &SceneSpatialTree, - allow_sticky_frames: bool, -) -> SpatialNodeIndex { - if prev_scroll_root_cache.0 == spatial_node_index { - return prev_scroll_root_cache.1; - } - - let scroll_root = spatial_tree.find_scroll_root(spatial_node_index, allow_sticky_frames); - *prev_scroll_root_cache = (spatial_node_index, scroll_root); - - scroll_root -} - -/// Given a PrimitiveList and scroll root, construct a tile cache primitive instance -/// that wraps the primitive list. -fn create_tile_cache( - debug_flags: DebugFlags, - slice_flags: SliceFlags, - scroll_root: SpatialNodeIndex, - visibility_node: SpatialNodeIndex, - iframe_clip: Option<ClipId>, - prim_list: PrimitiveList, - background_color: Option<ColorF>, - prim_store: &mut PrimitiveStore, - prim_instances: &[PrimitiveInstance], - frame_builder_config: &FrameBuilderConfig, - tile_caches: &mut FastHashMap<SliceId, TileCacheParams>, - tile_cache_pictures: &mut Vec<PictureIndex>, - clip_tree_builder: &mut ClipTreeBuilder, - interners: &Interners, - spatial_tree: &SceneSpatialTree, -) { - // Accumulate any clip instances from the iframe_clip into the shared clips - // that will be applied by this tile cache during compositing. - let mut additional_clips = Vec::new(); - - if let Some(clip_id) = iframe_clip { - additional_clips.push(clip_id); - } - - // Find the best shared clip node that we can apply while compositing tiles, - // rather than applying to each item individually. - - // Step 1: Walk the primitive list, and find the LCA of the clip-tree that - // matches all primitives. This gives us our "best-case" shared - // clip node that moves as many clips as possible to compositing. - let mut shared_clip_node_id = None; - - for cluster in &prim_list.clusters { - for prim_instance in &prim_instances[cluster.prim_range()] { - let leaf = clip_tree_builder.get_leaf(prim_instance.clip_leaf_id); - - // TODO(gw): Need to cache last clip-node id here? - shared_clip_node_id = match shared_clip_node_id { - Some(current) => { - Some(clip_tree_builder.find_lowest_common_ancestor(current, leaf.node_id)) - } - None => { - Some(leaf.node_id) - } - } - } - } - - // Step 2: Now we need to walk up the shared clip node hierarchy, and remove clips - // that we can't handle during compositing, such as: - // (a) Non axis-aligned clips - // (b) Box-shadow or image-mask clips - // (c) Rounded rect clips. - // - // A follow up patch to this series will relax the condition on (c) to - // allow tile caches to apply a single rounded-rect clip during compositing. - let mut shared_clip_node_id = shared_clip_node_id.unwrap_or(ClipNodeId::NONE); - let mut current_node_id = shared_clip_node_id; - let mut rounded_rect_count = 0; - - // Walk up the hierarchy to the root of the clip-tree - while current_node_id != ClipNodeId::NONE { - let node = clip_tree_builder.get_node(current_node_id); - let clip_node_data = &interners.clip[node.handle]; - - // Check if this clip is in the root coord system (i.e. is axis-aligned with tile-cache) - let is_rcs = spatial_tree.is_root_coord_system(clip_node_data.key.spatial_node_index); - - let node_valid = if is_rcs { - match clip_node_data.key.kind { - ClipItemKeyKind::BoxShadow(..) | - ClipItemKeyKind::ImageMask(..) | - ClipItemKeyKind::Rectangle(_, ClipMode::ClipOut) | - ClipItemKeyKind::RoundedRectangle(_, _, ClipMode::ClipOut) => { - // Has a box-shadow / image-mask, we can't handle this as a shared clip - false - } - ClipItemKeyKind::RoundedRectangle(rect, radius, ClipMode::Clip) => { - // The shader and CoreAnimation rely on certain constraints such - // as uniform radii to be able to apply the clip during compositing. - if BorderRadius::from(radius).can_use_fast_path_in(&rect.into()) { - rounded_rect_count += 1; - - true - } else { - false - } - } - ClipItemKeyKind::Rectangle(_, ClipMode::Clip) => { - // We can apply multiple (via combining) axis-aligned rectangle - // clips to the shared compositing clip. - true - } - } - } else { - // Has a complex transform, we can't handle this as a shared clip - false - }; - - if node_valid { - // This node was found to be one we can apply during compositing. - if rounded_rect_count > 1 { - // However, we plan to only support one rounded-rect clip. If - // we have found > 1 rounded rect, drop children from the shared - // clip, and continue looking up the chain. - shared_clip_node_id = current_node_id; - rounded_rect_count = 1; - } - } else { - // Node was invalid, due to transform / clip type. Drop this clip - // and reset the rounded rect count to 0, since we drop children - // from here too. - shared_clip_node_id = node.parent; - rounded_rect_count = 0; - } - - current_node_id = node.parent; - } - - let shared_clip_leaf_id = Some(clip_tree_builder.build_for_tile_cache( - shared_clip_node_id, - &additional_clips, - )); - - // Build a clip-chain for the tile cache, that contains any of the shared clips - // we will apply when drawing the tiles. In all cases provided by Gecko, these - // are rectangle clips with a scale/offset transform only, and get handled as - // a simple local clip rect in the vertex shader. However, this should in theory - // also work with any complex clips, such as rounded rects and image masks, by - // producing a clip mask that is applied to the picture cache tiles. - - let slice = tile_cache_pictures.len(); - - let background_color = if slice == 0 { - background_color - } else { - None - }; - - let slice_id = SliceId::new(slice); - - // Store some information about the picture cache slice. This is used when we swap the - // new scene into the frame builder to either reuse existing slices, or create new ones. - tile_caches.insert(slice_id, TileCacheParams { - debug_flags, - slice, - slice_flags, - spatial_node_index: scroll_root, - visibility_node_index: visibility_node, - background_color, - shared_clip_node_id, - shared_clip_leaf_id, - virtual_surface_size: frame_builder_config.compositor_kind.get_virtual_surface_size(), - image_surface_count: prim_list.image_surface_count, - yuv_image_surface_count: prim_list.yuv_image_surface_count, - }); - - let pic_index = prim_store.pictures.alloc().init(PicturePrimitive::new_image( - Some(PictureCompositeMode::TileCache { slice_id }), - Picture3DContext::Out, - PrimitiveFlags::IS_BACKFACE_VISIBLE, - prim_list, - scroll_root, - RasterSpace::Screen, - PictureFlags::empty(), - None, - )); - - tile_cache_pictures.push(PictureIndex(pic_index)); -} - -/// Debug information about a set of picture cache slices, exposed via RenderResults -#[derive(Debug)] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub struct PictureCacheDebugInfo { - pub slices: FastHashMap<usize, SliceDebugInfo>, -} - -impl PictureCacheDebugInfo { - pub fn new() -> Self { - PictureCacheDebugInfo { - slices: FastHashMap::default(), - } - } - - /// Convenience method to retrieve a given slice. Deliberately panics - /// if the slice isn't present. - pub fn slice(&self, slice: usize) -> &SliceDebugInfo { - &self.slices[&slice] - } -} - -impl Default for PictureCacheDebugInfo { - fn default() -> PictureCacheDebugInfo { - PictureCacheDebugInfo::new() - } -} - -/// Debug information about a set of picture cache tiles, exposed via RenderResults -#[derive(Debug)] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub struct SliceDebugInfo { - pub tiles: FastHashMap<TileOffset, TileDebugInfo>, -} - -impl SliceDebugInfo { - pub fn new() -> Self { - SliceDebugInfo { - tiles: FastHashMap::default(), - } - } - - /// Convenience method to retrieve a given tile. Deliberately panics - /// if the tile isn't present. - pub fn tile(&self, x: i32, y: i32) -> &TileDebugInfo { - &self.tiles[&TileOffset::new(x, y)] - } -} - -/// Debug information about a tile that was dirty and was rasterized -#[derive(Debug, PartialEq)] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub struct DirtyTileDebugInfo { - pub local_valid_rect: PictureRect, - pub local_dirty_rect: PictureRect, -} - -/// Debug information about the state of a tile -#[derive(Debug, PartialEq)] -#[cfg_attr(feature = "capture", derive(Serialize))] -#[cfg_attr(feature = "replay", derive(Deserialize))] -pub enum TileDebugInfo { - /// Tile was occluded by a tile in front of it - Occluded, - /// Tile was culled (not visible in current display port) - Culled, - /// Tile was valid (no rasterization was done) and visible - Valid, - /// Tile was dirty, and was updated - Dirty(DirtyTileDebugInfo), -} - -impl TileDebugInfo { - pub fn is_occluded(&self) -> bool { - match self { - TileDebugInfo::Occluded => true, - TileDebugInfo::Culled | - TileDebugInfo::Valid | - TileDebugInfo::Dirty(..) => false, - } - } - - pub fn is_valid(&self) -> bool { - match self { - TileDebugInfo::Valid => true, - TileDebugInfo::Culled | - TileDebugInfo::Occluded | - TileDebugInfo::Dirty(..) => false, - } - } - - pub fn is_culled(&self) -> bool { - match self { - TileDebugInfo::Culled => true, - TileDebugInfo::Valid | - TileDebugInfo::Occluded | - TileDebugInfo::Dirty(..) => false, - } - } - - pub fn as_dirty(&self) -> &DirtyTileDebugInfo { - match self { - TileDebugInfo::Occluded | - TileDebugInfo::Culled | - TileDebugInfo::Valid => { - panic!("not a dirty tile!"); - } - TileDebugInfo::Dirty(ref info) => { - info - } - } - } -} diff --git a/gfx/wr/webrender/src/visibility.rs b/gfx/wr/webrender/src/visibility.rs @@ -17,10 +17,8 @@ use crate::renderer::GpuBufferBuilder; use crate::spatial_tree::{SpatialTree, SpatialNodeIndex}; use crate::clip::{ClipChainInstance, ClipTree}; use crate::frame_builder::FrameBuilderConfig; -use crate::picture::{PictureCompositeMode, ClusterFlags, SurfaceInfo}; -use crate::tile_cache::TileCacheInstance; -use crate::picture::{SurfaceIndex, RasterConfig}; -use crate::tile_cache::SubSliceIndex; +use crate::picture::{PictureCompositeMode, ClusterFlags, SurfaceInfo, TileCacheInstance}; +use crate::picture::{SurfaceIndex, RasterConfig, SubSliceIndex}; use crate::prim_store::{ClipTaskIndex, PictureIndex, PrimitiveInstanceKind}; use crate::prim_store::{PrimitiveStore, PrimitiveInstance}; use crate::render_backend::{DataStores, ScratchBuffer};