Bug 1431582 - WR support non-locally-rasterized split planes r=gw a=lizzard
authorDzmitry Malyshau <dmalyshau@mozilla.com>
Tue, 29 Jan 2019 19:34:55 +0000
changeset 515647 dc3e444e4a2ced7aeee4164899ef93c78582381e
parent 515646 8f2ea35d460169b553a44f269b8357037ad51cbc
child 515648 34bde0c12172e514a8062c3aa83af6ba6ed94115
push id1953
push userffxbld-merge
push dateMon, 11 Mar 2019 12:10:20 +0000
treeherdermozilla-release@9c35dcbaa899 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgw, lizzard
bugs1431582
milestone66.0
Bug 1431582 - WR support non-locally-rasterized split planes r=gw a=lizzard Now that we no longer guarantee that a picture with perspective transform is rasterized in local space, we need to ensure that the shaders don't apply perspective correction to the texture coordinates twice. For that to be the case, we pass an extra flag to the plane splitting shader, and un-do the perspective correction if it's not enabled. Differential Revision: https://phabricator.services.mozilla.com/D17854
gfx/wr/webrender/res/brush_blend.glsl
gfx/wr/webrender/res/brush_image.glsl
gfx/wr/webrender/res/ps_split_composite.glsl
gfx/wr/webrender/src/batch.rs
gfx/wr/webrender/src/picture.rs
gfx/wr/webrender/src/prim_store/mod.rs
gfx/wr/wrench/reftests/split/reftest.list
--- a/gfx/wr/webrender/res/brush_blend.glsl
+++ b/gfx/wr/webrender/res/brush_blend.glsl
@@ -1,18 +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/. */
 
 #define VECS_PER_SPECIFIC_BRUSH 3
 
 #include shared,prim_shared,brush
 
-varying vec3 vUv;
+// Interpolated UV coordinates to sample.
+varying vec2 vUv;
 
+// X = layer index to sample, Y = flag to allow perspective interpolation of UV.
+flat varying vec2 vLayerAndPerspective;
 flat varying float vAmount;
 flat varying int vOp;
 flat varying mat3 vColorMat;
 flat varying vec3 vColorOffset;
 flat varying vec4 vUvClipBounds;
 
 #ifdef WR_VERTEX_SHADER
 
