Bug 1432789 - Update webrender to 1d8157c71f88d5c673f5d084f02515ab74263814. r=jrmuizel
authorKartikaya Gupta <kgupta@mozilla.com>
Fri, 26 Jan 2018 15:54:36 -0500
changeset 401067 c84117f5a978c4368c510b93cc4a5582cc6dca8c
parent 401066 59ac85f056505d33bc90a77ef47f42c0ca62da2b
child 401068 ee3ef79bf57b4789b71c6ee1ee6ed11c5f23f4e7
push id99287
push usercsabou@mozilla.com
push dateSat, 27 Jan 2018 09:59:12 +0000
treeherdermozilla-inbound@fa72e94a9055 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjrmuizel
bugs1432789
milestone60.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 1432789 - Update webrender to 1d8157c71f88d5c673f5d084f02515ab74263814. r=jrmuizel MozReview-Commit-ID: JakzOipPChe
gfx/doc/README.webrender
gfx/webrender/Cargo.toml
gfx/webrender/res/cs_text_run.glsl
gfx/webrender/res/ps_border_corner.glsl
gfx/webrender/res/ps_border_edge.glsl
gfx/webrender/src/capture.rs
gfx/webrender/src/frame_builder.rs
gfx/webrender/src/gpu_cache.rs
gfx/webrender/src/internal_types.rs
gfx/webrender/src/render_backend.rs
gfx/webrender/src/renderer.rs
gfx/webrender/src/tiling.rs
gfx/wrench/.gitignore
gfx/wrench/Cargo.toml
gfx/wrench/README.md
gfx/wrench/build.rs
gfx/wrench/res/wrench.exe.manifest
gfx/wrench/src/args.yaml
gfx/wrench/src/binary_frame_reader.rs
gfx/wrench/src/blob.rs
gfx/wrench/src/cgfont_to_data.rs
gfx/wrench/src/json_frame_writer.rs
gfx/wrench/src/main.rs
gfx/wrench/src/parse_function.rs
gfx/wrench/src/perf.rs
gfx/wrench/src/png.rs
gfx/wrench/src/premultiply.rs
gfx/wrench/src/rawtest.rs
gfx/wrench/src/reftest.rs
gfx/wrench/src/ron_frame_writer.rs
gfx/wrench/src/scene.rs
gfx/wrench/src/wrench.rs
gfx/wrench/src/yaml_frame_reader.rs
gfx/wrench/src/yaml_frame_writer.rs
gfx/wrench/src/yaml_helper.rs
--- a/gfx/doc/README.webrender
+++ b/gfx/doc/README.webrender
@@ -43,20 +43,22 @@ Option A:
 Option B:
    Do the update manually. This is a little more cumbersome but may be required
    if the script doesn't work or the repos are in a state that violates hidden
    assumptions in the script (e.g. if the webrender_bindings/Cargo.toml file is
    no longer in the format expected by the script). The steps to do this are,
    roughly:
    - Update your mozilla-central checkout to the latest code on mozilla-central.
    - Check out and update the webrender repo to the version you want
-   - Copy over the webrender and webrender_api folders into gfx/. The best way
-     to do this is to simply delete the gfx/webrender and gfx/webrender_api
-     folders and use |cp -R| to copy them in again from the webrender repo. Update
-     the "latest commit" information at the bottom of this file with the version.
+   - Copy over the webrender, webrender_api, and part of the wrench folders into
+     gfx/. The best way to do this is to simply delete the gfx/webrender,
+     gfx/webrender_api, and gfx/wrench folders and use |cp -R| to copy them in
+     again from the webrender repo, and then delete the gfx/wrench/reftests,
+     gfx/wrench/benchmarks, and gfx/wrench/script folders. Update the "latest
+     commit" information at the bottom of this file with the version.
    - If you need to modify webrender_bindings/Cargo.toml file, do so now. Changes
      at this step usually consist of:
      (a) Updating version numbers. Go through the version numbers of ALL the
          dependencies in the Cargo.toml file (webrender, euclid, etc.) and make
          sure the version numbers listed match what's in the new
          gfx/webrender/Cargo.toml and gfx/webrender_api/Cargo.toml files.
      (b) Turning on or off any new features that were added in upstream WR. This
          used to happen a lot but is pretty rare now.
@@ -170,9 +172,9 @@ 2. Sometimes autoland tip has changed en
    has an env var you can set to do this). In theory you can get the same
    result by resolving the conflict manually but Cargo.lock files are usually not
    trivial to merge by hand. If it's just the third_party/rust dir that has conflicts
    you can delete it and run |mach vendor rust| again to repopulate it.
 
 -------------------------------------------------------------------------------
 
 The version of WebRender currently in the tree is:
-c0943271eb8c6440a61db37e2f1e84201dcac2e3
+1d8157c71f88d5c673f5d084f02515ab74263814
--- a/gfx/webrender/Cargo.toml
+++ b/gfx/webrender/Cargo.toml
@@ -31,17 +31,17 @@ thread_profiler = "0.1.1"
 plane-split = "0.7"
 png = { optional = true, version = "0.11" }
 smallvec = "0.6"
 ws = { optional = true, version = "0.7.3" }
 serde_json = { optional = true, version = "1.0" }
 serde = { optional = true, version = "1.0", features = ["serde_derive"] }
 image = { optional = true, version = "0.17" }
 base64 = { optional = true, version = "0.3.0" }
-ron = { optional = true, version = "0.1.5" }
+ron = { optional = true, version = "0.1.7" }
 
 [dev-dependencies]
 angle = {git = "https://github.com/servo/angle", branch = "servo"}
 env_logger = "0.4"
 rand = "0.3"                # for the benchmarks
 servo-glutin = "0.13"     # for the example apps
 
 [target.'cfg(any(target_os = "android", all(unix, not(target_os = "macos"))))'.dependencies]
--- a/gfx/webrender/res/cs_text_run.glsl
+++ b/gfx/webrender/res/cs_text_run.glsl
@@ -1,16 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include shared,prim_shared
 
 varying vec3 vUv;
 flat varying vec4 vColor;
