xref: /aosp_15_r20/external/skia/src/gpu/graphite/render/CircularArcRenderStep.cpp (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1 /*
2  * Copyright 2024 Google LLC
3  *
4  * Use of this source code is governed by a BSD-style license that can be
5  * found in the LICENSE file.
6  */
7 
8 #include "src/gpu/graphite/render/CircularArcRenderStep.h"
9 
10 #include "include/core/SkArc.h"
11 #include "include/core/SkM44.h"
12 #include "include/core/SkPaint.h"
13 #include "include/core/SkRect.h"
14 #include "include/core/SkScalar.h"
15 #include "include/private/base/SkAssert.h"
16 #include "include/private/base/SkPoint_impl.h"
17 #include "src/base/SkEnumBitMask.h"
18 #include "src/core/SkSLTypeShared.h"
19 #include "src/gpu/BufferWriter.h"
20 #include "src/gpu/graphite/Attribute.h"
21 #include "src/gpu/graphite/BufferManager.h"
22 #include "src/gpu/graphite/DrawOrder.h"
23 #include "src/gpu/graphite/DrawParams.h"
24 #include "src/gpu/graphite/DrawTypes.h"
25 #include "src/gpu/graphite/DrawWriter.h"
26 #include "src/gpu/graphite/geom/Geometry.h"
27 #include "src/gpu/graphite/geom/Shape.h"
28 #include "src/gpu/graphite/geom/Transform_graphite.h"
29 #include "src/gpu/graphite/render/CommonDepthStencilSettings.h"
30 
31 #include <string_view>
32 
33 // This RenderStep is used to render filled circular arcs and stroked circular arcs that
34 // don't include the center. Currently it only supports butt caps but will be extended
35 // to include round caps.
36 //
37 // Each arc is represented by a single instance. The instance attributes are enough to
38 // describe the given arc types without relying on uniforms to define its operation.
39 // The attributes encode shape as follows:
40 
41 // float4 centerScales - used to transform the vertex data into local space.
42 //    The vertex data represents interleaved octagons that are respectively circumscribed
43 //    and inscribed on a unit circle, and have to be transformed into local space.
44 //    So the .xy values here are the center of the arc in local space, and .zw its outer and inner
45 //    radii, respectively. If the vertex is an outer vertex its local position will be computed as
46 //         centerScales.xy + position.xy * centerScales.z
47 //    Otherwise it will be computed as
48 //         centerScales.xy + position.xy * centerScales.w
49 //    We can tell whether a vertex is an outer or inner vertex by looking at the sign
50 //    of its z component. This z value is also used to compute half-pixel anti-aliasing offsets
51 //    once the vertex data is transformed into device space.
52 // float3 radiiAndFlags - in the fragment shader we will pass an offset in unit circle space to
53 //    determine the circle edge and for use for clipping. The .x value here is outerRadius+0.5 and
54 //    will be compared against the unit circle radius (i.e., 1.0) to compute the outer edge. The .y
55 //    value is innerRadius-0.5/outerRadius+0.5 and will be used as the comparison point for the
56 //    inner edge. The .z value is a flag which indicates whether fragClipPlane1 is for intersection
57 //    (+) or for union (-), and whether to set up rounded caps (-2/+2).
58 // float3 geoClipPlane - For very thin acute arcs, because of the 1/2 pixel boundary we can get
59 //    non-clipped artifacts beyond the center of the circle. To solve this, we clip the geometry
60 //    so any rendering doesn't cross that point.
61 
62 // In addition, these values will be passed to the fragment shader:
63 //
64 // float3 fragClipPlane0 - the arc will always be clipped against this half plane, and passed as
65 //    the varying clipPlane.
66 // float3 fragClipPlane1 - for convex/acute arcs, we pass this via the varying isectPlane to clip
67 //    against this and multiply its value by the ClipPlane clip result. For concave/obtuse arcs,
68 //    we pass this via the varying unionPlane which will clip against this and add its value to the
69 //    ClipPlane clip result. This is controlled by the flag value in radiiAndFlags: if the
70 //    flag is > 0, it's passed as isectClip, if it's < 0 it's passed as unionClip. We set default
71 //    values for the alternative clip plane that end up being a null clip.
72 // float  roundCapRadius - this is computed in the vertex shader. If we're using round caps (i.e.,
73 //    if abs(flags) > 1), this will be half the distance between the outer and inner radii.
74 //    Otherwise it will be 0 which will end up zeroing out any round cap calculation.
75 // float4 inRoundCapPos - locations of the centers of the round caps in normalized space. This
76 //    will be all zeroes if not needed.
77 
78 namespace skgpu::graphite {
79 
80 // Represents the per-vertex attributes used in each instance.
81 struct Vertex {
82     // Unit circle local space position (.xy) and AA offset (.z)
83     SkV3 fPosition;
84 };
85 
86 static constexpr int kVertexCount = 18;
87 
write_vertex_buffer(VertexWriter writer)88 static void write_vertex_buffer(VertexWriter writer) {
89     // Normalized geometry for octagons that circumscribe/inscribe a unit circle.
90     // Outer ring offset
91     static constexpr float kOctOffset = 0.41421356237f;  // sqrt(2) - 1
92     // Inner ring points
93     static constexpr SkScalar kCosPi8 = 0.923579533f;
94     static constexpr SkScalar kSinPi8 = 0.382683432f;
95 
96     // Directional offset for anti-aliasing.
97     // Also used as marker for whether this is an outer or inner vertex.
98     static constexpr float kOuterAAOffset = 0.5f;
99     static constexpr float kInnerAAOffset = -0.5f;
100 
101     static constexpr SkV3 kOctagonVertices[kVertexCount] = {
102         {-kOctOffset, -1,          kOuterAAOffset},
103         {-kSinPi8,    -kCosPi8,    kInnerAAOffset},
104         { kOctOffset, -1,          kOuterAAOffset},
105         {kSinPi8,     -kCosPi8,    kInnerAAOffset},
106         { 1,          -kOctOffset, kOuterAAOffset},
107         {kCosPi8,     -kSinPi8,    kInnerAAOffset},
108         { 1,           kOctOffset, kOuterAAOffset},
109         {kCosPi8,      kSinPi8,    kInnerAAOffset},
110         { kOctOffset,  1,          kOuterAAOffset},
111         {kSinPi8,      kCosPi8,    kInnerAAOffset},
112         {-kOctOffset,  1,          kOuterAAOffset},
113         {-kSinPi8,     kCosPi8,    kInnerAAOffset},
114         {-1,           kOctOffset, kOuterAAOffset},
115         {-kCosPi8,     kSinPi8,    kInnerAAOffset},
116         {-1,          -kOctOffset, kOuterAAOffset},
117         {-kCosPi8,    -kSinPi8,    kInnerAAOffset},
118         {-kOctOffset, -1,          kOuterAAOffset},
119         {-kSinPi8,    -kCosPi8,    kInnerAAOffset},
120     };
121 
122     if (writer) {
123         writer << kOctagonVertices;
124     } // otherwise static buffer creation failed, so do nothing; Context initialization will fail.
125 }
126 
CircularArcRenderStep(StaticBufferManager * bufferManager)127 CircularArcRenderStep::CircularArcRenderStep(StaticBufferManager* bufferManager)
128         : RenderStep("CircularArcRenderStep",
129                      "",
130                      Flags::kPerformsShading | Flags::kEmitsCoverage | Flags::kOutsetBoundsForAA,
131                      /*uniforms=*/{},
132                      PrimitiveType::kTriangleStrip,
133                      kDirectDepthGreaterPass,
134                      /*vertexAttrs=*/{
135                              {"position", VertexAttribType::kFloat3, SkSLType::kFloat3},
136                      },
137                      /*instanceAttrs=*/{
138                              // Center plus radii, used to transform to local position
139                              {"centerScales", VertexAttribType::kFloat4, SkSLType::kFloat4},
140                              // Outer (device space) and inner (normalized) radii
141                              // + flags for determining clipping and roundcaps
142                              {"radiiAndFlags", VertexAttribType::kFloat3, SkSLType::kFloat3},
143                              // Clips the geometry for acute arcs
144                              {"geoClipPlane", VertexAttribType::kFloat3, SkSLType::kFloat3},
145                              // Clip planes sent to the fragment shader for arc extents
146                              {"fragClipPlane0", VertexAttribType::kFloat3, SkSLType::kFloat3},
147                              {"fragClipPlane1", VertexAttribType::kFloat3, SkSLType::kFloat3},
148                              // Roundcap positions, if needed
149                              {"inRoundCapPos", VertexAttribType::kFloat4, SkSLType::kFloat4},
150 
151                              {"depth", VertexAttribType::kFloat, SkSLType::kFloat},
152                              {"ssboIndices", VertexAttribType::kUInt2, SkSLType::kUInt2},
153 
154                              {"mat0", VertexAttribType::kFloat3, SkSLType::kFloat3},
155                              {"mat1", VertexAttribType::kFloat3, SkSLType::kFloat3},
156                              {"mat2", VertexAttribType::kFloat3, SkSLType::kFloat3},
157                      },
158                      /*varyings=*/{
159                              // Normalized offset vector plus radii
160                              {"circleEdge", SkSLType::kFloat4},
161                              // Half-planes used to clip to arc shape.
162                              {"clipPlane", SkSLType::kFloat3},
163                              {"isectPlane", SkSLType::kFloat3},
164                              {"unionPlane", SkSLType::kFloat3},
165                              // Roundcap data
166                              {"roundCapRadius", SkSLType::kFloat},
167                              {"roundCapPos", SkSLType::kFloat4},
168                      }) {
169     // Initialize the static buffer we'll use when recording draw calls.
170     // NOTE: Each instance of this RenderStep gets its own copy of the data. Since there should only
171     // ever be one CircularArcRenderStep at a time, this shouldn't be an issue.
172     write_vertex_buffer(bufferManager->getVertexWriter(sizeof(Vertex) * kVertexCount,
173                                                        &fVertexBuffer));
174 }
175 
~CircularArcRenderStep()176 CircularArcRenderStep::~CircularArcRenderStep() {}
177 
vertexSkSL() const178 std::string CircularArcRenderStep::vertexSkSL() const {
179     // Returns the body of a vertex function, which must define a float4 devPosition variable and
180     // must write to an already-defined float2 stepLocalCoords variable.
181     return "float4 devPosition = circular_arc_vertex_fn("
182                    // Vertex Attributes
183                    "position, "
184                    // Instance Attributes
185                    "centerScales, radiiAndFlags, geoClipPlane, fragClipPlane0, fragClipPlane1, "
186                    "inRoundCapPos, depth, float3x3(mat0, mat1, mat2), "
187                    // Varyings
188                    "circleEdge, clipPlane, isectPlane, unionPlane, "
189                    "roundCapRadius, roundCapPos, "
190                    // Render Step
191                    "stepLocalCoords);\n";
192 }
193 
fragmentCoverageSkSL() const194 const char* CircularArcRenderStep::fragmentCoverageSkSL() const {
195     // The returned SkSL must write its coverage into a 'half4 outputCoverage' variable (defined in
196     // the calling code) with the actual coverage splatted out into all four channels.
197     return "outputCoverage = circular_arc_coverage_fn(circleEdge, "
198                                                      "clipPlane, "
199                                                      "isectPlane, "
200                                                      "unionPlane, "
201                                                      "roundCapRadius, "
202                                                      "roundCapPos);";
203 }
204 
writeVertices(DrawWriter * writer,const DrawParams & params,skvx::uint2 ssboIndices) const205 void CircularArcRenderStep::writeVertices(DrawWriter* writer,
206                                           const DrawParams& params,
207                                           skvx::uint2 ssboIndices) const {
208     SkASSERT(params.geometry().isShape() && params.geometry().shape().isArc());
209 
210     DrawWriter::Instances instance{*writer, fVertexBuffer, {}, kVertexCount};
211     auto vw = instance.append(1);
212 
213     const Shape& shape = params.geometry().shape();
214     const SkArc& arc = shape.arc();
215 
216     SkPoint localCenter = arc.oval().center();
217     float localOuterRadius = arc.oval().width() / 2;
218     float localInnerRadius = 0.0f;
219 
220     // We know that we have a similarity matrix so this will transform radius to device space
221     const Transform& transform = params.transform();
222     float radius = localOuterRadius * transform.maxScaleFactor();
223     bool isStroke = params.isStroke();
224 
225     float innerRadius = -SK_ScalarHalf;
226     float outerRadius = radius;
227     float halfWidth = 0;
228     if (isStroke) {
229         float localHalfWidth = params.strokeStyle().halfWidth();
230 
231         halfWidth = localHalfWidth * transform.maxScaleFactor();
232         if (SkScalarNearlyZero(halfWidth)) {
233             halfWidth = SK_ScalarHalf;
234             // Need to map this back to local space
235             localHalfWidth = halfWidth / transform.maxScaleFactor();
236         }
237 
238         outerRadius += halfWidth;
239         innerRadius = radius - halfWidth;
240         localInnerRadius = localOuterRadius - localHalfWidth;
241         localOuterRadius += localHalfWidth;
242     }
243 
244     // The radii are outset for two reasons. First, it allows the shader to simply perform
245     // simpler computation because the computed alpha is zero, rather than 50%, at the radius.
246     // Second, the outer radius is used to compute the verts of the bounding box that is
247     // rendered and the outset ensures the box will cover all partially covered by the circle.
248     outerRadius += SK_ScalarHalf;
249     innerRadius -= SK_ScalarHalf;
250 
251     // The shader operates in a space where the circle is translated to be centered at the
252     // origin. Here we compute points on the unit circle at the starting and ending angles.
253     SkV2 localPoints[3];
254     float startAngleRadians = SkDegreesToRadians(arc.startAngle());
255     float sweepAngleRadians = SkDegreesToRadians(arc.sweepAngle());
256     localPoints[0].y = SkScalarSin(startAngleRadians);
257     localPoints[0].x = SkScalarCos(startAngleRadians);
258     SkScalar endAngle = startAngleRadians + sweepAngleRadians;
259     localPoints[1].y = SkScalarSin(endAngle);
260     localPoints[1].x = SkScalarCos(endAngle);
261     localPoints[2] = {0, 0};
262 
263     // Adjust the start and end points based on the view matrix (to handle rotated arcs)
264     SkV4 devPoints[3];
265     transform.mapPoints(localPoints, devPoints, 3);
266     // Translate the point relative to the transformed origin
267     SkV2 startPoint = {devPoints[0].x - devPoints[2].x, devPoints[0].y - devPoints[2].y};
268     SkV2 stopPoint = {devPoints[1].x - devPoints[2].x, devPoints[1].y - devPoints[2].y};
269     startPoint = startPoint.normalize();
270     stopPoint = stopPoint.normalize();
271 
272     // We know the matrix is a similarity here. Detect mirroring which will affect how we
273     // should orient the clip planes for arcs.
274     const SkM44& m = transform.matrix();
275     auto upperLeftDet = m.rc(0,0) * m.rc(1,1) -
276                         m.rc(0,1) * m.rc(1,0);
277     if (upperLeftDet < 0) {
278         std::swap(startPoint, stopPoint);
279     }
280 
281     // Like a fill without useCenter, butt-cap stroke can be implemented by clipping against
282     // radial lines. We treat round caps the same way, but track coverage of circles at the
283     // center of the butts.
284     // However, in both cases we have to be careful about the half-circle.
285     // case. In that case the two radial lines are equal and so that edge gets clipped
286     // twice. Since the shared edge goes through the center we fall back on the !useCenter
287     // case.
288     auto absSweep = SkScalarAbs(sweepAngleRadians);
289     bool useCenter = (arc.isWedge() || isStroke) &&
290                      !SkScalarNearlyEqual(absSweep, SK_ScalarPI);
291 
292     // This makes every point fully inside the plane.
293     SkV3 geoClipPlane = {0.f, 0.f, 1.f};
294     SkV3 clipPlane0;
295     SkV3 clipPlane1;
296     SkV2 roundCapPos0 = {0, 0};
297     SkV2 roundCapPos1 = {0, 0};
298     static constexpr float kIntersection_NoRoundCaps = 1;
299     static constexpr float kIntersection_RoundCaps = 2;
300 
301     // Default to intersection and no round caps.
302     float flags = kIntersection_NoRoundCaps;
303     // Determine if we need round caps.
304     if (isStroke && innerRadius > -SK_ScalarHalf &&
305         params.strokeStyle().halfWidth() > 0 &&
306         params.strokeStyle().cap() == SkPaint::kRound_Cap) {
307         // Compute the cap center points in the normalized space.
308         float midRadius = (innerRadius + outerRadius) / (2 * outerRadius);
309         roundCapPos0 = startPoint * midRadius;
310         roundCapPos1 = stopPoint * midRadius;
311         flags = kIntersection_RoundCaps;
312     }
313 
314     // Determine clip planes.
315     if (useCenter) {
316         SkV2 norm0 = {startPoint.y, -startPoint.x};
317         SkV2 norm1 = {stopPoint.y, -stopPoint.x};
318         // This ensures that norm0 is always the clockwise plane, and norm1 is CCW.
319         if (sweepAngleRadians < 0) {
320             std::swap(norm0, norm1);
321         }
322         norm0 = -norm0;
323         clipPlane0 = {norm0.x, norm0.y, 0.5f};
324         clipPlane1 = {norm1.x, norm1.y, 0.5f};
325         if (absSweep > SK_ScalarPI) {
326             // Union
327             flags = -flags;
328         } else {
329             // Intersection
330             // Highly acute arc. We need to clip the vertices to the perpendicular half-plane.
331             if (!isStroke && absSweep < 0.5f*SK_ScalarPI) {
332                 // We do this clipping in normalized space so use our original local points.
333                 // Should already be normalized since they're sin/cos.
334                 SkV2 localNorm0 = {localPoints[0].y, -localPoints[0].x};
335                 SkV2 localNorm1 = {localPoints[1].y, -localPoints[1].x};
336                 // This ensures that norm0 is always the clockwise plane, and norm1 is CCW.
337                 if (sweepAngleRadians < 0) {
338                     std::swap(localNorm0, localNorm1);
339                 }
340                 // Negate norm0 and compute the perpendicular of the difference
341                 SkV2 clipNorm = {-localNorm0.y - localNorm1.y, localNorm1.x + localNorm0.x};
342                 clipNorm = clipNorm.normalize();
343                 // This should give us 1/2 pixel spacing from the half-plane
344                 // after transforming from normalized to local to device space.
345                 float dist = 0.5f / radius / transform.maxScaleFactor();
346                 geoClipPlane = {clipNorm.x, clipNorm.y, dist};
347             }
348         }
349     } else {
350         // We clip to a secant of the original circle, only one clip plane
351         startPoint *= radius;
352         stopPoint *= radius;
353         SkV2 norm = {startPoint.y - stopPoint.y, stopPoint.x - startPoint.x};
354         norm = norm.normalize();
355         if (sweepAngleRadians > 0) {
356             norm = -norm;
357         }
358         float d = -norm.dot(startPoint) + 0.5f;
359         clipPlane0 = {norm.x, norm.y, d};
360         clipPlane1 = {0.f, 0.f, 1.f}; // no clipping
361     }
362 
363     // The inner radius in the vertex data must be specified in normalized space.
364     innerRadius = innerRadius / outerRadius;
365 
366     vw << localCenter << localOuterRadius << localInnerRadius
367        << outerRadius << innerRadius << flags
368        << geoClipPlane << clipPlane0 << clipPlane1
369        << roundCapPos0 << roundCapPos1
370        << params.order().depthAsFloat()
371        << ssboIndices
372        << m.rc(0,0) << m.rc(1,0) << m.rc(3,0)  // mat0
373        << m.rc(0,1) << m.rc(1,1) << m.rc(3,1)  // mat1
374        << m.rc(0,3) << m.rc(1,3) << m.rc(3,3); // mat2
375 }
376 
writeUniformsAndTextures(const DrawParams &,PipelineDataGatherer *) const377 void CircularArcRenderStep::writeUniformsAndTextures(const DrawParams&,
378                                                      PipelineDataGatherer*) const {
379     // All data is uploaded as instance attributes, so no uniforms are needed.
380 }
381 
382 }  // namespace skgpu::graphite
383