Bug 1434723 - Update webrender to commit 08e49649f1fc9cacff4e10ebc390babcea752236. r=jrmuizel
authorKartikaya Gupta <kgupta@mozilla.com>
Tue, 06 Feb 2018 10:31:57 -0500
changeset 402615 29c8f502f43f2dc05ae715b824104194e39eedf2
parent 402614 d23fda609203ea1497cf22156a621d5514e5ca8e
child 402616 2d2a36f4aca9e1a132b1647f94d49cf65a230d6a
push id99611
push userrgurzau@mozilla.com
push dateTue, 06 Feb 2018 22:00:54 +0000
treeherdermozilla-inbound@b3a81e6e2c1e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjrmuizel
bugs1434723
milestone60.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 1434723 - Update webrender to commit 08e49649f1fc9cacff4e10ebc390babcea752236. r=jrmuizel MozReview-Commit-ID: IoduYBMrWJk
gfx/webrender/examples/basic.rs
gfx/webrender/examples/multiwindow.rs
gfx/webrender/res/ps_blend.glsl
gfx/webrender/src/batch.rs
gfx/webrender/src/clip.rs
gfx/webrender/src/clip_scroll_node.rs
gfx/webrender/src/clip_scroll_tree.rs
gfx/webrender/src/frame.rs
gfx/webrender/src/frame_builder.rs
gfx/webrender/src/hit_test.rs
gfx/webrender/src/lib.rs
gfx/webrender/src/picture.rs
gfx/webrender/src/render_backend.rs
gfx/webrender/src/scene.rs
gfx/webrender_api/src/api.rs
gfx/webrender_api/src/display_item.rs
gfx/webrender_bindings/revision.txt
gfx/wrench/src/wrench.rs
gfx/wrench/src/yaml_frame_reader.rs
gfx/wrench/src/yaml_frame_writer.rs
gfx/wrench/src/yaml_helper.rs
--- a/gfx/webrender/examples/basic.rs
+++ b/gfx/webrender/examples/basic.rs
@@ -6,23 +6,20 @@ extern crate app_units;
 extern crate euclid;
 extern crate gleam;
 extern crate glutin;
 extern crate webrender;
 
 #[path = "common/boilerplate.rs"]
 mod boilerplate;
 
-use app_units::Au;
 use boilerplate::{Example, HandyDandyRectBuilder};
 use euclid::vec2;
 use glutin::TouchPhase;
 use std::collections::HashMap;
-use std::fs::File;
-use std::io::Read;
 use webrender::api::*;
 
 #[derive(Debug)]
 enum Gesture {
     None,
     Pan,
     Zoom,
 }
@@ -161,23 +158,16 @@ impl TouchState {
                 self.current_gesture = Gesture::None;
             }
         }
 
         TouchResult::None
     }
 }
 
-fn load_file(name: &str) -> Vec<u8> {
-    let mut file = File::open(name).unwrap();
-    let mut buffer = vec![];
-    file.read_to_end(&mut buffer).unwrap();
-    buffer
-}
-
 fn main() {
     let mut app = App {
         touch_state: TouchState::new(),
     };
     boilerplate::main_wrapper(&mut app, None);
 }
 
 struct App {
@@ -251,87 +241,16 @@ impl Example for App {
             left: border_side,
             radius: BorderRadius::uniform(20.0),
         });
 
         let info = LayoutPrimitiveInfo::new((100, 100).to(200, 200));
         builder.push_border(&info, border_widths, border_details);
         builder.pop_clip_id();
 
