Fix the formula for fNumRadialSegmentsPerRadian in GrStrokePatchBuilder

GrStrokePatchBuilder had the wrong formula. This CL moves the formula to
GrStrokeTessellateOp and calculates it in one spot for the builder and
tessellator both. It also fixes a bug that was hiding behind this
formula error and updates some variable names.

Bug: skia:10419
Change-Id: I908d3960b16cac8dca0d40ff4116a3f5c5beed06
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/328416
Reviewed-by: Jim Van Verth <jvanverth@google.com>
Commit-Queue: Chris Dalton <csmartdalton@google.com>
This commit is contained in:
Chris Dalton 2020-10-19 19:51:28 -06:00 committed by Skia Commit-Bot
parent 8744a5c820
commit 58a26a8362
7 changed files with 78 additions and 72 deletions

View File

@ -283,8 +283,12 @@ class StrokePatchBuilderBench : public Benchmark {
}
for (int i = 0; i < loops; ++i) {
fPatchChunks.reset();
GrStrokePatchBuilder builder(&fTarget, &fPatchChunks, 1, fStrokeRec,
fPath.countVerbs());
// TODO: Combine GrStrokePatchBuilder with GrStrokeTessellateOp.
float parametricIntolerance = GrTessellationPathRenderer::kLinearizationIntolerance;
float numRadialSegmentsPerRadian = .5f /
acosf(1 - 2/(parametricIntolerance * fStrokeRec.getWidth()));
GrStrokePatchBuilder builder(&fTarget, &fPatchChunks, fStrokeRec, parametricIntolerance,
numRadialSegmentsPerRadian, fPath.countVerbs());
builder.addPath(fPath);
}
}

View File

