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:
parent
977715763d
commit
06b52ada19
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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},
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user