Reland "In SkCanvas destructor, discard (rather than blit) unbalanced layers"

This is a reland of 879b2f2e6ebd7fb465c0679e22c48a0cae58c61e

Now includes a test that demonstrates the bug found by Chrome's fuzzers,
and a different (safer) implementation.

Original change's description:
> In SkCanvas destructor, discard (rather than blit) unbalanced layers
>
> Bug: skia:12267
> Change-Id: I6808f62b2385a3466b1a93db905041a6529f58cb
> Reviewed-on: https://skia-review.googlesource.com/c/skia/+/433360
> Commit-Queue: Brian Osman <brianosman@google.com>
> Reviewed-by: Florin Malita <fmalita@google.com>
> Reviewed-by: Brian Salomon <bsalomon@google.com>
> Reviewed-by: Mike Reed <reed@google.com>

Bug: skia:12267
Change-Id: Ide7dc61b054761826faa5bca3eec6be2fc63c83a
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/440977
Reviewed-by: Brian Salomon <bsalomon@google.com>
Commit-Queue: Brian Osman <brianosman@google.com>
diff --git a/include/core/SkCanvas.h b/include/core/SkCanvas.h
index 34efd72..56bd689 100644
--- a/include/core/SkCanvas.h
+++ b/include/core/SkCanvas.h
@@ -2296,6 +2296,7 @@
         sk_sp<SkBaseDevice>  fDevice;
         sk_sp<SkImageFilter> fImageFilter; // applied to layer *before* being drawn by paint
         SkPaint              fPaint;
+        bool                 fDiscard;
 
         Layer(sk_sp<SkBaseDevice> device, sk_sp<SkImageFilter> imageFilter, const SkPaint& paint);
     };
diff --git a/src/core/SkCanvas.cpp b/src/core/SkCanvas.cpp
index 39fd744..99e86d0 100644
--- a/src/core/SkCanvas.cpp
+++ b/src/core/SkCanvas.cpp
@@ -194,7 +194,10 @@
 SkCanvas::Layer::Layer(sk_sp<SkBaseDevice> device,
                        sk_sp<SkImageFilter> imageFilter,
                        const SkPaint& paint)
-        : fDevice(std::move(device)), fImageFilter(std::move(imageFilter)), fPaint(paint) {
+        : fDevice(std::move(device))
+        , fImageFilter(std::move(imageFilter))
+        , fPaint(paint)
+        , fDiscard(false) {
     SkASSERT(fDevice);
     // Any image filter should have been pulled out and stored in 'imageFilter' so that 'paint'
     // can be used as-is to draw the result of the filter to the dst device.
@@ -461,6 +464,19 @@
 #endif
 
 SkCanvas::~SkCanvas() {
+    // Mark all pending layers to be discarded during restore (rather than drawn)
+    SkDeque::Iter iter(fMCStack, SkDeque::Iter::kFront_IterStart);
+    for (;;) {
+        MCRec* rec = (MCRec*)iter.next();
+        if (!rec) {
+            break;
+        }
+        if (rec->fLayer) {
+            rec->fLayer->fDiscard = true;
+        }
+    }
+
+    // free up the contents of our deque
     this->restoreToCount(1);    // restore everything but the last
     this->internalRestore();    // restore the last, since we're going away
 
@@ -1153,7 +1169,7 @@
 
     // Draw the layer's device contents into the now-current older device. We can't call public
     // draw functions since we don't want to record them.
-    if (layer && !layer->fDevice->isNoPixelsDevice()) {
+    if (layer && !layer->fDevice->isNoPixelsDevice() && !layer->fDiscard) {
         layer->fDevice->setImmutable();
 
         // Don't go through AutoLayerForImageFilter since device draws are so closely tied to
diff --git a/tests/CanvasTest.cpp b/tests/CanvasTest.cpp
index 0f41c7d..65ad343 100644
--- a/tests/CanvasTest.cpp
+++ b/tests/CanvasTest.cpp
@@ -752,36 +752,48 @@
         const SkPMColor pmc = SkPreMultiplyColor(expected);
         for (int y = 0; y < pm.info().height(); ++y) {
             for (int x = 0; x < pm.info().width(); ++x) {
-                REPORTER_ASSERT(reporter, *pm.addr32(x, y) == pmc);
+                if (*pm.addr32(x, y) != pmc) {
+                    ERRORF(reporter, "check_pixels_failed");
+                    return;
+                }
             }
         }
     };
 
-    auto do_test = [&](bool doRestore) {
+    auto do_test = [&](int saveCount, int restoreCount) {
+        SkASSERT(restoreCount <= saveCount);
+
         auto surf = SkSurface::MakeRasterDirect(pm);
         auto canvas = surf->getCanvas();
 
         canvas->clear(SK_ColorRED);
         check_pixels(SK_ColorRED);
 
-        canvas->saveLayer(nullptr, nullptr);
+        for (int i = 0; i < saveCount; ++i) {
+            canvas->saveLayer(nullptr, nullptr);
+        }
+
         canvas->clear(SK_ColorBLUE);
-        // so far, we still expect to see the red
+        // so far, we still expect to see the red, since the blue was drawn in a layer
         check_pixels(SK_ColorRED);
 
-        if (doRestore) {
+        for (int i = 0; i < restoreCount; ++i) {
             canvas->restore();
         }
         // by returning, we are implicitly deleting the surface, and its associated canvas
     };
 
-    do_test(true);
+    do_test(1, 1);
     // since we called restore, we expect to see now see blue
     check_pixels(SK_ColorBLUE);
 
-    // Now we're repeat that, but delete the canvas before we restore it
-    do_test(false);
-    // *if* we restore the layers in the destructor, we expect to see blue, even though
-    // we didn't call restore() as a client.
-    check_pixels(SK_ColorBLUE);
+    // Now repeat that, but delete the canvas before we restore it
+    do_test(1, 0);
+    // We don't blit the unbalanced saveLayers, so we expect to see red (not the layer's blue)
+    check_pixels(SK_ColorRED);
+
+    // Finally, test with multiple unbalanced saveLayers. This led to a crash in an earlier
+    // implementation (crbug.com/1238731)
+    do_test(2, 0);
+    check_pixels(SK_ColorRED);
 }