-        if true {
-            // draw text?
-            let font_key = api.generate_font_key();
-            let font_bytes = load_file("../wrench/reftests/text/FreeSans.ttf");
-            resources.add_raw_font(font_key, font_bytes, 0);
-
-            let font_instance_key = api.generate_font_instance_key();
-            resources.add_font_instance(font_instance_key, font_key, Au::from_px(32), None, None, Vec::new());
-
-            let text_bounds = (100, 50).by(700, 200);
-            let glyphs = vec![
-                GlyphInstance {
-                    index: 48,
-                    point: LayoutPoint::new(100.0, 100.0),
-                },
-                GlyphInstance {
-                    index: 68,
-                    point: LayoutPoint::new(150.0, 100.0),
-                },
-                GlyphInstance {
-                    index: 80,
-                    point: LayoutPoint::new(200.0, 100.0),
-                },
-                GlyphInstance {
-                    index: 82,
-                    point: LayoutPoint::new(250.0, 100.0),
-                },
-                GlyphInstance {
-                    index: 81,
-                    point: LayoutPoint::new(300.0, 100.0),
-                },
-                GlyphInstance {
-                    index: 3,
-                    point: LayoutPoint::new(350.0, 100.0),
-                },
-                GlyphInstance {
-                    index: 86,
-                    point: LayoutPoint::new(400.0, 100.0),
-                },
-                GlyphInstance {
-                    index: 79,
-                    point: LayoutPoint::new(450.0, 100.0),
-                },
-                GlyphInstance {
-                    index: 72,
-                    point: LayoutPoint::new(500.0, 100.0),
-                },
-                GlyphInstance {
-                    index: 83,
-                    point: LayoutPoint::new(550.0, 100.0),
-                },
-                GlyphInstance {
-                    index: 87,
-                    point: LayoutPoint::new(600.0, 100.0),
-                },
-                GlyphInstance {
-                    index: 17,
-                    point: LayoutPoint::new(650.0, 100.0),
-                },
-            ];
-
-            let info = LayoutPrimitiveInfo::new(text_bounds);
-            builder.push_text(
-                &info,
-                &glyphs,
-                font_instance_key,
-                ColorF::new(1.0, 1.0, 0.0, 1.0),
-                None,
-            );
-        }
-
         if false {
             // draw box shadow?
             let rect = LayoutRect::zero();
             let simple_box_bounds = (20, 200).by(50, 50);
             let offset = vec2(10.0, 10.0);
             let color = ColorF::new(1.0, 1.0, 1.0, 1.0);
             let blur_radius = 0.0;
             let spread_radius = 0.0;
new file mode 100644
--- /dev/null
+++ b/gfx/webrender/examples/multiwindow.rs
@@ -0,0 +1,282 @@
+/* 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/. */
+
+extern crate app_units;
+extern crate euclid;
+extern crate gleam;
+extern crate glutin;
+extern crate webrender;
+
+use app_units::Au;
+use std::fs::File;
+use std::io::Read;
+use webrender::api::*;
+use gleam::gl;
+
+struct Notifier {
+    window_proxy: glutin::WindowProxy,
+}
+
+impl Notifier {
+    fn new(window_proxy: glutin::WindowProxy) -> Notifier {
+        Notifier { window_proxy }
+    }
+}
+
+impl RenderNotifier for Notifier {
+    fn clone(&self) -> Box<RenderNotifier> {
+        Box::new(Notifier {
+            window_proxy: self.window_proxy.clone(),
+        })
+    }
+
+    fn wake_up(&self) {
+        #[cfg(not(target_os = "android"))]
+        self.window_proxy.wakeup_event_loop();
+    }
+
+    fn new_document_ready(&self, _: DocumentId, _scrolled: bool, _composite_needed: bool) {
+        self.wake_up();
+    }
+}
+
+struct Window {
+    window: glutin::Window,
+    renderer: webrender::Renderer,
+    name: &'static str,
+    pipeline_id: PipelineId,
+    document_id: DocumentId,
+    epoch: Epoch,
+    api: RenderApi,
+    font_instance_key: FontInstanceKey,
+}
+
+impl Window {
+    fn new(name: &'static str, clear_color: ColorF) -> Window {
+        let window = glutin::WindowBuilder::new()
+            .with_title(name)
+            .with_multitouch()
+            .with_dimensions(800, 600)
+            .with_gl(glutin::GlRequest::GlThenGles {
+                opengl_version: (3, 2),
+                opengles_version: (3, 0),
+            })
+            .build()
+            .unwrap();
+
+        unsafe {
+            window.make_current().ok();
+        }
+
+        let gl = match window.get_api() {
+            glutin::Api::OpenGl => unsafe {
+                gl::GlFns::load_with(|symbol| window.get_proc_address(symbol) as *const _)
+            },
+            glutin::Api::OpenGlEs => unsafe {
+                gl::GlesFns::load_with(|symbol| window.get_proc_address(symbol) as *const _)
+            },
+            glutin::Api::WebGl => unimplemented!(),
+        };
+
+        let device_pixel_ratio = window.hidpi_factor();
+
+        let opts = webrender::RendererOptions {
+            debug: true,
+            device_pixel_ratio,
+            clear_color: Some(clear_color),
+            ..webrender::RendererOptions::default()
+        };
+
+        let framebuffer_size = {
+            let (width, height) = window.get_inner_size_pixels().unwrap();
+            DeviceUintSize::new(width, height)
+        };
+        let notifier = Box::new(Notifier::new(window.create_window_proxy()));
+        let (renderer, sender) = webrender::Renderer::new(gl.clone(), notifier, opts).unwrap();
+        let api = sender.create_api();
+        let document_id = api.add_document(framebuffer_size, 0);
+
+        let epoch = Epoch(0);
+        let pipeline_id = PipelineId(0, 0);
+        let mut resources = ResourceUpdates::new();
+
+        let font_key = api.generate_font_key();
+        let font_bytes = load_file("../wrench/reftests/text/FreeSans.ttf");
+        resources.add_raw_font(font_key, font_bytes, 0);
+
+        let font_instance_key = api.generate_font_instance_key();
+        resources.add_font_instance(font_instance_key, font_key, Au::from_px(32), None, None, Vec::new());
+
+        let mut txn = Transaction::new();
+        txn.update_resources(resources);
+        api.send_transaction(document_id, txn);
+
+        Window {
+            window,
+            renderer,
+            name,
+            epoch,
+            pipeline_id,
+            document_id,
+            api,
+            font_instance_key,
+        }
+    }
+
+    fn tick(&mut self) -> bool {
+        unsafe {
+            self.window.make_current().ok();
+        }
+
+        for event in self.window.poll_events() {
+            match event {
+                glutin::Event::Closed |
+                glutin::Event::KeyboardInput(_, _, Some(glutin::VirtualKeyCode::Escape)) => return true,
+
+                glutin::Event::KeyboardInput(
+                    glutin::ElementState::Pressed,
+                    _,
+                    Some(glutin::VirtualKeyCode::P),
+                ) => {
+                    println!("toggle flags {}", self.name);
+                    self.renderer.toggle_debug_flags(webrender::DebugFlags::PROFILER_DBG);
+                }
+
+                _ => {}
+            }
+        }
+
+        let framebuffer_size = {
+            let (width, height) = self.window.get_inner_size_pixels().unwrap();
+            DeviceUintSize::new(width, height)
+        };
+        let device_pixel_ratio = self.window.hidpi_factor();
+        let layout_size = framebuffer_size.to_f32() / euclid::TypedScale::new(device_pixel_ratio);
+        let mut txn = Transaction::new();
+        let mut builder = DisplayListBuilder::new(self.pipeline_id, layout_size);
+
+        let bounds = LayoutRect::new(LayoutPoint::zero(), builder.content_size());
+        let info = LayoutPrimitiveInfo::new(bounds);
+        builder.push_stacking_context(
+            &info,
+            ScrollPolicy::Scrollable,
+            None,
+            TransformStyle::Flat,
+            None,
+            MixBlendMode::Normal,
+            Vec::new(),
+        );
+
+        let info = LayoutPrimitiveInfo::new(LayoutRect::new(LayoutPoint::new(100.0, 100.0), LayoutSize::new(100.0, 200.0)));
+        builder.push_rect(&info, ColorF::new(0.0, 1.0, 0.0, 1.0));
+
+        let text_bounds = LayoutRect::new(LayoutPoint::new(100.0, 50.0), LayoutSize::new(700.0, 200.0));
+        let glyphs = vec![
+            GlyphInstance {
+                index: 48,
+                point: LayoutPoint::new(100.0, 100.0),
+            },
+            GlyphInstance {
+                index: 68,
+                point: LayoutPoint::new(150.0, 100.0),
+            },
+            GlyphInstance {
+                index: 80,
+                point: LayoutPoint::new(200.0, 100.0),
+            },
+            GlyphInstance {
+                index: 82,
+                point: LayoutPoint::new(250.0, 100.0),
+            },
+            GlyphInstance {
+                index: 81,
+                point: LayoutPoint::new(300.0, 100.0),
+            },
+            GlyphInstance {
+                index: 3,
+                point: LayoutPoint::new(350.0, 100.0),
+            },
+            GlyphInstance {
+                index: 86,
+                point: LayoutPoint::new(400.0, 100.0),
+            },
+            GlyphInstance {
+                index: 79,
+                point: LayoutPoint::new(450.0, 100.0),
+            },
+            GlyphInstance {
+                index: 72,
+                point: LayoutPoint::new(500.0, 100.0),
+            },
+            GlyphInstance {
+                index: 83,
+                point: LayoutPoint::new(550.0, 100.0),
+            },
+            GlyphInstance {
+                index: 87,
+                point: LayoutPoint::new(600.0, 100.0),
+            },
+            GlyphInstance {
+                index: 17,
+                point: LayoutPoint::new(650.0, 100.0),
+            },
+        ];
+
+        let info = LayoutPrimitiveInfo::new(text_bounds);
+        builder.push_text(
+            &info,
+            &glyphs,
+            self.font_instance_key,
+            ColorF::new(1.0, 1.0, 0.0, 1.0),
+            None,
+        );
+
+        builder.pop_stacking_context();
+
+        txn.set_display_list(
+            self.epoch,
+            None,
+            layout_size,
+            builder.finalize(),
+            true,
+        );
+        txn.set_root_pipeline(self.pipeline_id);
+        txn.generate_frame();
+        self.api.send_transaction(self.document_id, txn);
+
+        self.renderer.update();
+        self.renderer.render(framebuffer_size).unwrap();
+        self.window.swap_buffers().ok();
+
+        false
+    }
+
+    fn deinit(self) {
+        self.renderer.deinit();
+    }
+}
+
+fn main() {
+    let mut win1 = Window::new("window1", ColorF::new(0.3, 0.0, 0.0, 1.0));
+    let mut win2 = Window::new("window2", ColorF::new(0.0, 0.3, 0.0, 1.0));
+
+    loop {
+        if win1.tick() {
+            break;
+        }
+        if win2.tick() {
+            break;
+        }
+    }
+
+    win1.deinit();
+    win2.deinit();
+}
+
+fn load_file(name: &str) -> Vec<u8> {
+    let mut file = File::open(name).unwrap();
+    let mut buffer = vec![];
+    file.read_to_end(&mut buffer).unwrap();
+    buffer
+}
--- a/gfx/webrender/res/ps_blend.glsl
+++ b/gfx/webrender/res/ps_blend.glsl
@@ -4,16 +4,17 @@
 
 #include shared,prim_shared
 
 varying vec3 vUv;
 flat varying vec4 vUvBounds;
 flat varying float vAmount;
 flat varying int vOp;
 flat varying mat4 vColorMat;
+flat varying vec4 vColorOffset;
 
 #ifdef WR_VERTEX_SHADER
 void main(void) {
     CompositeInstance ci = fetch_composite_instance();
     PictureTask dest_task = fetch_picture_task(ci.render_task_index);
     PictureTask src_task = fetch_picture_task(ci.src_task_index);
 
     vec2 dest_origin = dest_task.common_data.task_rect.p0 -
@@ -45,44 +46,58 @@ void main(void) {
 
     switch (vOp) {
         case 2: {
             // Grayscale
             vColorMat = mat4(vec4(lumR + oneMinusLumR * oneMinusAmount, lumR - lumR * oneMinusAmount, lumR - lumR * oneMinusAmount, 0.0),
                              vec4(lumG - lumG * oneMinusAmount, lumG + oneMinusLumG * oneMinusAmount, lumG - lumG * oneMinusAmount, 0.0),
                              vec4(lumB - lumB * oneMinusAmount, lumB - lumB * oneMinusAmount, lumB + oneMinusLumB * oneMinusAmount, 0.0),
                              vec4(0.0, 0.0, 0.0, 1.0));
+            vColorOffset = vec4(0.0);
             break;
         }
         case 3: {
             // HueRotate
             float c = cos(vAmount * 0.01745329251);
             float s = sin(vAmount * 0.01745329251);
             vColorMat = mat4(vec4(lumR + oneMinusLumR * c - lumR * s, lumR - lumR * c + 0.143 * s, lumR - lumR * c - oneMinusLumR * s, 0.0),
                             vec4(lumG - lumG * c - lumG * s, lumG + oneMinusLumG * c + 0.140 * s, lumG - lumG * c + lumG * s, 0.0),
                             vec4(lumB - lumB * c + oneMinusLumB * s, lumB - lumB * c - 0.283 * s, lumB + oneMinusLumB * c + lumB * s, 0.0),
                             vec4(0.0, 0.0, 0.0, 1.0));
+            vColorOffset = vec4(0.0);
             break;
         }
         case 5: {
             // Saturate
             vColorMat = mat4(vec4(oneMinusAmount * lumR + vAmount, oneMinusAmount * lumR, oneMinusAmount * lumR, 0.0),
                              vec4(oneMinusAmount * lumG, oneMinusAmount * lumG + vAmount, oneMinusAmount * lumG, 0.0),
                              vec4(oneMinusAmount * lumB, oneMinusAmount * lumB, oneMinusAmount * lumB + vAmount, 0.0),
                              vec4(0.0, 0.0, 0.0, 1.0));
+            vColorOffset = vec4(0.0);
             break;
         }
         case 6: {
             // Sepia
             vColorMat = mat4(vec4(0.393 + 0.607 * oneMinusAmount, 0.349 - 0.349 * oneMinusAmount, 0.272 - 0.272 * oneMinusAmount, 0.0),
                              vec4(0.769 - 0.769 * oneMinusAmount, 0.686 + 0.314 * oneMinusAmount, 0.534 - 0.534 * oneMinusAmount, 0.0),
                              vec4(0.189 - 0.189 * oneMinusAmount, 0.168 - 0.168 * oneMinusAmount, 0.131 + 0.869 * oneMinusAmount, 0.0),
                              vec4(0.0, 0.0, 0.0, 1.0));
+            vColorOffset = vec4(0.0);
             break;
         }
+        case 10: {
+          // Color Matrix
+          const int dataOffset = 2; // Offset in GPU cache where matrix starts
+          vec4 data[8] = fetch_from_resource_cache_8(ci.user_data2);
+
+          vColorMat = mat4(data[dataOffset], data[dataOffset + 1],
+                           data[dataOffset + 2], data[dataOffset + 3]);
+          vColorOffset = data[dataOffset + 4];
+          break;
+        }
     }
 
 
     gl_Position = uTransform * vec4(local_pos, ci.z, 1.0);
 }
 #endif
 
 #ifdef WR_FRAGMENT_SHADER
@@ -132,15 +147,15 @@ void main(void) {
             break;
         case 7:
             oFragColor = Brightness(Cs, vAmount);
             break;
         case 8:
             oFragColor = Opacity(Cs, vAmount);
             break;
         default:
-            oFragColor = vColorMat * Cs;
+            oFragColor = vColorMat * Cs + vColorOffset;
     }
 
     // Pre-multiply the alpha into the output value.
     oFragColor.rgb *= oFragColor.a;
 }
 #endif
--- a/gfx/webrender/src/batch.rs
+++ b/gfx/webrender/src/batch.rs
@@ -1038,29 +1038,30 @@ impl AlphaBatcher {
                                                     FilterOp::Grayscale(amount) => (2, amount),
                                                     FilterOp::HueRotate(angle) => (3, angle),
                                                     FilterOp::Invert(amount) => (4, amount),
                                                     FilterOp::Saturate(amount) => (5, amount),
                                                     FilterOp::Sepia(amount) => (6, amount),
                                                     FilterOp::Brightness(amount) => (7, amount),
                                                     FilterOp::Opacity(_, amount) => (8, amount),
                                                     FilterOp::DropShadow(..) => unreachable!(),
+                                                    FilterOp::ColorMatrix(_) => (10, 0.0),
                                                 };
 
                                                 let amount = (amount * 65535.0).round() as i32;
                                                 let batch = self.batch_list.get_suitable_batch(key, &item_bounding_rect);
 
                                                 let instance = CompositePrimitiveInstance::new(
                                                     task_address,
                                                     src_task_address,
                                                     RenderTaskAddress(0),
                                                     filter_mode,
                                                     amount,
                                                     z,
-                                                    0,
+                                                    prim_cache_address.as_int(),
                                                     0,
                                                 );
 
                                                 batch.push(PrimitiveInstance::from(instance));
                                             }
                                         }
                                     }
                                     PictureCompositeMode::MixBlend(mode) => {
--- a/gfx/webrender/src/clip.rs
+++ b/gfx/webrender/src/clip.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::{BorderRadius, ClipMode, ComplexClipRegion, DeviceIntRect, ImageMask, ImageRendering};
-use api::{LayerPoint, LayerRect, LayoutPoint, LayoutVector2D, LocalClip};
-use api::{DevicePixelScale, LayerToWorldTransform};
+use api::{BorderRadius, ClipMode, ComplexClipRegion, DeviceIntRect, DevicePixelScale, ImageMask};
+use api::{ImageRendering, LayerRect, LayerToWorldTransform, LayoutPoint, LayoutVector2D};
+use api::LocalClip;
 use border::{BorderCornerClipSource, ensure_no_corner_overlap};
 use ellipse::Ellipse;
 use freelist::{FreeList, FreeListHandle, WeakFreeListHandle};
 use gpu_cache::{GpuCache, GpuCacheHandle, ToGpuBlocks};
 use prim_store::{ClipData, ImageMaskData};
 use resource_cache::ResourceCache;
 use util::{MaxRect, MatrixHelpers, calculate_screen_bounding_rect, extract_inner_rect_safe};
 
@@ -107,32 +107,16 @@ impl ClipSource {
     ) -> ClipSource {
         ensure_no_corner_overlap(&mut radii, &rect);
         ClipSource::RoundedRectangle(
             rect,
             radii,
             clip_mode,
         )
     }
-
-    pub fn contains(&self, point: &LayerPoint) -> bool {
-        // We currently do not handle all BorderCorners, because they aren't used for
-        // ClipScrollNodes and this method is only used during hit testing.
-        match self {
-            &ClipSource::Rectangle(ref rectangle) => rectangle.contains(point),
-            &ClipSource::RoundedRectangle(rect, radii, ClipMode::Clip) =>
-                rounded_rectangle_contains_point(point, &rect, &radii),
-            &ClipSource::RoundedRectangle(rect, radii, ClipMode::ClipOut) =>
-                !rounded_rectangle_contains_point(point, &rect, &radii),
-            &ClipSource::Image(mask) => mask.rect.contains(point),
-            &ClipSource::BorderCorner(_) =>
-                unreachable!("Tried to call contains on a BorderCornerr."),
-        }
-    }
-
 }
 
 #[derive(Debug)]
 pub struct ClipSources {
     pub clips: Vec<(ClipSource, GpuCacheHandle)>,
     pub local_inner_rect: LayerRect,
     pub local_outer_rect: Option<LayerRect>
 }
