Bug 1609805 - Support a new reftest kind, for verifying rasterizer accuracy. r=nical,Bert
authorGlenn Watson <gw@intuitionlibrary.com>
Sun, 19 Jan 2020 19:45:37 +0000
changeset 510675 918e156c75aa1ac1fbfdd0a81434c68eb9d11f18
parent 510674 06b1d6b3800216a1d8ca31db900f89f702a70d78
child 510676 2c45e23bf71f150468921cd8a96ed522a42116f8
push id37033
push useraciure@mozilla.com
push dateMon, 20 Jan 2020 09:42:16 +0000
treeherdermozilla-central@206cec28723a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnical, Bert
bugs1609805
milestone74.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1609805 - Support a new reftest kind, for verifying rasterizer accuracy. r=nical,Bert This patch introduces a new reftest (syntax ** or !* in reftest files). This type of test renders a single input file multiple times, at a range of different picture cache tile sizes. It then verifies that each of the images matches (or doesn't). This can be used to verify rasterizer accuracy when drawing primitives with different tile sizes and/or dirty rect update strategies. One of the included tests in this patch fails the accuracy test - the intent is to fix this inaccuracy in a follow up patch and then be able to mark it pixel exact. Differential Revision: https://phabricator.services.mozilla.com/D60185
gfx/wr/webrender/src/frame_builder.rs
gfx/wr/webrender/src/picture.rs
gfx/wr/webrender/src/render_backend.rs
gfx/wr/webrender/src/renderer.rs
gfx/wr/webrender/src/scene.rs
gfx/wr/webrender_api/src/api.rs
gfx/wr/wrench/reftests/border/reftest.list
gfx/wr/wrench/reftests/reftest.list
gfx/wr/wrench/reftests/text/reftest.list
gfx/wr/wrench/reftests/tiles/prim-suite.yaml
gfx/wr/wrench/reftests/tiles/rect.yaml
gfx/wr/wrench/reftests/tiles/reftest.list
gfx/wr/wrench/reftests/tiles/simple-gradient.yaml
gfx/wr/wrench/src/reftest.rs
--- a/gfx/wr/webrender/src/frame_builder.rs
+++ b/gfx/wr/webrender/src/frame_builder.rs
@@ -60,16 +60,17 @@ pub struct FrameBuilderConfig {
     /// True if we're running tests (i.e. via wrench).
     pub testing: bool,
     pub gpu_supports_fast_clears: bool,
     pub gpu_supports_advanced_blend: bool,
     pub advanced_blend_is_coherent: bool,
     pub batch_lookback_count: usize,
     pub background_color: Option<ColorF>,
     pub compositor_kind: CompositorKind,
+    pub tile_size_override: Option<DeviceIntSize>,
 }
 
 /// A set of common / global resources that are retained between
 /// new display lists, such that any GPU cache handles can be
 /// persisted even when a new display list arrives.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 pub struct FrameGlobalResources {
     /// The image shader block for the most common / default
@@ -112,17 +113,17 @@ pub struct FrameBuilder {
 
 pub struct FrameVisibilityContext<'a> {
     pub clip_scroll_tree: &'a ClipScrollTree,
     pub global_screen_world_rect: WorldRect,
     pub global_device_pixel_scale: DevicePixelScale,
     pub surfaces: &'a [SurfaceInfo],
     pub debug_flags: DebugFlags,
     pub scene_properties: &'a SceneProperties,
-    pub config: &'a FrameBuilderConfig,
+    pub config: FrameBuilderConfig,
 }
 
 pub struct FrameVisibilityState<'a> {
     pub clip_store: &'a mut ClipStore,
     pub resource_cache: &'a mut ResourceCache,
     pub gpu_cache: &'a mut GpuCache,
     pub scratch: &'a mut PrimitiveScratchBuffer,
     pub tile_cache: Option<Box<TileCacheInstance>>,
@@ -237,16 +238,17 @@ impl FrameBuilder {
         transform_palette: &mut TransformPalette,
         data_stores: &mut DataStores,
         surfaces: &mut Vec<SurfaceInfo>,
         scratch: &mut PrimitiveScratchBuffer,
         debug_flags: DebugFlags,
         texture_cache_profile: &mut TextureCacheProfileCounters,
         composite_state: &mut CompositeState,
         tile_cache_logger: &mut TileCacheLogger,
+        config: FrameBuilderConfig,
     ) -> Option<RenderTaskId> {
         profile_scope!("cull");
 
         if scene.prim_store.pictures.is_empty() {
             return None
         }
 
         scratch.begin_frame();
@@ -322,17 +324,17 @@ impl FrameBuilder {
 
             let visibility_context = FrameVisibilityContext {
                 global_device_pixel_scale,
                 clip_scroll_tree: &scene.clip_scroll_tree,
                 global_screen_world_rect,
                 surfaces,
                 debug_flags,
                 scene_properties,
-                config: &scene.config,
+                config,
             };
 
             let mut visibility_state = FrameVisibilityState {
                 resource_cache,
                 gpu_cache,
                 clip_store: &mut scene.clip_store,
                 scratch,
                 tile_cache: None,
@@ -467,16 +469,17 @@ impl FrameBuilder {
         pan: WorldPoint,
         resource_profile: &mut ResourceProfileCounters,
         scene_properties: &SceneProperties,
         data_stores: &mut DataStores,
         scratch: &mut PrimitiveScratchBuffer,
         render_task_counters: &mut RenderTaskGraphCounters,
         debug_flags: DebugFlags,
         tile_cache_logger: &mut TileCacheLogger,
+        config: FrameBuilderConfig,
     ) -> Frame {
         profile_scope!("build");
         profile_marker!("BuildFrame");
 
         let mut profile_counters = FrameProfileCounters::new();
         profile_counters
             .total_primitives
             .set(scene.prim_store.prim_count());
@@ -537,16 +540,17 @@ impl FrameBuilder {
             &mut transform_palette,
             data_stores,
             &mut surfaces,
             scratch,
             debug_flags,
             &mut resource_profile.texture_cache,
             &mut composite_state,
             tile_cache_logger,
+            config,
         );
 
         let mut passes;
         let mut deferred_resolves = vec![];
         let mut has_texture_cache_tasks = false;
         let mut prim_headers = PrimitiveHeaders::new();
 
         {
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -273,23 +273,48 @@ pub const TILE_SIZE_SCROLLBAR_HORIZONTAL
 
 /// The size in device pixels of a tile for vertical scroll bars
 pub const TILE_SIZE_SCROLLBAR_VERTICAL: DeviceIntSize = DeviceIntSize {
     width: 16,
     height: 512,
     _unit: marker::PhantomData,
 };
 
+const TILE_SIZE_FOR_TESTS: [DeviceIntSize; 6] = [
+    DeviceIntSize {
+        width: 128,
+        height: 128,
+        _unit: marker::PhantomData,
+    },
+    DeviceIntSize {
+        width: 256,
+        height: 256,
+        _unit: marker::PhantomData,
+    },
+    DeviceIntSize {
+        width: 512,
+        height: 512,
+        _unit: marker::PhantomData,
+    },
+    TILE_SIZE_DEFAULT,
+    TILE_SIZE_SCROLLBAR_VERTICAL,
+    TILE_SIZE_SCROLLBAR_HORIZONTAL,
+];
+
 // Return the list of tile sizes for the renderer to allocate texture arrays for.
-pub fn tile_cache_sizes() -> &'static [DeviceIntSize] {
-    &[
-        TILE_SIZE_DEFAULT,
-        TILE_SIZE_SCROLLBAR_HORIZONTAL,
-        TILE_SIZE_SCROLLBAR_VERTICAL,
-    ]
+pub fn tile_cache_sizes(testing: bool) -> &'static [DeviceIntSize] {
+    if testing {
+        &TILE_SIZE_FOR_TESTS
+    } else {
+        &[
+            TILE_SIZE_DEFAULT,
+            TILE_SIZE_SCROLLBAR_HORIZONTAL,
+            TILE_SIZE_SCROLLBAR_VERTICAL,
+        ]
+    }
 }
 
 /// The maximum size per axis of a surface,
 ///  in WorldPixel coordinates.
 const MAX_SURFACE_SIZE: f32 = 4096.0;
 
 /// The maximum number of sub-dependencies (e.g. clips, transforms) we can handle
 /// per-primitive. If a primitive has more than this, it will invalidate every frame.
@@ -1649,16 +1674,19 @@ pub struct TileCacheInstance {
     /// not using native compositor, or if the surface was destroyed and needs
     /// to be reallocated next time this surface contains valid tiles.
     pub native_surface_id: Option<NativeSurfaceId>,
     /// The current device position of this cache. Used to set the compositor
     /// offset of the surface when building the visual tree.
     pub device_position: DevicePoint,
     /// True if the entire picture cache surface is opaque.
     is_opaque: bool,
+    /// The currently considered tile size override. Used to check if we should
+    /// re-evaluate tile size, even if the frame timer hasn't expired.
+    tile_size_override: Option<DeviceIntSize>,
 }
 
 impl TileCacheInstance {
     pub fn new(
         slice: usize,
         spatial_node_index: SpatialNodeIndex,
         background_color: Option<ColorF>,
         shared_clips: Vec<ClipDataHandle>,
@@ -1699,16 +1727,17 @@ impl TileCacheInstance {
             shared_clip_chain,
             current_tile_size: DeviceIntSize::zero(),
             frames_until_size_eval: 0,
             fract_offset: PictureVector2D::zero(),
             compare_cache: FastHashMap::default(),
             native_surface_id: None,
             device_position: DevicePoint::zero(),
             is_opaque: true,
+            tile_size_override: None,
         }
     }
 
     /// Returns true if this tile cache is considered opaque.
     pub fn is_opaque(&self) -> bool {
         // If known opaque due to background clear color and being the first slice.
         // The background_color will only be Some(..) if this is the first slice.
         match self.background_color {
@@ -1856,48 +1885,55 @@ impl TileCacheInstance {
                 &mut self.compare_cache,
                 prev_state.allocations.compare_cache,
             );
         }
 
         // Only evaluate what tile size to use fairly infrequently, so that we don't end
         // up constantly invalidating and reallocating tiles if the picture rect size is
         // changing near a threshold value.
-        if self.frames_until_size_eval == 0 {
+        if self.frames_until_size_eval == 0 ||
+           self.tile_size_override != frame_context.config.tile_size_override {
             const TILE_SIZE_TINY: f32 = 32.0;
 
             // Work out what size tile is appropriate for this picture cache.
-            let desired_tile_size;
-
-            // There's no need to check the other dimension. If we encounter a picture
-            // that is small on one dimension, it's a reasonable choice to use a scrollbar
-            // sized tile configuration regardless of the other dimension.
-            if pic_rect.size.width <= TILE_SIZE_TINY {
-                desired_tile_size = TILE_SIZE_SCROLLBAR_VERTICAL;
-            } else if pic_rect.size.height <= TILE_SIZE_TINY {
-                desired_tile_size = TILE_SIZE_SCROLLBAR_HORIZONTAL;
-            } else {
-                desired_tile_size = TILE_SIZE_DEFAULT;
-            }
+            let desired_tile_size = match frame_context.config.tile_size_override {
+                Some(tile_size_override) => {
+                    tile_size_override
+                }
+                None => {
+                    // There's no need to check the other dimension. If we encounter a picture
+                    // that is small on one dimension, it's a reasonable choice to use a scrollbar
+                    // sized tile configuration regardless of the other dimension.
+                    if pic_rect.size.width <= TILE_SIZE_TINY {
+                        TILE_SIZE_SCROLLBAR_VERTICAL
+                    } else if pic_rect.size.height <= TILE_SIZE_TINY {
+                        TILE_SIZE_SCROLLBAR_HORIZONTAL
+                    } else {
+                        TILE_SIZE_DEFAULT
+                    }
+                }
+            };
 
             // If the desired tile size has changed, then invalidate and drop any
             // existing tiles.
             if desired_tile_size != self.current_tile_size {
                 // Destroy any native surfaces on the tiles that will be dropped due
                 // to resizing.
                 if let Some(native_surface_id) = self.native_surface_id.take() {
                     frame_state.resource_cache.destroy_compositor_surface(native_surface_id);
                 }
                 self.tiles.clear();
                 self.current_tile_size = desired_tile_size;
             }
 
             // Reset counter until next evaluating the desired tile size. This is an
             // arbitrary value.
             self.frames_until_size_eval = 120;
+            self.tile_size_override = frame_context.config.tile_size_override;
         }
 
         // Map an arbitrary point in picture space to world space, to work out
         // what the fractional translation is that's applied by this scroll root.
         // TODO(gw): I'm not 100% sure this is right. At least, in future, we should
         //           make a specific API for this, and/or enforce that the picture
         //           cache transform only includes scale and/or translation (we
         //           already ensure it doesn't have perspective).
--- a/gfx/wr/webrender/src/render_backend.rs
+++ b/gfx/wr/webrender/src/render_backend.rs
@@ -536,16 +536,17 @@ impl Document {
 
     fn build_frame(
         &mut self,
         resource_cache: &mut ResourceCache,
         gpu_cache: &mut GpuCache,
         resource_profile: &mut ResourceProfileCounters,
         debug_flags: DebugFlags,
         tile_cache_logger: &mut TileCacheLogger,
+        config: FrameBuilderConfig,
     ) -> RenderedDocument {
         let accumulated_scale_factor = self.view.accumulated_scale_factor();
         let pan = self.view.pan.to_f32() / accumulated_scale_factor;
 
         // Advance to the next frame.
         self.stamp.advance();
 
         assert!(self.stamp.frame_id() != FrameId::INVALID,
@@ -563,16 +564,17 @@ impl Document {
                 pan,
                 resource_profile,
                 &self.dynamic_properties,
                 &mut self.data_stores,
                 &mut self.scratch,
                 &mut self.render_task_counters,
                 debug_flags,
                 tile_cache_logger,
+                config,
             );
             self.hit_tester = Some(self.scene.create_hit_tester(&self.data_stores.clip));
             frame
         };
 
         self.frame_is_valid = true;
         self.hit_tester_is_valid = true;
 
@@ -1137,16 +1139,21 @@ impl RenderBackend {
 
                         self.low_priority_scene_tx.send(SceneBuilderRequest::SetFrameBuilderConfig(
                             self.frame_config.clone()
                         )).unwrap();
 
                         // We don't want to forward this message to the renderer.
                         return RenderBackendStatus::Continue;
                     }
+                    DebugCommand::SetPictureTileSize(tile_size) => {
+                        self.frame_config.tile_size_override = tile_size;
+
+                        return RenderBackendStatus::Continue;
+                    }
                     DebugCommand::FetchDocuments => {
                         // Ask SceneBuilderThread to send JSON presentation of the documents,
                         // that will be forwarded to Renderer.
                         self.scene_tx.send(SceneBuilderRequest::DocumentsForDebugger).unwrap();
                         return RenderBackendStatus::Continue;
                     }
                     DebugCommand::FetchClipScrollTree => {
                         let json = self.get_clip_scroll_tree_for_debugger();
@@ -1529,16 +1536,17 @@ impl RenderBackend {
                 let frame_build_start_time = precise_time_ns();
 
                 let rendered_document = doc.build_frame(
                     &mut self.resource_cache,
                     &mut self.gpu_cache,
                     &mut profile_counters.resources,
                     self.debug_flags,
                     &mut self.tile_cache_logger,
+                    self.frame_config,
                 );
 
                 debug!("generated frame for document {:?} with {} passes",
                     document_id, rendered_document.frame.passes.len());
 
                 let msg = ResultMsg::UpdateGpuCache(self.gpu_cache.extract_updates());
                 self.result_tx.send(msg).unwrap();
 
@@ -1707,16 +1715,17 @@ impl RenderBackend {
             debug!("\tdocument {:?}", id);
             if config.bits.contains(CaptureBits::FRAME) {
                 let rendered_document = doc.build_frame(
                     &mut self.resource_cache,
                     &mut self.gpu_cache,
                     &mut profile_counters.resources,
                     self.debug_flags,
                     &mut self.tile_cache_logger,
+                    self.frame_config,
                 );
                 // After we rendered the frames, there are pending updates to both
                 // GPU cache and resources. Instead of serializing them, we are going to make sure
                 // they are applied on the `Renderer` side.
                 let msg_update_gpu_cache = ResultMsg::UpdateGpuCache(self.gpu_cache.extract_updates());
                 self.result_tx.send(msg_update_gpu_cache).unwrap();
                 //TODO: write down doc's pipeline info?
                 // it has `pipeline_epoch_map`,
--- a/gfx/wr/webrender/src/renderer.rs
+++ b/gfx/wr/webrender/src/renderer.rs
@@ -2180,16 +2180,17 @@ impl Renderer {
             global_enable_picture_caching: options.enable_picture_caching,
             testing: options.testing,
             gpu_supports_fast_clears: options.gpu_supports_fast_clears,
             gpu_supports_advanced_blend: ext_blend_equation_advanced,
             advanced_blend_is_coherent: ext_blend_equation_advanced_coherent,
             batch_lookback_count: options.batch_lookback_count,
             background_color: options.clear_color,
             compositor_kind,
+            tile_size_override: None,
         };
         info!("WR {:?}", config);
 
         let device_pixel_ratio = options.device_pixel_ratio;
         let debug_flags = options.debug_flags;
         let payload_rx_for_backend = payload_rx.to_mpsc_receiver();
         let size_of_op = options.size_of_op;
         let enclosing_size_of_op = options.enclosing_size_of_op;
@@ -2287,17 +2288,17 @@ impl Renderer {
             if let Some(ref thread_listener) = *thread_listener_for_render_backend {
                 thread_listener.thread_started(&rb_thread_name);
             }
 
             let texture_cache = TextureCache::new(
                 max_texture_size,
                 max_texture_layers,
                 if config.global_enable_picture_caching {
-                    tile_cache_sizes()
+                    tile_cache_sizes(config.testing)
                 } else {
                     &[]
                 },
                 start_size,
                 color_cache_formats,
                 swizzle_settings,
             );
 
@@ -2840,17 +2841,18 @@ impl Renderer {
         }
 
         serde_json::to_string(&debug_root).unwrap()
     }
 
     fn handle_debug_command(&mut self, command: DebugCommand) {
         match command {
             DebugCommand::EnableDualSourceBlending(_) |
-            DebugCommand::SetTransactionLogging(_) => {
+            DebugCommand::SetTransactionLogging(_) |
+            DebugCommand::SetPictureTileSize(_) => {
                 panic!("Should be handled by render backend");
             }
             DebugCommand::FetchDocuments |
             DebugCommand::FetchClipScrollTree => {}
             DebugCommand::FetchRenderTasks => {
                 let json = self.get_render_tasks_for_debugger();
                 self.debug_server.send(json);
             }
--- a/gfx/wr/webrender/src/scene.rs
+++ b/gfx/wr/webrender/src/scene.rs
@@ -253,16 +253,17 @@ impl BuiltScene {
                 global_enable_picture_caching: false,
                 testing: false,
                 gpu_supports_fast_clears: false,
                 gpu_supports_advanced_blend: false,
                 advanced_blend_is_coherent: false,
                 batch_lookback_count: 0,
                 background_color: None,
                 compositor_kind: CompositorKind::default(),
+                tile_size_override: None,
             },
         }
     }
 
     /// Get the memory usage statistics to pre-allocate for the next scene.
     pub fn get_stats(&self) -> SceneStats {
         SceneStats {
             prim_store_stats: self.prim_store.get_stats(),
--- a/gfx/wr/webrender_api/src/api.rs
+++ b/gfx/wr/webrender_api/src/api.rs
@@ -972,16 +972,18 @@ pub enum DebugCommand {
     /// Causes the scene builder to pause for a given amount of milliseconds each time it
     /// processes a transaction.
     SimulateLongSceneBuild(u32),
     /// Causes the low priority scene builder to pause for a given amount of milliseconds
     /// each time it processes a transaction.
     SimulateLongLowPrioritySceneBuild(u32),
     /// Logs transactions to a file for debugging purposes
     SetTransactionLogging(bool),
+    /// Set an override tile size to use for picture caches
+    SetPictureTileSize(Option<DeviceIntSize>),
 }
 
 /// Message sent by the `RenderApi` to the render backend thread.
 #[derive(Clone, Deserialize, Serialize)]
 pub enum ApiMsg {
     /// Add/remove/update images and fonts.
     UpdateResources(Vec<ResourceUpdate>),
     /// Gets the glyph dimensions
--- a/gfx/wr/wrench/reftests/border/reftest.list
+++ b/gfx/wr/wrench/reftests/border/reftest.list
@@ -23,10 +23,10 @@ platform(linux,mac) == border-image.yaml
 platform(linux,mac) == dotted-corner-small-radius.yaml dotted-corner-small-radius.png
 == overlapping.yaml overlapping.png
 == zero-width.yaml blank.yaml
 platform(linux,mac) == small-dotted-border.yaml small-dotted-border.png
 == discontinued-dash.yaml discontinued-dash.png
 platform(linux,mac) == border-dashed-dotted-caching.yaml border-dashed-dotted-caching.png
 != small-inset-outset.yaml small-inset-outset-notref.yaml
 fuzzy(1,16) == no-aa.yaml green-square.yaml
-skip_on(android,device) border-double-1px.yaml border-double-1px-ref.yaml  # Fails on Pixel2
+skip_on(android,device) == border-double-1px.yaml border-double-1px-ref.yaml  # Fails on Pixel2
 == max-scale.yaml max-scale-ref.yaml
--- a/gfx/wr/wrench/reftests/reftest.list
+++ b/gfx/wr/wrench/reftests/reftest.list
@@ -10,8 +10,9 @@ include image/reftest.list
 include invalidation/reftest.list
 include mask/reftest.list
 include performance/reftest.list
 include scrolling/reftest.list
 include snap/reftest.list
 include split/reftest.list
 include text/reftest.list
 include transforms/reftest.list
+include tiles/reftest.list
--- a/gfx/wr/wrench/reftests/text/reftest.list
+++ b/gfx/wr/wrench/reftests/text/reftest.list
@@ -68,10 +68,10 @@ skip_on(android,device) == bg-color.yaml
 != large-glyphs.yaml blank.yaml
 skip_on(android,device) == snap-text-offset.yaml snap-text-offset-ref.yaml
 fuzzy(5,4435) == shadow-border.yaml shadow-solid-ref.yaml
 fuzzy(5,4435) == shadow-image.yaml shadow-solid-ref.yaml
 options(disable-aa) == snap-clip.yaml snap-clip-ref.yaml
 platform(linux) == perspective-clip.yaml perspective-clip.png
 fuzzy(1,39) options(disable-subpixel) == raster-space-snap.yaml raster-space-snap-ref.yaml
 # == intermediate-transform.yaml intermediate-transform-ref.yaml # fails because of AA inavailable with an intermediate surface
-platform(linux) allow_sacrificing_subpixel_aa(false) text-fixed-slice.yaml text-fixed-slice-slow.png
-platform(linux) allow_sacrificing_subpixel_aa(true) text-fixed-slice.yaml text-fixed-slice-fast.png
+platform(linux) allow_sacrificing_subpixel_aa(false) == text-fixed-slice.yaml text-fixed-slice-slow.png
+platform(linux) allow_sacrificing_subpixel_aa(true) == text-fixed-slice.yaml text-fixed-slice-fast.png
new file mode 100644
--- /dev/null
+++ b/gfx/wr/wrench/reftests/tiles/prim-suite.yaml
@@ -0,0 +1,45 @@
+---
+root:
+  items:
+        - type: stacking-context
+          bounds: [50, 50, 100, 100]
+          transform: rotate(30)
+          items:
+            - type: rect
+              bounds: [ 10, 10, 80, 80 ]
+              color: [0, 255, 0]
+            - type: box-shadow
+              bounds: [ 10, 10, 80, 80 ]
+              blur-radius: 25
+              clip-mode: inset
+
+            - type: rect
+              bounds: [ 140, 10, 80, 80 ]
+              color: [0, 255, 0]
+            - type: box-shadow
+              bounds: [ 140, 10, 80, 80 ]
+              blur-radius: 25
+              clip-mode: outset
+
+            - type: border
+              bounds: [ 250, 10, 100, 100 ]
+              width: [ 10, 10, 10, 10 ]
+              border-type: normal
+              style: solid
+              color: [ red, green, blue, black ]
+              radius: {
+                top-left: [20, 20],
+                top-right: [10, 10],
+                bottom-left: [25, 25],
+                bottom-right: [0, 0],
+              }
+
+            - bounds: [150, 150, 128, 128]
+              image: checkerboard(4, 15, 8)
+              stretch-size: 128 128
+
+            - type: radial-gradient
+              bounds: 300 150 100 100
+              center: 50 50
+              radius: 50 50
+              stops: [0, red, 1, blue]
new file mode 100644
--- /dev/null
+++ b/gfx/wr/wrench/reftests/tiles/rect.yaml
@@ -0,0 +1,6 @@
+---
+root:
+  items:
+  - type: rect
+    bounds: 50 50 200 200
+    color: red
new file mode 100644
--- /dev/null
+++ b/gfx/wr/wrench/reftests/tiles/reftest.list
@@ -0,0 +1,4 @@
+** rect.yaml
+** simple-gradient.yaml
+# TODO: Fix rasterizer inaccuracies so this is the same regardless of tile size!
+!* prim-suite.yaml
new file mode 100644
--- /dev/null
+++ b/gfx/wr/wrench/reftests/tiles/simple-gradient.yaml
@@ -0,0 +1,9 @@
+---
+root:
+  items:
+    - type: gradient
+      bounds: [ 0, 0, 1980, 1080]
+      start: [ 0, -2000 ]
+      end: [ 0, 4000 ]
+      stops: [ 0.0, red, 1.0, green ]
+      repeat: false
--- a/gfx/wr/wrench/src/reftest.rs
+++ b/gfx/wr/wrench/src/reftest.rs
@@ -39,29 +39,38 @@ impl ReftestOptions {
     pub fn default() -> Self {
         ReftestOptions {
             allow_max_difference: 0,
             allow_num_differences: 0,
         }
     }
 }
 
+#[derive(Debug, Copy, Clone)]
 pub enum ReftestOp {
+    /// Expect that the images match the reference
     Equal,
+    /// Expect that the images *don't* match the reference
     NotEqual,
+    /// Expect that drawing the reference at different tiles sizes gives the same pixel exact result.
+    Accurate,
+    /// Expect that drawing the reference at different tiles sizes gives a *different* pixel exact result.
+    Inaccurate,
 }
 
 impl Display for ReftestOp {
     fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
         write!(
             f,
             "{}",
             match *self {
                 ReftestOp::Equal => "==".to_owned(),
                 ReftestOp::NotEqual => "!=".to_owned(),
+                ReftestOp::Accurate => "**".to_owned(),
+                ReftestOp::Inaccurate => "!*".to_owned(),
             }
         )
     }
 }
 
 #[derive(Debug)]
 enum ExtraCheck {
     DrawCalls(usize),
@@ -97,33 +106,92 @@ pub struct Reftest {
     num_differences: usize,
     extra_checks: Vec<ExtraCheck>,
     disable_dual_source_blending: bool,
     allow_mipmaps: bool,
     zoom_factor: f32,
     allow_sacrificing_subpixel_aa: Option<bool>,
 }
 
+impl Reftest {
+    /// Check the positive case (expecting equality) and report details if different
+    fn check_and_report_equality_failure(
+        &self,
+        comparison: ReftestImageComparison,
+        test: &ReftestImage,
+        reference: &ReftestImage,
+    ) -> bool {
+        match comparison {
+            ReftestImageComparison::Equal => {
+                true
+            }
+            ReftestImageComparison::NotEqual { max_difference, count_different } => {
+                if max_difference > self.max_difference || count_different > self.num_differences {
+                    println!(
+                        "{} | {} | {}: {}, {}: {}",
+                        "REFTEST TEST-UNEXPECTED-FAIL",
+                        self,
+                        "image comparison, max difference",
+                        max_difference,
+                        "number of differing pixels",
+                        count_different
+                    );
+                    println!("REFTEST   IMAGE 1 (TEST): {}", test.clone().create_data_uri());
+                    println!(
+                        "REFTEST   IMAGE 2 (REFERENCE): {}",
+                        reference.clone().create_data_uri()
+                    );
+                    println!("REFTEST TEST-END | {}", self);
+
+                    false
+                } else {
+                    true
+                }
+            }
+        }
+    }
+
+    /// Check the negative case (expecting inequality) and report details if same
+    fn check_and_report_inequality_failure(
+        &self,
+        comparison: ReftestImageComparison,
+    ) -> bool {
+        match comparison {
+            ReftestImageComparison::Equal => {
+                println!("REFTEST TEST-UNEXPECTED-FAIL | {} | image comparison", self);
+                println!("REFTEST TEST-END | {}", self);
+                false
+            }
+            ReftestImageComparison::NotEqual { .. } => {
+                true
+            }
+        }
+    }
+}
+
 impl Display for Reftest {
     fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
         let paths: Vec<String> = self.test.iter().map(|t| t.display().to_string()).collect();
         write!(
             f,
             "{} {} {}",
             paths.join(", "),
             self.op,
             self.reference.display()
         )
     }
 }
 
+#[derive(Clone)]
 pub struct ReftestImage {
     pub data: Vec<u8>,
     pub size: DeviceIntSize,
 }
+
+#[derive(Debug, Copy, Clone)]
 pub enum ReftestImageComparison {
     Equal,
     NotEqual {
         max_difference: usize,
         count_different: usize,
     },
 }
 
@@ -207,17 +275,17 @@ impl ReftestManifest {
             if s.is_empty() {
                 continue;
             }
 
             let tokens: Vec<&str> = s.split_whitespace().collect();
 
             let mut max_difference = 0;
             let mut max_count = 0;
-            let mut op = ReftestOp::Equal;
+            let mut op = None;
             let mut font_render_mode = None;
             let mut extra_checks = vec![];
             let mut disable_dual_source_blending = false;
             let mut zoom_factor = 1.0;
             let mut allow_mipmaps = false;
             let mut dirty_region_index = 0;
             let mut allow_sacrificing_subpixel_aa = None;
 
@@ -294,32 +362,41 @@ impl ReftestManifest {
                         if args.iter().any(|arg| arg == &OPTION_DISABLE_DUAL_SOURCE_BLENDING) {
                             disable_dual_source_blending = true;
                         }
                         if args.iter().any(|arg| arg == &OPTION_ALLOW_MIPMAPS) {
                             allow_mipmaps = true;
                         }
                     }
                     "==" => {
-                        op = ReftestOp::Equal;
+                        op = Some(ReftestOp::Equal);
                     }
                     "!=" => {
-                        op = ReftestOp::NotEqual;
+                        op = Some(ReftestOp::NotEqual);
+                    }
+                    "**" => {
+                        op = Some(ReftestOp::Accurate);
+                    }
+                    "!*" => {
+                        op = Some(ReftestOp::Inaccurate);
                     }
                     _ => {
                         paths.push(dir.join(*token));
                     }
                 }
             }
 
             // Don't try to add tests for include lines.
-            if paths.len() < 2 {
-                assert_eq!(paths.len(), 0, "Only one path provided: {:?}", paths[0]);
-                continue;
-            }
+            let op = match op {
+                Some(op) => op,
+                None => {
+                    assert!(paths.is_empty(), format!("paths = {:?}", paths));
+                    continue;
+                }
+            };
 
             // The reference is the last path provided. If multiple paths are
             // passed for the test, they render sequentially before being
             // compared to the reference, which is useful for testing
             // invalidation.
             let reference = paths.pop().unwrap();
             let test = paths;
 
@@ -525,25 +602,62 @@ impl<'a> ReftestHarness<'a> {
         //
         // Note also that, when we have multiple test scenes in sequence, we
         // want to test the picture caching machinery. But since picture caching
         // only takes effect after the result has been the same several frames in
         // a row, we need to render the scene multiple times.
         let mut images = vec![];
         let mut results = vec![];
 
-        for filename in t.test.iter() {
-            let output = self.render_yaml(
-                &filename,
-                test_size,
-                t.font_render_mode,
-                t.allow_mipmaps,
-            );
-            images.push(output.image);
-            results.push(output.results);
+        match t.op {
+            ReftestOp::Equal | ReftestOp::NotEqual => {
+                // For equality tests, render each test image and store result
+                for filename in t.test.iter() {
+                    let output = self.render_yaml(
+                        &filename,
+                        test_size,
+                        t.font_render_mode,
+                        t.allow_mipmaps,
+                    );
+                    images.push(output.image);
+                    results.push(output.results);
+                }
+            }
+            ReftestOp::Accurate | ReftestOp::Inaccurate => {
+                // For accuracy tests, render the reference yaml at an arbitrary series
+                // of tile sizes, and compare to the reference drawn at normal tile size.
+                let tile_sizes = [
+                    DeviceIntSize::new(128, 128),
+                    DeviceIntSize::new(256, 256),
+                    DeviceIntSize::new(512, 512),
+                ];
+
+                for tile_size in &tile_sizes {
+                    self.wrench
+                        .api
+                        .send_debug_cmd(
+                            DebugCommand::SetPictureTileSize(Some(*tile_size))
+                        );
+
+                    let output = self.render_yaml(
+                        &t.reference,
+                        test_size,
+                        t.font_render_mode,
+                        t.allow_mipmaps,
+                    );
+                    images.push(output.image);
+                    results.push(output.results);
+                }
+
+                self.wrench
+                    .api
+                    .send_debug_cmd(
+                        DebugCommand::SetPictureTileSize(None)
+                    );
+            }
         }
 
         let reference = match reference_image {
             Some(image) => image,
             None => {
                 let output = self.render_yaml(
                     &t.reference,
                     test_size,
@@ -570,54 +684,60 @@ impl<'a> ReftestHarness<'a> {
                     extra_check,
                     results,
                 );
                 println!("REFTEST TEST-END | {}", t);
                 return false;
             }
         }
 
-        let test = images.pop().unwrap();
-        let comparison = test.compare(&reference);
-        match (&t.op, comparison) {
-            (&ReftestOp::Equal, ReftestImageComparison::Equal) => true,
-            (
-                &ReftestOp::Equal,
-                ReftestImageComparison::NotEqual {
-                    max_difference,
-                    count_different,
-                },
-            ) => if max_difference > t.max_difference || count_different > t.num_differences {
-                println!(
-                    "{} | {} | {}: {}, {}: {}",
-                    "REFTEST TEST-UNEXPECTED-FAIL",
-                    t,
-                    "image comparison, max difference",
-                    max_difference,
-                    "number of differing pixels",
-                    count_different
-                );
-                println!("REFTEST   IMAGE 1 (TEST): {}", test.create_data_uri());
-                println!(
-                    "REFTEST   IMAGE 2 (REFERENCE): {}",
-                    reference.create_data_uri()
-                );
-                println!("REFTEST TEST-END | {}", t);
+        match t.op {
+            ReftestOp::Equal => {
+                // Ensure that the final image matches the reference
+                let test = images.pop().unwrap();
+                let comparison = test.compare(&reference);
+                t.check_and_report_equality_failure(
+                    comparison,
+                    &test,
+                    &reference,
+                )
+            }
+            ReftestOp::NotEqual => {
+                // Ensure that the final image *doesn't* match the reference
+                let test = images.pop().unwrap();
+                let comparison = test.compare(&reference);
+                t.check_and_report_inequality_failure(comparison)
+            }
+            ReftestOp::Accurate => {
+                // Ensure that *all* images match the reference
+                for test in images.drain(..) {
+                    let comparison = test.compare(&reference);
 
-                false
-            } else {
+                    if !t.check_and_report_equality_failure(
+                        comparison,
+                        &test,
+                        &reference,
+                    ) {
+                        return false;
+                    }
+                }
+
                 true
-            },
-            (&ReftestOp::NotEqual, ReftestImageComparison::Equal) => {
-                println!("REFTEST TEST-UNEXPECTED-FAIL | {} | image comparison", t);
-                println!("REFTEST TEST-END | {}", t);
+            }
+            ReftestOp::Inaccurate => {
+                // Ensure that at least one of the images doesn't match the reference
+                let mut found_mismatch = false;
 
-                false
+                for test in images.drain(..) {
+                    let comparison = test.compare(&reference);
+                    found_mismatch |= t.check_and_report_inequality_failure(comparison);
+                }
+
+                found_mismatch
             }
-            (&ReftestOp::NotEqual, ReftestImageComparison::NotEqual { .. }) => true,
         }
     }
 
     fn load_image(&mut self, filename: &Path, format: ImageFormat) -> ReftestImage {
         let file = BufReader::new(File::open(filename).unwrap());
         let img_raw = load_piston_image(file, format).unwrap();
         let img = img_raw.flipv().to_rgba();
         let size = img.dimensions();