Reland "ccpr: Implement stroking with fine triangle strips"
This is a reland of 2f2757fa6ba8134330e05694d08907f6e37abb41
Original change's description:
> ccpr: Implement stroking with fine triangle strips
>
> Implements strokes by linearizing the curve into fine triangle strips
> and interpolating a coverage ramp for edge AA. Each triangle in the
> strip emits either positive or negative coverage, depending on its
> winding direction. Joins and caps are drawn with the existing CCPR
> shaders for triangles and conics.
>
> Conic strokes and non-rigid-body transforms are not yet supported.
>
> Bug: skia:
> Change-Id: I45a819abd64e91c2b62e992587eb85c703e09e77
> Reviewed-on: https://skia-review.googlesource.com/148243
> Commit-Queue: Chris Dalton <csmartdalton@google.com>
> Reviewed-by: Brian Salomon <bsalomon@google.com>
> Reviewed-by: Allan MacKinnon <allanmac@google.com>
TBR=robertphillips@google.com
Bug: skia:
Change-Id: I3f0065e80975ee8334300bc5e934231b66b49178
Reviewed-on: https://skia-review.googlesource.com/151188
Commit-Queue: Chris Dalton <csmartdalton@google.com>
Reviewed-by: Chris Dalton <csmartdalton@google.com>
diff --git a/src/gpu/ccpr/GrCCDrawPathsOp.cpp b/src/gpu/ccpr/GrCCDrawPathsOp.cpp
index aed0672..2ec8379 100644
--- a/src/gpu/ccpr/GrCCDrawPathsOp.cpp
+++ b/src/gpu/ccpr/GrCCDrawPathsOp.cpp
@@ -28,25 +28,82 @@
return sk_64_mul(r.height(), r.width());
}
-std::unique_ptr<GrCCDrawPathsOp> GrCCDrawPathsOp::Make(GrContext* context,
- const SkIRect& clipIBounds,
- const SkMatrix& m,
- const GrShape& shape,
- const SkRect& devBounds,
- GrPaint&& paint) {
- SkIRect shapeDevIBounds;
- devBounds.roundOut(&shapeDevIBounds); // GrCCPathParser might find slightly tighter bounds.
+std::unique_ptr<GrCCDrawPathsOp> GrCCDrawPathsOp::Make(
+ GrContext* context, const SkIRect& clipIBounds, const SkMatrix& m, const GrShape& shape,
+ GrPaint&& paint) {
+ static constexpr int kPathCropThreshold = GrCoverageCountingPathRenderer::kPathCropThreshold;
+
+ SkRect conservativeDevBounds;
+ m.mapRect(&conservativeDevBounds, shape.bounds());
+
+ const SkStrokeRec& stroke = shape.style().strokeRec();
+ float strokeDevWidth = 0;
+ float conservativeInflationRadius = 0;
+ if (!stroke.isFillStyle()) {
+ if (stroke.isHairlineStyle()) {
+ strokeDevWidth = 1;
+ } else {
+ SkASSERT(m.isSimilarity()); // Otherwise matrixScaleFactor = m.getMaxScale().
+ float matrixScaleFactor = SkVector::Length(m.getScaleX(), m.getSkewY());
+ strokeDevWidth = stroke.getWidth() * matrixScaleFactor;
+ }
+ // Inflate for a minimum stroke width of 1. In some cases when the stroke is less than 1px
+ // wide, we may inflate it to 1px and instead reduce the opacity.
+ conservativeInflationRadius = SkStrokeRec::GetInflationRadius(
+ stroke.getJoin(), stroke.getMiter(), stroke.getCap(), SkTMax(strokeDevWidth, 1.f));
+ conservativeDevBounds.outset(conservativeInflationRadius, conservativeInflationRadius);
+ }
+
+ std::unique_ptr<GrCCDrawPathsOp> op;
+ float conservativeSize = SkTMax(conservativeDevBounds.height(), conservativeDevBounds.width());
+ if (conservativeSize > kPathCropThreshold) {
+ // The path is too large. Crop it or analytic AA can run out of fp32 precision.
+ SkPath croppedDevPath;
+ shape.asPath(&croppedDevPath);
+ croppedDevPath.transform(m, &croppedDevPath);
+
+ SkIRect cropBox = clipIBounds;
+ GrShape croppedDevShape;
+ if (stroke.isFillStyle()) {
+ GrCoverageCountingPathRenderer::CropPath(croppedDevPath, cropBox, &croppedDevPath);
+ croppedDevShape = GrShape(croppedDevPath);
+ conservativeDevBounds = croppedDevShape.bounds();
+ } else {
+ int r = SkScalarCeilToInt(conservativeInflationRadius);
+ cropBox.outset(r, r);
+ GrCoverageCountingPathRenderer::CropPath(croppedDevPath, cropBox, &croppedDevPath);
+ SkStrokeRec devStroke = stroke;
+ devStroke.setStrokeStyle(strokeDevWidth);
+ croppedDevShape = GrShape(croppedDevPath, GrStyle(devStroke, nullptr));
+ conservativeDevBounds = croppedDevPath.getBounds();
+ conservativeDevBounds.outset(conservativeInflationRadius, conservativeInflationRadius);
+ }
+
+ // FIXME: This breaks local coords: http://skbug.com/8003
+ return InternalMake(context, clipIBounds, SkMatrix::I(), croppedDevShape, strokeDevWidth,
+ conservativeDevBounds, std::move(paint));
+ }
+
+ return InternalMake(context, clipIBounds, m, shape, strokeDevWidth, conservativeDevBounds,
+ std::move(paint));
+}
+
+std::unique_ptr<GrCCDrawPathsOp> GrCCDrawPathsOp::InternalMake(
+ GrContext* context, const SkIRect& clipIBounds, const SkMatrix& m, const GrShape& shape,
+ float strokeDevWidth, const SkRect& conservativeDevBounds, GrPaint&& paint) {
+ SkIRect shapeConservativeIBounds;
+ conservativeDevBounds.roundOut(&shapeConservativeIBounds);
SkIRect maskDevIBounds;
Visibility maskVisibility;
- if (clipIBounds.contains(shapeDevIBounds)) {
- maskDevIBounds = shapeDevIBounds;
+ if (clipIBounds.contains(shapeConservativeIBounds)) {
+ maskDevIBounds = shapeConservativeIBounds;
maskVisibility = Visibility::kComplete;
} else {
- if (!maskDevIBounds.intersect(clipIBounds, shapeDevIBounds)) {
+ if (!maskDevIBounds.intersect(clipIBounds, shapeConservativeIBounds)) {
return nullptr;
}
- int64_t unclippedArea = area(shapeDevIBounds);
+ int64_t unclippedArea = area(shapeConservativeIBounds);
int64_t clippedArea = area(maskDevIBounds);
maskVisibility = (clippedArea >= unclippedArea/2 || unclippedArea < 100*100)
? Visibility::kMostlyComplete // i.e., visible enough to justify rendering the
@@ -56,22 +113,24 @@
GrOpMemoryPool* pool = context->contextPriv().opMemoryPool();
- return pool->allocate<GrCCDrawPathsOp>(m, shape, shapeDevIBounds, maskDevIBounds,
- maskVisibility, devBounds, std::move(paint));
+ return pool->allocate<GrCCDrawPathsOp>(m, shape, strokeDevWidth, shapeConservativeIBounds,
+ maskDevIBounds, maskVisibility, conservativeDevBounds,
+ std::move(paint));
}
-GrCCDrawPathsOp::GrCCDrawPathsOp(const SkMatrix& m, const GrShape& shape,
- const SkIRect& shapeDevIBounds, const SkIRect& maskDevIBounds,
- Visibility maskVisibility, const SkRect& devBounds,
- GrPaint&& paint)
+GrCCDrawPathsOp::GrCCDrawPathsOp(const SkMatrix& m, const GrShape& shape, float strokeDevWidth,
+ const SkIRect& shapeConservativeIBounds,
+ const SkIRect& maskDevIBounds, Visibility maskVisibility,
+ const SkRect& conservativeDevBounds, GrPaint&& paint)
: GrDrawOp(ClassID())
, fViewMatrixIfUsingLocalCoords(has_coord_transforms(paint) ? m : SkMatrix::I())
- , fDraws(m, shape, shapeDevIBounds, maskDevIBounds, maskVisibility, paint.getColor())
+ , fDraws(m, shape, strokeDevWidth, shapeConservativeIBounds, maskDevIBounds, maskVisibility,
+ paint.getColor())
, fProcessors(std::move(paint)) { // Paint must be moved after fetching its color above.
SkDEBUGCODE(fBaseInstance = -1);
// FIXME: intersect with clip bounds to (hopefully) improve batching.
// (This is nontrivial due to assumptions in generating the octagon cover geometry.)
- this->setBounds(devBounds, GrOp::HasAABloat::kYes, GrOp::IsZeroArea::kNo);
+ this->setBounds(conservativeDevBounds, GrOp::HasAABloat::kYes, GrOp::IsZeroArea::kNo);
}
GrCCDrawPathsOp::~GrCCDrawPathsOp() {
@@ -82,12 +141,14 @@
}
GrCCDrawPathsOp::SingleDraw::SingleDraw(const SkMatrix& m, const GrShape& shape,
- const SkIRect& shapeDevIBounds,
+ float strokeDevWidth,
+ const SkIRect& shapeConservativeIBounds,
const SkIRect& maskDevIBounds, Visibility maskVisibility,
GrColor color)
: fMatrix(m)
, fShape(shape)
- , fShapeDevIBounds(shapeDevIBounds)
+ , fStrokeDevWidth(strokeDevWidth)
+ , fShapeConservativeIBounds(shapeConservativeIBounds)
, fMaskDevIBounds(maskDevIBounds)
, fMaskVisibility(maskVisibility)
, fColor(color) {
@@ -111,9 +172,39 @@
GrDrawOp::RequiresDstTexture GrCCDrawPathsOp::finalize(const GrCaps& caps,
const GrAppliedClip* clip) {
SkASSERT(1 == fNumDraws); // There should only be one single path draw in this Op right now.
- GrProcessorSet::Analysis analysis =
- fProcessors.finalize(fDraws.head().fColor, GrProcessorAnalysisCoverage::kSingleChannel,
- clip, false, caps, &fDraws.head().fColor);
+ SingleDraw* draw = &fDraws.head();
+
+ const GrProcessorSet::Analysis& analysis = fProcessors.finalize(
+ draw->fColor, GrProcessorAnalysisCoverage::kSingleChannel, clip, false, caps,
+ &draw->fColor);
+
+ // Lines start looking jagged when they get thinner than 1px. For thin strokes it looks better
+ // if we can convert them to hairline (i.e., inflate the stroke width to 1px), and instead
+ // reduce the opacity to create the illusion of thin-ness. This strategy also helps reduce
+ // artifacts from coverage dilation when there are self intersections.
+ if (analysis.isCompatibleWithCoverageAsAlpha() &&
+ !draw->fShape.style().strokeRec().isFillStyle() && draw->fStrokeDevWidth < 1) {
+ // Modifying the shape affects its cache key. The draw can't have a cache entry yet or else
+ // our next step would invalidate it.
+ SkASSERT(!draw->fCacheEntry);
+ SkASSERT(SkStrokeRec::kStroke_Style == draw->fShape.style().strokeRec().getStyle());
+
+ SkPath path;
+ draw->fShape.asPath(&path);
+
+ // Create a hairline version of our stroke.
+ SkStrokeRec hairlineStroke = draw->fShape.style().strokeRec();
+ hairlineStroke.setStrokeStyle(0);
+
+ // How transparent does a 1px stroke have to be in order to appear as thin as the real one?
+ GrColor coverageAsAlpha = GrColorPackA4(SkScalarFloorToInt(draw->fStrokeDevWidth * 255));
+
+ draw->fShape = GrShape(path, GrStyle(hairlineStroke, nullptr));
+ draw->fStrokeDevWidth = 1;
+ // fShapeConservativeIBounds already accounted for this possibility of inflating the stroke.
+ draw->fColor = GrColorMul(draw->fColor, coverageAsAlpha);
+ }
+
return RequiresDstTexture(analysis.requiresDstTexture());
}
@@ -180,8 +271,11 @@
// can copy it into a new 8-bit atlas and keep it in the resource cache.
if (stashedAtlasKey.isValid() && stashedAtlasKey == cacheEntry->atlasKey()) {
SkASSERT(!cacheEntry->hasCachedAtlas());
- ++specs->fNumCopiedPaths;
- specs->fCopyPathStats.statPath(path);
+ int idx = (draw.fShape.style().strokeRec().isFillStyle())
+ ? GrCCPerFlushResourceSpecs::kFillIdx
+ : GrCCPerFlushResourceSpecs::kStrokeIdx;
+ ++specs->fNumCopiedPaths[idx];
+ specs->fCopyPathStats[idx].statPath(path);
specs->fCopyAtlasSpecs.accountForSpace(cacheEntry->width(),
cacheEntry->height());
continue;
@@ -191,18 +285,23 @@
cacheEntry->resetAtlasKeyAndInfo();
}
- if (Visibility::kMostlyComplete == draw.fMaskVisibility && cacheEntry->hitCount() > 1 &&
- SkTMax(draw.fShapeDevIBounds.height(),
- draw.fShapeDevIBounds.width()) <= onFlushRP->caps()->maxRenderTargetSize()) {
- // We've seen this path before with a compatible matrix, and it's mostly visible.
- // Just render the whole mask so we can try to cache it.
- draw.fMaskDevIBounds = draw.fShapeDevIBounds;
- draw.fMaskVisibility = Visibility::kComplete;
+ if (Visibility::kMostlyComplete == draw.fMaskVisibility && cacheEntry->hitCount() > 1) {
+ int shapeSize = SkTMax(draw.fShapeConservativeIBounds.height(),
+ draw.fShapeConservativeIBounds.width());
+ if (shapeSize <= onFlushRP->caps()->maxRenderTargetSize()) {
+ // We've seen this path before with a compatible matrix, and it's mostly
+ // visible. Just render the whole mask so we can try to cache it.
+ draw.fMaskDevIBounds = draw.fShapeConservativeIBounds;
+ draw.fMaskVisibility = Visibility::kComplete;
+ }
}
}
- ++specs->fNumRenderedPaths;
- specs->fRenderedPathStats.statPath(path);
+ int idx = (draw.fShape.style().strokeRec().isFillStyle())
+ ? GrCCPerFlushResourceSpecs::kFillIdx
+ : GrCCPerFlushResourceSpecs::kStrokeIdx;
+ ++specs->fNumRenderedPaths[idx];
+ specs->fRenderedPathStats[idx].statPath(path);
specs->fRenderedAtlasSpecs.accountForSpace(draw.fMaskDevIBounds.width(),
draw.fMaskDevIBounds.height());
}
@@ -219,7 +318,8 @@
SkPath path;
draw.fShape.asPath(&path);
- auto doEvenOddFill = DoEvenOddFill(SkPath::kEvenOdd_FillType == path.getFillType());
+ auto doEvenOddFill = DoEvenOddFill(draw.fShape.style().strokeRec().isFillStyle() &&
+ SkPath::kEvenOdd_FillType == path.getFillType());
SkASSERT(SkPath::kEvenOdd_FillType == path.getFillType() ||
SkPath::kWinding_FillType == path.getFillType());
@@ -270,9 +370,9 @@
SkRect devBounds, devBounds45;
SkIRect devIBounds;
SkIVector devToAtlasOffset;
- if (auto atlas = resources->renderPathInAtlas(draw.fMaskDevIBounds, draw.fMatrix, path,
- &devBounds, &devBounds45, &devIBounds,
- &devToAtlasOffset)) {
+ if (auto atlas = resources->renderShapeInAtlas(
+ draw.fMaskDevIBounds, draw.fMatrix, draw.fShape, draw.fStrokeDevWidth,
+ &devBounds, &devBounds45, &devIBounds, &devToAtlasOffset)) {
this->recordInstance(atlas->textureProxy(), resources->nextPathInstanceIdx());
resources->appendDrawPathInstance().set(devBounds, devBounds45, devToAtlasOffset,
draw.fColor, doEvenOddFill);