Bug 1521011 - Support multiple dirty rects for picture caching. r=kvark
authorGlenn Watson <github@intuitionlibrary.com>
Sat, 19 Jan 2019 00:53:45 +0000
changeset 514484 7b123a6c5f5a6b680da3a9c387a0508f1819baf7
parent 514483 8d878be3b4bf0851b5c945ee3871b8aa0ab334b5
child 514485 259e581af979d775e91f6ce2b15bb6e29fdc996d
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)
reviewerskvark
bugs1521011
milestone66.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 1521011 - Support multiple dirty rects for picture caching. r=kvark Differential Revision: https://phabricator.services.mozilla.com/D16942
gfx/wr/webrender/src/batch.rs
gfx/wr/webrender/src/device/gl.rs
gfx/wr/webrender/src/frame_builder.rs
gfx/wr/webrender/src/picture.rs
gfx/wr/webrender/src/prim_store/mod.rs
gfx/wr/webrender/src/renderer.rs
gfx/wr/webrender/src/tiling.rs
--- a/gfx/wr/webrender/src/batch.rs
+++ b/gfx/wr/webrender/src/batch.rs
@@ -279,34 +279,36 @@ impl OpaqueBatchList {
             batch.instances.reverse();
         }
     }
 }
 
 pub struct BatchList {
     pub alpha_batch_list: AlphaBatchList,
     pub opaque_batch_list: OpaqueBatchList,
-    pub scissor_rect: Option<DeviceIntRect>,
+    /// A list of rectangle regions this batch should be drawn
+    /// in. Each region will have scissor rect set before drawing.
+    pub regions: Vec<DeviceIntRect>,
     pub tile_blits: Vec<TileBlit>,
 }
 
 impl BatchList {
     pub fn new(
         screen_size: DeviceIntSize,
-        scissor_rect: Option<DeviceIntRect>,
+        regions: Vec<DeviceIntRect>,
         tile_blits: Vec<TileBlit>,
     ) -> Self {
         // The threshold for creating a new batch is
         // one quarter the screen size.
         let batch_area_threshold = (screen_size.width * screen_size.height) as f32 / 4.0;
 
         BatchList {
             alpha_batch_list: AlphaBatchList::new(),
             opaque_batch_list: OpaqueBatchList::new(batch_area_threshold),
-            scissor_rect,
+            regions,
             tile_blits,
         }
     }
 
     pub fn push_single_instance(
         &mut self,
         key: BatchKey,
         bounding_rect: &PictureRect,
@@ -376,28 +378,35 @@ impl PrimitiveBatch {
     }
 }
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct AlphaBatchContainer {
     pub opaque_batches: Vec<PrimitiveBatch>,
     pub alpha_batches: Vec<PrimitiveBatch>,
-    pub scissor_rect: Option<DeviceIntRect>,
+    /// The overall scissor rect for this render task, if one
+    /// is required.
+    pub task_scissor_rect: Option<DeviceIntRect>,
+    /// A list of rectangle regions this batch should be drawn
+    /// in. Each region will have scissor rect set before drawing.
+    pub regions: Vec<DeviceIntRect>,
     pub tile_blits: Vec<TileBlit>,
 }
 
 impl AlphaBatchContainer {
     pub fn new(
-        scissor_rect: Option<DeviceIntRect>,
+        task_scissor_rect: Option<DeviceIntRect>,
+        regions: Vec<DeviceIntRect>,
     ) -> AlphaBatchContainer {
         AlphaBatchContainer {
             opaque_batches: Vec::new(),
             alpha_batches: Vec::new(),
-            scissor_rect,
+            task_scissor_rect,
+            regions,
             tile_blits: Vec::new(),
         }
     }
 
     pub fn is_empty(&self) -> bool {
         self.opaque_batches.is_empty() &&
         self.alpha_batches.is_empty()
     }
@@ -447,68 +456,59 @@ struct SegmentInstanceData {
     textures: BatchTextures,
     user_data: i32,
 }
 
 /// Encapsulates the logic of building batches for items that are blended.
 pub struct AlphaBatchBuilder {
     pub batch_lists: Vec<BatchList>,
     screen_size: DeviceIntSize,
-    scissor_rect: Option<DeviceIntRect>,
+    task_scissor_rect: Option<DeviceIntRect>,
     glyph_fetch_buffer: Vec<GlyphFetchResult>,
 }
 
 impl AlphaBatchBuilder {
     pub fn new(
         screen_size: DeviceIntSize,
-        scissor_rect: Option<DeviceIntRect>,
+        task_scissor_rect: Option<DeviceIntRect>,
     ) -> Self {
         let batch_lists = vec![
             BatchList::new(
                 screen_size,
-                scissor_rect,
+                Vec::new(),
                 Vec::new(),
             ),
         ];
 
         AlphaBatchBuilder {
             batch_lists,
-            scissor_rect,
+            task_scissor_rect,
             screen_size,
             glyph_fetch_buffer: Vec::new(),
         }
     }
 
     fn push_new_batch_list(
         &mut self,
-        scissor_rect: Option<DeviceIntRect>,
+        regions: Vec<DeviceIntRect>,
         tile_blits: Vec<TileBlit>,
     ) {
-        let scissor_rect = match (scissor_rect, self.scissor_rect) {
-            (Some(rect0), Some(rect1)) => {
-                Some(rect0.intersection(&rect1).unwrap_or(DeviceIntRect::zero()))
-            }
-            (Some(rect0), None) => Some(rect0),
-            (None, Some(rect1)) => Some(rect1),
-            (None, None) => None,
-        };
-
         self.batch_lists.push(BatchList::new(
             self.screen_size,
-            scissor_rect,
+            regions,
             tile_blits,
         ));
     }
 
     fn current_batch_list(&mut self) -> &mut BatchList {
         self.batch_lists.last_mut().unwrap()
     }
 
     fn can_merge(&self) -> bool {
-        self.scissor_rect.is_none() &&
+        self.task_scissor_rect.is_none() &&
         self.batch_lists.len() == 1
     }
 
     pub fn build(
         mut self,
         batch_containers: &mut Vec<AlphaBatchContainer>,
         merged_batches: &mut AlphaBatchContainer,
     ) {
@@ -520,17 +520,18 @@ impl AlphaBatchBuilder {
             let batch_list = self.batch_lists.pop().unwrap();
             debug_assert!(batch_list.tile_blits.is_empty());
             merged_batches.merge(batch_list);
         } else {
             for batch_list in self.batch_lists {
                 batch_containers.push(AlphaBatchContainer {
                     alpha_batches: batch_list.alpha_batch_list.batches,
                     opaque_batches: batch_list.opaque_batch_list.batches,
-                    scissor_rect: batch_list.scissor_rect,
+                    task_scissor_rect: self.task_scissor_rect,
+                    regions: batch_list.regions,
                     tile_blits: batch_list.tile_blits,
                 });
             }
         }
     }
 
     pub fn add_pic_to_batch(
         &mut self,
@@ -1118,35 +1119,44 @@ impl AlphaBatchBuilder {
                                             z_id,
                                         );
 
                                         batch.push(PrimitiveInstanceData::from(instance));
                                     }
 
                                     // If there is a dirty rect for the tile cache, recurse into the
                                     // main picture primitive list, and draw them first.
-                                    if let Some(ref dirty_region) = tile_cache.dirty_region {
+                                    if !tile_cache.dirty_region.is_empty() {
                                         let mut tile_blits = Vec::new();
 
                                         let (target_rect, _) = render_tasks[task_id].get_target_rect();
 
                                         for blit in &tile_cache.pending_blits {
                                             tile_blits.push(TileBlit {
                                                 dest_offset: blit.dest_offset,
                                                 size: blit.size,
                                                 target: blit.target.clone(),
                                                 src_offset: DeviceIntPoint::new(
                                                     blit.src_offset.x + target_rect.origin.x,
                                                     blit.src_offset.y + target_rect.origin.y,
                                                 ),
                                             })
                                         }
 
+                                        // Collect the list of regions to scissor and repeat
+                                        // the draw calls into, based on dirty rects.
+                                        let batch_regions = tile_cache
+                                            .dirty_region
+                                            .dirty_rects
+                                            .iter()
+                                            .map(|dirty_rect| dirty_rect.device_rect)
+                                            .collect();
+
                                         self.push_new_batch_list(
-                                            Some(dirty_region.dirty_device_rect),
+                                            batch_regions,
                                             tile_blits,
                                         );
 
                                         self.add_pic_to_batch(
                                             picture,
                                             task_id,
                                             ctx,
                                             gpu_cache,
@@ -1154,17 +1164,17 @@ impl AlphaBatchBuilder {
                                             deferred_resolves,
                                             prim_headers,
                                             transforms,
                                             root_spatial_node_index,
                                             z_generator,
                                         );
 
                                         self.push_new_batch_list(
-                                            None,
+                                            Vec::new(),
                                             Vec::new(),
                                         );
                                     }
                                 }
                             }
                             PictureCompositeMode::Filter(filter) => {
                                 let surface = ctx.surfaces[raster_config.surface_index.0]
                                     .surface
--- a/gfx/wr/webrender/src/device/gl.rs
+++ b/gfx/wr/webrender/src/device/gl.rs
@@ -1004,16 +1004,48 @@ impl<'a> DrawTarget<'a> {
 
     /// Returns the dimensions of this draw-target.
     pub fn dimensions(&self) -> DeviceIntSize {
         match *self {
             DrawTarget::Default(d) => d,
             DrawTarget::Texture { texture, .. } => texture.get_dimensions(),
         }
     }
+
+    /// Given a scissor rect, convert it to the right coordinate space
+    /// depending on the draw target kind. If no scissor rect was supplied,
+    /// returns a scissor rect that encloses the entire render target.
+    pub fn build_scissor_rect(
+        &self,
+        scissor_rect: Option<DeviceIntRect>,
+        framebuffer_target_rect: DeviceIntRect,
+    ) -> DeviceIntRect {
+        let dimensions = self.dimensions();
+
+        match scissor_rect {
+            Some(scissor_rect) => {
+                // Note: `framebuffer_target_rect` needs a Y-flip before going to GL
+                if self.is_default() {
+                    let mut rect = scissor_rect
+                        .intersection(&framebuffer_target_rect.to_i32())
+                        .unwrap_or(DeviceIntRect::zero());
+                    rect.origin.y = dimensions.height as i32 - rect.origin.y - rect.size.height;
+                    rect
+                } else {
+                    scissor_rect
+                }
+            }
+            None => {
+                DeviceIntRect::new(
+                    DeviceIntPoint::zero(),
+                    dimensions,
+                )
+            }
+        }
+    }
 }
 
 /// Contains the parameters necessary to bind a texture-backed read target.
 #[derive(Clone, Copy)]
 pub enum ReadTarget<'a> {
     /// Use the device's default draw target.
     Default,
     /// Use the provided texture,
--- a/gfx/wr/webrender/src/frame_builder.rs
+++ b/gfx/wr/webrender/src/frame_builder.rs
@@ -8,17 +8,17 @@ use api::{LayoutPoint, LayoutRect, Layou
 use clip::{ClipDataStore, ClipStore};
 use clip_scroll_tree::{ClipScrollTree, ROOT_SPATIAL_NODE_INDEX, SpatialNodeIndex};
 use display_list_flattener::{DisplayListFlattener};
 use gpu_cache::GpuCache;
 use gpu_types::{PrimitiveHeaders, TransformPalette, UvRectKind, ZBufferIdGenerator};
 use hit_test::{HitTester, HitTestingRun};
 use internal_types::{FastHashMap, PlaneSplitter};
 use picture::{PictureSurface, PictureUpdateState, SurfaceInfo, ROOT_SURFACE_INDEX, SurfaceIndex};
-use picture::{RetainedTiles, TileCache};
+use picture::{RetainedTiles, TileCache, DirtyRegion};
 use prim_store::{PrimitiveStore, SpaceMapper, PictureIndex, PrimitiveDebugId, PrimitiveScratchBuffer};
 #[cfg(feature = "replay")]
 use prim_store::{PrimitiveStoreStats};
 use profiler::{FrameProfileCounters, GpuCacheProfileCounters, TextureCacheProfileCounters};
 use render_backend::{DataStores, FrameStamp};
 use render_task::{RenderTask, RenderTaskId, RenderTaskLocation, RenderTaskTree};
 use resource_cache::{ResourceCache};
 use scene::{ScenePipeline, SceneProperties};
@@ -103,32 +103,49 @@ pub struct FrameBuildingState<'a> {
     pub render_tasks: &'a mut RenderTaskTree,
     pub profile_counters: &'a mut FrameProfileCounters,
     pub clip_store: &'a mut ClipStore,
     pub resource_cache: &'a mut ResourceCache,
     pub gpu_cache: &'a mut GpuCache,
     pub transforms: &'a mut TransformPalette,
     pub segment_builder: SegmentBuilder,
     pub surfaces: &'a mut Vec<SurfaceInfo>,
+    pub dirty_region_stack: Vec<DirtyRegion>,
+}
+
+impl<'a> FrameBuildingState<'a> {
+    /// Retrieve the current dirty region during primitive traversal.
+    pub fn current_dirty_region(&self) -> &DirtyRegion {
+        self.dirty_region_stack.last().unwrap()
+    }
+
+    /// Push a new dirty region for child primitives to cull / clip against.
+    pub fn push_dirty_region(&mut self, region: DirtyRegion) {
+        self.dirty_region_stack.push(region);
+    }
+
+    /// Pop the top dirty region from the stack.
+    pub fn pop_dirty_region(&mut self) {
+        self.dirty_region_stack.pop().unwrap();
+    }
 }
 
 /// Immutable context of a picture when processing children.
 #[derive(Debug)]
 pub struct PictureContext {
     pub pic_index: PictureIndex,
     pub pipeline_id: PipelineId,
     pub apply_local_clip_rect: bool,
     pub allow_subpixel_aa: bool,
     pub is_passthrough: bool,
     pub raster_space: RasterSpace,
     pub surface_spatial_node_index: SpatialNodeIndex,
     pub raster_spatial_node_index: SpatialNodeIndex,
     /// The surface that this picture will render on.
     pub surface_index: SurfaceIndex,
-    pub dirty_world_rect: WorldRect,
 }
 
 /// Mutable state of a picture that gets modified when
 /// the children are processed.
 pub struct PictureState {
     pub is_cacheable: bool,
     pub map_local_to_pic: SpaceMapper<LayoutPixel, PicturePixel>,
     pub map_pic_to_world: SpaceMapper<PicturePixel, WorldPixel>,
@@ -341,30 +358,40 @@ impl FrameBuilder {
             render_tasks,
             profile_counters,
             clip_store: &mut self.clip_store,
             resource_cache,
             gpu_cache,
             transforms: transform_palette,
             segment_builder: SegmentBuilder::new(),
             surfaces: pic_update_state.surfaces,
+            dirty_region_stack: Vec::new(),
         };
 
+        // Push a default dirty region which culls primitives
+        // against the screen world rect, in absence of any
+        // other dirty regions.
+        let mut default_dirty_region = DirtyRegion::new();
+        default_dirty_region.push(
+            frame_context.screen_world_rect,
+            frame_context.device_pixel_scale,
+        );
+        frame_state.push_dirty_region(default_dirty_region);
+
         let (pic_context, mut pic_state, mut prim_list) = self
             .prim_store
             .pictures[self.root_pic_index.0]
             .take_context(
                 self.root_pic_index,
                 root_spatial_node_index,
                 root_spatial_node_index,
                 ROOT_SURFACE_INDEX,
                 true,
                 &mut frame_state,
                 &frame_context,
-                screen_world_rect,
             )
             .unwrap();
 
         self.prim_store.prepare_primitives(
             &mut prim_list,
             &pic_context,
             &mut pic_state,
             &frame_context,
@@ -373,18 +400,21 @@ impl FrameBuilder {
             scratch,
         );
 
         let pic = &mut self.prim_store.pictures[self.root_pic_index.0];
         pic.restore_context(
             prim_list,
             pic_context,
             pic_state,
+            &mut frame_state,
         );
 
+        frame_state.pop_dirty_region();
+
         let child_tasks = frame_state
             .surfaces[ROOT_SURFACE_INDEX.0]
             .take_render_tasks();
 
         let root_render_task = RenderTask::new_picture(
             RenderTaskLocation::Fixed(self.screen_rect.to_i32()),
             self.screen_rect.size.to_f32(),
             self.root_pic_index,
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -95,16 +95,17 @@ pub type TileSize = TypedSize2D<i32, Til
 pub struct TileIndex(pub usize);
 
 /// The size in device pixels of a cached tile. The currently chosen
 /// size is arbitrary. We should do some profiling to find the best
 /// size for real world pages.
 pub const TILE_SIZE_WIDTH: i32 = 1024;
 pub const TILE_SIZE_HEIGHT: i32 = 256;
 const FRAMES_BEFORE_CACHING: usize = 2;
+const MAX_DIRTY_RECTS: usize = 3;
 
 /// The maximum size per axis of texture cache item,
 ///  in WorldPixel coordinates.
 // TODO(gw): This size is quite arbitrary - we should do some
 //           profiling / telemetry to see when it makes sense
 //           to cache a picture.
 const MAX_CACHE_SIZE: f32 = 2048.0;
 /// The maximum size per axis of a surface,
@@ -181,16 +182,19 @@ pub struct Tile {
     /// The set of transforms that affect primitives on this tile we
     /// care about. Stored as a set here, and then collected, sorted
     /// and converted to transform key values during post_update.
     transforms: FastHashSet<SpatialNodeIndex>,
     /// A list of potentially important clips. We can't know if
     /// they were important or can be discarded until we know the
     /// tile cache bounding rect.
     potential_clips: FastHashMap<RectangleKey, SpatialNodeIndex>,
+    /// If true, this tile should still be considered as part of
+    /// the dirty rect calculations.
+    consider_for_dirty_rect: bool,
 }
 
 impl Tile {
     /// Construct a new, invalid tile.
     fn new(
         id: TileId,
     ) -> Self {
         Tile {
@@ -201,16 +205,17 @@ impl Tile {
             handle: TextureCacheHandle::invalid(),
             descriptor: TileDescriptor::new(),
             is_same_content: false,
             is_valid: false,
             same_frames: 0,
             transforms: FastHashSet::default(),
             potential_clips: FastHashMap::default(),
             id,
+            consider_for_dirty_rect: false,
         }
     }
 
     /// Clear the dependencies for a tile.
     fn clear(&mut self) {
         self.transforms.clear();
         self.descriptor.clear();
         self.potential_clips.clear();
@@ -333,40 +338,204 @@ impl TileDescriptor {
         if !self.transforms.is_valid() {
             return false;
         }
 
         true
     }
 }
 
+/// Stores both the world and devices rects for a single dirty rect.
+#[derive(Debug, Clone)]
+pub struct DirtyRegionRect {
+    pub world_rect: WorldRect,
+    pub device_rect: DeviceIntRect,
+}
+
 /// Represents the dirty region of a tile cache picture.
-/// In future, we will want to support multiple dirty
-/// regions.
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct DirtyRegion {
-    pub dirty_world_rect: WorldRect,
-    pub dirty_device_rect: DeviceIntRect,
+    /// The individual dirty rects of this region.
+    pub dirty_rects: Vec<DirtyRegionRect>,
+
+    /// The overall dirty rect, a combination of dirty_rects
+    pub combined: DirtyRegionRect,
+}
+
+impl DirtyRegion {
+    /// Construct a new dirty region tracker.
+    pub fn new() -> Self {
+        DirtyRegion {
+            dirty_rects: Vec::with_capacity(MAX_DIRTY_RECTS),
+            combined: DirtyRegionRect {
+                world_rect: WorldRect::zero(),
+                device_rect: DeviceIntRect::zero(),
+            },
+        }
+    }
+
+    /// Reset the dirty regions back to empty
+    pub fn clear(&mut self) {
+        self.dirty_rects.clear();
+        self.combined = DirtyRegionRect {
+            world_rect: WorldRect::zero(),
+            device_rect: DeviceIntRect::zero(),
+        }
+    }
+
+    /// Push a dirty rect into this region
+    pub fn push(
+        &mut self,
+        rect: WorldRect,
+        device_pixel_scale: DevicePixelScale,
+    ) {
+        let device_rect = (rect * device_pixel_scale).round().to_i32();
+
+        // Include this in the overall dirty rect
+        self.combined.world_rect = self.combined.world_rect.union(&rect);
+        self.combined.device_rect = self.combined.device_rect.union(&device_rect);
+
+        // Store the individual dirty rect.
+        self.dirty_rects.push(DirtyRegionRect {
+            world_rect: rect,
+            device_rect,
+        });
+    }
+
+    /// Returns true if this region has no dirty rects
+    pub fn is_empty(&self) -> bool {
+        self.dirty_rects.is_empty()
+    }
+
+    /// Collapse all dirty rects into a single dirty rect.
+    pub fn collapse(&mut self) {
+        self.dirty_rects.clear();
+        self.dirty_rects.push(self.combined.clone());
+    }
+}
+
+/// A helper struct to build a (roughly) minimal set of dirty rectangles
+/// from a list of individual dirty rectangles. This minimizes the number
+/// of scissors rects and batch resubmissions that are needed.
+struct DirtyRegionBuilder<'a> {
+    tiles: &'a mut [Tile],
+    tile_count: TileSize,
+    device_pixel_scale: DevicePixelScale,
+}
+
+impl<'a> DirtyRegionBuilder<'a> {
+    fn new(
+        tiles: &'a mut [Tile],
+        tile_count: TileSize,
+        device_pixel_scale: DevicePixelScale,
+    ) -> Self {
+        DirtyRegionBuilder {
+            tiles,
+            tile_count,
+            device_pixel_scale,
+        }
+    }
+
+    fn tile_index(&self, x: i32, y: i32) -> usize {
+        (y * self.tile_count.width + x) as usize
+    }
+
+    fn is_dirty(&self, x: i32, y: i32) -> bool {
+        if x == self.tile_count.width || y == self.tile_count.height {
+            return false;
+        }
+
+        self.get_tile(x, y).consider_for_dirty_rect
+    }
+
+    fn get_tile(&self, x: i32, y: i32) -> &Tile {
+        &self.tiles[self.tile_index(x, y)]
+    }
+
+    fn get_tile_mut(&mut self, x: i32, y: i32) -> &mut Tile {
+        &mut self.tiles[self.tile_index(x, y)]
+    }
+
+    /// Return true if the entire column is dirty
+    fn column_is_dirty(&self, x: i32, y0: i32, y1: i32) -> bool {
+        for y in y0 .. y1 {
+            if !self.is_dirty(x, y) {
+                return false;
+            }
+        }
+
+        true
+    }
+
+    /// Push a dirty rect into the final region list.
+    fn push_dirty_rect(
+        &mut self,
+        x0: i32,
+        y0: i32,
+        x1: i32,
+        y1: i32,
+        dirty_region: &mut DirtyRegion,
+    ) {
+        // Construct the overall dirty rect by combining the visible
+        // parts of the dirty rects that were combined.
+        let mut dirty_world_rect = WorldRect::zero();
+
+        for y in y0 .. y1 {
+            for x in x0 .. x1 {
+                let tile = self.get_tile_mut(x, y);
+                tile.consider_for_dirty_rect = false;
+                if let Some(visible_rect) = tile.visible_rect {
+                    dirty_world_rect = dirty_world_rect.union(&visible_rect);
+                }
+            }
+        }
+
+        dirty_region.push(dirty_world_rect, self.device_pixel_scale);
+    }
+
+    /// Simple sweep through the tile grid to try and coalesce individual
+    /// dirty rects into a smaller number of larger dirty rectangles.
+    fn build(&mut self, dirty_region: &mut DirtyRegion) {
+        for x0 in 0 .. self.tile_count.width {
+            for y0 in 0 .. self.tile_count.height {
+                let mut y1 = y0;
+
+                while self.is_dirty(x0, y1) {
+                    y1 += 1;
+                }
+
+                if y1 > y0 {
+                    let mut x1 = x0;
+
+                    while self.column_is_dirty(x1, y0, y1) {
+                        x1 += 1;
+                    }
+
+                    self.push_dirty_rect(x0, y0, x1, y1, dirty_region);
+                }
+            }
+        }
+    }
 }
 
 /// Represents a cache of tiles that make up a picture primitives.
 pub struct TileCache {
     /// The positioning node for this tile cache.
     spatial_node_index: SpatialNodeIndex,
     /// List of tiles present in this picture (stored as a 2D array)
     pub tiles: Vec<Tile>,
     /// A helper struct to map local rects into world coords.
     map_local_to_world: SpaceMapper<LayoutPixel, WorldPixel>,
     /// A list of tiles to draw during batching.
     pub tiles_to_draw: Vec<TileIndex>,
     /// List of opacity bindings, with some extra information
     /// about whether they changed since last frame.
     opacity_bindings: FastHashMap<PropertyBindingId, OpacityBindingInfo>,
-    /// If Some(..) the region that is dirty in this picture.
-    pub dirty_region: Option<DirtyRegion>,
+    /// The current dirty region tracker for this picture.
+    pub dirty_region: DirtyRegion,
     /// If true, we need to update the prim dependencies, due
     /// to relative transforms changing. The dependencies are
     /// stored in each tile, and are a list of things that
     /// force the tile to re-rasterize if they change (e.g.
     /// images, transforms).
     needs_update: bool,
     /// The current world reference point that tiles are created around.
     world_origin: WorldPoint,
@@ -501,17 +670,17 @@ impl TileCache {
             spatial_node_index,
             tiles: Vec::new(),
             map_local_to_world: SpaceMapper::new(
                 ROOT_SPATIAL_NODE_INDEX,
                 WorldRect::zero(),
             ),
             tiles_to_draw: Vec::new(),
             opacity_bindings: FastHashMap::default(),
-            dirty_region: None,
+            dirty_region: DirtyRegion::new(),
             needs_update: true,
             world_origin: WorldPoint::zero(),
             world_tile_size: WorldSize::zero(),
             tile_count: TileSize::zero(),
             scroll_offset: None,
             pending_blits: Vec::new(),
             world_bounding_rect: WorldRect::zero(),
             root_clip_rect: WorldRect::max_rect(),
@@ -1132,19 +1301,17 @@ impl TileCache {
     /// set of tile blits.
     pub fn post_update(
         &mut self,
         resource_cache: &mut ResourceCache,
         gpu_cache: &mut GpuCache,
         frame_context: &FrameVisibilityContext,
         _scratch: &mut PrimitiveScratchBuffer,
     ) -> LayoutRect {
-        let mut dirty_world_rect = WorldRect::zero();
-
-        self.dirty_region = None;
+        self.dirty_region.clear();
         self.pending_blits.clear();
 
         let descriptor = ImageDescriptor::new(
             TILE_SIZE_WIDTH,
             TILE_SIZE_HEIGHT,
             ImageFormat::BGRA8,
             true,
             false,
@@ -1228,16 +1395,18 @@ impl TileCache {
 
             // If there are no primitives there is no need to draw or cache it.
             if tile.descriptor.prims.is_empty() {
                 continue;
             }
 
             // Decide how to handle this tile when drawing this frame.
             if tile.is_valid {
+                // No need to include this is any dirty rect calculations.
+                tile.consider_for_dirty_rect = false;
                 self.tiles_to_draw.push(TileIndex(i));
 
                 #[cfg(feature = "debug_renderer")]
                 {
                     if frame_context.debug_flags.contains(DebugFlags::PICTURE_CACHING_DBG) {
                         let tile_device_rect = tile.world_rect * frame_context.device_pixel_scale;
                         let mut label_pos = tile_device_rect.origin + DeviceVector2D::new(20.0, 30.0);
                         _scratch.push_debug_rect(
@@ -1253,19 +1422,16 @@ impl TileCache {
                         _scratch.push_debug_string(
                             label_pos,
                             debug_colors::RED,
                             format!("same: {} frames", tile.same_frames),
                         );
                     }
                 }
             } else {
-                // Add the tile rect to the dirty rect.
-                dirty_world_rect = dirty_world_rect.union(&visible_rect);
-
                 #[cfg(feature = "debug_renderer")]
                 {
                     if frame_context.debug_flags.contains(DebugFlags::PICTURE_CACHING_DBG) {
                         _scratch.push_debug_rect(
                             visible_rect * frame_context.device_pixel_scale,
                             debug_colors::RED,
                         );
                     }
@@ -1308,30 +1474,40 @@ impl TileCache {
                         src_offset: src_origin,
                         dest_offset: dest_rect.origin,
                         size: dest_rect.size,
                     });
 
                     // We can consider this tile valid now.
                     tile.is_valid = true;
                 }
+
+                // This tile should be considered as part of the dirty rect calculations.
+                tile.consider_for_dirty_rect = true;
             }
         }
 
-        // Store the dirty region for drawing the main scene.
-        self.dirty_region = if dirty_world_rect.is_empty() {
-            None
-        } else {
-            let dirty_device_rect = dirty_world_rect * frame_context.device_pixel_scale;
-
-            Some(DirtyRegion {
-                dirty_world_rect,
-                dirty_device_rect: dirty_device_rect.round().to_i32(),
-            })
-        };
+        // Build a minimal set of dirty rects from the set of dirty tiles that
+        // were found above.
+        let mut builder = DirtyRegionBuilder::new(
+            &mut self.tiles,
+            self.tile_count,
+            frame_context.device_pixel_scale,
+        );
+
+        builder.build(&mut self.dirty_region);
+
+        // If we end up with too many dirty rects, then it's going to be a lot
+        // of extra draw calls to submit (since we currently just submit every
+        // draw call for every dirty rect). In this case, bail out and work
+        // with a single, large dirty rect. In future we can consider improving
+        // on this by supporting batching per dirty region.
+        if self.dirty_region.dirty_rects.len() > MAX_DIRTY_RECTS {
+            self.dirty_region.collapse();
+        }
 
         local_clip_rect
     }
 }
 
 /// Maintains a stack of picture and surface information, that
 /// is used during the initial picture traversal.
 pub struct PictureUpdateState<'a> {
@@ -1928,42 +2104,21 @@ impl PicturePrimitive {
         &mut self,
         pic_index: PictureIndex,
         surface_spatial_node_index: SpatialNodeIndex,
         raster_spatial_node_index: SpatialNodeIndex,
         surface_index: SurfaceIndex,
         parent_allows_subpixel_aa: bool,
         frame_state: &mut FrameBuildingState,
         frame_context: &FrameBuildingContext,
-        dirty_world_rect: WorldRect,
     ) -> Option<(PictureContext, PictureState, PrimitiveList)> {
         if !self.is_visible() {
             return None;
         }
 
-        // Work out the dirty world rect for this picture.
-        let dirty_world_rect = match self.tile_cache {
-            Some(ref tile_cache) => {
-                // If a tile cache is present, extract the dirty
-                // world rect from the dirty region. If there is
-                // no dirty region there is nothing to render.
-                // TODO(gw): We could early out here in that case?
-                tile_cache
-                    .dirty_region
-                    .as_ref()
-                    .map_or(WorldRect::zero(), |region| {
-                        region.dirty_world_rect
-                    })
-            }
-            None => {
-                // No tile cache - just assume the current dirty world rect.
-                dirty_world_rect
-            }
-        };
-
         // Extract the raster and surface spatial nodes from the raster
         // config, if this picture establishes a surface. Otherwise just
         // pass in the spatial node indices from the parent context.
         let (raster_spatial_node_index, surface_spatial_node_index, surface_index) = match self.raster_config {
             Some(ref raster_config) => {
                 let surface = &frame_state.surfaces[raster_config.surface_index.0];
 
                 (surface.raster_spatial_node_index, self.spatial_node_index, raster_config.surface_index)
@@ -1971,32 +2126,32 @@ impl PicturePrimitive {
             None => {
                 (raster_spatial_node_index, surface_spatial_node_index, surface_index)
             }
         };
 
         let map_pic_to_world = SpaceMapper::new_with_target(
             ROOT_SPATIAL_NODE_INDEX,
             surface_spatial_node_index,
-            dirty_world_rect,
+            frame_context.screen_world_rect,
             frame_context.clip_scroll_tree,
         );
 
         let pic_bounds = map_pic_to_world.unmap(&map_pic_to_world.bounds)
                                          .unwrap_or(PictureRect::max_rect());
 
         let map_local_to_pic = SpaceMapper::new(
             surface_spatial_node_index,
             pic_bounds,
         );
 
         let (map_raster_to_world, map_pic_to_raster) = create_raster_mappers(
             surface_spatial_node_index,
             raster_spatial_node_index,
-            dirty_world_rect,
+            frame_context.screen_world_rect,
             frame_context.clip_scroll_tree,
         );
 
         let plane_splitter = match self.context_3d {
             Picture3DContext::Out => {
                 None
             }
             Picture3DContext::In { root_data: Some(_), .. } => {
@@ -2040,30 +2195,41 @@ impl PicturePrimitive {
             pipeline_id: self.pipeline_id,
             apply_local_clip_rect: self.apply_local_clip_rect,
             allow_subpixel_aa,
             is_passthrough: self.raster_config.is_none(),
             raster_space: self.requested_raster_space,
             raster_spatial_node_index,
             surface_spatial_node_index,
             surface_index,
-            dirty_world_rect,
         };
 
+        // If this is a picture cache, push the dirty region to ensure any
+        // child primitives are culled and clipped to the dirty rect(s).
+        if let Some(ref tile_cache) = self.tile_cache {
+            frame_state.push_dirty_region(tile_cache.dirty_region.clone());
+        }
+
         let prim_list = mem::replace(&mut self.prim_list, PrimitiveList::empty());
 
         Some((context, state, prim_list))
     }
 
     pub fn restore_context(
         &mut self,
         prim_list: PrimitiveList,
         context: PictureContext,
         state: PictureState,
+        frame_state: &mut FrameBuildingState,
     ) {
+        // Pop the dirty region for this picture cache
+        if self.tile_cache.is_some() {
+            frame_state.pop_dirty_region();
+        }
+
         self.prim_list = prim_list;
         self.state = Some((state, context));
     }
 
     pub fn take_state_and_context(&mut self) -> (PictureState, PictureContext) {
         self.state.take().expect("bug: no state present!")
     }
 
@@ -2446,17 +2612,17 @@ impl PicturePrimitive {
             let surface_info = &mut frame_state.surfaces[raster_config.surface_index.0];
             (surface_info.raster_spatial_node_index, surface_info.take_render_tasks())
         };
         let surfaces = &mut frame_state.surfaces;
 
         let (map_raster_to_world, map_pic_to_raster) = create_raster_mappers(
             prim_instance.spatial_node_index,
             raster_spatial_node_index,
-            pic_context.dirty_world_rect,
+            frame_context.screen_world_rect,
             frame_context.clip_scroll_tree,
         );
 
         let pic_rect = PictureRect::from_untyped(&self.local_rect.to_untyped());
 
         let (clipped, unclipped) = match get_raster_rects(
             pic_rect,
             &map_pic_to_raster,
@@ -2890,27 +3056,27 @@ fn calculate_uv_rect_kind(
         bottom_left,
         bottom_right,
     }
 }
 
 fn create_raster_mappers(
     surface_spatial_node_index: SpatialNodeIndex,
     raster_spatial_node_index: SpatialNodeIndex,
-    dirty_world_rect: WorldRect,
+    world_rect: WorldRect,
     clip_scroll_tree: &ClipScrollTree,
 ) -> (SpaceMapper<RasterPixel, WorldPixel>, SpaceMapper<PicturePixel, RasterPixel>) {
     let map_raster_to_world = SpaceMapper::new_with_target(
         ROOT_SPATIAL_NODE_INDEX,
         raster_spatial_node_index,
-        dirty_world_rect,
+        world_rect,
         clip_scroll_tree,
     );
 
-    let raster_bounds = map_raster_to_world.unmap(&dirty_world_rect)
+    let raster_bounds = map_raster_to_world.unmap(&world_rect)
                                            .unwrap_or(RasterRect::max_rect());
 
     let map_pic_to_raster = SpaceMapper::new_with_target(
         raster_spatial_node_index,
         surface_spatial_node_index,
         raster_bounds,
         clip_scroll_tree,
     );
--- a/gfx/wr/webrender/src/prim_store/mod.rs
+++ b/gfx/wr/webrender/src/prim_store/mod.rs
@@ -2198,17 +2198,16 @@ impl PrimitiveStore {
                     match pic.take_context(
                         pic_index,
                         pic_context.surface_spatial_node_index,
                         pic_context.raster_spatial_node_index,
                         pic_context.surface_index,
                         pic_context.allow_subpixel_aa,
                         frame_state,
                         frame_context,
-                        pic_context.dirty_world_rect,
                     ) {
                         Some(info) => Some(info),
                         None => {
                             if prim_instance.is_chased() {
                                 println!("\tculled for carrying an invisible composite filter");
                             }
 
                             return false;
@@ -2250,16 +2249,17 @@ impl PrimitiveStore {
                 }
 
                 // Restore the dependencies (borrow check dance)
                 self.pictures[pic_context_for_children.pic_index.0]
                     .restore_context(
                         prim_list,
                         pic_context_for_children,
                         pic_state_for_children,
+                        frame_state,
                     );
 
                 is_passthrough
             }
             None => {
                 false
             }
         };
@@ -2306,17 +2306,17 @@ impl PrimitiveStore {
                 ) {
                     if let Some(ref mut splitter) = pic_state.plane_splitter {
                         PicturePrimitive::add_split_plane(
                             splitter,
                             frame_state.transforms,
                             prim_instance,
                             pic.local_rect,
                             &prim_info.combined_local_clip_rect,
-                            pic_context.dirty_world_rect,
+                            frame_context.screen_world_rect,
                             plane_split_anchor,
                         );
                     }
                 } else {
                     prim_instance.visibility_info = PrimitiveVisibilityIndex::INVALID;
                 }
 
                 if let Some(mut request) = frame_state.gpu_cache.request(&mut pic.gpu_location) {
@@ -2372,22 +2372,46 @@ impl PrimitiveStore {
 
             // The original clipped world rect was calculated during the initial visibility pass.
             // However, it's possible that the dirty rect has got smaller, if tiles were not
             // dirty. Intersecting with the dirty rect here eliminates preparing any primitives
             // outside the dirty rect, and reduces the size of any off-screen surface allocations
             // for clip masks / render tasks that we make.
             {
                 let visibility_info = &mut scratch.prim_info[prim_instance.visibility_info.0 as usize];
-
-                match visibility_info.clipped_world_rect.intersection(&pic_context.dirty_world_rect) {
+                let dirty_region = frame_state.current_dirty_region();
+
+                // Check if the primitive world rect intersects with the overall dirty rect first.
+                match visibility_info.clipped_world_rect.intersection(&dirty_region.combined.world_rect) {
                     Some(rect) => {
+                        // It does intersect the overall dirty rect, so it *might* be visible.
+                        // Store this reduced rect here, which is used for clip mask and other
+                        // render task size calculations. In future, we may consider creating multiple
+                        // render task trees, one per dirty region.
                         visibility_info.clipped_world_rect = rect;
+
+                        // If there is more than one dirty region, it's possible that this primitive
+                        // is inside the overal dirty rect, but doesn't intersect any of the individual
+                        // dirty rects. If that's the case, then we can skip drawing this primitive too.
+                        if dirty_region.dirty_rects.len() > 1 {
+                            let in_dirty_rects = dirty_region
+                                .dirty_rects
+                                .iter()
+                                .any(|dirty_rect| {
+                                    visibility_info.clipped_world_rect.intersects(&dirty_rect.world_rect)
+                                });
+
+                            if !in_dirty_rects {
+                                prim_instance.visibility_info = PrimitiveVisibilityIndex::INVALID;
+                                continue;
+                            }
+                        }
                     }
                     None => {
+                        // Outside the overall dirty rect, so can be skipped.
                         prim_instance.visibility_info = PrimitiveVisibilityIndex::INVALID;
                         continue;
                     }
                 }
             }
 
             let spatial_node = &frame_context
                 .clip_scroll_tree
@@ -2653,17 +2677,17 @@ impl PrimitiveStore {
                             common_data.prim_size,
                         );
                         let tight_clip_rect = prim_info
                             .combined_local_clip_rect
                             .intersection(&prim_rect).unwrap();
 
                         let visible_rect = compute_conservative_visible_rect(
                             prim_context,
-                            &pic_context.dirty_world_rect,
+                            &frame_state.current_dirty_region().combined.world_rect,
                             &tight_clip_rect
                         );
 
                         let base_edge_flags = edge_flags_for_tile_spacing(&image_data.tile_spacing);
 
                         let stride = image_data.stretch_size + image_data.tile_spacing;
 
                         let repetitions = ::image::repetitions(
@@ -2747,17 +2771,16 @@ impl PrimitiveStore {
 
                     *visible_tiles_range = decompose_repeated_primitive(
                         &prim_info.combined_local_clip_rect,
                         &prim_rect,
                         &prim_data.stretch_size,
                         &prim_data.tile_spacing,
                         prim_context,
                         frame_state,
-                        &pic_context.dirty_world_rect,
                         &mut scratch.gradient_tiles,
                         &mut |_, mut request| {
                             request.push([
                                 prim_data.start_point.x,
                                 prim_data.start_point.y,
                                 prim_data.end_point.x,
                                 prim_data.end_point.y,
                             ]);
@@ -2794,17 +2817,16 @@ impl PrimitiveStore {
 
                     *visible_tiles_range = decompose_repeated_primitive(
                         &prim_info.combined_local_clip_rect,
                         &prim_rect,
                         &prim_data.stretch_size,
                         &prim_data.tile_spacing,
                         prim_context,
                         frame_state,
-                        &pic_context.dirty_world_rect,
                         &mut scratch.gradient_tiles,
                         &mut |_, mut request| {
                             request.push([
                                 prim_data.center.x,
                                 prim_data.center.y,
                                 prim_data.params.start_radius,
                                 prim_data.params.end_radius,
                             ]);
@@ -2859,31 +2881,31 @@ fn write_segment<F>(
 
 fn decompose_repeated_primitive(
     combined_local_clip_rect: &LayoutRect,
     prim_local_rect: &LayoutRect,
     stretch_size: &LayoutSize,
     tile_spacing: &LayoutSize,
     prim_context: &PrimitiveContext,
     frame_state: &mut FrameBuildingState,
-    world_rect: &WorldRect,
     gradient_tiles: &mut GradientTileStorage,
     callback: &mut FnMut(&LayoutRect, GpuDataRequest),
 ) -> GradientTileRange {
     let mut visible_tiles = Vec::new();
+    let world_rect = frame_state.current_dirty_region().combined.world_rect;
 
     // Tighten the clip rect because decomposing the repeated image can
     // produce primitives that are partially covering the original image
     // rect and we want to clip these extra parts out.
     let tight_clip_rect = combined_local_clip_rect
         .intersection(prim_local_rect).unwrap();
 
     let visible_rect = compute_conservative_visible_rect(
         prim_context,
-        world_rect,
+        &world_rect,
         &tight_clip_rect
     );
     let stride = *stretch_size + *tile_spacing;
 
     let repetitions = ::image::repetitions(prim_local_rect, &visible_rect, stride);
     for Repetition { origin, .. } in repetitions {
         let mut handle = GpuCacheHandle::new();
         let rect = LayoutRect {
@@ -3281,37 +3303,40 @@ impl PrimitiveInstance {
                 pic_context.surface_index,
                 pic_state,
                 frame_context,
                 frame_state,
                 &mut data_stores.clip,
             );
             clip_mask_instances.push(clip_mask_kind);
         } else {
+            let dirty_world_rect = frame_state.current_dirty_region().combined.world_rect;
+
             for segment in segments {
                 // Build a clip chain for the smaller segment rect. This will
                 // often manage to eliminate most/all clips, and sometimes
                 // clip the segment completely.
+
                 let segment_clip_chain = frame_state
                     .clip_store
                     .build_clip_chain_instance(
                         self,
                         segment.local_rect.translate(&LayoutVector2D::new(
                             self.prim_origin.x,
                             self.prim_origin.y,
                         )),
                         self.local_clip_rect,
                         prim_context.spatial_node_index,
                         &pic_state.map_local_to_pic,
                         &pic_state.map_pic_to_world,
                         &frame_context.clip_scroll_tree,
                         frame_state.gpu_cache,
                         frame_state.resource_cache,
                         frame_context.device_pixel_scale,
-                        &pic_context.dirty_world_rect,
+                        &dirty_world_rect,
                         None,
                         &mut data_stores.clip,
                     );
 
                 let clip_mask_kind = segment.update_clip_task(
                     segment_clip_chain.as_ref(),
                     prim_info.clipped_world_rect,
                     root_spatial_node_index,
--- a/gfx/wr/webrender/src/renderer.rs
+++ b/gfx/wr/webrender/src/renderer.rs
@@ -3001,22 +3001,22 @@ impl Renderer {
         }
 
         self.profile_counters.vertices.add(6 * data.len());
     }
 
     fn handle_readback_composite(
         &mut self,
         draw_target: DrawTarget,
-        scissor_rect: Option<DeviceIntRect>,
+        uses_scissor: bool,
         source: &RenderTask,
         backdrop: &RenderTask,
         readback: &RenderTask,
     ) {
-        if scissor_rect.is_some() {
+        if uses_scissor {
             self.device.disable_scissor();
         }
 
         let cache_texture = self.texture_resolver
             .resolve(&TextureSource::PrevPassColor)
             .unwrap();
 
         // Before submitting the composite batch, do the
@@ -3061,17 +3061,17 @@ impl Renderer {
         self.device.bind_read_target(draw_target.into());
         self.device.blit_render_target(src, dest);
 
         // Restore draw target to current pass render target + layer, and reset
         // the read target.
         self.device.bind_draw_target(draw_target);
         self.device.reset_read_target();
 
-        if scissor_rect.is_some() {
+        if uses_scissor {
             self.device.enable_scissor();
         }
     }
 
     fn handle_blits(
         &mut self,
         blits: &[BlitJob],
         render_tasks: &RenderTaskTree,
@@ -3270,30 +3270,42 @@ impl Renderer {
                     &BatchTextures::no_texture(),
                     stats,
                 );
             }
         }
 
         self.handle_scaling(&target.scalings, TextureSource::PrevPassColor, projection, stats);
 
+        // Small helper fn to iterate a regions list, also invoking the closure
+        // if there are no regions.
+        fn iterate_regions<F>(
+            regions: &[DeviceIntRect],
+            mut f: F,
+        ) where F: FnMut(Option<DeviceIntRect>) {
+            if regions.is_empty() {
+                f(None)
+            } else {
+                for region in regions {
+                    f(Some(*region))
+                }
+            }
+        }
+
         for alpha_batch_container in &target.alpha_batch_containers {
-            if let Some(scissor_rect) = alpha_batch_container.scissor_rect {
-                // Note: `framebuffer_target_rect` needs a Y-flip before going to GL
-                let rect = if draw_target.is_default() {
-                    let mut rect = scissor_rect
-                        .intersection(&framebuffer_target_rect.to_i32())
-                        .unwrap_or(DeviceIntRect::zero());
-                    rect.origin.y = draw_target.dimensions().height as i32 - rect.origin.y - rect.size.height;
-                    rect
-                } else {
-                    scissor_rect
-                };
+            let uses_scissor = alpha_batch_container.task_scissor_rect.is_some() ||
+                               !alpha_batch_container.regions.is_empty();
+
+            if uses_scissor {
                 self.device.enable_scissor();
-                self.device.set_scissor_rect(rect);
+                let scissor_rect = draw_target.build_scissor_rect(
+                    alpha_batch_container.task_scissor_rect,
+                    framebuffer_target_rect,
+                );
+                self.device.set_scissor_rect(scissor_rect)
             }
 
             if !alpha_batch_container.opaque_batches.is_empty() {
                 let _gl = self.gpu_profile.start_marker("opaque batches");
                 let opaque_sampler = self.gpu_profile.start_sampler(GPU_SAMPLER_TAG_OPAQUE);
                 self.set_blend(false, framebuffer_kind);
                 //Note: depth equality is needed for split planes
                 self.device.set_depth_func(DepthFunction::LessEqual);
@@ -3310,21 +3322,35 @@ impl Renderer {
                     self.shaders.borrow_mut()
                         .get(&batch.key, self.debug_flags)
                         .bind(
                             &mut self.device, projection,
                             &mut self.renderer_errors,
                         );
 
                     let _timer = self.gpu_profile.start_timer(batch.key.kind.sampler_tag());
-                    self.draw_instanced_batch(
-                        &batch.instances,
-                        VertexArrayKind::Primitive,
-                        &batch.key.textures,
-                        stats
+
+                    iterate_regions(
+                        &alpha_batch_container.regions,
+                        |region| {
+                            if let Some(region) = region {
+                                let scissor_rect = draw_target.build_scissor_rect(
+                                    Some(region),
+                                    framebuffer_target_rect,
+                                );
+                                self.device.set_scissor_rect(scissor_rect);
+                            }
+
+                            self.draw_instanced_batch(
+                                &batch.instances,
+                                VertexArrayKind::Primitive,
+                                &batch.key.textures,
+                                stats
+                            );
+                        }
                     );
                 }
 
                 self.device.disable_depth_write();
                 self.gpu_profile.finish_sampler(opaque_sampler);
             }
 
             if !alpha_batch_container.alpha_batches.is_empty() {
@@ -3380,58 +3406,74 @@ impl Renderer {
 
                     // Handle special case readback for composites.
                     if let BatchKind::Brush(BrushBatchKind::MixBlend { task_id, source_id, backdrop_id }) = batch.key.kind {
                         // composites can't be grouped together because
                         // they may overlap and affect each other.
                         debug_assert_eq!(batch.instances.len(), 1);
                         self.handle_readback_composite(
                             draw_target,
-                            alpha_batch_container.scissor_rect,
+                            uses_scissor,
                             &render_tasks[source_id],
                             &render_tasks[task_id],
                             &render_tasks[backdrop_id],
                         );
                     }
 
                     let _timer = self.gpu_profile.start_timer(batch.key.kind.sampler_tag());
-                    self.draw_instanced_batch(
-                        &batch.instances,
-                        VertexArrayKind::Primitive,
-                        &batch.key.textures,
-                        stats
+
+                    iterate_regions(
+                        &alpha_batch_container.regions,
+                        |region| {
+                            if let Some(region) = region {
+                                let scissor_rect = draw_target.build_scissor_rect(
+                                    Some(region),
+                                    framebuffer_target_rect,
+                                );
+                                self.device.set_scissor_rect(scissor_rect);
+                            }
+
+                            self.draw_instanced_batch(
+                                &batch.instances,
+                                VertexArrayKind::Primitive,
+                                &batch.key.textures,
+                                stats
+                            );
+
+                            if batch.key.blend_mode == BlendMode::SubpixelWithBgColor {
+                                self.set_blend_mode_subpixel_with_bg_color_pass1(framebuffer_kind);
+                                self.device.switch_mode(ShaderColorMode::SubpixelWithBgColorPass1 as _);
+
+                                // When drawing the 2nd and 3rd passes, we know that the VAO, textures etc
+                                // are all set up from the previous draw_instanced_batch call,
+                                // so just issue a draw call here to avoid re-uploading the
+                                // instances and re-binding textures etc.
+                                self.device
+                                    .draw_indexed_triangles_instanced_u16(6, batch.instances.len() as i32);
+
+                                self.set_blend_mode_subpixel_with_bg_color_pass2(framebuffer_kind);
+                                self.device.switch_mode(ShaderColorMode::SubpixelWithBgColorPass2 as _);
+
+                                self.device
+                                    .draw_indexed_triangles_instanced_u16(6, batch.instances.len() as i32);
+                            }
+                        }
                     );
 
                     if batch.key.blend_mode == BlendMode::SubpixelWithBgColor {
-                        self.set_blend_mode_subpixel_with_bg_color_pass1(framebuffer_kind);
-                        self.device.switch_mode(ShaderColorMode::SubpixelWithBgColorPass1 as _);
-
-                        // When drawing the 2nd and 3rd passes, we know that the VAO, textures etc
-                        // are all set up from the previous draw_instanced_batch call,
-                        // so just issue a draw call here to avoid re-uploading the
-                        // instances and re-binding textures etc.
-                        self.device
-                            .draw_indexed_triangles_instanced_u16(6, batch.instances.len() as i32);
-
-                        self.set_blend_mode_subpixel_with_bg_color_pass2(framebuffer_kind);
-                        self.device.switch_mode(ShaderColorMode::SubpixelWithBgColorPass2 as _);
-
-                        self.device
-                            .draw_indexed_triangles_instanced_u16(6, batch.instances.len() as i32);
-
                         prev_blend_mode = BlendMode::None;
                     }
                 }
 
                 self.device.disable_depth();
                 self.set_blend(false, framebuffer_kind);
                 self.gpu_profile.finish_sampler(transparent_sampler);
             }
 
-            if alpha_batch_container.scissor_rect.is_some() {
+            if uses_scissor {
                 self.device.disable_scissor();
             }
 
             // At the end of rendering a container, blit across any cache tiles
             // to the texture cache for use on subsequent frames.
             if !alpha_batch_container.tile_blits.is_empty() {
                 let _timer = self.gpu_profile.start_timer(GPU_TAG_BLIT);
 
@@ -5334,8 +5376,9 @@ fn get_vao<'a>(vertex_array_kind: Vertex
     }
 }
 
 #[derive(Clone, Copy, PartialEq)]
 enum FramebufferKind {
     Main,
     Other,
 }
+
--- a/gfx/wr/webrender/src/tiling.rs
+++ b/gfx/wr/webrender/src/tiling.rs
@@ -376,17 +376,17 @@ impl RenderTarget for ColorRenderTarget 
         ctx: &mut RenderTargetContext,
         gpu_cache: &mut GpuCache,
         render_tasks: &mut RenderTaskTree,
         deferred_resolves: &mut Vec<DeferredResolve>,
         prim_headers: &mut PrimitiveHeaders,
         transforms: &mut TransformPalette,
         z_generator: &mut ZBufferIdGenerator,
     ) {
-        let mut merged_batches = AlphaBatchContainer::new(None);
+        let mut merged_batches = AlphaBatchContainer::new(None, Vec::new());
 
         for task_id in &self.alpha_tasks {
             let task = &render_tasks[*task_id];
 
             match task.clear_mode {
                 ClearMode::One |
                 ClearMode::Zero => {
                     panic!("bug: invalid clear mode for color task");