[graphite] Analytic blur for rrects

Bug: b/238762890
Change-Id: I9be704ade0247c04a0a9f6ca5dd5735d5cbf3c19
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/844837
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
Commit-Queue: James Godfrey-Kittle <jamesgk@google.com>
Reviewed-by: John Stiles <johnstiles@google.com>
diff --git a/src/gpu/graphite/geom/AnalyticBlurMask.cpp b/src/gpu/graphite/geom/AnalyticBlurMask.cpp
index 07f907e..9d08f6f 100644
--- a/src/gpu/graphite/geom/AnalyticBlurMask.cpp
+++ b/src/gpu/graphite/geom/AnalyticBlurMask.cpp
@@ -73,7 +73,11 @@
         return MakeCircle(recorder, localToDevice, deviceSigma, srcRect, devRect);
     }
 
-    // TODO(b/238762890) Support analytic blurring with rrects.
+    if (devRRectIsValid && SkRRectPriv::IsSimpleCircular(devRRect) &&
+        localToDevice.isScaleTranslate()) {
+        return MakeRRect(recorder, localToDevice, deviceSigma, srcRRect, devRRect);
+    }
+
     return std::nullopt;
 }
 
@@ -157,10 +161,9 @@
 
     return AnalyticBlurMask(*drawBounds,
                             SkM44(devToScaledShape),
-                            shapeData,
                             ShapeType::kRect,
-                            isFast,
-                            invSixSigma,
+                            shapeData,
+                            {static_cast<float>(isFast), invSixSigma},
                             integral);
 }
 
@@ -233,11 +236,80 @@
     constexpr float kUnusedBlurData = 0.0f;
     return AnalyticBlurMask(*drawBounds,
                             SkM44(),
-                            shapeData,
                             ShapeType::kCircle,
-                            kUnusedBlurData,
-                            kUnusedBlurData,
+                            shapeData,
+                            {kUnusedBlurData, kUnusedBlurData},
                             profile);
 }
 
+std::optional<AnalyticBlurMask> AnalyticBlurMask::MakeRRect(Recorder* recorder,
+                                                            const SkMatrix& localToDevice,
+                                                            float devSigma,
+                                                            const SkRRect& srcRRect,
+                                                            const SkRRect& devRRect) {
+    const int devBlurRadius = 3 * SkScalarCeilToInt(devSigma - 1.0f / 6.0f);
+
+    const SkVector& devRadiiUL = devRRect.radii(SkRRect::kUpperLeft_Corner);
+    const SkVector& devRadiiUR = devRRect.radii(SkRRect::kUpperRight_Corner);
+    const SkVector& devRadiiLR = devRRect.radii(SkRRect::kLowerRight_Corner);
+    const SkVector& devRadiiLL = devRRect.radii(SkRRect::kLowerLeft_Corner);
+
+    const int devLeft = SkScalarCeilToInt(std::max<float>(devRadiiUL.fX, devRadiiLL.fX));
+    const int devTop = SkScalarCeilToInt(std::max<float>(devRadiiUL.fY, devRadiiUR.fY));
+    const int devRight = SkScalarCeilToInt(std::max<float>(devRadiiUR.fX, devRadiiLR.fX));
+    const int devBot = SkScalarCeilToInt(std::max<float>(devRadiiLL.fY, devRadiiLR.fY));
+
+    // This is a conservative check for nine-patchability.
+    const SkRect& devOrig = devRRect.getBounds();
+    if (devOrig.fLeft + devLeft + devBlurRadius >= devOrig.fRight - devRight - devBlurRadius ||
+        devOrig.fTop + devTop + devBlurRadius >= devOrig.fBottom - devBot - devBlurRadius) {
+        return std::nullopt;
+    }
+
+    const int newRRWidth = 2 * devBlurRadius + devLeft + devRight + 1;
+    const int newRRHeight = 2 * devBlurRadius + devTop + devBot + 1;
+
+    const SkRect newRect = SkRect::MakeXYWH(SkIntToScalar(devBlurRadius),
+                                            SkIntToScalar(devBlurRadius),
+                                            SkIntToScalar(newRRWidth),
+                                            SkIntToScalar(newRRHeight));
+    SkVector newRadii[4];
+    newRadii[0] = {SkScalarCeilToScalar(devRadiiUL.fX), SkScalarCeilToScalar(devRadiiUL.fY)};
+    newRadii[1] = {SkScalarCeilToScalar(devRadiiUR.fX), SkScalarCeilToScalar(devRadiiUR.fY)};
+    newRadii[2] = {SkScalarCeilToScalar(devRadiiLR.fX), SkScalarCeilToScalar(devRadiiLR.fY)};
+    newRadii[3] = {SkScalarCeilToScalar(devRadiiLL.fX), SkScalarCeilToScalar(devRadiiLL.fY)};
+
+    SkRRect rrectToDraw;
+    rrectToDraw.setRectRadii(newRect, newRadii);
+    const SkISize dimensions =
+            SkISize::Make(newRRWidth + 2 * devBlurRadius, newRRHeight + 2 * devBlurRadius);
+    SkBitmap ninePatchBitmap = skgpu::CreateRRectBlurMask(rrectToDraw, dimensions, devSigma);
+    if (ninePatchBitmap.empty()) {
+        return std::nullopt;
+    }
+
+    sk_sp<TextureProxy> ninePatch = RecorderPriv::CreateCachedProxy(recorder, ninePatchBitmap);
+    if (!ninePatch) {
+        return std::nullopt;
+    }
+
+    const float blurRadius = 3.0f * SkScalarCeilToScalar(devSigma - 1.0f / 6.0f);
+    const float edgeSize = 2.0f * blurRadius + SkRRectPriv::GetSimpleRadii(devRRect).fX + 0.5f;
+    const Rect shapeData = devRRect.rect().makeOutset(blurRadius, blurRadius);
+
+    // Determine how much to outset the draw bounds to ensure we hit pixels within 3*sigma.
+    std::optional<Rect> drawBounds = outset_bounds(localToDevice, devSigma, srcRRect.rect());
+    if (!drawBounds) {
+        return std::nullopt;
+    }
+
+    constexpr float kUnusedBlurData = 0.0f;
+    return AnalyticBlurMask(*drawBounds,
+                            SkM44(),
+                            ShapeType::kRRect,
+                            shapeData,
+                            {edgeSize, kUnusedBlurData},
+                            ninePatch);
+}
+
 }  // namespace skgpu::graphite
