/* * Copyright 2011 Google Inc. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "src/gpu/ops/GrDefaultPathRenderer.h" #include "include/core/SkString.h" #include "include/core/SkStrokeRec.h" #include "src/core/SkGeometry.h" #include "src/core/SkMatrixPriv.h" #include "src/core/SkTLazy.h" #include "src/core/SkTraceEvent.h" #include "src/gpu/GrAuditTrail.h" #include "src/gpu/GrCaps.h" #include "src/gpu/GrClip.h" #include "src/gpu/GrDefaultGeoProcFactory.h" #include "src/gpu/GrDrawOpTest.h" #include "src/gpu/GrOpFlushState.h" #include "src/gpu/GrProgramInfo.h" #include "src/gpu/GrSimpleMesh.h" #include "src/gpu/GrStyle.h" #include "src/gpu/geometry/GrPathUtils.h" #include "src/gpu/geometry/GrStyledShape.h" #include "src/gpu/ops/GrMeshDrawOp.h" #include "src/gpu/ops/GrSimpleMeshDrawOpHelperWithStencil.h" GrDefaultPathRenderer::GrDefaultPathRenderer() { } //////////////////////////////////////////////////////////////////////////////// // Helpers for drawPath #define STENCIL_OFF 0 // Always disable stencil (even when needed) static inline bool single_pass_shape(const GrStyledShape& shape) { #if STENCIL_OFF return true; #else // Inverse fill is always two pass. if (shape.inverseFilled()) { return false; } // This path renderer only accepts simple fill paths or stroke paths that are either hairline // or have a stroke width small enough to treat as hairline. Hairline paths are always single // pass. Filled paths are single pass if they're convex. if (shape.style().isSimpleFill()) { return shape.knownToBeConvex(); } return true; #endif } GrPathRenderer::StencilSupport GrDefaultPathRenderer::onGetStencilSupport(const GrStyledShape& shape) const { if (single_pass_shape(shape)) { return GrPathRenderer::kNoRestriction_StencilSupport; } else { return GrPathRenderer::kStencilOnly_StencilSupport; } } namespace { class PathGeoBuilder { public: PathGeoBuilder(GrPrimitiveType primitiveType, GrMeshDrawOp::Target* target, SkTDArray* meshes) : fPrimitiveType(primitiveType) , fTarget(target) , fVertexStride(sizeof(SkPoint)) , fFirstIndex(0) , fIndicesInChunk(0) , fIndices(nullptr) , fMeshes(meshes) { this->allocNewBuffers(); } ~PathGeoBuilder() { this->createMeshAndPutBackReserve(); } /** * Path verbs */ void moveTo(const SkPoint& p) { this->needSpace(1); if (!this->isHairline()) { fSubpathIndexStart = this->currentIndex(); fSubpathStartPoint = p; } *(fCurVert++) = p; } void addLine(const SkPoint pts[]) { this->needSpace(1, this->indexScale(), &pts[0]); if (this->isIndexed()) { uint16_t prevIdx = this->currentIndex() - 1; this->appendCountourEdgeIndices(prevIdx); } *(fCurVert++) = pts[1]; } void addQuad(const SkPoint pts[], SkScalar srcSpaceTolSqd, SkScalar srcSpaceTol) { this->needSpace(GrPathUtils::kMaxPointsPerCurve, GrPathUtils::kMaxPointsPerCurve * this->indexScale(), &pts[0]); // First pt of quad is the pt we ended on in previous step uint16_t firstQPtIdx = this->currentIndex() - 1; uint16_t numPts = (uint16_t)GrPathUtils::generateQuadraticPoints( pts[0], pts[1], pts[2], srcSpaceTolSqd, &fCurVert, GrPathUtils::quadraticPointCount(pts, srcSpaceTol)); if (this->isIndexed()) { for (uint16_t i = 0; i < numPts; ++i) { this->appendCountourEdgeIndices(firstQPtIdx + i); } } } void addConic(SkScalar weight, const SkPoint pts[], SkScalar srcSpaceTolSqd, SkScalar srcSpaceTol) { SkAutoConicToQuads converter; const SkPoint* quadPts = converter.computeQuads(pts, weight, srcSpaceTol); for (int i = 0; i < converter.countQuads(); ++i) { this->addQuad(quadPts + i * 2, srcSpaceTolSqd, srcSpaceTol); } } void addCubic(const SkPoint pts[], SkScalar srcSpaceTolSqd, SkScalar srcSpaceTol) { this->needSpace(GrPathUtils::kMaxPointsPerCurve, GrPathUtils::kMaxPointsPerCurve * this->indexScale(), &pts[0]); // First pt of cubic is the pt we ended on in previous step uint16_t firstCPtIdx = this->currentIndex() - 1; uint16_t numPts = (uint16_t) GrPathUtils::generateCubicPoints( pts[0], pts[1], pts[2], pts[3], srcSpaceTolSqd, &fCurVert, GrPathUtils::cubicPointCount(pts, srcSpaceTol)); if (this->isIndexed()) { for (uint16_t i = 0; i < numPts; ++i) { this->appendCountourEdgeIndices(firstCPtIdx + i); } } } void addPath(const SkPath& path, SkScalar srcSpaceTol) { SkScalar srcSpaceTolSqd = srcSpaceTol * srcSpaceTol; SkPath::Iter iter(path, false); SkPoint pts[4]; bool done = false; while (!done) { SkPath::Verb verb = iter.next(pts); switch (verb) { case SkPath::kMove_Verb: this->moveTo(pts[0]); break; case SkPath::kLine_Verb: this->addLine(pts); break; case SkPath::kConic_Verb: this->addConic(iter.conicWeight(), pts, srcSpaceTolSqd, srcSpaceTol); break; case SkPath::kQuad_Verb: this->addQuad(pts, srcSpaceTolSqd, srcSpaceTol); break; case SkPath::kCubic_Verb: this->addCubic(pts, srcSpaceTolSqd, srcSpaceTol); break; case SkPath::kClose_Verb: break; case SkPath::kDone_Verb: done = true; } } } static bool PathHasMultipleSubpaths(const SkPath& path) { bool first = true; SkPath::Iter iter(path, false); SkPath::Verb verb; SkPoint pts[4]; while ((verb = iter.next(pts)) != SkPath::kDone_Verb) { if (SkPath::kMove_Verb == verb && !first) { return true; } first = false; } return false; } private: /** * Derived properties * TODO: Cache some of these for better performance, rather than re-computing? */ bool isIndexed() const { return GrPrimitiveType::kLines == fPrimitiveType || GrPrimitiveType::kTriangles == fPrimitiveType; } bool isHairline() const { return GrPrimitiveType::kLines == fPrimitiveType || GrPrimitiveType::kLineStrip == fPrimitiveType; } int indexScale() const { switch (fPrimitiveType) { case GrPrimitiveType::kLines: return 2; case GrPrimitiveType::kTriangles: return 3; default: return 0; } } uint16_t currentIndex() const { return fCurVert - fVertices; } // Allocate vertex and (possibly) index buffers void allocNewBuffers() { // Ensure that we always get enough verts for a worst-case quad/cubic, plus leftover points // from previous mesh piece (up to two verts to continue fanning). If we can't get that // many, ask for a much larger number. This needs to be fairly big to handle quads/cubics, // which have a worst-case of 1k points. static const int kMinVerticesPerChunk = GrPathUtils::kMaxPointsPerCurve + 2; static const int kFallbackVerticesPerChunk = 16384; fVertices = static_cast(fTarget->makeVertexSpaceAtLeast(fVertexStride, kMinVerticesPerChunk, kFallbackVerticesPerChunk, &fVertexBuffer, &fFirstVertex, &fVerticesInChunk)); if (this->isIndexed()) { // Similar to above: Ensure we get enough indices for one worst-case quad/cubic. // No extra indices are needed for stitching, though. If we can't get that many, ask // for enough to match our large vertex request. const int kMinIndicesPerChunk = GrPathUtils::kMaxPointsPerCurve * this->indexScale(); const int kFallbackIndicesPerChunk = kFallbackVerticesPerChunk * this->indexScale(); fIndices = fTarget->makeIndexSpaceAtLeast(kMinIndicesPerChunk, kFallbackIndicesPerChunk, &fIndexBuffer, &fFirstIndex, &fIndicesInChunk); } fCurVert = fVertices; fCurIdx = fIndices; fSubpathIndexStart = 0; } void appendCountourEdgeIndices(uint16_t edgeV0Idx) { // When drawing lines we're appending line segments along the countour. When applying the // other fill rules we're drawing triangle fans around the start of the current (sub)path. if (!this->isHairline()) { *(fCurIdx++) = fSubpathIndexStart; } *(fCurIdx++) = edgeV0Idx; *(fCurIdx++) = edgeV0Idx + 1; } // Emits a single draw with all accumulated vertex/index data void createMeshAndPutBackReserve() { int vertexCount = fCurVert - fVertices; int indexCount = fCurIdx - fIndices; SkASSERT(vertexCount <= fVerticesInChunk); SkASSERT(indexCount <= fIndicesInChunk); GrSimpleMesh* mesh = nullptr; if (this->isIndexed() ? SkToBool(indexCount) : SkToBool(vertexCount)) { mesh = fTarget->allocMesh(); if (!this->isIndexed()) { mesh->set(std::move(fVertexBuffer), vertexCount, fFirstVertex); } else { mesh->setIndexed(std::move(fIndexBuffer), indexCount, fFirstIndex, 0, vertexCount - 1, GrPrimitiveRestart::kNo, std::move(fVertexBuffer), fFirstVertex); } } fTarget->putBackIndices((size_t)(fIndicesInChunk - indexCount)); fTarget->putBackVertices((size_t)(fVerticesInChunk - vertexCount), fVertexStride); if (mesh) { fMeshes->push_back(mesh); } } void needSpace(int vertsNeeded, int indicesNeeded = 0, const SkPoint* lastPoint = nullptr) { if (fCurVert + vertsNeeded > fVertices + fVerticesInChunk || fCurIdx + indicesNeeded > fIndices + fIndicesInChunk) { // We are about to run out of space (possibly) #ifdef SK_DEBUG // To maintain continuity, we need to remember one or two points from the current mesh. // Lines only need the last point, fills need the first point from the current contour. // We always grab both here, and append the ones we need at the end of this process. SkASSERT(fSubpathIndexStart < fVerticesInChunk); // This assert is reading from the gpu buffer fVertices and will be slow, but for debug // that is okay. if (!this->isHairline()) { SkASSERT(fSubpathStartPoint == fVertices[fSubpathIndexStart]); } if (lastPoint) { SkASSERT(*(fCurVert - 1) == *lastPoint); } #endif // Draw the mesh we've accumulated, and put back any unused space this->createMeshAndPutBackReserve(); // Get new buffers this->allocNewBuffers(); // On moves we don't need to copy over any points to the new buffer and we pass in a // null lastPoint. if (lastPoint) { // Append copies of the points we saved so the two meshes will weld properly if (!this->isHairline()) { *(fCurVert++) = fSubpathStartPoint; } *(fCurVert++) = *lastPoint; } } } GrPrimitiveType fPrimitiveType; GrMeshDrawOp::Target* fTarget; size_t fVertexStride; sk_sp fVertexBuffer; int fFirstVertex; int fVerticesInChunk; SkPoint* fVertices; SkPoint* fCurVert; sk_sp fIndexBuffer; int fFirstIndex; int fIndicesInChunk; uint16_t* fIndices; uint16_t* fCurIdx; uint16_t fSubpathIndexStart; SkPoint fSubpathStartPoint; SkTDArray* fMeshes; }; class DefaultPathOp final : public GrMeshDrawOp { private: using Helper = GrSimpleMeshDrawOpHelperWithStencil; public: DEFINE_OP_CLASS_ID static GrOp::Owner Make(GrRecordingContext* context, GrPaint&& paint, const SkPath& path, SkScalar tolerance, uint8_t coverage, const SkMatrix& viewMatrix, bool isHairline, GrAAType aaType, const SkRect& devBounds, const GrUserStencilSettings* stencilSettings) { return Helper::FactoryHelper(context, std::move(paint), path, tolerance, coverage, viewMatrix, isHairline, aaType, devBounds, stencilSettings); } const char* name() const override { return "DefaultPathOp"; } void visitProxies(const VisitProxyFunc& func) const override { if (fProgramInfo) { fProgramInfo->visitFPProxies(func); } else { fHelper.visitProxies(func); } } DefaultPathOp(GrProcessorSet* processorSet, const SkPMColor4f& color, const SkPath& path, SkScalar tolerance, uint8_t coverage, const SkMatrix& viewMatrix, bool isHairline, GrAAType aaType, const SkRect& devBounds, const GrUserStencilSettings* stencilSettings) : INHERITED(ClassID()) , fHelper(processorSet, aaType, stencilSettings) , fColor(color) , fCoverage(coverage) , fViewMatrix(viewMatrix) , fIsHairline(isHairline) { fPaths.emplace_back(PathData{path, tolerance}); HasAABloat aaBloat = (aaType == GrAAType::kNone) ? HasAABloat ::kNo : HasAABloat::kYes; this->setBounds(devBounds, aaBloat, isHairline ? IsHairline::kYes : IsHairline::kNo); } FixedFunctionFlags fixedFunctionFlags() const override { return fHelper.fixedFunctionFlags(); } GrProcessorSet::Analysis finalize(const GrCaps& caps, const GrAppliedClip* clip, GrClampType clampType) override { GrProcessorAnalysisCoverage gpCoverage = this->coverage() == 0xFF ? GrProcessorAnalysisCoverage::kNone : GrProcessorAnalysisCoverage::kSingleChannel; // This Op uses uniform (not vertex) color, so doesn't need to track wide color. return fHelper.finalizeProcessors(caps, clip, clampType, gpCoverage, &fColor, nullptr); } private: GrPrimitiveType primType() const { if (this->isHairline()) { int instanceCount = fPaths.count(); // We avoid indices when we have a single hairline contour. bool isIndexed = instanceCount > 1 || PathGeoBuilder::PathHasMultipleSubpaths(fPaths[0].fPath); return isIndexed ? GrPrimitiveType::kLines : GrPrimitiveType::kLineStrip; } return GrPrimitiveType::kTriangles; } GrProgramInfo* programInfo() override { return fProgramInfo; } void onCreateProgramInfo(const GrCaps* caps, SkArenaAlloc* arena, const GrSurfaceProxyView& writeView, GrAppliedClip&& appliedClip, const GrXferProcessor::DstProxyView& dstProxyView, GrXferBarrierFlags renderPassXferBarriers, GrLoadOp colorLoadOp) override { GrGeometryProcessor* gp; { using namespace GrDefaultGeoProcFactory; Color color(this->color()); Coverage coverage(this->coverage()); LocalCoords localCoords(fHelper.usesLocalCoords() ? LocalCoords::kUsePosition_Type : LocalCoords::kUnused_Type); gp = GrDefaultGeoProcFactory::Make(arena, color, coverage, localCoords, this->viewMatrix()); } SkASSERT(gp->vertexStride() == sizeof(SkPoint)); fProgramInfo = fHelper.createProgramInfoWithStencil(caps, arena, writeView, std::move(appliedClip), dstProxyView, gp, this->primType(), renderPassXferBarriers, colorLoadOp); } void onPrepareDraws(Target* target) override { PathGeoBuilder pathGeoBuilder(this->primType(), target, &fMeshes); // fill buffers for (int i = 0; i < fPaths.count(); i++) { const PathData& args = fPaths[i]; pathGeoBuilder.addPath(args.fPath, args.fTolerance); } } void onExecute(GrOpFlushState* flushState, const SkRect& chainBounds) override { if (!fProgramInfo) { this->createProgramInfo(flushState); } if (!fProgramInfo || !fMeshes.count()) { return; } flushState->bindPipelineAndScissorClip(*fProgramInfo, chainBounds); flushState->bindTextures(fProgramInfo->geomProc(), nullptr, fProgramInfo->pipeline()); for (int i = 0; i < fMeshes.count(); ++i) { flushState->drawMesh(*fMeshes[i]); } } CombineResult onCombineIfPossible(GrOp* t, SkArenaAlloc*, const GrCaps& caps) override { DefaultPathOp* that = t->cast(); if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) { return CombineResult::kCannotCombine; } if (this->color() != that->color()) { return CombineResult::kCannotCombine; } if (this->coverage() != that->coverage()) { return CombineResult::kCannotCombine; } if (!SkMatrixPriv::CheapEqual(this->viewMatrix(), that->viewMatrix())) { return CombineResult::kCannotCombine; } if (this->isHairline() != that->isHairline()) { return CombineResult::kCannotCombine; } fPaths.push_back_n(that->fPaths.count(), that->fPaths.begin()); return CombineResult::kMerged; } #if GR_TEST_UTILS SkString onDumpInfo() const override { SkString string = SkStringPrintf("Color: 0x%08x Count: %d\n", fColor.toBytes_RGBA(), fPaths.count()); for (const auto& path : fPaths) { string.appendf("Tolerance: %.2f\n", path.fTolerance); } string += fHelper.dumpInfo(); return string; } #endif const SkPMColor4f& color() const { return fColor; } uint8_t coverage() const { return fCoverage; } const SkMatrix& viewMatrix() const { return fViewMatrix; } bool isHairline() const { return fIsHairline; } struct PathData { SkPath fPath; SkScalar fTolerance; }; SkSTArray<1, PathData, true> fPaths; Helper fHelper; SkPMColor4f fColor; uint8_t fCoverage; SkMatrix fViewMatrix; bool fIsHairline; SkTDArray fMeshes; GrProgramInfo* fProgramInfo = nullptr; using INHERITED = GrMeshDrawOp; }; } // anonymous namespace bool GrDefaultPathRenderer::internalDrawPath(GrSurfaceDrawContext* surfaceDrawContext, GrPaint&& paint, GrAAType aaType, const GrUserStencilSettings& userStencilSettings, const GrClip* clip, const SkMatrix& viewMatrix, const GrStyledShape& shape, bool stencilOnly) { auto context = surfaceDrawContext->recordingContext(); SkASSERT(GrAAType::kCoverage != aaType); SkPath path; shape.asPath(&path); SkScalar hairlineCoverage; uint8_t newCoverage = 0xff; bool isHairline = false; if (IsStrokeHairlineOrEquivalent(shape.style(), viewMatrix, &hairlineCoverage)) { newCoverage = SkScalarRoundToInt(hairlineCoverage * 0xff); isHairline = true; } else { SkASSERT(shape.style().isSimpleFill()); } int passCount = 0; const GrUserStencilSettings* passes[2]; bool reverse = false; bool lastPassIsBounds; if (isHairline) { passCount = 1; if (stencilOnly) { passes[0] = &gDirectToStencil; } else { passes[0] = &userStencilSettings; } lastPassIsBounds = false; } else { if (single_pass_shape(shape)) { passCount = 1; if (stencilOnly) { passes[0] = &gDirectToStencil; } else { passes[0] = &userStencilSettings; } lastPassIsBounds = false; } else { switch (path.getFillType()) { case SkPathFillType::kInverseEvenOdd: reverse = true; [[fallthrough]]; case SkPathFillType::kEvenOdd: passes[0] = &gEOStencilPass; if (stencilOnly) { passCount = 1; lastPassIsBounds = false; } else { passCount = 2; lastPassIsBounds = true; if (reverse) { passes[1] = &gInvEOColorPass; } else { passes[1] = &gEOColorPass; } } break; case SkPathFillType::kInverseWinding: reverse = true; [[fallthrough]]; case SkPathFillType::kWinding: passes[0] = &gWindStencilPass; passCount = 2; if (stencilOnly) { lastPassIsBounds = false; --passCount; } else { lastPassIsBounds = true; if (reverse) { passes[passCount-1] = &gInvWindColorPass; } else { passes[passCount-1] = &gWindColorPass; } } break; default: SkDEBUGFAIL("Unknown path fFill!"); return false; } } } SkScalar tol = GrPathUtils::kDefaultTolerance; SkScalar srcSpaceTol = GrPathUtils::scaleToleranceToSrc(tol, viewMatrix, path.getBounds()); SkRect devBounds; GetPathDevBounds(path, surfaceDrawContext->asRenderTargetProxy()->backingStoreDimensions(), viewMatrix, &devBounds); for (int p = 0; p < passCount; ++p) { if (lastPassIsBounds && (p == passCount-1)) { SkRect bounds; SkMatrix localMatrix = SkMatrix::I(); if (reverse) { // draw over the dev bounds (which will be the whole dst surface for inv fill). bounds = devBounds; SkMatrix vmi; // mapRect through persp matrix may not be correct if (!viewMatrix.hasPerspective() && viewMatrix.invert(&vmi)) { vmi.mapRect(&bounds); } else { if (!viewMatrix.invert(&localMatrix)) { return false; } } } else { bounds = path.getBounds(); } const SkMatrix& viewM = (reverse && viewMatrix.hasPerspective()) ? SkMatrix::I() : viewMatrix; // This is a non-coverage aa rect op since we assert aaType != kCoverage at the start assert_alive(paint); surfaceDrawContext->stencilRect(clip, passes[p], std::move(paint), GrAA(aaType == GrAAType::kMSAA), viewM, bounds, &localMatrix); } else { bool stencilPass = stencilOnly || passCount > 1; GrOp::Owner op; if (stencilPass) { GrPaint stencilPaint; stencilPaint.setXPFactory(GrDisableColorXPFactory::Get()); op = DefaultPathOp::Make(context, std::move(stencilPaint), path, srcSpaceTol, newCoverage, viewMatrix, isHairline, aaType, devBounds, passes[p]); } else { assert_alive(paint); op = DefaultPathOp::Make(context, std::move(paint), path, srcSpaceTol, newCoverage, viewMatrix, isHairline, aaType, devBounds, passes[p]); } surfaceDrawContext->addDrawOp(clip, std::move(op)); } } return true; } GrPathRenderer::CanDrawPath GrDefaultPathRenderer::onCanDrawPath(const CanDrawPathArgs& args) const { bool isHairline = IsStrokeHairlineOrEquivalent( args.fShape->style(), *args.fViewMatrix, nullptr); // If we aren't a single_pass_shape or hairline, we require stencil buffers. if (!(single_pass_shape(*args.fShape) || isHairline) && !args.fProxy->canUseStencil(*args.fCaps)) { return CanDrawPath::kNo; } // If antialiasing is required, we only support MSAA. if (GrAAType::kNone != args.fAAType && GrAAType::kMSAA != args.fAAType) { return CanDrawPath::kNo; } // This can draw any path with any simple fill style. if (!args.fShape->style().isSimpleFill() && !isHairline) { return CanDrawPath::kNo; } // This is the fallback renderer for when a path is too complicated for the others to draw. return CanDrawPath::kAsBackup; } bool GrDefaultPathRenderer::onDrawPath(const DrawPathArgs& args) { GR_AUDIT_TRAIL_AUTO_FRAME(args.fRenderTargetContext->auditTrail(), "GrDefaultPathRenderer::onDrawPath"); GrAAType aaType = (GrAAType::kNone != args.fAAType) ? GrAAType::kMSAA : GrAAType::kNone; return this->internalDrawPath( args.fRenderTargetContext, std::move(args.fPaint), aaType, *args.fUserStencilSettings, args.fClip, *args.fViewMatrix, *args.fShape, false); } void GrDefaultPathRenderer::onStencilPath(const StencilPathArgs& args) { GR_AUDIT_TRAIL_AUTO_FRAME(args.fRenderTargetContext->auditTrail(), "GrDefaultPathRenderer::onStencilPath"); SkASSERT(!args.fShape->inverseFilled()); GrPaint paint; paint.setXPFactory(GrDisableColorXPFactory::Get()); auto aaType = (GrAA::kYes == args.fDoStencilMSAA) ? GrAAType::kMSAA : GrAAType::kNone; this->internalDrawPath( args.fRenderTargetContext, std::move(paint), aaType, GrUserStencilSettings::kUnused, args.fClip, *args.fViewMatrix, *args.fShape, true); } /////////////////////////////////////////////////////////////////////////////////////////////////// #if GR_TEST_UTILS GR_DRAW_OP_TEST_DEFINE(DefaultPathOp) { SkMatrix viewMatrix = GrTest::TestMatrix(random); // For now just hairlines because the other types of draws require two ops. // TODO we should figure out a way to combine the stencil and cover steps into one op. GrStyle style(SkStrokeRec::kHairline_InitStyle); const SkPath& path = GrTest::TestPath(random); // Compute srcSpaceTol SkRect bounds = path.getBounds(); SkScalar tol = GrPathUtils::kDefaultTolerance; SkScalar srcSpaceTol = GrPathUtils::scaleToleranceToSrc(tol, viewMatrix, bounds); viewMatrix.mapRect(&bounds); uint8_t coverage = GrRandomCoverage(random); GrAAType aaType = GrAAType::kNone; if (numSamples > 1 && random->nextBool()) { aaType = GrAAType::kMSAA; } return DefaultPathOp::Make(context, std::move(paint), path, srcSpaceTol, coverage, viewMatrix, true, aaType, bounds, GrGetRandomStencil(random, context)); } #endif