| /* |
| * 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/SkDebug.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/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. |
| } |
| |
| CircularArcRenderStep::CircularArcRenderStep(StaticBufferManager* bufferManager) |
| : RenderStep(RenderStepID::kCircularArc, |
| Flags::kPerformsShading | Flags::kEmitsCoverage | Flags::kOutsetBoundsForAA | |
| Flags::kAppendInstances, |
| /*uniforms=*/{}, |
| PrimitiveType::kTriangleStrip, |
| kDirectDepthLessPass, |
| /*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}, |
| {"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(kVertexCount, sizeof(Vertex), |
| &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(" |
| // 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, |
| 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; |
| |
| 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() |
| << 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* gatherer) const { |
| // All data is uploaded as instance attributes, so no uniforms are needed. |
| SkDEBUGCODE(gatherer->checkRewind()); |
| } |
| |
| } // namespace skgpu::graphite |