/* * Copyright 2014 Google Inc. * Copyright 2017 ARM Ltd. * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #include "src/gpu/ops/GrSmallPathRenderer.h" #include "include/core/SkPaint.h" #include "src/core/SkAutoPixmapStorage.h" #include "src/core/SkDistanceFieldGen.h" #include "src/core/SkDraw.h" #include "src/core/SkMatrixPriv.h" #include "src/core/SkMatrixProvider.h" #include "src/core/SkPointPriv.h" #include "src/core/SkRasterClip.h" #include "src/gpu/GrBuffer.h" #include "src/gpu/GrCaps.h" #include "src/gpu/GrDistanceFieldGenFromVector.h" #include "src/gpu/GrDrawOpTest.h" #include "src/gpu/GrResourceProvider.h" #include "src/gpu/GrSurfaceDrawContext.h" #include "src/gpu/GrVertexWriter.h" #include "src/gpu/effects/GrBitmapTextGeoProc.h" #include "src/gpu/effects/GrDistanceFieldGeoProc.h" #include "src/gpu/geometry/GrQuad.h" #include "src/gpu/geometry/GrStyledShape.h" #include "src/gpu/ops/GrMeshDrawOp.h" #include "src/gpu/ops/GrSimpleMeshDrawOpHelperWithStencil.h" #include "src/gpu/ops/GrSmallPathAtlasMgr.h" #include "src/gpu/ops/GrSmallPathShapeData.h" // mip levels static constexpr SkScalar kIdealMinMIP = 12; static constexpr SkScalar kMaxMIP = 162; static constexpr SkScalar kMaxDim = 73; static constexpr SkScalar kMinSize = SK_ScalarHalf; static constexpr SkScalar kMaxSize = 2*kMaxMIP; GrSmallPathRenderer::GrSmallPathRenderer() {} GrSmallPathRenderer::~GrSmallPathRenderer() {} GrPathRenderer::CanDrawPath GrSmallPathRenderer::onCanDrawPath(const CanDrawPathArgs& args) const { if (!args.fCaps->shaderCaps()->shaderDerivativeSupport()) { return CanDrawPath::kNo; } // If the shape has no key then we won't get any reuse. if (!args.fShape->hasUnstyledKey()) { return CanDrawPath::kNo; } // This only supports filled paths, however, the caller may apply the style to make a filled // path and try again. if (!args.fShape->style().isSimpleFill()) { return CanDrawPath::kNo; } // This does non-inverse coverage-based antialiased fills. if (GrAAType::kCoverage != args.fAAType) { return CanDrawPath::kNo; } // TODO: Support inverse fill if (args.fShape->inverseFilled()) { return CanDrawPath::kNo; } SkScalar scaleFactors[2] = { 1, 1 }; // TODO: handle perspective distortion if (!args.fViewMatrix->hasPerspective() && !args.fViewMatrix->getMinMaxScales(scaleFactors)) { return CanDrawPath::kNo; } // For affine transformations, too much shear can produce artifacts. if (scaleFactors[1]/scaleFactors[0] > 4) { return CanDrawPath::kNo; } // Only support paths with bounds within kMaxDim by kMaxDim, // scaled to have bounds within kMaxSize by kMaxSize. // The goal is to accelerate rendering of lots of small paths that may be scaling. SkRect bounds = args.fShape->styledBounds(); SkScalar minDim = std::min(bounds.width(), bounds.height()); SkScalar maxDim = std::max(bounds.width(), bounds.height()); SkScalar minSize = minDim * SkScalarAbs(scaleFactors[0]); SkScalar maxSize = maxDim * SkScalarAbs(scaleFactors[1]); if (maxDim > kMaxDim || kMinSize > minSize || maxSize > kMaxSize) { return CanDrawPath::kNo; } return CanDrawPath::kYes; } //////////////////////////////////////////////////////////////////////////////// // padding around path bounds to allow for antialiased pixels static const int kAntiAliasPad = 1; class GrSmallPathRenderer::SmallPathOp final : public GrMeshDrawOp { private: using Helper = GrSimpleMeshDrawOpHelperWithStencil; public: DEFINE_OP_CLASS_ID static GrOp::Owner Make(GrRecordingContext* context, GrPaint&& paint, const GrStyledShape& shape, const SkMatrix& viewMatrix, bool gammaCorrect, const GrUserStencilSettings* stencilSettings) { return Helper::FactoryHelper(context, std::move(paint), shape, viewMatrix, gammaCorrect, stencilSettings); } SmallPathOp(GrProcessorSet* processorSet, const SkPMColor4f& color, const GrStyledShape& shape, const SkMatrix& viewMatrix, bool gammaCorrect, const GrUserStencilSettings* stencilSettings) : INHERITED(ClassID()) , fHelper(processorSet, GrAAType::kCoverage, stencilSettings) { SkASSERT(shape.hasUnstyledKey()); // Compute bounds this->setTransformedBounds(shape.bounds(), viewMatrix, HasAABloat::kYes, IsHairline::kNo); #if defined(SK_BUILD_FOR_ANDROID) && !defined(SK_BUILD_FOR_ANDROID_FRAMEWORK) fUsesDistanceField = true; #else // only use distance fields on desktop and Android framework to save space in the atlas fUsesDistanceField = this->bounds().width() > kMaxMIP || this->bounds().height() > kMaxMIP; #endif // always use distance fields if in perspective fUsesDistanceField = fUsesDistanceField || viewMatrix.hasPerspective(); fShapes.emplace_back(Entry{color, shape, viewMatrix}); fGammaCorrect = gammaCorrect; } const char* name() const override { return "SmallPathOp"; } void visitProxies(const VisitProxyFunc& func) const override { fHelper.visitProxies(func); } FixedFunctionFlags fixedFunctionFlags() const override { return fHelper.fixedFunctionFlags(); } GrProcessorSet::Analysis finalize(const GrCaps& caps, const GrAppliedClip* clip, GrClampType clampType) override { return fHelper.finalizeProcessors(caps, clip, clampType, GrProcessorAnalysisCoverage::kSingleChannel, &fShapes.front().fColor, &fWideColor); } private: struct FlushInfo { sk_sp fVertexBuffer; sk_sp fIndexBuffer; GrGeometryProcessor* fGeometryProcessor; const GrSurfaceProxy** fPrimProcProxies; int fVertexOffset; int fInstancesToFlush; }; GrProgramInfo* programInfo() override { // TODO [PI]: implement return nullptr; } void onCreateProgramInfo(const GrCaps*, SkArenaAlloc*, const GrSurfaceProxyView& writeView, GrAppliedClip&&, const GrXferProcessor::DstProxyView&, GrXferBarrierFlags renderPassXferBarriers, GrLoadOp colorLoadOp) override { // We cannot surface the SmallPathOp's programInfo at record time. As currently // implemented, the GP is modified at flush time based on the number of pages in the // atlas. } void onPrePrepareDraws(GrRecordingContext*, const GrSurfaceProxyView& writeView, GrAppliedClip*, const GrXferProcessor::DstProxyView&, GrXferBarrierFlags renderPassXferBarriers, GrLoadOp colorLoadOp) override { // TODO [PI]: implement } void onPrepareDraws(Target* target) override { int instanceCount = fShapes.count(); GrSmallPathAtlasMgr* atlasMgr = target->smallPathAtlasManager(); if (!atlasMgr) { return; } static constexpr int kMaxTextures = GrDistanceFieldPathGeoProc::kMaxTextures; static_assert(GrBitmapTextGeoProc::kMaxTextures == kMaxTextures); FlushInfo flushInfo; flushInfo.fPrimProcProxies = target->allocPrimProcProxyPtrs(kMaxTextures); int numActiveProxies; const GrSurfaceProxyView* views = atlasMgr->getViews(&numActiveProxies); for (int i = 0; i < numActiveProxies; ++i) { // This op does not know its atlas proxies when it is added to a GrOpsTasks, so the // proxies don't get added during the visitProxies call. Thus we add them here. flushInfo.fPrimProcProxies[i] = views[i].proxy(); target->sampledProxyArray()->push_back(views[i].proxy()); } // Setup GrGeometryProcessor const SkMatrix& ctm = fShapes[0].fViewMatrix; if (fUsesDistanceField) { uint32_t flags = 0; // Still need to key off of ctm to pick the right shader for the transformed quad flags |= ctm.isScaleTranslate() ? kScaleOnly_DistanceFieldEffectFlag : 0; flags |= ctm.isSimilarity() ? kSimilarity_DistanceFieldEffectFlag : 0; flags |= fGammaCorrect ? kGammaCorrect_DistanceFieldEffectFlag : 0; const SkMatrix* matrix; SkMatrix invert; if (ctm.hasPerspective()) { matrix = &ctm; } else if (fHelper.usesLocalCoords()) { if (!ctm.invert(&invert)) { return; } matrix = &invert; } else { matrix = &SkMatrix::I(); } flushInfo.fGeometryProcessor = GrDistanceFieldPathGeoProc::Make( target->allocator(), *target->caps().shaderCaps(), *matrix, fWideColor, views, numActiveProxies, GrSamplerState::Filter::kLinear, flags); } else { SkMatrix invert; if (fHelper.usesLocalCoords()) { if (!ctm.invert(&invert)) { return; } } flushInfo.fGeometryProcessor = GrBitmapTextGeoProc::Make( target->allocator(), *target->caps().shaderCaps(), this->color(), fWideColor, views, numActiveProxies, GrSamplerState::Filter::kNearest, kA8_GrMaskFormat, invert, false); } // allocate vertices const size_t kVertexStride = flushInfo.fGeometryProcessor->vertexStride(); // We need to make sure we don't overflow a 32 bit int when we request space in the // makeVertexSpace call below. if (instanceCount > SK_MaxS32 / GrResourceProvider::NumVertsPerNonAAQuad()) { return; } GrVertexWriter vertices{ target->makeVertexSpace( kVertexStride, GrResourceProvider::NumVertsPerNonAAQuad() * instanceCount, &flushInfo.fVertexBuffer, &flushInfo.fVertexOffset)}; flushInfo.fIndexBuffer = target->resourceProvider()->refNonAAQuadIndexBuffer(); if (!vertices.fPtr || !flushInfo.fIndexBuffer) { SkDebugf("Could not allocate vertices\n"); return; } flushInfo.fInstancesToFlush = 0; for (int i = 0; i < instanceCount; i++) { const Entry& args = fShapes[i]; GrSmallPathShapeData* shapeData; if (fUsesDistanceField) { // get mip level SkScalar maxScale; const SkRect& bounds = args.fShape.bounds(); if (args.fViewMatrix.hasPerspective()) { // approximate the scale since we can't get it from the matrix SkRect xformedBounds; args.fViewMatrix.mapRect(&xformedBounds, bounds); maxScale = SkScalarAbs(std::max(xformedBounds.width() / bounds.width(), xformedBounds.height() / bounds.height())); } else { maxScale = SkScalarAbs(args.fViewMatrix.getMaxScale()); } SkScalar maxDim = std::max(bounds.width(), bounds.height()); // We try to create the DF at a 2^n scaled path resolution (1/2, 1, 2, 4, etc.) // In the majority of cases this will yield a crisper rendering. SkScalar mipScale = 1.0f; // Our mipscale is the maxScale clamped to the next highest power of 2 if (maxScale <= SK_ScalarHalf) { SkScalar log = SkScalarFloorToScalar(SkScalarLog2(SkScalarInvert(maxScale))); mipScale = SkScalarPow(2, -log); } else if (maxScale > SK_Scalar1) { SkScalar log = SkScalarCeilToScalar(SkScalarLog2(maxScale)); mipScale = SkScalarPow(2, log); } // Log2 isn't very precise at values close to a power of 2, // so add a little tolerance here. A little bit of scaling up is fine. SkASSERT(maxScale <= mipScale + SK_ScalarNearlyZero); SkScalar mipSize = mipScale*SkScalarAbs(maxDim); // For sizes less than kIdealMinMIP we want to use as large a distance field as we can // so we can preserve as much detail as possible. However, we can't scale down more // than a 1/4 of the size without artifacts. So the idea is that we pick the mipsize // just bigger than the ideal, and then scale down until we are no more than 4x the // original mipsize. if (mipSize < kIdealMinMIP) { SkScalar newMipSize = mipSize; do { newMipSize *= 2; } while (newMipSize < kIdealMinMIP); while (newMipSize > 4 * mipSize) { newMipSize *= 0.25f; } mipSize = newMipSize; } SkScalar desiredDimension = std::min(mipSize, kMaxMIP); int ceilDesiredDimension = SkScalarCeilToInt(desiredDimension); // check to see if df path is cached shapeData = atlasMgr->findOrCreate(args.fShape, ceilDesiredDimension); if (!shapeData->fAtlasLocator.plotLocator().isValid()) { SkScalar scale = desiredDimension / maxDim; if (!this->addDFPathToAtlas(target, &flushInfo, atlasMgr, shapeData, args.fShape, ceilDesiredDimension, scale)) { atlasMgr->deleteCacheEntry(shapeData); continue; } } } else { // check to see if bitmap path is cached shapeData = atlasMgr->findOrCreate(args.fShape, args.fViewMatrix); if (!shapeData->fAtlasLocator.plotLocator().isValid()) { if (!this->addBMPathToAtlas(target, &flushInfo, atlasMgr, shapeData, args.fShape, args.fViewMatrix)) { atlasMgr->deleteCacheEntry(shapeData); continue; } } } auto uploadTarget = target->deferredUploadTarget(); atlasMgr->setUseToken(shapeData, uploadTarget->tokenTracker()->nextDrawToken()); this->writePathVertices(vertices, GrVertexColor(args.fColor, fWideColor), args.fViewMatrix, shapeData); flushInfo.fInstancesToFlush++; } this->flush(target, &flushInfo); } bool addToAtlasWithRetry(GrMeshDrawOp::Target* target, FlushInfo* flushInfo, GrSmallPathAtlasMgr* atlasMgr, int width, int height, const void* image, const SkRect& bounds, int srcInset, GrSmallPathShapeData* shapeData) const { auto resourceProvider = target->resourceProvider(); auto uploadTarget = target->deferredUploadTarget(); auto code = atlasMgr->addToAtlas(resourceProvider, uploadTarget, width, height, image, &shapeData->fAtlasLocator); if (GrDrawOpAtlas::ErrorCode::kError == code) { return false; } if (GrDrawOpAtlas::ErrorCode::kTryAgain == code) { this->flush(target, flushInfo); code = atlasMgr->addToAtlas(resourceProvider, uploadTarget, width, height, image, &shapeData->fAtlasLocator); } shapeData->fAtlasLocator.insetSrc(srcInset); shapeData->fBounds = bounds; return GrDrawOpAtlas::ErrorCode::kSucceeded == code; } bool addDFPathToAtlas(GrMeshDrawOp::Target* target, FlushInfo* flushInfo, GrSmallPathAtlasMgr* atlasMgr, GrSmallPathShapeData* shapeData, const GrStyledShape& shape, uint32_t dimension, SkScalar scale) const { const SkRect& bounds = shape.bounds(); // generate bounding rect for bitmap draw SkRect scaledBounds = bounds; // scale to mip level size scaledBounds.fLeft *= scale; scaledBounds.fTop *= scale; scaledBounds.fRight *= scale; scaledBounds.fBottom *= scale; // subtract out integer portion of origin // (SDF created will be placed with fractional offset burnt in) SkScalar dx = SkScalarFloorToScalar(scaledBounds.fLeft); SkScalar dy = SkScalarFloorToScalar(scaledBounds.fTop); scaledBounds.offset(-dx, -dy); // get integer boundary SkIRect devPathBounds; scaledBounds.roundOut(&devPathBounds); // place devBounds at origin with padding to allow room for antialiasing int width = devPathBounds.width() + 2 * kAntiAliasPad; int height = devPathBounds.height() + 2 * kAntiAliasPad; devPathBounds = SkIRect::MakeWH(width, height); SkScalar translateX = kAntiAliasPad - dx; SkScalar translateY = kAntiAliasPad - dy; // draw path to bitmap SkMatrix drawMatrix; drawMatrix.setScale(scale, scale); drawMatrix.postTranslate(translateX, translateY); SkASSERT(devPathBounds.fLeft == 0); SkASSERT(devPathBounds.fTop == 0); SkASSERT(devPathBounds.width() > 0); SkASSERT(devPathBounds.height() > 0); // setup signed distance field storage SkIRect dfBounds = devPathBounds.makeOutset(SK_DistanceFieldPad, SK_DistanceFieldPad); width = dfBounds.width(); height = dfBounds.height(); // TODO We should really generate this directly into the plot somehow SkAutoSMalloc<1024> dfStorage(width * height * sizeof(unsigned char)); SkPath path; shape.asPath(&path); // Generate signed distance field directly from SkPath bool succeed = GrGenerateDistanceFieldFromPath((unsigned char*)dfStorage.get(), path, drawMatrix, width, height, width * sizeof(unsigned char)); if (!succeed) { // setup bitmap backing SkAutoPixmapStorage dst; if (!dst.tryAlloc(SkImageInfo::MakeA8(devPathBounds.width(), devPathBounds.height()))) { return false; } sk_bzero(dst.writable_addr(), dst.computeByteSize()); // rasterize path SkPaint paint; paint.setStyle(SkPaint::kFill_Style); paint.setAntiAlias(true); SkDraw draw; SkRasterClip rasterClip; rasterClip.setRect(devPathBounds); draw.fRC = &rasterClip; SkSimpleMatrixProvider matrixProvider(drawMatrix); draw.fMatrixProvider = &matrixProvider; draw.fDst = dst; draw.drawPathCoverage(path, paint); // Generate signed distance field SkGenerateDistanceFieldFromA8Image((unsigned char*)dfStorage.get(), (const unsigned char*)dst.addr(), dst.width(), dst.height(), dst.rowBytes()); } SkRect drawBounds = SkRect::Make(devPathBounds).makeOffset(-translateX, -translateY); drawBounds.fLeft /= scale; drawBounds.fTop /= scale; drawBounds.fRight /= scale; drawBounds.fBottom /= scale; return this->addToAtlasWithRetry(target, flushInfo, atlasMgr, width, height, dfStorage.get(), drawBounds, SK_DistanceFieldPad, shapeData); } bool addBMPathToAtlas(GrMeshDrawOp::Target* target, FlushInfo* flushInfo, GrSmallPathAtlasMgr* atlasMgr, GrSmallPathShapeData* shapeData, const GrStyledShape& shape, const SkMatrix& ctm) const { const SkRect& bounds = shape.bounds(); if (bounds.isEmpty()) { return false; } SkMatrix drawMatrix(ctm); SkScalar tx = ctm.getTranslateX(); SkScalar ty = ctm.getTranslateY(); tx -= SkScalarFloorToScalar(tx); ty -= SkScalarFloorToScalar(ty); drawMatrix.set(SkMatrix::kMTransX, tx); drawMatrix.set(SkMatrix::kMTransY, ty); SkRect shapeDevBounds; drawMatrix.mapRect(&shapeDevBounds, bounds); SkScalar dx = SkScalarFloorToScalar(shapeDevBounds.fLeft); SkScalar dy = SkScalarFloorToScalar(shapeDevBounds.fTop); // get integer boundary SkIRect devPathBounds; shapeDevBounds.roundOut(&devPathBounds); // place devBounds at origin with padding to allow room for antialiasing int width = devPathBounds.width() + 2 * kAntiAliasPad; int height = devPathBounds.height() + 2 * kAntiAliasPad; devPathBounds = SkIRect::MakeWH(width, height); SkScalar translateX = kAntiAliasPad - dx; SkScalar translateY = kAntiAliasPad - dy; SkASSERT(devPathBounds.fLeft == 0); SkASSERT(devPathBounds.fTop == 0); SkASSERT(devPathBounds.width() > 0); SkASSERT(devPathBounds.height() > 0); SkPath path; shape.asPath(&path); // setup bitmap backing SkAutoPixmapStorage dst; if (!dst.tryAlloc(SkImageInfo::MakeA8(devPathBounds.width(), devPathBounds.height()))) { return false; } sk_bzero(dst.writable_addr(), dst.computeByteSize()); // rasterize path SkPaint paint; paint.setStyle(SkPaint::kFill_Style); paint.setAntiAlias(true); SkDraw draw; SkRasterClip rasterClip; rasterClip.setRect(devPathBounds); draw.fRC = &rasterClip; drawMatrix.postTranslate(translateX, translateY); SkSimpleMatrixProvider matrixProvider(drawMatrix); draw.fMatrixProvider = &matrixProvider; draw.fDst = dst; draw.drawPathCoverage(path, paint); SkRect drawBounds = SkRect::Make(devPathBounds).makeOffset(-translateX, -translateY); return this->addToAtlasWithRetry(target, flushInfo, atlasMgr, dst.width(), dst.height(), dst.addr(), drawBounds, 0, shapeData); } void writePathVertices(GrVertexWriter& vertices, const GrVertexColor& color, const SkMatrix& ctm, const GrSmallPathShapeData* shapeData) const { SkRect translatedBounds(shapeData->fBounds); if (!fUsesDistanceField) { translatedBounds.offset(SkScalarFloorToScalar(ctm.get(SkMatrix::kMTransX)), SkScalarFloorToScalar(ctm.get(SkMatrix::kMTransY))); } // set up texture coordinates auto texCoords = GrVertexWriter::TriStripFromUVs(shapeData->fAtlasLocator.getUVs()); if (fUsesDistanceField && !ctm.hasPerspective()) { vertices.writeQuad(GrQuad::MakeFromRect(translatedBounds, ctm), color, texCoords); } else { vertices.writeQuad(GrVertexWriter::TriStripFromRect(translatedBounds), color, texCoords); } } void flush(GrMeshDrawOp::Target* target, FlushInfo* flushInfo) const { GrSmallPathAtlasMgr* atlasMgr = target->smallPathAtlasManager(); if (!atlasMgr) { return; } int numActiveProxies; const GrSurfaceProxyView* views = atlasMgr->getViews(&numActiveProxies); GrGeometryProcessor* gp = flushInfo->fGeometryProcessor; if (gp->numTextureSamplers() != numActiveProxies) { for (int i = gp->numTextureSamplers(); i < numActiveProxies; ++i) { flushInfo->fPrimProcProxies[i] = views[i].proxy(); // This op does not know its atlas proxies when it is added to a GrOpsTasks, so the // proxies don't get added during the visitProxies call. Thus we add them here. target->sampledProxyArray()->push_back(views[i].proxy()); } // During preparation the number of atlas pages has increased. // Update the proxies used in the GP to match. if (fUsesDistanceField) { reinterpret_cast(gp)->addNewViews( views, numActiveProxies, GrSamplerState::Filter::kLinear); } else { reinterpret_cast(gp)->addNewViews( views, numActiveProxies, GrSamplerState::Filter::kNearest); } } if (flushInfo->fInstancesToFlush) { GrSimpleMesh* mesh = target->allocMesh(); mesh->setIndexedPatterned(flushInfo->fIndexBuffer, GrResourceProvider::NumIndicesPerNonAAQuad(), flushInfo->fInstancesToFlush, GrResourceProvider::MaxNumNonAAQuads(), flushInfo->fVertexBuffer, GrResourceProvider::NumVertsPerNonAAQuad(), flushInfo->fVertexOffset); target->recordDraw(flushInfo->fGeometryProcessor, mesh, 1, flushInfo->fPrimProcProxies, GrPrimitiveType::kTriangles); flushInfo->fVertexOffset += GrResourceProvider::NumVertsPerNonAAQuad() * flushInfo->fInstancesToFlush; flushInfo->fInstancesToFlush = 0; } } void onExecute(GrOpFlushState* flushState, const SkRect& chainBounds) override { auto pipeline = fHelper.createPipeline(flushState); flushState->executeDrawsAndUploadsForMeshDrawOp(this, chainBounds, pipeline, fHelper.stencilSettings()); } const SkPMColor4f& color() const { return fShapes[0].fColor; } bool usesDistanceField() const { return fUsesDistanceField; } CombineResult onCombineIfPossible(GrOp* t, SkArenaAlloc*, const GrCaps& caps) override { SmallPathOp* that = t->cast(); if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) { return CombineResult::kCannotCombine; } if (this->usesDistanceField() != that->usesDistanceField()) { return CombineResult::kCannotCombine; } const SkMatrix& thisCtm = this->fShapes[0].fViewMatrix; const SkMatrix& thatCtm = that->fShapes[0].fViewMatrix; if (thisCtm.hasPerspective() != thatCtm.hasPerspective()) { return CombineResult::kCannotCombine; } // We can position on the cpu unless we're in perspective, // but also need to make sure local matrices are identical if ((thisCtm.hasPerspective() || fHelper.usesLocalCoords()) && !SkMatrixPriv::CheapEqual(thisCtm, thatCtm)) { return CombineResult::kCannotCombine; } // Depending on the ctm we may have a different shader for SDF paths if (this->usesDistanceField()) { if (thisCtm.isScaleTranslate() != thatCtm.isScaleTranslate() || thisCtm.isSimilarity() != thatCtm.isSimilarity()) { return CombineResult::kCannotCombine; } } fShapes.push_back_n(that->fShapes.count(), that->fShapes.begin()); fWideColor |= that->fWideColor; return CombineResult::kMerged; } #if GR_TEST_UTILS SkString onDumpInfo() const override { SkString string; for (const auto& geo : fShapes) { string.appendf("Color: 0x%08x\n", geo.fColor.toBytes_RGBA()); } string += fHelper.dumpInfo(); return string; } #endif bool fUsesDistanceField; struct Entry { SkPMColor4f fColor; GrStyledShape fShape; SkMatrix fViewMatrix; }; SkSTArray<1, Entry> fShapes; Helper fHelper; bool fGammaCorrect; bool fWideColor; using INHERITED = GrMeshDrawOp; }; bool GrSmallPathRenderer::onDrawPath(const DrawPathArgs& args) { GR_AUDIT_TRAIL_AUTO_FRAME(args.fRenderTargetContext->auditTrail(), "GrSmallPathRenderer::onDrawPath"); // we've already bailed on inverse filled paths, so this is safe SkASSERT(!args.fShape->isEmpty()); SkASSERT(args.fShape->hasUnstyledKey()); GrOp::Owner op = SmallPathOp::Make( args.fContext, std::move(args.fPaint), *args.fShape, *args.fViewMatrix, args.fGammaCorrect, args.fUserStencilSettings); args.fRenderTargetContext->addDrawOp(args.fClip, std::move(op)); return true; } /////////////////////////////////////////////////////////////////////////////////////////////////// #if GR_TEST_UTILS GrOp::Owner GrSmallPathRenderer::createOp_TestingOnly( GrRecordingContext* context, GrPaint&& paint, const GrStyledShape& shape, const SkMatrix& viewMatrix, bool gammaCorrect, const GrUserStencilSettings* stencil) { return GrSmallPathRenderer::SmallPathOp::Make(context, std::move(paint), shape, viewMatrix, gammaCorrect, stencil); } GR_DRAW_OP_TEST_DEFINE(SmallPathOp) { SkMatrix viewMatrix = GrTest::TestMatrix(random); bool gammaCorrect = random->nextBool(); // This path renderer only allows fill styles. GrStyledShape shape(GrTest::TestPath(random), GrStyle::SimpleFill()); return GrSmallPathRenderer::createOp_TestingOnly( context, std::move(paint), shape, viewMatrix, gammaCorrect, GrGetRandomStencil(random, context)); } #endif