Add first class hairline support to tessellated stroking

Bug: skia:10419
Change-Id: I63f000e7b3c5623c1e40c3ce6950c8f5565bc11c
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/343477
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
Commit-Queue: Chris Dalton <csmartdalton@google.com>
This commit is contained in:
Chris Dalton 2020-12-15 10:01:35 -07:00 committed by Skia Commit-Bot
parent 977715763d
commit 06b52ada19
9 changed files with 272 additions and 158 deletions

View File

@ -332,7 +332,9 @@ class Dashing4GM : public skiagm::GM {
for (int width = 0; width <= 2; ++width) {
for (const Intervals& data : {Intervals{1, 1},
Intervals{4, 2},
Intervals{0, 4}}) { // test for zero length on interval
Intervals{0, 4}}) { // test for zero length on interval.
// zero length intervals should draw
// a line of squares or circles
for (bool aa : {false, true}) {
for (auto cap : {SkPaint::kRound_Cap, SkPaint::kSquare_Cap}) {
int w = width * width * width;

View File

@ -22,17 +22,14 @@ void GrStrokeIndirectOp::onPrePrepare(GrRecordingContext* context,
GrXferBarrierFlags renderPassXferBarriers,
GrLoadOp colorLoadOp) {
auto* arena = context->priv().recordTimeAllocator();
this->prePrepareResolveLevels(context->priv().recordTimeAllocator());
this->prePrepareResolveLevels(arena);
SkASSERT(fResolveLevels);
if (!fTotalInstanceCount) {
return;
}
auto* strokeTessellateShader = arena->make<GrStrokeTessellateShader>(
GrStrokeTessellateShader::Mode::kIndirect, fTotalConicWeightCnt, fStroke, fViewMatrix,
fColor);
this->prePreparePrograms(context->priv().recordTimeAllocator(), strokeTessellateShader,
writeView, std::move(*clip), dstProxyView, renderPassXferBarriers,
colorLoadOp, *context->priv().caps());
this->prePreparePrograms(GrStrokeTessellateShader::Mode::kIndirect, arena, writeView,
std::move(*clip), dstProxyView, renderPassXferBarriers, colorLoadOp,
*context->priv().caps());
if (fFillProgram) {
context->priv().recordProgramInfo(fFillProgram);
}
@ -441,7 +438,7 @@ void GrStrokeIndirectOp::prePrepareResolveLevels(SkArenaAlloc* alloc) {
fChopTs = alloc->makeArrayDefault<float>(chopTAllocCount);
float* nextChopTs = fChopTs;
GrStrokeTessellateShader::Tolerances tolerances(fViewMatrix.getMaxScale(), fStroke.getWidth());
auto tolerances = this->preTransformTolerances();
fResolveLevelForCircles =
sk_float_nextlog2(tolerances.fNumRadialSegmentsPerRadian * SK_ScalarPI);
ResolveLevelCounter counter(fStroke, tolerances, fResolveLevelCounts);
@ -449,7 +446,7 @@ void GrStrokeIndirectOp::prePrepareResolveLevels(SkArenaAlloc* alloc) {
SkPoint lastControlPoint = {0,0};
for (const SkPath& path : fPathList) {
// Iterate through each verb in the stroke, counting its resolveLevel(s).
GrStrokeIterator iter(path, fStroke);
GrStrokeIterator iter(path, &fStroke, &fViewMatrix);
while (iter.next()) {
using Verb = GrStrokeIterator::Verb;
Verb verb = iter.verb();
@ -586,13 +583,10 @@ void GrStrokeIndirectOp::onPrepare(GrOpFlushState* flushState) {
if (!fTotalInstanceCount) {
return;
}
auto* strokeTessellateShader = arena->make<GrStrokeTessellateShader>(
GrStrokeTessellateShader::Mode::kIndirect, fTotalConicWeightCnt, fStroke,
fViewMatrix, fColor);
this->prePreparePrograms(arena, strokeTessellateShader, flushState->writeView(),
flushState->detachAppliedClip(), flushState->dstProxyView(),
flushState->renderPassBarriers(), flushState->colorLoadOp(),
flushState->caps());
this->prePreparePrograms(GrStrokeTessellateShader::Mode::kIndirect, arena,
flushState->writeView(), flushState->detachAppliedClip(),
flushState->dstProxyView(), flushState->renderPassBarriers(),
flushState->colorLoadOp(), flushState->caps());
}
SkASSERT(fResolveLevels);
@ -684,7 +678,7 @@ void GrStrokeIndirectOp::prepareBuffers(GrMeshDrawOp::Target* target) {
// Now write out each instance to its resolveLevel's designated location in the instance buffer.
for (const SkPath& path : fPathList) {
GrStrokeIterator iter(path, fStroke);
GrStrokeIterator iter(path, &fStroke, &fViewMatrix);
bool hasLastControlPoint = false;
while (iter.next()) {
using Verb = GrStrokeIterator::Verb;

View File

@ -29,8 +29,8 @@
//
class GrStrokeIterator {
public:
GrStrokeIterator(const SkPath& path, const SkStrokeRec& stroke)
: fCapType(stroke.getCap()), fStrokeRadius(stroke.getWidth() * .5) {
GrStrokeIterator(const SkPath& path, const SkStrokeRec* stroke, const SkMatrix* viewMatrix)
: fViewMatrix(viewMatrix), fStroke(stroke) {
SkPathPriv::Iterate it(path);
fIter = it.begin();
fEnd = it.end();
@ -174,7 +174,7 @@ private:
if (fQueueCount) {
SkASSERT(this->backVerb() == Verb::kLine || this->backVerb() == Verb::kQuad ||
this->backVerb() == Verb::kConic || this->backVerb() == Verb::kCubic);
switch (fCapType) {
switch (fStroke->getCap()) {
case SkPaint::kButt_Cap:
// There are no caps, but inject a "move" so the first stroke doesn't get joined
// with the end of the contour when it's processed.
@ -207,7 +207,7 @@ private:
//
// (https://www.w3.org/TR/SVG11/painting.html#StrokeProperties)
//
switch (fCapType) {
switch (fStroke->getCap()) {
case SkPaint::kButt_Cap:
// Zero-length contour with butt caps. There are no caps and no first stroke to
// generate.
@ -220,9 +220,39 @@ private:
fFirstPtsInContour = fLastDegenerateStrokePt;
fFirstWInContour = nullptr;
break;
case SkPaint::kSquare_Cap:
fEndingCapPts = {*fLastDegenerateStrokePt - SkPoint{fStrokeRadius, 0},
*fLastDegenerateStrokePt + SkPoint{fStrokeRadius, 0}};
case SkPaint::kSquare_Cap: {
SkPoint outset;
if (!fStroke->isHairlineStyle()) {
// Implement degenerate square caps as a stroke-width square in path space.
outset = {fStroke->getWidth() * .5f, 0};
} else {
// If the stroke is hairline, draw a 1x1 device-space square instead. This
// is equivalent to using:
//
// outset = inverse(fViewMatrix).mapVector(.5, 0)
//
// And since the matrix cannot have perspective, we only need to invert the
// upper 2x2 of the viewMatrix to achieve this.
SkASSERT(!fViewMatrix->hasPerspective());
float a=fViewMatrix->getScaleX(), b=fViewMatrix->getSkewX(),
c=fViewMatrix->getSkewY(), d=fViewMatrix->getScaleY();
float det = a*d - b*c;
if (det > 0) {
// outset = inverse(|a b|) * |.5|
// |c d| | 0|
//
// == 1/det * | d -b| * |.5|
// |-c a| | 0|
//
// == | d| * .5/det
// |-c|
outset = SkVector{d, -c} * (.5f / det);
} else {
outset = {1, 0};
}
}
fEndingCapPts = {*fLastDegenerateStrokePt - outset,
*fLastDegenerateStrokePt + outset};
// Add the square first as the "prev" join.
this->enqueue(Verb::kLine, fEndingCapPts.data(), nullptr);
this->enqueue(Verb::kMoveWithinContour, fEndingCapPts.data(), nullptr);
@ -233,6 +263,7 @@ private:
fFirstWInContour = nullptr;
break;
}
}
} else {
// This contour had no lines, beziers, or "close" verbs. There are no caps and no first
// stroke to generate.
@ -274,9 +305,15 @@ private:
default:
SkUNREACHABLE;
}
lastTangent.normalize();
if (!fStroke->isHairlineStyle()) {
// Extend the cap by 1/2 stroke width.
lastTangent *= (.5f * fStroke->getWidth()) / lastTangent.length();
} else {
// Extend the cap by what will be 1/2 pixel after transformation.
lastTangent *= .5f / fViewMatrix->mapVector(lastTangent.fX, lastTangent.fY).length();
}
SkPoint lastPoint = lastPts[SkPathPriv::PtsInIter((unsigned)lastVerb) - 1];
fEndingCapPts = {lastPoint, lastPoint + lastTangent * fStrokeRadius};
fEndingCapPts = {lastPoint, lastPoint + lastTangent};
// Find the endpoints of the cap at the beginning of the contour.
SkVector firstTangent = fFirstPtsInContour[1] - fFirstPtsInContour[0];
@ -290,14 +327,20 @@ private:
SkASSERT(!firstTangent.isZero());
}
}
firstTangent.normalize();
fBeginningCapPts = {fFirstPtsInContour[0] - firstTangent * fStrokeRadius,
fFirstPtsInContour[0]};
if (!fStroke->isHairlineStyle()) {
// Set the the cap back by 1/2 stroke width.
firstTangent *= (-.5f * fStroke->getWidth()) / firstTangent.length();
} else {
// Set the cap back by what will be 1/2 pixel after transformation.
firstTangent *=
-.5f / fViewMatrix->mapVector(firstTangent.fX, firstTangent.fY).length();
}
fBeginningCapPts = {fFirstPtsInContour[0] + firstTangent, fFirstPtsInContour[0]};
}
// Info and iterators from the original path.
const SkPaint::Cap fCapType;
const float fStrokeRadius;
const SkMatrix* const fViewMatrix; // For hairlines.
const SkStrokeRec* const fStroke;
SkPathPriv::RangeIter fIter;
SkPathPriv::RangeIter fEnd;

View File

@ -10,7 +10,6 @@
#include "src/core/SkPathPriv.h"
#include "src/gpu/GrRecordingContextPriv.h"
#include "src/gpu/tessellate/GrStrokeTessellateOp.h"
#include "src/gpu/tessellate/GrStrokeTessellateShader.h"
GrStrokeOp::GrStrokeOp(uint32_t classID, GrAAType aaType, const SkMatrix& viewMatrix,
const SkStrokeRec& stroke, const SkPath& path, GrPaint&& paint)
@ -23,9 +22,6 @@ GrStrokeOp::GrStrokeOp(uint32_t classID, GrAAType aaType, const SkMatrix& viewMa
, fPathList(path)
, fTotalCombinedVerbCnt(path.countVerbs())
, fTotalConicWeightCnt(SkPathPriv::ConicWeightCnt(path)) {
// 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);
SkRect devBounds = path.getBounds();
float inflationRadius = fStroke.getInflationRadius();
devBounds.outset(inflationRadius, inflationRadius);
@ -97,8 +93,7 @@ constexpr static GrUserStencilSettings kTestAndResetStencil(
GrUserStencilOp::kReplace,
0xffff>());
void GrStrokeOp::prePreparePrograms(SkArenaAlloc* arena,
GrStrokeTessellateShader* strokeTessellateShader,
void GrStrokeOp::prePreparePrograms(GrStrokeTessellateShader::Mode shaderMode, SkArenaAlloc* arena,
const GrSurfaceProxyView& writeView, GrAppliedClip&& clip,
const GrXferProcessor::DstProxyView& dstProxyView,
GrXferBarrierFlags renderPassXferBarriers,
@ -139,6 +134,8 @@ void GrStrokeOp::prePreparePrograms(SkArenaAlloc* arena,
}
}
auto* strokeTessellateShader = arena->make<GrStrokeTessellateShader>(
shaderMode, fTotalConicWeightCnt, fStroke, fViewMatrix, fColor);
auto fillPipeline = arena->make<GrPipeline>(fillArgs, std::move(fProcessors), std::move(clip));
auto fillStencil = &GrUserStencilSettings::kUnused;
auto fillXferFlags = renderPassXferBarriers;

View File

@ -12,6 +12,8 @@
#include "include/gpu/GrRecordingContext.h"
#include "src/gpu/GrSTArenaList.h"
#include "src/gpu/ops/GrDrawOp.h"
#include "src/gpu/tessellate/GrStrokeTessellateShader.h"
#include <array>
class GrStrokeTessellateShader;
@ -34,7 +36,7 @@ protected:
bool hasMixedSampledCoverage, GrClampType) override;
CombineResult onCombineIfPossible(GrOp*, SkArenaAlloc*, const GrCaps&) override;
void prePreparePrograms(SkArenaAlloc* arena, GrStrokeTessellateShader*,
void prePreparePrograms(GrStrokeTessellateShader::Mode, SkArenaAlloc*,
const GrSurfaceProxyView&, GrAppliedClip&&,
const GrXferProcessor::DstProxyView&, GrXferBarrierFlags,
GrLoadOp colorLoadOp, const GrCaps&);
@ -64,6 +66,29 @@ protected:
return std::max(numCombinedSegments + 1 - numRadialSegments, 0.f);
}
// Returns the equivalent tolerances in (pre-viewMatrix) local path space that the tessellator
// will use when rendering this stroke.
GrStrokeTessellateShader::Tolerances preTransformTolerances() const {
std::array<float,2> matrixScales;
if (!fViewMatrix.getMinMaxScales(matrixScales.data())) {
matrixScales.fill(1);
}
auto [matrixMinScale, matrixMaxScale] = matrixScales;
float localStrokeWidth = fStroke.getWidth();
if (fStroke.isHairlineStyle()) {
// If the stroke is hairline then the tessellator will operate in post-transform space
// instead. But for the sake of CPU methods that need to conservatively approximate the
// number of segments to emit, we use localStrokeWidth ~= 1/matrixMinScale.
float approxScale = matrixMinScale;
// If the matrix has strong skew, don't let the scale shoot off to infinity. (This does
// not affect the tessellator; only the CPU methods that approximate the number of
// segments to emit.)
approxScale = std::max(matrixMinScale, matrixMaxScale * .25f);
localStrokeWidth = 1/approxScale;
}
return GrStrokeTessellateShader::Tolerances(matrixMaxScale, localStrokeWidth);
}
const GrAAType fAAType;
const SkMatrix fViewMatrix;
const SkStrokeRec fStroke;

View File

@ -19,11 +19,8 @@ void GrStrokeTessellateOp::onPrePrepare(GrRecordingContext* context,
const GrXferProcessor::DstProxyView& dstProxyView,
GrXferBarrierFlags renderPassXferBarriers,
GrLoadOp colorLoadOp) {
SkArenaAlloc* arena = context->priv().recordTimeAllocator();
auto* strokeTessellateShader = arena->make<GrStrokeTessellateShader>(
GrStrokeTessellateShader::Mode::kTessellation, false/*hasConics*/, fStroke,
fViewMatrix, fColor);
this->prePreparePrograms(arena, strokeTessellateShader, writeView, std::move(*clip),
this->prePreparePrograms(GrStrokeTessellateShader::Mode::kTessellation,
context->priv().recordTimeAllocator(), writeView, std::move(*clip),
dstProxyView, renderPassXferBarriers, colorLoadOp,
*context->priv().caps());
if (fStencilProgram) {
@ -36,14 +33,11 @@ void GrStrokeTessellateOp::onPrePrepare(GrRecordingContext* context,
void GrStrokeTessellateOp::onPrepare(GrOpFlushState* flushState) {
if (!fFillProgram && !fStencilProgram) {
SkArenaAlloc* arena = flushState->allocator();
auto* strokeTessellateShader = arena->make<GrStrokeTessellateShader>(
GrStrokeTessellateShader::Mode::kTessellation, false/*hasConics*/, fStroke,
fViewMatrix, fColor);
this->prePreparePrograms(flushState->allocator(), strokeTessellateShader,
flushState->writeView(), flushState->detachAppliedClip(),
flushState->dstProxyView(), flushState->renderPassBarriers(),
flushState->colorLoadOp(), flushState->caps());
this->prePreparePrograms(GrStrokeTessellateShader::Mode::kTessellation,
flushState->allocator(), flushState->writeView(),
flushState->detachAppliedClip(), flushState->dstProxyView(),
flushState->renderPassBarriers(), flushState->colorLoadOp(),
flushState->caps());
}
SkASSERT(fFillProgram || fStencilProgram);
@ -64,7 +58,7 @@ void GrStrokeTessellateOp::prepareBuffers() {
// has the potential to introduce an extra segment.
fMaxTessellationSegments = fTarget->caps().shaderCaps()->maxTessellationSegments() - 2;
fTolerances.set(fViewMatrix.getMaxScale(), fStroke.getWidth());
fTolerances = this->preTransformTolerances();
// 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
@ -465,12 +459,6 @@ void GrStrokeTessellateOp::close() {
SkDEBUGCODE(fHasCurrentPoint = false;)
}
static SkVector normalize(const SkVector& v) {
SkVector norm = v;
norm.normalize();
return norm;
}
void GrStrokeTessellateOp::cap() {
SkASSERT(fHasCurrentPoint);
@ -478,8 +466,32 @@ void GrStrokeTessellateOp::cap() {
// We don't have any control points to orient the caps. In this case, square and round caps
// are specified to be drawn as an axis-aligned square or circle respectively. Assign
// default control points that achieve this.
fCurrContourFirstControlPoint = fCurrContourStartPoint - SkPoint{1,0};
fLastControlPoint = fCurrContourStartPoint + SkPoint{1,0};
SkVector outset;
if (!fStroke.isHairlineStyle()) {
outset = {1, 0};
} else {
// If the stroke is hairline, orient the square on the post-transform x-axis instead.
// We don't need to worry about the vector length since it will be normalized later.
// Since the matrix cannot have perspective, the below is equivalent to:
//
// outset = inverse(|a b|) * |1| * arbitrary_scale
// |c d| |0|
//
// == 1/det * | d -b| * |1| * arbitrary_scale
// |-c a| |0|
//
// == 1/det * | d| * arbitrary_scale
// |-c|
//
// == | d|
// |-c|
//
SkASSERT(!fViewMatrix.hasPerspective());
float c=fViewMatrix.getSkewY(), d=fViewMatrix.getScaleY();
outset = {d, -c};
}
fCurrContourFirstControlPoint = fCurrContourStartPoint - outset;
fLastControlPoint = fCurrContourStartPoint + outset;
fCurrentPoint = fCurrContourStartPoint;
fHasLastControlPoint = true;
}
@ -497,14 +509,28 @@ void GrStrokeTessellateOp::cap() {
this->joinTo(roundCapJoinType, fCurrContourFirstControlPoint);
break;
}
case SkPaint::kSquare_Cap: {
// A square cap is the same as appending lineTos.
float rad = fStroke.getWidth() * .5f;
this->lineTo(fCurrentPoint + normalize(fCurrentPoint - fLastControlPoint) * rad);
SkVector lastTangent = fCurrentPoint - fLastControlPoint;
if (!fStroke.isHairlineStyle()) {
// Extend the cap by 1/2 stroke width.
lastTangent *= (.5f * fStroke.getWidth()) / lastTangent.length();
} else {
// Extend the cap by what will be 1/2 pixel after transformation.
lastTangent *= .5f / fViewMatrix.mapVector(lastTangent.fX, lastTangent.fY).length();
}
this->lineTo(fCurrentPoint + lastTangent);
this->moveTo(fCurrContourStartPoint, fCurrContourFirstControlPoint);
this->lineTo(fCurrContourStartPoint +
normalize(fCurrContourStartPoint - fCurrContourFirstControlPoint) * rad);
SkVector firstTangent = fCurrContourFirstControlPoint - fCurrContourStartPoint;
if (!fStroke.isHairlineStyle()) {
// Set the the cap back by 1/2 stroke width.
firstTangent *= (-.5f * fStroke.getWidth()) / firstTangent.length();
} else {
// Set the cap back by what will be 1/2 pixel after transformation.
firstTangent *=
-.5f / fViewMatrix.mapVector(firstTangent.fX, firstTangent.fY).length();
}
this->lineTo(fCurrContourStartPoint + firstTangent);
break;
}
}

View File

@ -32,6 +32,7 @@ private:
void onEmitCode(EmitArgs& args, GrGPArgs* gpArgs) override {
const auto& shader = args.fGP.cast<GrStrokeTessellateShader>();
auto* uniHandler = args.fUniformHandler;
auto* v = args.fVertBuilder;
SkASSERT(!shader.fHasConics);
@ -48,9 +49,17 @@ private:
if (!shader.viewMatrix().isIdentity()) {
fTranslateUniform = uniHandler->addUniform(nullptr, kTessEvaluation_GrShaderFlag,
kFloat2_GrSLType, "translate", nullptr);
fAffineMatrixUniform = uniHandler->addUniform(nullptr, kTessEvaluation_GrShaderFlag,
const char* affineMatrixName;
// Hairlines apply the affine matrix in their vertex shader, prior to tessellation.
// Otherwise the entire view matrix gets applied at the end of the tess eval shader.
auto affineMatrixVisibility = (shader.fStroke.isHairlineStyle()) ?
kVertex_GrShaderFlag : kTessEvaluation_GrShaderFlag;
fAffineMatrixUniform = uniHandler->addUniform(nullptr, affineMatrixVisibility,
kFloat4_GrSLType, "affineMatrix",
nullptr);
&affineMatrixName);
if (affineMatrixVisibility & kVertex_GrShaderFlag) {
v->codeAppendf("float2x2 uAffineMatrix = float2x2(%s);\n", affineMatrixName);
}
}
const char* colorUniformName;
fColorUniform = uniHandler->addUniform(nullptr, kFragment_GrShaderFlag, kHalf4_GrSLType,
@ -64,7 +73,6 @@ private:
// still don't have 3 sections after that then we just subdivide uniformly in parametric
// space.
using TypeModifier = GrShaderVar::TypeModifier;
auto* v = args.fVertBuilder;
v->defineConstantf("float", "kParametricEpsilon", "1.0 / (%i * 128)",
args.fShaderCaps->maxTessellationSegments()); // 1/128 of a segment.
v->declareGlobal(GrShaderVar("vsPts01", kFloat4_GrSLType, TypeModifier::Out));
@ -87,7 +95,16 @@ private:
v->codeAppendf(R"(
// Unpack the control points.
float4x2 P = float4x2(inputPts01, inputPts23);
float2 prevJoinTangent = P[0] - inputPrevCtrlPt;
float2 prevControlPoint = inputPrevCtrlPt;)");
if (shader.fStroke.isHairlineStyle() && !shader.viewMatrix().isIdentity()) {
// Hairline case. Transform the points before tessellation. We can still hold off on the
// translate until the end; we just need to perform the scale and skew right now.
v->codeAppend(R"(
P = uAffineMatrix * P;
prevControlPoint = uAffineMatrix * prevControlPoint;)");
}
v->codeAppendf(R"(
float2 prevJoinTangent = P[0] - prevControlPoint;
// Find the beginning and ending tangents. It's imperative that we compute these tangents
// form the original input points or else the seams might crack.
@ -249,19 +266,27 @@ private:
void setData(const GrGLSLProgramDataManager& pdman,
const GrPrimitiveProcessor& primProc) override {
const auto& shader = primProc.cast<GrStrokeTessellateShader>();
const auto& stroke = shader.fStroke;
float numSegmentsInJoin;
switch (shader.fStroke.getJoin()) {
switch (stroke.getJoin()) {
case SkPaint::kBevel_Join:
numSegmentsInJoin = 1;
break;
case SkPaint::kMiter_Join:
numSegmentsInJoin = (shader.fStroke.getMiter() > 0) ? 2 : 1;
numSegmentsInJoin = (stroke.getMiter() > 0) ? 2 : 1;
break;
case SkPaint::kRound_Join:
numSegmentsInJoin = 0; // Use the rotation to calculate the number of segments.
break;
}
Tolerances tolerances(shader.viewMatrix().getMaxScale(), shader.fStroke.getWidth());
Tolerances tolerances;
if (!stroke.isHairlineStyle()) {
tolerances.set(shader.viewMatrix().getMaxScale(), stroke.getWidth());
} else {
// In the hairline case we transform prior to tessellation. Set up tolerances for an
// identity viewMatrix and a strokeWidth of 1.
tolerances.set(1, 1);
}
float miterLimit = shader.fStroke.getMiter();
pdman.set4f(fTessArgs1Uniform,
numSegmentsInJoin, // uNumSegmentsInJoin
@ -269,7 +294,7 @@ private:
tolerances.fParametricIntolerance), // uWangsTermPow2
tolerances.fNumRadialSegmentsPerRadian, // uNumRadialSegmentsPerRadian
1 / (miterLimit * miterLimit)); // uMiterLimitInvPow2
float strokeRadius = shader.fStroke.getWidth() * .5;
float strokeRadius = (stroke.isHairlineStyle()) ? .5f : stroke.getWidth() * .5;
float joinTolerance = 1 / (strokeRadius * tolerances.fParametricIntolerance);
pdman.set2f(fTessArgs2Uniform,
joinTolerance * joinTolerance, // uJoinTolerancePow2
@ -722,9 +747,13 @@ SkString GrStrokeTessellateShader::getTessEvaluationShaderGLSL(
const char* translateName = impl->getTranslateUniformName(uniformHandler);
code.appendf("uniform vec2 %s;\n", translateName);
code.appendf("#define uTranslate %s\n", translateName);
if (!fStroke.isHairlineStyle()) {
// In the normal case we need the affine matrix too. (In the hairline case we already
// applied the affine matrix in the vertex shader.)
const char* affineMatrixName = impl->getAffineMatrixUniformName(uniformHandler);
code.appendf("uniform vec4 %s;\n", affineMatrixName);
code.appendf("#define uAffineMatrix %s\n", affineMatrixName);
code.appendf("#define uAffineMatrix mat2(%s)\n", affineMatrixName);
}
}
code.append(R"(
@ -811,10 +840,14 @@ SkString GrStrokeTessellateShader::getTessEvaluationShaderGLSL(
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.append("vertexPos = mat2(uAffineMatrix) * vertexPos + uTranslate;");
if (!fStroke.isHairlineStyle()) {
// Normal case. Do the transform after tessellation.
code.append("vertexPos = uAffineMatrix * vertexPos + uTranslate;");
} else {
// Hairline case. The scale and skew already happened before tessellation.
code.append("vertexPos = vertexPos + uTranslate;");
}
}
code.append(R"(
@ -862,9 +895,23 @@ class GrStrokeTessellateShader::IndirectImpl : public GrGLSLGeometryProcessor {
args.fVertBuilder->codeAppendf("float uMiterLimitInvPow2 = %s.z;\n", tessArgsName);
args.fVertBuilder->codeAppendf("float uStrokeRadius = %s.w;\n", tessArgsName);
// View matrix uniforms.
if (!shader.viewMatrix().isIdentity()) {
const char* translateName, *affineMatrixName;
fAffineMatrixUniform = args.fUniformHandler->addUniform(
nullptr, kVertex_GrShaderFlag, kFloat4_GrSLType, "affineMatrix",
&affineMatrixName);
fTranslateUniform = args.fUniformHandler->addUniform(
nullptr, kVertex_GrShaderFlag, kFloat2_GrSLType, "translate", &translateName);
args.fVertBuilder->codeAppendf("float2x2 uAffineMatrix = float2x2(%s);\n",
affineMatrixName);
args.fVertBuilder->codeAppendf("float2 uTranslate = %s;\n", translateName);
}
// Tessellation code.
args.fVertBuilder->codeAppend(R"(
float4x2 P = float4x2(pts01, pts23);)");
float4x2 P = float4x2(pts01, pts23);
float2 lastControlPoint = args.xy;)");
if (shader.fHasConics) {
args.fVertBuilder->codeAppend(R"(
float w = -1; // w<0 means the curve is an integral cubic.
@ -873,8 +920,14 @@ class GrStrokeTessellateShader::IndirectImpl : public GrGLSLGeometryProcessor {
P[3] = P[2]; // Setting p3 equal to p2 works for the remaining rotational logic.
})");
}
if (shader.fStroke.isHairlineStyle() && !shader.viewMatrix().isIdentity()) {
// Hairline case. Transform the points before tessellation. We can still hold off on the
// translate until the end; we just need to perform the scale and skew right now.
args.fVertBuilder->codeAppend(R"(
P = uAffineMatrix * P;
lastControlPoint = uAffineMatrix * lastControlPoint;)");
}
args.fVertBuilder->codeAppend(R"(
float2 lastControlPoint = args.xy;
float numTotalEdges = abs(args.z);
// Use wang's formula to find how many parametric segments this stroke requires.
@ -1003,7 +1056,7 @@ class GrStrokeTessellateShader::IndirectImpl : public GrGLSLGeometryProcessor {
}
args.fVertBuilder->codeAppendf(R"(
float2 tangent, localCoord;
float2 tangent, strokeCoord;
eval_stroke_edge(P,)");
if (shader.fHasConics) {
args.fVertBuilder->codeAppend(R"(
@ -1011,42 +1064,44 @@ class GrStrokeTessellateShader::IndirectImpl : public GrGLSLGeometryProcessor {
}
args.fVertBuilder->codeAppend(R"(
numParametricSegments, combinedEdgeID, tan0, radsPerSegment, angle0,
tangent, localCoord);)");
tangent, strokeCoord);)");
args.fVertBuilder->codeAppend(R"(
if (combinedEdgeID == 0) {
// Edges at the beginning of their section use P[0] and tan0. This ensures crack-free
// seaming between instances.
localCoord = P[0];
strokeCoord = P[0];
tangent = tan0;
}
if (combinedEdgeID == numCombinedSegments) {
// Edges at the end of their section use P[1] and tan1. This ensures crack-free seaming
// between instances.
localCoord = P[3];
strokeCoord = P[3];
tangent = tan1;
}
float2 ortho = normalize(float2(tangent.y, -tangent.x));
localCoord += ortho * (uStrokeRadius * outset);)");
strokeCoord += ortho * (uStrokeRadius * outset);)");
// Do the transform after tessellation. Stroke widths and normals are defined in
// (pre-transform) local path space.
if (!shader.viewMatrix().isIdentity()) {
const char* translateName, *affineMatrixName;
fTranslateUniform = args.fUniformHandler->addUniform(
nullptr, kVertex_GrShaderFlag, kFloat2_GrSLType, "translate", &translateName);
fAffineMatrixUniform = args.fUniformHandler->addUniform(
nullptr, kVertex_GrShaderFlag, kFloat4_GrSLType, "affineMatrix",
&affineMatrixName);
args.fVertBuilder->codeAppendf("float2 devCoord = float2x2(%s) * localCoord + %s;",
affineMatrixName, translateName);
if (shader.viewMatrix().isIdentity()) {
// No transform matrix.
gpArgs->fPositionVar.set(kFloat2_GrSLType, "strokeCoord");
gpArgs->fLocalCoordVar.set(kFloat2_GrSLType, "strokeCoord");
} else if (!shader.fStroke.isHairlineStyle()) {
// Normal case. Do the transform after tessellation.
args.fVertBuilder->codeAppend(R"(
float2 devCoord = uAffineMatrix * strokeCoord + uTranslate;)");
gpArgs->fPositionVar.set(kFloat2_GrSLType, "devCoord");
gpArgs->fLocalCoordVar.set(kFloat2_GrSLType, "strokeCoord");
} else {
gpArgs->fPositionVar.set(kFloat2_GrSLType, "localCoord");
}
// Hairline case. The scale and skew already happened before tessellation.
args.fVertBuilder->codeAppend(R"(
float2 devCoord = strokeCoord + uTranslate;
float2 localCoord = inverse(uAffineMatrix) * strokeCoord;)");
gpArgs->fPositionVar.set(kFloat2_GrSLType, "devCoord");
gpArgs->fLocalCoordVar.set(kFloat2_GrSLType, "localCoord");
}
// The fragment shader just outputs a uniform color.
const char* colorUniformName;
@ -1059,15 +1114,23 @@ class GrStrokeTessellateShader::IndirectImpl : public GrGLSLGeometryProcessor {
void setData(const GrGLSLProgramDataManager& pdman,
const GrPrimitiveProcessor& primProc) override {
const auto& shader = primProc.cast<GrStrokeTessellateShader>();
const auto& stroke = shader.fStroke;
// Set up the tessellation control uniforms.
Tolerances tolerances(shader.viewMatrix().getMaxScale(), shader.fStroke.getWidth());
float miterLimit = shader.fStroke.getMiter();
Tolerances tolerances;
if (!stroke.isHairlineStyle()) {
tolerances.set(shader.viewMatrix().getMaxScale(), stroke.getWidth());
} else {
// In the hairline case we transform prior to tessellation. Set up tolerances for an
// identity viewMatrix and a strokeWidth of 1.
tolerances.set(1, 1);
}
float miterLimit = stroke.getMiter();
pdman.set4f(fTessControlArgsUniform,
tolerances.fParametricIntolerance, // uParametricIntolerance
tolerances.fNumRadialSegmentsPerRadian, // uNumRadialSegmentsPerRadian
1 / (miterLimit * miterLimit), // uMiterLimitInvPow2
shader.fStroke.getWidth() * .5); // uStrokeRadius
(stroke.isHairlineStyle()) ? .5f : stroke.getWidth() * .5); // uStrokeRadius
// Set up the view matrix, if any.
const SkMatrix& m = shader.viewMatrix();
@ -1093,6 +1156,7 @@ void GrStrokeTessellateShader::getGLSLProcessorKey(const GrShaderCaps&,
SkASSERT(fStroke.getJoin() >> 2 == 0);
key = (key << 2) | fStroke.getJoin();
}
key = (key << 1) | (uint32_t)fStroke.isHairlineStyle();
key = (key << 1) | (uint32_t)fHasConics;
key = (key << 1) | (uint32_t)fMode; // Must be last.
b->add32(key);

View File

@ -133,7 +133,6 @@ public:
, fHasConics(hasConics)
, fStroke(stroke)
, fColor(color) {
SkASSERT(!fStroke.isHairlineStyle()); // No hairline support yet.
if (fMode == Mode::kTessellation) {
constexpr static Attribute kTessellationAttribs[] = {
{"inputPrevCtrlPt", kFloat2_GrVertexAttribType, kFloat2_GrSLType},

View File

@ -125,36 +125,18 @@ void GrTessellationPathRenderer::initAtlasFlags(GrRecordingContext* rContext) {
GrPathRenderer::CanDrawPath GrTessellationPathRenderer::onCanDrawPath(
const CanDrawPathArgs& args) const {
const GrStyledShape& shape = *args.fShape;
if (shape.inverseFilled() || shape.style().hasPathEffect() ||
args.fViewMatrix->hasPerspective()) {
if (shape.style().hasPathEffect() ||
args.fViewMatrix->hasPerspective() ||
shape.style().strokeRec().getStyle() == SkStrokeRec::kStrokeAndFill_Style ||
shape.inverseFilled()) {
return CanDrawPath::kNo;
}
if (GrAAType::kCoverage == args.fAAType) {
SkASSERT(1 == args.fProxy->numSamples());
if (!args.fProxy->canUseMixedSamples(*args.fCaps)) {
return CanDrawPath::kNo;
}
}
SkPath path;
shape.asPath(&path);
if (!shape.style().isSimpleFill()) {
// These are only temporary restrictions while we bootstrap tessellated stroking. Every one
// of them will eventually go away.
if (shape.style().strokeRec().getStyle() == SkStrokeRec::kStrokeAndFill_Style) {
return CanDrawPath::kNo;
}
if (shape.style().isSimpleHairline()) {
// For the time being we translate hairline paths to device space. We can't do this if
// it's possible the paint might use local coordinates.
if (args.fPaint->usesVaryingCoords()) {
return CanDrawPath::kNo;
}
}
}
return CanDrawPath::kYes;
}
@ -263,30 +245,13 @@ bool GrTessellationPathRenderer::onDrawPath(const DrawPathArgs& args) {
SkASSERT(worstCaseResolveLevel <= kMaxResolveLevel);
}
if (args.fShape->style().isSimpleHairline()) {
// Since we will be transforming the path, just double check that we are still in a position
// where the paint will not use local coordinates.
SkASSERT(!args.fPaint.usesVaryingCoords());
// Pre-transform the path into device space and use a stroke width of 1.
SkPath devPath;
path.transform(*args.fViewMatrix, &devPath);
SkStrokeRec devStroke = args.fShape->style().strokeRec();
devStroke.setStrokeStyle(1);
auto op = make_stroke_op(args.fContext, args.fAAType, SkMatrix::I(), devStroke, devPath,
std::move(args.fPaint), shaderCaps);
surfaceDrawContext->addDrawOp(args.fClip, std::move(op));
return true;
}
GrOp::Owner op;
if (!args.fShape->style().isSimpleFill()) {
const SkStrokeRec& stroke = args.fShape->style().strokeRec();
SkASSERT(stroke.getStyle() == SkStrokeRec::kStroke_Style);
auto op = make_stroke_op(args.fContext, args.fAAType, *args.fViewMatrix, stroke, path,
SkASSERT(stroke.getStyle() != SkStrokeRec::kStrokeAndFill_Style);
op = make_stroke_op(args.fContext, args.fAAType, *args.fViewMatrix, stroke, path,
std::move(args.fPaint), shaderCaps);
surfaceDrawContext->addDrawOp(args.fClip, std::move(op));
return true;
}
} else {
auto drawPathFlags = OpFlags::kNone;
if ((1 << worstCaseResolveLevel) > shaderCaps.maxTessellationSegments()) {
// The path is too large for hardware tessellation; a curve in this bounding box could
@ -294,10 +259,9 @@ bool GrTessellationPathRenderer::onDrawPath(const DrawPathArgs& args) {
// indirect draws.
drawPathFlags |= OpFlags::kDisableHWTessellation;
}
auto op = GrOp::Make<GrPathTessellateOp>(
args.fContext, *args.fViewMatrix, path, std::move(args.fPaint),
args.fAAType, drawPathFlags);
op = GrOp::Make<GrPathTessellateOp>(args.fContext, *args.fViewMatrix, path,
std::move(args.fPaint), args.fAAType, drawPathFlags);
}
surfaceDrawContext->addDrawOp(args.fClip, std::move(op));
return true;
}