Fold alpha to the inner savelayer in savelayer-savelayer-restore patterns
authorkkinnunen <kkinnunen@nvidia.com>
Mon, 26 Jan 2015 08:14:26 +0000 (00:14 -0800)
committerCommit bot <commit-bot@chromium.org>
Mon, 26 Jan 2015 08:14:26 +0000 (00:14 -0800)
Fold alpha to the inner savelayer in savelayer-savelayer-restore
patterns such as this:

  SaveLayer (non-opaque)
    Save
      ClipRect
      SaveLayer
      Restore
    Restore
  Restore

Current blink generates these for example for SVG content such as this:

<path style="opacity:0.5 filter:url(#blur_filter)"/>

The outer save layer is due to the opacity and the inner one is due to
blur filter being implemented with picture image filter.

Reduces layers in desk_carsvg.skp testcase from 115 to 78.

BUG=skia:3119

Review URL: https://codereview.chromium.org/835973005

gm/recordopts.cpp [new file with mode: 0644]
gyp/gmslides.gypi
src/core/SkRecordOpts.cpp
src/core/SkRecordOpts.h
src/core/SkRecordPattern.h
tests/RecordOptsTest.cpp

diff --git a/gm/recordopts.cpp b/gm/recordopts.cpp
new file mode 100644 (file)
index 0000000..0820211
--- /dev/null
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2014 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "gm.h"
+#include "SkCanvas.h"
+#include "SkPath.h"
+#include "SkPictureRecorder.h"
+#include "SkTableColorFilter.h"
+#include "SkColorFilterImageFilter.h"
+#include "SkPictureImageFilter.h"
+
+static const int kTestRectSize = 50;
+static const int kDetectorGreenValue = 50;
+
+// Below are few functions to install "detector" color filters. The filter is there to assert that
+// the color value it sees is the expected. It will trigger only with kDetectorGreenValue, and
+// turn that value into full green. The idea is that if an optimization incorrectly changes
+// kDetectorGreenValue and then the incorrect value is observable by some part of the drawing
+// pipeline, that pixel will remain empty.
+
+static SkColorFilter* make_detector_color_filter() {
+    uint8_t tableA[256] = { 0, };
+    uint8_t tableR[256] = { 0, };
+    uint8_t tableG[256] = { 0, };
+    uint8_t tableB[256] = { 0, };
+    tableA[255] = 255;
+    tableG[kDetectorGreenValue] = 255;
+    return SkTableColorFilter::CreateARGB(tableA, tableR, tableG, tableB);
+}
+
+// This detector detects that color filter phase of the pixel pipeline receives the correct value.
+static void install_detector_color_filter(SkPaint* drawPaint) {
+    drawPaint->setColorFilter(make_detector_color_filter())->unref();
+}
+
+// This detector detects that image filter phase of the pixel pipeline receives the correct value.
+static void install_detector_image_filter(SkPaint* drawPaint) {
+    SkAutoTUnref<SkColorFilter> colorFilter(make_detector_color_filter());
+    SkImageFilter* imageFilter =
+            SkColorFilterImageFilter::Create(colorFilter, drawPaint->getImageFilter());
+    drawPaint->setImageFilter(imageFilter)->unref();
+}
+
+static void no_detector_install(SkPaint*) {
+}
+
+typedef void(*InstallDetectorFunc)(SkPaint*);
+
+
+// Draws an pattern that can be optimized by alpha folding outer savelayer alpha value to
+// inner draw. Since we know that folding will happen to the inner draw, install a detector
+// to make sure that optimization does not change anything observable.
+static void draw_save_layer_draw_rect_restore_sequence(SkCanvas* canvas, SkColor shapeColor,
+                                                       InstallDetectorFunc installDetector) {
+    SkRect targetRect(SkRect::MakeWH(SkIntToScalar(kTestRectSize), SkIntToScalar(kTestRectSize)));
+    SkPaint layerPaint;
+    layerPaint.setColor(SkColorSetARGB(128, 0, 0, 0));
+    canvas->saveLayer(&targetRect, &layerPaint);
+        SkPaint drawPaint;
+        drawPaint.setColor(shapeColor);
+        installDetector(&drawPaint);
+        canvas->drawRect(targetRect, drawPaint);
+    canvas->restore();
+}
+
+// Draws an pattern that can be optimized by alpha folding outer savelayer alpha value to
+// inner draw. A variant where the draw is not uniform color.
+static void draw_save_layer_draw_bitmap_restore_sequence(SkCanvas* canvas, SkColor shapeColor,
+                                                         InstallDetectorFunc installDetector) {
+    SkBitmap bitmap;
+    bitmap.allocN32Pixels(kTestRectSize, kTestRectSize);
+    bitmap.eraseColor(shapeColor);
+    {
+        // Make the bitmap non-uniform color, so that it can not be optimized as uniform drawRect.
+        SkCanvas canvas(bitmap);
+        SkPaint p;
+        p.setColor(SK_ColorWHITE);
+        SkASSERT(shapeColor != SK_ColorWHITE);
+        canvas.drawRect(SkRect::MakeWH(SkIntToScalar(7), SkIntToScalar(7)), p);
+        canvas.flush();
+    }
+
+    SkRect targetRect(SkRect::MakeWH(SkIntToScalar(kTestRectSize), SkIntToScalar(kTestRectSize)));
+    SkPaint layerPaint;
+    layerPaint.setColor(SkColorSetARGB(129, 0, 0, 0));
+    canvas->saveLayer(&targetRect, &layerPaint);
+        SkPaint drawPaint;
+        installDetector(&drawPaint);
+        canvas->drawBitmap(bitmap, SkIntToScalar(0), SkIntToScalar(0), &drawPaint);
+    canvas->restore();
+}
+
+// Draws an pattern that can be optimized by alpha folding outer savelayer alpha value to
+// inner savelayer. We know that alpha folding happens to inner savelayer, so add detector there.
+static void draw_svg_opacity_and_filter_layer_sequence(SkCanvas* canvas, SkColor shapeColor,
+                                                       InstallDetectorFunc installDetector) {
+
+    SkRect targetRect(SkRect::MakeWH(SkIntToScalar(kTestRectSize), SkIntToScalar(kTestRectSize)));
+    SkAutoTUnref<SkPicture> shape;
+    {
+        SkPictureRecorder recorder;
+        SkCanvas* canvas = recorder.beginRecording(SkIntToScalar(kTestRectSize + 2),
+                                                   SkIntToScalar(kTestRectSize + 2));
+        SkPaint shapePaint;
+        shapePaint.setColor(shapeColor);
+        canvas->drawRect(targetRect, shapePaint);
+        shape.reset(recorder.endRecordingAsPicture());
+    }
+
+    SkPaint layerPaint;
+    layerPaint.setColor(SkColorSetARGB(130, 0, 0, 0));
+    canvas->saveLayer(&targetRect, &layerPaint);
+        canvas->save();
+            canvas->clipRect(targetRect);
+            SkPaint drawPaint;
+            drawPaint.setImageFilter(SkPictureImageFilter::Create(shape))->unref();
+            installDetector(&drawPaint);
+            canvas->saveLayer(&targetRect, &drawPaint);
+            canvas->restore();
+        canvas->restore();
+    canvas->restore();
+}
+
+// Draws two columns of rectangles. The test is correct when:
+//  - Left and right columns always identical
+//  - First 3 rows are green, with a white dent in the middle row
+//  - Next 6 rows are green, with a grey dent in the middle row
+//    (the grey dent is from the color filter removing everything but the "good" green, see below)
+//  - Last 6 rows are grey
+DEF_SIMPLE_GM(recordopts, canvas, (kTestRectSize+1)*2, (kTestRectSize+1)*15) {
+    canvas->clear(SK_ColorTRANSPARENT);
+
+    typedef void (*TestVariantSequence)(SkCanvas*, SkColor, InstallDetectorFunc);
+    TestVariantSequence funcs[] = {
+        draw_save_layer_draw_rect_restore_sequence,
+        draw_save_layer_draw_bitmap_restore_sequence,
+        draw_svg_opacity_and_filter_layer_sequence,
+    };
+
+    // Draw layer-related sequences that can be optimized by folding the opacity layer alpha to
+    // the inner draw operation. This tries to trigger the optimization, and relies on gm diffs
+    // to keep the color value correct over time.
+
+    // Draws two green rects side by side: one is without the optimization, the other is with
+    // the optimization applied.
+
+    SkColor shapeColor = SkColorSetARGB(255, 0, 255, 0);
+    for (size_t k = 0; k < SK_ARRAY_COUNT(funcs); ++k) {
+        canvas->save();
+
+        TestVariantSequence drawTestSequence = funcs[k];
+        drawTestSequence(canvas, shapeColor, no_detector_install);
+        canvas->flush();
+        canvas->translate(SkIntToScalar(kTestRectSize) + SkIntToScalar(1), SkIntToScalar(0));
+        {
+            SkPictureRecorder recorder;
+            drawTestSequence(recorder.beginRecording(SkIntToScalar(kTestRectSize),
+                                                     SkIntToScalar(kTestRectSize)),
+                             shapeColor, no_detector_install);
+            SkAutoTUnref<SkPicture> optimizedPicture(recorder.endRecordingAsPicture());
+            optimizedPicture->playback(canvas);
+            canvas->flush();
+        }
+        canvas->restore();
+        canvas->translate(SkIntToScalar(0), SkIntToScalar(kTestRectSize) + SkIntToScalar(1));
+    }
+
+    // Draw the same layer related sequences, but manipulate the sequences so that the result is
+    // incorrect if the alpha is folded or folded incorrectly. These test the observable state
+    // throughout the pixel pipeline, and thus may turn off the optimizations (this is why we
+    // trigger the optimizations above).
+
+    // Draws two green rects side by side: one is without the optimization, the other is with
+    // the possibility that optimization is applied.
+    // At the end, draws the same patterns in translucent black. This tests that the detectors
+    // work, eg. that if the value the detector sees is wrong, the resulting image shows this.
+    SkColor shapeColors[] = {
+        SkColorSetARGB(255, 0, kDetectorGreenValue, 0),
+        SkColorSetARGB(255, 0, kDetectorGreenValue + 1, 0) // This tests that detectors work.
+    };
+
+    InstallDetectorFunc detectorInstallFuncs[] = {
+        install_detector_image_filter,
+        install_detector_color_filter
+    };
+
+    for (size_t i = 0; i < SK_ARRAY_COUNT(shapeColors); ++i) {
+        shapeColor = shapeColors[i];
+        for (size_t j = 0; j < SK_ARRAY_COUNT(shapeColors); ++j) {
+            InstallDetectorFunc detectorInstallFunc = detectorInstallFuncs[j];
+            for (size_t k = 0; k < SK_ARRAY_COUNT(funcs); ++k) {
+                TestVariantSequence drawTestSequence = funcs[k];
+                canvas->save();
+                drawTestSequence(canvas, shapeColor, detectorInstallFunc);
+                canvas->flush();
+                canvas->translate(SkIntToScalar(kTestRectSize) + SkIntToScalar(1), SkIntToScalar(0));
+                {
+                    SkPictureRecorder recorder;
+                    drawTestSequence(recorder.beginRecording(SkIntToScalar(kTestRectSize),
+                                                             SkIntToScalar(kTestRectSize)),
+                                     shapeColor, detectorInstallFunc);
+                    SkAutoTUnref<SkPicture> optimizedPicture(recorder.endRecordingAsPicture());
+                    optimizedPicture->playback(canvas);
+                    canvas->flush();
+                }
+
+                canvas->restore();
+                canvas->translate(SkIntToScalar(0), SkIntToScalar(kTestRectSize) + SkIntToScalar(1));
+            }
+
+        }
+    }
+}
+
index 6ee6bd7..abcb867 100644 (file)
         '../gm/poly2poly.cpp',
         '../gm/polygons.cpp',
         '../gm/quadpaths.cpp',
