Bug 1501322 - Update webrender to 9b1a2f7e46cfb3496d43421b828543b08bb45810. r=kats
authorWR Updater Bot <graphics-team@mozilla.staktrace.com>
Tue, 23 Oct 2018 15:13:53 +0000
changeset 490939 ed389461fa718da263a79ca2a9f1cd3d1b0eee27
parent 490938 52569b2e27a8018e945ea0e2ade37b0b325458c6
child 490940 1df447ff4c775115522c4c70366db19e59b0e12c
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewerskats
bugs1501322
milestone65.0a1
Bug 1501322 - Update webrender to 9b1a2f7e46cfb3496d43421b828543b08bb45810. r=kats Differential Revision: https://phabricator.services.mozilla.com/D9539
gfx/webrender/res/cs_clip_image.glsl
gfx/webrender/src/batch.rs
gfx/webrender/src/clip.rs
gfx/webrender/src/hit_test.rs
gfx/webrender/src/image.rs
gfx/webrender/src/prim_store.rs
gfx/webrender/src/render_task.rs
gfx/webrender/src/resource_cache.rs
gfx/webrender_bindings/revision.txt
gfx/wrench/src/yaml_frame_reader.rs
--- a/gfx/webrender/res/cs_clip_image.glsl
+++ b/gfx/webrender/res/cs_clip_image.glsl
@@ -1,70 +1,71 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include shared,clip_shared
 
-varying vec3 vLocalPos;
-varying vec3 vClipMaskImageUv;
+varying vec2 vLocalPos;
+varying vec2 vClipMaskImageUv;
 
 flat varying vec4 vClipMaskUvRect;
 flat varying vec4 vClipMaskUvInnerRect;
 flat varying float vLayer;
 
 #ifdef WR_VERTEX_SHADER
 struct ImageMaskData {
-    RectWithSize local_rect;
+    RectWithSize local_mask_rect;
+    RectWithSize local_tile_rect;
 };
 
 ImageMaskData fetch_mask_data(ivec2 address) {
-    vec4 data = fetch_from_gpu_cache_1_direct(address);
-    RectWithSize local_rect = RectWithSize(data.xy, data.zw);
-    ImageMaskData mask_data = ImageMaskData(local_rect);
+    vec4 data[2] = fetch_from_gpu_cache_2_direct(address);
+    RectWithSize mask_rect = RectWithSize(data[0].xy, data[0].zw);
+    RectWithSize tile_rect = RectWithSize(data[1].xy, data[1].zw);
+    ImageMaskData mask_data = ImageMaskData(mask_rect, tile_rect);
     return mask_data;
 }
 
 void main(void) {
     ClipMaskInstance cmi = fetch_clip_item();
     ClipArea area = fetch_clip_area(cmi.render_task_address);
     Transform clip_transform = fetch_transform(cmi.clip_transform_id);
     Transform prim_transform = fetch_transform(cmi.prim_transform_id);
     ImageMaskData mask = fetch_mask_data(cmi.clip_data_address);
-    RectWithSize local_rect = mask.local_rect;
+    RectWithSize local_rect = mask.local_mask_rect;
     ImageResource res = fetch_image_resource_direct(cmi.resource_address);
 
     ClipVertexInfo vi = write_clip_tile_vertex(
         local_rect,
         prim_transform,
         clip_transform,
         area
     );
-    vLocalPos = vi.local_pos;
+    vLocalPos = vi.local_pos.xy / vi.local_pos.z;
     vLayer = res.layer;
-
-    vec2 local_pos = vLocalPos.xy / vLocalPos.z;
-
-    vClipMaskImageUv = vec3((local_pos - local_rect.p0) / local_rect.size, 0.0);
+    vClipMaskImageUv = (vLocalPos - mask.local_tile_rect.p0) / mask.local_tile_rect.size;
     vec2 texture_size = vec2(textureSize(sColor0, 0));
     vClipMaskUvRect = vec4(res.uv_rect.p0, res.uv_rect.p1 - res.uv_rect.p0) / texture_size.xyxy;
     // applying a half-texel offset to the UV boundaries to prevent linear samples from the outside
     vec4 inner_rect = vec4(res.uv_rect.p0, res.uv_rect.p1);
     vClipMaskUvInnerRect = (inner_rect + vec4(0.5, 0.5, -0.5, -0.5)) / texture_size.xyxy;
 }
 #endif
 
 #ifdef WR_FRAGMENT_SHADER
 void main(void) {
-    vec2 local_pos = vLocalPos.xy / vLocalPos.z;
+    float alpha = init_transform_fs(vLocalPos);
 
-    float alpha = init_transform_fs(local_pos);
+    // TODO: Handle repeating masks?
+    vec2 clamped_mask_uv = clamp(vClipMaskImageUv, vec2(0.0, 0.0), vec2(1.0, 1.0));
 
-    bool repeat_mask = false; //TODO
-    vec2 clamped_mask_uv = repeat_mask ? fract(vClipMaskImageUv.xy) :
-        clamp(vClipMaskImageUv.xy, vec2(0.0, 0.0), vec2(1.0, 1.0));
+    // Ensure we don't draw outside of our tile.
+    // FIXME(emilio): Can we do this earlier?
+    if (clamped_mask_uv != vClipMaskImageUv)
+        discard;
+
     vec2 source_uv = clamp(clamped_mask_uv * vClipMaskUvRect.zw + vClipMaskUvRect.xy,
         vClipMaskUvInnerRect.xy, vClipMaskUvInnerRect.zw);
     float clip_alpha = texture(sColor0, vec3(source_uv, vLayer)).r; //careful: texture has type A8
-
     oFragColor = vec4(alpha * clip_alpha, 1.0, 1.0, 1.0);
 }
 #endif
--- a/gfx/webrender/src/batch.rs
+++ b/gfx/webrender/src/batch.rs
@@ -1944,47 +1944,61 @@ impl ClipBatcher {
                 render_task_address: task_address,
                 clip_transform_id,
                 prim_transform_id,
                 segment: 0,
                 clip_data_address: GpuCacheAddress::invalid(),
                 resource_address: GpuCacheAddress::invalid(),
             };
 
