1/*
2 * Copyright 2018 Google Inc.
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/ccpr/GrCCDrawPathsOp.h"
9
10#include "include/private/GrRecordingContext.h"
11#include "src/gpu/GrMemoryPool.h"
12#include "src/gpu/GrOpFlushState.h"
13#include "src/gpu/GrRecordingContextPriv.h"
14#include "src/gpu/ccpr/GrCCPathCache.h"
15#include "src/gpu/ccpr/GrCCPerFlushResources.h"
16#include "src/gpu/ccpr/GrCoverageCountingPathRenderer.h"
17#include "src/gpu/ccpr/GrOctoBounds.h"
18
19static bool has_coord_transforms(const GrPaint& paint) {
20 for (const auto& fp : GrFragmentProcessor::PaintCRange(paint)) {
21 if (!fp.coordTransforms().empty()) {
22 return true;
23 }
24 }
25 return false;
26}
27
28std::unique_ptr<GrCCDrawPathsOp> GrCCDrawPathsOp::Make(
29 GrRecordingContext* context, const SkIRect& clipIBounds, const SkMatrix& m,
30 const GrShape& shape, GrPaint&& paint) {
31 SkRect conservativeDevBounds;
32 m.mapRect(&conservativeDevBounds, shape.bounds());
33
34 const SkStrokeRec& stroke = shape.style().strokeRec();
35 float strokeDevWidth = 0;
36 float conservativeInflationRadius = 0;
37 if (!stroke.isFillStyle()) {
38 strokeDevWidth = GrCoverageCountingPathRenderer::GetStrokeDevWidth(
39 m, stroke, &conservativeInflationRadius);
40 conservativeDevBounds.outset(conservativeInflationRadius, conservativeInflationRadius);
41 }
42
43 std::unique_ptr<GrCCDrawPathsOp> op;
44 float conservativeSize = std::max(conservativeDevBounds.height(), conservativeDevBounds.width());
45 if (conservativeSize > GrCoverageCountingPathRenderer::kPathCropThreshold) {
46 // The path is too large. Crop it or analytic AA can run out of fp32 precision.
47 SkPath croppedDevPath;
48 shape.asPath(&croppedDevPath);
49 croppedDevPath.transform(m, &croppedDevPath);
50
51 SkIRect cropBox = clipIBounds;
52 GrShape croppedDevShape;
53 if (stroke.isFillStyle()) {
54 GrCoverageCountingPathRenderer::CropPath(croppedDevPath, cropBox, &croppedDevPath);
55 croppedDevShape = GrShape(croppedDevPath);
56 conservativeDevBounds = croppedDevShape.bounds();
57 } else {
58 int r = SkScalarCeilToInt(conservativeInflationRadius);
59 cropBox.outset(r, r);
60 GrCoverageCountingPathRenderer::CropPath(croppedDevPath, cropBox, &croppedDevPath);
61 SkStrokeRec devStroke = stroke;
62 devStroke.setStrokeStyle(strokeDevWidth);
63 croppedDevShape = GrShape(croppedDevPath, GrStyle(devStroke, nullptr));
64 conservativeDevBounds = croppedDevPath.getBounds();
65 conservativeDevBounds.outset(conservativeInflationRadius, conservativeInflationRadius);
66 }
67
68 // FIXME: This breaks local coords: http://skbug.com/8003
69 return InternalMake(context, clipIBounds, SkMatrix::I(), croppedDevShape, strokeDevWidth,
70 conservativeDevBounds, std::move(paint));
71 }
72
73 return InternalMake(context, clipIBounds, m, shape, strokeDevWidth, conservativeDevBounds,
74 std::move(paint));
75}
76
77std::unique_ptr<GrCCDrawPathsOp> GrCCDrawPathsOp::InternalMake(
78 GrRecordingContext* context, const SkIRect& clipIBounds, const SkMatrix& m,
79 const GrShape& shape, float strokeDevWidth, const SkRect& conservativeDevBounds,
80 GrPaint&& paint) {
81 // The path itself should have been cropped if larger than kPathCropThreshold. If it had a
82 // stroke, that would have further inflated its draw bounds.
83 SkASSERT(std::max(conservativeDevBounds.height(), conservativeDevBounds.width()) <
84 GrCoverageCountingPathRenderer::kPathCropThreshold +
85 GrCoverageCountingPathRenderer::kMaxBoundsInflationFromStroke*2 + 1);
86
87 SkIRect shapeConservativeIBounds;
88 conservativeDevBounds.roundOut(&shapeConservativeIBounds);
89
90 SkIRect maskDevIBounds;
91 if (!maskDevIBounds.intersect(clipIBounds, shapeConservativeIBounds)) {
92 return nullptr;
93 }
94
95 GrOpMemoryPool* pool = context->priv().opMemoryPool();
96 return pool->allocate<GrCCDrawPathsOp>(m, shape, strokeDevWidth, shapeConservativeIBounds,
97 maskDevIBounds, conservativeDevBounds, std::move(paint));
98}
99
100GrCCDrawPathsOp::GrCCDrawPathsOp(const SkMatrix& m, const GrShape& shape, float strokeDevWidth,
101 const SkIRect& shapeConservativeIBounds,
102 const SkIRect& maskDevIBounds, const SkRect& conservativeDevBounds,
103 GrPaint&& paint)
104 : GrDrawOp(ClassID())
105 , fViewMatrixIfUsingLocalCoords(has_coord_transforms(paint) ? m : SkMatrix::I())
106 , fDraws(m, shape, strokeDevWidth, shapeConservativeIBounds, maskDevIBounds,
107 paint.getColor4f())
108 , fProcessors(std::move(paint)) { // Paint must be moved after fetching its color above.
109 SkDEBUGCODE(fBaseInstance = -1);
110 // If the path is clipped, CCPR will only draw the visible portion. This helps improve batching,
111 // since it eliminates the need for scissor when drawing to the main canvas.
112 // FIXME: We should parse the path right here. It will provide a tighter bounding box for us to
113 // give the opsTask, as well as enabling threaded parsing when using DDL.
114 SkRect clippedDrawBounds;
115 if (!clippedDrawBounds.intersect(conservativeDevBounds, SkRect::Make(maskDevIBounds))) {
116 clippedDrawBounds.setEmpty();
117 }
118 // We always have AA bloat, even in MSAA atlas mode. This is because by the time this Op comes
119 // along and draws to the main canvas, the atlas has been resolved to analytic coverage.
120 this->setBounds(clippedDrawBounds, GrOp::HasAABloat::kYes, GrOp::IsHairline::kNo);
121}
122
123GrCCDrawPathsOp::~GrCCDrawPathsOp() {
124 if (fOwningPerOpsTaskPaths) {
125 // Remove the list's dangling pointer to this Op before deleting it.
126 fOwningPerOpsTaskPaths->fDrawOps.remove(this);
127 }
128}
129
130GrCCDrawPathsOp::SingleDraw::SingleDraw(const SkMatrix& m, const GrShape& shape,
131 float strokeDevWidth,
132 const SkIRect& shapeConservativeIBounds,
133 const SkIRect& maskDevIBounds, const SkPMColor4f& color)
134 : fMatrix(m)
135 , fShape(shape)
136 , fStrokeDevWidth(strokeDevWidth)
137 , fShapeConservativeIBounds(shapeConservativeIBounds)
138 , fMaskDevIBounds(maskDevIBounds)
139 , fColor(color) {
140#ifdef SK_BUILD_FOR_ANDROID_FRAMEWORK
141 if (fShape.hasUnstyledKey()) {
142 // On AOSP we round view matrix translates to integer values for cachable paths. We do this
143 // to match HWUI's cache hit ratio, which doesn't consider the matrix when caching paths.
144 fMatrix.setTranslateX(SkScalarRoundToScalar(fMatrix.getTranslateX()));
145 fMatrix.setTranslateY(SkScalarRoundToScalar(fMatrix.getTranslateY()));
146 }
147#endif
148}
149
150GrProcessorSet::Analysis GrCCDrawPathsOp::finalize(
151 const GrCaps& caps, const GrAppliedClip* clip, bool hasMixedSampledCoverage,
152 GrClampType clampType) {
153 SkASSERT(1 == fNumDraws); // There should only be one single path draw in this Op right now.
154 return fDraws.head().finalize(caps, clip, hasMixedSampledCoverage, clampType, &fProcessors);
155}
156
157GrProcessorSet::Analysis GrCCDrawPathsOp::SingleDraw::finalize(
158 const GrCaps& caps, const GrAppliedClip* clip, bool hasMixedSampledCoverage, GrClampType
159 clampType, GrProcessorSet* processors) {
160 const GrProcessorSet::Analysis& analysis = processors->finalize(
161 fColor, GrProcessorAnalysisCoverage::kSingleChannel, clip,
162 &GrUserStencilSettings::kUnused, hasMixedSampledCoverage, caps, clampType, &fColor);
163
164 // Lines start looking jagged when they get thinner than 1px. For thin strokes it looks better
165 // if we can convert them to hairline (i.e., inflate the stroke width to 1px), and instead
166 // reduce the opacity to create the illusion of thin-ness. This strategy also helps reduce
167 // artifacts from coverage dilation when there are self intersections.
168 if (analysis.isCompatibleWithCoverageAsAlpha() &&
169 !fShape.style().strokeRec().isFillStyle() && fStrokeDevWidth < 1) {
170 // Modifying the shape affects its cache key. The draw can't have a cache entry yet or else
171 // our next step would invalidate it.
172 SkASSERT(!fCacheEntry);
173 SkASSERT(SkStrokeRec::kStroke_Style == fShape.style().strokeRec().getStyle());
174
175 SkPath path;
176 fShape.asPath(&path);
177
178 // Create a hairline version of our stroke.
179 SkStrokeRec hairlineStroke = fShape.style().strokeRec();
180 hairlineStroke.setStrokeStyle(0);
181
182 // How transparent does a 1px stroke have to be in order to appear as thin as the real one?
183 float coverage = fStrokeDevWidth;
184
185 fShape = GrShape(path, GrStyle(hairlineStroke, nullptr));
186 fStrokeDevWidth = 1;
187
188 // fShapeConservativeIBounds already accounted for this possibility of inflating the stroke.
189 fColor = fColor * coverage;
190 }
191
192 return analysis;
193}
194
195GrOp::CombineResult GrCCDrawPathsOp::onCombineIfPossible(GrOp* op, GrRecordingContext::Arenas*,
196 const GrCaps&) {
197 GrCCDrawPathsOp* that = op->cast<GrCCDrawPathsOp>();
198 SkASSERT(fOwningPerOpsTaskPaths);
199 SkASSERT(fNumDraws);
200 SkASSERT(!that->fOwningPerOpsTaskPaths ||
201 that->fOwningPerOpsTaskPaths == fOwningPerOpsTaskPaths);
202 SkASSERT(that->fNumDraws);
203
204 if (fProcessors != that->fProcessors ||
205 fViewMatrixIfUsingLocalCoords != that->fViewMatrixIfUsingLocalCoords) {
206 return CombineResult::kCannotCombine;
207 }
208
209 fDraws.append(std::move(that->fDraws), &fOwningPerOpsTaskPaths->fAllocator);
210
211 SkDEBUGCODE(fNumDraws += that->fNumDraws);
212 SkDEBUGCODE(that->fNumDraws = 0);
213 return CombineResult::kMerged;
214}
215
216void GrCCDrawPathsOp::addToOwningPerOpsTaskPaths(sk_sp<GrCCPerOpsTaskPaths> owningPerOpsTaskPaths) {
217 SkASSERT(1 == fNumDraws);
218 SkASSERT(!fOwningPerOpsTaskPaths);
219 fOwningPerOpsTaskPaths = std::move(owningPerOpsTaskPaths);
220 fOwningPerOpsTaskPaths->fDrawOps.addToTail(this);
221}
222
223void GrCCDrawPathsOp::accountForOwnPaths(GrCCPathCache* pathCache,
224 GrOnFlushResourceProvider* onFlushRP,
225 GrCCPerFlushResourceSpecs* specs) {
226 for (SingleDraw& draw : fDraws) {
227 draw.accountForOwnPath(pathCache, onFlushRP, specs);
228 }
229}
230
231void GrCCDrawPathsOp::SingleDraw::accountForOwnPath(
232 GrCCPathCache* pathCache, GrOnFlushResourceProvider* onFlushRP,
233 GrCCPerFlushResourceSpecs* specs) {
234 using CoverageType = GrCCAtlas::CoverageType;
235
236 SkPath path;
237 fShape.asPath(&path);
238
239 SkASSERT(!fCacheEntry);
240
241 if (pathCache) {
242 fCacheEntry = pathCache->find(
243 onFlushRP, fShape, fMaskDevIBounds, fMatrix, &fCachedMaskShift);
244 }
245
246 if (fCacheEntry) {
247 if (const GrCCCachedAtlas* cachedAtlas = fCacheEntry->cachedAtlas()) {
248 SkASSERT(cachedAtlas->getOnFlushProxy());
249 if (CoverageType::kA8_LiteralCoverage == cachedAtlas->coverageType()) {
250 ++specs->fNumCachedPaths;
251 } else {
252 // Suggest that this path be copied to a literal coverage atlas, to save memory.
253 // (The client may decline this copy via DoCopiesToA8Coverage::kNo.)
254 int idx = (fShape.style().strokeRec().isFillStyle())
255 ? GrCCPerFlushResourceSpecs::kFillIdx
256 : GrCCPerFlushResourceSpecs::kStrokeIdx;
257 ++specs->fNumCopiedPaths[idx];
258 specs->fCopyPathStats[idx].statPath(path);
259 specs->fCopyAtlasSpecs.accountForSpace(fCacheEntry->width(), fCacheEntry->height());
260 fDoCopyToA8Coverage = true;
261 }
262 return;
263 }
264
265 if (this->shouldCachePathMask(onFlushRP->caps()->maxRenderTargetSize())) {
266 fDoCachePathMask = true;
267 // We don't cache partial masks; ensure the bounds include the entire path.
268 fMaskDevIBounds = fShapeConservativeIBounds;
269 }
270 }
271
272 // Plan on rendering this path in a new atlas.
273 int idx = (fShape.style().strokeRec().isFillStyle())
274 ? GrCCPerFlushResourceSpecs::kFillIdx
275 : GrCCPerFlushResourceSpecs::kStrokeIdx;
276 ++specs->fNumRenderedPaths[idx];
277 specs->fRenderedPathStats[idx].statPath(path);
278 specs->fRenderedAtlasSpecs.accountForSpace(fMaskDevIBounds.width(), fMaskDevIBounds.height());
279 SkDEBUGCODE(fWasCountedAsRender = true);
280}
281
282bool GrCCDrawPathsOp::SingleDraw::shouldCachePathMask(int maxRenderTargetSize) const {
283 SkASSERT(fCacheEntry);
284 SkASSERT(!fCacheEntry->cachedAtlas());
285 if (fCacheEntry->hitCount() <= 1) {
286 return false; // Don't cache a path mask until at least its second hit.
287 }
288
289 int shapeMaxDimension = std::max(
290 fShapeConservativeIBounds.height(), fShapeConservativeIBounds.width());
291 if (shapeMaxDimension > maxRenderTargetSize) {
292 return false; // This path isn't cachable.
293 }
294
295 int64_t shapeArea = sk_64_mul(
296 fShapeConservativeIBounds.height(), fShapeConservativeIBounds.width());
297 if (shapeArea < 100*100) {
298 // If a path is small enough, we might as well try to render and cache the entire thing, no
299 // matter how much of it is actually visible.
300 return true;
301 }
302
303 // The hitRect should already be contained within the shape's bounds, but we still intersect it
304 // because it's possible for edges very near pixel boundaries (e.g., 0.999999), to round out
305 // inconsistently, depending on the integer translation values and fp32 precision.
306 SkIRect hitRect = fCacheEntry->hitRect().makeOffset(fCachedMaskShift);
307 hitRect.intersect(fShapeConservativeIBounds);
308
309 // Render and cache the entire path mask if we see enough of it to justify rendering all the
310 // pixels. Our criteria for "enough" is that we must have seen at least 50% of the path in the
311 // past, and in this particular draw we must see at least 10% of it.
312 int64_t hitArea = sk_64_mul(hitRect.height(), hitRect.width());
313 int64_t drawArea = sk_64_mul(fMaskDevIBounds.height(), fMaskDevIBounds.width());
314 return hitArea*2 >= shapeArea && drawArea*10 >= shapeArea;
315}
316
317void GrCCDrawPathsOp::setupResources(
318 GrCCPathCache* pathCache, GrOnFlushResourceProvider* onFlushRP,
319 GrCCPerFlushResources* resources, DoCopiesToA8Coverage doCopies) {
320 SkASSERT(fNumDraws > 0);
321 SkASSERT(-1 == fBaseInstance);
322 fBaseInstance = resources->nextPathInstanceIdx();
323
324 for (SingleDraw& draw : fDraws) {
325 draw.setupResources(pathCache, onFlushRP, resources, doCopies, this);
326 }
327
328 if (!fInstanceRanges.empty()) {
329 fInstanceRanges.back().fEndInstanceIdx = resources->nextPathInstanceIdx();
330 }
331}
332
333void GrCCDrawPathsOp::SingleDraw::setupResources(
334 GrCCPathCache* pathCache, GrOnFlushResourceProvider* onFlushRP,
335 GrCCPerFlushResources* resources, DoCopiesToA8Coverage doCopies, GrCCDrawPathsOp* op) {
336 SkPath path;
337 fShape.asPath(&path);
338
339 auto fillRule = (fShape.style().strokeRec().isFillStyle())
340 ? GrFillRuleForSkPath(path)
341 : GrFillRule::kNonzero;
342
343 if (fCacheEntry) {
344 // Does the path already exist in a cached atlas texture?
345 if (fCacheEntry->cachedAtlas()) {
346 SkASSERT(fCacheEntry->cachedAtlas()->getOnFlushProxy());
347 if (DoCopiesToA8Coverage::kYes == doCopies && fDoCopyToA8Coverage) {
348 resources->upgradeEntryToLiteralCoverageAtlas(
349 pathCache, onFlushRP, fCacheEntry.get(), fillRule);
350 SkASSERT(fCacheEntry->cachedAtlas());
351 SkASSERT(GrCCAtlas::CoverageType::kA8_LiteralCoverage
352 == fCacheEntry->cachedAtlas()->coverageType());
353 SkASSERT(fCacheEntry->cachedAtlas()->getOnFlushProxy());
354 }
355#if 0
356 // Simple color manipulation to visualize cached paths.
357 fColor = (GrCCAtlas::CoverageType::kA8_LiteralCoverage
358 == fCacheEntry->cachedAtlas()->coverageType())
359 ? SkPMColor4f{0,0,.25,.25} : SkPMColor4f{0,.25,0,.25};
360#endif
361 auto coverageMode = GrCCAtlas::CoverageTypeToPathCoverageMode(
362 fCacheEntry->cachedAtlas()->coverageType());
363 op->recordInstance(coverageMode, fCacheEntry->cachedAtlas()->getOnFlushProxy(),
364 resources->nextPathInstanceIdx());
365 resources->appendDrawPathInstance().set(*fCacheEntry, fCachedMaskShift, fColor,
366 fillRule);
367#ifdef SK_DEBUG
368 if (fWasCountedAsRender) {
369 // A path mask didn't exist for this path at the beginning of flush, but we have one
370 // now. What this means is that we've drawn the same path multiple times this flush.
371 // Let the resources know that we reused one for their internal debug counters.
372 resources->debugOnly_didReuseRenderedPath();
373 }
374#endif
375 return;
376 }
377 }
378
379 // Render the raw path into a coverage count atlas. renderShapeInAtlas() gives us two tight
380 // bounding boxes: One in device space, as well as a second one rotated an additional 45
381 // degrees. The path vertex shader uses these two bounding boxes to generate an octagon that
382 // circumscribes the path.
383 GrOctoBounds octoBounds;
384 SkIRect devIBounds;
385 SkIVector devToAtlasOffset;
386 if (auto atlas = resources->renderShapeInAtlas(
387 fMaskDevIBounds, fMatrix, fShape, fStrokeDevWidth, &octoBounds, &devIBounds,
388 &devToAtlasOffset)) {
389 auto coverageMode = GrCCAtlas::CoverageTypeToPathCoverageMode(
390 resources->renderedPathCoverageType());
391 op->recordInstance(coverageMode, atlas->textureProxy(), resources->nextPathInstanceIdx());
392 resources->appendDrawPathInstance().set(octoBounds, devToAtlasOffset, fColor, fillRule);
393
394 if (fDoCachePathMask) {
395 SkASSERT(fCacheEntry);
396 SkASSERT(!fCacheEntry->cachedAtlas());
397 SkASSERT(fShapeConservativeIBounds == fMaskDevIBounds);
398 fCacheEntry->setCoverageCountAtlas(
399 onFlushRP, atlas, devToAtlasOffset, octoBounds, devIBounds, fCachedMaskShift);
400 }
401 }
402}
403
404inline void GrCCDrawPathsOp::recordInstance(
405 GrCCPathProcessor::CoverageMode coverageMode, GrTextureProxy* atlasProxy, int instanceIdx) {
406 if (fInstanceRanges.empty()) {
407 fInstanceRanges.push_back({coverageMode, atlasProxy, instanceIdx});
408 } else if (fInstanceRanges.back().fAtlasProxy != atlasProxy) {
409 fInstanceRanges.back().fEndInstanceIdx = instanceIdx;
410 fInstanceRanges.push_back({coverageMode, atlasProxy, instanceIdx});
411 }
412 SkASSERT(fInstanceRanges.back().fCoverageMode == coverageMode);
413 SkASSERT(fInstanceRanges.back().fAtlasProxy == atlasProxy);
414}
415
416void GrCCDrawPathsOp::onPrepare(GrOpFlushState* flushState) {
417 // The CCPR ops don't know their atlas textures until after the preFlush calls have been
418 // executed at the start GrDrawingManger::flush. Thus the proxies are not added during the
419 // normal visitProxies calls doing addDrawOp. Therefore, the atlas proxies are added now.
420 for (const InstanceRange& range : fInstanceRanges) {
421 flushState->sampledProxyArray()->push_back(range.fAtlasProxy);
422 }
423}
424
425void GrCCDrawPathsOp::onExecute(GrOpFlushState* flushState, const SkRect& chainBounds) {
426 SkASSERT(fOwningPerOpsTaskPaths);
427
428 const GrCCPerFlushResources* resources = fOwningPerOpsTaskPaths->fFlushResources.get();
429 if (!resources) {
430 return; // Setup failed.
431 }
432
433 GrPipeline::InitArgs initArgs;
434 initArgs.fCaps = &flushState->caps();
435 initArgs.fDstProxyView = flushState->drawOpArgs().dstProxyView();
436 initArgs.fWriteSwizzle = flushState->drawOpArgs().writeSwizzle();
437 auto clip = flushState->detachAppliedClip();
438 GrPipeline pipeline(initArgs, std::move(fProcessors), std::move(clip));
439
440 int baseInstance = fBaseInstance;
441 SkASSERT(baseInstance >= 0); // Make sure setupResources() has been called.
442
443 for (const InstanceRange& range : fInstanceRanges) {
444 SkASSERT(range.fEndInstanceIdx > baseInstance);
445
446 GrSurfaceProxy* atlas = range.fAtlasProxy;
447 if (atlas->isInstantiated()) { // Instantiation can fail in exceptional circumstances.
448 GrColorType ct = GrCCPathProcessor::GetColorTypeFromCoverageMode(range.fCoverageMode);
449 GrSwizzle swizzle = flushState->caps().getReadSwizzle(atlas->backendFormat(), ct);
450 GrCCPathProcessor pathProc(range.fCoverageMode, atlas->peekTexture(), swizzle,
451 GrCCAtlas::kTextureOrigin, fViewMatrixIfUsingLocalCoords);
452 pathProc.drawPaths(flushState, pipeline, *atlas, *resources, baseInstance,
453 range.fEndInstanceIdx, this->bounds());
454 }
455
456 baseInstance = range.fEndInstanceIdx;
457 }
458}
459