diff --git a/src/gpu/graphite/geom/AnalyticBlurMask.h b/src/gpu/graphite/geom/AnalyticBlurMask.h
index 762abbb..d6a8b85 100644
--- a/src/gpu/graphite/geom/AnalyticBlurMask.h
+++ b/src/gpu/graphite/geom/AnalyticBlurMask.h
@@ -52,24 +52,21 @@
     const SkM44& deviceToScaledShape() const { return fDevToScaledShape; }
     const Rect& shapeData() const { return fShapeData; }
     ShapeType shapeType() const { return fShapeType; }
-    bool isFast() const { return fIsFast; }
-    float invSixSigma() const { return fInvSixSigma; }
+    const SkV2& blurData() const { return fBlurData; }
     sk_sp<TextureProxy> refProxy() const { return fProxy; }
 
 private:
     AnalyticBlurMask(const Rect& drawBounds,
                      const SkM44& devToScaledShape,
-                     const Rect& shapeData,
                      ShapeType shapeType,
-                     bool isFast,
-                     float invSixSigma,
+                     const Rect& shapeData,
+                     const SkV2& blurData,
                      sk_sp<TextureProxy> proxy)
             : fDrawBounds(drawBounds)
             , fDevToScaledShape(devToScaledShape)
             , fShapeData(shapeData)
+            , fBlurData(blurData)
             , fShapeType(shapeType)
-            , fIsFast(isFast)
-            , fInvSixSigma(invSixSigma)
             , fProxy(std::move(proxy)) {}
 
     static std::optional<AnalyticBlurMask> MakeRect(Recorder*,
@@ -83,6 +80,12 @@
                                                       const SkRect& srcRect,
                                                       const SkRect& devRect);
 
+    static std::optional<AnalyticBlurMask> MakeRRect(Recorder* recorder,
+                                                     const SkMatrix& localToDevice,
+                                                     float devSigma,
+                                                     const SkRRect& srcRRect,
+                                                     const SkRRect& devRRect);
+
     // Draw bounds in local space.
     Rect fDrawBounds;
 
@@ -94,9 +97,19 @@
     // fDevToScaledShape.
     Rect fShapeData;
 
+    // "fBlurData" holds different data depending on the shape type, for the unique needs of the
+    // shape types' respective shaders.
+    // In the rectangle case, it holds:
+    //   x = a boolean indicating whether we can use a fast path for sampling the blur integral
+    //       because the rectangle is larger than 6*sigma in both dimensions, and
+    //   y = the value "1 / (6*sigma)".
+    // In the rounded rectangle case, it holds:
+    //   x = the size of the blurred edge, defined as "2*blurRadius + cornerRadius", and
+    //   y is unused.
+    // In the circle case, this data is unused.
+    SkV2 fBlurData;
+
     ShapeType fShapeType;
