diff options
author | Brian Salomon <bsalomon@google.com> | 2018-04-20 13:54:11 -0400 |
---|---|---|
committer | Skia Commit-Bot <skia-commit-bot@chromium.org> | 2018-04-20 18:45:35 +0000 |
commit | 62e4f3daf7262463774ca0434a9232a8e2292350 (patch) | |
tree | 0402b8e6fb48469d3dc23c697ecc4fda9ee39fb2 | |
parent | 2fda63abcdd4ba7ee41e5226b68c93b102b76fb2 (diff) |
Analytic dashing of circles with single on/off intervals and butt caps.
Change-Id: If19ac52cb78af57572a102cec0084f5b6c037680
Reviewed-on: https://skia-review.googlesource.com/121882
Auto-Submit: Brian Salomon <bsalomon@google.com>
Reviewed-by: Greg Daniel <egdaniel@google.com>
Commit-Queue: Brian Salomon <bsalomon@google.com>
-rw-r--r-- | gm/dashcircle.cpp | 117 | ||||
-rw-r--r-- | src/gpu/GrProcessor.h | 1 | ||||
-rw-r--r-- | src/gpu/GrRenderTargetContext.cpp | 15 | ||||
-rw-r--r-- | src/gpu/SkGpuDevice.cpp | 8 | ||||
-rw-r--r-- | src/gpu/ops/GrOvalOpFactory.cpp | 593 | ||||
-rw-r--r-- | src/gpu/ops/GrOvalOpFactory.h | 2 |
6 files changed, 712 insertions, 24 deletions
diff --git a/gm/dashcircle.cpp b/gm/dashcircle.cpp index fbe08b7bbe..72e3d1c549 100644 --- a/gm/dashcircle.cpp +++ b/gm/dashcircle.cpp @@ -108,3 +108,120 @@ private: }; DEF_GM(return new DashCircleGM; ) + +class DashCircle2GM : public skiagm::GM { +public: + DashCircle2GM() {} + +protected: + SkString onShortName() override { return SkString("dashcircle2"); } + + SkISize onISize() override { return SkISize::Make(635, 900); } + + void onDraw(SkCanvas* canvas) override { + // These intervals are defined relative to tau. + static constexpr SkScalar kIntervals[][2]{ + {0.333f, 0.333f}, + {0.015f, 0.015f}, + {0.01f , 0.09f }, + {0.097f, 0.003f}, + {0.02f , 0.04f }, + {0.1f , 0.2f }, + {0.25f , 0.25f }, + {0.6f , 0.7f }, // adds to > 1 + {1.2f , 0.8f }, // on is > 1 + {0.1f , 1.1f }, // off is > 1*/ + }; + + static constexpr int kN = SK_ARRAY_COUNT(kIntervals); + static constexpr SkScalar kRadius = 20.f; + static constexpr SkScalar kStrokeWidth = 15.f; + static constexpr SkScalar kPad = 5.f; + static constexpr SkRect kCircle = {-kRadius, -kRadius, kRadius, kRadius}; + + static constexpr SkScalar kThinRadius = kRadius * 1.5; + static constexpr SkRect kThinCircle = {-kThinRadius, -kThinRadius, + kThinRadius, kThinRadius}; + static constexpr SkScalar kThinStrokeWidth = 0.4f; + + sk_sp<SkPathEffect> deffects[SK_ARRAY_COUNT(kIntervals)]; + sk_sp<SkPathEffect> thinDEffects[SK_ARRAY_COUNT(kIntervals)]; + for (int i = 0; i < kN; ++i) { + static constexpr SkScalar kTau = 2 * SK_ScalarPI; + static constexpr SkScalar kCircumference = kRadius * kTau; + SkScalar scaledIntervals[2] = {kCircumference * kIntervals[i][0], + kCircumference * kIntervals[i][1]}; + deffects[i] = SkDashPathEffect::Make( + scaledIntervals, 2, kCircumference * fPhaseDegrees * kTau / 360.f); + static constexpr SkScalar kThinCircumference = kThinRadius * kTau; + scaledIntervals[0] = kThinCircumference * kIntervals[i][0]; + scaledIntervals[1] = kThinCircumference * kIntervals[i][1]; + thinDEffects[i] = SkDashPathEffect::Make( + scaledIntervals, 2, kThinCircumference * fPhaseDegrees * kTau / 360.f); + } + + SkMatrix rotate; + rotate.setRotate(25.f); + static const SkMatrix kMatrices[]{ + SkMatrix::I(), + SkMatrix::MakeScale(1.2f), + SkMatrix::MakeAll(1, 0, 0, 0, -1, 0, 0, 0, 1), // y flipper + SkMatrix::MakeAll(-1, 0, 0, 0, 1, 0, 0, 0, 1), // x flipper + SkMatrix::MakeScale(0.7f), + rotate, + SkMatrix::Concat( + SkMatrix::Concat(SkMatrix::MakeAll(-1, 0, 0, 0, 1, 0, 0, 0, 1), rotate), + rotate) + }; + + SkPaint paint; + paint.setAntiAlias(true); + paint.setStrokeWidth(kStrokeWidth); + paint.setStyle(SkPaint::kStroke_Style); + + // Compute the union of bounds of all of our test cases. + SkRect bounds = SkRect::MakeEmpty(); + static const SkRect kBounds = kThinCircle.makeOutset(kThinStrokeWidth / 2.f, + kThinStrokeWidth / 2.f); + for (const auto& m : kMatrices) { + SkRect devBounds; + m.mapRect(&devBounds, kBounds); + bounds.join(devBounds); + } + + canvas->save(); + canvas->translate(-bounds.fLeft + kPad, -bounds.fTop + kPad); + for (size_t i = 0; i < SK_ARRAY_COUNT(deffects); ++i) { + canvas->save(); + for (const auto& m : kMatrices) { + canvas->save(); + canvas->concat(m); + + paint.setPathEffect(deffects[i]); + paint.setStrokeWidth(kStrokeWidth); + canvas->drawOval(kCircle, paint); + + paint.setPathEffect(thinDEffects[i]); + paint.setStrokeWidth(kThinStrokeWidth); + canvas->drawOval(kThinCircle, paint); + + canvas->restore(); + canvas->translate(bounds.width() + kPad, 0); + } + canvas->restore(); + canvas->translate(0, bounds.height() + kPad); + } + canvas->restore(); + } + +protected: + bool onAnimate(const SkAnimTimer& timer) override { + fPhaseDegrees = timer.secs(); + return true; + } + + // Init with a non-zero phase for when run as a non-animating GM. + SkScalar fPhaseDegrees = 12.f; +}; + +DEF_GM(return new DashCircle2GM;) diff --git a/src/gpu/GrProcessor.h b/src/gpu/GrProcessor.h index 28cb995918..64a28d83e9 100644 --- a/src/gpu/GrProcessor.h +++ b/src/gpu/GrProcessor.h @@ -68,6 +68,7 @@ public: enum ClassID { kBigKeyProcessor_ClassID, kBlockInputFragmentProcessor_ClassID, + kButtCapStrokedCircleGeometryProcessor_ClassID, kCircleGeometryProcessor_ClassID, kCircularRRectEffect_ClassID, kColorMatrixEffect_ClassID, diff --git a/src/gpu/GrRenderTargetContext.cpp b/src/gpu/GrRenderTargetContext.cpp index 6feb345ffa..3c12755de0 100644 --- a/src/gpu/GrRenderTargetContext.cpp +++ b/src/gpu/GrRenderTargetContext.cpp @@ -1162,7 +1162,8 @@ bool GrRenderTargetContext::drawFilledDRRect(const GrClip& clip, SkStrokeRec stroke(SkStrokeRec::kFill_InitStyle); stroke.setStrokeStyle(outerR - innerR); auto op = GrOvalOpFactory::MakeOvalOp(std::move(paint), viewMatrix, circleBounds, - stroke, this->caps()->shaderCaps()); + GrStyle(stroke, nullptr), + this->caps()->shaderCaps()); if (op) { this->addDrawOp(clip, std::move(op)); return true; @@ -1294,21 +1295,17 @@ void GrRenderTargetContext::drawOval(const GrClip& clip, SkDEBUGCODE(this->validate();) GR_CREATE_TRACE_MARKER_CONTEXT("GrRenderTargetContext", "drawOval", fContext); - if (oval.isEmpty()) { - return; + if (oval.isEmpty() && !style.pathEffect()) { + return; } - SkASSERT(!style.pathEffect()); // this should've been devolved to a path in SkGpuDevice - AutoCheckFlush acf(this->drawingManager()); - const SkStrokeRec& stroke = style.strokeRec(); GrAAType aaType = this->chooseAAType(aa, GrAllowMixedSamples::kNo); if (GrAAType::kCoverage == aaType) { const GrShaderCaps* shaderCaps = fContext->caps()->shaderCaps(); - std::unique_ptr<GrDrawOp> op = - GrOvalOpFactory::MakeOvalOp(std::move(paint), viewMatrix, oval, stroke, shaderCaps); - if (op) { + if (auto op = GrOvalOpFactory::MakeOvalOp(std::move(paint), viewMatrix, oval, style, + shaderCaps)) { this->addDrawOp(clip, std::move(op)); return; } diff --git a/src/gpu/SkGpuDevice.cpp b/src/gpu/SkGpuDevice.cpp index f26ed7b8ab..3a48e8cbe6 100644 --- a/src/gpu/SkGpuDevice.cpp +++ b/src/gpu/SkGpuDevice.cpp @@ -509,14 +509,6 @@ void SkGpuDevice::drawRegion(const SkRegion& region, const SkPaint& paint) { void SkGpuDevice::drawOval(const SkRect& oval, const SkPaint& paint) { ASSERT_SINGLE_OWNER GR_CREATE_TRACE_MARKER_CONTEXT("SkGpuDevice", "drawOval", fContext.get()); - // Presumably the path effect warps this to something other than an oval - if (paint.getPathEffect()) { - SkPath path; - path.setIsVolatile(true); - path.addOval(oval); - this->drawPath(path, paint, nullptr, true); - return; - } if (paint.getMaskFilter()) { // The RRect path can handle special case blurring diff --git a/src/gpu/ops/GrOvalOpFactory.cpp b/src/gpu/ops/GrOvalOpFactory.cpp index 9dc1c91e5b..2ff01f4060 100644 --- a/src/gpu/ops/GrOvalOpFactory.cpp +++ b/src/gpu/ops/GrOvalOpFactory.cpp @@ -264,6 +264,240 @@ sk_sp<GrGeometryProcessor> CircleGeometryProcessor::TestCreate(GrProcessorTestDa } #endif +class ButtCapDashedCircleGeometryProcessor : public GrGeometryProcessor { +public: + ButtCapDashedCircleGeometryProcessor(const SkMatrix& localMatrix) + : INHERITED(kButtCapStrokedCircleGeometryProcessor_ClassID), fLocalMatrix(localMatrix) { + fInPosition = &this->addVertexAttrib("inPosition", kFloat2_GrVertexAttribType); + fInColor = &this->addVertexAttrib("inColor", kUByte4_norm_GrVertexAttribType); + fInCircleEdge = &this->addVertexAttrib("inCircleEdge", kFloat4_GrVertexAttribType); + fInDashParams = &this->addVertexAttrib("inDashParams", kFloat4_GrVertexAttribType); + } + + ~ButtCapDashedCircleGeometryProcessor() override {} + + const char* name() const override { return "ButtCapDashedCircleGeometryProcessor"; } + + void getGLSLProcessorKey(const GrShaderCaps& caps, GrProcessorKeyBuilder* b) const override { + GLSLProcessor::GenKey(*this, caps, b); + } + + GrGLSLPrimitiveProcessor* createGLSLInstance(const GrShaderCaps&) const override { + return new GLSLProcessor(); + } + +private: + class GLSLProcessor : public GrGLSLGeometryProcessor { + public: + GLSLProcessor() {} + + void onEmitCode(EmitArgs& args, GrGPArgs* gpArgs) override { + const ButtCapDashedCircleGeometryProcessor& bcscgp = + args.fGP.cast<ButtCapDashedCircleGeometryProcessor>(); + GrGLSLVertexBuilder* vertBuilder = args.fVertBuilder; + GrGLSLVaryingHandler* varyingHandler = args.fVaryingHandler; + GrGLSLUniformHandler* uniformHandler = args.fUniformHandler; + GrGLSLFPFragmentBuilder* fragBuilder = args.fFragBuilder; + + // emit attributes + varyingHandler->emitAttributes(bcscgp); + fragBuilder->codeAppend("float4 circleEdge;"); + varyingHandler->addPassThroughAttribute(bcscgp.fInCircleEdge, "circleEdge"); + + fragBuilder->codeAppend("float4 dashParams;"); + varyingHandler->addPassThroughAttribute( + bcscgp.fInDashParams, "dashParams", + GrGLSLVaryingHandler::Interpolation::kCanBeFlat); + GrGLSLVarying wrapDashes(kHalf4_GrSLType); + varyingHandler->addVarying("wrapDashes", &wrapDashes, + GrGLSLVaryingHandler::Interpolation::kCanBeFlat); + GrGLSLVarying lastIntervalLength(kHalf_GrSLType); + varyingHandler->addVarying("lastIntervalLength", &lastIntervalLength, + GrGLSLVaryingHandler::Interpolation::kCanBeFlat); + vertBuilder->codeAppendf("float4 dashParams = %s;", bcscgp.fInDashParams->fName); + // Our fragment shader works in on/off intervals as specified by dashParams.xy: + // x = length of on interval, y = length of on + off. + // There are two other parameters in dashParams.zw: + // z = start angle in radians, w = phase offset in radians in range -y/2..y/2. + // Each interval has a "corresponding" dash which may be shifted partially or + // fully out of its interval by the phase. So there may be up to two "visual" + // dashes in an interval. + // When computing coverage in an interval we look at three dashes. These are the + // "corresponding" dashes from the current, previous, and next intervals. Any of these + // may be phase shifted into our interval or even when phase=0 they may be within half a + // pixel distance of a pixel center in the interval. + // When in the first interval we need to check the dash from the last interval. And + // similarly when in the last interval we need to check the dash from the first + // interval. When 2pi is not perfectly divisible dashParams.y this is a boundary case. + // We compute the dash begin/end angles in the vertex shader and apply them in the + // fragment shader when we detect we're in the first/last interval. + vertBuilder->codeAppend(R"( + // The two boundary dash intervals are stored in wrapDashes.xy and .zw and fed + // to the fragment shader as a varying. + float4 wrapDashes; + half lastIntervalLength = mod(6.28318530718, dashParams.y); + // We can happen to be perfectly divisible. + if (0 == lastIntervalLength) { + lastIntervalLength = dashParams.y; + } + // Let 'l' be the last interval before reaching 2 pi. + // Based on the phase determine whether (l-1)th, l-th, or (l+1)th interval's + // "corresponding" dash appears in the l-th interval and is closest to the 0-th + // interval. + half offset = 0; + if (-dashParams.w >= lastIntervalLength) { + offset = -dashParams.y; + } else if (dashParams.w > dashParams.y - lastIntervalLength) { + offset = dashParams.y; + } + wrapDashes.x = -lastIntervalLength + offset - dashParams.w; + // The end of this dash may be beyond the 2 pi and therefore clipped. Hence the + // min. + wrapDashes.y = min(wrapDashes.x + dashParams.x, 0); + + // Based on the phase determine whether the -1st, 0th, or 1st interval's + // "corresponding" dash appears in the 0th interval and is closest to l. + offset = 0; + if (dashParams.w >= dashParams.x) { + offset = dashParams.y; + } else if (-dashParams.w > dashParams.y - dashParams.x) { + offset = -dashParams.y; + } + wrapDashes.z = lastIntervalLength + offset - dashParams.w; + wrapDashes.w = wrapDashes.z + dashParams.x; + // The start of the dash we're considering may be clipped by the start of the + // circle. + wrapDashes.z = max(wrapDashes.z, lastIntervalLength); + )"); + vertBuilder->codeAppendf("%s = wrapDashes;", wrapDashes.vsOut()); + vertBuilder->codeAppendf("%s = lastIntervalLength;", lastIntervalLength.vsOut()); + fragBuilder->codeAppendf("half4 wrapDashes = %s;", wrapDashes.fsIn()); + fragBuilder->codeAppendf("half lastIntervalLength = %s;", lastIntervalLength.fsIn()); + + // setup pass through color + varyingHandler->addPassThroughAttribute( + bcscgp.fInColor, args.fOutputColor, + GrGLSLVaryingHandler::Interpolation::kCanBeFlat); + + // Setup position + this->writeOutputPosition(vertBuilder, gpArgs, bcscgp.fInPosition->fName); + + // emit transforms + this->emitTransforms(vertBuilder, + varyingHandler, + uniformHandler, + bcscgp.fInPosition->asShaderVar(), + bcscgp.fLocalMatrix, + args.fFPCoordTransformHandler); + GrShaderVar fnArgs[] = { + GrShaderVar("angleToEdge", kFloat_GrSLType), + GrShaderVar("diameter", kFloat_GrSLType), + }; + SkString fnName; + fragBuilder->emitFunction(kFloat_GrSLType, "coverage_from_dash_edge", + SK_ARRAY_COUNT(fnArgs), fnArgs, R"( + float linearDist; + angleToEdge = clamp(angleToEdge, -3.1415, 3.1415); + linearDist = diameter * sin(angleToEdge / 2); + return clamp(linearDist + 0.5, 0, 1); + )", + &fnName); + fragBuilder->codeAppend(R"( + float d = length(circleEdge.xy) * circleEdge.z; + + // Compute coverage from outer/inner edges of the stroke. + half distanceToOuterEdge = circleEdge.z - d; + half edgeAlpha = clamp(distanceToOuterEdge, 0.0, 1.0); + half distanceToInnerEdge = d - circleEdge.z * circleEdge.w; + half innerAlpha = clamp(distanceToInnerEdge, 0.0, 1.0); + edgeAlpha *= innerAlpha; + + half angleFromStart = atan(circleEdge.y, circleEdge.x) - dashParams.z; + angleFromStart = mod(angleFromStart, 6.28318530718); + float x = mod(angleFromStart, dashParams.y); + // Convert the radial distance from center to pixel into a diameter. + d *= 2; + half2 currDash = half2(-dashParams.w, dashParams.x - dashParams.w); + half2 nextDash = half2(dashParams.y - dashParams.w, + dashParams.y + dashParams.x - dashParams.w); + half2 prevDash = half2(-dashParams.y - dashParams.w, + -dashParams.y + dashParams.x - dashParams.w); + half dashAlpha = 0; + )"); + fragBuilder->codeAppendf(R"( + if (angleFromStart - x + dashParams.y >= 6.28318530718) { + dashAlpha += %s(x - wrapDashes.z, d) * %s(wrapDashes.w - x, d); + currDash.y = min(currDash.y, lastIntervalLength); + if (nextDash.x >= lastIntervalLength) { + // The next dash is outside the 0..2pi range, throw it away + nextDash.xy = half2(1000); + } else { + // Clip the end of the next dash to the end of the circle + nextDash.y = min(nextDash.y, lastIntervalLength); + } + } + )", fnName.c_str(), fnName.c_str()); + fragBuilder->codeAppendf(R"( + if (angleFromStart - x - dashParams.y < -0.01) { + dashAlpha += %s(x - wrapDashes.x, d) * %s(wrapDashes.y - x, d); + currDash.x = max(currDash.x, 0); + if (prevDash.y <= 0) { + // The previous dash is outside the 0..2pi range, throw it away + prevDash.xy = half2(1000); + } else { + // Clip the start previous dash to the start of the circle + prevDash.x = max(prevDash.x, 0); + } + } + )", fnName.c_str(), fnName.c_str()); + fragBuilder->codeAppendf(R"( + dashAlpha += %s(x - currDash.x, d) * %s(currDash.y - x, d); + dashAlpha += %s(x - nextDash.x, d) * %s(nextDash.y - x, d); + dashAlpha += %s(x - prevDash.x, d) * %s(prevDash.y - x, d); + dashAlpha = min(dashAlpha, 1); + edgeAlpha *= dashAlpha; + )", fnName.c_str(), fnName.c_str(), fnName.c_str(), fnName.c_str(), fnName.c_str(), + fnName.c_str()); + fragBuilder->codeAppendf("%s = half4(edgeAlpha);", args.fOutputCoverage); + } + + static void GenKey(const GrGeometryProcessor& gp, + const GrShaderCaps&, + GrProcessorKeyBuilder* b) { + const ButtCapDashedCircleGeometryProcessor& bcscgp = + gp.cast<ButtCapDashedCircleGeometryProcessor>(); + b->add32(bcscgp.fLocalMatrix.hasPerspective()); + } + + void setData(const GrGLSLProgramDataManager& pdman, const GrPrimitiveProcessor& primProc, + FPCoordTransformIter&& transformIter) override { + this->setTransformDataHelper( + primProc.cast<ButtCapDashedCircleGeometryProcessor>().fLocalMatrix, pdman, + &transformIter); + } + + private: + typedef GrGLSLGeometryProcessor INHERITED; + }; + + SkMatrix fLocalMatrix; + const Attribute* fInPosition; + const Attribute* fInColor; + const Attribute* fInCircleEdge; + const Attribute* fInDashParams; + + GR_DECLARE_GEOMETRY_PROCESSOR_TEST + + typedef GrGeometryProcessor INHERITED; +}; + +#if GR_TEST_UTILS +sk_sp<GrGeometryProcessor> ButtCapDashedCircleGeometryProcessor::TestCreate(GrProcessorTestData* d) { + const SkMatrix& matrix = GrTest::TestMatrix(d->fRandom); + return sk_sp<GrGeometryProcessor>(new ButtCapDashedCircleGeometryProcessor(matrix)); +} +#endif + /////////////////////////////////////////////////////////////////////////////// /** @@ -1240,6 +1474,301 @@ private: typedef GrMeshDrawOp INHERITED; }; +class ButtCapDashedCircleOp final : public GrMeshDrawOp { +private: + using Helper = GrSimpleMeshDrawOpHelper; + +public: + DEFINE_OP_CLASS_ID + + static std::unique_ptr<GrDrawOp> Make(GrPaint&& paint, const SkMatrix& viewMatrix, + SkPoint center, SkScalar radius, SkScalar strokeWidth, + SkScalar startAngle, SkScalar onAngle, SkScalar offAngle, + SkScalar phaseAngle) { + SkASSERT(circle_stays_circle(viewMatrix)); + SkASSERT(strokeWidth < 2 * radius); + return Helper::FactoryHelper<ButtCapDashedCircleOp>(std::move(paint), viewMatrix, center, + radius, strokeWidth, startAngle, + onAngle, offAngle, phaseAngle); + } + + ButtCapDashedCircleOp(const Helper::MakeArgs& helperArgs, GrColor color, + const SkMatrix& viewMatrix, SkPoint center, SkScalar radius, + SkScalar strokeWidth, SkScalar startAngle, SkScalar onAngle, + SkScalar offAngle, SkScalar phaseAngle) + : GrMeshDrawOp(ClassID()), fHelper(helperArgs, GrAAType::kCoverage) { + SkASSERT(circle_stays_circle(viewMatrix)); + viewMatrix.mapPoints(¢er, 1); + radius = viewMatrix.mapRadius(radius); + strokeWidth = viewMatrix.mapRadius(strokeWidth); + + // Determine the angle where the circle starts in device space and whether its orientation + // has been reversed. + SkVector start; + bool reflection; + if (!startAngle) { + start = {1, 0}; + } else { + start.fY = SkScalarSinCos(startAngle, &start.fX); + } + viewMatrix.mapVectors(&start, 1); + startAngle = SkScalarATan2(start.fY, start.fX); + reflection = (viewMatrix.getScaleX() * viewMatrix.getScaleY() - + viewMatrix.getSkewX() * viewMatrix.getSkewY()) < 0; + + auto totalAngle = onAngle + offAngle; + phaseAngle = SkScalarMod(phaseAngle + totalAngle / 2, totalAngle) - totalAngle / 2; + + SkScalar halfWidth = 0; + if (SkScalarNearlyZero(strokeWidth)) { + halfWidth = SK_ScalarHalf; + } else { + halfWidth = SkScalarHalf(strokeWidth); + } + + SkScalar outerRadius = radius + halfWidth; + SkScalar innerRadius = radius - halfWidth; + + // The radii are outset for two reasons. First, it allows the shader to simply perform + // simpler computation because the computed alpha is zero, rather than 50%, at the radius. + // Second, the outer radius is used to compute the verts of the bounding box that is + // rendered and the outset ensures the box will cover all partially covered by the circle. + outerRadius += SK_ScalarHalf; + innerRadius -= SK_ScalarHalf; + fViewMatrixIfUsingLocalCoords = viewMatrix; + + SkRect devBounds = SkRect::MakeLTRB(center.fX - outerRadius, center.fY - outerRadius, + center.fX + outerRadius, center.fY + outerRadius); + + // We store whether there is a reflection as a negative total angle. + if (reflection) { + totalAngle = -totalAngle; + } + fCircles.push_back(Circle{ + color, + outerRadius, + innerRadius, + onAngle, + totalAngle, + startAngle, + phaseAngle, + devBounds + }); + // Use the original radius and stroke radius for the bounds so that it does not include the + // AA bloat. + radius += halfWidth; + this->setBounds( + {center.fX - radius, center.fY - radius, center.fX + radius, center.fY + radius}, + HasAABloat::kYes, IsZeroArea::kNo); + fVertCount = circle_type_to_vert_count(true); + fIndexCount = circle_type_to_index_count(true); + } + + const char* name() const override { return "ButtCappedDashedCircleOp"; } + + void visitProxies(const VisitProxyFunc& func) const override { fHelper.visitProxies(func); } + + SkString dumpInfo() const override { + SkString string; + for (int i = 0; i < fCircles.count(); ++i) { + string.appendf( + "Color: 0x%08x Rect [L: %.2f, T: %.2f, R: %.2f, B: %.2f]," + "InnerRad: %.2f, OuterRad: %.2f, OnAngle: %.2f, TotalAngle: %.2f, " + "Phase: %.2f\n", + fCircles[i].fColor, fCircles[i].fDevBounds.fLeft, fCircles[i].fDevBounds.fTop, + fCircles[i].fDevBounds.fRight, fCircles[i].fDevBounds.fBottom, + fCircles[i].fInnerRadius, fCircles[i].fOuterRadius, fCircles[i].fOnAngle, + fCircles[i].fTotalAngle, fCircles[i].fPhaseAngle); + } + string += fHelper.dumpInfo(); + string += INHERITED::dumpInfo(); + return string; + } + + RequiresDstTexture finalize(const GrCaps& caps, const GrAppliedClip* clip, + GrPixelConfigIsClamped dstIsClamped) override { + GrColor* color = &fCircles.front().fColor; + return fHelper.xpRequiresDstTexture(caps, clip, dstIsClamped, + GrProcessorAnalysisCoverage::kSingleChannel, color); + } + + FixedFunctionFlags fixedFunctionFlags() const override { return fHelper.fixedFunctionFlags(); } + +private: + void onPrepareDraws(Target* target) override { + SkMatrix localMatrix; + if (!fViewMatrixIfUsingLocalCoords.invert(&localMatrix)) { + return; + } + + // Setup geometry processor + sk_sp<GrGeometryProcessor> gp(new ButtCapDashedCircleGeometryProcessor(localMatrix)); + + struct CircleVertex { + SkPoint fPos; + GrColor fColor; + SkPoint fOffset; + SkScalar fOuterRadius; + SkScalar fInnerRadius; + SkScalar fOnAngle; + SkScalar fTotalAngle; + SkScalar fStartAngle; + SkScalar fPhaseAngle; + }; + + size_t vertexStride = gp->getVertexStride(); + SkASSERT(vertexStride == sizeof(CircleVertex)); + + const GrBuffer* vertexBuffer; + int firstVertex; + char* vertices = (char*)target->makeVertexSpace(vertexStride, fVertCount, &vertexBuffer, + &firstVertex); + if (!vertices) { + SkDebugf("Could not allocate vertices\n"); + return; + } + + const GrBuffer* indexBuffer = nullptr; + int firstIndex = 0; + uint16_t* indices = target->makeIndexSpace(fIndexCount, &indexBuffer, &firstIndex); + if (!indices) { + SkDebugf("Could not allocate indices\n"); + return; + } + + int currStartVertex = 0; + for (const auto& circle : fCircles) { + // The inner radius in the vertex data must be specified in normalized space so that + // length() can be called with smaller values to avoid precision issues with half + // floats. + auto normInnerRadius = circle.fInnerRadius / circle.fOuterRadius; + const SkRect& bounds = circle.fDevBounds; + bool reflect = false; + SkScalar totalAngle = circle.fTotalAngle; + if (totalAngle < 0) { + reflect = true; + totalAngle = -totalAngle; + } + + // The bounding geometry for the circle is composed of an outer bounding octagon and + // an inner bounded octagon. + + // Initializes the attributes that are the same at each vertex. Also applies reflection. + auto init_const_attrs_and_reflect = [&](CircleVertex* v) { + v->fColor = circle.fColor; + v->fOuterRadius = circle.fOuterRadius; + v->fInnerRadius = normInnerRadius; + v->fOnAngle = circle.fOnAngle; + v->fTotalAngle = totalAngle; + v->fStartAngle = circle.fStartAngle; + v->fPhaseAngle = circle.fPhaseAngle; + if (reflect) { + v->fStartAngle = -v->fStartAngle; + v->fOffset.fY = -v->fOffset.fY; + } + }; + + // Compute the vertices of the outer octagon. + SkPoint center = SkPoint::Make(bounds.centerX(), bounds.centerY()); + SkScalar halfWidth = 0.5f * bounds.width(); + auto init_outer_vertex = [&](int idx, SkScalar x, SkScalar y) { + CircleVertex* v = reinterpret_cast<CircleVertex*>(vertices + idx * vertexStride); + v->fPos = center + SkPoint{x * halfWidth, y * halfWidth}; + v->fOffset = {x, y}; + init_const_attrs_and_reflect(v); + }; + static constexpr SkScalar kOctOffset = 0.41421356237f; // sqrt(2) - 1 + init_outer_vertex(0, -kOctOffset, -1); + init_outer_vertex(1, kOctOffset, -1); + init_outer_vertex(2, 1, -kOctOffset); + init_outer_vertex(3, 1, kOctOffset); + init_outer_vertex(4, kOctOffset, 1); + init_outer_vertex(5, -kOctOffset, 1); + init_outer_vertex(6, -1, kOctOffset); + init_outer_vertex(7, -1, -kOctOffset); + + // Compute the vertices of the inner octagon. + auto init_inner_vertex = [&](int idx, SkScalar x, SkScalar y) { + CircleVertex* v = + reinterpret_cast<CircleVertex*>(vertices + (idx + 8) * vertexStride); + v->fPos = center + SkPoint{x * circle.fInnerRadius, y * circle.fInnerRadius}; + v->fOffset = {x * normInnerRadius, y * normInnerRadius}; + init_const_attrs_and_reflect(v); + }; + + // cosine and sine of pi/8 + static constexpr SkScalar kCos = 0.923579533f; + static constexpr SkScalar kSin = 0.382683432f; + + init_inner_vertex(0, -kSin, -kCos); + init_inner_vertex(1, kSin, -kCos); + init_inner_vertex(2, kCos, -kSin); + init_inner_vertex(3, kCos, kSin); + init_inner_vertex(4, kSin, kCos); + init_inner_vertex(5, -kSin, kCos); + init_inner_vertex(6, -kCos, kSin); + init_inner_vertex(7, -kCos, -kSin); + + const uint16_t* primIndices = circle_type_to_indices(true); + const int primIndexCount = circle_type_to_index_count(true); + for (int i = 0; i < primIndexCount; ++i) { + *indices++ = primIndices[i] + currStartVertex; + } + + currStartVertex += circle_type_to_vert_count(true); + vertices += circle_type_to_vert_count(true) * vertexStride; + } + + GrMesh mesh(GrPrimitiveType::kTriangles); + mesh.setIndexed(indexBuffer, fIndexCount, firstIndex, 0, fVertCount - 1); + mesh.setVertexData(vertexBuffer, firstVertex); + target->draw(gp.get(), fHelper.makePipeline(target), mesh); + } + + bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override { + ButtCapDashedCircleOp* that = t->cast<ButtCapDashedCircleOp>(); + + // can only represent 65535 unique vertices with 16-bit indices + if (fVertCount + that->fVertCount > 65536) { + return false; + } + + if (!fHelper.isCompatible(that->fHelper, caps, this->bounds(), that->bounds())) { + return false; + } + + if (fHelper.usesLocalCoords() && + !fViewMatrixIfUsingLocalCoords.cheapEqualTo(that->fViewMatrixIfUsingLocalCoords)) { + return false; + } + + fCircles.push_back_n(that->fCircles.count(), that->fCircles.begin()); + this->joinBounds(*that); + fVertCount += that->fVertCount; + fIndexCount += that->fIndexCount; + return true; + } + + struct Circle { + GrColor fColor; + SkScalar fOuterRadius; + SkScalar fInnerRadius; + SkScalar fOnAngle; + SkScalar fTotalAngle; + SkScalar fStartAngle; + SkScalar fPhaseAngle; + SkRect fDevBounds; + }; + + SkMatrix fViewMatrixIfUsingLocalCoords; + Helper fHelper; + SkSTArray<1, Circle, true> fCircles; + int fVertCount; + int fIndexCount; + + typedef GrMeshDrawOp INHERITED; +}; + /////////////////////////////////////////////////////////////////////////////// class EllipseOp : public GrMeshDrawOp { @@ -2487,7 +3016,8 @@ std::unique_ptr<GrDrawOp> GrOvalOpFactory::MakeRRectOp(GrPaint&& paint, const SkStrokeRec& stroke, const GrShaderCaps* shaderCaps) { if (rrect.isOval()) { - return MakeOvalOp(std::move(paint), viewMatrix, rrect.getBounds(), stroke, shaderCaps); + return MakeOvalOp(std::move(paint), viewMatrix, rrect.getBounds(), GrStyle(stroke, nullptr), + shaderCaps); } if (!viewMatrix.rectStaysRect() || !rrect.isSimple()) { @@ -2502,25 +3032,55 @@ std::unique_ptr<GrDrawOp> GrOvalOpFactory::MakeRRectOp(GrPaint&& paint, std::unique_ptr<GrDrawOp> GrOvalOpFactory::MakeOvalOp(GrPaint&& paint, const SkMatrix& viewMatrix, const SkRect& oval, - const SkStrokeRec& stroke, + const GrStyle& style, const GrShaderCaps* shaderCaps) { // we can draw circles SkScalar width = oval.width(); if (width > SK_ScalarNearlyZero && SkScalarNearlyEqual(width, oval.height()) && circle_stays_circle(viewMatrix)) { + auto r = width / 2.f; SkPoint center = {oval.centerX(), oval.centerY()}; - return CircleOp::Make(std::move(paint), viewMatrix, center, width / 2.f, - GrStyle(stroke, nullptr)); + if (style.hasNonDashPathEffect()) { + return nullptr; + } else if (style.isDashed()) { + if (style.strokeRec().getCap() != SkPaint::kButt_Cap || + style.dashIntervalCnt() != 2 || style.strokeRec().getWidth() >= width) { + return nullptr; + } + auto onInterval = style.dashIntervals()[0]; + auto offInterval = style.dashIntervals()[1]; + if (offInterval == 0) { + GrStyle strokeStyle(style.strokeRec(), nullptr); + return MakeOvalOp(std::move(paint), viewMatrix, oval, strokeStyle, shaderCaps); + } else if (onInterval == 0) { + // There is nothing to draw but we have no way to indicate that here. + return nullptr; + } + auto angularOnInterval = onInterval / r; + auto angularOffInterval = offInterval / r; + auto phaseAngle = style.dashPhase() / r; + // Currently this function doesn't accept ovals with different start angles, though + // it could. + static const SkScalar kStartAngle = 0.f; + return ButtCapDashedCircleOp::Make(std::move(paint), viewMatrix, center, r, + style.strokeRec().getWidth(), kStartAngle, + angularOnInterval, angularOffInterval, phaseAngle); + } + return CircleOp::Make(std::move(paint), viewMatrix, center, r, style); + } + + if (style.pathEffect()) { + return nullptr; } // prefer the device space ellipse op for batchability if (viewMatrix.rectStaysRect()) { - return EllipseOp::Make(std::move(paint), viewMatrix, oval, stroke); + return EllipseOp::Make(std::move(paint), viewMatrix, oval, style.strokeRec()); } // Otherwise, if we have shader derivative support, render as device-independent if (shaderCaps->shaderDerivativeSupport()) { - return DIEllipseOp::Make(std::move(paint), viewMatrix, oval, stroke); + return DIEllipseOp::Make(std::move(paint), viewMatrix, oval, style.strokeRec()); } return nullptr; @@ -2582,6 +3142,27 @@ GR_DRAW_OP_TEST_DEFINE(CircleOp) { } while (true); } +GR_DRAW_OP_TEST_DEFINE(ButtCapDashedCircleOp) { + SkScalar rotate = random->nextSScalar1() * 360.f; + SkScalar translateX = random->nextSScalar1() * 1000.f; + SkScalar translateY = random->nextSScalar1() * 1000.f; + SkScalar scale = random->nextSScalar1() * 100.f; + SkMatrix viewMatrix; + viewMatrix.setRotate(rotate); + viewMatrix.postTranslate(translateX, translateY); + viewMatrix.postScale(scale, scale); + SkRect circle = GrTest::TestSquare(random); + SkPoint center = {circle.centerX(), circle.centerY()}; + SkScalar radius = circle.width() / 2.f; + SkScalar strokeWidth = random->nextRangeScalar(0.001f * radius, 1.8f * radius); + SkScalar onAngle = random->nextRangeScalar(0.01f, 1000.f); + SkScalar offAngle = random->nextRangeScalar(0.01f, 1000.f); + SkScalar startAngle = random->nextRangeScalar(-1000.f, 1000.f); + SkScalar phase = random->nextRangeScalar(-1000.f, 1000.f); + return ButtCapDashedCircleOp::Make(std::move(paint), viewMatrix, center, radius, strokeWidth, + startAngle, onAngle, offAngle, phase); +} + GR_DRAW_OP_TEST_DEFINE(EllipseOp) { SkMatrix viewMatrix = GrTest::TestMatrixRectStaysRect(random); SkRect ellipse = GrTest::TestSquare(random); diff --git a/src/gpu/ops/GrOvalOpFactory.h b/src/gpu/ops/GrOvalOpFactory.h index 1bdf3ee6f6..0c10d6b2ed 100644 --- a/src/gpu/ops/GrOvalOpFactory.h +++ b/src/gpu/ops/GrOvalOpFactory.h @@ -28,7 +28,7 @@ public: static std::unique_ptr<GrDrawOp> MakeOvalOp(GrPaint&&, const SkMatrix&, const SkRect& oval, - const SkStrokeRec&, + const GrStyle& style, const GrShaderCaps*); static std::unique_ptr<GrDrawOp> MakeRRectOp(GrPaint&&, |