Bug 1492566. Update webrender to f17f6a491d6ff3dc3e13e998dda788d8f5856338
authorJeff Muizelaar <jmuizelaar@mozilla.com>
Thu, 20 Sep 2018 11:05:20 -0400
changeset 437472 14c6b338e32c1da04218311b26e14cd2b9a3102b
parent 437471 a1a0f861a0ae43b0a5668aa292e4705885ff19be
child 437473 3d099fce8fa41d9d93981b743761c88c282a3ce9
push id108071
push userjmuizelaar@mozilla.com
push dateThu, 20 Sep 2018 15:07:09 +0000
treeherdermozilla-inbound@14c6b338e32c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1492566
milestone64.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 1492566. Update webrender to f17f6a491d6ff3dc3e13e998dda788d8f5856338
gfx/webrender/res/brush.glsl
gfx/webrender/res/prim_shared.glsl
gfx/webrender/res/ps_split_composite.glsl
gfx/webrender/res/ps_text_run.glsl
gfx/webrender/src/batch.rs
gfx/webrender/src/border.rs
gfx/webrender/src/box_shadow.rs
gfx/webrender/src/capture.rs
gfx/webrender/src/clip.rs
gfx/webrender/src/display_list_flattener.rs
gfx/webrender/src/frame_builder.rs
gfx/webrender/src/hit_test.rs
gfx/webrender/src/intern.rs
gfx/webrender/src/lib.rs
gfx/webrender/src/prim_store.rs
gfx/webrender/src/render_backend.rs
gfx/webrender/src/render_task.rs
gfx/webrender/src/scene_builder.rs
gfx/webrender/src/tiling.rs
gfx/webrender_api/src/display_item.rs
gfx/webrender_api/src/image.rs
gfx/webrender_api/src/units.rs
gfx/webrender_bindings/revision.txt
gfx/wrench/src/yaml_frame_reader.rs
--- a/gfx/webrender/res/brush.glsl
+++ b/gfx/webrender/res/brush.glsl
@@ -86,16 +86,17 @@ void main(void) {
     // out clip information.
     // TODO(gw): It's possible that we might want alpha
     //           shaders that don't clip in the future,
     //           but it's reasonable to assume that one
     //           implies the other, for now.
 #ifdef WR_FEATURE_ALPHA_PASS
     write_clip(
         vi.world_pos,
+        vi.snap_offset,
         clip_area
     );
 #endif
 
     // Run the specific brush VS code to write interpolators.
     brush_vs(
         vi,
         ph.specific_prim_address,
--- a/gfx/webrender/res/prim_shared.glsl
+++ b/gfx/webrender/res/prim_shared.glsl
@@ -22,16 +22,17 @@ uniform sampler2DArray sCacheRGBA8;
 uniform sampler2DArray sSharedCacheA8;
 
 vec2 clamp_rect(vec2 pt, RectWithSize rect) {
     return clamp(pt, rect.p0, rect.p0 + rect.size);
 }
 
 // TODO: convert back to RectWithEndPoint if driver issues are resolved, if ever.
 flat varying vec4 vClipMaskUvBounds;
+flat varying vec4 vClipMaskUvSampleBounds;
 // XY and W are homogeneous coordinates, Z is the layer index
 varying vec4 vClipMaskUv;
 
 
 #ifdef WR_VERTEX_SHADER
 
 #define COLOR_MODE_FROM_PASS          0
 #define COLOR_MODE_ALPHA              1
@@ -217,24 +218,31 @@ VertexInfo write_transform_vertex(RectWi
         local_pos,
         vec2(0.0),
         world_pos
     );
 
     return vi;
 }
 
-void write_clip(vec4 world_pos, ClipArea area) {
+void write_clip(vec4 world_pos, vec2 snap_offset, ClipArea area) {
     vec2 uv = world_pos.xy * uDevicePixelRatio +
-        world_pos.w * (area.common_data.task_rect.p0 - area.screen_origin);
+        world_pos.w * (snap_offset + area.common_data.task_rect.p0 - area.screen_origin);
     vClipMaskUvBounds = vec4(
         area.common_data.task_rect.p0,
         area.common_data.task_rect.p0 + area.common_data.task_rect.size
     );
+    vClipMaskUvSampleBounds.xy = vClipMaskUvBounds.xy + vec2(0.5);
+    vClipMaskUvSampleBounds.zw = vClipMaskUvBounds.zw - vec2(0.5);
     vClipMaskUv = vec4(uv, area.common_data.texture_layer_index, world_pos.w);
+
+    vec2 texture_size = vec2(textureSize(sCacheA8, 0).xy);
+    vClipMaskUv.xy /= texture_size;
+    vClipMaskUvBounds /= texture_size.xyxy;
+    vClipMaskUvSampleBounds /= texture_size.xyxy;
 }
 #endif //WR_VERTEX_SHADER
 
 #ifdef WR_FRAGMENT_SHADER
 
 float do_clip() {
     // check for the dummy bounds, which are given to the opaque objects
     if (vClipMaskUvBounds.xy == vClipMaskUvBounds.zw) {
@@ -245,19 +253,24 @@ float do_clip() {
     vec2 mask_uv = vClipMaskUv.xy * gl_FragCoord.w;
     bvec4 inside = lessThanEqual(
         vec4(vClipMaskUvBounds.xy, mask_uv),
         vec4(mask_uv, vClipMaskUvBounds.zw));
     // bail out if the pixel is outside the valid bounds
     if (!all(inside)) {
         return 0.0;
     }
+
     // finally, the slow path - fetch the mask value from an image
-    ivec3 tc = ivec3(mask_uv, vClipMaskUv.z);
-    return texelFetch(sCacheA8, tc, 0).r;
+
+    // TODO(gw): texelFetch here fails on some nVidia hardware in
+    //           some cases. For now, just use texture()
+    //           unconditionally.
+    mask_uv = clamp(mask_uv, vClipMaskUvSampleBounds.xy, vClipMaskUvSampleBounds.zw);
+    return texture(sCacheA8, vec3(mask_uv, vClipMaskUv.z)).r;
 }
 
 #ifdef WR_FEATURE_DITHERING
 vec4 dither(vec4 color) {
     const int matrix_mask = 7;
 
     ivec2 pos = ivec2(gl_FragCoord.xy) & ivec2(matrix_mask);
     float noise_normalized = (texelFetch(sDither, pos, 0).r * 255.0 + 0.5) / 64.0;
--- a/gfx/webrender/res/ps_split_composite.glsl
+++ b/gfx/webrender/res/ps_split_composite.glsl
@@ -75,16 +75,17 @@ void main(void) {
     vec4 final_pos = vec4(
         dest_origin * world_pos.w + world_pos.xy * uDevicePixelRatio,
         world_pos.w * ci.z,
         world_pos.w
     );
 
     write_clip(
         world_pos,
+        vec2(0.0),
         clip_area
     );
 
     gl_Position = uTransform * final_pos;
 
     vec2 texture_size = vec2(textureSize(sCacheRGBA8, 0));
     vec2 uv0 = res.uv_rect.p0;
     vec2 uv1 = res.uv_rect.p1;
--- a/gfx/webrender/res/ps_text_run.glsl
+++ b/gfx/webrender/res/ps_text_run.glsl
@@ -224,17 +224,17 @@ void main(void) {
 
 #ifdef WR_FEATURE_GLYPH_TRANSFORM
     vec2 f = (glyph_transform * vi.local_pos - glyph_rect.p0) / glyph_rect.size;
     vUvClip = vec4(f, 1.0 - f);
 #else
     vec2 f = (vi.local_pos - glyph_rect.p0) / glyph_rect.size;
 #endif
 
-    write_clip(vi.world_pos, clip_area);
+    write_clip(vi.world_pos, vi.snap_offset, clip_area);
 
     switch (color_mode) {
         case COLOR_MODE_ALPHA:
         case COLOR_MODE_BITMAP:
             vMaskSwizzle = vec2(0.0, 1.0);
             vColor = text.color;
             break;
         case COLOR_MODE_SUBPX_BG_PASS2:
--- a/gfx/webrender/src/batch.rs
+++ b/gfx/webrender/src/batch.rs
@@ -1,16 +1,16 @@
 /* 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::{AlphaType, ClipMode, DeviceIntRect, DeviceIntSize};
 use api::{DeviceUintRect, DeviceUintPoint, ExternalImageType, FilterOp, ImageRendering};
 use api::{YuvColorSpace, YuvFormat, WorldPixel, WorldRect};
-use clip::{ClipNodeFlags, ClipNodeRange, ClipItem, ClipStore};
+use clip::{ClipDataStore, ClipNodeFlags, ClipNodeRange, ClipItem, ClipStore};
 use clip_scroll_tree::{ClipScrollTree, ROOT_SPATIAL_NODE_INDEX, SpatialNodeIndex};
 use euclid::vec3;
 use glyph_rasterizer::GlyphFormat;
 use gpu_cache::{GpuCache, GpuCacheHandle, GpuCacheAddress};
 use gpu_types::{BrushFlags, BrushInstance, PrimitiveHeaders};
 use gpu_types::{ClipMaskInstance, SplitCompositeInstance};
 use gpu_types::{PrimitiveInstance, RasterizationSpace, GlyphInstance};
 use gpu_types::{PrimitiveHeader, PrimitiveHeaderIndex, TransformPaletteId, TransformPalette};
@@ -1770,22 +1770,24 @@ impl ClipBatcher {
         task_address: RenderTaskAddress,
         clip_node_range: ClipNodeRange,
         root_spatial_node_index: SpatialNodeIndex,
         resource_cache: &ResourceCache,
         gpu_cache: &GpuCache,
         clip_store: &ClipStore,
         clip_scroll_tree: &ClipScrollTree,
         transforms: &mut TransformPalette,
+        clip_data_store: &ClipDataStore,
     ) {
         for i in 0 .. clip_node_range.count {
-            let (clip_node, flags, spatial_node_index) = clip_store.get_node_from_range(&clip_node_range, i);
+            let clip_instance = clip_store.get_instance_from_range(&clip_node_range, i);
+            let clip_node = &clip_data_store[clip_instance.handle];
 
             let clip_transform_id = transforms.get_id(
-                spatial_node_index,
+                clip_instance.spatial_node_index,
                 ROOT_SPATIAL_NODE_INDEX,
                 clip_scroll_tree,
             );
 
             let prim_transform_id = transforms.get_id(
                 root_spatial_node_index,
                 ROOT_SPATIAL_NODE_INDEX,
                 clip_scroll_tree,
@@ -1847,17 +1849,17 @@ impl ClipBatcher {
                         .or_insert(Vec::new())
                         .push(ClipMaskInstance {
                             clip_data_address: gpu_address,
                             resource_address: gpu_cache.get_address(&cache_item.uv_rect_handle),
                             ..instance
                         });
                 }
                 ClipItem::Rectangle(_, mode) => {
-                    if !flags.contains(ClipNodeFlags::SAME_COORD_SYSTEM) ||
+                    if !clip_instance.flags.contains(ClipNodeFlags::SAME_COORD_SYSTEM) ||
                         mode == ClipMode::ClipOut {
                         self.rectangles.push(ClipMaskInstance {
                             clip_data_address: gpu_address,
                             ..instance
                         });
                     }
                 }
                 ClipItem::RoundedRectangle(..) => {
--- a/gfx/webrender/src/border.rs
+++ b/gfx/webrender/src/border.rs
@@ -1,15 +1,16 @@
 /* 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::{BorderRadius, BorderSide, BorderStyle, ColorF, ColorU, DeviceRect, DeviceSize};
-use api::{LayoutSizeAu, LayoutSideOffsets, LayoutPrimitiveInfo, LayoutToDeviceScale};
+use api::{LayoutSideOffsets, LayoutSizeAu, LayoutPrimitiveInfo, LayoutToDeviceScale};
 use api::{DeviceVector2D, DevicePoint, DeviceIntSize, LayoutRect, LayoutSize, NormalBorder};
+use api::{AuHelpers};
 use app_units::Au;
 use ellipse::Ellipse;
 use display_list_flattener::DisplayListFlattener;
 use gpu_types::{BorderInstance, BorderSegment, BrushFlags};
 use prim_store::{BrushKind, BrushPrimitive, BrushSegment};
 use prim_store::{EdgeAaSegmentMask, PrimitiveContainer, ScrollNodeAndClipChain};
 use util::{lerp, RectHelpers};
 
@@ -21,55 +22,64 @@ use util::{lerp, RectHelpers};
 
 /// Maximum resolution in device pixels at which borders are rasterized.
 pub const MAX_BORDER_RESOLUTION: u32 = 2048;
 /// Maximum number of dots or dashes per segment to avoid freezing and filling up
 /// memory with unreasonable inputs. It would be better to address this by not building
 /// a list of per-dot information in the first place.
 pub const MAX_DASH_COUNT: u32 = 2048;
 
-trait AuSizeConverter {
-    fn to_au(&self) -> LayoutSizeAu;
-}
-
-impl AuSizeConverter for LayoutSize {
-    fn to_au(&self) -> LayoutSizeAu {
-        LayoutSizeAu::new(
-            Au::from_f32_px(self.width),
-            Au::from_f32_px(self.height),
-        )
-    }
-}
-
 // TODO(gw): Perhaps there is a better way to store
 //           the border cache key than duplicating
 //           all the border structs with hashable
 //           variants...
 
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct BorderRadiusAu {
     pub top_left: LayoutSizeAu,
     pub top_right: LayoutSizeAu,
     pub bottom_left: LayoutSizeAu,
     pub bottom_right: LayoutSizeAu,
 }
 
+impl BorderRadiusAu {
+    pub fn zero() -> Self {
+        BorderRadiusAu {
+            top_left: LayoutSizeAu::zero(),
+            top_right: LayoutSizeAu::zero(),
+            bottom_left: LayoutSizeAu::zero(),
+            bottom_right: LayoutSizeAu::zero(),
+        }
+    }
+}
+
 impl From<BorderRadius> for BorderRadiusAu {
     fn from(radius: BorderRadius) -> BorderRadiusAu {
         BorderRadiusAu {
             top_left: radius.top_left.to_au(),
             top_right: radius.top_right.to_au(),
             bottom_right: radius.bottom_right.to_au(),
             bottom_left: radius.bottom_left.to_au(),
         }
     }
 }
 
+impl From<BorderRadiusAu> for BorderRadius {
+    fn from(radius: BorderRadiusAu) -> Self {
+        BorderRadius {
+            top_left: LayoutSize::from_au(radius.top_left),
+            top_right: LayoutSize::from_au(radius.top_right),
+            bottom_right: LayoutSize::from_au(radius.bottom_right),
+            bottom_left: LayoutSize::from_au(radius.bottom_left),
+        }
+    }
+}
+
 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct BorderWidthsAu {
     pub left: Au,
     pub top: Au,
     pub right: Au,
     pub bottom: Au,
--- a/gfx/webrender/src/box_shadow.rs
+++ b/gfx/webrender/src/box_shadow.rs
@@ -1,24 +1,26 @@
 /* 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::{BorderRadius, BoxShadowClipMode, ClipMode, ColorF, DeviceIntSize, LayoutPrimitiveInfo};
 use api::{LayoutRect, LayoutSize, LayoutVector2D};
-use clip::ClipItem;
+use clip::ClipItemKey;
 use display_list_flattener::DisplayListFlattener;
 use gpu_cache::GpuCacheHandle;
 use gpu_types::BoxShadowStretchMode;
 use prim_store::{BrushKind, BrushPrimitive, PrimitiveContainer};
 use prim_store::ScrollNodeAndClipChain;
 use render_task::RenderTaskCacheEntryHandle;
 use util::RectHelpers;
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct BoxShadowClipSource {
     // Parameters that define the shadow and are constant.
     pub shadow_radius: BorderRadius,
     pub blur_radius: f32,
     pub clip_mode: BoxShadowClipMode,
     pub stretch_mode_x: BoxShadowStretchMode,
     pub stretch_mode_y: BoxShadowStretchMode,
 
@@ -117,38 +119,42 @@ impl<'a> DisplayListFlattener<'a> {
             let mut clips = Vec::with_capacity(2);
             let (final_prim_rect, clip_radius) = match clip_mode {
                 BoxShadowClipMode::Outset => {
                     if !shadow_rect.is_well_formed_and_nonempty() {
                         return;
                     }
 
                     // TODO(gw): Add a fast path for ClipOut + zero border radius!
-                    clips.push(ClipItem::new_rounded_rect(
+                    clips.push(ClipItemKey::rounded_rect(
                         prim_info.rect,
                         border_radius,
                         ClipMode::ClipOut
                     ));
 
                     (shadow_rect, shadow_radius)
                 }
                 BoxShadowClipMode::Inset => {
                     if shadow_rect.is_well_formed_and_nonempty() {
-                        clips.push(ClipItem::new_rounded_rect(
+                        clips.push(ClipItemKey::rounded_rect(
                             shadow_rect,
                             shadow_radius,
                             ClipMode::ClipOut
                         ));
                     }
 
                     (prim_info.rect, border_radius)
                 }
             };
 
-            clips.push(ClipItem::new_rounded_rect(final_prim_rect, clip_radius, ClipMode::Clip));
+            clips.push(ClipItemKey::rounded_rect(
+                final_prim_rect,
+                clip_radius,
+                ClipMode::Clip,
+            ));
 
             self.add_primitive(
                 clip_and_scroll,
                 &LayoutPrimitiveInfo::with_clip_rect(final_prim_rect, prim_info.clip_rect),
                 clips,
                 PrimitiveContainer::Brush(
                     BrushPrimitive::new(
                         BrushKind::new_solid(*color),
@@ -158,17 +164,17 @@ impl<'a> DisplayListFlattener<'a> {
             );
         } else {
             // Normal path for box-shadows with a valid blur radius.
             let blur_offset = BLUR_SAMPLE_SCALE * blur_radius;
             let mut extra_clips = vec![];
 
             // Add a normal clip mask to clip out the contents
             // of the surrounding primitive.
-            extra_clips.push(ClipItem::new_rounded_rect(
+            extra_clips.push(ClipItemKey::rounded_rect(
                 prim_info.rect,
                 border_radius,
                 prim_clip_mode,
             ));
 
             // Get the local rect of where the shadow will be drawn,
             // expanded to include room for the blurred region.
             let dest_rect = shadow_rect.inflate(blur_offset, blur_offset);
@@ -176,17 +182,17 @@ impl<'a> DisplayListFlattener<'a> {
             // Draw the box-shadow as a solid rect, using a box-shadow
             // clip mask item.
             let prim = BrushPrimitive::new(
                 BrushKind::new_solid(*color),
                 None,
             );
 
             // Create the box-shadow clip item.
-            let shadow_clip_source = ClipItem::new_box_shadow(
+            let shadow_clip_source = ClipItemKey::box_shadow(
                 shadow_rect,
                 shadow_radius,
                 dest_rect,
                 blur_radius,
                 clip_mode,
             );
 
             let prim_info = match clip_mode {
--- a/gfx/webrender/src/capture.rs
+++ b/gfx/webrender/src/capture.rs
@@ -7,16 +7,17 @@ use std::path::{Path, PathBuf};
 
 use api::{CaptureBits, ExternalImageData, ImageDescriptor, TexelRect};
 #[cfg(feature = "png")]
 use device::ReadPixelsFormat;
 use ron;
 use serde;
 
 
+#[derive(Clone)]
 pub struct CaptureConfig {
     pub root: PathBuf,
     pub bits: CaptureBits,
     #[cfg(feature = "capture")]
     pretty: ron::ser::PrettyConfig,
 }
 
 impl CaptureConfig {
--- a/gfx/webrender/src/clip.rs
+++ b/gfx/webrender/src/clip.rs
@@ -1,23 +1,25 @@
 /* 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::{BorderRadius, ClipMode, ComplexClipRegion, DeviceIntRect, DevicePixelScale, ImageMask};
 use api::{ImageRendering, LayoutRect, LayoutSize, LayoutPoint, LayoutVector2D, LocalClip};
 use api::{BoxShadowClipMode, LayoutToWorldScale, LineOrientation, LineStyle, PicturePixel, WorldPixel};
 use api::{PictureRect, LayoutPixel, WorldPoint, WorldSize, WorldRect, LayoutToWorldTransform};
-use api::{VoidPtrToSizeFn};
-use border::{ensure_no_corner_overlap};
+use api::{VoidPtrToSizeFn, LayoutRectAu, ImageKey, AuHelpers};
+use app_units::Au;
+use border::{ensure_no_corner_overlap, BorderRadiusAu};
 use box_shadow::{BLUR_SAMPLE_SCALE, BoxShadowClipSource, BoxShadowCacheKey};
 use clip_scroll_tree::{ClipScrollTree, CoordinateSystemId, ROOT_SPATIAL_NODE_INDEX, SpatialNodeIndex};
 use ellipse::Ellipse;
 use gpu_cache::{GpuCache, GpuCacheHandle, ToGpuBlocks};
 use gpu_types::{BoxShadowStretchMode};
+use intern;
 use internal_types::FastHashSet;
 use prim_store::{ClipData, ImageMaskData, SpaceMapper};
 use render_task::to_cache_size;
 use resource_cache::{ImageRequest, ResourceCache};
 use std::{cmp, u32};
 use std::os::raw::c_void;
 use util::{extract_inner_rect_safe, pack_as_float, project_rect, ScaleOffset};
 
@@ -85,39 +87,105 @@ use util::{extract_inner_rect_safe, pack
     | ClipNodeInstance | ClipNodeInstance | ClipNodeInstance | ClipNodeInstance | ClipNodeInstance |
     +------------------+------------------+------------------+------------------+------------------+
     | flags            | flags            | flags            | flags            | flags            |
     | ClipNodeIndex    | ClipNodeIndex    | ClipNodeIndex    | ClipNodeIndex    | ClipNodeIndex    |
     +------------------+------------------+------------------+------------------+------------------+
 
  */
 
+// Type definitions for interning clip nodes.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Clone, Copy, Debug)]
+pub struct ClipDataMarker;
+
+pub type ClipDataStore = intern::DataStore<ClipItemKey, ClipNode, ClipDataMarker>;
+pub type ClipDataHandle = intern::Handle<ClipDataMarker>;
+pub type ClipDataUpdateList = intern::UpdateList<ClipItemKey>;
+pub type ClipDataInterner = intern::Interner<ClipItemKey, ClipDataMarker>;
+
 // Result of comparing a clip node instance against a local rect.
 #[derive(Debug)]
 enum ClipResult {
     // The clip does not affect the region at all.
     Accept,
     // The clip prevents the region from being drawn.
     Reject,
     // The clip affects part of the region. This may
     // require a clip mask, depending on other factors.
     Partial,
 }
 
 // A clip node is a single clip source, along with some
 // positioning information and implementation details
 // that control where the GPU data for this clip source
 // can be found.
+#[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct ClipNode {
     pub item: ClipItem,
     pub gpu_cache_handle: GpuCacheHandle,
 }
 
+// Convert from an interning key for a clip item
+// to a clip node, which is cached in the document.
+// TODO(gw): These enums are a bit messy - we should
+//           convert them to use named fields.
+impl From<ClipItemKey> for ClipNode {
+    fn from(item: ClipItemKey) -> Self {
+        let item = match item {
+            ClipItemKey::Rectangle(rect, mode) => {
+                ClipItem::Rectangle(LayoutRect::from_au(rect), mode)
+            }
+            ClipItemKey::RoundedRectangle(rect, radius, mode) => {
+                ClipItem::RoundedRectangle(
+                    LayoutRect::from_au(rect),
+                    radius.into(),
+                    mode,
+                )
+            }
+            ClipItemKey::LineDecoration(rect, style, orientation, wavy_line_thickness) => {
+                ClipItem::LineDecoration(LineDecorationClipSource {
+                    rect: LayoutRect::from_au(rect),
+                    style,
+                    orientation,
+                    wavy_line_thickness: wavy_line_thickness.to_f32_px(),
+                })
+            }
+            ClipItemKey::ImageMask(rect, image, repeat) => {
+                ClipItem::Image(ImageMask {
+                    image,
+                    rect: LayoutRect::from_au(rect),
+                    repeat,
+                })
+            }
+            ClipItemKey::BoxShadow(shadow_rect, shadow_radius, prim_shadow_rect, blur_radius, clip_mode) => {
+                ClipItem::new_box_shadow(
+                    LayoutRect::from_au(shadow_rect),
+                    shadow_radius.into(),
+                    LayoutRect::from_au(prim_shadow_rect),
+                    blur_radius.to_f32_px(),
+                    clip_mode,
+                )
+            }
+        };
+
+        ClipNode {
+            item,
+            gpu_cache_handle: GpuCacheHandle::new(),
+        }
+    }
+}
+
 // Flags that are attached to instances of clip nodes.
 bitflags! {
+    #[cfg_attr(feature = "capture", derive(Serialize))]
+    #[cfg_attr(feature = "replay", derive(Deserialize))]
     pub struct ClipNodeFlags: u8 {
         const SAME_SPATIAL_NODE = 0x1;
         const SAME_COORD_SYSTEM = 0x2;
     }
 }
 
 // Identifier for a clip chain. Clip chains are stored
 // in a contiguous array in the clip store. They are
@@ -132,60 +200,40 @@ pub struct ClipChainId(pub u32);
 impl ClipChainId {
     pub const NONE: Self = ClipChainId(u32::MAX);
 }
 
 // A clip chain node is an id for a range of clip sources,
 // and a link to a parent clip chain node, or ClipChainId::NONE.
 #[derive(Clone)]
 pub struct ClipChainNode {
-    pub clip_node_index: ClipNodeIndex,
+    pub handle: ClipDataHandle,
     pub spatial_node_index: SpatialNodeIndex,
     pub parent_clip_chain_id: ClipChainId,
 }
 
 // An index into the clip_nodes array.
 #[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct ClipNodeIndex(pub u32);
 
 // When a clip node is found to be valid for a
 // clip chain instance, it's stored in an index
 // buffer style structure. This struct contains
 // an index to the node data itself, as well as
 // some flags describing how this clip node instance
 // is positioned.
-#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)]
+#[derive(Clone, Copy, Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct ClipNodeInstance {
-    index_and_flags: u32,
-    spatial_node_index: u32,
-}
-
-impl ClipNodeInstance {
-    fn new(
-        index: ClipNodeIndex,
-        flags: ClipNodeFlags,
-        spatial_node_index: SpatialNodeIndex,
-    ) -> ClipNodeInstance {
-        ClipNodeInstance {
-            index_and_flags: (index.0 & 0x00ffffff) | ((flags.bits() as u32) << 24),
-            spatial_node_index: spatial_node_index.0 as u32,
-        }
-    }
-
-    fn flags(&self) -> ClipNodeFlags {
-        ClipNodeFlags::from_bits_truncate((self.index_and_flags >> 24) as u8)
-    }
-
-    fn index(&self) -> usize {
-        (self.index_and_flags & 0x00ffffff) as usize
-    }
+    pub handle: ClipDataHandle,
+    pub flags: ClipNodeFlags,
+    pub spatial_node_index: SpatialNodeIndex,
 }
 
 // A range of clip node instances that were found by
 // building a clip chain instance.
 #[derive(Debug, Copy, Clone)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct ClipNodeRange {
@@ -204,17 +252,17 @@ enum ClipSpaceConversion {
     ScaleOffset(ScaleOffset),
     Transform(LayoutToWorldTransform),
 }
 
 // Temporary information that is cached and reused
 // during building of a clip chain instance.
 struct ClipNodeInfo {
     conversion: ClipSpaceConversion,
-    node_index: ClipNodeIndex,
+    handle: ClipDataHandle,
     spatial_node_index: SpatialNodeIndex,
     has_non_root_coord_system: bool,
 }
 
 impl ClipNode {
     pub fn update(
         &mut self,
         gpu_cache: &mut GpuCache,
@@ -308,19 +356,18 @@ impl ClipNode {
             ClipItem::RoundedRectangle(..) |
             ClipItem::LineDecoration(..) => {}
         }
     }
 }
 
 // The main clipping public interface that other modules access.
 pub struct ClipStore {
-    pub clip_nodes: Vec<ClipNode>,
     pub clip_chain_nodes: Vec<ClipChainNode>,
-    clip_node_indices: Vec<ClipNodeInstance>,
+    clip_node_instances: Vec<ClipNodeInstance>,
     clip_node_info: Vec<ClipNodeInfo>,
     clip_node_collectors: Vec<ClipNodeCollector>,
 }
 
 // A clip chain instance is what gets built for a given clip
 // chain id + local primitive region + positioning node.
 #[derive(Debug)]
 pub struct ClipChainInstance {
@@ -336,82 +383,48 @@ pub struct ClipChainInstance {
     // Combined clip rect in picture space (may
     // be more conservative that local_clip_rect).
     pub pic_clip_rect: PictureRect,
 }
 
 impl ClipStore {
     pub fn new() -> Self {
         ClipStore {
-            clip_nodes: Vec::new(),
             clip_chain_nodes: Vec::new(),
-            clip_node_indices: Vec::new(),
+            clip_node_instances: Vec::new(),
             clip_node_info: Vec::new(),
             clip_node_collectors: Vec::new(),
         }
     }
 
     pub fn get_clip_chain(&self, clip_chain_id: ClipChainId) -> &ClipChainNode {
         &self.clip_chain_nodes[clip_chain_id.0 as usize]
     }
 
-    pub fn add_clip_chain_node_index(
+    pub fn add_clip_chain_node(
         &mut self,
-        clip_node_index: ClipNodeIndex,
+        handle: ClipDataHandle,
         spatial_node_index: SpatialNodeIndex,
         parent_clip_chain_id: ClipChainId,
     ) -> ClipChainId {
         let id = ClipChainId(self.clip_chain_nodes.len() as u32);
         self.clip_chain_nodes.push(ClipChainNode {
-            clip_node_index,
+            handle,
             spatial_node_index,
             parent_clip_chain_id,
         });
         id
     }
 
-    pub fn add_clip_chain_node(
-        &mut self,
-        item: ClipItem,
-        spatial_node_index: SpatialNodeIndex,
-        parent_clip_chain_id: ClipChainId,
-    ) -> ClipChainId {
-        let clip_node_index = ClipNodeIndex(self.clip_nodes.len() as u32);
-        self.clip_nodes.push(ClipNode {
-            item,
-            gpu_cache_handle: GpuCacheHandle::new(),
-        });
-
-        self.add_clip_chain_node_index(
-            clip_node_index,
-            spatial_node_index,
-            parent_clip_chain_id,
-        )
-    }
-
-    pub fn get_node_from_range(
+    pub fn get_instance_from_range(
         &self,
         node_range: &ClipNodeRange,
         index: u32,
-    ) -> (&ClipNode, ClipNodeFlags, SpatialNodeIndex) {
-        let instance = self.clip_node_indices[(node_range.first + index) as usize];
-        (
-            &self.clip_nodes[instance.index()],
-            instance.flags(),
-            SpatialNodeIndex(instance.spatial_node_index as usize),
-        )
-    }
-
-    pub fn get_node_from_range_mut(
-        &mut self,
-        node_range: &ClipNodeRange,
-        index: u32,
-    ) -> (&mut ClipNode, ClipNodeFlags) {
-        let instance = self.clip_node_indices[(node_range.first + index) as usize];
-        (&mut self.clip_nodes[instance.index()], instance.flags())
+    ) -> &ClipNodeInstance {
+        &self.clip_node_instances[(node_range.first + index) as usize]
     }
 
     // Notify the clip store that a new rasterization root has been created.
     // This means any clips from an earlier root should be collected rather
     // than applied on the primitive itself.
     pub fn push_raster_root(
         &mut self,
         raster_spatial_node_index: SpatialNodeIndex,
@@ -439,16 +452,17 @@ impl ClipStore {
         prim_to_pic_mapper: &SpaceMapper<LayoutPixel, PicturePixel>,
         pic_to_world_mapper: &SpaceMapper<PicturePixel, WorldPixel>,
         clip_scroll_tree: &ClipScrollTree,
         gpu_cache: &mut GpuCache,
         resource_cache: &mut ResourceCache,
         device_pixel_scale: DevicePixelScale,
         world_rect: &WorldRect,
         clip_node_collector: &Option<ClipNodeCollector>,
+        clip_data_store: &mut ClipDataStore,
     ) -> Option<ClipChainInstance> {
         let mut local_clip_rect = local_prim_clip_rect;
 
         // Walk the clip chain to build local rects, and collect the
         // smallest possible local/device clip area.
 
         self.clip_node_info.clear();
         let mut current_clip_chain_id = clip_chain_id;
@@ -462,72 +476,72 @@ impl ClipStore {
             match self.clip_node_collectors.iter_mut().find(|c| {
                 clip_chain_node.spatial_node_index < c.raster_root
             }) {
                 Some(collector) => {
                     collector.insert(current_clip_chain_id);
                 }
                 None => {
                     if !add_clip_node_to_current_chain(
-                        clip_chain_node.clip_node_index,
+                        clip_chain_node.handle,
                         clip_chain_node.spatial_node_index,
                         spatial_node_index,
                         &mut local_clip_rect,
                         &mut self.clip_node_info,
-                        &self.clip_nodes,
+                        clip_data_store,
                         clip_scroll_tree,
                     ) {
                         return None;
                     }
                 }
             }
 
             current_clip_chain_id = clip_chain_node.parent_clip_chain_id;
         }
 
         // Add any collected clips from primitives that should be
         // handled as part of this rasterization root.
         if let Some(clip_node_collector) = clip_node_collector {
             for clip_chain_id in &clip_node_collector.clips {
-                let (clip_node_index, clip_spatial_node_index) = {
+                let (handle, clip_spatial_node_index) = {
                     let clip_chain_node = &self.clip_chain_nodes[clip_chain_id.0 as usize];
-                    (clip_chain_node.clip_node_index, clip_chain_node.spatial_node_index)
+                    (clip_chain_node.handle, clip_chain_node.spatial_node_index)
                 };
 
                 if !add_clip_node_to_current_chain(
-                    clip_node_index,
+                    handle,
                     clip_spatial_node_index,
                     spatial_node_index,
                     &mut local_clip_rect,
                     &mut self.clip_node_info,
-                    &self.clip_nodes,
+                    clip_data_store,
                     clip_scroll_tree,
                 ) {
                     return None;
                 }
             }
         }
 
         let local_bounding_rect = local_prim_rect.intersection(&local_clip_rect)?;
         let pic_clip_rect = prim_to_pic_mapper.map(&local_bounding_rect)?;
         let world_clip_rect = pic_to_world_mapper.map(&pic_clip_rect)?;
 
         // Now, we've collected all the clip nodes that *potentially* affect this
         // primitive region, and reduced the size of the prim region as much as possible.
 
         // Run through the clip nodes, and see which ones affect this prim region.
 
-        let first_clip_node_index = self.clip_node_indices.len() as u32;
+        let first_clip_node_index = self.clip_node_instances.len() as u32;
         let mut has_non_root_coord_system = false;
         let mut has_non_local_clips = false;
         let mut needs_mask = false;
 
         // For each potential clip node
         for node_info in self.clip_node_info.drain(..) {
-            let node = &mut self.clip_nodes[node_info.node_index.0 as usize];
+            let node = &mut clip_data_store[node_info.handle];
 
             // See how this clip affects the prim region.
             let clip_result = match node_info.conversion {
                 ClipSpaceConversion::Local => {
                     node.item.get_clip_result(&local_bounding_rect)
                 }
                 ClipSpaceConversion::ScaleOffset(ref scale_offset) => {
                     has_non_local_clips = true;
@@ -591,32 +605,32 @@ impl ClipStore {
                         }
 
                         ClipItem::Rectangle(_, ClipMode::Clip) => {
                             !flags.contains(ClipNodeFlags::SAME_COORD_SYSTEM)
                         }
                     };
 
                     // Store this in the index buffer for this clip chain instance.
-                    let instance = ClipNodeInstance::new(
-                        node_info.node_index,
+                    let instance = ClipNodeInstance {
+                        handle: node_info.handle,
                         flags,
-                        node_info.spatial_node_index,
-                    );
-                    self.clip_node_indices.push(instance);
+                        spatial_node_index: node_info.spatial_node_index,
+                    };
+                    self.clip_node_instances.push(instance);
 
                     has_non_root_coord_system |= node_info.has_non_root_coord_system;
                 }
             }
         }
 
         // Get the range identifying the clip nodes in the index buffer.
         let clips_range = ClipNodeRange {
             first: first_clip_node_index,
-            count: self.clip_node_indices.len() as u32 - first_clip_node_index,
+            count: self.clip_node_instances.len() as u32 - first_clip_node_index,
         };
 
         // Return a valid clip chain instance
         Some(ClipChainInstance {
             clips_range,
             has_non_root_coord_system,
             has_non_local_clips,
             local_clip_rect,
@@ -624,26 +638,27 @@ impl ClipStore {
             needs_mask,
         })
     }
 
     /// Reports the heap usage of this clip store.
     pub fn malloc_size_of(&self, op: VoidPtrToSizeFn) -> usize {
         let mut size = 0;
         unsafe {
-            size += op(self.clip_nodes.as_ptr() as *const c_void);
             size += op(self.clip_chain_nodes.as_ptr() as *const c_void);
-            size += op(self.clip_node_indices.as_ptr() as *const c_void);
+            size += op(self.clip_node_instances.as_ptr() as *const c_void);
             size += op(self.clip_node_info.as_ptr() as *const c_void);
         }
         size
     }
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct LineDecorationClipSource {
     rect: LayoutRect,
     style: LineStyle,
     orientation: LineOrientation,
     wavy_line_thickness: f32,
 }
 
 
@@ -715,66 +730,132 @@ impl ClipRegion<Option<ComplexClipRegion
                         mode: region.mode,
                     })
                 },
             }
         }
     }
 }
 
-#[derive(Debug)]
+// The ClipItemKey is a hashable representation of the contents
+// of a clip item. It is used during interning to de-duplicate
+// clip nodes between frames and display lists. This allows quick
+// comparison of clip node equality by handle, and also allows
+// the uploaded GPU cache handle to be retained between display lists.
+// TODO(gw): Maybe we should consider constructing these directly
+//           in the DL builder?
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum ClipItemKey {
+    Rectangle(LayoutRectAu, ClipMode),
+    RoundedRectangle(LayoutRectAu, BorderRadiusAu, ClipMode),
+    ImageMask(LayoutRectAu, ImageKey, bool),
+    BoxShadow(LayoutRectAu, BorderRadiusAu, LayoutRectAu, Au, BoxShadowClipMode),
+    LineDecoration(LayoutRectAu, LineStyle, LineOrientation, Au),
+}
+
+impl ClipItemKey {
+    pub fn rectangle(rect: LayoutRect, mode: ClipMode) -> Self {
+        ClipItemKey::Rectangle(rect.to_au(), mode)
+    }
+
+    pub fn rounded_rect(rect: LayoutRect, mut radii: BorderRadius, mode: ClipMode) -> Self {
+        if radii.is_zero() {
+            ClipItemKey::rectangle(rect, mode)
+        } else {
+            ensure_no_corner_overlap(&mut radii, &rect);
+            ClipItemKey::RoundedRectangle(
+                rect.to_au(),
+                radii.into(),
+                mode,
+            )
+        }
+    }
+
+    pub fn image_mask(image_mask: &ImageMask) -> Self {
+        ClipItemKey::ImageMask(
+            image_mask.rect.to_au(),
+            image_mask.image,
+            image_mask.repeat,
+        )
+    }
+
+    pub fn line_decoration(
+        rect: LayoutRect,
+        style: LineStyle,
+        orientation: LineOrientation,
+        wavy_line_thickness: f32,
+    ) -> Self {
+        ClipItemKey::LineDecoration(
+            rect.to_au(),
+            style,
+            orientation,
+            Au::from_f32_px(wavy_line_thickness),
+        )
+    }
+
+    pub fn box_shadow(
+        shadow_rect: LayoutRect,
+        shadow_radius: BorderRadius,
+        prim_shadow_rect: LayoutRect,
+        blur_radius: f32,
+        clip_mode: BoxShadowClipMode,
+    ) -> Self {
+        ClipItemKey::BoxShadow(
+            shadow_rect.to_au(),
+            shadow_radius.into(),
+            prim_shadow_rect.to_au(),
+            Au::from_f32_px(blur_radius),
+            clip_mode,
+        )
+    }
+
+    // Return a modified clip source that is the same as self
+    // but offset in local-space by a specified amount.
+    pub fn offset(&self, offset: &LayoutVector2D) -> Self {
+        let offset = offset.to_au();
+        match *self {
+            ClipItemKey::LineDecoration(rect, style, orientation, wavy_line_thickness) => {
+                ClipItemKey::LineDecoration(
+                    rect.translate(&offset),
+                    style,
+                    orientation,
+                    wavy_line_thickness,
+                )
+            }
+            _ => {
+                panic!("bug: other clip sources not expected here yet");
+            }
+        }
+    }
+}
+
+#[derive(Debug, Clone)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
 pub enum ClipItem {
     Rectangle(LayoutRect, ClipMode),
     RoundedRectangle(LayoutRect, BorderRadius, ClipMode),
     Image(ImageMask),
     BoxShadow(BoxShadowClipSource),
     LineDecoration(LineDecorationClipSource),
 }
 
 impl ClipItem {
-    pub fn new_rounded_rect(
-        rect: LayoutRect,
-        mut radii: BorderRadius,
-        clip_mode: ClipMode
-    ) -> Self {
-        if radii.is_zero() {
-            ClipItem::Rectangle(rect, clip_mode)
-        } else {
-            ensure_no_corner_overlap(&mut radii, &rect);
-            ClipItem::RoundedRectangle(
-                rect,
-                radii,
-                clip_mode,
-            )
-        }
-    }
-
-    pub fn new_line_decoration(
-        rect: LayoutRect,
-        style: LineStyle,
-        orientation: LineOrientation,
-        wavy_line_thickness: f32,
-    ) -> Self {
-        ClipItem::LineDecoration(
-            LineDecorationClipSource {
-                rect,
-                style,
-                orientation,
-                wavy_line_thickness,
-            }
-        )
-    }
-
     pub fn new_box_shadow(
         shadow_rect: LayoutRect,
-        shadow_radius: BorderRadius,
+        mut shadow_radius: BorderRadius,
         prim_shadow_rect: LayoutRect,
         blur_radius: f32,
         clip_mode: BoxShadowClipMode,
     ) -> Self {
+        // Make sure corners don't overlap.
+        ensure_no_corner_overlap(&mut shadow_radius, &shadow_rect);
+
         // Get the fractional offsets required to match the
         // source rect with a minimal rect.
         let fract_offset = LayoutPoint::new(
             shadow_rect.origin.x.fract().abs(),
             shadow_rect.origin.y.fract().abs(),
         );
         let fract_size = LayoutSize::new(
             shadow_rect.size.width.fract().abs(),
@@ -853,32 +934,16 @@ impl ClipItem {
             stretch_mode_y,
             cache_handle: None,
             cache_key: None,
             clip_data_handle: GpuCacheHandle::new(),
             minimal_shadow_rect,
         })
     }
 
-    // Return a modified clip source that is the same as self
-    // but offset in local-space by a specified amount.
-    pub fn offset(&self, offset: &LayoutVector2D) -> Self {
-        match *self {
-            ClipItem::LineDecoration(ref info) => {
-                ClipItem::LineDecoration(LineDecorationClipSource {
-                    rect: info.rect.translate(offset),
-                    ..*info
-                })
-            }
-            _ => {
-                panic!("bug: other clip sources not expected here yet");
-            }
-        }
-    }
-
     // Get an optional clip rect that a clip source can provide to
     // reduce the size of a primitive region. This is typically
     // used to eliminate redundant clips, and reduce the size of
     // any clip mask that eventually gets drawn.
     fn get_local_clip_rect(&self) -> Option<LayoutRect> {
         match *self {
             ClipItem::Rectangle(clip_rect, ClipMode::Clip) => Some(clip_rect),
             ClipItem::Rectangle(_, ClipMode::ClipOut) => None,
@@ -1142,25 +1207,25 @@ impl ClipNodeCollector {
         self.clips.insert(clip_chain_id);
     }
 }
 
 // Add a clip node into the list of clips to be processed
 // for the current clip chain. Returns false if the clip
 // results in the entire primitive being culled out.
 fn add_clip_node_to_current_chain(
-    clip_node_index: ClipNodeIndex,
+    handle: ClipDataHandle,
     clip_spatial_node_index: SpatialNodeIndex,
     spatial_node_index: SpatialNodeIndex,
     local_clip_rect: &mut LayoutRect,
     clip_node_info: &mut Vec<ClipNodeInfo>,
-    clip_nodes: &[ClipNode],
+    clip_data_store: &ClipDataStore,
     clip_scroll_tree: &ClipScrollTree,
 ) -> bool {
-    let clip_node = &clip_nodes[clip_node_index.0 as usize];
+    let clip_node = &clip_data_store[handle];
     let clip_spatial_node = &clip_scroll_tree.spatial_nodes[clip_spatial_node_index.0];
     let ref_spatial_node = &clip_scroll_tree.spatial_nodes[spatial_node_index.0];
 
     // Determine the most efficient way to convert between coordinate
     // systems of the primitive and clip node.
     let conversion = if spatial_node_index == clip_spatial_node_index {
         Some(ClipSpaceConversion::Local)
     } else if ref_spatial_node.coordinate_system_id == clip_spatial_node.coordinate_system_id {
@@ -1209,16 +1274,16 @@ fn add_clip_node_to_current_chain(
                     //           I have left this for now until we
                     //           find some good test cases where this
                     //           would be a worthwhile perf win.
                 }
             }
         }
         clip_node_info.push(ClipNodeInfo {
             conversion,
-            node_index: clip_node_index,
+            handle,
             spatial_node_index: clip_spatial_node_index,
             has_non_root_coord_system: clip_spatial_node.coordinate_system_id != CoordinateSystemId::root(),
         })
     }
 
     true
 }
--- a/gfx/webrender/src/display_list_flattener.rs
+++ b/gfx/webrender/src/display_list_flattener.rs
@@ -8,17 +8,17 @@ use api::{ClipId, ColorF, ComplexClipReg
 use api::{DevicePixelScale, DeviceUintRect, DisplayItemRef, ExtendMode, ExternalScrollId};
 use api::{FilterOp, FontInstanceKey, GlyphInstance, GlyphOptions, GlyphRasterSpace, GradientStop};
 use api::{IframeDisplayItem, ImageKey, ImageRendering, ItemRange, LayoutPoint};
 use api::{LayoutPrimitiveInfo, LayoutRect, LayoutSize, LayoutTransform, LayoutVector2D};
 use api::{LineOrientation, LineStyle, LocalClip, NinePatchBorderSource, PipelineId};
 use api::{PropertyBinding, ReferenceFrame, RepeatMode, ScrollFrameDisplayItem, ScrollSensitivity};
 use api::{Shadow, SpecificDisplayItem, StackingContext, StickyFrameDisplayItem, TexelRect};
 use api::{ClipMode, TransformStyle, YuvColorSpace, YuvData};
-use clip::{ClipChainId, ClipRegion, ClipItem, ClipStore};
+use clip::{ClipDataInterner, ClipChainId, ClipRegion, ClipItemKey, ClipStore};
 use clip_scroll_tree::{ClipScrollTree, SpatialNodeIndex};
 use euclid::vec2;
 use frame_builder::{ChasePrimitive, FrameBuilder, FrameBuilderConfig};
 use glyph_rasterizer::FontInstance;
 use gpu_cache::GpuCacheHandle;
 use gpu_types::BrushFlags;
 use hit_test::{HitTestingItem, HitTestingRun};
 use image::simplify_repeated_primitive;
@@ -154,29 +154,33 @@ pub struct DisplayListFlattener<'a> {
     pub hit_testing_runs: Vec<HitTestingRun>,
 
     /// The store which holds all complex clipping information.
     pub clip_store: ClipStore,
 
     /// The configuration to use for the FrameBuilder. We consult this in
     /// order to determine the default font.
     pub config: FrameBuilderConfig,
+
+    /// Reference to the clip interner for this document.
+    clip_interner: &'a mut ClipDataInterner,
 }
 
 impl<'a> DisplayListFlattener<'a> {
     pub fn create_frame_builder(
         scene: &Scene,
         clip_scroll_tree: &mut ClipScrollTree,
         font_instances: FontInstanceMap,
         view: &DocumentView,
         output_pipelines: &FastHashSet<PipelineId>,
         frame_builder_config: &FrameBuilderConfig,
         new_scene: &mut Scene,
         scene_id: u64,
         picture_id_generator: &mut PictureIdGenerator,
+        clip_interner: &mut ClipDataInterner,
     ) -> FrameBuilder {
         // We checked that the root pipeline is available on the render backend.
         let root_pipeline_id = scene.root_pipeline_id.unwrap();
         let root_pipeline = scene.pipelines.get(&root_pipeline_id).unwrap();
 
         let background_color = root_pipeline
             .background_color
             .and_then(|color| if color.a > 0.0 { Some(color) } else { None });
@@ -191,16 +195,17 @@ impl<'a> DisplayListFlattener<'a> {
             hit_testing_runs: Vec::new(),
             scrollbar_prims: Vec::new(),
             shadow_stack: Vec::new(),
             sc_stack: Vec::new(),
             pipeline_clip_chain_stack: vec![ClipChainId::NONE],
             prim_store: PrimitiveStore::new(),
             clip_store: ClipStore::new(),
             picture_id_generator,
+            clip_interner,
         };
 
         flattener.push_root(
             root_pipeline_id,
             &root_pipeline.viewport_size,
             &root_pipeline.content_size,
         );
         flattener.setup_viewport_offset(view.inner_rect, view.accumulated_scale_factor());
@@ -719,32 +724,32 @@ impl<'a> DisplayListFlattener<'a> {
                     // as a clip chain (one clip item per node), eventually parented
                     // to the parent clip node. For a user defined clip chain, we will
                     // need to walk the linked list of clip chain nodes for each clip
                     // node, accumulating them into one clip chain that is then
                     // parented to the clip chain parent.
 
                     for _ in 0 .. item_clip_node.count {
                         // Get the id of the clip sources entry for that clip chain node.
-                        let (clip_node_index, spatial_node_index) = {
+                        let (handle, spatial_node_index) = {
                             let clip_chain = self
                                 .clip_store
                                 .get_clip_chain(clip_node_clip_chain_id);
 
                             clip_node_clip_chain_id = clip_chain.parent_clip_chain_id;
 
-                            (clip_chain.clip_node_index, clip_chain.spatial_node_index)
+                            (clip_chain.handle, clip_chain.spatial_node_index)
                         };
 
                         // Add a new clip chain node, which references the same clip sources, and
                         // parent it to the current parent.
                         clip_chain_id = self
                             .clip_store
-                            .add_clip_chain_node_index(
-                                clip_node_index,
+                            .add_clip_chain_node(
+                                handle,
                                 spatial_node_index,
                                 clip_chain_id,
                             );
                     }
                 }
 
                 // Map the last entry in the clip chain to the supplied ClipId. This makes
                 // this ClipId available as a source to other user defined clip chains.
@@ -786,29 +791,33 @@ impl<'a> DisplayListFlattener<'a> {
     }
 
     // Given a list of clip sources, a positioning node and
     // a parent clip chain, return a new clip chain entry.
     // If the supplied list of clip sources is empty, then
     // just return the parent clip chain id directly.
     fn build_clip_chain(
         &mut self,
-        clip_items: Vec<ClipItem>,
+        clip_items: Vec<ClipItemKey>,
         spatial_node_index: SpatialNodeIndex,
         parent_clip_chain_id: ClipChainId,
     ) -> ClipChainId {
         if clip_items.is_empty() {
             parent_clip_chain_id
         } else {
             let mut clip_chain_id = parent_clip_chain_id;
 
             for item in clip_items {
+                // Intern this clip item, and store the handle
+                // in the clip chain node.
+                let handle = self.clip_interner.intern(&item);
+
                 clip_chain_id = self.clip_store
                                     .add_clip_chain_node(
-                                        item,
+                                        handle,
                                         spatial_node_index,
                                         clip_chain_id,
                                     );
             }
 
             clip_chain_id
         }
     }
@@ -861,41 +870,44 @@ impl<'a> DisplayListFlattener<'a> {
 
     /// Add an already created primitive to the draw lists.
     pub fn add_primitive_to_draw_list(
         &mut self,
         prim_index: PrimitiveIndex,
     ) {
         // Add primitive to the top-most Picture on the stack.
         let pic_prim_index = self.sc_stack.last().unwrap().leaf_prim_index;
+        if cfg!(debug_assertions) && self.prim_store.chase_id == Some(prim_index) {
+            println!("\tadded to picture primitive {:?}", pic_prim_index);
+        }
         let pic = self.prim_store.get_pic_mut(pic_prim_index);
         pic.add_primitive(prim_index);
     }
 
     /// Convenience interface that creates a primitive entry and adds it
     /// to the draw list.
     pub fn add_primitive(
         &mut self,
         clip_and_scroll: ScrollNodeAndClipChain,
         info: &LayoutPrimitiveInfo,
-        clip_items: Vec<ClipItem>,
+        clip_items: Vec<ClipItemKey>,
         container: PrimitiveContainer,
     ) {
         if !self.shadow_stack.is_empty() {
             // TODO(gw): Restructure this so we don't need to move the shadow
             //           stack out (borrowck due to create_primitive below).
             let shadow_stack = mem::replace(&mut self.shadow_stack, Vec::new());
             for &(ref shadow, shadow_pic_prim_index) in &shadow_stack {
                 // Offset the local rect and clip rect by the shadow offset.
                 let mut info = info.clone();
                 info.rect = info.rect.translate(&shadow.offset);
                 info.clip_rect = info.clip_rect.translate(&shadow.offset);
 
                 // Offset any local clip sources by the shadow offset.
-                let clip_items: Vec<ClipItem> = clip_items
+                let clip_items: Vec<ClipItemKey> = clip_items
                     .iter()
                     .map(|cs| cs.offset(&shadow.offset))
                     .collect();
                 let clip_chain_id = self.build_clip_chain(
                     clip_items,
                     clip_and_scroll.spatial_node_index,
                     clip_and_scroll.clip_chain_id,
                 );
@@ -923,17 +935,17 @@ impl<'a> DisplayListFlattener<'a> {
             );
             let prim_index = self.create_primitive(
                 info,
                 clip_chain_id,
                 clip_and_scroll.spatial_node_index,
                 container,
             );
             if cfg!(debug_assertions) && ChasePrimitive::LocalRect(info.rect) == self.config.chase_primitive {
-                println!("Chasing {:?}", prim_index);
+                println!("Chasing {:?} by local rect", prim_index);
                 self.prim_store.chase_id = Some(prim_index);
             }
             self.add_primitive_to_hit_testing_list(info, clip_and_scroll);
             self.add_primitive_to_draw_list(
                 prim_index,
             );
         }
     }
@@ -1247,16 +1259,21 @@ impl<'a> DisplayListFlattener<'a> {
     }
 
     pub fn push_root(
         &mut self,
         pipeline_id: PipelineId,
         viewport_size: &LayoutSize,
         content_size: &LayoutSize,
     ) {
+        if let ChasePrimitive::Index(prim_index) = self.config.chase_primitive {
+            println!("Chasing {:?} by index", prim_index);
+            self.prim_store.chase_id = Some(prim_index);
+        }
+
         self.push_reference_frame(
             ClipId::root_reference_frame(pipeline_id),
             None,
             pipeline_id,
             None,
             None,
             LayoutVector2D::zero(),
         );
@@ -1292,48 +1309,59 @@ impl<'a> DisplayListFlattener<'a> {
         let spatial_node = self.id_to_index_mapper.get_spatial_node_index(parent_id);
 
         // Add a mapping for this ClipId in case it's referenced as a positioning node.
         self.id_to_index_mapper
             .map_spatial_node(new_node_id, spatial_node);
 
         let mut clip_count = 0;
 
+        // Intern each clip item in this clip node, and add the interned
+        // handle to a clip chain node, parented to form a chain.
+        // TODO(gw): We could re-structure this to share some of the
+        //           interning and chaining code.
+
         // Build the clip sources from the supplied region.
+        let handle = self
+            .clip_interner
+            .intern(&ClipItemKey::rectangle(clip_region.main, ClipMode::Clip));
+
         parent_clip_chain_index = self
             .clip_store
             .add_clip_chain_node(
-                ClipItem::Rectangle(clip_region.main, ClipMode::Clip),
+                handle,
                 spatial_node,
                 parent_clip_chain_index,
             );
         clip_count += 1;
 
-        if let Some(image_mask) = clip_region.image_mask {
+        if let Some(ref image_mask) = clip_region.image_mask {
+            let handle = self
+                .clip_interner
+                .intern(&ClipItemKey::image_mask(image_mask));
+
             parent_clip_chain_index = self
                 .clip_store
                 .add_clip_chain_node(
-                    ClipItem::Image(image_mask),
+                    handle,
                     spatial_node,
                     parent_clip_chain_index,
                 );
             clip_count += 1;
         }
 
         for region in clip_region.complex_clips {
-            let clip_item = ClipItem::new_rounded_rect(
-                region.rect,
-                region.radii,
-                region.mode,
-            );
+            let handle = self
+                .clip_interner
+                .intern(&ClipItemKey::rounded_rect(region.rect, region.radii, region.mode));
 
             parent_clip_chain_index = self
                 .clip_store
                 .add_clip_chain_node(
-                    clip_item,
+                    handle,
                     spatial_node,
                     parent_clip_chain_index,
                 );
             clip_count += 1;
         }
 
         // Map the supplied ClipId -> clip chain id.
         self.id_to_index_mapper.add_clip_chain(
@@ -1427,17 +1455,17 @@ impl<'a> DisplayListFlattener<'a> {
     }
 
     pub fn add_solid_rectangle(
         &mut self,
         clip_and_scroll: ScrollNodeAndClipChain,
         info: &LayoutPrimitiveInfo,
         color: ColorF,
         segments: Option<BrushSegmentDescriptor>,
-        extra_clips: Vec<ClipItem>,
+        extra_clips: Vec<ClipItemKey>,
     ) {
         if color.a == 0.0 {
             // Don't add transparent rectangles to the draw list, but do consider them for hit
             // testing. This allows specifying invisible hit testing areas.
             self.add_primitive_to_hit_testing_list(info, clip_and_scroll);
             return;
         }
 
@@ -1523,17 +1551,17 @@ impl<'a> DisplayListFlattener<'a> {
         let extra_clips = match style {
             LineStyle::Solid => {
                 Vec::new()
             }
             LineStyle::Wavy |
             LineStyle::Dotted |
             LineStyle::Dashed => {
                 vec![
-                    ClipItem::new_line_decoration(
+                    ClipItemKey::line_decoration(
                         info.rect,
                         style,
                         orientation,
                         wavy_line_thickness,
                     ),
                 ]
             }
         };
--- a/gfx/webrender/src/frame_builder.rs
+++ b/gfx/webrender/src/frame_builder.rs
@@ -1,16 +1,16 @@
 /* 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::{ColorF, DeviceIntPoint, DevicePixelScale, LayoutPixel, PicturePixel, RasterPixel};
 use api::{DeviceUintPoint, DeviceUintRect, DeviceUintSize, DocumentLayer, FontRenderMode, PictureRect};
 use api::{LayoutPoint, LayoutRect, LayoutSize, PipelineId, WorldPoint, WorldRect, WorldPixel};
-use clip::ClipStore;
+use clip::{ClipDataStore, ClipStore};
 use clip_scroll_tree::{ClipScrollTree, ROOT_SPATIAL_NODE_INDEX, SpatialNodeIndex};
 use display_list_flattener::{DisplayListFlattener};
 use gpu_cache::GpuCache;
 use gpu_types::{PrimitiveHeaders, TransformPalette, UvRectKind};
 use hit_test::{HitTester, HitTestingRun};
 use internal_types::{FastHashMap};
 use picture::{PictureCompositeMode, PictureSurface, RasterConfig};
 use prim_store::{PrimitiveIndex, PrimitiveRun, PrimitiveStore, SpaceMapper};
@@ -27,16 +27,17 @@ use tiling::{ScrollbarPrimitive, Special
 use util;
 
 
 #[derive(Clone, Copy, Debug, PartialEq)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub enum ChasePrimitive {
     Nothing,
+    Index(PrimitiveIndex),
     LocalRect(LayoutRect),
 }
 
 impl Default for ChasePrimitive {
     fn default() -> Self {
         ChasePrimitive::Nothing
     }
 }
@@ -78,16 +79,17 @@ pub struct FrameBuildingContext<'a> {
 pub struct FrameBuildingState<'a> {
     pub render_tasks: &'a mut RenderTaskTree,
     pub profile_counters: &'a mut FrameProfileCounters,
     pub clip_store: &'a mut ClipStore,
     pub resource_cache: &'a mut ResourceCache,
     pub gpu_cache: &'a mut GpuCache,
     pub special_render_passes: &'a mut SpecialRenderPasses,
     pub transforms: &'a mut TransformPalette,
+    pub clip_data_store: &'a mut ClipDataStore,
 }
 
 pub struct PictureContext {
     pub pipeline_id: PipelineId,
     pub prim_runs: Vec<PrimitiveRun>,
     pub apply_local_clip_rect: bool,
     pub inflation_factor: f32,
     pub allow_subpixel_aa: bool,
@@ -177,16 +179,17 @@ impl FrameBuilder {
         resource_cache: &mut ResourceCache,
         gpu_cache: &mut GpuCache,
         render_tasks: &mut RenderTaskTree,
         special_render_passes: &mut SpecialRenderPasses,
         profile_counters: &mut FrameProfileCounters,
         device_pixel_scale: DevicePixelScale,
         scene_properties: &SceneProperties,
         transform_palette: &mut TransformPalette,
+        clip_data_store: &mut ClipDataStore,
     ) -> Option<RenderTaskId> {
         profile_scope!("cull");
 
         if self.prim_store.primitives.is_empty() {
             return None
         }
         self.prim_store.reset_prim_visibility();
 
@@ -214,16 +217,17 @@ impl FrameBuilder {
         let mut frame_state = FrameBuildingState {
             render_tasks,
             profile_counters,
             clip_store: &mut self.clip_store,
             resource_cache,
             gpu_cache,
             special_render_passes,
             transforms: transform_palette,
+            clip_data_store,
         };
 
         let prim_context = PrimitiveContext::new(
             &clip_scroll_tree.spatial_nodes[root_spatial_node_index.0],
             root_spatial_node_index,
         );
 
         let (pic_context, mut pic_state) = self
@@ -318,16 +322,17 @@ impl FrameBuilder {
         clip_scroll_tree: &mut ClipScrollTree,
         pipelines: &FastHashMap<PipelineId, Arc<ScenePipeline>>,
         device_pixel_scale: DevicePixelScale,
         layer: DocumentLayer,
         pan: WorldPoint,
         texture_cache_profile: &mut TextureCacheProfileCounters,
         gpu_cache_profile: &mut GpuCacheProfileCounters,
         scene_properties: &SceneProperties,
+        clip_data_store: &mut ClipDataStore,
     ) -> Frame {
         profile_scope!("build");
         debug_assert!(
             DeviceUintRect::new(DeviceUintPoint::zero(), self.window_size)
                 .contains_rect(&self.screen_rect)
         );
 
         let mut profile_counters = FrameProfileCounters::new();
@@ -356,16 +361,17 @@ impl FrameBuilder {
             resource_cache,
             gpu_cache,
             &mut render_tasks,
             &mut special_render_passes,
             &mut profile_counters,
             device_pixel_scale,
             scene_properties,
             &mut transform_palette,
+            clip_data_store,
         );
 
         resource_cache.block_until_all_resources_added(gpu_cache,
                                                        &mut render_tasks,
                                                        texture_cache_profile);
 
         let mut passes = vec![
             special_render_passes.alpha_glyph_pass,
@@ -399,16 +405,17 @@ impl FrameBuilder {
 
         for pass in &mut passes {
             let mut ctx = RenderTargetContext {
                 device_pixel_scale,
                 prim_store: &self.prim_store,
                 resource_cache,
                 use_dual_source_blending,
                 clip_scroll_tree,
+                clip_data_store,
             };
 
             pass.build(
                 &mut ctx,
                 gpu_cache,
                 &mut render_tasks,
                 &mut deferred_resolves,
                 &self.clip_store,
@@ -440,17 +447,22 @@ impl FrameBuilder {
             deferred_resolves,
             gpu_cache_frame_id,
             has_been_rendered: false,
             has_texture_cache_tasks,
             prim_headers,
         }
     }
 
-    pub fn create_hit_tester(&mut self, clip_scroll_tree: &ClipScrollTree) -> HitTester {
+    pub fn create_hit_tester(
+        &mut self,
+        clip_scroll_tree: &ClipScrollTree,
+        clip_data_store: &ClipDataStore,
+    ) -> HitTester {
         HitTester::new(
             &self.hit_testing_runs,
             clip_scroll_tree,
-            &self.clip_store
+            &self.clip_store,
+            clip_data_store,
         )
     }
 }
 
--- a/gfx/webrender/src/hit_test.rs
+++ b/gfx/webrender/src/hit_test.rs
@@ -1,20 +1,21 @@
 /* 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::{BorderRadius, ClipMode, HitTestFlags, HitTestItem, HitTestResult, ItemTag, LayoutPoint};
 use api::{LayoutPrimitiveInfo, LayoutRect, PipelineId, VoidPtrToSizeFn, WorldPoint};
-use clip::{ClipNodeIndex, ClipChainNode, ClipNode, ClipItem, ClipStore};
-use clip::{ClipChainId, rounded_rectangle_contains_point};
+use clip::{ClipDataStore, ClipNode, ClipItem, ClipStore};
+use clip::{rounded_rectangle_contains_point};
 use clip_scroll_tree::{SpatialNodeIndex, ClipScrollTree};
 use internal_types::FastHashMap;
 use prim_store::ScrollNodeAndClipChain;
 use std::os::raw::c_void;
+use std::u32;
 use util::LayoutToWorldFastTransform;
 
 /// A copy of important clip scroll node data to use during hit testing. This a copy of
 /// data from the ClipScrollTree that will persist as a new frame is under construction,
 /// allowing hit tests consistent with the currently rendered frame.
 pub struct HitTestSpatialNode {
     /// The pipeline id of this node.
     pipeline_id: PipelineId,
@@ -44,16 +45,36 @@ impl HitTestClipNode {
         };
 
         HitTestClipNode {
             region,
         }
     }
 }
 
+// A hit testing clip chain node is the same as a
+// normal clip chain node, except that the clip
+// node is embedded inside the clip chain, rather
+// than referenced. This means we don't need to
+// copy the complete interned clip data store for
+// hit testing.
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
+pub struct HitTestClipChainId(u32);
+
+impl HitTestClipChainId {
+    pub const NONE: Self = HitTestClipChainId(u32::MAX);
+}
+
+pub struct HitTestClipChainNode {
+    pub region: HitTestClipNode,
+    pub spatial_node_index: SpatialNodeIndex,
+    pub parent_clip_chain_id: HitTestClipChainId,
+}
+
 #[derive(Clone)]
 pub struct HitTestingItem {
     rect: LayoutRect,
     clip_rect: LayoutRect,
     tag: ItemTag,
     is_backface_visible: bool,
 }
 
@@ -91,76 +112,83 @@ impl HitTestRegion {
             HitTestRegion::Invalid => true,
         }
     }
 }
 
 pub struct HitTester {
     runs: Vec<HitTestingRun>,
     spatial_nodes: Vec<HitTestSpatialNode>,
-    clip_nodes: Vec<HitTestClipNode>,
-    clip_chains: Vec<ClipChainNode>,
+    clip_chains: Vec<HitTestClipChainNode>,
     pipeline_root_nodes: FastHashMap<PipelineId, SpatialNodeIndex>,
 }
 
 impl HitTester {
     pub fn new(
         runs: &Vec<HitTestingRun>,
         clip_scroll_tree: &ClipScrollTree,
-        clip_store: &ClipStore
+        clip_store: &ClipStore,
+        clip_data_store: &ClipDataStore,
     ) -> HitTester {
         let mut hit_tester = HitTester {
             runs: runs.clone(),
             spatial_nodes: Vec::new(),
-            clip_nodes: Vec::new(),
             clip_chains: Vec::new(),
             pipeline_root_nodes: FastHashMap::default(),
         };
-        hit_tester.read_clip_scroll_tree(clip_scroll_tree, clip_store);
+        hit_tester.read_clip_scroll_tree(
+            clip_scroll_tree,
+            clip_store,
+            clip_data_store,
+        );
         hit_tester
     }
 
     fn read_clip_scroll_tree(
         &mut self,
         clip_scroll_tree: &ClipScrollTree,
-        clip_store: &ClipStore
+        clip_store: &ClipStore,
+        clip_data_store: &ClipDataStore,
     ) {
         self.spatial_nodes.clear();
         self.clip_chains.clear();
-        self.clip_nodes.clear();
 
         for (index, node) in clip_scroll_tree.spatial_nodes.iter().enumerate() {
             let index = SpatialNodeIndex(index);
 
             // If we haven't already seen a node for this pipeline, record this one as the root
             // node.
             self.pipeline_root_nodes.entry(node.pipeline_id).or_insert(index);
 
             self.spatial_nodes.push(HitTestSpatialNode {
                 pipeline_id: node.pipeline_id,
                 world_content_transform: node.world_content_transform,
                 world_viewport_transform: node.world_viewport_transform,
             });
         }
 
-        for node in &clip_store.clip_nodes {
-            self.clip_nodes.push(HitTestClipNode::new(node));
+        // For each clip chain node, extract the clip node from the clip
+        // data store, and store it inline with the clip chain node.
+        for node in &clip_store.clip_chain_nodes {
+            let clip_node = &clip_data_store[node.handle];
+            self.clip_chains.push(HitTestClipChainNode {
+                region: HitTestClipNode::new(clip_node),
+                spatial_node_index: node.spatial_node_index,
+                parent_clip_chain_id: HitTestClipChainId(node.parent_clip_chain_id.0),
+            });
         }
-
-        self.clip_chains
-            .extend_from_slice(&clip_store.clip_chain_nodes);
     }
 
     fn is_point_clipped_in_for_clip_chain(
         &self,
         point: WorldPoint,
-        clip_chain_id: ClipChainId,
+        clip_chain_id: HitTestClipChainId,
         test: &mut HitTest
     ) -> bool {
-        if clip_chain_id == ClipChainId::NONE {
+        if clip_chain_id == HitTestClipChainId::NONE {
             return true;
         }
 
         if let Some(result) = test.get_from_clip_chain_cache(clip_chain_id) {
             return result == ClippedIn::ClippedIn;
         }
 
         let descriptor = &self.clip_chains[clip_chain_id.0 as usize];
@@ -172,60 +200,60 @@ impl HitTester {
 
         if !parent_clipped_in {
             test.set_in_clip_chain_cache(clip_chain_id, ClippedIn::NotClippedIn);
             return false;
         }
 
         if !self.is_point_clipped_in_for_clip_node(
             point,
-            descriptor.clip_node_index,
+            clip_chain_id,
             descriptor.spatial_node_index,
             test,
         ) {
             test.set_in_clip_chain_cache(clip_chain_id, ClippedIn::NotClippedIn);
             return false;
         }
 
         test.set_in_clip_chain_cache(clip_chain_id, ClippedIn::ClippedIn);
         true
     }
 
     fn is_point_clipped_in_for_clip_node(
         &self,
         point: WorldPoint,
-        node_index: ClipNodeIndex,
+        clip_chain_node_id: HitTestClipChainId,
         spatial_node_index: SpatialNodeIndex,
         test: &mut HitTest
     ) -> bool {
-        if let Some(clipped_in) = test.node_cache.get(&node_index) {
+        if let Some(clipped_in) = test.node_cache.get(&clip_chain_node_id) {
             return *clipped_in == ClippedIn::ClippedIn;
         }
 
-        let node = &self.clip_nodes[node_index.0 as usize];
+        let node = &self.clip_chains[clip_chain_node_id.0 as usize].region;
         let transform = self
             .spatial_nodes[spatial_node_index.0]
             .world_viewport_transform;
         let transformed_point = match transform
             .inverse()
             .and_then(|inverted| inverted.transform_point2d(&point))
         {
             Some(point) => point,
             None => {
-                test.node_cache.insert(node_index, ClippedIn::NotClippedIn);
+                test.node_cache.insert(clip_chain_node_id, ClippedIn::NotClippedIn);
                 return false;
             }
         };
 
         if !node.region.contains(&transformed_point) {
-            test.node_cache.insert(node_index, ClippedIn::NotClippedIn);
+            test.node_cache.insert(clip_chain_node_id, ClippedIn::NotClippedIn);
             return false;
         }
 
-        test.node_cache.insert(node_index, ClippedIn::ClippedIn);
+        test.node_cache.insert(clip_chain_node_id, ClippedIn::ClippedIn);
         true
     }
 
     pub fn find_node_under_point(&self, mut test: HitTest) -> Option<SpatialNodeIndex> {
         let point = test.get_absolute_point(self);
 
         for &HitTestingRun(ref items, ref clip_and_scroll) in self.runs.iter().rev() {
             let spatial_node_index = clip_and_scroll.spatial_node_index;
@@ -241,17 +269,17 @@ impl HitTester {
 
             let mut clipped_in = false;
             for item in items.iter().rev() {
                 if !item.rect.contains(&point_in_layer) ||
                     !item.clip_rect.contains(&point_in_layer) {
                     continue;
                 }
 
-                let clip_chain_id = clip_and_scroll.clip_chain_id;
+                let clip_chain_id = HitTestClipChainId(clip_and_scroll.clip_chain_id.0);
                 clipped_in |=
                     self.is_point_clipped_in_for_clip_chain(point, clip_chain_id, &mut test);
                 if !clipped_in {
                     break;
                 }
 
                 return Some(spatial_node_index);
             }
@@ -285,17 +313,17 @@ impl HitTester {
 
             let mut clipped_in = false;
             for item in items.iter().rev() {
                 if !item.rect.contains(&point_in_layer) ||
                     !item.clip_rect.contains(&point_in_layer) {
                     continue;
                 }
 
-                let clip_chain_id = clip_and_scroll.clip_chain_id;
+                let clip_chain_id = HitTestClipChainId(clip_and_scroll.clip_chain_id.0);
                 clipped_in = clipped_in ||
                     self.is_point_clipped_in_for_clip_chain(point, clip_chain_id, &mut test);
                 if !clipped_in {
                     break;
                 }
 
                 // Don't hit items with backface-visibility:hidden if they are facing the back.
                 if !item.is_backface_visible {
@@ -338,17 +366,16 @@ impl HitTester {
     }
 
     // Reports the CPU heap usage of this HitTester struct.
     pub fn malloc_size_of(&self, op: VoidPtrToSizeFn) -> usize {
         let mut size = 0;
         unsafe {
             size += op(self.runs.as_ptr() as *const c_void);
             size += op(self.spatial_nodes.as_ptr() as *const c_void);
-            size += op(self.clip_nodes.as_ptr() as *const c_void);
             size += op(self.clip_chains.as_ptr() as *const c_void);
             // We can't measure pipeline_root_nodes because we don't have the
             // real machinery from the malloc_size_of crate. We could estimate
             // it but it should generally be very small so we don't bother.
         }
         size
     }
 }
@@ -358,17 +385,17 @@ enum ClippedIn {
     ClippedIn,
     NotClippedIn,
 }
 
 pub struct HitTest {
     pipeline_id: Option<PipelineId>,
     point: WorldPoint,
     flags: HitTestFlags,
-    node_cache: FastHashMap<ClipNodeIndex, ClippedIn>,
+    node_cache: FastHashMap<HitTestClipChainId, ClippedIn>,
     clip_chain_cache: Vec<Option<ClippedIn>>,
 }
 
 impl HitTest {
     pub fn new(
         pipeline_id: Option<PipelineId>,
         point: WorldPoint,
         flags: HitTestFlags,
@@ -377,26 +404,26 @@ impl HitTest {
             pipeline_id,
             point,
             flags,
             node_cache: FastHashMap::default(),
             clip_chain_cache: Vec::new(),
         }
     }
 
-    fn get_from_clip_chain_cache(&mut self, index: ClipChainId) -> Option<ClippedIn> {
+    fn get_from_clip_chain_cache(&mut self, index: HitTestClipChainId) -> Option<ClippedIn> {
         let index = index.0 as usize;
         if index >= self.clip_chain_cache.len() {
             None
         } else {
             self.clip_chain_cache[index]
         }
     }
 
-    fn set_in_clip_chain_cache(&mut self, index: ClipChainId, value: ClippedIn) {
+    fn set_in_clip_chain_cache(&mut self, index: HitTestClipChainId, value: ClippedIn) {
         let index = index.0 as usize;
         if index >= self.clip_chain_cache.len() {
             self.clip_chain_cache.resize(index + 1, None);
         }
         self.clip_chain_cache[index] = Some(value);
     }
 
     fn get_absolute_point(&self, hit_tester: &HitTester) -> WorldPoint {
new file mode 100644
--- /dev/null
+++ b/gfx/webrender/src/intern.rs
@@ -0,0 +1,301 @@
+/* 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 internal_types::FastHashMap;
+use std::fmt::Debug;
+use std::hash::Hash;
+use std::marker::PhantomData;
+use std::mem;
+use std::ops;
+use std::u64;
+
+/*
+
+ The interning module provides a generic data structure
+ interning container. It is similar in concept to a
+ traditional string interning container, but it is
+ specialized to the WR thread model.
+
+ There is an Interner structure, that lives in the
+ scene builder thread, and a DataStore structure
+ that lives in the frame builder thread.
+
+ Hashing, interning and handle creation is done by
+ the interner structure during scene building.
+
+ Delta changes for the interner are pushed during
+ a transaction to the frame builder. The frame builder
+ is then able to access the content of the interned
+ handles quickly, via array indexing.
+
+ Epoch tracking ensures that the garbage collection
+ step which the interner uses to remove items is
+ only invoked on items that the frame builder thread
+ is no longer referencing.
+
+ Items in the data store are stored in a traditional
+ free-list structure, for content access and memory
+ usage efficiency.
+
+ */
+
+/// The epoch is incremented each time a scene is
+/// built. The most recently used scene epoch is
+/// stored inside each item and handle. This is
+/// then used for cache invalidation (item) and
+/// correctness validation (handle).
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone, PartialEq)]
+struct Epoch(u64);
+
+impl Epoch {
+    pub const INVALID: Self = Epoch(u64::MAX);
+}
+
+/// A list of updates to be applied to the data store,
+/// provided by the interning structure.
+pub struct UpdateList<S> {
+    /// The current epoch of the scene builder.
+    epoch: Epoch,
+    /// The additions and removals to apply.
+    updates: Vec<Update<S>>,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+#[derive(Debug, Copy, Clone)]
+pub struct Handle<T> {
+    index: usize,
+    epoch: Epoch,
+    _marker: PhantomData<T>,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub enum UpdateKind<S> {
+    Insert(S),
+    Remove,
+    UpdateEpoch,
+}
+
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct Update<S> {
+    index: usize,
+    kind: UpdateKind<S>,
+}
+
+/// The data item is stored with an epoch, for validating
+/// correct access patterns.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+struct Item<T> {
+    epoch: Epoch,
+    data: T,
+}
+
+/// The data store lives in the frame builder thread. It
+/// contains a free-list of items for fast access.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct DataStore<S, T, M> {
+    items: Vec<Item<T>>,
+    _source: PhantomData<S>,
+    _marker: PhantomData<M>,
+}
+
+impl<S, T, M> DataStore<S, T, M> where S: Debug, T: From<S>, M: Debug {
+    /// Construct a new data store
+    pub fn new() -> Self {
+        DataStore {
+            items: Vec::new(),
+            _source: PhantomData,
+            _marker: PhantomData,
+        }
+    }
+
+    /// Apply any updates from the scene builder thread to
+    /// this data store.
+    pub fn apply_updates(
+        &mut self,
+        update_list: UpdateList<S>,
+    ) {
+        for update in update_list.updates {
+            match update.kind {
+                UpdateKind::Insert(data) => {
+                    let item = Item {
+                        data: T::from(data),
+                        epoch: update_list.epoch,
+                    };
+                    if self.items.len() == update.index {
+                        self.items.push(item)
+                    } else {
+                        self.items[update.index] = item;
+                    }
+                }
+                UpdateKind::Remove => {
+                    self.items[update.index].epoch = Epoch::INVALID;
+                }
+                UpdateKind::UpdateEpoch => {
+                    self.items[update.index].epoch = update_list.epoch;
+                }
+            }
+        }
+    }
+}
+
+/// Retrieve an item from the store via handle
+impl<S, T, M> ops::Index<Handle<M>> for DataStore<S, T, M> {
+    type Output = T;
+    fn index(&self, handle: Handle<M>) -> &T {
+        let item = &self.items[handle.index];
+        assert_eq!(item.epoch, handle.epoch);
+        &item.data
+    }
+}
+
+/// Retrieve a mutable item from the store via handle
+/// Retrieve an item from the store via handle
+impl<S, T, M> ops::IndexMut<Handle<M>> for DataStore<S, T, M> {
+    fn index_mut(&mut self, handle: Handle<M>) -> &mut T {
+        let item = &mut self.items[handle.index];
+        assert_eq!(item.epoch, handle.epoch);
+        &mut item.data
+    }
+}
+
+/// The main interning data structure. This lives in the
+/// scene builder thread, and handles hashing and interning
+/// unique data structures. It also manages a free-list for
+/// the items in the data store, which is synchronized via
+/// an update list of additions / removals.
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct Interner<S : Eq + Hash + Clone + Debug, M> {
+    /// Uniquely map an interning key to a handle
+    map: FastHashMap<S, Handle<M>>,
+    /// List of free slots in the data store for re-use.
+    free_list: Vec<usize>,
+    /// The next index to append items to if free-list is empty.
+    next_index: usize,
+    /// Pending list of updates that need to be applied.
+    updates: Vec<Update<S>>,
+    /// The current epoch for the interner.
+    current_epoch: Epoch,
+}
+
+impl<S, M> Interner<S, M> where S: Eq + Hash + Clone + Debug, M: Copy + Debug {
+    /// Construct a new interner
+    pub fn new() -> Self {
+        Interner {
+            map: FastHashMap::default(),
+            free_list: Vec::new(),
+            next_index: 0,
+            updates: Vec::new(),
+            current_epoch: Epoch(1),
+        }
+    }
+
+    /// Intern a data structure, and return a handle to
+    /// that data. The handle can then be stored in the
+    /// frame builder, and safely accessed via the data
+    /// store that lives in the frame builder thread.
+    pub fn intern(
+        &mut self,
+        data: &S,
+    ) -> Handle<M> {
+        // Use get_mut rather than entry here to avoid
+        // cloning the (sometimes large) key in the common
+        // case, where the data already exists in the interner.
+        if let Some(handle) = self.map.get_mut(data) {
+            // Update the epoch in the data store. This
+            // is not strictly needed for correctness, but
+            // is used to ensure items are only accessed
+            // via valid handles.
+            if handle.epoch != self.current_epoch {
+                self.updates.push(Update {
+                    index: handle.index,
+                    kind: UpdateKind::UpdateEpoch,
+                })
+            }
+            handle.epoch = self.current_epoch;
+            return *handle;
+        }
+
+        // We need to intern a new data item. First, find out
+        // if there is a spare slot in the free-list that we
+        // can use. Otherwise, append to the end of the list.
+        let index = match self.free_list.pop() {
+            Some(index) => index,
+            None => {
+                let index = self.next_index;
+                self.next_index += 1;
+                index
+            }
+        };
+
+        // Add a pending update to insert the new data.
+        self.updates.push(Update {
+            index,
+            kind: UpdateKind::Insert(data.clone()),
+        });
+
+        // Generate a handle for access via the data store.
+        let handle = Handle {
+            index,
+            epoch: self.current_epoch,
+            _marker: PhantomData,
+        };
+
+        // Store this handle so the next time it is
+        // interned, it gets re-used.
+        self.map.insert(data.clone(), handle);
+
+        handle
+    }
+
+    /// Retrieve the pending list of updates for an interner
+    /// that need to be applied to the data store. Also run
+    /// a GC step that removes old entries.
+    pub fn end_frame_and_get_pending_updates(&mut self) -> UpdateList<S> {
+        let mut updates = mem::replace(&mut self.updates, Vec::new());
+        let free_list = &mut self.free_list;
+        let current_epoch = self.current_epoch.0;
+
+        // First, run a GC step. Walk through the handles, and
+        // if we find any that haven't been used for some time,
+        // remove them. If this ever shows up in profiles, we
+        // can make the GC step partial (scan only part of the
+        // map each frame). It also might make sense in the
+        // future to adjust how long items remain in the cache
+        // based on the current size of the list.
+        self.map.retain(|_, handle| {
+            if handle.epoch.0 + 10 < current_epoch {
+                // To expire an item:
+                //  - Add index to the free-list for re-use.
+                //  - Add an update to the data store to invalidate this slow.
+                //  - Remove from the hash map.
+                free_list.push(handle.index);
+                updates.push(Update {
+                    index: handle.index,
+                    kind: UpdateKind::Remove,
+                });
+                return false;
+            }
+
+            true
+        });
+
+        let updates = UpdateList {
+            updates,
+            epoch: self.current_epoch,
+        };
+
+        // Begin the next epoch
+        self.current_epoch = Epoch(self.current_epoch.0 + 1);
+
+        updates
+    }
+}
--- a/gfx/webrender/src/lib.rs
+++ b/gfx/webrender/src/lib.rs
@@ -78,16 +78,17 @@ mod gamma_lut;
 mod glyph_cache;
 mod glyph_rasterizer;
 mod gpu_cache;
 #[cfg(feature = "pathfinder")]
 mod gpu_glyph_renderer;
 mod gpu_types;
 mod hit_test;
 mod image;
+mod intern;
 mod internal_types;
 mod picture;
 mod prim_store;
 mod print_tree;
 mod profiler;
 mod record;
 mod render_backend;
 mod render_task;
--- a/gfx/webrender/src/prim_store.rs
+++ b/gfx/webrender/src/prim_store.rs
@@ -1720,16 +1720,17 @@ impl PrimitiveStore {
                     &pic_state.map_local_to_pic,
                     &pic_state.map_pic_to_world,
                     &frame_context.clip_scroll_tree,
                     frame_state.gpu_cache,
                     frame_state.resource_cache,
                     frame_context.device_pixel_scale,
                     &frame_context.world_rect,
                     &clip_node_collector,
+                    frame_state.clip_data_store,
                 );
 
             let clip_chain = match clip_chain {
                 Some(clip_chain) => clip_chain,
                 None => {
                     prim.metadata.clipped_world_rect = None;
                     return false;
                 }
@@ -2069,26 +2070,27 @@ fn write_brush_segment_description(
         metadata.local_rect,
         None,
         metadata.local_clip_rect
     );
 
     // Segment the primitive on all the local-space clip sources that we can.
     let mut local_clip_count = 0;
     for i in 0 .. clip_chain.clips_range.count {
-        let (clip_node, flags, _) = frame_state
+        let clip_instance = frame_state
             .clip_store
-            .get_node_from_range(&clip_chain.clips_range, i);
+            .get_instance_from_range(&clip_chain.clips_range, i);
+        let clip_node = &frame_state.clip_data_store[clip_instance.handle];
 
         // If this clip item is positioned by another positioning node, its relative position
         // could change during scrolling. This means that we would need to resegment. Instead
         // of doing that, only segment with clips that have the same positioning node.
         // TODO(mrobinson, #2858): It may make sense to include these nodes, resegmenting only
         // when necessary while scrolling.
-        if !flags.contains(ClipNodeFlags::SAME_SPATIAL_NODE) {
+        if !clip_instance.flags.contains(ClipNodeFlags::SAME_SPATIAL_NODE) {
             continue;
         }
 
         local_clip_count += 1;
 
         let (local_clip_rect, radius, mode) = match clip_node.item {
             ClipItem::RoundedRectangle(rect, radii, clip_mode) => {
                 rect_clips_only = false;
@@ -2238,16 +2240,17 @@ impl Primitive {
                     &pic_state.map_local_to_pic,
                     &pic_state.map_pic_to_world,
                     &frame_context.clip_scroll_tree,
                     frame_state.gpu_cache,
                     frame_state.resource_cache,
                     frame_context.device_pixel_scale,
                     &frame_context.world_rect,
                     clip_node_collector,
+                    frame_state.clip_data_store,
                 );
 
             match segment_clip_chain {
                 Some(segment_clip_chain) => {
                     if !segment_clip_chain.needs_mask ||
                        (!segment.may_need_clip_mask && !segment_clip_chain.has_non_local_clips) {
                         segment.clip_task_id = BrushSegmentTaskId::Opaque;
                         continue;
@@ -2270,16 +2273,17 @@ impl Primitive {
                     let clip_task = RenderTask::new_mask(
                         device_rect.to_i32(),
                         segment_clip_chain.clips_range,
                         root_spatial_node_index,
                         frame_state.clip_store,
                         frame_state.gpu_cache,
                         frame_state.resource_cache,
                         frame_state.render_tasks,
+                        frame_state.clip_data_store,
                     );
 
                     let clip_task_id = frame_state.render_tasks.add(clip_task);
                     pic_state.tasks.push(clip_task_id);
                     segment.clip_task_id = BrushSegmentTaskId::RenderTaskId(clip_task_id);
                 }
                 None => {
                     segment.clip_task_id = BrushSegmentTaskId::Empty;
@@ -2807,16 +2811,17 @@ impl Primitive {
                 let clip_task = RenderTask::new_mask(
                     device_rect,
                     clip_chain.clips_range,
                     root_spatial_node_index,
                     frame_state.clip_store,
                     frame_state.gpu_cache,
                     frame_state.resource_cache,
                     frame_state.render_tasks,
+                    frame_state.clip_data_store,
                 );
 
                 let clip_task_id = frame_state.render_tasks.add(clip_task);
                 if cfg!(debug_assertions) && is_chased {
                     println!("\tcreated task {:?} with world rect {:?}",
                         clip_task_id, self.metadata.clipped_world_rect);
                 }
                 self.metadata.clip_task_id = Some(clip_task_id);
--- a/gfx/webrender/src/render_backend.rs
+++ b/gfx/webrender/src/render_backend.rs
@@ -11,16 +11,19 @@ use api::{IdNamespace, LayoutPoint, Pipe
 use api::{MemoryReport, VoidPtrToSizeFn};
 use api::{ScrollLocation, ScrollNodeState, TransactionMsg, ResourceUpdate, ImageKey};
 use api::{NotificationRequest, Checkpoint};
 use api::channel::{MsgReceiver, Payload};
 #[cfg(feature = "capture")]
 use api::CaptureBits;
 #[cfg(feature = "replay")]
 use api::CapturedDocument;
+#[cfg(feature = "replay")]
+use clip::ClipDataInterner;
+use clip::{ClipDataUpdateList, ClipDataStore};
 use clip_scroll_tree::{SpatialNodeIndex, ClipScrollTree};
 #[cfg(feature = "debugger")]
 use debug_server;
 use frame_builder::{FrameBuilder, FrameBuilderConfig};
 use gpu_cache::GpuCache;
 use hit_test::{HitTest, HitTester};
 use internal_types::{DebugOutput, FastHashMap, FastHashSet, RenderedDocument, ResultMsg};
 use profiler::{BackendProfileCounters, IpcProfileCounters, ResourceProfileCounters};
@@ -109,16 +112,20 @@ struct Document {
     /// Properties that are resolved during frame building and can be changed at any time
     /// without requiring the scene to be re-built.
     dynamic_properties: SceneProperties,
 
     /// Track whether the last built frame is up to date or if it will need to be re-built
     /// before rendering again.
     frame_is_valid: bool,
     hit_tester_is_valid: bool,
+
+    // The store of currently active / available clip nodes. This is kept
+    // in sync with the clip interner in the scene builder for each document.
+    clip_data_store: ClipDataStore,
 }
 
 impl Document {
     pub fn new(
         window_size: DeviceUintSize,
         layer: DocumentLayer,
         default_device_pixel_ratio: f32,
     ) -> Self {
@@ -137,16 +144,17 @@ impl Document {
             clip_scroll_tree: ClipScrollTree::new(),
             frame_id: FrameId(0),
             frame_builder: None,
             output_pipelines: FastHashSet::default(),
             hit_tester: None,
             dynamic_properties: SceneProperties::new(),
             frame_is_valid: false,
             hit_tester_is_valid: false,
+            clip_data_store: ClipDataStore::new(),
         }
     }
 
     fn can_render(&self) -> bool {
         self.frame_builder.is_some() && self.scene.has_root_pipeline()
     }
 
     fn has_pixels(&self) -> bool {
@@ -262,18 +270,22 @@ impl Document {
                 &mut self.clip_scroll_tree,
                 &self.scene.pipelines,
                 accumulated_scale_factor,
                 self.view.layer,
                 pan,
                 &mut resource_profile.texture_cache,
                 &mut resource_profile.gpu_cache,
                 &self.dynamic_properties,
+                &mut self.clip_data_store,
             );
-            self.hit_tester = Some(frame_builder.create_hit_tester(&self.clip_scroll_tree));
+            self.hit_tester = Some(frame_builder.create_hit_tester(
+                &self.clip_scroll_tree,
+                &self.clip_data_store,
+            ));
             frame
         };
 
         self.frame_is_valid = true;
         self.hit_tester_is_valid = true;
 
         RenderedDocument {
             frame,
@@ -595,16 +607,17 @@ impl RenderBackend {
                         );
                         if let Some(rasterizer) = txn.blob_rasterizer.take() {
                             self.resource_cache.set_blob_rasterizer(rasterizer);
                         }
 
                         self.update_document(
                             txn.document_id,
                             replace(&mut txn.resource_updates, Vec::new()),
+                            txn.clip_updates.take(),
                             replace(&mut txn.frame_ops, Vec::new()),
                             replace(&mut txn.notifications, Vec::new()),
                             txn.build_frame,
                             txn.render_frame,
                             &mut frame_counter,
                             &mut profile_counters,
                             has_built_scene,
                         );
@@ -900,16 +913,17 @@ impl RenderBackend {
             txn.blob_requests = blob_requests;
             txn.blob_rasterizer = blob_rasterizer;
         }
 
         if !transaction_msg.use_scene_builder_thread && txn.can_skip_scene_builder() {
             self.update_document(
                 txn.document_id,
                 replace(&mut txn.resource_updates, Vec::new()),
+                None,
                 replace(&mut txn.frame_ops, Vec::new()),
                 replace(&mut txn.notifications, Vec::new()),
                 txn.build_frame,
                 txn.render_frame,
                 frame_counter,
                 profile_counters,
                 false
             );
@@ -937,16 +951,17 @@ impl RenderBackend {
 
         tx.send(SceneBuilderRequest::Transaction(txn)).unwrap();
     }
 
     fn update_document(
         &mut self,
         document_id: DocumentId,
         resource_updates: Vec<ResourceUpdate>,
+        clip_updates: Option<ClipDataUpdateList>,
         mut frame_ops: Vec<FrameMsg>,
         mut notifications: Vec<NotificationRequest>,
         mut build_frame: bool,
         mut render_frame: bool,
         frame_counter: &mut u32,
         profile_counters: &mut BackendProfileCounters,
         has_built_scene: bool,
     ) {
@@ -960,16 +975,22 @@ impl RenderBackend {
         if build_frame {
             if let Some(ref sampler) = self.sampler {
                 frame_ops.append(&mut sampler.sample());
             }
         }
 
         let doc = self.documents.get_mut(&document_id).unwrap();
 
+        // If there are any additions or removals of clip modes
+        // during the scene build, apply them to the data store now.
+        if let Some(clip_updates) = clip_updates {
+            doc.clip_data_store.apply_updates(clip_updates);
+        }
+
         // TODO: this scroll variable doesn't necessarily mean we scrolled. It is only used
         // for something wrench specific and we should remove it.
         let mut scroll = false;
         for frame_msg in frame_ops {
             let _timer = profile_counters.total_time.timer();
             let op = doc.process_frame_msg(frame_msg);
             build_frame |= op.build_frame;
             scroll |= op.scroll;
@@ -1275,18 +1296,24 @@ impl RenderBackend {
                     true,
                 );
                 //TODO: write down doc's pipeline info?
                 // it has `pipeline_epoch_map`,
                 // which may capture necessary details for some cases.
                 let file_name = format!("frame-{}-{}", (id.0).0, id.1);
                 config.serialize(&rendered_document.frame, file_name);
             }
+
+            let clip_data_name = format!("clip-data-{}-{}", (id.0).0, id.1);
+            config.serialize(&doc.clip_data_store, clip_data_name);
         }
 
+        debug!("\tscene builder");
+        self.scene_tx.send(SceneBuilderRequest::SaveScene(config.clone())).unwrap();
+
         debug!("\tresource cache");
         let (resources, deferred) = self.resource_cache.save_capture(&config.root);
 
         info!("\tbackend");
         let backend = PlainRenderBackend {
             default_device_pixel_ratio: self.default_device_pixel_ratio,
             frame_config: self.frame_config.clone(),
             documents: self.documents
@@ -1357,28 +1384,37 @@ impl RenderBackend {
 
         let mut last_scene_id = backend.last_scene_id;
         for (id, view) in backend.documents {
             debug!("\tdocument {:?}", id);
             let scene_name = format!("scene-{}-{}", (id.0).0, id.1);
             let scene = CaptureConfig::deserialize::<Scene, _>(root, &scene_name)
                 .expect(&format!("Unable to open {}.ron", scene_name));
 
+            let clip_interner_name = format!("clip-interner-{}-{}", (id.0).0, id.1);
+            let clip_interner = CaptureConfig::deserialize::<ClipDataInterner, _>(root, &clip_interner_name)
+                .expect(&format!("Unable to open {}.ron", clip_interner_name));
+
+            let clip_data_name = format!("clip-data-{}-{}", (id.0).0, id.1);
+            let clip_data_store = CaptureConfig::deserialize::<ClipDataStore, _>(root, &clip_data_name)
+                .expect(&format!("Unable to open {}.ron", clip_data_name));
+
             let mut doc = Document {
                 scene: scene.clone(),
                 removed_pipelines: Vec::new(),
                 view: view.clone(),
                 clip_scroll_tree: ClipScrollTree::new(),
                 frame_id: FrameId(0),
                 frame_builder: Some(FrameBuilder::empty()),
                 output_pipelines: FastHashSet::default(),
                 dynamic_properties: SceneProperties::new(),
                 hit_tester: None,
                 frame_is_valid: false,
                 hit_tester_is_valid: false,
+                clip_data_store,
             };
 
             let frame_name = format!("frame-{}-{}", (id.0).0, id.1);
             let frame = CaptureConfig::deserialize::<Frame, _>(root, frame_name);
             let build_frame = match frame {
                 Some(frame) => {
                     info!("\tloaded a built frame with {} passes", frame.passes.len());
 
@@ -1409,16 +1445,17 @@ impl RenderBackend {
                 document_id: id,
                 scene: doc.scene.clone(),
                 view: view.clone(),
                 config: self.frame_config.clone(),
                 output_pipelines: doc.output_pipelines.clone(),
                 font_instances: self.resource_cache.get_font_instances(),
                 scene_id: last_scene_id,
                 build_frame,
+                clip_interner,
             });
 
             self.documents.insert(id, doc);
         }
 
         if !scenes_to_build.is_empty() {
             self.low_priority_scene_tx.send(
                 SceneBuilderRequest::LoadScenes(scenes_to_build)
--- a/gfx/webrender/src/render_task.rs
+++ b/gfx/webrender/src/render_task.rs
@@ -2,17 +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::{DeviceIntPoint, DeviceIntRect, DeviceIntSize, DeviceSize, DeviceIntSideOffsets, ImageDescriptor, ImageFormat};
 #[cfg(feature = "pathfinder")]
 use api::FontRenderMode;
 use border::BorderCacheKey;
 use box_shadow::{BoxShadowCacheKey};
-use clip::{ClipItem, ClipStore, ClipNodeRange};
+use clip::{ClipDataStore, ClipItem, ClipStore, ClipNodeRange};
 use clip_scroll_tree::SpatialNodeIndex;
 use device::TextureFilter;
 #[cfg(feature = "pathfinder")]
 use euclid::{TypedPoint2D, TypedVector2D};
 use freelist::{FreeList, FreeListHandle, WeakFreeListHandle};
 use glyph_rasterizer::GpuGlyphCacheKey;
 use gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
 use gpu_types::{BorderInstance, ImageSource, RasterizationSpace, UvRectKind};
@@ -427,30 +427,32 @@ impl RenderTask {
     pub fn new_mask(
         outer_rect: DeviceIntRect,
         clip_node_range: ClipNodeRange,
         root_spatial_node_index: SpatialNodeIndex,
         clip_store: &mut ClipStore,
         gpu_cache: &mut GpuCache,
         resource_cache: &mut ResourceCache,
         render_tasks: &mut RenderTaskTree,
+        clip_data_store: &mut ClipDataStore,
     ) -> Self {
         let mut children = Vec::new();
 
         // Step through the clip sources that make up this mask. If we find
         // any box-shadow clip sources, request that image from the render
         // task cache. This allows the blurred box-shadow rect to be cached
         // in the texture cache across frames.
         // TODO(gw): Consider moving this logic outside this function, especially
         //           as we add more clip sources that depend on render tasks.
         // TODO(gw): If this ever shows up in a profile, we could pre-calculate
         //           whether a ClipSources contains any box-shadows and skip
         //           this iteration for the majority of cases.
         for i in 0 .. clip_node_range.count {
-            let (clip_node, _) = clip_store.get_node_from_range_mut(&clip_node_range, i);
+            let clip_instance = clip_store.get_instance_from_range(&clip_node_range, i);
+            let clip_node = &mut clip_data_store[clip_instance.handle];
             match clip_node.item {
                 ClipItem::BoxShadow(ref mut info) => {
                     let (cache_size, cache_key) = info.cache_key
                         .as_ref()
                         .expect("bug: no cache key set")
                         .clone();
                     let blur_radius_dp = cache_key.blur_radius_dp as f32;
                     let clip_data_address = gpu_cache.get_address(&info.clip_data_handle);
@@ -735,17 +737,17 @@ impl RenderTask {
         };
 
         let (mut target_rect, target_index) = self.get_target_rect();
         // The primitives inside a fixed-location render task
         // are already placed to their corresponding positions,
         // so the shader doesn't need to shift by the origin.
         if let RenderTaskLocation::Fixed(_) = self.location {
             target_rect.origin = DeviceIntPoint::origin();
-        };
+        }
 
         RenderTaskData {
             data: [
                 target_rect.origin.x as f32,
                 target_rect.origin.y as f32,
                 target_rect.size.width as f32,
                 target_rect.size.height as f32,
                 target_index.0 as f32,
--- a/gfx/webrender/src/scene_builder.rs
+++ b/gfx/webrender/src/scene_builder.rs
@@ -1,17 +1,20 @@
 /* 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::{AsyncBlobImageRasterizer, BlobImageRequest, BlobImageParams, BlobImageResult};
 use api::{DocumentId, PipelineId, ApiMsg, FrameMsg, ResourceUpdate, Epoch};
 use api::{BuiltDisplayList, ColorF, LayoutSize, NotificationRequest, Checkpoint};
 use api::channel::MsgSender;
+#[cfg(feature = "capture")]
+use capture::CaptureConfig;
 use frame_builder::{FrameBuilderConfig, FrameBuilder};
+use clip::{ClipDataInterner, ClipDataUpdateList};
 use clip_scroll_tree::ClipScrollTree;
 use display_list_flattener::DisplayListFlattener;
 use internal_types::{FastHashMap, FastHashSet};
 use picture::PictureIdGenerator;
 use resource_cache::FontInstanceMap;
 use render_backend::DocumentView;
 use renderer::{PipelineInfo, SceneBuilderHooks};
 use scene::Scene;
@@ -62,16 +65,17 @@ pub struct BuiltTransaction {
     pub document_id: DocumentId,
     pub built_scene: Option<BuiltScene>,
     pub resource_updates: Vec<ResourceUpdate>,
     pub rasterized_blobs: Vec<(BlobImageRequest, BlobImageResult)>,
     pub blob_rasterizer: Option<Box<AsyncBlobImageRasterizer>>,
     pub frame_ops: Vec<FrameMsg>,
     pub removed_pipelines: Vec<PipelineId>,
     pub notifications: Vec<NotificationRequest>,
+    pub clip_updates: Option<ClipDataUpdateList>,
     pub scene_build_start_time: u64,
     pub scene_build_end_time: u64,
     pub build_frame: bool,
     pub render_frame: bool,
 }
 
 pub struct DisplayListUpdate {
     pub pipeline_id: PipelineId,
@@ -95,16 +99,17 @@ pub struct LoadScene {
     pub document_id: DocumentId,
     pub scene: Scene,
     pub scene_id: u64,
     pub output_pipelines: FastHashSet<PipelineId>,
     pub font_instances: FontInstanceMap,
     pub view: DocumentView,
     pub config: FrameBuilderConfig,
     pub build_frame: bool,
+    pub clip_interner: ClipDataInterner,
 }
 
 pub struct BuiltScene {
     pub scene: Scene,
     pub frame_builder: FrameBuilder,
     pub clip_scroll_tree: ClipScrollTree,
 }
 
@@ -113,16 +118,18 @@ pub enum SceneBuilderRequest {
     Transaction(Box<Transaction>),
     DeleteDocument(DocumentId),
     WakeUp,
     Flush(MsgSender<()>),
     SetFrameBuilderConfig(FrameBuilderConfig),
     SimulateLongSceneBuild(u32),
     SimulateLongLowPrioritySceneBuild(u32),
     Stop,
+    #[cfg(feature = "capture")]
+    SaveScene(CaptureConfig),
     #[cfg(feature = "replay")]
     LoadScenes(Vec<LoadScene>),
 }
 
 // Message from scene builder to render backend.
 pub enum SceneBuilderResult {
     Transaction(Box<BuiltTransaction>, Option<Sender<SceneSwapResult>>),
     FlushComplete(MsgSender<()>),
@@ -132,18 +139,36 @@ pub enum SceneBuilderResult {
 // Message from render backend to scene builder to indicate the
 // scene swap was completed. We need a separate channel for this
 // so that they don't get mixed with SceneBuilderRequest messages.
 pub enum SceneSwapResult {
     Complete(Sender<()>),
     Aborted,
 }
 
+// A document in the scene builder contains the current scene,
+// as well as a persistent clip interner. This allows clips
+// to be de-duplicated, and persisted in the GPU cache between
+// display lists.
+struct Document {
+    scene: Scene,
+    clip_interner: ClipDataInterner,
+}
+
+impl Document {
+    fn new(scene: Scene) -> Self {
+        Document {
+            scene,
+            clip_interner: ClipDataInterner::new(),
+        }
+    }
+}
+
 pub struct SceneBuilder {
-    documents: FastHashMap<DocumentId, Scene>,
+    documents: FastHashMap<DocumentId, Document>,
     rx: Receiver<SceneBuilderRequest>,
     tx: Sender<SceneBuilderResult>,
     api_tx: MsgSender<ApiMsg>,
     config: FrameBuilderConfig,
     hooks: Option<Box<SceneBuilderHooks + Send>>,
     picture_id_generator: PictureIdGenerator,
     simulate_slow_ms: u32,
 }
@@ -194,16 +219,20 @@ impl SceneBuilder {
                 }
                 Ok(SceneBuilderRequest::SetFrameBuilderConfig(cfg)) => {
                     self.config = cfg;
                 }
                 #[cfg(feature = "replay")]
                 Ok(SceneBuilderRequest::LoadScenes(msg)) => {
                     self.load_scenes(msg);
                 }
+                #[cfg(feature = "capture")]
+                Ok(SceneBuilderRequest::SaveScene(config)) => {
+                    self.save_scene(config);
+                }
                 Ok(SceneBuilderRequest::Stop) => {
                     self.tx.send(SceneBuilderResult::Stopped).unwrap();
                     // We don't need to send a WakeUp to api_tx because we only
                     // get the Stop when the RenderBackend loop is exiting.
                     break;
                 }
                 Ok(SceneBuilderRequest::SimulateLongSceneBuild(time_ms)) => {
                     self.simulate_slow_ms = time_ms
@@ -219,74 +248,97 @@ impl SceneBuilder {
             }
         }
 
         if let Some(ref hooks) = self.hooks {
             hooks.deregister();
         }
     }
 
+    #[cfg(feature = "capture")]
+    fn save_scene(&mut self, config: CaptureConfig) {
+        for (id, doc) in &self.documents {
+            let clip_interner_name = format!("clip-interner-{}-{}", (id.0).0, id.1);
+            config.serialize(&doc.clip_interner, clip_interner_name);
+        }
+    }
+
     #[cfg(feature = "replay")]
     fn load_scenes(&mut self, scenes: Vec<LoadScene>) {
-        for item in scenes {
+        for mut item in scenes {
             self.config = item.config;
 
             let scene_build_start_time = precise_time_ns();
 
             let mut built_scene = None;
+            let mut clip_updates = None;
+
             if item.scene.has_root_pipeline() {
                 let mut clip_scroll_tree = ClipScrollTree::new();
                 let mut new_scene = Scene::new();
 
                 let frame_builder = DisplayListFlattener::create_frame_builder(
                     &item.scene,
                     &mut clip_scroll_tree,
                     item.font_instances,
                     &item.view,
                     &item.output_pipelines,
                     &self.config,
                     &mut new_scene,
                     item.scene_id,
                     &mut self.picture_id_generator,
+                    &mut item.clip_interner,
                 );
 
+                clip_updates = Some(item.clip_interner.end_frame_and_get_pending_updates());
+
                 built_scene = Some(BuiltScene {
                     scene: new_scene,
                     frame_builder,
                     clip_scroll_tree,
                 });
             }
 
-            self.documents.insert(item.document_id, item.scene);
+            self.documents.insert(
+                item.document_id,
+                Document {
+                    scene: item.scene,
+                    clip_interner: item.clip_interner,
+                },
+            );
 
             let txn = Box::new(BuiltTransaction {
                 document_id: item.document_id,
                 build_frame: true,
                 render_frame: item.build_frame,
                 built_scene,
                 resource_updates: Vec::new(),
                 rasterized_blobs: Vec::new(),
                 blob_rasterizer: None,
                 frame_ops: Vec::new(),
                 removed_pipelines: Vec::new(),
                 notifications: Vec::new(),
                 scene_build_start_time,
                 scene_build_end_time: precise_time_ns(),
+                clip_updates,
             });
 
             self.forward_built_transaction(txn);
         }
     }
 
     /// Do the bulk of the work of the scene builder thread.
     fn process_transaction(&mut self, txn: &mut Transaction) -> Box<BuiltTransaction> {
 
         let scene_build_start_time = precise_time_ns();
 
-        let scene = self.documents.entry(txn.document_id).or_insert(Scene::new());
+        let doc = self.documents
+                      .entry(txn.document_id)
+                      .or_insert(Document::new(Scene::new()));
+        let scene = &mut doc.scene;
 
         for update in txn.display_list_updates.drain(..) {
             scene.set_display_list(
                 update.pipeline_id,
                 update.epoch,
                 update.built_display_list,
                 update.background,
                 update.viewport_size,
@@ -302,33 +354,38 @@ impl SceneBuilder {
             scene.set_root_pipeline_id(id);
         }
 
         for pipeline_id in &txn.removed_pipelines {
             scene.remove_pipeline(*pipeline_id)
         }
 
         let mut built_scene = None;
+        let mut clip_updates = None;
         if scene.has_root_pipeline() {
             if let Some(request) = txn.request_scene_build.take() {
                 let mut clip_scroll_tree = ClipScrollTree::new();
                 let mut new_scene = Scene::new();
 
                 let frame_builder = DisplayListFlattener::create_frame_builder(
                     &scene,
                     &mut clip_scroll_tree,
                     request.font_instances,
                     &request.view,
                     &request.output_pipelines,
                     &self.config,
                     &mut new_scene,
                     request.scene_id,
                     &mut self.picture_id_generator,
+                    &mut doc.clip_interner,
                 );
 
+                // Retrieve the list of updates from the clip interner.
+                clip_updates = Some(doc.clip_interner.end_frame_and_get_pending_updates());
+
                 built_scene = Some(BuiltScene {
                     scene: new_scene,
                     frame_builder,
                     clip_scroll_tree,
                 });
             }
         }
 
@@ -355,16 +412,17 @@ impl SceneBuilder {
             render_frame: txn.render_frame,
             built_scene,
             rasterized_blobs,
             resource_updates: replace(&mut txn.resource_updates, Vec::new()),
             blob_rasterizer: replace(&mut txn.blob_rasterizer, None),
             frame_ops: replace(&mut txn.frame_ops, Vec::new()),
             removed_pipelines: replace(&mut txn.removed_pipelines, Vec::new()),
             notifications: replace(&mut txn.notifications, Vec::new()),
+            clip_updates,
             scene_build_start_time,
             scene_build_end_time: precise_time_ns(),
         })
     }
 
     /// Send the result of process_transaction back to the render backend.
     fn forward_built_transaction(&mut self, txn: Box<BuiltTransaction>) {
         // We only need the pipeline info and the result channel if we
--- a/gfx/webrender/src/tiling.rs
+++ b/gfx/webrender/src/tiling.rs
@@ -1,17 +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/. */
 
 use api::{ColorF, BorderStyle, DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixelScale};
 use api::{DeviceUintPoint, DeviceUintRect, DeviceUintSize, DocumentLayer, FilterOp, ImageFormat};
 use api::{LayoutRect, MixBlendMode, PipelineId};
 use batch::{AlphaBatchBuilder, AlphaBatchContainer, ClipBatcher, resolve_image};
-use clip::{ClipStore};
+use clip::{ClipDataStore, ClipStore};
 use clip_scroll_tree::{ClipScrollTree, SpatialNodeIndex};
 use device::{FrameId, Texture};
 #[cfg(feature = "pathfinder")]
 use euclid::{TypedPoint2D, TypedVector2D};
 use gpu_cache::{GpuCache};
 use gpu_types::{BorderInstance, BlurDirection, BlurInstance, PrimitiveHeaders, TransformData, TransformPalette};
 use internal_types::{FastHashMap, SavedTargetIndex, SourceTexture};
 #[cfg(feature = "pathfinder")]
@@ -43,16 +43,17 @@ pub struct ScrollbarPrimitive {
 pub struct RenderTargetIndex(pub usize);
 
 pub struct RenderTargetContext<'a, 'rc> {
     pub device_pixel_scale: DevicePixelScale,
     pub prim_store: &'a PrimitiveStore,
     pub resource_cache: &'rc mut ResourceCache,
     pub use_dual_source_blending: bool,
     pub clip_scroll_tree: &'a ClipScrollTree,
+    pub clip_data_store: &'a ClipDataStore,
 }
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 struct TextureAllocator {
     // TODO(gw): Replace this with a simpler allocator for
     // render target allocation - this use case doesn't need
     // to deal with coalescing etc that the general texture
@@ -594,16 +595,17 @@ impl RenderTarget for AlphaRenderTarget 
                     task_address,
                     task_info.clip_node_range,
                     task_info.root_spatial_node_index,
                     ctx.resource_cache,
                     gpu_cache,
                     clip_store,
                     ctx.clip_scroll_tree,
                     transforms,
+                    ctx.clip_data_store,
                 );
             }
             RenderTaskKind::ClipRegion(ref task) => {
                 let task_address = render_tasks.get_task_address(task_id);
                 self.clip_batcher.add_clip_region(
                     task_address,
                     task.clip_data_address,
                 );
--- a/gfx/webrender_api/src/display_item.rs
+++ b/gfx/webrender_api/src/display_item.rs
@@ -228,24 +228,24 @@ pub struct RectangleDisplayItem {
 pub struct LineDisplayItem {
     pub orientation: LineOrientation, // toggles whether above values are interpreted as x/y values
     pub wavy_line_thickness: f32,
     pub color: ColorF,
     pub style: LineStyle,
 }
 
 #[repr(u8)]
-#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize, Eq, Hash)]
 pub enum LineOrientation {
     Vertical,
     Horizontal,
 }
 
 #[repr(u8)]
-#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize, Eq, Hash)]
 pub enum LineStyle {
     Solid,
     Dotted,
     Dashed,
     Wavy,
 }
 
 #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
@@ -735,17 +735,17 @@ impl LocalClip {
                     complex,
                 )
             }
         }
     }
 }
 
 #[repr(C)]
-#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
+#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize, Eq, Hash)]
 pub enum ClipMode {
     Clip,    // Pixels inside the region are visible.
     ClipOut, // Pixels outside the region are visible.
 }
 
 impl Not for ClipMode {
     type Output = ClipMode;
 
--- a/gfx/webrender_api/src/image.rs
+++ b/gfx/webrender_api/src/image.rs
@@ -1,95 +1,155 @@
 /* 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/. */
 
+#![deny(missing_docs)]
+
 extern crate serde_bytes;
 
 use font::{FontInstanceKey, FontInstanceData, FontKey, FontTemplate};
 use std::sync::Arc;
 use {DevicePoint, DeviceUintPoint, DeviceUintRect, DeviceUintSize};
 use {IdNamespace, TileOffset, TileSize};
 use euclid::size2;
 
+/// An opaque identifier describing an image registered with WebRender.
+/// This is used as a handle to reference images, and is used as the
+/// hash map key for the actual image storage in the `ResourceCache`.
 #[repr(C)]
 #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
 pub struct ImageKey(pub IdNamespace, pub u32);
 
 impl ImageKey {
+    /// Placeholder Image key, used to represent None.
     pub const DUMMY: Self = ImageKey(IdNamespace(0), 0);
 
+    /// Mints a new ImageKey. The given ID must be unique.
     pub fn new(namespace: IdNamespace, key: u32) -> Self {
         ImageKey(namespace, key)
     }
 }
 
 /// An arbitrary identifier for an external image provided by the
 /// application. It must be a unique identifier for each external
 /// image.
 #[repr(C)]
 #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
 pub struct ExternalImageId(pub u64);
 
+/// Specifies the type of texture target in driver terms.
 #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
 pub enum TextureTarget {
+    /// Standard texture. This maps to GL_TEXTURE_2D in OpenGL.
     Default = 0,
+    /// Array texture. This maps to GL_TEXTURE_2D_ARRAY in OpenGL. See
+    /// https://www.khronos.org/opengl/wiki/Array_Texture for background
+    /// on Array textures.
     Array = 1,
+    /// Rectange texture. This maps to GL_TEXTURE_RECTANGLE in OpenGL. This
+    /// is similar to a standard texture, with a few subtle differences
+    /// (no mipmaps, non-power-of-two dimensions, different coordinate space)
+    /// that make it useful for representing the kinds of textures we use
+    /// in WebRender. See https://www.khronos.org/opengl/wiki/Rectangle_Texture
+    /// for background on Rectangle textures.
     Rect = 2,
+    /// External texture. This maps to GL_TEXTURE_EXTERNAL_OES in OpenGL, which
+    /// is an extension. This is used for image formats that OpenGL doesn't
+    /// understand, particularly YUV. See
+    /// https://www.khronos.org/registry/OpenGL/extensions/OES/OES_EGL_image_external.txt
     External = 3,
 }
 
+/// Storage format identifier for externally-managed images.
 #[repr(u32)]
 #[derive(Debug, Copy, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
 pub enum ExternalImageType {
+    /// The image is texture-backed.
     TextureHandle(TextureTarget),
+    /// The image is heap-allocated by the embedding.
     Buffer,
 }
 
+/// Descriptor for external image resources. See `ImageData`.
 #[repr(C)]
 #[derive(Debug, Copy, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
 pub struct ExternalImageData {
+    /// The identifier of this external image, provided by the embedding.
     pub id: ExternalImageId,
+    /// For multi-plane images (i.e. YUV), indicates the plane of the
+    /// original image that this struct represents. 0 for single-plane images.
     pub channel_index: u8,
+    /// Storage format identifier.
     pub image_type: ExternalImageType,
 }
 
+/// Specifies the format of a series of pixels, in driver terms.
 #[repr(u32)]
 #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
 pub enum ImageFormat {
+    /// One-channel, byte storage. The "red" doesn't map to the color
+    /// red per se, and is just the way that OpenGL has historically referred
+    /// to single-channel buffers.
     R8 = 1,
+    /// Four channels, byte storage.
     BGRA8 = 3,
+    /// Four channels, float storage.
     RGBAF32 = 4,
+    /// Two-channels, byte storage. Similar to `R8`, this just means
+    /// "two channels" rather than "red and green".
     RG8 = 5,
+    /// Four channels, signed integer storage.
     RGBAI32 = 6,
 }
 
 impl ImageFormat {
+    /// Returns the number of bytes per pixel for the given format.
     pub fn bytes_per_pixel(self) -> u32 {
         match self {
             ImageFormat::R8 => 1,
             ImageFormat::BGRA8 => 4,
             ImageFormat::RGBAF32 => 16,
             ImageFormat::RG8 => 2,
             ImageFormat::RGBAI32 => 16,
         }
     }
 }
 
+/// Metadata (but not storage) describing an image In WebRender.
 #[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
 pub struct ImageDescriptor {
+    /// Format of the image data.
     pub format: ImageFormat,
+    /// Width and length of the image data, in pixels.
     pub size: DeviceUintSize,
+    /// The number of bytes from the start of one row to the next. If non-None,
+    /// `compute_stride` will return this value, otherwise it returns
+    /// `width * bpp`. Different source of images have different alignment
+    /// constraints for rows, so the stride isn't always equal to width * bpp.
     pub stride: Option<u32>,
+    /// Offset in bytes of the first pixel of this image in its backing buffer.
+    /// This is used for tiling, wherein WebRender extracts chunks of input images
+    /// in order to cache, manipulate, and render them individually. This offset
+    /// tells the texture upload machinery where to find the bytes to upload for
+    /// this tile. Non-tiled images generally set this to zero.
     pub offset: u32,
+    /// Whether this image is opaque, or has an alpha channel. Avoiding blending
+    /// for opaque surfaces is an important optimization.
     pub is_opaque: bool,
+    /// Whether to allow the driver to automatically generate mipmaps. If images
+    /// are already downscaled appropriately, mipmap generation can be wasted
+    /// work, and cause performance problems on some cards/drivers.
+    ///
+    /// See https://github.com/servo/webrender/pull/2555/
     pub allow_mipmaps: bool,
 }
 
 impl ImageDescriptor {
+    /// Mints a new ImageDescriptor.
     pub fn new(
         width: u32,
         height: u32,
         format: ImageFormat,
         is_opaque: bool,
         allow_mipmaps: bool,
     ) -> Self {
         ImageDescriptor {
@@ -97,36 +157,48 @@ impl ImageDescriptor {
             format,
             stride: None,
             offset: 0,
             is_opaque,
             allow_mipmaps,
         }
     }
 
+    /// Returns the stride, either via an explicit stride stashed on the object
+    /// or by the default computation.
     pub fn compute_stride(&self) -> u32 {
         self.stride.unwrap_or(self.size.width * self.format.bytes_per_pixel())
     }
 
+    /// Computes the total size of the image, in bytes.
     pub fn compute_total_size(&self) -> u32 {
         self.compute_stride() * self.size.height
     }
 
+    /// Computes the bounding rectangle for the image, rooted at (0, 0).
     pub fn full_rect(&self) -> DeviceUintRect {
         DeviceUintRect::new(
             DeviceUintPoint::zero(),
             self.size,
         )
     }
 }
 
+/// Represents the backing store of an arbitrary series of pixels for display by
+/// WebRender. This storage can take several forms.
 #[derive(Clone, Debug, Serialize, Deserialize)]
 pub enum ImageData {
+    /// A simple series of bytes, provided by the embedding and owned by WebRender.
+    /// The format is stored out-of-band, currently in ImageDescriptor.
     Raw(#[serde(with = "serde_image_data_raw")] Arc<Vec<u8>>),
+    /// An series of commands that can be rasterized into an image via an
+    /// embedding-provided callback.
     Blob(#[serde(with = "serde_image_data_raw")] Arc<BlobImageData>),
+    /// An image owned by the embedding, and referenced by WebRender. This may
+    /// take the form of a texture or a heap-allocated buffer.
     External(ExternalImageData),
 }
 
 mod serde_image_data_raw {
     extern crate serde_bytes;
 
     use std::sync::Arc;
     use serde::{Deserializer, Serializer};
@@ -136,53 +208,62 @@ mod serde_image_data_raw {
     }
 
     pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Arc<Vec<u8>>, D::Error> {
         serde_bytes::deserialize(deserializer).map(Arc::new)
     }
 }
 
 impl ImageData {
+    /// Mints a new raw ImageData, taking ownership of the bytes.
     pub fn new(bytes: Vec<u8>) -> Self {
         ImageData::Raw(Arc::new(bytes))
     }
 
+    /// Mints a new raw ImageData from Arc-ed bytes.
     pub fn new_shared(bytes: Arc<Vec<u8>>) -> Self {
         ImageData::Raw(bytes)
     }
 
+    /// Mints a new Blob ImageData.
     pub fn new_blob_image(commands: BlobImageData) -> Self {
         ImageData::Blob(Arc::new(commands))
     }
 
+    /// Returns true if this ImageData represents a blob.
     #[inline]
     pub fn is_blob(&self) -> bool {
         match *self {
             ImageData::Blob(_) => true,
             _ => false,
         }
     }
 
+    /// Returns true if this variant of ImageData should go through the texture
+    /// cache.
     #[inline]
     pub fn uses_texture_cache(&self) -> bool {
         match *self {
             ImageData::External(ref ext_data) => match ext_data.image_type {
                 ExternalImageType::TextureHandle(_) => false,
                 ExternalImageType::Buffer => true,
             },
             ImageData::Blob(_) => true,
             ImageData::Raw(_) => true,
         }
     }
 }
 
 /// The resources exposed by the resource cache available for use by the blob rasterizer.
 pub trait BlobImageResources {
+    /// Returns the `FontTemplate` for the given key.
     fn get_font_data(&self, key: FontKey) -> &FontTemplate;
+    /// Returns the `FontInstanceData` for the given key, if found.
     fn get_font_instance_data(&self, key: FontInstanceKey) -> Option<FontInstanceData>;
+    /// Returns the image metadata and backing store for the given key, if found.
     fn get_image(&self, key: ImageKey) -> Option<(&ImageData, &ImageDescriptor)>;
 }
 
 /// A handler on the render backend that can create rasterizer objects which will
 /// be sent to the scene builder thread to execute the rasterization.
 ///
 /// The handler is responsible for collecting resources, managing/updating blob commands
 /// and creating the rasterizer objects, but isn't expected to do any rasterization itself.
@@ -217,49 +298,77 @@ pub trait BlobImageHandler: Send {
 
     /// A hook to let the handler clean up any state related a given namespace before the
     /// resource cache deletes them.
     fn clear_namespace(&mut self, namespace: IdNamespace);
 }
 
 /// A group of rasterization requests to execute synchronously on the scene builder thread.
 pub trait AsyncBlobImageRasterizer : Send {
+    /// Rasterize the requests.
     fn rasterize(&mut self, requests: &[BlobImageParams]) -> Vec<(BlobImageRequest, BlobImageResult)>;
 }
 
 
+/// Input parameters for the BlobImageRasterizer.
 #[derive(Copy, Clone, Debug)]
 pub struct BlobImageParams {
+    /// A key that identifies the blob image rasterization request.
     pub request: BlobImageRequest,
+    /// Description of the format of the blob's output image.
     pub descriptor: BlobImageDescriptor,
+    /// An optional sub-rectangle of the image to avoid re-rasterizing
+    /// the entire image when only a portion is updated.
+    ///
+    /// If set to None the entire image is rasterized.
     pub dirty_rect: Option<DeviceUintRect>,
 }
 
+/// Backing store for blob image command streams.
 pub type BlobImageData = Vec<u8>;
 
+/// Result type for blob raserization.
 pub type BlobImageResult = Result<RasterizedBlobImage, BlobImageError>;
 
+/// Metadata (but not storage) for a blob image.
 #[repr(C)]
 #[derive(Copy, Clone, Debug)]
 pub struct BlobImageDescriptor {
+    /// Size in device pixels of the blob's output image.
     pub size: DeviceUintSize,
+    /// When tiling, offset point in device pixels of this tile in the full
+    /// image. Generally (0, 0) outside of tiling.
     pub offset: DevicePoint,
+    /// Format for the data in the backing store.
     pub format: ImageFormat,
 }
 
+/// Representation of a rasterized blob image. This is obtained by passing
+/// `BlobImageData` to the embedding via the rasterization callback.
 pub struct RasterizedBlobImage {
+    /// The bounding rectangle for this bob image.
     pub rasterized_rect: DeviceUintRect,
+    /// Backing store. The format is stored out of band in `BlobImageDescriptor`.
     pub data: Arc<Vec<u8>>,
 }
 
+/// Error code for when blob rasterization failed.
 #[derive(Clone, Debug)]
 pub enum BlobImageError {
+    /// Out of memory.
     Oom,
-    InvalidKey,
-    InvalidData,
+    /// Other failure, embedding-specified.
     Other(String),
 }
 
+
+
+/// A key identifying blob image rasterization work requested from the blob
+/// image rasterizer.
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub struct BlobImageRequest {
+    /// Unique handle to the image.
     pub key: ImageKey,
+    /// Tiling offset in number of tiles, if applicable.
+    ///
+    /// `None` if the image will not be tiled.
     pub tile: Option<TileOffset>,
 }
--- a/gfx/webrender_api/src/units.rs
+++ b/gfx/webrender_api/src/units.rs
@@ -116,16 +116,17 @@ pub type RasterToLayoutTransform = Typed
 
 pub type PictureToRasterTransform = TypedTransform3D<f32, PicturePixel, RasterPixel>;
 pub type RasterToPictureTransform = TypedTransform3D<f32, RasterPixel, PicturePixel>;
 
 // Fixed position coordinates, to avoid float precision errors.
 pub type LayoutPointAu = TypedPoint2D<Au, LayoutPixel>;
 pub type LayoutRectAu = TypedRect<Au, LayoutPixel>;
 pub type LayoutSizeAu = TypedSize2D<Au, LayoutPixel>;
+pub type LayoutVector2DAu = TypedVector2D<Au, LayoutPixel>;
 
 /// Coordinates in normalized space (between zero and one).
 #[derive(Hash, Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
 pub struct NormalizedCoordinates;
 
 pub type NormalizedRect = TypedRect<f32, NormalizedCoordinates>;
 
 /// Stores two coordinates in texel space. The coordinates
@@ -149,8 +150,85 @@ impl TexelRect {
 
     pub fn invalid() -> Self {
         TexelRect {
             uv0: DevicePoint::new(-1.0, -1.0),
             uv1: DevicePoint::new(-1.0, -1.0),
         }
     }
 }
+
+const MAX_AU_FLOAT: f32 = 1.0e6;
+
+pub trait AuHelpers<T> {
+    fn from_au(data: T) -> Self;
+    fn to_au(&self) -> T;
+}
+
+impl AuHelpers<LayoutSizeAu> for LayoutSize {
+    fn from_au(size: LayoutSizeAu) -> Self {
+        LayoutSize::new(
+            size.width.to_f32_px(),
+            size.height.to_f32_px(),
+        )
+    }
+
+    fn to_au(&self) -> LayoutSizeAu {
+        let width = self.width.min(2.0 * MAX_AU_FLOAT);
+        let height = self.height.min(2.0 * MAX_AU_FLOAT);
+
+        LayoutSizeAu::new(
+            Au::from_f32_px(width),
+            Au::from_f32_px(height),
+        )
+    }
+}
+
+impl AuHelpers<LayoutVector2DAu> for LayoutVector2D {
+    fn from_au(size: LayoutVector2DAu) -> Self {
+        LayoutVector2D::new(
+            size.x.to_f32_px(),
+            size.y.to_f32_px(),
+        )
+    }
+
+    fn to_au(&self) -> LayoutVector2DAu {
+        LayoutVector2DAu::new(
+            Au::from_f32_px(self.x),
+            Au::from_f32_px(self.y),
+        )
+    }
+}
+
+impl AuHelpers<LayoutPointAu> for LayoutPoint {
+    fn from_au(point: LayoutPointAu) -> Self {
+        LayoutPoint::new(
+            point.x.to_f32_px(),
+            point.y.to_f32_px(),
+        )
+    }
+
+    fn to_au(&self) -> LayoutPointAu {
+        let x = self.x.min(MAX_AU_FLOAT).max(-MAX_AU_FLOAT);
+        let y = self.y.min(MAX_AU_FLOAT).max(-MAX_AU_FLOAT);
+
+        LayoutPointAu::new(
+            Au::from_f32_px(x),
+            Au::from_f32_px(y),
+        )
+    }
+}
+
+impl AuHelpers<LayoutRectAu> for LayoutRect {
+    fn from_au(rect: LayoutRectAu) -> Self {
+        LayoutRect::new(
+            LayoutPoint::from_au(rect.origin),
+            LayoutSize::from_au(rect.size),
+        )
+    }
+
+    fn to_au(&self) -> LayoutRectAu {
+        LayoutRectAu::new(
+            self.origin.to_au(),
+            self.size.to_au(),
+        )
+    }
+}
--- a/gfx/webrender_bindings/revision.txt
+++ b/gfx/webrender_bindings/revision.txt
@@ -1,1 +1,1 @@
-5b5b4145ecef117acb02fd1f9b72bf02e85c650b
+f17f6a491d6ff3dc3e13e998dda788d8f5856338
--- a/gfx/wrench/src/yaml_frame_reader.rs
+++ b/gfx/wrench/src/yaml_frame_reader.rs
@@ -1540,17 +1540,17 @@ impl YamlFrameReader {
                 Some(make_perspective(perspective_origin, value as f32))
             }
             Some(_) => None,
             _ => yaml["perspective"].as_matrix4d(),
         };
 
         let reference_frame_id = dl.push_reference_frame(info, transform.into(), perspective);
 
-        let numeric_id = yaml["reference-frame-id"].as_i64();
+        let numeric_id = yaml["id"].as_i64();
         if let Some(numeric_id) = numeric_id {
             self.add_clip_id_mapping(numeric_id as u64, reference_frame_id);
         }
 
         reference_frame_id
     }
 
     pub fn handle_reference_frame(