Source code

Revision control

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at */
//! A picture represents a dynamically rendered image.
//! # Overview
//! Pictures consists of:
//! - A number of primitives that are drawn onto the picture.
//! - A composite operation describing how to composite this
//! picture into its parent.
//! - A configuration describing how to draw the primitives on
//! this picture (e.g. in screen space or local space).
//! The tree of pictures are generated during scene building.
//! Depending on their composite operations pictures can be rendered into
//! intermediate targets or folded into their parent picture.
//! ## Picture caching
//! Pictures can be cached to reduce the amount of rasterization happening per
//! frame.
//! When picture caching is enabled, the scene is cut into a small number of slices,
//! typically:
//! - content slice
//! - UI slice
//! - background UI slice which is hidden by the other two slices most of the time.
//! Each of these slice is made up of fixed-size large tiles of 2048x512 pixels
//! (or 128x128 for the UI slice).
//! Tiles can be either cached rasterized content into a texture or "clear tiles"
//! that contain only a solid color rectangle rendered directly during the composite
//! pass.
//! ## Invalidation
//! Each tile keeps track of the elements that affect it, which can be:
//! - primitives
//! - clips
//! - image keys
//! - opacity bindings
//! - transforms
//! These dependency lists are built each frame and compared to the previous frame to
//! see if the tile changed.
//! The tile's primitive dependency information is organized in a quadtree, each node
//! storing an index buffer of tile primitive dependencies.
//! The union of the invalidated leaves of each quadtree produces a per-tile dirty rect
//! which defines the scissor rect used when replaying the tile's drawing commands and
//! can be used for partial present.
//! ## Display List shape
//! WR will first look for an iframe item in the root stacking context to apply
//! picture caching to. If that's not found, it will apply to the entire root
//! stacking context of the display list. Apart from that, the format of the
//! display list is not important to picture caching. Each time a new scroll root
//! is encountered, a new picture cache slice will be created. If the display
//! list contains more than some arbitrary number of slices (currently 8), the
//! content will all be squashed into a single slice, in order to save GPU memory
//! and compositing performance.
//! ## Compositor Surfaces
//! Sometimes, a primitive would prefer to exist as a native compositor surface.
//! This allows a large and/or regularly changing primitive (such as a video, or
//! webgl canvas) to be updated each frame without invalidating the content of
//! tiles, and can provide a significant performance win and battery saving.
//! Since drawing a primitive as a compositor surface alters the ordering of
//! primitives in a tile, we use 'overlay tiles' to ensure correctness. If a
//! tile has a compositor surface, _and_ that tile has primitives that overlap
//! the compositor surface rect, the tile switches to be drawn in alpha mode.
//! We rely on only promoting compositor surfaces that are opaque primitives.
//! With this assumption, the tile(s) that intersect the compositor surface get
//! a 'cutout' in the rectangle where the compositor surface exists (not the
//! entire tile), allowing that tile to be drawn as an alpha tile after the
//! compositor surface.
//! Tiles are only drawn in overlay mode if there is content that exists on top
//! of the compositor surface. Otherwise, we can draw the tiles in the normal fast
//! path before the compositor surface is drawn. Use of the per-tile valid and
//! dirty rects ensure that we do a minimal amount of per-pixel work here to
//! blend the overlay tile (this is not always optimal right now, but will be
//! improved as a follow up).
use api::{MixBlendMode, PremultipliedColorF, FilterPrimitiveKind};
use api::{PropertyBinding, PropertyBindingId, FilterPrimitive};
use api::{DebugFlags, ImageKey, ColorF, ColorU, PrimitiveFlags};
use api::{ImageRendering, ColorDepth, YuvRangedColorSpace, YuvFormat, AlphaType};
use api::units::*;
use crate::batch::BatchFilter;
use crate::box_shadow::BLUR_SAMPLE_SCALE;
use crate::clip::{ClipStore, ClipChainInstance, ClipChainId, ClipInstance};
use crate::spatial_tree::{SpatialTree, CoordinateSpaceMapping, SpatialNodeIndex, VisibleFace};
use crate::composite::{CompositorKind, CompositeState, NativeSurfaceId, NativeTileId, CompositeTileSurface, tile_kind};
use crate::composite::{ExternalSurfaceDescriptor, ExternalSurfaceDependency, CompositeTileDescriptor, CompositeTile};
use crate::composite::{CompositorTransformIndex};
use crate::debug_colors;
use euclid::{vec2, vec3, Point2D, Scale, Vector2D, Box2D, Transform3D, SideOffsets2D};
use euclid::approxeq::ApproxEq;
use crate::filterdata::SFilterData;
use crate::intern::ItemUid;
use crate::internal_types::{FastHashMap, FastHashSet, PlaneSplitter, Filter, FrameId};
use crate::internal_types::{PlaneSplitterIndex, PlaneSplitAnchor, TextureSource};
use crate::frame_builder::{FrameBuildingContext, FrameBuildingState, PictureState, PictureContext};
use crate::gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
use crate::gpu_types::{UvRectKind, ZBufferId};
use plane_split::{Clipper, Polygon, Splitter};
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;
use crate::resource_cache::{ResourceCache, ImageGeneration, ImageRequest};
use crate::space::SpaceMapper;
use crate::scene::SceneProperties;
use smallvec::SmallVec;
use std::{mem, u8, marker, u32};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::collections::hash_map::Entry;
use std::ops::Range;
use crate::picture_textures::PictureCacheTextureHandle;
use crate::util::{MaxRect, VecHelper, MatrixHelpers, Recycler, raster_rect_to_device_pixels, ScaleOffset};
use crate::filterdata::{FilterDataHandle};
use crate::tile_cache::{SliceDebugInfo, TileDebugInfo, DirtyTileDebugInfo};
use crate::visibility::{PrimitiveVisibilityFlags, FrameVisibilityContext};
use crate::visibility::{VisibilityState, FrameVisibilityState};
#[cfg(any(feature = "capture", feature = "replay"))]
use ron;
#[cfg(feature = "capture")]
use crate::scene_builder_thread::InternerUpdates;
#[cfg(any(feature = "capture", feature = "replay"))]
use crate::intern::{Internable, UpdateList};
#[cfg(any(feature = "capture", feature = "replay"))]
use crate::clip::{ClipIntern, PolygonIntern};
#[cfg(any(feature = "capture", feature = "replay"))]
use crate::filterdata::FilterDataIntern;
#[cfg(any(feature = "capture", feature = "replay"))]
use api::PrimitiveKeyKind;
#[cfg(any(feature = "capture", feature = "replay"))]
use crate::prim_store::backdrop::Backdrop;
#[cfg(any(feature = "capture", feature = "replay"))]
use crate::prim_store::borders::{ImageBorder, NormalBorderPrim};
#[cfg(any(feature = "capture", feature = "replay"))]
use crate::prim_store::gradient::{LinearGradient, RadialGradient, ConicGradient};
#[cfg(any(feature = "capture", feature = "replay"))]
use crate::prim_store::image::{Image, YuvImage};
#[cfg(any(feature = "capture", feature = "replay"))]
use crate::prim_store::line_dec::LineDecoration;
#[cfg(any(feature = "capture", feature = "replay"))]
use crate::prim_store::picture::Picture;
#[cfg(any(feature = "capture", feature = "replay"))]
use crate::prim_store::text_run::TextRun;
#[cfg(feature = "capture")]
use std::fs::File;
#[cfg(feature = "capture")]
use std::io::prelude::*;
#[cfg(feature = "capture")]
use std::path::PathBuf;
use crate::scene_building::{SliceFlags};
#[cfg(feature = "replay")]
// used by tileview so don't use an internal_types FastHashMap
use std::collections::HashMap;
// Maximum blur radius for blur filter (different than box-shadow blur).
// Taken from FilterNodeSoftware.cpp in Gecko.
pub 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
/// Subpixel AA text cannot be drawn on this surface
/// Subpixel AA can be drawn on this surface, if not intersecting
/// with the excluded regions, and inside the allowed rect.
Conditional {
allowed_rect: PictureRect,
/// A comparable transform matrix, that compares with epsilon checks.
#[derive(Debug, Clone)]
struct MatrixKey {
m: [f32; 16],
impl PartialEq for MatrixKey {
fn eq(&self, other: &Self) -> bool {
const EPSILON: f32 = 0.001;
// TODO(gw): It's possible that we may need to adjust the epsilon
// to be tighter on most of the matrix, except the
// translation parts?
for (i, j) in self.m.iter().zip(other.m.iter()) {
if !i.approx_eq_eps(j, &EPSILON) {
return false;
/// A comparable scale-offset, that compares with epsilon checks.
#[derive(Debug, Clone)]
struct ScaleOffsetKey {
sx: f32,
sy: f32,
tx: f32,
ty: f32,
impl PartialEq for ScaleOffsetKey {
fn eq(&self, other: &Self) -> bool {
const EPSILON: f32 = 0.001;, &EPSILON) &&, &EPSILON) &&
self.tx.approx_eq_eps(&other.tx, &EPSILON) &&
self.ty.approx_eq_eps(&other.ty, &EPSILON)
/// A comparable / hashable version of a coordinate space mapping. Used to determine
/// if a transform dependency for a tile has changed.
#[derive(Debug, PartialEq, Clone)]
enum TransformKey {
ScaleOffset {
so: ScaleOffsetKey,
Transform {
m: MatrixKey,
impl<Src, Dst> From<CoordinateSpaceMapping<Src, Dst>> for TransformKey {
fn from(transform: CoordinateSpaceMapping<Src, Dst>) -> TransformKey {
match transform {
CoordinateSpaceMapping::Local => {
CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
TransformKey::ScaleOffset {
so: ScaleOffsetKey {
sx: scale_offset.scale.x,
sy: scale_offset.scale.y,
tx: scale_offset.offset.x,
ty: scale_offset.offset.y,
CoordinateSpaceMapping::Transform(ref m) => {
TransformKey::Transform {
m: MatrixKey {
m: m.to_array(),
/// Unit for tile coordinates.
#[derive(Hash, Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct TileCoordinate;
// Geometry types for tile coordinates.
pub type TileOffset = Point2D<i32, TileCoordinate>;
pub type TileRect = Box2D<i32, TileCoordinate>;
/// The maximum number of compositor surfaces that are allowed per picture cache. This
/// is an arbitrary number that should be enough for common cases, but low enough to
/// prevent performance and memory usage drastically degrading in pathological cases.
/// The size in device pixels of a normal cached tile.
pub const TILE_SIZE_DEFAULT: DeviceIntSize = DeviceIntSize {
width: 1024,
height: 512,
_unit: marker::PhantomData,
/// The size in device pixels of a tile for horizontal scroll bars
pub const TILE_SIZE_SCROLLBAR_HORIZONTAL: DeviceIntSize = DeviceIntSize {
width: 1024,
height: 32,
_unit: marker::PhantomData,
/// The size in device pixels of a tile for vertical scroll bars
pub const TILE_SIZE_SCROLLBAR_VERTICAL: DeviceIntSize = DeviceIntSize {
width: 32,
height: 1024,
_unit: marker::PhantomData,
/// The maximum size per axis of a surface,
/// in WorldPixel coordinates.
const MAX_SURFACE_SIZE: f32 = 4096.0;
/// Maximum size of a compositor surface.
/// The maximum number of sub-dependencies (e.g. clips, transforms) we can handle
/// per-primitive. If a primitive has more than this, it will invalidate every frame.
const MAX_PRIM_SUB_DEPS: usize = u8::MAX as usize;
/// Used to get unique tile IDs, even when the tile cache is
/// destroyed between display lists / scenes.
static NEXT_TILE_ID: AtomicUsize = AtomicUsize::new(0);
fn clamp(value: i32, low: i32, high: i32) -> i32 {
fn clampf(value: f32, low: f32, high: f32) -> f32 {
/// Clamps the blur radius depending on scale factors.
fn clamp_blur_radius(blur_radius: f32, scale_factors: (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.
// TODO: the clamping should be done on a per-axis basis, but WR currently only supports
// having a single value for both x and y blur.
let largest_scale_factor = f32::max(scale_factors.0, scale_factors.1);
let scaled_blur_radius = blur_radius * largest_scale_factor;
if scaled_blur_radius > MAX_BLUR_RADIUS {
MAX_BLUR_RADIUS / largest_scale_factor
} else {
// Return the original blur radius to avoid any rounding errors
/// An index into the prims array in a TileDescriptor.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct PrimitiveDependencyIndex(pub u32);
/// Information about the state of a binding.
pub struct BindingInfo<T> {
/// The current value retrieved from dynamic scene properties.
value: T,
/// True if it was changed (or is new) since the last frame build.
changed: bool,
/// Information stored in a tile descriptor for a binding.
#[derive(Debug, PartialEq, Clone, Copy)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub enum Binding<T> {
impl<T> From<PropertyBinding<T>> for Binding<T> {
fn from(binding: PropertyBinding<T>) -> Binding<T> {
match binding {
PropertyBinding::Binding(key, _) => Binding::Binding(,
PropertyBinding::Value(value) => Binding::Value(value),
pub type OpacityBinding = Binding<f32>;
pub type OpacityBindingInfo = BindingInfo<f32>;
pub type ColorBinding = Binding<ColorU>;
pub type ColorBindingInfo = BindingInfo<ColorU>;
/// A dependency for a transform is defined by the spatial node index + frame it was used
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct SpatialNodeKey {
spatial_node_index: SpatialNodeIndex,
frame_id: FrameId,
/// A helper for comparing spatial nodes between frames. The comparisons
/// are done by-value, so that if the shape of the spatial node tree
/// changes, invalidations aren't done simply due to the spatial node
/// index changing between display lists.
struct SpatialNodeComparer {
/// The root spatial node index of the tile cache
ref_spatial_node_index: SpatialNodeIndex,
/// Maintains a map of currently active transform keys
spatial_nodes: FastHashMap<SpatialNodeKey, TransformKey>,
/// A cache of recent comparisons between prev and current spatial nodes
compare_cache: FastHashMap<(SpatialNodeKey, SpatialNodeKey), bool>,
/// A set of frames that we need to retain spatial node entries for
referenced_frames: FastHashSet<FrameId>,
impl SpatialNodeComparer {
/// Construct a new comparer
fn new() -> Self {
SpatialNodeComparer {
ref_spatial_node_index: SpatialNodeIndex::INVALID,
spatial_nodes: FastHashMap::default(),
compare_cache: FastHashMap::default(),
referenced_frames: FastHashSet::default(),
/// Advance to the next frame
fn next_frame(
&mut self,
ref_spatial_node_index: SpatialNodeIndex,
) {
// Drop any node information for unreferenced frames, to ensure that the
// hashmap doesn't grow indefinitely!
let referenced_frames = &self.referenced_frames;
self.spatial_nodes.retain(|key, _| {
// Update the root spatial node for this comparer
self.ref_spatial_node_index = ref_spatial_node_index;
/// Register a transform that is used, and build the transform key for it if new.
fn register_used_transform(
&mut self,
spatial_node_index: SpatialNodeIndex,
frame_id: FrameId,
spatial_tree: &SpatialTree,
) {
let key = SpatialNodeKey {
if let Entry::Vacant(entry) = self.spatial_nodes.entry(key) {
/// Return true if the transforms for two given spatial nodes are considered equivalent
fn are_transforms_equivalent(
&mut self,
prev_spatial_node_key: &SpatialNodeKey,
curr_spatial_node_key: &SpatialNodeKey,
) -> bool {
let key = (*prev_spatial_node_key, *curr_spatial_node_key);
let spatial_nodes = &self.spatial_nodes;
.or_insert_with(|| {
let prev = &spatial_nodes[&prev_spatial_node_key];
let curr = &spatial_nodes[&curr_spatial_node_key];
curr == prev
/// Ensure that the comparer won't GC any nodes for a given frame id
fn retain_for_frame(&mut self, frame_id: FrameId) {
// 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 post_update
struct TilePostUpdateContext<'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,
/// 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>,
/// 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>,
/// Current size in device pixels of tiles for this cache
current_tile_size: DeviceIntSize,
/// The local rect of the overall picture cache
local_rect: PictureRect,
/// Pre-allocated z-id to assign to tiles during post_update.
z_id: ZBufferId,
/// 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 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,
/// 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,
/// 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 {
images: SmallVec::new(),
opacity_bindings: SmallVec::new(),
color_binding: None,
clips: SmallVec::new(),
spatial_nodes: SmallVec::new(),
/// A stable ID for a given tile, to help debugging. These are also used
/// as unique identifiers for tile surfaces when using a native compositor.
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd, Ord, Eq)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct TileId(pub usize);
/// A descriptor for the kind of texture that a picture cache tile will
/// be drawn into.
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(
resource_cache: &ResourceCache,
size: DeviceIntSize,
) -> ResolvedSurfaceTexture {
match self {
SurfaceTextureDescriptor::TextureCache { handle } => {
let texture = resource_cache
ResolvedSurfaceTexture::TextureCache { texture }
SurfaceTextureDescriptor::Native { id } => {
ResolvedSurfaceTexture::Native {
id: id.expect("bug: native surface not allocated"),
/// The backing surface for this tile.
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",
TileSurface::Clear => "Clear",
/// Optional extra information returned by is_same when
/// logging is enabled.
#[derive(Debug, Copy, Clone, PartialEq)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub enum CompareHelperResult<T> {
/// Primitives match
/// Counts differ
Count {
prev_count: u8,
curr_count: u8,
/// Sentinel
/// Two items are not equal
NotEqual {
prev: T,
curr: T,
/// User callback returned true on item
PredicateTrue {
curr: T
/// The result of a primitive dependency comparison. Size is a u8
/// since this is a hot path in the code, and keeping the data small
/// is a performance win.
#[derive(Debug, Copy, Clone, PartialEq)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub enum PrimitiveCompareResult {
/// Primitives match
/// Something in the PrimitiveDescriptor was different
/// The clip node content or spatial node changed
/// The value of the transform changed
/// An image dependency was dirty
/// The value of an opacity binding changed
/// The value of a color binding changed
/// A more detailed version of PrimitiveCompareResult used when
/// debug logging is enabled.
#[derive(Debug, Copy, Clone, PartialEq)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub enum PrimitiveCompareResultDetail {
/// Primitives match
/// Something in the PrimitiveDescriptor was different
Descriptor {
old: PrimitiveDescriptor,
new: PrimitiveDescriptor,
/// The clip node content or spatial node changed
Clip {
detail: CompareHelperResult<ItemUid>,
/// The value of the transform changed
Transform {
detail: CompareHelperResult<SpatialNodeKey>,
/// An image dependency was dirty
Image {
detail: CompareHelperResult<ImageDependency>,
/// The value of an opacity binding changed
OpacityBinding {
detail: CompareHelperResult<OpacityBinding>,
/// The value of a color binding changed
ColorBinding {
detail: CompareHelperResult<ColorBinding>,
/// Debugging information about why a tile was invalidated
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub enum InvalidationReason {
/// The background color changed
BackgroundColor {
old: Option<ColorF>,
new: Option<ColorF>,
/// The opaque state of the backing native surface changed
became_opaque: bool
/// There was no backing texture (evicted or never rendered)
/// There was no backing native surface (never rendered, or recreated)
/// The primitive count in the dependency list was different
PrimCount {
old: Option<Vec<ItemUid>>,
new: Option<Vec<ItemUid>>,
/// The content of one of the primitives was different
Content {
/// What changed in the primitive that was different
prim_compare_result: PrimitiveCompareResult,
prim_compare_result_detail: Option<PrimitiveCompareResultDetail>,
// The compositor type changed
// The valid region of the tile changed
// The overall scale of the picture cache changed
/// A minimal subset of Tile for debug capturing
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct TileSerializer {
pub rect: PictureRect,
pub current_descriptor: TileDescriptor,
pub id: TileId,
pub root: TileNode,
pub background_color: Option<ColorF>,
pub invalidation_reason: Option<InvalidationReason>
/// A minimal subset of TileCacheInstance for debug capturing
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct TileCacheInstanceSerializer {
pub slice: usize,
pub tiles: FastHashMap<TileOffset, TileSerializer>,
pub background_color: Option<ColorF>,
/// 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,
/// The last frame this tile had its dependencies updated (dependency updating is
/// skipped if a tile is off-screen).
pub last_updated_frame_id: FrameId,
impl Tile {
/// Construct a new, invalid tile.
fn new(tile_offset: TileOffset) -> Self {
let id = TileId(NEXT_TILE_ID.fetch_add(1, Ordering::Relaxed));
Tile {
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,
is_opaque: false,
root: TileNode::new_leaf(Vec::new()),
background_color: None,
invalidation_reason: None,
local_valid_rect: PictureBox2D::zero(),
z_id: ZBufferId::invalid(),
last_updated_frame_id: FrameId::INVALID,
/// Print debug information about this tile to a tree printer.
fn print(&self, pt: &mut dyn PrintTreePrinter) {
pt.new_level(format!("Tile {:?}",;
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));
/// Check if the content of the previous and current tile descriptors match
fn update_dirty_rects(
&mut self,
ctx: &TilePostUpdateContext,
state: &mut TilePostUpdateState,
invalidation_reason: &mut Option<InvalidationReason>,
frame_context: &FrameVisibilityContext,
) -> PictureRect {
let mut prim_comparer = PrimitiveComparer::new(
let mut dirty_rect = PictureBox2D::zero();
&mut prim_comparer,
&mut 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: &TilePostUpdateContext,
state: &mut TilePostUpdateState,
frame_context: &FrameVisibilityContext,
) {
// Check if the contents of the primitives, clips, and
// other dependencies are the same.
let mut invalidation_reason = None;
let dirty_rect = self.update_dirty_rects(
&mut invalidation_reason,
if !dirty_rect.is_empty() {
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::from_origin_and_size(
self.tile_offset.x as f32 * ctx.tile_size.width,
self.tile_offset.y 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.world_tile_rect = ctx.pic_to_world_mapper
.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 {
if ctx.background_color != self.background_color {
self.invalidate(None, InvalidationReason::BackgroundColor {
old: self.background_color,
new: ctx.background_color });
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.
&mut self.current_descriptor,
&mut self.prev_descriptor,
// Since this tile is determined to be visible, it will get updated
// dependencies, so update the frame id we are storing dependencies for.
self.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 {
// 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);
// Include any image keys this tile depends on.
// Include any opacity bindings this primitive depends on.
// Include any clip nodes that this primitive depends on.
// Include any transforms that this primitive depends on.
for spatial_node_index in &info.spatial_nodes {
SpatialNodeKey {
spatial_node_index: *spatial_node_index,
frame_id: self.last_updated_frame_id,
// Include any color bindings this primitive depends on.
if info.color_binding.is_some() {
self.current_descriptor.color_bindings.len(), info.color_binding.unwrap());
// 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(
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),
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);
// We know that the casts below will never overflow because the array lengths are
// truncated to MAX_PRIM_SUB_DEPS during update_prim_dependencies.
debug_assert!(info.spatial_nodes.len() <= MAX_PRIM_SUB_DEPS);
debug_assert!(info.clips.len() <= MAX_PRIM_SUB_DEPS);
debug_assert!(info.images.len() <= MAX_PRIM_SUB_DEPS);
debug_assert!(info.opacity_bindings.len() <= MAX_PRIM_SUB_DEPS);
self.current_descriptor.prims.push(PrimitiveDescriptor {
prim_uid: info.prim_uid,
transform_dep_count: info.spatial_nodes.len() as u8,
clip_dep_count: info.clips.len() as u8,
image_dep_count: info.images.len() as u8,
opacity_binding_dep_count: info.opacity_bindings.len() as u8,
color_binding_dep_count: if info.color_binding.is_some() { 1 } else { 0 } as u8,
// 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 post_update(
&mut self,
ctx: &TilePostUpdateContext,
state: &mut TilePostUpdateState,
frame_context: &FrameVisibilityContext,
) -> bool {
// 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.
// 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 false;
// 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
.and_then(|r| r.intersection(&self.current_descriptor.local_valid_rect))
// 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
.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)
// Invalidate the tile based on the content changing.
self.update_content_validity(ctx, state, frame_context);
// 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() {
self.is_visible = false;
return false;
// 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
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 is_opaque = has_opaque_bg_color || has_opaque_backdrop;
// 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() {
// Invalidate the entire tile to force a redraw.
self.invalidate(None, InvalidationReason::SurfaceOpacityChanged { became_opaque: is_opaque });
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 { .. } => {
(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
// 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 &&
// 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 {
Some(BackdropKind::Clear) => {
None => {
// This should be prevented by the is_simple_prim check above.
} 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 {
Some(TileSurface::Color { .. }) | Some(TileSurface::Clear) | 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 { .. } => {
// 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 {
// Store the current surface backing info for use during batching.
self.surface = Some(surface);
/// Defines a key that uniquely identifies a primitive instance.
#[derive(Debug, Copy, Clone)]
#[cfg_attr(feature = "capture", derive(Serialize))]
#[cfg_attr(feature = "replay", derive(Deserialize))]
pub struct PrimitiveDescriptor {
/// Uniquely identifies the content of the primitive template.
pub prim_uid: ItemUid,
/// The clip rect for this primitive. Included here in
/// dependencies since there is no entry in the clip chain
/// dependencies for the local clip rect.
pub prim_clip_box: PictureBox2D,
/// The number of extra dependencies that this primitive has.
transform_dep_count: u8,
image_dep_count: u8,
opacity_binding_dep_count: u8,
clip_dep_count: u8,
color_binding_dep_count: u8,
impl PartialEq for PrimitiveDescriptor {
fn eq(&self, other: &Self) -> bool {
const EPSILON: f32 = 0.001;
if self.prim_uid != other.prim_uid {
return false;
if !self.prim_clip_box.min.x.approx_eq_eps(&other.prim_clip_box.min.x, &EPSILON) {
return false;
if !self.prim_clip_box.min.y.approx_eq_eps(&other.prim_clip_box.min.y, &EPSILON) {
return false;
if !self.prim_clip_box.max.x.approx_eq_eps(&other.prim_clip_box.max.x, &EPSILON) {
return false;
if !self.prim_clip_box.max.y.approx_eq_eps(&other.prim_clip_box.max.y, &EPSILON) {
return false;
/// A small helper to compare two arrays of primitive dependencies.
struct CompareHelper<'a, T> where T: Copy {
offset_curr: usize,
offset_prev: usize,
curr_items: &'a [T],
prev_items: &'a [T],
impl<'a, T> CompareHelper<'a, T> where T: Copy + PartialEq {
/// Construct a new compare helper for a current / previous set of dependency information.
fn new(
prev_items: &'a [T],
curr_items: &'a [T],
) -> Self {
CompareHelper {
offset_curr: 0,
offset_prev: 0,
/// Reset the current position in the dependency array to the start
fn reset(&mut self) {
self.offset_prev = 0;
self.offset_curr = 0;
/// Test if two sections of the dependency arrays are the same, by checking both
/// item equality, and a user closure to see if the content of the item changed.
fn is_same<F>(
prev_count: u8,
curr_count: u8,
mut f: F,
opt_detail: Option<&mut CompareHelperResult<T>>,
) -> bool where F: FnMut(&T, &T) -> bool {
// If the number of items is different, trivial reject.
if prev_count != curr_count {