Avoid use of SkMatrixImageFilter to handle CTM for backdrop filtering

The DrawDeviceWithFilter function used to rely on SkImageFilter's
applyCTMForBackdrop, which would create a new DAG and use an
SkMatrixImageFilter node to account for the transformation. However,
that filter is really meant to perform a transformation prior to the
CTM matrix. Abusing matrix math to instead have it map the backdrop
contents back into the CTM local space grossly inflated the size of the
temporary images needed when under many perspective transforms.

This CL instead takes over control of the CTM decomposition and draws
the backdrop (src) content into a temporary layer directly, and then
feeds that through the image filtering. The code is somewhat cumbersome
to follow because of the combination of coordinate spaces and need to
make everything relative to the top left corner of the images.

When the rest of my image filter refactor is more underway, I believe that
a lot of the coordinate space math and image origin accounting will
be much cleaner, but the overall backdrop filtering strategy would remain
similar to this new implementation.

Bug: skia:9074
Change-Id: Ibd2995e81d2b308c74b2d298da5e38daa74c8677
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/239108
Reviewed-by: Robert Phillips <robertphillips@google.com>
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
diff --git a/src/core/SkCanvas.cpp b/src/core/SkCanvas.cpp
index 3951f85..519558d 100644
--- a/src/core/SkCanvas.cpp
+++ b/src/core/SkCanvas.cpp
@@ -885,44 +885,148 @@
 void SkCanvas::DrawDeviceWithFilter(SkBaseDevice* src, const SkImageFilter* filter,
                                     SkBaseDevice* dst, const SkIPoint& dstOrigin,
                                     const SkMatrix& ctm) {
-    SkPaint p;
-    SkIRect snapBounds = SkIRect::MakeXYWH(dstOrigin.x() - src->getOrigin().x(),
-                                           dstOrigin.y() - src->getOrigin().y(),
-                                           dst->width(), dst->height());
-    int x = 0;
-    int y = 0;
+    // The local bounds of the src device; all snap bounds must be intersected with this rect
+    const SkIRect srcDevRect = SkIRect::MakeWH(src->width(), src->height());
 
-    if (filter) {
-        // Calculate expanded snap bounds
-        SkIRect newBounds = filter->filterBounds(
-                snapBounds, ctm, SkImageFilter::kReverse_MapDirection, &snapBounds);
-        // Must clamp to valid src since the filter or rotations may expand beyond what's readable
-        SkIRect srcR = SkIRect::MakeWH(src->width(), src->height());
-        if (!newBounds.intersect(srcR)) {
+    SkPaint p;
+    if (!filter) {
+        // All devices are currently axis aligned, so they only differ by their origin. This means
+        // that we only have to copy a dst-sized block of pixels out of src and translate it to the
+        // matching position relative to dst's origin.
+        SkIRect snapBounds = SkIRect::MakeXYWH(dstOrigin.x() - src->getOrigin().x(),
+                                               dstOrigin.y() - src->getOrigin().y(),
+                                               dst->width(), dst->height());
+        if (!snapBounds.intersect(srcDevRect)) {
             return;
         }
 
-        x = newBounds.fLeft - snapBounds.fLeft;
-        y = newBounds.fTop - snapBounds.fTop;
-        snapBounds = newBounds;
-
-        SkMatrix localCTM;
-        sk_sp<SkImageFilter> modifiedFilter = as_IFB(filter)->applyCTMForBackdrop(ctm, &localCTM);
-        // Account for the origin offset in the CTM
-        localCTM.postTranslate(-dstOrigin.x(), -dstOrigin.y());
-
-        // In this case we always wrap the filter (even when it's the original) with 'localCTM'
-        // since there's no device CTM stack that provides it to the image filter context.
-        // FIXME skbug.com/9074 - once perspective is properly supported, drop the
-        // localCTM.hasPerspective condition from assert.
-        SkASSERT(localCTM.isScaleTranslate() || as_IFB(filter)->canHandleComplexCTM() ||
-                 localCTM.hasPerspective());
-        p.setImageFilter(modifiedFilter->makeWithLocalMatrix(localCTM));
+        auto special = src->snapSpecial(snapBounds);
+        if (special) {
+            dst->drawSpecial(special.get(), 0, 0, p, nullptr, SkMatrix::I());
+        }
+        return;
     }
 
-    auto special = src->snapSpecial(snapBounds);
+    // First decompose the ctm into a post-filter transform and a filter matrix that is supported
+    // by the backdrop filter.
+    SkMatrix toRoot, layerMatrix;
+    SkSize scale;
+    if (ctm.isScaleTranslate() || as_IFB(filter)->canHandleComplexCTM()) {
+        toRoot = SkMatrix::I();
+        layerMatrix = ctm;
+    } else if (ctm.decomposeScale(&scale, &toRoot)) {
+        layerMatrix = SkMatrix::MakeScale(scale.fWidth, scale.fHeight);
+    } else {
+        // Perspective, for now, do no scaling of the layer itself.
+        // TODO (michaelludwig) - perhaps it'd be better to explore a heuristic scale pulled from
+        // the matrix, e.g. based on the midpoint of the near/far planes?
+        toRoot = ctm;
+        layerMatrix = SkMatrix::I();
+    }
+
+    // We have to map the dst bounds from the root space into the layer space where filtering will
+    // occur. If we knew the input bounds of the content that defined the original dst bounds, we
+    // could map that forward by layerMatrix and have tighter bounds, but toRoot^-1 * dst bounds
+    // is a safe, conservative estimate.
+    SkMatrix fromRoot;
+    if (!toRoot.invert(&fromRoot)) {
+        return;
+    }
+
+    // This represents what the backdrop filter needs to produce in the layer space, and is sized
+    // such that drawing it into dst with the toRoot transform will cover the actual dst device.
+    SkIRect layerTargetBounds = fromRoot.mapRect(
+            SkRect::MakeXYWH(dstOrigin.x(), dstOrigin.y(), dst->width(), dst->height())).roundOut();
+    // While layerTargetBounds is what needs to be output by the filter, the filtering process may
+    // require some extra input pixels.
+    SkIRect layerInputBounds = filter->filterBounds(
+            layerTargetBounds, layerMatrix, SkImageFilter::kReverse_MapDirection,
+            &layerTargetBounds);
+
+    // Map the required input into the root space, then make relative to the src device. This will
+    // be conservative contents required to fill a layerInputBounds-sized surface with the backdrop
+    // content (transformed back into the layer space using fromRoot).
+    SkIRect backdropBounds = toRoot.mapRect(SkRect::Make(layerInputBounds)).roundOut();
+    backdropBounds.offset(-src->getOrigin().x(), -src->getOrigin().y());
+    if (!backdropBounds.intersect(srcDevRect)) {
+        return;
+    }
+
+    auto special = src->snapSpecial(backdropBounds);
+    if (!special) {
+        return;
+    }
+
+    SkColorType colorType = src->imageInfo().colorType();
+    if (colorType == kUnknown_SkColorType) {
+        colorType = kRGBA_8888_SkColorType;
+    }
+    SkColorSpace* colorSpace = src->imageInfo().colorSpace();
+    if (!toRoot.isIdentity()) {
+        // The snapped backdrop content needs to be transformed by fromRoot into the layer space,
+        // and stored in a temporary surface, which is then used as the input to the actual filter.
+        auto tmpSurface = special->makeSurface(colorType, colorSpace, layerInputBounds.size());
+        if (!tmpSurface) {
+            return;
+        }
+        p.setFilterQuality(kHigh_SkFilterQuality);
+
+        auto tmpCanvas = tmpSurface->getCanvas();
+        tmpCanvas->clear(SK_ColorTRANSPARENT);
+        // Reading in reverse, this takes the backdrop bounds from src device space into the root
+        // space, then maps from root space into the layer space, then maps it so the input layer's
+        // top left corner is (0, 0). This transformation automatically accounts for any cropping
+        // performed on backdropBounds.
+        tmpCanvas->translate(-layerInputBounds.fLeft, -layerInputBounds.fTop);
+        tmpCanvas->concat(fromRoot);
+        tmpCanvas->translate(src->getOrigin().x(), src->getOrigin().y());
+        tmpCanvas->drawImageRect(special->asImage(), special->subset(),
+                                 SkRect::Make(backdropBounds), &p, kStrict_SrcRectConstraint);
+        special = tmpSurface->makeImageSnapshot();
+    } else {
+        // Since there is no extra transform that was done, update the input bounds to reflect
+        // cropping of the snapped backdrop image. In this case toRoot = I, so layerInputBounds
+        // was equal to backdropBounds before it was made relative to the src device and cropped.
+        // When we use the original snapped image directly, just map the update backdrop bounds
+        // back into the shared layer space
+        layerInputBounds = backdropBounds;
+        layerInputBounds.offset(src->getOrigin().x(), src->getOrigin().y());
+    }
+
+    // Now evaluate the filter on 'special', which contains the backdrop content mapped back into
+    // layer space. This has to further offset everything so that filter evaluation thinks the
+    // source image's top left corner is (0, 0).
+    // TODO (michaelludwig) - Once image filters are robust to non-(0,0) image origins for inputs,
+    // this can be simplified.
+    layerTargetBounds.offset(-layerInputBounds.fLeft, -layerInputBounds.fTop);
+    SkMatrix filterCTM = layerMatrix;
+    filterCTM.postTranslate(-layerInputBounds.fLeft, -layerInputBounds.fTop);
+    skif::Context ctx(filterCTM, layerTargetBounds, nullptr, colorType, colorSpace, special.get());
+
+    SkIPoint offset;
+    special = as_IFB(filter)->filterImage(ctx).imageAndOffset(&offset);
     if (special) {
-        dst->drawSpecial(special.get(), x, y, p, nullptr, SkMatrix::I());
+        // Draw the filtered backdrop content into the dst device. Must adjust the reported offset
+        // to include the layerInputBounds origin shift, since the returned offset is in the coord
+        // system defined by filterCTM, and we want it to be in that of layerMatrix.
+        offset += layerInputBounds.topLeft();
+
+        // Manually setting the device's CTM requires accounting for the device's origin.
+        // TODO (michaelludwig) - This could be simpler if the dst device had its origin configured
+        // before filtering the backdrop device, and if SkAutoDeviceCTMRestore had a way to accept
+        // a global CTM instead of a device CTM.
+        SkMatrix dstCTM = toRoot;
+        dstCTM.postTranslate(-dstOrigin.x(), -dstOrigin.y());
+        SkAutoDeviceCTMRestore acr(dst, dstCTM);
+
+        // And because devices don't have a special-image draw function that supports arbitrary
+        // matrices, we are abusing the asImage() functionality here...
+        SkRect specialSrc = SkRect::Make(special->subset());
+        auto hackImageMeh = special->asImage();
+        dst->drawImageRect(
+                hackImageMeh.get(), &specialSrc,
+                SkRect::MakeXYWH(offset.x(), offset.y(), special->width(), special->height()),
+                p, kStrict_SrcRectConstraint);
     }
 }