Bug 1532174 - Repace WR RelativeTransform with CoordinateSpaceMapping, improve flattening semantics. r=gw
authorDzmitry Malyshau <dmalyshau@mozilla.com>
Fri, 10 May 2019 02:22:51 +0000
changeset 532196 73254a69497b209b372ed209ce9d004080ea8052
parent 532195 b86c8998a2b246e0c5f13b32481ace92f1be0d99
child 532197 c5798de806e28251aeda7d23123ab2b05eb7e8fb
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgw
bugs1532174
milestone68.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 1532174 - Repace WR RelativeTransform with CoordinateSpaceMapping, improve flattening semantics. r=gw This change makes get_relative_transform() to no longer rely on any flattening done before in the pipeline. This makes it correct is some of the cases we failed previously (see ini files removed). It now does flattening on every flat coordinate system it passes through, and it's used for SpaceMapper. The old RelativeTransform is now replaced with CoordinateSpaceMapping, which reduces the zoo of our types :) Differential Revision: https://phabricator.services.mozilla.com/D30600
gfx/wr/webrender/src/batch.rs
gfx/wr/webrender/src/clip.rs
gfx/wr/webrender/src/clip_scroll_tree.rs
gfx/wr/webrender/src/display_list_flattener.rs
gfx/wr/webrender/src/frame_builder.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/render_backend.rs
gfx/wr/webrender/src/spatial_node.rs
gfx/wr/wrench/reftests/transforms/flatten-twice-ref.yaml
gfx/wr/wrench/reftests/transforms/flatten-twice.yaml
gfx/wr/wrench/reftests/transforms/reftest.list
testing/web-platform/meta/css/compositing/mix-blend-mode/mix-blend-mode-both-parent-and-blended-with-3D-transform.html.ini
testing/web-platform/meta/css/css-transforms/transform-flattening-001.html.ini
testing/web-platform/meta/css/css-transforms/transform3d-preserve3d-008.html.ini
testing/web-platform/meta/css/css-transforms/transform3d-preserve3d-009.html.ini
testing/web-platform/meta/css/css-transforms/transform3d-preserve3d-013.html.ini
--- a/gfx/wr/webrender/src/batch.rs
+++ b/gfx/wr/webrender/src/batch.rs
@@ -2862,22 +2862,21 @@ impl ClipBatcher {
         }
 
         // Get the world rect of the clip rectangle. If we can't transform it due
         // to the matrix, just fall back to drawing the entire clip mask.
         let local_clip_rect = LayoutRect::new(
             clip_instance.local_pos,
             clip_rect_size,
         );
