Bug 1594644 - Add debugging infrastructure for picture cache invalidation. r=nical
authorGlenn Watson <git@intuitionlibrary.com>
Sun, 10 Nov 2019 19:05:15 +0000
changeset 501431 260dfcecdb6287707bd90040a4d2bf7988274ef1
parent 501430 c79b90bae420d6b32db7dc58fb263dc4aaa00ef2
child 501432 287aa26ecfbd4b84e81f7e19ece1f7f2d357f899
push id114170
push usermalexandru@mozilla.com
push dateTue, 12 Nov 2019 21:58:32 +0000
treeherdermozilla-inbound@9e3f44e87a1a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnical
bugs1594644
milestone72.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1594644 - Add debugging infrastructure for picture cache invalidation. r=nical Add a first pass at invalidation debugging infrastructure. Also tidy up the locations that invalidate into a common method, that sets the invalidation reason. Differential Revision: https://phabricator.services.mozilla.com/D52128
gfx/wr/webrender/src/intern.rs
gfx/wr/webrender/src/picture.rs
gfx/wr/webrender_api/src/api.rs
gfx/wr/wrench/src/main.rs
--- a/gfx/wr/webrender/src/intern.rs
+++ b/gfx/wr/webrender/src/intern.rs
@@ -69,16 +69,21 @@ pub struct ItemUid {
     uid: usize,
 }
 
 impl ItemUid {
     pub fn next_uid() -> ItemUid {
         let uid = NEXT_UID.fetch_add(1, Ordering::Relaxed);
         ItemUid { uid }
     }
+
+    // Intended for debug usage only
+    pub fn get_uid(&self) -> usize {
+        self.uid
+    }
 }
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(Debug, MallocSizeOf)]
 pub struct Handle<I> {
     index: u32,
     epoch: Epoch,
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -89,17 +89,17 @@ use crate::internal_types::{FastHashMap,
 use crate::frame_builder::{FrameBuildingContext, FrameBuildingState, PictureState, PictureContext};
 use crate::gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
 use crate::gpu_types::UvRectKind;
 use plane_split::{Clipper, Polygon, Splitter};
 use crate::prim_store::{SpaceMapper, PrimitiveVisibilityMask, PointKey, PrimitiveTemplateKind};
 use crate::prim_store::{SpaceSnapper, PictureIndex, PrimitiveInstance, PrimitiveInstanceKind};
 use crate::prim_store::{get_raster_rects, PrimitiveScratchBuffer, RectangleKey};
 use crate::prim_store::{OpacityBindingStorage, ImageInstanceStorage, OpacityBindingIndex};
-use crate::print_tree::PrintTreePrinter;
+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::{RenderTask, RenderTaskLocation, BlurTaskCache, ClearMode};
 use crate::resource_cache::ResourceCache;
 use crate::scene::SceneProperties;
 use smallvec::SmallVec;
 use std::{mem, u8, marker, u32};
@@ -512,16 +512,60 @@ impl TileSurface {
         match *self {
             TileSurface::Color { .. } => "Color",
             TileSurface::Texture { .. } => "Texture",
             TileSurface::Clear => "Clear",
         }
     }
 }
 
+/// 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)]
+#[repr(u8)]
+enum PrimitiveCompareResult {
+    /// Primitives match
+    Equal,
+    /// Something in the PrimitiveDescriptor was different
+    Descriptor,
+    /// The clip node content or spatial node changed
+    Clip,
+    /// The value of the transform changed
+    Transform,
+    /// An image dependency was dirty
+    Image,
+    /// The value of an opacity binding changed
+    OpacityBinding,
+}
+
+/// Debugging information about why a tile was invalidated
+#[derive(Debug)]
+enum InvalidationReason {
+    /// The fractional offset changed
+    FractionalOffset,
+    /// The background color changed
+    BackgroundColor,
+    /// Tile was not cacheable (e.g. video element)
+    NonCacheable,
+    /// The opaque state of the backing native surface changed
+    SurfaceOpacityChanged,
+    /// There was no backing texture (evicted or never rendered)
+    NoTexture,
+    /// There was no backing native surface (never rendered, or recreated)
+    NoSurface,
+    /// The primitive count in the dependency list was different
+    PrimCount,
+    /// The content of one of the primitives was different
+    Content {
+        /// What changed in the primitive that was different
+        prim_compare_result: PrimitiveCompareResult,
+    },
+}
+
 /// Information about a cached tile.
 pub struct Tile {
     /// The current world rect of this tile.
     pub world_rect: WorldRect,
     /// The current local rect of this tile.
     pub rect: PictureRect,
     /// The local rect of the tile clipped to the overall picture local rect.
     clipped_rect: PictureRect,
@@ -538,110 +582,161 @@ pub struct Tile {
     pub is_valid: bool,
     /// If true, this tile intersects with the currently visible screen
     /// rect, and will be drawn.
     pub is_visible: bool,
     /// The current fractional offset of the cache transform root. If this changes,
     /// all tiles need to be invalidated and redrawn, since snapping differences are
     /// likely to occur.
     fract_offset: PictureVector2D,
-    /// If true, the content on this tile is the same as last frame.
-    is_same_content: 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 picture space dirty rect for this tile.
     dirty_rect: PictureRect,
     /// The world 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 world_dirty_rect: WorldRect,
     /// 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>,
 }
 
 impl Tile {
     /// Construct a new, invalid tile.
     fn new(
         id: TileId,
     ) -> Self {
         Tile {
             rect: PictureRect::zero(),
             clipped_rect: PictureRect::zero(),
             world_rect: WorldRect::zero(),
             surface: None,
             current_descriptor: TileDescriptor::new(),
             prev_descriptor: TileDescriptor::new(),
-            is_same_content: false,
             is_valid: false,
             is_visible: false,
             fract_offset: PictureVector2D::zero(),
             id,
             is_opaque: false,
             root: TileNode::new_leaf(Vec::new()),
             dirty_rect: PictureRect::zero(),
             world_dirty_rect: WorldRect::zero(),
             background_color: None,
+            invalidation_reason: None,
         }
     }
 
+    /// 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!("rect: {}", self.rect));
+        pt.add_item(format!("fract_offset: {:?}", self.fract_offset));
+        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: &TilePostUpdateContext,
         state: &TilePostUpdateState,
-        compare_cache: &mut FastHashMap<PrimitiveComparisonKey, bool>,
-    ) {
+        compare_cache: &mut FastHashMap<PrimitiveComparisonKey, PrimitiveCompareResult>,
+        invalidation_reason: &mut Option<InvalidationReason>,
+    ) -> PictureRect {
         let mut prim_comparer = PrimitiveComparer::new(
             &self.prev_descriptor,
             &self.current_descriptor,
             state.resource_cache,
             ctx.spatial_nodes,
             ctx.opacity_bindings,
         );
 
+        let mut dirty_rect = PictureRect::zero();
+
         self.root.update_dirty_rects(
             &self.prev_descriptor.prims,
             &self.current_descriptor.prims,
             &mut prim_comparer,
-            &mut self.dirty_rect,
+            &mut dirty_rect,
             compare_cache,
+            invalidation_reason,
         );
+
+        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: &TilePostUpdateState,
     ) {
         // Check if the contents of the primitives, clips, and
         // other dependencies are the same.
         let mut compare_cache = FastHashMap::default();
-        self.update_dirty_rects(ctx, state, &mut compare_cache);
-        self.is_same_content &= self.dirty_rect.is_empty();
-        self.is_valid &= self.is_same_content;
+        let mut invalidation_reason = None;
+        let dirty_rect = self.update_dirty_rects(
+            ctx,
+            state,
+            &mut compare_cache,
+            &mut invalidation_reason,
+        );
+        if !dirty_rect.is_empty() {
+            self.invalidate(
+                Some(dirty_rect),
+                invalidation_reason.expect("bug: no invalidation_reason"),
+            );
+        }
+    }
+
+    /// 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.dirty_rect = self.dirty_rect.union(&rect);
+            }
+            None => {
+                self.dirty_rect = self.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,
         rect: PictureRect,
         ctx: &TilePreUpdateContext,
     ) {
         self.rect = rect;
+        self.invalidation_reason  = None;
 
         self.clipped_rect = self.rect
             .intersection(&ctx.local_rect)
             .and_then(|r| r.intersection(&ctx.local_clip_rect))
             .unwrap_or(PictureRect::zero());
 
         self.world_rect = ctx.pic_to_world_mapper
             .map(&self.rect)
@@ -652,33 +747,28 @@ impl Tile {
 
         // 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;
         }
 
-        // Start frame assuming that the tile has the same content.
-        self.is_same_content = true;
-
         // Determine if the fractional offset of the transform is different this frame
         // from the currently cached tile set.
         let fract_changed = (self.fract_offset.x - ctx.fract_offset.x).abs() > 0.001 ||
                             (self.fract_offset.y - ctx.fract_offset.y).abs() > 0.001;
         if fract_changed {
+            self.invalidate(None, InvalidationReason::FractionalOffset);
             self.fract_offset = ctx.fract_offset;
         }
 
-        // If the fractional offset of the transform root changed, or tthe background
-        // color of this tile changed, invalidate the whole thing.
-        if fract_changed || ctx.background_color != self.background_color {
+        if ctx.background_color != self.background_color {
+            self.invalidate(None, InvalidationReason::BackgroundColor);
             self.background_color = ctx.background_color;
-            self.is_same_content = false;
-            self.dirty_rect = rect;
         }
 
         // 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,
         );
@@ -694,18 +784,17 @@ impl Tile {
         // 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;
         }
 
         // Mark if the tile is cacheable at all.
         if !info.is_cacheable {
-            self.is_same_content = false;
-            self.dirty_rect = self.dirty_rect.union(&info.prim_rect);
+            self.invalidate(Some(info.prim_rect), InvalidationReason::NonCacheable);
         }
 
         // Include any image keys this tile depends on.
         self.current_descriptor.image_keys.extend_from_slice(&info.image_keys);
 
         // Include any opacity bindings this primitive depends on.
         self.current_descriptor.opacity_bindings.extend_from_slice(&info.opacity_bindings);
 
@@ -877,18 +966,17 @@ impl Tile {
                     // TODO(gw): This is a limitation of the DirectComposite APIs. It might
                     //           make sense on other platforms to be able to change this as
                     //           a property on a surface, if we ever see pages where this
                     //           is changing frequently.
                     if opacity_changed {
                         if let SurfaceTextureDescriptor::NativeSurface { ref mut id, .. } = descriptor {
                             // Reset the dirty rect and tile validity in this case, to
                             // force the new tile to be completely redrawn.
-                            self.dirty_rect = self.rect;
-                            self.is_valid = false;
+                            self.invalidate(None, InvalidationReason::SurfaceOpacityChanged);
 
                             // If this tile has a currently allocated native surface, destroy it. It
                             // will be re-allocated next time it's determined to be visible.
                             if let Some(id) = id.take() {
                                 state.composite_state.destroy_surface(id);
                             }
                         }
                     }
@@ -1097,16 +1185,79 @@ impl TileDescriptor {
             prims: Vec::new(),
             clips: Vec::new(),
             opacity_bindings: Vec::new(),
             image_keys: Vec::new(),
             transforms: Vec::new(),
         }
     }
 
+    /// Print debug information about this tile descriptor to a tree printer.
+    fn print(&self, pt: &mut dyn PrintTreePrinter) {
+        pt.new_level("current_descriptor".to_string());
+
+        pt.new_level("prims".to_string());
+        for prim in &self.prims {
+            pt.new_level(format!("prim uid={}", prim.prim_uid.get_uid()));
+            pt.add_item(format!("origin: {},{}", prim.origin.x, prim.origin.y));
+            pt.add_item(format!("clip: origin={},{} size={}x{}",
+                prim.prim_clip_rect.x,
+                prim.prim_clip_rect.y,
+                prim.prim_clip_rect.w,
+                prim.prim_clip_rect.h,
+            ));
+            pt.add_item(format!("deps: t={} i={} o={} c={}",
+                prim.transform_dep_count,
+                prim.image_dep_count,
+                prim.opacity_binding_dep_count,
+                prim.clip_dep_count,
+            ));
+            pt.end_level();
+        }
+        pt.end_level();
+
+        if !self.clips.is_empty() {
+            pt.new_level("clips".to_string());
+            for clip in &self.clips {
+                pt.new_level(format!("clip uid={}", clip.get_uid()));
+                pt.end_level();
+            }
+            pt.end_level();
+        }
+
+        if !self.image_keys.is_empty() {
+            pt.new_level("image_keys".to_string());
+            for key in &self.image_keys {
+                pt.new_level(format!("key={:?}", key));
+                pt.end_level();
+            }
+            pt.end_level();
+        }
+
+        if !self.opacity_bindings.is_empty() {
+            pt.new_level("opacity_bindings".to_string());
+            for opacity_binding in &self.opacity_bindings {
+                pt.new_level(format!("binding={:?}", opacity_binding));
+                pt.end_level();
+            }
+            pt.end_level();
+        }
+
+        if !self.transforms.is_empty() {
+            pt.new_level("transforms".to_string());
+            for transform in &self.transforms {
+                pt.new_level(format!("spatial_node={:?}", transform));
+                pt.end_level();
+            }
+            pt.end_level();
+        }
+
+        pt.end_level();
+    }
+
     /// Clear the dependency information for a tile, when the dependencies
     /// are being rebuilt.
     fn clear(&mut self) {
         self.prims.clear();
         self.clips.clear();
         self.opacity_bindings.clear();
         self.image_keys.clear();
         self.transforms.clear();
@@ -1336,16 +1487,18 @@ pub struct TileCacheInstance {
     shared_clip_chain: ClipChainId,
     /// The current transform of the picture cache root spatial node
     root_transform: TransformKey,
     /// 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,
+    /// The current fractional offset of the cached picture
+    fract_offset: PictureVector2D,
 }
 
 impl TileCacheInstance {
     pub fn new(
         slice: usize,
         spatial_node_index: SpatialNodeIndex,
         background_color: Option<ColorF>,
         shared_clips: Vec<ClipDataHandle>,
@@ -1374,16 +1527,17 @@ impl TileCacheInstance {
             background_color,
             backdrop: BackdropInfo::empty(),
             subpixel_mode: SubpixelMode::Allow,
             root_transform: TransformKey::Local,
             shared_clips,
             shared_clip_chain,
             current_tile_size: DeviceIntSize::zero(),
             frames_until_size_eval: 0,
+            fract_offset: PictureVector2D::zero(),
         }
     }
 
     /// Returns true if this tile cache is considered opaque.
     pub fn is_opaque(&self) -> bool {
         // If known opaque due to background clear color and being the first slice.
         // The background_color will only be Some(..) if this is the first slice.
         match self.background_color {
@@ -1555,17 +1709,17 @@ impl TileCacheInstance {
 
         // Unmap from world space to picture space
         let ref_point = pic_to_world_mapper
             .unmap(&ref_world_rect)
             .expect("bug: unable to unmap ref world rect")
             .origin;
 
         // Extract the fractional offset required in picture space to align in device space
-        let fract_offset = PictureVector2D::new(
+        self.fract_offset = PictureVector2D::new(
             ref_point.x.fract(),
             ref_point.y.fract(),
         );
 
         // 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();
         let old_properties = mem::replace(&mut self.opacity_bindings, FastHashMap::default());
@@ -1635,17 +1789,17 @@ impl TileCacheInstance {
             &mut self.tiles,
             FastHashMap::default(),
         );
 
         let ctx = TilePreUpdateContext {
             local_rect: self.local_rect,
             local_clip_rect: self.local_clip_rect,
             pic_to_world_mapper,
-            fract_offset,
+            fract_offset: self.fract_offset,
             background_color: self.background_color,
             global_screen_world_rect: frame_context.global_screen_world_rect,
         };
 
         for y in y0 .. y1 {
             for x in x0 .. x1 {
                 let key = TileOffset::new(x, y);
 
@@ -1656,18 +1810,18 @@ impl TileCacheInstance {
                         Tile::new(next_id)
                     });
 
                 // Ensure each tile is offset by the appropriate amount from the
                 // origin, such that the content origin will be a whole number and
                 // the snapping will be consistent.
                 let rect = PictureRect::new(
                     PicturePoint::new(
-                        x as f32 * self.tile_size.width + fract_offset.x,
-                        y as f32 * self.tile_size.height + fract_offset.y,
+                        x as f32 * self.tile_size.width + self.fract_offset.x,
+                        y as f32 * self.tile_size.height + self.fract_offset.y,
                     ),
                     self.tile_size,
                 );
 
                 tile.pre_update(
                     rect,
                     &ctx,
                 );
@@ -1964,16 +2118,41 @@ impl TileCacheInstance {
 
                 tile.add_prim_dependency(&prim_info);
             }
         }
 
         true
     }
 
+    /// 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!("fract_offset: {:?}", self.fract_offset));
+        pt.add_item(format!("background_color: {:?}", self.background_color));
+
+        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 = &self.tiles[&key];
+                tile.print(&mut pt);
+            }
+        }
+
+        pt.end_level();
+    }
+
     /// 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,
         frame_state: &mut FrameVisibilityState,
     ) {
@@ -3323,27 +3502,27 @@ impl PicturePrimitive {
                             let tile_draw_rect = match world_clip_rect.intersection(&tile.world_rect) {
                                 Some(rect) => rect,
                                 None => {
                                     tile.is_visible = false;
                                     continue;
                                 }
                             };
 
-                            let surface = tile.surface.as_mut().expect("no tile surface set!");
-
                             // If that draw rect is occluded by some set of tiles in front of it,
                             // then mark it as not visible and skip drawing. When it's not occluded
                             // it will fail this test, and get rasterized by the render task setup
                             // code below.
                             if frame_state.composite_state.is_tile_occluded(tile_cache.slice, tile_draw_rect) {
                                 // If this tile has an allocated native surface, free it, since it's completely
                                 // occluded. We will need to re-allocate this surface if it becomes visible,
                                 // but that's likely to be rare (e.g. when there is no content display list
                                 // for a frame or two during a tab switch).
+                                let surface = tile.surface.as_mut().expect("no tile surface set!");
+
                                 if let TileSurface::Texture { descriptor: SurfaceTextureDescriptor::NativeSurface { id, .. }, .. } = surface {
                                     if let Some(id) = id.take() {
                                         frame_state.composite_state.destroy_surface(id);
                                     }
                                 }
 
                                 tile.is_visible = false;
                                 continue;
@@ -3365,69 +3544,69 @@ impl PicturePrimitive {
                                     tile.is_opaque,
                                     scratch,
                                     frame_context.global_device_pixel_scale,
                                 );
 
                                 let label_offset = DeviceVector2D::new(20.0, 30.0);
                                 let tile_device_rect = tile.world_rect * frame_context.global_device_pixel_scale;
                                 if tile_device_rect.size.height >= label_offset.y {
+                                    let surface = tile.surface.as_ref().expect("no tile surface set!");
+
                                     scratch.push_debug_string(
                                         tile_device_rect.origin + label_offset,
                                         debug_colors::RED,
                                         format!("{:?}: s={} is_opaque={} surface={}",
                                                 tile.id,
                                                 tile_cache.slice,
                                                 tile.is_opaque,
                                                 surface.kind(),
                                         ),
                                     );
                                 }
                             }
 
-                            if let TileSurface::Texture { descriptor, .. } = surface {
+                            if let TileSurface::Texture { descriptor, .. } = tile.surface.as_mut().unwrap() {
                                 match descriptor {
                                     SurfaceTextureDescriptor::TextureCache { ref handle, .. } => {
                                         // Invalidate if the backing texture was evicted.
                                         if frame_state.resource_cache.texture_cache.is_allocated(handle) {
                                             // Request the backing texture so it won't get evicted this frame.
                                             // We specifically want to mark the tile texture as used, even
                                             // if it's detected not visible below and skipped. This is because
                                             // we maintain the set of tiles we care about based on visibility
                                             // during pre_update. If a tile still exists after that, we are
                                             // assuming that it's either visible or we want to retain it for
                                             // a while in case it gets scrolled back onto screen soon.
                                             // TODO(gw): Consider switching to manual eviction policy?
                                             frame_state.resource_cache.texture_cache.request(handle, frame_state.gpu_cache);
                                         } else {
                                             // If the texture was evicted on a previous frame, we need to assume
                                             // that the entire tile rect is dirty.
-                                            tile.is_valid = false;
-                                            tile.dirty_rect = tile.rect;
+                                            tile.invalidate(None, InvalidationReason::NoTexture);
                                         }
                                     }
                                     SurfaceTextureDescriptor::NativeSurface { id, .. } => {
                                         if id.is_none() {
                                             // There is no current surface allocation, so ensure the entire tile is invalidated
-                                            tile.is_valid = false;
-                                            tile.dirty_rect = tile.rect;
+                                            tile.invalidate(None, InvalidationReason::NoSurface);
                                         }
                                     }
                                 }
                             }
 
                             // Update the world dirty rect
                             tile.world_dirty_rect = map_pic_to_world.map(&tile.dirty_rect).expect("bug");
 
                             if tile.is_valid {
                                 continue;
                             }
 
                             // Ensure that this texture is allocated.
-                            if let TileSurface::Texture { ref mut descriptor, ref mut visibility_mask } = surface {
+                            if let TileSurface::Texture { ref mut descriptor, ref mut visibility_mask } = tile.surface.as_mut().unwrap() {
                                 match descriptor {
                                     SurfaceTextureDescriptor::TextureCache { ref mut handle } => {
                                         if !frame_state.resource_cache.texture_cache.is_allocated(handle) {
                                             frame_state.resource_cache.texture_cache.update_picture_cache(
                                                 tile_cache.current_tile_size,
                                                 handle,
                                                 frame_state.gpu_cache,
                                             );
@@ -3520,16 +3699,21 @@ impl PicturePrimitive {
                                 }
                             }
 
                             // Now that the tile is valid, reset the dirty rect.
                             tile.dirty_rect = PictureRect::zero();
                             tile.is_valid = true;
                         }
 
+                        // If invalidation debugging is enabled, dump the picture cache state to a tree printer.
+                        if frame_context.debug_flags.contains(DebugFlags::INVALIDATION_DBG) {
+                            tile_cache.print();
+                        }
+
                         None
                     }
                     PictureCompositeMode::MixBlend(..) |
                     PictureCompositeMode::Blit(_) => {
                         // The SplitComposite shader used for 3d contexts doesn't snap
                         // to pixels, so we shouldn't snap our uv coordinates either.
                         let supports_snapping = match self.context_3d {
                             Picture3DContext::In{ .. } => false,
@@ -4429,61 +4613,61 @@ impl<'a> PrimitiveComparer<'a> {
     fn advance_curr(&mut self, prim: &PrimitiveDescriptor) {
         self.clip_comparer.advance_curr(prim.clip_dep_count);
         self.transform_comparer.advance_curr(prim.transform_dep_count);
         self.image_comparer.advance_curr(prim.image_dep_count);
         self.opacity_comparer.advance_curr(prim.opacity_binding_dep_count);
     }
 
     /// Check if two primitive descriptors are the same.
-    fn is_prim_same(
+    fn compare_prim(
         &mut self,
         prev: &PrimitiveDescriptor,
         curr: &PrimitiveDescriptor,
-    ) -> bool {
+    ) -> PrimitiveCompareResult {
         let resource_cache = self.resource_cache;
         let spatial_nodes = self.spatial_nodes;
         let opacity_bindings = self.opacity_bindings;
 
         // Check equality of the PrimitiveDescriptor
         if prev != curr {
-            return false;
+            return PrimitiveCompareResult::Descriptor;
         }
 
         // Check if any of the clips  this prim has are different.
         if !self.clip_comparer.is_same(
             prev.clip_dep_count,
             curr.clip_dep_count,
             |_| {
                 false
             }
         ) {
-            return false;
+            return PrimitiveCompareResult::Clip;
         }
 
         // Check if any of the transforms  this prim has are different.
         if !self.transform_comparer.is_same(
             prev.transform_dep_count,
             curr.transform_dep_count,
             |curr| {
                 spatial_nodes[curr].changed
             }
         ) {
-            return false;
+            return PrimitiveCompareResult::Transform;
         }
 
         // Check if any of the images this prim has are different.
         if !self.image_comparer.is_same(
             prev.image_dep_count,
             curr.image_dep_count,
             |curr| {
                 resource_cache.is_image_dirty(*curr)
             }
         ) {
-            return false;
+            return PrimitiveCompareResult::Image;
         }
 
         // Check if any of the opacity bindings this prim has are different.
         if !self.opacity_comparer.is_same(
             prev.opacity_binding_dep_count,
             curr.opacity_binding_dep_count,
             |curr| {
                 if let OpacityBinding::Binding(id) = curr {
@@ -4492,20 +4676,20 @@ impl<'a> PrimitiveComparer<'a> {
                         .map_or(true, |info| info.changed) {
                         return true;
                     }
                 }
 
                 false
             }
         ) {
-            return false;
+            return PrimitiveCompareResult::OpacityBinding;
         }
 
-        true
+        PrimitiveCompareResult::Equal
     }
 }
 
 /// Details for a node in a quadtree that tracks dirty rects for a tile.
 enum TileNodeKind {
     Leaf {
         /// The index buffer of primitives that affected this tile previous frame
         prev_indices: Vec<PrimitiveDependencyIndex>,
@@ -4812,27 +4996,29 @@ impl TileNode {
 
     /// Update the dirty state of this node, building the overall dirty rect
     fn update_dirty_rects(
         &mut self,
         prev_prims: &[PrimitiveDescriptor],
         curr_prims: &[PrimitiveDescriptor],
         prim_comparer: &mut PrimitiveComparer,
         dirty_rect: &mut PictureRect,
-        compare_cache: &mut FastHashMap<PrimitiveComparisonKey, bool>,
+        compare_cache: &mut FastHashMap<PrimitiveComparisonKey, PrimitiveCompareResult>,
+        invalidation_reason: &mut Option<InvalidationReason>,
     ) {
         match self.kind {
             TileNodeKind::Node { ref mut children, .. } => {
                 for child in children.iter_mut() {
                     child.update_dirty_rects(
                         prev_prims,
                         curr_prims,
                         prim_comparer,
                         dirty_rect,
                         compare_cache,
+                        invalidation_reason,
                     );
                 }
             }
             TileNodeKind::Leaf { ref prev_indices, ref curr_indices, ref mut dirty_tracker, .. } => {
                 // If the index buffers are of different length, they must be different
                 if prev_indices.len() == curr_indices.len() {
                     let mut prev_i0 = 0;
                     let mut prev_i1 = 0;
@@ -4854,35 +5040,43 @@ impl TileNode {
 
                         // Compare the primitives, caching the result in a hash map
                         // to save comparisons in other tree nodes.
                         let key = PrimitiveComparisonKey {
                             prev_index: *prev_index,
                             curr_index: *curr_index,
                         };
 
-                        let is_prim_same = *compare_cache
+                        let prim_compare_result = *compare_cache
                             .entry(key)
                             .or_insert_with(|| {
                                 let prev = &prev_prims[i0];
                                 let curr = &curr_prims[i1];
-                                prim_comparer.is_prim_same(prev, curr)
+                                prim_comparer.compare_prim(prev, curr)
                             });
 
                         // If not the same, mark this node as dirty and update the dirty rect
-                        if !is_prim_same {
+                        if prim_compare_result != PrimitiveCompareResult::Equal {
+                            if invalidation_reason.is_none() {
+                                *invalidation_reason = Some(InvalidationReason::Content {
+                                    prim_compare_result,
+                                });
+                            }
                             *dirty_rect = self.rect.union(dirty_rect);
                             *dirty_tracker = *dirty_tracker | 1;
                             break;
                         }
 
                         prev_i0 = i0;
                         prev_i1 = i1;
                     }
                 } else {
+                    if invalidation_reason.is_none() {
+                        *invalidation_reason = Some(InvalidationReason::PrimCount);
+                    }
                     *dirty_rect = self.rect.union(dirty_rect);
                     *dirty_tracker = *dirty_tracker | 1;
                 }
             }
         }
     }
 }
 
--- a/gfx/wr/webrender_api/src/api.rs
+++ b/gfx/wr/webrender_api/src/api.rs
@@ -1142,16 +1142,18 @@ bitflags! {
         const DISABLE_TEXT_PRIMS = 1 << 22;
         const DISABLE_GRADIENT_PRIMS = 1 << 23;
         const OBSCURE_IMAGES = 1 << 24;
         const GLYPH_FLASHING = 1 << 25;
         /// The profiler only displays information that is out of the ordinary.
         const SMART_PROFILER        = 1 << 26;
         /// Dynamically control whether picture caching is enabled.
         const DISABLE_PICTURE_CACHING = 1 << 27;
+        /// If set, dump picture cache invalidation debug to console.
+        const INVALIDATION_DBG = 1 << 28;
     }
 }
 
 pub struct RenderApi {
     api_sender: MsgSender<ApiMsg>,
     payload_sender: PayloadSender,
     namespace_id: IdNamespace,
     next_id: Cell<ResourceId>,
--- a/gfx/wr/wrench/src/main.rs
+++ b/gfx/wr/wrench/src/main.rs
@@ -705,16 +705,21 @@ fn render<'a>(
                         VirtualKeyCode::Escape => {
                             return winit::ControlFlow::Break;
                         }
                         VirtualKeyCode::A => {
                             debug_flags.toggle(DebugFlags::DISABLE_PICTURE_CACHING);
                             wrench.api.send_debug_cmd(DebugCommand::SetFlags(debug_flags));
                             do_render = true;
                         }
+                        VirtualKeyCode::B => {
+                            debug_flags.toggle(DebugFlags::INVALIDATION_DBG);
+                            wrench.api.send_debug_cmd(DebugCommand::SetFlags(debug_flags));
+                            do_render = true;
+                        }
                         VirtualKeyCode::P => {
                             debug_flags.toggle(DebugFlags::PROFILER_DBG);
                             wrench.api.send_debug_cmd(DebugCommand::SetFlags(debug_flags));
                             do_render = true;
                         }
                         VirtualKeyCode::O => {
                             debug_flags.toggle(DebugFlags::RENDER_TARGET_DBG);
                             wrench.api.send_debug_cmd(DebugCommand::SetFlags(debug_flags));