| /* | |
| * Copyright 2026 Google LLC | |
| * | |
| * Use of this source code is governed by a BSD-style license that can be | |
| * found in the LICENSE file. | |
| */ | |
| #include "src/gpu/graphite/render/AnalyticRRectBlurRenderStep.h" | |
| #include "include/core/SkM44.h" | |
| #include "include/core/SkRRect.h" | |
| #include "include/private/SkDebug.h" | |
| #include "src/core/SkSLTypeShared.h" | |
| #include "src/gpu/BufferWriter.h" | |
| #include "src/gpu/graphite/Attribute.h" | |
| #include "src/gpu/graphite/BufferManager.h" | |
| #include "src/gpu/graphite/ContextUtils.h" | |
| #include "src/gpu/graphite/PipelineData.h" | |
| #include "src/gpu/graphite/UniformManager.h" | |
| #include "src/gpu/graphite/draw/DrawOrder.h" | |
| #include "src/gpu/graphite/draw/DrawParams.h" | |
| #include "src/gpu/graphite/draw/DrawWriter.h" | |
| #include "src/gpu/graphite/geom/AnalyticRRectBlurMask.h" | |
| #include "src/gpu/graphite/geom/Transform.h" | |
| #include "src/gpu/graphite/render/CommonDepthStencilSettings.h" | |
| namespace skgpu::graphite { | |
| struct Vertex { | |
| // x and y determine our index into xBounds and yBounds respectively, within the range [0, 5]. | |
| // z and w determine if we should apply our bevel and the direction, expected to be -1, 0, or 1. | |
| // The bevel coefficient is multiplied by the bevel fraction and the radii of the current cell | |
| // (see analytic_rrect_blur_vertex_fn), this bevel is only set and applied for the two | |
| // outermost corner vertices in the corner cells. | |
| int32_t fGridAndBevel[4]; | |
| // The cell index of the current cell. Bits 28-31 are set if the current cell is a corner safe | |
| // edge. The encoding from least significant to most significant is kept the same as | |
| // `canSaturateEdge`: left, right, top, and bottom. | |
| uint32_t fCellID; | |
| }; | |
| static constexpr int kVertexCount = 104; | |
| static constexpr int kIndexCount = 162; | |
| // We construct a 25 cell grid (5x5) for our rrect, with 6 distinct boundaries for each axis: | |
| // - Outset blur padding: the provided rrect's bounding box outset by 3 sigma. | |
| // - Inset edge: the provided rrect's bounding box inset by 3.5 sigma. | |
| // - Safe bounds: The furthest point from the edge which is inset by the deepest corner radii on | |
| // for that edge, then inset by an additional 3 sigma. | |
| // | |
| // This is an example of the layout of vertices for a rounded rect with a non-zero corner radius | |
| // and a low sigma, where the corner radius plus three sigma from the edge of the rect doesn't | |
| // exceed the halfway point of the rect (5x5 grid): | |
| // | |
| // x0 x1 x2 x3 x4 x5 | |
| // | | | | | | | |
| // y0 --+----+--------+--------+--------+----+-- Outset blur padding | |
| // | | | | | | | |
| // y1 --+----+--------+--------+--------+----+-- Inset edge | |
| // | | | Inside | | | | |
| // y2 --+----+--------+--------+--------+----+-- Safe bounds | |
| // | | Inside | Inside | Inside | | | |
| // y3 --+----+--------+--------+--------+----+-- Safe Bounds | |
| // | | | Inside | | | | |
| // y4 --+----+--------+--------+--------+----+-- Inset edge | |
| // | | | | | | | |
| // y5 --+----+--------+--------+--------+----+-- Outset blur padding | |
| // | |
| // The labels on the y-axis also apply to the x-axis as well, this is the ideal case where we can | |
| // classify 5 cells as fully saturated (not affected by the blur), so we can skip evaluating the | |
| // fragment shader for those pixels since we guarantee they won't be affected by the blur. | |
| // For the outermost edge cells between x2 and x3, or y2 and y3, we can further simplify the blur | |
| // evaluation given the blur radius 3 sigma away doesn't go past the opposite edge of the rect. This | |
| // allows us to simplify the CDF evaluation for that edge since we know the opposite edge CDF | |
| // evalation is fully saturated (either 0 or 1), and for the other axis, we are fully | |
| // within the rounded rect. | |
| // | |
| // In the case that the sigma is very large or the radii leaves no room for a straight edge on an | |
| // axis, the inset and/or safe bounds are snapped to the middle of that axis and will result in | |
| // degenerate triangles which can be ignored. | |
| // | |
| // Example of the resulting vertices with a large corner radius (4x4 grid): | |
| // | |
| // x0 x1 x2/x3 x4 x5 | |
| // | | | | | | |
| // y0 --+----+----+----+----+-- Outset blur padding | |
| // | | | | | | |
| // y1 --+----+----+----+----+-- Inset edge | |
| // | | | | | | |
| // y2/y3 --+----+----+----+----+-- Safe Bounds | |
| // | | | | | | |
| // y4 --+----+----+----+----+-- Inset edge | |
| // | | | | | | |
| // y5 --+----+----+----+----+-- Outset blur padding | |
| // | |
| // Example of the resulting verices with a large sigma (2x2 grid): | |
| // | |
| // x0 x1/x2/x3/x4 x5 | |
| // | | | | |
| // y0 --+-------+-------+-- Outset blur padding | |
| // | | | | |
| // y1/y2/y3/y4 --+-------+-------+-- Inset edge/safe Bounds | |
| // | | | | |
| // y5 --+-------+-------+-- Outset blur padding | |
| // | |
| // Since there is no area between the safe bounds for the 4x4 case, we cannot classify any inner | |
| // cell as fully saturated as its possible that the corner's blur may affect one of the inner cells. | |
| // For the 2x2 case, we are only left with the corner cells so every pixel may be affected by the | |
| // blur. | |
| // | |
| // For each corner cell, we perform beveling dependent on the corner radius to reduce the number | |
| // of pixels we must evaluate for the blur. For this, we use the following template of 5 vertices | |
| // and 3 triangles, where v0 and v4 have a non-zero bevel value associated with them: | |
| // | |
| // v0-----v1 | |
| // / \ f0 | | |
| // / \__ | | |
| // v4 f1 \ | | |
| // | \____ \ | | |
| // | f2 \__\| | |
| // v3--------v2 | |
| // | |
| static void write_vertex_buffer(VertexWriter writer) { | |
| if (!writer) return; | |
| // Corner 0: TL, cell 0. | |
| writer << Vertex{{0, 0, 1, 0}, 0} // v0 | |
| << Vertex{{1, 0, 0, 0}, 0} // v1 | |
| << Vertex{{1, 1, 0, 0}, 0} // v2 | |
| << Vertex{{0, 1, 0, 0}, 0} // v3 | |
| << Vertex{{0, 0, 0, 1}, 0}; // v4 | |
| // Corner 1: TR, cell 4. | |
| writer << Vertex{{4, 0, 0, 0}, 4} // v5 | |
| << Vertex{{5, 0, -1, 0}, 4} // v6 | |
| << Vertex{{5, 0, 0, 1}, 4} // v7 | |
| << Vertex{{5, 1, 0, 0}, 4} // v8 | |
| << Vertex{{4, 1, 0, 0}, 4}; // v9 | |
| // Corner 2: BR, cell 24. | |
| writer << Vertex{{4, 4, 0, 0}, 24} // v10 | |
| << Vertex{{5, 4, 0, 0}, 24} // v11 | |
| << Vertex{{5, 5, 0, -1}, 24} // v12 | |
| << Vertex{{5, 5, -1, 0}, 24} // v13 | |
| << Vertex{{4, 5, 0, 0}, 24}; // v14 | |
| // Corner 3: BL, cell 20. | |
| writer << Vertex{{0, 4, 0, 0}, 20} // v15 | |
| << Vertex{{1, 4, 0, 0}, 20} // v16 | |
| << Vertex{{1, 5, 0, 0}, 20} // v17 | |
| << Vertex{{0, 5, 1, 0}, 20} // v18 | |
| << Vertex{{0, 5, 0, -1}, 20}; // v19 | |
| // 21 quads for the remaining cells. | |
| for (int row = 0; row < 5; row++) { | |
| for (int col = 0; col < 5; col++) { | |
| // Skip corners. | |
| if ((row == 0 && col == 0) || (row == 0 && col == 4) || | |
| (row == 4 && col == 0) || (row == 4 && col == 4)) { | |
| continue; | |
| } | |
| uint32_t cId = row * 5 + col; | |
| int x0 = col; | |
| int x1 = col + 1; | |
| int y0 = row; | |
| int y1 = row + 1; | |
| // Encode the corner safe edge bit. | |
| if (row == 2) { | |
| if (col == 0) { | |
| cId |= 1 << 28; // Left | |
| } else if (col == 4) { | |
| cId |= 1 << 29; // Right | |
| } | |
| } else if (col == 2) { | |
| if (row == 0) { | |
| cId |= 1 << 30; // Top | |
| } else if (row == 4) { | |
| cId |= 1 << 31; // Bottom | |
| } | |
| } | |
| writer << Vertex{{x0, y0, 0, 0}, cId} | |
| << Vertex{{x1, y0, 0, 0}, cId} | |
| << Vertex{{x1, y1, 0, 0}, cId} | |
| << Vertex{{x0, y1, 0, 0}, cId}; | |
| } | |
| } | |
| } | |
| static void write_index_buffer(VertexWriter writer) { | |
| if (!writer) return; | |
| // Corner 0: TL Corner, fan from v2. | |
| writer << uint16_t(2) << uint16_t(0) << uint16_t(1) | |
| << uint16_t(2) << uint16_t(4) << uint16_t(0) | |
| << uint16_t(2) << uint16_t(3) << uint16_t(4); | |
| // Corner 1: TR Corner, fan from v9. | |
| writer << uint16_t(9) << uint16_t(5) << uint16_t(6) | |
| << uint16_t(9) << uint16_t(6) << uint16_t(7) | |
| << uint16_t(9) << uint16_t(7) << uint16_t(8); | |
| // Corner 2: BR Corner, fan from v10. | |
| writer << uint16_t(10) << uint16_t(11) << uint16_t(12) | |
| << uint16_t(10) << uint16_t(12) << uint16_t(13) | |
| << uint16_t(10) << uint16_t(13) << uint16_t(14); | |
| // Corner 3: BL Corner, fan from v16. | |
| writer << uint16_t(16) << uint16_t(17) << uint16_t(18) | |
| << uint16_t(16) << uint16_t(18) << uint16_t(19) | |
| << uint16_t(16) << uint16_t(19) << uint16_t(15); | |
| // Create remaining quads. | |
| uint16_t base = 20; | |
| for (int i = 0; i < 21; i++) { | |
| writer << uint16_t(base + 0) << uint16_t(base + 1) << uint16_t(base + 3) | |
| << uint16_t(base + 1) << uint16_t(base + 2) << uint16_t(base + 3); | |
| base += 4; | |
| } | |
| } | |
| AnalyticRRectBlurRenderStep::AnalyticRRectBlurRenderStep(Layout layout, | |
| StaticBufferManager* bufferManager) | |
| : RenderStep(layout, | |
| RenderStepID::kAnalyticRRectBlur, | |
| Flags::kPerformsShading | Flags::kHasTextures | |
| | Flags::kEmitsCoverage | |
| | Flags::kNoSelfIntersections | |
| | Flags::kAppendInstances, | |
| /*uniforms=*/ | |
| {{"rect", SkSLType::kFloat4}, | |
| {"drawPad", SkSLType::kFloat2}, | |
| {"doFastPath", SkSLType::kFloat}, | |
| {"sqrtHalfOverSigma", SkSLType::kHalf2}, | |
| {"rrectRadii", SkSLType::kFloat4, 2}, | |
| {"blurRadius", SkSLType::kFloat2}}, | |
| PrimitiveType::kTriangles, | |
| /*stencilSettings=*/{}, | |
| /*staticAttrs=*/ | |
| {{"gridAndBevel", VertexAttribType::kInt4, SkSLType::kInt4}, | |
| {"cellID", VertexAttribType::kUInt, SkSLType::kUInt}}, | |
| /*appendAttrs=*/ | |
| {{"bounds0", VertexAttribType::kFloat4, SkSLType::kFloat4}, | |
| {"bounds1", VertexAttribType::kFloat4, SkSLType::kFloat4}, | |
| {"bounds2", VertexAttribType::kFloat4, SkSLType::kFloat4}, | |
| {"cornerSafeBounds", VertexAttribType::kFloat4, SkSLType::kFloat4}, | |
| {"canSaturateEdge", VertexAttribType::kUInt, SkSLType::kUInt}, | |
| {"localToDevice0", VertexAttribType::kFloat3, SkSLType::kFloat3}, | |
| {"localToDevice1", VertexAttribType::kFloat3, SkSLType::kFloat3}, | |
| {"localToDevice2", VertexAttribType::kFloat3, SkSLType::kFloat3}, | |
| {"depth", VertexAttribType::kFloat, SkSLType::kFloat}, | |
| {"ssboIndex", VertexAttribType::kUInt, SkSLType::kUInt}}, | |
| /*storageUniforms=*/{}, | |
| /*varyings=*/ | |
| {{"scaledShapeCoords", SkSLType::kFloat2}, | |
| {"vFlags", SkSLType::kHalf4}}) { | |
| write_vertex_buffer(bufferManager->getVertexWriter(kVertexCount, sizeof(Vertex), | |
| &fVertexBuffer)); | |
| write_index_buffer(bufferManager->getIndexWriter(sizeof(uint16_t) * kIndexCount, | |
| &fIndexBuffer)); | |
| } | |
| std::string AnalyticRRectBlurRenderStep::vertexSkSL(const RootNodesInfo&) const { | |
| return "float4 devPosition = analytic_rrect_blur_vertex_fn(" | |
| "gridAndBevel, cellID, rect, " | |
| "bounds0, bounds1, bounds2, " | |
| "cornerSafeBounds, canSaturateEdge, drawPad, rrectRadii, depth, " | |
| "localToDevice0, localToDevice1, localToDevice2, " | |
| "scaledShapeCoords, vFlags, stepLocalCoords);\n"; | |
| } | |
| std::string AnalyticRRectBlurRenderStep::texturesAndSamplersSkSL( | |
| const ResourceBindingRequirements& bindingReqs, int* nextBindingIndex) const { | |
| return EmitSamplerLayout(bindingReqs, nextBindingIndex) + " sampler2D cdfLut;"; | |
| } | |
| const char* AnalyticRRectBlurRenderStep::fragmentCoverageSkSL() const { | |
| return "outputCoverage = analytic_rrect_blur_coverage_fn(scaledShapeCoords, " | |
| "vFlags, " | |
| "rect, " | |
| "sqrtHalfOverSigma, " | |
| "rrectRadii, " | |
| "blurRadius, " | |
| "doFastPath, " | |
| "cdfLut);"; | |
| } | |
| void AnalyticRRectBlurRenderStep::writeVertices(DrawWriter* writer, | |
| StorageContext* /*storageContext*/, | |
| const DrawParams& params, | |
| uint32_t ssboIndex) const { | |
| const AnalyticRRectBlurMask& blur = params.geometry().analyticRRectBlurMask(); | |
| SkRect rect = blur.rrect().getBounds(); | |
| SkSpan<const SkVector> radii = blur.rrect().radii(); | |
| float drawPadX = blur.drawPadX(); | |
| float drawPadY = blur.drawPadY(); | |
| // Calculate distance from edge where the corner curvature has ended and is saturated. | |
| float safeOffsetLeft = std::max(radii[0].fX, radii[3].fX) + drawPadX; | |
| float safeOffsetRight = std::max(radii[1].fX, radii[2].fX) + drawPadX; | |
| float safeOffsetTop = std::max(radii[0].fY, radii[1].fY) + drawPadY; | |
| float safeOffsetBottom = std::max(radii[2].fY, radii[3].fY) + drawPadY; | |
| // Innermost safe bounds that we define as our fully saturated bounds. | |
| float cornerSafeXMin = rect.fLeft + safeOffsetLeft; | |
| float cornerSafeXMax = rect.fRight - safeOffsetRight; | |
| float cornerSafeYMin = rect.fTop + safeOffsetTop; | |
| float cornerSafeYMax = rect.fBottom - safeOffsetBottom; | |
| // Snap innermost inset bounds to the center if they are overlapping. | |
| if (cornerSafeXMin >= cornerSafeXMax) { | |
| cornerSafeXMin = cornerSafeXMax = rect.centerX(); | |
| } | |
| if (cornerSafeYMin >= cornerSafeYMax) { | |
| cornerSafeYMin = cornerSafeYMax = rect.centerY(); | |
| } | |
| // Our outermost edge safe inset bounds. This allows us to assume full coverage when we are | |
| // far enough from an edge on one axis and within the corner radius safe limits on the other | |
| // axis. | |
| float edgeInsXMin = std::min(cornerSafeXMin, rect.fLeft + drawPadX); | |
| float edgeInsXMax = std::max(cornerSafeXMax, rect.fRight - drawPadX); | |
| float edgeInsYMin = std::min(cornerSafeYMin, rect.fTop + drawPadY); | |
| float edgeInsYMax = std::max(cornerSafeYMax, rect.fBottom - drawPadY); | |
| // Bounds mapping to each row and columns of our 5x5 vertex grid. | |
| const float xBounds[6] = {rect.fLeft - drawPadX, edgeInsXMin, cornerSafeXMin, | |
| cornerSafeXMax, edgeInsXMax, rect.fRight + drawPadX}; | |
| const float yBounds[6] = {rect.fTop - drawPadY, edgeInsYMin, cornerSafeYMin, | |
| cornerSafeYMax, edgeInsYMax, rect.fBottom + drawPadY}; | |
| const SkM44& mat = params.transform().matrix(); | |
| // Bitmask determining which edges we can assume are fully saturated by the opposite edge. | |
| // Dependent on the current edge cell's blur not going past the opposite edge of the rect. | |
| // In order from least significant to most significant: left, right, top, and bottom. | |
| uint32_t canSaturateEdge = 0; | |
| if (edgeInsXMin + drawPadX < rect.fRight) { | |
| canSaturateEdge |= 1; | |
| } | |
| if (edgeInsXMax - drawPadX > rect.fLeft) { | |
| canSaturateEdge |= 2; | |
| } | |
| if (edgeInsYMin + drawPadY < rect.fBottom) { | |
| canSaturateEdge |= 4; | |
| } | |
| if (edgeInsYMax - drawPadY > rect.fTop) { | |
| canSaturateEdge |= 8; | |
| } | |
| DrawWriter::Instances instances{*writer, fVertexBuffer, fIndexBuffer, kIndexCount}; | |
| instances.append(1) << VertexWriter::Array(xBounds, 6) // bounds0, bounds1 | |
| << VertexWriter::Array(yBounds, 6) // bounds2 | |
| << cornerSafeXMin << cornerSafeYMin | |
| << cornerSafeXMax << cornerSafeYMax // cornerSafeBounds | |
| << canSaturateEdge | |
| << mat.rc(0, 0) << mat.rc(1, 0) << mat.rc(3, 0) // localToDevice0 | |
| << mat.rc(0, 1) << mat.rc(1, 1) << mat.rc(3, 1) // localToDevice1 | |
| << mat.rc(0, 3) << mat.rc(1, 3) << mat.rc(3, 3) // localToDevice2 | |
| << params.order().depthAsFloat() | |
| << ssboIndex; | |
| } | |
| void AnalyticRRectBlurRenderStep::writeUniformsAndTextures(const DrawParams& params, | |
| PipelineDataGatherer* gatherer) const { | |
| SkDEBUGCODE(UniformExpectationsValidator uev(gatherer, this->uniforms());) | |
| const AnalyticRRectBlurMask& blur = params.geometry().analyticRRectBlurMask(); | |
| const SkRRect& rrect = blur.rrect(); | |
| SkSpan<const SkVector> radii = rrect.radii(); | |
| SkV2 localSigma = blur.localSigma(); | |
| SkV2 sqrtHalfOverSigma = {(1.f / SK_FloatSqrt2) / localSigma.x, | |
| (1.f / SK_FloatSqrt2) / localSigma.y}; | |
| SkSize blurRadius = {static_cast<SkScalar>(std::floor(std::ceil(6.f * localSigma.x) / 2.0)), | |
| static_cast<SkScalar>(std::floor(std::ceil(6.f * localSigma.y) / 2.0))}; | |
| SkRect bounds = rrect.getBounds(); | |
| // When the blur radius doesn't exceed the halfway point of the rect starting from the edge, | |
| // we know we can perform a fast path for evaluating the blur due to the rect by only sampling | |
| // the CDF against the closest edge for each axis. | |
| float doFastPath = (bounds.width() >= 2.f * blurRadius.width() && | |
| bounds.height() >= 2.f * blurRadius.height()) ? 1.f : 0.f; | |
| gatherer->write(bounds); | |
| gatherer->write(blur.drawPad()); | |
| gatherer->write(doFastPath); | |
| gatherer->writeHalf(sqrtHalfOverSigma); | |
| const SkV4 radiiArr[2] = {SkV4{radii[0].fX, radii[0].fY, radii[1].fX, radii[1].fY}, | |
| SkV4{radii[2].fX, radii[2].fY, radii[3].fX, radii[3].fY}}; | |
| gatherer->writeArray(SkSpan(radiiArr, 2)); | |
| gatherer->write(blurRadius); | |
| gatherer->add(blur.refCdfProxy(), {SkFilterMode::kLinear, SkTileMode::kClamp}); | |
| } | |
| } // namespace skgpu::graphite |