[sksg] 4x4 matrix support

Refactor the scene graph transform hierarchy to support 4x4 matrices:

  * rename current Transform to TransformEffect (operates as a render tree effect)
  * introduce a new Transform abstract base class, to replace current Matrix
  * refactor existing Matrix as a Transform specialization
  * introduce a new Matrix44 Transform specialization
  * refactor the existing composition helper (ComposedMatrix) as Concat,
    a Transform specialization (using composition instead of Matrix inheritance)

Change-Id: Ic3c1b499e10a0a229a7a76d4bef3dbc6a8b49194
Reviewed-on: https://skia-review.googlesource.com/c/182666
Reviewed-by: Mike Reed <reed@google.com>
Commit-Queue: Florin Malita <fmalita@chromium.org>
diff --git a/modules/skottie/src/Skottie.cpp b/modules/skottie/src/Skottie.cpp
index 65fed85..516d0eb 100644
--- a/modules/skottie/src/Skottie.cpp
+++ b/modules/skottie/src/Skottie.cpp
@@ -61,13 +61,13 @@
     fLogger->log(lvl, buff, jsonstr.c_str());
 }
 
-sk_sp<sksg::Matrix> AnimationBuilder::attachMatrix2D(const skjson::ObjectValue& t,
-                                                     AnimatorScope* ascope,
-                                                     sk_sp<sksg::Matrix> parentMatrix) const {
+sk_sp<sksg::Transform> AnimationBuilder::attachMatrix2D(const skjson::ObjectValue& t,
+                                                        AnimatorScope* ascope,
+                                                        sk_sp<sksg::Transform> parent) const {
     static const VectorValue g_default_vec_0   = {  0,   0},
                              g_default_vec_100 = {100, 100};
 
-    auto matrix = sksg::Matrix::Make(SkMatrix::I(), parentMatrix);
+    auto matrix = sksg::Matrix::Make(SkMatrix::I());
     auto adapter = sk_make_sp<TransformAdapter2D>(matrix);
 
     auto bound = this->bindProperty<VectorValue>(t["a"], ascope,
@@ -104,16 +104,18 @@
 
     const auto dispatched = this->dispatchTransformProperty(adapter);
 
-    return (bound || dispatched) ? matrix : parentMatrix;
+    return (bound || dispatched)
+        ? sksg::Transform::MakeConcat(std::move(parent), std::move(matrix))
+        : parent;
 }
 
-sk_sp<sksg::Matrix> AnimationBuilder::attachMatrix3D(const skjson::ObjectValue& t,
-                                                     AnimatorScope* ascope,
-                                                     sk_sp<sksg::Matrix> parentMatrix) const {
+sk_sp<sksg::Transform> AnimationBuilder::attachMatrix3D(const skjson::ObjectValue& t,
+                                                        AnimatorScope* ascope,
+                                                        sk_sp<sksg::Transform> parent) const {
     static const VectorValue g_default_vec_0   = {  0,   0,   0},
                              g_default_vec_100 = {100, 100, 100};
 
-    auto matrix = sksg::Matrix::Make(SkMatrix::I(), parentMatrix);
+    auto matrix = sksg::Matrix44::Make(SkMatrix::I());
     auto adapter = sk_make_sp<TransformAdapter3D>(matrix);
 
     auto bound = this->bindProperty<VectorValue>(t["a"], ascope,
@@ -156,7 +158,9 @@
 
     // TODO: dispatch 3D transform properties
 
-    return bound ? matrix : parentMatrix;
+    return (bound)
+        ? sksg::Transform::MakeConcat(std::move(parent), std::move(matrix))
+        : parent;
 }
 
 sk_sp<sksg::RenderNode> AnimationBuilder::attachOpacity(const skjson::ObjectValue& jtransform,
diff --git a/modules/skottie/src/SkottieAdapter.cpp b/modules/skottie/src/SkottieAdapter.cpp
index 0d53869..805f693 100644
--- a/modules/skottie/src/SkottieAdapter.cpp
+++ b/modules/skottie/src/SkottieAdapter.cpp
@@ -73,12 +73,12 @@
     fZ = v.size() > 2 ? v[2] : 0;
 }
 
-TransformAdapter3D::TransformAdapter3D(sk_sp<sksg::Matrix> matrix)
+TransformAdapter3D::TransformAdapter3D(sk_sp<sksg::Matrix44> matrix)
     : fMatrixNode(std::move(matrix)) {}
 
 TransformAdapter3D::~TransformAdapter3D() = default;
 
-SkMatrix TransformAdapter3D::totalMatrix() const {
+SkMatrix44 TransformAdapter3D::totalMatrix() const {
     SkMatrix44 t;
 
     t.setTranslate(-fAnchorPoint.fX, -fAnchorPoint.fY, -fAnchorPoint.fZ);
diff --git a/modules/skottie/src/SkottieAdapter.h b/modules/skottie/src/SkottieAdapter.h
index 1ab9a7b..42ea7f8 100644
--- a/modules/skottie/src/SkottieAdapter.h
+++ b/modules/skottie/src/SkottieAdapter.h
@@ -21,6 +21,7 @@
 class Group;
 class LinearGradient;
 class Matrix;
+class Matrix44;
 class Path;
 class RadialGradient;
 class RRect;
@@ -105,7 +106,7 @@
 
 class TransformAdapter3D final : public SkNVRefCnt<TransformAdapter3D> {
 public:
-    explicit TransformAdapter3D(sk_sp<sksg::Matrix>);
+    explicit TransformAdapter3D(sk_sp<sksg::Matrix44>);
     ~TransformAdapter3D();
 
     struct Vec3 {
@@ -124,12 +125,12 @@
     ADAPTER_PROPERTY(Rotation   , Vec3, Vec3({  0,   0,   0}))
     ADAPTER_PROPERTY(Scale      , Vec3, Vec3({100, 100, 100}))
 
-    SkMatrix totalMatrix() const;
+    SkMatrix44 totalMatrix() const;
 
 private:
     void apply();
 
-    sk_sp<sksg::Matrix> fMatrixNode;
+    sk_sp<sksg::Matrix44> fMatrixNode;
 };
 
 class GradientAdapter : public SkRefCnt {
diff --git a/modules/skottie/src/SkottieLayer.cpp b/modules/skottie/src/SkottieLayer.cpp
index 2bb6264..3cc4122 100644
--- a/modules/skottie/src/SkottieLayer.cpp
+++ b/modules/skottie/src/SkottieLayer.cpp
@@ -372,7 +372,7 @@
         return std::move(image_node);
     }
 
-    return sksg::Transform::Make(std::move(image_node),
+    return sksg::TransformEffect::Make(std::move(image_node),
         SkMatrix::MakeRectToRect(SkRect::Make(image->bounds()),
                                  SkRect::Make(asset_size),
                                  SkMatrix::kCenter_ScaleToFit));
@@ -399,13 +399,13 @@
     AttachLayerContext(const skjson::ArrayValue& jlayers, AnimatorScope* scope)
         : fLayerList(jlayers), fScope(scope) {}
 
-    const skjson::ArrayValue&            fLayerList;
-    AnimatorScope*                       fScope;
-    SkTHashMap<int, sk_sp<sksg::Matrix>> fLayerMatrixMap;
-    sk_sp<sksg::RenderNode>              fCurrentMatte;
+    const skjson::ArrayValue&               fLayerList;
+    AnimatorScope*                          fScope;
+    SkTHashMap<int, sk_sp<sksg::Transform>> fLayerMatrixMap;
+    sk_sp<sksg::RenderNode>                 fCurrentMatte;
 
-    sk_sp<sksg::Matrix> AttachLayerMatrix(const skjson::ObjectValue& jlayer,
-                                          const AnimationBuilder* abuilder) {
+    sk_sp<sksg::Transform> attachLayerTransform(const skjson::ObjectValue& jlayer,
+                                                const AnimationBuilder* abuilder) {
         const auto layer_index = ParseDefault<int>(jlayer["ind"], -1);
         if (layer_index < 0)
             return nullptr;
@@ -413,13 +413,13 @@
         if (auto* m = fLayerMatrixMap.find(layer_index))
             return *m;
 
-        return this->AttachLayerMatrixImpl(jlayer, abuilder, layer_index);
+        return this->attachLayerTransformImpl(jlayer, abuilder, layer_index);
     }
 
 private:
-    sk_sp<sksg::Matrix> AttachParentLayerMatrix(const skjson::ObjectValue& jlayer,
-                                                const AnimationBuilder* abuilder,
-                                                int layer_index) {
+    sk_sp<sksg::Transform> attachParentLayerTransform(const skjson::ObjectValue& jlayer,
+                                                      const AnimationBuilder* abuilder,
+                                                      int layer_index) {
         const auto parent_index = ParseDefault<int>(jlayer["parent"], -1);
         if (parent_index < 0 || parent_index == layer_index)
             return nullptr;
@@ -431,29 +431,29 @@
             if (!l) continue;
 
             if (ParseDefault<int>((*l)["ind"], -1) == parent_index) {
-                return this->AttachLayerMatrixImpl(*l, abuilder, parent_index);
+                return this->attachLayerTransformImpl(*l, abuilder, parent_index);
             }
         }
 
         return nullptr;
     }
 
-    sk_sp<sksg::Matrix> AttachLayerMatrixImpl(const skjson::ObjectValue& jlayer,
-                                              const AnimationBuilder* abuilder,
-                                              int layer_index) {
+    sk_sp<sksg::Transform> attachLayerTransformImpl(const skjson::ObjectValue& jlayer,
+                                                    const AnimationBuilder* abuilder,
+                                                    int layer_index) {
         SkASSERT(!fLayerMatrixMap.find(layer_index));
 
         // Add a stub entry to break recursion cycles.
         fLayerMatrixMap.set(layer_index, nullptr);
 
-        auto parent_matrix = this->AttachParentLayerMatrix(jlayer, abuilder, layer_index);
+        auto parent_matrix = this->attachParentLayerTransform(jlayer, abuilder, layer_index);
 
         if (const skjson::ObjectValue* jtransform = jlayer["ks"]) {
-            auto matrix_node = (ParseDefault<int>(jlayer["ddd"], 0) == 0)
+            auto transform_node = (ParseDefault<int>(jlayer["ddd"], 0) == 0)
                 ? abuilder->attachMatrix2D(*jtransform, fScope, std::move(parent_matrix))
                 : abuilder->attachMatrix3D(*jtransform, fScope, std::move(parent_matrix));
 
-            return *fLayerMatrixMap.set(layer_index, std::move(matrix_node));
+            return *fLayerMatrixMap.set(layer_index, std::move(transform_node));
         }
         return nullptr;
     }
@@ -509,8 +509,8 @@
     layer = AttachMask((*jlayer)["masksProperties"], this, &layer_animators, std::move(layer));
 
     // Optional layer transform.
-    if (auto layerMatrix = layerCtx->AttachLayerMatrix(*jlayer, this)) {
-        layer = sksg::Transform::Make(std::move(layer), std::move(layerMatrix));
+    if (auto layer_transform = layerCtx->attachLayerTransform(*jlayer, this)) {
+        layer = sksg::TransformEffect::Make(std::move(layer), std::move(layer_transform));
     }
 
     // Optional layer opacity.
diff --git a/modules/skottie/src/SkottiePriv.h b/modules/skottie/src/SkottiePriv.h
index ee41f37..f6cdfa3 100644
--- a/modules/skottie/src/SkottiePriv.h
+++ b/modules/skottie/src/SkottiePriv.h
@@ -33,6 +33,7 @@
 class Matrix;
 class Path;
 class RenderNode;
+class Transform;
 } // namespace sksg
 
 namespace skottie {
@@ -71,10 +72,10 @@
 
     sk_sp<sksg::Color> attachColor(const skjson::ObjectValue&, AnimatorScope*,
                                    const char prop_name[]) const;
-    sk_sp<sksg::Matrix> attachMatrix2D(const skjson::ObjectValue&, AnimatorScope*,
-                                       sk_sp<sksg::Matrix>) const;
-    sk_sp<sksg::Matrix> attachMatrix3D(const skjson::ObjectValue&, AnimatorScope*,
-                                       sk_sp<sksg::Matrix>) const;
+    sk_sp<sksg::Transform> attachMatrix2D(const skjson::ObjectValue&, AnimatorScope*,
+                                          sk_sp<sksg::Transform>) const;
+    sk_sp<sksg::Transform> attachMatrix3D(const skjson::ObjectValue&, AnimatorScope*,
+                                          sk_sp<sksg::Transform>) const;
     sk_sp<sksg::RenderNode> attachOpacity(const skjson::ObjectValue&, AnimatorScope*,
                                       sk_sp<sksg::RenderNode>) const;
     sk_sp<sksg::Path> attachPath(const skjson::Value&, AnimatorScope*) const;
diff --git a/modules/skottie/src/SkottieShapeLayer.cpp b/modules/skottie/src/SkottieShapeLayer.cpp
index 76b5658..2731b75 100644
--- a/modules/skottie/src/SkottieShapeLayer.cpp
+++ b/modules/skottie/src/SkottieShapeLayer.cpp
@@ -587,7 +587,7 @@
         shape_wrapper = sksg::Group::Make(std::move(draws));
     }
 
-    sk_sp<sksg::Matrix> shape_matrix;
+    sk_sp<sksg::Transform> shape_transform;
     if (jtransform) {
         const AutoPropertyTracker apt(this, *jtransform);
 
@@ -596,8 +596,8 @@
         // of the dangling/uncommitted ones.
         AnimatorScope local_scope;
 
-        if ((shape_matrix = this->attachMatrix2D(*jtransform, &local_scope, nullptr))) {
-            shape_wrapper = sksg::Transform::Make(std::move(shape_wrapper), shape_matrix);
+        if ((shape_transform = this->attachMatrix2D(*jtransform, &local_scope, nullptr))) {
+            shape_wrapper = sksg::TransformEffect::Make(std::move(shape_wrapper), shape_transform);
         }
         shape_wrapper = this->attachOpacity(*jtransform, &local_scope, std::move(shape_wrapper));
 
@@ -609,8 +609,8 @@
 
     // Push transformed local geometries to parent list, for subsequent paints.
     for (auto& geo : geos) {
-        ctx->fGeometryStack->push_back(shape_matrix
-            ? sksg::GeometryTransform::Make(std::move(geo), shape_matrix)
+        ctx->fGeometryStack->push_back(shape_transform
+            ? sksg::GeometryTransform::Make(std::move(geo), shape_transform)
             : std::move(geo));
     }
 
diff --git a/modules/sksg/include/SkSGGeometryTransform.h b/modules/sksg/include/SkSGGeometryTransform.h
index fe7e026..a0c45ac 100644
--- a/modules/sksg/include/SkSGGeometryTransform.h
+++ b/modules/sksg/include/SkSGGeometryTransform.h
@@ -13,8 +13,6 @@
 #include "SkPath.h"
 #include "SkSGTransform.h"
 
-class SkMatrix;
-
 namespace sksg {
 
 /**
@@ -22,9 +20,10 @@
  */
 class GeometryTransform final : public GeometryNode {
 public:
-    static sk_sp<GeometryTransform> Make(sk_sp<GeometryNode> child, sk_sp<Matrix> matrix) {
-        return child && matrix
-            ? sk_sp<GeometryTransform>(new GeometryTransform(std::move(child), std::move(matrix)))
+    static sk_sp<GeometryTransform> Make(sk_sp<GeometryNode> child, sk_sp<Transform> transform) {
+        return child && transform
+            ? sk_sp<GeometryTransform>(new GeometryTransform(std::move(child),
+                                                             std::move(transform)))
             : nullptr;
     }
 
@@ -34,7 +33,7 @@
 
     ~GeometryTransform() override;
 
-    const sk_sp<Matrix>& getMatrix() const { return fMatrix; }
+    const sk_sp<Transform>& getTransform() const { return fTransform; }
 
 protected:
     void onClip(SkCanvas*, bool antiAlias) const override;
@@ -44,11 +43,11 @@
     SkPath onAsPath() const override;
 
 private:
-    GeometryTransform(sk_sp<GeometryNode>, sk_sp<Matrix>);
+    GeometryTransform(sk_sp<GeometryNode>, sk_sp<Transform>);
 
     const sk_sp<GeometryNode> fChild;
-    const sk_sp<Matrix>       fMatrix;
-    SkPath                    fTransformed;
+    const sk_sp<Transform>    fTransform;
+    SkPath                    fTransformedPath;
 
     using INHERITED = GeometryNode;
 };
diff --git a/modules/sksg/include/SkSGTransform.h b/modules/sksg/include/SkSGTransform.h
index 790373b..e08f4ec 100644
--- a/modules/sksg/include/SkSGTransform.h
+++ b/modules/sksg/include/SkSGTransform.h
@@ -11,21 +11,40 @@
 #include "SkSGEffectNode.h"
 
 #include "SkMatrix.h"
+#include "SkMatrix44.h"
 
 namespace sksg {
 
 /**
- * Concrete node, wrapping an SkMatrix, with an optional parent Matrix (to allow chaining):
- *
- *    M' = parent x M
+ * Transformations base class.
  */
-class Matrix : public Node {
+class Transform : public Node {
 public:
-    static sk_sp<Matrix> Make(const SkMatrix& m, sk_sp<Matrix> parent = nullptr);
+    // Compose T = A x B
+    static sk_sp<Transform> MakeConcat(sk_sp<Transform> a, sk_sp<Transform> b);
+
+    // TODO: hide these from public API?
+    virtual SkMatrix   asMatrix  () const = 0;
+    virtual SkMatrix44 asMatrix44() const = 0;
+
+protected:
+    Transform();
+
+private:
+    using INHERITED = Node;
+};
+
+/**
+ * Concrete, SkMatrix-backed transformation.
+ */
+class Matrix final : public Transform {
+public:
+    static sk_sp<Matrix> Make(const SkMatrix& m);
 
     SG_ATTRIBUTE(Matrix, SkMatrix, fMatrix)
 
-    virtual const SkMatrix& getTotalMatrix() const;
+    SkMatrix   asMatrix  () const override;
+    SkMatrix44 asMatrix44() const override;
 
 protected:
     explicit Matrix(const SkMatrix&);
@@ -35,27 +54,50 @@
 private:
     SkMatrix fMatrix;
 
-    typedef Node INHERITED;
+    using INHERITED = Transform;
 };
 
 /**
- * Concrete Effect node, binding a Matrix to a RenderNode.
+ * Concrete, SkMatrix44-backed transformation.
  */
-class Transform final : public EffectNode {
+class Matrix44 final : public Transform {
 public:
-    static sk_sp<Transform> Make(sk_sp<RenderNode> child, sk_sp<Matrix> matrix) {
-        return child && matrix
-            ? sk_sp<Transform>(new Transform(std::move(child), std::move(matrix)))
+    static sk_sp<Matrix44> Make(const SkMatrix44& m);
+
+    SG_ATTRIBUTE(Matrix, SkMatrix44, fMatrix)
+
+    SkMatrix   asMatrix  () const override;
+    SkMatrix44 asMatrix44() const override;
+
+protected:
+    explicit Matrix44(const SkMatrix44&);
+
+    SkRect onRevalidate(InvalidationController*, const SkMatrix&) override;
+
+private:
+    SkMatrix44 fMatrix;
+
+    using INHERITED = Transform;
+};
+
+/**
+ * Concrete Effect node, binding a Transform to a RenderNode.
+ */
+class TransformEffect final : public EffectNode {
+public:
+    static sk_sp<TransformEffect> Make(sk_sp<RenderNode> child, sk_sp<Transform> transform) {
+        return child && transform
+            ? sk_sp<TransformEffect>(new TransformEffect(std::move(child), std::move(transform)))
             : nullptr;
     }
 
-    static sk_sp<Transform> Make(sk_sp<RenderNode> child, const SkMatrix& m) {
+    static sk_sp<TransformEffect> Make(sk_sp<RenderNode> child, const SkMatrix& m) {
         return Make(std::move(child), Matrix::Make(m));
     }
 
-    ~Transform() override;
+    ~TransformEffect() override;
 
-    const sk_sp<Matrix>& getMatrix() const { return fMatrix; }
+    const sk_sp<Transform>& getTransform() const { return fTransform; }
 
 protected:
     void onRender(SkCanvas*, const RenderContext*) const override;
@@ -63,9 +105,9 @@
     SkRect onRevalidate(InvalidationController*, const SkMatrix&) override;
 
 private:
-    Transform(sk_sp<RenderNode>, sk_sp<Matrix>);
+    TransformEffect(sk_sp<RenderNode>, sk_sp<Transform>);
 
-    const sk_sp<Matrix> fMatrix;
+    const sk_sp<Transform> fTransform;
 
     typedef EffectNode INHERITED;
 };
diff --git a/modules/sksg/samples/SampleSVGPong.cpp b/modules/sksg/samples/SampleSVGPong.cpp
index 15a1afe..a849add 100644
--- a/modules/sksg/samples/SampleSVGPong.cpp
+++ b/modules/sksg/samples/SampleSVGPong.cpp
@@ -144,7 +144,7 @@
             SkMatrix::MakeRectToRect(SkRect::MakeWH(1, 1),
                                      SkRect::MakeIWH(this->width(), this->height()),
                                      SkMatrix::kFill_ScaleToFit));
-        auto root = sksg::Transform::Make(std::move(group), fContentMatrix);
+        auto root = sksg::TransformEffect::Make(std::move(group), fContentMatrix);
         fScene = sksg::Scene::Make(std::move(root), sksg::AnimatorList());
 
         // Off we go.
diff --git a/modules/sksg/src/SkSGGeometryTransform.cpp b/modules/sksg/src/SkSGGeometryTransform.cpp
index 220b056..5630099 100644
--- a/modules/sksg/src/SkSGGeometryTransform.cpp
+++ b/modules/sksg/src/SkSGGeometryTransform.cpp
@@ -11,44 +11,44 @@
 
 namespace sksg {
 
-GeometryTransform::GeometryTransform(sk_sp<GeometryNode> child, sk_sp<Matrix> matrix)
+GeometryTransform::GeometryTransform(sk_sp<GeometryNode> child, sk_sp<Transform> transform)
     : fChild(std::move(child))
-    , fMatrix(std::move(matrix)) {
+    , fTransform(std::move(transform)) {
     this->observeInval(fChild);
-    this->observeInval(fMatrix);
+    this->observeInval(fTransform);
 }
 
 GeometryTransform::~GeometryTransform() {
     this->unobserveInval(fChild);
-    this->unobserveInval(fMatrix);
+    this->unobserveInval(fTransform);
 }
 
 void GeometryTransform::onClip(SkCanvas* canvas, bool antiAlias) const {
-    canvas->clipPath(fTransformed, SkClipOp::kIntersect, antiAlias);
+    canvas->clipPath(fTransformedPath, SkClipOp::kIntersect, antiAlias);
 }
 
 void GeometryTransform::onDraw(SkCanvas* canvas, const SkPaint& paint) const {
-    canvas->drawPath(fTransformed, paint);
+    canvas->drawPath(fTransformedPath, paint);
 }
 
 SkRect GeometryTransform::onRevalidate(InvalidationController* ic, const SkMatrix& ctm) {
     SkASSERT(this->hasInval());
 
     // We don't care about matrix reval results.
-    fMatrix->revalidate(ic, ctm);
-    const auto& m = fMatrix->getMatrix();
+    fTransform->revalidate(ic, ctm);
+    const auto m = fTransform->asMatrix();
 
     auto bounds = fChild->revalidate(ic, ctm);
-    fTransformed = fChild->asPath();
-    fTransformed.transform(m);
-    fTransformed.shrinkToFit();
+    fTransformedPath = fChild->asPath();
+    fTransformedPath.transform(m);
+    fTransformedPath.shrinkToFit();
 
     m.mapRect(&bounds);
     return  bounds;
 }
 
 SkPath GeometryTransform::onAsPath() const {
-    return fTransformed;
+    return fTransformedPath;
 }
 
 } // namespace sksg
diff --git a/modules/sksg/src/SkSGTransform.cpp b/modules/sksg/src/SkSGTransform.cpp
index 76b180a..ddfabd9 100644
--- a/modules/sksg/src/SkSGTransform.cpp
+++ b/modules/sksg/src/SkSGTransform.cpp
@@ -10,53 +10,75 @@
 #include "SkCanvas.h"
 
 namespace sksg {
+
 namespace {
 
-class ComposedMatrix final : public Matrix {
+// Always compose in 4x4 for now.
+class Concat final : public Transform {
 public:
-    ComposedMatrix(const SkMatrix& m, sk_sp<Matrix> parent)
-        : INHERITED(m)
-        , fParent(std::move(parent)) {
-        SkASSERT(fParent);
-        this->observeInval(fParent);
+    Concat(sk_sp<Transform> a, sk_sp<Transform> b)
+        : fA(std::move(a)), fB(std::move(b)) {
+        SkASSERT(fA);
+        SkASSERT(fB);
+
+        this->observeInval(fA);
+        this->observeInval(fB);
     }
 
-    ~ComposedMatrix() override {
-        this->unobserveInval(fParent);
+    ~Concat() override {
+        this->unobserveInval(fA);
+        this->unobserveInval(fB);
     }
 
-    const SkMatrix& getTotalMatrix() const override {
-        SkASSERT(!this->hasInval());
-        return fTotalMatrix;
+    SkMatrix asMatrix() const override {
+        return fComposed;
+    }
+
+    SkMatrix44 asMatrix44() const override {
+        return fComposed;
     }
 
 protected:
     SkRect onRevalidate(InvalidationController* ic, const SkMatrix& ctm) override {
-        fParent->revalidate(ic, ctm);
-        fTotalMatrix = SkMatrix::Concat(fParent->getTotalMatrix(), this->getMatrix());
+        fA->revalidate(ic, ctm);
+        fB->revalidate(ic, ctm);
+
+        fComposed.setConcat(fA->asMatrix44(), fB->asMatrix44());
         return SkRect::MakeEmpty();
     }
 
 private:
-    const sk_sp<Matrix> fParent;
-    SkMatrix            fTotalMatrix; // cached during revalidation.
+    const sk_sp<Transform> fA, fB;
+    SkMatrix44             fComposed;
 
-    using INHERITED = Matrix;
+    using INHERITED = Transform;
 };
 
 } // namespace
 
-sk_sp<Matrix> Matrix::Make(const SkMatrix& m, sk_sp<Matrix> parent) {
-    return sk_sp<Matrix>(parent ? new ComposedMatrix(m, std::move(parent))
-                                : new Matrix(m));
+// Transform nodes don't generate damage on their own, but via ancestor TransformEffects.
+Transform::Transform() : INHERITED(kBubbleDamage_Trait) {}
+
+sk_sp<Transform> Transform::MakeConcat(sk_sp<Transform> a, sk_sp<Transform> b) {
+    if (!a) {
+        return b;
+    }
+
+    return b ? sk_make_sp<Concat>(std::move(a), std::move(b))
+             : a;
 }
 
-// Matrix nodes don't generate damage on their own, but via aggregation ancestor Transform nodes.
-Matrix::Matrix(const SkMatrix& m)
-    : INHERITED(kBubbleDamage_Trait)
-    , fMatrix(m) {}
+sk_sp<Matrix> Matrix::Make(const SkMatrix& m) {
+    return sk_sp<Matrix>(new Matrix(m));
+}
 
-const SkMatrix& Matrix::getTotalMatrix() const {
+Matrix::Matrix(const SkMatrix& m) : fMatrix(m) {}
+
+SkMatrix Matrix::asMatrix() const {
+    return fMatrix;
+}
+
+SkMatrix44 Matrix::asMatrix44() const {
     return fMatrix;
 }
 
@@ -64,30 +86,48 @@
     return SkRect::MakeEmpty();
 }
 
-Transform::Transform(sk_sp<RenderNode> child, sk_sp<Matrix> matrix)
+sk_sp<Matrix44> Matrix44::Make(const SkMatrix44& m) {
+    return sk_sp<Matrix44>(new Matrix44(m));
+}
+
+Matrix44::Matrix44(const SkMatrix44& m) : fMatrix(m) {}
+
+SkMatrix Matrix44::asMatrix() const {
+    return fMatrix;
+}
+
+SkMatrix44 Matrix44::asMatrix44() const {
+    return fMatrix;
+}
+
+SkRect Matrix44::onRevalidate(InvalidationController*, const SkMatrix&) {
+    return SkRect::MakeEmpty();
+}
+
+TransformEffect::TransformEffect(sk_sp<RenderNode> child, sk_sp<Transform> transform)
     : INHERITED(std::move(child))
-    , fMatrix(std::move(matrix)) {
-    this->observeInval(fMatrix);
+    , fTransform(std::move(transform)) {
+    this->observeInval(fTransform);
 }
 
-Transform::~Transform() {
-    this->unobserveInval(fMatrix);
+TransformEffect::~TransformEffect() {
+    this->unobserveInval(fTransform);
 }
 
-void Transform::onRender(SkCanvas* canvas, const RenderContext* ctx) const {
-    const auto& m = fMatrix->getTotalMatrix();
+void TransformEffect::onRender(SkCanvas* canvas, const RenderContext* ctx) const {
+    const auto m = fTransform->asMatrix();
     SkAutoCanvasRestore acr(canvas, !m.isIdentity());
     canvas->concat(m);
     this->INHERITED::onRender(canvas, ctx);
 }
 
-SkRect Transform::onRevalidate(InvalidationController* ic, const SkMatrix& ctm) {
+SkRect TransformEffect::onRevalidate(InvalidationController* ic, const SkMatrix& ctm) {
     SkASSERT(this->hasInval());
 
     // We don't care about matrix reval results.
-    fMatrix->revalidate(ic, ctm);
+    fTransform->revalidate(ic, ctm);
 
-    const auto& m = fMatrix->getTotalMatrix();
+    const auto m = fTransform->asMatrix();
     auto bounds = this->INHERITED::onRevalidate(ic, SkMatrix::Concat(ctm, m));
     m.mapRect(&bounds);
 
diff --git a/modules/sksg/tests/SGTest.cpp b/modules/sksg/tests/SGTest.cpp
index 1b6e4f9..0d614ff 100644
--- a/modules/sksg/tests/SGTest.cpp
+++ b/modules/sksg/tests/SGTest.cpp
@@ -61,7 +61,7 @@
          r2     = sksg::Rect::Make(SkRect::MakeWH(100, 100));
     auto grp    = sksg::Group::Make();
     auto matrix = sksg::Matrix::Make(SkMatrix::I());
-    auto root   = sksg::Transform::Make(grp, matrix);
+    auto root   = sksg::TransformEffect::Make(grp, matrix);
 
     grp->addChild(sksg::Draw::Make(r1, color));
     grp->addChild(sksg::Draw::Make(r2, color));
@@ -129,9 +129,10 @@
     auto color = sksg::Color::Make(0xff000000);
     auto rect  = sksg::Rect::Make(SkRect::MakeWH(100, 100));
     auto m1    = sksg::Matrix::Make(SkMatrix::I()),
-         m2    = sksg::Matrix::Make(SkMatrix::I(), m1);
-    auto t1    = sksg::Transform::Make(sksg::Draw::Make(rect, color), m2),
-         t2    = sksg::Transform::Make(sksg::Draw::Make(rect, color), m1);
+         m2    = sksg::Matrix::Make(SkMatrix::I());
+    auto t1    = sksg::TransformEffect::Make(sksg::Draw::Make(rect, color),
+                                             sksg::Transform::MakeConcat(m1, m2)),
+         t2    = sksg::TransformEffect::Make(sksg::Draw::Make(rect, color), m1);
     auto root  = sksg::Group::Make();
     root->addChild(t1);
     root->addChild(t2);
diff --git a/src/core/SkMatrix44.cpp b/src/core/SkMatrix44.cpp
index 1b08987..07aba07 100644
--- a/src/core/SkMatrix44.cpp
+++ b/src/core/SkMatrix44.cpp
@@ -998,7 +998,7 @@
 SkMatrix44::operator SkMatrix() const {
     SkMatrix dst;
 
-    dst[SkMatrix::kMScaleX]  = SkMScalarToScalar(fMat[0][0]);
+    dst[SkMatrix::kMScaleX] = SkMScalarToScalar(fMat[0][0]);
     dst[SkMatrix::kMSkewX]  = SkMScalarToScalar(fMat[1][0]);
     dst[SkMatrix::kMTransX] = SkMScalarToScalar(fMat[3][0]);
 
diff --git a/tools/viewer/SlideDir.cpp b/tools/viewer/SlideDir.cpp
index 09ecd7c..54db653 100644
--- a/tools/viewer/SlideDir.cpp
+++ b/tools/viewer/SlideDir.cpp
@@ -101,9 +101,10 @@
 } // namespace
 
 struct SlideDir::Rec {
-    sk_sp<Slide>           fSlide;
-    sk_sp<sksg::Transform> fTransform;
-    SkRect                 fRect;
+    sk_sp<Slide>            fSlide;
+    sk_sp<sksg::RenderNode> fSlideRoot;
+    sk_sp<sksg::Matrix>     fMatrix;
+    SkRect                  fRect;
 };
 
 class SlideDir::FocusController final : public sksg::Animator {
@@ -128,9 +129,9 @@
         fTarget = target;
 
         // Move the shade & slide to front.
-        fDir->fRoot->removeChild(fTarget->fTransform);
+        fDir->fRoot->removeChild(fTarget->fSlideRoot);
         fDir->fRoot->addChild(fShade);
-        fDir->fRoot->addChild(fTarget->fTransform);
+        fDir->fRoot->addChild(fTarget->fSlideRoot);
 
         fM0 = SlideMatrix(fTarget->fSlide, fTarget->fRect);
         fM1 = SlideMatrix(fTarget->fSlide, fRect);
@@ -197,7 +198,7 @@
         }
 
         SkASSERT(fTarget);
-        fTarget->fTransform->getMatrix()->setMatrix(m);
+        fTarget->fMatrix->setMatrix(m);
 
         const auto shadeOpacity = fOpacity0 + map_t * (fOpacity1 - fOpacity0);
         fShadePaint->setOpacity(shadeOpacity);
@@ -305,7 +306,7 @@
                                                  fCellSize.height()),
                     slideRect = cell.makeInset(kPadding.width(), kPadding.height());
 
-        auto slideMatrix = SlideMatrix(slide, slideRect);
+        auto slideMatrix = sksg::Matrix::Make(SlideMatrix(slide, slideRect));
         auto adapter     = sk_make_sp<SlideAdapter>(slide);
         auto slideGrp    = sksg::Group::Make();
         slideGrp->addChild(sksg::Draw::Make(sksg::Rect::Make(SkRect::MakeIWH(slideSize.width(),
@@ -314,13 +315,13 @@
         slideGrp->addChild(adapter);
         slideGrp->addChild(MakeLabel(slide->getName(),
                                      SkPoint::Make(slideSize.width() / 2, slideSize.height()),
-                                     slideMatrix));
-        auto slideTransform = sksg::Transform::Make(std::move(slideGrp), slideMatrix);
+                                     slideMatrix->getMatrix()));
+        auto slideRoot = sksg::TransformEffect::Make(std::move(slideGrp), slideMatrix);
 
         sceneAnimators.push_back(adapter->makeForwardingAnimator());
 
-        fRoot->addChild(slideTransform);
-        fRecs.push_back({ slide, slideTransform, slideRect });
+        fRoot->addChild(slideRoot);
+        fRecs.push_back({ slide, slideRoot, slideMatrix, slideRect });
     }
 
     fScene = sksg::Scene::Make(fRoot, std::move(sceneAnimators));