@@ -317,20 +301,20 @@ impl Contains for LocalClip {
 }
 
 impl Contains for ComplexClipRegion {
     fn contains(&self, point: &LayoutPoint) -> bool {
         rounded_rectangle_contains_point(point, &self.rect, &self.radii)
     }
 }
 
-fn rounded_rectangle_contains_point(point: &LayoutPoint,
-                                    rect: &LayerRect,
-                                    radii: &BorderRadius)
-                                    -> bool {
+pub fn rounded_rectangle_contains_point(point: &LayoutPoint,
+                                        rect: &LayerRect,
+                                        radii: &BorderRadius)
+                                        -> bool {
     if !rect.contains(point) {
         return false;
     }
 
     let top_left_center = rect.origin + radii.top_left.to_vector();
     if top_left_center.x > point.x && top_left_center.y > point.y &&
        !Ellipse::new(radii.top_left).contains(*point - top_left_center.to_vector()) {
         return false;
--- a/gfx/webrender/src/clip_scroll_node.rs
+++ b/gfx/webrender/src/clip_scroll_node.rs
@@ -1,16 +1,16 @@
 /* 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::{ClipId, DevicePixelScale, ExternalScrollId, IdType, LayerPixel, LayerPoint, LayerRect};
+use api::{ClipId, DevicePixelScale, ExternalScrollId, LayerPixel, LayerPoint, LayerRect};
 use api::{LayerSize, LayerToWorldTransform, LayerTransform, LayerVector2D, LayoutTransform};
 use api::{LayoutVector2D, PipelineId, PropertyBinding, ScrollClamping, ScrollEventPhase};
-use api::{ScrollLocation, ScrollSensitivity, StickyOffsetBounds, WorldPoint};
+use api::{ScrollLocation, ScrollNodeIdType, ScrollSensitivity, StickyOffsetBounds, WorldPoint};
 use clip::{ClipSourcesHandle, ClipStore};
 use clip_scroll_tree::{CoordinateSystemId, TransformUpdateState};
 use euclid::SideOffsets2D;
 use geometry::ray_intersects_rect;
 use gpu_cache::GpuCache;
 use gpu_types::{ClipScrollNodeIndex, ClipScrollNodeData};
 use render_task::{ClipChain, ClipWorkItem};
 use resource_cache::ResourceCache;
@@ -763,20 +763,20 @@ impl ClipScrollNode {
 
     pub fn is_overscrolling(&self) -> bool {
         match self.node_type {
             NodeType::ScrollFrame(ref state) => state.overscroll_amount() != LayerVector2D::zero(),
             _ => false,
         }
     }
 
-    pub fn matches_id(&self, node_id: ClipId, id_to_match: IdType) -> bool {
+    pub fn matches_id(&self, node_id: ClipId, id_to_match: ScrollNodeIdType) -> bool {
         let external_id = match id_to_match {
-            IdType::ExternalScrollId(id) => id,
-            IdType::ClipId(clip_id) => return node_id == clip_id,
+            ScrollNodeIdType::ExternalScrollId(id) => id,
+            ScrollNodeIdType::ClipId(clip_id) => return node_id == clip_id,
         };
 
         match self.node_type {
             NodeType::ScrollFrame(info) if info.external_id == Some(external_id) => true,
             _ => false,
         }
     }
 }
--- a/gfx/webrender/src/clip_scroll_tree.rs
+++ b/gfx/webrender/src/clip_scroll_tree.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::{ClipChainId, ClipId, DeviceIntRect, DevicePixelScale, ExternalScrollId, IdType};
-use api::{LayerPoint, LayerRect, LayerToWorldTransform, LayerVector2D, LayoutTransform};
-use api::{PipelineId, PropertyBinding, ScrollClamping, ScrollEventPhase, ScrollLocation};
+use api::{ClipChainId, ClipId, DeviceIntRect, DevicePixelScale, ExternalScrollId, LayerPoint};
+use api::{LayerRect, LayerToWorldTransform, LayerVector2D, LayoutTransform, PipelineId};
+use api::{PropertyBinding, ScrollClamping, ScrollEventPhase, ScrollLocation, ScrollNodeIdType};
 use api::{ScrollNodeState, WorldPoint};
 use clip::ClipStore;
 use clip_scroll_node::{ClipScrollNode, NodeType, ScrollFrameInfo, StickyFrameInfo};
 use gpu_cache::GpuCache;
 use gpu_types::{ClipScrollNodeIndex, ClipScrollNodeData};
 use internal_types::{FastHashMap, FastHashSet};
 use print_tree::{PrintTree, PrintTreePrinter};
 use render_task::ClipChain;
@@ -55,17 +55,17 @@ pub struct ClipScrollTree {
     /// A Vec of all descriptors that describe ClipChains in the order in which they are
     /// encountered during display list flattening. ClipChains are expected to never be
     /// the children of ClipChains later in the list.
     clip_chains_descriptors: Vec<ClipChainDescriptor>,
 
     /// A HashMap of built ClipChains that are described by `clip_chains_descriptors`.
     pub clip_chains: FastHashMap<ClipChainId, ClipChain>,
 
-    pub pending_scroll_offsets: FastHashMap<IdType, (LayerPoint, ScrollClamping)>,
+    pub pending_scroll_offsets: FastHashMap<ScrollNodeIdType, (LayerPoint, ScrollClamping)>,
 
     /// The ClipId of the currently scrolling node. Used to allow the same
     /// node to scroll even if a touch operation leaves the boundaries of that node.
     pub currently_scrolling_node_id: Option<ClipId>,
 
     /// The current frame id, used for giving a unique id to all new dynamically
     /// added frames and clips. The ClipScrollTree increments this by one every
     /// time a new dynamic frame is created.
@@ -176,70 +176,16 @@ impl ClipScrollTree {
         })
     }
 
     pub fn find_scrolling_node_at_point(&self, cursor: &WorldPoint) -> ClipId {
         self.find_scrolling_node_at_point_in_node(cursor, self.root_reference_frame_id())
             .unwrap_or(self.topmost_scrolling_node_id())
     }
 
-    pub fn is_point_clipped_in_for_node(
-        &self,
-        point: WorldPoint,
-        node_id: &ClipId,
-        cache: &mut FastHashMap<ClipId, Option<LayerPoint>>,
-        clip_store: &ClipStore
-    ) -> bool {
-        if let Some(point) = cache.get(node_id) {
-            return point.is_some();
-        }
-
-        let node = self.nodes.get(node_id).unwrap();
-        let parent_clipped_in = match node.parent {
-            None => true, // This is the root node.
-            Some(ref parent_id) => {
-                self.is_point_clipped_in_for_node(point, parent_id, cache, clip_store)
-            }
-        };
-
-        if !parent_clipped_in {
-            cache.insert(*node_id, None);
-            return false;
-        }
-
-        let transform = node.world_viewport_transform;
-        let transformed_point = match transform.inverse() {
-            Some(inverted) => inverted.transform_point2d(&point),
-            None => {
-                cache.insert(*node_id, None);
-                return false;
-            }
-        };
-
-        let point_in_layer = transformed_point - node.local_viewport_rect.origin.to_vector();
-        let clip_sources_handle = match node.node_type {
-            NodeType::Clip(ref clip_sources_handle) => clip_sources_handle,
-            _ => {
-                cache.insert(*node_id, Some(point_in_layer));
-                return true;
-            }
-        };
-
-        for &(ref clip, _) in clip_store.get(&clip_sources_handle).clips() {
-            if !clip.contains(&transformed_point) {
-                cache.insert(*node_id, None);
-                return false;
-            }
-        }
-
-        cache.insert(*node_id, Some(point_in_layer));
-
-        true
-    }
-
     pub fn get_scroll_node_state(&self) -> Vec<ScrollNodeState> {
         let mut result = vec![];
         for node in self.nodes.values() {
             if let NodeType::ScrollFrame(info) = node.node_type {
                 if let Some(id) = info.external_id {
                     result.push(ScrollNodeState { id, scroll_offset: info.offset })
                 }
             }
@@ -265,17 +211,22 @@ impl ClipScrollTree {
         }
 
         self.pipelines_to_discard.clear();
         self.clip_chains.clear();
         self.clip_chains_descriptors.clear();
         scroll_states
     }
 
-    pub fn scroll_node(&mut self, origin: LayerPoint, id: IdType, clamp: ScrollClamping) -> bool {
+    pub fn scroll_node(
+        &mut self,
+        origin: LayerPoint,
+        id: ScrollNodeIdType,
+        clamp: ScrollClamping
+    ) -> bool {
         for (clip_id, node) in &mut self.nodes {
             if node.matches_id(*clip_id, id) {
                 return node.set_scroll_origin(&origin, clamp);
             }
         }
 
         self.pending_scroll_offsets.insert(id, (origin, clamp));
         false
@@ -488,38 +439,35 @@ impl ClipScrollTree {
         for (_, node) in &mut self.nodes {
             node.tick_scrolling_bounce_animation()
         }
     }
 
     pub fn finalize_and_apply_pending_scroll_offsets(&mut self, old_states: ScrollStates) {
         for (clip_id, node) in &mut self.nodes {
             let external_id = match node.node_type {
-                NodeType::ScrollFrame(info) if info.external_id.is_some() => info.external_id,
+                NodeType::ScrollFrame(info) => info.external_id,
                 _ => None,
             };
 
             if let Some(external_id) = external_id {
                 if let Some(scrolling_state) = old_states.get(&external_id) {
                     node.apply_old_scrolling_state(scrolling_state);
                 }
-            }
+
 
-            let id = IdType::ClipId(*clip_id);
-            if let Some((offset, clamping)) = self.pending_scroll_offsets.remove(&id) {
-                node.set_scroll_origin(&offset, clamping);
-            }
-
-            if let Some(external_id) = external_id {
-                let id = IdType::ExternalScrollId(external_id);
+                let id = external_id.into();
                 if let Some((offset, clamping)) = self.pending_scroll_offsets.remove(&id) {
                     node.set_scroll_origin(&offset, clamping);
                 }
             }
 
+            if let Some((offset, clamping)) = self.pending_scroll_offsets.remove(&clip_id.into()) {
+                node.set_scroll_origin(&offset, clamping);
+            }
         }
     }
 
     pub fn generate_new_clip_id(&mut self, pipeline_id: PipelineId) -> ClipId {
         let new_id = ClipId::DynamicallyAddedNode(self.current_new_node_item, pipeline_id);
         self.current_new_node_item += 1;
         new_id
     }
@@ -662,27 +610,16 @@ impl ClipScrollTree {
     }
 
     pub fn print_with<T: PrintTreePrinter>(&self, clip_store: &ClipStore, pt: &mut T) {
         if !self.nodes.is_empty() {
             self.print_node(&self.root_reference_frame_id, pt, clip_store);
         }
     }
 
-    pub fn make_node_relative_point_absolute(
-        &self,
-        pipeline_id: Option<PipelineId>,
-        point: &LayerPoint
-    ) -> WorldPoint {
-        pipeline_id.and_then(|id| self.nodes.get(&ClipId::root_reference_frame(id)))
-                   .map(|node| node.world_viewport_transform.transform_point2d(point))
-                   .unwrap_or_else(|| WorldPoint::new(point.x, point.y))
-
-    }
-
     pub fn get_clip_chain(&self, id: &ClipId) -> Option<&ClipChain> {
         match id {
             &ClipId::ClipChain(clip_chain_id) => Some(&self.clip_chains[&clip_chain_id]),
             _ => self.nodes[id].clip_chain.as_ref(),
         }
     }
 
 }
--- a/gfx/webrender/src/frame.rs
+++ b/gfx/webrender/src/frame.rs
@@ -1,26 +1,27 @@
 
 /* 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::{BuiltDisplayListIter, ClipAndScrollInfo, ClipId, ColorF, ComplexClipRegion};
 use api::{DevicePixelScale, DeviceUintRect, DeviceUintSize, DisplayItemRef, DocumentLayer, Epoch};
-use api::{ExternalScrollId, FilterOp, IdType, ImageDisplayItem, ItemRange, LayerPoint};
-use api::{LayerPrimitiveInfo, LayerRect, LayerSize, LayerVector2D, LayoutSize, LocalClip};
-use api::{PipelineId, ScrollClamping, ScrollEventPhase, ScrollLocation, ScrollNodeState};
-use api::{ScrollPolicy, ScrollSensitivity, SpecificDisplayItem, StackingContext, TileOffset};
-use api::{TransformStyle, WorldPoint};
+use api::{ExternalScrollId, FilterOp, ImageDisplayItem, ItemRange, LayerPoint, LayerPrimitiveInfo};
+use api::{LayerRect, LayerSize, LayerVector2D, LayoutSize, LocalClip, PipelineId, ScrollClamping};
+use api::{ScrollEventPhase, ScrollLocation, ScrollNodeIdType, ScrollNodeState, ScrollPolicy};
+use api::{ScrollSensitivity, SpecificDisplayItem, StackingContext, TileOffset, TransformStyle};
+use api::WorldPoint;
 use clip::ClipRegion;
 use clip_scroll_node::StickyFrameInfo;
 use clip_scroll_tree::{ClipScrollTree, ScrollStates};
 use euclid::rect;
 use frame_builder::{FrameBuilder, FrameBuilderConfig, ScrollbarInfo};
 use gpu_cache::GpuCache;
+use hit_test::HitTester;
 use internal_types::{FastHashMap, FastHashSet, RenderedDocument};
 use profiler::{GpuCacheProfileCounters, TextureCacheProfileCounters};
 use resource_cache::{FontInstanceMap,ResourceCache, TiledImageMap};
 use scene::{Scene, StackingContextHelpers, ScenePipeline, SceneProperties};
 use tiling::{CompositeOps, Frame};
 
 #[derive(Copy, Clone, PartialEq, PartialOrd, Debug, Eq, Ord)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
@@ -976,26 +977,32 @@ impl FrameContext {
         self.pipeline_epoch_map.clear();
 
         // Advance to the next frame.
         self.id.0 += 1;
 
         self.clip_scroll_tree.drain()
     }
 
+    #[cfg(feature = "debugger")]
     pub fn get_clip_scroll_tree(&self) -> &ClipScrollTree {
         &self.clip_scroll_tree
     }
 
     pub fn get_scroll_node_state(&self) -> Vec<ScrollNodeState> {
         self.clip_scroll_tree.get_scroll_node_state()
     }
 
     /// Returns true if the node actually changed position or false otherwise.
-    pub fn scroll_node(&mut self, origin: LayerPoint, id: IdType, clamp: ScrollClamping) -> bool {
+    pub fn scroll_node(
+        &mut self,
+        origin: LayerPoint,
+        id: ScrollNodeIdType,
+        clamp: ScrollClamping
+    ) -> bool {
         self.clip_scroll_tree.scroll_node(origin, id, clamp)
     }
 
     /// Returns true if any nodes actually changed position or false otherwise.
     pub fn scroll(
         &mut self,
         scroll_location: ScrollLocation,
         cursor: WorldPoint,
@@ -1008,17 +1015,17 @@ impl FrameContext {
         self.clip_scroll_tree.tick_scrolling_bounce_animations();
     }
 
     pub fn discard_frame_state_for_pipeline(&mut self, pipeline_id: PipelineId) {
         self.clip_scroll_tree
             .discard_frame_state_for_pipeline(pipeline_id);
     }
 
-    pub fn create(
+    pub fn create_frame_builder(
         &mut self,
         old_builder: FrameBuilder,
         scene: &Scene,
         resource_cache: &mut ResourceCache,
         window_size: DeviceUintSize,
         inner_rect: DeviceUintRect,
         device_pixel_scale: DevicePixelScale,
         output_pipelines: &FastHashSet<PipelineId>,
@@ -1114,27 +1121,30 @@ impl FrameContext {
         resource_cache: &mut ResourceCache,
         gpu_cache: &mut GpuCache,
         pipelines: &FastHashMap<PipelineId, ScenePipeline>,
         device_pixel_scale: DevicePixelScale,
         layer: DocumentLayer,
         pan: WorldPoint,
         texture_cache_profile: &mut TextureCacheProfileCounters,
         gpu_cache_profile: &mut GpuCacheProfileCounters,
-		scene_properties: &SceneProperties,
-    ) -> RenderedDocument {
+        scene_properties: &SceneProperties,
+    ) -> (HitTester, RenderedDocument) {
         let frame = frame_builder.build(
             resource_cache,
             gpu_cache,
             self.id,
             &mut self.clip_scroll_tree,
             pipelines,
             self.window_size,
             device_pixel_scale,
             layer,
             pan,
             texture_cache_profile,
             gpu_cache_profile,
             scene_properties,
         );
-        self.make_rendered_document(frame)
+
+        let hit_tester = frame_builder.create_hit_tester(&self.clip_scroll_tree);
+
+        (hit_tester, self.make_rendered_document(frame))
     }
 }
--- a/gfx/webrender/src/frame_builder.rs
+++ b/gfx/webrender/src/frame_builder.rs
@@ -1,31 +1,32 @@
 /* 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::{AlphaType, BorderDetails, BorderDisplayItem, BuiltDisplayList, ClipAndScrollInfo};
 use api::{ClipId, ColorF, ColorU, DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixelScale};
 use api::{DeviceUintPoint, DeviceUintRect, DeviceUintSize, DocumentLayer, Epoch, ExtendMode};
-use api::{ExternalScrollId, FontRenderMode, GlyphInstance, GlyphOptions, GradientStop};
-use api::{HitTestFlags, HitTestItem, HitTestResult, ImageKey, ImageRendering, ItemRange, ItemTag};
-use api::{LayerPoint, LayerPrimitiveInfo, LayerRect, LayerSize, LayerTransform, LayerVector2D};
-use api::{LayoutTransform, LayoutVector2D, LineOrientation, LineStyle, LocalClip, PipelineId};
-use api::{PremultipliedColorF, PropertyBinding, RepeatMode, ScrollSensitivity, Shadow, TexelRect};
-use api::{TileOffset, TransformStyle, WorldPoint, WorldToLayerTransform, YuvColorSpace, YuvData};
+use api::{ExternalScrollId, FontRenderMode, GlyphInstance, GlyphOptions, GradientStop, ImageKey};
+use api::{ImageRendering, ItemRange, LayerPoint, LayerPrimitiveInfo, LayerRect, LayerSize};
+use api::{LayerTransform, LayerVector2D, LayoutTransform, LayoutVector2D, LineOrientation};
+use api::{LineStyle, LocalClip, PipelineId, PremultipliedColorF, PropertyBinding, RepeatMode};
+use api::{ScrollSensitivity, Shadow, TexelRect, TileOffset, TransformStyle, WorldPoint};
+use api::{WorldToLayerTransform, YuvColorSpace, YuvData};
 use app_units::Au;
 use border::ImageBorderSegment;
-use clip::{ClipRegion, ClipSource, ClipSources, ClipStore, Contains};
+use clip::{ClipRegion, ClipSource, ClipSources, ClipStore};
 use clip_scroll_node::{ClipScrollNode, NodeType};
 use clip_scroll_tree::ClipScrollTree;
 use euclid::{SideOffsets2D, vec2};
 use frame::FrameId;
 use glyph_rasterizer::FontInstance;
 use gpu_cache::GpuCache;
 use gpu_types::{ClipChainRectIndex, ClipScrollNodeData, PictureType};
+use hit_test::{HitTester, HitTestingItem, HitTestingRun};
 use internal_types::{FastHashMap, FastHashSet, RenderPassIndex};
 use picture::{ContentOrigin, PictureCompositeMode, PictureKind, PicturePrimitive, PictureSurface};
 use prim_store::{BrushKind, BrushPrimitive, ImageCacheKey, YuvImagePrimitiveCpu};
 use prim_store::{GradientPrimitiveCpu, ImagePrimitiveCpu, ImageSource, PrimitiveKind};
 use prim_store::{PrimitiveContainer, PrimitiveIndex};
 use prim_store::{PrimitiveStore, RadialGradientPrimitiveCpu};
 use prim_store::{BrushSegmentDescriptor, PrimitiveRun, TextRunPrimitiveCpu};
 use profiler::{FrameProfileCounters, GpuCacheProfileCounters, TextureCacheProfileCounters};
@@ -56,51 +57,33 @@ struct StackingContext {
     /// Allow subpixel AA for text runs on this stacking context.
     /// This is a temporary hack while we don't support subpixel AA
     /// on transparent stacking contexts.
     allow_subpixel_aa: bool,
 
     /// CSS transform-style property.
     transform_style: TransformStyle,
 
-    /// The primitive index for the root Picture primitive
-    /// that this stacking context is mapped to.
-    pic_prim_index: PrimitiveIndex,
+    /// If Some(..), this stacking context establishes a new
+    /// 3d rendering context, and the value is the primitive
+    // index of the 3d context container.
+    rendering_context_3d_prim_index: Option<PrimitiveIndex>,
 }
 
 #[derive(Clone, Copy)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct FrameBuilderConfig {
     pub enable_scrollbars: bool,
     pub default_font_render_mode: FontRenderMode,
     pub debug: bool,
     pub dual_source_blending_is_supported: bool,
     pub dual_source_blending_is_enabled: bool,
 }
 
-#[derive(Debug)]
-pub struct HitTestingItem {
-    rect: LayerRect,
-    clip: LocalClip,
-    tag: ItemTag,
-}
-
-impl HitTestingItem {
-    fn new(tag: ItemTag, info: &LayerPrimitiveInfo) -> HitTestingItem {
-        HitTestingItem {
-            rect: info.rect,
-            clip: info.local_clip,
-            tag: tag,
-        }
-    }
-}
-
-pub struct HitTestingRun(Vec<HitTestingItem>, ClipAndScrollInfo);
-
 /// A builder structure for `tiling::Frame`
 pub struct FrameBuilder {
     screen_rect: DeviceUintRect,
     background_color: Option<ColorF>,
     prim_store: PrimitiveStore,
     pub clip_store: ClipStore,
     hit_testing_runs: Vec<HitTestingRun>,
     pub config: FrameBuilderConfig,
@@ -407,38 +390,82 @@ impl FrameBuilder {
         // Get the transform-style of the parent stacking context,
         // which determines if we *might* need to draw this on
         // an intermediate surface for plane splitting purposes.
         let parent_transform_style = match self.sc_stack.last() {
             Some(sc) => sc.transform_style,
             None => TransformStyle::Flat,
         };
 
-        // If either the parent or this stacking context is preserve-3d
-        // then we are in a 3D context.
-        let is_in_3d_context = composite_ops.count() == 0 &&
-                               (parent_transform_style == TransformStyle::Preserve3D ||
-                                transform_style == TransformStyle::Preserve3D);
+        // If this is preserve-3d *or* the parent is, then this stacking
+        // context is participating in the 3d rendering context. In that
+        // case, hoist the picture up to the 3d rendering context
+        // container, so that it's rendered as a sibling with other
+        // elements in this context.
+        let participating_in_3d_context =
+            composite_ops.count() == 0 &&
+            (parent_transform_style == TransformStyle::Preserve3D ||
+             transform_style == TransformStyle::Preserve3D);
+
+        // If this is participating in a 3d context *and* the
+        // parent was not a 3d context, then this must be the
+        // element that establishes a new 3d context.
+        let establishes_3d_context =
+            participating_in_3d_context &&
+            parent_transform_style == TransformStyle::Flat;
+
+        let rendering_context_3d_prim_index = if establishes_3d_context {
+            // If establishing a 3d context, we need to add a picture
+            // that will be the container for all the planes and any
+            // un-transformed content.
+            let container = PicturePrimitive::new_image(
+                None,
+                false,
+                pipeline_id,
+                current_reference_frame_id,
+                None,
+            );
 
-        // TODO(gw): For now, we don't handle filters and mix-blend-mode when there
-        //           is a 3D rendering context. We can easily do this in the future
-        //           by creating a chain of pictures for the effects, and ensuring
-        //           that the last composited picture is what's used as the input to
-        //           the plane splitting code.
-        let mut parent_pic_prim_index = if is_in_3d_context {
+            let clip_sources = self.clip_store.insert(ClipSources::new(Vec::new()));
+
+            let prim_index = self.prim_store.add_primitive(
+                &LayerRect::zero(),
+                &max_clip,
+                is_backface_visible,
+                clip_sources,
+                None,
+                PrimitiveContainer::Picture(container),
+            );
+
+            let parent_pic_prim_index = *self.picture_stack.last().unwrap();
+            let pic_prim_index = self.prim_store.cpu_metadata[parent_pic_prim_index.0].cpu_prim_index;
+            let pic = &mut self.prim_store.cpu_pictures[pic_prim_index.0];
+            pic.add_primitive(
+                prim_index,
+                clip_and_scroll,
+            );
+
+            self.picture_stack.push(prim_index);
+
+            Some(prim_index)
+        } else {
+            None
+        };
+
+        let mut parent_pic_prim_index = if !establishes_3d_context && participating_in_3d_context {
             // If we're in a 3D context, we will parent the picture
-            // to the first stacking context we find in the stack that
-            // is transform-style: flat. This follows the spec
+            // to the first stacking context we find that is a
+            // 3D rendering context container. This follows the spec
             // by hoisting these items out into the same 3D context
             // for plane splitting.
             self.sc_stack
                 .iter()
                 .rev()
-                .find(|sc| sc.transform_style == TransformStyle::Flat)
-                .map(|sc| sc.pic_prim_index)
+                .find(|sc| sc.rendering_context_3d_prim_index.is_some())
+                .map(|sc| sc.rendering_context_3d_prim_index.unwrap())
                 .unwrap()
         } else {
             *self.picture_stack.last().unwrap()
         };
 
         // For each filter, create a new image with that composite mode.
         for filter in composite_ops.filters.iter().rev() {
             let src_prim = PicturePrimitive::new_image(
@@ -508,31 +535,31 @@ impl FrameBuilder {
 
         // If this stacking context if the root of a pipeline, and the caller
         // has requested it as an output frame, create a render task to isolate it.
         if is_pipeline_root && output_pipelines.contains(&pipeline_id) {
             composite_mode = Some(PictureCompositeMode::Blit);
             frame_output_pipeline_id = Some(pipeline_id);
         }
 
-        if is_in_3d_context {
+        if participating_in_3d_context {
             // TODO(gw): For now, as soon as this picture is in
             //           a 3D context, we draw it to an intermediate
             //           surface and apply plane splitting. However,
             //           there is a large optimization opportunity here.
             //           During culling, we can check if there is actually
             //           perspective present, and skip the plane splitting
             //           completely when that is not the case.
             composite_mode = Some(PictureCompositeMode::Blit);
         }
 
         // Add picture for this actual stacking context contents to render into.
         let sc_prim = PicturePrimitive::new_image(
             composite_mode,
-            is_in_3d_context,
+            participating_in_3d_context,
             pipeline_id,
             current_reference_frame_id,
             frame_output_pipeline_id,
         );
 
         let sc_clip_sources = self.clip_store.insert(ClipSources::new(Vec::new()));
         let sc_prim_index = self.prim_store.add_primitive(
             &LayerRect::zero(),
@@ -561,32 +588,37 @@ impl FrameBuilder {
         // Push the SC onto the stack, so we know how to handle things in
         // pop_stacking_context.
         let sc = StackingContext {
             composite_ops,
             is_backface_visible,
             pipeline_id,
             allow_subpixel_aa,
             transform_style,
-            // TODO(gw): This is not right when filters are present (but we
-            //           don't handle that right now, per comment above).
-            pic_prim_index: sc_prim_index,
+            rendering_context_3d_prim_index,
         };
 
         self.sc_stack.push(sc);
     }
 
     pub fn pop_stacking_context(&mut self) {
         let sc = self.sc_stack.pop().unwrap();
 
-        // Remove the picture for this stacking contents.
-        self.picture_stack.pop().expect("bug");
+        // Always pop at least the main picture for this stacking context.
+        let mut pop_count = 1;
 
         // Remove the picture for any filter/mix-blend-mode effects.
-        for _ in 0 .. sc.composite_ops.count() {
+        pop_count += sc.composite_ops.count();
+
+        // Remove the 3d context container if created
+        if sc.rendering_context_3d_prim_index.is_some() {
+            pop_count += 1;
+        }
+
+        for _ in 0 .. pop_count {
             self.picture_stack.pop().expect("bug: mismatched picture stack");
         }
 
         // By the time the stacking context stack is empty, we should
         // also have cleared the picture stack.
         if self.sc_stack.is_empty() {
             self.picture_stack.pop().expect("bug: picture stack invalid");
             debug_assert!(self.picture_stack.is_empty());
@@ -1538,85 +1570,16 @@ impl FrameBuilder {
         self.add_primitive(
             clip_and_scroll,
             info,
             Vec::new(),
             PrimitiveContainer::YuvImage(prim_cpu),
         );
     }
 
-    pub fn hit_test(
-        &self,
-        clip_scroll_tree: &ClipScrollTree,
-        pipeline_id: Option<PipelineId>,
-        point: WorldPoint,
-        flags: HitTestFlags
-    ) -> HitTestResult {
-        let point = if flags.contains(HitTestFlags::POINT_RELATIVE_TO_PIPELINE_VIEWPORT) {
-            let point = LayerPoint::new(point.x, point.y);
-            clip_scroll_tree.make_node_relative_point_absolute(pipeline_id, &point)
-        } else {
-            point
-        };
-
-        let mut node_cache = FastHashMap::default();
-        let mut result = HitTestResult::default();
-        for &HitTestingRun(ref items, ref clip_and_scroll) in self.hit_testing_runs.iter().rev() {
-            let scroll_node = &clip_scroll_tree.nodes[&clip_and_scroll.scroll_node_id];
-            match (pipeline_id, scroll_node.pipeline_id) {
-                (Some(id), node_id) if node_id != id => continue,
-                _ => {},
-            }
-
-            let transform = scroll_node.world_content_transform;
-            let point_in_layer = match transform.inverse() {
-                Some(inverted) => inverted.transform_point2d(&point),
-                None => continue,
-            };
-
-            let mut clipped_in = false;
-            for item in items.iter().rev() {
-                if !item.rect.contains(&point_in_layer) || !item.clip.contains(&point_in_layer) {
-                    continue;
-                }
-
-                let clip_id = &clip_and_scroll.clip_node_id();
-                if !clipped_in {
-                    clipped_in = clip_scroll_tree.is_point_clipped_in_for_node(point,
-                                                                               clip_id,
-                                                                               &mut node_cache,
-                                                                               &self.clip_store);
-                    if !clipped_in {
-                        break;
-                    }
-                }
-
-                let root_pipeline_reference_frame_id =
-                    ClipId::root_reference_frame(clip_id.pipeline_id());
-                let point_in_viewport = match node_cache.get(&root_pipeline_reference_frame_id) {
-                    Some(&Some(point)) => point,
-                    _ => unreachable!("Hittest target's root reference frame not hit."),
-                };
-
-                result.items.push(HitTestItem {
-                    pipeline: clip_and_scroll.clip_node_id().pipeline_id(),
-                    tag: item.tag,
-                    point_in_viewport,
-                    point_relative_to_item: point_in_layer - item.rect.origin.to_vector(),
-                });
-                if !flags.contains(HitTestFlags::FIND_ALL) {
-                    return result;
-                }
-            }
-        }
-
-        result.items.dedup();
-        result
-    }
-
     /// Compute the contribution (bounding rectangles, and resources) of layers and their
     /// primitives in screen space.
     fn build_layer_screen_rects_and_cull_layers(
         &mut self,
         clip_scroll_tree: &ClipScrollTree,
         pipelines: &FastHashMap<PipelineId, ScenePipeline>,
         resource_cache: &mut ResourceCache,
         gpu_cache: &mut GpuCache,
@@ -1859,9 +1822,17 @@ impl FrameBuilder {
             clip_chain_local_clip_rects,
             render_tasks,
             deferred_resolves,
             gpu_cache_frame_id,
             has_been_rendered: false,
             has_texture_cache_tasks,
         }
     }
+
+    pub fn create_hit_tester(&mut self, clip_scroll_tree: &ClipScrollTree) -> HitTester {
+        HitTester::new(
+            &self.hit_testing_runs,
+            &clip_scroll_tree,
+            &self.clip_store
+        )
+    }
 }
new file mode 100644
--- /dev/null
+++ b/gfx/webrender/src/hit_test.rs
@@ -0,0 +1,246 @@
+/* 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::{BorderRadius, ClipAndScrollInfo, ClipId, ClipMode, HitTestFlags, HitTestItem};
+use api::{HitTestResult, ItemTag, LayerPoint, LayerPrimitiveInfo, LayerRect};
+use api::{LayerToWorldTransform, LocalClip, PipelineId, WorldPoint};
+use clip::{ClipSource, ClipStore, Contains, rounded_rectangle_contains_point};
+use clip_scroll_node::{ClipScrollNode, NodeType};
+use clip_scroll_tree::ClipScrollTree;
+use internal_types::FastHashMap;
+
+/// A copy of important clip scroll node data to use during hit testing. This a copy of
+/// data from the ClipScrollTree that will persist as a new frame is under construction,
+/// allowing hit tests consistent with the currently rendered frame.
+pub struct HitTestClipScrollNode {
+    /// This node's parent in the ClipScrollTree. This is used to ensure that we can
+    /// travel up the tree of nodes.
+    parent: Option<ClipId>,
+
+    /// A particular point must be inside all of these regions to be considered clipped in
+    /// for the purposes of a hit test.
+    regions: Vec<HitTestRegion>,
+
+    /// World transform for content transformed by this node.
+    world_content_transform: LayerToWorldTransform,
+
+    /// World viewport transform for content transformed by this node.
+    world_viewport_transform: LayerToWorldTransform,
+
+    /// Origin of the viewport of the node, used to calculate node-relative positions.
+    node_origin: LayerPoint,
+}
+
+#[derive(Clone)]
+pub struct HitTestingItem {
+    rect: LayerRect,
+    clip: LocalClip,
+    tag: ItemTag,
+}
+
+impl HitTestingItem {
+    pub fn new(tag: ItemTag, info: &LayerPrimitiveInfo) -> HitTestingItem {
+        HitTestingItem {
+            rect: info.rect,
+            clip: info.local_clip,
+            tag: tag,
+        }
+    }
+}
+
+#[derive(Clone)]
+pub struct HitTestingRun(pub Vec<HitTestingItem>, pub ClipAndScrollInfo);
+
+enum HitTestRegion {
+    Rectangle(LayerRect),
+    RoundedRectangle(LayerRect, BorderRadius, ClipMode),
+}
+
+impl HitTestRegion {
+    pub fn contains(&self, point: &LayerPoint) -> bool {
+        match self {
+            &HitTestRegion::Rectangle(ref rectangle) => rectangle.contains(point),
+            &HitTestRegion::RoundedRectangle(rect, radii, ClipMode::Clip) =>
+                rounded_rectangle_contains_point(point, &rect, &radii),
+            &HitTestRegion::RoundedRectangle(rect, radii, ClipMode::ClipOut) =>
+                !rounded_rectangle_contains_point(point, &rect, &radii),
+        }
+    }
+}
+
+pub struct HitTester {
+    runs: Vec<HitTestingRun>,
+    nodes: FastHashMap<ClipId, HitTestClipScrollNode>,
+}
+
+impl HitTester {
+    pub fn new(
+        runs: &Vec<HitTestingRun>,
+        clip_scroll_tree: &ClipScrollTree,
+        clip_store: &ClipStore
+    ) -> HitTester {
+        let mut hit_tester = HitTester {
+            runs: runs.clone(),
+            nodes: FastHashMap::default(),
+        };
+        hit_tester.read_clip_scroll_tree(clip_scroll_tree, clip_store);
+        hit_tester
+    }
+
+    fn read_clip_scroll_tree(
+        &mut self,
+        clip_scroll_tree: &ClipScrollTree,
+        clip_store: &ClipStore
+    ) {
+        self.nodes.clear();
+
+        for (id, node) in &clip_scroll_tree.nodes {
+            self.nodes.insert(*id, HitTestClipScrollNode {
+                parent: node.parent,
+                regions: get_regions_for_clip_scroll_node(node, clip_store),
+                world_content_transform: node.world_content_transform,
+                world_viewport_transform: node.world_viewport_transform,
+                node_origin: node.local_viewport_rect.origin,
+            });
+        }
+    }
+
+    pub fn is_point_clipped_in_for_node(
+        &self,
+        point: WorldPoint,
+        node_id: &ClipId,
+        cache: &mut FastHashMap<ClipId, Option<LayerPoint>>,
+    ) -> bool {
+        if let Some(point) = cache.get(node_id) {
+            return point.is_some();
+        }
+
+        let node = self.nodes.get(node_id).unwrap();
+        let parent_clipped_in = match node.parent {
+            None => true, // This is the root node.
+            Some(ref parent_id) => {
+                self.is_point_clipped_in_for_node(point, parent_id, cache)
+            }
+        };
+
+        if !parent_clipped_in {
+            cache.insert(*node_id, None);
+            return false;
+        }
+
+        let transform = node.world_viewport_transform;
+        let transformed_point = match transform.inverse() {
+            Some(inverted) => inverted.transform_point2d(&point),
+            None => {
+                cache.insert(*node_id, None);
+                return false;
+            }
+        };
+
+        let point_in_layer = transformed_point - node.node_origin.to_vector();
+        for region in &node.regions {
+            if !region.contains(&transformed_point) {
+                cache.insert(*node_id, None);
+                return false;
+            }
+        }
+
+        cache.insert(*node_id, Some(point_in_layer));
+        true
+    }
+
+    pub fn make_node_relative_point_absolute(
+        &self,
+        pipeline_id: Option<PipelineId>,
+        point: &LayerPoint
+    ) -> WorldPoint {
+        pipeline_id.and_then(|id| self.nodes.get(&ClipId::root_reference_frame(id)))
+                   .map(|node| node.world_viewport_transform.transform_point2d(point))
+                   .unwrap_or_else(|| WorldPoint::new(point.x, point.y))
+
+    }
+
+    pub fn hit_test(
+        &self,
+        pipeline_id: Option<PipelineId>,
+        point: WorldPoint,
+        flags: HitTestFlags
+    ) -> HitTestResult {
+        let point = if flags.contains(HitTestFlags::POINT_RELATIVE_TO_PIPELINE_VIEWPORT) {
+            self.make_node_relative_point_absolute(pipeline_id, &LayerPoint::new(point.x, point.y))
+        } else {
+            point
+        };
+
+        let mut node_cache = FastHashMap::default();
+        let mut result = HitTestResult::default();
+        for &HitTestingRun(ref items, ref clip_and_scroll) in self.runs.iter().rev() {
+            let scroll_node = &self.nodes[&clip_and_scroll.scroll_node_id];
+            match (pipeline_id, clip_and_scroll.scroll_node_id.pipeline_id()) {
+                (Some(id), node_id) if node_id != id => continue,
+                _ => {},
+            }
+
+            let transform = scroll_node.world_content_transform;
+            let point_in_layer = match transform.inverse() {
+                Some(inverted) => inverted.transform_point2d(&point),
+                None => continue,
+            };
+
+            let mut clipped_in = false;
+            for item in items.iter().rev() {
+                if !item.rect.contains(&point_in_layer) || !item.clip.contains(&point_in_layer) {
+                    continue;
+                }
+
+                let clip_id = &clip_and_scroll.clip_node_id();
+                if !clipped_in {
+                    clipped_in = self.is_point_clipped_in_for_node(point, clip_id, &mut node_cache);
+                    if !clipped_in {
+                        break;
+                    }
+                }
+
+                let point_in_viewport = node_cache
+                    .get(&ClipId::root_reference_frame(clip_id.pipeline_id()))
+                    .expect("Hittest target's root reference frame not hit.")
+                    .expect("Hittest target's root reference frame not hit.");
+
+                result.items.push(HitTestItem {
+                    pipeline: clip_and_scroll.clip_node_id().pipeline_id(),
+                    tag: item.tag,
+                    point_in_viewport,
+                    point_relative_to_item: point_in_layer - item.rect.origin.to_vector(),
+                });
+                if !flags.contains(HitTestFlags::FIND_ALL) {
+                    return result;
+                }
+            }
+        }
+
+        result.items.dedup();
+        result
+    }
+}
+
+fn get_regions_for_clip_scroll_node(
+    node: &ClipScrollNode,
+    clip_store: &ClipStore
+) -> Vec<HitTestRegion> {
+    let clips = match node.node_type {
+        NodeType::Clip(ref handle) => clip_store.get(handle).clips(),
+        _ => return Vec::new(),
+    };
+
+    clips.iter().map(|ref source| {
+        match source.0 {
+            ClipSource::Rectangle(ref rect) => HitTestRegion::Rectangle(*rect),
+            ClipSource::RoundedRectangle(ref rect, ref radii, ref mode) =>
+                HitTestRegion::RoundedRectangle(*rect, *radii, *mode),
+            ClipSource::Image(ref mask) => HitTestRegion::Rectangle(mask.rect),
+            ClipSource::BorderCorner(_) =>
+                unreachable!("Didn't expect to hit test against BorderCorner"),
+        }
+    }).collect()
+}
--- a/gfx/webrender/src/lib.rs
+++ b/gfx/webrender/src/lib.rs
@@ -72,16 +72,17 @@ mod frame_builder;
 mod freelist;
 #[cfg(any(target_os = "macos", target_os = "windows"))]
 mod gamma_lut;
 mod geometry;
 mod glyph_cache;
 mod glyph_rasterizer;
 mod gpu_cache;
 mod gpu_types;
+mod hit_test;
 mod internal_types;
 mod picture;
 mod prim_store;
 mod print_tree;
 mod profiler;
 mod query;
 mod record;
 mod render_backend;
--- a/gfx/webrender/src/picture.rs
+++ b/gfx/webrender/src/picture.rs
@@ -590,19 +590,31 @@ impl PicturePrimitive {
 
                 self.surface = Some(PictureSurface::TextureCache(cache_item));
             }
         }
     }
 
     pub fn write_gpu_blocks(&self, request: &mut GpuDataRequest) {
         match self.kind {
-            PictureKind::TextShadow { .. } |
-            PictureKind::Image { .. } => {
-                request.push([0.0; 4]);
+            PictureKind::TextShadow { .. } => {
+                request.push([0.0; 4])
+            }
+            PictureKind::Image { composite_mode, .. } => {
+                match composite_mode {
+                    Some(PictureCompositeMode::Filter(FilterOp::ColorMatrix(m))) => {
+                        // When we start pushing Image pictures through the brush path
+                        // this may need to change as the number of GPU blocks written will
+                        // need to be determinate.
+                        for i in 0..5 {
+                            request.push([m[i], m[i+5], m[i+10], m[i+15]]);
+                        }
+                    },
+                    _ => request.push([0.0; 4]),
+                }
             }
             PictureKind::BoxShadow { color, .. } => {
                 request.push(color.premultiplied());
             }
         }
     }
 
     pub fn target_kind(&self) -> RenderTargetKind {
--- a/gfx/webrender/src/render_backend.rs
+++ b/gfx/webrender/src/render_backend.rs
@@ -1,29 +1,30 @@
 /* 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::{ApiMsg, BuiltDisplayList, ClearCache, DebugCommand};
 #[cfg(feature = "debugger")]
 use api::{BuiltDisplayListIter, SpecificDisplayItem};
 use api::{DeviceIntPoint, DevicePixelScale, DeviceUintPoint, DeviceUintRect, DeviceUintSize};
-use api::{DocumentId, DocumentLayer, DocumentMsg, HitTestFlags, HitTestResult};
-use api::{IdNamespace, PipelineId, RenderNotifier, WorldPoint};
-use api::channel::{MsgReceiver, MsgSender, PayloadReceiver, PayloadReceiverHelperMethods};
+use api::{DocumentId, DocumentLayer, DocumentMsg, HitTestResult, IdNamespace, PipelineId};
+use api::RenderNotifier;
+use api::channel::{MsgReceiver, PayloadReceiver, PayloadReceiverHelperMethods};
 use api::channel::{PayloadSender, PayloadSenderHelperMethods};
 #[cfg(feature = "capture")]
 use api::CaptureBits;
 #[cfg(feature = "replay")]
 use api::CapturedDocument;
 #[cfg(feature = "debugger")]
 use debug_server;
 use frame::FrameContext;
 use frame_builder::{FrameBuilder, FrameBuilderConfig};
 use gpu_cache::GpuCache;
+use hit_test::HitTester;
 use internal_types::{DebugOutput, FastHashMap, FastHashSet, RenderedDocument, ResultMsg};
 use profiler::{BackendProfileCounters, IpcProfileCounters, ResourceProfileCounters};
 use record::ApiRecordingReceiver;
 use resource_cache::ResourceCache;
 #[cfg(feature = "replay")]
 use resource_cache::PlainCacheOwn;
 #[cfg(any(feature = "capture", feature = "replay"))]
 use resource_cache::PlainResources;
@@ -77,16 +78,20 @@ struct Document {
     // the first frame would produce inconsistent rendering results, because
     // scroll events are not necessarily received in deterministic order.
     render_on_scroll: Option<bool>,
     // A helper flag to prevent any hit-tests from happening between calls
     // to build_scene and rendering the document. In between these two calls,
     // hit-tests produce inconsistent results because the clip_scroll_tree
     // is out of sync with the display list.
     render_on_hittest: bool,
+
+    /// A data structure to allow hit testing against rendered frames. This is updated
+    /// every time we produce a fully rendered frame.
+    hit_tester: Option<HitTester>,
 }
 
 impl Document {
     pub fn new(
         config: FrameBuilderConfig,
         window_size: DeviceUintSize,
         layer: DocumentLayer,
         enable_render_on_scroll: bool,
@@ -108,22 +113,23 @@ impl Document {
                 pinch_zoom_factor: 1.0,
                 device_pixel_ratio: default_device_pixel_ratio,
             },
             frame_ctx: FrameContext::new(config),
             frame_builder: Some(FrameBuilder::empty()),
             output_pipelines: FastHashSet::default(),
             render_on_scroll,
             render_on_hittest: false,
+            hit_tester: None,
         }
     }
 
     fn build_scene(&mut self, resource_cache: &mut ResourceCache) {
         // this code is why we have `Option`, which is never `None`
-        let frame_builder = self.frame_ctx.create(
+        let frame_builder = self.frame_ctx.create_frame_builder(
             self.frame_builder.take().unwrap(),
             &self.scene,
             resource_cache,
             self.view.window_size,
             self.view.inner_rect,
             self.view.accumulated_scale_factor(),
             &self.output_pipelines,
         );
@@ -133,65 +139,64 @@ impl Document {
     fn render(
         &mut self,
         resource_cache: &mut ResourceCache,
         gpu_cache: &mut GpuCache,
         resource_profile: &mut ResourceProfileCounters,
     ) -> RenderedDocument {
         let accumulated_scale_factor = self.view.accumulated_scale_factor();
         let pan = self.view.pan.to_f32() / accumulated_scale_factor;
-        self.frame_ctx.build_rendered_document(
+        let (hit_tester, rendered_document) = self.frame_ctx.build_rendered_document(
             self.frame_builder.as_mut().unwrap(),
             resource_cache,
             gpu_cache,
             &self.scene.pipelines,
             accumulated_scale_factor,
             self.view.layer,
             pan,
             &mut resource_profile.texture_cache,
             &mut resource_profile.gpu_cache,
             &self.scene.properties,
-        )
+        );
+
+        self.hit_tester = Some(hit_tester);
+
+        rendered_document
     }
 }
 
-type HitTestQuery = (Option<PipelineId>, WorldPoint, HitTestFlags, MsgSender<HitTestResult>);
-
 struct DocumentOps {
     scroll: bool,
     build: bool,
     render: bool,
     composite: bool,
-    queries: Vec<HitTestQuery>,
 }
 
 impl DocumentOps {
     fn nop() -> Self {
         DocumentOps {
             scroll: false,
             build: false,
             render: false,
             composite: false,
-            queries: vec![],
         }
     }
 
     fn build() -> Self {
         DocumentOps {
             build: true,
             ..DocumentOps::nop()
         }
     }
 
-    fn combine(&mut self, mut other: Self) {
+    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;
-        self.queries.extend(other.queries.drain(..));
     }
 }
 
 /// 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))]
@@ -407,26 +412,24 @@ impl RenderBackend {
                 DocumentOps {
                     scroll: true,
                     render: should_render,
                     composite: should_render,
                     ..DocumentOps::nop()
                 }
             }
             DocumentMsg::HitTest(pipeline_id, point, flags, tx) => {
-                // note that we render without compositing here; we only
-                // need to render so we have an updated clip-scroll tree
-                // for handling the hit-test queries. Doing a composite
-                // here (without having being explicitly requested) causes
-                // Gecko assertion failures.
-                DocumentOps {
-                    render: doc.render_on_hittest,
-                    queries: vec![(pipeline_id, point, flags, tx)],
-                    ..DocumentOps::nop()
-                }
+
+                let result = match doc.hit_tester {
+                    Some(ref hit_tester) => hit_tester.hit_test(pipeline_id, point, flags),
+                    None => HitTestResult { items: Vec::new() },
+                };
+
+                tx.send(result).unwrap();
+                DocumentOps::nop()
             }
             DocumentMsg::ScrollNodeWithId(origin, id, clamp) => {
                 profile_scope!("ScrollNodeWithScrollId");
 
                 let should_render = doc.frame_ctx.scroll_node(origin, id, clamp)
                     && doc.render_on_scroll == Some(true);
 
                 DocumentOps {
@@ -723,26 +726,16 @@ impl RenderBackend {
             self.result_tx.send(msg).unwrap();
             profile_counters.reset();
             doc.render_on_hittest = false;
         }
 
         if op.render || op.scroll {
             self.notifier.new_document_ready(document_id, op.scroll, op.composite);
         }
-
-        for (pipeline_id, point, flags, tx) in op.queries {
-            profile_scope!("HitTest");
-            let cst = doc.frame_ctx.get_clip_scroll_tree();
-            let result = doc.frame_builder
-                .as_ref()
-                .unwrap()
-                .hit_test(cst, pipeline_id, point, flags);
-            tx.send(result).unwrap();
-        }
     }
 
     #[cfg(not(feature = "debugger"))]
     fn get_docs_for_debugger(&self) -> String {
         String::new()
     }
 
     #[cfg(feature = "debugger")]
@@ -978,16 +971,17 @@ impl RenderBackend {
             let mut doc = Document {
                 scene,
                 view,
                 frame_ctx: FrameContext::new(self.frame_config.clone()),
                 frame_builder: Some(FrameBuilder::empty()),
                 output_pipelines: FastHashSet::default(),
                 render_on_scroll: None,
                 render_on_hittest: false,
+                hit_tester: None,
             };
 
             let frame_name = format!("frame-{}-{}", (id.0).0, id.1);
             let render_doc = match CaptureConfig::deserialize::<Frame, _>(root, frame_name) {
                 Some(frame) => {
                     info!("\tloaded a built frame with {} passes", frame.passes.len());
                     doc.frame_ctx.make_rendered_document(frame)
                 }
--- a/gfx/webrender/src/scene.rs
+++ b/gfx/webrender/src/scene.rs
@@ -166,17 +166,18 @@ impl FilterOpHelpers for FilterOp {
             FilterOp::Blur(..) |
             FilterOp::Brightness(..) |
             FilterOp::Contrast(..) |
             FilterOp::Grayscale(..) |
             FilterOp::HueRotate(..) |
             FilterOp::Invert(..) |
             FilterOp::Saturate(..) |
             FilterOp::Sepia(..) |
-            FilterOp::DropShadow(..) => true,
+            FilterOp::DropShadow(..) |
+            FilterOp::ColorMatrix(..) => true,
             FilterOp::Opacity(_, amount) => {
                 amount > OPACITY_EPSILON
             }
         }
     }
 
     fn is_noop(&self) -> bool {
         match *self {
@@ -186,16 +187,22 @@ impl FilterOpHelpers for FilterOp {
             FilterOp::Grayscale(amount) => amount == 0.0,
             FilterOp::HueRotate(amount) => amount == 0.0,
             FilterOp::Invert(amount) => amount == 0.0,
             FilterOp::Opacity(_, amount) => amount >= 1.0,
             FilterOp::Saturate(amount) => amount == 1.0,
             FilterOp::Sepia(amount) => amount == 0.0,
             FilterOp::DropShadow(offset, blur, _) => {
                 offset.x == 0.0 && offset.y == 0.0 && blur == 0.0
+            },
+            FilterOp::ColorMatrix(matrix) => {
+                matrix == [1.0, 0.0, 0.0, 0.0, 0.0,
+                           0.0, 1.0, 0.0, 0.0, 0.0,
+                           0.0, 0.0, 1.0, 0.0, 0.0,
+                           0.0, 0.0, 0.0, 1.0, 0.0]
             }
         }
     }
 }
 
 pub trait StackingContextHelpers {
     fn mix_blend_mode_for_compositing(&self) -> Option<MixBlendMode>;
     fn filter_ops_for_compositing(
--- a/gfx/webrender_api/src/api.rs
+++ b/gfx/webrender_api/src/api.rs
@@ -249,17 +249,17 @@ impl Transaction {
         phase: ScrollEventPhase,
     ) {
         self.ops.push(DocumentMsg::Scroll(scroll_location, cursor, phase));
     }
 
     pub fn scroll_node_with_id(
         &mut self,
         origin: LayoutPoint,
-        id: IdType,
+        id: ScrollNodeIdType,
         clamp: ScrollClamping,
     ) {
         self.ops.push(DocumentMsg::ScrollNodeWithId(origin, id, clamp));
     }
 
     pub fn set_page_zoom(&mut self, page_zoom: ZoomFactor) {
         self.ops.push(DocumentMsg::SetPageZoom(page_zoom));
     }
@@ -379,29 +379,47 @@ pub enum DocumentMsg {
     RemovePipeline(PipelineId),
     EnableFrameOutput(PipelineId, bool),
     SetWindowParameters {
         window_size: DeviceUintSize,
         inner_rect: DeviceUintRect,
         device_pixel_ratio: f32,
     },
     Scroll(ScrollLocation, WorldPoint, ScrollEventPhase),
-    ScrollNodeWithId(LayoutPoint, IdType, ScrollClamping),
+    ScrollNodeWithId(LayoutPoint, ScrollNodeIdType, ScrollClamping),
     TickScrollingBounce,
     GetScrollNodeState(MsgSender<Vec<ScrollNodeState>>),
     GenerateFrame,
     UpdateDynamicProperties(DynamicProperties),
 }
 
 #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
-pub enum IdType {
+pub enum ScrollNodeIdType {
     ExternalScrollId(ExternalScrollId),
     ClipId(ClipId),
 }
 
+impl From<ExternalScrollId> for ScrollNodeIdType {
+    fn from(id: ExternalScrollId) -> Self {
+        ScrollNodeIdType::ExternalScrollId(id)
+    }
+}
+
+impl From<ClipId> for ScrollNodeIdType {
+    fn from(id: ClipId) -> Self {
+        ScrollNodeIdType::ClipId(id)
+    }
+}
+
+impl<'a> From<&'a ClipId> for ScrollNodeIdType {
+    fn from(id: &'a ClipId) -> Self {
+        ScrollNodeIdType::ClipId(*id)
+    }
+}
+
 impl fmt::Debug for DocumentMsg {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         f.write_str(match *self {
             DocumentMsg::SetDisplayList { .. } => "DocumentMsg::SetDisplayList",
             DocumentMsg::HitTest(..) => "DocumentMsg::HitTest",
             DocumentMsg::SetPageZoom(..) => "DocumentMsg::SetPageZoom",
             DocumentMsg::SetPinchZoom(..) => "DocumentMsg::SetPinchZoom",
             DocumentMsg::SetPan(..) => "DocumentMsg::SetPan",
--- a/gfx/webrender_api/src/display_item.rs
+++ b/gfx/webrender_api/src/display_item.rs
@@ -499,16 +499,17 @@ pub enum FilterOp {
     Contrast(f32),
     Grayscale(f32),
     HueRotate(f32),
     Invert(f32),
     Opacity(PropertyBinding<f32>, f32),
     Saturate(f32),
     Sepia(f32),
     DropShadow(LayoutVector2D, f32, ColorF),
+    ColorMatrix([f32; 20]),
 }
 
 #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
 pub struct IframeDisplayItem {
     pub pipeline_id: PipelineId,
 }
 
 #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
--- a/gfx/webrender_bindings/revision.txt
+++ b/gfx/webrender_bindings/revision.txt
@@ -1,1 +1,1 @@
-e772c3cb8ea0a35e6477e9dc8dd2144e2de87b56
+08e49649f1fc9cacff4e10ebc390babcea752236
--- a/gfx/wrench/src/wrench.rs
+++ b/gfx/wrench/src/wrench.rs
@@ -517,21 +517,17 @@ impl Wrench {
         }
         // TODO(nical) - Need to separate the set_display_list from the scrolling
         // operations into separate transactions for mysterious -but probably related
         // to the other comment below- reasons.
         self.api.send_transaction(self.document_id, txn);
 
         let mut txn = Transaction::new();
         for (id, offset) in scroll_offsets {
-            txn.scroll_node_with_id(
-                *offset,
-                IdType::ClipId(*id),
-                ScrollClamping::NoClamping,
-            );
+            txn.scroll_node_with_id(*offset, id.into(), ScrollClamping::NoClamping);
         }
         // TODO(nical) - Wrench does not notify frames when there was scrolling
         // in the transaction (See RenderNotifier implementations). If we don't
         // generate a frame after scrolling, wrench just stops and some tests
         // will time out.
         // I suppose this was to avoid taking the snapshot after scrolling if
         // there was other updates coming in a subsequent messages but it's very
         // error-prone with transactions.
--- a/gfx/wrench/src/yaml_frame_reader.rs
+++ b/gfx/wrench/src/yaml_frame_reader.rs
@@ -10,17 +10,17 @@ use image::GenericImage;
 use parse_function::parse_function;
 use premultiply::premultiply;
 use std::collections::HashMap;
 use std::fs::File;
 use std::io::Read;
 use std::path::{Path, PathBuf};
 use webrender::api::*;
 use wrench::{FontDescriptor, Wrench, WrenchThing};
-use yaml_helper::{StringEnum, YamlHelper};
+use yaml_helper::{StringEnum, YamlHelper, make_perspective};
 use yaml_rust::{Yaml, YamlLoader};
 use {BLACK_COLOR, PLATFORM_DEFAULT_FACE_NAME, WHITE_COLOR};
 
 fn rsrc_path(item: &Yaml, aux_dir: &PathBuf) -> PathBuf {
     let filename = item.as_str().unwrap();
     let mut file = aux_dir.clone();
     file.push(filename);
     file
@@ -1415,29 +1415,37 @@ impl YamlFrameReader {
         wrench: &mut Wrench,
         yaml: &Yaml,
         is_root: bool,
         info: &mut LayoutPrimitiveInfo,
     ) {
         let default_bounds = LayoutRect::new(LayoutPoint::zero(), wrench.window_size_f32());
         let bounds = yaml["bounds"].as_rect().unwrap_or(default_bounds);
 
-        // TODO(gw): Add support for specifying the transform origin in yaml.
-        let transform_origin = LayoutPoint::new(
+        let default_transform_origin = LayoutPoint::new(
             bounds.origin.x + bounds.size.width * 0.5,
             bounds.origin.y + bounds.size.height * 0.5,
         );
 
+        let transform_origin = yaml["transform-origin"]
+            .as_point()
+            .unwrap_or(default_transform_origin);
+
+        let perspective_origin = yaml["perspective-origin"]
+            .as_point()
+            .unwrap_or(default_transform_origin);
+
         let transform = yaml["transform"]
             .as_transform(&transform_origin)
             .map(|transform| transform.into());
 
-        // TODO(gw): Support perspective-origin.
         let perspective = match yaml["perspective"].as_f32() {
-            Some(value) if value != 0.0 => Some(LayoutTransform::create_perspective(value as f32)),
+            Some(value) if value != 0.0 => {
+                Some(make_perspective(perspective_origin, value as f32))
+            }
             Some(_) => None,
             _ => yaml["perspective"].as_matrix4d(),
         };
 
         let transform_style = yaml["transform-style"]
             .as_transform_style()
             .unwrap_or(TransformStyle::Flat);
         let mix_blend_mode = yaml["mix-blend-mode"]
--- a/gfx/wrench/src/yaml_frame_writer.rs
+++ b/gfx/wrench/src/yaml_frame_writer.rs
@@ -210,16 +210,19 @@ fn write_sc(parent: &mut Table, sc: &Sta
             FilterOp::Saturate(x) => { filters.push(Yaml::String(format!("saturate({})", x))) }
             FilterOp::Sepia(x) => { filters.push(Yaml::String(format!("sepia({})", x))) }
             FilterOp::DropShadow(offset, blur, color) => {
                 filters.push(Yaml::String(format!("drop-shadow([{},{}],{},[{}])",
                                                   offset.x, offset.y,
                                                   blur,
                                                   color_to_string(color))))
             }
+            FilterOp::ColorMatrix(matrix) => {
+                filters.push(Yaml::String(format!("color-matrix({:?})", matrix)))
+            }
         }
     }
 
     yaml_node(parent, "filters", Yaml::Array(filters));
 }
 
 #[cfg(target_os = "windows")]
 fn native_font_handle_to_yaml(
--- a/gfx/wrench/src/yaml_helper.rs
+++ b/gfx/wrench/src/yaml_helper.rs
@@ -153,16 +153,26 @@ fn make_rotation(
 
     let theta = 2.0f32 * f32::consts::PI - degrees.to_radians();
     let transform =
         LayoutTransform::identity().pre_rotate(axis_x, axis_y, axis_z, Angle::radians(theta));
 
     pre_transform.pre_mul(&transform).pre_mul(&post_transform)
 }
 
+pub fn make_perspective(
+    origin: LayoutPoint,
+    perspective: f32,
+) -> LayoutTransform {
+    let pre_transform = LayoutTransform::create_translation(origin.x, origin.y, 0.0);
+    let post_transform = LayoutTransform::create_translation(-origin.x, -origin.y, -0.0);
+    let transform = LayoutTransform::create_perspective(perspective);
+    pre_transform.pre_mul(&transform).pre_mul(&post_transform)
+}
+
 // Create a skew matrix, specified in degrees.
 fn make_skew(
     skew_x: f32,
     skew_y: f32,
 ) -> LayoutTransform {
     let alpha = Angle::radians(skew_x.to_radians());
     let beta = Angle::radians(skew_y.to_radians());
     LayoutTransform::create_skew(alpha, beta)
@@ -555,16 +565,22 @@ impl YamlHelper for Yaml {
                 ("drop-shadow", ref args, _) if args.len() == 3 => {
                     let str = format!("---\noffset: {}\nblur-radius: {}\ncolor: {}\n", args[0], args[1], args[2]);
                     let mut yaml_doc = YamlLoader::load_from_str(&str).expect("Failed to parse drop-shadow");
                     let yaml = yaml_doc.pop().unwrap();
                     Some(FilterOp::DropShadow(yaml["offset"].as_vector().unwrap(),
                                               yaml["blur-radius"].as_f32().unwrap(),
                                               yaml["color"].as_colorf().unwrap()))
                 }
+                ("color-matrix", ref args, _) if args.len() == 20 => {
+                    let m: Vec<f32> = args.iter().map(|f| f.parse().unwrap()).collect();
+                    let mut matrix: [f32; 20] = [0.0; 20];
+                    matrix.clone_from_slice(&m);
+                    Some(FilterOp::ColorMatrix(matrix))
+                }
                 (_, _, _) => None,
             }
         } else {
             None
         }
     }
 
     fn as_vec_filter_op(&self) -> Option<Vec<FilterOp>> {