1 /*
2 * Copyright 2019 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/ops/PathInnerTriangulateOp.h"
9
10 #include "src/gpu/GrEagerVertexAllocator.h"
11 #include "src/gpu/GrGpu.h"
12 #include "src/gpu/GrOpFlushState.h"
13 #include "src/gpu/GrRecordingContextPriv.h"
14 #include "src/gpu/GrResourceProvider.h"
15 #include "src/gpu/glsl/GrGLSLVertexGeoBuilder.h"
16 #include "src/gpu/tessellate/PathCurveTessellator.h"
17 #include "src/gpu/tessellate/shaders/GrPathTessellationShader.h"
18
19 namespace {
20
21 // Fills an array of convex hulls surrounding 4-point cubic or conic instances. This shader is used
22 // for the "cover" pass after the curves have been fully stencilled.
23 class HullShader : public GrPathTessellationShader {
24 public:
HullShader(const SkMatrix & viewMatrix,SkPMColor4f color,const GrShaderCaps & shaderCaps)25 HullShader(const SkMatrix& viewMatrix, SkPMColor4f color, const GrShaderCaps& shaderCaps)
26 : GrPathTessellationShader(kTessellate_HullShader_ClassID,
27 GrPrimitiveType::kTriangleStrip, 0, viewMatrix, color,
28 skgpu::PatchAttribs::kNone) {
29 fInstanceAttribs.emplace_back("p01", kFloat4_GrVertexAttribType, kFloat4_GrSLType);
30 fInstanceAttribs.emplace_back("p23", kFloat4_GrVertexAttribType, kFloat4_GrSLType);
31 if (!shaderCaps.infinitySupport()) {
32 // A conic curve is written out with p3=[w,Infinity], but GPUs that don't support
33 // infinity can't detect this. On these platforms we also write out an extra float with
34 // each patch that explicitly tells the shader what type of curve it is.
35 fInstanceAttribs.emplace_back("curveType", kFloat_GrVertexAttribType, kFloat_GrSLType);
36 }
37 this->setInstanceAttributes(fInstanceAttribs.data(), fInstanceAttribs.count());
38 SkASSERT(fInstanceAttribs.count() <= kMaxInstanceAttribCount);
39
40 if (!shaderCaps.vertexIDSupport()) {
41 constexpr static Attribute kVertexIdxAttrib("vertexidx", kFloat_GrVertexAttribType,
42 kFloat_GrSLType);
43 this->setVertexAttributes(&kVertexIdxAttrib, 1);
44 }
45 }
46
maxTessellationSegments(const GrShaderCaps &) const47 int maxTessellationSegments(const GrShaderCaps&) const override { SkUNREACHABLE; }
48
49 private:
name() const50 const char* name() const final { return "tessellate_HullShader"; }
getShaderDfxInfo() const51 SkString getShaderDfxInfo() const override { return SkString("ShaderDfx_HullShader"); }
addToKey(const GrShaderCaps &,GrProcessorKeyBuilder *) const52 void addToKey(const GrShaderCaps&, GrProcessorKeyBuilder*) const final {}
53 std::unique_ptr<ProgramImpl> makeProgramImpl(const GrShaderCaps&) const final;
54
55 constexpr static int kMaxInstanceAttribCount = 3;
56 SkSTArray<kMaxInstanceAttribCount, Attribute> fInstanceAttribs;
57 };
58
makeProgramImpl(const GrShaderCaps &) const59 std::unique_ptr<GrGeometryProcessor::ProgramImpl> HullShader::makeProgramImpl(
60 const GrShaderCaps&) const {
61 class Impl : public GrPathTessellationShader::Impl {
62 void emitVertexCode(const GrShaderCaps& shaderCaps,
63 const GrPathTessellationShader&,
64 GrGLSLVertexBuilder* v,
65 GrGLSLVaryingHandler*,
66 GrGPArgs* gpArgs) override {
67 if (shaderCaps.infinitySupport()) {
68 v->insertFunction(R"(
69 bool is_conic_curve() { return isinf(p23.w); }
70 bool is_non_triangular_conic_curve() {
71 // We consider a conic non-triangular as long as its weight isn't infinity.
72 // NOTE: "isinf == false" works on Mac Radeon GLSL; "!isinf" can get the wrong
73 // answer.
74 return isinf(p23.z) == false;
75 })");
76 } else {
77 v->insertFunction(SkStringPrintf(R"(
78 bool is_conic_curve() { return curveType != %g; })", kCubicCurveType).c_str());
79 v->insertFunction(SkStringPrintf(R"(
80 bool is_non_triangular_conic_curve() {
81 return curveType == %g;
82 })", kConicCurveType).c_str());
83 }
84 v->codeAppend(R"(
85 float2 p0=p01.xy, p1=p01.zw, p2=p23.xy, p3=p23.zw;
86 if (is_conic_curve()) {
87 // Conics are 3 points, with the weight in p3.
88 float w = p3.x;
89 p3 = p2; // Duplicate the endpoint for shared code that also runs on cubics.
90 if (is_non_triangular_conic_curve()) {
91 // Convert the points to a trapeziodal hull that circumcscribes the conic.
92 float2 p1w = p1 * w;
93 float T = .51; // Bias outward a bit to ensure we cover the outermost samples.
94 float2 c1 = mix(p0, p1w, T);
95 float2 c2 = mix(p2, p1w, T);
96 float iw = 1 / mix(1, w, T);
97 p2 = c2 * iw;
98 p1 = c1 * iw;
99 }
100 }
101
102 // Translate the points to v0..3 where v0=0.
103 float2 v1 = p1 - p0;
104 float2 v2 = p2 - p0;
105 float2 v3 = p3 - p0;
106
107 // Reorder the points so v2 bisects v1 and v3.
108 if (sign(cross(v2, v1)) == sign(cross(v2, v3))) {
109 float2 tmp = p2;
110 if (sign(cross(v1, v2)) != sign(cross(v1, v3))) {
111 p2 = p1; // swap(p2, p1)
112 p1 = tmp;
113 } else {
114 p2 = p3; // swap(p2, p3)
115 p3 = tmp;
116 }
117 })");
118
119 if (shaderCaps.vertexIDSupport()) {
120 // If we don't have sk_VertexID support then "vertexidx" already came in as a
121 // vertex attrib.
122 v->codeAppend(R"(
123 // sk_VertexID comes in fan order. Convert to strip order.
124 int vertexidx = sk_VertexID;
125 vertexidx ^= vertexidx >> 1;)");
126 }
127
128 v->codeAppend(R"(
129 // Find the "turn direction" of each corner and net turn direction.
130 float vertexdir = 0;
131 float netdir = 0;
132 float2 prev, next;
133 float dir;
134 float2 localcoord;
135 float2 nextcoord;)");
136
137 for (int i = 0; i < 4; ++i) {
138 v->codeAppendf(R"(
139 prev = p%i - p%i;)", i, (i + 3) % 4);
140 v->codeAppendf(R"(
141 next = p%i - p%i;)", (i + 1) % 4, i);
142 v->codeAppendf(R"(
143 dir = sign(cross(prev, next));
144 if (vertexidx == %i) {
145 vertexdir = dir;
146 localcoord = p%i;
147 nextcoord = p%i;
148 }
149 netdir += dir;)", i, i, (i + 1) % 4);
150 }
151
152 v->codeAppend(R"(
153 // Remove the non-convex vertex, if any.
154 if (vertexdir != sign(netdir)) {
155 localcoord = nextcoord;
156 }
157
158 float2 vertexpos = AFFINE_MATRIX * localcoord + TRANSLATE;)");
159 gpArgs->fLocalCoordVar.set(kFloat2_GrSLType, "localcoord");
160 gpArgs->fPositionVar.set(kFloat2_GrSLType, "vertexpos");
161 }
162 };
163 return std::make_unique<Impl>();
164 }
165
166 } // anonymous namespace
167
168 namespace skgpu::v1 {
169
visitProxies(const GrVisitProxyFunc & func) const170 void PathInnerTriangulateOp::visitProxies(const GrVisitProxyFunc& func) const {
171 if (fPipelineForFills) {
172 fPipelineForFills->visitProxies(func);
173 } else {
174 fProcessors.visitProxies(func);
175 }
176 }
177
fixedFunctionFlags() const178 GrDrawOp::FixedFunctionFlags PathInnerTriangulateOp::fixedFunctionFlags() const {
179 auto flags = FixedFunctionFlags::kUsesStencil;
180 if (GrAAType::kNone != fAAType) {
181 flags |= FixedFunctionFlags::kUsesHWAA;
182 }
183 return flags;
184 }
185
finalize(const GrCaps & caps,const GrAppliedClip * clip,GrClampType clampType)186 GrProcessorSet::Analysis PathInnerTriangulateOp::finalize(const GrCaps& caps,
187 const GrAppliedClip* clip,
188 GrClampType clampType) {
189 return fProcessors.finalize(fColor, GrProcessorAnalysisCoverage::kNone, clip, nullptr, caps,
190 clampType, &fColor);
191 }
192
pushFanStencilProgram(const GrTessellationShader::ProgramArgs & args,const GrPipeline * pipelineForStencils,const GrUserStencilSettings * stencil)193 void PathInnerTriangulateOp::pushFanStencilProgram(const GrTessellationShader::ProgramArgs& args,
194 const GrPipeline* pipelineForStencils,
195 const GrUserStencilSettings* stencil) {
196 SkASSERT(pipelineForStencils);
197 auto shader = GrPathTessellationShader::MakeSimpleTriangleShader(args.fArena, fViewMatrix,
198 SK_PMColor4fTRANSPARENT);
199 fFanPrograms.push_back(GrTessellationShader::MakeProgram(args, shader, pipelineForStencils,
200 stencil)); }
201
pushFanFillProgram(const GrTessellationShader::ProgramArgs & args,const GrUserStencilSettings * stencil)202 void PathInnerTriangulateOp::pushFanFillProgram(const GrTessellationShader::ProgramArgs& args,
203 const GrUserStencilSettings* stencil) {
204 SkASSERT(fPipelineForFills);
205 auto shader = GrPathTessellationShader::MakeSimpleTriangleShader(args.fArena, fViewMatrix,
206 fColor);
207 fFanPrograms.push_back(GrTessellationShader::MakeProgram(args, shader, fPipelineForFills,
208 stencil));
209 }
210
prePreparePrograms(const GrTessellationShader::ProgramArgs & args,GrAppliedClip && appliedClip)211 void PathInnerTriangulateOp::prePreparePrograms(const GrTessellationShader::ProgramArgs& args,
212 GrAppliedClip&& appliedClip) {
213 SkASSERT(!fFanTriangulator);
214 SkASSERT(!fFanPolys);
215 SkASSERT(!fPipelineForFills);
216 SkASSERT(!fTessellator);
217 SkASSERT(!fStencilCurvesProgram);
218 SkASSERT(fFanPrograms.empty());
219 SkASSERT(!fCoverHullsProgram);
220
221 if (fPath.countVerbs() <= 0) {
222 return;
223 }
224
225 // If using wireframe, we have to fall back on a standard Redbook "stencil then cover" algorithm
226 // instead of bypassing the stencil buffer to fill the fan directly.
227 bool forceRedbookStencilPass =
228 (fPathFlags & (FillPathFlags::kStencilOnly | FillPathFlags::kWireframe));
229 bool doFill = !(fPathFlags & FillPathFlags::kStencilOnly);
230
231 bool isLinear;
232 fFanTriangulator = args.fArena->make<GrInnerFanTriangulator>(fPath, args.fArena);
233 fFanPolys = fFanTriangulator->pathToPolys(&fFanBreadcrumbs, &isLinear);
234
235 // Create a pipeline for stencil passes if needed.
236 const GrPipeline* pipelineForStencils = nullptr;
237 if (forceRedbookStencilPass || !isLinear) { // Curves always get stencilled.
238 auto pipelineFlags = (fPathFlags & FillPathFlags::kWireframe)
239 ? GrPipeline::InputFlags::kWireframe
240 : GrPipeline::InputFlags::kNone;
241 pipelineForStencils = GrPathTessellationShader::MakeStencilOnlyPipeline(
242 args, fAAType, appliedClip.hardClip(), pipelineFlags);
243 }
244
245 // Create a pipeline for fill passes if needed.
246 if (doFill) {
247 fPipelineForFills = GrTessellationShader::MakePipeline(args, fAAType,
248 std::move(appliedClip),
249 std::move(fProcessors));
250 }
251
252 // Pass 1: Tessellate the outer curves into the stencil buffer.
253 if (!isLinear) {
254 fTessellator = PathCurveTessellator::Make(args.fArena,
255 args.fCaps->shaderCaps()->infinitySupport());
256 auto* tessShader = GrPathTessellationShader::Make(args.fArena,
257 fViewMatrix,
258 SK_PMColor4fTRANSPARENT,
259 fPath.countVerbs(),
260 *pipelineForStencils,
261 fTessellator->patchAttribs(),
262 *args.fCaps);
263 const GrUserStencilSettings* stencilPathSettings =
264 GrPathTessellationShader::StencilPathSettings(GrFillRuleForSkPath(fPath));
265 fStencilCurvesProgram = GrTessellationShader::MakeProgram(args,
266 tessShader,
267 pipelineForStencils,
268 stencilPathSettings);
269 }
270
271 // Pass 2: Fill the path's inner fan with a stencil test against the curves.
272 if (fFanPolys) {
273 if (forceRedbookStencilPass) {
274 // Use a standard Redbook "stencil then cover" algorithm instead of bypassing the
275 // stencil buffer to fill the fan directly.
276 const GrUserStencilSettings* stencilPathSettings =
277 GrPathTessellationShader::StencilPathSettings(GrFillRuleForSkPath(fPath));
278 this->pushFanStencilProgram(args, pipelineForStencils, stencilPathSettings);
279 if (doFill) {
280 this->pushFanFillProgram(args,
281 GrPathTessellationShader::TestAndResetStencilSettings());
282 }
283 } else if (isLinear) {
284 // There are no outer curves! Ignore stencil and fill the path directly.
285 SkASSERT(!pipelineForStencils);
286 this->pushFanFillProgram(args, &GrUserStencilSettings::kUnused);
287 } else if (!fPipelineForFills->hasStencilClip()) {
288 // These are a twist on the standard Redbook stencil settings that allow us to fill the
289 // inner polygon directly to the final render target. By the time these programs
290 // execute, the outer curves will already be stencilled in. So if the stencil value is
291 // zero, then it means the sample in question is not affected by any curves and we can
292 // fill it in directly. If the stencil value is nonzero, then we don't fill and instead
293 // continue the standard Redbook counting process.
294 constexpr static GrUserStencilSettings kFillOrIncrDecrStencil(
295 GrUserStencilSettings::StaticInitSeparate<
296 0x0000, 0x0000,
297 GrUserStencilTest::kEqual, GrUserStencilTest::kEqual,
298 0xffff, 0xffff,
299 GrUserStencilOp::kKeep, GrUserStencilOp::kKeep,
300 GrUserStencilOp::kIncWrap, GrUserStencilOp::kDecWrap,
301 0xffff, 0xffff>());
302
303 constexpr static GrUserStencilSettings kFillOrInvertStencil(
304 GrUserStencilSettings::StaticInit<
305 0x0000,
306 GrUserStencilTest::kEqual,
307 0xffff,
308 GrUserStencilOp::kKeep,
309 // "Zero" instead of "Invert" because the fan only touches any given pixel once.
310 GrUserStencilOp::kZero,
311 0xffff>());
312
313 auto* stencil = (fPath.getFillType() == SkPathFillType::kWinding)
314 ? &kFillOrIncrDecrStencil
315 : &kFillOrInvertStencil;
316 this->pushFanFillProgram(args, stencil);
317 } else {
318 // This is the same idea as above, but we use two passes instead of one because there is
319 // a stencil clip. The stencil test isn't expressive enough to do the above tests and
320 // also check the clip bit in a single pass.
321 constexpr static GrUserStencilSettings kFillIfZeroAndInClip(
322 GrUserStencilSettings::StaticInit<
323 0x0000,
324 GrUserStencilTest::kEqualIfInClip,
325 0xffff,
326 GrUserStencilOp::kKeep,
327 GrUserStencilOp::kKeep,
328 0xffff>());
329
330 constexpr static GrUserStencilSettings kIncrDecrStencilIfNonzero(
331 GrUserStencilSettings::StaticInitSeparate<
332 0x0000, 0x0000,
333 // No need to check the clip because the previous stencil pass will have only
334 // written to samples already inside the clip.
335 GrUserStencilTest::kNotEqual, GrUserStencilTest::kNotEqual,
336 0xffff, 0xffff,
337 GrUserStencilOp::kIncWrap, GrUserStencilOp::kDecWrap,
338 GrUserStencilOp::kKeep, GrUserStencilOp::kKeep,
339 0xffff, 0xffff>());
340
341 constexpr static GrUserStencilSettings kInvertStencilIfNonZero(
342 GrUserStencilSettings::StaticInit<
343 0x0000,
344 // No need to check the clip because the previous stencil pass will have only
345 // written to samples already inside the clip.
346 GrUserStencilTest::kNotEqual,
347 0xffff,
348 // "Zero" instead of "Invert" because the fan only touches any given pixel once.
349 GrUserStencilOp::kZero,
350 GrUserStencilOp::kKeep,
351 0xffff>());
352
353 // Pass 2a: Directly fill fan samples whose stencil values (from curves) are zero.
354 this->pushFanFillProgram(args, &kFillIfZeroAndInClip);
355
356 // Pass 2b: Redbook counting on fan samples whose stencil values (from curves) != 0.
357 auto* stencil = (fPath.getFillType() == SkPathFillType::kWinding)
358 ? &kIncrDecrStencilIfNonzero
359 : &kInvertStencilIfNonZero;
360 this->pushFanStencilProgram(args, pipelineForStencils, stencil);
361 }
362 }
363
364 // Pass 3: Draw convex hulls around each curve.
365 if (doFill && !isLinear) {
366 // By the time this program executes, every pixel will be filled in except the ones touched
367 // by curves. We issue a final cover pass over the curves by drawing their convex hulls.
368 // This will fill in any remaining samples and reset the stencil values back to zero.
369 SkASSERT(fTessellator);
370 auto* hullShader = args.fArena->make<HullShader>(fViewMatrix, fColor,
371 *args.fCaps->shaderCaps());
372 fCoverHullsProgram = GrTessellationShader::MakeProgram(
373 args, hullShader, fPipelineForFills,
374 GrPathTessellationShader::TestAndResetStencilSettings());
375 }
376 }
377
onPrePrepare(GrRecordingContext * context,const GrSurfaceProxyView & writeView,GrAppliedClip * clip,const GrDstProxyView & dstProxyView,GrXferBarrierFlags renderPassXferBarriers,GrLoadOp colorLoadOp)378 void PathInnerTriangulateOp::onPrePrepare(GrRecordingContext* context,
379 const GrSurfaceProxyView& writeView,
380 GrAppliedClip* clip,
381 const GrDstProxyView& dstProxyView,
382 GrXferBarrierFlags renderPassXferBarriers,
383 GrLoadOp colorLoadOp) {
384 // DMSAA is not supported on DDL.
385 bool usesMSAASurface = writeView.asRenderTargetProxy()->numSamples() > 1;
386 this->prePreparePrograms({context->priv().recordTimeAllocator(), writeView, usesMSAASurface,
387 &dstProxyView, renderPassXferBarriers, colorLoadOp,
388 context->priv().caps()},
389 (clip) ? std::move(*clip) : GrAppliedClip::Disabled());
390 if (fStencilCurvesProgram) {
391 context->priv().recordProgramInfo(fStencilCurvesProgram);
392 }
393 for (const GrProgramInfo* fanProgram : fFanPrograms) {
394 context->priv().recordProgramInfo(fanProgram);
395 }
396 if (fCoverHullsProgram) {
397 context->priv().recordProgramInfo(fCoverHullsProgram);
398 }
399 }
400
401 GR_DECLARE_STATIC_UNIQUE_KEY(gHullVertexBufferKey);
402
onPrepare(GrOpFlushState * flushState)403 void PathInnerTriangulateOp::onPrepare(GrOpFlushState* flushState) {
404 const GrCaps& caps = flushState->caps();
405
406 if (!fFanTriangulator) {
407 this->prePreparePrograms({flushState->allocator(), flushState->writeView(),
408 flushState->usesMSAASurface(), &flushState->dstProxyView(),
409 flushState->renderPassBarriers(), flushState->colorLoadOp(),
410 &caps}, flushState->detachAppliedClip());
411 if (!fFanTriangulator) {
412 return;
413 }
414 }
415
416 if (fFanPolys) {
417 GrEagerDynamicVertexAllocator alloc(flushState, &fFanBuffer, &fBaseFanVertex);
418 fFanVertexCount = fFanTriangulator->polysToTriangles(fFanPolys, &alloc, &fFanBreadcrumbs);
419 }
420
421 if (fTessellator) {
422 int patchPreallocCount = fFanBreadcrumbs.count() +
423 fTessellator->patchPreallocCount(fPath.countVerbs());
424 SkASSERT(patchPreallocCount); // Otherwise fTessellator should be null.
425
426 PatchWriter patchWriter(flushState, fTessellator, patchPreallocCount);
427
428 // Write out breadcrumb triangles. This must be called after polysToTriangles() in order for
429 // fFanBreadcrumbs to be complete.
430 SkDEBUGCODE(int breadcrumbCount = 0;)
431 for (const auto* tri = fFanBreadcrumbs.head(); tri; tri = tri->fNext) {
432 SkDEBUGCODE(++breadcrumbCount;)
433 auto p0 = float2::Load(tri->fPts);
434 auto p1 = float2::Load(tri->fPts + 1);
435 auto p2 = float2::Load(tri->fPts + 2);
436 if (skvx::any((p0 == p1) & (p1 == p2))) {
437 // Cull completely horizontal or vertical triangles. GrTriangulator can't always
438 // get these breadcrumb edges right when they run parallel to the sweep
439 // direction because their winding is undefined by its current definition.
440 // FIXME(skia:12060): This seemed safe, but if there is a view matrix it will
441 // introduce T-junctions.
442 continue;
443 }
444 PatchWriter::TrianglePatch(patchWriter) << p0 << p1 << p2;
445 }
446 SkASSERT(breadcrumbCount == fFanBreadcrumbs.count());
447
448 // Write out the curves.
449 auto tessShader = &fStencilCurvesProgram->geomProc().cast<GrPathTessellationShader>();
450 fTessellator->writePatches(patchWriter,
451 tessShader->maxTessellationSegments(*caps.shaderCaps()),
452 tessShader->viewMatrix(),
453 {SkMatrix::I(), fPath, SK_PMColor4fTRANSPARENT});
454
455 if (!tessShader->willUseTessellationShaders()) {
456 fTessellator->prepareFixedCountBuffers(flushState);
457 }
458 }
459
460 if (!caps.shaderCaps()->vertexIDSupport()) {
461 constexpr static float kStripOrderIDs[4] = {0, 1, 3, 2};
462
463 GR_DEFINE_STATIC_UNIQUE_KEY(gHullVertexBufferKey);
464
465 fHullVertexBufferIfNoIDSupport = flushState->resourceProvider()->findOrMakeStaticBuffer(
466 GrGpuBufferType::kVertex, sizeof(kStripOrderIDs), kStripOrderIDs,
467 gHullVertexBufferKey);
468 }
469 }
470
onExecute(GrOpFlushState * flushState,const SkRect & chainBounds)471 void PathInnerTriangulateOp::onExecute(GrOpFlushState* flushState, const SkRect& chainBounds) {
472 if (fCoverHullsProgram &&
473 fCoverHullsProgram->geomProc().hasVertexAttributes() &&
474 !fHullVertexBufferIfNoIDSupport) {
475 return;
476 }
477
478 if (fStencilCurvesProgram) {
479 SkASSERT(fTessellator);
480 flushState->bindPipelineAndScissorClip(*fStencilCurvesProgram, this->bounds());
481 fTessellator->draw(flushState,
482 fStencilCurvesProgram->geomProc().willUseTessellationShaders());
483 if (flushState->caps().requiresManualFBBarrierAfterTessellatedStencilDraw()) {
484 flushState->gpu()->insertManualFramebufferBarrier(); // http://skbug.com/9739
485 }
486 }
487
488 // Allocation of the fan vertex buffer may have failed but we already pushed back fan programs.
489 if (fFanBuffer) {
490 for (const GrProgramInfo* fanProgram : fFanPrograms) {
491 flushState->bindPipelineAndScissorClip(*fanProgram, this->bounds());
492 flushState->bindTextures(fanProgram->geomProc(), nullptr, fanProgram->pipeline());
493 flushState->bindBuffers(nullptr, nullptr, fFanBuffer);
494 flushState->draw(fFanVertexCount, fBaseFanVertex);
495 }
496 }
497
498 if (fCoverHullsProgram) {
499 SkASSERT(fTessellator);
500 flushState->bindPipelineAndScissorClip(*fCoverHullsProgram, this->bounds());
501 flushState->bindTextures(fCoverHullsProgram->geomProc(), nullptr, *fPipelineForFills);
502 fTessellator->drawHullInstances(flushState, fHullVertexBufferIfNoIDSupport);
503 }
504 }
505
506 } // namespace skgpu::v1
507