[skottie] Optimize opacity layerization

  - plumb a RenderContext RenderNode::render() argument to track pending
    opacity

  - defer opacity application until we can determine whether a layer is
    required (group with multiple children) or the opacity can be pushed
    onto the draw paint (for single/atomic draws)

Bug: skia:
Change-Id: I962ba08bad780395d5d738307bde986e9efa502b
Reviewed-on: https://skia-review.googlesource.com/146445
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 2d30292..0331cbb 100644
--- a/modules/skottie/src/Skottie.cpp
+++ b/modules/skottie/src/Skottie.cpp
@@ -715,7 +715,9 @@
             return SkRect::MakeSize(fAnimation->size());
         }
 
-        void onRender(SkCanvas* canvas) const override {
+        void onRender(SkCanvas* canvas, const RenderContext* ctx) const override {
+            const auto local_scope =
+                ScopedRenderContext(canvas, ctx).setIsolation(this->bounds(), true);
             fAnimation->render(canvas);
         }
 
diff --git a/modules/sksg/include/SkSGClipEffect.h b/modules/sksg/include/SkSGClipEffect.h
index 674edb2..52ba7c7 100644
--- a/modules/sksg/include/SkSGClipEffect.h
+++ b/modules/sksg/include/SkSGClipEffect.h
@@ -32,7 +32,7 @@
 protected:
     ClipEffect(sk_sp<RenderNode>, sk_sp<GeometryNode>, bool aa);
 
-    void onRender(SkCanvas*) const override;
+    void onRender(SkCanvas*, const RenderContext*) const override;
 
     SkRect onRevalidate(InvalidationController*, const SkMatrix&) override;
 
diff --git a/modules/sksg/include/SkSGColorFilter.h b/modules/sksg/include/SkSGColorFilter.h
index 9a7a837..a9e12e7 100644
--- a/modules/sksg/include/SkSGColorFilter.h
+++ b/modules/sksg/include/SkSGColorFilter.h
@@ -26,7 +26,7 @@
 protected:
     explicit ColorFilter(sk_sp<RenderNode>);
 
-    void onRender(SkCanvas*) const final;
+    void onRender(SkCanvas*, const RenderContext*) const final;
 
     sk_sp<SkColorFilter> fColorFilter;
 
diff --git a/modules/sksg/include/SkSGDraw.h b/modules/sksg/include/SkSGDraw.h
index 20ead3d..018dd1a 100644
--- a/modules/sksg/include/SkSGDraw.h
+++ b/modules/sksg/include/SkSGDraw.h
@@ -32,7 +32,7 @@
     Draw(sk_sp<GeometryNode>, sk_sp<PaintNode> paint);
     ~Draw() override;
 
-    void onRender(SkCanvas*) const override;
+    void onRender(SkCanvas*, const RenderContext*) const override;
 
     SkRect onRevalidate(InvalidationController*, const SkMatrix&) override;
 
diff --git a/modules/sksg/include/SkSGEffectNode.h b/modules/sksg/include/SkSGEffectNode.h
index ab0968e..575432d 100644
--- a/modules/sksg/include/SkSGEffectNode.h
+++ b/modules/sksg/include/SkSGEffectNode.h
@@ -23,7 +23,7 @@
     explicit EffectNode(sk_sp<RenderNode>);
     ~EffectNode() override;
 
-    void onRender(SkCanvas*) const override;
+    void onRender(SkCanvas*, const RenderContext*) const override;
 
     SkRect onRevalidate(InvalidationController*, const SkMatrix&) override;
 
diff --git a/modules/sksg/include/SkSGGroup.h b/modules/sksg/include/SkSGGroup.h
index b32f9af..3b8de32 100644
--- a/modules/sksg/include/SkSGGroup.h
+++ b/modules/sksg/include/SkSGGroup.h
@@ -33,7 +33,7 @@
     Group();
     ~Group() override;
 
-    void onRender(SkCanvas*) const override;
+    void onRender(SkCanvas*, const RenderContext*) const override;
     SkRect onRevalidate(InvalidationController*, const SkMatrix&) override;
 
 private:
diff --git a/modules/sksg/include/SkSGImage.h b/modules/sksg/include/SkSGImage.h
index 7d17a50..dd524c3 100644
--- a/modules/sksg/include/SkSGImage.h
+++ b/modules/sksg/include/SkSGImage.h
@@ -32,7 +32,7 @@
 protected:
     explicit Image(sk_sp<SkImage>);
 
-    void onRender(SkCanvas*) const override;
+    void onRender(SkCanvas*, const RenderContext*) const override;
 
     SkRect onRevalidate(InvalidationController*, const SkMatrix&) override;
 
diff --git a/modules/sksg/include/SkSGMaskEffect.h b/modules/sksg/include/SkSGMaskEffect.h
index c4fd012..f668777 100644
--- a/modules/sksg/include/SkSGMaskEffect.h
+++ b/modules/sksg/include/SkSGMaskEffect.h
@@ -35,7 +35,7 @@
 protected:
     MaskEffect(sk_sp<RenderNode>, sk_sp<RenderNode> mask, Mode);
 
-    void onRender(SkCanvas*) const override;
+    void onRender(SkCanvas*, const RenderContext*) const override;
 
     SkRect onRevalidate(InvalidationController*, const SkMatrix&) override;
 
diff --git a/modules/sksg/include/SkSGOpacityEffect.h b/modules/sksg/include/SkSGOpacityEffect.h
index d906775..a982304 100644
--- a/modules/sksg/include/SkSGOpacityEffect.h
+++ b/modules/sksg/include/SkSGOpacityEffect.h
@@ -27,7 +27,7 @@
 protected:
     OpacityEffect(sk_sp<RenderNode>, float);
 
-    void onRender(SkCanvas*) const override;
+    void onRender(SkCanvas*, const RenderContext*) const override;
 
     SkRect onRevalidate(InvalidationController*, const SkMatrix&) override;
 
diff --git a/modules/sksg/include/SkSGRenderNode.h b/modules/sksg/include/SkSGRenderNode.h
index 4ca1aec..df69270 100644
--- a/modules/sksg/include/SkSGRenderNode.h
+++ b/modules/sksg/include/SkSGRenderNode.h
@@ -9,8 +9,10 @@
 #define SkSGRenderNode_DEFINED
 
 #include "SkSGNode.h"
+#include "SkTLazy.h"
 
 class SkCanvas;
+class SkPaint;
 
 namespace sksg {
 
@@ -18,14 +20,70 @@
  * Base class for nodes which can render to a canvas.
  */
 class RenderNode : public Node {
+protected:
+    struct RenderContext;
+
 public:
     // Render the node and its descendants to the canvas.
-    void render(SkCanvas*) const;
+    void render(SkCanvas*, const RenderContext* = nullptr) const;
 
 protected:
     RenderNode();
 
-    virtual void onRender(SkCanvas*) const = 0;
+    virtual void onRender(SkCanvas*, const RenderContext*) const = 0;
+
+    // Paint property overrides.
+    // These are deferred until we can determine whether they can be applied to the individual
+    // draw paints, or whether they require content isolation (applied to a layer).
+    struct RenderContext {
+        float fOpacity = 1;
+
+        // Returns true if the paint was modified.
+        bool modulatePaint(SkPaint*) const;
+    };
+
+    class ScopedRenderContext final {
+    public:
+        ScopedRenderContext(SkCanvas*, const RenderContext*);
+        ~ScopedRenderContext();
+
+        ScopedRenderContext(ScopedRenderContext&& that) { *this = std::move(that); }
+
+        ScopedRenderContext& operator=(ScopedRenderContext&& that) {
+            fCanvas       = that.fCanvas;
+            fCtx          = std::move(that.fCtx);
+            fRestoreCount = that.fRestoreCount;
+
+            // scope ownership is being transferred
+            that.fRestoreCount = -1;
+
+            return *this;
+        }
+
+        operator const RenderContext* () const { return fCtx.get(); }
+
+        // Add opacity to a render node sub-DAG.
+        ScopedRenderContext&& modulateOpacity(float opacity);
+
+        // Force content isolation for a node sub-DAG by applying the RenderContext
+        // overrides via a layer.
+        ScopedRenderContext&& setIsolation(const SkRect& bounds, bool do_isolate);
+
+    private:
+        // stack-only
+        void* operator new(size_t)        = delete;
+        void* operator new(size_t, void*) = delete;
+
+        // Scopes cannot be copied.
+        ScopedRenderContext(const ScopedRenderContext&)            = delete;
+        ScopedRenderContext& operator=(const ScopedRenderContext&) = delete;
+
+        RenderContext* writableContext();
+
+        SkCanvas*                          fCanvas;
+        SkTCopyOnFirstWrite<RenderContext> fCtx;
+        int                                fRestoreCount;
+    };
 
 private:
     typedef Node INHERITED;
diff --git a/modules/sksg/include/SkSGTransform.h b/modules/sksg/include/SkSGTransform.h
index 6b7fbc0..c878773 100644
--- a/modules/sksg/include/SkSGTransform.h
+++ b/modules/sksg/include/SkSGTransform.h
@@ -64,7 +64,7 @@
     const sk_sp<Matrix>& getMatrix() const { return fMatrix; }
 
 protected:
-    void onRender(SkCanvas*) const override;
+    void onRender(SkCanvas*, const RenderContext*) const override;
 
     SkRect onRevalidate(InvalidationController*, const SkMatrix&) override;
 
diff --git a/modules/sksg/src/SkSGClipEffect.cpp b/modules/sksg/src/SkSGClipEffect.cpp
index b2d68fc..045605d 100644
--- a/modules/sksg/src/SkSGClipEffect.cpp
+++ b/modules/sksg/src/SkSGClipEffect.cpp
@@ -24,7 +24,7 @@
     this->unobserveInval(fClipNode);
 }
 
-void ClipEffect::onRender(SkCanvas* canvas) const {
+void ClipEffect::onRender(SkCanvas* canvas, const RenderContext* ctx) const {
     if (this->bounds().isEmpty())
         return;
 
@@ -33,7 +33,7 @@
         fClipNode->clip(canvas, fAntiAlias);
     }
 
-    this->INHERITED::onRender(canvas);
+    this->INHERITED::onRender(canvas, ctx);
 }
 
 SkRect ClipEffect::onRevalidate(InvalidationController* ic, const SkMatrix& ctm) {
diff --git a/modules/sksg/src/SkSGColorFilter.cpp b/modules/sksg/src/SkSGColorFilter.cpp
index d10b0ce..ab21117 100644
--- a/modules/sksg/src/SkSGColorFilter.cpp
+++ b/modules/sksg/src/SkSGColorFilter.cpp
@@ -16,7 +16,7 @@
 ColorFilter::ColorFilter(sk_sp<RenderNode> child)
     : INHERITED(std::move(child)) {}
 
-void ColorFilter::onRender(SkCanvas* canvas) const {
+void ColorFilter::onRender(SkCanvas* canvas, const RenderContext* ctx) const {
     if (this->bounds().isEmpty())
         return;
 
@@ -28,7 +28,7 @@
         canvas->saveLayer(this->bounds(), &p);
     }
 
-    this->INHERITED::onRender(canvas);
+    this->INHERITED::onRender(canvas, ctx);
 }
 
 ColorModeFilter::ColorModeFilter(sk_sp<RenderNode> child, sk_sp<Color> color, SkBlendMode mode)
diff --git a/modules/sksg/src/SkSGDraw.cpp b/modules/sksg/src/SkSGDraw.cpp
index b73bf3b..a75886f 100644
--- a/modules/sksg/src/SkSGDraw.cpp
+++ b/modules/sksg/src/SkSGDraw.cpp
@@ -10,6 +10,7 @@
 #include "SkSGGeometryNode.h"
 #include "SkSGInvalidationController.h"
 #include "SkSGPaintNode.h"
+#include "SkTLazy.h"
 
 namespace sksg {
 
@@ -25,13 +26,17 @@
     this->unobserveInval(fPaint);
 }
 
-void Draw::onRender(SkCanvas* canvas) const {
-    const auto& paint   = fPaint->makePaint();
-    const auto skipDraw = paint.nothingToDraw() ||
-            (paint.getStyle() == SkPaint::kStroke_Style && paint.getStrokeWidth() <= 0);
+void Draw::onRender(SkCanvas* canvas, const RenderContext* ctx) const {
+    SkTCopyOnFirstWrite<SkPaint> paint(fPaint->makePaint());
+    if (ctx) {
+        ctx->modulatePaint(paint.writable());
+    }
+
+    const auto skipDraw = paint->nothingToDraw() ||
+            (paint->getStyle() == SkPaint::kStroke_Style && paint->getStrokeWidth() <= 0);
 
     if (!skipDraw) {
-        fGeometry->draw(canvas, paint);
+        fGeometry->draw(canvas, *paint);
     }
 }
 
diff --git a/modules/sksg/src/SkSGEffectNode.cpp b/modules/sksg/src/SkSGEffectNode.cpp
index 70050cc..272d509 100644
--- a/modules/sksg/src/SkSGEffectNode.cpp
+++ b/modules/sksg/src/SkSGEffectNode.cpp
@@ -18,8 +18,8 @@
     this->unobserveInval(fChild);
 }
 
-void EffectNode::onRender(SkCanvas* canvas) const {
-    fChild->render(canvas);
+void EffectNode::onRender(SkCanvas* canvas, const RenderContext* ctx) const {
+    fChild->render(canvas, ctx);
 }
 
 SkRect EffectNode::onRevalidate(InvalidationController* ic, const SkMatrix& ctm) {
diff --git a/modules/sksg/src/SkSGGroup.cpp b/modules/sksg/src/SkSGGroup.cpp
index aeccf23..9911292 100644
--- a/modules/sksg/src/SkSGGroup.cpp
+++ b/modules/sksg/src/SkSGGroup.cpp
@@ -45,9 +45,15 @@
     this->invalidate();
 }
 
-void Group::onRender(SkCanvas* canvas) const {
+void Group::onRender(SkCanvas* canvas, const RenderContext* ctx) const {
+    // TODO: this heuristic works at the moment, but:
+    //   a) it is fragile because it relies on all leaf render nodes being atomic draws
+    //   b) could be improved by e.g. detecting all leaf render draws are non-overlapping
+    const auto isolate = fChildren.count() > 1;
+    const auto local_ctx = ScopedRenderContext(canvas, ctx).setIsolation(this->bounds(), isolate);
+
     for (const auto& child : fChildren) {
-        child->render(canvas);
+        child->render(canvas, local_ctx);
     }
 }
 
diff --git a/modules/sksg/src/SkSGImage.cpp b/modules/sksg/src/SkSGImage.cpp
index d06f6be..87c686d 100644
--- a/modules/sksg/src/SkSGImage.cpp
+++ b/modules/sksg/src/SkSGImage.cpp
@@ -14,11 +14,15 @@
 
 Image::Image(sk_sp<SkImage> image) : fImage(std::move(image)) {}
 
-void Image::onRender(SkCanvas* canvas) const {
+void Image::onRender(SkCanvas* canvas, const RenderContext* ctx) const {
     SkPaint paint;
     paint.setAntiAlias(fAntiAlias);
     paint.setFilterQuality(fQuality);
 
+    if (ctx) {
+        ctx->modulatePaint(&paint);
+    }
+
     canvas->drawImage(fImage, 0, 0, &paint);
 }
 
diff --git a/modules/sksg/src/SkSGMaskEffect.cpp b/modules/sksg/src/SkSGMaskEffect.cpp
index 16e4c0d..ff869fd 100644
--- a/modules/sksg/src/SkSGMaskEffect.cpp
+++ b/modules/sksg/src/SkSGMaskEffect.cpp
@@ -22,13 +22,14 @@
     this->unobserveInval(fMaskNode);
 }
 
-void MaskEffect::onRender(SkCanvas* canvas) const {
+void MaskEffect::onRender(SkCanvas* canvas, const RenderContext* ctx) const {
     if (this->bounds().isEmpty())
         return;
 
     SkAutoCanvasRestore acr(canvas, false);
 
     canvas->saveLayer(this->bounds(), nullptr);
+    // Note: the paint overrides in ctx don't apply to the mask.
     fMaskNode->render(canvas);
 
 
@@ -36,7 +37,7 @@
     p.setBlendMode(fMaskMode == Mode::kNormal ? SkBlendMode::kSrcIn : SkBlendMode::kSrcOut);
     canvas->saveLayer(this->bounds(), &p);
 
-    this->INHERITED::onRender(canvas);
+    this->INHERITED::onRender(canvas, ctx);
 }
 
 
diff --git a/modules/sksg/src/SkSGOpacityEffect.cpp b/modules/sksg/src/SkSGOpacityEffect.cpp
index b1ff10d..bb2fadf 100644
--- a/modules/sksg/src/SkSGOpacityEffect.cpp
+++ b/modules/sksg/src/SkSGOpacityEffect.cpp
@@ -17,19 +17,14 @@
     : INHERITED(std::move(child))
     , fOpacity(opacity) {}
 
-void OpacityEffect::onRender(SkCanvas* canvas) const {
+void OpacityEffect::onRender(SkCanvas* canvas, const RenderContext* ctx) const {
     // opacity <= 0 disables rendering
     if (fOpacity <= 0)
         return;
 
-    // TODO: we could avoid savelayer if there is no more than one drawing primitive
-    //       in the sub-DAG.
-    SkAutoCanvasRestore acr(canvas, false);
-    if (fOpacity < 1) {
-        canvas->saveLayerAlpha(&this->bounds(), roundf(fOpacity * 255));
-    }
+    const auto local_context = ScopedRenderContext(canvas, ctx).modulateOpacity(fOpacity);
 
-    this->INHERITED::onRender(canvas);
+    this->INHERITED::onRender(canvas, local_context);
 }
 
 SkRect OpacityEffect::onRevalidate(InvalidationController* ic, const SkMatrix& ctm) {
diff --git a/modules/sksg/src/SkSGRenderNode.cpp b/modules/sksg/src/SkSGRenderNode.cpp
index e952c69..417cae9 100644
--- a/modules/sksg/src/SkSGRenderNode.cpp
+++ b/modules/sksg/src/SkSGRenderNode.cpp
@@ -7,13 +7,72 @@
 
 #include "SkSGRenderNode.h"
 
+#include "SkCanvas.h"
+#include "SkPaint.h"
+
 namespace sksg {
 
 RenderNode::RenderNode() : INHERITED(0) {}
 
-void RenderNode::render(SkCanvas* canvas) const {
+void RenderNode::render(SkCanvas* canvas, const RenderContext* ctx) const {
     SkASSERT(!this->hasInval());
-    this->onRender(canvas);
+    this->onRender(canvas, ctx);
+}
+
+bool RenderNode::RenderContext::modulatePaint(SkPaint* paint) const {
+    const auto initial_alpha = paint->getAlpha(),
+                       alpha = SkToU8(sk_float_round2int(initial_alpha * fOpacity));
+
+    if (alpha != initial_alpha) {
+        paint->setAlpha(alpha);
+        return true;
+    }
+
+    return false;
+}
+
+RenderNode::ScopedRenderContext::ScopedRenderContext(SkCanvas* canvas, const RenderContext* ctx)
+    : fCanvas(canvas)
+    , fCtx(ctx)
+    , fRestoreCount(canvas->getSaveCount()) {}
+
+RenderNode::ScopedRenderContext::~ScopedRenderContext() {
+    if (fRestoreCount >= 0) {
+        fCanvas->restoreToCount(fRestoreCount);
+    }
+}
+
+RenderNode::RenderContext* RenderNode::ScopedRenderContext::writableContext() {
+    // If no inherited context is present, allocate one in local storage.
+    if (!fCtx.get()) {
+        // N.B.: we have to force a copy while the default source is in scope.
+        // TODO: add SkTCopyOnWrite::init_copy() to simplify this
+        RenderContext default_ctx;
+        fCtx.init(default_ctx);
+        return fCtx.writable();
+    }
+    return fCtx.writable();
+}
+
+RenderNode::ScopedRenderContext&&
+RenderNode::ScopedRenderContext::modulateOpacity(float opacity) {
+    SkASSERT(opacity >= 0 && opacity <= 1);
+    if (opacity < 1) {
+        this->writableContext()->fOpacity *= opacity;
+    }
+    return std::move(*this);
+}
+
+RenderNode::ScopedRenderContext&&
+RenderNode::ScopedRenderContext::setIsolation(const SkRect& bounds, bool isolation) {
+    if (isolation && fCtx.get()) {
+        SkPaint layer_paint;
+        if (fCtx->modulatePaint(&layer_paint)) {
+            fCanvas->saveLayer(bounds, &layer_paint);
+            *fCtx.writable() = RenderContext();
+        }
+    }
+    return std::move(*this);
 }
 
 } // namespace sksg
diff --git a/modules/sksg/src/SkSGTransform.cpp b/modules/sksg/src/SkSGTransform.cpp
index 6a985a9..41b6a35 100644
--- a/modules/sksg/src/SkSGTransform.cpp
+++ b/modules/sksg/src/SkSGTransform.cpp
@@ -47,11 +47,11 @@
     this->unobserveInval(fMatrix);
 }
 
-void Transform::onRender(SkCanvas* canvas) const {
+void Transform::onRender(SkCanvas* canvas, const RenderContext* ctx) const {
     const auto& m = fMatrix->getTotalMatrix();
     SkAutoCanvasRestore acr(canvas, !m.isIdentity());
     canvas->concat(m);
-    this->INHERITED::onRender(canvas);
+    this->INHERITED::onRender(canvas, ctx);
 }
 
 SkRect Transform::onRevalidate(InvalidationController* ic, const SkMatrix& ctm) {
diff --git a/tools/viewer/SlideDir.cpp b/tools/viewer/SlideDir.cpp
index 517d823..584bfe8 100644
--- a/tools/viewer/SlideDir.cpp
+++ b/tools/viewer/SlideDir.cpp
@@ -72,9 +72,11 @@
         return SkRect::MakeIWH(isize.width(), isize.height());
     }
 
-    void onRender(SkCanvas* canvas) const override {
+    void onRender(SkCanvas* canvas, const RenderContext* ctx) const override {
         SkAutoCanvasRestore acr(canvas, true);
         canvas->clipRect(SkRect::Make(fSlide->getDimensions()), true);
+
+        // TODO: commit the context?
         fSlide->draw(canvas);
     }