Bug 1488887. Update webrender to 46af5bf17978a97f0ab2a899a0d785d58c140a0a
authorJeff Muizelaar <jmuizelaar@mozilla.com>
Wed, 05 Sep 2018 21:21:57 -0400
changeset 434988 ef1988b6f9b8a85dfff8ceaeccbedbf02bca81ce
parent 434987 809efffd8f8011137c03517d619df4a89cd9473c
child 434989 f471c397a043fc454277234b6182f6ef62aabaab
push id107528
push userjmuizelaar@mozilla.com
push dateThu, 06 Sep 2018 01:22:22 +0000
treeherdermozilla-inbound@5d90cbaeceb8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1488887
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1488887. Update webrender to 46af5bf17978a97f0ab2a899a0d785d58c140a0a
gfx/webrender/Cargo.toml
gfx/webrender/res/cs_border_segment.glsl
gfx/webrender/res/cs_border_solid.glsl
gfx/webrender/res/prim_shared.glsl
gfx/webrender/res/ps_text_run.glsl
gfx/webrender/res/render_task.glsl
gfx/webrender/res/resource_cache.glsl
gfx/webrender/res/shared.glsl
gfx/webrender/res/transform.glsl
gfx/webrender/src/batch.rs
gfx/webrender/src/display_list_flattener.rs
gfx/webrender/src/prim_store.rs
gfx/webrender/src/render_backend.rs
gfx/webrender/src/renderer.rs
gfx/webrender/src/scene.rs
gfx/webrender/src/scene_builder.rs
gfx/webrender/src/shade.rs
gfx/webrender/src/tiling.rs
gfx/webrender/src/util.rs
gfx/webrender/tests/angle_shader_validation.rs
gfx/webrender_api/src/api.rs
gfx/webrender_bindings/revision.txt
--- a/gfx/webrender/Cargo.toml
+++ b/gfx/webrender/Cargo.toml
@@ -27,17 +27,17 @@ byteorder = "1.0"
 cfg-if = "0.1.2"
 euclid = "0.19"
 fxhash = "0.2.1"
 gleam = "0.6"
 image = { optional = true, version = "0.19" }
 lazy_static = "1"
 log = "0.4"
 num-traits = "0.2"
-plane-split = "0.12.1"
+plane-split = "0.13"
 png = { optional = true, version = "0.12" }
 rayon = "1"
 ron = { optional = true, version = "0.1.7" }
 serde = { optional = true, version = "1.0", features = ["serde_derive"] }
 serde_json = { optional = true, version = "1.0" }
 smallvec = "0.6"
 thread_profiler = "0.1.1"
 time = "0.1"
--- a/gfx/webrender/res/cs_border_segment.glsl
+++ b/gfx/webrender/res/cs_border_segment.glsl
@@ -89,17 +89,17 @@ vec2 get_outer_corner_scale(int segment)
             break;
         case SEGMENT_BOTTOM_RIGHT:
             p = vec2(1.0, 1.0);
             break;
         case SEGMENT_BOTTOM_LEFT:
             p = vec2(0.0, 1.0);
             break;
         default:
-            // Should never get hit
+            // The result is only used for non-default segment cases
             p = vec2(0.0);
             break;
     }
 
     return p;
 }
 
 // NOTE(emilio): If you change this algorithm, do the same change
new file mode 100644
--- /dev/null
+++ b/gfx/webrender/res/cs_border_solid.glsl
@@ -0,0 +1,223 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include shared,ellipse
+
+// For edges, the colors are the same. For corners, these
+// are the colors of each edge making up the corner.
+flat varying vec4 vColor0;
+flat varying vec4 vColor1;
+
+// A point + tangent defining the line where the edge
+// transition occurs. Used for corners only.
+flat varying vec4 vColorLine;
+
+// x = segment, z = edge axes, w = clip mode
+flat varying ivec4 vConfig;
+
+// xy = Local space position of the clip center.
+// zw = Scale the rect origin by this to get the outer
+// corner from the segment rectangle.
+flat varying vec4 vClipCenter_Sign;
+
+// An outer and inner elliptical radii for border
+// corner clipping.
+flat varying vec4 vClipRadii;
+
+// Reference point for determine edge clip lines.
+flat varying vec4 vEdgeReference;
+
+// Stores widths/2 and widths/3 to save doing this in FS.
+flat varying vec4 vPartialWidths;
+
+// Clipping parameters for dot or dash.
+flat varying vec4 vClipParams1;
+flat varying vec4 vClipParams2;
+
+// Local space position
+varying vec2 vPos;
+
+#define SEGMENT_TOP_LEFT        0
+#define SEGMENT_TOP_RIGHT       1
+#define SEGMENT_BOTTOM_RIGHT    2
+#define SEGMENT_BOTTOM_LEFT     3
+#define SEGMENT_LEFT            4
+#define SEGMENT_TOP             5
+#define SEGMENT_RIGHT           6
+#define SEGMENT_BOTTOM          7
+
+#define CLIP_NONE   0
+#define CLIP_DASH   1
+#define CLIP_DOT    2
+
+#ifdef WR_VERTEX_SHADER
+
+in vec2 aTaskOrigin;
+in vec4 aRect;
+in vec4 aColor0;
+in vec4 aColor1;
+in int aFlags;
+in vec2 aWidths;
+in vec2 aRadii;
+in vec4 aClipParams1;
+in vec4 aClipParams2;
+
+vec2 get_outer_corner_scale(int segment) {
+    vec2 p;
+
+    switch (segment) {
+        case SEGMENT_TOP_LEFT:
+            p = vec2(0.0, 0.0);
+            break;
+        case SEGMENT_TOP_RIGHT:
+            p = vec2(1.0, 0.0);
+            break;
+        case SEGMENT_BOTTOM_RIGHT:
+            p = vec2(1.0, 1.0);
+            break;
+        case SEGMENT_BOTTOM_LEFT:
+            p = vec2(0.0, 1.0);
+            break;
+        default:
+            // The result is only used for non-default segment cases
+            p = vec2(0.0);
+            break;
+    }
+
+    return p;
+}
+
+void main(void) {
+    int segment = aFlags & 0xff;
+    int clip_mode = (aFlags >> 24) & 0xff;
+
+    vec2 outer_scale = get_outer_corner_scale(segment);
+    vec2 outer = outer_scale * aRect.zw;
+    vec2 clip_sign = 1.0 - 2.0 * outer_scale;
+
+    // Set some flags used by the FS to determine the
+    // orientation of the two edges in this corner.
+    ivec2 edge_axis;
+    // Derive the positions for the edge clips, which must be handled
+    // differently between corners and edges.
+    vec2 edge_reference;
+    switch (segment) {
+        case SEGMENT_TOP_LEFT:
+            edge_axis = ivec2(0, 1);
+            edge_reference = outer;
+            break;
+        case SEGMENT_TOP_RIGHT:
+            edge_axis = ivec2(1, 0);
+            edge_reference = vec2(outer.x - aWidths.x, outer.y);
+            break;
+        case SEGMENT_BOTTOM_RIGHT:
+            edge_axis = ivec2(0, 1);
+            edge_reference = outer - aWidths;
+            break;
+        case SEGMENT_BOTTOM_LEFT:
+            edge_axis = ivec2(1, 0);
+            edge_reference = vec2(outer.x, outer.y - aWidths.y);
+            break;
+        case SEGMENT_TOP:
+        case SEGMENT_BOTTOM:
+            edge_axis = ivec2(1, 1);
+            edge_reference = vec2(0.0);
+            break;
+        case SEGMENT_LEFT:
+        case SEGMENT_RIGHT:
+        default:
+            edge_axis = ivec2(0, 0);
+            edge_reference = vec2(0.0);
+            break;
+    }
+
+    vConfig = ivec4(
+        segment,
+        0,
+        edge_axis.x | (edge_axis.y << 16),
+        clip_mode
+    );
+    vPartialWidths = vec4(aWidths / 3.0, aWidths / 2.0);
+    vPos = aRect.zw * aPosition.xy;
+
+    vColor0 = aColor0;
+    vColor1 = aColor1;
+    vClipCenter_Sign = vec4(outer + clip_sign * aRadii, clip_sign);
+    vClipRadii = vec4(aRadii, max(aRadii - aWidths, 0.0));
+    vColorLine = vec4(outer, aWidths.y * -clip_sign.y, aWidths.x * clip_sign.x);
+    vEdgeReference = vec4(edge_reference, edge_reference + aWidths);
+    vClipParams1 = aClipParams1;
+    vClipParams2 = aClipParams2;
+
+    // For the case of dot clips, optimize the number of pixels that
+    // are hit to just include the dot itself.
+    // TODO(gw): We should do something similar in the future for
+    //           dash clips!
+    if (clip_mode == CLIP_DOT) {
+        // Expand by a small amount to allow room for AA around
+        // the dot.
+        float expanded_radius = aClipParams1.z + 2.0;
+        vPos = vClipParams1.xy + expanded_radius * (2.0 * aPosition.xy - 1.0);
+        vPos = clamp(vPos, vec2(0.0), aRect.zw);
+    }
+
+    gl_Position = uTransform * vec4(aTaskOrigin + aRect.xy + vPos, 0.0, 1.0);
+}
+#endif
+
+#ifdef WR_FRAGMENT_SHADER
+void main(void) {
+    float aa_range = compute_aa_range(vPos);
+    vec4 color0, color1;
+
+    int segment = vConfig.x;
+    ivec2 edge_axis = ivec2(vConfig.z & 0xffff, vConfig.z >> 16);
+    int clip_mode = vConfig.w;
+
+    float mix_factor = 0.0;
+    if (edge_axis.x != edge_axis.y) {
+        float d_line = distance_to_line(vColorLine.xy, vColorLine.zw, vPos);
+        mix_factor = distance_aa(aa_range, -d_line);
+    }
+
+    // Check if inside corner clip-region
+    vec2 clip_relative_pos = vPos - vClipCenter_Sign.xy;
+    bool in_clip_region = all(lessThan(vClipCenter_Sign.zw * clip_relative_pos, vec2(0.0)));
+    float d = -1.0;
+
+    switch (clip_mode) {
+        case CLIP_DOT: {
+            // Set clip distance based or dot position and radius.
+            d = distance(vClipParams1.xy, vPos) - vClipParams1.z;
+            break;
+        }
+        case CLIP_DASH: {
+            // Get SDF for the two line/tangent clip lines,
+            // do SDF subtract to get clip distance.
+            float d0 = distance_to_line(vClipParams1.xy,
+                                        vClipParams1.zw,
+                                        vPos);
+            float d1 = distance_to_line(vClipParams2.xy,
+                                        vClipParams2.zw,
+                                        vPos);
+            d = max(d0, -d1);
+            break;
+        }
+        case CLIP_NONE:
+        default:
+            break;
+    }
+
+    if (in_clip_region) {
+        float d_radii_a = distance_to_ellipse(clip_relative_pos, vClipRadii.xy, aa_range);
+        float d_radii_b = distance_to_ellipse(clip_relative_pos, vClipRadii.zw, aa_range);
+        float d_radii = max(d_radii_a, -d_radii_b);
+        d = max(d, d_radii);
+    }
+
+    float alpha = distance_aa(aa_range, d);
+    vec4 color = mix(vColor0, vColor1, mix_factor);
+    oFragColor = color * alpha;
+}
+#endif
--- a/gfx/webrender/res/prim_shared.glsl
+++ b/gfx/webrender/res/prim_shared.glsl
@@ -45,18 +45,18 @@ varying vec4 vClipMaskUv;
 #define COLOR_MODE_IMAGE              9
 
 uniform HIGHP_SAMPLER_FLOAT sampler2D sPrimitiveHeadersF;
 uniform HIGHP_SAMPLER_FLOAT isampler2D sPrimitiveHeadersI;
 
 // Instanced attributes
 in ivec4 aData;
 
