Bug 1605283 - Improve support for invalidation debugging and testing r=gw a=reland CLOSED TREE
authorBert Peers <bpeers@mozilla.com>
Tue, 28 Jan 2020 20:05:38 +0000
changeset 512032 748cc6e57cd0b0fab4357ae38eb5bdae505afdac
parent 512031 dd5f0e2d407e0f8481fe15739bad434ef0fb048b
child 512033 b82950f789cdd7037e8616199dbb13f532cfdd29
push id37066
push usershindli@mozilla.com
push dateWed, 29 Jan 2020 03:50:12 +0000
treeherdermozilla-central@31e91b3d071e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgw, reland
bugs1605283
milestone74.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 1605283 - Improve support for invalidation debugging and testing r=gw a=reland CLOSED TREE Third iteration: Fix broken scrolling (and incorrect positioning of quad tree lines) by serializing the SpaceMapper(-transform) from take_context, and using it to transform the primitive rects (instead of the previous translation based on unclipped.origin); Note: this is done at visualization time and not at export time to distinguish actually moving elements from merely-scrolling ones. Serialize the entire UpdateList, so we get the data (Keys) that's being added; add it to the overview; Move the static CSS code into tilecache_base.css; add this and the .js file to the binary, write them as part of output (instead of manual copy); clean up CSS a bit; Differential Revision: https://phabricator.services.mozilla.com/D61049
gfx/wr/Cargo.lock
gfx/wr/tileview/Cargo.toml
gfx/wr/tileview/src/main.rs
gfx/wr/tileview/src/tilecache_base.css
gfx/wr/webrender/src/intern.rs
gfx/wr/webrender/src/picture.rs
--- a/gfx/wr/Cargo.lock
+++ b/gfx/wr/Cargo.lock
@@ -1629,16 +1629,17 @@ dependencies = [
  "serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
  "time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "tileview"
 version = "0.1.0"
 dependencies = [
+ "euclid 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "ron 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",
  "webrender 0.61.0",
  "webrender_api 0.61.0",
 ]
 
 [[package]]
 name = "time"
--- a/gfx/wr/tileview/Cargo.toml
+++ b/gfx/wr/tileview/Cargo.toml
@@ -7,8 +7,9 @@ edition = "2018"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
 ron = "0.1.7"
 serde = {version = "1.0.88", features = ["derive"] }
 webrender = {path = "../webrender", features=["capture","replay","debugger","png","profiler","no_static_freetype", "leak_checks"]}
 webrender_api = {path = "../webrender_api", features=["serialize","deserialize"]}
+euclid = { version = "0.20.0", features = ["serde"] }
--- a/gfx/wr/tileview/src/main.rs
+++ b/gfx/wr/tileview/src/main.rs
@@ -7,59 +7,65 @@ use webrender::{TileSerializer, TileCach
 use serde::Deserialize;
 //use ron::de::from_reader;
 use std::fs::File;
 use std::io::prelude::*;
 use std::path::Path;
 use std::ffi::OsString;
 use webrender::api::enumerate_interners;
 use webrender::UpdateKind;
+use euclid::{Rect, Transform3D};
+use webrender_api::units::{PicturePoint, PictureSize, PicturePixel, WorldPixel};
+
+static RES_JAVASCRIPT: &'static str = include_str!("tilecache.js");
+static RES_BASE_CSS: &'static str   = include_str!("tilecache_base.css");
 
 #[derive(Deserialize)]
 pub struct Slice {
-    pub x: f32,
-    pub y: f32,
+    pub transform: Transform3D<f32, PicturePixel, WorldPixel>,
     pub tile_cache: TileCacheInstanceSerializer
 }
 
 // invalidation reason CSS colors
 static CSS_FRACTIONAL_OFFSET: &str       = "fill:#4040c0;fill-opacity:0.1;";
 static CSS_BACKGROUND_COLOR: &str        = "fill:#10c070;fill-opacity:0.1;";
 static CSS_SURFACE_OPACITY_CHANNEL: &str = "fill:#c040c0;fill-opacity:0.1;";
 static CSS_NO_TEXTURE: &str              = "fill:#c04040;fill-opacity:0.1;";
 static CSS_NO_SURFACE: &str              = "fill:#40c040;fill-opacity:0.1;";
 static CSS_PRIM_COUNT: &str              = "fill:#40f0f0;fill-opacity:0.1;";
 static CSS_CONTENT: &str                 = "fill:#f04040;fill-opacity:0.1;";
 static CSS_COMPOSITOR_KIND_CHANGED: &str = "fill:#f0c070;fill-opacity:0.1;";
 
-fn tile_node_to_svg(node: &TileNode, x: f32, y: f32) -> String
+fn tile_node_to_svg(node: &TileNode, transform: &Transform3D<f32, PicturePixel, WorldPixel>) -> String
 {
     match &node.kind {
         TileNodeKind::Leaf { .. } => {
-            format!("<rect x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" />\n",
-                    (node.rect.origin.x + x) as i32,
-                    (node.rect.origin.y + y) as i32,
-                    node.rect.size.width, node.rect.size.height)
+            let rect_world = transform.transform_rect(&node.rect).unwrap();
+            format!("<rect x=\"{:.2}\" y=\"{:.2}\" width=\"{:.2}\" height=\"{:.2}\" />\n",
+                    rect_world.origin.x,
+                    rect_world.origin.y,
+                    rect_world.size.width,
+                    rect_world.size.height)
         },
         TileNodeKind::Node { children } => {
-            children.iter().fold(String::new(), |acc, child| acc + &tile_node_to_svg(child, x, y) )
+            children.iter().fold(String::new(), |acc, child| acc + &tile_node_to_svg(child, transform) )
         }
     }
 }
 
 fn tile_to_svg(key: TileOffset,
                tile: &TileSerializer,
                slice: &Slice,
                prev_tile: Option<&TileSerializer>,
                tile_stroke: &str,
                prim_class: &str,
                invalidation_report: &mut String,
                svg_width: &mut i32, svg_height: &mut i32 ) -> String
 {
-    let mut svg = format!("\n<!-- tile key {},{} ; slice x {} y {}-->\n", key.x, key.y, slice.x, slice.y);
+    let mut svg = format!("\n<!-- tile key {},{} ; -->\n", key.x, key.y);
 
 
     let tile_fill =
         match tile.invalidation_reason {
             Some(InvalidationReason::FractionalOffset) => CSS_FRACTIONAL_OFFSET.to_string(),
             Some(InvalidationReason::BackgroundColor) => CSS_BACKGROUND_COLOR.to_string(),
             Some(InvalidationReason::SurfaceOpacityChanged) => CSS_SURFACE_OPACITY_CHANNEL.to_string(),
             Some(InvalidationReason::NoTexture) => CSS_NO_TEXTURE.to_string(),
@@ -100,69 +106,68 @@ fn tile_to_svg(key: TileOffset,
     if let Some(reason) = tile.invalidation_reason {
         invalidation_report.push_str(
             &format!("\n<tspan x=\"0\" dy=\"16px\">slice {} tile key ({},{}) invalidated: {:?}</tspan>\n",
                      slice.tile_cache.slice, key.x, key.y, reason));
     }
 
     svg = format!(r#"{}<rect x="{}" y="{}" width="{}" height="{}" style="{}" ></rect>"#,
             svg,
-            //TODO --bpe are these in local space or screen space?
             tile.rect.origin.x,
             tile.rect.origin.y,
             tile.rect.size.width,
             tile.rect.size.height,
             tile_style);
 
     svg = format!("{}\n\n<g class=\"svg_quadtree\">\n{}</g>\n",
                    svg,
-                   //tile_node_to_svg(&tile.root, tile.rect.origin.x, tile.rect.origin.y));
-                   tile_node_to_svg(&tile.root, 0.0, 0.0));
+                   tile_node_to_svg(&tile.root, &slice.transform));
 
     let right  = (tile.rect.origin.x + tile.rect.size.width) as i32;
     let bottom = (tile.rect.origin.y + tile.rect.size.height) as i32;
 
     *svg_width  = if right  > *svg_width  { right  } else { *svg_width  };
     *svg_height = if bottom > *svg_height { bottom } else { *svg_height };
 
     svg += "\n<!-- primitives -->\n";
 
     svg = format!("{}<g id=\"{}\">\n\t",
                   svg,
                   prim_class);
 
     for prim in &tile.current_descriptor.prims {
         let rect = prim.prim_clip_rect;
-        //TODO proper positioning of prims, especially when scrolling
-        // this version seems closest, but introduces gaps (eg in about:config)
-        let x = (rect.x + slice.x) as i32;
-        let y = (rect.y + slice.y) as i32;
-        // this version is .. interesting: when scrolling, nothing moves in about:config,
-        // instead the searchbox shifts down.. hmm..
-        //let x = rect.x as i32;
-        //let y = rect.y as i32;
-        let w = rect.w as i32;
-        let h = rect.h as i32;
+
+        // the transform could also be part of the CSS, let the browser do it;
+        // might be a bit faster and also enable actual 3D transforms.
+        let rect_pixel = Rect {
+            origin: PicturePoint::new(rect.x, rect.y),
+            size: PictureSize::new(rect.w, rect.h),
+        };
+        let rect_world = slice.transform.transform_rect(&rect_pixel).unwrap();
 
         let style =
             if let Some(prev_tile) = prev_tile {
                 // when this O(n^2) gets too slow, stop brute-forcing and use a set or something
                 if prev_tile.current_descriptor.prims.iter().find(|&prim| prim.prim_clip_rect == rect).is_some() {
                     ""
                 } else {
                     "class=\"svg_changed_prim\" "
                 }
             } else {
                 "class=\"svg_changed_prim\" "
             };
 
-        svg = format!(r#"{}<rect x="{}" y="{}" width="{}" height="{}" {}/>"#,
-            svg,
-            x, y, w, h,
-            style);
+        svg += &format!("<rect x=\"{:.2}\" y=\"{:.2}\" width=\"{:.2}\" height=\"{:.2}\" {}/>",
+                        svg,
+                        rect_world.origin.x,
+                        rect_world.origin.y,
+                        rect_world.size.width,
+                        rect_world.size.height,
+                        style);
 
         svg += "\n\t";
     }
 
     svg += "\n</g>\n";
 
     // nearly invisible, all we want is the toolip really
     let style = "style=\"fill-opacity:0.001;";
@@ -177,17 +182,18 @@ fn tile_to_svg(key: TileOffset,
 
     svg
 }
 
 fn slices_to_svg(slices: &[Slice], prev_slices: Option<Vec<Slice>>,
                  svg_width: &mut i32, svg_height: &mut i32,
                  max_slice_index: &mut usize) -> String
 {
-    let svg_begin = "<?xml\u{2d}stylesheet type\u{3d}\"text/css\" href\u{3d}\"tilecache.css\" ?>\n";
+    let svg_begin = "<?xml\u{2d}stylesheet type\u{3d}\"text/css\" href\u{3d}\"tilecache_base.css\" ?>\n\
+                     <?xml\u{2d}stylesheet type\u{3d}\"text/css\" href\u{3d}\"tilecache.css\" ?>\n";
 
     let mut svg = String::new();
     let mut invalidation_report = String::new();
 
     for slice in slices {
         let tile_cache = &slice.tile_cache;
         *max_slice_index = if tile_cache.slice > *max_slice_index { tile_cache.slice } else { *max_slice_index };
 
@@ -236,16 +242,17 @@ fn slices_to_svg(slices: &[Slice], prev_
             + "\n</svg>\n"
 }
 
 fn write_html(output_dir: &Path, svg_files: &[String], intern_files: &[String]) {
     let html_head = "<!DOCTYPE html>\n\
                      <html>\n\
                      <head>\n\
                      <meta charset=\"UTF-8\">\n\
+                     <link rel=\"stylesheet\" type=\"text/css\" href=\"tilecache_base.css\"></link>\n\
                      <link rel=\"stylesheet\" type=\"text/css\" href=\"tilecache.css\"></link>\n\
                      </head>\n"
                      .to_string();
 
     let html_body = "<body bgcolor=\"#000000\" onload=\"load()\">\n"
                      .to_string();
 
 
@@ -300,120 +307,75 @@ fn write_html(output_dir: &Path, svg_fil
     let html = format!("{}{}{}{}", html_head, html_body, script, html_end);
 
     let output_file = output_dir.join("index.html");
     let mut html_output = File::create(output_file).unwrap();
     html_output.write_all(html.as_bytes()).unwrap();
 }
 
 fn write_css(output_dir: &Path, max_slice_index: usize) {
-    let mut css = ".tile_svg {\n\
-                    float: left;\n\
-                   }\n\
-                   \n\
-                   .split {\n\
-                      position: fixed;\n\
-                      z-index: 1;\n\
-                      top: 0;\n\
-                      padding-top: 20px;\n\
-                   }\n\
-                   \n\
-                   .left {\n\
-                      left: 0;\n\
-                   }\n\
-                   \n\
-                   .right {\n\
-                      right: 0;\n\
-                      width: 20%;\n\
-                      height: 100%;\n\
-                      opacity: 90%;\n\
-                   }\n\
-                   \n\
-                   #intern {\n\
-                    position:relative;\n\
-                    top:60px;\n\
-                    width: 100%;\n\
-                    height: 100%;\n\
-                    color: orange;\n\
-                    background-color:white;\n\
-                   }\n\
-                   .svg_invalidated {\n\
-                       fill: white;\n\
-                       font-family:monospace;\n\
-                   }\n\n\n\
-                   #svg_ui_overlay {\n\
-                       position:absolute;\n\
-                       right:0; \n\
-                       top:0; \n\
-                       z-index:70; \n\
-                       color: rgb(255,255,100);\n\
-                       font-family:monospace;\n\
-                       background-color: #404040a0;\n\
-                   }\n\n\n\
-                   .svg_quadtree {\n\
-                       fill: none;\n\
-                       stroke-width: 1;\n\
-                       stroke: orange;\n\
-                   }\n\n\n\
-                   .svg_changed_prim {\n\
-                       stroke: red;\n\
-                       stroke-width: 2.0;\n\
-                   }\n\n\n\
-                   #svg_ui_slider {\n\
-                       width:90%;\n\
-                   }\n\n".to_string();
+    let mut css = String::new();
 
     for ix in 0..max_slice_index + 1 {
         let color = ( ix % 7 ) + 1;
         let rgb = format!("rgb({},{},{})",
                             if color & 2 != 0 { 205 } else { 90 },
                             if color & 4 != 0 { 205 } else { 90 },
                             if color & 1 != 0 { 225 } else { 90 });
 
         let prim_class = format!("tile_slice{}", ix);
 
-        css = format!("{}\n\
-                       #{} {{\n\
+        css += &format!("#{} {{\n\
                            fill: {};\n\
                            fill-opacity: 0.03;\n\
                            stroke-width: 0.8;\n\
                            stroke: {};\n\
-                       }}\n\n",
-                       css,
-                       prim_class,
-                       //rgb,
-                       "none",
-                       rgb);
+                        }}\n\n",
+                        prim_class,
+                        //rgb,
+                        "none",
+                        rgb);
     }
 
     let output_file = output_dir.join("tilecache.css");
     let mut css_output = File::create(output_file).unwrap();
     css_output.write_all(css.as_bytes()).unwrap();
 }
 
 macro_rules! updatelist_to_html_macro {
     ( $( $name:ident: $ty:ty, )+ ) => {
         fn updatelist_to_html(update_lists: &TileCacheLoggerUpdateLists) -> String {
-            let mut html = String::new();
+            let mut html = "\
+                <!DOCTYPE html>\n\
+                <html> <head> <meta charset=\"UTF-8\">\n\
+                <link rel=\"stylesheet\" type=\"text/css\" href=\"tilecache_base.css\"></link>\n\
+                <link rel=\"stylesheet\" type=\"text/css\" href=\"tilecache.css\"></link>\n\
+                </head> <body>\n".to_string();
+
             $(
-                html += &format!("<h4 style=\"margin:5px;\">{}</h4>\n<font color=\"green\">\n", stringify!($name));
-                let mut was_insert = true;
-                for update in &update_lists.$name.1 {
-                    let is_insert = match update.kind {
-                        UpdateKind::Insert => true,
-                        _ => false
+                html += &format!("<div class=\"intern_header\">{}</div>\n<div class=\"intern_data\">\n",
+                                 stringify!($name));
+                let mut insert_count = 0;
+                for update in &update_lists.$name.1.updates {
+                    match update.kind {
+                        UpdateKind::Insert => {
+                            html += &format!("<div class=\"insert\">{} {}</div>\n",
+                                             update.index,
+                                             format!("({:?})", update_lists.$name.1.data[insert_count]));
+                            insert_count = insert_count + 1;
+                        }
+                        _ => {
+                            html += &format!("<div class=\"remove\">{}</div>\n",
+                                             update.index);
+                        }
                     };
-                    if was_insert != is_insert {
-                        html += &format!("</font><font color=\"{}\">", if is_insert { "green" } else { "red" });
-                    }
-                    html += &format!("{}, \n", update.index);
-                    was_insert = is_insert;
                 }
-                html += &"</font><hr/>\n";
+                html += "</div><br/>\n";
             )+
+            html += "</body> </html>\n";
             html
         }
     }
 }
 enumerate_interners!(updatelist_to_html_macro);
 
 fn write_tile_cache_visualizer_svg(entry: &std::fs::DirEntry, output_dir: &Path,
                                    slices: &[Slice], prev_slices: Option<Vec<Slice>>,
@@ -504,10 +466,13 @@ fn main() {
 
         print!("\r");
         prev_slices = Some(slices);
     }
 
     write_html(output_dir, &svg_files, &intern_files);
     write_css(output_dir, max_slice_index);
 
-    println!("OK. For now, manually copy tilecache.js to the output folder please.                           ");
+    std::fs::write(output_dir.join("tilecache.js"), RES_JAVASCRIPT).unwrap();
+    std::fs::write(output_dir.join("tilecache_base.css"), RES_BASE_CSS).unwrap();
+
+    println!("\n");
 }
new file mode 100644
--- /dev/null
+++ b/gfx/wr/tileview/src/tilecache_base.css
@@ -0,0 +1,94 @@
+.tile_svg {
+	float: left;
+}
+
+#intern {
+	position:relative;
+	top:60px;
+	width: 100%;
+	height: 100%;
+	color: orange;
+	border: 0px;
+	overflow: auto;
+	background: white;
+}
+
+.intern_header {
+	color: blue;
+	font-family: Arial;
+	font-weight: bold;
+	line-height: 200%;
+	background-color: lightgrey;
+	margin-top: 5px;
+	margin-bottom: 5px;
+}
+
+.intern_data {
+	font-family: monospace;
+	font-size: small;
+}
+
+.intern_data .insert:nth-child(even) {
+	background: #FFFFFF;
+}
+.intern_data .insert:nth-child(odd) {
+	background: #EFEFEF;
+}
+
+.intern_data .insert {
+	color: #008000;
+}
+
+.intern_data .remove {
+	color: #800000;
+}
+
+
+
+.split {
+	position: fixed;
+	z-index: 1;
+	top: 0;
+	padding-top: 14px;
+}
+
+.left {
+	left: 0;
+}
+
+.right {
+	right: 0;
+	width: 25%;
+	height: 90%;
+}
+
+#svg_ui_overlay {
+	position:absolute;
+	right:0; 
+	top:0; 
+	z-index:70; 
+	color: rgb(255,255,100);
+	font-family:monospace;
+	background-color: #404040a0;
+}
+
+#svg_ui_slider {
+	width:90%;
+}
+
+.svg_invalidated {
+	fill: white;
+	font-family:monospace;
+}
+
+.svg_quadtree {
+	fill: none;
+	stroke-width: 1;
+	stroke: orange;
+}
+
+.svg_changed_prim {
+	stroke: red;
+	stroke-width: 2.0;
+}
+
--- a/gfx/wr/webrender/src/intern.rs
+++ b/gfx/wr/webrender/src/intern.rs
@@ -45,21 +45,23 @@ use crate::util::VecHelper;
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 #[derive(Debug, Copy, Clone, MallocSizeOf, PartialEq)]
 struct Epoch(u64);
 
 /// A list of updates to be applied to the data store,
 /// provided by the interning structure.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct UpdateList<S> {
     /// The additions and removals to apply.
     pub updates: Vec<Update>,
     /// Actual new data to insert.
-    data: Vec<S>,
+    pub data: Vec<S>,
 }
 
 lazy_static! {
     static ref NEXT_UID: AtomicUsize = AtomicUsize::new(0);
 }
 
 /// A globally, unique identifier
 #[cfg_attr(feature = "capture", derive(Serialize))]
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -75,17 +75,17 @@ use api::{DebugFlags, RasterSpace, Image
 use api::units::*;
 use crate::box_shadow::{BLUR_SAMPLE_SCALE};
 use crate::clip::{ClipStore, ClipChainInstance, ClipDataHandle, ClipChainId};
 use crate::clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX,
     ClipScrollTree, CoordinateSpaceMapping, SpatialNodeIndex, VisibleFace
 };
 use crate::composite::{CompositorKind, CompositeState, NativeSurfaceId, NativeTileId};
 use crate::debug_colors;