-    bool fIsFast;
-    float fInvSixSigma;
     sk_sp<TextureProxy> fProxy;
 };
 
diff --git a/src/gpu/graphite/render/AnalyticBlurRenderStep.cpp b/src/gpu/graphite/render/AnalyticBlurRenderStep.cpp
index 7699661..9abdc89 100644
--- a/src/gpu/graphite/render/AnalyticBlurRenderStep.cpp
+++ b/src/gpu/graphite/render/AnalyticBlurRenderStep.cpp
@@ -23,10 +23,9 @@
                      {{"localToDevice", SkSLType::kFloat4x4},
                       {"deviceToScaledShape", SkSLType::kFloat3x3},
                       {"shapeData", SkSLType::kFloat4},
-                      {"depth", SkSLType::kFloat},
+                      {"blurData", SkSLType::kHalf2},
                       {"shapeType", SkSLType::kInt},
-                      {"isFast", SkSLType::kInt},
-                      {"invSixSigma", SkSLType::kHalf}},
+                      {"depth", SkSLType::kFloat}},
                      PrimitiveType::kTriangleStrip,
                      kDirectDepthGreaterPass,
                      /*vertexAttrs=*/
@@ -54,9 +53,8 @@
 const char* AnalyticBlurRenderStep::fragmentCoverageSkSL() const {
     return "outputCoverage = blur_coverage_fn(scaledShapeCoords, "
                                              "shapeData, "
+                                             "blurData, "
                                              "shapeType, "
-                                             "isFast, "
-                                             "invSixSigma, "
                                              "s);";
 }
 
@@ -80,10 +78,9 @@
     const AnalyticBlurMask& blur = params.geometry().analyticBlurMask();
     gatherer->write(blur.deviceToScaledShape().asM33());
     gatherer->write(blur.shapeData().asSkRect());
-    gatherer->write(params.order().depthAsFloat());
+    gatherer->writeHalf(blur.blurData());
     gatherer->write(static_cast<int>(blur.shapeType()));
-    gatherer->write(blur.isFast());
-    gatherer->writeHalf(blur.invSixSigma());
+    gatherer->write(params.order().depthAsFloat());
 
     SkSamplingOptions samplingOptions = blur.shapeType() == AnalyticBlurMask::ShapeType::kRect
                                                 ? SkFilterMode::kLinear
diff --git a/src/sksl/generated/sksl_graphite_frag.minified.sksl b/src/sksl/generated/sksl_graphite_frag.minified.sksl
index d5f3f27..bafd296 100644
--- a/src/sksl/generated/sksl_graphite_frag.minified.sksl
+++ b/src/sksl/generated/sksl_graphite_frag.minified.sksl
@@ -213,13 +213,17 @@
 "(k,h,f,c,d,e);float n=min(g.y,0.)*a.w;float o=l*(min(k.x+n,-k.y)+m);return half4"
 "(half(saturate(o)));}}$pure half4 per_edge_aa_quad_coverage_fn(float4 a,float4"
 " b){float2 d=min(b.xy,b.zw);float e=min(d.x,d.y)*a.w;return half4(half(saturate"
-"(e)));}$pure half4 $G(float2 a,float4 b,int c,half d,sampler2D e){half f;half"
-" g;if(bool(c)){half2 h=max(half2(b.xy-a),half2(a-b.zw));f=sample(e,float2(float"
+"(e)));}$pure half4 $G(float2 a,float4 b,half c,half d,sampler2D e){half f;half"
+" g;if(c!=0.){half2 h=max(half2(b.xy-a),half2(a-b.zw));f=sample(e,float2(float"
 "(d*h.x),.5)).x;g=sample(e,float2(float(d*h.y),.5)).x;}else{half4 h=half4(half2"
 "(b.xy-a),half2(a-b.zw));f=(1.-sample(e,float2(float(d*h.x),.5)).x)-sample(e"
 ",float2(float(d*h.z),.5)).x;g=(1.-sample(e,float2(float(d*h.y),.5)).x)-sample"
 "(e,float2(float(d*h.w),.5)).x;}return half4(f*g);}$pure half4 $H(float2 a,float4"
 " b,sampler2D c){float d=b.z;float e=b.w;half2 f=half2((a-b.xy)*d);float g=float"
