/* * Copyright 2024 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/CircularArcRenderStep.h" #include "include/core/SkArc.h" #include "include/core/SkM44.h" #include "include/core/SkPaint.h" #include "include/core/SkRect.h" #include "include/core/SkScalar.h" #include "include/private/base/SkAssert.h" #include "include/private/base/SkPoint_impl.h" #include "src/base/SkEnumBitMask.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/DrawOrder.h" #include "src/gpu/graphite/DrawParams.h" #include "src/gpu/graphite/DrawTypes.h" #include "src/gpu/graphite/DrawWriter.h" #include "src/gpu/graphite/geom/Geometry.h" #include "src/gpu/graphite/geom/Shape.h" #include "src/gpu/graphite/geom/Transform_graphite.h" #include "src/gpu/graphite/render/CommonDepthStencilSettings.h" #include // This RenderStep is used to render filled circular arcs and stroked circular arcs that // don't include the center. Currently it only supports butt caps but will be extended // to include round caps. // // Each arc is represented by a single instance. The instance attributes are enough to // describe the given arc types without relying on uniforms to define its operation. // The attributes encode shape as follows: // float4 centerScales - used to transform the vertex data into local space. // The vertex data represents interleaved octagons that are respectively circumscribed // and inscribed on a unit circle, and have to be transformed into local space. // So the .xy values here are the center of the arc in local space, and .zw its outer and inner // radii, respectively. If the vertex is an outer vertex its local position will be computed as // centerScales.xy + position.xy * centerScales.z // Otherwise it will be computed as // centerScales.xy + position.xy * centerScales.w // We can tell whether a vertex is an outer or inner vertex by looking at the sign // of its z component. This z value is also used to compute half-pixel anti-aliasing offsets // once the vertex data is transformed into device space. // float3 radiiAndFlags - in the fragment shader we will pass an offset in unit circle space to // determine the circle edge and for use for clipping. The .x value here is outerRadius+0.5 and // will be compared against the unit circle radius (i.e., 1.0) to compute the outer edge. The .y // value is innerRadius-0.5/outerRadius+0.5 and will be used as the comparison point for the // inner edge. The .z value is a flag which indicates whether fragClipPlane1 is for intersection // (+) or for union (-), and whether to set up rounded caps (-2/+2). // float3 geoClipPlane - For very thin acute arcs, because of the 1/2 pixel boundary we can get // non-clipped artifacts beyond the center of the circle. To solve this, we clip the geometry // so any rendering doesn't cross that point. // In addition, these values will be passed to the fragment shader: // // float3 fragClipPlane0 - the arc will always be clipped against this half plane, and passed as // the varying clipPlane. // float3 fragClipPlane1 - for convex/acute arcs, we pass this via the varying isectPlane to clip // against this and multiply its value by the ClipPlane clip result. For concave/obtuse arcs, // we pass this via the varying unionPlane which will clip against this and add its value to the // ClipPlane clip result. This is controlled by the flag value in radiiAndFlags: if the // flag is > 0, it's passed as isectClip, if it's < 0 it's passed as unionClip. We set default // values for the alternative clip plane that end up being a null clip. // float roundCapRadius - this is computed in the vertex shader. If we're using round caps (i.e., // if abs(flags) > 1), this will be half the distance between the outer and inner radii. // Otherwise it will be 0 which will end up zeroing out any round cap calculation. // float4 inRoundCapPos - locations of the centers of the round caps in normalized space. This // will be all zeroes if not needed. namespace skgpu::graphite { // Represents the per-vertex attributes used in each instance. struct Vertex { // Unit circle local space position (.xy) and AA offset (.z) SkV3 fPosition; }; static constexpr int kVertexCount = 18; static void write_vertex_buffer(VertexWriter writer) { // Normalized geometry for octagons that circumscribe/inscribe a unit circle. // Outer ring offset static constexpr float kOctOffset = 0.41421356237f; // sqrt(2) - 1 // Inner ring points static constexpr SkScalar kCosPi8 = 0.923579533f; static constexpr SkScalar kSinPi8 = 0.382683432f; // Directional offset for anti-aliasing. // Also used as marker for whether this is an outer or inner vertex. static constexpr float kOuterAAOffset = 0.5f; static constexpr float kInnerAAOffset = -0.5f; static constexpr SkV3 kOctagonVertices[kVertexCount] = { {-kOctOffset, -1, kOuterAAOffset}, {-kSinPi8, -kCosPi8, kInnerAAOffset}, { kOctOffset, -1, kOuterAAOffset}, {kSinPi8, -kCosPi8, kInnerAAOffset}, { 1, -kOctOffset, kOuterAAOffset}, {kCosPi8, -kSinPi8, kInnerAAOffset}, { 1, kOctOffset, kOuterAAOffset}, {kCosPi8, kSinPi8, kInnerAAOffset}, { kOctOffset, 1, kOuterAAOffset}, {kSinPi8, kCosPi8, kInnerAAOffset}, {-kOctOffset, 1, kOuterAAOffset}, {-kSinPi8, kCosPi8, kInnerAAOffset}, {-1, kOctOffset, kOuterAAOffset}, {-kCosPi8, kSinPi8, kInnerAAOffset}, {-1, -kOctOffset, kOuterAAOffset}, {-kCosPi8, -kSinPi8, kInnerAAOffset}, {-kOctOffset, -1, kOuterAAOffset}, {-kSinPi8, -kCosPi8, kInnerAAOffset}, }; if (writer) { writer << kOctagonVertices; } // otherwise static buffer creation failed, so do nothing; Context initialization will fail. } CircularArcRenderStep::CircularArcRenderStep(StaticBufferManager* bufferManager) : RenderStep("CircularArcRenderStep", "", Flags::kPerformsShading | Flags::kEmitsCoverage | Flags::kOutsetBoundsForAA, /*uniforms=*/{}, PrimitiveType::kTriangleStrip, kDirectDepthGreaterPass, /*vertexAttrs=*/{ {"position", VertexAttribType::kFloat3, SkSLType::kFloat3}, }, /*instanceAttrs=*/{ // Center plus radii, used to transform to local position {"centerScales", VertexAttribType::kFloat4, SkSLType::kFloat4}, // Outer (device space) and inner (normalized) radii // + flags for determining clipping and roundcaps {"radiiAndFlags", VertexAttribType::kFloat3, SkSLType::kFloat3}, // Clips the geometry for acute arcs {"geoClipPlane", VertexAttribType::kFloat3, SkSLType::kFloat3}, // Clip planes sent to the fragment shader for arc extents {"fragClipPlane0", VertexAttribType::kFloat3, SkSLType::kFloat3}, {"fragClipPlane1", VertexAttribType::kFloat3, SkSLType::kFloat3}, // Roundcap positions, if needed {"inRoundCapPos", VertexAttribType::kFloat4, SkSLType::kFloat4}, {"depth", VertexAttribType::kFloat, SkSLType::kFloat}, {"ssboIndices", VertexAttribType::kUInt2, SkSLType::kUInt2}, {"mat0", VertexAttribType::kFloat3, SkSLType::kFloat3}, {"mat1", VertexAttribType::kFloat3, SkSLType::kFloat3}, {"mat2", VertexAttribType::kFloat3, SkSLType::kFloat3}, }, /*varyings=*/{ // Normalized offset vector plus radii {"circleEdge", SkSLType::kFloat4}, // Half-planes used to clip to arc shape. {"clipPlane", SkSLType::kFloat3}, {"isectPlane", SkSLType::kFloat3}, {"unionPlane", SkSLType::kFloat3}, // Roundcap data {"roundCapRadius", SkSLType::kFloat}, {"roundCapPos", SkSLType::kFloat4}, }) { // Initialize the static buffer we'll use when recording draw calls. // NOTE: Each instance of this RenderStep gets its own copy of the data. Since there should only // ever be one CircularArcRenderStep at a time, this shouldn't be an issue. write_vertex_buffer(bufferManager->getVertexWriter(sizeof(Vertex) * kVertexCount, &fVertexBuffer)); } CircularArcRenderStep::~CircularArcRenderStep() {} std::string CircularArcRenderStep::vertexSkSL() const { // Returns the body of a vertex function, which must define a float4 devPosition variable and // must write to an already-defined float2 stepLocalCoords variable. return "float4 devPosition = circular_arc_vertex_fn(" // Vertex Attributes "position, " // Instance Attributes "centerScales, radiiAndFlags, geoClipPlane, fragClipPlane0, fragClipPlane1, " "inRoundCapPos, depth, float3x3(mat0, mat1, mat2), " // Varyings "circleEdge, clipPlane, isectPlane, unionPlane, " "roundCapRadius, roundCapPos, " // Render Step "stepLocalCoords);\n"; } const char* CircularArcRenderStep::fragmentCoverageSkSL() const { // The returned SkSL must write its coverage into a 'half4 outputCoverage' variable (defined in // the calling code) with the actual coverage splatted out into all four channels. return "outputCoverage = circular_arc_coverage_fn(circleEdge, " "clipPlane, " "isectPlane, " "unionPlane, " "roundCapRadius, " "roundCapPos);"; } void CircularArcRenderStep::writeVertices(DrawWriter* writer, const DrawParams& params, skvx::uint2 ssboIndices) const { SkASSERT(params.geometry().isShape() && params.geometry().shape().isArc()); DrawWriter::Instances instance{*writer, fVertexBuffer, {}, kVertexCount}; auto vw = instance.append(1); const Shape& shape = params.geometry().shape(); const SkArc& arc = shape.arc(); SkPoint localCenter = arc.oval().center(); float localOuterRadius = arc.oval().width() / 2; float localInnerRadius = 0.0f; // We know that we have a similarity matrix so this will transform radius to device space const Transform& transform = params.transform(); float radius = localOuterRadius * transform.maxScaleFactor(); bool isStroke = params.isStroke(); float innerRadius = -SK_ScalarHalf; float outerRadius = radius; float halfWidth = 0; if (isStroke) { float localHalfWidth = params.strokeStyle().halfWidth(); halfWidth = localHalfWidth * transform.maxScaleFactor(); if (SkScalarNearlyZero(halfWidth)) { halfWidth = SK_ScalarHalf; // Need to map this back to local space localHalfWidth = halfWidth / transform.maxScaleFactor(); } outerRadius += halfWidth; innerRadius = radius - halfWidth; localInnerRadius = localOuterRadius - localHalfWidth; localOuterRadius += localHalfWidth; } // The radii are outset for two reasons. First, it allows the shader to simply perform // simpler computation because the computed alpha is zero, rather than 50%, at the radius. // Second, the outer radius is used to compute the verts of the bounding box that is // rendered and the outset ensures the box will cover all partially covered by the circle. outerRadius += SK_ScalarHalf; innerRadius -= SK_ScalarHalf; // The shader operates in a space where the circle is translated to be centered at the // origin. Here we compute points on the unit circle at the starting and ending angles. SkV2 localPoints[3]; float startAngleRadians = SkDegreesToRadians(arc.startAngle()); float sweepAngleRadians = SkDegreesToRadians(arc.sweepAngle()); localPoints[0].y = SkScalarSin(startAngleRadians); localPoints[0].x = SkScalarCos(startAngleRadians); SkScalar endAngle = startAngleRadians + sweepAngleRadians; localPoints[1].y = SkScalarSin(endAngle); localPoints[1].x = SkScalarCos(endAngle); localPoints[2] = {0, 0}; // Adjust the start and end points based on the view matrix (to handle rotated arcs) SkV4 devPoints[3]; transform.mapPoints(localPoints, devPoints, 3); // Translate the point relative to the transformed origin SkV2 startPoint = {devPoints[0].x - devPoints[2].x, devPoints[0].y - devPoints[2].y}; SkV2 stopPoint = {devPoints[1].x - devPoints[2].x, devPoints[1].y - devPoints[2].y}; startPoint = startPoint.normalize(); stopPoint = stopPoint.normalize(); // We know the matrix is a similarity here. Detect mirroring which will affect how we // should orient the clip planes for arcs. const SkM44& m = transform.matrix(); auto upperLeftDet = m.rc(0,0) * m.rc(1,1) - m.rc(0,1) * m.rc(1,0); if (upperLeftDet < 0) { std::swap(startPoint, stopPoint); } // Like a fill without useCenter, butt-cap stroke can be implemented by clipping against // radial lines. We treat round caps the same way, but track coverage of circles at the // center of the butts. // However, in both cases we have to be careful about the half-circle. // case. In that case the two radial lines are equal and so that edge gets clipped // twice. Since the shared edge goes through the center we fall back on the !useCenter // case. auto absSweep = SkScalarAbs(sweepAngleRadians); bool useCenter = (arc.isWedge() || isStroke) && !SkScalarNearlyEqual(absSweep, SK_ScalarPI); // This makes every point fully inside the plane. SkV3 geoClipPlane = {0.f, 0.f, 1.f}; SkV3 clipPlane0; SkV3 clipPlane1; SkV2 roundCapPos0 = {0, 0}; SkV2 roundCapPos1 = {0, 0}; static constexpr float kIntersection_NoRoundCaps = 1; static constexpr float kIntersection_RoundCaps = 2; // Default to intersection and no round caps. float flags = kIntersection_NoRoundCaps; // Determine if we need round caps. if (isStroke && innerRadius > -SK_ScalarHalf && params.strokeStyle().halfWidth() > 0 && params.strokeStyle().cap() == SkPaint::kRound_Cap) { // Compute the cap center points in the normalized space. float midRadius = (innerRadius + outerRadius) / (2 * outerRadius); roundCapPos0 = startPoint * midRadius; roundCapPos1 = stopPoint * midRadius; flags = kIntersection_RoundCaps; } // Determine clip planes. if (useCenter) { SkV2 norm0 = {startPoint.y, -startPoint.x}; SkV2 norm1 = {stopPoint.y, -stopPoint.x}; // This ensures that norm0 is always the clockwise plane, and norm1 is CCW. if (sweepAngleRadians < 0) { std::swap(norm0, norm1); } norm0 = -norm0; clipPlane0 = {norm0.x, norm0.y, 0.5f}; clipPlane1 = {norm1.x, norm1.y, 0.5f}; if (absSweep > SK_ScalarPI) { // Union flags = -flags; } else { // Intersection // Highly acute arc. We need to clip the vertices to the perpendicular half-plane. if (!isStroke && absSweep < 0.5f*SK_ScalarPI) { // We do this clipping in normalized space so use our original local points. // Should already be normalized since they're sin/cos. SkV2 localNorm0 = {localPoints[0].y, -localPoints[0].x}; SkV2 localNorm1 = {localPoints[1].y, -localPoints[1].x}; // This ensures that norm0 is always the clockwise plane, and norm1 is CCW. if (sweepAngleRadians < 0) { std::swap(localNorm0, localNorm1); } // Negate norm0 and compute the perpendicular of the difference SkV2 clipNorm = {-localNorm0.y - localNorm1.y, localNorm1.x + localNorm0.x}; clipNorm = clipNorm.normalize(); // This should give us 1/2 pixel spacing from the half-plane // after transforming from normalized to local to device space. float dist = 0.5f / radius / transform.maxScaleFactor(); geoClipPlane = {clipNorm.x, clipNorm.y, dist}; } } } else { // We clip to a secant of the original circle, only one clip plane startPoint *= radius; stopPoint *= radius; SkV2 norm = {startPoint.y - stopPoint.y, stopPoint.x - startPoint.x}; norm = norm.normalize(); if (sweepAngleRadians > 0) { norm = -norm; } float d = -norm.dot(startPoint) + 0.5f; clipPlane0 = {norm.x, norm.y, d}; clipPlane1 = {0.f, 0.f, 1.f}; // no clipping } // The inner radius in the vertex data must be specified in normalized space. innerRadius = innerRadius / outerRadius; vw << localCenter << localOuterRadius << localInnerRadius << outerRadius << innerRadius << flags << geoClipPlane << clipPlane0 << clipPlane1 << roundCapPos0 << roundCapPos1 << params.order().depthAsFloat() << ssboIndices << m.rc(0,0) << m.rc(1,0) << m.rc(3,0) // mat0 << m.rc(0,1) << m.rc(1,1) << m.rc(3,1) // mat1 << m.rc(0,3) << m.rc(1,3) << m.rc(3,3); // mat2 } void CircularArcRenderStep::writeUniformsAndTextures(const DrawParams&, PipelineDataGatherer*) const { // All data is uploaded as instance attributes, so no uniforms are needed. } } // namespace skgpu::graphite