+        '../gm/recordopts.cpp',
         '../gm/rects.cpp',
         '../gm/resizeimagefilter.cpp',
         '../gm/rrect.cpp',
index df29b1b..8351292 100644 (file)
@@ -21,6 +21,7 @@ void SkRecordOptimize(SkRecord* record) {
     //SkRecordNoopSaveRestores(record);
 
     SkRecordNoopSaveLayerDrawRestores(record);
+    SkRecordMergeSvgOpacityAndFilterLayers(record);
 }
 
 // Most of the optimizations in this file are pattern-based.  These are all defined as structs with:
@@ -56,6 +57,64 @@ struct SaveOnlyDrawsRestoreNooper {
         return true;
     }
 };
+
+static bool fold_opacity_layer_color_to_paint(const SkPaint& layerPaint,
+                                              bool isSaveLayer,
+                                              SkPaint* paint) {
+    // We assume layerPaint is always from a saveLayer.  If isSaveLayer is
+    // true, we assume paint is too.
+
+    // The alpha folding can proceed if the filter layer paint does not have properties which cause
+    // the resulting filter layer to be "blended" in complex ways to the parent layer. For example,
+    // looper drawing unmodulated filter layer twice and then modulating the result produces
+    // different image to drawing modulated filter layer twice.
+    // TODO: most likely the looper and only some xfer modes are the hard constraints
+    if (paint->getXfermode() || paint->getLooper()) {
+        return false;
+    }
+
+    if (!isSaveLayer && paint->getImageFilter()) {
+        // For normal draws, the paint color is used as one input for the color for the draw. Image
+        // filter will operate on the result, and thus we can not change the input.
+        // For layer saves, the image filter is applied to the layer contents. The layer is then
+        // modulated with the paint color, so it's fine to proceed with the fold for saveLayer
+        // paints with image filters.
+        return false;
+    }
+
+    if (paint->getColorFilter()) {
+        // Filter input depends on the paint color.
+
+        // Here we could filter the color if we knew the draw is going to be uniform color.  This
+        // should be detectable as drawPath/drawRect/.. without a shader being uniform, while
+        // drawBitmap/drawSprite or a shader being non-uniform. However, current matchers don't
+        // give the type out easily, so just do not optimize that at the moment.
+        return false;
+    }
+
+    const uint32_t layerColor = layerPaint.getColor();
+    // The layer paint color must have only alpha component.
+    if (SK_ColorTRANSPARENT != SkColorSetA(layerColor, SK_AlphaTRANSPARENT)) {
+        return false;
+    }
+
+    // The layer paint can not have any effects.
+    if (layerPaint.getPathEffect() ||
+        layerPaint.getShader()      ||
+        layerPaint.getXfermode()    ||
+        layerPaint.getMaskFilter()  ||
+        layerPaint.getColorFilter() ||
+        layerPaint.getRasterizer()  ||
+        layerPaint.getLooper()      ||
+        layerPaint.getImageFilter()) {
+        return false;
+    }
+
+    paint->setAlpha(SkMulDiv255Round(paint->getAlpha(), SkColorGetA(layerColor)));
+
+    return true;
+}
+
 // Turns logical no-op Save-[non-drawing command]*-Restore patterns into actual no-ops.
 struct SaveNoDrawsRestoreNooper {
     // Star matches greedily, so we also have to exclude Save and Restore.
@@ -104,14 +163,10 @@ struct SaveLayerDrawRestoreNooper {
             return false;
         }
 
-        const uint32_t layerColor = layerPaint->getColor();
-        const uint32_t  drawColor =  drawPaint->getColor();
-        if (!IsOnlyAlpha(layerColor) || HasAnyEffect(*layerPaint) || CantFoldAlpha(*drawPaint)) {
-            // Too fancy for us.
+        if (!fold_opacity_layer_color_to_paint(*layerPaint, false /*isSaveLayer*/, drawPaint)) {
             return false;
         }
 
-        drawPaint->setAlpha(SkMulDiv255Round(SkColorGetA(drawColor), SkColorGetA(layerColor)));
         return KillSaveLayerAndRestore(record, begin);
     }
 
@@ -120,35 +175,58 @@ struct SaveLayerDrawRestoreNooper {
         record->replace<NoOp>(saveLayerIndex+2);  // Restore
         return true;
     }
+};
+void SkRecordNoopSaveLayerDrawRestores(SkRecord* record) {
+    SaveLayerDrawRestoreNooper pass;
+    apply(&pass, record);
+}
 