@@ -34,18 +37,20 @@ void brush_vs(
     // PictureTask src_task = fetch_picture_task(user_data.x);
     vec2 texture_size = vec2(textureSize(sColor0, 0).xy);
     vec2 f = (vi.local_pos - local_rect.p0) / local_rect.size;
     ImageResourceExtra extra_data = fetch_image_resource_extra(user_data.x);
     vec2 x = mix(extra_data.st_tl, extra_data.st_tr, f.x);
     vec2 y = mix(extra_data.st_bl, extra_data.st_br, f.x);
     f = mix(x, y, f.y);
     vec2 uv = mix(uv0, uv1, f);
-    vUv = vec3(uv / texture_size, res.layer);
+    float perspective_interpolate = (brush_flags & BRUSH_FLAG_PERSPECTIVE_INTERPOLATION) != 0 ? 1.0 : 0.0;
 
+    vUv = uv / texture_size * mix(vi.world_pos.w, 1.0, perspective_interpolate);
+    vLayerAndPerspective = vec2(res.layer, perspective_interpolate);
     vUvClipBounds = vec4(uv0, uv1) / texture_size.xyxy;
 
     float lumR = 0.2126;
     float lumG = 0.7152;
     float lumB = 0.0722;
     float oneMinusLumR = 1.0 - lumR;
     float oneMinusLumG = 1.0 - lumG;
     float oneMinusLumB = 1.0 - lumB;
@@ -141,17 +146,19 @@ vec3 SrgbToLinear(vec3 color) {
 
 vec3 LinearToSrgb(vec3 color) {
     vec3 c1 = color * 12.92;
     vec3 c2 = vec3(1.055) * pow(color, vec3(1.0 / 2.4)) - vec3(0.055);
     return if_then_else(lessThanEqual(color, vec3(0.0031308)), c1, c2);
 }
 
 Fragment brush_fs() {
-    vec4 Cs = texture(sColor0, vUv);
+    float perspective_divisor = mix(gl_FragCoord.w, 1.0, vLayerAndPerspective.y);
+    vec2 uv = vUv * perspective_divisor;
+    vec4 Cs = texture(sColor0, vec3(uv, vLayerAndPerspective.x));
 
     // Un-premultiply the input.
     float alpha = Cs.a;
     vec3 color = alpha != 0.0 ? Cs.rgb / alpha : Cs.rgb;
 
     switch (vOp) {
         case 0:
             break;
@@ -174,14 +181,14 @@ Fragment brush_fs() {
             color = LinearToSrgb(color);
             break;
         default:
             color = vColorMat * color + vColorOffset;
     }
 
     // Fail-safe to ensure that we don't sample outside the rendered
     // portion of a blend source.
-    alpha *= point_inside_rect(vUv.xy, vUvClipBounds.xy, vUvClipBounds.zw);
+    alpha *= point_inside_rect(uv, vUvClipBounds.xy, vUvClipBounds.zw);
 
     // Pre-multiply the alpha into the output value.
     return Fragment(alpha * vec4(color, 1.0));
 }
 #endif
--- a/gfx/wr/webrender/res/brush_image.glsl
+++ b/gfx/wr/webrender/res/brush_image.glsl
@@ -5,18 +5,20 @@
 #define VECS_PER_SPECIFIC_BRUSH 3
 
 #include shared,prim_shared,brush
 
 #ifdef WR_FEATURE_ALPHA_PASS
 varying vec2 vLocalPos;
 #endif
 
-// Interpolated uv coordinates in xy, and layer in z.
-varying vec3 vUv;
+// Interpolated UV coordinates to sample.
+varying vec2 vUv;
+// X = layer index to sample, Y = flag to allow perspective interpolation of UV.
+flat varying vec2 vLayerAndPerspective;
 // Normalized bounds of the source image in the texture.
 flat varying vec4 vUvBounds;
 // Normalized bounds of the source image in the texture, adjusted to avoid
 // sampling artifacts.
 flat varying vec4 vUvSampleBounds;
 
 #ifdef WR_FEATURE_ALPHA_PASS
 flat varying vec4 vColor;
@@ -92,17 +94,18 @@ void brush_vs(
 
         // If the extra data is a texel rect, modify the UVs.
         if ((brush_flags & BRUSH_FLAG_TEXEL_RECT) != 0) {
             uv0 = res.uv_rect.p0 + segment_data.xy;
             uv1 = res.uv_rect.p0 + segment_data.zw;
         }
     }
 
-    vUv.z = res.layer;
+    float perspective_interpolate = (brush_flags & BRUSH_FLAG_PERSPECTIVE_INTERPOLATION) != 0 ? 1.0 : 0.0;
+    vLayerAndPerspective = vec2(res.layer, perspective_interpolate);
 
     // Handle case where the UV coords are inverted (e.g. from an
     // external image).
     vec2 min_uv = min(uv0, uv1);
     vec2 max_uv = max(uv0, uv1);
 
     vUvSampleBounds = vec4(
         min_uv + vec2(0.5),
@@ -136,19 +139,22 @@ void brush_vs(
         }
         default:
             break;
     }
 #endif
 
     // Offset and scale vUv here to avoid doing it in the fragment shader.
     vec2 repeat = local_rect.size / stretch_size;
-    vUv.xy = mix(uv0, uv1, f) - min_uv;
-    vUv.xy /= texture_size;
-    vUv.xy *= repeat.xy;
+    vUv = mix(uv0, uv1, f) - min_uv;
+    vUv /= texture_size;
+    vUv *= repeat.xy;
+    if (perspective_interpolate == 0.0) {
+        vUv *= vi.world_pos.w;
+    }
 
 #ifdef WR_FEATURE_TEXTURE_RECT
     vUvBounds = vec4(0.0, 0.0, vec2(textureSize(sColor0)));
 #else
     vUvBounds = vec4(min_uv, max_uv) / texture_size.xyxy;
 #endif
 
 #ifdef WR_FEATURE_ALPHA_PASS
@@ -196,44 +202,45 @@ void brush_vs(
 #endif
 }
 #endif
 
 #ifdef WR_FRAGMENT_SHADER
 
 Fragment brush_fs() {
     vec2 uv_size = vUvBounds.zw - vUvBounds.xy;
+    float perspective_divisor = mix(gl_FragCoord.w, 1.0, vLayerAndPerspective.y);
 
 #ifdef WR_FEATURE_ALPHA_PASS
     // This prevents the uv on the top and left parts of the primitive that was inflated
     // for anti-aliasing purposes from going beyound the range covered by the regular
     // (non-inflated) primitive.
-    vec2 local_uv = max(vUv.xy, vec2(0.0));
+    vec2 local_uv = max(vUv * perspective_divisor, vec2(0.0));
 
     // Handle horizontal and vertical repetitions.
     vec2 repeated_uv = mod(local_uv, uv_size) + vUvBounds.xy;
 
     // This takes care of the bottom and right inflated parts.
     // We do it after the modulo because the latter wraps around the values exactly on
     // the right and bottom edges, which we do not want.
     if (local_uv.x >= vTileRepeat.x * uv_size.x) {
         repeated_uv.x = vUvBounds.z;
     }
     if (local_uv.y >= vTileRepeat.y * uv_size.y) {
         repeated_uv.y = vUvBounds.w;
     }
 #else
     // Handle horizontal and vertical repetitions.
-    vec2 repeated_uv = mod(vUv.xy, uv_size) + vUvBounds.xy;
+    vec2 repeated_uv = mod(vUv * perspective_divisor, uv_size) + vUvBounds.xy;
 #endif
 
     // Clamp the uvs to avoid sampling artifacts.
     vec2 uv = clamp(repeated_uv, vUvSampleBounds.xy, vUvSampleBounds.zw);
 
-    vec4 texel = TEX_SAMPLE(sColor0, vec3(uv, vUv.z));
+    vec4 texel = TEX_SAMPLE(sColor0, vec3(uv, vLayerAndPerspective.x));
 
     Fragment frag;
 
 #ifdef WR_FEATURE_ALPHA_PASS
     float alpha = init_transform_fs(vLocalPos);
     texel.rgb = texel.rgb * vMaskSwizzle.x + texel.aaa * vMaskSwizzle.y;
 
     vec4 alpha_mask = texel * alpha;
--- a/gfx/wr/webrender/res/ps_split_composite.glsl
+++ b/gfx/wr/webrender/res/ps_split_composite.glsl
@@ -1,15 +1,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include shared,prim_shared
 
-varying vec3 vUv;
+// interpolated UV coordinates to sample.
+varying vec2 vUv;
+// X = layer index to sample, Y = flag to allow perspective interpolation of UV.
+flat varying vec2 vLayerAndPerspective;
 flat varying vec4 vUvSampleBounds;
 
 #ifdef WR_VERTEX_SHADER
 struct SplitGeometry {
     vec2 local[4];
 };
 
 SplitGeometry fetch_split_geometry(int address) {
@@ -98,20 +101,23 @@ void main(void) {
     vec2 f = (local_pos - ph.local_rect.p0) / ph.local_rect.size;
 
     f = bilerp(
         extra_data.st_tl, extra_data.st_tr,
         extra_data.st_bl, extra_data.st_br,
         f.y, f.x
     );
     vec2 uv = mix(uv0, uv1, f);
+    float perspective_interpolate = float(ph.user_data.y);
 
-    vUv = vec3(uv / texture_size, res.layer);
+    vUv = uv / texture_size * mix(gl_Position.w, 1.0, perspective_interpolate);
+    vLayerAndPerspective = vec2(res.layer, perspective_interpolate);
 }
 #endif
 
 #ifdef WR_FRAGMENT_SHADER
 void main(void) {
     float alpha = do_clip();
-    vec2 uv = clamp(vUv.xy, vUvSampleBounds.xy, vUvSampleBounds.zw);
-    oFragColor = alpha * textureLod(sPrevPassColor, vec3(uv, vUv.z), 0.0);
+    float perspective_divisor = mix(gl_FragCoord.w, 1.0, vLayerAndPerspective.y);
+    vec2 uv = clamp(vUv * perspective_divisor, vUvSampleBounds.xy, vUvSampleBounds.zw);
+    oFragColor = alpha * textureLod(sPrevPassColor, vec3(uv, vLayerAndPerspective.x), 0.0);
 }
 #endif
--- a/gfx/wr/webrender/src/batch.rs
+++ b/gfx/wr/webrender/src/batch.rs
@@ -592,16 +592,18 @@ impl AlphaBatchBuilder {
     ) {
         if prim_instance.visibility_info == PrimitiveVisibilityIndex::INVALID {
             return;
         }
 
         #[cfg(debug_assertions)] //TODO: why is this needed?
         debug_assert_eq!(prim_instance.prepared_frame_id, render_tasks.frame_id());
 
+        let is_chased = prim_instance.is_chased();
+
         let transform_id = transforms
             .get_id(
                 prim_instance.spatial_node_index,
                 root_spatial_node_index,
                 ctx.clip_scroll_tree,
             );
 
         // TODO(gw): Calculating this for every primitive is a bit
@@ -622,16 +624,22 @@ impl AlphaBatchBuilder {
         ).unwrap_or(OPAQUE_TASK_ADDRESS);
 
         let prim_common_data = &ctx.data_stores.as_common_data(&prim_instance);
         let prim_rect = LayoutRect::new(
             prim_instance.prim_origin,
             prim_common_data.prim_size,
         );
 
+        if is_chased {
+            println!("\tbatch {:?} with clip {:?} and bound {:?}",
+                prim_rect, clip_task_address, bounding_rect);
+        }
+
+
         match prim_instance.kind {
             PrimitiveInstanceKind::Clear { data_handle } => {
                 let prim_data = &ctx.data_stores.prim[data_handle];
                 let prim_cache_address = gpu_cache.get_address(&prim_data.gpu_cache_handle);
 
                 // TODO(gw): We can abstract some of the common code below into
                 //           helper methods, as we port more primitives to make
                 //           use of interning.
@@ -967,32 +975,32 @@ impl AlphaBatchBuilder {
 
                 match picture.context_3d {
                     // Convert all children of the 3D hierarchy root into batches.
                     Picture3DContext::In { root_data: Some(ref list), .. } => {
                         for child in list {
                             let prim_instance = &picture.prim_list.prim_instances[child.anchor];
                             let prim_info = &ctx.scratch.prim_info[prim_instance.visibility_info.0 as usize];
 
-                            let pic_index = match prim_instance.kind {
+                            let child_pic_index = match prim_instance.kind {
                                 PrimitiveInstanceKind::Picture { pic_index, .. } => pic_index,
                                 PrimitiveInstanceKind::LineDecoration { .. } |
                                 PrimitiveInstanceKind::TextRun { .. } |
                                 PrimitiveInstanceKind::NormalBorder { .. } |
                                 PrimitiveInstanceKind::ImageBorder { .. } |
                                 PrimitiveInstanceKind::Rectangle { .. } |
                                 PrimitiveInstanceKind::YuvImage { .. } |
                                 PrimitiveInstanceKind::Image { .. } |
                                 PrimitiveInstanceKind::LinearGradient { .. } |
                                 PrimitiveInstanceKind::RadialGradient { .. } |
                                 PrimitiveInstanceKind::Clear { .. } => {
                                     unreachable!();
                                 }
                             };
-                            let pic = &ctx.prim_store.pictures[pic_index.0];
+                            let pic = &ctx.prim_store.pictures[child_pic_index.0];
 
 
                             // Get clip task, if set, for the picture primitive.
                             let clip_task_address = get_clip_task_address(
                                 &ctx.scratch.clip_mask_instances,
                                 prim_info.clip_task_index,
                                 0,
                                 render_tasks,
@@ -1002,35 +1010,34 @@ impl AlphaBatchBuilder {
                                 local_rect: pic.local_rect,
                                 local_clip_rect: prim_info.combined_local_clip_rect,
                                 task_address,
                                 specific_prim_address: GpuCacheAddress::invalid(),
                                 clip_task_address,
                                 transform_id: child.transform_id,
                             };
 
-                            let surface_index = pic
+                            let raster_config = pic
                                 .raster_config
                                 .as_ref()
-                                .expect("BUG: 3d primitive was not assigned a surface")
-                                .surface_index;
+                                .expect("BUG: 3d primitive was not assigned a surface");
                             let (uv_rect_address, _) = ctx
-                                .surfaces[surface_index.0]
+                                .surfaces[raster_config.surface_index.0]
                                 .surface
                                 .as_ref()
                                 .expect("BUG: no surface")
                                 .resolve(
                                     render_tasks,
                                     ctx.resource_cache,
                                     gpu_cache,
                                 );
 
                             let prim_header_index = prim_headers.push(&prim_header, z_id, [
                                 uv_rect_address.as_int(),
-                                0,
+                                if raster_config.establishes_raster_root { 1 } else { 0 },
                                 0,
                             ]);
 
                             let key = BatchKey::new(
                                 BatchKind::SplitComposite,
                                 BlendMode::PremultipliedAlpha,
                                 BatchTextures::no_texture(),
                             );
@@ -1053,16 +1060,24 @@ impl AlphaBatchBuilder {
                     // hierarchy, since we process them with the root.
                     Picture3DContext::In { root_data: None, .. } => return,
                     // Proceed for non-3D pictures.
                     Picture3DContext::Out => ()
                 }
 
                 match picture.raster_config {
                     Some(ref raster_config) => {
+                        // If the child picture was rendered in local space, we can safely
+                        // interpolate the UV coordinates with perspective correction.
+                        let brush_flags = if raster_config.establishes_raster_root {
+                            BrushFlags::PERSPECTIVE_INTERPOLATION
+                        } else {
+                            BrushFlags::empty()
+                        };
+
                         match raster_config.composite_mode {
                             PictureCompositeMode::TileCache { .. } => {
                                 // Construct a local clip rect that ensures we only draw pixels where
                                 // the local bounds of the picture extend to within the edge tiles.
                                 let local_clip_rect = prim_info
                                     .combined_local_clip_rect
                                     .intersection(&picture.local_rect)
                                     .and_then(|rect| {
@@ -1114,17 +1129,17 @@ impl AlphaBatchBuilder {
                                             .get_address(&cache_item.uv_rect_handle)
                                             .as_int();
 
                                         let instance = BrushInstance {
                                             prim_header_index,
                                             clip_task_address,
                                             segment_index: INVALID_SEGMENT_INDEX,
                                             edge_flags: EdgeAaSegmentMask::empty(),
-                                            brush_flags: BrushFlags::empty(),
+                                            brush_flags,
                                             user_data: uv_rect_address,
                                         };
 
                                         // Instead of retrieving the batch once and adding each tile instance,
                                         // use this API to get an appropriate batch for each tile, since
                                         // the batch textures may be different. The batch list internally
                                         // caches the current batch if the key hasn't changed.
                                         let batch = self.current_batch_list().set_params_and_get_batch(
@@ -1218,17 +1233,17 @@ impl AlphaBatchBuilder {
                                             RasterizationSpace::Screen as i32,
                                             get_shader_opacity(1.0),
                                         ]);
 
                                         let instance = BrushInstance {
                                             prim_header_index,
                                             segment_index: INVALID_SEGMENT_INDEX,
                                             edge_flags: EdgeAaSegmentMask::empty(),
-                                            brush_flags: BrushFlags::empty(),
+                                            brush_flags,
                                             clip_task_address,
                                             user_data: uv_rect_address.as_int(),
                                         };
 
                                         self.current_batch_list().push_single_instance(
                                             key,
                                             bounding_rect,
                                             z_id,
@@ -1298,26 +1313,26 @@ impl AlphaBatchBuilder {
                                             get_shader_opacity(1.0),
                                         ]);
 
                                         let shadow_instance = BrushInstance {
                                             prim_header_index: shadow_prim_header_index,
                                             clip_task_address,
                                             segment_index: INVALID_SEGMENT_INDEX,
                                             edge_flags: EdgeAaSegmentMask::empty(),
-                                            brush_flags: BrushFlags::empty(),
+                                            brush_flags,
                                             user_data: shadow_uv_rect_address,
                                         };
 
                                         let content_instance = BrushInstance {
                                             prim_header_index: content_prim_header_index,
                                             clip_task_address,
                                             segment_index: INVALID_SEGMENT_INDEX,
                                             edge_flags: EdgeAaSegmentMask::empty(),
-                                            brush_flags: BrushFlags::empty(),
+                                            brush_flags,
                                             user_data: content_uv_rect_address,
                                         };
 
                                         self.current_batch_list().push_single_instance(
                                             shadow_key,
                                             bounding_rect,
                                             z_id_shadow,
                                             PrimitiveInstanceData::from(shadow_instance),
@@ -1392,17 +1407,17 @@ impl AlphaBatchBuilder {
                                             user_data,
                                         ]);
 
                                         let instance = BrushInstance {
                                             prim_header_index,
                                             clip_task_address,
                                             segment_index: INVALID_SEGMENT_INDEX,
                                             edge_flags: EdgeAaSegmentMask::empty(),
-                                            brush_flags: BrushFlags::empty(),
+                                            brush_flags,
                                             user_data: 0,
                                         };
 
                                         self.current_batch_list().push_single_instance(
                                             key,
                                             bounding_rect,
                                             z_id,
                                             PrimitiveInstanceData::from(instance),
@@ -1437,17 +1452,17 @@ impl AlphaBatchBuilder {
                                     source_task_address.0 as i32,
                                 ]);
 
                                 let instance = BrushInstance {
                                     prim_header_index,
                                     clip_task_address,
                                     segment_index: INVALID_SEGMENT_INDEX,
                                     edge_flags: EdgeAaSegmentMask::empty(),
-                                    brush_flags: BrushFlags::empty(),
+                                    brush_flags,
                                     user_data: 0,
                                 };
 
                                 self.current_batch_list().push_single_instance(
                                     key,
                                     bounding_rect,
                                     z_id,
                                     PrimitiveInstanceData::from(instance),
@@ -1477,17 +1492,17 @@ impl AlphaBatchBuilder {
                                     get_shader_opacity(1.0),
                                 ]);
 
                                 let instance = BrushInstance {
                                     prim_header_index,
                                     clip_task_address,
                                     segment_index: INVALID_SEGMENT_INDEX,
                                     edge_flags: EdgeAaSegmentMask::empty(),
-                                    brush_flags: BrushFlags::empty(),
+                                    brush_flags,
                                     user_data: uv_rect_address,
                                 };
 
                                 self.current_batch_list().push_single_instance(
                                     key,
                                     bounding_rect,
                                     z_id,
                                     PrimitiveInstanceData::from(instance),
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -2437,17 +2437,17 @@ impl PicturePrimitive {
                     inflation_factor,
                     frame_context.screen_world_rect,
                     &frame_context.clip_scroll_tree,
                 );
             };
 
             self.raster_config = Some(RasterConfig {
                 composite_mode,
-                establishes_raster_root: surface_spatial_node_index == surface.raster_spatial_node_index,
+                establishes_raster_root: surface.raster_spatial_node_index != parent_raster_spatial_node_index,
                 surface_index: state.push_surface(surface),
             });
         }
 
         Some(mem::replace(&mut self.prim_list.pictures, SmallVec::new()))
     }
 
     /// Called after updating child pictures during the initial
--- a/gfx/wr/webrender/src/prim_store/mod.rs
+++ b/gfx/wr/webrender/src/prim_store/mod.rs
@@ -3164,17 +3164,17 @@ impl PrimitiveInstance {
             ) {
                 frame_state.segment_builder.build(|segment| {
                     segments.push(
                         BrushSegment::new(
                             segment.rect.translate(&LayoutVector2D::new(-prim_local_rect.origin.x, -prim_local_rect.origin.y)),
                             segment.has_mask,
                             segment.edge_flags,
                             [0.0; 4],
-                            BrushFlags::empty(),
+                            BrushFlags::PERSPECTIVE_INTERPOLATION,
                         ),
                     );
                 });
             }
 
             if segments.is_empty() {
                 *segment_instance_index = SegmentInstanceIndex::UNUSED;
             } else {
--- a/gfx/wr/wrench/reftests/split/reftest.list
+++ b/gfx/wr/wrench/reftests/split/reftest.list
@@ -11,9 +11,9 @@
 fuzzy(1,20) == near-plane.yaml near-plane.png
 # Note: on windows the image is rendered at a slightly different spot.
 # similarly, a lot of tests in "transform" are non-windows. TODO: investigate
 platform(linux,mac) fuzzy(1,20) == same-plane.yaml same-plane.png
 #TODO: https://github.com/servo/webrender/issues/2946
 #== cross.yaml cross-ref.yaml
 == mixed-order.yaml mixed-order-ref.yaml
 fuzzy(1,40000) == filter.yaml filter-ref.yaml
-#fuzzy(1,10000) == gradient.yaml gradient-ref.yaml
+fuzzy(1,10000) == gradient.yaml gradient-ref.yaml