-"(length(f))-e;return sample(c,float2(g,.5)).xxxx;}$pure half4 blur_coverage_fn"
-"(float2 a,float4 b,int c,int d,half e,sampler2D f){switch(c){case 0:{return"
-" $G(a,b,d,e,f);}case 2:{return $H(a,b,f);}}return half4(0.);}";
+"(length(f))-e;return sample(c,float2(g,.5)).xxxx;}$pure half4 $I(float2 a,float4"
+" b,half c,sampler2D d){float2 e=a-b.xy;float2 f=(b.zw-b.xy)*.5;e-=f;half2 g"
+"=half2(sign(e));e=abs(e);half2 h=half2(e-(f-float(c)));h=max(h,0.);h*=g;h+="
+"half2(c);half2 i=half2(2.*c);half2 j=h/i;return sample(d,float2(j)).xxxx;}$pure"
+" half4 blur_coverage_fn(float2 a,float4 b,half2 c,int d,sampler2D e){switch"
+"(d){case 0:{return $G(a,b,c.x,c.y,e);}case 2:{return $H(a,b,e);}case 1:{return"
+" $I(a,b,c.x,e);}}return half4(0.);}";
diff --git a/src/sksl/generated/sksl_graphite_frag.unoptimized.sksl b/src/sksl/generated/sksl_graphite_frag.unoptimized.sksl
index 7031e57..00f374a 100644
--- a/src/sksl/generated/sksl_graphite_frag.unoptimized.sksl
+++ b/src/sksl/generated/sksl_graphite_frag.unoptimized.sksl
@@ -382,9 +382,9 @@
 ")));}}$pure half4 per_edge_aa_quad_coverage_fn(float4 coords,float4 edgeDistances"
 "){float2 outerDist=min(edgeDistances.xy,edgeDistances.zw);float c=min(outerDist"
 ".x,outerDist.y)*coords.w;return half4(half(saturate(c)));}$pure half4 $rect_blur_coverage_fn"
-"(float2 coords,float4 rect,int isFast,half invSixSigma,sampler2D integral){"
-"half xCoverage;half yCoverage;if(bool(isFast)){half2 pos=max(half2(rect.xy-"
-"coords),half2(coords-rect.zw));xCoverage=sample(integral,float2(float(invSixSigma"
+"(float2 coords,float4 rect,half isFast,half invSixSigma,sampler2D integral)"
+"{half xCoverage;half yCoverage;if(isFast!=0.){half2 pos=max(half2(rect.xy-coords"
+"),half2(coords-rect.zw));xCoverage=sample(integral,float2(float(invSixSigma"
 "*pos.x),.5)).x;yCoverage=sample(integral,float2(float(invSixSigma*pos.y),.5"
 ")).x;}else{half4 rect=half4(half2(rect.xy-coords),half2(coords-rect.zw));xCoverage"
 "=(1.-sample(integral,float2(float(invSixSigma*rect.x),.5)).x)-sample(integral"
@@ -394,8 +394,17 @@
 "(float2 coords,float4 circle,sampler2D blurProfile){float invTextureRadius="
 "circle.z;float normSolidRadius=circle.w;half2 vec=half2((coords-circle.xy)*"
 "invTextureRadius);float dist=float(length(vec))-normSolidRadius;return sample"