-    static bool HasAnyEffect(const SkPaint& paint) {
-        return paint.getPathEffect()  ||
-               paint.getShader()      ||
-               paint.getXfermode()    ||
-               paint.getMaskFilter()  ||
-               paint.getColorFilter() ||
-               paint.getRasterizer()  ||
-               paint.getLooper()      ||
-               paint.getImageFilter();
-    }
 
-    // The alpha folding can proceed if the single draw's paint has a shader,
-    // path effect, mask filter and/or rasterizer.
-    // TODO: most likely the looper and only some xfer modes are the hard
-    // constraints
-    static bool CantFoldAlpha(const SkPaint& paint) {
-        return paint.getXfermode()    ||
-               paint.getColorFilter() ||
-               paint.getLooper()      ||
-               paint.getImageFilter();
+/* For SVG generated:
+  SaveLayer (non-opaque, typically for CSS opacity)
+    Save
+      ClipRect
+      SaveLayer (typically for SVG filter)
+      Restore
+    Restore
+  Restore
+*/
+struct SvgOpacityAndFilterLayerMergePass {
+    typedef Pattern7<Is<SaveLayer>, Is<Save>, Is<ClipRect>, Is<SaveLayer>,
+                     Is<Restore>, Is<Restore>, Is<Restore> > Pattern;
+
+    bool onMatch(SkRecord* record, Pattern* pattern, unsigned begin, unsigned end) {
+        SkPaint* opacityPaint = pattern->first<SaveLayer>()->paint;
+        if (NULL == opacityPaint) {
+            // There wasn't really any point to this SaveLayer at all.
+            return KillSaveLayerAndRestore(record, begin);
+        }
+
+        // This layer typically contains a filter, but this should work for layers with for other
+        // purposes too.
+        SkPaint* filterLayerPaint = pattern->fourth<SaveLayer>()->paint;
+        if (filterLayerPaint == NULL) {
+            // We can just give the inner SaveLayer the paint of the outer SaveLayer.
+            // TODO(mtklein): figure out how to do this clearly
+            return false;
+        }
+
+        if (!fold_opacity_layer_color_to_paint(*opacityPaint, true /*isSaveLayer*/,
+                                               filterLayerPaint)) {
+            return false;
+        }
+
+        return KillSaveLayerAndRestore(record, begin);
     }
 
-    static bool IsOnlyAlpha(SkColor color) {
-        return SK_ColorTRANSPARENT == SkColorSetA(color, SK_AlphaTRANSPARENT);
+    static bool KillSaveLayerAndRestore(SkRecord* record, unsigned saveLayerIndex) {
+        record->replace<NoOp>(saveLayerIndex);     // SaveLayer
+        record->replace<NoOp>(saveLayerIndex + 6); // Restore
+        return true;
     }
 };
-void SkRecordNoopSaveLayerDrawRestores(SkRecord* record) {
-    SaveLayerDrawRestoreNooper pass;
+
+void SkRecordMergeSvgOpacityAndFilterLayers(SkRecord* record) {
+    SvgOpacityAndFilterLayerMergePass pass;
     apply(&pass, record);
 }
-
index 936eeff..a560d00 100644 (file)
@@ -20,4 +20,8 @@ void SkRecordNoopSaveRestores(SkRecord*);
 // draw, and no-op the SaveLayer and Restore.
 void SkRecordNoopSaveLayerDrawRestores(SkRecord*);
 
+// For SVG generated SaveLayer-Save-ClipRect-SaveLayer-3xRestore patterns, merge
+// the alpha of the first SaveLayer to the second SaveLayer.
+void SkRecordMergeSvgOpacityAndFilterLayers(SkRecord*);
+
 #endif//SkRecordOpts_DEFINED
index 68a3aa3..85c38ac 100644 (file)
@@ -132,6 +132,7 @@ public:
     template <typename T> T* first()  { return fHead.get(); }
     template <typename T> T* second() { return fTail.fHead.get(); }
     template <typename T> T* third()  { return fTail.fTail.fHead.get(); }
+    template <typename T> T* fourth()  { return fTail.fTail.fTail.fHead.get(); }
 
 private:
     // If head isn't a Star, try to match at i once.
@@ -182,6 +183,18 @@ struct Pattern2 : Cons<A, Pattern1<B> > {};
 template <typename A, typename B, typename C>
 struct Pattern3 : Cons<A, Pattern2<B, C> > {};
 
+template <typename A, typename B, typename C, typename D>
+struct Pattern4 : Cons<A, Pattern3<B, C, D> > {};
+
+template <typename A, typename B, typename C, typename D, typename E>
+struct Pattern5 : Cons<A, Pattern4<B, C, D, E> > {};
+
+template <typename A, typename B, typename C, typename D, typename E, typename F>
+struct Pattern6 : Cons<A, Pattern5<B, C, D, E, F> > {};
+
+template <typename A, typename B, typename C, typename D, typename E, typename F, typename G>
+struct Pattern7 : Cons<A, Pattern6<B, C, D, E, F, G> > {};
+
 }  // namespace SkRecords
 
 #endif//SkRecordPattern_DEFINED
index b6bfba4..e550244 100644 (file)
@@ -8,11 +8,14 @@
 #include "Test.h"
 #include "RecordTestUtils.h"
 
+#include "SkColorFilter.h"
 #include "SkRecord.h"
 #include "SkRecordOpts.h"
 #include "SkRecorder.h"
 #include "SkRecords.h"
 #include "SkXfermode.h"
+#include "SkPictureRecorder.h"
+#include "SkPictureImageFilter.h"
 
 static const int W = 1920, H = 1080;
 
@@ -169,3 +172,139 @@ DEF_TEST(RecordOpts_NoopSaveLayerDrawRestore, r) {
     REPORTER_ASSERT(r, drawRect != NULL);
     REPORTER_ASSERT(r, drawRect->paint.getColor() == 0x03020202);
 }
+
+static void assert_merge_svg_opacity_and_filter_layers(skiatest::Reporter* r,
+                                                       SkRecord* record,
+                                                       unsigned i,
+                                                       bool shouldBeNoOped) {
+    SkRecordMergeSvgOpacityAndFilterLayers(record);
+    if (shouldBeNoOped) {
+        assert_type<SkRecords::NoOp>(r, *record, i);
+        assert_type<SkRecords::NoOp>(r, *record, i + 6);
+    } else {
+        assert_type<SkRecords::SaveLayer>(r, *record, i);
+        assert_type<SkRecords::Restore>(r, *record, i + 6);
+    }
+}
+
+DEF_TEST(RecordOpts_MergeSvgOpacityAndFilterLayers, r) {
+    SkRecord record;
+    SkRecorder recorder(&record, W, H);
+
+    SkRect bounds = SkRect::MakeWH(SkIntToScalar(100), SkIntToScalar(200));
+    SkRect clip = SkRect::MakeWH(SkIntToScalar(50), SkIntToScalar(60));
+
+    SkPaint alphaOnlyLayerPaint;
+    alphaOnlyLayerPaint.setColor(0x03000000);  // Only alpha.
+    SkPaint translucentLayerPaint;
+    translucentLayerPaint.setColor(0x03040506);  // Not only alpha.
+    SkPaint xfermodePaint;
+    xfermodePaint.setXfermodeMode(SkXfermode::kDstIn_Mode);
+    SkPaint colorFilterPaint;
+    colorFilterPaint.setColorFilter(
+        SkColorFilter::CreateModeFilter(SK_ColorLTGRAY, SkXfermode::kSrcIn_Mode))->unref();
+
+    SkPaint opaqueFilterLayerPaint;
+    opaqueFilterLayerPaint.setColor(0xFF020202);  // Opaque.
+    SkPaint translucentFilterLayerPaint;
+    translucentFilterLayerPaint.setColor(0x0F020202);  // Not opaque.
+    SkAutoTUnref<SkPicture> shape;
+    {
+        SkPictureRecorder recorder;
+        SkCanvas* canvas = recorder.beginRecording(SkIntToScalar(100), SkIntToScalar(100));
+        SkPaint shapePaint;
+        shapePaint.setColor(SK_ColorWHITE);
+        canvas->drawRect(SkRect::MakeWH(SkIntToScalar(50), SkIntToScalar(50)), shapePaint);
+        shape.reset(recorder.endRecordingAsPicture());
+    }
+    translucentFilterLayerPaint.setImageFilter(SkPictureImageFilter::Create(shape))->unref();
+
+    int index = 0;
+
+    {
+        // Any combination of these should cause the pattern to be optimized.
+        SkRect* firstBounds[] = { NULL, &bounds };
+        SkPaint* firstPaints[] = { NULL, &alphaOnlyLayerPaint };
+        SkRect* secondBounds[] = { NULL, &bounds };
+        SkPaint* secondPaints[] = { &opaqueFilterLayerPaint, &translucentFilterLayerPaint };
+
+        for (size_t i = 0; i < SK_ARRAY_COUNT(firstBounds); ++ i) {
+            for (size_t j = 0; j < SK_ARRAY_COUNT(firstPaints); ++j) {
+                for (size_t k = 0; k < SK_ARRAY_COUNT(secondBounds); ++k) {
+                    for (size_t m = 0; m < SK_ARRAY_COUNT(secondPaints); ++m) {
+                        recorder.saveLayer(firstBounds[i], firstPaints[j]);
+                        recorder.save();
+                        recorder.clipRect(clip);
+                        recorder.saveLayer(secondBounds[k], secondPaints[m]);
+                        recorder.restore();
+                        recorder.restore();
+                        recorder.restore();
+                        assert_merge_svg_opacity_and_filter_layers(r, &record, index, true);
+                        index += 7;
+                    }
+                }
+            }
+        }
+    }
+
+    // These should cause the pattern to stay unoptimized:
+    struct {
+        SkPaint* firstPaint;
+        SkPaint* secondPaint;
+    } noChangeTests[] = {
+        // No change: NULL filter layer paint not implemented.
+        { &alphaOnlyLayerPaint, NULL },
+        // No change: layer paint is not alpha-only.
+        { &translucentLayerPaint, &opaqueFilterLayerPaint },
+        // No change: layer paint has an xfereffect.
+        { &xfermodePaint, &opaqueFilterLayerPaint },
+        // No change: filter layer paint has an xfereffect.
+        { &alphaOnlyLayerPaint, &xfermodePaint },
+        // No change: layer paint has a color filter.
+        { &colorFilterPaint, &opaqueFilterLayerPaint },
+        // No change: filter layer paint has a color filter (until the optimization accounts for
+        // constant color draws that can filter the color).
+        { &alphaOnlyLayerPaint, &colorFilterPaint }
+    };
+
+    for (size_t i = 0; i < SK_ARRAY_COUNT(noChangeTests); ++i) {
+        recorder.saveLayer(NULL, noChangeTests[i].firstPaint);
+        recorder.save();
+        recorder.clipRect(clip);
+        recorder.saveLayer(NULL, noChangeTests[i].secondPaint);
+        recorder.restore();
+        recorder.restore();
+        recorder.restore();
+        assert_merge_svg_opacity_and_filter_layers(r, &record, index, false);
+        index += 7;
+    }
+
+    // Test the folded alpha value.
+    recorder.saveLayer(NULL, &alphaOnlyLayerPaint);
+    recorder.save();
+    recorder.clipRect(clip);
+    recorder.saveLayer(NULL, &opaqueFilterLayerPaint);
+    recorder.restore();
+    recorder.restore();
+    recorder.restore();
+    assert_merge_svg_opacity_and_filter_layers(r, &record, index, true);
+
+    const SkRecords::SaveLayer* saveLayer = assert_type<SkRecords::SaveLayer>(r, record, index + 3);
+    REPORTER_ASSERT(r, saveLayer != NULL);
+    REPORTER_ASSERT(r, saveLayer->paint->getColor() == 0x03020202);
+
+    index += 7;
+
+    // Test that currently we do not fold alphas for patterns without the clip. This is just not
+    // implemented.
+    recorder.saveLayer(NULL, &alphaOnlyLayerPaint);
+    recorder.saveLayer(NULL, &opaqueFilterLayerPaint);
+    recorder.restore();
+    recorder.restore();
+    SkRecordMergeSvgOpacityAndFilterLayers(&record);
+    assert_type<SkRecords::SaveLayer>(r, record, index);
+    assert_type<SkRecords::SaveLayer>(r, record, index + 1);
+    assert_type<SkRecords::Restore>(r, record, index + 2);
+    assert_type<SkRecords::Restore>(r, record, index + 3);
+    index += 4;
+}