aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar Brian Salomon <bsalomon@google.com>2018-04-20 13:54:11 -0400
committerGravatar Skia Commit-Bot <skia-commit-bot@chromium.org>2018-04-20 18:45:35 +0000
commit62e4f3daf7262463774ca0434a9232a8e2292350 (patch)
tree0402b8e6fb48469d3dc23c697ecc4fda9ee39fb2
parent2fda63abcdd4ba7ee41e5226b68c93b102b76fb2 (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.cpp117
-rw-r--r--src/gpu/GrProcessor.h1
-rw-r--r--src/gpu/GrRenderTargetContext.cpp15
-rw-r--r--src/gpu/SkGpuDevice.cpp8
-rw-r--r--src/gpu/ops/GrOvalOpFactory.cpp593
-rw-r--r--src/gpu/ops/GrOvalOpFactory.h2
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(&center, 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&&,