@ -55,24 +55,23 @@ static float pow4(float x) {
}
GrStrokePatchBuilder::GrStrokePatchBuilder(GrMeshDrawOp::Target* target,
SkTArray<PatchChunk>* patchChunkArray, float matrixScale,
const SkStrokeRec& stroke, int totalCombinedVerbCnt)
SkTArray<PatchChunk>* patchChunkArray,
const SkStrokeRec& stroke,
float parametricIntolerance,
float numRadialSegmentsPerRadian,
int totalCombinedVerbCnt)
: fTarget(target)
, fPatchChunkArray(patchChunkArray)
, fMaxTessellationSegments(target->caps().shaderCaps()->maxTessellationSegments())
, fLinearizationIntolerance(matrixScale *
GrTessellationPathRenderer::kLinearizationIntolerance)
, fStroke(stroke) {
// Subtract 2 because the tessellation shader chops every cubic at two locations, and each
// chop has the potential to introduce an extra segment.
, fMaxTessellationSegments(target->caps().shaderCaps()->maxTessellationSegments() - 2)
, fStroke(stroke)
, fParametricIntolerance(parametricIntolerance)
, fNumRadialSegmentsPerRadian(numRadialSegmentsPerRadian) {
// We don't support hairline strokes. For now, the client can transform the path into device
// space and then use a stroke width of 1.
SkASSERT(fStroke.getWidth() > 0);
// This is the number of radial segments we need to add to a triangle strip for each radian
// of rotation, given the current stroke radius. Any fewer radial segments and our error
// would fall outside the linearization tolerance.
fNumRadialSegmentsPerRadian = 1 / std::acos(
std::max(1 - 1 / (fLinearizationIntolerance * fStroke.getWidth() * .5f), -1.f));
// Calculate the worst-case numbers of parametric segments our hardware can support for the
// current stroke radius, in the event that there are also enough radial segments to rotate
// 180 and 360 degrees respectively. These are used for "quick accepts" that allow us to
@ -225,7 +224,7 @@ void GrStrokePatchBuilder::quadraticTo(const SkPoint p[3], JoinType prevJoinType
//
// An informal survey of skottie animations and gms revealed that even with a bare minimum of 64
// tessellation segments, 99.9%+ of quadratics take this early out.
float numParametricSegments_pow4 = GrWangsFormula::quadratic_pow4(fLinearizationIntolerance, p);
float numParametricSegments_pow4 = GrWangsFormula::quadratic_pow4(fParametricIntolerance, p);
if (numParametricSegments_pow4 <= fMaxParametricSegments180_pow4_withJoin &&
prevJoinType != JoinType::kCusp) {
this->cubicToRaw(prevJoinType, asCubic);
@ -311,7 +310,7 @@ void GrStrokePatchBuilder::cubicTo(const SkPoint p[4], JoinType prevJoinType,
//
// An informal survey of skottie animations revealed that with a bare minimum of 64 tessellation
// segments, 95% of cubics take this early out.
float numParametricSegments_pow4 = GrWangsFormula::cubic_pow4(fLinearizationIntolerance, p);
float numParametricSegments_pow4 = GrWangsFormula::cubic_pow4(fParametricIntolerance, p);
if (numParametricSegments_pow4 <= fMaxParametricSegments360_pow4_withJoin &&
prevJoinType != JoinType::kCusp) {
this->cubicToRaw(prevJoinType, p);

View File

@ -43,9 +43,16 @@ public:
// push to as addPath is called. The caller is responsible to bind and draw each chunk that gets
// pushed to the array. (See GrStrokeTessellateShader.)
//
// All points are multiplied by 'matrixScale' before being written to the GPU buffer.
GrStrokePatchBuilder(GrMeshDrawOp::Target*, SkTArray<PatchChunk>*, float matrixScale,
const SkStrokeRec&, int totalCombinedVerbCnt);
// 'parametricIntolerance' controls the number of parametric segments we need to account for on
// each curve. The tessellator will add enough parametric segments so that the center of each
// one falls within 1/parametricIntolerance local path units from the true curve.
//
// 'numRadialSegmentsPerRadian' controls the number of radial segments we need to account for on
// each curve. The tessellator will add this number of radial segments for each radian of
// rotation, in order to guarantee smoothness.
GrStrokePatchBuilder(GrMeshDrawOp::Target*, SkTArray<PatchChunk>*, const SkStrokeRec&,
float parametricIntolerance, float numRadialSegmentsPerRadian,
int totalCombinedVerbCnt);
// "Releases" the target to be used externally again by putting back any unused pre-allocated
// vertices.
@ -97,12 +104,10 @@ private:
SkTArray<PatchChunk>* const fPatchChunkArray;
const int fMaxTessellationSegments;
// GrTessellationPathRenderer::kIntolerance adjusted for the matrix scale.
const float fLinearizationIntolerance;
// Variables related to the stroke parameters.
const SkStrokeRec fStroke;
float fNumRadialSegmentsPerRadian;
const float fParametricIntolerance;
const float fNumRadialSegmentsPerRadian;
// These values contain worst-case numbers of parametric segments, raised to the 4th power, that
// our hardware can support for the current stroke radius. They assume curve rotations of 180
// and 360 degrees respectively. These are used for "quick accepts" that allow us to send almost

View File

@ -26,14 +26,17 @@ GrStrokeTessellateOp::GrStrokeTessellateOp(GrAAType aaType, const SkMatrix& view
: GrDrawOp(ClassID())
, fAAType(aaType)
, fViewMatrix(viewMatrix)
, fMatrixScale(fViewMatrix.getMaxScale())
, fStroke(stroke)
, fParametricIntolerance(fViewMatrix.getMaxScale() *
GrTessellationPathRenderer::kLinearizationIntolerance)
, fNumRadialSegmentsPerRadian(.5f / acosf(
std::max(1 - 2/(fParametricIntolerance * fStroke.getWidth()), -1.f)))
, fColor(get_paint_constant_blended_color(paint))
, fProcessors(std::move(paint))
, fPaths(path)
, fTotalCombinedVerbCnt(path.countVerbs()) {
SkASSERT(fAAType != GrAAType::kCoverage); // No mixed samples support yet.
SkASSERT(fMatrixScale >= 0);
SkASSERT(fParametricIntolerance >= 0);
SkRect devBounds = path.getBounds();
float inflationRadius = fStroke.getInflationRadius();
devBounds.outset(inflationRadius, inflationRadius);
@ -91,8 +94,9 @@ void GrStrokeTessellateOp::prePrepareColorProgram(SkArenaAlloc* arena,
const GrXferProcessor::DstProxyView& dstProxyView,
GrXferBarrierFlags renderPassXferBarriers,
const GrCaps& caps) {
auto* strokeShader = arena->make<GrStrokeTessellateShader>(fStroke, fMatrixScale, fViewMatrix,
fColor);
auto* strokeShader = arena->make<GrStrokeTessellateShader>(fStroke, fParametricIntolerance,
fNumRadialSegmentsPerRadian,
fViewMatrix, fColor);
auto pipelineFlags = GrPipeline::InputFlags::kNone;
if (GrAAType::kNone != fAAType) {
pipelineFlags |= GrPipeline::InputFlags::kHWAntialias;
@ -111,8 +115,8 @@ void GrStrokeTessellateOp::onPrepare(GrOpFlushState* flushState) {
flushState->detachAppliedClip(), flushState->dstProxyView(),
flushState->renderPassBarriers(), flushState->caps());
}
GrStrokePatchBuilder builder(flushState, &fPatchChunks, fMatrixScale, fStroke,
fTotalCombinedVerbCnt);
GrStrokePatchBuilder builder(flushState, &fPatchChunks, fStroke, fParametricIntolerance,
fNumRadialSegmentsPerRadian, fTotalCombinedVerbCnt);
for (const SkPath& path : fPaths) {
builder.addPath(path);
}

View File

@ -45,8 +45,15 @@ private:
const GrAAType fAAType;
const SkMatrix fViewMatrix;
const float fMatrixScale;
const SkStrokeRec fStroke;
// Controls the number of parametric segments the tessellator adds for each curve. The
// tessellator will add enough parametric segments so that the center of each one falls within
// 1/parametricIntolerance local path units from the true curve.
const float fParametricIntolerance;
// Controls the number of radial segments the tessellator adds for each curve. The tessellator
// will add this number of radial segments for each radian of rotation, in order to guarantee
// smoothness.
const float fNumRadialSegmentsPerRadian;
SkPMColor4f fColor;
GrProcessorSet fProcessors;

View File

@ -13,9 +13,6 @@
#include "src/gpu/glsl/GrGLSLVertexGeoBuilder.h"
#include "src/gpu/tessellate/GrWangsFormula.h"
constexpr static float kLinearizationIntolerance =
GrTessellationPathRenderer::kLinearizationIntolerance;
class GrStrokeTessellateShader::Impl : public GrGLSLGeometryProcessor {
public:
const char* getTessArgs1UniformName(const GrGLSLUniformHandler& uniformHandler) const {
@ -41,7 +38,7 @@ private:
// uNumSegmentsInJoin, uCubicConstantPow2, uNumRadialSegmentsPerRadian, uMiterLimitInvPow2.
fTessArgs1Uniform = uniHandler->addUniform(nullptr, kTessControl_GrShaderFlag,
kFloat4_GrSLType, "tessArgs1", nullptr);
// uRadialTolerancePow2, uStrokeRadius.
// uJoinTolerancePow2, uStrokeRadius.
fTessArgs2Uniform = uniHandler->addUniform(nullptr, kTessControl_GrShaderFlag |
kTessEvaluation_GrShaderFlag,
kFloat2_GrSLType,
@ -262,18 +259,17 @@ private:
numSegmentsInJoin = 0; // Use the rotation to calculate the number of segments.
break;
}
float intolerance = kLinearizationIntolerance * shader.fMatrixScale;
float cubicConstant = GrWangsFormula::cubic_constant(intolerance);
float strokeRadius = shader.fStroke.getWidth() * .5;
float radialIntolerance = 1 / (strokeRadius * intolerance);
float cubicConstant = GrWangsFormula::cubic_constant(shader.fParametricIntolerance);
float miterLimit = shader.fStroke.getMiter();
pdman.set4f(fTessArgs1Uniform,
numSegmentsInJoin, // uNumSegmentsInJoin
cubicConstant * cubicConstant, // uCubicConstantPow2 in path space.
.5f / acosf(std::max(1 - radialIntolerance, -1.f)), // uNumRadialSegmentsPerRadian
shader.fNumRadialSegmentsPerRadian, // uNumRadialSegmentsPerRadian
1 / (miterLimit * miterLimit)); // uMiterLimitInvPow2.
float strokeRadius = shader.fStroke.getWidth() * .5;
float joinTolerance = 1 / (strokeRadius * shader.fParametricIntolerance);
pdman.set2f(fTessArgs2Uniform,
radialIntolerance * radialIntolerance, // uRadialTolerancePow2.
joinTolerance * joinTolerance, // uJoinTolerancePow2.
strokeRadius); // uStrokeRadius.
const SkMatrix& m = shader.viewMatrix();
if (!m.isIdentity()) {
@ -314,7 +310,7 @@ SkString GrStrokeTessellateShader::getTessControlShaderGLSL(
const char* tessArgs2Name = impl->getTessArgs2UniformName(uniformHandler);
code.appendf("uniform vec2 %s;\n", tessArgs2Name);
code.appendf("#define uRadialTolerancePow2 %s.x\n", tessArgs2Name);
code.appendf("#define uJoinTolerancePow2 %s.x\n", tessArgs2Name);
code.append(R"(
in vec4 vsPts01[];
@ -424,7 +420,7 @@ SkString GrStrokeTessellateShader::getTessControlShaderGLSL(
innerStrokeRadiusMultiplier = (x >= uMiterLimitInvPow2) ? inversesqrt(x) : sqrt(x);
}
vec2 strokeOutsetClamp = vec2(-1, 1);
if (length_pow2(tan1norm - tan0norm) > uRadialTolerancePow2) {
if (length_pow2(tan1norm - tan0norm) > uJoinTolerancePow2) {
// Clamp the join to the exterior side of its junction. We only do this if the join
// angle is large enough to guarantee there won't be cracks on the interior side of
// the junction.

View File

@ -16,27 +16,13 @@
class GrGLSLUniformHandler;
// Tessellates a batch of stroke patches directly to the canvas.
//
// Current limitations: (These restrictions will hopefully all be lifted in the future.)
//
// * A given curve must not have inflection points. Chop curves at inflection points on CPU
// before sending them down.
//
// * A given curve must not rotate more than 180 degrees. Chop curves that rotate more than 180
// degrees at midtangent before sending them down.
//
// * It is illegal for P1 and P2 to both be coincident with P0 or P3. If this is the case, send
// the curve [P0, P0, P3, P3] instead.
//
// * Perspective is not supported.
//
// Tessellated stroking works by creating stroke-width, orthogonal edges at set locations along the
// curve and then connecting them with a quad strip. These orthogonal edges come from two different
// sets: "parametric edges" and "radial edges". Parametric edges are spaced evenly in the parametric
// sense, and radial edges divide the curve's _rotation_ into even steps. The tessellation shader
// evaluates both sets of edges and sorts them into a single quad strip. With this combined set of
// edges we can stroke any curve, regardless of curvature.
// Tessellates a batch of stroke patches directly to the canvas. Tessellated stroking works by
// creating stroke-width, orthogonal edges at set locations along the curve and then connecting them
// with a quad strip. These orthogonal edges come from two different sets: "parametric edges" and
// "radial edges". Parametric edges are spaced evenly in the parametric sense, and radial edges
// divide the curve's _rotation_ into even steps. The tessellation shader evaluates both sets of
// edges and sorts them into a single quad strip. With this combined set of edges we can stroke any
// curve, regardless of curvature.
class GrStrokeTessellateShader : public GrPathShader {
public:
// The vertex array bound for this shader should contain a vector of Patch structs. A Patch is
@ -56,19 +42,23 @@ public:
std::array<SkPoint, 4> fPts;
};
// 'matrixScale' is used to set up an appropriate number of tessellation triangles. It should be
// equal to viewMatrix.getMaxScale(). (This works because perspective isn't supported.)
// 'parametricIntolerance' controls the number of parametric segments we add for each curve.
// We add enough parametric segments so that the center of each one falls within
// 1/parametricIntolerance local path units from the true curve.
//
// 'miterLimit' contains the stroke's miter limit, or may be zero if no patches being drawn will
// be miter joins.
// 'numRadialSegmentsPerRadian' controls the number of radial segments we add for each curve.
// We add this number of radial segments for each radian of rotation, in order to guarantee
// smoothness.
//
// 'viewMatrix' is applied to the geometry post tessellation. It cannot have perspective.
GrStrokeTessellateShader(const SkStrokeRec& stroke, float matrixScale,
const SkMatrix& viewMatrix, SkPMColor4f color)
GrStrokeTessellateShader(const SkStrokeRec& stroke, float parametricIntolerance,
float numRadialSegmentsPerRadian, const SkMatrix& viewMatrix,
SkPMColor4f color)
: GrPathShader(kTessellate_GrStrokeTessellateShader_ClassID, viewMatrix,
GrPrimitiveType::kPatches, 1)
, fStroke(stroke)
, fMatrixScale(matrixScale)
, fParametricIntolerance(parametricIntolerance)
, fNumRadialSegmentsPerRadian(numRadialSegmentsPerRadian)
, fColor(color) {
SkASSERT(!fStroke.isHairlineStyle()); // No hairline support yet.
constexpr static Attribute kInputPointAttribs[] = {
@ -96,7 +86,8 @@ private:
const GrShaderCaps&) const override;
const SkStrokeRec fStroke;
const float fMatrixScale;
const float fParametricIntolerance;
const float fNumRadialSegmentsPerRadian;
const SkPMColor4f fColor;
class Impl;