-            let gpu_address = gpu_cache.get_address(&clip_node.gpu_cache_handle);
-
             match clip_node.item {
-                ClipItem::Image(ref mask, is_valid) => {
-                    if is_valid {
-                        if let Ok(cache_item) = resource_cache.get_cached_image(
-                            ImageRequest {
-                                key: mask.image,
-                                rendering: ImageRendering::Auto,
-                                tile: None,
+                ClipItem::Image { ref mask, ref visible_tiles } => {
+                    let request = ImageRequest {
+                        key: mask.image,
+                        rendering: ImageRendering::Auto,
+                        tile: None,
+                    };
+                    let mut add_image = |request: ImageRequest, clip_data_address: GpuCacheAddress| {
+                        let cache_item = match resource_cache.get_cached_image(request) {
+                            Ok(item) => item,
+                            Err(..) => {
+                                warn!("Warnings: skip a image mask");
+                                debug!("Mask: {:?}, request: {:?}", mask, request);
+                                return;
                             }
-                        ) {
-                            self.images
-                                .entry(cache_item.texture_id)
-                                .or_insert(Vec::new())
-                                .push(ClipMaskInstance {
-                                    clip_data_address: gpu_address,
-                                    resource_address: gpu_cache.get_address(&cache_item.uv_rect_handle),
-                                    ..instance
-                                });
-                        } else {
-                            warn!("Warnings: skip a image mask");
-                            debug!("Key:{:?} Rect::{:?}", mask.image, mask.rect);
-                            continue;
+                        };
+                        self.images
+                            .entry(cache_item.texture_id)
+                            .or_insert(Vec::new())
+                            .push(ClipMaskInstance {
+                                clip_data_address,
+                                resource_address: gpu_cache.get_address(&cache_item.uv_rect_handle),
+                                ..instance
+                            });
+                    };
+
+                    match *visible_tiles {
+                        Some(ref tiles) => {
+                            for tile in tiles {
+                                add_image(
+                                    request.with_tile(tile.tile_offset),
+                                    gpu_cache.get_address(&tile.handle),
+                                )
+                            }
                         }
-                    } else {
-                        warn!("Warnings: clip masks that are tiled blobs are not yet supported (#2852)");
-                        continue;
+                        None => {
+                            let gpu_address =
+                                gpu_cache.get_address(&clip_node.gpu_cache_handle);
+                            add_image(request, gpu_address)
+                        }
                     }
                 }
                 ClipItem::BoxShadow(ref info) => {
+                    let gpu_address =
+                        gpu_cache.get_address(&clip_node.gpu_cache_handle);
                     let rt_handle = info
                         .cache_handle
                         .as_ref()
                         .expect("bug: render task handle not allocated");
                     let rt_cache_entry = resource_cache
                         .get_cached_render_task(rt_handle);
                     let cache_item = resource_cache
                         .get_texture_cache_item(&rt_cache_entry.handle);
@@ -1997,23 +2011,27 @@ impl ClipBatcher {
                             clip_data_address: gpu_address,
                             resource_address: gpu_cache.get_address(&cache_item.uv_rect_handle),
                             ..instance
                         });
                 }
                 ClipItem::Rectangle(_, mode) => {
                     if !clip_instance.flags.contains(ClipNodeFlags::SAME_COORD_SYSTEM) ||
                         mode == ClipMode::ClipOut {
+                        let gpu_address =
+                            gpu_cache.get_address(&clip_node.gpu_cache_handle);
                         self.rectangles.push(ClipMaskInstance {
                             clip_data_address: gpu_address,
                             ..instance
                         });
                     }
                 }
                 ClipItem::RoundedRectangle(..) => {
+                    let gpu_address =
+                        gpu_cache.get_address(&clip_node.gpu_cache_handle);
                     self.rectangles.push(ClipMaskInstance {
                         clip_data_address: gpu_address,
                         ..instance
                     });
                 }
             }
         }
     }
--- a/gfx/webrender/src/clip.rs
+++ b/gfx/webrender/src/clip.rs
@@ -9,19 +9,20 @@ use api::{PictureRect, LayoutPixel, Worl
 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 image::{self, Repetition};
 use intern;
 use internal_types::FastHashSet;
-use prim_store::{ClipData, ImageMaskData, SpaceMapper};
+use prim_store::{ClipData, ImageMaskData, SpaceMapper, VisibleMaskImageTile};
 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, project_rect, ScaleOffset};
 
 /*
 
@@ -140,24 +141,24 @@ impl From<ClipItemKey> for ClipNode {
             ClipItemKey::RoundedRectangle(rect, radius, mode) => {
                 ClipItem::RoundedRectangle(
                     LayoutRect::from_au(rect),
                     radius.into(),
                     mode,
                 )
             }
             ClipItemKey::ImageMask(rect, image, repeat) => {
-                ClipItem::Image(
-                    ImageMask {
+                ClipItem::Image {
+                    mask: ImageMask {
                         image,
                         rect: LayoutRect::from_au(rect),
                         repeat,
                     },
-                    true,
-                )
+                    visible_tiles: None,
+                }
             }
             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,
@@ -258,71 +259,104 @@ struct ClipNodeInfo {
 }
 
 impl ClipNode {
     pub fn update(
         &mut self,
         gpu_cache: &mut GpuCache,
         resource_cache: &mut ResourceCache,
         device_pixel_scale: DevicePixelScale,
+        clipped_rect: &LayoutRect,
     ) {
-        if let Some(mut request) = gpu_cache.request(&mut self.gpu_cache_handle) {
-            match self.item {
-                ClipItem::Image(ref mask, ..) => {
-                    let data = ImageMaskData { local_rect: mask.rect };
-                    data.write_gpu_blocks(request);
+        match self.item {
+            ClipItem::Image { ref mask, ref mut visible_tiles } => {
+                let request = ImageRequest {
+                    key: mask.image,
+                    rendering: ImageRendering::Auto,
+                    tile: None,
+                };
+                *visible_tiles = None;
+                if let Some(props) = resource_cache.get_image_properties(mask.image) {
+                    if let Some(tile_size) = props.tiling {
+                        let mut mask_tiles = Vec::new();
+
+                        let device_image_size = props.descriptor.size;
+                        let visible_rect = if mask.repeat {
+                            *clipped_rect
+                        } else {
+                            clipped_rect.intersection(&mask.rect).unwrap()
+                        };
+
+                        let repetitions = image::repetitions(
+                            &mask.rect,
+                            &visible_rect,
+                            mask.rect.size,
+                        );
+
+                        for Repetition { origin, .. } in repetitions {
+                            let image_rect = LayoutRect {
+                                origin,
+                                size: mask.rect.size,
+                            };
+                            let tiles = image::tiles(
+                                &image_rect,
+                                &visible_rect,
+                                &device_image_size,
+                                tile_size as u32,
+                            );
+                            for tile in tiles {
+                                resource_cache.request_image(
+                                    request.with_tile(tile.offset),
+                                    gpu_cache,
+                                );
+                                let mut handle = GpuCacheHandle::new();
+                                if let Some(request) = gpu_cache.request(&mut handle) {
+                                    let data = ImageMaskData {
+                                        local_mask_rect: mask.rect,
+                                        local_tile_rect: tile.rect,
+                                    };
+                                    data.write_gpu_blocks(request);
+                                }
+
+                                mask_tiles.push(VisibleMaskImageTile {
+                                    tile_offset: tile.offset,
+                                    handle,
+                                });
+                            }
+                        }
+                        *visible_tiles = Some(mask_tiles);
+                    } else {
+                        if let Some(request) = gpu_cache.request(&mut self.gpu_cache_handle) {
+                            let data = ImageMaskData {
+                                local_mask_rect: mask.rect,
+                                local_tile_rect: mask.rect,
+                            };
+                            data.write_gpu_blocks(request);
+                        }
+                        resource_cache.request_image(request, gpu_cache);
+                    }
                 }
-                ClipItem::BoxShadow(ref info) => {
+            }
+            ClipItem::BoxShadow(ref mut info) => {
+                if let Some(mut request) = gpu_cache.request(&mut self.gpu_cache_handle) {
                     request.push([
                         info.shadow_rect_alloc_size.width,
                         info.shadow_rect_alloc_size.height,
                         info.clip_mode as i32 as f32,
                         0.0,
                     ]);
                     request.push([
                         info.stretch_mode_x as i32 as f32,
                         info.stretch_mode_y as i32 as f32,
                         0.0,
                         0.0,
                     ]);
                     request.push(info.prim_shadow_rect);
                 }
-                ClipItem::Rectangle(rect, mode) => {
-                    let data = ClipData::uniform(rect, 0.0, mode);
-                    data.write(&mut request);
-                }
-                ClipItem::RoundedRectangle(ref rect, ref radius, mode) => {
-                    let data = ClipData::rounded_rect(rect, radius, mode);
-                    data.write(&mut request);
-                }
-            }
-        }
 
-        match self.item {
-            ClipItem::Image(ref mask, ref mut is_valid) => {
-                if let Some(properties) = resource_cache.get_image_properties(mask.image) {
-                    // Clip masks with tiled blob images are not currently supported.
-                    // This results in them being ignored, which is not ideal, but
-                    // is better than crashing in resource cache!
-                    // See https://github.com/servo/webrender/issues/2852.
-                    *is_valid = properties.tiling.is_none();
-                }
-
-                if *is_valid {
-                    resource_cache.request_image(
-                        ImageRequest {
-                            key: mask.image,
-                            rendering: ImageRendering::Auto,
-                            tile: None,
-                        },
-                        gpu_cache,
-                    );
-                }
-            }
-            ClipItem::BoxShadow(ref mut info) => {
                 // Quote from https://drafts.csswg.org/css-backgrounds-3/#shadow-blur
                 // "the image that would be generated by applying to the shadow a
                 // Gaussian blur with a standard deviation equal to half the blur radius."
                 let blur_radius_dp = (info.blur_radius * 0.5 * device_pixel_scale.0).round();
 
                 // Create the cache key for this box-shadow render task.
                 let content_scale = LayoutToWorldScale::new(1.0) * device_pixel_scale;
                 let cache_size = to_cache_size(info.shadow_rect_alloc_size * content_scale);
@@ -343,18 +377,28 @@ impl ClipNode {
                         &info.minimal_shadow_rect,
                         &info.shadow_radius,
                         ClipMode::Clip,
                     );
 
                     data.write(&mut request);
                 }
             }
-            ClipItem::Rectangle(..) |
-            ClipItem::RoundedRectangle(..) => {}
+            ClipItem::Rectangle(rect, mode) => {
+                if let Some(mut request) = gpu_cache.request(&mut self.gpu_cache_handle) {
+                    let data = ClipData::uniform(rect, 0.0, mode);
+                    data.write(&mut request);
+                }
+            }
+            ClipItem::RoundedRectangle(ref rect, ref radius, mode) => {
+                if let Some(mut request) = gpu_cache.request(&mut self.gpu_cache_handle) {
+                    let data = ClipData::rounded_rect(rect, radius, mode);
+                    data.write(&mut request);
+                }
+            }
         }
     }
 }
 
 // The main clipping public interface that other modules access.
 pub struct ClipStore {
     pub clip_chain_nodes: Vec<ClipChainNode>,
     clip_node_instances: Vec<ClipNodeInstance>,
@@ -563,16 +607,17 @@ impl ClipStore {
                 ClipResult::Partial => {
                     // Needs a mask -> add to clip node indices
 
                     // TODO(gw): Ensure this only runs once on each node per frame?
                     node.update(
                         gpu_cache,
                         resource_cache,
                         device_pixel_scale,
+                        &local_bounding_rect,
                     );
 
                     // Calculate some flags that are required for the segment
                     // building logic.
                     let flags = match node_info.conversion {
                         ClipSpaceConversion::Local => {
                             ClipNodeFlags::SAME_SPATIAL_NODE | ClipNodeFlags::SAME_COORD_SYSTEM
                         }
@@ -588,17 +633,17 @@ impl ClipStore {
                     // in the same coordinate system as the primitive doesn't need
                     // a clip mask. Instead, it can be handled by the primitive
                     // vertex shader as part of the local clip rect. This is an
                     // important optimization for reducing the number of clip
                     // masks that are allocated on common pages.
                     needs_mask |= match node.item {
                         ClipItem::Rectangle(_, ClipMode::ClipOut) |
                         ClipItem::RoundedRectangle(..) |
-                        ClipItem::Image(..) |
+                        ClipItem::Image { .. } |
                         ClipItem::BoxShadow(..) => {
                             true
                         }
 
                         ClipItem::Rectangle(_, ClipMode::Clip) => {
                             !flags.contains(ClipNodeFlags::SAME_COORD_SYSTEM)
                         }
                     };
@@ -762,25 +807,23 @@ impl ClipItemKey {
             shadow_radius.into(),
             prim_shadow_rect.to_au(),
             Au::from_f32_px(blur_radius),
             clip_mode,
         )
     }
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub enum ClipItem {
     Rectangle(LayoutRect, ClipMode),
     RoundedRectangle(LayoutRect, BorderRadius, ClipMode),
-    /// The boolean below is a crash workaround for #2852, will be true unless
-    /// the mask is a tiled blob.
-    Image(ImageMask, bool),
+    Image { mask: ImageMask, visible_tiles: Option<Vec<VisibleMaskImageTile>> },
     BoxShadow(BoxShadowClipSource),
 }
 
 impl ClipItem {
     pub fn new_box_shadow(
         shadow_rect: LayoutRect,
         mut shadow_radius: BorderRadius,
         prim_shadow_rect: LayoutRect,
@@ -883,18 +926,18 @@ impl ClipItem {
     // 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,
             ClipItem::RoundedRectangle(clip_rect, _, ClipMode::Clip) => Some(clip_rect),
             ClipItem::RoundedRectangle(_, _, ClipMode::ClipOut) => None,
-            ClipItem::Image(ref mask, ..) if mask.repeat => None,
-            ClipItem::Image(ref mask, ..) => Some(mask.rect),
+            ClipItem::Image { ref mask, .. } if mask.repeat => None,
+            ClipItem::Image { ref mask, .. } => Some(mask.rect),
             ClipItem::BoxShadow(..) => None,
         }
     }
 
     fn get_clip_result_complex(
         &self,
         transform: &LayoutToWorldTransform,
         prim_world_rect: &WorldRect,
@@ -905,17 +948,17 @@ impl ClipItem {
                 (clip_rect, Some(clip_rect))
             }
             ClipItem::RoundedRectangle(ref clip_rect, ref radius, ClipMode::Clip) => {
                 let inner_clip_rect = extract_inner_rect_safe(clip_rect, radius);
                 (*clip_rect, inner_clip_rect)
             }
             ClipItem::Rectangle(_, ClipMode::ClipOut) |
             ClipItem::RoundedRectangle(_, _, ClipMode::ClipOut) |
-            ClipItem::Image(..) |
+            ClipItem::Image { .. } |
             ClipItem::BoxShadow(..) => {
                 return ClipResult::Partial
             }
         };
 
         let inner_clip_rect = inner_rect.and_then(|ref inner_rect| {
             project_inner_rect(transform, inner_rect)
         });
@@ -1016,17 +1059,17 @@ impl ClipItem {
                     Some(_) => {
                         ClipResult::Partial
                     }
                     None => {
                         ClipResult::Accept
                     }
                 }
             }
-            ClipItem::Image(ref mask, ..) => {
+            ClipItem::Image { ref mask, .. } => {
                 if mask.repeat {
                     ClipResult::Partial
                 } else {
                     match mask.rect.intersection(prim_rect) {
                         Some(..) => {
                             ClipResult::Partial
                         }
                         None => {
--- a/gfx/webrender/src/hit_test.rs
+++ b/gfx/webrender/src/hit_test.rs
@@ -34,17 +34,18 @@ pub struct HitTestClipNode {
 }
 
 impl HitTestClipNode {
     fn new(node: &ClipNode) -> Self {
         let region = match node.item {
             ClipItem::Rectangle(ref rect, mode) => HitTestRegion::Rectangle(*rect, mode),
             ClipItem::RoundedRectangle(ref rect, ref radii, ref mode) =>
                 HitTestRegion::RoundedRectangle(*rect, *radii, *mode),
-            ClipItem::Image(ref mask, ..) => HitTestRegion::Rectangle(mask.rect, ClipMode::Clip),
+            ClipItem::Image { ref mask, .. } =>
+                HitTestRegion::Rectangle(mask.rect, ClipMode::Clip),
             ClipItem::BoxShadow(_) => HitTestRegion::Invalid,
         };
 
         HitTestClipNode {
             region,
         }
     }
 }
--- a/gfx/webrender/src/image.rs
+++ b/gfx/webrender/src/image.rs
@@ -24,28 +24,95 @@ pub fn simplify_repeated_primitive(
         prim_rect.size.width = f32::min(prim_rect.size.width, stretch_size.width);
     }
     if stride.height >= prim_rect.size.height {
         tile_spacing.height = 0.0;
         prim_rect.size.height = f32::min(prim_rect.size.height, stretch_size.height);
     }
 }
 
-pub fn for_each_repetition(
+pub struct Repetition {
+    pub origin: LayoutPoint,
+    pub edge_flags: EdgeAaSegmentMask,
+}
+
+pub struct RepetitionIterator {
+    current_x: i32,
+    x_count: i32,
+    current_y: i32,
+    y_count: i32,
+    row_flags: EdgeAaSegmentMask,
+    current_origin: LayoutPoint,
+    initial_origin: LayoutPoint,
+    stride: LayoutSize,
+}
+
+impl Iterator for RepetitionIterator {
+    type Item = Repetition;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.current_x == self.x_count {
+            self.current_y += 1;
+            if self.current_y >= self.y_count {
+                return None;
+            }
+            self.current_x = 0;
+
+            self.row_flags = EdgeAaSegmentMask::empty();
+            if self.current_y == self.y_count - 1 {
+                self.row_flags |= EdgeAaSegmentMask::BOTTOM;
+            }
+
+            self.current_origin.x = self.initial_origin.x;
+            self.current_origin.y += self.stride.height;
+        }
+
+        let mut edge_flags = self.row_flags;
+        if self.current_x == 0 {
+            edge_flags |= EdgeAaSegmentMask::LEFT;
+        }
+
+        if self.current_x == self.x_count - 1 {
+            edge_flags |= EdgeAaSegmentMask::RIGHT;
+        }
+
+        let repetition = Repetition {
+            origin: self.current_origin,
+            edge_flags: edge_flags,
+        };
+
+        self.current_origin.x += self.stride.width;
+        self.current_x += 1;
+
+        Some(repetition)
+    }
+}
+
+pub fn repetitions(
     prim_rect: &LayoutRect,
     visible_rect: &LayoutRect,
-    stride: &LayoutSize,
-    callback: &mut FnMut(&LayoutPoint, EdgeAaSegmentMask),
-) {
+    stride: LayoutSize,
+) -> RepetitionIterator {
     assert!(stride.width > 0.0);
     assert!(stride.height > 0.0);
 
     let visible_rect = match prim_rect.intersection(&visible_rect) {
-       Some(rect) => rect,
-       None => return,
+        Some(rect) => rect,
+        None => {
+            return RepetitionIterator {
+                current_origin: LayoutPoint::zero(),
+                initial_origin: LayoutPoint::zero(),
+                current_x: 0,
+                current_y: 0,
+                x_count: 0,
+                y_count: 0,
+                stride,
+                row_flags: EdgeAaSegmentMask::empty(),
+            }
+        }
     };
 
     let nx = if visible_rect.origin.x > prim_rect.origin.x {
         f32::floor((visible_rect.origin.x - prim_rect.origin.x) / stride.width)
     } else {
         0.0
     };
 
@@ -53,59 +120,119 @@ pub fn for_each_repetition(
         f32::floor((visible_rect.origin.y - prim_rect.origin.y) / stride.height)
     } else {
         0.0
     };
 
     let x0 = prim_rect.origin.x + nx * stride.width;
     let y0 = prim_rect.origin.y + ny * stride.height;
 
-    let mut p = LayoutPoint::new(x0, y0);
-
     let x_most = visible_rect.max_x();
     let y_most = visible_rect.max_y();
 
     let x_count = f32::ceil((x_most - x0) / stride.width) as i32;
     let y_count = f32::ceil((y_most - y0) / stride.height) as i32;
 
-    for y in 0..y_count {
-        let mut row_flags = EdgeAaSegmentMask::empty();
-        if y == 0 {
-            row_flags |= EdgeAaSegmentMask::TOP;
-        }
-        if y == y_count - 1 {
-            row_flags |= EdgeAaSegmentMask::BOTTOM;
-        }
+    let mut row_flags = EdgeAaSegmentMask::TOP;
+    if y_count == 1 {
+        row_flags |= EdgeAaSegmentMask::BOTTOM;
+    }
 
-        for x in 0..x_count {
-            let mut edge_flags = row_flags;
-            if x == 0 {
-                edge_flags |= EdgeAaSegmentMask::LEFT;
-            }
-            if x == x_count - 1 {
-                edge_flags |= EdgeAaSegmentMask::RIGHT;
-            }
-
-            callback(&p, edge_flags);
-
-            p.x += stride.width;
-        }
-
-        p.x = x0;
-        p.y += stride.height;
+    RepetitionIterator {
+        current_origin: LayoutPoint::new(x0, y0),
+        initial_origin: LayoutPoint::new(x0, y0),
+        current_x: 0,
+        current_y: 0,
+        x_count,
+        y_count,
+        row_flags,
+        stride,
     }
 }
 
-pub fn for_each_tile(
+#[derive(Debug)]
+pub struct Tile {
+    pub rect: LayoutRect,
+    pub offset: TileOffset,
+    pub edge_flags: EdgeAaSegmentMask,
+}
+
+pub struct TileIterator {
+    current_x: u16,
+    x_count: u16,
+    current_y: u16,
+    y_count: u16,
+    origin: TileOffset,
+    tile_size: LayoutSize,
+    leftover_offset: TileOffset,
+    leftover_size: LayoutSize,
+    local_origin: LayoutPoint,
+    row_flags: EdgeAaSegmentMask,
+}
+
+impl Iterator for TileIterator {
+    type Item = Tile;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.current_x == self.x_count {
+            self.current_y += 1;
+            if self.current_y >= self.y_count {
+                return None;
+            }
+            self.current_x = 0;
+            self.row_flags = EdgeAaSegmentMask::empty();
+            if self.current_y == self.y_count - 1 {
+                self.row_flags |= EdgeAaSegmentMask::BOTTOM;
+            }
+        }
+
+        let tile_offset = self.origin + vec2(self.current_x, self.current_y);
+
+        let mut segment_rect = LayoutRect {
+            origin: LayoutPoint::new(
+                self.local_origin.x + tile_offset.x as f32 * self.tile_size.width,
+                self.local_origin.y + tile_offset.y as f32 * self.tile_size.height,
+            ),
+            size: self.tile_size,
+        };
+
+        if tile_offset.x == self.leftover_offset.x {
+            segment_rect.size.width = self.leftover_size.width;
+        }
+
+        if tile_offset.y == self.leftover_offset.y {
+            segment_rect.size.height = self.leftover_size.height;
+        }
+
+        let mut edge_flags = self.row_flags;
+        if self.current_x == 0 {
+            edge_flags |= EdgeAaSegmentMask::LEFT;
+        }
+
+        if self.current_x == self.x_count - 1 {
+            edge_flags |= EdgeAaSegmentMask::RIGHT;
+        }
+
+        let tile = Tile {
+            rect: segment_rect,
+            offset: tile_offset,
+            edge_flags,
+        };
+
+        self.current_x += 1;
+        Some(tile)
+    }
+}
+
+pub fn tiles(
     prim_rect: &LayoutRect,
     visible_rect: &LayoutRect,
     device_image_size: &DeviceUintSize,
     device_tile_size: u32,
-    callback: &mut FnMut(&LayoutRect, TileOffset, EdgeAaSegmentMask),
-) {
+) -> TileIterator {
     // The image resource is tiled. We have to generate an image primitive
     // for each tile.
     // We need to do this because the image is broken up into smaller tiles in the texture
     // cache and the image shader is not able to work with this type of sparse representation.
 
     // The tiling logic works as follows:
     //
     //  ###################-+  -+
@@ -125,18 +252,31 @@ pub fn for_each_tile(
     // each generated segment corresponds to a tile in the texture cache, with the
     // assumption that the smaller tiles with leftover sizes are sized to fit their own
     // irregular size in the texture cache.
 
     // Because we can have very large virtual images we iterate over the visible portion of
     // the image in layer space intead of iterating over device tiles.
 
     let visible_rect = match prim_rect.intersection(&visible_rect) {
-       Some(rect) => rect,
-       None => return,
+        Some(rect) => rect,
+        None => {
+            return TileIterator {
+                current_x: 0,
+                current_y: 0,
+                x_count: 0,
+                y_count: 0,
+                row_flags: EdgeAaSegmentMask::empty(),
+                origin: TileOffset::zero(),
+                tile_size: LayoutSize::zero(),
+                leftover_offset: TileOffset::zero(),
+                leftover_size: LayoutSize::zero(),
+                local_origin: LayoutPoint::zero(),
+            }
+        }
     };
 
     let device_tile_size_f32 = device_tile_size as f32;
 
     // Ratio between (image space) tile size and image size .
     let tile_dw = device_tile_size_f32 / (device_image_size.width as f32);
     let tile_dh = device_tile_size_f32 / (device_image_size.height as f32);
 
@@ -178,56 +318,31 @@ pub fn for_each_tile(
         } else {
             0
         },
     );
 
     let x_count = f32::ceil((visible_rect.max_x() - prim_rect.origin.x) / layer_tile_size.width) as u16 - t0.x;
     let y_count = f32::ceil((visible_rect.max_y() - prim_rect.origin.y) / layer_tile_size.height) as u16 - t0.y;
 
-    for y in 0..y_count {
-
-        let mut row_flags = EdgeAaSegmentMask::empty();
-        if y == 0 {
-            row_flags |= EdgeAaSegmentMask::TOP;
-        }
-        if y == y_count - 1 {
-            row_flags |= EdgeAaSegmentMask::BOTTOM;
-        }
-
-        for x in 0..x_count {
-            let tile_offset = t0 + vec2(x, y);
-
-
-            let mut segment_rect = LayoutRect {
-                origin: LayoutPoint::new(
-                    prim_rect.origin.x + tile_offset.x as f32 * layer_tile_size.width,
-                    prim_rect.origin.y + tile_offset.y as f32 * layer_tile_size.height,
-                ),
-                size: layer_tile_size,
-            };
-
-            if tile_offset.x == leftover_offset.x {
-                segment_rect.size.width = leftover_layer_size.width;
-            }
-
-            if tile_offset.y == leftover_offset.y {
-                segment_rect.size.height = leftover_layer_size.height;
-            }
-
-            let mut edge_flags = row_flags;
-            if x == 0 {
-                edge_flags |= EdgeAaSegmentMask::LEFT;
-            }
-            if x == x_count - 1 {
-                edge_flags |= EdgeAaSegmentMask::RIGHT;
-            }
-
-            callback(&segment_rect, tile_offset, edge_flags);
-        }
+    let mut row_flags = EdgeAaSegmentMask::TOP;
+    if y_count == 1 {
+        row_flags |= EdgeAaSegmentMask::BOTTOM;
+    }
+    TileIterator {
+        current_x: 0,
+        current_y: 0,
+        x_count,
+        y_count,
+        row_flags,
+        origin: t0,
+        tile_size: layer_tile_size,
+        leftover_offset,
+        leftover_size: leftover_layer_size,
+        local_origin: prim_rect.origin,
     }
 }
 
 pub fn compute_tile_range(
     visible_area: &DeviceUintRect,
     tile_size: u16,
 ) -> TileRange {
     // Tile dimensions in normalized coordinates.
@@ -247,17 +362,17 @@ pub fn compute_tile_range(
     TileRange {
         origin: t0,
         size: (t1 - t0).to_size(),
     }
 }
 
 pub fn for_each_tile_in_range(
     range: &TileRange,
-    callback: &mut FnMut(TileOffset),
+    mut callback: impl FnMut(TileOffset),
 ) {
     for y in 0..range.size.height {
         for x in 0..range.size.width {
             callback(range.origin + vec2(x, y));
         }
     }
 }
 
@@ -272,30 +387,30 @@ mod tests {
     fn checked_for_each_tile(
         prim_rect: &LayoutRect,
         visible_rect: &LayoutRect,
         device_image_size: &DeviceUintSize,
         device_tile_size: u32,
         callback: &mut FnMut(&LayoutRect, TileOffset, EdgeAaSegmentMask),
     ) {
         let mut coverage = LayoutRect::zero();
-        let mut tiles = HashSet::new();
-        for_each_tile(prim_rect,
-                      visible_rect,
-                      device_image_size,
-                      device_tile_size,
-                      &mut |tile_rect, tile_offset, tile_flags| {
-                          // make sure we don't get sent duplicate tiles
-                          assert!(!tiles.contains(&tile_offset));
-                          tiles.insert(tile_offset);
-                          coverage = coverage.union(tile_rect);
-                          assert!(prim_rect.contains_rect(&tile_rect));
-                          callback(tile_rect, tile_offset, tile_flags);
-                      },
-        );
+        let mut seen_tiles = HashSet::new();
+        for tile in tiles(
+            prim_rect,
+            visible_rect,
+            device_image_size,
+            device_tile_size,
+        ) {
+            // make sure we don't get sent duplicate tiles
+            assert!(!seen_tiles.contains(&tile.offset));
+            seen_tiles.insert(tile.offset);
+            coverage = coverage.union(&tile.rect);
+            assert!(prim_rect.contains_rect(&tile.rect));
+            callback(&tile.rect, tile.offset, tile.edge_flags);
+        }
         assert!(prim_rect.contains_rect(&coverage));
         assert!(coverage.contains_rect(&visible_rect.intersection(&prim_rect).unwrap_or(LayoutRect::zero())));
     }
 
     #[test]
     fn basic() {
         let mut count = 0;
         checked_for_each_tile(&rect(0., 0., 1000., 1000.),
--- a/gfx/webrender/src/prim_store.rs
+++ b/gfx/webrender/src/prim_store.rs
@@ -15,17 +15,17 @@ use clip_scroll_tree::{ClipScrollTree, C
 use clip::{ClipNodeFlags, ClipChainId, ClipChainInstance, ClipItem, ClipNodeCollector};
 use euclid::{TypedTransform3D, TypedRect, TypedScale};
 use frame_builder::{FrameBuildingContext, FrameBuildingState, PictureContext, PictureState};
 use frame_builder::PrimitiveContext;
 use glyph_rasterizer::{FontInstance, FontTransform, GlyphKey, FONT_SIZE_LIMIT};
 use gpu_cache::{GpuBlockData, GpuCache, GpuCacheAddress, GpuCacheHandle, GpuDataRequest,
                 ToGpuBlocks};
 use gpu_types::BrushFlags;
-use image::{for_each_tile, for_each_repetition};
+use image::{self, Repetition};
 use intern;
 use picture::{PictureCompositeMode, PicturePrimitive};
 #[cfg(debug_assertions)]
 use render_backend::FrameId;
 use render_task::{BlitSource, RenderTask, RenderTaskCacheKey, to_cache_size};
 use render_task::{RenderTaskCacheKeyKind, RenderTaskId, RenderTaskCacheEntryHandle};
 use renderer::{MAX_VERTEX_TEXTURE_WIDTH};
 use resource_cache::{ImageProperties, ImageRequest, ResourceCache};
@@ -354,25 +354,35 @@ impl OpacityBinding {
         let changed = new_opacity != self.current;
         self.current = new_opacity;
 
         changed
     }
 }
 
 #[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct VisibleImageTile {
     pub tile_offset: TileOffset,
     pub handle: GpuCacheHandle,
     pub edge_flags: EdgeAaSegmentMask,
     pub local_rect: LayoutRect,
     pub local_clip_rect: LayoutRect,
 }
 
 #[derive(Debug)]
+#[cfg_attr(feature = "capture", derive(Serialize))]
+#[cfg_attr(feature = "replay", derive(Deserialize))]
+pub struct VisibleMaskImageTile {
+    pub tile_offset: TileOffset,
+    pub handle: GpuCacheHandle,
+}
+
+#[derive(Debug)]
 pub struct VisibleGradientTile {
     pub handle: GpuCacheHandle,
     pub local_rect: LayoutRect,
     pub local_clip_rect: LayoutRect,
 }
 
 /// Information about how to cache a border segment,
 /// along with the current render task cache entry.
@@ -534,16 +544,18 @@ impl BrushKind {
 
 bitflags! {
     /// Each bit of the edge AA mask is:
     /// 0, when the edge of the primitive needs to be considered for AA
     /// 1, when the edge of the segment needs to be considered for AA
     ///
     /// *Note*: the bit values have to match the shader logic in
     /// `write_transform_vertex()` function.
+    #[cfg_attr(feature = "capture", derive(Serialize))]
+    #[cfg_attr(feature = "replay", derive(Deserialize))]
     pub struct EdgeAaSegmentMask: u8 {
         const LEFT = 0x1;
         const TOP = 0x2;
         const RIGHT = 0x4;
         const BOTTOM = 0x8;
     }
 }
 
@@ -1257,22 +1269,26 @@ impl ClipCorner {
             inner_radius_y: inner_radius,
         }
     }
 }
 
 #[derive(Debug)]
 #[repr(C)]
 pub struct ImageMaskData {
-    pub local_rect: LayoutRect,
+    /// The local rect of the whole masked area.
+    pub local_mask_rect: LayoutRect,
+    /// The local rect of an individual tile.
+    pub local_tile_rect: LayoutRect,
 }
 
 impl ToGpuBlocks for ImageMaskData {
     fn write_gpu_blocks(&self, mut request: GpuDataRequest) {
-        request.push(self.local_rect);
+        request.push(self.local_mask_rect);
+        request.push(self.local_tile_rect);
     }
 }
 
 #[derive(Debug)]
 pub struct ClipData {
     rect: ClipRect,
     top_left: ClipCorner,
     top_right: ClipCorner,
@@ -2154,38 +2170,34 @@ fn decompose_repeated_primitive(
 
     let visible_rect = compute_conservative_visible_rect(
         prim_context,
         clipped_world_rect,
         &tight_clip_rect
     );
     let stride = *stretch_size + *tile_spacing;
 
-    for_each_repetition(
-        prim_local_rect,
-        &visible_rect,
-        &stride,
-        &mut |origin, _| {
-
-            let mut handle = GpuCacheHandle::new();
-            let rect = LayoutRect {
-                origin: *origin,
-                size: *stretch_size,
-            };
-            if let Some(request) = frame_state.gpu_cache.request(&mut handle) {
-                callback(&rect, request);
-            }
-
-            visible_tiles.push(VisibleGradientTile {
-                local_rect: rect,
-                local_clip_rect: tight_clip_rect,
-                handle
-            });
+    let repetitions = image::repetitions(prim_local_rect, &visible_rect, stride);
+    for Repetition { origin, .. } in repetitions {
+        let mut handle = GpuCacheHandle::new();
+        let rect = LayoutRect {
+            origin: origin,
+            size: *stretch_size,
+        };
+
+        if let Some(request) = frame_state.gpu_cache.request(&mut handle) {
+            callback(&rect, request);
         }
-    );
+
+        visible_tiles.push(VisibleGradientTile {
+            local_rect: rect,
+            local_clip_rect: tight_clip_rect,
+            handle
+        });
+    }
 
     if visible_tiles.is_empty() {
         // At this point if we don't have tiles to show it means we could probably
         // have done a better a job at culling during an earlier stage.
         // Clearing the screen rect has the effect of "culling out" the primitive
         // from the point of view of the batch builder, and ensures we don't hit
         // assertions later on because we didn't request any image.
         instance.clipped_world_rect = None;
@@ -2324,17 +2336,17 @@ impl BrushPrimitive {
                             -0.5 * info.shadow_rect_alloc_size.width,
                             -0.5 * info.shadow_rect_alloc_size.height,
                         ),
                         inner_clip_mode,
                     );
 
                     continue;
                 }
-                ClipItem::Image(..) => {
+                ClipItem::Image { .. } => {
                     rect_clips_only = false;
                     continue;
                 }
             };
 
             segment_builder.push_clip_rect(local_clip_rect, radius, mode);
         }
 
@@ -2681,59 +2693,60 @@ impl Primitive {
                                 );
 
                                 let base_edge_flags = edge_flags_for_tile_spacing(tile_spacing);
 
                                 let stride = stretch_size + *tile_spacing;
 
                                 visible_tiles.clear();
 
-                                for_each_repetition(
+                                let repetitions = image::repetitions(
                                     &self.local_rect,
                                     &visible_rect,
-                                    &stride,
-                                    &mut |origin, edge_flags| {
-                                        let edge_flags = base_edge_flags | edge_flags;
-
-                                        let image_rect = LayoutRect {
-                                            origin: *origin,
-                                            size: stretch_size,
-                                        };
-
-                                        for_each_tile(
-                                            &image_rect,
-                                            &visible_rect,
-                                            &device_image_size,
-                                            tile_size as u32,
-                                            &mut |tile_rect, tile_offset, tile_flags| {
-
-                                                frame_state.resource_cache.request_image(
-                                                    request.with_tile(tile_offset),
-                                                    frame_state.gpu_cache,
-                                                );
-
-                                                let mut handle = GpuCacheHandle::new();
-                                                if let Some(mut request) = frame_state.gpu_cache.request(&mut handle) {
-                                                    request.push(ColorF::new(1.0, 1.0, 1.0, opacity_binding.current).premultiplied());
-                                                    request.push(PremultipliedColorF::WHITE);
-                                                    request.push([tile_rect.size.width, tile_rect.size.height, 0.0, 0.0]);
-                                                    request.write_segment(*tile_rect, [0.0; 4]);
-                                                }
-
-                                                visible_tiles.push(VisibleImageTile {
-                                                    tile_offset,
-                                                    handle,
-                                                    edge_flags: tile_flags & edge_flags,
-                                                    local_rect: *tile_rect,
-                                                    local_clip_rect: tight_clip_rect,
-                                                });
-                                            }
+                                    stride,
+                                );
+
+                                for Repetition { origin, edge_flags } in repetitions {
+                                    let edge_flags = base_edge_flags | edge_flags;
+
+                                    let image_rect = LayoutRect {
+                                        origin,
+                                        size: stretch_size,
+                                    };
+
+                                    let tiles = image::tiles(
+                                        &image_rect,
+                                        &visible_rect,
+                                        &device_image_size,
+                                        tile_size as u32,
+                                    );
+
+                                    for tile in tiles {
+                                        frame_state.resource_cache.request_image(
+                                            request.with_tile(tile.offset),
+                                            frame_state.gpu_cache,
                                         );
+
+                                        let mut handle = GpuCacheHandle::new();
+                                        if let Some(mut request) = frame_state.gpu_cache.request(&mut handle) {
+                                            request.push(ColorF::new(1.0, 1.0, 1.0, opacity_binding.current).premultiplied());
+                                            request.push(PremultipliedColorF::WHITE);
+                                            request.push([tile.rect.size.width, tile.rect.size.height, 0.0, 0.0]);
+                                            request.write_segment(tile.rect, [0.0; 4]);
+                                        }
+
+                                        visible_tiles.push(VisibleImageTile {
+                                            tile_offset: tile.offset,
+                                            handle,
+                                            edge_flags: tile.edge_flags & edge_flags,
+                                            local_rect: tile.rect,
+                                            local_clip_rect: tight_clip_rect,
+                                        });
                                     }
-                                );
+                                }
 
                                 if visible_tiles.is_empty() {
                                     // At this point if we don't have tiles to show it means we could probably
                                     // have done a better a job at culling during an earlier stage.
                                     // Clearing the screen rect has the effect of "culling out" the primitive
                                     // from the point of view of the batch builder, and ensures we don't hit
                                     // assertions later on because we didn't request any image.
                                     prim_instance.clipped_world_rect = None;
--- a/gfx/webrender/src/render_task.rs
+++ b/gfx/webrender/src/render_task.rs
@@ -549,17 +549,17 @@ impl RenderTask {
                             children.push(root_task_id);
 
                             root_task_id
                         }
                     ));
                 }
                 ClipItem::Rectangle(..) |
                 ClipItem::RoundedRectangle(..) |
-                ClipItem::Image(..) => {}
+                ClipItem::Image { .. } => {}
             }
         }
 
         RenderTask::with_dynamic_location(
             outer_rect.size,
             children,
             RenderTaskKind::CacheMask(CacheMaskTask {
                 actual_rect: outer_rect,
--- a/gfx/webrender/src/resource_cache.rs
+++ b/gfx/webrender/src/resource_cache.rs
@@ -1077,17 +1077,17 @@ impl ResourceCache {
                         tiles.size.width -= 2;
                         tiles.origin.x += 1;
                     } else {
                         tiles.size.height -= 2;
                         tiles.origin.y += 1;
                     }
                 }
 
-                for_each_tile_in_range(&tiles, &mut|tile| {
+                for_each_tile_in_range(&tiles, |tile| {
                     let descriptor = BlobImageDescriptor {
                         offset: DevicePoint::new(
                             tile.x as f32 * tile_size as f32,
                             tile.y as f32 * tile_size as f32,
                         ),
                         size: compute_tile_size(
                             &template.descriptor,
                             tile_size,
--- a/gfx/webrender_bindings/revision.txt
+++ b/gfx/webrender_bindings/revision.txt
@@ -1,1 +1,1 @@
-838ce479e6ef8eed44d68e5d283649d0963152b6
+9b1a2f7e46cfb3496d43421b828543b08bb45810
--- a/gfx/wrench/src/yaml_frame_reader.rs
+++ b/gfx/wrench/src/yaml_frame_reader.rs
@@ -67,43 +67,60 @@ fn broadcast<T: Clone>(base_vals: &[T], 
         if vals.len() == num_items {
             break;
         }
         vals.extend_from_slice(base_vals);
     }
     vals
 }
 
+enum CheckerboardKind {
+    BlackGrey,
+    BlackTransparent,
+}
+
 fn generate_checkerboard_image(
     border: u32,
     tile_x_size: u32,
     tile_y_size: u32,
     tile_x_count: u32,
     tile_y_count: u32,
+    kind: CheckerboardKind,
 ) -> (ImageDescriptor, ImageData) {
     let width = 2 * border + tile_x_size * tile_x_count;
     let height = 2 * border + tile_y_size * tile_y_count;
     let mut pixels = Vec::new();
 
     for y in 0 .. height {
         for x in 0 .. width {
             if y < border || y >= (height - border) ||
                x < border || x >= (width - border) {
                 pixels.push(0);
                 pixels.push(0);
                 pixels.push(0xff);
                 pixels.push(0xff);
             } else {
                 let xon = ((x - border) % (2 * tile_x_size)) < tile_x_size;
                 let yon = ((y - border) % (2 * tile_y_size)) < tile_y_size;
-                let value = if xon ^ yon { 0xff } else { 0x7f };
-                pixels.push(value);
-                pixels.push(value);
-                pixels.push(value);
-                pixels.push(0xff);
+                match kind {
+                    CheckerboardKind::BlackGrey => {
+                        let value = if xon ^ yon { 0xff } else { 0x7f };
+                        pixels.push(value);
+                        pixels.push(value);
+                        pixels.push(value);
+                        pixels.push(0xff);
+                    }
+                    CheckerboardKind::BlackTransparent => {
+                        let value = if xon ^ yon { 0xff } else { 0x00 };
+                        pixels.push(value);
+                        pixels.push(value);
+                        pixels.push(value);
+                        pixels.push(value);
+                    }
+                }
             }
         }
     }
 
     (
         ImageDescriptor::new(width, height, ImageFormat::BGRA8, true, false),
         ImageData::new(pixels),
     )
@@ -400,20 +417,20 @@ impl YamlFrameReader {
                 }
             }
             _ => None,
         }
 
     }
 
     pub fn add_or_get_image(
-            &mut self,
-            file: &Path,
-            tiling: Option<i64>,
-            wrench: &mut Wrench,
+        &mut self,
+        file: &Path,
+        tiling: Option<i64>,
+        wrench: &mut Wrench,
     ) -> (ImageKey, LayoutSize) {
         let key = (file.to_owned(), tiling);
         if let Some(k) = self.image_map.get(&key) {
             return *k;
         }
 
         if self.list_resources { println!("{}", file.to_string_lossy()); }
         let (descriptor, image_data) = match image::open(file) {
@@ -471,17 +488,18 @@ impl YamlFrameReader {
                     ("solid-color", args, _) => generate_solid_color_image(
                         args.get(0).unwrap_or(&"255").parse::<u8>().unwrap(),
                         args.get(1).unwrap_or(&"255").parse::<u8>().unwrap(),
                         args.get(2).unwrap_or(&"255").parse::<u8>().unwrap(),
                         args.get(3).unwrap_or(&"255").parse::<u8>().unwrap(),
                         args.get(4).unwrap_or(&"1000").parse::<u32>().unwrap(),
                         args.get(5).unwrap_or(&"1000").parse::<u32>().unwrap(),
                     ),
-                    ("checkerboard", args, _) => {
+                    (name @ "transparent-checkerboard", args, _) |
+                    (name @ "checkerboard", args, _) => {
                         let border = args.get(0).unwrap_or(&"4").parse::<u32>().unwrap();
 
                         let (x_size, y_size, x_count, y_count) = match args.len() {
                             3 => {
                                 let size = args.get(1).unwrap_or(&"32").parse::<u32>().unwrap();
                                 let count = args.get(2).unwrap_or(&"8").parse::<u32>().unwrap();
                                 (size, size, count, count)
                             }
@@ -492,22 +510,29 @@ impl YamlFrameReader {
                                 let y_count = args.get(4).unwrap_or(&"8").parse::<u32>().unwrap();
                                 (x_size, y_size, x_count, y_count)
                             }
                             _ => {
                                 panic!("invalid checkerboard function");
                             }
                         };
 
+                        let kind = if name == "transparent-checkerboard" {
+                            CheckerboardKind::BlackTransparent
+                        } else {
+                            CheckerboardKind::BlackGrey
+                        };
+
                         generate_checkerboard_image(
                             border,
                             x_size,
                             y_size,
                             x_count,
                             y_count,
+                            kind,
                         )
                     }
                     _ => {
                         panic!("Failed to load image {:?}", file.to_str());
                     }
                 }
             }
         };
@@ -595,18 +620,19 @@ impl YamlFrameReader {
                 file
             }
             None => {
                 warn!("No image provided for the image-mask!");
                 return None;
             }
         };
 
+        let tiling = item["tile-size"].as_i64();
         let (image_key, image_dims) =
-            self.add_or_get_image(&file, None, wrench);
+            self.add_or_get_image(&file, tiling, wrench);
         let image_rect = item["rect"]
             .as_rect()
             .unwrap_or(LayoutRect::new(LayoutPoint::zero(), image_dims));
         let image_repeat = item["repeat"].as_bool().expect("Expected boolean");
         Some(ImageMask {
             image: image_key,
             rect: image_rect,
             repeat: image_repeat,