-        let transform = clip_scroll_tree.get_relative_transform(
+        let transform = clip_scroll_tree.get_world_transform(
             clip_instance.spatial_node_index,
-            ROOT_SPATIAL_NODE_INDEX,
         );
         let world_clip_rect = match project_rect(
-            &transform.flattened.with_destination::<WorldPixel>(),
+            &transform.into_transform(),
             &local_clip_rect,
             world_rect,
         ) {
             Some(rect) => rect,
             None => return false,
         };
 
         // Work out how many tiles to draw this clip mask in, stretched across the
--- a/gfx/wr/webrender/src/clip.rs
+++ b/gfx/wr/webrender/src/clip.rs
@@ -234,21 +234,22 @@ pub struct ClipNodeInstance {
 #[derive(Debug, Copy, Clone)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
 pub struct ClipNodeRange {
     pub first: u32,
     pub count: u32,
 }
 
-// A helper struct for converting between coordinate systems
-// of clip sources and primitives.
+/// A helper struct for converting between coordinate systems
+/// of clip sources and primitives.
 // todo(gw): optimize:
 //  separate arrays for matrices
 //  cache and only build as needed.
+//TODO: merge with `CoordinateSpaceMapping`?
 #[derive(Debug, MallocSizeOf)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 enum ClipSpaceConversion {
     Local,
     ScaleOffset(ScaleOffset),
     Transform(LayoutToWorldTransform),
 }
 
@@ -1337,28 +1338,31 @@ fn add_clip_node_to_current_chain(
     clip_scroll_tree: &ClipScrollTree,
 ) -> bool {
     let clip_node = &clip_data_store[node.handle];
     let clip_spatial_node = &clip_scroll_tree.spatial_nodes[node.spatial_node_index.0 as usize];
     let ref_spatial_node = &clip_scroll_tree.spatial_nodes[spatial_node_index.0 as usize];
 
     // Determine the most efficient way to convert between coordinate
     // systems of the primitive and clip node.
+    //Note: this code is different from `get_relative_transform` in a way that we only try
+    // getting the relative transform if it's Local or ScaleOffset,
+    // falling back to the world transform otherwise.
     let conversion = if spatial_node_index == node.spatial_node_index {
         ClipSpaceConversion::Local
     } else if ref_spatial_node.coordinate_system_id == clip_spatial_node.coordinate_system_id {
         let scale_offset = ref_spatial_node.coordinate_system_relative_scale_offset
             .inverse()
             .accumulate(&clip_spatial_node.coordinate_system_relative_scale_offset);
         ClipSpaceConversion::ScaleOffset(scale_offset)
     } else {
         ClipSpaceConversion::Transform(
             clip_scroll_tree
                 .get_world_transform(node.spatial_node_index)
-                .flattened
+                .into_transform()
         )
     };
 
     // 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
@@ -1,23 +1,23 @@
 /* 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::{ExternalScrollId, PropertyBinding, ReferenceFrameKind, TransformStyle};
 use api::{PipelineId, ScrollClamping, ScrollNodeState, ScrollLocation, ScrollSensitivity};
 use api::units::*;
-use euclid::TypedTransform3D;
+use euclid::{TypedPoint2D, TypedScale, TypedTransform3D};
 use crate::gpu_types::TransformPalette;
 use crate::internal_types::{FastHashMap, FastHashSet};
 use crate::print_tree::{PrintableTree, PrintTree, PrintTreePrinter};
 use crate::scene::SceneProperties;
 use crate::spatial_node::{ScrollFrameInfo, SpatialNode, SpatialNodeType, StickyFrameInfo, ScrollFrameKind};
 use std::{ops, u32};
-use crate::util::{LayoutToWorldFastTransform, MatrixHelpers, ScaleOffset};
+use crate::util::{LayoutToWorldFastTransform, MatrixHelpers, ScaleOffset, scale_factors};
 
 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)]
@@ -25,25 +25,25 @@ 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,
-    pub transform_style: TransformStyle,
+    pub should_flatten: bool,
     pub parent: Option<CoordinateSystemId>,
 }
 
 impl CoordinateSystem {
     fn root() -> Self {
         CoordinateSystem {
             transform: LayoutTransform::identity(),
-            transform_style: TransformStyle::Flat,
+            should_flatten: false,
             parent: None,
         }
     }
 }
 
 #[derive(Debug, Copy, Clone, Eq, Hash, MallocSizeOf, PartialEq, PartialOrd, Ord)]
 #[cfg_attr(feature = "capture", derive(Serialize))]
 #[cfg_attr(feature = "replay", derive(Deserialize))]
@@ -87,27 +87,16 @@ impl ops::Not for VisibleFace {
     fn not(self) -> Self {
         match self {
             VisibleFace::Front => VisibleFace::Back,
             VisibleFace::Back => VisibleFace::Front,
         }
     }
 }
 
-impl VisibleFace {
-    /// A convenient constructor from methods like `is_backface_visible()`
-    pub fn from_bool(is_backface: bool) -> Self {
-        if is_backface {
-            VisibleFace::Back
-        } else {
-            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.
@@ -143,23 +132,119 @@ pub struct TransformUpdateState {
     /// 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<U> {
-    /// The flattened transform, produces Z = 0 at all times.
-    pub flattened: TypedTransform3D<f32, LayoutPixel, U>,
-    /// True if the original transform had perspective.
-    pub has_perspective: bool,
+
+/// Transformation between two nodes in the clip-scroll tree that can sometimes be
+/// encoded more efficiently than with a full matrix.
+#[derive(Debug, Clone)]
+pub enum CoordinateSpaceMapping<Src, Dst> {
+    Local,
+    ScaleOffset(ScaleOffset),
+    Transform(TypedTransform3D<f32, Src, Dst>),
+}
+
+impl<Src, Dst> CoordinateSpaceMapping<Src, Dst> {
+    pub fn into_transform(self) -> TypedTransform3D<f32, Src, Dst> {
+        match self {
+            CoordinateSpaceMapping::Local => TypedTransform3D::identity(),
+            CoordinateSpaceMapping::ScaleOffset(scale_offset) => scale_offset.to_transform(),
+            CoordinateSpaceMapping::Transform(transform) => transform,
+        }
+    }
+
+    pub fn visible_face(&self) -> VisibleFace {
+        match *self {
+            CoordinateSpaceMapping::Transform(ref transform) if transform.is_backface_visible() => VisibleFace::Back,
+            CoordinateSpaceMapping::Local |
+            CoordinateSpaceMapping::Transform(_) |
+            CoordinateSpaceMapping::ScaleOffset(_) => VisibleFace::Front,
+
+        }
+    }
+
+    pub fn is_perspective(&self) -> bool {
+        match *self {
+            CoordinateSpaceMapping::Local |
+            CoordinateSpaceMapping::ScaleOffset(_) => false,
+            CoordinateSpaceMapping::Transform(ref transform) => transform.has_perspective_component(),
+        }
+    }
+
+    pub fn project_2d_origin(&self) -> Option<TypedPoint2D<f32, Dst>> {
+        match *self {
+            CoordinateSpaceMapping::Local => Some(TypedPoint2D::zero()),
+            CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => Some(
+                scale_offset.offset.to_point() * TypedScale::new(1.0)
+            ),
+            CoordinateSpaceMapping::Transform(ref transform) => {
+                transform.transform_point2d(&TypedPoint2D::zero())
+            }
+        }
+    }
+
+    pub fn inverse_project_2d_origin(&self) -> Option<TypedPoint2D<f32, Src>> {
+        match *self {
+            CoordinateSpaceMapping::Local => Some(TypedPoint2D::zero()),
+            CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => Some(
+                scale_offset.inverse().offset.to_point() * TypedScale::new(1.0)
+            ),
+            CoordinateSpaceMapping::Transform(ref transform) => {
+                transform.inverse_project_2d_origin()
+            }
+        }
+    }
+
+    pub fn scale_factors(&self) -> (f32, f32) {
+        match *self {
+            CoordinateSpaceMapping::Local => (1.0, 1.0),
+            CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => (scale_offset.scale.x, scale_offset.scale.y),
+            CoordinateSpaceMapping::Transform(ref transform) => scale_factors(transform),
+        }
+    }
+
+    pub fn inverse(&self) -> Option<CoordinateSpaceMapping<Dst, Src>> {
+        match *self {
+            CoordinateSpaceMapping::Local => Some(CoordinateSpaceMapping::Local),
+            CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
+                Some(CoordinateSpaceMapping::ScaleOffset(scale_offset.inverse()))
+            }
+            CoordinateSpaceMapping::Transform(ref transform) => {
+                transform.inverse().map(CoordinateSpaceMapping::Transform)
+            }
+        }
+    }
+
+    pub fn with_destination<NewDst>(self) -> CoordinateSpaceMapping<Src, NewDst> {
+        match self {
+            CoordinateSpaceMapping::Local => CoordinateSpaceMapping::Local,
+            CoordinateSpaceMapping::ScaleOffset(scale_offset) => CoordinateSpaceMapping::ScaleOffset(scale_offset),
+            CoordinateSpaceMapping::Transform(transform) => CoordinateSpaceMapping::Transform(
+                transform.with_destination::<NewDst>()
+            ),
+        }
+    }
+
+    pub fn post_mul_transform<NewDst>(
+        &self, other: &CoordinateSpaceMapping<Dst, NewDst>
+    ) -> TypedTransform3D<f32, Src, NewDst>
+    where Self: Clone
+    {
+        let matrix = self.clone().into_transform();
+        match *other {
+            CoordinateSpaceMapping::Local => matrix.with_destination::<NewDst>(),
+            CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => matrix.post_mul(&scale_offset.to_transform()),
+            CoordinateSpaceMapping::Transform(ref transform) => matrix.post_mul(transform),
+        }
+    }
 }
 
 impl ClipScrollTree {
     pub fn new() -> Self {
         ClipScrollTree {
             spatial_nodes: Vec::new(),
             coord_systems: Vec::new(),
             pending_scroll_offsets: FastHashMap::default(),
@@ -198,85 +283,69 @@ impl ClipScrollTree {
     }
 
     /// 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,
         child_index: SpatialNodeIndex,
         parent_index: SpatialNodeIndex,
-    ) -> RelativeTransform<LayoutPixel> {
+    ) -> CoordinateSpaceMapping<LayoutPixel, LayoutPixel> {
         assert!(child_index.0 >= parent_index.0);
+        if child_index == parent_index {
+            return CoordinateSpaceMapping::Local;
+        }
+
         let child = &self.spatial_nodes[child_index.0 as usize];
         let parent = &self.spatial_nodes[parent_index.0 as usize];
-        let mut has_perspective = false;
 
         if child.coordinate_system_id == parent.coordinate_system_id {
-            return RelativeTransform {
-                flattened: parent.coordinate_system_relative_scale_offset
-                    .inverse()
-                    .accumulate(&child.coordinate_system_relative_scale_offset)
-                    .to_transform(),
-                has_perspective,
-            }
+            let scale_offset = parent.coordinate_system_relative_scale_offset
+                .inverse()
+                .accumulate(&child.coordinate_system_relative_scale_offset);
+            return CoordinateSpaceMapping::ScaleOffset(scale_offset);
         }
 
         let mut coordinate_system_id = child.coordinate_system_id;
         let mut transform = child.coordinate_system_relative_scale_offset.to_transform();
-        let mut transform_style = child.transform_style();
 
         // 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
 
         while coordinate_system_id != parent.coordinate_system_id {
             let coord_system = &self.coord_systems[coordinate_system_id.0 as usize];
 
-            if coord_system.transform_style == TransformStyle::Flat {
-                has_perspective |= transform.has_perspective_component();
-                if transform_style != TransformStyle::Flat {
-                    //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 = 1.0;
-                    transform.m43 = 0.0;
-                }
+            if coord_system.should_flatten {
+                transform.m13 = 0.0;
+                transform.m23 = 0.0;
+                transform.m33 = 1.0;
+                transform.m43 = 0.0;
             }
 
             coordinate_system_id = coord_system.parent.expect("invalid parent!");
             transform = transform.post_mul(&coord_system.transform);
-            transform_style = coord_system.transform_style;
         }
 
-        has_perspective |= transform.has_perspective_component();
-
         transform = transform.post_mul(
             &parent.coordinate_system_relative_scale_offset
                 .inverse()
                 .to_transform(),
         );
 
-        RelativeTransform {
-            flattened: transform,
-            has_perspective,
-        }
+        CoordinateSpaceMapping::Transform(transform)
     }
 
     /// Calculate the relative transform from `child_index` to the scene root.
     pub fn get_world_transform(
         &self,
         index: SpatialNodeIndex,
-    ) -> RelativeTransform<WorldPixel> {
-        let relative = self.get_relative_transform(index, ROOT_SPATIAL_NODE_INDEX);
-        RelativeTransform {
-            flattened: relative.flattened.with_destination::<WorldPixel>(),
-            has_perspective: relative.has_perspective,
-        }
+    ) -> CoordinateSpaceMapping<LayoutPixel, WorldPixel> {
+        self.get_relative_transform(index, ROOT_SPATIAL_NODE_INDEX)
+            .with_destination::<WorldPixel>()
     }
 
     /// 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,
@@ -389,26 +458,21 @@ impl ClipScrollTree {
         let node_index = self.find_nearest_scrolling_ancestor(node_index);
         self.spatial_nodes[node_index.0 as usize].scroll(scroll_location)
     }
 
     pub fn update_tree(
         &mut self,
         pan: WorldPoint,
         scene_properties: &SceneProperties,
-        mut transform_palette: Option<&mut TransformPalette>,
     ) {
         if self.spatial_nodes.is_empty() {
             return;
         }
 
-        if let Some(ref mut palette) = transform_palette {
-            palette.allocate(self.spatial_nodes.len());
-        }
-
         self.coord_systems.clear();
         self.coord_systems.push(CoordinateSystem::root());
 
         let root_node_index = self.root_reference_frame_index();
         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(),
@@ -424,31 +488,39 @@ impl ClipScrollTree {
         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,
                 None => continue,
             };
 
             node.update(&mut state, &mut self.coord_systems, scene_properties, &*previous);
-            if let Some(ref mut palette) = transform_palette {
-                node.push_gpu_data(palette, node_index);
-            }
 
             if !node.children.is_empty() {
                 node.prepare_state_for_children(&mut state);
                 self.nodes_to_update.extend(node.children
                     .iter()
                     .rev()
                     .map(|child_index| (*child_index, state.clone()))
                 );
             }
         }
     }
 
+    pub fn build_transform_palette(&self) -> TransformPalette {
+        let mut palette = TransformPalette::new(self.spatial_nodes.len());
+        //TODO: this could be faster by a bit of dynamic programming
+        for i in 0 .. self.spatial_nodes.len() {
+            let index = SpatialNodeIndex(i as u32);
+            let world_transform = self.get_world_transform(index).into_transform();
+            palette.set_world_transform(index, world_transform);
+        }
+        palette
+    }
+
     pub fn finalize_and_apply_pending_scroll_offsets(&mut self, old_states: ScrollStates) {
         for node in &mut self.spatial_nodes {
             let external_id = match node.node_type {
                 SpatialNodeType::ScrollFrame(ScrollFrameInfo { external_id: Some(id), ..} ) => id,
                 _ => continue,
             };
 
             if let Some(scrolling_state) = old_states.get(&external_id) {
@@ -539,57 +611,53 @@ impl ClipScrollTree {
         &self,
         index: SpatialNodeIndex,
         pt: &mut T,
     ) {
         let node = &self.spatial_nodes[index.0 as usize];
         match node.node_type {
             SpatialNodeType::StickyFrame(ref sticky_frame_info) => {
                 pt.new_level(format!("StickyFrame"));
-                pt.add_item(format!("index: {:?}", index));
                 pt.add_item(format!("sticky info: {:?}", sticky_frame_info));
             }
             SpatialNodeType::ScrollFrame(scrolling_info) => {
                 pt.new_level(format!("ScrollFrame"));
-                pt.add_item(format!("index: {:?}", index));
                 pt.add_item(format!("viewport: {:?}", scrolling_info.viewport_rect));
                 pt.add_item(format!("scrollable_size: {:?}", scrolling_info.scrollable_size));
                 pt.add_item(format!("scroll offset: {:?}", scrolling_info.offset));
                 pt.add_item(format!("external_scroll_offset: {:?}", scrolling_info.external_scroll_offset));
             }
-            SpatialNodeType::ReferenceFrame(ref _info) => {
+            SpatialNodeType::ReferenceFrame(ref info) => {
                 pt.new_level(format!("ReferenceFrame"));
-                pt.add_item(format!("index: {:?}", index));
+                pt.add_item(format!("kind: {:?}", info.kind));
+                pt.add_item(format!("transform_style: {:?}", info.transform_style));
             }
         }
 
+        pt.add_item(format!("index: {:?}", index));
         pt.add_item(format!("world_viewport_transform: {:?}", node.world_viewport_transform));
-        pt.add_item(format!("world_content_transform: {:?}", node.world_content_transform));
         pt.add_item(format!("coordinate_system_id: {:?}", node.coordinate_system_id));
+        pt.add_item(format!("coordinate_system_scale_offset: {:?}", node.coordinate_system_relative_scale_offset));
 
         for child_index in &node.children {
             self.print_node(*child_index, pt);
         }
 
         pt.end_level();
     }
 
     /// Get the visible face of the transfrom from the specified node to its parent.
     pub fn get_local_visible_face(&self, node_index: SpatialNodeIndex) -> VisibleFace {
         let node = &self.spatial_nodes[node_index.0 as usize];
         let parent_index = match node.parent {
             Some(index) => index,
             None => return VisibleFace::Front
         };
-        VisibleFace::from_bool(
-            self
-                .get_relative_transform(node_index, parent_index)
-                .flattened
-                .is_backface_visible()
-        )
+        self.get_relative_transform(node_index, parent_index)
+            .visible_face()
     }
 
     #[allow(dead_code)]
     pub fn print(&self) {
         if !self.spatial_nodes.is_empty() {
             let mut pt = PrintTree::new("clip_scroll tree");
             self.print_with(&mut pt);
         }
@@ -630,17 +698,17 @@ fn test_pt(
     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(child, parent).flattened;
+    let m = cst.get_relative_transform(child, parent).into_transform();
     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,
             );
 }
 
@@ -673,17 +741,17 @@ fn test_cst_simple_translation() {
 
     let child3 = add_reference_frame(
         &mut cst,
         Some(child2),
         LayoutTransform::create_translation(200.0, 200.0, 0.0),
         LayoutVector2D::zero(),
     );
 
-    cst.update_tree(WorldPoint::zero(), &SceneProperties::new(), None);
+    cst.update_tree(WorldPoint::zero(), &SceneProperties::new());
 
     test_pt(100.0, 100.0, &cst, child1, root, 200.0, 100.0);
     test_pt(100.0, 100.0, &cst, child2, root, 200.0, 150.0);
     test_pt(100.0, 100.0, &cst, child2, child1, 100.0, 150.0);
     test_pt(100.0, 100.0, &cst, child3, root, 400.0, 350.0);
 }
 
 #[test]
@@ -715,17 +783,17 @@ fn test_cst_simple_scale() {
 
     let child3 = add_reference_frame(
         &mut cst,
         Some(child2),
         LayoutTransform::create_scale(2.0, 2.0, 1.0),
         LayoutVector2D::zero(),
     );
 
-    cst.update_tree(WorldPoint::zero(), &SceneProperties::new(), None);
+    cst.update_tree(WorldPoint::zero(), &SceneProperties::new());
 
     test_pt(100.0, 100.0, &cst, child1, root, 400.0, 100.0);
     test_pt(100.0, 100.0, &cst, child2, root, 400.0, 200.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, child3, child1, 200.0, 400.0);
 }
 
@@ -765,17 +833,17 @@ fn test_cst_scale_translation() {
 
     let child4 = add_reference_frame(
         &mut cst,
         Some(child3),
         LayoutTransform::create_scale(3.0, 2.0, 1.0),
         LayoutVector2D::zero(),
     );
 
-    cst.update_tree(WorldPoint::zero(), &SceneProperties::new(), None);
+    cst.update_tree(WorldPoint::zero(), &SceneProperties::new());
 
     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, child4, root, 1100.0, 450.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);
@@ -799,12 +867,12 @@ fn test_cst_translation_rotate() {
 
     let child1 = add_reference_frame(
         &mut cst,
         Some(root),
         LayoutTransform::create_rotation(0.0, 0.0, 1.0, Angle::degrees(90.0)),
         LayoutVector2D::zero(),
     );
 
-    cst.update_tree(WorldPoint::zero(), &SceneProperties::new(), None);
+    cst.update_tree(WorldPoint::zero(), &SceneProperties::new());
 
     test_pt(100.0, 0.0, &cst, child1, root, 0.0, -100.0);
 }
--- a/gfx/wr/webrender/src/display_list_flattener.rs
+++ b/gfx/wr/webrender/src/display_list_flattener.rs
@@ -507,16 +507,17 @@ impl<'a> DisplayListFlattener<'a> {
             }
         );
 
         let tile_cache = TileCache::new(
             main_scroll_root,
             &prim_list.prim_instances,
             *self.pipeline_clip_chain_stack.last().unwrap(),
             &self.prim_store.pictures,
+            &self.clip_scroll_tree,
         );
 
         let pic_index = self.prim_store.pictures.alloc().init(PicturePrimitive::new_image(
             Some(PictureCompositeMode::TileCache { clear_color: ColorF::new(1.0, 1.0, 1.0, 1.0) }),
             Picture3DContext::Out,
             self.scene.root_pipeline_id.unwrap(),
             None,
             true,
--- a/gfx/wr/webrender/src/frame_builder.rs
+++ b/gfx/wr/webrender/src/frame_builder.rs
@@ -527,22 +527,21 @@ impl FrameBuilder {
             .total_primitives
             .set(self.prim_store.prim_count());
 
         resource_cache.begin_frame(stamp);
         gpu_cache.begin_frame(stamp);
 
         self.globals.update(gpu_cache);
 
-        let mut transform_palette = TransformPalette::new();
         clip_scroll_tree.update_tree(
             pan,
             scene_properties,
-            Some(&mut transform_palette),
         );
+        let mut transform_palette = clip_scroll_tree.build_transform_palette();
         self.clip_store.clear_old_instances();
 
         let mut render_tasks = RenderTaskTree::new(
             stamp.frame_id(),
             render_task_counters,
         );
         let mut surfaces = Vec::new();
 
--- a/gfx/wr/webrender/src/gpu_types.rs
+++ b/gfx/wr/webrender/src/gpu_types.rs
@@ -471,34 +471,29 @@ struct RelativeTransformKey {
 //           should be relative to.
 pub struct TransformPalette {
     transforms: Vec<TransformData>,
     metadata: Vec<TransformMetadata>,
     map: FastHashMap<RelativeTransformKey, usize>,
 }
 
 impl TransformPalette {
-    pub fn new() -> Self {
+    pub fn new(count: usize) -> Self {
         let _ = VECS_PER_TRANSFORM;
         TransformPalette {
-            transforms: Vec::new(),
-            metadata: Vec::new(),
+            transforms: vec![TransformData::invalid(); count],
+            metadata: vec![TransformMetadata::invalid(); count],
             map: FastHashMap::default(),
         }
     }
 
     pub fn finish(self) -> Vec<TransformData> {
         self.transforms
     }
 
-    pub fn allocate(&mut self, count: usize) {
-        self.transforms = vec![TransformData::invalid(); count];
-        self.metadata = vec![TransformMetadata::invalid(); count];
-    }
-
     pub fn set_world_transform(
         &mut self,
         index: SpatialNodeIndex,
         transform: LayoutToWorldTransform,
     ) {
         register_transform(
             &mut self.metadata,
             &mut self.transforms,
@@ -530,17 +525,17 @@ impl TransformPalette {
 
             *self.map
                 .entry(key)
                 .or_insert_with(|| {
                     let transform = clip_scroll_tree.get_relative_transform(
                         child_index,
                         parent_index,
                     )
-                    .flattened
+                    .into_transform()
                     .with_destination::<PicturePixel>();
 
                     register_transform(
                         metadata,
                         transforms,
                         child_index,
                         parent_index,
                         transform,
--- a/gfx/wr/webrender/src/picture.rs
+++ b/gfx/wr/webrender/src/picture.rs
@@ -4,29 +4,29 @@
 
 use api::{FilterOp, MixBlendMode, PipelineId, PremultipliedColorF};
 use api::{PropertyBinding, PropertyBindingId};
 use api::{DebugFlags, RasterSpace, ColorF, ImageKey, ClipMode};
 use api::units::*;
 use crate::box_shadow::{BLUR_SAMPLE_SCALE};
 use crate::clip::{ClipChainId, ClipChainNode, ClipItem, ClipStore, ClipDataStore, ClipChainStack};
 use crate::clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX,
-    ClipScrollTree, CoordinateSystemId, SpatialNodeIndex, VisibleFace
+    ClipScrollTree, CoordinateSystemId, CoordinateSpaceMapping, SpatialNodeIndex, VisibleFace
 };
 use crate::debug_colors;
-use euclid::{size2, vec3, TypedPoint2D, TypedScale, TypedSize2D};
+use euclid::{size2, vec3, TypedPoint2D, TypedScale, TypedSize2D, Vector2D};
 use euclid::approxeq::ApproxEq;
 use crate::frame_builder::{FrameVisibilityContext, FrameVisibilityState};
 use crate::intern::ItemUid;
 use crate::internal_types::{FastHashMap, FastHashSet, PlaneSplitter};
 use crate::frame_builder::{FrameBuildingContext, FrameBuildingState, PictureState, PictureContext};
 use crate::gpu_cache::{GpuCache, GpuCacheAddress, GpuCacheHandle};
 use crate::gpu_types::UvRectKind;
 use plane_split::{Clipper, Polygon, Splitter};
-use crate::prim_store::{CoordinateSpaceMapping, SpaceMapper};
+use crate::prim_store::SpaceMapper;
 use crate::prim_store::{PictureIndex, PrimitiveInstance, PrimitiveInstanceKind};
 use crate::prim_store::{get_raster_rects, PrimitiveScratchBuffer, VectorKey, PointKey};
 use crate::prim_store::{OpacityBindingStorage, ImageInstanceStorage, OpacityBindingIndex, RectangleKey};
 use crate::print_tree::PrintTreePrinter;
 use crate::render_backend::DataStores;
 use crate::render_task::{ClearMode, RenderTask, TileBlit};
 use crate::render_task::{RenderTaskId, RenderTaskLocation};
 use crate::resource_cache::ResourceCache;
@@ -698,30 +698,32 @@ fn collect_ref_prims(
 }
 
 impl TileCache {
     pub fn new(
         spatial_node_index: SpatialNodeIndex,
         prim_instances: &[PrimitiveInstance],
         root_clip_chain_id: ClipChainId,
         pictures: &[PicturePrimitive],
+        clip_scroll_tree: &ClipScrollTree,
     ) -> Self {
         // Build the list of reference primitives
         // for this picture cache.
         let reference_prims = ReferencePrimitiveList::new(
             prim_instances,
             pictures,
         );
 
         TileCache {
             spatial_node_index,
             tiles: Vec::new(),
             map_local_to_world: SpaceMapper::new(
                 ROOT_SPATIAL_NODE_INDEX,
                 WorldRect::zero(),
+                clip_scroll_tree,
             ),
             tiles_to_draw: Vec::new(),
             opacity_bindings: FastHashMap::default(),
             dirty_region: DirtyRegion::new(),
             world_origin: WorldPoint::zero(),
             world_tile_size: WorldSize::zero(),
             tile_count: TileSize::zero(),
             scroll_offset: None,
@@ -788,17 +790,16 @@ impl TileCache {
         }
 
         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_offset_point = frame_context.clip_scroll_tree
             .get_world_transform(self.spatial_node_index)
-            .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(),
         };
@@ -828,16 +829,17 @@ impl TileCache {
         }.unwrap_or(WorldVector2D::zero());
 
         // Assume no tiles are valid to draw by default
         self.tiles_to_draw.clear();
 
         self.map_local_to_world = SpaceMapper::new(
             ROOT_SPATIAL_NODE_INDEX,
             frame_context.screen_world_rect,
+            frame_context.clip_scroll_tree,
         );
 
         let world_mapper = SpaceMapper::new_with_target(
             ROOT_SPATIAL_NODE_INDEX,
             self.spatial_node_index,
             frame_context.screen_world_rect,
             frame_context.clip_scroll_tree,
         );
@@ -1434,25 +1436,23 @@ impl TileCache {
                 // 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,
                         )
-                        .flattened
-                        .transform_point2d(&LayoutPoint::zero())
+                        .project_2d_origin()
                 } else {
                     frame_context.clip_scroll_tree
                         .get_relative_transform(
                             spatial_node_index,
                             self.spatial_node_index,
                         )
-                        .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 = inverse_origin
                     .unwrap_or_else(LayoutPoint::zero)
@@ -1838,16 +1838,17 @@ impl SurfaceInfo {
 
         let pic_bounds = map_surface_to_world
             .unmap(&map_surface_to_world.bounds)
             .unwrap_or(PictureRect::max_rect());
 
         let map_local_to_surface = SpaceMapper::new(
             surface_spatial_node_index,
             pic_bounds,
+            clip_scroll_tree,
         );
 
         SurfaceInfo {
             rect: PictureRect::zero(),
             map_local_to_surface,
             render_tasks: None,
             raster_spatial_node_index,
             surface_spatial_node_index,
@@ -2395,16 +2396,17 @@ impl PicturePrimitive {
         );
 
         let pic_bounds = map_pic_to_world.unmap(&map_pic_to_world.bounds)
                                          .unwrap_or(PictureRect::max_rect());
 
         let map_local_to_pic = SpaceMapper::new(
             surface_spatial_node_index,
             pic_bounds,
+            frame_context.clip_scroll_tree,
         );
 
         let (map_raster_to_world, map_pic_to_raster) = create_raster_mappers(
             surface_spatial_node_index,
             raster_spatial_node_index,
             frame_context.screen_world_rect,
             frame_context.clip_scroll_tree,
         );
@@ -2792,60 +2794,66 @@ impl PicturePrimitive {
         clip_scroll_tree: &ClipScrollTree,
         prim_spatial_node_index: SpatialNodeIndex,
         original_local_rect: LayoutRect,
         combined_local_clip_rect: &LayoutRect,
         world_rect: WorldRect,
         plane_split_anchor: usize,
     ) -> bool {
         let transform = clip_scroll_tree
-            .get_world_transform(prim_spatial_node_index)
-            .flattened;
-        let matrix = transform.cast();
+            .get_world_transform(prim_spatial_node_index);
+        let matrix = transform.clone().into_transform().cast();
 
         // Apply the local clip rect here, before splitting. This is
         // because the local clip rect can't be applied in the vertex
         // shader for split composites, since we are drawing polygons
         // rather that rectangles. The interpolation still works correctly
         // since we determine the UVs by doing a bilerp with a factor
         // from the original local rect.
         let local_rect = match original_local_rect
             .intersection(combined_local_clip_rect)
         {
             Some(rect) => rect.cast(),
             None => return false,
         };
         let world_rect = world_rect.cast();
 
-        if transform.is_simple_translation() {
-            let inv_transform = clip_scroll_tree
-                .get_world_transform(prim_spatial_node_index)
-                .flattened
-                .inverse()
-                .expect("Simple translation, really?");
-            let polygon = Polygon::from_transformed_rect_with_inverse(
-                local_rect,
-                &matrix,
-                &inv_transform.cast(),
-                plane_split_anchor,
-            ).unwrap();
-            splitter.add(polygon);
-        } else {
-            let mut clipper = Clipper::new();
-            let results = clipper.clip_transformed(
-                Polygon::from_rect(
+        match transform {
+            CoordinateSpaceMapping::Local => {
+                let polygon = Polygon::from_rect(
+                    local_rect * TypedScale::new(1.0),
+                    plane_split_anchor,
+                );
+                splitter.add(polygon);
+            }
+            CoordinateSpaceMapping::ScaleOffset(scale_offset) if scale_offset.scale == Vector2D::new(1.0, 1.0) => {
+                let inv_matrix = scale_offset.inverse().to_transform().cast();
+                let polygon = Polygon::from_transformed_rect_with_inverse(
                     local_rect,
+                    &matrix,
+                    &inv_matrix,
                     plane_split_anchor,
-                ),
-                &matrix,
-                Some(world_rect),
-            );
-            if let Ok(results) = results {
-                for poly in results {
-                    splitter.add(poly);
+                ).unwrap();
+                splitter.add(polygon);
+            }
+            CoordinateSpaceMapping::ScaleOffset(_) |
+            CoordinateSpaceMapping::Transform(_) => {
+                let mut clipper = Clipper::new();
+                let results = clipper.clip_transformed(
+                    Polygon::from_rect(
+                        local_rect,
+                        plane_split_anchor,
+                    ),
+                    &matrix,
+                    Some(world_rect),
+                );
+                if let Ok(results) = results {
+                    for poly in results {
+                        splitter.add(poly);
+                    }
                 }
             }
         }
 
         true
     }
 
     pub fn resolve_split_planes(
@@ -2861,20 +2869,19 @@ impl PicturePrimitive {
         ordered.clear();
 
         // Process the accumulated split planes and order them for rendering.
         // Z axis is directed at the screen, `sort` is ascending, and we need back-to-front order.
         for poly in splitter.sort(vec3(0.0, 0.0, 1.0)) {
             let spatial_node_index = self.prim_list.prim_instances[poly.anchor].spatial_node_index;
             let transform = match clip_scroll_tree
                 .get_world_transform(spatial_node_index)
-                .flattened
                 .inverse()
             {
-                Some(transform) => transform,
+                Some(transform) => transform.into_transform(),
                 // logging this would be a bit too verbose
                 None => continue,
             };
 
             let local_points = [
                 transform.transform_point3d(&poly.points[0].cast()).unwrap(),
                 transform.transform_point3d(&poly.points[1].cast()).unwrap(),
                 transform.transform_point3d(&poly.points[2].cast()).unwrap(),
@@ -3010,17 +3017,17 @@ 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(surface_spatial_node_index, parent_raster_node_index)
-                .has_perspective;
+                .is_perspective();
 
             // Disallow subpixel AA if an intermediate surface is needed.
             // TODO(lsalzman): allow overriding parent if intermediate surface is opaque
             let allow_subpixel_aa = match composite_mode {
                 PictureCompositeMode::TileCache { clear_color, .. } => {
                     // If the tile cache has an opaque background, then it's fine to use
                     // subpixel rendering (this is the common case).
                     clear_color.a >= 1.0
@@ -3074,23 +3081,22 @@ impl PicturePrimitive {
         state.pop_picture();
 
         for cluster in &mut self.prim_list.clusters {
             // Skip the cluster if backface culled.
             if !cluster.is_backface_visible {
                 // For in-preserve-3d primitives and pictures, the backface visibility is
                 // evaluated relative to the containing block.
                 if let Picture3DContext::In { ancestor_index, .. } = self.context_3d {
-                    match CoordinateSpaceMapping::<LayoutPoint, LayoutPoint>::new(
-                        ancestor_index,
-                        cluster.spatial_node_index,
-                        &frame_context.clip_scroll_tree,
-                    ) {
-                        (_, VisibleFace::Back) => continue,
-                        (_, VisibleFace::Front) => (),
+                    match frame_context.clip_scroll_tree
+                        .get_relative_transform(cluster.spatial_node_index, ancestor_index)
+                        .visible_face()
+                    {
+                        VisibleFace::Back => continue,
+                        VisibleFace::Front => (),
                     }
                 }
             }
 
             // No point including this cluster if it can't be transformed
             let spatial_node = &frame_context
                 .clip_scroll_tree
                 .spatial_nodes[cluster.spatial_node_index.0 as usize];
@@ -3378,16 +3384,17 @@ fn build_ref_prims(
     prim_map: &mut FastHashMap<ItemUid, WorldPoint>,
     clip_scroll_tree: &ClipScrollTree,
 ) {
     prim_map.clear();
 
     let mut map_local_to_world = SpaceMapper::new(
         ROOT_SPATIAL_NODE_INDEX,
         WorldRect::zero(),
+        clip_scroll_tree,
     );
 
     for ref_prim in ref_prims {
         map_local_to_world.set_target_spatial_node(
             ref_prim.spatial_node_index,
             clip_scroll_tree,
         );
 
--- a/gfx/wr/webrender/src/prim_store/mod.rs
+++ b/gfx/wr/webrender/src/prim_store/mod.rs
@@ -7,17 +7,17 @@ use api::{FilterOp, ImageRendering, Repe
 use api::{PremultipliedColorF, PropertyBinding, Shadow, GradientStop};
 use api::{BoxShadowClipMode, LineStyle, LineOrientation};
 use api::{PrimitiveKeyKind, RasterSpace};
 use api::units::*;
 use crate::border::{get_max_scale_for_border, build_border_instances};
 use crate::border::BorderSegmentCacheKey;
 use crate::box_shadow::{BLUR_SAMPLE_SCALE};
 use crate::clip::{ClipStore};
-use crate::clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX, ClipScrollTree, SpatialNodeIndex, VisibleFace};
+use crate::clip_scroll_tree::{ROOT_SPATIAL_NODE_INDEX, ClipScrollTree, CoordinateSpaceMapping, SpatialNodeIndex, VisibleFace};
 use crate::clip::{ClipDataStore, ClipNodeFlags, ClipChainId, ClipChainInstance, ClipItem};
 use crate::debug_colors;
 use crate::debug_render::DebugItem;
 use crate::display_list_flattener::{CreateShadow, IsVisible};
 use euclid::{SideOffsets2D, TypedTransform3D, TypedRect, TypedScale, TypedSize2D, TypedPoint2D};
 use euclid::approxeq::ApproxEq;
 use crate::frame_builder::{FrameBuildingContext, FrameBuildingState, PictureContext, PictureState};
 use crate::frame_builder::{FrameVisibilityContext, FrameVisibilityState};
@@ -45,19 +45,18 @@ use crate::renderer::{MAX_VERTEX_TEXTURE
 use crate::resource_cache::{ImageProperties, ImageRequest};
 use crate::scene::SceneProperties;
 use crate::segment::SegmentBuilder;
 use std::{cmp, fmt, hash, ops, u32, usize, mem};
 #[cfg(debug_assertions)]
 use std::sync::atomic::{AtomicUsize, Ordering};
 use crate::storage;
 use crate::texture_cache::TEXTURE_REGION_DIMENSIONS;
-use crate::util::{ScaleOffset, MatrixHelpers, MaxRect, Recycler};
-use crate::util::{pack_as_float, project_rect, raster_rect_to_device_pixels};
-use crate::util::{scale_factors, clamp_to_scale_factor};
+use crate::util::{MatrixHelpers, MaxRect, Recycler};
+use crate::util::{clamp_to_scale_factor, pack_as_float, project_rect, raster_rect_to_device_pixels};
 use crate::internal_types::LayoutPrimitiveInfo;
 use smallvec::SmallVec;
 
 pub mod borders;
 pub mod gradient;
 pub mod image;
 pub mod line_dec;
 pub mod picture;
@@ -127,103 +126,85 @@ impl PrimitiveOpacity {
         PrimitiveOpacity{
             is_opaque: self.is_opaque && other.is_opaque
         }
     }
 }
 
 
 #[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,
-    ) -> (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 {
-            (CoordinateSpaceMapping::Local, VisibleFace::Front)
-        } else if ref_spatial_node.coordinate_system_id == target_spatial_node.coordinate_system_id {
-            let scale_offset = ref_spatial_node.coordinate_system_relative_scale_offset
-                .inverse()
-                .accumulate(&target_spatial_node.coordinate_system_relative_scale_offset);
-            (CoordinateSpaceMapping::ScaleOffset(scale_offset), VisibleFace::Front)
-        } else {
-            let relative = clip_scroll_tree
-                .get_relative_transform(target_node_index, ref_spatial_node_index);
-            (
-                CoordinateSpaceMapping::Transform(
-                    relative.flattened.with_source::<F>().with_destination::<T>()
-                ),
-                VisibleFace::from_bool(relative.flattened.is_backface_visible())
-            )
-        }
-    }
-}
-
-#[derive(Debug, Clone)]
 pub struct SpaceMapper<F, T> {
     kind: CoordinateSpaceMapping<F, T>,
     pub ref_spatial_node_index: SpatialNodeIndex,
+    ref_world_inv_transform: CoordinateSpaceMapping<WorldPixel, T>,
     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>,
+        clip_scroll_tree: &ClipScrollTree,
     ) -> Self {
         SpaceMapper {
             kind: CoordinateSpaceMapping::Local,
             ref_spatial_node_index,
+            ref_world_inv_transform: clip_scroll_tree
+                .get_world_transform(ref_spatial_node_index)
+                .inverse()
+                .unwrap()
+                .with_destination::<T>(),
             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,
     ) -> Self {
-        let mut mapper = SpaceMapper::new(ref_spatial_node_index, bounds);
+        let mut mapper = SpaceMapper::new(ref_spatial_node_index, bounds, clip_scroll_tree);
         mapper.set_target_spatial_node(target_node_index, clip_scroll_tree);
         mapper
     }
 
     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;
-
-            let (kind, visible_face) = CoordinateSpaceMapping::new(
-                self.ref_spatial_node_index,
-                target_node_index,
-                clip_scroll_tree,
-            );
-
-            self.kind = kind;
-            self.visible_face = visible_face;
+        if target_node_index == self.current_target_spatial_node_index {
+            return
         }
+
+        let ref_spatial_node = &clip_scroll_tree.spatial_nodes[self.ref_spatial_node_index.0 as usize];
+        let target_spatial_node = &clip_scroll_tree.spatial_nodes[target_node_index.0 as usize];
+
+        self.kind = if self.ref_spatial_node_index == target_node_index {
+            CoordinateSpaceMapping::Local
+        } else if ref_spatial_node.coordinate_system_id == target_spatial_node.coordinate_system_id {
+            let scale_offset = ref_spatial_node.coordinate_system_relative_scale_offset
+                .inverse()
+                .accumulate(&target_spatial_node.coordinate_system_relative_scale_offset);
+            CoordinateSpaceMapping::ScaleOffset(scale_offset)
+        } else {
+            let transform = clip_scroll_tree
+                .get_world_transform(target_node_index)
+                .post_mul_transform(&self.ref_world_inv_transform)
+                .with_source::<F>();
+            CoordinateSpaceMapping::Transform(transform)
+        };
+
+        self.visible_face = self.kind.visible_face();
+        self.current_target_spatial_node_index = target_node_index;
     }
 
     pub fn get_transform(&self) -> TypedTransform3D<f32, F, T> {
         match self.kind {
             CoordinateSpaceMapping::Local => {
                 TypedTransform3D::identity()
             }
             CoordinateSpaceMapping::ScaleOffset(ref scale_offset) => {
@@ -1778,16 +1759,17 @@ impl PrimitiveStore {
             surface.surface_spatial_node_index,
             frame_context.screen_world_rect,
             frame_context.clip_scroll_tree,
         );
 
         let mut map_local_to_raster = SpaceMapper::new(
             surface.raster_spatial_node_index,
             RasterRect::max_rect(),
+            frame_context.clip_scroll_tree,
         );
 
         let mut surface_rect = PictureRect::zero();
 
         for prim_instance in &mut prim_list.prim_instances {
             prim_instance.reset();
 
             if prim_instance.is_chased() {
@@ -2192,27 +2174,25 @@ impl PrimitiveStore {
         match prim_instance.kind {
             PrimitiveInstanceKind::TextRun { data_handle, run_index, .. } => {
                 let prim_data = &mut frame_state.data_stores.text_run[data_handle];
                 let run = &mut self.text_runs[run_index];
 
                 // The transform only makes sense for screen space rasterization
                 let relative_transform = frame_context
                     .clip_scroll_tree
-                    .get_relative_transform(
-                        prim_instance.spatial_node_index,
-                        ROOT_SPATIAL_NODE_INDEX,
-                    );
+                    .get_world_transform(prim_instance.spatial_node_index)
+                    .into_transform();
                 let prim_offset = prim_instance.prim_origin.to_vector() - run.reference_frame_relative_offset;
 
                 run.request_resources(
                     prim_offset,
                     &prim_data.font,
                     &prim_data.glyphs,
-                    &relative_transform.flattened.with_destination::<WorldPixel>(),
+                    &relative_transform,
                     surface,
                     raster_space,
                     frame_state.resource_cache,
                     frame_state.gpu_cache,
                     frame_state.render_tasks,
                     frame_state.scratch,
                 );
             }
@@ -2768,26 +2748,23 @@ impl PrimitiveStore {
                 // Update the template this instane references, which may refresh the GPU
                 // cache with any shared template data.
                 border_data.update(common_data, frame_state);
 
                 // TODO(gw): For now, the scale factors to rasterize borders at are
                 //           based on the true world transform of the primitive. When
                 //           raster roots with local scale are supported in future,
                 //           that will need to be accounted for here.
-                let relative_transform = frame_context
+                let scale = frame_context
                     .clip_scroll_tree
-                    .get_relative_transform(
-                        prim_instance.spatial_node_index,
-                        ROOT_SPATIAL_NODE_INDEX,
-                    );
+                    .get_world_transform(prim_instance.spatial_node_index)
+                    .scale_factors();
 
                 // Scale factors are normalized to a power of 2 to reduce the number of
-                // resolution changes
-                let scale = scale_factors(&relative_transform.flattened);
+                // resolution changes.
                 // For frames with a changing scale transform round scale factors up to
                 // nearest power-of-2 boundary so that we don't keep having to redraw
                 // the content as it scales up and down. Rounding up to nearest
                 // power-of-2 boundary ensures we never scale up, only down --- avoiding
                 // jaggies. It also ensures we never scale down by more than a factor of
                 // 2, avoiding bad downscaling quality.
                 let scale_width = clamp_to_scale_factor(scale.0, false);
                 let scale_height = clamp_to_scale_factor(scale.1, false);
--- a/gfx/wr/webrender/src/render_backend.rs
+++ b/gfx/wr/webrender/src/render_backend.rs
@@ -549,17 +549,16 @@ impl Document {
     fn rebuild_hit_tester(&mut self) {
         if let Some(ref mut frame_builder) = self.frame_builder {
             let accumulated_scale_factor = self.view.accumulated_scale_factor();
             let pan = self.view.pan.to_f32() / accumulated_scale_factor;
 
             self.clip_scroll_tree.update_tree(
                 pan,
                 &self.dynamic_properties,
-                None,
             );
 
             self.hit_tester = Some(frame_builder.create_hit_tester(
                 &self.clip_scroll_tree,
                 &self.data_stores.clip,
             ));
             self.hit_tester_is_valid = true;
         }
@@ -1201,17 +1200,17 @@ impl RenderBackend {
 
     fn prepare_for_frames(&mut self) {
         self.resource_cache.prepare_for_frames(SystemTime::now());
         self.gpu_cache.prepare_for_frames();
     }
 
     fn bookkeep_after_frames(&mut self) {
         self.resource_cache.bookkeep_after_frames();
-        self.gpu_cache.bookkeep_after_frames();   
+        self.gpu_cache.bookkeep_after_frames();
     }
 
     fn requires_frame_build(&mut self) -> bool {
         self.resource_cache.requires_frame_build() || self.gpu_cache.requires_frame_build()
     }
 
     fn prepare_transactions(
         &mut self,
--- a/gfx/wr/webrender/src/spatial_node.rs
+++ b/gfx/wr/webrender/src/spatial_node.rs
@@ -3,17 +3,16 @@
  * 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::{ExternalScrollId, PipelineId, PropertyBinding, ReferenceFrameKind, ScrollClamping, ScrollLocation};
 use api::{TransformStyle, ScrollSensitivity, StickyOffsetBounds};
 use api::units::*;
 use crate::clip_scroll_tree::{CoordinateSystem, CoordinateSystemId, SpatialNodeIndex, TransformUpdateState};
 use euclid::SideOffsets2D;
-use crate::gpu_types::TransformPalette;
 use crate::scene::SceneProperties;
 use crate::util::{LayoutFastTransform, LayoutToWorldFastTransform, ScaleOffset, TransformedRectKind};
 
 #[derive(Clone, Debug)]
 pub enum SpatialNodeType {
     /// A special kind of node that adjusts its position based on the position
     /// of its parent node and a given set of sticky positioning offset bounds.
     /// Sticky positioned is described in the CSS Positioned Layout Module Level 3 here:
@@ -35,17 +34,17 @@ pub struct SpatialNode {
     /// our parent reference frame, plus any accumulated scrolling offsets from nodes
     /// between our reference frame and this node. For reference frames, we also include
     /// whatever local transformation this reference frame provides.
     pub world_viewport_transform: LayoutToWorldFastTransform,
 
     /// World transform for content transformed by this node.
     pub world_content_transform: LayoutToWorldFastTransform,
 
-    /// The current transform kind of world_content_transform.
+    /// The current transform kind of this node.
     pub transform_kind: TransformedRectKind,
 
     /// Pipeline that this layer belongs to
     pub pipeline_id: PipelineId,
 
     /// Parent layer. If this is None, we are the root node.
     pub parent: Option<SpatialNodeIndex>,
 
@@ -231,47 +230,32 @@ impl SpatialNode {
         state: &TransformUpdateState,
     ) {
         self.invertible = false;
         self.coordinate_system_id = state.current_coordinate_system_id;
         self.world_content_transform = LayoutToWorldFastTransform::identity();
         self.world_viewport_transform = LayoutToWorldFastTransform::identity();
     }
 
-    pub fn push_gpu_data(
-        &mut self,
-        transform_palette: &mut TransformPalette,
-        node_index: SpatialNodeIndex,
-    ) {
-        if self.invertible {
-            transform_palette.set_world_transform(
-                node_index,
-                self.world_content_transform
-                    .to_transform()
-                    .into_owned()
-            );
-        }
-    }
-
     pub fn update(
         &mut self,
         state: &mut TransformUpdateState,
         coord_systems: &mut Vec<CoordinateSystem>,
         scene_properties: &SceneProperties,
         previous_spatial_nodes: &[SpatialNode],
     ) {
         // If any of our parents was not rendered, we are not rendered either and can just
         // quit here.
         if !state.invertible {
             self.mark_uninvertible(state);
             return;
         }
 
         self.update_transform(state, coord_systems, scene_properties, previous_spatial_nodes);
-        self.transform_kind = self.world_content_transform.kind();
+        self.transform_kind = self.world_viewport_transform.kind();
 
         // If this node is a reference frame, we check if it has a non-invertible matrix.
         // For non-reference-frames we assume that they will produce only additional
         // translations which should be invertible.
         match self.node_type {
             SpatialNodeType::ReferenceFrame(info) if !info.invertible => {
                 self.mark_uninvertible(state);
                 return;
@@ -358,17 +342,20 @@ 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,
-                            transform_style: info.transform_style,
+                            should_flatten: match (info.transform_style, info.kind) {
+                                (TransformStyle::Flat, ReferenceFrameKind::Transform) => true,
+                                (_, _) => false,
+                            },
                             parent: Some(state.current_coordinate_system_id),
                         };
                         state.current_coordinate_system_id = CoordinateSystemId(coord_systems.len() as u32);
                         coord_systems.push(coord_system);
                     }
                 }
 
                 // Ensure that the current coordinate system ID is propagated to child
@@ -388,26 +375,27 @@ impl SpatialNode {
                 // provided by our own sticky positioning.
                 let accumulated_offset = state.parent_accumulated_scroll_offset + sticky_offset;
                 self.world_viewport_transform = if accumulated_offset != LayoutVector2D::zero() {
                     state.parent_reference_frame_transform.pre_translate(&accumulated_offset)
                 } else {
                     state.parent_reference_frame_transform
                 };
 
-                // The transformation for any content inside of us is the viewport transformation, plus
-                // whatever scrolling offset we supply as well.
                 let scroll_offset = self.scroll_offset();
                 self.world_content_transform = if scroll_offset != LayoutVector2D::zero() {
                     self.world_viewport_transform.pre_translate(&scroll_offset)
                 } else {
                     self.world_viewport_transform
                 };
 
-                let added_offset = state.parent_accumulated_scroll_offset + sticky_offset + scroll_offset;
+
+                // The transformation for any content inside of us is the viewport transformation, plus
+                // whatever scrolling offset we supply as well.
+                let added_offset = accumulated_offset + self.scroll_offset();
                 self.coordinate_system_relative_scale_offset =
                     state.coordinate_system_relative_scale_offset.offset(added_offset.to_untyped());
 
                 if let SpatialNodeType::StickyFrame(ref mut info) = self.node_type {
                     info.current_offset = sticky_offset;
                 }
 
                 self.coordinate_system_id = state.current_coordinate_system_id;
@@ -637,24 +625,16 @@ impl SpatialNode {
     }
 
     pub fn matches_external_id(&self, external_id: ExternalScrollId) -> bool {
         match self.node_type {
             SpatialNodeType::ScrollFrame(info) if info.external_id == Some(external_id) => true,
             _ => false,
         }
     }
-
-    pub fn transform_style(&self) -> TransformStyle {
-        match self.node_type {
-            SpatialNodeType::ReferenceFrame(ref info) => info.transform_style,
-            SpatialNodeType::StickyFrame(_) |
-            SpatialNodeType::ScrollFrame(_) => TransformStyle::Flat,
-        }
-    }
 }
 
 /// Defines whether we have an implicit scroll frame for a pipeline root,
 /// or an explicitly defined scroll frame from the display list.
 #[derive(Copy, Clone, Debug)]
 pub enum ScrollFrameKind {
     PipelineRoot,
     Explicit,
@@ -847,17 +827,17 @@ fn test_cst_perspective_relative_scroll(
         PropertyBinding::Value(transform),
         ReferenceFrameKind::Perspective {
             scrolling_relative_to: Some(ext_scroll_id),
         },
         LayoutVector2D::zero(),
         pipeline_id,
     );
 
-    cst.update_tree(WorldPoint::zero(), &SceneProperties::new(), None);
+    cst.update_tree(WorldPoint::zero(), &SceneProperties::new());
 
     let scroll_offset = compute_offset_from(
         cst.spatial_nodes[ref_frame.0 as usize].parent,
         ext_scroll_id,
         &cst.spatial_nodes,
     );
 
     assert!(scroll_offset.x.approx_eq(&0.0));
new file mode 100644
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/flatten-twice-ref.yaml
@@ -0,0 +1,6 @@
+---
+root:
+  items:
+    - type: rect
+      bounds: [100, 100, 200, 100]
+      color: green
new file mode 100644
--- /dev/null
+++ b/gfx/wr/wrench/reftests/transforms/flatten-twice.yaml
@@ -0,0 +1,21 @@
+# This test ensures that we flatten the "flat" style trasformations.
+# If the flattening doesn't happen here, the rect gets rotated back
+# to the original position.
+---
+root:
+  items:
+    -
+      bounds: [100, 100, 0, 0]
+      type: stacking-context
+      transform: rotate-x(45)
+      transform-origin: 0 0
+      items:
+        -
+          type: "stacking-context"
+          transform: rotate-x(-45)
+          transform-origin: 0 0
+          items:
+            -
+              bounds: [0, 0, 200, 200]
+              type: rect
+              color: green
--- a/gfx/wr/wrench/reftests/transforms/reftest.list
+++ b/gfx/wr/wrench/reftests/transforms/reftest.list
@@ -32,10 +32,11 @@ platform(linux,mac) fuzzy(9,348) == pers
 == 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
+== flatten-twice.yaml flatten-twice-ref.yaml
 == strange-w.yaml strange-w-ref.yaml
 == big-axis-aligned-scale.yaml big-axis-aligned-scale-ref.yaml
--- a/testing/web-platform/meta/css/compositing/mix-blend-mode/mix-blend-mode-both-parent-and-blended-with-3D-transform.html.ini
+++ b/testing/web-platform/meta/css/compositing/mix-blend-mode/mix-blend-mode-both-parent-and-blended-with-3D-transform.html.ini
@@ -1,5 +1,4 @@
 [mix-blend-mode-both-parent-and-blended-with-3D-transform.html]
   expected:
     if (os == "android") and not e10s: FAIL
     if (os == "android") and e10s: FAIL
-    if webrender or (os == "android" and not e10s): FAIL
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-transforms/transform-flattening-001.html.ini
+++ /dev/null
@@ -1,3 +0,0 @@
-[transform-flattening-001.html]
-  expected:
-    if webrender: FAIL
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-transforms/transform3d-preserve3d-008.html.ini
+++ /dev/null
@@ -1,3 +0,0 @@
-[transform3d-preserve3d-008.html]
-  expected:
-    if webrender: FAIL
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-transforms/transform3d-preserve3d-009.html.ini
+++ /dev/null
@@ -1,3 +0,0 @@
-[transform3d-preserve3d-009.html]
-  expected:
-    if webrender: FAIL
deleted file mode 100644
--- a/testing/web-platform/meta/css/css-transforms/transform3d-preserve3d-013.html.ini
+++ /dev/null
@@ -1,3 +0,0 @@
-[transform3d-preserve3d-013.html]
-  expected:
-    if webrender: FAIL