+flat varying vec4 vStRect;
 
 #ifdef WR_VERTEX_SHADER
 // Draw a text run to a cache target. These are always
 // drawn un-transformed. These are used for effects such
 // as text-shadow.
 
 void main(void) {
     Primitive prim = load_primitive();
@@ -36,17 +37,23 @@ void main(void) {
     gl_Position = uTransform * vec4(local_pos, 0.0, 1.0);
 
     vec2 texture_size = vec2(textureSize(sColor0, 0));
     vec2 st0 = res.uv_rect.xy / texture_size;
     vec2 st1 = res.uv_rect.zw / texture_size;
 
     vUv = vec3(mix(st0, st1, aPosition.xy), res.layer);
     vColor = prim.task.color;
+
+    // We clamp the texture coordinates to the half-pixel offset from the borders
+    // in order to avoid sampling outside of the texture area.
+    vec2 half_texel = vec2(0.5) / texture_size;
+    vStRect = vec4(min(st0, st1) + half_texel, max(st0, st1) - half_texel);
 }
 #endif
 
 #ifdef WR_FRAGMENT_SHADER
 void main(void) {
-    float a = texture(sColor0, vUv).a;
+    vec2 uv = clamp(vUv.xy, vStRect.xy, vStRect.zw);
+    float a = texture(sColor0, vec3(uv, vUv.z)).a;
     oFragColor = vColor * a;
 }
 #endif
--- a/gfx/webrender/res/ps_border_corner.glsl
+++ b/gfx/webrender/res/ps_border_corner.glsl
@@ -148,16 +148,17 @@ void main(void) {
     vec2 p0, p1;
 
     // TODO(gw): We'll need to pass through multiple styles
     //           once we support style transitions per corner.
     int style;
     vec4 edge_distances;
     vec4 color0, color1;
     vec2 color_delta;
+    vec4 edge_mask;
 
     // TODO(gw): Now that all border styles are supported, the switch
     //           statement below can be tidied up quite a bit.
 
     switch (sub_part) {
         case 0: {
             p0 = corners.tl_outer;
             p1 = corners.tl_inner;
@@ -175,16 +176,17 @@ void main(void) {
             set_edge_line(border.widths.xy,
                           corners.tl_outer,
                           vec2(1.0, 1.0));
             edge_distances = vec4(p0 + adjusted_widths.xy,
                                   p0 + inv_adjusted_widths.xy);
             color_delta = vec2(1.0);
             vIsBorderRadiusLessThanBorderWidth = any(lessThan(border.radii[0].xy,
                                                               border.widths.xy)) ? 1.0 : 0.0;
+            edge_mask = vec4(1.0, 1.0, 0.0, 0.0);
             break;
         }
         case 1: {
             p0 = vec2(corners.tr_inner.x, corners.tr_outer.y);
             p1 = vec2(corners.tr_outer.x, corners.tr_inner.y);
             color0 = border.colors[1];
             color1 = border.colors[2];
             vClipCenter = corners.tr_outer + vec2(-border.radii[0].z, border.radii[0].w);
@@ -201,16 +203,17 @@ void main(void) {
                           vec2(-1.0, 1.0));
             edge_distances = vec4(p1.x - adjusted_widths.z,
                                   p0.y + adjusted_widths.y,
                                   p1.x - border.widths.z + adjusted_widths.z,
                                   p0.y + inv_adjusted_widths.y);
             color_delta = vec2(1.0, -1.0);
             vIsBorderRadiusLessThanBorderWidth = any(lessThan(border.radii[0].zw,
                                                               border.widths.zy)) ? 1.0 : 0.0;
+            edge_mask = vec4(0.0, 1.0, 1.0, 0.0);
             break;
         }
         case 2: {
             p0 = corners.br_inner;
             p1 = corners.br_outer;
             color0 = border.colors[2];
             color1 = border.colors[3];
             vClipCenter = corners.br_outer - border.radii[1].xy;
@@ -227,16 +230,17 @@ void main(void) {
                           vec2(-1.0, -1.0));
             edge_distances = vec4(p1.x - adjusted_widths.z,
                                   p1.y - adjusted_widths.w,
                                   p1.x - border.widths.z + adjusted_widths.z,
                                   p1.y - border.widths.w + adjusted_widths.w);
             color_delta = vec2(-1.0);
             vIsBorderRadiusLessThanBorderWidth = any(lessThan(border.radii[1].xy,
                                                               border.widths.zw)) ? 1.0 : 0.0;
+            edge_mask = vec4(0.0, 0.0, 1.0, 1.0);
             break;
         }
         case 3: {
             p0 = vec2(corners.bl_outer.x, corners.bl_inner.y);
             p1 = vec2(corners.bl_inner.x, corners.bl_outer.y);
             color0 = border.colors[3];
             color1 = border.colors[0];
             vClipCenter = corners.bl_outer + vec2(border.radii[1].z, -border.radii[1].w);
@@ -253,16 +257,17 @@ void main(void) {
                           vec2(1.0, -1.0));
             edge_distances = vec4(p0.x + adjusted_widths.x,
                                   p1.y - adjusted_widths.w,
                                   p0.x + inv_adjusted_widths.x,
                                   p1.y - border.widths.w + adjusted_widths.w);
             color_delta = vec2(-1.0, 1.0);
             vIsBorderRadiusLessThanBorderWidth = any(lessThan(border.radii[1].zw,
                                                               border.widths.xw)) ? 1.0 : 0.0;
+            edge_mask = vec4(1.0, 0.0, 0.0, 1.0);
             break;
         }
     }
 
     switch (style) {
         case BORDER_STYLE_DOUBLE: {
             vEdgeDistance = edge_distances;
             vAlphaSelect = 0.0;
@@ -296,17 +301,17 @@ void main(void) {
     RectWithSize segment_rect;
     segment_rect.p0 = p0;
     segment_rect.size = p1 - p0;
 
 #ifdef WR_FEATURE_TRANSFORM
     VertexInfo vi = write_transform_vertex(segment_rect,
                                            prim.local_rect,
                                            prim.local_clip_rect,
-                                           vec4(1.0),
+                                           edge_mask,
                                            prim.z,
                                            prim.scroll_node,
                                            prim.task);
 #else
     VertexInfo vi = write_vertex(segment_rect,
                                  prim.local_clip_rect,
                                  prim.z,
                                  prim.scroll_node,
--- a/gfx/webrender/res/ps_border_edge.glsl
+++ b/gfx/webrender/res/ps_border_edge.glsl
@@ -147,84 +147,90 @@ void main(void) {
 
     // TODO(gw): Now that all border styles are supported, the switch
     //           statement below can be tidied up quite a bit.
 
     float style;
     bool color_flip;
 
     RectWithSize segment_rect;
+    vec4 edge_mask;
+
     switch (sub_part) {
         case 0: {
             segment_rect.p0 = vec2(corners.tl_outer.x, corners.tl_inner.y);
             segment_rect.size = vec2(border.widths.x, corners.bl_inner.y - corners.tl_inner.y);
             vec4 adjusted_widths = get_effective_border_widths(border, int(border.style.x));
             write_edge_distance(segment_rect.p0.x, border.widths.x, adjusted_widths.x, border.style.x, 0.0, 1.0);
             style = border.style.x;
             color_flip = false;
             write_clip_params(border.style.x,
                               border.widths.x,
                               segment_rect.size.y,
                               segment_rect.p0.y,
                               segment_rect.p0.x + 0.5 * segment_rect.size.x);
+            edge_mask = vec4(1.0, 0.0, 1.0, 0.0);
             break;
         }
         case 1: {
             segment_rect.p0 = vec2(corners.tl_inner.x, corners.tl_outer.y);
             segment_rect.size = vec2(corners.tr_inner.x - corners.tl_inner.x, border.widths.y);
             vec4 adjusted_widths = get_effective_border_widths(border, int(border.style.y));
             write_edge_distance(segment_rect.p0.y, border.widths.y, adjusted_widths.y, border.style.y, 1.0, 1.0);
             style = border.style.y;
             color_flip = false;
             write_clip_params(border.style.y,
                               border.widths.y,
                               segment_rect.size.x,
                               segment_rect.p0.x,
                               segment_rect.p0.y + 0.5 * segment_rect.size.y);
+            edge_mask = vec4(0.0, 1.0, 0.0, 1.0);
             break;
         }
         case 2: {
             segment_rect.p0 = vec2(corners.tr_outer.x - border.widths.z, corners.tr_inner.y);
             segment_rect.size = vec2(border.widths.z, corners.br_inner.y - corners.tr_inner.y);
             vec4 adjusted_widths = get_effective_border_widths(border, int(border.style.z));
             write_edge_distance(segment_rect.p0.x, border.widths.z, adjusted_widths.z, border.style.z, 0.0, -1.0);
             style = border.style.z;
             color_flip = true;
             write_clip_params(border.style.z,
                               border.widths.z,
                               segment_rect.size.y,
                               segment_rect.p0.y,
                               segment_rect.p0.x + 0.5 * segment_rect.size.x);
+            edge_mask = vec4(1.0, 0.0, 1.0, 0.0);
             break;
         }
         case 3: {
             segment_rect.p0 = vec2(corners.bl_inner.x, corners.bl_outer.y - border.widths.w);
             segment_rect.size = vec2(corners.br_inner.x - corners.bl_inner.x, border.widths.w);
             vec4 adjusted_widths = get_effective_border_widths(border, int(border.style.w));
             write_edge_distance(segment_rect.p0.y, border.widths.w, adjusted_widths.w, border.style.w, 1.0, -1.0);
             style = border.style.w;
             color_flip = true;
             write_clip_params(border.style.w,
                               border.widths.w,
                               segment_rect.size.x,
                               segment_rect.p0.x,
                               segment_rect.p0.y + 0.5 * segment_rect.size.y);
+            edge_mask = vec4(0.0, 1.0, 0.0, 1.0);
             break;
         }
     }
 
     write_alpha_select(style);
     write_color0(color, style, color_flip);
     write_color1(color, style, color_flip);
 
 #ifdef WR_FEATURE_TRANSFORM
     VertexInfo vi = write_transform_vertex(segment_rect,
                                            prim.local_rect,
                                            prim.local_clip_rect,
-                                           vec4(1.0),
+                                           edge_mask,
                                            prim.z,
                                            prim.scroll_node,
                                            prim.task);
 #else
     VertexInfo vi = write_vertex(segment_rect,
                                  prim.local_clip_rect,
                                  prim.z,
                                  prim.scroll_node,
--- a/gfx/webrender/src/capture.rs
+++ b/gfx/webrender/src/capture.rs
@@ -19,17 +19,20 @@ pub struct CaptureConfig {
     pretty: ser::PrettyConfig,
 }
 
 impl CaptureConfig {
     pub fn new(root: PathBuf, bits: CaptureBits) -> Self {
         CaptureConfig {
             root,
             bits,
-            pretty: ser::PrettyConfig::default(),
+            pretty: ser::PrettyConfig {
+                enumerate_arrays: true,
+                .. ser::PrettyConfig::default()
+            },
         }
     }
 
     pub fn serialize<T, P>(&self, data: &T, name: P)
     where
         T: Serialize,
         P: AsRef<Path>,
     {
--- a/gfx/webrender/src/frame_builder.rs
+++ b/gfx/webrender/src/frame_builder.rs
@@ -1788,17 +1788,17 @@ impl FrameBuilder {
                 RenderPassIndex(pass_index),
             );
 
             if let RenderPassKind::OffScreen { ref texture_cache, .. } = pass.kind {
                 has_texture_cache_tasks |= !texture_cache.is_empty();
             }
         }
 
-        let gpu_cache_updates = gpu_cache.end_frame(gpu_cache_profile);
+        let gpu_cache_frame_id = gpu_cache.end_frame(gpu_cache_profile);
 
         render_tasks.build();
 
         resource_cache.end_frame();
 
         Frame {
             window_size,
             inner_rect: self.screen_rect,
@@ -1806,14 +1806,14 @@ impl FrameBuilder {
             background_color: self.background_color,
             layer,
             profile_counters,
             passes,
             node_data,
             clip_chain_local_clip_rects,
             render_tasks,
             deferred_resolves,
-            gpu_cache_updates: Some(gpu_cache_updates),
+            gpu_cache_frame_id,
             has_been_rendered: false,
             has_texture_cache_tasks,
         }
     }
 }
--- a/gfx/webrender/src/gpu_cache.rs
+++ b/gfx/webrender/src/gpu_cache.rs
@@ -216,25 +216,28 @@ impl Row {
 pub enum GpuCacheUpdate {
     Copy {
         block_index: usize,
         block_count: usize,
         address: GpuCacheAddress,
     },
 }
 
+#[must_use]
 #[cfg_attr(feature = "capture", derive(Deserialize, Serialize))]
 pub struct GpuCacheUpdateList {
-    // The current height of the texture. The render thread
-    // should resize the texture if required.
+    /// The frame current update list was generated from.
+    pub frame_id: FrameId,
+    /// The current height of the texture. The render thread
+    /// should resize the texture if required.
     pub height: u32,
-    // List of updates to apply.
+    /// List of updates to apply.
     pub updates: Vec<GpuCacheUpdate>,
-    // A flat list of GPU blocks that are pending upload
-    // to GPU memory.
+    /// A flat list of GPU blocks that are pending upload
+    /// to GPU memory.
     pub blocks: Vec<GpuBlockData>,
 }
 
 // Holds the free lists of fixed size blocks. Mostly
 // just serves to work around the borrow checker.
 #[cfg_attr(feature = "capture", derive(Deserialize, Serialize))]
 struct FreeBlockLists {
     free_list_1: Option<BlockIndex>,
@@ -590,30 +593,35 @@ impl GpuCache {
         GpuCacheHandle {
             location: Some(location),
         }
     }
 
     /// End the frame. Return the list of updates to apply to the
     /// device specific cache texture.
     pub fn end_frame(
-        &mut self,
+        &self,
         profile_counters: &mut GpuCacheProfileCounters,
-    ) -> GpuCacheUpdateList {
+    ) -> FrameId {
         profile_counters
             .allocated_rows
             .set(self.texture.rows.len());
         profile_counters
             .allocated_blocks
             .set(self.texture.allocated_block_count);
         profile_counters
             .saved_blocks
             .set(self.saved_block_count);
+        self.frame_id
+    }
 
+    /// Extract the pending updates from the cache.
+    pub fn extract_updates(&mut self) -> GpuCacheUpdateList {
         GpuCacheUpdateList {
+            frame_id: self.frame_id,
             height: self.texture.height,
             updates: mem::replace(&mut self.texture.updates, Vec::new()),
             blocks: mem::replace(&mut self.texture.pending_blocks, Vec::new()),
         }
     }
 
     /// Get the actual GPU address in the texture for a given slot ID.
     /// It's assumed at this point that the given slot has been requested
--- a/gfx/webrender/src/internal_types.rs
+++ b/gfx/webrender/src/internal_types.rs
@@ -2,16 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{ClipId, DeviceUintRect, DocumentId, Epoch};
 use api::{ExternalImageData, ExternalImageId};
 use api::{ImageFormat, PipelineId};
 use api::DebugCommand;
 use device::TextureFilter;
+use gpu_cache::GpuCacheUpdateList;
 use fxhash::FxHasher;
 use profiler::BackendProfileCounters;
 use std::{usize, i32};
 use std::collections::{HashMap, HashSet};
 use std::f32;
 use std::hash::BuildHasherDefault;
 use std::path::PathBuf;
 use std::sync::Arc;
@@ -156,19 +157,20 @@ pub enum DebugOutput {
     #[cfg(feature = "capture")]
     LoadCapture(PathBuf, Vec<PlainExternalImage>),
 }
 
 pub enum ResultMsg {
     DebugCommand(DebugCommand),
     DebugOutput(DebugOutput),
     RefreshShader(PathBuf),
+    UpdateGpuCache(GpuCacheUpdateList),
+    UpdateResources {
+        updates: TextureUpdateList,
+        cancel_rendering: bool,
+    },
     PublishDocument(
         DocumentId,
         RenderedDocument,
         TextureUpdateList,
         BackendProfileCounters,
     ),
-    UpdateResources {
-        updates: TextureUpdateList,
-        cancel_rendering: bool,
-    },
 }
--- a/gfx/webrender/src/render_backend.rs
+++ b/gfx/webrender/src/render_backend.rs
@@ -1,31 +1,31 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{ApiMsg, BlobImageRenderer, BuiltDisplayList, DebugCommand, DeviceIntPoint};
 #[cfg(feature = "debugger")]
 use api::{BuiltDisplayListIter, SpecificDisplayItem};
 use api::{DevicePixelScale, DeviceUintPoint, DeviceUintRect, DeviceUintSize};
-use api::{DocumentId, DocumentLayer, DocumentMsg};
-use api::{IdNamespace, PipelineId, RenderNotifier};
-use api::channel::{MsgReceiver, PayloadReceiver, PayloadReceiverHelperMethods};
+use api::{DocumentId, DocumentLayer, DocumentMsg, HitTestFlags, HitTestResult};
+use api::{IdNamespace, PipelineId, RenderNotifier, WorldPoint};
+use api::channel::{MsgReceiver, MsgSender, PayloadReceiver, PayloadReceiverHelperMethods};
 use api::channel::{PayloadSender, PayloadSenderHelperMethods};
 #[cfg(feature = "capture")]
 use api::CapturedDocument;
 #[cfg(feature = "capture")]
 use capture::{CaptureConfig, ExternalCaptureImage};
 #[cfg(feature = "debugger")]
 use debug_server;
 use frame::FrameContext;
 use frame_builder::{FrameBuilder, FrameBuilderConfig};
 use gpu_cache::GpuCache;
 use internal_types::{DebugOutput, FastHashMap, FastHashSet, RenderedDocument, ResultMsg};
-use profiler::{BackendProfileCounters, ResourceProfileCounters};
+use profiler::{BackendProfileCounters, IpcProfileCounters, ResourceProfileCounters};
 use rayon::ThreadPool;
 use record::ApiRecordingReceiver;
 use resource_cache::ResourceCache;
 #[cfg(feature = "capture")]
 use resource_cache::{PlainCacheOwn, PlainResources};
 use scene::Scene;
 #[cfg(feature = "serialize")]
 use serde::{Serialize, Deserialize};
@@ -148,42 +148,47 @@ impl Document {
             pan,
             &mut resource_profile.texture_cache,
             &mut resource_profile.gpu_cache,
             &self.scene.properties,
         )
     }
 }
 
+type HitTestQuery = (Option<PipelineId>, WorldPoint, HitTestFlags, MsgSender<HitTestResult>);
+
 struct DocumentOps {
     scroll: bool,
     build: bool,
     render: bool,
+    queries: Vec<HitTestQuery>,
 }
 
 impl DocumentOps {
     fn nop() -> Self {
         DocumentOps {
             scroll: false,
             build: false,
             render: false,
+            queries: vec![],
         }
     }
 
     fn build() -> Self {
         DocumentOps {
             build: true,
             ..DocumentOps::nop()
         }
     }
 
-    fn combine(&mut self, other: Self) {
+    fn combine(&mut self, mut other: Self) {
         self.scroll = self.scroll || other.scroll;
         self.build = self.build || other.build;
         self.render = self.render || other.render;
+        self.queries.extend(other.queries.drain(..));
     }
 }
 
 /// The unique id for WR resource identification.
 static NEXT_NAMESPACE_ID: AtomicUsize = ATOMIC_USIZE_INIT;
 
 #[cfg(feature = "capture")]
 #[derive(Serialize, Deserialize)]
@@ -255,17 +260,18 @@ impl RenderBackend {
         }
     }
 
     fn process_document(
         &mut self,
         document_id: DocumentId,
         message: DocumentMsg,
         frame_counter: u32,
-        profile_counters: &mut BackendProfileCounters,
+        ipc_profile_counters: &mut IpcProfileCounters,
+        resource_profile_counters: &mut ResourceProfileCounters,
     ) -> DocumentOps {
         let doc = self.documents.get_mut(&document_id).expect("No document?");
 
         match message {
             //TODO: move view-related messages in a separate enum?
             DocumentMsg::SetPageZoom(factor) => {
                 doc.view.page_zoom_factor = factor.get();
                 DocumentOps::nop()
@@ -327,17 +333,16 @@ impl RenderBackend {
                 }
 
                 let display_list_len = built_display_list.data().len();
                 let (builder_start_time, builder_finish_time, send_start_time) =
                     built_display_list.times();
                 let display_list_received_time = precise_time_ns();
 
                 {
-                    let _timer = profile_counters.total_time.timer();
                     doc.scene.set_display_list(
                         pipeline_id,
                         epoch,
                         built_display_list,
                         background,
                         viewport_size,
                         content_size,
                     );
@@ -347,33 +352,33 @@ impl RenderBackend {
                     *ros = false; //wait for `GenerateFrame`
                 }
 
                 // Note: this isn't quite right as auxiliary values will be
                 // pulled out somewhere in the prim_store, but aux values are
                 // really simple and cheap to access, so it's not a big deal.
                 let display_list_consumed_time = precise_time_ns();
 
-                profile_counters.ipc.set(
+                ipc_profile_counters.set(
                     builder_start_time,
                     builder_finish_time,
                     send_start_time,
                     display_list_received_time,
                     display_list_consumed_time,
                     display_list_len,
                 );
 
                 DocumentOps::build()
             }
             DocumentMsg::UpdateResources(updates) => {
                 profile_scope!("UpdateResources");
 
                 self.resource_cache.update_resources(
                     updates,
-                    &mut profile_counters.resources
+                    resource_profile_counters
                 );
 
                 DocumentOps::nop()
             }
             DocumentMsg::UpdateEpoch(pipeline_id, epoch) => {
                 doc.scene.update_epoch(pipeline_id, epoch);
                 doc.frame_ctx.update_epoch(pipeline_id, epoch);
                 DocumentOps::nop()
@@ -391,69 +396,54 @@ impl RenderBackend {
             DocumentMsg::RemovePipeline(pipeline_id) => {
                 profile_scope!("RemovePipeline");
 
                 doc.scene.remove_pipeline(pipeline_id);
                 DocumentOps::nop()
             }
             DocumentMsg::Scroll(delta, cursor, move_phase) => {
                 profile_scope!("Scroll");
-                let _timer = profile_counters.total_time.timer();
 
                 let should_render = doc.frame_ctx.scroll(delta, cursor, move_phase)
                     && doc.render_on_scroll == Some(true);
 
                 DocumentOps {
                     scroll: true,
-                    build: false,
                     render: should_render,
+                    ..DocumentOps::nop()
                 }
             }
             DocumentMsg::HitTest(pipeline_id, point, flags, tx) => {
-                profile_scope!("HitTest");
-                if doc.render_on_hittest {
-                    doc.render(
-                        &mut self.resource_cache,
-                        &mut self.gpu_cache,
-                        &mut profile_counters.resources,
-                    );
-                    doc.render_on_hittest = false;
+                DocumentOps {
+                    render: doc.render_on_hittest,
+                    queries: vec![(pipeline_id, point, flags, tx)],
+                    ..DocumentOps::nop()
                 }
-
-                let cst = doc.frame_ctx.get_clip_scroll_tree();
-                let result = doc.frame_builder
-                    .as_ref()
-                    .unwrap()
-                    .hit_test(cst, pipeline_id, point, flags);
-                tx.send(result).unwrap();
-                DocumentOps::nop()
             }
             DocumentMsg::ScrollNodeWithId(origin, id, clamp) => {
                 profile_scope!("ScrollNodeWithScrollId");
-                let _timer = profile_counters.total_time.timer();
 
                 let should_render = doc.frame_ctx.scroll_node(origin, id, clamp)
                     && doc.render_on_scroll == Some(true);
 
                 DocumentOps {
                     scroll: true,
-                    build: false,
                     render: should_render,
+                    ..DocumentOps::nop()
                 }
             }
             DocumentMsg::TickScrollingBounce => {
                 profile_scope!("TickScrollingBounce");
-                let _timer = profile_counters.total_time.timer();
 
                 doc.frame_ctx.tick_scrolling_bounce_animations();
 
                 DocumentOps {
                     scroll: true,
-                    build: false,
                     render: doc.render_on_scroll == Some(true),
+                    ..DocumentOps::nop()
                 }
             }
             DocumentMsg::GetScrollNodeState(tx) => {
                 profile_scope!("GetScrollNodeState");
                 tx.send(doc.frame_ctx.get_scroll_node_state()).unwrap();
                 DocumentOps::nop()
             }
             DocumentMsg::UpdateDynamicProperties(property_bindings) => {
@@ -466,18 +456,16 @@ impl RenderBackend {
                 // TODO(gw): Once the scrolling / reference frame changes
                 //           are completed, optimize the internals of
                 //           animated properties to not require a full
                 //           rebuild of the frame!
                 doc.scene.properties.set_properties(property_bindings);
                 DocumentOps::build()
             }
             DocumentMsg::GenerateFrame => {
-                let _timer = profile_counters.total_time.timer();
-
                 let mut op = DocumentOps::nop();
 
                 if let Some(ref mut ros) = doc.render_on_scroll {
                     *ros = true;
                 }
 
                 if doc.scene.root_pipeline_id.is_some() {
                     op.render = true;
@@ -651,63 +639,87 @@ impl RenderBackend {
         &mut self,
         document_id: DocumentId,
         doc_msgs: Vec<DocumentMsg>,
         frame_counter: &mut u32,
         profile_counters: &mut BackendProfileCounters,
     ) {
         let mut op = DocumentOps::nop();
         for doc_msg in doc_msgs {
+            let _timer = profile_counters.total_time.timer();
             op.combine(
                 self.process_document(
                     document_id,
                     doc_msg,
                     *frame_counter,
-                    profile_counters,
+                    &mut profile_counters.ipc,
+                    &mut profile_counters.resources,
                 )
             );
         }
 
         let doc = self.documents.get_mut(&document_id).unwrap();
 
         if op.build {
+            let _timer = profile_counters.total_time.timer();
             profile_scope!("build scene");
             doc.build_scene(&mut self.resource_cache);
             doc.render_on_hittest = true;
         }
 
         if op.render {
             profile_scope!("generate frame");
 
             *frame_counter += 1;
-            let rendered_document = doc.render(
-                &mut self.resource_cache,
-                &mut self.gpu_cache,
-                &mut profile_counters.resources,
-            );
+
+            // borrow ck hack for profile_counters
+            let (pending_update, rendered_document) = {
+                let _timer = profile_counters.total_time.timer();
 
-            info!("generated frame for document {:?} with {} passes",
-                document_id, rendered_document.frame.passes.len());
+                let rendered_document = doc.render(
+                    &mut self.resource_cache,
+                    &mut self.gpu_cache,
+                    &mut profile_counters.resources,
+                );
+
+                info!("generated frame for document {:?} with {} passes",
+                    document_id, rendered_document.frame.passes.len());
+
+                let msg = ResultMsg::UpdateGpuCache(self.gpu_cache.extract_updates());
+                self.result_tx.send(msg).unwrap();
+
+                let pending_update = self.resource_cache.pending_updates();
+                (pending_update, rendered_document)
+            };
 
             // Publish the frame
-            let pending_update = self.resource_cache.pending_updates();
             let msg = ResultMsg::PublishDocument(
                 document_id,
                 rendered_document,
                 pending_update,
                 profile_counters.clone()
             );
             self.result_tx.send(msg).unwrap();
             profile_counters.reset();
             doc.render_on_hittest = false;
         }
 
         if op.render || op.scroll {
             self.notifier.new_document_ready(document_id, op.scroll, op.render);
         }
+
+        for (pipeline_id, point, flags, tx) in op.queries {
+            profile_scope!("HitTest");
+            let cst = doc.frame_ctx.get_clip_scroll_tree();
+            let result = doc.frame_builder
+                .as_ref()
+                .unwrap()
+                .hit_test(cst, pipeline_id, point, flags);
+            tx.send(result).unwrap();
+        }
     }
 
     #[cfg(not(feature = "debugger"))]
     fn get_docs_for_debugger(&self) -> String {
         String::new()
     }
 
     #[cfg(feature = "debugger")]
--- a/gfx/webrender/src/renderer.rs
+++ b/gfx/webrender/src/renderer.rs
@@ -1651,16 +1651,18 @@ pub struct Renderer {
     blur_vao: VAO,
     clip_vao: VAO,
 
     node_data_texture: VertexDataTexture,
     local_clip_rects_texture: VertexDataTexture,
     render_task_texture: VertexDataTexture,
     gpu_cache_texture: CacheTexture,
 
+    gpu_cache_frame_id: FrameId,
+
     pipeline_epoch_map: FastHashMap<PipelineId, Epoch>,
 
     // Manages and resolves source textures IDs to real texture IDs.
     texture_resolver: SourceTextureResolver,
 
     // A PBO used to do asynchronous texture cache uploads.
     texture_cache_upload_pbo: PBO,
 
@@ -2308,16 +2310,17 @@ impl Renderer {
             pipeline_epoch_map: FastHashMap::default(),
             dither_matrix_texture,
             external_image_handler: None,
             output_image_handler: None,
             output_targets: FastHashMap::default(),
             cpu_profiles: VecDeque::new(),
             gpu_profiles: VecDeque::new(),
             gpu_cache_texture,
+            gpu_cache_frame_id: FrameId::new(0),
             texture_cache_upload_pbo,
             texture_resolver,
             renderer_errors: Vec::new(),
             capture,
         };
 
         renderer.set_debug_flags(options.debug_flags);
 
@@ -2363,29 +2366,27 @@ impl Renderer {
         self.device.update_program_cache(cached_programs);
     }
 
     /// Processes the result queue.
     ///
     /// Should be called before `render()`, as texture cache updates are done here.
     pub fn update(&mut self) {
         profile_scope!("update");
-
         // Pull any pending results and return the most recent.
         while let Ok(msg) = self.result_rx.try_recv() {
             match msg {
                 ResultMsg::PublishDocument(
                     document_id,
-                    mut doc,
+                    doc,
                     texture_update_list,
                     profile_counters,
                 ) => {
                     //TODO: associate `document_id` with target window
                     self.pending_texture_updates.push(texture_update_list);
-                    self.pending_gpu_cache_updates.extend(doc.frame.gpu_cache_updates.take());
                     self.backend_profile_counters = profile_counters;
 
                     // Update the list of available epochs for use during reftests.
                     // This is a workaround for https://github.com/servo/servo/issues/13149.
                     for (pipeline_id, epoch) in &doc.pipeline_epoch_map {
                         self.pipeline_epoch_map.insert(*pipeline_id, *epoch);
                     }
 
@@ -2399,16 +2400,19 @@ impl Renderer {
                             if self.active_documents[pos].1.frame.must_be_drawn() {
                                 self.render_impl(None).ok();
                             }
                             self.active_documents[pos].1 = doc;
                         }
                         None => self.active_documents.push((document_id, doc)),
                     }
                 }
+                ResultMsg::UpdateGpuCache(list) => {
+                    self.pending_gpu_cache_updates.push(list);
+                }
                 ResultMsg::UpdateResources {
                     updates,
                     cancel_rendering,
                 } => {
                     self.pending_texture_updates.push(updates);
                     self.update_texture_cache();
                     // If we receive a `PublishDocument` message followed by this one
                     // within the same update we need ot cancel the frame because we
@@ -2748,17 +2752,16 @@ impl Renderer {
     // to the main frame buffer. This is useful
     // to update texture cache render tasks but
     // avoid doing a full frame render.
     fn render_impl(
         &mut self,
         framebuffer_size: Option<DeviceUintSize>
     ) -> Result<RendererStats, Vec<RendererError>> {
         profile_scope!("render");
-
         if self.active_documents.is_empty() {
             self.last_time = precise_time_ns();
             return Ok(RendererStats::empty());
         }
 
         let mut stats = RendererStats::empty();
         let mut frame_profiles = Vec::new();
         let mut profile_timers = RendererProfileTimers::new();
@@ -2839,16 +2842,17 @@ impl Renderer {
 
             #[cfg(feature = "capture")]
             self.texture_resolver.external_images.extend(
                 self.capture.owned_external_images.iter().map(|(key, value)| (*key, value.clone()))
             );
 
             for &mut (_, RenderedDocument { ref mut frame, .. }) in &mut active_documents {
                 self.prepare_gpu_cache(frame);
+                assert!(frame.gpu_cache_frame_id <= self.gpu_cache_frame_id);
 
                 self.draw_tile_frame(
                     frame,
                     framebuffer_size,
                     clear_depth_value.is_some(),
                     cpu_frame_id,
                     &mut stats
                 );
@@ -2891,16 +2895,17 @@ impl Renderer {
                     &profile_samplers,
                     screen_fraction,
                     &mut self.debug,
                     self.debug_flags.contains(DebugFlags::COMPACT_PROFILER),
                 );
             }
         }
 
+        self.backend_profile_counters.reset();
         self.profile_counters.reset();
         self.profile_counters.frame_counter.inc();
 
         profile_timers.cpu_time.profile(|| {
             let _gm = self.gpu_profile.start_marker("end frame");
             self.gpu_profile.end_frame();
             self.debug.render(&mut self.device, framebuffer_size);
             self.device.end_frame();
@@ -2926,16 +2931,17 @@ impl Renderer {
         let deferred_update_list = self.update_deferred_resolves(&frame.deferred_resolves);
         self.pending_gpu_cache_updates.extend(deferred_update_list);
 
         // For an artificial stress test of GPU cache resizing,
         // always pass an extra update list with at least one block in it.
         let gpu_cache_height = self.gpu_cache_texture.get_height();
         if gpu_cache_height != 0 &&  GPU_CACHE_RESIZE_TEST {
             self.pending_gpu_cache_updates.push(GpuCacheUpdateList {
+                frame_id: FrameId::new(0),
                 height: gpu_cache_height,
                 blocks: vec![[1f32; 4].into()],
                 updates: Vec::new(),
             });
         }
 
         let (updated_blocks, max_requested_height) = self
             .pending_gpu_cache_updates
@@ -2950,16 +2956,19 @@ impl Renderer {
         self.gpu_cache_texture.prepare_for_updates(
             &mut self.device,
             updated_blocks,
             max_requested_height,
         );
 
         for update_list in self.pending_gpu_cache_updates.drain(..) {
             assert!(update_list.height <= max_requested_height);
+            if update_list.frame_id > self.gpu_cache_frame_id {
+                self.gpu_cache_frame_id = update_list.frame_id
+            }
             self.gpu_cache_texture
                 .update(&mut self.device, &update_list);
         }
 
         let updated_rows = self.gpu_cache_texture.flush(&mut self.device);
 
         // Note: the texture might have changed during the `update`,
         // so we need to bind it here.
@@ -4084,16 +4093,17 @@ impl Renderer {
             return None;
         }
 
         let handler = self.external_image_handler
             .as_mut()
             .expect("Found external image, but no handler set!");
 
         let mut list = GpuCacheUpdateList {
+            frame_id: FrameId::new(0),
             height: self.gpu_cache_texture.get_height(),
             blocks: Vec::new(),
             updates: Vec::new(),
         };
 
         for deferred_resolve in deferred_resolves {
             self.gpu_profile.place_marker("deferred resolve");
             let props = &deferred_resolve.image_properties;
--- a/gfx/webrender/src/tiling.rs
+++ b/gfx/webrender/src/tiling.rs
@@ -4,18 +4,18 @@
 
 use api::{ClipId, ColorF, DeviceIntPoint, DeviceIntRect, DeviceIntSize};
 use api::{DevicePixelScale, DeviceUintPoint, DeviceUintRect, DeviceUintSize};
 use api::{DocumentLayer, FilterOp, ImageFormat};
 use api::{LayerRect, MixBlendMode, PipelineId};
 use batch::{AlphaBatcher, ClipBatcher, resolve_image};
 use clip::{ClipStore};
 use clip_scroll_tree::{ClipScrollTree};
-use device::Texture;
-use gpu_cache::{GpuCache, GpuCacheUpdateList};
+use device::{FrameId, Texture};
+use gpu_cache::{GpuCache};
 use gpu_types::{BlurDirection, BlurInstance, BrushInstance, ClipChainRectIndex};
 use gpu_types::{ClipScrollNodeData, ClipScrollNodeIndex};
 use gpu_types::{PrimitiveInstance};
 use internal_types::{FastHashMap, RenderPassIndex, SourceTexture};
 use picture::{PictureKind};
 use prim_store::{PrimitiveIndex, PrimitiveKind, PrimitiveStore};
 use prim_store::{BrushMaskKind, BrushKind, DeferredResolve, EdgeAaSegmentMask};
 use profiler::FrameProfileCounters;
@@ -856,45 +856,45 @@ impl CompositeOps {
         self.filters.len() + if self.mix_blend_mode.is_some() { 1 } else { 0 }
     }
 }
 
 /// A rendering-oriented representation of frame::Frame built by the render backend
 /// and presented to the renderer.
 #[cfg_attr(feature = "capture", derive(Deserialize, Serialize))]
 pub struct Frame {
+    //TODO: share the fields with DocumentView struct
     pub window_size: DeviceUintSize,
     pub inner_rect: DeviceUintRect,
     pub background_color: Option<ColorF>,
     pub layer: DocumentLayer,
     pub device_pixel_ratio: f32,
     pub passes: Vec<RenderPass>,
     #[cfg_attr(feature = "capture", serde(default = "FrameProfileCounters::new", skip))]
     pub profile_counters: FrameProfileCounters,
 
     pub node_data: Vec<ClipScrollNodeData>,
     pub clip_chain_local_clip_rects: Vec<LayerRect>,
     pub render_tasks: RenderTaskTree,
 
-    // List of updates that need to be pushed to the
-    // gpu resource cache.
-    pub gpu_cache_updates: Option<GpuCacheUpdateList>,
+    /// The GPU cache frame that the contents of Self depend on
+    pub gpu_cache_frame_id: FrameId,
 
-    // List of textures that we don't know about yet
-    // from the backend thread. The render thread
-    // will use a callback to resolve these and
-    // patch the data structures.
+    /// List of textures that we don't know about yet
+    /// from the backend thread. The render thread
+    /// will use a callback to resolve these and
+    /// patch the data structures.
     pub deferred_resolves: Vec<DeferredResolve>,
 
-    // True if this frame contains any render tasks
-    // that write to the texture cache.
+    /// True if this frame contains any render tasks
+    /// that write to the texture cache.
     pub has_texture_cache_tasks: bool,
 
-    // True if this frame has been drawn by the
-    // renderer.
+    /// True if this frame has been drawn by the
+    /// renderer.
     pub has_been_rendered: bool,
 }
 
 impl Frame {
     // This frame must be flushed if it writes to the
     // texture cache, and hasn't been drawn yet.
     pub fn must_be_drawn(&self) -> bool {
         self.has_texture_cache_tasks && !self.has_been_rendered
new file mode 100644
--- /dev/null
+++ b/gfx/wrench/.gitignore
@@ -0,0 +1,7 @@
+Cargo.lock
+target/
+*#
+*~
+yaml_frames/
+json_frames/
+bin_frames/
new file mode 100644
--- /dev/null
+++ b/gfx/wrench/Cargo.toml
@@ -0,0 +1,44 @@
+[package]
+name = "wrench"
+version = "0.2.6"
+authors = ["Vladimir Vukicevic <vladimir@pobox.com>"]
+build = "build.rs"
+license = "MPL-2.0"
+
+[dependencies]
+base64 = "0.3"
+bincode = "0.9"
+byteorder = "1.0"
+env_logger = { version = "0.4", optional = true }
+euclid = "0.16"
+gleam = "0.4"
+servo-glutin = "0.13"
+app_units = "0.6"
+image = "0.17"
+clap = { version = "2", features = ["yaml"] }
+lazy_static = "1"
+log = "0.3"
+yaml-rust = { git = "https://github.com/vvuk/yaml-rust", features = ["preserve_order"] }
+serde_json = "1.0"
+ron = "0.1.5"
+time = "0.1"
+crossbeam = "0.2"
+osmesa-sys = { version = "0.1.2", optional = true }
+osmesa-src = { git = "https://github.com/servo/osmesa-src", optional = true }
+webrender = {path = "../webrender", features=["capture","debugger","png","profiler"]}
+webrender_api = {path = "../webrender_api", features=["debug-serialization"]}
+serde = {version = "1.0", features = ["derive"] }
+
+[target.'cfg(target_os = "macos")'.dependencies]
+core-graphics = "0.12.4"
+core-foundation = "0.4"
+
+[features]
+headless = [ "osmesa-sys", "osmesa-src" ]
+logging = [ "env_logger" ]
+
+[target.'cfg(target_os = "windows")'.dependencies]
+dwrote = "0.4.1"
+
+[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies]
+font-loader = "0.5"
new file mode 100644
--- /dev/null
+++ b/gfx/wrench/README.md
@@ -0,0 +1,35 @@
+# wrench
+
+`wrench` is a tool for debugging webrender outside of a browser engine.
+
+## headless
+
+`wrench` has an optional headless mode for use in continuous integration. To run in headless mode, instead of using `cargo run -- args`, use `./headless.py args`.
+
+## `replay` and `show`
+
+Binary recordings can be generated by webrender and replayed with `wrench replay`. Enable binary recording in `RendererOptions`.
+
+```rust
+RendererOptions {
+    ...
+    recorder: Some(Box::new(BinaryRecorder::new("wr-frame.bin"))),
+    ...
+}
+```
+
+If you are working on gecko integration you can enable recording in `webrender_bindings/src/bindings.rs` by setting
+
+```rust
+static ENABLE_RECORDING: bool = true;
+```
+
+`wrench replay --save yaml` will convert the recording into frames described in yaml. Frames can then be replayed with `wrench show`.
+
+## `reftest`
+
+Wrench also has a reftest system for catching regressions.
+* To run all reftests, run `./headless.py reftest`
+* To run specific reftests, run `./headless.py reftest path/to/test/or/dir`
+* To examine test failures, use the [reftest analyzer](https://hg.mozilla.org/mozilla-central/raw-file/tip/layout/tools/reftest/reftest-analyzer.xhtml)
+* To add a new reftest, create an example frame and a reference frame in `reftests/` and then add an entry to `reftests/reftest.list`
new file mode 100644
--- /dev/null
+++ b/gfx/wrench/build.rs
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use std::env;
+use std::fs;
+use std::path::PathBuf;
+
+fn main() {
+    let target = env::var("TARGET").unwrap();
+    let out_dir = env::var_os("OUT_DIR").unwrap();
+    let out_dir = PathBuf::from(out_dir);
+
+    println!("cargo:rerun-if-changed=res/wrench.exe.manifest");
+    if target.contains("windows") {
+        let src = PathBuf::from("res/wrench.exe.manifest");
+        let mut dst = out_dir
+            .parent()
+            .unwrap()
+            .parent()
+            .unwrap()
+            .parent()
+            .unwrap()
+            .to_owned();
+        dst.push("wrench.exe.manifest");
+        fs::copy(&src, &dst).unwrap();
+    }
+}
new file mode 100644
--- /dev/null
+++ b/gfx/wrench/res/wrench.exe.manifest
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1"
+          manifestVersion="1.0"
+          xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
+  <assemblyIdentity type="win32"
+                    name="webrender.Wrench"
+                    version="0.1.0.0"/>
+
+  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+    <application>
+      <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> <!-- Windows 7 -->
+      <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> <!-- Windows 8 -->
+      <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> <!-- Windows 8.1 -->
+      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> <!-- Windows 10 -->
+    </application>
+  </compatibility>
+
+  <asmv3:application>
+    <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
+      <dpiAware>true</dpiAware> 
+    </asmv3:windowsSettings>
+  </asmv3:application>
+</assembly>
+
new file mode 100644
--- /dev/null
+++ b/gfx/wrench/src/args.yaml
@@ -0,0 +1,164 @@
+name: wrench
+version: "0.1"
+author: Vladimir Vukicevic <vladimir@pobox.com>
+about: WebRender testing and debugging utility
+
+args:
+  - precache:
+      short: c
+      long: precache
+      help: Precache shaders
+  - verbose:
+      short: v
+      long: verbose
+      help: Enable verbose display
+  - zoom:
+      short: z
+      long: zoom
+      help: Set zoom factor
+      takes_value: true
+  - debug:
+      short: d
+      long: debug
+      help: Enable debug renderer
+  - shaders:
+      long: shaders
+      help: Override path for shaders
+      takes_value: true
+  - rebuild:
+      short: r
+      long: rebuild
+      help: Rebuild display list from scratch every frame
+  - save:
+      long: save
+      help: 'Save frames, one of: yaml, json, ron, or binary'
+      takes_value: true
+  - no_subpixel_aa:
+      short: a
+      long: no-subpixel-aa
+      help: Disable subpixel aa
+  - slow_subpixel:
+      long: slow-subpixel
+      help: Disable dual source blending
+  - headless:
+      short: h
+      long: headless
+      help: Enable headless rendering
+  - dp_ratio:
+      short: p
+      long: device-pixel-ratio
+      help: Device pixel ratio
+      takes_value: true
+  - size:
+      short: s
+      long: size
+      help: Window size, specified as widthxheight (e.g. 1024x768), in pixels
+      takes_value: true
+  - time:
+      short: t
+      long: time
+      help: Time limit (in seconds)
+      takes_value: true
+  - vsync:
+      long: vsync
+      help: Enable vsync for OpenGL window
+  - no_scissor:
+      long: no-scissor
+      help: Disable scissors when clearing render targets
+  - no_batch:
+      long: no-batch
+      help: Disable batching of instanced draw calls
+
+subcommands:
+    - png:
+        about: render frame described by YAML and save it to a png file
+        args:
+          - surface:
+              short: s
+              long: surface
+              help: 'What rendered surface to save as PNG, one of: screen, gpu-cache'
+              takes_value: true
+          - INPUT:
+              help: The input YAML file
+              required: true
+              index: 1
+    - show:
+        about: show frame(s) described by YAML
+        args:
+          - queue:
+              short: q
+              long: queue
+              help: How many frames to submit to WR ahead of time (default 1)
+              takes_value: true
+          - include:
+              long: include
+              help: Include the given element type. Can be specified multiple times. (rect/image/text/glyphs/border)
+              multiple: true
+              takes_value: true
+          - list-resources:
+              long: list-resources
+              help: List the resources used by this YAML file
+          - watch:
+              short: w
+              long: watch
+              help: Watch the given YAML file, reloading whenever it changes
+          - INPUT:
+              help: The input YAML file
+              required: true
+              index: 1
+    - replay:
+        about: replay binary recording
+        args:
+          - api:
+              long: api
+              help: Reissue Api messsages for each frame
+          - skip-uploads:
+              long: skip-uploads
+              help: Skip re-uploads while reissuing Api messages (BROKEN)
+          - play:
+              long: play
+              help: Play entire recording through, then quit (useful with --save)
+          - INPUT:
+              help: The input binary file or directory
+              required: true
+              index: 1
+    - reftest:
+        about: run reftests
+        args:
+          - fuzz_tolerance:
+              long: fuzzy
+              takes_value: true
+              help: Add a minimum fuzziness tolerance to all tests.
+              required: false
+          - REFTEST:
+              help: a specific reftest or directory to run
+              required: false
+              index: 1
+    - rawtest:
+        about: run rawtests
+    - perf:
+        about: run benchmarks
+        args:
+          - filename:
+              help: name of the file to save benchmarks to
+              required: true
+              index: 1
+    - compare_perf:
+        about: compare two benchmark files
+        args:
+          - first_filename:
+              help: first benchmark file to compare
+              required: true
+              index: 1
+          - second_filename:
+              help: second benchmark file to compare
+              required: true
+              index: 2
+    - load:
+        about: load a capture
+        args:
+          - path:
+              help: directory containing the capture
+              takes_value: true
+              required: true
+              index: 1
new file mode 100644
--- /dev/null
+++ b/gfx/wrench/src/binary_frame_reader.rs
@@ -0,0 +1,237 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use bincode::deserialize;
+use byteorder::{LittleEndian, ReadBytesExt};
+use clap;
+use std::any::TypeId;
+use std::fs::File;
+use std::io::{Read, Seek, SeekFrom};
+use std::path::{Path, PathBuf};
+use std::{mem, process};
+use webrender::WEBRENDER_RECORDING_HEADER;
+use webrender::api::{ApiMsg, DocumentMsg};
+use wrench::{Wrench, WrenchThing};
+
+#[derive(Clone)]
+enum Item {
+    Message(ApiMsg),
+    Data(Vec<u8>),
+}
+
+pub struct BinaryFrameReader {
+    file: File,
+    eof: bool,
+    frame_offsets: Vec<u64>,
+
+    skip_uploads: bool,
+    replay_api: bool,
+    play_through: bool,
+
+    frame_data: Vec<Item>,
+    frame_num: u32,
+    frame_read: bool,
+}
+
+impl BinaryFrameReader {
+    pub fn new(file_path: &Path) -> BinaryFrameReader {
+        let mut file = File::open(&file_path).expect("Can't open recording file");
+        let header = file.read_u64::<LittleEndian>().unwrap();
+        assert_eq!(
+            header,
+            WEBRENDER_RECORDING_HEADER,
+            "Binary recording is missing recording header!"
+        );
+
+        let apimsg_type_id = unsafe {
+            assert_eq!(mem::size_of::<TypeId>(), mem::size_of::<u64>());
+            mem::transmute::<TypeId, u64>(TypeId::of::<ApiMsg>())
+        };
+
+        let written_apimsg_type_id = file.read_u64::<LittleEndian>().unwrap();
+        if written_apimsg_type_id != apimsg_type_id {
+            println!(
+                "Warning: binary file ApiMsg type mismatch: expected 0x{:x}, found 0x{:x}",
+                apimsg_type_id,
+                written_apimsg_type_id
+            );
+        }
+
+        BinaryFrameReader {
+            file,
+            eof: false,
+            frame_offsets: vec![],
+
+            skip_uploads: false,
+            replay_api: false,
+            play_through: false,
+
+            frame_data: vec![],
+            frame_num: 0,
+            frame_read: false,
+        }
+    }
+
+    pub fn new_from_args(args: &clap::ArgMatches) -> BinaryFrameReader {
+        let bin_file = args.value_of("INPUT").map(|s| PathBuf::from(s)).unwrap();
+        let mut r = BinaryFrameReader::new(&bin_file);
+        r.skip_uploads = args.is_present("skip-uploads");
+        r.replay_api = args.is_present("api");
+        r.play_through = args.is_present("play");
+        r
+    }
+
+    // FIXME I don't think we can skip uploads without also skipping
+    // payload (I think? Unused payload ranges may also be ignored.). But
+    // either way we'd need to track image updates and deletions -- if we
+    // delete an image, we can't go back to a previous frame.
+    //
+    // We could probably introduce a mode where going backwards replays all
+    // frames up until that frame, so that we know we can be accurate.
+    fn should_skip_upload_msg(&self, msg: &ApiMsg) -> bool {
+        if !self.skip_uploads {
+            return false;
+        }
+
+        match *msg {
+            ApiMsg::UpdateResources(..) => true,
+            _ => false,
+        }
+    }
+
+    // a frame exists if we either haven't hit eof yet, or if
+    // we have, then if we've seen its offset.
+    fn frame_exists(&self, frame: u32) -> bool {
+        !self.eof || (frame as usize) < self.frame_offsets.len()
+    }
+}
+
+impl WrenchThing for BinaryFrameReader {
+    fn do_frame(&mut self, wrench: &mut Wrench) -> u32 {
+        // save where the frame begins as we read through the file
+        if self.frame_num as usize >= self.frame_offsets.len() {
+            self.frame_num = self.frame_offsets.len() as u32;
+            let pos = self.file.seek(SeekFrom::Current(0)).unwrap();
+            println!("Frame {} offset: {}", self.frame_offsets.len(), pos);
+            self.frame_offsets.push(pos);
+        }
+
+        let first_time = !self.frame_read;
+        if first_time {
+            let offset = self.frame_offsets[self.frame_num as usize];
+            self.file.seek(SeekFrom::Start(offset)).unwrap();
+
+            wrench.set_title(&format!("frame {}", self.frame_num));
+
+            self.frame_data.clear();
+            let mut found_frame_marker = false;
+            let mut found_display_list = false;
+            let mut found_pipeline = false;
+            while let Ok(mut len) = self.file.read_u32::<LittleEndian>() {
+                if len > 0 {
+                    let mut buffer = vec![0; len as usize];
+                    self.file.read_exact(&mut buffer).unwrap();
+                    let msg = deserialize(&buffer).unwrap();
+                    let mut store_message = true;
+                    // In order to detect the first valid frame, we
+                    // need to find:
+                    // (a) SetRootPipeline
+                    // (b) SetDisplayList
+                    // (c) GenerateFrame that occurs *after* (a) and (b)
+                    match msg {
+                        ApiMsg::UpdateDocument(_, ref doc_msgs) => {
+                            for doc_msg in doc_msgs {
+                                match *doc_msg {
+                                    DocumentMsg::GenerateFrame => {
+                                        found_frame_marker = true;
+                                    }
+                                    DocumentMsg::SetDisplayList { .. } => {
+                                        found_frame_marker = false;
+                                        found_display_list = true;
+                                    }
+                                    DocumentMsg::SetRootPipeline(..) => {
+                                        found_frame_marker = false;
+                                        found_pipeline = true;
+                                    }
+                                    _ => {}
+                                }
+                            }
+                        }
+                        // Wrench depends on the document always existing
+                        ApiMsg::DeleteDocument(_) => {
+                            store_message = false;
+                        }
+                        _ => {}
+                    }
+                    if store_message {
+                        self.frame_data.push(Item::Message(msg));
+                    }
+                    // Frames are marked by the GenerateFrame message.
+                    // On the first frame, we additionally need to find at least
+                    // a SetDisplayList and a SetRootPipeline.
+                    // After the first frame, any GenerateFrame message marks a new
+                    // frame being rendered.
+                    if found_frame_marker && (self.frame_num > 0 || (found_display_list && found_pipeline)) {
+                        break;
+                    }
+                } else {
+                    len = self.file.read_u32::<LittleEndian>().unwrap();
+                    let mut buffer = vec![0; len as usize];
+                    self.file.read_exact(&mut buffer).unwrap();
+                    self.frame_data.push(Item::Data(buffer));
+                }
+            }
+
+            if self.eof == false &&
+                self.file.seek(SeekFrom::Current(0)).unwrap() == self.file.metadata().unwrap().len()
+            {
+                self.eof = true;
+            }
+
+            self.frame_read = true;
+        }
+
+        if first_time || self.replay_api {
+            wrench.begin_frame();
+            let frame_items = self.frame_data.clone();
+            for item in frame_items {
+                match item {
+                    Item::Message(msg) => if !self.should_skip_upload_msg(&msg) {
+                        wrench.api.send_message(msg);
+                    },
+                    Item::Data(buf) => {
+                        wrench.api.send_payload(&buf);
+                    }
+                }
+            }
+        } else if self.play_through {
+            if !self.frame_exists(self.frame_num + 1) {
+                process::exit(0);
+            }
+            self.next_frame();
+            self.do_frame(wrench);
+        } else {
+            wrench.refresh();
+        }
+
+        self.frame_num
+    }
+
+    // note that we don't loop here; we could, but that'll require
+    // some tracking to avoid reuploading resources every run.  We
+    // sort of try in should_skip_upload_msg, but this needs work.
+    fn next_frame(&mut self) {
+        if self.frame_exists(self.frame_num + 1) {
+            self.frame_num += 1;
+            self.frame_read = false;
+        }
+    }
+
+    fn prev_frame(&mut self) {
+        if self.frame_num > 0 {
+            self.frame_num -= 1;
+            self.frame_read = false;
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/gfx/wrench/src/blob.rs
@@ -0,0 +1,162 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// A very basic BlobImageRenderer that can only render a checkerboard pattern.
+
+use std::collections::HashMap;
+use std::sync::Arc;
+use std::sync::Mutex;
+use webrender::api::*;
+
+// Serialize/deserialze the blob.
+
+pub fn serialize_blob(color: ColorU) -> Vec<u8> {
+    vec![color.r, color.g, color.b, color.a]
+}
+
+fn deserialize_blob(blob: &[u8]) -> Result<ColorU, ()> {
+    let mut iter = blob.iter();
+    return match (iter.next(), iter.next(), iter.next(), iter.next()) {
+        (Some(&r), Some(&g), Some(&b), Some(&a)) => Ok(ColorU::new(r, g, b, a)),
+        (Some(&a), None, None, None) => Ok(ColorU::new(a, a, a, a)),
+        _ => Err(()),
+    };
+}
+
+// This is the function that applies the deserialized drawing commands and generates
+// actual image data.
+fn render_blob(
+    color: ColorU,
+    descriptor: &BlobImageDescriptor,
+    tile: Option<TileOffset>,
+) -> BlobImageResult {
+    // Allocate storage for the result. Right now the resource cache expects the
+    // tiles to have have no stride or offset.
+    let mut texels = Vec::with_capacity((descriptor.width * descriptor.height * 4) as usize);
+
+    // Generate a per-tile pattern to see it in the demo. For a real use case it would not
+    // make sense for the rendered content to depend on its tile.
+    let tile_checker = match tile {
+        Some(tile) => (tile.x % 2 == 0) != (tile.y % 2 == 0),
+        None => true,
+    };
+
+    for y in 0 .. descriptor.height {
+        for x in 0 .. descriptor.width {
+            // Apply the tile's offset. This is important: all drawing commands should be
+            // translated by this offset to give correct results with tiled blob images.
+            let x2 = x + descriptor.offset.x as u32;
+            let y2 = y + descriptor.offset.y as u32;
+
+            // Render a simple checkerboard pattern
+            let checker = if (x2 % 20 >= 10) != (y2 % 20 >= 10) {
+                1
+            } else {
+                0
+            };
+            // ..nested in the per-tile cherkerboard pattern
+            let tc = if tile_checker { 0 } else { (1 - checker) * 40 };
+
+            match descriptor.format {
+                ImageFormat::BGRA8 => {
+                    texels.push(color.b * checker + tc);
+                    texels.push(color.g * checker + tc);
+                    texels.push(color.r * checker + tc);
+                    texels.push(color.a * checker + tc);
+                }
+                ImageFormat::R8 => {
+                    texels.push(color.a * checker + tc);
+                }
+                _ => {
+                    return Err(BlobImageError::Other(
+                        format!("Usupported image format {:?}", descriptor.format),
+                    ));
+                }
+            }
+        }
+    }
+
+    Ok(RasterizedBlobImage {
+        data: texels,
+        width: descriptor.width,
+        height: descriptor.height,
+    })
+}
+
+pub struct BlobCallbacks {
+    pub request: Box<Fn(&BlobImageRequest) + Send + 'static>,
+    pub resolve: Box<Fn() + Send + 'static>,
+}
+
+impl BlobCallbacks {
+    pub fn new() -> Self {
+        BlobCallbacks { request: Box::new(|_|()), resolve: Box::new(|| (())) }
+    }
+}
+
+pub struct CheckerboardRenderer {
+    image_cmds: HashMap<ImageKey, ColorU>,
+    callbacks: Arc<Mutex<BlobCallbacks>>,
+
+    // The images rendered in the current frame (not kept here between frames).
+    rendered_images: HashMap<BlobImageRequest, BlobImageResult>,
+}
+
+impl CheckerboardRenderer {
+    pub fn new(callbacks: Arc<Mutex<BlobCallbacks>>) -> Self {
+        CheckerboardRenderer {
+            callbacks,
+            image_cmds: HashMap::new(),
+            rendered_images: HashMap::new(),
+        }
+    }
+}
+
+impl BlobImageRenderer for CheckerboardRenderer {
+    fn add(&mut self, key: ImageKey, cmds: BlobImageData, _: Option<TileSize>) {
+        self.image_cmds
+            .insert(key, deserialize_blob(&cmds[..]).unwrap());
+    }
+
+    fn update(&mut self, key: ImageKey, cmds: BlobImageData, _dirty_rect: Option<DeviceUintRect>) {
+        // Here, updating is just replacing the current version of the commands with
+        // the new one (no incremental updates).
+        self.image_cmds
+            .insert(key, deserialize_blob(&cmds[..]).unwrap());
+    }
+
+    fn delete(&mut self, key: ImageKey) {
+        self.image_cmds.remove(&key);
+    }
+
+    fn request(
+        &mut self,
+        _resources: &BlobImageResources,
+        request: BlobImageRequest,
+        descriptor: &BlobImageDescriptor,
+        _dirty_rect: Option<DeviceUintRect>,
+    ) {
+        (self.callbacks.lock().unwrap().request)(&request);
+        assert!(!self.rendered_images.contains_key(&request));
+        // This method is where we kick off our rendering jobs.
+        // It should avoid doing work on the calling thread as much as possible.
+        // In this example we will use the thread pool to render individual tiles.
+
+        // Gather the input data to send to a worker thread.
+        let cmds = self.image_cmds.get(&request.key).unwrap();
+
+        let result = render_blob(*cmds, descriptor, request.tile);
+
+        self.rendered_images.insert(request, result);
+    }
+
+    fn resolve(&mut self, request: BlobImageRequest) -> BlobImageResult {
+        (self.callbacks.lock().unwrap().resolve)();
+        self.rendered_images.remove(&request).unwrap()
+    }
+
+    fn delete_font(&mut self, _key: FontKey) {}
+
+    fn delete_font_instance(&mut self, _key: FontInstanceKey) {}
+}
new file mode 100644
--- /dev/null
+++ b/gfx/wrench/src/cgfont_to_data.rs
@@ -0,0 +1,124 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use byteorder::{BigEndian, ByteOrder, ReadBytesExt, WriteBytesExt};
+use core_foundation::data::CFData;
+use core_graphics::font::CGFont;
+use std;
+use std::io::Cursor;
+use std::io::Read;
+use std::io::Write;
+
+
+fn calc_table_checksum<D: Read>(mut data: D) -> u32 {
+    let mut sum: u32 = 0;
+
+    while let Ok(x) = data.read_u32::<BigEndian>() {
+        sum = sum.wrapping_add(x);
+    }
+    // read the remaining bytes
+    let mut buf: [u8; 4] = [0; 4];
+    data.read(&mut buf).unwrap();
+    // if there was nothing left in buf we'll just read a 0
+    // which won't affect the checksum
+    sum = sum.wrapping_add(BigEndian::read_u32(&buf));
+    sum
+}
+
+fn max_pow_2_less_than(a: u16) -> u16 {
+    let x = 1;
+    let mut shift = 0;
+    while (x << (shift + 1)) < a {
+        shift += 1;
+    }
+    shift
+}
+
+struct TableRecord {
+    tag: u32,
+    checksum: u32,
+    offset: u32,
+    length: u32,
+    data: CFData
+}
+
+const CFF_TAG: u32 = 0x43464620; // 'CFF '
+const HEAD_TAG: u32 = 0x68656164; // 'head'
+const OTTO_TAG: u32 = 0x4f54544f; // 'OTTO'
+const TRUE_TAG: u32 = 0x00010000;
+
+pub fn font_to_data(font: CGFont) -> Result<Vec<u8>, std::io::Error> {
+    // We'll reconstruct a TTF font from the tables we can get from the CGFont
+    let mut cff = false;
+    let tags = font.copy_table_tags();
+    let count = tags.len() as u16;
+
+    let mut records = Vec::new();
+    let mut offset: u32 = 0;
+    offset += 4 * 3;
+    offset += 4 * 4 * (count as u32);
+    for tag in tags.iter() {
+        if tag == CFF_TAG {
+            cff = true;
+        }
+        let data = font.copy_table_for_tag(tag).unwrap();
+        let length = data.len() as u32;
+        let checksum;
+        if tag == HEAD_TAG {
+            // we need to skip the checksum field
+            checksum = calc_table_checksum(&data.bytes()[0..2])
+                .wrapping_add(calc_table_checksum(&data.bytes()[3..]))
+        } else {
+            checksum = calc_table_checksum(data.bytes());
+        }
+        records.push(TableRecord { tag, offset, data, length, checksum } );
+        offset += length;
+        // 32 bit align the tables
+        offset = (offset + 3) & !3;
+    }
+
+    let mut buf = Vec::new();
+    if cff {
+        buf.write_u32::<BigEndian>(OTTO_TAG)?;
+    } else {
+        buf.write_u32::<BigEndian>(TRUE_TAG)?;
+    }
+
+    buf.write_u16::<BigEndian>(count)?;
+    buf.write_u16::<BigEndian>((1 << max_pow_2_less_than(count)) * 16)?;
+    buf.write_u16::<BigEndian>(max_pow_2_less_than(count))?;
+    buf.write_u16::<BigEndian>(count * 16 - ((1 << max_pow_2_less_than(count)) * 16))?;
+
+    // write table record entries
+    for r in &records {
+        buf.write_u32::<BigEndian>(r.tag)?;
+        buf.write_u32::<BigEndian>(r.checksum)?;
+        buf.write_u32::<BigEndian>(r.offset)?;
+        buf.write_u32::<BigEndian>(r.length)?;
+    }
+
+    // write tables
+    let mut check_sum_adjustment_offset = 0;
+    for r in &records {
+        if r.tag == 0x68656164 {
+            check_sum_adjustment_offset = buf.len() + 2 * 4;
+        }
+        buf.write(r.data.bytes())?;
+        // 32 bit align the tables
+        while buf.len() & 3 != 0 {
+            buf.push(0);
+        }
+    }
+
+    let mut c = Cursor::new(buf);
+    c.set_position(check_sum_adjustment_offset as u64);
+    // clear the checksumAdjust field before checksumming the whole font
+    c.write_u32::<BigEndian>(0)?;
+    let sum = 0xb1b0afba_u32.wrapping_sub(calc_table_checksum(&c.get_mut()[..]));
+    // set checkSumAdjust to the computed checksum
+    c.set_position(check_sum_adjustment_offset as u64);
+    c.write_u32::<BigEndian>(sum)?;
+
+    Ok(c.into_inner())
+}
new file mode 100644
--- /dev/null
+++ b/gfx/wrench/src/json_frame_writer.rs
@@ -0,0 +1,317 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// the json code is largely unfinished; allow these to silence a bunch of warnings
+#![allow(unused_variables)]
+#![allow(dead_code)]
+
+use app_units::Au;
+use image::{save_buffer, ColorType};
+use premultiply::unpremultiply;
+use serde_json;
+use std::collections::HashMap;
+use std::io::Write;
+use std::path::{Path, PathBuf};
+use std::{fmt, fs};
+use super::CURRENT_FRAME_NUMBER;
+use time;
+use webrender;
+use webrender::api::*;
+use webrender::api::channel::Payload;
+
+enum CachedFont {
+    Native(NativeFontHandle),
+    Raw(Option<Vec<u8>>, u32, Option<PathBuf>),
+}
+
+struct CachedFontInstance {
+    font_key: FontKey,
+    glyph_size: Au,
+}
+
+struct CachedImage {
+    width: u32,
+    height: u32,
+    stride: u32,
+    format: ImageFormat,
+    bytes: Option<Vec<u8>>,
+    path: Option<PathBuf>,
+}
+
+pub struct JsonFrameWriter {
+    frame_base: PathBuf,
+    rsrc_base: PathBuf,
+    rsrc_prefix: String,
+    next_rsrc_num: u32,
+    images: HashMap<ImageKey, CachedImage>,
+    fonts: HashMap<FontKey, CachedFont>,
+    font_instances: HashMap<FontInstanceKey, CachedFontInstance>,
+
+    last_frame_written: u32,
+
+    dl_descriptor: Option<BuiltDisplayListDescriptor>,
+}
+
+impl JsonFrameWriter {
+    pub fn new(path: &Path) -> Self {
+        let mut rsrc_base = path.to_owned();
+        rsrc_base.push("res");
+        fs::create_dir_all(&rsrc_base).ok();
+
+        let rsrc_prefix = format!("{}", time::get_time().sec);
+
+        JsonFrameWriter {
+            frame_base: path.to_owned(),
+            rsrc_base,
+            rsrc_prefix,
+            next_rsrc_num: 1,
+            images: HashMap::new(),
+            fonts: HashMap::new(),
+            font_instances: HashMap::new(),
+
+            dl_descriptor: None,
+
+            last_frame_written: u32::max_value(),
+        }
+    }
+
+    pub fn begin_write_display_list(
+        &mut self,
+        _: &Epoch,
+        _: &PipelineId,
+        _: &Option<ColorF>,
+        _: &LayoutSize,
+        display_list: &BuiltDisplayListDescriptor,
+    ) {
+        unsafe {
+            if CURRENT_FRAME_NUMBER == self.last_frame_written {
+                return;
+            }
+            self.last_frame_written = CURRENT_FRAME_NUMBER;
+        }
+
+        self.dl_descriptor = Some(display_list.clone());
+    }
+
+    pub fn finish_write_display_list(&mut self, frame: u32, data: &[u8]) {
+        let payload = Payload::from_data(data);
+        let dl_desc = self.dl_descriptor.take().unwrap();
+
+        let dl = BuiltDisplayList::from_data(payload.display_list_data, dl_desc);
+
+        let mut frame_file_name = self.frame_base.clone();
+        let current_shown_frame = unsafe { CURRENT_FRAME_NUMBER };
+        frame_file_name.push(format!("frame-{}.json", current_shown_frame));
+
+        let mut file = fs::File::create(&frame_file_name).unwrap();
+
+        let s = serde_json::to_string_pretty(&dl).unwrap();
+        file.write_all(&s.into_bytes()).unwrap();
+        file.write_all(b"\n").unwrap();
+    }
+
+    fn update_resources(&mut self, updates: &ResourceUpdates) {
+        for update in &updates.updates {
+            match *update {
+                ResourceUpdate::AddImage(ref img) => {
+                    let stride = img.descriptor.stride.unwrap_or(
+                        img.descriptor.width * img.descriptor.format.bytes_per_pixel(),
+                    );
+                    let bytes = match img.data {
+                        ImageData::Raw(ref v) => (**v).clone(),
+                        ImageData::External(_) | ImageData::Blob(_) => {
+                            return;
+                        }
+                    };
+                    self.images.insert(
+                        img.key,
+                        CachedImage {
+                            width: img.descriptor.width,
+                            height: img.descriptor.height,
+                            stride,
+                            format: img.descriptor.format,
+                            bytes: Some(bytes),
+                            path: None,
+                        },
+                    );
+                }
+                ResourceUpdate::UpdateImage(ref img) => {
+                    if let Some(ref mut data) = self.images.get_mut(&img.key) {
+                        assert_eq!(data.width, img.descriptor.width);
+                        assert_eq!(data.height, img.descriptor.height);
+                        assert_eq!(data.format, img.descriptor.format);
+
+                        if let ImageData::Raw(ref bytes) = img.data {
+                            data.path = None;
+                            data.bytes = Some((**bytes).clone());
+                        } else {
+                            // Other existing image types only make sense within the gecko integration.
+                            println!(
+                                "Wrench only supports updating buffer images ({}).",
+                                "ignoring update commands"
+                            );
+                        }
+                    }
+                }
+                ResourceUpdate::DeleteImage(img) => {
+                    self.images.remove(&img);
+                }
+                ResourceUpdate::AddFont(ref font) => match font {
+                    &AddFont::Raw(key, ref bytes, index) => {
+                        self.fonts
+                            .insert(key, CachedFont::Raw(Some(bytes.clone()), index, None));
+                    }
+                    &AddFont::Native(key, ref handle) => {
+                        self.fonts.insert(key, CachedFont::Native(handle.clone()));
+                    }
+                },
+                ResourceUpdate::DeleteFont(_) => {}
+                ResourceUpdate::AddFontInstance(ref instance) => {
+                    self.font_instances.insert(
+                        instance.key,
+                        CachedFontInstance {
+                            font_key: instance.font_key,
+                            glyph_size: instance.glyph_size,
+                        },
+                    );
+                }
+                ResourceUpdate::DeleteFontInstance(_) => {}
+            }
+        }
+    }
+
+    fn next_rsrc_paths(
+        prefix: &str,
+        counter: &mut u32,
+        base_path: &Path,
+        base: &str,
+        ext: &str,
+    ) -> (PathBuf, PathBuf) {
+        let mut path_file = base_path.to_owned();
+        let mut path = PathBuf::from("res");
+
+        let fstr = format!("{}-{}-{}.{}", prefix, base, counter, ext);
+        path_file.push(&fstr);
+        path.push(&fstr);
+
+        *counter += 1;
+
+        (path_file, path)
+    }
+
+    fn path_for_image(&mut self, key: ImageKey) -> Option<PathBuf> {
+        if let Some(ref mut data) = self.images.get_mut(&key) {
+            if data.path.is_some() {
+                return data.path.clone();
+            }
+        } else {
+            return None;
+        };
+
+        // Remove the data to munge it
+        let mut data = self.images.remove(&key).unwrap();
+        let mut bytes = data.bytes.take().unwrap();
+        let (path_file, path) = Self::next_rsrc_paths(
+            &self.rsrc_prefix,
+            &mut self.next_rsrc_num,
+            &self.rsrc_base,
+            "img",
+            "png",
+        );
+
+        let ok = match data.format {
+            ImageFormat::BGRA8 => if data.stride == data.width * 4 {
+                unpremultiply(bytes.as_mut_slice());
+                save_buffer(
+                    &path_file,
+                    &bytes,
+                    data.width,
+                    data.height,
+                    ColorType::RGBA(8),
+                ).unwrap();
+                true
+            } else {
+                false
+            },
+            ImageFormat::R8 => if data.stride == data.width {
+                save_buffer(
+                    &path_file,
+                    &bytes,
+                    data.width,
+                    data.height,
+                    ColorType::Gray(8),
+                ).unwrap();
+                true
+            } else {
+                false
+            },
+            _ => false,
+        };
+
+        if !ok {
+            println!(
+                "Failed to write image with format {:?}, dimensions {}x{}, stride {}",
+                data.format,
+                data.width,
+                data.height,
+                data.stride
+            );
+            return None;
+        }
+
+        data.path = Some(path.clone());
+        // put it back
+        self.images.insert(key, data);
+        Some(path)
+    }
+}
+
+impl fmt::Debug for JsonFrameWriter {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "JsonFrameWriter")
+    }
+}
+
+impl webrender::ApiRecordingReceiver for JsonFrameWriter {
+    fn write_msg(&mut self, _: u32, msg: &ApiMsg) {
+        match *msg {
+            ApiMsg::UpdateResources(ref updates) => self.update_resources(updates),
+
+            ApiMsg::UpdateDocument(_, ref doc_msgs) => {
+                for doc_msg in doc_msgs {
+                    match *doc_msg {
+                        DocumentMsg::UpdateResources(ref resources) => {
+                            self.update_resources(resources);
+                        }
+                        DocumentMsg::SetDisplayList {
+                            ref epoch,
+                            ref pipeline_id,
+                            ref background,
+                            ref viewport_size,
+                            ref list_descriptor,
+                            ..
+                        } => {
+                            self.begin_write_display_list(
+                                epoch,
+                                pipeline_id,
+                                background,
+                                viewport_size,
+                                list_descriptor,
+                            );
+                        }
+                        _ => {}
+                    }
+                }
+            }
+            ApiMsg::CloneApi(..) => {}
+            _ => {}
+        }
+    }
+
+    fn write_payload(&mut self, frame: u32, data: &[u8]) {
+        if self.dl_descriptor.is_some() {
+            self.finish_write_display_list(frame, data);
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/gfx/wrench/src/main.rs
@@ -0,0 +1,597 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate app_units;
+extern crate base64;
+extern crate bincode;
+extern crate byteorder;
+#[macro_use]
+extern crate clap;
+#[cfg(target_os = "macos")]
+extern crate core_foundation;
+#[cfg(target_os = "macos")]
+extern crate core_graphics;
+extern crate crossbeam;
+#[cfg(target_os = "windows")]
+extern crate dwrote;
+#[cfg(feature = "logging")]
+extern crate env_logger;
+extern crate euclid;
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+extern crate font_loader;
+extern crate gleam;
+extern crate glutin;
+extern crate image;
+#[macro_use]
+extern crate lazy_static;
+#[macro_use]
+extern crate log;
+#[cfg(feature = "headless")]
+extern crate osmesa_sys;
+extern crate ron;
+#[macro_use]
+extern crate serde;
+extern crate serde_json;
+extern crate time;
+extern crate webrender;
+extern crate yaml_rust;
+
+mod binary_frame_reader;
+mod blob;
+mod json_frame_writer;
+mod parse_function;
+mod perf;
+mod png;
+mod premultiply;
+mod rawtest;
+mod reftest;
+mod ron_frame_writer;
+mod scene;
+mod wrench;
+mod yaml_frame_reader;
+mod yaml_frame_writer;
+mod yaml_helper;
+#[cfg(target_os = "macos")]
+mod cgfont_to_data;
+
+use binary_frame_reader::BinaryFrameReader;
+use gleam::gl;
+use glutin::{ElementState, VirtualKeyCode, WindowProxy};
+use perf::PerfHarness;
+use png::save_flipped;
+use rawtest::RawtestHarness;
+use reftest::{ReftestHarness, ReftestOptions};
+#[cfg(feature = "headless")]
+use std::ffi::CString;
+#[cfg(feature = "headless")]
+use std::mem;
+use std::os::raw::c_void;
+use std::path::{Path, PathBuf};
+use std::ptr;
+use std::rc::Rc;
+use std::sync::mpsc::{channel, Sender, Receiver};
+use webrender::DebugFlags;
+use webrender::api::*;
+use wrench::{Wrench, WrenchThing};
+use yaml_frame_reader::YamlFrameReader;
+
+lazy_static! {
+    static ref PLATFORM_DEFAULT_FACE_NAME: String = String::from("Arial");
+    static ref WHITE_COLOR: ColorF = ColorF::new(1.0, 1.0, 1.0, 1.0);
+    static ref BLACK_COLOR: ColorF = ColorF::new(0.0, 0.0, 0.0, 1.0);
+}
+
+pub static mut CURRENT_FRAME_NUMBER: u32 = 0;
+
+#[cfg(feature = "headless")]
+pub struct HeadlessContext {
+    width: u32,
+    height: u32,
+    _context: osmesa_sys::OSMesaContext,
+    _buffer: Vec<u32>,
+}
+
+#[cfg(not(feature = "headless"))]
+pub struct HeadlessContext {
+    width: u32,
+    height: u32,
+}
+
+impl HeadlessContext {
+    #[cfg(feature = "headless")]
+    fn new(width: u32, height: u32) -> HeadlessContext {
+        let mut attribs = Vec::new();
+
+        attribs.push(osmesa_sys::OSMESA_PROFILE);
+        attribs.push(osmesa_sys::OSMESA_CORE_PROFILE);
+        attribs.push(osmesa_sys::OSMESA_CONTEXT_MAJOR_VERSION);
+        attribs.push(3);
+        attribs.push(osmesa_sys::OSMESA_CONTEXT_MINOR_VERSION);
+        attribs.push(3);
+        attribs.push(osmesa_sys::OSMESA_DEPTH_BITS);
+        attribs.push(24);
+        attribs.push(0);
+
+        let context =
+            unsafe { osmesa_sys::OSMesaCreateContextAttribs(attribs.as_ptr(), ptr::null_mut()) };
+
+        assert!(!context.is_null());
+
+        let mut buffer = vec![0; (width * height) as usize];
+
+        unsafe {
+            let ret = osmesa_sys::OSMesaMakeCurrent(
+                context,
+                buffer.as_mut_ptr() as *mut _,
+                gl::UNSIGNED_BYTE,
+                width as i32,
+                height as i32,
+            );
+            assert!(ret != 0);
+        };
+
+        HeadlessContext {
+            width,
+            height,
+            _context: context,
+            _buffer: buffer,
+        }
+    }
+
+    #[cfg(not(feature = "headless"))]
+    fn new(width: u32, height: u32) -> HeadlessContext {
+        HeadlessContext { width, height }
+    }
+
+    #[cfg(feature = "headless")]
+    fn get_proc_address(s: &str) -> *const c_void {
+        let c_str = CString::new(s).expect("Unable to create CString");
+        unsafe { mem::transmute(osmesa_sys::OSMesaGetProcAddress(c_str.as_ptr())) }
+    }
+
+    #[cfg(not(feature = "headless"))]
+    fn get_proc_address(_: &str) -> *const c_void {
+        ptr::null() as *const _
+    }
+}
+
+pub enum WindowWrapper {
+    Window(glutin::Window, Rc<gl::Gl>),
+    Headless(HeadlessContext, Rc<gl::Gl>),
+}
+
+pub struct HeadlessEventIterater;
+
+impl WindowWrapper {
+    fn swap_buffers(&self) {
+        match *self {
+            WindowWrapper::Window(ref window, _) => window.swap_buffers().unwrap(),
+            WindowWrapper::Headless(..) => {}
+        }
+    }
+
+    fn get_inner_size(&self) -> DeviceUintSize {
+        let (w, h) = match *self {
+            WindowWrapper::Window(ref window, _) => window.get_inner_size_pixels().unwrap(),
+            WindowWrapper::Headless(ref context, _) => (context.width, context.height),
+        };
+        DeviceUintSize::new(w, h)
+    }
+
+    fn hidpi_factor(&self) -> f32 {
+        match *self {
+            WindowWrapper::Window(ref window, _) => window.hidpi_factor(),
+            WindowWrapper::Headless(..) => 1.0,
+        }
+    }
+
+    fn resize(&mut self, size: DeviceUintSize) {
+        match *self {
+            WindowWrapper::Window(ref mut window, _) => window.set_inner_size(size.width, size.height),
+            WindowWrapper::Headless(_, _) => unimplemented!(), // requites Glutin update
+        }
+    }
+
+    fn create_window_proxy(&mut self) -> Option<WindowProxy> {
+        match *self {
+            WindowWrapper::Window(ref window, _) => Some(window.create_window_proxy()),
+            WindowWrapper::Headless(..) => None,
+        }
+    }
+
+    fn set_title(&mut self, title: &str) {
+        match *self {
+            WindowWrapper::Window(ref window, _) => window.set_title(title),
+            WindowWrapper::Headless(..) => (),
+        }
+    }
+
+    pub fn gl(&self) -> &gl::Gl {
+        match *self {
+            WindowWrapper::Window(_, ref gl) | WindowWrapper::Headless(_, ref gl) => &**gl,
+        }
+    }
+
+    pub fn clone_gl(&self) -> Rc<gl::Gl> {
+        match *self {
+            WindowWrapper::Window(_, ref gl) | WindowWrapper::Headless(_, ref gl) => gl.clone(),
+        }
+    }
+}
+
+fn make_window(
+    size: DeviceUintSize,
+    dp_ratio: Option<f32>,
+    vsync: bool,
+    headless: bool,
+) -> WindowWrapper {
+    let wrapper = if headless {
+        let gl = match gl::GlType::default() {
+            gl::GlType::Gl => unsafe {
+                gl::GlFns::load_with(|symbol| {
+                    HeadlessContext::get_proc_address(symbol) as *const _
+                })
+            },
+            gl::GlType::Gles => unsafe {
+                gl::GlesFns::load_with(|symbol| {
+                    HeadlessContext::get_proc_address(symbol) as *const _
+                })
+            },
+        };
+        WindowWrapper::Headless(HeadlessContext::new(size.width, size.height), gl)
+    } else {
+        let mut builder = glutin::WindowBuilder::new()
+            .with_gl(glutin::GlRequest::GlThenGles {
+                opengl_version: (3, 2),
+                opengles_version: (3, 0),
+            })
+            .with_dimensions(size.width, size.height);
+        builder.opengl.vsync = vsync;
+        let window = builder.build().unwrap();
+        unsafe {
+            window
+                .make_current()
+                .expect("unable to make context current!");
+        }
+
+        let gl = match window.get_api() {
+            glutin::Api::OpenGl => unsafe {
+                gl::GlFns::load_with(|symbol| window.get_proc_address(symbol) as *const _)
+            },
+            glutin::Api::OpenGlEs => unsafe {
+                gl::GlesFns::load_with(|symbol| window.get_proc_address(symbol) as *const _)
+            },
+            glutin::Api::WebGl => unimplemented!(),
+        };
+        WindowWrapper::Window(window, gl)
+    };
+
+    wrapper.gl().clear_color(0.3, 0.0, 0.0, 1.0);
+
+    let gl_version = wrapper.gl().get_string(gl::VERSION);
+    let gl_renderer = wrapper.gl().get_string(gl::RENDERER);
+
+    let dp_ratio = dp_ratio.unwrap_or(wrapper.hidpi_factor());
+    println!("OpenGL version {}, {}", gl_version, gl_renderer);
+    println!(
+        "hidpi factor: {} (native {})",
+        dp_ratio,
+        wrapper.hidpi_factor()
+    );
+
+    wrapper
+}
+
+struct Notifier {
+    tx: Sender<()>,
+}
+
+// setup a notifier so we can wait for frames to be finished
+impl RenderNotifier for Notifier {
+    fn clone(&self) -> Box<RenderNotifier> {
+        Box::new(Notifier {
+            tx: self.tx.clone(),
+        })
+    }
+
+    fn wake_up(&self) {
+        self.tx.send(()).unwrap();
+    }
+
+    fn new_document_ready(&self, _: DocumentId, scrolled: bool, _composite_needed: bool) {
+        if !scrolled {
+            self.wake_up();
+        }
+    }
+}
+
+fn create_notifier() -> (Box<RenderNotifier>, Receiver<()>) {
+    let (tx, rx) = channel();
+    (Box::new(Notifier { tx: tx }), rx)
+}
+
+fn main() {
+    #[cfg(feature = "logging")]
+    env_logger::init().unwrap();
+
+    let args_yaml = load_yaml!("args.yaml");
+    let args = clap::App::from_yaml(args_yaml)
+        .setting(clap::AppSettings::ArgRequiredElseHelp)
+        .get_matches();
+
+    // handle some global arguments
+    let res_path = args.value_of("shaders").map(|s| PathBuf::from(s));
+    let dp_ratio = args.value_of("dp_ratio").map(|v| v.parse::<f32>().unwrap());
+    let save_type = args.value_of("save").map(|s| match s {
+        "yaml" => wrench::SaveType::Yaml,
+        "json" => wrench::SaveType::Json,
+        "ron" => wrench::SaveType::Ron,
+        "binary" => wrench::SaveType::Binary,
+        _ => panic!("Save type must be json, ron, yaml, or binary")
+    });
+    let size = args.value_of("size")
+        .map(|s| if s == "720p" {
+            DeviceUintSize::new(1280, 720)
+        } else if s == "1080p" {
+            DeviceUintSize::new(1920, 1080)
+        } else if s == "4k" {
+            DeviceUintSize::new(3840, 2160)
+        } else {
+            let x = s.find('x').expect(
+                "Size must be specified exactly as 720p, 1080p, 4k, or width x height",
+            );
+            let w = s[0 .. x].parse::<u32>().expect("Invalid size width");
+            let h = s[x + 1 ..].parse::<u32>().expect("Invalid size height");
+            DeviceUintSize::new(w, h)
+        })
+        .unwrap_or(DeviceUintSize::new(1920, 1080));
+    let is_headless = args.is_present("headless");
+    let zoom_factor = args.value_of("zoom").map(|z| z.parse::<f32>().unwrap());
+    let mut window = make_window(size, dp_ratio, args.is_present("vsync"), is_headless);
+    let dp_ratio = dp_ratio.unwrap_or(window.hidpi_factor());
+    let dim = window.get_inner_size();
+
+    let needs_frame_notifier = ["perf", "reftest", "png", "rawtest"]
+        .iter()
+        .any(|s| args.subcommand_matches(s).is_some());
+    let (notifier, rx) = if needs_frame_notifier {
+        let (notifier, rx) = create_notifier();
+        (Some(notifier), Some(rx))
+    } else {
+        (None, None)
+    };
+
+    let mut wrench = Wrench::new(
+        &mut window,
+        res_path,
+        dp_ratio,
+        save_type,
+        dim,
+        args.is_present("rebuild"),
+        args.is_present("no_subpixel_aa"),
+        args.is_present("debug"),
+        args.is_present("verbose"),
+        args.is_present("no_scissor"),
+        args.is_present("no_batch"),
+        args.is_present("precache"),
+        args.is_present("slow_subpixel"),
+        zoom_factor.unwrap_or(1.0),
+        notifier,
+    );
+
+    let mut thing = if let Some(subargs) = args.subcommand_matches("show") {
+        Box::new(YamlFrameReader::new_from_args(subargs)) as Box<WrenchThing>
+    } else if let Some(subargs) = args.subcommand_matches("replay") {
+        Box::new(BinaryFrameReader::new_from_args(subargs)) as Box<WrenchThing>
+    } else if let Some(subargs) = args.subcommand_matches("png") {
+        let surface = match subargs.value_of("surface") {
+            Some("screen") | None => png::ReadSurface::Screen,
+            Some("gpu-cache") => png::ReadSurface::GpuCache,
+            _ => panic!("Unknown surface argument value")
+        };
+        let reader = YamlFrameReader::new_from_args(subargs);
+        png::png(&mut wrench, surface, &mut window, reader, rx.unwrap());
+        wrench.renderer.deinit();
+        return;
+    } else if let Some(subargs) = args.subcommand_matches("reftest") {
+        let dim = window.get_inner_size();
+        let harness = ReftestHarness::new(&mut wrench, &mut window, rx.unwrap());
+        let base_manifest = Path::new("reftests/reftest.list");
+        let specific_reftest = subargs.value_of("REFTEST").map(|x| Path::new(x));
+        let mut reftest_options = ReftestOptions::default();
+        if let Some(allow_max_diff) = subargs.value_of("fuzz_tolerance") {
+            reftest_options.allow_max_difference = allow_max_diff.parse().unwrap_or(1);
+            reftest_options.allow_num_differences = dim.width as usize * dim.height as usize;
+        }
+        harness.run(base_manifest, specific_reftest, &reftest_options);
+        return;
+    } else if let Some(_) = args.subcommand_matches("rawtest") {
+        {
+            let harness = RawtestHarness::new(&mut wrench, &mut window, rx.unwrap());
+            harness.run();
+        }
+        wrench.renderer.deinit();
+        return;
+    } else if let Some(subargs) = args.subcommand_matches("perf") {
+        // Perf mode wants to benchmark the total cost of drawing
+        // a new displaty list each frame.
+        wrench.rebuild_display_lists = true;
+        let harness = PerfHarness::new(&mut wrench, &mut window, rx.unwrap());
+        let base_manifest = Path::new("benchmarks/benchmarks.list");
+        let filename = subargs.value_of("filename").unwrap();
+        harness.run(base_manifest, filename);
+        return;
+    } else if let Some(subargs) = args.subcommand_matches("compare_perf") {
+        let first_filename = subargs.value_of("first_filename").unwrap();
+        let second_filename = subargs.value_of("second_filename").unwrap();
+        perf::compare(first_filename, second_filename);
+        return;
+    } else if let Some(subargs) = args.subcommand_matches("load") {
+        let path = PathBuf::from(subargs.value_of("path").unwrap());
+        let mut documents = wrench.api.load_capture(path);
+        println!("loaded {:?}", documents.iter().map(|cd| cd.document_id).collect::<Vec<_>>());
+        let captured = documents.swap_remove(0);
+        window.resize(captured.window_size);
+        wrench.document_id = captured.document_id;
+        Box::new(captured) as Box<WrenchThing>
+    } else {
+        panic!("Should never have gotten here! {:?}", args);
+    };
+
+    let mut show_help = false;
+    let mut do_loop = false;
+    let mut cpu_profile_index = 0;
+
+    let dim = window.get_inner_size();
+    wrench.update(dim);
+    thing.do_frame(&mut wrench);
+
+    'outer: loop {
+        if let Some(window_title) = wrench.take_title() {
+            window.set_title(&window_title);
+        }
+
+        let mut events = Vec::new();
+
+        match window {
+            WindowWrapper::Headless(..) => {
+                events.push(glutin::Event::Awakened);
+            }
+            WindowWrapper::Window(ref window, _) => {
+                events.push(window.wait_events().next().unwrap());
+                events.extend(window.poll_events());
+            }
+        }
+
+        let mut do_frame = false;
+        let mut do_render = false;
+
+        for event in events {
+            match event {
+                glutin::Event::Closed => {
+                    break 'outer;
+                }
+
+                glutin::Event::Refresh |
+                glutin::Event::Awakened |
+                glutin::Event::Focused(..) |
+                glutin::Event::MouseMoved(..) => {
+                    do_render = true;
+                }
+
+                glutin::Event::KeyboardInput(ElementState::Pressed, _scan_code, Some(vk)) => match vk {
+                    VirtualKeyCode::Escape => {
+                        break 'outer;
+                    }
+                    VirtualKeyCode::P => {
+                        wrench.renderer.toggle_debug_flags(DebugFlags::PROFILER_DBG);
+                        do_render = true;
+                    }
+                    VirtualKeyCode::O => {
+                        wrench.renderer.toggle_debug_flags(DebugFlags::RENDER_TARGET_DBG);
+                        do_render = true;
+                    }
+                    VirtualKeyCode::I => {
+                        wrench.renderer.toggle_debug_flags(DebugFlags::TEXTURE_CACHE_DBG);
+                        do_render = true;
+                    }
+                    VirtualKeyCode::B => {
+                        wrench.renderer.toggle_debug_flags(DebugFlags::ALPHA_PRIM_DBG);
+                        do_render = true;
+                    }
+                    VirtualKeyCode::S => {
+                        wrench.renderer.toggle_debug_flags(DebugFlags::COMPACT_PROFILER);
+                        do_render = true;
+                    }
+                    VirtualKeyCode::Q => {
+                        wrench.renderer.toggle_debug_flags(
+                            DebugFlags::GPU_TIME_QUERIES | DebugFlags::GPU_SAMPLE_QUERIES
+                        );
+                        do_render = true;
+                    }
+                    VirtualKeyCode::R => {
+                        wrench.set_page_zoom(ZoomFactor::new(1.0));
+                        do_frame = true;
+                    }
+                    VirtualKeyCode::M => {
+                        wrench.api.notify_memory_pressure();
+                        do_render = true;
+                    }
+                    VirtualKeyCode::L => {
+                        do_loop = !do_loop;
+                        do_render = true;
+                    }
+                    VirtualKeyCode::Left => {
+                        thing.prev_frame();
+                        do_frame = true;
+                    }
+                    VirtualKeyCode::Right => {
+                        thing.next_frame();
+                        do_frame = true;
+                    }
+                    VirtualKeyCode::H => {
+                        show_help = !show_help;
+                        do_render = true;
+                    }
+                    VirtualKeyCode::T => {
+                        let file_name = format!("profile-{}.json", cpu_profile_index);
+                        wrench.renderer.save_cpu_profile(&file_name);
+                        cpu_profile_index += 1;
+                    }
+                    VirtualKeyCode::C => {
+                        let path = PathBuf::from("../captures/wrench");
+                        wrench.api.save_capture(path, CaptureBits::all());
+                    }
+                    VirtualKeyCode::Up => {
+                        let current_zoom = wrench.get_page_zoom();
+                        let new_zoom_factor = ZoomFactor::new(current_zoom.get() + 0.1);
+
+                        wrench.set_page_zoom(new_zoom_factor);
+                        do_frame = true;
+                    }
+                    VirtualKeyCode::Down => {
+                        let current_zoom = wrench.get_page_zoom();
+                        let new_zoom_factor = ZoomFactor::new((current_zoom.get() - 0.1).max(0.1));
+
+                        wrench.set_page_zoom(new_zoom_factor);
+                        do_frame = true;
+                    }
+                    _ => {}
+                }
+                _ => {}
+            }
+        }
+
+        let dim = window.get_inner_size();
+        wrench.update(dim);
+
+        if do_frame {
+            let frame_num = thing.do_frame(&mut wrench);
+            unsafe {
+                CURRENT_FRAME_NUMBER = frame_num;
+            }
+        }
+
+        if do_render {
+            if show_help {
+                wrench.show_onscreen_help();
+            }
+
+            wrench.render();
+            window.swap_buffers();
+
+            if do_loop {
+                thing.next_frame();
+            }
+        }
+    }
+
+    if is_headless {
+        let rect = DeviceUintRect::new(DeviceUintPoint::zero(), size);
+        let pixels = wrench.renderer.read_pixels_rgba8(rect);
+        save_flipped("screenshot.png", pixels, size);
+    }
+
+    wrench.renderer.deinit();
+}
new file mode 100644
--- /dev/null
+++ b/gfx/wrench/src/parse_function.rs
@@ -0,0 +1,121 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use std::str::CharIndices;
+
+// support arguments like '4', 'ab', '4.0'
+fn acceptable_arg_character(c: char) -> bool {
+    c.is_alphanumeric() || c == '.' || c == '-'
+}
+
+// A crapy parser for parsing strings like "translate(1, 3)"
+pub fn parse_function(s: &str) -> (&str, Vec<&str>, &str) {
+    // XXX: This it not particular easy to read. Sorry.
+    struct Parser<'a> {
+        itr: CharIndices<'a>,
+        start: usize,
+        o: Option<(usize, char)>,
+    }
+    impl<'a> Parser<'a> {
+        fn skip_whitespace(&mut self) {
+            while let Some(k) = self.o {
+                if !k.1.is_whitespace() {
+                    break;
+                }
+                self.start = k.0 + k.1.len_utf8();
+                self.o = self.itr.next();
+            }
+        }
+    }
+    let mut c = s.char_indices();
+    let o = c.next();
+    let mut p = Parser {
+        itr: c,
+        start: 0,
+        o: o,
+    };
+
+    p.skip_whitespace();
+
+    let mut end = p.start;
+    while let Some(k) = p.o {
+        if !k.1.is_alphabetic() && k.1 != '_' && k.1 != '-' {
+            break;
+        }
+        end = k.0 + k.1.len_utf8();
+        p.o = p.itr.next();
+    }
+
+    let name = &s[p.start .. end];
+    let mut args = Vec::new();
+
+    p.skip_whitespace();
+
+    if let Some(k) = p.o {
+        if k.1 != '(' {
+            return (name, args, &s[p.start ..]);
+        }
+        p.start = k.0 + k.1.len_utf8();
+        p.o = p.itr.next();
+    }
+
+    loop {
+        p.skip_whitespace();
+
+        let mut end = p.start;
+        let mut bracket_count: i32 = 0;
+        while let Some(k) = p.o {
+            let prev_bracket_count = bracket_count;
+            if k.1 == '[' {
+                bracket_count = bracket_count + 1;
+            } else if k.1 == ']' {
+                bracket_count = bracket_count - 1;
+            }
+
+            if bracket_count < 0 {
+                println!("Unexpected closing bracket");
+                break;
+            }
+
+            let not_in_bracket = bracket_count == 0 && prev_bracket_count == 0;
+            if !acceptable_arg_character(k.1) && not_in_bracket {
+                break;
+            }
+            end = k.0 + k.1.len_utf8();
+            p.o = p.itr.next();
+        }
+
+        args.push(&s[p.start .. end]);
+
+        p.skip_whitespace();
+
+        if let Some(k) = p.o {
+            p.start = k.0 + k.1.len_utf8();
+            p.o = p.itr.next();
+            // unless we find a comma we're done
+            if k.1 != ',' {
+                if k.1 != ')' {
+                    println!("Unexpected closing character: {}", k.1);
+                }
+                break;
+            }
+        } else {
+            break;
+        }
+    }
+    (name, args, &s[p.start ..])
+}
+
+#[test]
+fn test() {
+    assert_eq!(parse_function("rotate(40)").0, "rotate");
+    assert_eq!(parse_function("  rotate(40)").0, "rotate");
+    assert_eq!(parse_function("  rotate  (40)").0, "rotate");
+    assert_eq!(parse_function("  rotate  (  40 )").1[0], "40");
+    assert_eq!(parse_function("rotate(-40.0)").1[0], "-40.0");
+    assert_eq!(parse_function("drop-shadow(0, [1, 2, 3, 4], 5)").1[0], "0");
+    assert_eq!(parse_function("drop-shadow(0, [1, 2, 3, 4], 5)").1[1], "[1, 2, 3, 4]");
+    assert_eq!(parse_function("drop-shadow(0, [1, 2, 3, 4], 5)").1[2], "5");
+    assert_eq!(parse_function("drop-shadow(0, [1, 2, [3, 4]], 5)").1[1], "[1, 2, [3, 4]]");
+}
new file mode 100644
--- /dev/null
+++ b/gfx/wrench/src/perf.rs
@@ -0,0 +1,279 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use WindowWrapper;
+use serde_json;
+use std::collections::{HashMap, HashSet};
+use std::fs::File;
+use std::io::{BufRead, BufReader};
+use std::io::{Read, Write};
+use std::path::{Path, PathBuf};
+use std::sync::mpsc::Receiver;
+use wrench::{Wrench, WrenchThing};
+use yaml_frame_reader::YamlFrameReader;
+
+const COLOR_DEFAULT: &str = "\x1b[0m";
+const COLOR_RED: &str = "\x1b[31m";
+const COLOR_GREEN: &str = "\x1b[32m";
+const COLOR_MAGENTA: &str = "\x1b[95m";
+
+const MIN_SAMPLE_COUNT: usize = 50;
+const SAMPLE_EXCLUDE_COUNT: usize = 10;
+
+pub struct Benchmark {
+    pub test: PathBuf,
+}
+
+pub struct BenchmarkManifest {
+    pub benchmarks: Vec<Benchmark>,
+}
+
+impl BenchmarkManifest {
+    pub fn new(manifest: &Path) -> BenchmarkManifest {
+        let dir = manifest.parent().unwrap();
+        let f =
+            File::open(manifest).expect(&format!("couldn't open manifest: {}", manifest.display()));
+        let file = BufReader::new(&f);
+
+        let mut benchmarks = Vec::new();
+
+        for line in file.lines() {
+            let l = line.unwrap();
+
+            // strip the comments
+            let s = &l[0 .. l.find('#').unwrap_or(l.len())];
+            let s = s.trim();
+            if s.is_empty() {
+                continue;
+            }
+
+            let mut items = s.split_whitespace();
+
+            match items.next() {
+                Some("include") => {
+                    let include = dir.join(items.next().unwrap());
+
+                    benchmarks.append(&mut BenchmarkManifest::new(include.as_path()).benchmarks);
+                }
+                Some(name) => {
+                    let test = dir.join(name);
+                    benchmarks.push(Benchmark { test });
+                }
+                _ => panic!(),
+            };
+        }
+
+        BenchmarkManifest {
+            benchmarks: benchmarks,
+        }
+    }
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+struct TestProfile {
+    name: String,
+    backend_time_ns: u64,
+    composite_time_ns: u64,
+    paint_time_ns: u64,
+    draw_calls: usize,
+}
+
+#[derive(Serialize, Deserialize)]
+struct Profile {
+    tests: Vec<TestProfile>,
+}
+
+impl Profile {
+    fn new() -> Profile {
+        Profile { tests: Vec::new() }
+    }
+
+    fn add(&mut self, profile: TestProfile) {
+        self.tests.push(profile);
+    }
+
+    fn save(&self, filename: &str) {
+        let mut file = File::create(&filename).unwrap();
+        let s = serde_json::to_string_pretty(self).unwrap();
+        file.write_all(&s.into_bytes()).unwrap();
+        file.write_all(b"\n").unwrap();
+    }
+
+    fn load(filename: &str) -> Profile {
+        let mut file = File::open(&filename).unwrap();
+        let mut string = String::new();
+        file.read_to_string(&mut string).unwrap();
+        serde_json::from_str(&string).expect("Unable to load profile!")
+    }
+
+    fn build_set_and_map_of_tests(&self) -> (HashSet<String>, HashMap<String, TestProfile>) {
+        let mut hash_set = HashSet::new();
+        let mut hash_map = HashMap::new();
+
+        for test in &self.tests {
+            hash_set.insert(test.name.clone());
+            hash_map.insert(test.name.clone(), test.clone());
+        }
+
+        (hash_set, hash_map)
+    }
+}
+
+pub struct PerfHarness<'a> {
+    wrench: &'a mut Wrench,
+    window: &'a mut WindowWrapper,
+    rx: Receiver<()>,
+}
+
+impl<'a> PerfHarness<'a> {
+    pub fn new(wrench: &'a mut Wrench, window: &'a mut WindowWrapper, rx: Receiver<()>) -> Self {
+        PerfHarness { wrench, window, rx }
+    }
+
+    pub fn run(mut self, base_manifest: &Path, filename: &str) {
+        let manifest = BenchmarkManifest::new(base_manifest);
+
+        let mut profile = Profile::new();
+
+        for t in manifest.benchmarks {
+            let stats = self.render_yaml(t.test.as_path());
+            profile.add(stats);
+        }
+
+        profile.save(filename);
+    }
+
+    fn render_yaml(&mut self, filename: &Path) -> TestProfile {
+        let mut reader = YamlFrameReader::new(filename);
+
+        // Loop until we get a reasonable number of CPU and GPU
+        // frame profiles. Then take the mean.
+        let mut cpu_frame_profiles = Vec::new();
+        let mut gpu_frame_profiles = Vec::new();
+
+        while cpu_frame_profiles.len() < MIN_SAMPLE_COUNT ||
+            gpu_frame_profiles.len() < MIN_SAMPLE_COUNT
+        {
+            reader.do_frame(self.wrench);
+            self.rx.recv().unwrap();
+            self.wrench.render();
+            self.window.swap_buffers();
+            let (cpu_profiles, gpu_profiles) = self.wrench.get_frame_profiles();
+            cpu_frame_profiles.extend(cpu_profiles);
+            gpu_frame_profiles.extend(gpu_profiles);
+        }
+
+        // Ensure the draw calls match in every sample.
+        let draw_calls = cpu_frame_profiles[0].draw_calls;
+        assert!(
+            cpu_frame_profiles
+                .iter()
+                .all(|s| s.draw_calls == draw_calls)
+        );
+
+        let composite_time_ns = extract_sample(&mut cpu_frame_profiles, |a| a.composite_time_ns);
+        let paint_time_ns = extract_sample(&mut gpu_frame_profiles, |a| a.paint_time_ns);
+        let backend_time_ns = extract_sample(&mut cpu_frame_profiles, |a| a.backend_time_ns);
+
+        TestProfile {
+            name: filename.to_str().unwrap().to_string(),
+            composite_time_ns,
+            paint_time_ns,
+            backend_time_ns,
+            draw_calls,
+        }
+    }
+}
+
+fn extract_sample<F, T>(profiles: &mut [T], f: F) -> u64
+where
+    F: Fn(&T) -> u64,
+{
+    let mut samples: Vec<u64> = profiles.iter().map(f).collect();
+    samples.sort();
+    let useful_samples = &samples[SAMPLE_EXCLUDE_COUNT .. samples.len() - SAMPLE_EXCLUDE_COUNT];
+    let total_time: u64 = useful_samples.iter().sum();
+    total_time / useful_samples.len() as u64
+}
+
+fn select_color(base: f32, value: f32) -> &'static str {
+    let tolerance = base * 0.1;
+    if (value - base).abs() < tolerance {
+        COLOR_DEFAULT
+    } else if value > base {
+        COLOR_RED
+    } else {
+        COLOR_GREEN
+    }
+}
+
+pub fn compare(first_filename: &str, second_filename: &str) {
+    let profile0 = Profile::load(first_filename);
+    let profile1 = Profile::load(second_filename);
+
+    let (set0, map0) = profile0.build_set_and_map_of_tests();
+    let (set1, map1) = profile1.build_set_and_map_of_tests();
+
+    print!("+------------------------------------------------");
+    println!("+--------------+------------------+------------------+");
+    print!("|  Test name                                     ");
+    println!("| Draw Calls   | Composite (ms)   | Paint (ms)       |");
+    print!("+------------------------------------------------");
+    println!("+--------------+------------------+------------------+");
+
+    for test_name in set0.symmetric_difference(&set1) {
+        println!(
+            "| {}{:47}{}|{:14}|{:18}|{:18}|",
+            COLOR_MAGENTA,
+            test_name,
+            COLOR_DEFAULT,
+            " -",
+            " -",
+            " -"
+        );
+    }
+
+    for test_name in set0.intersection(&set1) {
+        let test0 = &map0[test_name];
+        let test1 = &map1[test_name];
+
+        let composite_time0 = test0.composite_time_ns as f32 / 1000000.0;
+        let composite_time1 = test1.composite_time_ns as f32 / 1000000.0;
+
+        let paint_time0 = test0.paint_time_ns as f32 / 1000000.0;
+        let paint_time1 = test1.paint_time_ns as f32 / 1000000.0;
+
+        let draw_calls_color = if test0.draw_calls == test1.draw_calls {
+            COLOR_DEFAULT
+        } else if test0.draw_calls > test1.draw_calls {
+            COLOR_GREEN
+        } else {
+            COLOR_RED
+        };
+
+        let composite_time_color = select_color(composite_time0, composite_time1);
+        let paint_time_color = select_color(paint_time0, paint_time1);
+
+        let draw_call_string = format!(" {} -> {}", test0.draw_calls, test1.draw_calls);
+        let composite_time_string = format!(" {:.2} -> {:.2}", composite_time0, composite_time1);
+        let paint_time_string = format!(" {:.2} -> {:.2}", paint_time0, paint_time1);
+
+        println!(
+            "| {:47}|{}{:14}{}|{}{:18}{}|{}{:18}{}|",
+            test_name,
+            draw_calls_color,
+            draw_call_string,
+            COLOR_DEFAULT,
+            composite_time_color,
+            composite_time_string,
+            COLOR_DEFAULT,
+            paint_time_color,
+            paint_time_string,
+            COLOR_DEFAULT
+        );
+    }
+
+    print!("+------------------------------------------------");
+    println!("+--------------+------------------+------------------+");
+}
new file mode 100644
--- /dev/null
+++ b/gfx/wrench/src/png.rs
@@ -0,0 +1,113 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use WindowWrapper;
+use image::png::PNGEncoder;
+use image::{self, ColorType, GenericImage};
+use std::fs::File;
+use std::path::Path;
+use std::sync::mpsc::Receiver;
+use webrender::api::*;
+use wrench::{Wrench, WrenchThing};
+use yaml_frame_reader::YamlFrameReader;
+
+pub enum ReadSurface {
+    Screen,
+    GpuCache,
+}
+
+pub struct SaveSettings {
+    pub flip_vertical: bool,
+    pub try_crop: bool,
+}
+
+pub fn save<P: Clone + AsRef<Path>>(
+    path: P,
+    orig_pixels: Vec<u8>,
+    mut size: DeviceUintSize,
+    settings: SaveSettings
+) {
+    let mut buffer = image::RgbaImage::from_raw(
+        size.width,
+        size.height,
+        orig_pixels,
+    ).expect("bug: unable to construct image buffer");
+
+    if settings.flip_vertical {
+        // flip image vertically (texture is upside down)
+        buffer = image::imageops::flip_vertical(&buffer);
+    }
+
+    if settings.try_crop {
+        if let Ok(existing_image) = image::open(path.clone()) {
+            let old_dims = existing_image.dimensions();
+            println!("Crop from {:?} to {:?}", size, old_dims);
+            size.width = old_dims.0;
+            size.height = old_dims.1;
+            buffer = image::imageops::crop(
+                &mut buffer,
+                0,
+                0,
+                size.width,
+                size.height
+            ).to_image();
+        }
+    }
+
+    let encoder = PNGEncoder::new(File::create(path).unwrap());
+    encoder
+        .encode(&buffer, size.width, size.height, ColorType::RGBA(8))
+        .expect("Unable to encode PNG!");
+}
+
+pub fn save_flipped<P: Clone + AsRef<Path>>(
+    path: P,
+    orig_pixels: Vec<u8>,
+    size: DeviceUintSize,
+) {
+    save(path, orig_pixels, size, SaveSettings {
+        flip_vertical: true,
+        try_crop: true,
+    })
+}
+
+pub fn png(
+    wrench: &mut Wrench,
+    surface: ReadSurface,
+    window: &mut WindowWrapper,
+    mut reader: YamlFrameReader,
+    rx: Receiver<()>,
+) {
+    reader.do_frame(wrench);
+
+    // wait for the frame
+    rx.recv().unwrap();
+    wrench.render();
+
+    let (device_size, data, settings) = match surface {
+        ReadSurface::Screen => {
+            let dim = window.get_inner_size();
+            let rect = DeviceUintRect::new(DeviceUintPoint::zero(), dim);
+            let data = wrench.renderer
+                .read_pixels_rgba8(rect);
+            (rect.size, data, SaveSettings {
+                flip_vertical: true,
+                try_crop: true,
+            })
+        }
+        ReadSurface::GpuCache => {
+            let (size, data) = wrench.renderer
+                .read_gpu_cache();
+            (size, data, SaveSettings {
+                flip_vertical: false,
+                try_crop: false,
+            })
+        }
+    };
+
+    let mut out_path = reader.yaml_path().clone();
+    out_path.set_extension("png");
+
+    save(out_path, data, device_size, settings);
+}
new file mode 100644
--- /dev/null
+++ b/gfx/wrench/src/premultiply.rs
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// These are slow. Gecko's gfx/2d/Swizzle.cpp has better versions
+pub fn premultiply(data: &mut [u8]) {
+    for pixel in data.chunks_mut(4) {
+        let a = pixel[3] as u32;
+        let b = pixel[2] as u32;
+        let g = pixel[1] as u32;
+        let r = pixel[0] as u32;
+
+        pixel[3] = a as u8;
+        pixel[2] = ((r * a + 128) / 255) as u8;
+        pixel[1] = ((g * a + 128) / 255) as u8;
+        pixel[0] = ((b * a + 128) / 255) as u8;
+    }
+}
+
+pub fn unpremultiply(data: &mut [u8]) {
+    for pixel in data.chunks_mut(4) {
+        let a = pixel[3] as u32;
+        let mut b = pixel[2] as u32;
+        let mut g = pixel[1] as u32;
+        let mut r = pixel[0] as u32;
+
+        if a > 0 {
+            r = r * 255 / a;
+            g = g * 255 / a;
+            b = b * 255 / a;
+        }
+
+        pixel[3] = a as u8;
+        pixel[2] = r as u8;
+        pixel[1] = g as u8;
+        pixel[0] = b as u8;
+    }
+}
+
+#[test]
+fn it_works() {
+    let mut f = [0xff, 0xff, 0xff, 0x80, 0x00, 0xff, 0x00, 0x80];
+    premultiply(&mut f);
+    println!("{:?}", f);
+    assert!(
+        f[0] == 0x80 && f[1] == 0x80 && f[2] == 0x80 && f[3] == 0x80 && f[4] == 0x00 &&
+            f[5] == 0x80 && f[6] == 0x00 && f[7] == 0x80
+    );
+    unpremultiply(&mut f);
+    println!("{:?}", f);
+    assert!(
+        f[0] == 0xff && f[1] == 0xff && f[2] == 0xff && f[3] == 0x80 && f[4] == 0x00 &&
+            f[5] == 0xff && f[6] == 0x00 && f[7] == 0x80
+    );
+}
new file mode 100644
--- /dev/null
+++ b/gfx/wrench/src/rawtest.rs
@@ -0,0 +1,567 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use WindowWrapper;
+use blob;
+use euclid::{TypedRect, TypedSize2D, TypedPoint2D};
+use std::sync::Arc;
+use std::sync::atomic::{AtomicIsize, Ordering};
+use std::sync::mpsc::Receiver;
+use webrender::api::*;
+use wrench::Wrench;
+
+pub struct RawtestHarness<'a> {
+    wrench: &'a mut Wrench,
+    rx: Receiver<()>,
+    window: &'a mut WindowWrapper,
+}
+
+fn point<T: Copy, U>(x: T, y: T) -> TypedPoint2D<T, U> {
+    TypedPoint2D::new(x, y)
+}
+
+fn size<T: Copy, U>(x: T, y: T) -> TypedSize2D<T, U> {
+    TypedSize2D::new(x, y)
+}
+
+fn rect<T: Copy, U>(x: T, y: T, width: T, height: T) -> TypedRect<T, U> {
+    TypedRect::new(point(x, y), size(width, height))
+}
+
+impl<'a> RawtestHarness<'a> {
+    pub fn new(wrench: &'a mut Wrench, window: &'a mut WindowWrapper, rx: Receiver<()>) -> Self {
+        RawtestHarness {
+            wrench,
+            rx,
+            window,
+        }
+    }
+
+    pub fn run(mut self) {
+        self.test_retained_blob_images_test();
+        self.test_blob_update_test();
+        self.test_blob_update_epoch_test();
+        self.test_tile_decomposition();
+        self.test_save_restore();
+        self.test_capture();
+    }
+
+    fn render_and_get_pixels(&mut self, window_rect: DeviceUintRect) -> Vec<u8> {
+        self.rx.recv().unwrap();
+        self.wrench.render();
+        self.wrench.renderer.read_pixels_rgba8(window_rect)
+    }
+
+    fn submit_dl(
+        &mut self,
+        epoch: &mut Epoch,
+        layout_size: LayoutSize,
+        builder: DisplayListBuilder,
+        resources: Option<ResourceUpdates>
+    ) {
+        let mut txn = Transaction::new();
+        let root_background_color = Some(ColorF::new(1.0, 1.0, 1.0, 1.0));
+        if let Some(resources) = resources {
+            txn.update_resources(resources);
+        }
+        txn.set_display_list(
+            *epoch,
+            root_background_color,
+            layout_size,
+            builder.finalize(),
+            false,
+        );
+        epoch.0 += 1;
+
+        txn.generate_frame();
+        self.wrench.api.send_transaction(self.wrench.document_id, txn);
+    }
+
+    fn test_tile_decomposition(&mut self) {
+        println!("\ttile decomposition...");
+        // This exposes a crash in tile decomposition
+        let layout_size = LayoutSize::new(800., 800.);
+        let mut resources = ResourceUpdates::new();
+
+        let blob_img = self.wrench.api.generate_image_key();
+        resources.add_image(
+            blob_img,
+            ImageDescriptor::new(151, 56, ImageFormat::BGRA8, true),
+            ImageData::new_blob_image(blob::serialize_blob(ColorU::new(50, 50, 150, 255))),
+            Some(128),
+        );
+
+        let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id, layout_size);
+
+        let info = LayoutPrimitiveInfo::new(rect(448.899994, 74.0, 151.000031, 56.));
+
+        // setup some malicious image size parameters
+        builder.push_image(
+            &info,
+            size(151., 56.0),
+            size(151.0, 56.0),
+            ImageRendering::Auto,
+            AlphaType::PremultipliedAlpha,
+            blob_img,
+        );
+
+        let mut epoch = Epoch(0);
+
+        self.submit_dl(&mut epoch, layout_size, builder, Some(resources));
+
+        self.rx.recv().unwrap();
+        self.wrench.render();
+
+        // Leaving a tiled blob image in the resource cache
+        // confuses the `test_capture`. TODO: remove this
+        resources = ResourceUpdates::new();
+        resources.delete_image(blob_img);
+        self.wrench.api.update_resources(resources);
+    }
+
+    fn test_retained_blob_images_test(&mut self) {
+        println!("\tretained blob images test...");
+        let blob_img;
+        let window_size = self.window.get_inner_size();
+
+        let test_size = DeviceUintSize::new(400, 400);
+
+        let window_rect = DeviceUintRect::new(
+            DeviceUintPoint::new(0, window_size.height - test_size.height),
+            test_size,
+        );
+        let layout_size = LayoutSize::new(400., 400.);
+        let mut resources = ResourceUpdates::new();
+        {
+            let api = &self.wrench.api;
+
+            blob_img = api.generate_image_key();
+            resources.add_image(
+                blob_img,
+                ImageDescriptor::new(500, 500, ImageFormat::BGRA8, true),
+                ImageData::new_blob_image(blob::serialize_blob(ColorU::new(50, 50, 150, 255))),
+                None,
+            );
+        }
+
+        // draw the blob the first time
+        let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id, layout_size);
+        let info = LayoutPrimitiveInfo::new(rect(0.0, 60.0, 200.0, 200.0));
+
+        builder.push_image(
+            &info,
+            size(200.0, 200.0),
+            size(0.0, 0.0),
+            ImageRendering::Auto,
+            AlphaType::PremultipliedAlpha,
+            blob_img,
+        );
+
+        let mut epoch = Epoch(0);
+
+        self.submit_dl(&mut epoch, layout_size, builder, Some(resources));
+
+        // draw the blob image a second time at a different location
+
+        // make a new display list that refers to the first image
+        let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id, layout_size);
+        let info = LayoutPrimitiveInfo::new(rect(1.0, 60.0, 200.0, 200.0));
+        builder.push_image(
+            &info,
+            size(200.0, 200.0),
+            size(0.0, 0.0),
+            ImageRendering::Auto,
+            AlphaType::PremultipliedAlpha,
+            blob_img,
+        );
+
+        self.submit_dl(&mut epoch, layout_size, builder, None);
+
+        let called = Arc::new(AtomicIsize::new(0));
+        let called_inner = Arc::clone(&called);
+
+        self.wrench.callbacks.lock().unwrap().request = Box::new(move |_| {
+            called_inner.fetch_add(1, Ordering::SeqCst);
+        });
+
+        let pixels_first = self.render_and_get_pixels(window_rect);
+        assert!(called.load(Ordering::SeqCst) == 1);
+
+        let pixels_second = self.render_and_get_pixels(window_rect);
+
+        // make sure we only requested once
+        assert!(called.load(Ordering::SeqCst) == 1);
+
+        // use png;
+        // png::save_flipped("out1.png", &pixels_first, window_rect.size);
+        // png::save_flipped("out2.png", &pixels_second, window_rect.size);
+
+        assert!(pixels_first != pixels_second);
+    }
+
+    fn test_blob_update_epoch_test(&mut self) {
+        println!("\tblob update epoch test...");
+        let (blob_img, blob_img2);
+        let window_size = self.window.get_inner_size();
+
+        let test_size = DeviceUintSize::new(400, 400);
+
+        let window_rect = DeviceUintRect::new(
+            point(0, window_size.height - test_size.height),
+            test_size,
+        );
+        let layout_size = LayoutSize::new(400., 400.);
+        let mut resources = ResourceUpdates::new();
+        let (blob_img, blob_img2) = {
+            let api = &self.wrench.api;
+
+            blob_img = api.generate_image_key();
+            resources.add_image(
+                blob_img,
+                ImageDescriptor::new(500, 500, ImageFormat::BGRA8, true),
+                ImageData::new_blob_image(blob::serialize_blob(ColorU::new(50, 50, 150, 255))),
+                None,
+            );
+            blob_img2 = api.generate_image_key();
+            resources.add_image(
+                blob_img2,
+                ImageDescriptor::new(500, 500, ImageFormat::BGRA8, true),
+                ImageData::new_blob_image(blob::serialize_blob(ColorU::new(80, 50, 150, 255))),
+                None,
+            );
+            (blob_img, blob_img2)
+        };
+
+        // setup some counters to count how many times each image is requested
+        let img1_requested = Arc::new(AtomicIsize::new(0));
+        let img1_requested_inner = Arc::clone(&img1_requested);
+        let img2_requested = Arc::new(AtomicIsize::new(0));
+        let img2_requested_inner = Arc::clone(&img2_requested);
+
+        // track the number of times that the second image has been requested
+        self.wrench.callbacks.lock().unwrap().request = Box::new(move |&desc| {
+            if desc.key == blob_img {
+                img1_requested_inner.fetch_add(1, Ordering::SeqCst);
+            }
+            if desc.key == blob_img2 {
+                img2_requested_inner.fetch_add(1, Ordering::SeqCst);
+            }
+        });
+
+        // create two blob images and draw them
+        let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id, layout_size);
+        let info = LayoutPrimitiveInfo::new(rect(0.0, 60.0, 200.0, 200.0));
+        let info2 = LayoutPrimitiveInfo::new(rect(200.0, 60.0, 200.0, 200.0));
+        let push_images = |builder: &mut DisplayListBuilder| {
+            builder.push_image(
+                &info,
+                size(200.0, 200.0),
+                size(0.0, 0.0),
+                ImageRendering::Auto,
+                AlphaType::PremultipliedAlpha,
+                blob_img,
+            );
+            builder.push_image(
+                &info2,
+                size(200.0, 200.0),
+                size(0.0, 0.0),
+                ImageRendering::Auto,
+                AlphaType::PremultipliedAlpha,
+                blob_img2,
+            );
+        };
+
+        push_images(&mut builder);
+
+        let mut epoch = Epoch(0);
+
+        self.submit_dl(&mut epoch, layout_size, builder, Some(resources));
+        let _pixels_first = self.render_and_get_pixels(window_rect);
+
+
+        // update and redraw both images
+        let mut resources = ResourceUpdates::new();
+        resources.update_image(
+            blob_img,
+            ImageDescriptor::new(500, 500, ImageFormat::BGRA8, true),
+            ImageData::new_blob_image(blob::serialize_blob(ColorU::new(50, 50, 150, 255))),
+            Some(rect(100, 100, 100, 100)),
+        );
+        resources.update_image(
+            blob_img2,
+            ImageDescriptor::new(500, 500, ImageFormat::BGRA8, true),
+            ImageData::new_blob_image(blob::serialize_blob(ColorU::new(59, 50, 150, 255))),
+            Some(rect(100, 100, 100, 100)),
+        );
+
+        let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id, layout_size);
+        push_images(&mut builder);
+        self.submit_dl(&mut epoch, layout_size, builder, Some(resources));
+        let _pixels_second = self.render_and_get_pixels(window_rect);
+
+
+        // only update the first image
+        let mut resources = ResourceUpdates::new();
+        resources.update_image(
+            blob_img,
+            ImageDescriptor::new(500, 500, ImageFormat::BGRA8, true),
+            ImageData::new_blob_image(blob::serialize_blob(ColorU::new(50, 150, 150, 255))),
+            Some(rect(200, 200, 100, 100)),
+        );
+
+        let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id, layout_size);
+        push_images(&mut builder);
+        self.submit_dl(&mut epoch, layout_size, builder, Some(resources));
+        let _pixels_third = self.render_and_get_pixels(window_rect);
+
+        // the first image should be requested 3 times
+        assert_eq!(img1_requested.load(Ordering::SeqCst), 3);
+        // the second image should've been requested twice
+        assert_eq!(img2_requested.load(Ordering::SeqCst), 2);
+    }
+
+    fn test_blob_update_test(&mut self) {
+        println!("\tblob update test...");
+        let window_size = self.window.get_inner_size();
+
+        let test_size = DeviceUintSize::new(400, 400);
+
+        let window_rect = DeviceUintRect::new(
+            point(0, window_size.height - test_size.height),
+            test_size,
+        );
+        let layout_size = LayoutSize::new(400., 400.);
+        let mut resources = ResourceUpdates::new();
+
+        let blob_img = {
+            let img = self.wrench.api.generate_image_key();
+            resources.add_image(
+                img,
+                ImageDescriptor::new(500, 500, ImageFormat::BGRA8, true),
+                ImageData::new_blob_image(blob::serialize_blob(ColorU::new(50, 50, 150, 255))),
+                None,
+            );
+            img
+        };
+
+        // draw the blobs the first time
+        let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id, layout_size);
+        let info = LayoutPrimitiveInfo::new(rect(0.0, 60.0, 200.0, 200.0));
+
+        builder.push_image(
+            &info,
+            size(200.0, 200.0),
+            size(0.0, 0.0),
+            ImageRendering::Auto,
+            AlphaType::PremultipliedAlpha,
+            blob_img,
+        );
+
+        let mut epoch = Epoch(0);
+
+        self.submit_dl(&mut epoch, layout_size, builder, Some(resources));
+        let pixels_first = self.render_and_get_pixels(window_rect);
+
+        // draw the blob image a second time after updating it with the same color
+        let mut resources = ResourceUpdates::new();
+        resources.update_image(
+            blob_img,
+            ImageDescriptor::new(500, 500, ImageFormat::BGRA8, true),
+            ImageData::new_blob_image(blob::serialize_blob(ColorU::new(50, 50, 150, 255))),
+            Some(rect(100, 100, 100, 100)),
+        );
+
+        // make a new display list that refers to the first image
+        let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id, layout_size);
+        let info = LayoutPrimitiveInfo::new(rect(0.0, 60.0, 200.0, 200.0));
+        builder.push_image(
+            &info,
+            size(200.0, 200.0),
+            size(0.0, 0.0),
+            ImageRendering::Auto,
+            AlphaType::PremultipliedAlpha,
+            blob_img,
+        );
+
+        self.submit_dl(&mut epoch, layout_size, builder, Some(resources));
+        let pixels_second = self.render_and_get_pixels(window_rect);
+
+        // draw the blob image a third time after updating it with a different color
+        let mut resources = ResourceUpdates::new();
+        resources.update_image(
+            blob_img,
+            ImageDescriptor::new(500, 500, ImageFormat::BGRA8, true),
+            ImageData::new_blob_image(blob::serialize_blob(ColorU::new(50, 150, 150, 255))),
+            Some(rect(200, 200, 100, 100)),
+        );
+
+        // make a new display list that refers to the first image
+        let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id, layout_size);
+        let info = LayoutPrimitiveInfo::new(rect(0.0, 60.0, 200.0, 200.0));
+        builder.push_image(
+            &info,
+            size(200.0, 200.0),
+            size(0.0, 0.0),
+            ImageRendering::Auto,
+            AlphaType::PremultipliedAlpha,
+            blob_img,
+        );
+
+        self.submit_dl(&mut epoch, layout_size, builder, Some(resources));
+        let pixels_third = self.render_and_get_pixels(window_rect);
+
+        assert!(pixels_first == pixels_second);
+        assert!(pixels_first != pixels_third);
+    }
+
+    // Ensures that content doing a save-restore produces the same results as not
+    fn test_save_restore(&mut self) {
+        println!("\tsave/restore...");
+        let window_size = self.window.get_inner_size();
+
+        let test_size = DeviceUintSize::new(400, 400);
+
+        let window_rect = DeviceUintRect::new(
+            DeviceUintPoint::new(0, window_size.height - test_size.height),
+            test_size,
+        );
+        let layout_size = LayoutSize::new(400., 400.);
+
+        let mut do_test = |should_try_and_fail| {
+            let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id, layout_size);
+
+            let clip = builder.define_clip(None, rect(110., 120., 200., 200.),
+                                           None::<ComplexClipRegion>, None);
+            builder.push_clip_id(clip);
+            builder.push_rect(&PrimitiveInfo::new(rect(100., 100., 100., 100.)),
+                              ColorF::new(0.0, 0.0, 1.0, 1.0));
+
+            if should_try_and_fail {
+                builder.save();
+                let clip = builder.define_clip(None, rect(80., 80., 90., 90.),
+                                               None::<ComplexClipRegion>, None);
+                builder.push_clip_id(clip);
+                builder.push_rect(&PrimitiveInfo::new(rect(110., 110., 50., 50.)),
+                              ColorF::new(0.0, 1.0, 0.0, 1.0));
+                builder.push_shadow(&PrimitiveInfo::new(rect(100., 100., 100., 100.)),
+                    Shadow {
+                        offset: LayoutVector2D::new(1.0, 1.0),
+                        blur_radius: 1.0,
+                        color: ColorF::new(0.0, 0.0, 0.0, 1.0),
+                    });
+                builder.push_line(&PrimitiveInfo::new(rect(110., 110., 50., 2.)),
+                                  0.0, LineOrientation::Horizontal,
+                                  &ColorF::new(0.0, 0.0, 0.0, 1.0), LineStyle::Solid);
+                builder.restore();
+            }
+
+            {
+                builder.save();
+                let clip = builder.define_clip(None, rect(80., 80., 100., 100.),
+                                               None::<ComplexClipRegion>, None);
+                builder.push_clip_id(clip);
+                builder.push_rect(&PrimitiveInfo::new(rect(150., 150., 100., 100.)),
+                                  ColorF::new(0.0, 0.0, 1.0, 1.0));
+
+                builder.pop_clip_id();
+                builder.clear_save();
+            }
+
+            builder.pop_clip_id();
+
+            self.submit_dl(&mut Epoch(0), layout_size, builder, None);
+
+            self.render_and_get_pixels(window_rect)
+        };
+
+        let first = do_test(false);
+        let second = do_test(true);
+
+        assert_eq!(first, second);
+    }
+
+    fn test_capture(&mut self) {
+        println!("\tcapture...");
+        let path = "../captures/test";
+        let layout_size = LayoutSize::new(400., 400.);
+        let dim = self.window.get_inner_size();
+        let window_rect = DeviceUintRect::new(
+            point(0, dim.height - layout_size.height as u32),
+            size(layout_size.width as u32, layout_size.height as u32),
+        );
+
+        // 1. render some scene
+
+        let mut resources = ResourceUpdates::new();
+        let image = self.wrench.api.generate_image_key();
+        resources.add_image(
+            image,
+            ImageDescriptor::new(1, 1, ImageFormat::BGRA8, true),
+            ImageData::new(vec![0xFF, 0, 0, 0xFF]),
+            None,
+        );
+
+        let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id, layout_size);
+
+        builder.push_image(
+            &LayoutPrimitiveInfo::new(rect(300.0, 70.0, 150.0, 50.0)),
+            size(150.0, 50.0),
+            size(0.0, 0.0),
+            ImageRendering::Auto,
+            AlphaType::PremultipliedAlpha,
+            image,
+        );
+
+        let mut txn = Transaction::new();
+
+        txn.set_display_list(
+            Epoch(0),
+            Some(ColorF::new(1.0, 1.0, 1.0, 1.0)),
+            layout_size,
+            builder.finalize(),
+            false,
+        );
+        txn.generate_frame();
+
+        self.wrench.api.send_transaction(self.wrench.document_id, txn);
+
+        let pixels0 = self.render_and_get_pixels(window_rect);
+
+        // 2. capture it
+
+        self.wrench.api.save_capture(path.into(), CaptureBits::all());
+        self.rx.recv().unwrap();
+
+        // 3. set a different scene
+
+        builder = DisplayListBuilder::new(self.wrench.root_pipeline_id, layout_size);
+
+        let mut txn = Transaction::new();
+        txn.set_display_list(
+            Epoch(1),
+            Some(ColorF::new(1.0, 0.0, 0.0, 1.0)),
+            layout_size,
+            builder.finalize(),
+            false,
+        );
+        self.wrench.api.send_transaction(self.wrench.document_id, txn);
+
+        // 4. load the first one
+
+        let mut documents = self.wrench.api.load_capture(path.into());
+        let captured = documents.swap_remove(0);
+
+        // 5. render the built frame and compare
+        let pixels1 = self.render_and_get_pixels(window_rect);
+        assert!(pixels0 == pixels1);
+
+        // 6. rebuild the scene and compare again
+        let mut txn = Transaction::new();
+        txn.set_root_pipeline(captured.root_pipeline_id.unwrap());
+        txn.generate_frame();
+        self.wrench.api.send_transaction(captured.document_id, txn);
+        let pixels2 = self.render_and_get_pixels(window_rect);
+        assert!(pixels0 == pixels2);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/gfx/wrench/src/reftest.rs
@@ -0,0 +1,495 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use WindowWrapper;
+use base64;
+use image::load as load_piston_image;
+use image::png::PNGEncoder;
+use image::{ColorType, ImageFormat};
+use parse_function::parse_function;
+use png::save_flipped;
+use std::cmp;
+use std::fmt::{Display, Error, Formatter};
+use std::fs::File;
+use std::io::{BufRead, BufReader};
+use std::path::{Path, PathBuf};
+use std::sync::mpsc::Receiver;
+use webrender::RendererStats;
+use webrender::api::*;
+use wrench::{Wrench, WrenchThing};
+use yaml_frame_reader::YamlFrameReader;
+
+#[cfg(target_os = "windows")]
+const PLATFORM: &str = "win";
+#[cfg(target_os = "linux")]
+const PLATFORM: &str = "linux";
+#[cfg(target_os = "macos")]
+const PLATFORM: &str = "mac";
+#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
+const PLATFORM: &str = "other";
+
+const OPTION_DISABLE_SUBPX: &str = "disable-subpixel";
+const OPTION_DISABLE_AA: &str = "disable-aa";
+const OPTION_DISABLE_DUAL_SOURCE_BLENDING: &str = "disable-dual-source-blending";
+
+pub struct ReftestOptions {
+    // These override values that are lower.
+    pub allow_max_difference: usize,
+    pub allow_num_differences: usize,
+}
+
+impl ReftestOptions {
+    pub fn default() -> Self {
+        ReftestOptions {
+            allow_max_difference: 0,
+            allow_num_differences: 0,
+        }
+    }
+}
+
+pub enum ReftestOp {
+    Equal,
+    NotEqual,
+}
+
+impl Display for ReftestOp {
+    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
+        write!(
+            f,
+            "{}",
+            match *self {
+                ReftestOp::Equal => "==".to_owned(),
+                ReftestOp::NotEqual => "!=".to_owned(),
+            }
+        )
+    }
+}
+
+pub struct Reftest {
+    op: ReftestOp,
+    test: PathBuf,
+    reference: PathBuf,
+    font_render_mode: Option<FontRenderMode>,
+    max_difference: usize,
+    num_differences: usize,
+    expected_draw_calls: Option<usize>,
+    expected_alpha_targets: Option<usize>,
+    expected_color_targets: Option<usize>,
+    disable_dual_source_blending: bool,
+    zoom_factor: f32,
+}
+
+impl Display for Reftest {
+    fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
+        write!(
+            f,
+            "{} {} {}",
+            self.test.display(),
+            self.op,
+            self.reference.display()
+        )
+    }
+}
+
+struct ReftestImage {
+    data: Vec<u8>,
+    size: DeviceUintSize,
+}
+enum ReftestImageComparison {
+    Equal,
+    NotEqual {
+        max_difference: usize,
+        count_different: usize,
+    },
+}
+
+impl ReftestImage {
+    fn compare(&self, other: &ReftestImage) -> ReftestImageComparison {
+        assert_eq!(self.size, other.size);
+        assert_eq!(self.data.len(), other.data.len());
+        assert_eq!(self.data.len() % 4, 0);
+
+        let mut count = 0;
+        let mut max = 0;
+
+        for (a, b) in self.data.chunks(4).zip(other.data.chunks(4)) {
+            if a != b {
+                let pixel_max = a.iter()
+                    .zip(b.iter())
+                    .map(|(x, y)| (*x as isize - *y as isize).abs() as usize)
+                    .max()
+                    .unwrap();
+
+                count += 1;
+                max = cmp::max(max, pixel_max);
+            }
+        }
+
+        if count != 0 {
+            ReftestImageComparison::NotEqual {
+                max_difference: max,
+                count_different: count,
+            }
+        } else {
+            ReftestImageComparison::Equal
+        }
+    }
+
+    fn create_data_uri(mut self) -> String {
+        let width = self.size.width;
+        let height = self.size.height;
+
+        // flip image vertically (texture is upside down)
+        let orig_pixels = self.data.clone();
+        let stride = width as usize * 4;
+        for y in 0 .. height as usize {
+            let dst_start = y * stride;
+            let src_start = (height as usize - y - 1) * stride;
+            let src_slice = &orig_pixels[src_start .. src_start + stride];
+            (&mut self.data[dst_start .. dst_start + stride])
+                .clone_from_slice(&src_slice[.. stride]);
+        }
+
+        let mut png: Vec<u8> = vec![];
+        {
+            let encoder = PNGEncoder::new(&mut png);
+            encoder
+                .encode(&self.data[..], width, height, ColorType::RGBA(8))
+                .expect("Unable to encode PNG!");
+        }
+        let png_base64 = base64::encode(&png);
+        format!("data:image/png;base64,{}", png_base64)
+    }
+}
+
+struct ReftestManifest {
+    reftests: Vec<Reftest>,
+}
+impl ReftestManifest {
+    fn new(manifest: &Path, options: &ReftestOptions) -> ReftestManifest {
+        let dir = manifest.parent().unwrap();
+        let f =
+            File::open(manifest).expect(&format!("couldn't open manifest: {}", manifest.display()));
+        let file = BufReader::new(&f);
+
+        let mut reftests = Vec::new();
+
+        for line in file.lines() {
+            let l = line.unwrap();
+
+            // strip the comments
+            let s = &l[0 .. l.find('#').unwrap_or(l.len())];
+            let s = s.trim();
+            if s.is_empty() {
+                continue;
+            }
+
+            let tokens: Vec<&str> = s.split_whitespace().collect();
+
+            let mut max_difference = 0;
+            let mut max_count = 0;
+            let mut op = ReftestOp::Equal;
+            let mut font_render_mode = None;
+            let mut expected_color_targets = None;
+            let mut expected_alpha_targets = None;
+            let mut expected_draw_calls = None;
+            let mut disable_dual_source_blending = false;
+            let mut zoom_factor = 1.0;
+
+            for (i, token) in tokens.iter().enumerate() {
+                match *token {
+                    "include" => {
+                        assert!(i == 0, "include must be by itself");
+                        let include = dir.join(tokens[1]);
+
+                        reftests.append(
+                            &mut ReftestManifest::new(include.as_path(), options).reftests,
+                        );
+
+                        break;
+                    }
+                    platform if platform.starts_with("platform") => {
+                        let (_, args, _) = parse_function(platform);
+                        if !args.iter().any(|arg| arg == &PLATFORM) {
+                            // Skip due to platform not matching
+                            break;
+                        }
+                    }
+                    function if function.starts_with("zoom") => {
+                        let (_, args, _) = parse_function(function);
+                        zoom_factor = args[0].parse().unwrap();
+                    }
+                    function if function.starts_with("fuzzy") => {
+                        let (_, args, _) = parse_function(function);
+                        max_difference = args[0].parse().unwrap();
+                        max_count = args[1].parse().unwrap();
+                    }
+                    function if function.starts_with("draw_calls") => {
+                        let (_, args, _) = parse_function(function);
+                        expected_draw_calls = Some(args[0].parse().unwrap());
+                    }
+                    function if function.starts_with("alpha_targets") => {
+                        let (_, args, _) = parse_function(function);
+                        expected_alpha_targets = Some(args[0].parse().unwrap());
+                    }
+                    function if function.starts_with("color_targets") => {
+                        let (_, args, _) = parse_function(function);
+                        expected_color_targets = Some(args[0].parse().unwrap());
+                    }
+                    options if options.starts_with("options") => {
+                        let (_, args, _) = parse_function(options);
+                        if args.iter().any(|arg| arg == &OPTION_DISABLE_SUBPX) {
+                            font_render_mode = Some(FontRenderMode::Alpha);
+                        }
+                        if args.iter().any(|arg| arg == &OPTION_DISABLE_AA) {
+                            font_render_mode = Some(FontRenderMode::Mono);
+                        }
+                        if args.iter().any(|arg| arg == &OPTION_DISABLE_DUAL_SOURCE_BLENDING) {
+                            disable_dual_source_blending = true;
+                        }
+                    }
+                    "==" => {
+                        op = ReftestOp::Equal;
+                    }
+                    "!=" => {
+                        op = ReftestOp::NotEqual;
+                    }
+                    _ => {
+                        reftests.push(Reftest {
+                            op,
+                            test: dir.join(tokens[i + 0]),
+                            reference: dir.join(tokens[i + 1]),
+                            font_render_mode,
+                            max_difference: cmp::max(max_difference, options.allow_max_difference),
+                            num_differences: cmp::max(max_count, options.allow_num_differences),
+                            expected_draw_calls,
+                            expected_alpha_targets,
+                            expected_color_targets,
+                            disable_dual_source_blending,
+                            zoom_factor,
+                        });
+
+                        break;
+                    }
+                }
+            }
+        }
+
+        ReftestManifest { reftests: reftests }
+    }
+
+    fn find(&self, prefix: &Path) -> Vec<&Reftest> {
+        self.reftests
+            .iter()
+            .filter(|x| {
+                x.test.starts_with(prefix) || x.reference.starts_with(prefix)
+            })
+            .collect()
+    }
+}
+
+pub struct ReftestHarness<'a> {
+    wrench: &'a mut Wrench,
+    window: &'a mut WindowWrapper,
+    rx: Receiver<()>,
+}
+impl<'a> ReftestHarness<'a> {
+    pub fn new(wrench: &'a mut Wrench, window: &'a mut WindowWrapper, rx: Receiver<()>) -> Self {
+        ReftestHarness { wrench, window, rx }
+    }
+
+    pub fn run(mut self, base_manifest: &Path, reftests: Option<&Path>, options: &ReftestOptions) {
+        let manifest = ReftestManifest::new(base_manifest, options);
+        let reftests = manifest.find(reftests.unwrap_or(&PathBuf::new()));
+
+        let mut total_passing = 0;
+        let mut failing = Vec::new();
+
+        for t in reftests {
+            if self.run_reftest(t) {
+                total_passing += 1;
+            } else {
+                failing.push(t);
+            }
+        }
+
+        println!(
+            "REFTEST INFO | {} passing, {} failing",
+            total_passing,
+            failing.len()
+        );
+
+        if !failing.is_empty() {
+            println!("\nReftests with unexpected results:");
+
+            for reftest in &failing {
+                println!("\t{}", reftest);
+            }
+        }
+
+        // panic here so that we fail CI
+        assert!(failing.is_empty());
+    }
+
+    fn run_reftest(&mut self, t: &Reftest) -> bool {
+        println!("REFTEST {}", t);
+
+        self.wrench.set_page_zoom(ZoomFactor::new(t.zoom_factor));
+
+        if t.disable_dual_source_blending {
+            self.wrench
+                .api
+                .send_debug_cmd(
+                    DebugCommand::EnableDualSourceBlending(false)
+                );
+        }
+
+        let window_size = self.window.get_inner_size();
+        let reference = match t.reference.extension().unwrap().to_str().unwrap() {
+            "yaml" => {
+                let (reference, _) = self.render_yaml(
+                    t.reference.as_path(),
+                    window_size,
+                    t.font_render_mode,
+                );
+                reference
+            }
+            "png" => {
+                self.load_image(t.reference.as_path(), ImageFormat::PNG)
+            }
+            other => panic!("Unknown reftest extension: {}", other),
+        };
+
+        // the reference can be smaller than the window size,
+        // in which case we only compare the intersection
+        let (test, stats) = self.render_yaml(
+            t.test.as_path(),
+            reference.size,
+            t.font_render_mode,
+        );
+
+        if t.disable_dual_source_blending {
+            self.wrench
+                .api
+                .send_debug_cmd(
+                    DebugCommand::EnableDualSourceBlending(true)
+                );
+        }
+
+        let comparison = test.compare(&reference);
+
+        if let Some(expected_draw_calls) = t.expected_draw_calls {
+            if expected_draw_calls != stats.total_draw_calls {
+                println!("REFTEST TEST-UNEXPECTED-FAIL | {}/{} | expected_draw_calls",
+                    stats.total_draw_calls,
+                    expected_draw_calls
+                );
+                println!("REFTEST TEST-END | {}", t);
+                return false;
+            }
+        }
+        if let Some(expected_alpha_targets) = t.expected_alpha_targets {
+            if expected_alpha_targets != stats.alpha_target_count {
+                println!("REFTEST TEST-UNEXPECTED-FAIL | {}/{} | alpha_target_count",
+                    stats.alpha_target_count,
+                    expected_alpha_targets
+                );
+                println!("REFTEST TEST-END | {}", t);
+                return false;
+            }
+        }
+        if let Some(expected_color_targets) = t.expected_color_targets {
+            if expected_color_targets != stats.color_target_count {
+                println!("REFTEST TEST-UNEXPECTED-FAIL | {}/{} | color_target_count",
+                    stats.color_target_count,
+                    expected_color_targets
+                );
+                println!("REFTEST TEST-END | {}", t);
+                return false;
+            }
+        }
+
+        match (&t.op, comparison) {
+            (&ReftestOp::Equal, ReftestImageComparison::Equal) => true,
+            (
+                &ReftestOp::Equal,
+                ReftestImageComparison::NotEqual {
+                    max_difference,
+                    count_different,
+                },
+            ) => if max_difference > t.max_difference || count_different > t.num_differences {
+                println!(
+                    "{} | {} | {}: {}, {}: {}",
+                    "REFTEST TEST-UNEXPECTED-FAIL",
+                    t,
+                    "image comparison, max difference",
+                    max_difference,
+                    "number of differing pixels",
+                    count_different
+                );
+                println!("REFTEST   IMAGE 1 (TEST): {}", test.create_data_uri());
+                println!(
+                    "REFTEST   IMAGE 2 (REFERENCE): {}",
+                    reference.create_data_uri()
+                );
+                println!("REFTEST TEST-END | {}", t);
+
+                false
+            } else {
+                true
+            },
+            (&ReftestOp::NotEqual, ReftestImageComparison::Equal) => {
+                println!("REFTEST TEST-UNEXPECTED-FAIL | {} | image comparison", t);
+                println!("REFTEST TEST-END | {}", t);
+
+                false
+            }
+            (&ReftestOp::NotEqual, ReftestImageComparison::NotEqual { .. }) => true,
+        }
+    }
+
+    fn load_image(&mut self, filename: &Path, format: ImageFormat) -> ReftestImage {
+        let file = BufReader::new(File::open(filename).unwrap());
+        let img_raw = load_piston_image(file, format).unwrap();
+        let img = img_raw.flipv().to_rgba();
+        let size = img.dimensions();
+        ReftestImage {
+            data: img.into_raw(),
+            size: DeviceUintSize::new(size.0, size.1),
+        }
+    }
+
+    fn render_yaml(
+        &mut self,
+        filename: &Path,
+        size: DeviceUintSize,
+        font_render_mode: Option<FontRenderMode>,
+    ) -> (ReftestImage, RendererStats) {
+        let mut reader = YamlFrameReader::new(filename);
+        reader.set_font_render_mode(font_render_mode);
+        reader.do_frame(self.wrench);
+
+        // wait for the frame
+        self.rx.recv().unwrap();
+        let stats = self.wrench.render();
+
+        let window_size = self.window.get_inner_size();
+        assert!(size.width <= window_size.width && size.height <= window_size.height);
+
+        // taking the bottom left sub-rectangle
+        let rect = DeviceUintRect::new(DeviceUintPoint::new(0, window_size.height - size.height), size);
+        let pixels = self.wrench.renderer.read_pixels_rgba8(rect);
+        self.window.swap_buffers();
+
+        let write_debug_images = false;
+        if write_debug_images {
+            let debug_path = filename.with_extension("yaml.png");
+            save_flipped(debug_path, pixels.clone(), size);
+        }
+
+        reader.deinit(self.wrench);
+
+        (ReftestImage { data: pixels, size }, stats)
+    }
+}
new file mode 100644
--- /dev/null
+++ b/gfx/wrench/src/ron_frame_writer.rs
@@ -0,0 +1,195 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use ron;
+use std::collections::HashMap;
+use std::io::Write;
+use std::path::{Path, PathBuf};
+use std::{fmt, fs};
+use super::CURRENT_FRAME_NUMBER;
+use webrender;
+use webrender::api::*;
+use webrender::api::channel::Payload;
+
+enum CachedFont {
+    Native(NativeFontHandle),
+    Raw(Option<Vec<u8>>, u32, Option<PathBuf>),
+}
+
+struct CachedImage {
+    width: u32,
+    height: u32,
+    format: ImageFormat,
+    bytes: Option<Vec<u8>>,
+    path: Option<PathBuf>,
+}
+
+pub struct RonFrameWriter {
+    frame_base: PathBuf,
+    images: HashMap<ImageKey, CachedImage>,
+    fonts: HashMap<FontKey, CachedFont>,
+
+    last_frame_written: u32,
+
+    dl_descriptor: Option<BuiltDisplayListDescriptor>,
+}
+
+impl RonFrameWriter {
+    pub fn new(path: &Path) -> Self {
+        let mut rsrc_base = path.to_owned();
+        rsrc_base.push("res");
+        fs::create_dir_all(&rsrc_base).ok();
+
+        RonFrameWriter {
+            frame_base: path.to_owned(),
+            images: HashMap::new(),
+            fonts: HashMap::new(),
+
+            dl_descriptor: None,
+
+            last_frame_written: u32::max_value(),
+        }
+    }
+
+    pub fn begin_write_display_list(
+        &mut self,
+        _: &Epoch,
+        _: &PipelineId,
+        _: &Option<ColorF>,
+        _: &LayoutSize,
+        display_list: &BuiltDisplayListDescriptor,
+    ) {
+        unsafe {
+            if CURRENT_FRAME_NUMBER == self.last_frame_written {
+                return;
+            }
+            self.last_frame_written = CURRENT_FRAME_NUMBER;
+        }
+
+        self.dl_descriptor = Some(display_list.clone());
+    }
+
+    pub fn finish_write_display_list(&mut self, _frame: u32, data: &[u8]) {
+        let payload = Payload::from_data(data);
+        let dl_desc = self.dl_descriptor.take().unwrap();
+
+        let dl = BuiltDisplayList::from_data(payload.display_list_data, dl_desc);
+
+        let mut frame_file_name = self.frame_base.clone();
+        let current_shown_frame = unsafe { CURRENT_FRAME_NUMBER };
+        frame_file_name.push(format!("frame-{}.ron", current_shown_frame));
+
+        let mut file = fs::File::create(&frame_file_name).unwrap();
+
+        let s = ron::ser::to_string_pretty(&dl, Default::default()).unwrap();
+        file.write_all(&s.into_bytes()).unwrap();
+        file.write_all(b"\n").unwrap();
+    }
+
+    fn update_resources(&mut self, updates: &ResourceUpdates) {
+        for update in &updates.updates {
+            match *update {
+                ResourceUpdate::AddImage(ref img) => {
+                    let bytes = match img.data {
+                        ImageData::Raw(ref v) => (**v).clone(),
+                        ImageData::External(_) | ImageData::Blob(_) => {
+                            return;
+                        }
+                    };
+                    self.images.insert(
+                        img.key,
+                        CachedImage {
+                            width: img.descriptor.width,
+                            height: img.descriptor.height,
+                            format: img.descriptor.format,
+                            bytes: Some(bytes),
+                            path: None,
+                        },
+                    );
+                }
+                ResourceUpdate::UpdateImage(ref img) => {
+                    if let Some(ref mut data) = self.images.get_mut(&img.key) {
+                        assert_eq!(data.width, img.descriptor.width);
+                        assert_eq!(data.height, img.descriptor.height);
+                        assert_eq!(data.format, img.descriptor.format);
+
+                        if let ImageData::Raw(ref bytes) = img.data {
+                            data.path = None;
+                            data.bytes = Some((**bytes).clone());
+                        } else {
+                            // Other existing image types only make sense within the gecko integration.
+                            println!(
+                                "Wrench only supports updating buffer images ({}).",
+                                "ignoring update commands"
+                            );
+                        }
+                    }
+                }
+                ResourceUpdate::DeleteImage(img) => {
+                    self.images.remove(&img);
+                }
+                ResourceUpdate::AddFont(ref font) => match font {
+                    &AddFont::Raw(key, ref bytes, index) => {
+                        self.fonts
+                            .insert(key, CachedFont::Raw(Some(bytes.clone()), index, None));
+                    }
+                    &AddFont::Native(key, ref handle) => {
+                        self.fonts.insert(key, CachedFont::Native(handle.clone()));
+                    }
+                },
+                ResourceUpdate::DeleteFont(_) => {}
+                ResourceUpdate::AddFontInstance(_) => {}
+                ResourceUpdate::DeleteFontInstance(_) => {}
+            }
+        }
+    }
+}
+
+impl fmt::Debug for RonFrameWriter {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "RonFrameWriter")
+    }
+}
+
+impl webrender::ApiRecordingReceiver for RonFrameWriter {
+    fn write_msg(&mut self, _: u32, msg: &ApiMsg) {
+        match *msg {
+            ApiMsg::UpdateResources(ref updates) => self.update_resources(updates),
+            ApiMsg::UpdateDocument(_, ref doc_msgs) => {
+                for doc_msg in doc_msgs {
+                    match *doc_msg {
+                        DocumentMsg::UpdateResources(ref resources) => {
+                            self.update_resources(resources);
+                        }
+                        DocumentMsg::SetDisplayList {
+                            ref epoch,
+                            ref pipeline_id,
+                            ref background,
+                            ref viewport_size,
+                            ref list_descriptor,
+                            ..
+                        } => {
+                            self.begin_write_display_list(
+                                epoch,
+                                pipeline_id,
+                                background,
+                                viewport_size,
+                                list_descriptor,
+                            );
+                        }
+                        _ => {}
+                    }
+                }
+            }
+            ApiMsg::CloneApi(..) => {}
+            _ => {}
+        }
+    }
+
+    fn write_payload(&mut self, frame: u32, data: &[u8]) {
+        if self.dl_descriptor.is_some() {
+            self.finish_write_display_list(frame, data);
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/gfx/wrench/src/scene.rs
@@ -0,0 +1,134 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use std::collections::HashMap;
+use webrender::api::{BuiltDisplayList, ColorF, Epoch};
+use webrender::api::{LayerSize, PipelineId};
+use webrender::api::{PropertyBinding, PropertyBindingId, LayoutTransform, DynamicProperties};
+
+/// Stores a map of the animated property bindings for the current display list. These
+/// can be used to animate the transform and/or opacity of a display list without
+/// re-submitting the display list itself.
+#[derive(Default)]
+pub struct SceneProperties {
+    transform_properties: HashMap<PropertyBindingId, LayoutTransform>,
+    float_properties: HashMap<PropertyBindingId, f32>,
+}
+
+impl SceneProperties {
+    /// Set the current property list for this display list.
+    pub fn set_properties(&mut self, properties: &DynamicProperties) {
+        self.transform_properties.clear();
+        self.float_properties.clear();
+
+        for property in &properties.transforms {
+            self.transform_properties
+                .insert(property.key.id, property.value);
+        }
+
+        for property in &properties.floats {
+            self.float_properties
+                .insert(property.key.id, property.value);
+        }
+    }
+
+    /// Get the current value for a transform property.
+    pub fn resolve_layout_transform(
+        &self,
+        property: &Option<PropertyBinding<LayoutTransform>>,
+    ) -> LayoutTransform {
+        let property = match *property {
+            Some(property) => property,
+            None => return LayoutTransform::identity(),
+        };
+
+        match property {
+            PropertyBinding::Value(matrix) => matrix,
+            PropertyBinding::Binding(ref key) => self.transform_properties
+                .get(&key.id)
+                .cloned()
+                .unwrap_or_else(|| {
+                    println!("Property binding {:?} has an invalid value.", key);
+                    LayoutTransform::identity()
+                }),
+        }
+    }
+
+    /// Get the current value for a float property.
+    pub fn resolve_float(&self, property: &PropertyBinding<f32>, default_value: f32) -> f32 {
+        match *property {
+            PropertyBinding::Value(value) => value,
+            PropertyBinding::Binding(ref key) => self.float_properties
+                .get(&key.id)
+                .cloned()
+                .unwrap_or_else(|| {
+                    println!("Property binding {:?} has an invalid value.", key);
+                    default_value
+                }),
+        }
+    }
+}
+
+/// A representation of the layout within the display port for a given document or iframe.
+#[derive(Debug)]
+pub struct ScenePipeline {
+    pub epoch: Epoch,
+    pub viewport_size: LayerSize,
+    pub background_color: Option<ColorF>,
+}
+
+/// A complete representation of the layout bundling visible pipelines together.
+pub struct Scene {
+    pub properties: SceneProperties,
+    pub root_pipeline_id: Option<PipelineId>,
+    pub pipeline_map: HashMap<PipelineId, ScenePipeline>,
+    pub display_lists: HashMap<PipelineId, BuiltDisplayList>,
+}
+
+impl Scene {
+    pub fn new() -> Scene {
+        Scene {
+            properties: SceneProperties::default(),
+            root_pipeline_id: None,
+            pipeline_map: HashMap::default(),
+            display_lists: HashMap::default(),
+        }
+    }
+
+    pub fn set_root_pipeline_id(&mut self, pipeline_id: PipelineId) {
+        self.root_pipeline_id = Some(pipeline_id);
+    }
+
+    pub fn remove_pipeline(&mut self, pipeline_id: &PipelineId) {
+        if self.root_pipeline_id == Some(*pipeline_id) {
+            self.root_pipeline_id = None;
+        }
+        self.pipeline_map.remove(pipeline_id);
+        self.display_lists.remove(pipeline_id);
+    }
+
+    pub fn begin_display_list(
+        &mut self,
+        pipeline_id: &PipelineId,
+        epoch: &Epoch,
+        background_color: &Option<ColorF>,
+        viewport_size: &LayerSize,
+    ) {
+        let new_pipeline = ScenePipeline {
+            epoch: epoch.clone(),
+            viewport_size: viewport_size.clone(),
+            background_color: background_color.clone(),
+        };
+
+        self.pipeline_map.insert(pipeline_id.clone(), new_pipeline);
+    }
+
+    pub fn finish_display_list(
+        &mut self,
+        pipeline_id: PipelineId,
+        built_display_list: BuiltDisplayList,
+    ) {
+        self.display_lists.insert(pipeline_id, built_display_list);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/gfx/wrench/src/wrench.rs
@@ -0,0 +1,595 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+use app_units::Au;
+use blob;
+use crossbeam::sync::chase_lev;
+#[cfg(windows)]
+use dwrote;
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+use font_loader::system_fonts;
+use glutin::WindowProxy;
+use json_frame_writer::JsonFrameWriter;
+use ron_frame_writer::RonFrameWriter;
+use std::collections::HashMap;
+use std::path::PathBuf;
+use std::sync::{Arc, Mutex};
+use time;
+use webrender;
+use webrender::api::*;
+use webrender::{DebugFlags, RendererStats};
+use yaml_frame_writer::YamlFrameWriterReceiver;
+use {WindowWrapper, BLACK_COLOR, WHITE_COLOR};
+
+// TODO(gw): This descriptor matches what we currently support for fonts
+//           but is quite a mess. We should at least document and
+//           use better types for things like the style and stretch.
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
+pub enum FontDescriptor {
+    Path { path: PathBuf, font_index: u32 },
+    Family { name: String },
+    Properties {
+        family: String,
+        weight: u32,
+        style: u32,
+        stretch: u32,
+    },
+}
+
+pub enum SaveType {
+    Yaml,
+    Json,
+    Ron,
+    Binary,
+}
+
+struct NotifierData {
+    window_proxy: Option<WindowProxy>,
+    frames_notified: u32,
+    timing_receiver: chase_lev::Stealer<time::SteadyTime>,
+    verbose: bool,
+}
+
+impl NotifierData {
+    fn new(
+        window_proxy: Option<WindowProxy>,
+        timing_receiver: chase_lev::Stealer<time::SteadyTime>,
+        verbose: bool,
+    ) -> Self {
+        NotifierData {
+            window_proxy,
+            frames_notified: 0,
+            timing_receiver,
+            verbose,
+        }
+    }
+}
+
+struct Notifier(Arc<Mutex<NotifierData>>);
+
+impl RenderNotifier for Notifier {
+    fn clone(&self) -> Box<RenderNotifier> {
+        Box::new(Notifier(self.0.clone()))
+    }
+
+    fn wake_up(&self) {
+        let mut data = self.0.lock();
+        let data = data.as_mut().unwrap();
+        match data.timing_receiver.steal() {
+            chase_lev::Steal::Data(last_timing) => {
+                data.frames_notified += 1;
+                if data.verbose && data.frames_notified == 600 {
+                    let elapsed = time::SteadyTime::now() - last_timing;
+                    println!(
+                        "frame latency (consider queue depth here): {:3.6} ms",
+                        elapsed.num_microseconds().unwrap() as f64 / 1000.
+                    );
+                    data.frames_notified = 0;
+                }
+            }
+            _ => {
+                println!("Notified of frame, but no frame was ready?");
+            }
+        }
+        if let Some(ref window_proxy) = data.window_proxy {
+            #[cfg(not(target_os = "android"))]
+            window_proxy.wakeup_event_loop();
+        }
+    }
+
+    fn new_document_ready(&self, _: DocumentId, scrolled: bool, _composite_needed: bool) {
+        if scrolled {
+            let data = self.0.lock();
+            if let Some(ref window_proxy) = data.unwrap().window_proxy {
+                #[cfg(not(target_os = "android"))]
+                window_proxy.wakeup_event_loop();
+            }
+        } else {
+            self.wake_up();
+        }
+    }
+}
+
+pub trait WrenchThing {
+    fn next_frame(&mut self);
+    fn prev_frame(&mut self);
+    fn do_frame(&mut self, &mut Wrench) -> u32;
+    fn queue_frames(&self) -> u32 {
+        0
+    }
+}
+
+impl WrenchThing for CapturedDocument {
+    fn next_frame(&mut self) {}
+    fn prev_frame(&mut self) {}
+    fn do_frame(&mut self, wrench: &mut Wrench) -> u32 {
+        match self.root_pipeline_id.take() {
+            Some(root_pipeline_id) => {
+                // skip the first frame - to not overwrite the loaded one
+                let mut txn = Transaction::new();
+                txn.set_root_pipeline(root_pipeline_id);
+                wrench.api.send_transaction(self.document_id, txn);
+            }
+            None => {
+                wrench.refresh();
+            }
+        }
+        0
+    }
+}
+
+pub struct Wrench {
+    window_size: DeviceUintSize,
+    device_pixel_ratio: f32,
+    page_zoom_factor: ZoomFactor,
+
+    pub renderer: webrender::Renderer,
+    pub api: RenderApi,
+    pub document_id: DocumentId,
+    pub root_pipeline_id: PipelineId,
+
+    window_title_to_set: Option<String>,
+
+    graphics_api: webrender::GraphicsApiInfo,
+
+    pub rebuild_display_lists: bool,
+    pub verbose: bool,
+
+    pub frame_start_sender: chase_lev::Worker<time::SteadyTime>,
+
+    pub callbacks: Arc<Mutex<blob::BlobCallbacks>>,
+}
+
+impl Wrench {
+    pub fn new(
+        window: &mut WindowWrapper,
+        shader_override_path: Option<PathBuf>,
+        dp_ratio: f32,
+        save_type: Option<SaveType>,
+        size: DeviceUintSize,
+        do_rebuild: bool,
+        no_subpixel_aa: bool,
+        debug: bool,
+        verbose: bool,
+        no_scissor: bool,
+        no_batch: bool,
+        precache_shaders: bool,
+        disable_dual_source_blending: bool,
+        zoom_factor: f32,
+        notifier: Option<Box<RenderNotifier>>,
+    ) -> Self {
+        println!("Shader override path: {:?}", shader_override_path);
+
+        let recorder = save_type.map(|save_type| match save_type {
+            SaveType::Yaml => Box::new(
+                YamlFrameWriterReceiver::new(&PathBuf::from("yaml_frames")),
+            ) as Box<webrender::ApiRecordingReceiver>,
+            SaveType::Json => Box::new(JsonFrameWriter::new(&PathBuf::from("json_frames"))) as
+                Box<webrender::ApiRecordingReceiver>,
+            SaveType::Ron => Box::new(RonFrameWriter::new(&PathBuf::from("ron_frames"))) as
+                Box<webrender::ApiRecordingReceiver>,
+            SaveType::Binary => Box::new(webrender::BinaryRecorder::new(
+                &PathBuf::from("wr-record.bin"),
+            )) as Box<webrender::ApiRecordingReceiver>,
+        });
+
+        let mut debug_flags = DebugFlags::default();
+        debug_flags.set(DebugFlags::DISABLE_BATCHING, no_batch);
+        let callbacks = Arc::new(Mutex::new(blob::BlobCallbacks::new()));
+
+        let opts = webrender::RendererOptions {
+            device_pixel_ratio: dp_ratio,
+            resource_override_path: shader_override_path,
+            recorder,
+            enable_subpixel_aa: !no_subpixel_aa,
+            debug,
+            debug_flags,
+            enable_clear_scissor: !no_scissor,
+            max_recorded_profiles: 16,
+            precache_shaders,
+            blob_image_renderer: Some(Box::new(blob::CheckerboardRenderer::new(callbacks.clone()))),
+            disable_dual_source_blending,
+            ..Default::default()
+        };
+
+        let proxy = window.create_window_proxy();
+        // put an Awakened event into the queue to kick off the first frame
+        if let Some(ref wp) = proxy {
+            #[cfg(not(target_os = "android"))]
+            wp.wakeup_event_loop();
+        }
+
+        let (timing_sender, timing_receiver) = chase_lev::deque();
+        let notifier = notifier.unwrap_or_else(|| {
+            let data = Arc::new(Mutex::new(NotifierData::new(proxy, timing_receiver, verbose)));
+            Box::new(Notifier(data))
+        });
+
+        let (renderer, sender) = webrender::Renderer::new(window.clone_gl(), notifier, opts).unwrap();
+        let api = sender.create_api();
+        let document_id = api.add_document(size, 0);
+
+        let graphics_api = renderer.get_graphics_api_info();
+        let zoom_factor = ZoomFactor::new(zoom_factor);
+
+        let mut wrench = Wrench {
+            window_size: size,
+
+            renderer,
+            api,
+            document_id,
+            window_title_to_set: None,
+
+            rebuild_display_lists: do_rebuild,
+            verbose,
+            device_pixel_ratio: dp_ratio,
+            page_zoom_factor: zoom_factor,
+
+            root_pipeline_id: PipelineId(0, 0),
+
+            graphics_api,
+            frame_start_sender: timing_sender,
+
+            callbacks,
+        };
+
+        wrench.set_page_zoom(zoom_factor);
+        wrench.set_title("start");
+        let mut txn = Transaction::new();
+        txn.set_root_pipeline(wrench.root_pipeline_id);
+        wrench.api.send_transaction(wrench.document_id, txn);
+
+        wrench
+    }
+
+    pub fn get_page_zoom(&self) -> ZoomFactor {
+        self.page_zoom_factor
+    }
+
+    pub fn set_page_zoom(&mut self, zoom_factor: ZoomFactor) {
+        self.page_zoom_factor = zoom_factor;
+        let mut txn = Transaction::new();
+        txn.set_page_zoom(self.page_zoom_factor);
+        self.api.send_transaction(self.document_id, txn);
+        self.set_title("");
+    }
+
+    pub fn layout_simple_ascii(
+        &mut self,
+        font_key: FontKey,
+        instance_key: FontInstanceKey,
+        render_mode: Option<FontRenderMode>,
+        text: &str,
+        size: Au,
+        origin: LayerPoint,
+        flags: FontInstanceFlags,
+    ) -> (Vec<u32>, Vec<LayerPoint>, LayoutRect) {
+        // Map the string codepoints to glyph indices in this font.
+        // Just drop any glyph that isn't present in this font.
+        let indices: Vec<u32> = self.api
+            .get_glyph_indices(font_key, text)
+            .iter()
+            .filter_map(|idx| *idx)
+            .collect();
+
+        let render_mode = render_mode.unwrap_or(<FontInstanceOptions as Default>::default().render_mode);
+        let subpx_dir = SubpixelDirection::Horizontal.limit_by(render_mode);
+
+        // Retrieve the metrics for each glyph.
+        let mut keys = Vec::new();
+        for glyph_index in &indices {
+            keys.push(GlyphKey::new(
+                *glyph_index,
+                LayerPoint::zero(),
+                render_mode,
+                subpx_dir,
+            ));
+        }
+        let metrics = self.api.get_glyph_dimensions(instance_key, keys);
+
+        let mut bounding_rect = LayoutRect::zero();
+        let mut positions = Vec::new();
+
+        let mut cursor = origin;
+        let direction = if flags.contains(FontInstanceFlags::TRANSPOSE) {
+            LayerVector2D::new(
+                0.0,
+                if flags.contains(FontInstanceFlags::FLIP_Y) { -1.0 } else { 1.0 },
+            )
+        } else {
+            LayerVector2D::new(
+                if flags.contains(FontInstanceFlags::FLIP_X) { -1.0 } else { 1.0 },
+                0.0,
+            )
+        };
+        for metric in metrics {
+            positions.push(cursor);
+
+            match metric {
+                Some(metric) => {
+                    let glyph_rect = LayoutRect::new(
+                        LayoutPoint::new(cursor.x + metric.left as f32, cursor.y - metric.top as f32),
+                        LayoutSize::new(metric.width as f32, metric.height as f32)
+                    );
+                    bounding_rect = bounding_rect.union(&glyph_rect);
+                    cursor += direction * metric.advance;
+                }
+                None => {
+                    // Extract the advances from the metrics. The get_glyph_dimensions API
+                    // has a limitation that it can't currently get dimensions for non-renderable
+                    // glyphs (e.g. spaces), so just use a rough estimate in that case.
+                    let space_advance = size.to_f32_px() / 3.0;
+                    cursor += direction * space_advance;
+                }
+            }
+        }
+
+        // The platform font implementations don't always handle
+        // the exact dimensions used when subpixel AA is enabled
+        // on glyphs. As a workaround, inflate the bounds by
+        // 2 pixels on either side, to give a slightly less
+        // tight fitting bounding rect.
+        let bounding_rect = bounding_rect.inflate(2.0, 2.0);
+
+        (indices, positions, bounding_rect)
+    }
+
+    pub fn set_title(&mut self, extra: &str) {
+        self.window_title_to_set = Some(format!(
+            "Wrench: {} ({}x zoom={}) - {} - {}",
+            extra,
+            self.device_pixel_ratio,
+            self.page_zoom_factor.get(),
+            self.graphics_api.renderer,
+            self.graphics_api.version
+        ));
+    }
+
+    pub fn take_title(&mut self) -> Option<String> {
+        self.window_title_to_set.take()
+    }
+
+    pub fn should_rebuild_display_lists(&self) -> bool {
+        self.rebuild_display_lists
+    }
+
+    pub fn window_size_f32(&self) -> LayoutSize {
+        LayoutSize::new(
+            self.window_size.width as f32,
+            self.window_size.height as f32,
+        )
+    }
+
+    #[cfg(target_os = "windows")]
+    pub fn font_key_from_native_handle(&mut self, descriptor: &NativeFontHandle) -> FontKey {
+        let key = self.api.generate_font_key();
+        let mut resources = ResourceUpdates::new();
+        resources.add_native_font(key, descriptor.clone());
+        self.api.update_resources(resources);
+        key
+    }
+
+    #[cfg(target_os = "windows")]
+    pub fn font_key_from_name(&mut self, font_name: &str) -> FontKey {
+        let system_fc = dwrote::FontCollection::system();
+        let family = system_fc.get_font_family_by_name(font_name).unwrap();
+        let font = family.get_first_matching_font(
+            dwrote::FontWeight::Regular,
+            dwrote::FontStretch::Normal,
+            dwrote::FontStyle::Normal,
+        );
+        let descriptor = font.to_descriptor();
+        self.font_key_from_native_handle(&descriptor)
+    }
+
+    #[cfg(target_os = "windows")]
+    pub fn font_key_from_properties(
+        &mut self,
+        family: &str,
+        weight: u32,
+        style: u32,
+        stretch: u32,
+    ) -> FontKey {
+        let weight = dwrote::FontWeight::from_u32(weight);
+        let style = dwrote::FontStyle::from_u32(style);
+        let stretch = dwrote::FontStretch::from_u32(stretch);
+
+        let desc = dwrote::FontDescriptor {
+            family_name: family.to_owned(),
+            weight,
+            style,
+            stretch,
+        };
+        self.font_key_from_native_handle(&desc)
+    }
+
+    #[cfg(any(target_os = "linux", target_os = "macos"))]
+    pub fn font_key_from_properties(
+        &mut self,
+        family: &str,
+        _weight: u32,
+        _style: u32,
+        _stretch: u32,
+    ) -> FontKey {
+        let property = system_fonts::FontPropertyBuilder::new()
+            .family(family)
+            .build();
+        let (font, index) = system_fonts::get(&property).unwrap();
+        self.font_key_from_bytes(font, index as u32)
+    }
+
+    #[cfg(unix)]
+    pub fn font_key_from_name(&mut self, font_name: &str) -> FontKey {
+        let property = system_fonts::FontPropertyBuilder::new()
+            .family(font_name)
+            .build();
+        let (font, index) = system_fonts::get(&property).unwrap();
+        self.font_key_from_bytes(font, index as u32)
+    }
+
+    #[cfg(target_os = "android")]
+    pub fn font_key_from_name(&mut self, font_name: &str) -> FontKey {
+        unimplemented!()
+    }
+
+    pub fn font_key_from_bytes(&mut self, bytes: Vec<u8>, index: u32) -> FontKey {
+        let key = self.api.generate_font_key();
+        let mut update = ResourceUpdates::new();
+        update.add_raw_font(key, bytes, index);
+        self.api.update_resources(update);
+        key
+    }
+
+    pub fn add_font_instance(&mut self,
+        font_key: FontKey,
+        size: Au,
+        flags: FontInstanceFlags,
+        render_mode: Option<FontRenderMode>,
+    ) -> FontInstanceKey {
+        let key = self.api.generate_font_instance_key();
+        let mut update = ResourceUpdates::new();
+        let mut options: FontInstanceOptions = Default::default();
+        options.flags |= flags;
+        if let Some(render_mode) = render_mode {
+            options.render_mode = render_mode;
+        }
+        update.add_font_instance(key, font_key, size, Some(options), None, Vec::new());
+        self.api.update_resources(update);
+        key
+    }
+
+    #[allow(dead_code)]
+    pub fn delete_font_instance(&mut self, key: FontInstanceKey) {
+        let mut update = ResourceUpdates::new();
+        update.delete_font_instance(key);
+        self.api.update_resources(update);
+    }
+
+    pub fn update(&mut self, dim: DeviceUintSize) {
+        if dim != self.window_size {
+            self.window_size = dim;
+        }
+    }
+
+    pub fn begin_frame(&mut self) {
+        self.frame_start_sender.push(time::SteadyTime::now());
+    }
+
+    pub fn send_lists(
+        &mut self,
+        frame_number: u32,
+        display_lists: Vec<(PipelineId, LayerSize, BuiltDisplayList)>,
+        scroll_offsets: &HashMap<ClipId, LayerPoint>,
+    ) {
+        let root_background_color = Some(ColorF::new(1.0, 1.0, 1.0, 1.0));
+
+        let mut txn = Transaction::new();
+        for display_list in display_lists {
+            txn.set_display_list(
+                Epoch(frame_number),
+                root_background_color,
+                self.window_size_f32(),
+                display_list,
+                false,
+            );
+        }
+        // TODO(nical) - Need to separate the set_display_list from the scrolling
+        // operations into separate transactions for mysterious -but probably related
+        // to the other comment below- reasons.
+        self.api.send_transaction(self.document_id, txn);
+
+        let mut txn = Transaction::new();
+        for (id, offset) in scroll_offsets {
+            txn.scroll_node_with_id(
+                *offset,
+                *id,
+                ScrollClamping::NoClamping,
+            );
+        }
+        // TODO(nical) - Wrench does not notify frames when there was scrolling
+        // in the transaction (See RenderNotifier implementations). If we don't
+        // generate a frame after scrolling, wrench just stops and some tests
+        // will time out.
+        // I suppose this was to avoid taking the snapshot after scrolling if
+        // there was other updates coming in a subsequent messages but it's very
+        // error-prone with transactions.
+        // For now just send two transactions to avoid the deadlock, but we should
+        // figure this out.
+        self.api.send_transaction(self.document_id, txn);
+
+        let mut txn = Transaction::new();
+        txn.generate_frame();
+        self.api.send_transaction(self.document_id, txn);
+    }
+
+    pub fn get_frame_profiles(
+        &mut self,
+    ) -> (Vec<webrender::CpuProfile>, Vec<webrender::GpuProfile>) {
+        self.renderer.get_frame_profiles()
+    }
+
+    pub fn render(&mut self) -> RendererStats {
+        self.renderer.update();
+        self.renderer
+            .render(self.window_size)
+            .expect("errors encountered during render!")
+    }
+
+    pub fn refresh(&mut self) {
+        self.begin_frame();
+        let mut txn = Transaction::new();
+        txn.generate_frame();
+        self.api.send_transaction(self.document_id, txn);
+    }
+
+    pub fn show_onscreen_help(&mut self) {
+        let help_lines = [
+            "Esc - Quit",
+            "H - Toggle help",
+            "R - Toggle recreating display items each frame",
+            "P - Toggle profiler",
+            "O - Toggle showing intermediate targets",
+            "I - Toggle showing texture caches",
+            "B - Toggle showing alpha primitive rects",
+            "S - Toggle compact profiler",
+            "Q - Toggle GPU queries for time and samples",
+            "M - Trigger memory pressure event",
+            "T - Save CPU profile to a file",
+            "C - Save a capture to captures/wrench/",
+        ];
+
+        let color_and_offset = [(*BLACK_COLOR, 2.0), (*WHITE_COLOR, 0.0)];
+        let dr = self.renderer.debug_renderer();
+
+        for ref co in &color_and_offset {
+            let x = self.device_pixel_ratio * (15.0 + co.1);
+            let mut y = self.device_pixel_ratio * (15.0 + co.1 + dr.line_height());
+            for ref line in &help_lines {
+                dr.add_text(x, y, line, co.0.into());
+                y += self.device_pixel_ratio * dr.line_height();
+            }
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/gfx/wrench/src/yaml_frame_reader.rs
@@ -0,0 +1,1510 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use app_units::Au;
+use clap;
+use euclid::SideOffsets2D;
+use image;
+use image::GenericImage;
+use parse_function::parse_function;
+use premultiply::premultiply;
+use std::collections::HashMap;
+use std::fs::File;
+use std::io::Read;
+use std::path::{Path, PathBuf};
+use webrender::api::*;
+use wrench::{FontDescriptor, Wrench, WrenchThing};
+use yaml_helper::{StringEnum, YamlHelper};
+use yaml_rust::{Yaml, YamlLoader};
+use {BLACK_COLOR, PLATFORM_DEFAULT_FACE_NAME, WHITE_COLOR};
+
+fn rsrc_path(item: &Yaml, aux_dir: &PathBuf) -> PathBuf {
+    let filename = item.as_str().unwrap();
+    let mut file = aux_dir.clone();
+    file.push(filename);
+    file
+}
+
+impl FontDescriptor {
+    fn from_yaml(item: &Yaml, aux_dir: &PathBuf) -> FontDescriptor {
+        if !item["family"].is_badvalue() {
+            FontDescriptor::Properties {
+                family: item["family"].as_str().unwrap().to_owned(),
+                weight: item["weight"].as_i64().unwrap_or(400) as u32,
+                style: item["style"].as_i64().unwrap_or(0) as u32,
+                stretch: item["stretch"].as_i64().unwrap_or(5) as u32,
+            }
+        } else if !item["font"].is_badvalue() {
+            let path = rsrc_path(&item["font"], aux_dir);
+            FontDescriptor::Path {
+                path,
+                font_index: item["font-index"].as_i64().unwrap_or(0) as u32,
+            }
+        } else {
+            FontDescriptor::Family {
+                name: PLATFORM_DEFAULT_FACE_NAME.clone(),
+            }
+        }
+    }
+}
+
+fn broadcast<T: Clone>(base_vals: &[T], num_items: usize) -> Vec<T> {
+    if base_vals.len() == num_items {
+        return base_vals.to_vec();
+    }
+
+    assert_eq!(
+        num_items % base_vals.len(),
+        0,
+        "Cannot broadcast {} elements into {}",
+        base_vals.len(),
+        num_items
+    );
+
+    let mut vals = vec![];
+    loop {
+        if vals.len() == num_items {
+            break;
+        }
+        vals.extend_from_slice(base_vals);
+    }
+    vals
+}
+
+fn generate_checkerboard_image(
+    border: u32,
+    tile_size: u32,
+    tile_count: u32
+) -> (ImageDescriptor, ImageData) {
+    let size = 2 * border + tile_size * tile_count;
+    let mut pixels = Vec::new();
+
+    for y in 0 .. size {
+        for x in 0 .. size {
+            if y < border || y >= (size-border) ||
+               x < border || x >= (size-border) {
+                pixels.push(0);
+                pixels.push(0);
+                pixels.push(0xff);
+                pixels.push(0xff);
+            } else {
+                let xon = ((x - border) % (2 * tile_size)) < tile_size;
+                let yon = ((y - border) % (2 * tile_size)) < tile_size;
+                let value = if xon ^ yon { 0xff } else { 0x7f };
+                pixels.push(value);
+                pixels.push(value);
+                pixels.push(value);
+                pixels.push(0xff);
+            }
+        }
+    }
+
+    (
+        ImageDescriptor::new(size, size, ImageFormat::BGRA8, true),
+        ImageData::new(pixels),
+    )
+}
+
+fn generate_xy_gradient_image(w: u32, h: u32) -> (ImageDescriptor, ImageData) {
+    let mut pixels = Vec::with_capacity((w * h * 4) as usize);
+    for y in 0 .. h {
+        for x in 0 .. w {
+            let grid = if x % 100 < 3 || y % 100 < 3 { 0.9 } else { 1.0 };
+            pixels.push((y as f32 / h as f32 * 255.0 * grid) as u8);
+            pixels.push(0);
+            pixels.push((x as f32 / w as f32 * 255.0 * grid) as u8);
+            pixels.push(255);
+        }
+    }
+
+    (
+        ImageDescriptor::new(w, h, ImageFormat::BGRA8, true),
+        ImageData::new(pixels),
+    )
+}
+
+fn generate_solid_color_image(
+    r: u8,
+    g: u8,
+    b: u8,
+    a: u8,
+    w: u32,
+    h: u32,
+) -> (ImageDescriptor, ImageData) {
+    let buf_size = (w * h * 4) as usize;
+    let mut pixels = Vec::with_capacity(buf_size);
+    // Unsafely filling the buffer is horrible. Unfortunately doing this idiomatically
+    // is terribly slow in debug builds to the point that reftests/image/very-big.yaml
+    // takes more than 20 seconds to run on a recent laptop.
+    unsafe {
+        pixels.set_len(buf_size);
+        let color: u32 = ::std::mem::transmute([b, g, r, a]);
+        let mut ptr: *mut u32 = ::std::mem::transmute(&mut pixels[0]);
+        let end = ptr.offset((w * h) as isize);
+        while ptr < end {
+            *ptr = color;
+            ptr = ptr.offset(1);
+        }
+    }
+
+    (
+        ImageDescriptor::new(w, h, ImageFormat::BGRA8, a == 255),
+        ImageData::new(pixels),
+    )
+}
+
+
+
+fn is_image_opaque(format: ImageFormat, bytes: &[u8]) -> bool {
+    match format {
+        ImageFormat::BGRA8 => {
+            let mut is_opaque = true;
+            for i in 0 .. (bytes.len() / 4) {
+                if bytes[i * 4 + 3] != 255 {
+                    is_opaque = false;
+                    break;
+                }
+            }
+            is_opaque
+        }
+        ImageFormat::RG8 => true,
+        ImageFormat::R8 => false,
+        ImageFormat::RGBAF32 => unreachable!(),
+    }
+}
+
+pub struct YamlFrameReader {
+    frame_built: bool,
+    yaml_path: PathBuf,
+    aux_dir: PathBuf,
+    frame_count: u32,
+
+    display_lists: Vec<(PipelineId, LayoutSize, BuiltDisplayList)>,
+    queue_depth: u32,
+
+    include_only: Vec<String>,
+
+    watch_source: bool,
+    list_resources: bool,
+
+    /// A HashMap of offsets which specify what scroll offsets particular
+    /// scroll layers should be initialized with.
+    scroll_offsets: HashMap<ClipId, LayerPoint>,
+
+    image_map: HashMap<(PathBuf, Option<i64>), (ImageKey, LayoutSize)>,
+
+    fonts: HashMap<FontDescriptor, FontKey>,
+    font_instances: HashMap<(FontKey, Au, FontInstanceFlags), FontInstanceKey>,
+    font_render_mode: Option<FontRenderMode>,
+
+    /// A HashMap that allows specifying a numeric id for clip and clip chains in YAML
+    /// and having each of those ids correspond to a unique ClipId.
+    clip_id_map: HashMap<u64, ClipId>,
+}
+
+impl YamlFrameReader {
+    pub fn new(yaml_path: &Path) -> YamlFrameReader {
+        YamlFrameReader {
+            watch_source: false,
+            list_resources: false,
+            frame_built: false,
+            yaml_path: yaml_path.to_owned(),
+            aux_dir: yaml_path.parent().unwrap().to_owned(),
+            frame_count: 0,
+            display_lists: Vec::new(),
+            queue_depth: 1,
+            include_only: vec![],
+            scroll_offsets: HashMap::new(),
+            fonts: HashMap::new(),
+            font_instances: HashMap::new(),
+            font_render_mode: None,
+            image_map: HashMap::new(),
+            clip_id_map: HashMap::new(),
+        }
+    }
+
+    pub fn deinit(mut self, wrench: &mut Wrench) {
+        let mut updates = ResourceUpdates::new();
+
+        for (_, font_instance) in self.font_instances.drain() {
+            updates.delete_font_instance(font_instance);
+        }
+
+        for (_, font) in self.fonts.drain() {
+            updates.delete_font(font);
+        }
+
+        wrench.api.update_resources(updates);
+    }
+
+    pub fn yaml_path(&self) -> &PathBuf {
+        &self.yaml_path
+    }
+
+    pub fn new_from_args(args: &clap::ArgMatches) -> YamlFrameReader {
+        let yaml_file = args.value_of("INPUT").map(|s| PathBuf::from(s)).unwrap();
+
+        let mut y = YamlFrameReader::new(&yaml_file);
+        y.list_resources = args.is_present("list-resources");
+        y.watch_source = args.is_present("watch");
+        y.queue_depth = args.value_of("queue")
+            .map(|s| s.parse::<u32>().unwrap())
+            .unwrap_or(1);
+        y.include_only = args.values_of("include")
+            .map(|v| v.map(|s| s.to_owned()).collect())
+            .unwrap_or(vec![]);
+        y
+    }
+
+    pub fn reset(&mut self) {
+        self.scroll_offsets.clear();
+        self.display_lists.clear();
+    }
+
+    pub fn build(&mut self, wrench: &mut Wrench) {
+        let mut file = File::open(&self.yaml_path).unwrap();
+        let mut src = String::new();
+        file.read_to_string(&mut src).unwrap();
+
+        let mut yaml_doc = YamlLoader::load_from_str(&src).expect("Failed to parse YAML file");
+        assert_eq!(yaml_doc.len(), 1);
+
+        self.reset();
+
+        let yaml = yaml_doc.pop().unwrap();
+        if let Some(pipelines) = yaml["pipelines"].as_vec() {
+            for pipeline in pipelines {
+                self.build_pipeline(wrench, pipeline["id"].as_pipeline_id().unwrap(), pipeline);
+            }
+        }
+
+        assert!(!yaml["root"].is_badvalue(), "Missing root stacking context");
+        let root_pipeline_id = wrench.root_pipeline_id;
+        self.build_pipeline(wrench, root_pipeline_id, &yaml["root"]);
+    }
+
+    pub fn build_pipeline(
+        &mut self,
+        wrench: &mut Wrench,
+        pipeline_id: PipelineId,
+        yaml: &Yaml
+    ) {
+        // Don't allow referencing clips between pipelines for now.
+        self.clip_id_map.clear();
+
+        let content_size = self.get_root_size_from_yaml(wrench, yaml);
+        let mut builder = DisplayListBuilder::new(pipeline_id, content_size);
+        let mut info = LayoutPrimitiveInfo::new(LayoutRect::zero());
+        self.add_stacking_context_from_yaml(&mut builder, wrench, yaml, true, &mut info);
+        self.display_lists.push(builder.finalize());
+    }
+
+    fn to_complex_clip_region(&mut self, item: &Yaml) -> ComplexClipRegion {
+        let rect = item["rect"]
+            .as_rect()
+            .expect("Complex clip entry must have rect");
+        let radius = item["radius"]
+            .as_border_radius()
+            .unwrap_or(BorderRadius::zero());
+        let mode = item["clip-mode"]
+            .as_clip_mode()
+            .unwrap_or(ClipMode::Clip);
+        ComplexClipRegion::new(rect, radius, mode)
+    }
+
+    fn to_complex_clip_regions(&mut self, item: &Yaml) -> Vec<ComplexClipRegion> {
+        match *item {
+            Yaml::Array(ref array) => array
+                .iter()
+                .map(|entry| self.to_complex_clip_region(entry))
+                .collect(),
+            Yaml::BadValue => vec![],
+            _ => {
+                println!("Unable to parse complex clip region {:?}", item);
+                vec![]
+            }
+        }
+    }
+
+    fn to_sticky_offset_bounds(&mut self, item: &Yaml) -> StickyOffsetBounds {
+        match *item {
+            Yaml::Array(ref array) => StickyOffsetBounds::new(
+                array[0].as_f32().unwrap_or(0.0),
+                array[1].as_f32().unwrap_or(0.0),
+            ),
+            _ => StickyOffsetBounds::new(0.0, 0.0),
+        }
+    }
+
+    pub fn to_clip_id(&self, id: u64, pipeline_id: PipelineId) -> ClipId {
+        if id == 0 {
+            return ClipId::root_scroll_node(pipeline_id);
+        }
+        self.clip_id_map[&id]
+    }
+
+    fn to_clip_and_scroll_info(
+        &self,
+        item: &Yaml,
+        pipeline_id: PipelineId
+    ) -> Option<ClipAndScrollInfo> {
+        match *item {
+            Yaml::Integer(value) => {
+                Some(ClipAndScrollInfo::simple(self.to_clip_id(value as u64, pipeline_id)))
+            }
+            Yaml::Array(ref array) if array.len() == 2 => {
+                let id_ints = (array[0].as_i64(), array[1].as_i64());
+                if let (Some(scroll_node_numeric_id), Some(clip_node_numeric_id)) = id_ints {
+                    Some(ClipAndScrollInfo::new(
+                        self.to_clip_id(scroll_node_numeric_id as u64, pipeline_id),
+                        self.to_clip_id(clip_node_numeric_id as u64, pipeline_id)
+                    ))
+                } else {
+                    None
+                }
+            }
+            _ => None,
+        }
+    }
+
+    pub fn add_or_get_image(
+            &mut self,
+            file: &Path,
+            tiling: Option<i64>,
+            wrench: &mut Wrench,
+    ) -> (ImageKey, LayoutSize) {
+        let key = (file.to_owned(), tiling);
+        if let Some(k) = self.image_map.get(&key) {
+            return *k;
+        }
+
+        if self.list_resources { println!("{}", file.to_string_lossy()); }
+        let (descriptor, image_data) = match image::open(file) {
+            Ok(image) => {
+                let image_dims = image.dimensions();
+                let (format, bytes) = match image {
+                    image::ImageLuma8(_) => {
+                        (ImageFormat::R8, image.raw_pixels())
+                    }
+                    image::ImageRgba8(_) => {
+                        let mut pixels = image.raw_pixels();
+                        premultiply(pixels.as_mut_slice());
+                        (ImageFormat::BGRA8, pixels)
+                    }
+                    image::ImageRgb8(_) => {
+                        let bytes = image.raw_pixels();
+                        let mut pixels = Vec::new();
+                        for bgr in bytes.chunks(3) {
+                            pixels.extend_from_slice(&[
+                                bgr[2],
+                                bgr[1],
+                                bgr[0],
+                                0xff
+                            ]);
+                        }
+                        (ImageFormat::BGRA8, pixels)
+                    }
+                    _ => panic!("We don't support whatever your crazy image type is, come on"),
+                };
+                let descriptor = ImageDescriptor::new(
+                    image_dims.0,
+                    image_dims.1,
+                    format,
+                    is_image_opaque(format, &bytes[..]),
+                );
+                let data = ImageData::new(bytes);
+                (descriptor, data)
+            }
+            _ => {
+                // This is a hack but it is convenient when generating test cases and avoids
+                // bloating the repository.
+                match parse_function(
+                    file.components()
+                        .last()
+                        .unwrap()
+                        .as_os_str()
+                        .to_str()
+                        .unwrap(),
+                ) {
+                    ("xy-gradient", args, _) => generate_xy_gradient_image(
+                        args.get(0).unwrap_or(&"1000").parse::<u32>().unwrap(),
+                        args.get(1).unwrap_or(&"1000").parse::<u32>().unwrap(),
+                    ),
+                    ("solid-color", args, _) => generate_solid_color_image(
+                        args.get(0).unwrap_or(&"255").parse::<u8>().unwrap(),
+                        args.get(1).unwrap_or(&"255").parse::<u8>().unwrap(),
+                        args.get(2).unwrap_or(&"255").parse::<u8>().unwrap(),
+                        args.get(3).unwrap_or(&"255").parse::<u8>().unwrap(),
+                        args.get(4).unwrap_or(&"1000").parse::<u32>().unwrap(),
+                        args.get(5).unwrap_or(&"1000").parse::<u32>().unwrap(),
+                    ),
+                    ("checkerboard", args, _) => generate_checkerboard_image(
+                        args.get(0).unwrap_or(&"4").parse::<u32>().unwrap(),
+                        args.get(1).unwrap_or(&"32").parse::<u32>().unwrap(),
+                        args.get(2).unwrap_or(&"8").parse::<u32>().unwrap(),
+                    ),
+                    _ => {
+                        panic!("Failed to load image {:?}", file.to_str());
+                    }
+                }
+            }
+        };
+        let tiling = tiling.map(|tile_size| tile_size as u16);
+        let image_key = wrench.api.generate_image_key();
+        let mut resources = ResourceUpdates::new();
+        resources.add_image(image_key, descriptor, image_data, tiling);
+        wrench.api.update_resources(resources);
+        let val = (
+            image_key,
+            LayoutSize::new(descriptor.width as f32, descriptor.height as f32),
+        );
+        self.image_map.insert(key, val);
+        val
+    }
+
+    fn get_or_create_font(&mut self, desc: FontDescriptor, wrench: &mut Wrench) -> FontKey {
+        let list_resources = self.list_resources;
+        *self.fonts
+            .entry(desc.clone())
+            .or_insert_with(|| match desc {
+                FontDescriptor::Path {
+                    ref path,
+                    font_index,
+                } => {
+                    if list_resources { println!("{}", path.to_string_lossy()); }
+                    let mut file = File::open(path).expect("Couldn't open font file");
+                    let mut bytes = vec![];
+                    file.read_to_end(&mut bytes)
+                        .expect("failed to read font file");
+                    wrench.font_key_from_bytes(bytes, font_index)
+                }
+                FontDescriptor::Family { ref name } => wrench.font_key_from_name(name),
+                FontDescriptor::Properties {
+                    ref family,
+                    weight,
+                    style,
+                    stretch,
+                } => wrench.font_key_from_properties(family, weight, style, stretch),
+            })
+    }
+
+    pub fn set_font_render_mode(&mut self, render_mode: Option<FontRenderMode>) {
+        self.font_render_mode = render_mode;
+    }
+
+    fn get_or_create_font_instance(
+        &mut self,
+        font_key: FontKey,
+        size: Au,
+        flags: FontInstanceFlags,
+        wrench: &mut Wrench,
+    ) -> FontInstanceKey {
+        let font_render_mode = self.font_render_mode;
+
+        *self.font_instances
+            .entry((font_key, size, flags))
+            .or_insert_with(|| {
+                wrench.add_font_instance(
+                    font_key,
+                    size,
+                    flags,
+                    font_render_mode,
+                )
+            })
+    }
+
+    fn to_image_mask(&mut self, item: &Yaml, wrench: &mut Wrench) -> Option<ImageMask> {
+        if item.as_hash().is_none() {
+            return None;
+        }
+
+        let file = match item["image"].as_str() {
+            Some(filename) => {
+                let mut file = self.aux_dir.clone();
+                file.push(filename);
+                file
+            }
+            None => {
+                warn!("No image provided for the image-mask!");
+                return None;
+            }
+        };
+
+        let (image_key, image_dims) =
+            self.add_or_get_image(&file, None, wrench);
+        let image_rect = item["rect"]
+            .as_rect()
+            .unwrap_or(LayoutRect::new(LayoutPoint::zero(), image_dims));
+        let image_repeat = item["repeat"].as_bool().expect("Expected boolean");
+        Some(ImageMask {
+            image: image_key,
+            rect: image_rect,
+            repeat: image_repeat,
+        })
+    }
+
+    fn to_gradient(&mut self, dl: &mut DisplayListBuilder, item: &Yaml) -> Gradient {
+        let start = item["start"].as_point().expect("gradient must have start");
+        let end = item["end"].as_point().expect("gradient must have end");
+        let stops = item["stops"]
+            .as_vec()
+            .expect("gradient must have stops")
+            .chunks(2)
+            .map(|chunk| {
+                GradientStop {
+                    offset: chunk[0]
+                        .as_force_f32()
+                        .expect("gradient stop offset is not f32"),
+                    color: chunk[1]
+                        .as_colorf()
+                        .expect("gradient stop color is not color"),
+                }
+            })
+            .collect::<Vec<_>>();
+        let extend_mode = if item["repeat"].as_bool().unwrap_or(false) {
+            ExtendMode::Repeat
+        } else {
+            ExtendMode::Clamp
+        };
+
+        dl.create_gradient(start, end, stops, extend_mode)
+    }
+
+    fn to_radial_gradient(&mut self, dl: &mut DisplayListBuilder, item: &Yaml) -> RadialGradient {
+        if item["start-center"].is_badvalue() {
+            let center = item["center"]
+                .as_point()
+                .expect("radial gradient must have start center");
+            let radius = item["radius"]
+                .as_size()
+                .expect("radial gradient must have start radius");
+            let stops = item["stops"]
+                .as_vec()
+                .expect("radial gradient must have stops")
+                .chunks(2)
+                .map(|chunk| {
+                    GradientStop {
+                        offset: chunk[0]
+                            .as_force_f32()
+                            .expect("gradient stop offset is not f32"),
+                        color: chunk[1]
+                            .as_colorf()
+                            .expect("gradient stop color is not color"),
+                    }
+                })
+                .collect::<Vec<_>>();
+            let extend_mode = if item["repeat"].as_bool().unwrap_or(false) {
+                ExtendMode::Repeat
+            } else {
+                ExtendMode::Clamp
+            };
+
+            dl.create_radial_gradient(center, radius, stops, extend_mode)
+        } else {
+            let start_center = item["start-center"]
+                .as_point()
+                .expect("radial gradient must have start center");
+            let start_radius = item["start-radius"]
+                .as_force_f32()
+                .expect("radial gradient must have start radius");
+            let end_center = item["end-center"]
+                .as_point()
+                .expect("radial gradient must have end center");
+            let end_radius = item["end-radius"]
+                .as_force_f32()
+                .expect("radial gradient must have end radius");
+            let ratio_xy = item["ratio-xy"].as_force_f32().unwrap_or(1.0);
+            let stops = item["stops"]
+                .as_vec()
+                .expect("radial gradient must have stops")
+                .chunks(2)
+                .map(|chunk| {
+                    GradientStop {
+                        offset: chunk[0]
+                            .as_force_f32()
+                            .expect("gradient stop offset is not f32"),
+                        color: chunk[1]
+                            .as_colorf()
+                            .expect("gradient stop color is not color"),
+                    }
+                })
+                .collect::<Vec<_>>();
+            let extend_mode = if item["repeat"].as_bool().unwrap_or(false) {
+                ExtendMode::Repeat
+            } else {
+                ExtendMode::Clamp
+            };
+
+            dl.create_complex_radial_gradient(
+                start_center,
+                start_radius,
+                end_center,
+                end_radius,
+                ratio_xy,
+                stops,
+                extend_mode,
+            )
+        }
+    }
+
+    fn handle_rect(
+        &mut self,
+        dl: &mut DisplayListBuilder,
+        item: &Yaml,
+        info: &mut LayoutPrimitiveInfo,
+    ) {
+        let bounds_key = if item["type"].is_badvalue() {
+            "rect"
+        } else {
+            "bounds"
+        };
+        info.rect = item[bounds_key]
+            .as_rect()
+            .expect("rect type must have bounds");
+        let color = item["color"].as_colorf().unwrap_or(*WHITE_COLOR);
+        dl.push_rect(&info, color);
+    }
+
+    fn handle_clear_rect(
+        &mut self,
+        dl: &mut DisplayListBuilder,
+        item: &Yaml,
+        info: &mut LayoutPrimitiveInfo,
+    ) {
+        info.rect = item["bounds"]
+            .as_rect()
+            .expect("clear-rect type must have bounds");
+        dl.push_clear_rect(&info);
+    }
+
+    fn handle_line(
+        &mut self,
+        dl: &mut DisplayListBuilder,
+        item: &Yaml,
+        info: &mut LayoutPrimitiveInfo,
+    ) {
+        let color = item["color"].as_colorf().unwrap_or(*BLACK_COLOR);
+        let orientation = item["orientation"]
+            .as_str()
+            .and_then(LineOrientation::from_str)
+            .expect("line must have orientation");
+        let style = item["style"]
+            .as_str()
+            .and_then(LineStyle::from_str)
+            .expect("line must have style");
+
+        let wavy_line_thickness = if let LineStyle::Wavy = style {
+            item["thickness"].as_f32().expect("wavy lines must have a thickness")
+        } else {
+            0.0
+        };
+
+        if item["baseline"].is_badvalue() {
+            let bounds_key = if item["type"].is_badvalue() {
+                "rect"
+            } else {
+                "bounds"
+            };
+
+            info.rect = item[bounds_key]
+                .as_rect()
+                .expect("line type must have bounds");
+        } else {
+            // Legacy line representation
+            let baseline = item["baseline"].as_f32().expect("line must have baseline");
+            let start = item["start"].as_f32().expect("line must have start");
+            let end = item["end"].as_f32().expect("line must have end");
+            let width = item["width"].as_f32().expect("line must have width");
+
+            info.rect = match orientation {
+                LineOrientation::Horizontal => {
+                    LayoutRect::new(LayoutPoint::new(start, baseline),
+                                    LayoutSize::new(end - start, width))
+                }
+                LineOrientation::Vertical => {
+                    LayoutRect::new(LayoutPoint::new(baseline, start),
+                                    LayoutSize::new(width, end - start))
+                }
+            };
+        }
+
+        dl.push_line(
+            &info,
+            wavy_line_thickness,
+            orientation,
+            &color,
+            style,
+        );
+    }
+
+    fn handle_gradient(
+        &mut self,
+        dl: &mut DisplayListBuilder,
+        item: &Yaml,
+        info: &mut LayoutPrimitiveInfo,
+    ) {
+        let bounds_key = if item["type"].is_badvalue() {
+            "gradient"
+        } else {
+            "bounds"
+        };
+        let bounds = item[bounds_key]
+            .as_rect()
+            .expect("gradient must have bounds");
+        info.rect = bounds;
+        let gradient = self.to_gradient(dl, item);
+        let tile_size = item["tile-size"].as_size().unwrap_or(bounds.size);
+        let tile_spacing = item["tile-spacing"].as_size().unwrap_or(LayoutSize::zero());
+
+        dl.push_gradient(&info, gradient, tile_size, tile_spacing);
+    }
+
+    fn handle_radial_gradient(
+        &mut self,
+        dl: &mut DisplayListBuilder,
+        item: &Yaml,
+        info: &mut LayoutPrimitiveInfo,
+    ) {
+        let bounds_key = if item["type"].is_badvalue() {
+            "radial-gradient"
+        } else {
+            "bounds"
+        };
+        let bounds = item[bounds_key]
+            .as_rect()
+            .expect("radial gradient must have bounds");
+        info.rect = bounds;
+        let gradient = self.to_radial_gradient(dl, item);
+        let tile_size = item["tile-size"].as_size().unwrap_or(bounds.size);
+        let tile_spacing = item["tile-spacing"].as_size().unwrap_or(LayoutSize::zero());
+
+        dl.push_radial_gradient(&info, gradient, tile_size, tile_spacing);
+    }
+
+    fn handle_border(
+        &mut self,
+        dl: &mut DisplayListBuilder,
+        wrench: &mut Wrench,
+        item: &Yaml,
+        info: &mut LayoutPrimitiveInfo,
+    ) {
+        let bounds_key = if item["type"].is_badvalue() {
+            "border"
+        } else {
+            "bounds"
+        };
+        info.rect = item[bounds_key]
+            .as_rect()
+            .expect("borders must have bounds");
+        let widths = item["width"]
+            .as_vec_f32()
+            .expect("borders must have width(s)");
+        let widths = broadcast(&widths, 4);
+        let widths = BorderWidths {
+            top: widths[0],
+            left: widths[1],
+            bottom: widths[2],
+            right: widths[3],
+        };
+        let border_details = if let Some(border_type) = item["border-type"].as_str() {
+            match border_type {
+                "normal" => {
+                    let colors = item["color"]
+                        .as_vec_colorf()
+                        .expect("borders must have color(s)");
+                    let styles = item["style"]
+                        .as_vec_string()
+                        .expect("borders must have style(s)");
+                    let styles = styles
+                        .iter()
+                        .map(|s| match s.as_str() {
+                            "none" => BorderStyle::None,
+                            "solid" => BorderStyle::Solid,
+                            "double" => BorderStyle::Double,
+                            "dotted" => BorderStyle::Dotted,
+                            "dashed" => BorderStyle::Dashed,
+                            "hidden" => BorderStyle::Hidden,
+                            "ridge" => BorderStyle::Ridge,
+                            "inset" => BorderStyle::Inset,
+                            "outset" => BorderStyle::Outset,
+                            "groove" => BorderStyle::Groove,
+                            s => {
+                                panic!("Unknown border style '{}'", s);
+                            }
+                        })
+                        .collect::<Vec<BorderStyle>>();
+                    let radius = item["radius"]
+                        .as_border_radius()
+                        .unwrap_or(BorderRadius::zero());
+
+                    let colors = broadcast(&colors, 4);
+                    let styles = broadcast(&styles, 4);
+
+                    let top = BorderSide {
+                        color: colors[0],
+                        style: styles[0],
+                    };
+                    let right = BorderSide {
+                        color: colors[1],
+                        style: styles[1],
+                    };
+                    let bottom = BorderSide {
+                        color: colors[2],
+                        style: styles[2],
+                    };
+                    let left = BorderSide {
+                        color: colors[3],
+                        style: styles[3],
+                    };
+                    Some(BorderDetails::Normal(NormalBorder {
+                        top,
+                        left,
+                        bottom,
+                        right,
+                        radius,
+                    }))
+                }
+                "image" => {
+                    let file = rsrc_path(&item["image-source"], &self.aux_dir);
+                    let (image_key, _) = self
+                        .add_or_get_image(&file, None, wrench);
+                    let image_width = item["image-width"]
+                        .as_i64()
+                        .expect("border must have image-width");
+                    let image_height = item["image-height"]
+                        .as_i64()
+                        .expect("border must have image-height");
+                    let fill = item["fill"].as_bool().unwrap_or(false);
+                    let slice = item["slice"].as_vec_u32().expect("border must have slice");
+                    let slice = broadcast(&slice, 4);
+                    let outset = item["outset"]
+                        .as_vec_f32()
+                        .expect("border must have outset");
+                    let outset = broadcast(&outset, 4);
+                    let repeat_horizontal = match item["repeat-horizontal"]
+                        .as_str()
+                        .expect("border must have repeat-horizontal")
+                    {
+                        "stretch" => RepeatMode::Stretch,
+                        "repeat" => RepeatMode::Repeat,
+                        "round" => RepeatMode::Round,
+                        "space" => RepeatMode::Space,
+                        s => panic!("Unknown box border image repeat mode {}", s),
+                    };
+                    let repeat_vertical = match item["repeat-vertical"]
+                        .as_str()
+                        .expect("border must have repeat-vertical")
+                    {
+                        "stretch" => RepeatMode::Stretch,
+                        "repeat" => RepeatMode::Repeat,
+                        "round" => RepeatMode::Round,
+                        "space" => RepeatMode::Space,
+                        s => panic!("Unknown box border image repeat mode {}", s),
+                    };
+                    Some(BorderDetails::Image(ImageBorder {
+                        image_key,
+                        patch: NinePatchDescriptor {
+                            width: image_width as u32,
+                            height: image_height as u32,
+                            slice: SideOffsets2D::new(slice[0], slice[1], slice[2], slice[3]),
+                        },
+                        fill,
+                        outset: SideOffsets2D::new(outset[0], outset[1], outset[2], outset[3]),
+                        repeat_horizontal,
+                        repeat_vertical,
+                    }))
+                }
+                "gradient" => {
+                    let gradient = self.to_gradient(dl, item);
+                    let outset = item["outset"]
+                        .as_vec_f32()
+                        .expect("borders must have outset");
+                    let outset = broadcast(&outset, 4);
+                    Some(BorderDetails::Gradient(GradientBorder {
+                        gradient,
+                        outset: SideOffsets2D::new(outset[0], outset[1], outset[2], outset[3]),
+                    }))
+                }
+                "radial-gradient" => {
+                    let gradient = self.to_radial_gradient(dl, item);
+                    let outset = item["outset"]
+                        .as_vec_f32()
+                        .expect("borders must have outset");
+                    let outset = broadcast(&outset, 4);
+                    Some(BorderDetails::RadialGradient(RadialGradientBorder {
+                        gradient,
+                        outset: SideOffsets2D::new(outset[0], outset[1], outset[2], outset[3]),
+                    }))
+                }
+                _ => {
+                    println!("Unable to parse border {:?}", item);
+                    None
+                }
+            }
+        } else {
+            println!("Unable to parse border {:?}", item);
+            None
+        };
+        if let Some(details) = border_details {
+            dl.push_border(&info, widths, details);
+        }
+    }
+
+    fn handle_box_shadow(
+        &mut self,
+        dl: &mut DisplayListBuilder,
+        item: &Yaml,
+        info: &mut LayoutPrimitiveInfo,
+    ) {
+        let bounds_key = if item["type"].is_badvalue() {
+            "box-shadow"
+        } else {
+            "bounds"
+        };
+        let bounds = item[bounds_key]
+            .as_rect()
+            .expect("box shadow must have bounds");
+        info.rect = bounds;
+        let box_bounds = item["box-bounds"].as_rect().unwrap_or(bounds);
+        let offset = item["offset"].as_vector().unwrap_or(LayoutVector2D::zero());
+        let color = item["color"]
+            .as_colorf()
+            .unwrap_or(ColorF::new(0.0, 0.0, 0.0, 1.0));
+        let blur_radius = item["blur-radius"].as_force_f32().unwrap_or(0.0);
+        let spread_radius = item["spread-radius"].as_force_f32().unwrap_or(0.0);
+        let border_radius = item["border-radius"]
+            .as_border_radius()
+            .unwrap_or(BorderRadius::zero());
+        let clip_mode = if let Some(mode) = item["clip-mode"].as_str() {
+            match mode {
+                "outset" => BoxShadowClipMode::Outset,
+                "inset" => BoxShadowClipMode::Inset,
+                s => panic!("Unknown box shadow clip mode {}", s),
+            }
+        } else {
+            BoxShadowClipMode::Outset
+        };
+
+        dl.push_box_shadow(
+            &info,
+            box_bounds,
+            offset,
+            color,
+            blur_radius,
+            spread_radius,
+            border_radius,
+            clip_mode,
+        );
+    }
+
+    fn handle_image(
+        &mut self,
+        dl: &mut DisplayListBuilder,
+        wrench: &mut Wrench,
+        item: &Yaml,
+        info: &mut LayoutPrimitiveInfo,
+    ) {
+        let filename = &item[if item["type"].is_badvalue() {
+                                 "image"
+                             } else {
+                                 "src"
+                             }];
+        let tiling = item["tile-size"].as_i64();
+        let file = rsrc_path(filename, &self.aux_dir);
+        let (image_key, image_dims) =
+            self.add_or_get_image(&file, tiling, wrench);
+
+        let bounds_raws = item["bounds"].as_vec_f32().unwrap();
+        info.rect = if bounds_raws.len() == 2 {
+            LayoutRect::new(LayoutPoint::new(bounds_raws[0], bounds_raws[1]), image_dims)
+        } else if bounds_raws.len() == 4 {
+            LayoutRect::new(
+                LayoutPoint::new(bounds_raws[0], bounds_raws[1]),
+                LayoutSize::new(bounds_raws[2], bounds_raws[3]),
+            )
+        } else {
+            panic!(
+                "image expected 2 or 4 values in bounds, got '{:?}'",
+                item["bounds"]
+            );
+        };
+
+        let stretch_size = item["stretch-size"].as_size().unwrap_or(image_dims);
+        let tile_spacing = item["tile-spacing"]
+            .as_size()
+            .unwrap_or(LayoutSize::new(0.0, 0.0));
+        let rendering = match item["rendering"].as_str() {
+            Some("auto") | None => ImageRendering::Auto,
+            Some("crisp-edges") => ImageRendering::CrispEdges,
+            Some("pixelated") => ImageRendering::Pixelated,
+            Some(_) => panic!(
+                "ImageRendering can be auto, crisp-edges, or pixelated -- got {:?}",
+                item
+            ),
+        };
+        let alpha_type = match item["alpha-type"].as_str() {
+            Some("premultiplied-alpha") | None => AlphaType::PremultipliedAlpha,
+            Some("alpha") => AlphaType::Alpha,
+            Some(_) => panic!(
+                "AlphaType can be premultiplied-alpha or alpha -- got {:?}",
+                item
+            ),
+        };
+        dl.push_image(&info, stretch_size, tile_spacing, rendering, alpha_type, image_key);
+    }
+
+    fn handle_text(
+        &mut self,
+        dl: &mut DisplayListBuilder,
+        wrench: &mut Wrench,
+        item: &Yaml,
+        info: &mut LayoutPrimitiveInfo,
+    ) {
+        let size = item["size"].as_pt_to_au().unwrap_or(Au::from_f32_px(16.0));
+        let color = item["color"].as_colorf().unwrap_or(*BLACK_COLOR);
+        let mut flags = FontInstanceFlags::empty();
+        if item["synthetic-italics"].as_bool().unwrap_or(false) {
+            flags |= FontInstanceFlags::SYNTHETIC_ITALICS;
+        }
+        if item["synthetic-bold"].as_bool().unwrap_or(false) {
+            flags |= FontInstanceFlags::SYNTHETIC_BOLD;
+        }
+        if item["embedded-bitmaps"].as_bool().unwrap_or(false) {
+            flags |= FontInstanceFlags::EMBEDDED_BITMAPS;
+        }
+        if item["transpose"].as_bool().unwrap_or(false) {
+            flags |= FontInstanceFlags::TRANSPOSE;
+        }
+        if item["flip-x"].as_bool().unwrap_or(false) {
+            flags |= FontInstanceFlags::FLIP_X;
+        }
+        if item["flip-y"].as_bool().unwrap_or(false) {
+            flags |= FontInstanceFlags::FLIP_Y;
+        }
+
+        assert!(
+            item["blur-radius"].is_badvalue(),
+            "text no longer has a blur radius, use PushShadow and PopAllShadows"
+        );
+
+        let desc = FontDescriptor::from_yaml(item, &self.aux_dir);
+        let font_key = self.get_or_create_font(desc, wrench);
+        let font_instance_key = self.get_or_create_font_instance(font_key,
+                                                                 size,
+                                                                 flags,
+                                                                 wrench);
+
+        assert!(
+            !(item["glyphs"].is_badvalue() && item["text"].is_badvalue()),
+            "text item had neither text nor glyphs!"
+        );
+
+        let (glyphs, rect) = if item["text"].is_badvalue() {
+            // if glyphs are specified, then the glyph positions can have the
+            // origin baked in.
+            let origin = item["origin"]
+                .as_point()
+                .unwrap_or(LayoutPoint::new(0.0, 0.0));
+            let glyph_indices = item["glyphs"].as_vec_u32().unwrap();
+            let glyph_offsets = item["offsets"].as_vec_f32().unwrap();
+            assert_eq!(glyph_offsets.len(), glyph_indices.len() * 2);
+
+            let glyphs = glyph_indices
+                .iter()
+                .enumerate()
+                .map(|k| {
+                    GlyphInstance {
+                        index: *k.1,
+                        point: LayoutPoint::new(
+                            origin.x + glyph_offsets[k.0 * 2],
+                            origin.y + glyph_offsets[k.0 * 2 + 1],
+                        ),
+                    }
+                })
+                .collect::<Vec<_>>();
+            // TODO(gw): We could optionally use the WR API to query glyph dimensions
+            //           here and calculate the bounding region here if we want to.
+            let rect = item["bounds"]
+                .as_rect()
+                .expect("Text items with glyphs require bounds [for now]");
+            (glyphs, rect)
+        } else {
+            let text = item["text"].as_str().unwrap();
+            let origin = item["origin"]
+                .as_point()
+                .expect("origin required for text without glyphs");
+            let (glyph_indices, glyph_positions, bounds) = wrench.layout_simple_ascii(
+                font_key,
+                font_instance_key,
+                self.font_render_mode,
+                text,
+                size,
+                origin,
+                flags,
+            );
+
+            let glyphs = glyph_indices
+                .iter()
+                .zip(glyph_positions)
+                .map(|arg| {
+                    let gi = GlyphInstance {
+                        index: *arg.0 as u32,
+                        point: arg.1,
+                    };
+                    gi
+                })
+                .collect::<Vec<_>>();
+            (glyphs, bounds)
+        };
+        info.rect = rect;
+
+        dl.push_text(&info, &glyphs, font_instance_key, color, None);
+    }
+
+    fn handle_iframe(
+        &mut self,
+        dl: &mut DisplayListBuilder,
+        item: &Yaml,
+        info: &mut LayoutPrimitiveInfo,
+    ) {
+        info.rect = item["bounds"].as_rect().expect("iframe must have bounds");
+        let pipeline_id = item["id"].as_pipeline_id().unwrap();
+        dl.push_iframe(&info, pipeline_id);
+    }
+
+    pub fn get_local_clip_for_item(&mut self, yaml: &Yaml, full_clip: LayoutRect) -> LocalClip {
+        let rect = yaml["clip-rect"].as_rect().unwrap_or(full_clip);
+        let complex_clip = &yaml["complex-clip"];
+        if !complex_clip.is_badvalue() {
+            LocalClip::RoundedRect(rect, self.to_complex_clip_region(complex_clip))
+        } else {
+            LocalClip::from(rect)
+        }
+    }
+
+    pub fn add_display_list_items_from_yaml(
+        &mut self,
+        dl: &mut DisplayListBuilder,
+        wrench: &mut Wrench,
+        yaml: &Yaml,
+    ) {
+        let full_clip = LayoutRect::new(LayoutPoint::zero(), wrench.window_size_f32());
+
+        for item in yaml.as_vec().unwrap() {
+            // an explicit type can be skipped with some shorthand
+            let item_type = if !item["rect"].is_badvalue() {
+                "rect"
+            } else if !item["image"].is_badvalue() {
+                "image"
+            } else if !item["text"].is_badvalue() {
+                "text"
+            } else if !item["glyphs"].is_badvalue() {
+                "glyphs"
+            } else if !item["box-shadow"].is_badvalue() {
+                // Note: box_shadow shorthand check has to come before border.
+                "box-shadow"
+            } else if !item["border"].is_badvalue() {
+                "border"
+            } else if !item["gradient"].is_badvalue() {
+                "gradient"
+            } else if !item["radial-gradient"].is_badvalue() {
+                "radial-gradient"
+            } else {
+                item["type"].as_str().unwrap_or("unknown")
+            };
+
+            // We never skip stacking contexts because they are structural elements
+            // of the display list.
+            if item_type != "stacking-context" && self.include_only.contains(&item_type.to_owned())
+            {
+                continue;
+            }
+
+            let clip_scroll_info = self.to_clip_and_scroll_info(
+                &item["clip-and-scroll"],
+                dl.pipeline_id
+            );
+            if let Some(clip_scroll_info) = clip_scroll_info {
+                dl.push_clip_and_scroll_info(clip_scroll_info);
+            }
+            let local_clip = self.get_local_clip_for_item(item, full_clip);
+            let mut info = LayoutPrimitiveInfo::with_clip(LayoutRect::zero(), local_clip);
+            info.is_backface_visible = item["backface-visible"].as_bool().unwrap_or(true);;
+            match item_type {
+                "rect" => self.handle_rect(dl, item, &mut info),
+                "clear-rect" => self.handle_clear_rect(dl, item, &mut info),
+                "line" => self.handle_line(dl, item, &mut info),
+                "image" => self.handle_image(dl, wrench, item, &mut info),
+                "text" | "glyphs" => self.handle_text(dl, wrench, item, &mut info),
+                "scroll-frame" => self.handle_scroll_frame(dl, wrench, item),
+                "sticky-frame" => self.handle_sticky_frame(dl, wrench, item),
+                "clip" => self.handle_clip(dl, wrench, item),
+                "clip-chain" => self.handle_clip_chain(dl, item),
+                "border" => self.handle_border(dl, wrench, item, &mut info),
+                "gradient" => self.handle_gradient(dl, item, &mut info),
+                "radial-gradient" => self.handle_radial_gradient(dl, item, &mut info),
+                "box-shadow" => self.handle_box_shadow(dl, item, &mut info),
+                "iframe" => self.handle_iframe(dl, item, &mut info),
+                "stacking-context" => {
+                    self.add_stacking_context_from_yaml(dl, wrench, item, false, &mut info)
+                }
+                "shadow" => self.handle_push_shadow(dl, item, &mut info),
+                "pop-all-shadows" => self.handle_pop_all_shadows(dl),
+                _ => println!("Skipping unknown item type: {:?}", item),
+            }
+
+            if clip_scroll_info.is_some() {
+                dl.pop_clip_id();
+            }
+        }
+    }
+
+    pub fn handle_scroll_frame(
+        &mut self,
+        dl: &mut DisplayListBuilder,
+        wrench: &mut Wrench,
+        yaml: &Yaml,
+    ) {
+        let clip_rect = yaml["bounds"]
+            .as_rect()
+            .expect("scroll frame must have a bounds");
+        let content_size = yaml["content-size"].as_size().unwrap_or(clip_rect.size);
+        let content_rect = LayerRect::new(clip_rect.origin, content_size);
+
+        let numeric_id = yaml["id"].as_i64().map(|id| id as u64);
+        let complex_clips = self.to_complex_clip_regions(&yaml["complex"]);
+        let image_mask = self.to_image_mask(&yaml["image-mask"], wrench);
+
+        let real_id = dl.define_scroll_frame(
+            None,
+            content_rect,
+            clip_rect,
+            complex_clips,
+            image_mask,
+            ScrollSensitivity::Script,
+        );
+        if let Some(numeric_id) = numeric_id {
+            self.clip_id_map.insert(numeric_id, real_id);
+        }
+
+        if let Some(size) = yaml["scroll-offset"].as_point() {
+            self.scroll_offsets.insert(real_id, LayerPoint::new(size.x, size.y));
+        }
+
+        if !yaml["items"].is_badvalue() {
+            dl.push_clip_id(real_id);
+            self.add_display_list_items_from_yaml(dl, wrench, &yaml["items"]);
+            dl.pop_clip_id();
+        }
+    }
+
+    pub fn handle_sticky_frame(
+        &mut self,
+        dl: &mut DisplayListBuilder,
+        wrench: &mut Wrench,
+        yaml: &Yaml,
+    ) {
+        let bounds = yaml["bounds"].as_rect().expect("sticky frame must have a bounds");
+        let numeric_id = yaml["id"].as_i64().map(|id| id as u64);
+
+        let real_id = dl.define_sticky_frame(
+            None,
+            bounds,
+            SideOffsets2D::new(
+                yaml["margin-top"].as_f32(),
+                yaml["margin-right"].as_f32(),
+                yaml["margin-bottom"].as_f32(),
+                yaml["margin-left"].as_f32(),
+            ),
+            self.to_sticky_offset_bounds(&yaml["vertical-offset-bounds"]),
+            self.to_sticky_offset_bounds(&yaml["horizontal-offset-bounds"]),
+            yaml["previously-applied-offset"].as_vector().unwrap_or(LayoutVector2D::zero()),
+        );
+
+        if let Some(numeric_id) = numeric_id {
+            self.clip_id_map.insert(numeric_id, real_id);
+        }
+
+        if !yaml["items"].is_badvalue() {
+            dl.push_clip_id(real_id);
+            self.add_display_list_items_from_yaml(dl, wrench, &yaml["items"]);
+            dl.pop_clip_id();
+        }
+    }
+
+    pub fn handle_push_shadow(
+        &mut self,
+        dl: &mut DisplayListBuilder,
+        yaml: &Yaml,
+        info: &mut LayoutPrimitiveInfo,
+    ) {
+        let rect = yaml["bounds"]
+            .as_rect()
+            .expect("Text shadows require bounds");
+        info.rect = rect;
+        info.local_clip = LocalClip::from(rect);
+        let blur_radius = yaml["blur-radius"].as_f32().unwrap_or(0.0);
+        let offset = yaml["offset"].as_vector().unwrap_or(LayoutVector2D::zero());
+        let color = yaml["color"].as_colorf().unwrap_or(*BLACK_COLOR);
+
+        dl.push_shadow(
+            &info,
+            Shadow {
+                blur_radius,
+                offset,
+                color,
+            },
+        );
+    }
+
+    pub fn handle_pop_all_shadows(&mut self, dl: &mut DisplayListBuilder) {
+        dl.pop_all_shadows();
+    }
+
+    pub fn handle_clip_chain(&mut self, builder: &mut DisplayListBuilder, yaml: &Yaml) {
+        let numeric_id = yaml["id"].as_i64().expect("clip chains must have an id");
+        let clip_ids: Vec<ClipId> = yaml["clips"]
+            .as_vec_u64()
+            .unwrap_or_else(Vec::new)
+            .iter().map(|id| self.to_clip_id(*id, builder.pipeline_id))
+            .collect();
+
+        let parent = yaml["parent"].as_i64().map(|id|
+            self.to_clip_id(id as u64, builder.pipeline_id)
+        );
+        let parent = match parent {
+            Some(ClipId::ClipChain(clip_chain_id)) => Some(clip_chain_id),
+            Some(_) => panic!("Tried to create a ClipChain with a non-ClipChain parent"),
+            None => None,
+        };
+
+        let real_id = builder.define_clip_chain(parent, clip_ids);
+        self.clip_id_map.insert(numeric_id as u64, ClipId::ClipChain(real_id));
+    }
+
+    pub fn handle_clip(&mut self, dl: &mut DisplayListBuilder, wrench: &mut Wrench, yaml: &Yaml) {
+        let clip_rect = yaml["bounds"].as_rect().expect("clip must have a bounds");
+        let numeric_id = yaml["id"].as_i64();
+        let complex_clips = self.to_complex_clip_regions(&yaml["complex"]);
+        let image_mask = self.to_image_mask(&yaml["image-mask"], wrench);
+
+        let real_id = dl.define_clip(None, clip_rect, complex_clips, image_mask);
+        if let Some(numeric_id) = numeric_id {
+            self.clip_id_map.insert(numeric_id as u64, real_id);
+        }
+
+        if !yaml["items"].is_badvalue() {
+            dl.push_clip_id(real_id);
+            self.add_display_list_items_from_yaml(dl, wrench, &yaml["items"]);
+            dl.pop_clip_id();
+        }
+    }
+
+    pub fn get_root_size_from_yaml(&mut self, wrench: &mut Wrench, yaml: &Yaml) -> LayoutSize {
+        yaml["bounds"]
+            .as_rect()
+            .map(|rect| rect.size)
+            .unwrap_or(wrench.window_size_f32())
+    }
+
+    pub fn add_stacking_context_from_yaml(
+        &mut self,
+        dl: &mut DisplayListBuilder,
+        wrench: &mut Wrench,
+        yaml: &Yaml,
+        is_root: bool,
+        info: &mut LayoutPrimitiveInfo,
+    ) {
+        let default_bounds = LayoutRect::new(LayoutPoint::zero(), wrench.window_size_f32());
+        let bounds = yaml["bounds"].as_rect().unwrap_or(default_bounds);
+
+        // TODO(gw): Add support for specifying the transform origin in yaml.
+        let transform_origin = LayoutPoint::new(
+            bounds.origin.x + bounds.size.width * 0.5,
+            bounds.origin.y + bounds.size.height * 0.5,
+        );
+
+        let transform = yaml["transform"]
+            .as_transform(&transform_origin)
+            .map(|transform| transform.into());
+
+        // TODO(gw): Support perspective-origin.
+        let perspective = match yaml["perspective"].as_f32() {
+            Some(value) if value != 0.0 => Some(LayoutTransform::create_perspective(value as f32)),
+            Some(_) => None,
+            _ => yaml["perspective"].as_matrix4d(),
+        };
+
+        let transform_style = yaml["transform-style"]
+            .as_transform_style()
+            .unwrap_or(TransformStyle::Flat);
+        let mix_blend_mode = yaml["mix-blend-mode"]
+            .as_mix_blend_mode()
+            .unwrap_or(MixBlendMode::Normal);
+        let scroll_policy = yaml["scroll-policy"]
+            .as_scroll_policy()
+            .unwrap_or(ScrollPolicy::Scrollable);
+
+        if is_root {
+            if let Some(size) = yaml["scroll-offset"].as_point() {
+                let id = ClipId::root_scroll_node(dl.pipeline_id);
+                self.scroll_offsets
+                    .insert(id, LayerPoint::new(size.x, size.y));
+            }
+        }
+
+        let filters = yaml["filters"].as_vec_filter_op().unwrap_or(vec![]);
+        info.rect = bounds;
+        info.local_clip = LocalClip::from(bounds);
+
+        dl.push_stacking_context(
+            &info,
+            scroll_policy,
+            transform.into(),
+            transform_style,
+            perspective,
+            mix_blend_mode,
+            filters,
+        );
+
+        if !yaml["items"].is_badvalue() {
+            self.add_display_list_items_from_yaml(dl, wrench, &yaml["items"]);
+        }
+
+        dl.pop_stacking_context();
+    }
+}
+
+impl WrenchThing for YamlFrameReader {
+    fn do_frame(&mut self, wrench: &mut Wrench) -> u32 {
+        if !self.frame_built || self.watch_source {
+            self.build(wrench);
+            self.frame_built = false;
+        }
+
+        self.frame_count += 1;
+
+        if !self.frame_built || wrench.should_rebuild_display_lists() {
+            wrench.begin_frame();
+            wrench.send_lists(
+                self.frame_count,
+                self.display_lists.clone(),
+                &self.scroll_offsets,
+            );
+        } else {
+            wrench.refresh();
+        }
+
+        self.frame_built = true;
+        self.frame_count
+    }
+
+    fn next_frame(&mut self) {}
+
+    fn prev_frame(&mut self) {}
+
+    fn queue_frames(&self) -> u32 {
+        self.queue_depth
+    }
+}
new file mode 100644
--- /dev/null
+++ b/gfx/wrench/src/yaml_frame_writer.rs
@@ -0,0 +1,1207 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate yaml_rust;
+
+use app_units::Au;
+use euclid::{TypedPoint2D, TypedRect, TypedSize2D, TypedTransform3D, TypedVector2D};
+use image::{save_buffer, ColorType};
+use premultiply::unpremultiply;
+use scene::{Scene, SceneProperties};
+use std::collections::HashMap;
+use std::io::Write;
+use std::path::{Path, PathBuf};
+use std::{fmt, fs};
+use super::CURRENT_FRAME_NUMBER;
+use time;
+use webrender;
+use webrender::api::*;
+use webrender::api::SpecificDisplayItem::*;
+use webrender::api::channel::Payload;
+use yaml_helper::StringEnum;
+use yaml_rust::{Yaml, YamlEmitter};
+
+type Table = yaml_rust::yaml::Hash;
+
+fn array_elements_are_same<T: PartialEq>(v: &[T]) -> bool {
+    if !v.is_empty() {
+        let first = &v[0];
+        for o in v.iter() {
+            if *first != *o {
+                return false;
+            }
+        }
+    }
+    true
+}
+
+fn new_table() -> Table {
+    Table::new()
+}
+
+fn yaml_node(parent: &mut Table, key: &str, value: Yaml) {
+    parent.insert(Yaml::String(key.to_owned()), value);
+}
+
+fn str_node(parent: &mut Table, key: &str, value: &str) {
+    yaml_node(parent, key, Yaml::String(value.to_owned()));
+}
+
+fn path_node(parent: &mut Table, key: &str, value: &Path) {
+    let pstr = value.to_str().unwrap().to_owned().replace("\\", "/");
+    yaml_node(parent, key, Yaml::String(pstr));
+}
+
+fn enum_node<E: StringEnum>(parent: &mut Table, key: &str, value: E) {
+    yaml_node(parent, key, Yaml::String(value.as_str().to_owned()));
+}
+
+fn color_to_string(value: ColorF) -> String {
+    if value.r == 1.0 && value.g == 1.0 && value.b == 1.0 && value.a == 1.0 {
+        "white".to_owned()
+    } else if value.r == 0.0 && value.g == 0.0 && value.b == 0.0 && value.a == 1.0 {
+        "black".to_owned()
+    } else {
+        format!(
+            "{} {} {} {:.4}",
+            value.r * 255.0,
+            value.g * 255.0,
+            value.b * 255.0,
+            value.a
+        )
+    }
+}
+
+fn color_node(parent: &mut Table, key: &str, value: ColorF) {
+    yaml_node(parent, key, Yaml::String(color_to_string(value)));
+}
+
+fn point_node<U>(parent: &mut Table, key: &str, value: &TypedPoint2D<f32, U>) {
+    f32_vec_node(parent, key, &[value.x, value.y]);
+}
+
+fn vector_node<U>(parent: &mut Table, key: &str, value: &TypedVector2D<f32, U>) {
+    f32_vec_node(parent, key, &[value.x, value.y]);
+}
+
+fn size_node<U>(parent: &mut Table, key: &str, value: &TypedSize2D<f32, U>) {
+    f32_vec_node(parent, key, &[value.width, value.height]);
+}
+
+fn rect_yaml<U>(value: &TypedRect<f32, U>) -> Yaml {
+    f32_vec_yaml(
+        &[
+            value.origin.x,
+            value.origin.y,
+            value.size.width,
+            value.size.height,
+        ],
+        false,
+    )
+}
+
+fn rect_node<U>(parent: &mut Table, key: &str, value: &TypedRect<f32, U>) {
+    yaml_node(parent, key, rect_yaml(value));
+}
+
+fn matrix4d_node<U1, U2>(parent: &mut Table, key: &str, value: &TypedTransform3D<f32, U1, U2>) {
+    f32_vec_node(parent, key, &value.to_row_major_array());
+}
+
+fn u32_node(parent: &mut Table, key: &str, value: u32) {
+    yaml_node(parent, key, Yaml::Integer(value as i64));
+}
+
+fn usize_node(parent: &mut Table, key: &str, value: usize) {
+    yaml_node(parent, key, Yaml::Integer(value as i64));
+}
+
+fn f32_node(parent: &mut Table, key: &str, value: f32) {
+    yaml_node(parent, key, Yaml::Real(value.to_string()));
+}
+
+fn bool_node(parent: &mut Table, key: &str, value: bool) {
+    yaml_node(parent, key, Yaml::Boolean(value));
+}
+
+fn table_node(parent: &mut Table, key: &str, value: Table) {
+    yaml_node(parent, key, Yaml::Hash(value));
+}
+
+fn string_vec_yaml(value: &[String], check_unique: bool) -> Yaml {
+    if !value.is_empty() && check_unique && array_elements_are_same(value) {
+        Yaml::String(value[0].clone())
+    } else {
+        Yaml::Array(value.iter().map(|v| Yaml::String(v.clone())).collect())
+    }
+}
+
+fn u32_vec_yaml(value: &[u32], check_unique: bool) -> Yaml {
+    if !value.is_empty() && check_unique && array_elements_are_same(value) {
+        Yaml::Integer(value[0] as i64)
+    } else {
+        Yaml::Array(value.iter().map(|v| Yaml::Integer(*v as i64)).collect())
+    }
+}
+
+fn u32_vec_node(parent: &mut Table, key: &str, value: &[u32]) {
+    yaml_node(parent, key, u32_vec_yaml(value, false));
+}
+
+fn f32_vec_yaml(value: &[f32], check_unique: bool) -> Yaml {
+    if !value.is_empty() && check_unique && array_elements_are_same(value) {
+        Yaml::Real(value[0].to_string())
+    } else {
+        Yaml::Array(value.iter().map(|v| Yaml::Real(v.to_string())).collect())
+    }
+}
+
+fn f32_vec_node(parent: &mut Table, key: &str, value: &[f32]) {
+    yaml_node(parent, key, f32_vec_yaml(value, false));
+}
+
+fn maybe_radius_yaml(radius: &BorderRadius) -> Option<Yaml> {
+    if let Some(radius) = radius.is_uniform_size() {
+        if radius == LayoutSize::zero() {
+            None
+        } else {
+            Some(f32_vec_yaml(&[radius.width, radius.height], false))
+        }
+    } else {
+        let mut table = new_table();
+        size_node(&mut table, "top-left", &radius.top_left);
+        size_node(&mut table, "top-right", &radius.top_right);
+        size_node(&mut table, "bottom-left", &radius.bottom_left);
+        size_node(&mut table, "bottom-right", &radius.bottom_right);
+        Some(Yaml::Hash(table))
+    }
+}
+
+fn write_sc(parent: &mut Table, sc: &StackingContext, properties: &SceneProperties, filter_iter: AuxIter<FilterOp>) {
+    enum_node(parent, "scroll-policy", sc.scroll_policy);
+
+    matrix4d_node(parent, "transform", &properties.resolve_layout_transform(&sc.transform));
+
+    enum_node(parent, "transform-style", sc.transform_style);
+
+    if let Some(perspective) = sc.perspective {
+        matrix4d_node(parent, "perspective", &perspective);
+    }
+
+    // mix_blend_mode
+    if sc.mix_blend_mode != MixBlendMode::Normal {
+        enum_node(parent, "mix-blend-mode", sc.mix_blend_mode)
+    }
+    // filters
+    let mut filters = vec![];
+    for filter in filter_iter {
+        match filter {
+            FilterOp::Blur(x) => { filters.push(Yaml::String(format!("blur({})", x))) }
+            FilterOp::Brightness(x) => { filters.push(Yaml::String(format!("brightness({})", x))) }
+            FilterOp::Contrast(x) => { filters.push(Yaml::String(format!("contrast({})", x))) }
+            FilterOp::Grayscale(x) => { filters.push(Yaml::String(format!("grayscale({})", x))) }
+            FilterOp::HueRotate(x) => { filters.push(Yaml::String(format!("hue-rotate({})", x))) }
+            FilterOp::Invert(x) => { filters.push(Yaml::String(format!("invert({})", x))) }
+            FilterOp::Opacity(x, _) => {
+                filters.push(Yaml::String(format!("opacity({})",
+                                                  properties.resolve_float(&x, 1.0))))
+            }
+            FilterOp::Saturate(x) => { filters.push(Yaml::String(format!("saturate({})", x))) }
+            FilterOp::Sepia(x) => { filters.push(Yaml::String(format!("sepia({})", x))) }
+            FilterOp::DropShadow(offset, blur, color) => {
+                filters.push(Yaml::String(format!("drop-shadow([{},{}],{},[{}])",
+                                                  offset.x, offset.y,
+                                                  blur,
+                                                  color_to_string(color))))
+            }
+        }
+    }
+
+    yaml_node(parent, "filters", Yaml::Array(filters));
+}
+
+#[cfg(target_os = "windows")]
+fn native_font_handle_to_yaml(
+    _rsrc: &mut ResourceGenerator,
+    handle: &NativeFontHandle,
+    parent: &mut yaml_rust::yaml::Hash,
+    _: &mut Option<PathBuf>,
+) {
+    str_node(parent, "family", &handle.family_name);
+    u32_node(parent, "weight", handle.weight.to_u32());
+    u32_node(parent, "style", handle.style.to_u32());
+    u32_node(parent, "stretch", handle.stretch.to_u32());
+}
+
+#[cfg(target_os = "macos")]
+fn native_font_handle_to_yaml(
+    rsrc: &mut ResourceGenerator,
+    handle: &NativeFontHandle,
+    parent: &mut yaml_rust::yaml::Hash,
+    path_opt: &mut Option<PathBuf>,
+) {
+    let path = match *path_opt {
+        Some(ref path) => { path.clone() },
+        None => {
+            use cgfont_to_data;
+            let bytes = cgfont_to_data::font_to_data(handle.0.clone()).unwrap();
+            let (path_file, path) = rsrc.next_rsrc_paths(
+                "font",
+                "ttf",
+            );
+            let mut file = fs::File::create(&path_file).unwrap();
+            file.write_all(&bytes).unwrap();
+            *path_opt = Some(path.clone());
+            path
+        }
+    };
+
+    path_node(parent, "font", &path);
+}
+
+#[cfg(not(any(target_os = "macos", target_os = "windows")))]
+fn native_font_handle_to_yaml(
+    _rsrc: &mut ResourceGenerator,
+    handle: &NativeFontHandle,
+    parent: &mut yaml_rust::yaml::Hash,
+    _: &mut Option<PathBuf>,
+) {
+    str_node(parent, "font", &handle.pathname);
+}
+
+enum CachedFont {
+    Native(NativeFontHandle, Option<PathBuf>),
+    Raw(Option<Vec<u8>>, u32, Option<PathBuf>),
+}
+
+struct CachedFontInstance {
+    font_key: FontKey,
+    glyph_size: Au,
+}
+
+struct CachedImage {
+    width: u32,
+    height: u32,
+    stride: u32,
+    format: ImageFormat,
+    bytes: Option<Vec<u8>>,
+    path: Option<PathBuf>,
+    tiling: Option<u16>,
+}
+
+struct ResourceGenerator {
+    base: PathBuf,
+    next_num: u32,
+    prefix: String,
+}
+
+impl ResourceGenerator {
+    fn next_rsrc_paths(&mut self, base: &str, ext: &str) -> (PathBuf, PathBuf) {
+        let mut path_file = self.base.to_owned();
+        let mut path = PathBuf::from("res");
+
+        let fstr = format!("{}-{}-{}.{}", self.prefix, base, self.next_num, ext);
+        path_file.push(&fstr);
+        path.push(&fstr);
+
+        self.next_num += 1;
+
+        (path_file, path)
+    }
+}
+
+pub struct YamlFrameWriter {
+    frame_base: PathBuf,
+    rsrc_gen: ResourceGenerator,
+    images: HashMap<ImageKey, CachedImage>,
+    fonts: HashMap<FontKey, CachedFont>,
+    font_instances: HashMap<FontInstanceKey, CachedFontInstance>,
+
+    last_frame_written: u32,
+    pipeline_id: Option<PipelineId>,
+
+    dl_descriptor: Option<BuiltDisplayListDescriptor>,
+}
+
+pub struct YamlFrameWriterReceiver {
+    frame_writer: YamlFrameWriter,
+    scene: Scene,
+}
+
+impl YamlFrameWriterReceiver {
+    pub fn new(path: &Path) -> YamlFrameWriterReceiver {
+        YamlFrameWriterReceiver {
+            frame_writer: YamlFrameWriter::new(path),
+            scene: Scene::new(),
+        }
+    }
+}
+
+impl fmt::Debug for YamlFrameWriterReceiver {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "YamlFrameWriterReceiver")
+    }
+}
+
+impl YamlFrameWriter {
+    pub fn new(path: &Path) -> YamlFrameWriter {
+        let mut rsrc_base = path.to_owned();
+        rsrc_base.push("res");
+        fs::create_dir_all(&rsrc_base).ok();
+
+        let rsrc_prefix = format!("{}", time::get_time().sec);
+
+        YamlFrameWriter {
+            frame_base: path.to_owned(),
+            rsrc_gen: ResourceGenerator {
+                base: rsrc_base,
+                prefix: rsrc_prefix,
+                next_num: 1,
+            },
+            images: HashMap::new(),
+            fonts: HashMap::new(),
+            font_instances: HashMap::new(),
+
+            dl_descriptor: None,
+
+            pipeline_id: None,
+
+            last_frame_written: u32::max_value(),
+        }
+    }
+
+    pub fn begin_write_display_list(
+        &mut self,
+        scene: &mut Scene,
+        epoch: &Epoch,
+        pipeline_id: &PipelineId,
+        background_color: &Option<ColorF>,
+        viewport_size: &LayoutSize,
+        display_list: &BuiltDisplayListDescriptor,
+    ) {
+        unsafe {
+            if CURRENT_FRAME_NUMBER == self.last_frame_written {
+                return;
+            }
+            self.last_frame_written = CURRENT_FRAME_NUMBER;
+        }
+
+        self.dl_descriptor = Some(display_list.clone());
+        self.pipeline_id = Some(pipeline_id.clone());
+
+        scene.begin_display_list(pipeline_id, epoch, background_color, viewport_size);
+    }
+
+    pub fn finish_write_display_list(&mut self, scene: &mut Scene, data: &[u8]) {
+        let dl_desc = self.dl_descriptor.take().unwrap();
+
+        let payload = Payload::from_data(data);
+
+        let dl = BuiltDisplayList::from_data(payload.display_list_data, dl_desc);
+
+        let mut root_dl_table = new_table();
+        {
+            let mut iter = dl.iter();
+            self.write_display_list(&mut root_dl_table, &dl, scene, &mut iter, &mut ClipIdMapper::new());
+        }
+
+        let mut root = new_table();
+        if let Some(root_pipeline_id) = scene.root_pipeline_id {
+            u32_vec_node(
+                &mut root_dl_table,
+                "id",
+                &[root_pipeline_id.0, root_pipeline_id.1],
+            );
+
+            let mut referenced_pipeline_ids = vec![];
+            let mut traversal = dl.iter();
+            while let Some(item) = traversal.next() {
+                if let &SpecificDisplayItem::Iframe(k) = item.item() {
+                    referenced_pipeline_ids.push(k.pipeline_id);
+                }
+            }
+
+            let mut pipelines = vec![];
+            for pipeline_id in referenced_pipeline_ids {
+                if !scene.display_lists.contains_key(&pipeline_id) {
+                    continue;
+                }
+                let mut pipeline = new_table();
+                u32_vec_node(&mut pipeline, "id", &[pipeline_id.0, pipeline_id.1]);
+
+                let dl = scene.display_lists.get(&pipeline_id).unwrap();
+                let mut iter = dl.iter();
+                self.write_display_list(&mut pipeline, &dl, scene, &mut iter, &mut ClipIdMapper::new());
+                pipelines.push(Yaml::Hash(pipeline));
+            }
+
+            table_node(&mut root, "root", root_dl_table);
+
+            root.insert(Yaml::String("pipelines".to_owned()), Yaml::Array(pipelines));
+
+            let mut s = String::new();
+            // FIXME YamlEmitter wants a std::fmt::Write, not a io::Write, so we can't pass a file
+            // directly.  This seems broken.
+            {
+                let mut emitter = YamlEmitter::new(&mut s);
+                emitter.dump(&Yaml::Hash(root)).unwrap();
+            }
+            let sb = s.into_bytes();
+            let mut frame_file_name = self.frame_base.clone();
+            let current_shown_frame = unsafe { CURRENT_FRAME_NUMBER };
+            frame_file_name.push(format!("frame-{}.yaml", current_shown_frame));
+            let mut file = fs::File::create(&frame_file_name).unwrap();
+            file.write_all(&sb).unwrap();
+        }
+
+        scene.finish_display_list(self.pipeline_id.unwrap(), dl);
+    }
+
+    fn update_resources(&mut self, updates: &ResourceUpdates) {
+        for update in &updates.updates {
+            match *update {
+                ResourceUpdate::AddImage(ref img) => {
+                    if let Some(ref data) = self.images.get(&img.key) {
+                          if data.path.is_some() {
+                              return;
+                          }
+                    }
+
+                    let stride = img.descriptor.stride.unwrap_or(
+                        img.descriptor.width * img.descriptor.format.bytes_per_pixel(),
+                    );
+                    let bytes = match img.data {
+                        ImageData::Raw(ref v) => (**v).clone(),
+                        ImageData::External(_) | ImageData::Blob(_) => {
+                            return;
+                        }
+                    };
+                    self.images.insert(
+                        img.key,
+                        CachedImage {
+                            width: img.descriptor.width,
+                            height: img.descriptor.height,
+                            stride,
+                            format: img.descriptor.format,
+                            bytes: Some(bytes),
+                            tiling: img.tiling,
+                            path: None,
+                        },
+                    );
+                }
+                ResourceUpdate::UpdateImage(ref img) => {
+                    if let Some(ref mut data) = self.images.get_mut(&img.key) {
+                        assert_eq!(data.width, img.descriptor.width);
+                        assert_eq!(data.height, img.descriptor.height);
+                        assert_eq!(data.format, img.descriptor.format);
+
+                        if let ImageData::Raw(ref bytes) = img.data {
+                            data.path = None;
+                            data.bytes = Some((**bytes).clone());
+                        } else {
+                            // Other existing image types only make sense within the gecko integration.
+                            println!(
+                                "Wrench only supports updating buffer images ({}).",
+                                "ignoring update command"
+                            );
+                        }
+                    }
+                }
+                ResourceUpdate::DeleteImage(img) => {
+                    self.images.remove(&img);
+                }
+                ResourceUpdate::AddFont(ref font) => match font {
+                    &AddFont::Raw(key, ref bytes, index) => {
+                        self.fonts
+                            .insert(key, CachedFont::Raw(Some(bytes.clone()), index, None));
+                    }
+                    &AddFont::Native(key, ref handle) => {
+                        self.fonts.insert(key, CachedFont::Native(handle.clone(), None));
+                    }
+                },
+                ResourceUpdate::DeleteFont(_) => {}
+                ResourceUpdate::AddFontInstance(ref instance) => {
+                    self.font_instances.insert(
+                        instance.key,
+                        CachedFontInstance {
+                            font_key: instance.font_key,
+                            glyph_size: instance.glyph_size,
+                        },
+                    );
+                }
+                ResourceUpdate::DeleteFontInstance(_) => {}
+            }
+        }
+    }
+
+
+
+    fn path_for_image(&mut self, key: ImageKey) -> Option<PathBuf> {
+        let data = match self.images.get_mut(&key) {
+            Some(data) => data,
+            None => return None,
+        };
+
+        if data.path.is_some() {
+            return data.path.clone();
+        }
+        let mut bytes = data.bytes.take().unwrap();
+        let (path_file, path) = self.rsrc_gen.next_rsrc_paths(
+            "img",
+            "png",
+        );
+
+        assert!(data.stride > 0);
+        let (color_type, bpp) = match data.format {
+            ImageFormat::BGRA8 => (ColorType::RGBA(8), 4),
+            ImageFormat::R8 => (ColorType::Gray(8), 1),
+            _ => {
+                println!(
+                    "Failed to write image with format {:?}, dimensions {}x{}, stride {}",
+                    data.format,
+                    data.width,
+                    data.height,
+                    data.stride
+                );
+                return None;
+            }
+        };
+
+        if data.stride == data.width * bpp {
+            if data.format == ImageFormat::BGRA8 {
+                unpremultiply(bytes.as_mut_slice());
+            }
+            save_buffer(&path_file, &bytes, data.width, data.height, color_type).unwrap();
+        } else {
+            // takes a buffer with a stride and copies it into a new buffer that has stride == width
+            assert!(data.stride > data.width * bpp);
+            let mut tmp: Vec<_> = bytes[..]
+                .chunks(data.stride as usize)
+                .flat_map(|chunk| {
+                    chunk[.. (data.width * bpp) as usize].iter().cloned()
+                })
+                .collect();
+            if data.format == ImageFormat::BGRA8 {
+                unpremultiply(tmp.as_mut_slice());
+            }
+
+            save_buffer(&path_file, &tmp, data.width, data.height, color_type).unwrap();
+        }
+
+        data.path = Some(path.clone());
+        Some(path)
+    }
+
+    fn make_complex_clip_node(&mut self, complex_clip: &ComplexClipRegion) -> Yaml {
+        let mut t = new_table();
+        rect_node(&mut t, "rect", &complex_clip.rect);
+        yaml_node(
+            &mut t,
+            "radius",
+            maybe_radius_yaml(&complex_clip.radii).unwrap(),
+        );
+        enum_node(&mut t, "clip-mode", complex_clip.mode);
+        Yaml::Hash(t)
+    }
+
+    fn make_complex_clips_node(
+        &mut self,
+        complex_clip_count: usize,
+        complex_clips: ItemRange<ComplexClipRegion>,
+        list: &BuiltDisplayList,
+    ) -> Option<Yaml> {
+        if complex_clip_count == 0 {
+            return None;
+        }
+
+        let complex_items = list.get(complex_clips)
+            .map(|ccx| if ccx.radii.is_zero() {
+                rect_yaml(&ccx.rect)
+            } else {
+                self.make_complex_clip_node(&ccx)
+            })
+            .collect();
+        Some(Yaml::Array(complex_items))
+    }
+
+    fn make_clip_mask_image_node(&mut self, image_mask: &Option<ImageMask>) -> Option<Yaml> {
+        let mask = match image_mask {
+            &Some(ref mask) => mask,
+            &None => return None,
+        };
+
+        let mut mask_table = new_table();
+        if let Some(path) = self.path_for_image(mask.image) {
+            path_node(&mut mask_table, "image", &path);
+        }
+        rect_node(&mut mask_table, "rect", &mask.rect);
+        bool_node(&mut mask_table, "repeat", mask.repeat);
+        Some(Yaml::Hash(mask_table))
+    }
+
+    fn write_display_list_items(
+        &mut self,
+        list: &mut Vec<Yaml>,
+        display_list: &BuiltDisplayList,
+        scene: &Scene,
+        list_iterator: &mut BuiltDisplayListIter,
+        clip_id_mapper: &mut ClipIdMapper,
+    ) {
+        // continue_traversal is a big borrowck hack
+        let mut continue_traversal = None;
+        loop {
+            if let Some(traversal) = continue_traversal.take() {
+                *list_iterator = traversal;
+            }
+            let base = match list_iterator.next() {
+                Some(base) => base,
+                None => break,
+            };
+
+            let mut v = new_table();
+            rect_node(&mut v, "bounds", &base.rect());
+
+            rect_node(&mut v, "clip-rect", base.local_clip().clip_rect());
+            if let &LocalClip::RoundedRect(_, ref region) = base.local_clip() {
+                yaml_node(&mut v, "complex-clip", self.make_complex_clip_node(region));
+            }
+
+            let clip_and_scroll_yaml = match clip_id_mapper.map_info(&base.clip_and_scroll()) {
+                (scroll_id, Some(clip_id)) => {
+                    Yaml::Array(vec![Yaml::Integer(scroll_id), Yaml::Integer(clip_id)])
+                }
+                (scroll_id, None) => Yaml::Integer(scroll_id),
+            };
+            yaml_node(&mut v, "clip-and-scroll", clip_and_scroll_yaml);
+            bool_node(&mut v, "backface-visible", base.is_backface_visible());
+
+            match *base.item() {
+                Rectangle(item) => {
+                    str_node(&mut v, "type", "rect");
+                    color_node(&mut v, "color", item.color);
+                }
+                ClearRectangle => {
+                    str_node(&mut v, "type", "clear-rect");;
+                }
+                Line(item) => {
+                    str_node(&mut v, "type", "line");
+                    if let LineStyle::Wavy = item.style {
+                        f32_node(&mut v, "thickness", item.wavy_line_thickness);
+                    }
+                    str_node(&mut v, "orientation", item.orientation.as_str());
+                    color_node(&mut v, "color", item.color);
+                    str_node(&mut v, "style", item.style.as_str());
+                }
+                Text(item) => {
+                    let gi = display_list.get(base.glyphs());
+                    let mut indices: Vec<u32> = vec![];
+                    let mut offsets: Vec<f32> = vec![];
+                    for g in gi {
+                        indices.push(g.index);
+                        offsets.push(g.point.x);
+                        offsets.push(g.point.y);
+                    }
+                    u32_vec_node(&mut v, "glyphs", &indices);
+                    f32_vec_node(&mut v, "offsets", &offsets);
+
+                    let instance = self.font_instances.entry(item.font_key).or_insert_with(|| {
+                        println!("Warning: font instance key not found in font instances table!");
+                        CachedFontInstance {
+                            font_key: FontKey::new(IdNamespace(0), 0),
+                            glyph_size: Au::from_px(16),
+                        }
+                    });
+
+                    f32_node(
+                        &mut v,
+                        "size",
+                        instance.glyph_size.to_f32_px() * 12.0 / 16.0,
+                    );
+                    color_node(&mut v, "color", item.color);
+
+                    let entry = self.fonts.entry(instance.font_key).or_insert_with(|| {
+                        println!("Warning: font key not found in fonts table!");
+                        CachedFont::Raw(Some(vec![]), 0, None)
+                    });
+
+                    match entry {
+                        &mut CachedFont::Native(ref handle, ref mut path_opt) => {
+                            native_font_handle_to_yaml(&mut self.rsrc_gen, handle, &mut v, path_opt);
+                        }
+                        &mut CachedFont::Raw(ref mut bytes_opt, index, ref mut path_opt) => {
+                            if let Some(bytes) = bytes_opt.take() {
+                                let (path_file, path) = self.rsrc_gen.next_rsrc_paths(
+                                    "font",
+                                    "ttf",
+                                );
+                                let mut file = fs::File::create(&path_file).unwrap();
+                                file.write_all(&bytes).unwrap();
+                                *path_opt = Some(path);
+                            }
+
+                            path_node(&mut v, "font", path_opt.as_ref().unwrap());
+                            if index != 0 {
+                                u32_node(&mut v, "font-index", index);
+                            }
+                        }
+                    }
+                }
+                Image(item) => {
+                    if let Some(path) = self.path_for_image(item.image_key) {
+                        path_node(&mut v, "image", &path);
+                    }
+                    if let Some(&CachedImage {
+                        tiling: Some(tile_size),
+                        ..
+                    }) = self.images.get(&item.image_key)
+                    {
+                        u32_node(&mut v, "tile-size", tile_size as u32);
+                    }
+                    size_node(&mut v, "stretch-size", &item.stretch_size);
+                    size_node(&mut v, "tile-spacing", &item.tile_spacing);
+                    match item.image_rendering {
+                        ImageRendering::Auto => (),
+                        ImageRendering::CrispEdges => str_node(&mut v, "rendering", "crisp-edges"),
+                        ImageRendering::Pixelated => str_node(&mut v, "rendering", "pixelated"),
+                    };
+                    match item.alpha_type {
+                        AlphaType::PremultipliedAlpha => str_node(&mut v, "alpha-type", "premultiplied-alpha"),
+                        AlphaType::Alpha => str_node(&mut v, "alpha-type", "alpha"),
+                    };
+                }
+                YuvImage(_) => {
+                    str_node(&mut v, "type", "yuv-image");
+                    // TODO
+                    println!("TODO YAML YuvImage");
+                }
+                Border(item) => {
+                    str_node(&mut v, "type", "border");
+                    match item.details {
+                        BorderDetails::Normal(ref details) => {
+                            let trbl =
+                                vec![&details.top, &details.right, &details.bottom, &details.left];
+                            let widths: Vec<f32> = vec![
+                                item.widths.top,
+                                item.widths.right,
+                                item.widths.bottom,
+                                item.widths.left,
+                            ];
+                            let colors: Vec<String> =
+                                trbl.iter().map(|x| color_to_string(x.color)).collect();
+                            let styles: Vec<String> = trbl.iter()
+                                .map(|x| {
+                                    match x.style {
+                                        BorderStyle::None => "none",
+                                        BorderStyle::Solid => "solid",
+                                        BorderStyle::Double => "double",
+                                        BorderStyle::Dotted => "dotted",
+                                        BorderStyle::Dashed => "dashed",
+                                        BorderStyle::Hidden => "hidden",
+                                        BorderStyle::Ridge => "ridge",
+                                        BorderStyle::Inset => "inset",
+                                        BorderStyle::Outset => "outset",
+                                        BorderStyle::Groove => "groove",
+                                    }.to_owned()
+                                })
+                                .collect();
+                            yaml_node(&mut v, "width", f32_vec_yaml(&widths, true));
+                            str_node(&mut v, "border-type", "normal");
+                            yaml_node(&mut v, "color", string_vec_yaml(&colors, true));
+                            yaml_node(&mut v, "style", string_vec_yaml(&styles, true));
+                            if let Some(radius_node) = maybe_radius_yaml(&details.radius) {
+                                yaml_node(&mut v, "radius", radius_node);
+                            }
+                        }
+                        BorderDetails::Image(ref details) => {
+                            let widths: Vec<f32> = vec![
+                                item.widths.top,
+                                item.widths.right,
+                                item.widths.bottom,
+                                item.widths.left,
+                            ];
+                            let outset: Vec<f32> = vec![
+                                details.outset.top,
+                                details.outset.right,
+                                details.outset.bottom,
+                                details.outset.left,
+                            ];
+                            yaml_node(&mut v, "width", f32_vec_yaml(&widths, true));
+                            str_node(&mut v, "border-type", "image");
+                            if let Some(path) = self.path_for_image(details.image_key) {
+                                path_node(&mut v, "image", &path);
+                            }
+                            u32_node(&mut v, "image-width", details.patch.width);
+                            u32_node(&mut v, "image-height", details.patch.height);
+                            let slice: Vec<u32> = vec![
+                                details.patch.slice.top,
+                                details.patch.slice.right,
+                                details.patch.slice.bottom,
+                                details.patch.slice.left,
+                            ];
+                            yaml_node(&mut v, "slice", u32_vec_yaml(&slice, true));
+                            yaml_node(&mut v, "outset", f32_vec_yaml(&outset, true));
+                            match details.repeat_horizontal {
+                                RepeatMode::Stretch => {
+                                    str_node(&mut v, "repeat-horizontal", "stretch")
+                                }
+                                RepeatMode::Repeat => {
+                                    str_node(&mut v, "repeat-horizontal", "repeat")
+                                }
+                                RepeatMode::Round => str_node(&mut v, "repeat-horizontal", "round"),
+                                RepeatMode::Space => str_node(&mut v, "repeat-horizontal", "space"),
+                            };
+                            match details.repeat_vertical {
+                                RepeatMode::Stretch => {
+                                    str_node(&mut v, "repeat-vertical", "stretch")
+                                }
+                                RepeatMode::Repeat => str_node(&mut v, "repeat-vertical", "repeat"),
+                                RepeatMode::Round => str_node(&mut v, "repeat-vertical", "round"),
+                                RepeatMode::Space => str_node(&mut v, "repeat-vertical", "space"),
+                            };
+                        }
+                        BorderDetails::Gradient(ref details) => {
+                            let widths: Vec<f32> = vec![
+                                item.widths.top,
+                                item.widths.right,
+                                item.widths.bottom,
+                                item.widths.left,
+                            ];
+                            let outset: Vec<f32> = vec![
+                                details.outset.top,
+                                details.outset.right,
+                                details.outset.bottom,
+                                details.outset.left,
+                            ];
+                            yaml_node(&mut v, "width", f32_vec_yaml(&widths, true));
+                            str_node(&mut v, "border-type", "gradient");
+                            point_node(&mut v, "start", &details.gradient.start_point);
+                            point_node(&mut v, "end", &details.gradient.end_point);
+                            let mut stops = vec![];
+                            for stop in display_list.get(base.gradient_stops()) {
+                                stops.push(Yaml::Real(stop.offset.to_string()));
+                                stops.push(Yaml::String(color_to_string(stop.color)));
+                            }
+                            yaml_node(&mut v, "stops", Yaml::Array(stops));
+                            bool_node(
+                                &mut v,
+                                "repeat",
+                                details.gradient.extend_mode == ExtendMode::Repeat,
+                            );
+                            yaml_node(&mut v, "outset", f32_vec_yaml(&outset, true));
+                        }
+                        BorderDetails::RadialGradient(ref details) => {
+                            let widths: Vec<f32> = vec![
+                                item.widths.top,
+                                item.widths.right,
+                                item.widths.bottom,
+                                item.widths.left,
+                            ];
+                            let outset: Vec<f32> = vec![
+                                details.outset.top,
+                                details.outset.right,
+                                details.outset.bottom,
+                                details.outset.left,
+                            ];
+                            yaml_node(&mut v, "width", f32_vec_yaml(&widths, true));
+                            str_node(&mut v, "border-type", "radial-gradient");
+                            point_node(&mut v, "start-center", &details.gradient.start_center);
+                            f32_node(&mut v, "start-radius", details.gradient.start_radius);
+                            point_node(&mut v, "end-center", &details.gradient.end_center);
+                            f32_node(&mut v, "end-radius", details.gradient.end_radius);
+                            f32_node(&mut v, "ratio-xy", details.gradient.ratio_xy);
+                            let mut stops = vec![];
+                            for stop in display_list.get(base.gradient_stops()) {
+                                stops.push(Yaml::Real(stop.offset.to_string()));
+                                stops.push(Yaml::String(color_to_string(stop.color)));
+                            }
+                            yaml_node(&mut v, "stops", Yaml::Array(stops));
+                            bool_node(
+                                &mut v,
+                                "repeat",
+                                details.gradient.extend_mode == ExtendMode::Repeat,
+                            );
+                            yaml_node(&mut v, "outset", f32_vec_yaml(&outset, true));
+                        }
+                    }
+                }
+                BoxShadow(item) => {
+                    str_node(&mut v, "type", "box-shadow");
+                    rect_node(&mut v, "box-bounds", &item.box_bounds);
+                    vector_node(&mut v, "offset", &item.offset);
+                    color_node(&mut v, "color", item.color);
+                    f32_node(&mut v, "blur-radius", item.blur_radius);
+                    f32_node(&mut v, "spread-radius", item.spread_radius);
+                    if let Some(radius_node) = maybe_radius_yaml(&item.border_radius) {
+                        yaml_node(&mut v, "border-radius", radius_node);
+                    }
+                    let clip_mode = match item.clip_mode {
+                        BoxShadowClipMode::Outset => "outset",
+                        BoxShadowClipMode::Inset => "inset",
+                    };
+                    str_node(&mut v, "clip-mode", clip_mode);
+                }
+                Gradient(item) => {
+                    str_node(&mut v, "type", "gradient");
+                    point_node(&mut v, "start", &item.gradient.start_point);
+                    point_node(&mut v, "end", &item.gradient.end_point);
+                    size_node(&mut v, "tile-size", &item.tile_size);
+                    size_node(&mut v, "tile-spacing", &item.tile_spacing);
+                    let mut stops = vec![];
+                    for stop in display_list.get(base.gradient_stops()) {
+                        stops.push(Yaml::Real(stop.offset.to_string()));
+                        stops.push(Yaml::String(color_to_string(stop.color)));
+                    }
+                    yaml_node(&mut v, "stops", Yaml::Array(stops));
+                    bool_node(
+                        &mut v,
+                        "repeat",
+                        item.gradient.extend_mode == ExtendMode::Repeat,
+                    );
+                }
+                RadialGradient(item) => {
+                    str_node(&mut v, "type", "radial-gradient");
+                    point_node(&mut v, "start-center", &item.gradient.start_center);
+                    f32_node(&mut v, "start-radius", item.gradient.start_radius);
+                    point_node(&mut v, "end-center", &item.gradient.end_center);
+                    f32_node(&mut v, "end-radius", item.gradient.end_radius);
+                    f32_node(&mut v, "ratio-xy", item.gradient.ratio_xy);
+                    size_node(&mut v, "tile-size", &item.tile_size);
+                    size_node(&mut v, "tile-spacing", &item.tile_spacing);
+                    let mut stops = vec![];
+                    for stop in display_list.get(base.gradient_stops()) {
+                        stops.push(Yaml::Real(stop.offset.to_string()));
+                        stops.push(Yaml::String(color_to_string(stop.color)));
+                    }
+                    yaml_node(&mut v, "stops", Yaml::Array(stops));
+                    bool_node(
+                        &mut v,
+                        "repeat",
+                        item.gradient.extend_mode == ExtendMode::Repeat,
+                    );
+                }
+                Iframe(item) => {
+                    str_node(&mut v, "type", "iframe");
+                    u32_vec_node(&mut v, "id", &[item.pipeline_id.0, item.pipeline_id.1]);
+                }
+                PushStackingContext(item) => {
+                    str_node(&mut v, "type", "stacking-context");
+                    let filters = display_list.get(base.filters());
+                    write_sc(&mut v, &item.stacking_context, &scene.properties, filters);
+
+                    let mut sub_iter = base.sub_iter();
+                    self.write_display_list(&mut v, display_list, scene, &mut sub_iter, clip_id_mapper);
+                    continue_traversal = Some(sub_iter);
+                }
+                Clip(item) => {
+                    str_node(&mut v, "type", "clip");
+                    usize_node(&mut v, "id", clip_id_mapper.add_id(item.id));
+                    size_node(&mut v, "content-size", &base.rect().size);
+
+                    let (complex_clips, complex_clip_count) = base.complex_clip();
+                    if let Some(complex) = self.make_complex_clips_node(
+                        complex_clip_count,
+                        complex_clips,
+                        display_list,
+                    ) {
+                        yaml_node(&mut v, "complex", complex);
+                    }
+
+                    if let Some(mask_yaml) = self.make_clip_mask_image_node(&item.image_mask) {
+                        yaml_node(&mut v, "image-mask", mask_yaml);
+                    }
+                }
+                ClipChain(item) => {
+                    str_node(&mut v, "type", "clip-chain");
+
+                    let id = ClipId::ClipChain(item.id);
+                    u32_node(&mut v, "id", clip_id_mapper.map_id(&id) as u32);
+
+                    let clip_ids: Vec<u32> = display_list.get(base.clip_chain_items()).map(|clip_id| {
+                        clip_id_mapper.map_id(&clip_id) as u32
+                    }).collect();
+                    u32_vec_node(&mut v, "clips", &clip_ids);
+
+                    if let Some(parent) = item.parent {
+                        let parent = ClipId::ClipChain(parent);
+                        u32_node(&mut v, "parent", clip_id_mapper.map_id(&parent) as u32);
+                    }
+                }
+                ScrollFrame(item) => {
+                    str_node(&mut v, "type", "scroll-frame");
+                    usize_node(&mut v, "id", clip_id_mapper.add_id(item.id));
+                    size_node(&mut v, "content-size", &base.rect().size);
+                    rect_node(&mut v, "bounds", &base.local_clip().clip_rect());
+
+                    let (complex_clips, complex_clip_count) = base.complex_clip();
+                    if let Some(complex) = self.make_complex_clips_node(
+                        complex_clip_count,
+                        complex_clips,
+                        display_list,
+                    ) {
+                        yaml_node(&mut v, "complex", complex);
+                    }
+
+                    if let Some(mask_yaml) = self.make_clip_mask_image_node(&item.image_mask) {
+                        yaml_node(&mut v, "image-mask", mask_yaml);
+                    }
+                }
+                StickyFrame(item) => {
+                    str_node(&mut v, "type", "sticky-frame");
+                    usize_node(&mut v, "id", clip_id_mapper.add_id(item.id));
+                    rect_node(&mut v, "bounds", &base.local_clip().clip_rect());
+
+                    if let Some(margin) = item.margins.top {
+                        f32_node(&mut v, "margin-top", margin);
+                    }
+                    if let Some(margin) = item.margins.bottom {
+                        f32_node(&mut v, "margin-bottom", margin);
+                    }
+                    if let Some(margin) = item.margins.left {
+                        f32_node(&mut v, "margin-left", margin);
+                    }
+                    if let Some(margin) = item.margins.right {
+                        f32_node(&mut v, "margin-right", margin);
+                    }
+
+                    let horizontal = vec![
+                        Yaml::Real(item.horizontal_offset_bounds.min.to_string()),
+                        Yaml::Real(item.horizontal_offset_bounds.max.to_string()),
+                    ];
+                    let vertical = vec![
+                        Yaml::Real(item.vertical_offset_bounds.min.to_string()),
+                        Yaml::Real(item.vertical_offset_bounds.max.to_string()),
+                    ];
+
+                    yaml_node(&mut v, "horizontal-offset-bounds", Yaml::Array(horizontal));
+                    yaml_node(&mut v, "vertical-offset-bounds", Yaml::Array(vertical));
+
+                    let applied = vec![
+                        Yaml::Real(item.previously_applied_offset.x.to_string()),
+                        Yaml::Real(item.previously_applied_offset.y.to_string()),
+                    ];
+                    yaml_node(&mut v, "previously-applied-offset", Yaml::Array(applied));
+                }
+
+                PopStackingContext => return,
+                SetGradientStops => panic!("dummy item yielded?"),
+                PushShadow(shadow) => {
+                    str_node(&mut v, "type", "shadow");
+                    vector_node(&mut v, "offset", &shadow.offset);
+                    color_node(&mut v, "color", shadow.color);
+                    f32_node(&mut v, "blur-radius", shadow.blur_radius);
+                }
+                PopAllShadows => {
+                    str_node(&mut v, "type", "pop-all-shadows");
+                }
+            }
+            if !v.is_empty() {
+                list.push(Yaml::Hash(v));
+            }
+        }
+    }
+
+    fn write_display_list(
+        &mut self,
+        parent: &mut Table,
+        display_list: &BuiltDisplayList,
+        scene: &Scene,
+        list_iterator: &mut BuiltDisplayListIter,
+        clip_id_mapper: &mut ClipIdMapper,
+    ) {
+        let mut list = vec![];
+        self.write_display_list_items(&mut list, display_list, scene, list_iterator, clip_id_mapper);
+        parent.insert(Yaml::String("items".to_owned()), Yaml::Array(list));
+    }
+}
+
+impl webrender::ApiRecordingReceiver for YamlFrameWriterReceiver {
+    fn write_msg(&mut self, _: u32, msg: &ApiMsg) {
+        match *msg {
+            ApiMsg::UpdateResources(ref updates) => {
+                self.frame_writer.update_resources(updates);
+            }
+            ApiMsg::UpdateDocument(_, ref doc_msgs) => {
+                for doc_msg in doc_msgs {
+                    match *doc_msg {
+                        DocumentMsg::UpdateResources(ref resources) => {
+                            self.frame_writer.update_resources(resources);
+                        }
+                        DocumentMsg::SetDisplayList {
+                            ref epoch,
+                            ref pipeline_id,
+                            ref background,
+                            ref viewport_size,
+                            ref list_descriptor,
+                            ..
+                        } => {
+                            self.frame_writer.begin_write_display_list(
+                                &mut self.scene,
+                                epoch,
+                                pipeline_id,
+                                background,
+                                viewport_size,
+                                list_descriptor,
+                            );
+                        }
+                        DocumentMsg::SetRootPipeline(ref pipeline_id) => {
+                            self.scene.set_root_pipeline_id(pipeline_id.clone());
+                        }
+                        DocumentMsg::RemovePipeline(ref pipeline_id) => {
+                            self.scene.remove_pipeline(pipeline_id);
+                        }
+                        DocumentMsg::UpdateDynamicProperties(ref properties) => {
+                            self.scene.properties.set_properties(properties);
+                        }
+                        _ => {}
+                    }
+                }
+            }
+            _ => {}
+        }
+    }
+
+    fn write_payload(&mut self, _frame: u32, data: &[u8]) {
+        if self.frame_writer.dl_descriptor.is_some() {
+            self.frame_writer
+                .finish_write_display_list(&mut self.scene, data);
+        }
+    }
+}
+
+/// This structure allows mapping both `Clip` and `ClipExternalId`
+/// `ClipIds` onto one set of numeric ids. This prevents ids
+/// from clashing in the yaml output.
+struct ClipIdMapper {
+    hash_map: HashMap<ClipId, usize>,
+    current_clip_id: usize,
+}
+
+impl ClipIdMapper {
+    fn new() -> ClipIdMapper {
+        ClipIdMapper {
+            hash_map: HashMap::new(),
+            current_clip_id: 1,
+        }
+    }
+
+    fn add_id(&mut self, id: ClipId) -> usize {
+        self.hash_map.insert(id, self.current_clip_id);
+        self.current_clip_id += 1;
+        self.current_clip_id - 1
+    }
+
+    fn map_id(&self, id: &ClipId) -> usize {
+        if id.is_root_scroll_node() {
+            return 0;
+        }
+        *self.hash_map.get(id).unwrap()
+    }
+
+    fn map_info(&self, info: &ClipAndScrollInfo) -> (i64, Option<i64>) {
+        (
+            self.map_id(&info.scroll_node_id) as i64,
+            info.clip_node_id.map(|ref id| self.map_id(id) as i64),
+        )
+    }
+}
new file mode 100644
--- /dev/null
+++ b/gfx/wrench/src/yaml_helper.rs
@@ -0,0 +1,577 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use app_units::Au;
+use euclid::{Angle, TypedSize2D};
+use parse_function::parse_function;
+use std::f32;
+use std::str::FromStr;
+use webrender::api::*;
+use yaml_rust::{Yaml, YamlLoader};
+
+pub trait YamlHelper {
+    fn as_f32(&self) -> Option<f32>;
+    fn as_force_f32(&self) -> Option<f32>;
+    fn as_vec_f32(&self) -> Option<Vec<f32>>;
+    fn as_vec_u32(&self) -> Option<Vec<u32>>;
+    fn as_vec_u64(&self) -> Option<Vec<u64>>;
+    fn as_pipeline_id(&self) -> Option<PipelineId>;
+    fn as_rect(&self) -> Option<LayoutRect>;
+    fn as_size(&self) -> Option<LayoutSize>;
+    fn as_point(&self) -> Option<LayoutPoint>;
+    fn as_vector(&self) -> Option<LayoutVector2D>;
+    fn as_matrix4d(&self) -> Option<LayoutTransform>;
+    fn as_transform(&self, transform_origin: &LayoutPoint) -> Option<LayoutTransform>;
+    fn as_colorf(&self) -> Option<ColorF>;
+    fn as_vec_colorf(&self) -> Option<Vec<ColorF>>;
+    fn as_px_to_au(&self) -> Option<Au>;
+    fn as_pt_to_au(&self) -> Option<Au>;
+    fn as_vec_string(&self) -> Option<Vec<String>>;
+    fn as_border_radius_component(&self) -> LayoutSize;
+    fn as_border_radius(&self) -> Option<BorderRadius>;
+    fn as_transform_style(&self) -> Option<TransformStyle>;
+    fn as_clip_mode(&self) -> Option<ClipMode>;
+    fn as_mix_blend_mode(&self) -> Option<MixBlendMode>;
+    fn as_scroll_policy(&self) -> Option<ScrollPolicy>;
+    fn as_filter_op(&self) -> Option<FilterOp>;
+    fn as_vec_filter_op(&self) -> Option<Vec<FilterOp>>;
+}
+
+fn string_to_color(color: &str) -> Option<ColorF> {
+    match color {
+        "red" => Some(ColorF::new(1.0, 0.0, 0.0, 1.0)),
+        "green" => Some(ColorF::new(0.0, 1.0, 0.0, 1.0)),
+        "blue" => Some(ColorF::new(0.0, 0.0, 1.0, 1.0)),
+        "white" => Some(ColorF::new(1.0, 1.0, 1.0, 1.0)),
+        "black" => Some(ColorF::new(0.0, 0.0, 0.0, 1.0)),
+        "yellow" => Some(ColorF::new(1.0, 1.0, 0.0, 1.0)),
+        s => {
+            let items: Vec<f32> = s.split_whitespace()
+                .map(|s| f32::from_str(s).unwrap())
+                .collect();
+            if items.len() == 3 {
+                Some(ColorF::new(
+                    items[0] / 255.0,
+                    items[1] / 255.0,
+                    items[2] / 255.0,
+                    1.0,
+                ))
+            } else if items.len() == 4 {
+                Some(ColorF::new(
+                    items[0] / 255.0,
+                    items[1] / 255.0,
+                    items[2] / 255.0,
+                    items[3],
+                ))
+            } else {
+                None
+            }
+        }
+    }
+}
+
+pub trait StringEnum: Sized {
+    fn from_str(&str) -> Option<Self>;
+    fn as_str(&self) -> &'static str;
+}
+
+macro_rules! define_string_enum {
+    ($T:ident, [ $( $y:ident = $x:expr ),* ]) => {
+        impl StringEnum for $T {
+            fn from_str(text: &str) -> Option<$T> {
+                match text {
+                $( $x => Some($T::$y), )*
+                    _ => {
+                        println!("Unrecognized {} value '{}'", stringify!($T), text);
+                        None
+                    }
+                }
+            }
+            fn as_str(&self) -> &'static str {
+                match *self {
+                $( $T::$y => $x, )*
+                }
+            }
+        }
+    }
+}
+
+define_string_enum!(TransformStyle, [Flat = "flat", Preserve3D = "preserve-3d"]);
+
+define_string_enum!(
+    MixBlendMode,
+    [
+        Normal = "normal",
+        Multiply = "multiply",
+        Screen = "screen",
+        Overlay = "overlay",
+        Darken = "darken",
+        Lighten = "lighten",
+        ColorDodge = "color-dodge",
+        ColorBurn = "color-burn",
+        HardLight = "hard-light",
+        SoftLight = "soft-light",
+        Difference = "difference",
+        Exclusion = "exclusion",
+        Hue = "hue",
+        Saturation = "saturation",
+        Color = "color",
+        Luminosity = "luminosity"
+    ]
+);
+
+define_string_enum!(ScrollPolicy, [Scrollable = "scrollable", Fixed = "fixed"]);
+
+define_string_enum!(
+    LineOrientation,
+    [Horizontal = "horizontal", Vertical = "vertical"]
+);
+
+define_string_enum!(
+    LineStyle,
+    [
+        Solid = "solid",
+        Dotted = "dotted",
+        Dashed = "dashed",
+        Wavy = "wavy"
+    ]
+);
+
+define_string_enum!(ClipMode, [Clip = "clip", ClipOut = "clip-out"]);
+
+// Rotate around `axis` by `degrees` angle
+fn make_rotation(
+    origin: &LayoutPoint,
+    degrees: f32,
+    axis_x: f32,
+    axis_y: f32,
+    axis_z: f32,
+) -> LayoutTransform {
+    let pre_transform = LayoutTransform::create_translation(origin.x, origin.y, 0.0);
+    let post_transform = LayoutTransform::create_translation(-origin.x, -origin.y, -0.0);
+
+    let theta = 2.0f32 * f32::consts::PI - degrees.to_radians();
+    let transform =
+        LayoutTransform::identity().pre_rotate(axis_x, axis_y, axis_z, Angle::radians(theta));
+
+    pre_transform.pre_mul(&transform).pre_mul(&post_transform)
+}
+
+// Create a skew matrix, specified in degrees.
+fn make_skew(
+    skew_x: f32,
+    skew_y: f32,
+) -> LayoutTransform {
+    let alpha = Angle::radians(skew_x.to_radians());
+    let beta = Angle::radians(skew_y.to_radians());
+    LayoutTransform::create_skew(alpha, beta)
+}
+
+impl YamlHelper for Yaml {
+    fn as_f32(&self) -> Option<f32> {
+        match *self {
+            Yaml::Integer(iv) => Some(iv as f32),
+            Yaml::Real(ref sv) => f32::from_str(sv.as_str()).ok(),
+            _ => None,
+        }
+    }
+
+    fn as_force_f32(&self) -> Option<f32> {
+        match *self {
+            Yaml::Integer(iv) => Some(iv as f32),
+            Yaml::String(ref sv) | Yaml::Real(ref sv) => f32::from_str(sv.as_str()).ok(),
+            _ => None,
+        }
+    }
+
+    fn as_vec_f32(&self) -> Option<Vec<f32>> {
+        match *self {
+            Yaml::String(ref s) | Yaml::Real(ref s) => s.split_whitespace()
+                .map(|v| f32::from_str(v))
+                .collect::<Result<Vec<_>, _>>()
+                .ok(),
+            Yaml::Array(ref v) => v.iter()
+                .map(|v| match *v {
+                    Yaml::Integer(k) => Ok(k as f32),
+                    Yaml::String(ref k) | Yaml::Real(ref k) => f32::from_str(k).map_err(|_| false),
+                    _ => Err(false),
+                })
+                .collect::<Result<Vec<_>, _>>()
+                .ok(),
+            Yaml::Integer(k) => Some(vec![k as f32]),
+            _ => None,
+        }
+    }
+
+    fn as_vec_u32(&self) -> Option<Vec<u32>> {
+        if let Some(v) = self.as_vec() {
+            Some(v.iter().map(|v| v.as_i64().unwrap() as u32).collect())
+        } else {
+            None
+        }
+    }
+
+    fn as_vec_u64(&self) -> Option<Vec<u64>> {
+        if let Some(v) = self.as_vec() {
+            Some(v.iter().map(|v| v.as_i64().unwrap() as u64).collect())
+        } else {
+            None
+        }
+    }
+
+    fn as_pipeline_id(&self) -> Option<PipelineId> {
+        if let Some(v) = self.as_vec() {
+            let a = v.get(0).and_then(|v| v.as_i64()).map(|v| v as u32);
+            let b = v.get(1).and_then(|v| v.as_i64()).map(|v| v as u32);
+            match (a, b) {
+                (Some(a), Some(b)) if v.len() == 2 => Some(PipelineId(a, b)),
+                _ => None,
+            }
+        } else {
+            None
+        }
+    }
+
+    fn as_px_to_au(&self) -> Option<Au> {
+        match self.as_force_f32() {
+            Some(fv) => Some(Au::from_f32_px(fv)),
+            None => None,
+        }
+    }
+
+    fn as_pt_to_au(&self) -> Option<Au> {
+        match self.as_force_f32() {
+            Some(fv) => Some(Au::from_f32_px(fv * 16. / 12.)),
+            None => None,
+        }
+    }
+
+    fn as_rect(&self) -> Option<LayoutRect> {
+        if self.is_badvalue() {
+            return None;
+        }
+
+        if let Some(nums) = self.as_vec_f32() {
+            if nums.len() == 4 {
+                return Some(LayoutRect::new(
+                    LayoutPoint::new(nums[0], nums[1]),
+                    LayoutSize::new(nums[2], nums[3]),
+                ));
+            }
+        }
+
+        None
+    }
+
+    fn as_size(&self) -> Option<LayoutSize> {
+        if self.is_badvalue() {
+            return None;
+        }
+
+        if let Some(nums) = self.as_vec_f32() {
+            if nums.len() == 2 {
+                return Some(LayoutSize::new(nums[0], nums[1]));
+            }
+        }
+
+        None
+    }
+
+    fn as_point(&self) -> Option<LayoutPoint> {
+        if self.is_badvalue() {
+            return None;
+        }
+
+        if let Some(nums) = self.as_vec_f32() {
+            if nums.len() == 2 {
+                return Some(LayoutPoint::new(nums[0], nums[1]));
+            }
+        }
+
+        None
+    }
+
+    fn as_vector(&self) -> Option<LayoutVector2D> {
+        self.as_point().map(|p| p.to_vector())
+    }
+
+    fn as_matrix4d(&self) -> Option<LayoutTransform> {
+        if let Some(nums) = self.as_vec_f32() {
+            assert_eq!(nums.len(), 16, "expected 16 floats, got '{:?}'", self);
+            Some(LayoutTransform::row_major(
+                nums[0],
+                nums[1],
+                nums[2],
+                nums[3],
+                nums[4],
+                nums[5],
+                nums[6],
+                nums[7],
+                nums[8],
+                nums[9],
+                nums[10],
+                nums[11],
+                nums[12],
+                nums[13],
+                nums[14],
+                nums[15],
+            ))
+        } else {
+            None
+        }
+    }
+
+    fn as_transform(&self, transform_origin: &LayoutPoint) -> Option<LayoutTransform> {
+        if let Some(transform) = self.as_matrix4d() {
+            return Some(transform);
+        }
+
+        match *self {
+            Yaml::String(ref string) => {
+                let mut slice = string.as_str();
+                let mut transform = LayoutTransform::identity();
+                while !slice.is_empty() {
+                    let (function, ref args, reminder) = parse_function(slice);
+                    slice = reminder;
+                    let mx = match function {
+                        "translate" if args.len() >= 2 => {
+                            let z = args.get(2).and_then(|a| a.parse().ok()).unwrap_or(0.);
+                            LayoutTransform::create_translation(
+                                args[0].parse().unwrap(),
+                                args[1].parse().unwrap(),
+                                z,
+                            )
+                        }
+                        "rotate" | "rotate-z" if args.len() == 1 => {
+                            make_rotation(transform_origin, args[0].parse().unwrap(), 0.0, 0.0, 1.0)
+                        }
+                        "rotate-x" if args.len() == 1 => {
+                            make_rotation(transform_origin, args[0].parse().unwrap(), 1.0, 0.0, 0.0)
+                        }
+                        "rotate-y" if args.len() == 1 => {
+                            make_rotation(transform_origin, args[0].parse().unwrap(), 0.0, 1.0, 0.0)
+                        }
+                        "scale" if args.len() >= 1 => {
+                            let x = args[0].parse().unwrap();
+                            // Default to uniform X/Y scale if Y unspecified.
+                            let y = args.get(1).and_then(|a| a.parse().ok()).unwrap_or(x);
+                            // Default to no Z scale if unspecified.
+                            let z = args.get(2).and_then(|a| a.parse().ok()).unwrap_or(1.0);
+                            LayoutTransform::create_scale(x, y, z)
+                        }
+                        "scale-x" if args.len() == 1 => {
+                            LayoutTransform::create_scale(args[0].parse().unwrap(), 1.0, 1.0)
+                        }
+                        "scale-y" if args.len() == 1 => {
+                            LayoutTransform::create_scale(1.0, args[0].parse().unwrap(), 1.0)
+                        }
+                        "scale-z" if args.len() == 1 => {
+                            LayoutTransform::create_scale(1.0, 1.0, args[0].parse().unwrap())
+                        }
+                        "skew" if args.len() >= 1 => {
+                            // Default to no Y skew if unspecified.
+                            let skew_y = args.get(1).and_then(|a| a.parse().ok()).unwrap_or(0.0);
+                            make_skew(args[0].parse().unwrap(), skew_y)
+                        }
+                        "skew-x" if args.len() == 1 => {
+                            make_skew(args[0].parse().unwrap(), 0.0)
+                        }
+                        "skew-y" if args.len() == 1 => {
+                            make_skew(0.0, args[0].parse().unwrap())
+                        }
+                        "perspective" if args.len() == 1 => {
+                            LayoutTransform::create_perspective(args[0].parse().unwrap())
+                        }
+                        _ => {
+                            println!("unknown function {}", function);
+                            break;
+                        }
+                    };
+                    transform = transform.post_mul(&mx);
+                }
+                Some(transform)
+            }
+            Yaml::Array(ref array) => {
+                let transform = array.iter().fold(
+                    LayoutTransform::identity(),
+                    |u, yaml| match yaml.as_transform(transform_origin) {
+                        Some(ref transform) => u.pre_mul(transform),
+                        None => u,
+                    },
+                );
+                Some(transform)
+            }
+            Yaml::BadValue => None,
+            _ => {
+                println!("unknown transform {:?}", self);
+                None
+            }
+        }
+    }
+
+    fn as_colorf(&self) -> Option<ColorF> {
+        if let Some(mut nums) = self.as_vec_f32() {
+            assert!(
+                nums.len() == 3 || nums.len() == 4,
+                "color expected a color name, or 3-4 floats; got '{:?}'",
+                self
+            );
+
+            if nums.len() == 3 {
+                nums.push(1.0);
+            }
+            return Some(ColorF::new(
+                nums[0] / 255.0,
+                nums[1] / 255.0,
+                nums[2] / 255.0,
+                nums[3],
+            ));
+        }
+
+        if let Some(s) = self.as_str() {
+            string_to_color(s)
+        } else {
+            None
+        }
+    }
+
+    fn as_vec_colorf(&self) -> Option<Vec<ColorF>> {
+        if let Some(v) = self.as_vec() {
+            Some(v.iter().map(|v| v.as_colorf().unwrap()).collect())
+        } else if let Some(color) = self.as_colorf() {
+            Some(vec![color])
+        } else {
+            None
+        }
+    }
+
+    fn as_vec_string(&self) -> Option<Vec<String>> {
+        if let Some(v) = self.as_vec() {
+            Some(v.iter().map(|v| v.as_str().unwrap().to_owned()).collect())
+        } else if let Some(s) = self.as_str() {
+            Some(vec![s.to_owned()])
+        } else {
+            None
+        }
+    }
+
+    fn as_border_radius_component(&self) -> LayoutSize {
+        if let Yaml::Integer(integer) = *self {
+            return LayoutSize::new(integer as f32, integer as f32);
+        }
+        self.as_size().unwrap_or(TypedSize2D::zero())
+    }
+
+    fn as_border_radius(&self) -> Option<BorderRadius> {
+        if let Some(size) = self.as_size() {
+            return Some(BorderRadius::uniform_size(size));
+        }
+
+        match *self {
+            Yaml::BadValue => None,
+            Yaml::String(ref s) | Yaml::Real(ref s) => {
+                let fv = f32::from_str(s).unwrap();
+                Some(BorderRadius::uniform(fv))
+            }
+            Yaml::Integer(v) => Some(BorderRadius::uniform(v as f32)),
+            Yaml::Array(ref array) if array.len() == 4 => {
+                let top_left = array[0].as_border_radius_component();
+                let top_right = array[1].as_border_radius_component();
+                let bottom_left = array[2].as_border_radius_component();
+                let bottom_right = array[3].as_border_radius_component();
+                Some(BorderRadius {
+                    top_left,
+                    top_right,
+                    bottom_left,
+                    bottom_right,
+                })
+            }
+            Yaml::Hash(_) => {
+                let top_left = self["top-left"].as_border_radius_component();
+                let top_right = self["top-right"].as_border_radius_component();
+                let bottom_left = self["bottom-left"].as_border_radius_component();
+                let bottom_right = self["bottom-right"].as_border_radius_component();
+                Some(BorderRadius {
+                    top_left,
+                    top_right,
+                    bottom_left,
+                    bottom_right,
+                })
+            }
+            _ => {
+                panic!("Invalid border radius specified: {:?}", self);
+            }
+        }
+    }
+
+    fn as_transform_style(&self) -> Option<TransformStyle> {
+        self.as_str().and_then(|x| StringEnum::from_str(x))
+    }
+
+    fn as_mix_blend_mode(&self) -> Option<MixBlendMode> {
+        self.as_str().and_then(|x| StringEnum::from_str(x))
+    }
+
+    fn as_scroll_policy(&self) -> Option<ScrollPolicy> {
+        self.as_str().and_then(|x| StringEnum::from_str(x))
+    }
+
+    fn as_clip_mode(&self) -> Option<ClipMode> {
+        self.as_str().and_then(|x| StringEnum::from_str(x))
+    }
+
+    fn as_filter_op(&self) -> Option<FilterOp> {
+        if let Some(s) = self.as_str() {
+            match parse_function(s) {
+                ("blur", ref args, _) if args.len() == 1 => {
+                    Some(FilterOp::Blur(args[0].parse().unwrap()))
+                }
+                ("brightness", ref args, _) if args.len() == 1 => {
+                    Some(FilterOp::Brightness(args[0].parse().unwrap()))
+                }
+                ("contrast", ref args, _) if args.len() == 1 => {
+                    Some(FilterOp::Contrast(args[0].parse().unwrap()))
+                }
+                ("grayscale", ref args, _) if args.len() == 1 => {
+                    Some(FilterOp::Grayscale(args[0].parse().unwrap()))
+                }
+                ("hue-rotate", ref args, _) if args.len() == 1 => {
+                    Some(FilterOp::HueRotate(args[0].parse().unwrap()))
+                }
+                ("invert", ref args, _) if args.len() == 1 => {
+                    Some(FilterOp::Invert(args[0].parse().unwrap()))
+                }
+                ("opacity", ref args, _) if args.len() == 1 => {
+                    let amount: f32 = args[0].parse().unwrap();
+                    Some(FilterOp::Opacity(amount.into(), amount))
+                }
+                ("saturate", ref args, _) if args.len() == 1 => {
+                    Some(FilterOp::Saturate(args[0].parse().unwrap()))
+                }
+                ("sepia", ref args, _) if args.len() == 1 => {
+                    Some(FilterOp::Sepia(args[0].parse().unwrap()))
+                }
+                ("drop-shadow", ref args, _) if args.len() == 3 => {
+                    let str = format!("---\noffset: {}\nblur-radius: {}\ncolor: {}\n", args[0], args[1], args[2]);
+                    let mut yaml_doc = YamlLoader::load_from_str(&str).expect("Failed to parse drop-shadow");
+                    let yaml = yaml_doc.pop().unwrap();
+                    Some(FilterOp::DropShadow(yaml["offset"].as_vector().unwrap(),
+                                              yaml["blur-radius"].as_f32().unwrap(),
+                                              yaml["color"].as_colorf().unwrap()))
+                }
+                (_, _, _) => None,
+            }
+        } else {
+            None
+        }
+    }
+
+    fn as_vec_filter_op(&self) -> Option<Vec<FilterOp>> {
+        if let Some(v) = self.as_vec() {
+            Some(v.iter().map(|x| x.as_filter_op().unwrap()).collect())
+        } else {
+            self.as_filter_op().map(|op| vec![op])
+        }
+    }
+}