| /* | |
| * 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/SkPoint.h" | |
| #include "include/core/SkRect.h" | |
| #include "include/core/SkScalar.h" | |
| #include "include/private/SkAssert.h" | |
| #include "include/private/SkDebug.h" | |
| #include "include/private/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/PipelineData.h" | |
| #include "src/gpu/graphite/geom/Geometry.h" | |
| #include "src/gpu/graphite/geom/Shape.h" | |
| #include "src/gpu/graphite/geom/Transform.h" | |
| #include "src/gpu/graphite/render/CommonDepthStencilSettings.h" | |
| #include <utility> | |
| // 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. | |
| } | |
| // NOTE: A circular arc with round caps can trigger can overlap when it's not quite a 360 | |
| // degree arc and the stroke width is sufficient for the caps to overlap. However, this is handled | |
| // in the fragment shader and the emitted triangles do not produce self intersections. | |
| CircularArcRenderStep::CircularArcRenderStep(Layout layout, StaticBufferManager* bufferManager) | |
| : RenderStep(layout, | |
| RenderStepID::kCircularArc, | |
| Flags::kPerformsShading | Flags::kEmitsCoverage | |
| | Flags::kOutsetBoundsForAA | |
| | Flags::kAppendInstances | |
| | Flags::kNoSelfIntersections, | |
| /*uniforms=*/{}, | |
| PrimitiveType::kTriangleStrip, | |
| /*stencilSettings=*/{}, | |
| /*staticAttrs=*/{{ | |
| {"position", VertexAttribType::kFloat3, SkSLType::kFloat3}, | |
| }}, | |
| /*appendAttrs=*/{{ | |
| // 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}, | |
| {"inRoundCapRadius", VertexAttribType::kFloat, SkSLType::kFloat}, | |
| {"depth", VertexAttribType::kFloat, SkSLType::kFloat}, | |
| {"ssboIndex", VertexAttribType::kUInt, SkSLType::kUInt}, | |
| {"mat0", VertexAttribType::kFloat3, SkSLType::kFloat3}, | |
| {"mat1", VertexAttribType::kFloat3, SkSLType::kFloat3}, | |
| {"mat2", VertexAttribType::kFloat3, SkSLType::kFloat3}, | |
| }}, | |
| /*storageUniforms=*/{}, | |
| /*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(kVertexCount, sizeof(Vertex), | |
| &fVertexBuffer)); | |
| } | |
| CircularArcRenderStep::~CircularArcRenderStep() {} | |
| std::string CircularArcRenderStep::vertexSkSL(const RootNodesInfo&) 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(" | |
| // Static Data Attributes | |
| "position, " | |
| // Append Data Attributes | |
| "centerScales, radiiAndFlags, geoClipPlane, fragClipPlane0, fragClipPlane1, " | |
| "inRoundCapPos, inRoundCapRadius, 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, | |
| StorageContext* /*storageContext*/, | |
| const DrawParams& params, | |
| uint32_t ssboIndex) 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; | |
| float roundCapRadius = 0; | |
| // Default to intersection and no round caps. | |
| float flags = kIntersection_NoRoundCaps; | |
| // Determine if we need round caps. | |
| if (isStroke && | |
| 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; | |
| // Compute the cap radius in the normalized space. | |
| roundCapRadius = (outerRadius - innerRadius) / (2 * outerRadius); | |
| } | |
| // 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 | |
| } | |
| if (isStroke && innerRadius < -SK_ScalarHalf) { | |
| // Reset the inner radius to render a filled arc instead of a stroked arc, as the stroke | |
| // width is greater than or equal to the oval's width. | |
| innerRadius = -SK_ScalarHalf; | |
| localInnerRadius = 0.f; | |
| } | |
| // 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 << roundCapRadius | |
| << params.order().depthAsFloat() | |
| << ssboIndex | |
| << 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* gatherer) const { | |
| // All data is uploaded as instance attributes, so no uniforms are needed. | |
| SkDEBUGCODE(gatherer->checkRewind()); | |
| } | |
| } // namespace skgpu::graphite |