tor-browser

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

commit e82e1453748b162494795ee283ac56da853cb5a6
parent 448ac4309f904559eed37e781e7a1f92a4a26154
Author: Nicolas Silva <nical@fastmail.com>
Date:   Fri, 12 Dec 2025 14:44:32 +0000

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

Differential Revision: https://phabricator.services.mozilla.com/D275938

Diffstat:
Mgfx/wr/webrender/src/composite.rs | 3++-
Mgfx/wr/webrender/src/frame_builder.rs | 3++-
Mgfx/wr/webrender/src/picture.rs | 4046+++----------------------------------------------------------------------------
Mgfx/wr/webrender/src/picture_graph.rs | 2+-
Mgfx/wr/webrender/src/prepare.rs | 5+++--
Mgfx/wr/webrender/src/render_backend.rs | 3++-
Mgfx/wr/webrender/src/render_target.rs | 3++-
Mgfx/wr/webrender/src/surface.rs | 612+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mgfx/wr/webrender/src/tile_cache/mod.rs | 3280++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mgfx/wr/webrender/src/tile_cache/slice_builder.rs | 4++--
Mgfx/wr/webrender/src/visibility.rs | 3++-
11 files changed, 4000 insertions(+), 3964 deletions(-)

diff --git a/gfx/wr/webrender/src/composite.rs b/gfx/wr/webrender/src/composite.rs @@ -10,7 +10,8 @@ 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, TileCacheInstance, TileSurface}; +use crate::picture::{ImageDependency, ResolvedSurfaceTexture}; +use crate::tile_cache::{TileCacheInstance, TileSurface}; use crate::tile_cache::TileId; use crate::prim_store::DeferredResolve; use crate::resource_cache::{ImageRequest, ResourceCache}; diff --git a/gfx/wr/webrender/src/frame_builder.rs b/gfx/wr/webrender/src/frame_builder.rs @@ -16,7 +16,8 @@ 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, SliceId, TileCacheInstance}; +use crate::picture::{DirtyRegion}; +use crate::tile_cache::{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/picture.rs b/gfx/wr/webrender/src/picture.rs @@ -94,3393 +94,133 @@ //! blend the overlay tile (this is not always optimal right now, but will be //! improved as a follow up). -use api::{BorderRadius, ClipMode, MixBlendMode, PremultipliedColorF, SVGFE_GRAPH_MAX}; -use api::{PropertyBindingId, FilterOpGraphPictureBufferId, RasterSpace}; -use api::{DebugFlags, ImageKey, ColorF, PrimitiveFlags, SnapshotInfo}; -use api::{ImageRendering, ColorDepth, YuvRangedColorSpace, YuvFormat, AlphaType}; +use api::{MixBlendMode, PremultipliedColorF, SVGFE_GRAPH_MAX}; +use api::{FilterOpGraphPictureBufferId, RasterSpace}; +use api::{DebugFlags, ColorF, PrimitiveFlags, SnapshotInfo}; 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::{ClipChainInstance, ClipItemKind, ClipLeafId, ClipNodeId, ClipSpaceConversion, ClipStore, ClipTreeBuilder}; -use crate::profiler::{self, add_text_marker, TransactionProfile}; +use crate::clip::{ClipNodeId, ClipTreeBuilder}; +use crate::profiler::add_text_marker; use crate::spatial_tree::{SpatialTree, CoordinateSpaceMapping, SpatialNodeIndex, VisibleFace}; -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::composite::{tile_kind, CompositeState, CompositeTileSurface, CompositorKind, NativeTileId}; +use crate::composite::{CompositeTileDescriptor, CompositeTile}; use crate::debug_colors; use euclid::{vec3, Scale, Vector2D, Box2D}; -use euclid::approxeq::ApproxEq; -use crate::intern::ItemUid; -use crate::internal_types::{FastHashMap, PlaneSplitter, FilterGraphOp, FilterGraphNode, Filter, FrameId}; +use crate::internal_types::{FastHashMap, PlaneSplitter, FilterGraphOp, FilterGraphNode, Filter}; use crate::internal_types::{PlaneSplitterIndex, PlaneSplitAnchor, TextureSource}; use crate::frame_builder::{FrameBuildingContext, FrameBuildingState, PictureState, PictureContext}; -use crate::gpu_types::{UvRectKind, ZBufferId, BlurEdgeMode, BrushSegmentGpuData, ImageBrushPrimitiveData}; -use peek_poke::{poke_into_vec, ensure_red_zone}; -use plane_split::{Clipper, Polygon}; -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, ImageRequest}; -use crate::space::SpaceMapper; -use crate::scene::SceneProperties; -use crate::spatial_tree::CoordinateSystemId; -use crate::surface::{SurfaceDescriptor, SurfaceTileDescriptor}; -use smallvec::SmallVec; -use std::{mem, u8, u32}; -use std::fmt::{Display, Error, Formatter}; -use std::ops::Range; -use crate::picture_textures::PictureCacheTextureHandle; -use crate::util::{MaxRect, MatrixHelpers, Recycler, ScaleOffset}; -use crate::filterdata::FilterDataHandle; -use crate::tile_cache::{SliceDebugInfo, TileDebugInfo, DirtyTileDebugInfo}; -use crate::tile_cache::{TileKey, TileId, TileRect, TileOffset, SubSliceIndex}; -use crate::invalidation::InvalidationReason; -use crate::tile_cache::{MAX_SURFACE_SIZE, MAX_COMPOSITOR_SURFACES}; -use crate::tile_cache::{TileDescriptor, PrimitiveDescriptor, PrimitiveDependencyIndex}; -use crate::tile_cache::{TILE_SIZE_SCROLLBAR_VERTICAL, TILE_SIZE_SCROLLBAR_HORIZONTAL}; -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}; -use crate::invalidation::dependency::{ - OpacityBindingInfo, ColorBindingInfo, - OpacityBinding, ColorBinding, - PrimitiveDependency, - SpatialNodeComparer, - PrimitiveComparisonKey, PrimitiveComparer, -}; - -use crate::invalidation::PrimitiveCompareResult; - -// 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, - }, -} - -/// Maximum size of a compositor surface. -const MAX_COMPOSITOR_SURFACES_SIZE: f32 = 8192.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) -} - -// 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 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", - } - } -} - -/// 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(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. - 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], -} - -/// 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, -} - -/// 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); - } - } - } - } - } - - // 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. - 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); - } - } +use crate::gpu_types::{BlurEdgeMode, BrushSegmentGpuData, ImageBrushPrimitiveData}; - // 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, - ); - } - } - } - } - } +use plane_split::{Clipper, Polygon}; +use crate::prim_store::{PictureIndex, PrimitiveInstance, PrimitiveInstanceKind}; +use crate::prim_store::PrimitiveScratchBuffer; +use crate::print_tree::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::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 smallvec::SmallVec; +use std::{mem, u8, u32}; +use std::ops::Range; +use crate::picture_textures::PictureCacheTextureHandle; +use crate::util::{MaxRect, 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 core::time::Duration; - 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, - }; +pub use crate::invalidation::DirtyRegion; +pub use crate::invalidation::dependency::ImageDependency; +pub use crate::invalidation::quadtree::{TileNode, TileNodeKind}; - let mut state = TilePostUpdateState { - resource_cache, - composite_state, - }; +// Maximum blur radius for blur filter (different than box-shadow blur). +// Taken from FilterNodeSoftware.cpp in Gecko. +const MAX_BLUR_RADIUS: f32 = 100.; - 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); - } +/// Maximum size of a compositor surface. +pub const MAX_COMPOSITOR_SURFACES_SIZE: f32 = 8192.0; - for compositor_surface in sub_slice.compositor_surfaces.iter_mut().rev() { - compositor_surface.descriptor.z_id = state.composite_state.z_generator.next(); - } +pub fn clamp(value: i32, low: i32, high: i32) -> i32 { + value.max(low).min(high) +} - ctx.z_id = state.composite_state.z_generator.next(); +pub fn clampf(value: f32, low: f32, high: f32) -> f32 { + value.max(low).min(high) +} - for tile in sub_slice.tiles.values_mut() { - tile.post_update(&ctx, &mut state, frame_context); - } - } +/// 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>, + }, +} - // Assign z-order for each underlay - for underlay in self.underlays.iter_mut().rev() { - underlay.z_id = state.composite_state.z_generator.next(); - } +/// 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, + } +} - // 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). +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()); - // 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, - ); + ResolvedSurfaceTexture::TextureCache { texture } } - } - - 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, - ); - } + SurfaceTextureDescriptor::Native { id } => { + ResolvedSurfaceTexture::Native { + id: id.expect("bug: native surface not allocated"), + size, } } } - - // 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, - ); - } - } } } @@ -3506,259 +246,6 @@ 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 { @@ -6737,16 +3224,6 @@ impl PicturePrimitive { } } -/// 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, -} - 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>>>( @@ -6773,7 +3250,7 @@ impl CompositeState { } } -fn get_relative_scale_offset( +pub fn get_relative_scale_offset( child_spatial_node_index: SpatialNodeIndex, parent_spatial_node_index: SpatialNodeIndex, spatial_tree: &SpatialTree, @@ -6798,319 +3275,6 @@ 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::picture::{TileCacheInstance, SliceId}; +use crate::tile_cache::{TileCacheInstance, SliceId}; use smallvec::SmallVec; #[derive(Debug)] diff --git a/gfx/wr/webrender/src/prepare.rs b/gfx/wr/webrender/src/prepare.rs @@ -23,8 +23,9 @@ 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, SliceId}; -use crate::picture::{PrimitiveList, PrimitiveCluster, SurfaceIndex, TileCacheInstance, SubpixelMode, Picture3DContext}; +use crate::picture::{ClusterFlags, PictureCompositeMode, PicturePrimitive}; +use crate::picture::{PrimitiveList, PrimitiveCluster, SurfaceIndex, SubpixelMode, Picture3DContext}; +use crate::tile_cache::{SliceId, TileCacheInstance}; use crate::prim_store::line_dec::MAX_LINE_DECORATION_RESOLUTION; use crate::prim_store::*; use crate::quad; diff --git a/gfx/wr/webrender/src/render_backend.rs b/gfx/wr/webrender/src/render_backend.rs @@ -36,7 +36,8 @@ 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, SliceId, TileCacheInstance, TileCacheParams, SurfaceInfo, RasterConfig}; +use crate::picture::{PictureScratchBuffer, SurfaceInfo, RasterConfig}; +use crate::tile_cache::{SliceId, TileCacheInstance, TileCacheParams}; 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 @@ -17,7 +17,8 @@ use crate::gpu_types::{BorderInstance, SVGFEFilterInstance, BlurDirection, BlurI use crate::gpu_types::{TransformPalette, ZBufferIdGenerator, MaskInstance, ClipSpace, BlurEdgeMode}; use crate::gpu_types::{ZBufferId, QuadSegment, PrimitiveInstanceData, TransformPaletteId}; use crate::internal_types::{CacheTextureId, FastHashMap, FilterGraphOp, FrameAllocator, FrameMemory, FrameVec, TextureSource}; -use crate::picture::{SliceId, SurfaceInfo, ResolvedSurfaceTexture, TileCacheInstance}; +use crate::picture::{SurfaceInfo, ResolvedSurfaceTexture}; +use crate::tile_cache::{SliceId, TileCacheInstance}; use crate::quad; use crate::prim_store::{PrimitiveInstance, PrimitiveStore, PrimitiveScratchBuffer}; use crate::prim_store::gradient::{ diff --git a/gfx/wr/webrender/src/surface.rs b/gfx/wr/webrender/src/surface.rs @@ -2,24 +2,302 @@ * 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::internal_types::FastHashMap; -use crate::picture::{SurfaceIndex, SurfaceInfo}; +use crate::gpu_types::UvRectKind; +use crate::internal_types::{FastHashMap, Filter}; +use crate::picture::PictureCompositeMode; use crate::tile_cache::{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::visibility::{VisibilityState, PrimitiveVisibility}; +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, +} -/* - 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))] @@ -618,3 +896,317 @@ 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 = 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, + } +} diff --git a/gfx/wr/webrender/src/tile_cache/mod.rs b/gfx/wr/webrender/src/tile_cache/mod.rs @@ -12,11 +12,43 @@ // 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::FrameId; -use peek_poke::PeekPoke; -use std::{marker, u32}; +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::{ @@ -233,3 +265,3245 @@ impl TileDescriptor { 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 @@ -7,8 +7,8 @@ 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, PictureFlags}; +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; diff --git a/gfx/wr/webrender/src/visibility.rs b/gfx/wr/webrender/src/visibility.rs @@ -17,7 +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, TileCacheInstance}; +use crate::picture::{PictureCompositeMode, ClusterFlags, SurfaceInfo}; +use crate::tile_cache::TileCacheInstance; use crate::picture::{SurfaceIndex, RasterConfig}; use crate::tile_cache::SubSliceIndex; use crate::prim_store::{ClipTaskIndex, PictureIndex, PrimitiveInstanceKind};