Don't pre-scale tessellated strokes by the matrix scale

In order to expand into the correct amount of triangles, we instead
factor the matrix scale into the tolerances.

Bug: skia:10419
Change-Id: I178b9600a8837ec5fc997199a8bf6be87227ec94
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/313300
Reviewed-by: Greg Daniel <egdaniel@google.com>
Reviewed-by: Chris Dalton <csmartdalton@google.com>
Commit-Queue: Chris Dalton <csmartdalton@google.com>
This commit is contained in:
Chris Dalton 2020-08-27 10:14:52 -06:00 committed by Skia Commit-Bot
parent 55f02eb3ff
commit b0ebb5a599
6 changed files with 82 additions and 102 deletions

View File

@ -17,9 +17,6 @@
#include "src/gpu/tessellate/GrVectorXform.h"
#include "src/gpu/tessellate/GrWangsFormula.h"
constexpr static float kLinearizationIntolerance =
GrTessellationPathRenderer::kLinearizationIntolerance;
constexpr static float kStandardCubicType = GrStrokeTessellateShader::kStandardCubicType;
constexpr static float kDoubleSidedRoundJoinType = -GrStrokeTessellateShader::kRoundJoinType;
@ -145,14 +142,14 @@ void GrStrokePatchBuilder::addPath(const SkPath& path, const SkStrokeRec& stroke
// space and then use a stroke width of 1.
SkASSERT(stroke.getWidth() > 0);
fCurrStrokeRadius = stroke.getWidth()/2 * fMatrixScale;
fCurrStrokeRadius = stroke.getWidth()/2;
fCurrStrokeJoinType = join_type_from_join(stroke.getJoin());
// 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.
fNumRadialSegmentsPerRad = 1 / std::acos(
std::max(1 - 1 / (kLinearizationIntolerance * fCurrStrokeRadius), -1.f));
std::max(1 - 1 / (fLinearizationIntolerance * fCurrStrokeRadius), -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
@ -165,15 +162,7 @@ void GrStrokePatchBuilder::addPath(const SkPath& path, const SkStrokeRec& stroke
fHasPreviousSegment = false;
SkPathVerb previousVerb = SkPathVerb::kClose;
for (auto [verb, rawPts, w] : SkPathPriv::Iterate(path)) {
SkPoint pts[4];
int numPtsInVerb = SkPathPriv::PtsInIter((unsigned)verb);
for (int i = 0; i < numPtsInVerb; ++i) {
// TEMPORORY: Scale all the points up front. SkFind*MaxCurvature and GrWangsFormula::*
// both expect arrays of points. As we refine this class and its math, this scale will
// hopefully be integrated more efficiently.
pts[i] = rawPts[i] * fMatrixScale;
}
for (auto [verb, pts, w] : SkPathPriv::Iterate(path)) {
switch (verb) {
case SkPathVerb::kMove:
// "A subpath ... consisting of a single moveto shall not be stroked."
@ -236,7 +225,7 @@ void GrStrokePatchBuilder::quadraticTo(float prevJoinType, const SkPoint p[3], i
// Ensure our hardware supports enough tessellation segments to render the curve. The first
// branch assumes a worst-case rotation of 180 degrees and checks if even then we have enough.
// In practice it is rare to take even the first branch.
float numParametricSegments = GrWangsFormula::quadratic(kLinearizationIntolerance, p);
float numParametricSegments = GrWangsFormula::quadratic(fLinearizationIntolerance, p);
if (numParametricSegments > fMaxParametricSegments180 && maxDepth != 0) {
// We still might have enough tessellation segments to render the curve. Check again with
// the actual rotation.
@ -321,7 +310,7 @@ void GrStrokePatchBuilder::nonInflectCubicTo(float prevJoinType, const SkPoint p
// NOTE: We could technically assume a worst-case rotation of 180 because cubicTo() chops at
// midtangents and inflections. However, this is only temporary so we leave it at 360 where it
// will arrive at in the future.
float numParametricSegments = GrWangsFormula::cubic(kLinearizationIntolerance, p);
float numParametricSegments = GrWangsFormula::cubic(fLinearizationIntolerance, p);
if (numParametricSegments > fMaxParametricSegments360 && maxDepth != 0) {
// We still might have enough tessellation segments to render the curve. Check again with
// the actual rotation.

View File

@ -48,7 +48,8 @@ public:
: fTarget(target)
, fVertexChunkArray(vertexChunkArray)
, fMaxTessellationSegments(target->caps().shaderCaps()->maxTessellationSegments())
, fMatrixScale(matrixScale) {
, fLinearizationIntolerance(matrixScale *
GrTessellationPathRenderer::kLinearizationIntolerance) {
this->allocVertexChunk(
(totalCombinedVerbCnt * 3) * GrStrokeTessellateShader::kNumVerticesPerPatch);
}
@ -84,7 +85,8 @@ private:
SkTArray<VertexChunk>* const fVertexChunkArray;
const int fMaxTessellationSegments;
const float fMatrixScale;
// GrTessellationPathRenderer::kIntolerance adjusted for the matrix scale.
const float fLinearizationIntolerance;
// Variables related to the vertex chunk that we are currently filling.
int fCurrChunkVertexCapacity;

View File

@ -26,9 +26,12 @@ GrStrokeTessellateOp::GrStrokeTessellateOp(GrAAType aaType, const SkMatrix& view
, fPathStrokes(path, stroke)
, fTotalCombinedVerbCnt(path.countVerbs())
, fAAType(aaType)
, fViewMatrix(viewMatrix)
, fMatrixScale(fViewMatrix.getMaxScale())
, fColor(get_paint_constant_blended_color(paint))
, fProcessors(std::move(paint)) {
SkASSERT(fAAType != GrAAType::kCoverage); // No mixed samples support yet.
SkASSERT(fMatrixScale >= 0);
if (stroke.getJoin() == SkPaint::kMiter_Join) {
float miter = stroke.getMiter();
if (miter <= 0) {
@ -37,18 +40,6 @@ GrStrokeTessellateOp::GrStrokeTessellateOp(GrAAType aaType, const SkMatrix& view
fMiterLimitOrZero = miter;
}
}
if (!(viewMatrix.getType() & ~SkMatrix::kScale_Mask) &&
viewMatrix.getScaleX() == viewMatrix.getScaleY()) {
fMatrixScale = viewMatrix.getScaleX();
fSkewMatrix = SkMatrix::I();
} else {
SkASSERT(!viewMatrix.hasPerspective()); // getMaxScale() doesn't work with perspective.
fMatrixScale = viewMatrix.getMaxScale();
float invScale = SkScalarInvert(fMatrixScale);
fSkewMatrix = viewMatrix;
fSkewMatrix.preScale(invScale, invScale);
}
SkASSERT(fMatrixScale >= 0);
SkRect devBounds = fPathStrokes.head().fPath.getBounds();
float inflationRadius = fPathStrokes.head().fStroke.getInflationRadius();
devBounds.outset(inflationRadius, inflationRadius);
@ -78,10 +69,7 @@ GrOp::CombineResult GrStrokeTessellateOp::onCombineIfPossible(GrOp* grOp,
const GrCaps&) {
auto* op = grOp->cast<GrStrokeTessellateOp>();
if (fColor != op->fColor ||
// TODO: When stroking is finished, we may want to consider whether a unique matrix scale
// can be stored with each PathStroke instead. This might improve batching.
fMatrixScale != op->fMatrixScale ||
fSkewMatrix != op->fSkewMatrix ||
fViewMatrix != op->fViewMatrix ||
fAAType != op->fAAType ||
((fMiterLimitOrZero * op->fMiterLimitOrZero != 0) && // Are both non-zero?
fMiterLimitOrZero != op->fMiterLimitOrZero) ||
@ -122,7 +110,7 @@ void GrStrokeTessellateOp::onExecute(GrOpFlushState* flushState, const SkRect& c
initArgs.fWriteSwizzle = flushState->drawOpArgs().writeSwizzle();
GrPipeline pipeline(initArgs, std::move(fProcessors), flushState->detachAppliedClip());
GrStrokeTessellateShader strokeShader(fSkewMatrix, fColor, fMiterLimitOrZero);
GrStrokeTessellateShader strokeShader(fMatrixScale, fMiterLimitOrZero, fViewMatrix, fColor);
GrPathShader::ProgramInfo programInfo(flushState->writeView(), &pipeline, &strokeShader);
SkASSERT(chainBounds == this->bounds());

View File

@ -49,8 +49,8 @@ private:
int fTotalCombinedVerbCnt;
const GrAAType fAAType;
float fMatrixScale; // The matrix scale is applied to control points before tessellation.
SkMatrix fSkewMatrix; // The skew matrix is applied to the post-tessellation triangles.
SkMatrix fViewMatrix;
float fMatrixScale;
float fMiterLimitOrZero = 0; // Zero if there is not a stroke with a miter join type.
SkPMColor4f fColor;
GrProcessorSet fProcessors;

View File

@ -18,12 +18,14 @@ constexpr static float kLinearizationIntolerance =
class GrStrokeTessellateShader::Impl : public GrGLSLGeometryProcessor {
public:
const char* getMiterLimitUniformName(const GrGLSLUniformHandler& uniformHandler) const {
return uniformHandler.getUniformCStr(fMiterLimitUniform);
const char* getTessControlArgsUniformName(const GrGLSLUniformHandler& uniformHandler) const {
return uniformHandler.getUniformCStr(fTessControlArgsUniform);
}
const char* getSkewMatrixUniformName(const GrGLSLUniformHandler& uniformHandler) const {
return uniformHandler.getUniformCStr(fSkewMatrixUniform);
const char* getTranslateUniformName(const GrGLSLUniformHandler& uniformHandler) const {
return uniformHandler.getUniformCStr(fTranslateUniform);
}
const char* getAffineMatrixUniformName(const GrGLSLUniformHandler& uniformHandler) const {
return uniformHandler.getUniformCStr(fAffineMatrixUniform);
}
private:
@ -31,21 +33,20 @@ private:
const auto& shader = args.fGP.cast<GrStrokeTessellateShader>();
args.fVaryingHandler->emitAttributes(shader);
fMiterLimitUniform = args.fUniformHandler->addUniform(nullptr, kTessControl_GrShaderFlag,
kFloat_GrSLType, "miterLimit",
nullptr);
auto* uniHandler = args.fUniformHandler;
fTessControlArgsUniform = uniHandler->addUniform(nullptr, kTessControl_GrShaderFlag,
kFloat2_GrSLType, "tessControlArgs",
nullptr);
if (!shader.viewMatrix().isIdentity()) {
fSkewMatrixUniform = args.fUniformHandler->addUniform(nullptr,
kTessEvaluation_GrShaderFlag,
kFloat3x3_GrSLType, "skewMatrix",
nullptr);
fTranslateUniform = uniHandler->addUniform(nullptr, kTessEvaluation_GrShaderFlag,
kFloat2_GrSLType, "translate", nullptr);
fAffineMatrixUniform = uniHandler->addUniform(nullptr, kTessEvaluation_GrShaderFlag,
kFloat2x2_GrSLType, "affineMatrix",
nullptr);
}
const char* colorUniformName;
fColorUniform = args.fUniformHandler->addUniform(nullptr, kFragment_GrShaderFlag,
kHalf4_GrSLType, "color",
&colorUniformName);
fColorUniform = uniHandler->addUniform(nullptr, kFragment_GrShaderFlag, kHalf4_GrSLType,
"color", &colorUniformName);
// The vertex shader is pure pass-through. Stroke widths and normals are defined in local
// path space, so we don't apply the view matrix until after tessellation.
@ -61,27 +62,22 @@ private:
void setData(const GrGLSLProgramDataManager& pdman,
const GrPrimitiveProcessor& primProc) override {
const auto& shader = primProc.cast<GrStrokeTessellateShader>();
if (shader.fMiterLimitOrZero != 0 && fCachedMiterLimitValue != shader.fMiterLimitOrZero) {
pdman.set1f(fMiterLimitUniform, shader.fMiterLimitOrZero);
fCachedMiterLimitValue = shader.fMiterLimitOrZero;
// tessControlArgs.x is the tolerance in pixels.
pdman.set2f(fTessControlArgsUniform, 1 / (kLinearizationIntolerance * shader.fMatrixScale),
shader.fMiterLimit);
const SkMatrix& m = shader.viewMatrix();
if (!m.isIdentity()) {
pdman.set2f(fTranslateUniform, m.getTranslateX(), m.getTranslateY());
float affineMatrix[4] = {m.getScaleX(), m.getSkewY(), m.getSkewX(), m.getScaleY()};
pdman.setMatrix2f(fAffineMatrixUniform, affineMatrix);
}
if (!shader.viewMatrix().isIdentity()) {
// Since the view matrix is applied after tessellation, it must not expand the geometry
// in any direction.
SkASSERT(shader.viewMatrix().getMaxScale() < 1 + SK_ScalarNearlyZero);
pdman.setSkMatrix(fSkewMatrixUniform, shader.viewMatrix());
}
pdman.set4fv(fColorUniform, 1, shader.fColor.vec());
}
GrGLSLUniformHandler::UniformHandle fMiterLimitUniform;
GrGLSLUniformHandler::UniformHandle fSkewMatrixUniform;
GrGLSLUniformHandler::UniformHandle fTessControlArgsUniform;
GrGLSLUniformHandler::UniformHandle fTranslateUniform;
GrGLSLUniformHandler::UniformHandle fAffineMatrixUniform;
GrGLSLUniformHandler::UniformHandle fColorUniform;
float fCachedMiterLimitValue = -1;
};
SkString GrStrokeTessellateShader::getTessControlShaderGLSL(
@ -92,15 +88,14 @@ SkString GrStrokeTessellateShader::getTessControlShaderGLSL(
SkString code(versionAndExtensionDecls);
code.append("layout(vertices = 1) out;\n");
code.appendf("const float kTolerance = %f;\n", 1/kLinearizationIntolerance);
code.appendf("const float kCubicK = %f;\n", GrWangsFormula::cubic_k(kLinearizationIntolerance));
code.appendf("const float kPI = 3.141592653589793238;\n");
code.appendf("const float kMaxTessellationSegments = %i;\n",
shaderCaps.maxTessellationSegments());
const char* miterLimitName = impl->getMiterLimitUniformName(uniformHandler);
code.appendf("uniform float %s;\n", miterLimitName);
code.appendf("#define uMiterLimit %s\n", miterLimitName);
const char* tessControlArgsName = impl->getTessControlArgsUniformName(uniformHandler);
code.appendf("uniform vec2 %s;\n", tessControlArgsName);
code.appendf("#define uTolerance %s.x\n", tessControlArgsName);
code.appendf("#define uMiterLimit %s.y\n", tessControlArgsName);
code.append(R"(
in vec2 P[];
@ -133,10 +128,12 @@ SkString GrStrokeTessellateShader::getTessControlShaderGLSL(
float strokeRadius = P[4].y;
// Calculate the number of evenly spaced (in the parametric sense) segments to chop the
// curve into. (See GrWangsFormula::cubic().) The final tessellated strip will be a
// composition of these parametric segments as well as radial segments.
float numParametricSegments = sqrt(kCubicK * length(max(abs(P[2] - P[1]*2.0 + P[0]),
abs(P[3] - P[2]*2.0 + P[1]))));
// curve into. (See GrWangsFormula::cubic() for more documentation on this formula.) The
// final tessellated strip will be a composition of these parametric segments as well as
// radial segments.
float numParametricSegments = sqrt(
.75/uTolerance * length(max(abs(P[2] - P[1]*2.0 + P[0]),
abs(P[3] - P[2]*2.0 + P[1]))));
if (P[1] == P[0] && P[2] == P[3]) {
// This type of curve is used to represent flat lines, but wang's formula does not
// return 1 segment. Force numParametricSegments to 1.
@ -178,7 +175,7 @@ SkString GrStrokeTessellateShader::getTessControlShaderGLSL(
// Calculate the number of evenly spaced radial segments to chop the curve into. Radial
// segments divide the curve's rotation into even steps. The final tessellated strip will be
// a composition of both parametric and radial segments.
float numRadialSegments = abs(rotation) / (2 * acos(max(1 - kTolerance/strokeRadius, -1)));
float numRadialSegments = abs(rotation) / (2 * acos(max(1 - uTolerance/strokeRadius, -1)));
numRadialSegments = max(ceil(numRadialSegments), 1);
// Set up joins.
@ -199,7 +196,7 @@ SkString GrStrokeTessellateShader::getTessControlShaderGLSL(
// Bevel join. Make a fan with only one segment.
numRadialSegments = 1;
}
if (length(tan0norm - tan1norm) * strokeRadius < kTolerance) {
if (length(tan0norm - tan1norm) * strokeRadius < uTolerance) {
// The join angle is too tight to guarantee there won't be gaps on the inside of the
// junction. Just in case our join was supposed to only go on the outside, switch to
// a double sided bevel that ties all 4 incoming vertices together. The join angle
@ -270,10 +267,13 @@ SkString GrStrokeTessellateShader::getTessEvaluationShaderGLSL(
code.appendf("const float kPI = 3.141592653589793238;\n");
const char* skewMatrixName = nullptr;
if (!this->viewMatrix().isIdentity()) {
skewMatrixName = impl->getSkewMatrixUniformName(uniformHandler);
code.appendf("uniform mat3x3 %s;\n", skewMatrixName);
const char* translateName = impl->getTranslateUniformName(uniformHandler);
code.appendf("uniform vec2 %s;\n", translateName);
code.appendf("#define uTranslate %s\n", translateName);
const char* affineMatrixName = impl->getAffineMatrixUniformName(uniformHandler);
code.appendf("uniform mat2x2 %s;\n", affineMatrixName);
code.appendf("#define uAffineMatrix %s\n", affineMatrixName);
}
code.append(R"(
@ -449,17 +449,17 @@ SkString GrStrokeTessellateShader::getTessEvaluationShaderGLSL(
outset = clamp(outset, strokeOutsetClamp.x, strokeOutsetClamp.y);
outset *= strokeRadius;
vec2 vertexpos = position + normalize(vec2(-tangent.y, tangent.x)) * outset;
vec2 vertexPos = position + normalize(vec2(-tangent.y, tangent.x)) * outset;
)");
// Transform after tessellation. Stroke widths and normals are defined in (pre-transform) local
// path space.
if (!this->viewMatrix().isIdentity()) {
code.appendf("vertexpos = (%s * vec3(vertexpos, 1)).xy;\n", skewMatrixName);
code.append("vertexPos = uAffineMatrix * vertexPos + uTranslate;");
}
code.append(R"(
gl_Position = vec4(vertexpos * sk_RTAdjust.xz + sk_RTAdjust.yw, 0.0, 1.0);
gl_Position = vec4(vertexPos * sk_RTAdjust.xz + sk_RTAdjust.yw, 0.0, 1.0);
}
)");

View File

@ -27,6 +27,8 @@ class GrGLSLUniformHandler;
// * 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 triangle strip. These orthogonal edges come from two
// different sets: "parametric edges" and "radial edges". Parametric edges are spaced evenly in the
@ -64,22 +66,20 @@ public:
constexpr static int kNumVerticesPerPatch = 5;
// 'skewMatrix' is applied to the post-tessellation triangles. It cannot expand the geometry in
// any direction. For now, patches should be pre-scaled on CPU by the view matrix's maxScale,
// which leaves 'skewMatrix' as the original view matrix divided by maxScale.
// '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.)
//
// If 'miterLimitOrZero' is zero, then the patches being drawn cannot include any miter joins.
// If a stroke uses miter joins with a miter limit of zero, then they need to be pre-converted
// to bevel joins.
GrStrokeTessellateShader(const SkMatrix& skewMatrix, SkPMColor4f color, float miterLimitOrZero)
: GrPathShader(kTessellate_GrStrokeTessellateShader_ClassID, skewMatrix,
// 'miterLimit' contains the stroke's miter limit, or may be zero if no patches being drawn will
// be miter joins.
//
// 'viewMatrix' is applied to the geometry post tessellation. It cannot have perspective.
GrStrokeTessellateShader(float matrixScale, float miterLimit, const SkMatrix& viewMatrix,
SkPMColor4f color)
: GrPathShader(kTessellate_GrStrokeTessellateShader_ClassID, viewMatrix,
GrPrimitiveType::kPatches, kNumVerticesPerPatch)
, fColor(color)
, fMiterLimitOrZero(miterLimitOrZero) {
// Since the skewMatrix is applied after tessellation, it cannot expand the geometry in any
// direction. (The caller can create a skewMatrix by dividing their viewMatrix by its
// maxScale and then pre-multiplying their control points by the same maxScale.)
SkASSERT(skewMatrix.getMaxScale() < 1 + SK_ScalarNearlyZero);
, fMatrixScale(matrixScale)
, fMiterLimit(miterLimit)
, fColor(color) {
constexpr static Attribute kInputPointAttrib{"inputPoint", kFloat2_GrVertexAttribType,
kFloat2_GrSLType};
this->setVertexAttributes(&kInputPointAttrib, 1);
@ -101,8 +101,9 @@ private:
const GrGLSLUniformHandler&,
const GrShaderCaps&) const override;
const float fMatrixScale;
const float fMiterLimit;
const SkPMColor4f fColor;
const float fMiterLimitOrZero; // Zero if there will not be any miter join patches.
class Impl;
};