-use euclid::{vec3, Point2D, Scale, Size2D, Vector2D, Rect};
+use euclid::{vec3, Point2D, Scale, Size2D, Vector2D, Rect, Transform3D};
 use euclid::approxeq::ApproxEq;
 use crate::filterdata::SFilterData;
 use crate::frame_builder::{FrameVisibilityContext, FrameVisibilityState};
 use crate::intern::ItemUid;
 use crate::internal_types::{FastHashMap, FastHashSet, PlaneSplitter, Filter, PlaneSplitAnchor, TextureSource};
 use crate::frame_builder::{FrameBuildingContext, FrameBuildingState, PictureState, PictureContext};
 use crate::gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
 use crate::gpu_types::UvRectKind;
@@ -107,18 +107,33 @@ use std::sync::atomic::{AtomicUsize, Ord
 use crate::texture_cache::TextureCacheHandle;
 use crate::util::{TransformedRectKind, MatrixHelpers, MaxRect, scale_factors, VecHelper, RectHelpers};
 use crate::filterdata::{FilterDataHandle};
 #[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::Update;
-
+use crate::intern::{Internable, UpdateList};
+#[cfg(any(feature = "capture", feature = "replay"))]
+use api::{ClipIntern, FilterDataIntern, 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};
+#[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;
 
@@ -1488,17 +1503,17 @@ impl BackdropInfo {
             },
         }
     }
 }
 
 #[derive(Clone)]
 pub struct TileCacheLoggerSlice {
     pub serialized_slice: String,
-    pub local_to_world_transform: DeviceRect
+    pub local_to_world_transform: Transform3D<f32, PicturePixel, WorldPixel>,
 }
 
 #[cfg(any(feature = "capture", feature = "replay"))]
 macro_rules! declare_tile_cache_logger_updatelists {
     ( $( $name:ident : $ty:ty, )+ ) => {
         #[cfg_attr(feature = "capture", derive(Serialize))]
         #[cfg_attr(feature = "replay", derive(Deserialize))]
         struct TileCacheLoggerUpdateListsSerializer {
@@ -1509,37 +1524,37 @@ macro_rules! declare_tile_cache_logger_u
             $(
                 /// Generate storage, one per interner.
                 /// the tuple is a workaround to avoid the need for multiple
                 /// fields that start with $name (macro concatenation).
                 /// the string is .ron serialized updatelist at capture time;
                 /// the updates is the list of DataStore updates (avoid UpdateList
                 /// due to Default() requirements on the Keys) reconstructed at
                 /// load time.
-                pub $name: (String, Vec<Update>),
+                pub $name: (String, UpdateList<<$ty as Internable>::Key>),
             )+
         }
 
         impl TileCacheLoggerUpdateLists {
             pub fn new() -> Self {
                 TileCacheLoggerUpdateLists {
                     $(
-                        $name : ( String::new(), Vec::<Update>::new() ),
+                        $name : ( String::new(), UpdateList{ updates: Vec::new(), data: Vec::new()} ),
                     )+
                 }
             }
 
             /// serialize all interners in updates to .ron
             #[cfg(feature = "capture")]
             fn serialize_updates(
                 &mut self,
                 updates: &InternerUpdates
             ) {
                 $(
-                    self.$name.0 = ron::ser::to_string_pretty(&updates.$name.updates, Default::default()).unwrap();
+                    self.$name.0 = ron::ser::to_string_pretty(&updates.$name, Default::default()).unwrap();
                 )+
             }
 
             fn is_empty(&self) -> bool {
                 $(
                     if !self.$name.0.is_empty() { return false; }
                 )+
                 true
@@ -1547,27 +1562,27 @@ macro_rules! declare_tile_cache_logger_u
 
             #[cfg(feature = "capture")]
             fn to_ron(&self) -> String {
                 let mut serializer =
                     TileCacheLoggerUpdateListsSerializer { ron_string: Vec::new() };
                 $(
                     serializer.ron_string.push(
                         if self.$name.0.is_empty() {
-                            "[]".to_string()
+                            "( updates: [], data: [] )".to_string()
                         } else {
                             self.$name.0.clone()
                         });
                 )+
                 ron::ser::to_string_pretty(&serializer, Default::default()).unwrap()
             }
 
             #[cfg(feature = "replay")]
             pub fn from_ron(&mut self, text: &str) {
-                let serializer : TileCacheLoggerUpdateListsSerializer = 
+                let serializer : TileCacheLoggerUpdateListsSerializer =
                     match ron::de::from_str(&text) {
                         Ok(data) => { data }
                         Err(e) => {
                             println!("ERROR: failed to deserialize updatelist: {:?}\n{:?}", &text, e);
                             return;
                         }
                     };
                 let mut index = 0;
@@ -1641,17 +1656,21 @@ impl TileCacheLogger {
         }
     }
 
     pub fn is_enabled(&self) -> bool {
         !self.frames.is_empty()
     }
 
     #[cfg(feature = "capture")]
-    pub fn add(&mut self, serialized_slice: String, local_to_world_transform: DeviceRect) {
+    pub fn add(
+            &mut self,
+            serialized_slice: String,
+            local_to_world_transform: Transform3D<f32, PicturePixel, WorldPixel>
+    ) {
         if !self.is_enabled() {
             return;
         }
         self.frames[self.write_index].slices.push(
             TileCacheLoggerSlice {
                 serialized_slice,
                 local_to_world_transform });
     }
@@ -1704,21 +1723,22 @@ impl TileCacheLogger {
                 continue;
             }
 
             let filename = path_tile_cache.join(format!("frame{:05}.ron", files_written));
             let mut output = File::create(filename).unwrap();
             output.write_all(b"// slice data\n").unwrap();
             output.write_all(b"[\n").unwrap();
             for item in &self.frames[index].slices {
-                output.write_all(format!("( x: {}, y: {},\n",
-                                         item.local_to_world_transform.origin.x,
-                                         item.local_to_world_transform.origin.y)
-                                 .as_bytes()).unwrap();
-                output.write_all(b"tile_cache:\n").unwrap();
+                output.write_all(b"( transform:\n").unwrap();
+                let transform =
+                    ron::ser::to_string_pretty(
+                        &item.local_to_world_transform, Default::default()).unwrap();
+                output.write_all(transform.as_bytes()).unwrap();
+                output.write_all(b",\n tile_cache:\n").unwrap();
                 output.write_all(item.serialized_slice.as_bytes()).unwrap();
                 output.write_all(b"\n),\n").unwrap();
             }
             output.write_all(b"]\n\n").unwrap();
 
             output.write_all(b"// @@@ chunk @@@\n\n").unwrap();
 
             output.write_all(b"// interning data\n").unwrap();
@@ -3701,17 +3721,16 @@ impl PicturePrimitive {
             Picture3DContext::In { root_data: Some(_), .. } => {
                 Some(PlaneSplitter::new())
             }
             Picture3DContext::In { root_data: None, .. } => {
                 None
             }
         };
 
-        let unclipped =
         match self.raster_config {
             Some(ref raster_config) => {
                 let pic_rect = PictureRect::from_untyped(&self.precise_local_rect.to_untyped());
 
                 let device_pixel_scale = frame_state
                     .surfaces[raster_config.surface_index.0]
                     .device_pixel_scale;
 
@@ -4292,20 +4311,18 @@ impl PicturePrimitive {
                         port,
                     });
 
                     frame_state.render_tasks.add_dependency(
                         frame_state.surfaces[parent_surface_index.0].render_tasks.unwrap().port,
                         root,
                     );
                 }
-
-                unclipped
             }
-            None => DeviceRect::zero()
+            None => {}
         };
 
         #[cfg(feature = "capture")]
         {
             if frame_context.debug_flags.contains(DebugFlags::TILE_CACHE_LOGGING_DBG) {
                 if let Some(ref tile_cache) = self.tile_cache
                 {
                     // extract just the fields that we're interested in
@@ -4322,24 +4339,23 @@ impl PicturePrimitive {
                             fract_offset: tile.fract_offset,
                             id: tile.id,
                             root: tile.root.clone(),
                             background_color: tile.background_color,
                             invalidation_reason: tile.invalidation_reason.clone()
                         });
                     }
                     let text = ron::ser::to_string_pretty(&tile_cache_tiny, Default::default()).unwrap();
-                    tile_cache_logger.add(text, unclipped);
+                    tile_cache_logger.add(text, map_pic_to_world.get_transform());
                 }
             }
         }
         #[cfg(not(feature = "capture"))]
         {
             let _tile_cache_logger = tile_cache_logger;   // unused variable fix
-            let _unclipped = unclipped;
         }
 
         let state = PictureState {
             //TODO: check for MAX_CACHE_SIZE here?
             map_local_to_pic,
             map_pic_to_world,
             map_pic_to_raster,
             map_raster_to_world,