-"(blurProfile,float2(dist,.5)).xxxx;}$pure half4 blur_coverage_fn(float2 coords"
-",float4 shapeData,int shapeType,int isFast,half invSixSigma,sampler2D s){switch"
-"(shapeType){case 0:{return $rect_blur_coverage_fn(coords,shapeData,isFast,invSixSigma"
-",s);}case 2:{return $circle_blur_coverage_fn(coords,shapeData,s);}}return half4"
-"(0.);}";
+"(blurProfile,float2(dist,.5)).xxxx;}$pure half4 $rrect_blur_coverage_fn(float2"
+" coords,float4 proxyRect,half edgeSize,sampler2D ninePatch){float2 translatedFragPosFloat"
+"=coords-proxyRect.xy;float2 proxyCenter=(proxyRect.zw-proxyRect.xy)*.5;translatedFragPosFloat"
+"-=proxyCenter;half2 fragDirection=half2(sign(translatedFragPosFloat));translatedFragPosFloat"
+"=abs(translatedFragPosFloat);half2 translatedFragPosHalf=half2(translatedFragPosFloat"
+"-(proxyCenter-float(edgeSize)));translatedFragPosHalf=max(translatedFragPosHalf"
+",0.);translatedFragPosHalf*=fragDirection;translatedFragPosHalf+=half2(edgeSize"
+");half2 proxyDims=half2(2.*edgeSize);half2 texCoord=translatedFragPosHalf/proxyDims"
+";return sample(ninePatch,float2(texCoord)).xxxx;}$pure half4 blur_coverage_fn"
+"(float2 coords,float4 shapeData,half2 blurData,int shapeType,sampler2D s){switch"
+"(shapeType){case 0:{return $rect_blur_coverage_fn(coords,shapeData,blurData"
+".x,blurData.y,s);}case 2:{return $circle_blur_coverage_fn(coords,shapeData,"
+"s);}case 1:{return $rrect_blur_coverage_fn(coords,shapeData,blurData.x,s);}"
+"}return half4(0.);}";
diff --git a/src/sksl/sksl_graphite_frag.sksl b/src/sksl/sksl_graphite_frag.sksl
index 7914fde..f04f2cc 100644
--- a/src/sksl/sksl_graphite_frag.sksl
+++ b/src/sksl/sksl_graphite_frag.sksl
@@ -1439,13 +1439,13 @@
 
 $pure half4 $rect_blur_coverage_fn(float2 coords,
                                    float4 rect,
-                                   int isFast,
+                                   half isFast,
                                    half invSixSigma,
                                    sampler2D integral) {
     half xCoverage;
     half yCoverage;
 
-    if (bool(isFast)) {
+    if (isFast != 0.0) {
         // Get the smaller of the signed distance from the frag coord to the left and right
         // edges and similar for y.
         // The integral texture goes "backwards" (from 3*sigma to -3*sigma), So, the below
@@ -1494,20 +1494,63 @@
     return sample(blurProfile, float2(dist, 0.5)).rrrr;
 }
 
+$pure half4 $rrect_blur_coverage_fn(float2 coords,
+                                    float4 proxyRect,
+                                    half edgeSize,
+                                    sampler2D ninePatch) {
+    // Warp the fragment position to the appropriate part of the 9-patch blur texture by
+    // snipping out the middle section of the proxy rect.
+    float2 translatedFragPosFloat = coords - proxyRect.LT;
+    float2 proxyCenter = (proxyRect.RB - proxyRect.LT) * 0.5;
+
+    // Position the fragment so that (0, 0) marks the center of the proxy rectangle.
+    // Negative coordinates are on the left/top side and positive numbers are on the
+    // right/bottom.
+    translatedFragPosFloat -= proxyCenter;
+
+    // Temporarily strip off the fragment's sign. x/y are now strictly increasing as we
+    // move away from the center.
+    half2 fragDirection = half2(sign(translatedFragPosFloat));
+    translatedFragPosFloat = abs(translatedFragPosFloat);
+
+    // Our goal is to snip out the "middle section" of the proxy rect (everything but the
+    // edge). We've repositioned our fragment position so that (0, 0) is the centerpoint
+    // and x/y are always positive, so we can subtract here and interpret negative results
+    // as being within the middle section.
+    half2 translatedFragPosHalf = half2(translatedFragPosFloat - (proxyCenter - edgeSize));
+
+    // Remove the middle section by clamping to zero.
+    translatedFragPosHalf = max(translatedFragPosHalf, 0);
+
+    // Reapply the fragment's sign, so that negative coordinates once again mean left/top
+    // side and positive means bottom/right side.
+    translatedFragPosHalf *= fragDirection;
+
+    // Offset the fragment so that (0, 0) marks the upper-left again, instead of the center
+    // point.
+    translatedFragPosHalf += half2(edgeSize);
+
+    half2 proxyDims = half2(2.0 * edgeSize);
+    half2 texCoord = translatedFragPosHalf / proxyDims;
+
+    return sample(ninePatch, texCoord).rrrr;
+}
+
 $pure half4 blur_coverage_fn(float2 coords,
                              float4 shapeData,
+                             half2 blurData,
                              int shapeType,
-                             int isFast,
-                             half invSixSigma,
                              sampler2D s) {
     switch (shapeType) {
         case $kShapeTypeRect: {
-            return $rect_blur_coverage_fn(coords, shapeData, isFast, invSixSigma, s);
+            return $rect_blur_coverage_fn(coords, shapeData, blurData.x, blurData.y, s);
         }
         case $kShapeTypeCircle: {
             return $circle_blur_coverage_fn(coords, shapeData, s);
         }
-        // TODO(b/238762890) Handle $kShapeTypeRRect.
+        case $kShapeTypeRRect: {
+            return $rrect_blur_coverage_fn(coords, shapeData, blurData.x, s);
+        }
     }
     return half4(0);
 }