Visualize perspective scaling in filter bounds sample

Bug: skia:9074
Change-Id: Icdac64276e0a403950fd990a619d523b0ee784de
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/326942
Reviewed-by: Jim Van Verth <jvanverth@google.com>
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
diff --git a/samplecode/SampleFilterBounds.cpp b/samplecode/SampleFilterBounds.cpp
index 48b31d3..56fc8bf 100644
--- a/samplecode/SampleFilterBounds.cpp
+++ b/samplecode/SampleFilterBounds.cpp
@@ -15,10 +15,12 @@
 #include "include/core/SkPoint.h"
 #include "include/core/SkRect.h"
 #include "include/effects/SkDashPathEffect.h"
+#include "include/effects/SkGradientShader.h"
 #include "include/effects/SkImageFilters.h"
 
 #include "src/core/SkImageFilterTypes.h"
 #include "src/core/SkImageFilter_Base.h"
+#include "src/core/SkMatrixPriv.h"
 
 #include "tools/ToolUtils.h"
 
@@ -58,6 +60,17 @@
     return y;
 }
 
+static void print_label(SkCanvas* canvas, float x, float y, float value) {
+    SkFont font(nullptr, 12);
+    SkPaint text;
+    text.setAntiAlias(true);
+
+    SkString label;
+    label.printf("%.3f", value);
+
+    canvas->drawString(label, x, y + kLineHeight / 2.f, font, text);
+}
+
 static SkPaint line_paint(SkColor color, bool dashed = false) {
     SkPaint paint;
     paint.setColor(color);
@@ -84,6 +97,71 @@
     return localSpace;
 }
 
