Bug 1524797 - WR: rework the relative transform query on a clip-scroll tree r=gw
authorDzmitry Malyshau <dmalyshau@mozilla.com>
Tue, 12 Feb 2019 15:05:01 +0000
changeset 458740 87dac4d84c6b
parent 458739 9a69e1fea9be
child 458741 9571930df748
push id35548
push useropoprus@mozilla.com
push dateWed, 13 Feb 2019 09:48:26 +0000
treeherdermozilla-central@93e37c529818 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgw
bugs1524797
milestone67.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 1524797 - WR: rework the relative transform query on a clip-scroll tree r=gw This change rewords get_relative_transform and assotiated pieces of logic, so that we flatten the transforms at preserve-3d context boundaries. It addresses a problem found by 1524797 but doesn't resolve the bug yet (!). There is another issue likely contributing here, and we can treat this PR as WIP and not merge until the case is completely resolved. Differential Revision: https://phabricator.services.mozilla.com/D19254
gfx/wr/webrender/src/clip.rs
gfx/wr/webrender/src/clip_scroll_tree.rs
gfx/wr/webrender/src/gpu_types.rs
gfx/wr/webrender/src/picture.rs
gfx/wr/webrender/src/prim_store/mod.rs
gfx/wr/webrender/src/spatial_node.rs
gfx/wr/webrender/src/util.rs
gfx/wr/wrench/reftests/transforms/content-offset.yaml
gfx/wr/wrench/reftests/transforms/flatten-preserve-3d-root-ref.yaml
gfx/wr/wrench/reftests/transforms/flatten-preserve-3d-root.yaml
gfx/wr/wrench/reftests/transforms/reftest.list
--- a/gfx/wr/webrender/src/clip.rs
+++ b/gfx/wr/webrender/src/clip.rs
@@ -1301,17 +1301,19 @@ fn add_clip_node_to_current_chain(
             .accumulate(&clip_spatial_node.coordinate_system_relative_scale_offset);
         ClipSpaceConversion::ScaleOffset(scale_offset)
     } else {
         match clip_scroll_tree.get_relative_transform(
             node.spatial_node_index,
             ROOT_SPATIAL_NODE_INDEX,
         ) {
             None => return true,
-            Some(xf) => ClipSpaceConversion::Transform(xf.with_destination::<WorldPixel>()),
+            Some(relative) => ClipSpaceConversion::Transform(
+                relative.flattened.with_destination::<WorldPixel>(),
+            ),
         }
     };
 
     // If we can convert spaces, try to reduce the size of the region
     // requested, and cache the conversion information for the next step.
     if let Some(clip_rect) = clip_node.item.get_local_clip_rect(node.local_pos) {
         match conversion {
             ClipSpaceConversion::Local => {
--- a/gfx/wr/webrender/src/clip_scroll_tree.rs
+++ b/gfx/wr/webrender/src/clip_scroll_tree.rs
@@ -4,19 +4,19 @@
 
 use api::{ExternalScrollId, LayoutPoint, LayoutRect, LayoutVector2D, ReferenceFrameKind};
 use api::{PipelineId, ScrollClamping, ScrollNodeState, ScrollLocation, ScrollSensitivity};
 use api::{LayoutSize, LayoutTransform, PropertyBinding, TransformStyle, WorldPoint};
 use gpu_types::TransformPalette;
 use internal_types::{FastHashMap, FastHashSet};
 use print_tree::{PrintableTree, PrintTree, PrintTreePrinter};
 use scene::SceneProperties;
-use smallvec::SmallVec;
 use spatial_node::{ScrollFrameInfo, SpatialNode, SpatialNodeType, StickyFrameInfo, ScrollFrameKind};
-use util::{LayoutToWorldFastTransform, ScaleOffset};
+use std::ops;
+use util::{LayoutToWorldFastTransform, MatrixHelpers, ScaleOffset};
 
 pub type ScrollStates = FastHashMap<ExternalScrollId, ScrollFrameInfo>;
 
 /// An id that identifies coordinate systems in the ClipScrollTree. Each
 /// coordinate system has an id and those ids will be shared when the coordinates
 /// system are the same or are in the same axis-aligned space. This allows
 /// for optimizing mask generation.
 #[derive(Debug, Copy, Clone, PartialEq)]
@@ -24,23 +24,27 @@ pub type ScrollStates = FastHashMap<Exte
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct CoordinateSystemId(pub u32);
 
 /// A node in the hierarchy of coordinate system
 /// transforms.
 #[derive(Debug)]
 pub struct CoordinateSystem {
     pub transform: LayoutTransform,
+    /// True if the Z component of the resulting transform, when ascending
+    /// from children to a parent, needs to be flattened upon passing this system.
+    pub is_flatten_root: bool,
     pub parent: Option<CoordinateSystemId>,
 }
 
 impl CoordinateSystem {
     fn root() -> Self {
         CoordinateSystem {
             transform: LayoutTransform::identity(),
+            is_flatten_root: true,
             parent: None,
         }
     }
 }
 
 #[derive(Debug, Copy, Clone, Eq, Hash, MallocSizeOf, PartialEq, PartialOrd, Ord)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
@@ -58,16 +62,38 @@ impl SpatialNodeIndex {
 }
 
 impl CoordinateSystemId {
     pub fn root() -> Self {
         CoordinateSystemId(0)
     }
 }
 
+#[derive(Debug, Copy, Clone)]
+pub enum VisibleFace {
+    Front,
+    Back,
+}
+
+impl Default for VisibleFace {
+    fn default() -> Self {
+        VisibleFace::Front
+    }
+}
+
+impl ops::Not for VisibleFace {
+    type Output = Self;
+    fn not(self) -> Self {
+        match self {
+            VisibleFace::Front => VisibleFace::Back,
+            VisibleFace::Back => VisibleFace::Front,
+        }
+    }
+}
+
 pub struct ClipScrollTree {
     /// Nodes which determine the positions (offsets and transforms) for primitives
     /// and clips.
     pub spatial_nodes: Vec<SpatialNode>,
 
     /// A list of transforms that establish new coordinate systems.
     /// Spatial nodes only establish a new coordinate system when
     /// they have a transform that is not a simple 2d translation.
@@ -98,76 +124,96 @@ pub struct TransformUpdateState {
 
     /// Scale and offset from the coordinate system that started this compatible coordinate system.
     pub coordinate_system_relative_scale_offset: ScaleOffset,
 
     /// True if this node is transformed by an invertible transform.  If not, display items
     /// transformed by this node will not be displayed and display items not transformed by this
     /// node will not be clipped by clips that are transformed by this node.
     pub invertible: bool,
+
+    /// True if this node is a part of Preserve3D hierarchy.
+    pub preserves_3d: bool,
+}
+
+/// A processed relative transform between two nodes in the clip-scroll tree.
+#[derive(Debug, Default)]
+pub struct RelativeTransform {
+    /// The flattened transform, produces Z = 0 at all times.
+    pub flattened: LayoutTransform,
+    /// Visible face of the original transform.
+    pub visible_face: VisibleFace,
+    /// True if the original transform had perspective.
+    pub is_perspective: bool,
 }
 
 impl ClipScrollTree {
     pub fn new() -> Self {
         ClipScrollTree {
             spatial_nodes: Vec::new(),
             coord_systems: Vec::new(),
             pending_scroll_offsets: FastHashMap::default(),
             pipelines_to_discard: FastHashSet::default(),
             nodes_to_update: Vec::new(),
         }
     }
 
-    /// Calculate the relative transform from `from_node_index`
-    /// to `to_node_index`. It's assumed that `from_node_index`
-    /// is an ancestor or a descendant of `to_node_index`. This method will
-    /// panic if that invariant isn't true!
+    /// Calculate the relative transform from `child_index` to `parent_index`.
+    /// This method will panic if the nodes are not connected!
     pub fn get_relative_transform(
         &self,
-        from_node_index: SpatialNodeIndex,
-        to_node_index: SpatialNodeIndex,
-    ) -> Option<LayoutTransform> {
-        let from_node = &self.spatial_nodes[from_node_index.0 as usize];
-        let to_node = &self.spatial_nodes[to_node_index.0 as usize];
-
-        let (child, parent, inverse) = if from_node_index.0 > to_node_index.0 {
-            (from_node, to_node, false)
-        } else {
-            (to_node, from_node, true)
-        };
+        child_index: SpatialNodeIndex,
+        parent_index: SpatialNodeIndex,
+    ) -> Option<RelativeTransform> {
+        assert!(child_index.0 >= parent_index.0);
+        let child = &self.spatial_nodes[child_index.0 as usize];
+        let parent = &self.spatial_nodes[parent_index.0 as usize];
 
         let mut coordinate_system_id = child.coordinate_system_id;
-        let mut nodes: SmallVec<[_; 16]> = SmallVec::new();
+        let mut transform = child.coordinate_system_relative_scale_offset.to_transform();
+        let mut visible_face = VisibleFace::Front;
+        let mut is_perspective = false;
 
         while coordinate_system_id != parent.coordinate_system_id {
-            nodes.push(coordinate_system_id);
             let coord_system = &self.coord_systems[coordinate_system_id.0 as usize];
             coordinate_system_id = coord_system.parent.expect("invalid parent!");
+            transform = transform.post_mul(&coord_system.transform);
+            // we need to update the associated parameters of a transform in two cases:
+            // 1) when the flattening happens, so that we don't lose that original 3D aspects
+            // 2) when we reach the end of iteration, so that our result is up to date
+            if coord_system.is_flatten_root || coordinate_system_id == parent.coordinate_system_id {
+                visible_face = if transform.is_backface_visible() {
+                    VisibleFace::Back
+                } else {
+                    VisibleFace::Front
+                };
+                is_perspective = transform.has_perspective_component();
+            }
+            if coord_system.is_flatten_root {
+                //Note: this function makes the transform to ignore the Z coordinate of inputs
+                // *even* for computing the X and Y coordinates of the output.
+                //transform = transform.project_to_2d();
+                transform.m13 = 0.0;
+                transform.m23 = 0.0;
+                transform.m33 = 0.0;
+                transform.m43 = 0.0;
+            }
         }
 
-        nodes.reverse();
-
-        let mut transform = parent.coordinate_system_relative_scale_offset
-                                  .inverse()
-                                  .to_transform();
-
-        for node in nodes {
-            let coord_system = &self.coord_systems[node.0 as usize];
-            transform = transform.pre_mul(&coord_system.transform);
-        }
-
-        let transform = transform.pre_mul(
-            &child.coordinate_system_relative_scale_offset.to_transform(),
+        transform = transform.post_mul(
+            &parent.coordinate_system_relative_scale_offset
+                .inverse()
+                .to_transform()
         );
 
-        if inverse {
-            transform.inverse()
-        } else {
-            Some(transform)
-        }
+        Some(RelativeTransform {
+            flattened: transform,
+            visible_face,
+            is_perspective,
+        })
     }
 
     /// Returns true if the spatial node is the same as the parent, or is
     /// a child of the parent.
     pub fn is_same_or_child_of(
         &self,
         spatial_node_index: SpatialNodeIndex,
         parent_spatial_node_index: SpatialNodeIndex,
@@ -299,16 +345,17 @@ impl ClipScrollTree {
         let state = TransformUpdateState {
             parent_reference_frame_transform: LayoutVector2D::new(pan.x, pan.y).into(),
             parent_accumulated_scroll_offset: LayoutVector2D::zero(),
             nearest_scrolling_ancestor_offset: LayoutVector2D::zero(),
             nearest_scrolling_ancestor_viewport: LayoutRect::zero(),
             current_coordinate_system_id: CoordinateSystemId::root(),
             coordinate_system_relative_scale_offset: ScaleOffset::identity(),
             invertible: true,
+            preserves_3d: false,
         };
         debug_assert!(self.nodes_to_update.is_empty());
         self.nodes_to_update.push((root_node_index, state));
 
         while let Some((node_index, mut state)) = self.nodes_to_update.pop() {
             let (previous, following) = self.spatial_nodes.split_at_mut(node_index.0 as usize);
             let node = match following.get_mut(0) {
                 Some(node) => node,
@@ -533,26 +580,26 @@ fn add_reference_frame(
     )
 }
 
 #[cfg(test)]
 fn test_pt(
     px: f32,
     py: f32,
     cst: &ClipScrollTree,
-    from: SpatialNodeIndex,
-    to: SpatialNodeIndex,
+    child: SpatialNodeIndex,
+    parent: SpatialNodeIndex,
     expected_x: f32,
     expected_y: f32,
 ) {
     use euclid::approxeq::ApproxEq;
     const EPSILON: f32 = 0.0001;
 
     let p = LayoutPoint::new(px, py);
-    let m = cst.get_relative_transform(from, to).unwrap();
+    let m = cst.get_relative_transform(child, parent).unwrap().flattened;
     let pt = m.transform_point2d(&p).unwrap();
     assert!(pt.x.approx_eq_eps(&expected_x, &EPSILON) &&
             pt.y.approx_eq_eps(&expected_y, &EPSILON),
             "p: {:?} -> {:?}\nm={:?}",
             p, pt, m,
             );
 }
 
@@ -588,21 +635,18 @@ fn test_cst_simple_translation() {
         Some(child2),
         LayoutTransform::create_translation(200.0, 200.0, 0.0),
         LayoutVector2D::zero(),
     );
 
     cst.update_tree(WorldPoint::zero(), &SceneProperties::new(), None);
 
     test_pt(100.0, 100.0, &cst, child1, root, 200.0, 100.0);
-    test_pt(100.0, 100.0, &cst, root, child1, 0.0, 100.0);
     test_pt(100.0, 100.0, &cst, child2, root, 200.0, 150.0);
-    test_pt(100.0, 100.0, &cst, root, child2, 0.0, 50.0);
     test_pt(100.0, 100.0, &cst, child2, child1, 100.0, 150.0);
-    test_pt(100.0, 100.0, &cst, child1, child2, 100.0, 50.0);
     test_pt(100.0, 100.0, &cst, child3, root, 400.0, 350.0);
 }
 
 #[test]
 fn test_cst_simple_scale() {
     // Basic scale only
 
     let mut cst = ClipScrollTree::new();
@@ -633,24 +677,20 @@ fn test_cst_simple_scale() {
         Some(child2),
         LayoutTransform::create_scale(2.0, 2.0, 1.0),
         LayoutVector2D::zero(),
     );
 
     cst.update_tree(WorldPoint::zero(), &SceneProperties::new(), None);
 
     test_pt(100.0, 100.0, &cst, child1, root, 400.0, 100.0);
-    test_pt(100.0, 100.0, &cst, root, child1, 25.0, 100.0);
     test_pt(100.0, 100.0, &cst, child2, root, 400.0, 200.0);
-    test_pt(100.0, 100.0, &cst, root, child2, 25.0, 50.0);
     test_pt(100.0, 100.0, &cst, child3, root, 800.0, 400.0);
     test_pt(100.0, 100.0, &cst, child2, child1, 100.0, 200.0);
-    test_pt(100.0, 100.0, &cst, child1, child2, 100.0, 50.0);
     test_pt(100.0, 100.0, &cst, child3, child1, 200.0, 400.0);
-    test_pt(100.0, 100.0, &cst, child1, child3, 50.0, 25.0);
 }
 
 #[test]
 fn test_cst_scale_translation() {
     // Scale + translation
 
     let mut cst = ClipScrollTree::new();
 
@@ -688,28 +728,23 @@ fn test_cst_scale_translation() {
         LayoutTransform::create_scale(3.0, 2.0, 1.0),
         LayoutVector2D::zero(),
     );
 
     cst.update_tree(WorldPoint::zero(), &SceneProperties::new(), None);
 
     test_pt(100.0, 100.0, &cst, child1, root, 200.0, 150.0);
     test_pt(100.0, 100.0, &cst, child2, root, 300.0, 450.0);
-    test_pt(100.0, 100.0, &cst, root, child1, 0.0, 50.0);
-    test_pt(100.0, 100.0, &cst, root, child2, 0.0, 12.5);
     test_pt(100.0, 100.0, &cst, child4, root, 1100.0, 450.0);
-    test_pt(1100.0, 450.0, &cst, root, child4, 100.0, 100.0);
 
     test_pt(0.0, 0.0, &cst, child4, child1, 400.0, -400.0);
     test_pt(100.0, 100.0, &cst, child4, child1, 1000.0, 400.0);
     test_pt(100.0, 100.0, &cst, child2, child1, 200.0, 400.0);
-    test_pt(200.0, 400.0, &cst, child1, child2, 100.0, 100.0);
 
     test_pt(100.0, 100.0, &cst, child3, child1, 600.0, 0.0);
-    test_pt(400.0, 300.0, &cst, child1, child3, 0.0, 175.0);
 }
 
 #[test]
 fn test_cst_translation_rotate() {
     // Rotation + translation
     use euclid::Angle;
 
     let mut cst = ClipScrollTree::new();
--- a/gfx/wr/webrender/src/gpu_types.rs
+++ b/gfx/wr/webrender/src/gpu_types.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::{
     DeviceHomogeneousVector, DevicePoint, DeviceSize, DeviceRect,
-    LayoutRect, LayoutToWorldTransform, LayoutTransform,
+    LayoutRect, LayoutToWorldTransform,
     PremultipliedColorF, LayoutToPictureTransform, PictureToLayoutTransform, PicturePixel,
     WorldPixel, WorldToLayoutTransform, LayoutPoint, DeviceVector2D
 };
 use clip_scroll_tree::{ClipScrollTree, ROOT_SPATIAL_NODE_INDEX, SpatialNodeIndex};
 use gpu_cache::{GpuCacheAddress, GpuDataRequest};
 use internal_types::FastHashMap;
 use prim_store::EdgeAaSegmentMask;
 use render_task::RenderTaskAddress;
@@ -467,48 +467,49 @@ impl TransformPalette {
             ROOT_SPATIAL_NODE_INDEX,
             // We know the root picture space == world space
             transform.with_destination::<PicturePixel>(),
         );
     }
 
     fn get_index(
         &mut self,
-        from_index: SpatialNodeIndex,
-        to_index: SpatialNodeIndex,
+        child_index: SpatialNodeIndex,
+        parent_index: SpatialNodeIndex,
         clip_scroll_tree: &ClipScrollTree,
     ) -> usize {
-        if to_index == ROOT_SPATIAL_NODE_INDEX {
-            from_index.0 as usize
-        } else if from_index == to_index {
+        if parent_index == ROOT_SPATIAL_NODE_INDEX {
+            child_index.0 as usize
+        } else if child_index == parent_index {
             0
         } else {
             let key = RelativeTransformKey {
-                from_index,
-                to_index,
+                from_index: child_index,
+                to_index: parent_index,
             };
 
             let metadata = &mut self.metadata;
             let transforms = &mut self.transforms;
 
             *self.map
                 .entry(key)
                 .or_insert_with(|| {
                     let transform = clip_scroll_tree.get_relative_transform(
-                        from_index,
-                        to_index,
+                        child_index,
+                        parent_index,
                     )
-                    .unwrap_or(LayoutTransform::identity())
+                    .unwrap_or_default()
+                    .flattened
                     .with_destination::<PicturePixel>();
 
                     register_transform(
                         metadata,
                         transforms,
-                        from_index,
-                        to_index,
+                        child_index,
+                        parent_index,
                         transform,
                     )
                 })
         }
     }
 
     pub fn get_world_transform(
         &self,
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -5,29 +5,29 @@
 use api::{FilterOp, MixBlendMode, PipelineId, PremultipliedColorF, PictureRect, PicturePoint, WorldPoint};
 use api::{DeviceIntRect, DeviceIntSize, DevicePoint, DeviceRect};
 use api::{LayoutRect, PictureToRasterTransform, LayoutPixel, PropertyBinding, PropertyBindingId};
 use api::{DevicePixelScale, RasterRect, RasterSpace, ColorF, ImageKey, DirtyRect, WorldSize, ClipMode, LayoutSize};
 use api::{PicturePixel, RasterPixel, WorldPixel, WorldRect, ImageFormat, ImageDescriptor, WorldVector2D, LayoutPoint};
 use api::{DebugFlags, DeviceHomogeneousVector, DeviceVector2D};
 use box_shadow::{BLUR_SAMPLE_SCALE};
 use clip::{ClipChainId, ClipChainNode, ClipItem, ClipStore, ClipDataStore, ClipChainStack};
-use clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX, ClipScrollTree, SpatialNodeIndex, CoordinateSystemId};
+use clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX, ClipScrollTree, SpatialNodeIndex, CoordinateSystemId, VisibleFace};
 use debug_colors;
 use device::TextureFilter;
 use euclid::{size2, vec3, TypedPoint2D, TypedScale, TypedSize2D};
 use euclid::approxeq::ApproxEq;
 use frame_builder::{FrameVisibilityContext, FrameVisibilityState};
 use intern::ItemUid;
 use internal_types::{FastHashMap, FastHashSet, PlaneSplitter};
 use frame_builder::{FrameBuildingContext, FrameBuildingState, PictureState, PictureContext};
 use gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
 use gpu_types::{TransformPalette, UvRectKind};
 use plane_split::{Clipper, Polygon, Splitter};
-use prim_store::{PictureIndex, PrimitiveInstance, SpaceMapper, VisibleFace, PrimitiveInstanceKind};
+use prim_store::{PictureIndex, PrimitiveInstance, SpaceMapper, PrimitiveInstanceKind};
 use prim_store::{get_raster_rects, PrimitiveScratchBuffer, VectorKey, PointKey};
 use prim_store::{OpacityBindingStorage, ImageInstanceStorage, OpacityBindingIndex, RectangleKey};
 use print_tree::PrintTreePrinter;
 use render_backend::DataStores;
 use render_task::{ClearMode, RenderTask, RenderTaskCacheEntryHandle, TileBlit};
 use render_task::{RenderTaskId, RenderTaskLocation};
 use resource_cache::ResourceCache;
 use scene::{FilterOpHelpers, SceneProperties};
@@ -781,24 +781,27 @@ impl TileCache {
             //           to see if any other memory should be freed.
             return;
         }
 
         let DeviceIntSize { width: tile_width, height: tile_height, _unit: _ } =
             self.tile_dimensions(frame_context.config.testing);
 
         // Work out the scroll offset to apply to the world reference point.
-        let scroll_transform = frame_context.clip_scroll_tree.get_relative_transform(
-            ROOT_SPATIAL_NODE_INDEX,
-            self.spatial_node_index,
-        ).expect("bug: unable to get scroll transform");
-        let scroll_offset = WorldVector2D::new(
-            scroll_transform.m41,
-            scroll_transform.m42,
-        );
+        let scroll_offset_point = frame_context.clip_scroll_tree
+            .get_relative_transform(
+                self.spatial_node_index,
+                ROOT_SPATIAL_NODE_INDEX,
+            )
+            .expect("bug: unable to get scroll transform")
+            .flattened
+            .inverse_project_2d_origin()
+            .unwrap_or_else(LayoutPoint::zero);
+
+        let scroll_offset = WorldVector2D::new(scroll_offset_point.x, scroll_offset_point.y);
         let scroll_delta = match self.scroll_offset {
             Some(prev) => prev - scroll_offset,
             None => WorldVector2D::zero(),
         };
         self.scroll_offset = Some(scroll_offset);
 
         // Pull any retained tiles from the previous scene.
         let world_offset = if frame_state.retained_tiles.tiles.is_empty() {
@@ -1425,25 +1428,44 @@ impl TileCache {
                     tile.transforms.insert(*spatial_node_index);
                 }
             }
 
             // Update tile transforms
             let mut transform_spatial_nodes: Vec<SpatialNodeIndex> = tile.transforms.drain().collect();
             transform_spatial_nodes.sort();
             for spatial_node_index in transform_spatial_nodes {
-                let xf = frame_context.clip_scroll_tree.get_relative_transform(
-                    self.spatial_node_index,
-                    spatial_node_index,
-                ).expect("BUG: unable to get relative transform");
+                // Note: this is the only place where we don't know beforehand if the tile-affecting
+                // spatial node is below or above the current picture.
+                let inverse_origin = if self.spatial_node_index >= spatial_node_index {
+                    frame_context.clip_scroll_tree
+                        .get_relative_transform(
+                            self.spatial_node_index,
+                            spatial_node_index,
+                        )
+                        .expect("BUG: unable to get relative transform")
+                        .flattened
+                        .transform_point2d(&LayoutPoint::zero())
+                } else {
+                    frame_context.clip_scroll_tree
+                        .get_relative_transform(
+                            spatial_node_index,
+                            self.spatial_node_index,
+                        )
+                        .expect("BUG: unable to get relative transform")
+                        .flattened
+                        .inverse_project_2d_origin()
+                };
                 // Store the result of transforming a fixed point by this
                 // transform.
                 // TODO(gw): This could in theory give incorrect results for a
                 //           primitive behind the near plane.
-                let key = xf.transform_point2d(&LayoutPoint::zero()).unwrap_or(LayoutPoint::zero()).round();
+                let key = inverse_origin
+                    .unwrap_or_else(LayoutPoint::zero)
+                    .round();
                 tile.descriptor.transforms.push(key.into());
             }
 
             // Invalidate if the backing texture was evicted.
             if resource_cache.texture_cache.is_allocated(&tile.handle) {
                 // Request the backing texture so it won't get evicted this frame.
                 // We specifically want to mark the tile texture as used, even
                 // if it's detected not visible below and skipped. This is because
@@ -2665,19 +2687,19 @@ impl PicturePrimitive {
                 _ => {
                     0.0
                 }
             };
 
             // Check if there is perspective, and thus whether a new
             // rasterization root should be established.
             let establishes_raster_root = frame_context.clip_scroll_tree
-                .get_relative_transform(parent_raster_node_index, surface_spatial_node_index)
+                .get_relative_transform(surface_spatial_node_index, parent_raster_node_index)
                 .expect("BUG: unable to get relative transform")
-                .has_perspective_component();
+                .is_perspective;
 
             let surface = SurfaceInfo::new(
                 surface_spatial_node_index,
                 if establishes_raster_root {
                     surface_spatial_node_index
                 } else {
                     parent_raster_node_index
                 },
--- a/gfx/wr/webrender/src/prim_store/mod.rs
+++ b/gfx/wr/webrender/src/prim_store/mod.rs
@@ -9,17 +9,17 @@ use api::{LayoutPoint, LayoutRect, Layou
 use api::{PremultipliedColorF, PropertyBinding, Shadow, DeviceVector2D};
 use api::{WorldPixel, BoxShadowClipMode, WorldRect, LayoutToWorldScale};
 use api::{PicturePixel, RasterPixel, LineStyle, LineOrientation, AuHelpers};
 use api::{LayoutPrimitiveInfo};
 use api::DevicePoint;
 use border::{get_max_scale_for_border, build_border_instances};
 use border::BorderSegmentCacheKey;
 use clip::{ClipStore};
-use clip_scroll_tree::{ClipScrollTree, SpatialNodeIndex, ROOT_SPATIAL_NODE_INDEX};
+use clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX, ClipScrollTree, SpatialNodeIndex, VisibleFace};
 use clip::{ClipDataStore, ClipNodeFlags, ClipChainId, ClipChainInstance, ClipItem};
 use debug_colors;
 use debug_render::DebugItem;
 use display_list_flattener::{AsInstanceKind, CreateShadow, IsVisible};
 use euclid::{SideOffsets2D, TypedTransform3D, TypedRect, TypedScale, TypedSize2D};
 use frame_builder::{FrameBuildingContext, FrameBuildingState, PictureContext, PictureState};
 use frame_builder::{PrimitiveContext, FrameVisibilityContext, FrameVisibilityState};
 use glyph_rasterizer::GlyphKey;
@@ -122,92 +122,74 @@ impl PrimitiveOpacity {
 
     pub fn combine(&self, other: PrimitiveOpacity) -> PrimitiveOpacity {
         PrimitiveOpacity{
             is_opaque: self.is_opaque && other.is_opaque
         }
     }
 }
 
-#[derive(Debug, Copy, Clone)]
-pub enum VisibleFace {
-    Front,
-    Back,
-}
-
-impl ops::Not for VisibleFace {
-    type Output = Self;
-    fn not(self) -> Self {
-        match self {
-            VisibleFace::Front => VisibleFace::Back,
-            VisibleFace::Back => VisibleFace::Front,
-        }
-    }
-}
 
 #[derive(Debug, Clone)]
 pub enum CoordinateSpaceMapping<F, T> {
     Local,
     ScaleOffset(ScaleOffset),
     Transform(TypedTransform3D<f32, F, T>),
 }
 
 impl<F, T> CoordinateSpaceMapping<F, T> {
     pub fn new(
         ref_spatial_node_index: SpatialNodeIndex,
         target_node_index: SpatialNodeIndex,
         clip_scroll_tree: &ClipScrollTree,
-    ) -> Option<Self> {
+    ) -> Option<(Self, VisibleFace)> {
         let spatial_nodes = &clip_scroll_tree.spatial_nodes;
         let ref_spatial_node = &spatial_nodes[ref_spatial_node_index.0 as usize];
         let target_spatial_node = &spatial_nodes[target_node_index.0 as usize];
 
         if ref_spatial_node_index == target_node_index {
-            Some(CoordinateSpaceMapping::Local)
+            Some((CoordinateSpaceMapping::Local, VisibleFace::Front))
         } else if ref_spatial_node.coordinate_system_id == target_spatial_node.coordinate_system_id {
-            Some(CoordinateSpaceMapping::ScaleOffset(
-                ref_spatial_node.coordinate_system_relative_scale_offset
-                    .inverse()
-                    .accumulate(
-                        &target_spatial_node.coordinate_system_relative_scale_offset
-                    )
-            ))
+            let scale_offset = ref_spatial_node.coordinate_system_relative_scale_offset
+                .inverse()
+                .accumulate(&target_spatial_node.coordinate_system_relative_scale_offset);
+            Some((CoordinateSpaceMapping::ScaleOffset(scale_offset), VisibleFace::Front))
         } else {
-            let transform = clip_scroll_tree.get_relative_transform(
-                target_node_index,
-                ref_spatial_node_index,
-            );
-
-            transform.map(|transform| {
-                CoordinateSpaceMapping::Transform(
-                    transform.with_source::<F>().with_destination::<T>()
-                )
-            })
+            clip_scroll_tree
+                .get_relative_transform(target_node_index, ref_spatial_node_index)
+                .map(|relative| (
+                    CoordinateSpaceMapping::Transform(
+                        relative.flattened.with_source::<F>().with_destination::<T>()
+                    ),
+                    relative.visible_face,
+                ))
         }
     }
 }
 
 #[derive(Debug, Clone)]
 pub struct SpaceMapper<F, T> {
     kind: CoordinateSpaceMapping<F, T>,
     pub ref_spatial_node_index: SpatialNodeIndex,
     pub current_target_spatial_node_index: SpatialNodeIndex,
     pub bounds: TypedRect<f32, T>,
+    visible_face: VisibleFace,
 }
 
 impl<F, T> SpaceMapper<F, T> where F: fmt::Debug {
     pub fn new(
         ref_spatial_node_index: SpatialNodeIndex,
         bounds: TypedRect<f32, T>,
     ) -> Self {
         SpaceMapper {
             kind: CoordinateSpaceMapping::Local,
             ref_spatial_node_index,
             current_target_spatial_node_index: ref_spatial_node_index,
             bounds,
+            visible_face: VisibleFace::Front,
         }
     }
 
     pub fn new_with_target(
         ref_spatial_node_index: SpatialNodeIndex,
         target_node_index: SpatialNodeIndex,
         bounds: TypedRect<f32, T>,
         clip_scroll_tree: &ClipScrollTree,
@@ -220,21 +202,24 @@ impl<F, T> SpaceMapper<F, T> where F: fm
     pub fn set_target_spatial_node(
         &mut self,
         target_node_index: SpatialNodeIndex,
         clip_scroll_tree: &ClipScrollTree,
     ) {
         if target_node_index != self.current_target_spatial_node_index {
             self.current_target_spatial_node_index = target_node_index;
 
-            self.kind = CoordinateSpaceMapping::new(
+            let (kind, visible_face) = CoordinateSpaceMapping::new(
                 self.ref_spatial_node_index,
                 target_node_index,
                 clip_scroll_tree,
             ).expect("bug: should have been culled by invalid node");
+
+            self.kind = kind;
+            self.visible_face = visible_face;
         }
     }
 
     pub fn get_transform(&self) -> TypedTransform3D<f32, F, T> {
         match self.kind {
             CoordinateSpaceMapping::Local => {
                 TypedTransform3D::identity()
             }
@@ -279,27 +264,17 @@ impl<F, T> SpaceMapper<F, T> where F: fm
                         None
                     }
                 }
             }
         }
     }
 
     pub fn visible_face(&self) -> VisibleFace {
-        match self.kind {
-            CoordinateSpaceMapping::Local => VisibleFace::Front,
-            CoordinateSpaceMapping::ScaleOffset(_) => VisibleFace::Front,
-            CoordinateSpaceMapping::Transform(ref transform) => {
-                if transform.is_backface_visible() {
-                    VisibleFace::Back
-                } else {
-                    VisibleFace::Front
-                }
-            }
-        }
+        self.visible_face
     }
 }
 
 /// 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
--- a/gfx/wr/webrender/src/spatial_node.rs
+++ b/gfx/wr/webrender/src/spatial_node.rs
@@ -341,16 +341,17 @@ impl SpatialNode {
 
                             let transform = state.coordinate_system_relative_scale_offset
                                                  .to_transform()
                                                  .pre_mul(&relative_transform);
 
                             // Push that new coordinate system and record the new id.
                             let coord_system = CoordinateSystem {
                                 transform,
+                                is_flatten_root: !state.preserves_3d && info.transform_style == TransformStyle::Preserve3D,
                                 parent: Some(state.current_coordinate_system_id),
                             };
                             state.current_coordinate_system_id = CoordinateSystemId(coord_systems.len() as u32);
                             coord_systems.push(coord_system);
                         }
                     }
                 }
 
@@ -521,32 +522,35 @@ impl SpatialNode {
             SpatialNodeType::StickyFrame(ref info) => {
                 // We don't translate the combined rect by the sticky offset, because sticky
                 // offsets actually adjust the node position itself, whereas scroll offsets
                 // only apply to contents inside the node.
                 state.parent_accumulated_scroll_offset += info.current_offset;
                 // We want nested sticky items to take into account the shift
                 // we applied as well.
                 state.nearest_scrolling_ancestor_offset += info.current_offset;
+                state.preserves_3d = false;
             }
             SpatialNodeType::ScrollFrame(ref scrolling) => {
                 state.parent_accumulated_scroll_offset += scrolling.offset;
                 state.nearest_scrolling_ancestor_offset = scrolling.offset;
                 state.nearest_scrolling_ancestor_viewport = scrolling.viewport_rect;
+                state.preserves_3d = false;
             }
             SpatialNodeType::ReferenceFrame(ref info) => {
                 state.parent_reference_frame_transform = self.world_viewport_transform;
 
                 let should_flatten =
                     info.kind == ReferenceFrameKind::Transform &&
                     info.transform_style == TransformStyle::Flat;
 
                 if should_flatten {
                     state.parent_reference_frame_transform = state.parent_reference_frame_transform.project_to_2d();
                 }
+                state.preserves_3d = info.transform_style == TransformStyle::Preserve3D;
 
                 state.parent_accumulated_scroll_offset = LayoutVector2D::zero();
                 state.coordinate_system_relative_scale_offset = self.coordinate_system_relative_scale_offset;
                 let translation = -info.origin_in_parent_reference_frame;
                 state.nearest_scrolling_ancestor_viewport =
                     state.nearest_scrolling_ancestor_viewport
                        .translate(&translation);
             }
--- a/gfx/wr/webrender/src/util.rs
+++ b/gfx/wr/webrender/src/util.rs
@@ -230,30 +230,38 @@ impl ScaleOffset {
             0.0,
             1.0,
         )
     }
 }
 
 // TODO: Implement these in euclid!
 pub trait MatrixHelpers<Src, Dst> {
+    /// A port of the preserves2dAxisAlignment function in Skia.
+    /// Defined in the SkMatrix44 class.
     fn preserves_2d_axis_alignment(&self) -> bool;
     fn has_perspective_component(&self) -> bool;
     fn has_2d_inverse(&self) -> bool;
+    /// Check if the matrix post-scaling on either the X or Y axes could cause geometry
+    /// transformed by this matrix to have scaling exceeding the supplied limit.
     fn exceeds_2d_scale(&self, limit: f64) -> bool;
     fn inverse_project(&self, target: &TypedPoint2D<f32, Dst>) -> Option<TypedPoint2D<f32, Src>>;
     fn inverse_rect_footprint(&self, rect: &TypedRect<f32, Dst>) -> Option<TypedRect<f32, Src>>;
     fn transform_kind(&self) -> TransformedRectKind;
     fn is_simple_translation(&self) -> bool;
     fn is_simple_2d_translation(&self) -> bool;
+    /// Return the determinant of the 2D part of the matrix.
+    fn determinant_2d(&self) -> f32;
+    /// This function returns a point in the `Src` space that projects into zero XY.
+    /// It ignores the Z coordinate and is usable for "flattened" transformations,
+    /// since they are not generally inversible.
+    fn inverse_project_2d_origin(&self) -> Option<TypedPoint2D<f32, Src>>;
 }
 
 impl<Src, Dst> MatrixHelpers<Src, Dst> for TypedTransform3D<f32, Src, Dst> {
-    // A port of the preserves2dAxisAlignment function in Skia.
-    // Defined in the SkMatrix44 class.
     fn preserves_2d_axis_alignment(&self) -> bool {
         if self.m14 != 0.0 || self.m24 != 0.0 {
             return false;
         }
 
         let mut col0 = 0;
         let mut col1 = 0;
         let mut row0 = 0;
@@ -282,21 +290,19 @@ impl<Src, Dst> MatrixHelpers<Src, Dst> f
     fn has_perspective_component(&self) -> bool {
          self.m14.abs() > NEARLY_ZERO ||
          self.m24.abs() > NEARLY_ZERO ||
          self.m34.abs() > NEARLY_ZERO ||
          (self.m44 - 1.0).abs() > NEARLY_ZERO
     }
 
     fn has_2d_inverse(&self) -> bool {
-        self.m11 * self.m22 - self.m12 * self.m21 != 0.0
+        self.determinant_2d() != 0.0
     }
 
-    // Check if the matrix post-scaling on either the X or Y axes could cause geometry
-    // transformed by this matrix to have scaling exceeding the supplied limit.
     fn exceeds_2d_scale(&self, limit: f64) -> bool {
         let limit2 = (limit * limit) as f32;
         self.m11 * self.m11 + self.m12 * self.m12 > limit2 ||
         self.m21 * self.m21 + self.m22 * self.m22 > limit2
     }
 
     fn inverse_project(&self, target: &TypedPoint2D<f32, Dst>) -> Option<TypedPoint2D<f32, Src>> {
         let m: TypedTransform2D<f32, Src, Dst>;
@@ -344,16 +350,31 @@ impl<Src, Dst> MatrixHelpers<Src, Dst> f
 
     fn is_simple_2d_translation(&self) -> bool {
         if !self.is_simple_translation() {
             return false;
         }
 
         self.m43.abs() < NEARLY_ZERO
     }
+
+    fn determinant_2d(&self) -> f32 {
+        self.m11 * self.m22 - self.m12 * self.m21
+    }
+
+    fn inverse_project_2d_origin(&self) -> Option<TypedPoint2D<f32, Src>> {
+        let det = self.determinant_2d();
+        if det != 0.0 {
+            let x = (self.m21 * self.m42 - self.m41 * self.m22) / det;
+            let y = (self.m12 * self.m41 - self.m11 * self.m42) / det;
+            Some(TypedPoint2D::new(x, y))
+        } else {
+            None
+        }
+    }
 }
 
 pub trait RectHelpers<U>
 where
     Self: Sized,
 {
     fn from_floats(x0: f32, y0: f32, x1: f32, y1: f32) -> Self;
     fn is_well_formed_and_nonempty(&self) -> bool;
@@ -521,16 +542,32 @@ pub mod test {
 
     #[test]
     fn scale_offset_accumulate() {
         let x0 = LayoutTransform::create_translation(130.0, 200.0, 0.0);
         let x1 = LayoutTransform::create_scale(7.0, 3.0, 1.0);
 
         validate_accumulate(&x0, &x1);
     }
+
+    #[test]
+    fn inverse_project_2d_origin() {
+        let mut m = Transform3D::identity();
+        assert_eq!(m.inverse_project_2d_origin(), Some(Point2D::zero()));
+        m.m11 = 0.0;
+        assert_eq!(m.inverse_project_2d_origin(), None);
+        m.m21 = -2.0;
+        m.m22 = 0.0;
+        m.m12 = -0.5;
+        m.m41 = 1.0;
+        m.m42 = 0.5;
+        let origin = m.inverse_project_2d_origin().unwrap();
+        assert_eq!(origin, Point2D::new(1.0, 0.5));
+        assert_eq!(m.transform_point2d(&origin), Some(Point2D::zero()));
+    }
 }
 
 pub trait MaxRect {
     fn max_rect() -> Self;
 }
 
 impl MaxRect for DeviceIntRect {
     fn max_rect() -> Self {
--- a/gfx/wr/wrench/reftests/transforms/content-offset.yaml
+++ b/gfx/wr/wrench/reftests/transforms/content-offset.yaml
@@ -1,15 +1,16 @@
 ---
 root:
   items:
     -
       type: "stacking-context"
       perspective: 1000
       perspective-origin: 0 0
+      "transform-style": "preserve-3d"
       items:
         -
           type: "stacking-context"
           transform: rotate-x(45) translate(100, 100, 0)
           "transform-style": "preserve-3d"
           items:
               -
                 bounds: [0, 0, 200, 200]
new file mode 100644
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/flatten-preserve-3d-root-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+  items:
+    - bounds: [100, 150, 150, 75]
+      type: rect
+      color: green
new file mode 100644
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/flatten-preserve-3d-root.yaml
@@ -0,0 +1,23 @@
+# This test ensures that we flatten the trasformations (i.e. zero out Z coordinates)
+# at the boundaries of preserve-3d hierarchies.
+# If the stacking context isn't flattened at the preserve-3d boundary here,
+# it's non-zero Z component starts affecting the screen space position
+# due to the "rotate-x" transform at the top level.
+---
+root:
+  items:
+    -
+      bounds: [100, 100, 0, 0]
+      type: stacking-context
+      transform: rotate-x(60)
+      transform-style: flat
+      items:
+        -
+          type: "stacking-context"
+          transform: translate(0, 0, 200)
+          transform-style: preserve-3d
+          items:
+            -
+              bounds: [0, 0, 150, 150]
+              type: rect
+              color: green
--- a/gfx/wr/wrench/reftests/transforms/reftest.list
+++ b/gfx/wr/wrench/reftests/transforms/reftest.list
@@ -31,8 +31,9 @@ platform(linux,mac) fuzzy(1,2) == perspe
 platform(linux,mac) fuzzy(9,348) == perspective-border-radius.yaml perspective-border-radius.png
 == snapped-preserve-3d.yaml snapped-preserve-3d-ref.yaml
 platform(linux,mac) fuzzy(1,122) == border-scale.yaml border-scale.png
 platform(linux,mac) fuzzy(1,16) == border-scale-2.yaml border-scale-2.png
 platform(linux,mac) fuzzy(1,69) == border-scale-3.yaml border-scale-3.png
 platform(linux,mac) fuzzy(1,74) == border-scale-4.yaml border-scale-4.png
 # Just make sure we aren't crashing here
 != large-raster-root.yaml blank.yaml
+== flatten-preserve-3d-root.yaml flatten-preserve-3d-root-ref.yaml