-#define VECS_PER_PRIM_HEADER_F 2
-#define VECS_PER_PRIM_HEADER_I 2
+#define VECS_PER_PRIM_HEADER_F 2U
+#define VECS_PER_PRIM_HEADER_I 2U
 
 struct PrimitiveHeader {
     RectWithSize local_rect;
     RectWithSize local_clip_rect;
     float z;
     int specific_prim_address;
     int render_task_index;
     int clip_task_index;
--- a/gfx/webrender/res/ps_text_run.glsl
+++ b/gfx/webrender/res/ps_text_run.glsl
@@ -11,32 +11,34 @@ flat varying vec2 vMaskSwizzle;
 
 #ifdef WR_FEATURE_GLYPH_TRANSFORM
 varying vec4 vUvClip;
 #endif
 
 #ifdef WR_VERTEX_SHADER
 
 #define VECS_PER_TEXT_RUN           3
+#define GLYPHS_PER_GPU_BLOCK        2U
 
 struct Glyph {
     vec2 offset;
 };
 
 Glyph fetch_glyph(int specific_prim_address,
                   int glyph_index) {
     // Two glyphs are packed in each texel in the GPU cache.
     int glyph_address = specific_prim_address +
                         VECS_PER_TEXT_RUN +
-                        glyph_index / 2;
+                        int(uint(glyph_index) / GLYPHS_PER_GPU_BLOCK);
     vec4 data = fetch_from_resource_cache_1(glyph_address);
     // Select XY or ZW based on glyph index.
     // We use "!= 0" instead of "== 1" here in order to work around a driver
     // bug with equality comparisons on integers.
-    vec2 glyph = mix(data.xy, data.zw, bvec2(glyph_index % 2 != 0));
+    vec2 glyph = mix(data.xy, data.zw,
+                     bvec2(uint(glyph_index) % GLYPHS_PER_GPU_BLOCK != 0U));
 
     return Glyph(glyph);
 }
 
 struct GlyphResource {
     vec4 uv_rect;
     float layer;
     vec2 offset;
--- a/gfx/webrender/res/render_task.glsl
+++ b/gfx/webrender/res/render_task.glsl
@@ -1,15 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 
 #ifdef WR_VERTEX_SHADER
-#define VECS_PER_RENDER_TASK        2
+#define VECS_PER_RENDER_TASK        2U
 
 uniform HIGHP_SAMPLER_FLOAT sampler2D sRenderTasks;
 
 struct RenderTaskCommonData {
     RectWithSize task_rect;
     float texture_layer_index;
 };
 
--- a/gfx/webrender/res/resource_cache.glsl
+++ b/gfx/webrender/res/resource_cache.glsl
@@ -9,18 +9,18 @@ uniform HIGHP_SAMPLER_FLOAT sampler2D sR
 // TODO(gw): This is here temporarily while we have
 //           both GPU store and cache. When the GPU
 //           store code is removed, we can change the
 //           PrimitiveInstance instance structure to
 //           use 2x unsigned shorts as vertex attributes
 //           instead of an int, and encode the UV directly
 //           in the vertices.
 ivec2 get_resource_cache_uv(int address) {
-    return ivec2(address % WR_MAX_VERTEX_TEXTURE_WIDTH,
-                 address / WR_MAX_VERTEX_TEXTURE_WIDTH);
+    return ivec2(uint(address) % WR_MAX_VERTEX_TEXTURE_WIDTH,
+                 uint(address) / WR_MAX_VERTEX_TEXTURE_WIDTH);
 }
 
 vec4[2] fetch_from_resource_cache_2_direct(ivec2 address) {
     return vec4[2](
         TEXEL_FETCH(sResourceCache, address, 0, ivec2(0, 0)),
         TEXEL_FETCH(sResourceCache, address, 0, ivec2(1, 0))
     );
 }
--- a/gfx/webrender/res/shared.glsl
+++ b/gfx/webrender/res/shared.glsl
@@ -35,17 +35,17 @@
     // Attribute inputs
     in vec3 aPosition;
 
     // get_fetch_uv is a macro to work around a macOS Intel driver parsing bug.
     // TODO: convert back to a function once the driver issues are resolved, if ever.
     // https://github.com/servo/webrender/pull/623
     // https://github.com/servo/servo/issues/13953
     // Do the division with unsigned ints because that's more efficient with D3D
-    #define get_fetch_uv(i, vpi)  ivec2(int(uint(vpi) * (uint(i) % uint(WR_MAX_VERTEX_TEXTURE_WIDTH/vpi))), int(uint(i) / uint(WR_MAX_VERTEX_TEXTURE_WIDTH/vpi)))
+    #define get_fetch_uv(i, vpi)  ivec2(int(vpi * (uint(i) % (WR_MAX_VERTEX_TEXTURE_WIDTH/vpi))), int(uint(i) / (WR_MAX_VERTEX_TEXTURE_WIDTH/vpi)))
 #endif
 
 //======================================================================================
 // Fragment shader attributes and uniforms
 //======================================================================================
 #ifdef WR_FRAGMENT_SHADER
     // Uniform inputs
 
--- a/gfx/webrender/res/transform.glsl
+++ b/gfx/webrender/res/transform.glsl
@@ -1,17 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 flat varying vec4 vTransformBounds;
 
 #ifdef WR_VERTEX_SHADER
 
-#define VECS_PER_TRANSFORM   8
+#define VECS_PER_TRANSFORM   8U
 uniform HIGHP_SAMPLER_FLOAT sampler2D sTransformPalette;
 
 void init_transform_vs(vec4 local_bounds) {
     vTransformBounds = local_bounds;
 }
 
 struct Transform {
     mat4 m;
--- a/gfx/webrender/src/batch.rs
+++ b/gfx/webrender/src/batch.rs
@@ -655,18 +655,20 @@ impl AlphaBatchBuilder {
                                 TransformedRectKind::Complex => {
                                     let mut clipper = Clipper::new();
                                     let matrix = transform.m.cast();
                                     let results = clipper.clip_transformed(
                                         Polygon::from_rect(prim_metadata.local_rect.cast(), prim_index.0),
                                         &matrix,
                                         Some(bounding_rect.to_f64()),
                                     );
-                                    for poly in results {
-                                        splitter.add(poly);
+                                    if let Ok(results) = results {
+                                        for poly in results {
+                                            splitter.add(poly);
+                                        }
                                     }
                                 }
                             }
 
                             return;
                         }
 
                         match picture.raster_config {
@@ -1606,16 +1608,17 @@ pub fn resolve_image(
                             image_properties.descriptor.size,
                         ),
                         texture_layer: 0,
                     };
 
                     deferred_resolves.push(DeferredResolve {
                         image_properties,
                         address: gpu_cache.get_address(&cache_handle),
+                        rendering: request.rendering,
                     });
 
                     cache_item
                 }
                 None => {
                     if let Ok(cache_item) = resource_cache.get_cached_image(request) {
                         cache_item
                     } else {
--- a/gfx/webrender/src/display_list_flattener.rs
+++ b/gfx/webrender/src/display_list_flattener.rs
@@ -26,17 +26,16 @@ use internal_types::{FastHashMap, FastHa
 use picture::{PictureCompositeMode, PictureId, PicturePrimitive};
 use prim_store::{BrushKind, BrushPrimitive, BrushSegmentDescriptor};
 use prim_store::{EdgeAaSegmentMask, ImageSource};
 use prim_store::{BorderSource, BrushSegment, PrimitiveContainer, PrimitiveIndex, PrimitiveStore};
 use prim_store::{OpacityBinding, ScrollNodeAndClipChain, TextRunPrimitive};
 use render_backend::{DocumentView};
 use resource_cache::{FontInstanceMap, ImageRequest};
 use scene::{Scene, ScenePipeline, StackingContextHelpers};
-use scene_builder::{BuiltScene, SceneRequest};
 use spatial_node::{SpatialNodeType, StickyFrameInfo};
 use std::{f32, iter, mem};
 use tiling::{CompositeOps, ScrollbarPrimitive};
 use util::{MaxRect, RectHelpers, recycle_vec};
 
 static DEFAULT_SCROLLBAR_COLOR: ColorF = ColorF {
     r: 0.3,
     g: 0.3,
@@ -1995,41 +1994,16 @@ impl<'a> DisplayListFlattener<'a> {
         )
     }
 
     pub fn simple_scroll_and_clip_chain(&mut self, id: &ClipId) -> ScrollNodeAndClipChain {
         self.map_clip_and_scroll(&ClipAndScrollInfo::simple(*id))
     }
 }
 
-pub fn build_scene(config: &FrameBuilderConfig, request: SceneRequest) -> BuiltScene {
-
-    let mut clip_scroll_tree = ClipScrollTree::new();
-    let mut new_scene = Scene::new();
-
-    let frame_builder = DisplayListFlattener::create_frame_builder(
-        FrameBuilder::empty(), // WIP, we're not really recycling anything here, clean this up.
-        &request.scene,
-        &mut clip_scroll_tree,
-        request.font_instances,
-        &request.view,
-        &request.output_pipelines,
-        config,
-        &mut new_scene,
-        request.scene_id,
-    );
-
-    BuiltScene {
-        scene: new_scene,
-        frame_builder,
-        clip_scroll_tree,
-        removed_pipelines: request.removed_pipelines,
-    }
-}
-
 /// Properties of a stacking context that are maintained
 /// during creation of the scene. These structures are
 /// not persisted after the initial scene build.
 struct FlattenedStackingContext {
     /// Pipeline this stacking context belongs to.
     pipeline_id: PipelineId,
 
     /// Filters / mix-blend-mode effects
--- a/gfx/webrender/src/prim_store.rs
+++ b/gfx/webrender/src/prim_store.rs
@@ -206,22 +206,25 @@ impl<F, T> SpaceMapper<F, T> where F: fm
 
 /// For external images, it's not possible to know the
 /// UV coords of the image (or the image data itself)
 /// until the render thread receives the frame and issues
 /// callbacks to the client application. For external
 /// images that are visible, a DeferredResolve is created
 /// that is stored in the frame. This allows the render
 /// thread to iterate this list and update any changed
-/// texture data and update the UV rect.
+/// texture data and update the UV rect. Any filtering
+/// is handled externally for NativeTexture external
+/// images.
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct DeferredResolve {
     pub address: GpuCacheAddress,
     pub image_properties: ImageProperties,
+    pub rendering: ImageRendering,
 }
 
 #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct PrimitiveIndex(pub usize);
 
 impl GpuCacheHandle {
--- a/gfx/webrender/src/render_backend.rs
+++ b/gfx/webrender/src/render_backend.rs
@@ -12,18 +12,16 @@ use api::{ScrollLocation, ScrollNodeStat
 use api::channel::{MsgReceiver, Payload};
 #[cfg(feature = "capture")]
 use api::CaptureBits;
 #[cfg(feature = "replay")]
 use api::CapturedDocument;
 use clip_scroll_tree::{SpatialNodeIndex, ClipScrollTree};
 #[cfg(feature = "debugger")]
 use debug_server;
-#[cfg(feature = "replay")]
-use display_list_flattener::build_scene;
 use frame_builder::{FrameBuilder, FrameBuilderConfig};
 use gpu_cache::GpuCache;
 use hit_test::{HitTest, HitTester};
 use internal_types::{DebugOutput, FastHashMap, FastHashSet, RenderedDocument, ResultMsg};
 use profiler::{BackendProfileCounters, IpcProfileCounters, ResourceProfileCounters};
 use record::ApiRecordingReceiver;
 use renderer::{AsyncPropertySampler, PipelineInfo};
 use resource_cache::ResourceCache;
@@ -65,47 +63,40 @@ impl DocumentView {
         DevicePixelScale::new(
             self.device_pixel_ratio *
             self.page_zoom_factor *
             self.pinch_zoom_factor
         )
     }
 }
 
-struct SceneData {
-    scene: Scene,
-    removed_pipelines: Vec<PipelineId>,
-}
-
 #[derive(Copy, Clone, Hash, PartialEq, PartialOrd, Debug, Eq, Ord)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct FrameId(pub u32);
 
 struct Document {
     // The latest built scene, usable to build frames.
     // received from the scene builder thread.
-    current: SceneData,
-    // The scene with the latest transactions applied, not necessarily built yet.
-    // what we will send to the scene builder.
-    pending: SceneData,
+    scene: Scene,
+
+    // Temporary list of removed pipelines received from the scene builder
+    // thread and forwarded to the renderer.
+    removed_pipelines: Vec<PipelineId>,
 
     view: DocumentView,
 
     /// The ClipScrollTree for this document which tracks SpatialNodes, ClipNodes, and ClipChains.
     /// This is stored here so that we are able to preserve scrolling positions between rendered
     /// frames.
     clip_scroll_tree: ClipScrollTree,
 
     /// The id of the current frame.
     frame_id: FrameId,
 
-    /// A configuration object for the FrameBuilder that we produce.
-    frame_builder_config: FrameBuilderConfig,
-
     // the `Option` here is only to deal with borrow checker
     frame_builder: Option<FrameBuilder>,
     // A set of pipelines that the caller has requested be
     // made available as output textures.
     output_pipelines: FastHashSet<PipelineId>,
     // A helper switch to prevent any frames rendering triggered by scrolling
     // messages between `SetDisplayList` and `GenerateFrame`.
     // If we allow them, then a reftest that scrolls a few layers before generating
@@ -119,79 +110,70 @@ struct Document {
 
     /// Properties that are resolved during frame building and can be changed at any time
     /// without requiring the scene to be re-built.
     dynamic_properties: SceneProperties,
 }
 
 impl Document {
     pub fn new(
-        frame_builder_config: FrameBuilderConfig,
         window_size: DeviceUintSize,
         layer: DocumentLayer,
         enable_render_on_scroll: bool,
         default_device_pixel_ratio: f32,
     ) -> Self {
         let render_on_scroll = if enable_render_on_scroll {
             Some(false)
         } else {
             None
         };
         Document {
-            current: SceneData {
-                scene: Scene::new(),
-                removed_pipelines: Vec::new(),
-            },
-            pending: SceneData {
-                scene: Scene::new(),
-                removed_pipelines: Vec::new(),
-            },
+            scene: Scene::new(),
+            removed_pipelines: Vec::new(),
             view: DocumentView {
                 window_size,
                 inner_rect: DeviceUintRect::new(DeviceUintPoint::zero(), window_size),
                 layer,
                 pan: DeviceIntPoint::zero(),
                 page_zoom_factor: 1.0,
                 pinch_zoom_factor: 1.0,
                 device_pixel_ratio: default_device_pixel_ratio,
             },
             clip_scroll_tree: ClipScrollTree::new(),
             frame_id: FrameId(0),
-            frame_builder_config,
             frame_builder: None,
             output_pipelines: FastHashSet::default(),
             render_on_scroll,
             hit_tester: None,
             dynamic_properties: SceneProperties::new(),
         }
     }
 
-    fn can_render(&self) -> bool { self.frame_builder.is_some() }
+    fn can_render(&self) -> bool {
+        self.frame_builder.is_some() && self.scene.has_root_pipeline()
+    }
 
     fn has_pixels(&self) -> bool {
         !self.view.window_size.is_empty_or_negative()
     }
 
     fn process_frame_msg(
         &mut self,
         message: FrameMsg,
     ) -> DocumentOps {
         match message {
             FrameMsg::UpdateEpoch(pipeline_id, epoch) => {
-                self.current.scene.update_epoch(pipeline_id, epoch);
-
-                DocumentOps::nop()
+                self.scene.update_epoch(pipeline_id, epoch);
             }
             FrameMsg::EnableFrameOutput(pipeline_id, enable) => {
                 if enable {
                     self.output_pipelines.insert(pipeline_id);
                 } else {
                     self.output_pipelines.remove(&pipeline_id);
                 }
-                DocumentOps::nop()
             }
             FrameMsg::Scroll(delta, cursor) => {
                 profile_scope!("Scroll");
 
                 let mut should_render = true;
                 let node_index = match self.hit_tester {
                     Some(ref hit_tester) => {
                         // Ideally we would call self.scroll_nearest_scrolling_ancestor here, but
@@ -204,131 +186,84 @@ impl Document {
                         None
                     }
                 };
 
                 let should_render =
                     should_render &&
                     self.scroll_nearest_scrolling_ancestor(delta, node_index) &&
                     self.render_on_scroll == Some(true);
-                DocumentOps {
+
+                return DocumentOps {
                     scroll: true,
-                    render: should_render,
-                    composite: should_render,
+                    build_frame: should_render,
+                    render_frame: should_render,
                     ..DocumentOps::nop()
-                }
+                };
             }
             FrameMsg::HitTest(pipeline_id, point, flags, tx) => {
 
                 let result = match self.hit_tester {
                     Some(ref hit_tester) => {
                         hit_tester.hit_test(HitTest::new(pipeline_id, point, flags))
                     }
                     None => HitTestResult { items: Vec::new() },
                 };
 
                 tx.send(result).unwrap();
-                DocumentOps::nop()
             }
             FrameMsg::SetPan(pan) => {
                 self.view.pan = pan;
-                DocumentOps::nop()
             }
             FrameMsg::ScrollNodeWithId(origin, id, clamp) => {
                 profile_scope!("ScrollNodeWithScrollId");
 
                 let should_render = self.scroll_node(origin, id, clamp)
                     && self.render_on_scroll == Some(true);
 
-                DocumentOps {
+                return DocumentOps {
                     scroll: true,
-                    render: should_render,
-                    composite: should_render,
+                    build_frame: should_render,
+                    render_frame: should_render,
                     ..DocumentOps::nop()
-                }
+                };
             }
             FrameMsg::GetScrollNodeState(tx) => {
                 profile_scope!("GetScrollNodeState");
                 tx.send(self.get_scroll_node_state()).unwrap();
-                DocumentOps::nop()
             }
             FrameMsg::UpdateDynamicProperties(property_bindings) => {
                 self.dynamic_properties.set_properties(property_bindings);
-                DocumentOps::nop()
             }
             FrameMsg::AppendDynamicProperties(property_bindings) => {
                 self.dynamic_properties.add_properties(property_bindings);
-                DocumentOps::nop()
             }
         }
+
+        DocumentOps::nop()
     }
 
-    fn forward_transaction_to_scene_builder(
-        &mut self,
-        transaction_msg: TransactionMsg,
-        blobs_to_rasterize: &[ImageKey],
-        document_ops: &DocumentOps,
-        document_id: DocumentId,
-        scene_id: u64,
-        resource_cache: &mut ResourceCache,
-        scene_tx: &Sender<SceneBuilderRequest>,
-    ) {
-        // Do as much of the error handling as possible here before dispatching to
-        // the scene builder thread.
-        let build_scene: bool = document_ops.build
-            && self.pending.scene.root_pipeline_id.map(
-                |id| { self.pending.scene.pipelines.contains_key(&id) }
-            ).unwrap_or(false);
-
-        let scene_request = if build_scene {
-            Some(SceneRequest {
-                scene: self.pending.scene.clone(),
-                removed_pipelines: replace(&mut self.pending.removed_pipelines, Vec::new()),
-                view: self.view.clone(),
-                font_instances: resource_cache.get_font_instances(),
-                output_pipelines: self.output_pipelines.clone(),
-                scene_id,
-            })
-        } else {
-            None
-        };
-
-        let (blob_rasterizer, blob_requests) = resource_cache.create_blob_scene_builder_requests(
-            blobs_to_rasterize
-        );
-
-        scene_tx.send(SceneBuilderRequest::Transaction {
-            scene: scene_request,
-            blob_requests,
-            blob_rasterizer,
-            resource_updates: transaction_msg.resource_updates,
-            frame_ops: transaction_msg.frame_ops,
-            render: transaction_msg.generate_frame,
-            document_id,
-        }).unwrap();
-    }
-
-    fn render(
+    fn build_frame(
         &mut self,
         resource_cache: &mut ResourceCache,
         gpu_cache: &mut GpuCache,
         resource_profile: &mut ResourceProfileCounters,
         is_new_scene: bool,
     ) -> RenderedDocument {
         let accumulated_scale_factor = self.view.accumulated_scale_factor();
         let pan = self.view.pan.to_f32() / accumulated_scale_factor;
 
         let frame = {
             let frame_builder = self.frame_builder.as_mut().unwrap();
             let frame = frame_builder.build(
                 resource_cache,
                 gpu_cache,
                 self.frame_id,
                 &mut self.clip_scroll_tree,
-                &self.current.scene.pipelines,
+                &self.scene.pipelines,
                 accumulated_scale_factor,
                 self.view.layer,
                 pan,
                 &mut resource_profile.texture_cache,
                 &mut resource_profile.gpu_cache,
                 &self.dynamic_properties,
             );
             self.hit_tester = Some(frame_builder.create_hit_tester(&self.clip_scroll_tree));
@@ -337,19 +272,19 @@ impl Document {
 
         RenderedDocument {
             frame,
             is_new_scene,
         }
     }
 
     pub fn updated_pipeline_info(&mut self) -> PipelineInfo {
-        let removed_pipelines = replace(&mut self.current.removed_pipelines, Vec::new());
+        let removed_pipelines = replace(&mut self.removed_pipelines, Vec::new());
         PipelineInfo {
-            epochs: self.current.scene.pipeline_epochs.clone(),
+            epochs: self.scene.pipeline_epochs.clone(),
             removed_pipelines,
         }
     }
 
     pub fn discard_frame_state_for_pipeline(&mut self, pipeline_id: PipelineId) {
         self.clip_scroll_tree
             .discard_frame_state_for_pipeline(pipeline_id);
     }
@@ -372,68 +307,44 @@ impl Document {
     ) -> bool {
         self.clip_scroll_tree.scroll_node(origin, id, clamp)
     }
 
     pub fn get_scroll_node_state(&self) -> Vec<ScrollNodeState> {
         self.clip_scroll_tree.get_scroll_node_state()
     }
 
-    pub fn new_async_scene_ready(&mut self, mut built_scene: BuiltScene) {
-        self.current.scene = built_scene.scene;
+    pub fn new_async_scene_ready(&mut self, built_scene: BuiltScene) {
+        self.scene = built_scene.scene;
 
         self.frame_builder = Some(built_scene.frame_builder);
-        self.current.removed_pipelines.extend(built_scene.removed_pipelines.drain(..));
 
         let old_scrolling_states = self.clip_scroll_tree.drain();
         self.clip_scroll_tree = built_scene.clip_scroll_tree;
         self.clip_scroll_tree.finalize_and_apply_pending_scroll_offsets(old_scrolling_states);
 
         // Advance to the next frame.
         self.frame_id.0 += 1;
     }
 }
 
 struct DocumentOps {
     scroll: bool,
-    build: bool,
-    render: bool,
-    composite: bool,
+    build_frame: bool,
+    render_frame: bool,
 }
 
 impl DocumentOps {
     fn nop() -> Self {
         DocumentOps {
             scroll: false,
-            build: false,
-            render: false,
-            composite: false,
-        }
-    }
-
-    fn build() -> Self {
-        DocumentOps {
-            build: true,
-            ..DocumentOps::nop()
+            build_frame: false,
+            render_frame: false,
         }
     }
-
-    fn render() -> Self {
-        DocumentOps {
-            render: true,
-            ..DocumentOps::nop()
-        }
-    }
-
-    fn combine(&mut self, other: Self) {
-        self.scroll = self.scroll || other.scroll;
-        self.build = self.build || other.build;
-        self.render = self.render || other.render;
-        self.composite = self.composite || other.composite;
-    }
 }
 
 /// The unique id for WR resource identification.
 static NEXT_NAMESPACE_ID: AtomicUsize = ATOMIC_USIZE_INIT;
 
 #[cfg(any(feature = "capture", feature = "replay"))]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
@@ -450,16 +361,17 @@ struct PlainRenderBackend {
 /// GPU-friendly work which is then submitted to the renderer in the form of a frame::Frame.
 ///
 /// The render backend operates on its own thread.
 pub struct RenderBackend {
     api_rx: MsgReceiver<ApiMsg>,
     payload_rx: Receiver<Payload>,
     result_tx: Sender<ResultMsg>,
     scene_tx: Sender<SceneBuilderRequest>,
+    low_priority_scene_tx: Sender<SceneBuilderRequest>,
     scene_rx: Receiver<SceneBuilderResult>,
 
     payload_buffer: Vec<Payload>,
 
     default_device_pixel_ratio: f32,
 
     gpu_cache: GpuCache,
     resource_cache: ResourceCache,
@@ -476,16 +388,17 @@ pub struct RenderBackend {
 }
 
 impl RenderBackend {
     pub fn new(
         api_rx: MsgReceiver<ApiMsg>,
         payload_rx: Receiver<Payload>,
         result_tx: Sender<ResultMsg>,
         scene_tx: Sender<SceneBuilderRequest>,
+        low_priority_scene_tx: Sender<SceneBuilderRequest>,
         scene_rx: Receiver<SceneBuilderResult>,
         default_device_pixel_ratio: f32,
         resource_cache: ResourceCache,
         notifier: Box<RenderNotifier>,
         frame_config: FrameBuilderConfig,
         recorder: Option<Box<ApiRecordingReceiver>>,
         sampler: Option<Box<AsyncPropertySampler + Send>>,
         enable_render_on_scroll: bool,
@@ -493,16 +406,17 @@ impl RenderBackend {
         // The namespace_id should start from 1.
         NEXT_NAMESPACE_ID.fetch_add(1, Ordering::Relaxed);
 
         RenderBackend {
             api_rx,
             payload_rx,
             result_tx,
             scene_tx,
+            low_priority_scene_tx,
             scene_rx,
             payload_buffer: Vec::new(),
             default_device_pixel_ratio,
             resource_cache,
             gpu_cache: GpuCache::new(),
             frame_config,
             documents: FastHashMap::default(),
             notifier,
@@ -513,43 +427,39 @@ impl RenderBackend {
         }
     }
 
     fn process_scene_msg(
         &mut self,
         document_id: DocumentId,
         message: SceneMsg,
         frame_counter: u32,
+        txn: &mut Transaction,
         ipc_profile_counters: &mut IpcProfileCounters,
-    ) -> DocumentOps {
+    ) {
         let doc = self.documents.get_mut(&document_id).expect("No document?");
 
         match message {
             SceneMsg::UpdateEpoch(pipeline_id, epoch) => {
-                doc.pending.scene.update_epoch(pipeline_id, epoch);
-
-                DocumentOps::nop()
+                txn.epoch_updates.push((pipeline_id, epoch));
             }
             SceneMsg::SetPageZoom(factor) => {
                 doc.view.page_zoom_factor = factor.get();
-                DocumentOps::nop()
             }
             SceneMsg::SetPinchZoom(factor) => {
                 doc.view.pinch_zoom_factor = factor.get();
-                DocumentOps::nop()
             }
             SceneMsg::SetWindowParameters {
                 window_size,
                 inner_rect,
                 device_pixel_ratio,
             } => {
                 doc.view.window_size = window_size;
                 doc.view.inner_rect = inner_rect;
                 doc.view.device_pixel_ratio = device_pixel_ratio;
-                DocumentOps::nop()
             }
             SceneMsg::SetDisplayList {
                 epoch,
                 pipeline_id,
                 background,
                 viewport_size,
                 content_size,
                 list_descriptor,
@@ -583,26 +493,24 @@ impl RenderBackend {
                     doc.discard_frame_state_for_pipeline(pipeline_id);
                 }
 
                 let display_list_len = built_display_list.data().len();
                 let (builder_start_time, builder_finish_time, send_start_time) =
                     built_display_list.times();
                 let display_list_received_time = precise_time_ns();
 
-                {
-                    doc.pending.scene.set_display_list(
-                        pipeline_id,
-                        epoch,
-                        built_display_list,
-                        background,
-                        viewport_size,
-                        content_size,
-                    );
-                }
+                txn.display_list_updates.push(DisplayListUpdate {
+                    built_display_list,
+                    pipeline_id,
+                    epoch,
+                    background,
+                    viewport_size,
+                    content_size,
+                });
 
                 if let Some(ref mut ros) = doc.render_on_scroll {
                     *ros = false; //wait for `GenerateFrame`
                 }
 
                 // Note: this isn't quite right as auxiliary values will be
                 // pulled out somewhere in the prim_store, but aux values are
                 // really simple and cheap to access, so it's not a big deal.
@@ -611,35 +519,26 @@ impl RenderBackend {
                 ipc_profile_counters.set(
                     builder_start_time,
                     builder_finish_time,
                     send_start_time,
                     display_list_received_time,
                     display_list_consumed_time,
                     display_list_len,
                 );
-
-                DocumentOps::build()
             }
             SceneMsg::SetRootPipeline(pipeline_id) => {
                 profile_scope!("SetRootPipeline");
 
-                doc.pending.scene.set_root_pipeline_id(pipeline_id);
-                if doc.pending.scene.pipelines.get(&pipeline_id).is_some() {
-                    DocumentOps::build()
-                } else {
-                    DocumentOps::nop()
-                }
+                txn.set_root_pipeline = Some(pipeline_id);
             }
             SceneMsg::RemovePipeline(pipeline_id) => {
                 profile_scope!("RemovePipeline");
 
-                doc.pending.scene.remove_pipeline(pipeline_id);
-                doc.pending.removed_pipelines.push(pipeline_id);
-                DocumentOps::nop()
+                txn.removed_pipelines.push(pipeline_id);
             }
         }
     }
 
     fn next_namespace_id(&self) -> IdNamespace {
         IdNamespace(NEXT_NAMESPACE_ID.fetch_add(1, Ordering::Relaxed) as u32)
     }
 
@@ -657,35 +556,26 @@ impl RenderBackend {
             sampler.register();
         }
 
         while keep_going {
             profile_scope!("handle_msg");
 
             while let Ok(msg) = self.scene_rx.try_recv() {
                 match msg {
-                    SceneBuilderResult::Transaction {
-                        document_id,
-                        mut built_scene,
-                        resource_updates,
-                        frame_ops,
-                        render,
-                        result_tx,
-                        rasterized_blobs,
-                        blob_rasterizer,
-                    } => {
-                        let mut ops = DocumentOps::nop();
-                        if let Some(doc) = self.documents.get_mut(&document_id) {
-                            if let Some(mut built_scene) = built_scene.take() {
+                    SceneBuilderResult::Transaction(mut txn, result_tx) => {
+                        let has_built_scene = txn.built_scene.is_some();
+                        if let Some(doc) = self.documents.get_mut(&txn.document_id) {
+
+                            doc.removed_pipelines.append(&mut txn.removed_pipelines);
+
+                            if let Some(mut built_scene) = txn.built_scene.take() {
                                 doc.new_async_scene_ready(built_scene);
-                                // After applying the new scene we need to
-                                // rebuild the hit-tester, so we trigger a render
-                                // step.
-                                ops = DocumentOps::render();
                             }
+
                             if let Some(tx) = result_tx {
                                 let (resume_tx, resume_rx) = channel();
                                 tx.send(SceneSwapResult::Complete(resume_tx)).unwrap();
                                 // Block until the post-swap hook has completed on
                                 // the scene builder thread. We need to do this before
                                 // we can sample from the sampler hook which might happen
                                 // in the update_document call below.
                                 resume_rx.recv().ok();
@@ -695,38 +585,33 @@ impl RenderBackend {
                             // TODO: we might want to just ensure that removed documents are
                             // always forwarded to the scene builder thread to avoid this case.
                             if let Some(tx) = result_tx {
                                 tx.send(SceneSwapResult::Aborted).unwrap();
                             }
                             continue;
                         }
 
-                        let transaction_msg = TransactionMsg {
-                            scene_ops: Vec::new(),
-                            frame_ops,
-                            resource_updates,
-                            generate_frame: render,
-                            use_scene_builder_thread: false,
-                        };
-
-                        self.resource_cache.add_rasterized_blob_images(rasterized_blobs);
-                        if let Some(rasterizer) = blob_rasterizer {
+                        self.resource_cache.add_rasterized_blob_images(
+                            replace(&mut txn.rasterized_blobs, Vec::new())
+                        );
+                        if let Some(rasterizer) = txn.blob_rasterizer.take() {
                             self.resource_cache.set_blob_rasterizer(rasterizer);
                         }
 
-                        if !transaction_msg.is_empty() || ops.render {
+                        if txn.build_frame || !txn.resource_updates.is_empty() || !txn.frame_ops.is_empty() {
                             self.update_document(
-                                document_id,
-                                transaction_msg,
-                                &[],
+                            txn.document_id,
+                                replace(&mut txn.resource_updates, Vec::new()),
+                                replace(&mut txn.frame_ops, Vec::new()),
+                                txn.build_frame,
+                                txn.render_frame,
                                 &mut frame_counter,
                                 &mut profile_counters,
-                                ops,
-                                true,
+                                has_built_scene,
                             );
                         }
                     },
                     SceneBuilderResult::FlushComplete(tx) => {
                         tx.send(()).ok();
                     }
                     SceneBuilderResult::Stopped => {
                         panic!("We haven't sent a Stop yet, how did we get a Stopped back?");
@@ -740,17 +625,17 @@ impl RenderBackend {
                         r.write_msg(frame_counter, &msg);
                     }
                     self.process_api_msg(msg, &mut profile_counters, &mut frame_counter)
                 }
                 Err(..) => { false }
             };
         }
 
-        let _ = self.scene_tx.send(SceneBuilderRequest::Stop);
+        let _ = self.low_priority_scene_tx.send(SceneBuilderRequest::Stop);
         // Ensure we read everything the scene builder is sending us from
         // inflight messages, otherwise the scene builder might panic.
         while let Ok(msg) = self.scene_rx.recv() {
             match msg {
                 SceneBuilderResult::FlushComplete(tx) => {
                     // If somebody's blocked waiting for a flush, how did they
                     // trigger the RB thread to shut down? This shouldn't happen
                     // but handle it gracefully anyway.
@@ -777,17 +662,17 @@ impl RenderBackend {
         frame_counter: &mut u32,
     ) -> bool {
         match msg {
             ApiMsg::WakeUp => {}
             ApiMsg::WakeSceneBuilder => {
                 self.scene_tx.send(SceneBuilderRequest::WakeUp).unwrap();
             }
             ApiMsg::FlushSceneBuilder(tx) => {
-                self.scene_tx.send(SceneBuilderRequest::Flush(tx)).unwrap();
+                self.low_priority_scene_tx.send(SceneBuilderRequest::Flush(tx)).unwrap();
             }
             ApiMsg::UpdateResources(mut updates) => {
                 self.resource_cache.pre_scene_building_update(
                     &mut updates,
                     &mut profile_counters.resources
                 );
                 self.resource_cache.post_scene_building_update(
                     updates,
@@ -812,26 +697,28 @@ impl RenderBackend {
                 }
                 tx.send(glyph_indices).unwrap();
             }
             ApiMsg::CloneApi(sender) => {
                 sender.send(self.next_namespace_id()).unwrap();
             }
             ApiMsg::AddDocument(document_id, initial_size, layer) => {
                 let document = Document::new(
-                    self.frame_config.clone(),
                     initial_size,
                     layer,
                     self.enable_render_on_scroll,
                     self.default_device_pixel_ratio,
                 );
                 self.documents.insert(document_id, document);
             }
             ApiMsg::DeleteDocument(document_id) => {
                 self.documents.remove(&document_id);
+                self.low_priority_scene_tx.send(
+                    SceneBuilderRequest::DeleteDocument(document_id)
+                ).unwrap();
             }
             ApiMsg::ExternalEvent(evt) => {
                 self.notifier.external_event(evt);
             }
             ApiMsg::ClearNamespace(namespace_id) => {
                 self.resource_cache.clear_namespace(namespace_id);
                 let document_ids = self.documents
                     .keys()
@@ -864,22 +751,17 @@ impl RenderBackend {
             ApiMsg::DebugCommand(option) => {
                 let msg = match option {
                     DebugCommand::EnableDualSourceBlending(enable) => {
                         // Set in the config used for any future documents
                         // that are created.
                         self.frame_config
                             .dual_source_blending_is_enabled = enable;
 
-                        // Set for any existing documents.
-                        for (_, doc) in &mut self.documents {
-                            doc.frame_builder_config.dual_source_blending_is_enabled = enable;
-                        }
-
-                        self.scene_tx.send(SceneBuilderRequest::SetFrameBuilderConfig(
+                        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 true;
                     }
                     DebugCommand::FetchDocuments => {
                         let json = self.get_docs_for_debugger();
@@ -899,17 +781,17 @@ impl RenderBackend {
                         NEXT_NAMESPACE_ID.fetch_add(1, Ordering::Relaxed);
                         *frame_counter += 1;
 
                         self.load_capture(&root, profile_counters);
 
                         for (id, doc) in &self.documents {
                             let captured = CapturedDocument {
                                 document_id: *id,
-                                root_pipeline_id: doc.current.scene.root_pipeline_id,
+                                root_pipeline_id: doc.scene.root_pipeline_id,
                                 window_size: doc.view.window_size,
                             };
                             tx.send(captured).unwrap();
                         }
                         // Note: we can't pass `LoadCapture` here since it needs to arrive
                         // before the `PublishDocument` messages sent by `load_capture`.
                         return true;
                     }
@@ -920,157 +802,206 @@ impl RenderBackend {
                     _ => ResultMsg::DebugCommand(option),
                 };
                 self.result_tx.send(msg).unwrap();
                 self.notifier.wake_up();
             }
             ApiMsg::ShutDown => {
                 return false;
             }
-            ApiMsg::UpdateDocument(document_id, mut doc_msgs) => {
-                let blob_requests = get_blob_image_updates(&doc_msgs.resource_updates);
-
-                self.resource_cache.pre_scene_building_update(
-                    &mut doc_msgs.resource_updates,
-                    &mut profile_counters.resources,
-                );
-
-                self.update_document(
+            ApiMsg::UpdateDocument(document_id, transaction_msg) => {
+                self.prepare_transaction(
                     document_id,
-                    doc_msgs,
-                    &blob_requests,
+                    transaction_msg,
                     frame_counter,
                     profile_counters,
-                    DocumentOps::nop(),
-                    false,
-                )
+                );
             }
         }
 
         true
     }
 
-    fn update_document(
+    fn prepare_transaction(
         &mut self,
         document_id: DocumentId,
         mut transaction_msg: TransactionMsg,
-        blob_requests: &[ImageKey],
         frame_counter: &mut u32,
         profile_counters: &mut BackendProfileCounters,
-        initial_op: DocumentOps,
-        has_built_scene: bool,
     ) {
-        let mut op = initial_op;
+        let mut txn = Box::new(Transaction {
+            document_id,
+            display_list_updates: Vec::new(),
+            removed_pipelines: Vec::new(),
+            epoch_updates: Vec::new(),
+            request_scene_build: None,
+            blob_rasterizer: None,
+            blob_requests: Vec::new(),
+            resource_updates: transaction_msg.resource_updates,
+            frame_ops: transaction_msg.frame_ops,
+            rasterized_blobs: Vec::new(),
+            set_root_pipeline: None,
+            build_frame: transaction_msg.generate_frame,
+            render_frame: transaction_msg.generate_frame,
+        });
 
-        if !blob_requests.is_empty() {
-            transaction_msg.use_scene_builder_thread = true;
-        }
+        self.resource_cache.pre_scene_building_update(
+            &mut txn.resource_updates,
+            &mut profile_counters.resources,
+        );
 
         for scene_msg in transaction_msg.scene_ops.drain(..) {
             let _timer = profile_counters.total_time.timer();
-            op.combine(
-                self.process_scene_msg(
-                    document_id,
-                    scene_msg,
-                    *frame_counter,
-                    &mut profile_counters.ipc,
-                )
-            );
+            self.process_scene_msg(
+                document_id,
+                scene_msg,
+                *frame_counter,
+                &mut txn,
+                &mut profile_counters.ipc,
+            )
         }
 
-        if !has_built_scene && (op.build || transaction_msg.use_scene_builder_thread) {
-            let scene_id = self.make_unique_scene_id();
-            let doc = self.documents.get_mut(&document_id).unwrap();
+        let blobs_to_rasterize = get_blob_image_updates(&txn.resource_updates);
+        if !blobs_to_rasterize.is_empty() {
+            let (blob_rasterizer, blob_requests) = self.resource_cache
+                .create_blob_scene_builder_requests(&blobs_to_rasterize);
+
+            txn.blob_requests = blob_requests;
+            txn.blob_rasterizer = blob_rasterizer;
+        }
 
-            doc.forward_transaction_to_scene_builder(
-                transaction_msg,
-                blob_requests,
-                &op,
-                document_id,
-                scene_id,
-                &mut self.resource_cache,
-                &self.scene_tx,
+        if !transaction_msg.use_scene_builder_thread && txn.can_skip_scene_builder() {
+            self.update_document(
+                txn.document_id,
+                replace(&mut txn.resource_updates, Vec::new()),
+                replace(&mut txn.frame_ops, Vec::new()),
+                txn.build_frame,
+                txn.render_frame,
+                frame_counter,
+                profile_counters,
+                false
             );
 
             return;
         }
 
+        let scene_id = self.make_unique_scene_id();
+        let doc = self.documents.get_mut(&document_id).unwrap();
+
+        if txn.should_build_scene() {
+            txn.request_scene_build = Some(SceneRequest {
+                view: doc.view.clone(),
+                font_instances: self.resource_cache.get_font_instances(),
+                output_pipelines: doc.output_pipelines.clone(),
+                scene_id,
+            });
+        }
+
+        let tx = if transaction_msg.low_priority {
+            &self.low_priority_scene_tx
+        } else {
+            &self.scene_tx
+        };
+
+        tx.send(SceneBuilderRequest::Transaction(txn)).unwrap();
+    }
+
+    fn update_document(
+        &mut self,
+        document_id: DocumentId,
+        resource_updates: Vec<ResourceUpdate>,
+        mut frame_ops: Vec<FrameMsg>,
+        mut build_frame: bool,
+        mut render_frame: bool,
+        frame_counter: &mut u32,
+        profile_counters: &mut BackendProfileCounters,
+        has_built_scene: bool,
+    ) {
         self.resource_cache.post_scene_building_update(
-            transaction_msg.resource_updates,
+            resource_updates,
             &mut profile_counters.resources,
         );
 
         // If we have a sampler, get more frame ops from it and add them
         // to the transaction. This is a hook to allow the WR user code to
         // fiddle with things after a potentially long scene build, but just
         // before rendering. This is useful for rendering with the latest
         // async transforms.
-        if op.render || transaction_msg.generate_frame {
+        if build_frame {
             if let Some(ref sampler) = self.sampler {
-                transaction_msg.frame_ops.append(&mut sampler.sample());
+                frame_ops.append(&mut sampler.sample());
             }
         }
 
+
         let doc = self.documents.get_mut(&document_id).unwrap();
 
-        for frame_msg in transaction_msg.frame_ops {
+        let mut scroll = false;
+        for frame_msg in frame_ops {
             let _timer = profile_counters.total_time.timer();
-            op.combine(doc.process_frame_msg(frame_msg));
-        }
-
-        if doc.dynamic_properties.flush_pending_updates() {
-            op.render = true;
+            let op = doc.process_frame_msg(frame_msg);
+            build_frame |= op.build_frame;
+            render_frame |= op.render_frame;
+            scroll |= op.scroll;
         }
 
-        if transaction_msg.generate_frame {
+        // After applying the new scene we need to
+        // rebuild the hit-tester, so we trigger a frame generation
+        // step.
+        //
+        // TODO: We could avoid some the cost of building the frame by only
+        // building the information required for hit-testing (See #2807).
+        build_frame |= has_built_scene;
+
+        if doc.dynamic_properties.flush_pending_updates() {
+            build_frame = true;
+        }
+
+        if render_frame {
             if let Some(ref mut ros) = doc.render_on_scroll {
                 *ros = true;
             }
-
-            if doc.current.scene.root_pipeline_id.is_some() {
-                op.render = true;
-                op.composite = true;
-            }
         }
 
         if !doc.can_render() {
             // TODO: this happens if we are building the first scene asynchronously and
             // scroll at the same time. we should keep track of the fact that we skipped
             // composition here and do it as soon as we receive the scene.
-            op.render = false;
-            op.composite = false;
+            build_frame = false;
+            render_frame = false;
         }
 
-        debug_assert!(op.render || !op.composite);
+        // If we don't generate a frame it makes no sense to render.
+        debug_assert!(build_frame || !render_frame);
 
-        let mut render_time = None;
-        if op.render && doc.has_pixels() {
+        let mut frame_build_time = None;
+        if build_frame && doc.has_pixels() {
             profile_scope!("generate frame");
 
             *frame_counter += 1;
 
             // borrow ck hack for profile_counters
             let (pending_update, rendered_document) = {
                 let _timer = profile_counters.total_time.timer();
-                let render_start_time = precise_time_ns();
+                let frame_build_start_time = precise_time_ns();
 
-                let rendered_document = doc.render(
+                let rendered_document = doc.build_frame(
                     &mut self.resource_cache,
                     &mut self.gpu_cache,
                     &mut profile_counters.resources,
                     has_built_scene,
                 );
 
                 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();
 
-                render_time = Some(precise_time_ns() - render_start_time);
+                frame_build_time = Some(precise_time_ns() - frame_build_start_time);
 
                 let pending_update = self.resource_cache.pending_updates();
                 (pending_update, rendered_document)
             };
 
             let msg = ResultMsg::PublishPipelineInfo(doc.updated_pipeline_info());
             self.result_tx.send(msg).unwrap();
 
@@ -1078,27 +1009,27 @@ impl RenderBackend {
             let msg = ResultMsg::PublishDocument(
                 document_id,
                 rendered_document,
                 pending_update,
                 profile_counters.clone()
             );
             self.result_tx.send(msg).unwrap();
             profile_counters.reset();
-        } else if op.render {
+        } else if build_frame {
             // WR-internal optimization to avoid doing a bunch of render work if
             // there's no pixels. We still want to pretend to render and request
-            // a composite to make sure that the callbacks (particularly the
+            // a render to make sure that the callbacks (particularly the
             // new_frame_ready callback below) has the right flags.
             let msg = ResultMsg::PublishPipelineInfo(doc.updated_pipeline_info());
             self.result_tx.send(msg).unwrap();
         }
 
-        if transaction_msg.generate_frame {
-            self.notifier.new_frame_ready(document_id, op.scroll, op.composite, render_time);
+        if render_frame {
+            self.notifier.new_frame_ready(document_id, scroll, render_frame, frame_build_time);
         }
     }
 
     #[cfg(not(feature = "debugger"))]
     fn get_docs_for_debugger(&self) -> String {
         String::new()
     }
 
@@ -1144,17 +1075,17 @@ impl RenderBackend {
 
     #[cfg(feature = "debugger")]
     fn get_docs_for_debugger(&self) -> String {
         let mut docs = debug_server::DocumentList::new();
 
         for (_, doc) in &self.documents {
             let mut debug_doc = debug_server::TreeNode::new("document");
 
-            for (_, pipeline) in &doc.current.scene.pipelines {
+            for (_, pipeline) in &doc.scene.pipelines {
                 let mut debug_dl = debug_server::TreeNode::new("display-list");
                 self.traverse_items(&mut pipeline.display_list.iter(), &mut debug_dl);
                 debug_doc.add_child(debug_dl);
             }
 
             docs.add(debug_doc);
         }
 
@@ -1262,20 +1193,20 @@ impl RenderBackend {
             }
         }
         let config = CaptureConfig::new(root, bits);
 
         for (&id, doc) in &mut self.documents {
             debug!("\tdocument {:?}", id);
             if config.bits.contains(CaptureBits::SCENE) {
                 let file_name = format!("scene-{}-{}", (id.0).0, id.1);
-                config.serialize(&doc.current.scene, file_name);
+                config.serialize(&doc.scene, file_name);
             }
             if config.bits.contains(CaptureBits::FRAME) {
-                let rendered_document = doc.render(
+                let rendered_document = doc.build_frame(
                     &mut self.resource_cache,
                     &mut self.gpu_cache,
                     &mut profile_counters.resources,
                     true,
                 );
                 //TODO: write down doc's pipeline info?
                 // it has `pipeline_epoch_map`,
                 // which may capture necessary details for some cases.
@@ -1352,79 +1283,81 @@ impl RenderBackend {
             None => GpuCache::new(),
         };
 
         self.documents.clear();
         self.default_device_pixel_ratio = backend.default_device_pixel_ratio;
         self.frame_config = backend.frame_config;
         self.enable_render_on_scroll = backend.enable_render_on_scroll;
 
+        let mut scenes_to_build = Vec::new();
+
         let mut last_scene_id = backend.last_scene_id;
         for (id, view) in backend.documents {
             debug!("\tdocument {:?}", id);
             let scene_name = format!("scene-{}-{}", (id.0).0, id.1);
             let scene = CaptureConfig::deserialize::<Scene, _>(root, &scene_name)
                 .expect(&format!("Unable to open {}.ron", scene_name));
 
             let mut doc = Document {
-                current: SceneData {
-                    scene: scene.clone(),
-                    removed_pipelines: Vec::new(),
-                },
-                pending: SceneData {
-                    scene,
-                    removed_pipelines: Vec::new(),
-                },
+                scene: scene.clone(),
+                removed_pipelines: Vec::new(),
                 view: view.clone(),
                 clip_scroll_tree: ClipScrollTree::new(),
                 frame_id: FrameId(0),
-                frame_builder_config: self.frame_config.clone(),
                 frame_builder: Some(FrameBuilder::empty()),
                 output_pipelines: FastHashSet::default(),
                 render_on_scroll: None,
                 dynamic_properties: SceneProperties::new(),
                 hit_tester: None,
             };
 
             let frame_name = format!("frame-{}-{}", (id.0).0, id.1);
-            let render_doc = match CaptureConfig::deserialize::<Frame, _>(root, frame_name) {
+            let frame = CaptureConfig::deserialize::<Frame, _>(root, frame_name);
+            let build_frame = match frame {
                 Some(frame) => {
                     info!("\tloaded a built frame with {} passes", frame.passes.len());
-                    RenderedDocument { frame, is_new_scene: true }
+
+                    let msg_update = ResultMsg::UpdateGpuCache(self.gpu_cache.extract_updates());
+                    self.result_tx.send(msg_update).unwrap();
+
+                    let msg_publish = ResultMsg::PublishDocument(
+                        id,
+                        RenderedDocument { frame, is_new_scene: true },
+                        self.resource_cache.pending_updates(),
+                        profile_counters.clone(),
+                    );
+                    self.result_tx.send(msg_publish).unwrap();
+                    profile_counters.reset();
+
+                    self.notifier.new_frame_ready(id, false, true, None);
+
+                    // We deserialized the state of the frame so we don't want to build
+                    // it (but we do want to update the scene builder's state)
+                    false
                 }
-                None => {
-                    last_scene_id += 1;
-                    let built_scene = build_scene(&self.frame_config, SceneRequest {
-                        scene: doc.pending.scene.clone(),
-                        view,
-                        font_instances: self.resource_cache.get_font_instances(),
-                        output_pipelines: doc.output_pipelines.clone(),
-                        removed_pipelines: Vec::new(),
-                        scene_id: last_scene_id,
-                    });
-                    doc.new_async_scene_ready(built_scene);
-                    doc.render(
-                        &mut self.resource_cache,
-                        &mut self.gpu_cache,
-                        &mut profile_counters.resources,
-                        true,
-                    )
-                }
+                None => true,
             };
 
-            let msg_update = ResultMsg::UpdateGpuCache(self.gpu_cache.extract_updates());
-            self.result_tx.send(msg_update).unwrap();
+            last_scene_id += 1;
 
-            let msg_publish = ResultMsg::PublishDocument(
-                id,
-                render_doc,
-                self.resource_cache.pending_updates(),
-                profile_counters.clone(),
-            );
-            self.result_tx.send(msg_publish).unwrap();
-            profile_counters.reset();
+            scenes_to_build.push(LoadScene {
+                document_id: id,
+                scene: doc.scene.clone(),
+                view: view.clone(),
+                config: self.frame_config.clone(),
+                output_pipelines: doc.output_pipelines.clone(),
+                font_instances: self.resource_cache.get_font_instances(),
+                scene_id: last_scene_id,
+                build_frame,
+            });
 
-            self.notifier.new_frame_ready(id, false, true, None);
             self.documents.insert(id, doc);
         }
+
+        if !scenes_to_build.is_empty() {
+            self.low_priority_scene_tx.send(
+                SceneBuilderRequest::LoadScenes(scenes_to_build)
+            ).unwrap();
+        }
     }
 }
 
--- a/gfx/webrender/src/renderer.rs
+++ b/gfx/webrender/src/renderer.rs
@@ -7,16 +7,17 @@
 //! The `webrender::renderer` module provides the interface to webrender, which
 //! is accessible through [`Renderer`][renderer]
 //!
 //! [renderer]: struct.Renderer.html
 
 use api::{BlobImageHandler, ColorF, DeviceIntPoint, DeviceIntRect, DeviceIntSize};
 use api::{DeviceUintPoint, DeviceUintRect, DeviceUintSize, DocumentId, Epoch, ExternalImageId};
 use api::{ExternalImageType, FontRenderMode, FrameMsg, ImageFormat, PipelineId};
+use api::{ImageRendering};
 use api::{RenderApiSender, RenderNotifier, TexelRect, TextureTarget};
 use api::{channel};
 use api::DebugCommand;
 use api::channel::PayloadReceiverHelperMethods;
 use batch::{BatchKind, BatchTextures, BrushBatchKind};
 #[cfg(any(feature = "capture", feature = "replay"))]
 use capture::{CaptureConfig, ExternalCaptureImage, PlainExternalImage};
 use debug_colors;
@@ -38,17 +39,17 @@ use internal_types::{TextureUpdateList, 
 use internal_types::{RenderTargetInfo, SavedTargetIndex};
 use prim_store::DeferredResolve;
 use profiler::{BackendProfileCounters, FrameProfileCounters,
                GpuProfileTag, RendererProfileCounters, RendererProfileTimers};
 use device::query::GpuProfiler;
 use rayon::{ThreadPool, ThreadPoolBuilder};
 use record::ApiRecordingReceiver;
 use render_backend::RenderBackend;
-use scene_builder::SceneBuilder;
+use scene_builder::{SceneBuilder, LowPrioritySceneBuilder};
 use shade::Shaders;
 use render_task::{RenderTask, RenderTaskKind, RenderTaskTree};
 use resource_cache::ResourceCache;
 
 use std;
 use std::cmp;
 use std::collections::VecDeque;
 use std::collections::hash_map::Entry;
@@ -1708,19 +1709,21 @@ impl Renderer {
                 Arc::new(worker.unwrap())
             });
         let sampler = options.sampler;
         let enable_render_on_scroll = options.enable_render_on_scroll;
 
         let blob_image_handler = options.blob_image_handler.take();
         let thread_listener_for_render_backend = thread_listener.clone();
         let thread_listener_for_scene_builder = thread_listener.clone();
+        let thread_listener_for_lp_scene_builder = thread_listener.clone();
         let scene_builder_hooks = options.scene_builder_hooks;
         let rb_thread_name = format!("WRRenderBackend#{}", options.renderer_id.unwrap_or(0));
         let scene_thread_name = format!("WRSceneBuilder#{}", options.renderer_id.unwrap_or(0));
+        let lp_scene_thread_name = format!("WRSceneBuilderLP#{}", options.renderer_id.unwrap_or(0));
         let glyph_rasterizer = GlyphRasterizer::new(workers)?;
 
         let (scene_builder, scene_tx, scene_rx) = SceneBuilder::new(
             config,
             api_tx.clone(),
             scene_builder_hooks);
         thread::Builder::new().name(scene_thread_name.clone()).spawn(move || {
             register_thread_with_profiler(scene_thread_name.clone());
@@ -1731,16 +1734,42 @@ impl Renderer {
             let mut scene_builder = scene_builder;
             scene_builder.run();
 
             if let Some(ref thread_listener) = *thread_listener_for_scene_builder {
                 thread_listener.thread_stopped(&scene_thread_name);
             }
         })?;
 
+        let low_priority_scene_tx = if options.support_low_priority_transactions {
+            let (low_priority_scene_tx, low_priority_scene_rx) = channel();
+            let lp_builder = LowPrioritySceneBuilder {
+                rx: low_priority_scene_rx,
+                tx: scene_tx.clone(),
+            };
+
+            thread::Builder::new().name(lp_scene_thread_name.clone()).spawn(move || {
+                register_thread_with_profiler(lp_scene_thread_name.clone());
+                if let Some(ref thread_listener) = *thread_listener_for_lp_scene_builder {
+                    thread_listener.thread_started(&lp_scene_thread_name);
+                }
+
+                let mut scene_builder = lp_builder;
+                scene_builder.run();
+
+                if let Some(ref thread_listener) = *thread_listener_for_lp_scene_builder {
+                    thread_listener.thread_stopped(&lp_scene_thread_name);
+                }
+            })?;
+
+            low_priority_scene_tx
+        } else {
+            scene_tx.clone()
+        };
+
         thread::Builder::new().name(rb_thread_name.clone()).spawn(move || {
             register_thread_with_profiler(rb_thread_name.clone());
             if let Some(ref thread_listener) = *thread_listener_for_render_backend {
                 thread_listener.thread_started(&rb_thread_name);
             }
 
             let texture_cache = TextureCache::new(max_device_size);
             let resource_cache = ResourceCache::new(
@@ -1749,16 +1778,17 @@ impl Renderer {
                 blob_image_handler,
             );
 
             let mut backend = RenderBackend::new(
                 api_rx,
                 payload_rx_for_backend,
                 result_tx,
                 scene_tx,
+                low_priority_scene_tx,
                 scene_rx,
                 device_pixel_ratio,
                 resource_cache,
                 backend_notifier,
                 config,
                 recorder,
                 sampler,
                 enable_render_on_scroll,
@@ -2596,17 +2626,18 @@ impl Renderer {
                                     rect, layer_index, stride,
                                     &data[offset as usize ..],
                                 )
                             }
                             TextureUpdateSource::External { id, channel_index } => {
                                 let handler = self.external_image_handler
                                     .as_mut()
                                     .expect("Found external image, but no handler set!");
-                                let size = match handler.lock(id, channel_index).source {
+                                // The filter is only relevant for NativeTexture external images.
+                                let size = match handler.lock(id, channel_index, ImageRendering::Auto).source {
                                     ExternalImageSource::RawData(data) => {
                                         uploader.upload(
                                             rect, layer_index, stride,
                                             &data[offset as usize ..],
                                         )
                                     }
                                     ExternalImageSource::Invalid => {
                                         // Create a local buffer to fill the pbo.
@@ -3361,30 +3392,53 @@ impl Renderer {
         for rect in &target.clears {
             self.device.clear_target(Some([0.0, 0.0, 0.0, 0.0]), None, Some(*rect));
         }
 
         // Handle any blits to this texture from child tasks.
         self.handle_blits(&target.blits, render_tasks);
 
         // Draw any borders for this target.
-        if !target.border_segments.is_empty() {
+        if !target.border_segments_solid.is_empty() ||
+           !target.border_segments_complex.is_empty() {
             let _timer = self.gpu_profile.start_timer(GPU_TAG_CACHE_BORDER);
-
+        }
+
+        if !target.border_segments_solid.is_empty() {
+            self.set_blend(true, FramebufferKind::Other);
+            self.set_blend_mode_premultiplied_alpha(FramebufferKind::Other);
+
+            self.shaders.cs_border_solid.bind(
+                &mut self.device,
+                &projection,
+                &mut self.renderer_errors,
+            );
+
+            self.draw_instanced_batch(
+                &target.border_segments_solid,
+                VertexArrayKind::Border,
+                &BatchTextures::no_texture(),
+                stats,
+            );
+
+            self.set_blend(false, FramebufferKind::Other);
+        }
+
+        if !target.border_segments_complex.is_empty() {
             self.set_blend(true, FramebufferKind::Other);
             self.set_blend_mode_premultiplied_alpha(FramebufferKind::Other);
 
             self.shaders.cs_border_segment.bind(
                 &mut self.device,
                 &projection,
                 &mut self.renderer_errors,
             );
 
             self.draw_instanced_batch(
-                &target.border_segments,
+                &target.border_segments_complex,
                 VertexArrayKind::Border,
                 &BatchTextures::no_texture(),
                 stats,
             );
 
             self.set_blend(false, FramebufferKind::Other);
         }
 
@@ -3448,17 +3502,18 @@ impl Renderer {
         };
 
         for deferred_resolve in deferred_resolves {
             self.gpu_profile.place_marker("deferred resolve");
             let props = &deferred_resolve.image_properties;
             let ext_image = props
                 .external_image
                 .expect("BUG: Deferred resolves must be external images!");
-            let image = handler.lock(ext_image.id, ext_image.channel_index);
+            // Provide rendering information for NativeTexture external images.
+            let image = handler.lock(ext_image.id, ext_image.channel_index, deferred_resolve.rendering);
             let texture_target = match ext_image.image_type {
                 ExternalImageType::TextureHandle(target) => target,
                 ExternalImageType::Buffer => {
                     panic!("not a suitable image type in update_deferred_resolves()");
                 }
             };
 
             // In order to produce the handle, the external image handler may call into
@@ -4100,18 +4155,18 @@ pub struct ExternalImage<'a> {
 /// external image buffers.
 /// When the the application passes an external image to WR, it should kepp that
 /// external image life time. People could check the epoch id in RenderNotifier
 /// at the client side to make sure that the external image is not used by WR.
 /// Then, do the clean up for that external image.
 pub trait ExternalImageHandler {
     /// Lock the external image. Then, WR could start to read the image content.
     /// The WR client should not change the image content until the unlock()
-    /// call.
-    fn lock(&mut self, key: ExternalImageId, channel_index: u8) -> ExternalImage;
+    /// call. Provide ImageRendering for NativeTexture external images.
+    fn lock(&mut self, key: ExternalImageId, channel_index: u8, rendering: ImageRendering) -> ExternalImage;
     /// Unlock the external image. The WR should not read the image content
     /// after this call.
     fn unlock(&mut self, key: ExternalImageId, channel_index: u8);
 }
 
 /// Allows callers to receive a texture with the contents of a specific
 /// pipeline copied to it. Lock should return the native texture handle
 /// and the size of the texture. Unlock will only be called if the lock()
@@ -4191,16 +4246,17 @@ pub struct RendererOptions {
     pub enable_render_on_scroll: bool,
     pub cached_programs: Option<Rc<ProgramCache>>,
     pub debug_flags: DebugFlags,
     pub renderer_id: Option<u64>,
     pub disable_dual_source_blending: bool,
     pub scene_builder_hooks: Option<Box<SceneBuilderHooks + Send>>,
     pub sampler: Option<Box<AsyncPropertySampler + Send>>,
     pub chase_primitive: ChasePrimitive,
+    pub support_low_priority_transactions: bool,
 }
 
 impl Default for RendererOptions {
     fn default() -> Self {
         RendererOptions {
             device_pixel_ratio: 1.0,
             resource_override_path: None,
             enable_aa: true,
@@ -4225,16 +4281,17 @@ impl Default for RendererOptions {
             thread_listener: None,
             enable_render_on_scroll: true,
             renderer_id: None,
             cached_programs: None,
             disable_dual_source_blending: false,
             scene_builder_hooks: None,
             sampler: None,
             chase_primitive: ChasePrimitive::Nothing,
+            support_low_priority_transactions: false,
         }
     }
 }
 
 #[cfg(not(feature = "debugger"))]
 pub struct DebugServer;
 
 #[cfg(not(feature = "debugger"))]
@@ -4298,17 +4355,17 @@ enum CapturedExternalImageData {
 
 #[cfg(feature = "replay")]
 struct DummyExternalImageHandler {
     data: FastHashMap<(ExternalImageId, u8), (CapturedExternalImageData, TexelRect)>,
 }
 
 #[cfg(feature = "replay")]
 impl ExternalImageHandler for DummyExternalImageHandler {
-    fn lock(&mut self, key: ExternalImageId, channel_index: u8) -> ExternalImage {
+    fn lock(&mut self, key: ExternalImageId, channel_index: u8, _rendering: ImageRendering) -> ExternalImage {
         let (ref captured_data, ref uv) = self.data[&(key, channel_index)];
         ExternalImage {
             uv: *uv,
             source: match *captured_data {
                 CapturedExternalImageData::NativeTexture(tid) => ExternalImageSource::NativeTexture(tid),
                 CapturedExternalImageData::Buffer(ref arc) => ExternalImageSource::RawData(&*arc),
             }
         }
@@ -4430,17 +4487,18 @@ impl Renderer {
             let mut arc_map = FastHashMap::<*const u8, String>::default();
             let mut tex_map = FastHashMap::<u32, String>::default();
             let handler = self.external_image_handler
                 .as_mut()
                 .expect("Unable to lock the external image handler!");
             for def in &deferred_images {
                 info!("\t{}", def.short_path);
                 let ExternalImageData { id, channel_index, image_type } = def.external;
-                let ext_image = handler.lock(id, channel_index);
+                // The image rendering parameter is irrelevant because no filtering happens during capturing.
+                let ext_image = handler.lock(id, channel_index, ImageRendering::Auto);
                 let (data, short_path) = match ext_image.source {
                     ExternalImageSource::RawData(data) => {
                         let arc_id = arc_map.len() + 1;
                         match arc_map.entry(data.as_ptr()) {
                             Entry::Occupied(e) => {
                                 (None, e.get().clone())
                             }
                             Entry::Vacant(e) => {
--- a/gfx/webrender/src/scene.rs
+++ b/gfx/webrender/src/scene.rs
@@ -183,16 +183,24 @@ impl Scene {
         }
         self.pipelines.remove(&pipeline_id);
         self.pipeline_epochs.remove(&pipeline_id);
     }
 
     pub fn update_epoch(&mut self, pipeline_id: PipelineId, epoch: Epoch) {
         self.pipeline_epochs.insert(pipeline_id, epoch);
     }
+
+    pub fn has_root_pipeline(&self) -> bool {
+        if let Some(ref root_id) = self.root_pipeline_id {
+            return self.pipelines.contains_key(root_id);
+        }
+
+        false
+    }
 }
 
 /// An arbitrary number which we assume opacity is invisible below.
 pub const OPACITY_EPSILON: f32 = 0.001;
 
 pub trait FilterOpHelpers {
     fn is_visible(&self) -> bool;
     fn is_noop(&self) -> bool;
--- a/gfx/webrender/src/scene_builder.rs
+++ b/gfx/webrender/src/scene_builder.rs
@@ -1,85 +1,141 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 use api::{AsyncBlobImageRasterizer, BlobImageRequest, BlobImageParams, BlobImageResult};
-use api::{DocumentId, PipelineId, ApiMsg, FrameMsg, ResourceUpdate};
+use api::{DocumentId, PipelineId, ApiMsg, FrameMsg, ResourceUpdate, Epoch};
+use api::{BuiltDisplayList, ColorF, LayoutSize};
 use api::channel::MsgSender;
-use display_list_flattener::build_scene;
 use frame_builder::{FrameBuilderConfig, FrameBuilder};
 use clip_scroll_tree::ClipScrollTree;
-use internal_types::FastHashSet;
+use display_list_flattener::DisplayListFlattener;
+use internal_types::{FastHashMap, FastHashSet};
 use resource_cache::FontInstanceMap;
 use render_backend::DocumentView;
 use renderer::{PipelineInfo, SceneBuilderHooks};
 use scene::Scene;
 use std::sync::mpsc::{channel, Receiver, Sender};
+use std::mem::replace;
 use time::precise_time_ns;
 
+/// Represents the work associated to a transaction before scene building.
+pub struct Transaction {
+    pub document_id: DocumentId,
+    pub display_list_updates: Vec<DisplayListUpdate>,
+    pub removed_pipelines: Vec<PipelineId>,
+    pub epoch_updates: Vec<(PipelineId, Epoch)>,
+    pub request_scene_build: Option<SceneRequest>,
+    pub blob_requests: Vec<BlobImageParams>,
+    pub blob_rasterizer: Option<Box<AsyncBlobImageRasterizer>>,
+    pub rasterized_blobs: Vec<(BlobImageRequest, BlobImageResult)>,
+    pub resource_updates: Vec<ResourceUpdate>,
+    pub frame_ops: Vec<FrameMsg>,
+    pub set_root_pipeline: Option<PipelineId>,
+    pub build_frame: bool,
+    pub render_frame: bool,
+}
+
+impl Transaction {
+    pub fn can_skip_scene_builder(&self) -> bool {
+        self.request_scene_build.is_none() &&
+            self.display_list_updates.is_empty() &&
+            self.epoch_updates.is_empty() &&
+            self.removed_pipelines.is_empty() &&
+            self.blob_requests.is_empty() &&
+            self.set_root_pipeline.is_none()
+    }
+
+    pub fn should_build_scene(&self) -> bool {
+        !self.display_list_updates.is_empty() ||
+            self.set_root_pipeline.is_some()
+    }
+}
+
+/// Represent the remaining work associated to a transaction after the scene building
+/// phase as well as the result of scene building itself if applicable.
+pub struct BuiltTransaction {
+    pub document_id: DocumentId,
+    pub built_scene: Option<BuiltScene>,
+    pub resource_updates: Vec<ResourceUpdate>,
+    pub rasterized_blobs: Vec<(BlobImageRequest, BlobImageResult)>,
+    pub blob_rasterizer: Option<Box<AsyncBlobImageRasterizer>>,
+    pub frame_ops: Vec<FrameMsg>,
+    pub removed_pipelines: Vec<PipelineId>,
+    pub scene_build_start_time: u64,
+    pub scene_build_end_time: u64,
+    pub build_frame: bool,
+    pub render_frame: bool,
+}
+
+pub struct DisplayListUpdate {
+    pub pipeline_id: PipelineId,
+    pub epoch: Epoch,
+    pub built_display_list: BuiltDisplayList,
+    pub background: Option<ColorF>,
+    pub viewport_size: LayoutSize,
+    pub content_size: LayoutSize,
+}
+
+/// Contains the render backend data needed to build a scene.
+pub struct SceneRequest {
+    pub view: DocumentView,
+    pub font_instances: FontInstanceMap,
+    pub output_pipelines: FastHashSet<PipelineId>,
+    pub scene_id: u64,
+}
+
+#[cfg(feature = "replay")]
+pub struct LoadScene {
+    pub document_id: DocumentId,
+    pub scene: Scene,
+    pub scene_id: u64,
+    pub output_pipelines: FastHashSet<PipelineId>,
+    pub font_instances: FontInstanceMap,
+    pub view: DocumentView,
+    pub config: FrameBuilderConfig,
+    pub build_frame: bool,
+}
+
+pub struct BuiltScene {
+    pub scene: Scene,
+    pub frame_builder: FrameBuilder,
+    pub clip_scroll_tree: ClipScrollTree,
+}
+
 // Message from render backend to scene builder.
 pub enum SceneBuilderRequest {
-    Transaction {
-        document_id: DocumentId,
-        scene: Option<SceneRequest>,
-        blob_requests: Vec<BlobImageParams>,
-        blob_rasterizer: Option<Box<AsyncBlobImageRasterizer>>,
-        resource_updates: Vec<ResourceUpdate>,
-        frame_ops: Vec<FrameMsg>,
-        render: bool,
-    },
+    Transaction(Box<Transaction>),
+    DeleteDocument(DocumentId),
     WakeUp,
     Flush(MsgSender<()>),
     SetFrameBuilderConfig(FrameBuilderConfig),
-    Stop
+    Stop,
+    #[cfg(feature = "replay")]
+    LoadScenes(Vec<LoadScene>),
 }
 
 // Message from scene builder to render backend.
 pub enum SceneBuilderResult {
-    Transaction {
-        document_id: DocumentId,
-        built_scene: Option<BuiltScene>,
-        resource_updates: Vec<ResourceUpdate>,
-        rasterized_blobs: Vec<(BlobImageRequest, BlobImageResult)>,
-        blob_rasterizer: Option<Box<AsyncBlobImageRasterizer>>,
-        frame_ops: Vec<FrameMsg>,
-        render: bool,
-        result_tx: Option<Sender<SceneSwapResult>>,
-    },
+    Transaction(Box<BuiltTransaction>, Option<Sender<SceneSwapResult>>),
     FlushComplete(MsgSender<()>),
     Stopped,
 }
 
 // Message from render backend to scene builder to indicate the
 // scene swap was completed. We need a separate channel for this
 // so that they don't get mixed with SceneBuilderRequest messages.
 pub enum SceneSwapResult {
     Complete(Sender<()>),
     Aborted,
 }
 
-/// Contains the render backend data needed to build a scene.
-pub struct SceneRequest {
-    pub scene: Scene,
-    pub view: DocumentView,
-    pub font_instances: FontInstanceMap,
-    pub output_pipelines: FastHashSet<PipelineId>,
-    pub removed_pipelines: Vec<PipelineId>,
-    pub scene_id: u64,
-}
-
-pub struct BuiltScene {
-    pub scene: Scene,
-    pub frame_builder: FrameBuilder,
-    pub clip_scroll_tree: ClipScrollTree,
-    pub removed_pipelines: Vec<PipelineId>,
-}
-
 pub struct SceneBuilder {
+    documents: FastHashMap<DocumentId, Scene>,
     rx: Receiver<SceneBuilderRequest>,
     tx: Sender<SceneBuilderResult>,
     api_tx: MsgSender<ApiMsg>,
     config: FrameBuilderConfig,
     hooks: Option<Box<SceneBuilderHooks + Send>>,
 }
 
 impl SceneBuilder {
@@ -87,140 +143,292 @@ impl SceneBuilder {
         config: FrameBuilderConfig,
         api_tx: MsgSender<ApiMsg>,
         hooks: Option<Box<SceneBuilderHooks + Send>>,
     ) -> (Self, Sender<SceneBuilderRequest>, Receiver<SceneBuilderResult>) {
         let (in_tx, in_rx) = channel();
         let (out_tx, out_rx) = channel();
         (
             SceneBuilder {
+                documents: FastHashMap::default(),
                 rx: in_rx,
                 tx: out_tx,
                 api_tx,
                 config,
                 hooks,
             },
             in_tx,
             out_rx,
         )
     }
 
+    /// The scene builder thread's event loop.
     pub fn run(&mut self) {
         if let Some(ref hooks) = self.hooks {
             hooks.register();
         }
 
         loop {
             match self.rx.recv() {
-                Ok(msg) => {
-                    if !self.process_message(msg) {
-                        break;
-                    }
+                Ok(SceneBuilderRequest::WakeUp) => {}
+                Ok(SceneBuilderRequest::Flush(tx)) => {
+                    self.tx.send(SceneBuilderResult::FlushComplete(tx)).unwrap();
+                    let _ = self.api_tx.send(ApiMsg::WakeUp);
+                }
+                Ok(SceneBuilderRequest::Transaction(mut txn)) => {
+                    let built_txn = self.process_transaction(&mut txn);
+                    self.forward_built_transaction(built_txn);
+                }
+                Ok(SceneBuilderRequest::DeleteDocument(document_id)) => {
+                    self.documents.remove(&document_id);
+                }
+                Ok(SceneBuilderRequest::SetFrameBuilderConfig(cfg)) => {
+                    self.config = cfg;
+                }
+                #[cfg(feature = "replay")]
+                Ok(SceneBuilderRequest::LoadScenes(msg)) => {
+                    self.load_scenes(msg);
+                }
+                Ok(SceneBuilderRequest::Stop) => {
+                    self.tx.send(SceneBuilderResult::Stopped).unwrap();
+                    // We don't need to send a WakeUp to api_tx because we only
+                    // get the Stop when the RenderBackend loop is exiting.
+                    break;
                 }
                 Err(_) => {
                     break;
                 }
             }
 
             if let Some(ref hooks) = self.hooks {
                 hooks.poke();
             }
         }
 
         if let Some(ref hooks) = self.hooks {
             hooks.deregister();
         }
     }
 
-    fn process_message(&mut self, msg: SceneBuilderRequest) -> bool {
-        match msg {
-            SceneBuilderRequest::WakeUp => {}
-            SceneBuilderRequest::Flush(tx) => {
-                self.tx.send(SceneBuilderResult::FlushComplete(tx)).unwrap();
-                let _ = self.api_tx.send(ApiMsg::WakeUp);
-            }
-            SceneBuilderRequest::Transaction {
-                document_id,
-                scene,
-                blob_requests,
-                mut blob_rasterizer,
-                resource_updates,
-                frame_ops,
-                render,
-            } => {
-                let scenebuild_start_time = precise_time_ns();
-                let built_scene = scene.map(|request|{
-                    build_scene(&self.config, request)
-                });
+    #[cfg(feature = "replay")]
+    fn load_scenes(&mut self, scenes: Vec<LoadScene>) {
+        for item in scenes {
+            self.config = item.config;
+
+            let scene_build_start_time = precise_time_ns();
 
-                let rasterized_blobs = blob_rasterizer.as_mut().map_or(
-                    Vec::new(),
-                    |rasterizer| rasterizer.rasterize(&blob_requests),
+            let mut built_scene = None;
+            if item.scene.has_root_pipeline() {
+                let mut clip_scroll_tree = ClipScrollTree::new();
+                let mut new_scene = Scene::new();
+
+                let frame_builder = DisplayListFlattener::create_frame_builder(
+                    FrameBuilder::empty(),
+                    &item.scene,
+                    &mut clip_scroll_tree,
+                    item.font_instances,
+                    &item.view,
+                    &item.output_pipelines,
+                    &self.config,
+                    &mut new_scene,
+                    item.scene_id,
                 );
 
-                // We only need the pipeline info and the result channel if we
-                // have a hook callback *and* if this transaction actually built
-                // a new scene that is going to get swapped in. In other cases
-                // pipeline_info can be None and we can avoid some overhead from
-                // invoking the hooks and blocking on the channel.
-                let (pipeline_info, result_tx, result_rx) = match (&self.hooks, &built_scene) {
-                    (&Some(ref hooks), &Some(ref built)) => {
-                        let info = PipelineInfo {
-                            epochs: built.scene.pipeline_epochs.clone(),
-                            removed_pipelines: built.removed_pipelines.clone(),
-                        };
-                        let (tx, rx) = channel();
+                built_scene = Some(BuiltScene {
+                    scene: new_scene,
+                    frame_builder,
+                    clip_scroll_tree,
+                });
+            }
+
+            self.documents.insert(item.document_id, item.scene);
 
-                        let scenebuild_time = precise_time_ns() - scenebuild_start_time;
-                        hooks.pre_scene_swap(scenebuild_time);
+            let txn = Box::new(BuiltTransaction {
+                document_id: item.document_id,
+                build_frame: true,
+                render_frame: item.build_frame,
+                built_scene,
+                resource_updates: Vec::new(),
+                rasterized_blobs: Vec::new(),
+                blob_rasterizer: None,
+                frame_ops: Vec::new(),
+                removed_pipelines: Vec::new(),
+                scene_build_start_time,
+                scene_build_end_time: precise_time_ns(),
+            });
 
-                        (Some(info), Some(tx), Some(rx))
-                    }
-                    _ => (None, None, None),
-                };
+            self.forward_built_transaction(txn);
+        }
+    }
+
+    /// Do the bulk of the work of the scene builder thread.
+    fn process_transaction(&mut self, txn: &mut Transaction) -> Box<BuiltTransaction> {
+
+        let scene_build_start_time = precise_time_ns();
+
+        let scene = self.documents.entry(txn.document_id).or_insert(Scene::new());
 
-                let sceneswap_start_time = precise_time_ns();
-                let has_resources_updates = !resource_updates.is_empty();
-                self.tx.send(SceneBuilderResult::Transaction {
-                    document_id,
-                    built_scene,
-                    resource_updates,
-                    rasterized_blobs,
-                    blob_rasterizer,
-                    frame_ops,
-                    render,
-                    result_tx,
-                }).unwrap();
+        for update in txn.display_list_updates.drain(..) {
+            scene.set_display_list(
+                update.pipeline_id,
+                update.epoch,
+                update.built_display_list,
+                update.background,
+                update.viewport_size,
+                update.content_size,
+            );
+        }
 
-                let _ = self.api_tx.send(ApiMsg::WakeUp);
+        for &(pipeline_id, epoch) in &txn.epoch_updates {
+            scene.update_epoch(pipeline_id, epoch);
+        }
+
+        if let Some(id) = txn.set_root_pipeline {
+            scene.set_root_pipeline_id(id);
+        }
+
+        for pipeline_id in &txn.removed_pipelines {
+            scene.remove_pipeline(*pipeline_id)
+        }
 
-                if let Some(pipeline_info) = pipeline_info {
-                    // Block until the swap is done, then invoke the hook.
-                    let swap_result = result_rx.unwrap().recv();
-                    let sceneswap_time = precise_time_ns() - sceneswap_start_time;
-                    self.hooks.as_ref().unwrap().post_scene_swap(pipeline_info, sceneswap_time);
-                    // Once the hook is done, allow the RB thread to resume
-                    match swap_result {
-                        Ok(SceneSwapResult::Complete(resume_tx)) => {
-                            resume_tx.send(()).ok();
-                        },
-                        _ => (),
-                    };
-                } else if has_resources_updates {
-                    if let &Some(ref hooks) = &self.hooks {
-                        hooks.post_resource_update();
-                    }
-                }
-            }
-            SceneBuilderRequest::Stop => {
-                self.tx.send(SceneBuilderResult::Stopped).unwrap();
-                // We don't need to send a WakeUp to api_tx because we only
-                // get the Stop when the RenderBackend loop is exiting.
-                return false;
-            }
-            SceneBuilderRequest::SetFrameBuilderConfig(cfg) => {
-                self.config = cfg;
+        let mut built_scene = None;
+        if scene.has_root_pipeline() {
+            if let Some(request) = txn.request_scene_build.take() {
+                let mut clip_scroll_tree = ClipScrollTree::new();
+                let mut new_scene = Scene::new();
+
+                let frame_builder = DisplayListFlattener::create_frame_builder(
+                    FrameBuilder::empty(),
+                    &scene,
+                    &mut clip_scroll_tree,
+                    request.font_instances,
+                    &request.view,
+                    &request.output_pipelines,
+                    &self.config,
+                    &mut new_scene,
+                    request.scene_id,
+                );
+
+                built_scene = Some(BuiltScene {
+                    scene: new_scene,
+                    frame_builder,
+                    clip_scroll_tree,
+                });
             }
         }
 
-        true
+        let blob_requests = replace(&mut txn.blob_requests, Vec::new());
+        let mut rasterized_blobs = txn.blob_rasterizer.as_mut().map_or(
+            Vec::new(),
+            |rasterizer| rasterizer.rasterize(&blob_requests),
+        );
+        rasterized_blobs.append(&mut txn.rasterized_blobs);
+
+        Box::new(BuiltTransaction {
+            document_id: txn.document_id,
+            build_frame: txn.build_frame || built_scene.is_some(),
+            render_frame: txn.render_frame,
+            built_scene,
+            rasterized_blobs,
+            resource_updates: replace(&mut txn.resource_updates, Vec::new()),
+            blob_rasterizer: replace(&mut txn.blob_rasterizer, None),
+            frame_ops: replace(&mut txn.frame_ops, Vec::new()),
+            removed_pipelines: replace(&mut txn.removed_pipelines, Vec::new()),
+            scene_build_start_time,
+            scene_build_end_time: precise_time_ns(),
+        })
+    }
+
+    /// Send the result of process_transaction back to the render backend.
+    fn forward_built_transaction(&mut self, txn: Box<BuiltTransaction>) {
+        // We only need the pipeline info and the result channel if we
+        // have a hook callback *and* if this transaction actually built
+        // a new scene that is going to get swapped in. In other cases
+        // pipeline_info can be None and we can avoid some overhead from
+        // invoking the hooks and blocking on the channel.
+        let (pipeline_info, result_tx, result_rx) = match (&self.hooks, &txn.built_scene) {
+            (&Some(ref hooks), &Some(ref built)) => {
+                let info = PipelineInfo {
+                    epochs: built.scene.pipeline_epochs.clone(),
+                    removed_pipelines: txn.removed_pipelines.clone(),
+                };
+                let (tx, rx) = channel();
+
+                hooks.pre_scene_swap(txn.scene_build_end_time - txn.scene_build_start_time);
+
+                (Some(info), Some(tx), Some(rx))
+            }
+            _ => (None, None, None),
+        };
+
+        let scene_swap_start_time = precise_time_ns();
+        let has_resources_updates = !txn.resource_updates.is_empty();
+
+        self.tx.send(SceneBuilderResult::Transaction(txn, result_tx)).unwrap();
+
+        let _ = self.api_tx.send(ApiMsg::WakeUp);
+
+        if let Some(pipeline_info) = pipeline_info {
+            // Block until the swap is done, then invoke the hook.
+            let swap_result = result_rx.unwrap().recv();
+            let scene_swap_time = precise_time_ns() - scene_swap_start_time;
+            self.hooks.as_ref().unwrap().post_scene_swap(pipeline_info, scene_swap_time);
+            // Once the hook is done, allow the RB thread to resume
+            match swap_result {
+                Ok(SceneSwapResult::Complete(resume_tx)) => {
+                    resume_tx.send(()).ok();
+                },
+                _ => (),
+            };
+        } else if has_resources_updates {
+            if let &Some(ref hooks) = &self.hooks {
+                hooks.post_resource_update();
+            }
+        }
     }
 }
+
+/// A scene builder thread which executes expensive operations such as blob rasterization
+/// with a lower priority than the normal scene builder thread.
+///
+/// After rasterizing blobs, the secene building request is forwarded to the normal scene
+/// builder where the FrameBuilder is generated.
+pub struct LowPrioritySceneBuilder {
+    pub rx: Receiver<SceneBuilderRequest>,
+    pub tx: Sender<SceneBuilderRequest>,
+}
+
+impl LowPrioritySceneBuilder {
+    pub fn run(&mut self) {
+        loop {
+            match self.rx.recv() {
+                Ok(SceneBuilderRequest::Transaction(txn)) => {
+                    let txn = self.process_transaction(txn);
+                    self.tx.send(SceneBuilderRequest::Transaction(txn)).unwrap();
+                }
+                Ok(SceneBuilderRequest::DeleteDocument(document_id)) => {
+                    self.tx.send(SceneBuilderRequest::DeleteDocument(document_id)).unwrap();
+                }
+                Ok(SceneBuilderRequest::Stop) => {
+                    self.tx.send(SceneBuilderRequest::Stop).unwrap();
+                    break;
+                }
+                Ok(other) => {
+                    self.tx.send(other).unwrap();
+                }
+                Err(_) => {
+                    break;
+                }
+            }
+        }
+    }
+
+    fn process_transaction(&mut self, mut txn: Box<Transaction>) -> Box<Transaction> {
+        let blob_requests = replace(&mut txn.blob_requests, Vec::new());
+        let mut more_rasterized_blobs = txn.blob_rasterizer.as_mut().map_or(
+            Vec::new(),
+            |rasterizer| rasterizer.rasterize(&blob_requests),
+        );
+        txn.rasterized_blobs.append(&mut more_rasterized_blobs);
+
+        txn
+    }
+}
--- a/gfx/webrender/src/shade.rs
+++ b/gfx/webrender/src/shade.rs
@@ -347,17 +347,17 @@ impl TextShader {
 
 fn create_prim_shader(
     name: &'static str,
     device: &mut Device,
     features: &[&'static str],
     vertex_format: VertexArrayKind,
 ) -> Result<Program, ShaderError> {
     let mut prefix = format!(
-        "#define WR_MAX_VERTEX_TEXTURE_WIDTH {}\n",
+        "#define WR_MAX_VERTEX_TEXTURE_WIDTH {}U\n",
         MAX_VERTEX_TEXTURE_WIDTH
     );
 
     for feature in features {
         prefix.push_str(&format!("#define WR_FEATURE_{}\n", feature));
     }
 
     debug!("PrimShader {}", name);
@@ -393,17 +393,17 @@ fn create_prim_shader(
         );
     }
 
     program
 }
 
 fn create_clip_shader(name: &'static str, device: &mut Device) -> Result<Program, ShaderError> {
     let prefix = format!(
-        "#define WR_MAX_VERTEX_TEXTURE_WIDTH {}\n
+        "#define WR_MAX_VERTEX_TEXTURE_WIDTH {}U\n
         #define WR_FEATURE_TRANSFORM\n",
         MAX_VERTEX_TEXTURE_WIDTH
     );
 
     debug!("ClipShader {}", name);
 
     let program = device.create_program(name, &prefix, &desc::CLIP);
 
@@ -428,16 +428,17 @@ fn create_clip_shader(name: &'static str
 
 pub struct Shaders {
     // These are "cache shaders". These shaders are used to
     // draw intermediate results to cache targets. The results
     // of these shaders are then used by the primitive shaders.
     pub cs_blur_a8: LazilyCompiledShader,
     pub cs_blur_rgba8: LazilyCompiledShader,
     pub cs_border_segment: LazilyCompiledShader,
+    pub cs_border_solid: LazilyCompiledShader,
 
     // Brush shaders
     brush_solid: BrushShader,
     brush_image: Vec<Option<BrushShader>>,
     brush_blend: BrushShader,
     brush_mix_blend: BrushShader,
     brush_yuv_image: Vec<Option<BrushShader>>,
     brush_radial_gradient: BrushShader,
@@ -658,32 +659,41 @@ impl Shaders {
         let cs_border_segment = LazilyCompiledShader::new(
             ShaderKind::Cache(VertexArrayKind::Border),
             "cs_border_segment",
              &[],
              device,
              options.precache_shaders,
         )?;
 
+        let cs_border_solid = LazilyCompiledShader::new(
+            ShaderKind::Cache(VertexArrayKind::Border),
+            "cs_border_solid",
+            &[],
+            device,
+            options.precache_shaders,
+        )?;
+
         let ps_split_composite = LazilyCompiledShader::new(
             ShaderKind::Primitive,
             "ps_split_composite",
             &[],
             device,
             options.precache_shaders,
         )?;
 
         if let Some(vao) = dummy_vao {
             device.delete_custom_vao(vao);
         }
 
         Ok(Shaders {
             cs_blur_a8,
             cs_blur_rgba8,
             cs_border_segment,
+            cs_border_solid,
             brush_solid,
             brush_image,
             brush_blend,
             brush_mix_blend,
             brush_yuv_image,
             brush_radial_gradient,
             brush_linear_gradient,
             cs_clip_rectangle,
@@ -771,12 +781,13 @@ impl Shaders {
                 shader.deinit(device);
             }
         }
         for shader in self.brush_yuv_image {
             if let Some(shader) = shader {
                 shader.deinit(device);
             }
         }
+        self.cs_border_solid.deinit(device);
         self.cs_border_segment.deinit(device);
         self.ps_split_composite.deinit(device);
     }
 }
--- a/gfx/webrender/src/tiling.rs
+++ b/gfx/webrender/src/tiling.rs
@@ -1,15 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-use api::{ColorF, DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixelScale, DeviceUintPoint};
-use api::{DeviceUintRect, DeviceUintSize, DocumentLayer, FilterOp, ImageFormat, LayoutRect};
-use api::{MixBlendMode, PipelineId};
+use api::{ColorF, BorderStyle, DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixelScale};
+use api::{DeviceUintPoint, DeviceUintRect, DeviceUintSize, DocumentLayer, FilterOp, ImageFormat};
+use api::{LayoutRect, MixBlendMode, PipelineId};
 use batch::{AlphaBatchBuilder, AlphaBatchContainer, ClipBatcher, resolve_image};
 use clip::{ClipStore};
 use clip_scroll_tree::SpatialNodeIndex;
 use device::{FrameId, Texture};
 #[cfg(feature = "pathfinder")]
 use euclid::{TypedPoint2D, TypedVector2D};
 use gpu_cache::{GpuCache};
 use gpu_types::{BorderInstance, BlurDirection, BlurInstance, PrimitiveHeaders, TransformData, TransformPalette};
@@ -22,16 +22,18 @@ use render_task::{BlitSource, RenderTask
 use render_task::{BlurTask, ClearMode, GlyphTask, RenderTaskLocation, RenderTaskTree};
 use resource_cache::ResourceCache;
 use std::{cmp, usize, f32, i32, mem};
 use texture_allocator::GuillotineAllocator;
 #[cfg(feature = "pathfinder")]
 use webrender_api::{DevicePixel, FontRenderMode};
 
 const MIN_TARGET_SIZE: u32 = 2048;
+const STYLE_SOLID: i32 = ((BorderStyle::Solid as i32) << 8) | ((BorderStyle::Solid as i32) << 16);
+const STYLE_MASK: i32 = 0x00FF_FF00;
 
 #[derive(Debug)]
 pub struct ScrollbarPrimitive {
     pub scroll_frame_index: SpatialNodeIndex,
     pub prim_index: PrimitiveIndex,
     pub frame_rect: LayoutRect,
 }
 
@@ -613,28 +615,30 @@ impl RenderTarget for AlphaRenderTarget 
 
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct TextureCacheRenderTarget {
     pub target_kind: RenderTargetKind,
     pub horizontal_blurs: Vec<BlurInstance>,
     pub blits: Vec<BlitJob>,
     pub glyphs: Vec<GlyphJob>,
-    pub border_segments: Vec<BorderInstance>,
+    pub border_segments_complex: Vec<BorderInstance>,
+    pub border_segments_solid: Vec<BorderInstance>,
     pub clears: Vec<DeviceIntRect>,
 }
 
 impl TextureCacheRenderTarget {
     fn new(target_kind: RenderTargetKind) -> Self {
         TextureCacheRenderTarget {
             target_kind,
             horizontal_blurs: vec![],
             blits: vec![],
             glyphs: vec![],
-            border_segments: vec![],
+            border_segments_complex: vec![],
+            border_segments_solid: vec![],
             clears: vec![],
         }
     }
 
     fn add_task(
         &mut self,
         task_id: RenderTaskId,
         render_tasks: &mut RenderTaskTree,
@@ -679,18 +683,23 @@ impl TextureCacheRenderTarget {
                 // TODO(gw): It may be better to store the task origin in
                 //           the render task data instead of per instance.
                 let task_origin = target_rect.0.origin.to_f32();
                 for instance in &mut task_info.instances {
                     instance.task_origin = task_origin;
                 }
 
                 let instances = mem::replace(&mut task_info.instances, Vec::new());
-
-                self.border_segments.extend(instances);
+                for instance in instances {
+                    if instance.flags & STYLE_MASK == STYLE_SOLID {
+                        self.border_segments_solid.push(instance);
+                    } else {
+                        self.border_segments_complex.push(instance);
+                    }
+                }
             }
             RenderTaskKind::Glyph(ref mut task_info) => {
                 self.add_glyph_task(task_info, target_rect.0)
             }
             RenderTaskKind::VerticalBlur(..) |
             RenderTaskKind::Picture(..) |
             RenderTaskKind::ClipRegion(..) |
             RenderTaskKind::CacheMask(..) |
--- a/gfx/webrender/src/util.rs
+++ b/gfx/webrender/src/util.rs
@@ -456,22 +456,30 @@ pub fn project_rect<F, T>(
         transform.transform_point2d_homogeneous(&rect.bottom_left()),
         transform.transform_point2d_homogeneous(&rect.bottom_right()),
     ];
 
     // Note: we only do the full frustum collision when the polygon approaches the camera plane.
     // Otherwise, it will be clamped to the screen bounds anyway.
     if homogens.iter().any(|h| h.w <= 0.0) {
         let mut clipper = Clipper::new();
-        clipper.add_frustum(
+        let polygon = Polygon::from_rect(*rect, 1);
+
+        let planes = match Clipper::frustum_planes(
             transform,
             Some(*bounds),
-        );
+        ) {
+            Ok(planes) => planes,
+            Err(..) => return None,
+        };
 
-        let polygon = Polygon::from_rect(*rect, 1);
+        for plane in planes {
+            clipper.add(plane);
+        }
+
         let results = clipper.clip(polygon);
         if results.is_empty() {
             return None
         }
 
         Some(TypedRect::from_points(results
             .into_iter()
             // filter out parts behind the view plane
--- a/gfx/webrender/tests/angle_shader_validation.rs
+++ b/gfx/webrender/tests/angle_shader_validation.rs
@@ -11,17 +11,17 @@ use mozangle::shaders::{BuiltInResources
 const FRAGMENT_SHADER: u32 = 0x8B30;
 const VERTEX_SHADER: u32 = 0x8B31;
 
 struct Shader {
     name: &'static str,
     features: &'static [&'static str],
 }
 
-const SHADER_PREFIX: &str = "#define WR_MAX_VERTEX_TEXTURE_WIDTH 1024\n";
+const SHADER_PREFIX: &str = "#define WR_MAX_VERTEX_TEXTURE_WIDTH 1024U\n";
 
 const BRUSH_FEATURES: &[&str] = &["", "ALPHA_PASS"];
 const CLIP_FEATURES: &[&str] = &["TRANSFORM"];
 const CACHE_FEATURES: &[&str] = &[""];
 const GRADIENT_FEATURES: &[&str] = &[ "", "DITHERING", "ALPHA_PASS", "DITHERING,ALPHA_PASS" ];
 const PRIM_FEATURES: &[&str] = &[""];
 
 const SHADERS: &[Shader] = &[
@@ -46,16 +46,20 @@ const SHADERS: &[Shader] = &[
     Shader {
         name: "cs_blur",
         features: &[ "ALPHA_TARGET", "COLOR_TARGET" ],
     },
     Shader {
         name: "cs_border_segment",
         features: CACHE_FEATURES,
     },
+    Shader {
+        name: "cs_border_solid",
+        features: CACHE_FEATURES,
+    },
     // Prim shaders
     Shader {
         name: "ps_split_composite",
         features: PRIM_FEATURES,
     },
     Shader {
         name: "ps_text_run",
         features: &[ "", "GLYPH_TRANSFORM" ],
--- a/gfx/webrender_api/src/api.rs
+++ b/gfx/webrender_api/src/api.rs
@@ -51,27 +51,30 @@ pub struct Transaction {
     // Resource updates are applied after scene building.
     pub resource_updates: Vec<ResourceUpdate>,
 
     // If true the transaction is piped through the scene building thread, if false
     // it will be applied directly on the render backend.
     use_scene_builder_thread: bool,
 
     generate_frame: bool,
+
+    low_priority: bool,
 }
 
 impl Transaction {
     pub fn new() -> Self {
         Transaction {
             scene_ops: Vec::new(),
             frame_ops: Vec::new(),
             resource_updates: Vec::new(),
             payloads: Vec::new(),
             use_scene_builder_thread: true,
             generate_frame: false,
+            low_priority: false,
         }
     }
 
     // TODO: better name?
     pub fn skip_scene_builder(&mut self) {
         self.use_scene_builder_thread = false;
     }
 
@@ -251,16 +254,17 @@ impl Transaction {
     fn finalize(self) -> (TransactionMsg, Vec<Payload>) {
         (
             TransactionMsg {
                 scene_ops: self.scene_ops,
                 frame_ops: self.frame_ops,
                 resource_updates: self.resource_updates,
                 use_scene_builder_thread: self.use_scene_builder_thread,
                 generate_frame: self.generate_frame,
+                low_priority: self.low_priority,
             },
             self.payloads,
         )
     }
 
     pub fn add_image(
         &mut self,
         key: ImageKey,
@@ -332,16 +336,28 @@ impl Transaction {
                 variations,
             }));
     }
 
     pub fn delete_font_instance(&mut self, key: FontInstanceKey) {
         self.resource_updates.push(ResourceUpdate::DeleteFontInstance(key));
     }
 
+    // A hint that this transaction can be processed at a lower priority. High-
+    // priority transactions can jump ahead of regular-priority transactions,
+    // but both high- and regular-priority transactions are processed in order
+    // relative to other transactions of the same priority.
+    pub fn set_low_priority(&mut self, low_priority: bool) {
+        self.low_priority = low_priority;
+    }
+
+    pub fn is_low_priority(&self) -> bool {
+        self.low_priority
+    }
+
     pub fn merge(&mut self, mut other: Vec<ResourceUpdate>) {
         self.resource_updates.append(&mut other);
     }
 
     pub fn clear(&mut self) {
         self.resource_updates.clear()
     }
 }
@@ -349,16 +365,17 @@ impl Transaction {
 /// Represents a transaction in the format sent through the channel.
 #[derive(Clone, Deserialize, Serialize)]
 pub struct TransactionMsg {
     pub scene_ops: Vec<SceneMsg>,
     pub frame_ops: Vec<FrameMsg>,
     pub resource_updates: Vec<ResourceUpdate>,
     pub generate_frame: bool,
     pub use_scene_builder_thread: bool,
+    pub low_priority: bool,
 }
 
 impl TransactionMsg {
     pub fn is_empty(&self) -> bool {
         !self.generate_frame &&
             self.scene_ops.is_empty() &&
             self.frame_ops.is_empty() &&
             self.resource_updates.is_empty()
@@ -367,26 +384,28 @@ impl TransactionMsg {
     // TODO: We only need this for a few RenderApi methods which we should remove.
     fn frame_message(msg: FrameMsg) -> Self {
         TransactionMsg {
             scene_ops: Vec::new(),
             frame_ops: vec![msg],
             resource_updates: Vec::new(),
             generate_frame: false,
             use_scene_builder_thread: false,
+            low_priority: false,
         }
     }
 
     fn scene_message(msg: SceneMsg) -> Self {
         TransactionMsg {
             scene_ops: vec![msg],
             frame_ops: Vec::new(),
             resource_updates: Vec::new(),
             generate_frame: false,
             use_scene_builder_thread: false,
+            low_priority: false,
         }
     }
 }
 
 #[derive(Clone, Deserialize, Serialize)]
 pub struct AddImage {
     pub key: ImageKey,
     pub descriptor: ImageDescriptor,
--- a/gfx/webrender_bindings/revision.txt
+++ b/gfx/webrender_bindings/revision.txt
@@ -1,1 +1,1 @@
-d89e290c57aab76c45d8016975240cf762354e39
+46af5bf17978a97f0ab2a899a0d785d58c140a0a