+static const SkColor4f kScaleGradientColors[] =
+                { { 0.05f, 0.0f, 6.f,  1.f },   // Severe downscaling, s < 1/8, log(s) < -3
+                  { 0.6f,  0.6f, 0.8f, 0.6f },  // Okay downscaling,   s < 1/2, log(s) < -1
+                  { 1.f,   1.f,  1.f,  0.2f },  // No scaling,         s = 1,   log(s) = 0
+                  { 0.95f, 0.6f, 0.5f, 0.6f },  // Okay upscaling,     s > 2,   log(s) > 1
+                  { 0.8f,  0.1f, 0.f,  1.f } }; // Severe upscaling,   s > 8,   log(s) > 3
+static const SkScalar kLogScaleFactors[] = { -3.f, -1.f, 0.f, 1.f, 3.f };
+static const SkScalar kGradientStops[] = { 0.f, 0.33333f, 0.5f, 0.66667f, 1.f };
+static const int kStopCount = (int) SK_ARRAY_COUNT(kScaleGradientColors);
+
+static void draw_scale_key(SkCanvas* canvas, float y) {
+    SkRect key = SkRect::MakeXYWH(15.f, y + 30.f, 15.f, 100.f);
+    SkPoint pts[] = {{key.centerX(), key.fTop}, {key.centerX(), key.fBottom}};
+    sk_sp<SkShader> gradient = SkGradientShader::MakeLinear(
+            pts, kScaleGradientColors, nullptr, kGradientStops, kStopCount, SkTileMode::kClamp,
+            SkGradientShader::kInterpolateColorsInPremul_Flag, nullptr);
+    SkPaint keyPaint;
+    keyPaint.setShader(gradient);
+    canvas->drawRect(key, keyPaint);
+    for (int i = 0; i < kStopCount; ++i) {
+        print_label(canvas, key.fRight + 5.f, key.fTop + kGradientStops[i] * key.height(),
+                    SkScalarPow(2.f, kLogScaleFactors[i]));
+    }
+}
+
+static void draw_scale_factors(SkCanvas* canvas, const skif::Mapping& mapping, const SkRect& rect) {
+    SkPoint testPoints[5];
+    testPoints[0] = {rect.centerX(), rect.centerY()};
+    rect.toQuad(testPoints + 1);
+    for (int i = 0; i < 5; ++i) {
+        float scale = SkMatrixPriv::DifferentialScale(
+                mapping.deviceMatrix(),
+                SkPoint(mapping.paramToLayer(skif::ParameterSpace<SkPoint>(testPoints[i]))));
+        SkColor4f color = {0.f, 0.f, 0.f, 1.f};
+
+        if (SkScalarIsFinite(scale)) {
+            float logScale = SkScalarLog2(scale);
+            for (int j = 0; j <= kStopCount; ++j) {
+                if (j == kStopCount) {
+                    color = kScaleGradientColors[j - 1];
+                    break;
+                } else if (kLogScaleFactors[j] >= logScale) {
+                    if (j == 0) {
+                        color = kScaleGradientColors[0];
+                    } else {
+                        SkScalar t = (logScale - kLogScaleFactors[j - 1]) /
+                                    (kLogScaleFactors[j] - kLogScaleFactors[j - 1]);
+
+                        SkColor4f a = kScaleGradientColors[j - 1] * (1.f - t);
+                        SkColor4f b = kScaleGradientColors[j] * t;
+                        color = {a.fR + b.fR, a.fG + b.fG, a.fB + b.fB, a.fA + b.fA};
+                    }
+                    break;
+                }
+            }
+        }
+
+        SkPaint p;
+        p.setAntiAlias(true);
+        p.setColor4f(color, nullptr);
+        canvas->drawRect(SkRect::MakeLTRB(testPoints[i].fX - 4.f, testPoints[i].fY - 4.f,
+                                          testPoints[i].fX + 4.f, testPoints[i].fY + 4.f), p);
+    }
+}
+
 class FilterBoundsSample : public Sample {
 public:
     FilterBoundsSample() {}
@@ -114,6 +192,9 @@
         // Add axis lines, to show perspective distortion
         canvas->drawPath(create_axis_path(localContentRect, 10.f), line_paint(SK_ColorGRAY));
 
+        // Visualize scale factors at the four corners and center of the local rect
+        draw_scale_factors(canvas, mapping, localContentRect);
+
         // The device content rect, e.g. the clip bounds if 'localContentRect' were used as a clip
         // before the draw or saveLayer, representing what the filter must cover if it affects
         // transparent black or doesn't have a local content hint.
@@ -146,10 +227,13 @@
         canvas->drawRect(SkRect::Make(SkIRect(hintedOutputBounds)), line_paint(SK_ColorBLUE));
 
         canvas->resetMatrix();
-        print_info(canvas, SkIRect(mapping.paramToLayer(contentBounds).roundOut()),
+        float y = print_info(canvas, SkIRect(mapping.paramToLayer(contentBounds).roundOut()),
                            devContentRect.roundOut(),
                            SkIRect(hintedOutputBounds),
                            SkIRect(unhintedLayerBounds));
+
+        // Draw color key for layer visualization
+        draw_scale_key(canvas, y);
     }
 
     SkString name() override { return SkString("FilterBounds"); }
diff --git a/src/core/SkMatrix.cpp b/src/core/SkMatrix.cpp
index 38de466..fbd8c5a 100644
--- a/src/core/SkMatrix.cpp
+++ b/src/core/SkMatrix.cpp
@@ -720,25 +720,27 @@
     return SkDoubleToScalar(dcross(a, b, c, d) * scale);
 }
 
