Initial scene graph (SkSG)

Sketching a thin (as in close-to-skia-semantics) scene graph API, focused on
external animation, inval tracking and minimal repaint.

Only a few concrete classes/features so far:

* Rect/Color/Transform/Group
* basic inval tracking
* a trivial animated sample with inval visualization

Pretty much everything (especially naming) is volatile, so treat accordingly.

The interesting bits to review are likely in Node.{h,cpp} for inval and
SampleSGInval.cpp for usage.

Initial class hierarchy:

  * Node: invalidation/ancestors tracking
  |
   -- * RenderNode: onRender(SkCanvas)
  |   |
  |    -- * Draw (concrete): rendering a [geometry, paint] tuple
  |   |
  |    -- * Group (concrete): grouping multiple RenderNodes
  |   |
  |    -- * EffectNode: single-descendant effect wrapper
  |       |
  |        -- * Transform (concrete): transform effect
  |
   -- * PaintNode: onMakePaint()
  |   |
  |    -- * Color (concrete): SkColor paint wrapper
  |
   -- * GeometryNode: onComputeBounds(), onDraw(SkCanvas, SkPaint)
      |
       -- * Rect (concrete): SkRect wrapper

TBR=

Change-Id: Iacf9b773c181a7582ecd31ee968562f179d1aa1b
Reviewed-on: https://skia-review.googlesource.com/85502
Reviewed-by: Florin Malita <fmalita@chromium.org>
Commit-Queue: Florin Malita <fmalita@chromium.org>
diff --git a/experimental/sksg/SkSGDraw.cpp b/experimental/sksg/SkSGDraw.cpp
new file mode 100644
index 0000000..7319c63
--- /dev/null
+++ b/experimental/sksg/SkSGDraw.cpp
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "SkSGDraw.h"
+
+#include "SkSGGeometryNode.h"
+#include "SkSGPaintNode.h"
+
+namespace sksg {
+
+Draw::Draw(sk_sp<GeometryNode> geometry, sk_sp<PaintNode> paint)
+    : fGeometry(std::move(geometry))
+    , fPaint(std::move(paint)) {
+    fGeometry->addInvalReceiver(this);
+    fPaint->addInvalReceiver(this);
+}
+
+Draw::~Draw() {
+    fGeometry->removeInvalReceiver(this);
+    fPaint->removeInvalReceiver(this);
+}
+
+void Draw::onRender(SkCanvas* canvas) const {
+    fGeometry->draw(canvas, fPaint->makePaint());
+}
+
+void Draw::onRevalidate(InvalidationController* ic, const SkMatrix& ctm) {
+    SkASSERT(this->isInvalidated());
+
+    fGeometry->revalidate(ic, ctm);
+    fPaint->revalidate(ic, ctm);
+}
+
+} // namespace sksg
diff --git a/experimental/sksg/SkSGDraw.h b/experimental/sksg/SkSGDraw.h
new file mode 100644
index 0000000..f5146de
--- /dev/null
+++ b/experimental/sksg/SkSGDraw.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkSGDraw_DEFINED
+#define SkSGDraw_DEFINED
+
+#include "SkSGRenderNode.h"
+
+namespace sksg {
+
+class GeometryNode;
+class PaintNode;
+
+/**
+ * Concrete rendering node.
+ *
+ * Wraps and draws a [geometry, paint] tuple.
+ *
+ * Think Skia SkCanvas::drawFoo(foo, paint) calls.
+ */
+class Draw : public RenderNode {
+public:
+    static sk_sp<Draw> Make(sk_sp<GeometryNode> geo, sk_sp<PaintNode> paint) {
+        return (geo && paint) ? sk_sp<Draw>(new Draw(std::move(geo), std::move(paint))) : nullptr;
+    }
+
+protected:
+    Draw(sk_sp<GeometryNode>, sk_sp<PaintNode> paint);
+    ~Draw() override;
+
+    void onRender(SkCanvas*) const override;
+
+    void onRevalidate(InvalidationController*, const SkMatrix&) override;
+
+private:
+    sk_sp<GeometryNode> fGeometry;
+    sk_sp<PaintNode>    fPaint;
+
+    typedef RenderNode INHERITED;
+};
+
+} // namespace sksg
+
+#endif // SkSGDraw_DEFINED
diff --git a/experimental/sksg/SkSGEffectNode.cpp b/experimental/sksg/SkSGEffectNode.cpp
new file mode 100644
index 0000000..c09bcf0
--- /dev/null
+++ b/experimental/sksg/SkSGEffectNode.cpp
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "SkSGEffectNode.h"
+
+namespace sksg {
+
+EffectNode::EffectNode(sk_sp<RenderNode> child)
+    : fChild(std::move(child)) {
+    fChild->addInvalReceiver(this);
+}
+
+EffectNode::~EffectNode() {
+    fChild->removeInvalReceiver(this);
+}
+
+void EffectNode::onRender(SkCanvas* canvas) const {
+    fChild->render(canvas);
+}
+
+void EffectNode::onRevalidate(InvalidationController* ic, const SkMatrix& ctm) {
+    fChild->revalidate(ic, ctm);
+}
+
+} // namespace sksg
diff --git a/experimental/sksg/SkSGEffectNode.h b/experimental/sksg/SkSGEffectNode.h
new file mode 100644
index 0000000..b9a44a6
--- /dev/null
+++ b/experimental/sksg/SkSGEffectNode.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkSGEffectNode_DEFINED
+#define SkSGEffectNode_DEFINED
+
+#include "SkSGRenderNode.h"
+
+namespace sksg {
+
+/**
+ * Base class for nodes which apply some transformation when rendering
+ * their descendants.
+ *
+ * This includes transforms, clipping, filters, etc.
+ */
+class EffectNode : public RenderNode {
+protected:
+    explicit EffectNode(sk_sp<RenderNode>);
+    ~EffectNode() override;
+
+    void onRender(SkCanvas*) const override;
+
+    void onRevalidate(InvalidationController*, const SkMatrix&) override;
+
+private:
+    sk_sp<RenderNode> fChild;
+
+    typedef RenderNode INHERITED;
+};
+
+} // namespace sksg
+
+#endif // SkSGEffectNode_DEFINED
diff --git a/experimental/sksg/SkSGGeometryNode.cpp b/experimental/sksg/SkSGGeometryNode.cpp
new file mode 100644
index 0000000..ea14ec0
--- /dev/null
+++ b/experimental/sksg/SkSGGeometryNode.cpp
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "SkSGGeometryNode.h"
+
+#include "SkMatrix.h"
+#include "SkSGInvalidationController.h"
+
+namespace sksg {
+
+GeometryNode::GeometryNode()
+    : fBounds(SkRect::MakeLTRB(SK_ScalarMin, SK_ScalarMin, SK_ScalarMax, SK_ScalarMax)) {}
+
+void GeometryNode::draw(SkCanvas* canvas, const SkPaint& paint) const {
+    SkASSERT(!this->isInvalidated());
+    this->onDraw(canvas, paint);
+}
+
+static void inval_rect(const SkRect& r, const SkMatrix& ctm, InvalidationController* ic) {
+    if (ctm.isIdentity()) {
+        ic->inval(r);
+        return;
+    }
+
+    SkRect mappedRect;
+    if (!ctm.mapRect(&mappedRect, r)) {
+        mappedRect = SkRect::MakeLTRB(SK_ScalarMin, SK_ScalarMin, SK_ScalarMax, SK_ScalarMax);
+    }
+    ic->inval(mappedRect);
+}
+
+void GeometryNode::onRevalidate(InvalidationController* ic, const SkMatrix& ctm) {
+    SkASSERT(this->isInvalidated());
+
+    const auto oldBounds = fBounds;
+    fBounds = this->onComputeBounds();
+
+    inval_rect(oldBounds, ctm, ic);
+    if (fBounds != oldBounds) {
+        inval_rect(fBounds, ctm, ic);
+    }
+}
+
+} // namespace sksg
diff --git a/experimental/sksg/SkSGGeometryNode.h b/experimental/sksg/SkSGGeometryNode.h
new file mode 100644
index 0000000..52b2df0
--- /dev/null
+++ b/experimental/sksg/SkSGGeometryNode.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkSGGeometryNode_DEFINED
+#define SkSGGeometryNode_DEFINED
+
+#include "SkSGNode.h"
+
+#include "SkRect.h"
+
+class SkCanvas;
+class SkPaint;
+
+namespace sksg {
+
+/**
+ * Base class for nodes which provide 'geometry' (as opposed to paint)
+ * for drawing.
+ *
+ * Think SkRect, SkPath, etc.
+ */
+class GeometryNode : public Node {
+public:
+    void draw(SkCanvas*, const SkPaint&) const;
+
+    // SkPath asPath() const;  // unused for now
+
+protected:
+    GeometryNode();
+
+    virtual void onDraw(SkCanvas*, const SkPaint&) const = 0;
+
+    virtual SkRect onComputeBounds() const = 0;
+
+    // virtual SkPath onAsPath() const = 0; // unused for now
+
+    void onRevalidate(InvalidationController*, const SkMatrix&) override;
+
+private:
+    SkRect fBounds;
+
+    typedef Node INHERITED;
+};
+
+} // namespace sksg
+
+#endif // SkSGGeometryNode_DEFINED
diff --git a/experimental/sksg/SkSGGroup.cpp b/experimental/sksg/SkSGGroup.cpp
new file mode 100644
index 0000000..b8e28f7
--- /dev/null
+++ b/experimental/sksg/SkSGGroup.cpp
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "SkSGGroup.h"
+
+namespace sksg {
+
+Group::Group() {}
+
+Group::~Group() {
+    for (const auto& child : fChildren) {
+        child->removeInvalReceiver(this);
+    }
+}
+
+void Group::addChild(sk_sp<RenderNode> node) {
+    // should we allow duplicates?
+    for (const auto& child : fChildren) {
+        if (child == node) {
+            return;
+        }
+    }
+
+    node->addInvalReceiver(this);
+    fChildren.push_back(std::move(node));
+}
+
+void Group::removeChild(const sk_sp<RenderNode>& node) {
+    int origCount = fChildren.count();
+    for (int i = 0; i < origCount; ++i) {
+        if (fChildren[i] == node) {
+            fChildren.removeShuffle(i);
+            node->removeInvalReceiver(this);
+            break;
+        }
+    }
+    SkASSERT(fChildren.count() == origCount - 1);
+}
+
+void Group::onRender(SkCanvas* canvas) const {
+    for (const auto& child : fChildren) {
+        child->render(canvas);
+    }
+}
+
+void Group::onRevalidate(InvalidationController* ic, const SkMatrix& ctm) {
+    for (const auto& child : fChildren) {
+        child->revalidate(ic, ctm);
+    }
+}
+
+} // namespace sksg
diff --git a/experimental/sksg/SkSGGroup.h b/experimental/sksg/SkSGGroup.h
new file mode 100644
index 0000000..2ed9d0e
--- /dev/null
+++ b/experimental/sksg/SkSGGroup.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkSGGroup_DEFINED
+#define SkSGGroup_DEFINED
+
+#include "SkSGRenderNode.h"
+
+#include "SkTArray.h"
+
+namespace sksg {
+
+/**
+ * Concrete node, grouping together multiple descendants.
+ */
+class Group : public RenderNode {
+public:
+    static sk_sp<Group> Make() {
+        return sk_sp<Group>(new Group());
+    }
+
+    void addChild(sk_sp<RenderNode>);
+    void removeChild(const sk_sp<RenderNode>&);
+
+protected:
+    Group();
+    ~Group() override;
+
+    void onRender(SkCanvas*) const override;
+    void onRevalidate(InvalidationController*, const SkMatrix&) override;
+
+private:
+    SkTArray<sk_sp<RenderNode>, true> fChildren;
+
+    typedef RenderNode INHERITED;
+};
+
+} // namespace sksg
+
+#endif // SkSGGroup_DEFINED
diff --git a/experimental/sksg/SkSGInvalidationController.cpp b/experimental/sksg/SkSGInvalidationController.cpp
new file mode 100644
index 0000000..9693f1e
--- /dev/null
+++ b/experimental/sksg/SkSGInvalidationController.cpp
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "SkSGInvalidationController.h"
+
+#include "SkRect.h"
+
+namespace sksg {
+
+InvalidationController::InvalidationController() {}
+
+void InvalidationController::inval(const SkRect& r) {
+    fRects.push(r);
+}
+
+} // namespace sksg
diff --git a/experimental/sksg/SkSGInvalidationController.h b/experimental/sksg/SkSGInvalidationController.h
new file mode 100644
index 0000000..e00ece5
--- /dev/null
+++ b/experimental/sksg/SkSGInvalidationController.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkSGInvalidationController_DEFINED
+#define SkSGInvalidationController_DEFINED
+
+#include "SkTDArray.h"
+#include "SkTypes.h"
+
+struct SkRect;
+
+namespace sksg {
+
+/**
+ * Receiver for invalidation events.
+ *
+ * Tracks dirty regions for repaint.
+ */
+class InvalidationController : public SkNoncopyable {
+public:
+    InvalidationController();
+
+    void inval(const SkRect&);
+
+    const SkRect* begin() const { return fRects.begin(); }
+    const SkRect* end()   const { return fRects.end(); }
+
+private:
+    SkTDArray<SkRect> fRects;
+
+    typedef SkNoncopyable INHERITED;
+};
+
+} // namespace sksg
+
+#endif // SkSGInvalidationController_DEFINED
diff --git a/experimental/sksg/SkSGNode.cpp b/experimental/sksg/SkSGNode.cpp
new file mode 100644
index 0000000..3b404ed
--- /dev/null
+++ b/experimental/sksg/SkSGNode.cpp
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "SkSGNode.h"
+
+namespace sksg {
+
+class Node::ScopedFlag {
+public:
+    ScopedFlag(Node* node, uint32_t flag)
+        : fNode(node)
+        , fFlag(flag) {
+        SkASSERT(!(fNode->fFlags & fFlag));
+        fNode->fFlags |= fFlag;
+    }
+    ~ScopedFlag() {
+        fNode->fFlags &= ~fFlag;;
+    }
+
+private:
+    Node*    fNode;
+    uint32_t fFlag;
+};
+
+#define TRAVERSAL_GUARD                     \
+    if (this->fFlags & kInTraversal_Flag) { \
+        return;                             \
+    }                                       \
+    ScopedFlag traversal_guard(this, kInTraversal_Flag);
+
+Node::Node()
+    : fInvalReceiver(nullptr)
+    , fFlags(kInvalidated_Flag) {}
+
+Node::~Node() {
+    if (fFlags & kReceiverArray_Flag) {
+        SkASSERT(fInvalReceiverArray->isEmpty());
+        delete fInvalReceiverArray;
+    } else {
+        SkASSERT(!fInvalReceiver);
+    }
+}
+
+void Node::addInvalReceiver(Node* receiver) {
+    if (!(fFlags & kReceiverArray_Flag)) {
+        if (!fInvalReceiver) {
+            fInvalReceiver = receiver;
+            return;
+        }
+
+        auto receivers = new SkTDArray<Node*>();
+        receivers->setReserve(2);
+        receivers->push(fInvalReceiver);
+
+        fInvalReceiverArray = receivers;
+        fFlags |= kReceiverArray_Flag;
+    }
+
+    // No duplicate receivers.
+    SkASSERT(fInvalReceiverArray->find(receiver) < 0);
+
+    fInvalReceiverArray->push(receiver);
+}
+
+void Node::removeInvalReceiver(Node* receiver) {
+    if (!(fFlags & kReceiverArray_Flag)) {
+        SkASSERT(fInvalReceiver == receiver);
+        fInvalReceiver = nullptr;
+        return;
+    }
+
+    const auto idx = fInvalReceiverArray->find(receiver);
+    SkASSERT(idx >= 0);
+    fInvalReceiverArray->remove(idx);
+}
+
+template <typename Func>
+void Node::forEachInvalReceiver(Func&& func) const {
+    if (fFlags & kReceiverArray_Flag) {
+        for (const auto& parent : *fInvalReceiverArray) {
+            func(parent);
+        }
+        return;
+    }
+
+    if (fInvalReceiver) {
+        func(fInvalReceiver);
+    }
+}
+
+void Node::invalidate() {
+    TRAVERSAL_GUARD
+
+    if (this->isInvalidated()) {
+        return;
+    }
+
+    fFlags |= kInvalidated_Flag;
+    forEachInvalReceiver([&](Node* receiver) {
+        receiver->invalidate();
+    });
+}
+
+void Node::revalidate(InvalidationController* ic, const SkMatrix& ctm) {
+    TRAVERSAL_GUARD
+
+    if (this->isInvalidated()) {
+        this->onRevalidate(ic, ctm);
+        fFlags &= ~kInvalidated_Flag;
+    }
+}
+
+} // namespace sksg
diff --git a/experimental/sksg/SkSGNode.h b/experimental/sksg/SkSGNode.h
new file mode 100644
index 0000000..08ee784
--- /dev/null
+++ b/experimental/sksg/SkSGNode.h
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkSGNode_DEFINED
+#define SkSGNode_DEFINED
+
+#include "SkRefCnt.h"
+#include "SkTDArray.h"
+
+class SkCanvas;
+class SkMatrix;
+
+namespace sksg {
+
+class InvalidationController;
+
+/**
+ * Base class for all scene graph nodes.
+ *
+ * Handles ingress edge management for the DAG (i.e. node -> "parent" node mapping),
+ * and invalidation.
+ *
+ * Note: egress edges are only implemented/supported in container subclasses
+ * (e.g. Group, Effect, Draw).
+ */
+class Node : public SkRefCnt {
+public:
+    // Traverse the DAG and revalidate any connected/invalidated nodes.
+    void revalidate(InvalidationController*, const SkMatrix&);
+
+protected:
+    Node();
+    ~Node() override;
+
+    // Mark this node and (transitively) any invalidation receivers for revalidation.
+    void invalidate();
+
+    bool isInvalidated() const { return fFlags & kInvalidated_Flag; }
+
+    // Dispatched on revalidation.  Subclasses are expected to recompute their geometry
+    // and push dirty rects to the InvalidationController.
+    virtual void onRevalidate(InvalidationController*, const SkMatrix& ctm) = 0;
+
+private:
+    void addInvalReceiver(Node*);
+    void removeInvalReceiver(Node*);
+    friend class Draw;
+    friend class EffectNode;
+    friend class Group;
+
+    template <typename Func>
+    void forEachInvalReceiver(Func&&) const;
+
+    enum Flags {
+        kInvalidated_Flag   = 1 << 0, // the node requires revalidation
+        kReceiverArray_Flag = 1 << 1, // the node has more than one inval receiver
+        kInTraversal_Flag   = 1 << 2, // the node is part of a traversal (cycle detection)
+    };
+
+    class ScopedFlag;
+
+    union {
+        Node*             fInvalReceiver;
+        SkTDArray<Node*>* fInvalReceiverArray;
+    };
+    uint32_t              fFlags;
+
+    typedef SkRefCnt INHERITED;
+};
+
+// Helper for defining attribute getters/setters in subclasses.
+#define SG_ATTRIBUTE(attr_name, attr_type, attr_container) \
+    attr_type get##attr_name() const { return attr_container; } \
+    void set##attr_name(attr_type v) {                    \
+        if (attr_container == v) return;                   \
+        attr_container = v;                                \
+        this->invalidate();                                \
+   }
+
+} // namespace sksg
+
+#endif // SkSGNode_DEFINED
diff --git a/experimental/sksg/SkSGPaintNode.cpp b/experimental/sksg/SkSGPaintNode.cpp
new file mode 100644
index 0000000..cd0bc81
--- /dev/null
+++ b/experimental/sksg/SkSGPaintNode.cpp
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "SkSGPaintNode.h"
+
+namespace sksg {
+
+PaintNode::PaintNode() {}
+
+const SkPaint& PaintNode::makePaint() {
+    SkASSERT(!this->isInvalidated());
+
+    return fPaint;
+}
+
+void PaintNode::onRevalidate(InvalidationController*, const SkMatrix&) {
+    SkASSERT(this->isInvalidated());
+
+    fPaint = this->onMakePaint();
+}
+
+} // namespace sksg
diff --git a/experimental/sksg/SkSGPaintNode.h b/experimental/sksg/SkSGPaintNode.h
new file mode 100644
index 0000000..d0a7787
--- /dev/null
+++ b/experimental/sksg/SkSGPaintNode.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkSGPaintNode_DEFINED
+#define SkSGPaintNode_DEFINED
+
+#include "SkSGNode.h"
+
+#include "SkPaint.h"
+
+namespace sksg {
+
+/**
+ * Base class for nodes which provide a 'paint' (as opposed to geometry) for
+ * drawing (e.g. colors, gradients, patterns).
+ *
+ * Roughly equivalent to Skia's SkPaint.
+ */
+class PaintNode : public Node {
+public:
+
+    const SkPaint& makePaint();
+
+protected:
+    PaintNode();
+
+    virtual SkPaint onMakePaint() const = 0;
+
+    void onRevalidate(InvalidationController*, const SkMatrix&) override;
+
+private:
+    SkPaint fPaint;
+
+    typedef Node INHERITED;
+};
+
+} // namespace sksg
+
+#endif // SkSGGeometryNode_DEFINED
diff --git a/experimental/sksg/SkSGRenderNode.cpp b/experimental/sksg/SkSGRenderNode.cpp
new file mode 100644
index 0000000..1609bac
--- /dev/null
+++ b/experimental/sksg/SkSGRenderNode.cpp
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "SkSGRenderNode.h"
+
+namespace sksg {
+
+RenderNode::RenderNode() {}
+
+void RenderNode::render(SkCanvas* canvas) const {
+    SkASSERT(!this->isInvalidated());
+    this->onRender(canvas);
+}
+
+} // namespace sksg
diff --git a/experimental/sksg/SkSGRenderNode.h b/experimental/sksg/SkSGRenderNode.h
new file mode 100644
index 0000000..4ca1aec
--- /dev/null
+++ b/experimental/sksg/SkSGRenderNode.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkSGRenderNode_DEFINED
+#define SkSGRenderNode_DEFINED
+
+#include "SkSGNode.h"
+
+class SkCanvas;
+
+namespace sksg {
+
+/**
+ * Base class for nodes which can render to a canvas.
+ */
+class RenderNode : public Node {
+public:
+    // Render the node and its descendants to the canvas.
+    void render(SkCanvas*) const;
+
+protected:
+    RenderNode();
+
+    virtual void onRender(SkCanvas*) const = 0;
+
+private:
+    typedef Node INHERITED;
+};
+
+} // namespace sksg
+
+#endif // SkSGRenderNode_DEFINED
diff --git a/experimental/sksg/effects/SkSGTransform.cpp b/experimental/sksg/effects/SkSGTransform.cpp
new file mode 100644
index 0000000..ce83599
--- /dev/null
+++ b/experimental/sksg/effects/SkSGTransform.cpp
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "SkSGTransform.h"
+
+#include "SkCanvas.h"
+
+namespace sksg {
+
+Transform::Transform(sk_sp<RenderNode> child, const SkMatrix& matrix)
+    : INHERITED(std::move(child))
+    , fMatrix(matrix) {}
+
+void Transform::onRender(SkCanvas* canvas) const {
+    SkAutoCanvasRestore acr(canvas, !fMatrix.isIdentity());
+    canvas->concat(fMatrix);
+    this->INHERITED::onRender(canvas);
+}
+
+void Transform::onRevalidate(InvalidationController* ic, const SkMatrix& ctm) {
+    const auto localCTM = SkMatrix::Concat(ctm, fMatrix);
+    this->INHERITED::onRevalidate(ic, localCTM);
+}
+
+} // namespace sksg
diff --git a/experimental/sksg/effects/SkSGTransform.h b/experimental/sksg/effects/SkSGTransform.h
new file mode 100644
index 0000000..d7bc5d4
--- /dev/null
+++ b/experimental/sksg/effects/SkSGTransform.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkSGTransform_DEFINED
+#define SkSGTransform_DEFINED
+
+#include "SkSGEffectNode.h"
+
+#include "SkMatrix.h"
+
+namespace sksg {
+
+/**
+ * Concrete Effect node, wrapping an SkMatrix.
+ */
+class Transform : public EffectNode {
+public:
+    static sk_sp<Transform> Make(sk_sp<RenderNode> child, const SkMatrix& matrix) {
+        return sk_sp<Transform>(new Transform(std::move(child), matrix));
+    }
+
+    SG_ATTRIBUTE(Matrix, SkMatrix, fMatrix)
+
+protected:
+    Transform(sk_sp<RenderNode>, const SkMatrix&);
+
+    void onRender(SkCanvas*) const override;
+
+    void onRevalidate(InvalidationController*, const SkMatrix&) override;
+
+private:
+    SkMatrix fMatrix;
+
+    typedef EffectNode INHERITED;
+};
+
+} // namespace sksg
+
+#endif // SkSGTransform_DEFINED
diff --git a/experimental/sksg/geometry/SkSGRect.cpp b/experimental/sksg/geometry/SkSGRect.cpp
new file mode 100644
index 0000000..69bd946
--- /dev/null
+++ b/experimental/sksg/geometry/SkSGRect.cpp
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "SkSGRect.h"
+
+#include "SkCanvas.h"
+#include "SkPaint.h"
+
+namespace sksg {
+
+Rect::Rect(const SkRect& rect) : fRect(rect) {}
+
+void Rect::onDraw(SkCanvas* canvas, const SkPaint& paint) const {
+    canvas->drawRect(fRect, paint);
+}
+
+SkRect Rect::onComputeBounds() const {
+    return fRect;
+}
+
+} // namespace sksg
diff --git a/experimental/sksg/geometry/SkSGRect.h b/experimental/sksg/geometry/SkSGRect.h
new file mode 100644
index 0000000..a0e5ec6
--- /dev/null
+++ b/experimental/sksg/geometry/SkSGRect.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkSGRect_DEFINED
+#define SkSGRect_DEFINED
+
+#include "SkSGGeometryNode.h"
+
+#include "SkRect.h"
+
+class SkCanvas;
+class SkPaint;
+
+namespace sksg {
+
+/**
+ * Concrete Geometry node, wrapping an SkRect.
+ */
+class Rect : public GeometryNode {
+public:
+    static sk_sp<Rect> Make()                { return sk_sp<Rect>(new Rect(SkRect::MakeEmpty())); }
+    static sk_sp<Rect> Make(const SkRect& r) { return sk_sp<Rect>(new Rect(r)); }
+
+    SG_ATTRIBUTE(L, SkScalar, fRect.fLeft  )
+    SG_ATTRIBUTE(T, SkScalar, fRect.fTop   )
+    SG_ATTRIBUTE(R, SkScalar, fRect.fRight )
+    SG_ATTRIBUTE(B, SkScalar, fRect.fBottom)
+
+protected:
+    void onDraw(SkCanvas*, const SkPaint&) const override;
+
+    SkRect onComputeBounds() const override;
+
+private:
+    explicit Rect(const SkRect&);
+
+    SkRect fRect;
+};
+
+} // namespace sksg
+
+#endif // SkSGRect_DEFINED
diff --git a/experimental/sksg/paint/SkSGColor.cpp b/experimental/sksg/paint/SkSGColor.cpp
new file mode 100644
index 0000000..826bc4b
--- /dev/null
+++ b/experimental/sksg/paint/SkSGColor.cpp
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "SkSGColor.h"
+
+namespace sksg {
+
+Color::Color(SkColor c) : fColor(c) {}
+
+SkPaint Color::onMakePaint() const {
+    SkPaint paint;
+    paint.setColor(fColor);
+    return paint;
+}
+
+} // namespace sksg
diff --git a/experimental/sksg/paint/SkSGColor.h b/experimental/sksg/paint/SkSGColor.h
new file mode 100644
index 0000000..a4e0862
--- /dev/null
+++ b/experimental/sksg/paint/SkSGColor.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef SkSGColor_DEFINED
+#define SkSGColor_DEFINED
+
+#include "SkSGPaintNode.h"
+
+#include "SkColor.h"
+
+namespace sksg {
+
+/**
+ * Concrete Paint node, wrapping an SkColor.
+ */
+class Color : public PaintNode {
+public:
+    static sk_sp<Color> Make(SkColor c) { return sk_sp<Color>(new Color(c)); }
+
+    SG_ATTRIBUTE(Color, SkColor, fColor)
+
+protected:
+    SkPaint onMakePaint() const override;
+
+private:
+    explicit Color(SkColor);
+
+    SkColor fColor;
+};
+
+} // namespace sksg
+
+#endif // SkSGColor_DEFINED