-static double sk_inv_determinant(const float mat[9], int isPerspective) {
-    double det;
-
+static double sk_determinant(const float mat[9], int isPerspective) {
     if (isPerspective) {
-        det = mat[SkMatrix::kMScaleX] *
-              dcross(mat[SkMatrix::kMScaleY], mat[SkMatrix::kMPersp2],
-                     mat[SkMatrix::kMTransY], mat[SkMatrix::kMPersp1])
-              +
-              mat[SkMatrix::kMSkewX]  *
-              dcross(mat[SkMatrix::kMTransY], mat[SkMatrix::kMPersp0],
-                     mat[SkMatrix::kMSkewY],  mat[SkMatrix::kMPersp2])
-              +
-              mat[SkMatrix::kMTransX] *
-              dcross(mat[SkMatrix::kMSkewY],  mat[SkMatrix::kMPersp1],
-                     mat[SkMatrix::kMScaleY], mat[SkMatrix::kMPersp0]);
+        return mat[SkMatrix::kMScaleX] *
+                    dcross(mat[SkMatrix::kMScaleY], mat[SkMatrix::kMPersp2],
+                           mat[SkMatrix::kMTransY], mat[SkMatrix::kMPersp1])
+                    +
+                    mat[SkMatrix::kMSkewX]  *
+                    dcross(mat[SkMatrix::kMTransY], mat[SkMatrix::kMPersp0],
+                           mat[SkMatrix::kMSkewY],  mat[SkMatrix::kMPersp2])
+                    +
+                    mat[SkMatrix::kMTransX] *
+                    dcross(mat[SkMatrix::kMSkewY],  mat[SkMatrix::kMPersp1],
+                           mat[SkMatrix::kMScaleY], mat[SkMatrix::kMPersp0]);
     } else {
-        det = dcross(mat[SkMatrix::kMScaleX], mat[SkMatrix::kMScaleY],
-                     mat[SkMatrix::kMSkewX], mat[SkMatrix::kMSkewY]);
+        return dcross(mat[SkMatrix::kMScaleX], mat[SkMatrix::kMScaleY],
+                      mat[SkMatrix::kMSkewX], mat[SkMatrix::kMSkewY]);
     }
+}
+
+static double sk_inv_determinant(const float mat[9], int isPerspective) {
+    double det = sk_determinant(mat, isPerspective);
 
     // Since the determinant is on the order of the cube of the matrix members,
     // compare to the cube of the default nearly-zero constant (although an
@@ -1855,3 +1857,38 @@
 
     return kHigh_SkFilterQuality;
 }
+
+SkScalar SkMatrixPriv::DifferentialScale(const SkMatrix& m, const SkPoint& p) {
+    //              [m00 m01 m02]                                 [f(u,v)]
+    // Assuming M = [m10 m11 m12], define the projected p'(u,v) = [g(u,v)] where
+    //              [m20 m12 m22]
+    //                                                        [x]     [u]
+    // f(u,v) = x(u,v) / w(u,v), g(u,v) = y(u,v) / w(u,v) and [y] = M*[v]
+    //                                                        [w]     [1]
+    //
+    // Then the differential scale factor between p = (u,v) and p' is |det J|,
+    // where J is the Jacobian for p': [df/du dg/du]
+    //                                 [df/dv dg/dv]
+    // and df/du = (w*dx/du - x*dw/du)/w^2,   dg/du = (w*dy/du - y*dw/du)/w^2
+    //     df/dv = (w*dx/dv - x*dw/dv)/w^2,   dg/dv = (w*dy/dv - y*dw/dv)/w^2
+    //
+    // From here, |det J| can be rewritten as |det J'/w^3|, where
+    //      [x     y     w    ]   [x   y   w  ]
+    // J' = [dx/du dy/du dw/du] = [m00 m10 m20]
+    //      [dx/dv dy/dv dw/dv]   [m01 m11 m21]
+    SkPoint3 xyw;
+    m.mapHomogeneousPoints(&xyw, &p, 1);
+
+    if (xyw.fZ < SK_ScalarNearlyZero) {
+        // Reaching the discontinuity of xy/w and where the point would clip to w >= 0
+        return SK_ScalarInfinity;
+    }
+    SkMatrix jacobian = SkMatrix::MakeAll(xyw.fX, xyw.fY, xyw.fZ,
+                                          m.getScaleX(), m.getSkewY(), m.getPerspX(),
+                                          m.getSkewX(), m.getScaleY(), m.getPerspY());
+
+    SkScalar denom = 1.f / xyw.fZ; // 1/w
+    denom = denom * denom * denom; // 1/w^3
+
+    return SkScalarAbs(SkDoubleToScalar(sk_determinant(jacobian.fMat, true)) * denom);
+}
diff --git a/src/core/SkMatrixPriv.h b/src/core/SkMatrixPriv.h
index bec7232..d90f0b7 100644
--- a/src/core/SkMatrixPriv.h
+++ b/src/core/SkMatrixPriv.h
@@ -171,6 +171,15 @@
                m.rc(3,3) == 1;
 
     }
+
+    // Returns the differential area scale factor for a local point 'p' that will be transformed
+    // by 'm' (which may have perspective). If 'm' does not have perspective, this scale factor is
+    // constant regardless of 'p'; when it does have perspective, it is specific to that point.
+    //
+    // This can be crudely thought of as "device pixel area" / "local pixel area" at 'p'.
+    //
+    // Returns positive infinity if the transformed homogeneous point has w <= 0.
+    static SkScalar DifferentialScale(const SkMatrix& m, const SkPoint& p);
 };
 
 #endif