Try to normalize tangents more robustly in stroke tessellators

I noticed the old version of cosine_between_vectors was causing issues
after https://skia-review.googlesource.com/c/skia/+/532014 landed, which
had the effect that tan0 and tan1 tended to be larger for lines.

That said, I could find instances of the same visual glitches on other
devices before that change landed, so depending on the hardware we were
running into issues either way.

This changes the cosine function to assume the tangents are already
normalized, and restructures the curve evaluation logic to assume that
as well. This is a reduction in work that hopefully makes up for the
work of always normalizing tan0 and tan1, whereas before the cosine_
function attempted to avoid that, and do a single inversesqrt call.

From a precision standpoint, just switching to calling normalize() on
tan0 and tan1 before getting the cosine improves the problem compared
to the precision required to store (dot(tan0,tan0)*dot(tan1,tan1))
before normalization. However, I also added some additional logic for
when coordinates get very large to try and scale the two end points of
the line to a more normalized exponent range. Since it returns the unit
vector, it should be equivalent but more accurate.

Bug: skia:11268, skia:12703, skia:13056
Change-Id: I34b6e1df1f57a8913443d75ed78710fcd27731e6
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/532777
Auto-Submit: Michael Ludwig <michaelludwig@google.com>
Reviewed-by: Jim Van Verth <jvanverth@google.com>
Commit-Queue: Jim Van Verth <jvanverth@google.com>
This commit is contained in:
Michael Ludwig 2022-04-26 20:58:11 -04:00 committed by SkCQ
parent d663bdb900
commit 5ce8f07271
4 changed files with 66 additions and 32 deletions

View File

@ -105,12 +105,30 @@ GrStrokeTessellationShader::GrStrokeTessellationShader(const GrShaderCaps& shade
SkASSERT(fAttribs.count() <= kMaxAttribCount);
}
const char* GrStrokeTessellationShader::Impl::kCosineBetweenVectorsFn = R"(
float cosine_between_vectors(float2 a, float2 b) {
// FIXME(crbug.com/800804,skbug.com/11268): This can overflow if we don't normalize exponents.
float ab_cosTheta = dot(a,b);
float ab_pow2 = dot(a,a) * dot(b,b);
return (ab_pow2 == 0.0) ? 1.0 : clamp(ab_cosTheta * inversesqrt(ab_pow2), -1.0, 1.0);
const char* GrStrokeTessellationShader::Impl::kRobustNormalizeDiffFn = R"(
float2 robust_normalize_diff(float2 a, float2 b) {
if (a == b) {
return float2(0.0, 0.0);
} else {
float2 magXY = max(abs(a), abs(b));
float mag = max(magXY.x, magXY.y);
if (mag > 16777216.0) { // 2^24, when f32 loses 1px precision
// This brings the components of a and b to be within [-1, 1] before the vector's length
// is calculated inside the normalize() call.
float2 scaled_diff = (a/mag) - (b/mag);
return normalize(scaled_diff);
} else {
// assume standard f32 precision will be sufficiently accurate
return normalize(a - b);
}
}
})";
const char* GrStrokeTessellationShader::Impl::kCosineBetweenUnitVectorsFn = R"(
float cosine_between_unit_vectors(float2 a, float2 b) {
// Since a and b are assumed to be normalized, the cosine is equal to the dot product, although
// we clamp that to ensure it falls within the expected range of [-1, 1].
return clamp(dot(a, b), -1.0, 1.0);
})";
// Extends the middle radius to either the miter point, or the bevel edge if we surpassed the miter
@ -163,8 +181,8 @@ void GrStrokeTessellationShader::Impl::emitTessellationCode(
// bool isFinalEdge;
// float numParametricSegments;
// float radsPerSegment;
// float2 tan0;
// float2 tan1;
// float2 tan0; // Must be pre-normalized
// float2 tan1; // Must be pre-normalized
// float strokeOutset;
//
code->appendf(R"(
@ -215,8 +233,6 @@ void GrStrokeTessellationShader::Impl::emitTessellationCode(
//
float lastParametricEdgeID = 0.0;
float maxParametricEdgeID = min(numParametricSegments - 1.0, combinedEdgeID);
// FIXME(crbug.com/800804,skbug.com/11268): This normalize() can overflow.
float2 tan0norm = normalize(tan0);
float negAbsRadsPerSegment = -abs(radsPerSegment);
float maxRotation0 = (1.0 + combinedEdgeID) * abs(radsPerSegment);
for (int exp = %i - 1; exp >= 0; --exp) {
@ -225,7 +241,7 @@ void GrStrokeTessellationShader::Impl::emitTessellationCode(
if (testParametricID <= maxParametricEdgeID) {
float2 testTan = fma(float2(testParametricID), A, B_);
testTan = fma(float2(testParametricID), testTan, C_);
float cosRotation = dot(normalize(testTan), tan0norm);
float cosRotation = dot(normalize(testTan), tan0);
float maxRotation = fma(testParametricID, negAbsRadsPerSegment, maxRotation0);
maxRotation = min(maxRotation, PI);
// Is rotation <= maxRotation? (i.e., is the number of complete radial segments
@ -244,11 +260,12 @@ void GrStrokeTessellationShader::Impl::emitTessellationCode(
// combinedEdgeID, the highest radial edge is easy:
float lastRadialEdgeID = combinedEdgeID - lastParametricEdgeID;
// Find the angle of tan0, or the angle between tan0norm and the positive x axis.
float angle0 = acos(clamp(tan0norm.x, -1.0, 1.0));
angle0 = tan0norm.y >= 0.0 ? angle0 : -angle0;
// Find the angle of tan0, i.e. the angle between tan0 and the positive x axis.
float angle0 = acos(clamp(tan0.x, -1.0, 1.0));
angle0 = tan0.y >= 0.0 ? angle0 : -angle0;
// Find the tangent vector on the edge at lastRadialEdgeID.
// Find the tangent vector on the edge at lastRadialEdgeID. By construction it is already
// normalized.
float radialAngle = fma(lastRadialEdgeID, radsPerSegment, angle0);
tangent = float2(cos(radialAngle), sin(radialAngle));
float2 norm = float2(-tangent.y, tangent.x);
@ -304,7 +321,9 @@ void GrStrokeTessellationShader::Impl::emitTessellationCode(
// tangent found previously. (In the event that parametricT == radialT, we keep the radial
// tangent.)
if (T != radialT) {
tangent = (w >= 0.0) ? bc*u - ab*v : bcd - abc;
// We must re-normalize here because the tangent is determined by the curve coefficients
tangent = w >= 0.0 ? robust_normalize_diff(bc*u, ab*v)
: robust_normalize_diff(bcd, abc);
}
strokeCoord = (w >= 0.0) ? abc/uv : abcd;
@ -316,8 +335,8 @@ void GrStrokeTessellationShader::Impl::emitTessellationCode(
})", shader.maxParametricSegments_log2() /* Parametric/radial sort loop count. */);
code->append(R"(
// FIXME(crbug.com/800804,skbug.com/11268): This normalize() can overflow.
float2 ortho = normalize(float2(tangent.y, -tangent.x));
// At this point 'tangent' is normalized, so the orthogonal vector is also normalized.
float2 ortho = float2(tangent.y, -tangent.x);
strokeCoord += ortho * (STROKE_RADIUS * strokeOutset);)");
if (!shader.stroke().isHairlineStyle()) {

View File

@ -89,10 +89,17 @@ private:
// emitTessellationCode and emitFragment code.
class GrStrokeTessellationShader::Impl : public ProgramImpl {
protected:
// float cosine_between_vectors(float2 a, float2 b) { ...
// float2 robust_normalize_diff(float2 a, float b) { ... }
//
// Returns dot(a, b) / (length(a) * length(b)).
static const char* kCosineBetweenVectorsFn;
// Returns the normalized difference between a and b, i.e. normalize(a - b), with care taken for
// if b and/or a have large coordinates.
static const char* kRobustNormalizeDiffFn;
// float cosine_between_unit_vectors(float2 a, float2 b) { ...
//
// Returns the cosine of the angle between a and b, assuming a and b are unit vectors already.
// Guaranteed to be between [-1, 1].
static const char* kCosineBetweenUnitVectorsFn;
// float miter_extent(float cosTheta, float miterLimit) { ...
//

View File

@ -54,7 +54,7 @@ void GrStrokeTessellationShader::HardwareImpl::onEmitCode(EmitArgs& args, GrGPAr
v->declareGlobal(GrShaderVar("vsColor", SkSLType::kHalf4, TypeModifier::Out));
}
v->insertFunction(kCosineBetweenVectorsFn);
v->insertFunction(kCosineBetweenUnitVectorsFn);
v->insertFunction(kMiterExtentFn);
v->insertFunction(kUncheckedMixFn);
if (shader.hasDynamicStroke()) {
@ -150,7 +150,7 @@ void GrStrokeTessellationShader::HardwareImpl::onEmitCode(EmitArgs& args, GrGPAr
}
// Calculate the number of segments to chop the join into.
float cosTheta = cosine_between_vectors(prevJoinTangent, tan0);
float cosTheta = cosine_between_unit_vectors(normalize(prevJoinTangent), normalize(tan0));
float joinRotation = (cosTheta == 1) ? 0 : acos(cosTheta);
if (cross_length_2d(prevJoinTangent, tan0) < 0) {
joinRotation = -joinRotation;
@ -351,7 +351,7 @@ SkString GrStrokeTessellationShader::HardwareImpl::getTessControlShaderGLSL(
}
code.append(GrTessellationShader::WangsFormulaSkSL());
code.append(kCosineBetweenVectorsFn);
code.append(kCosineBetweenUnitVectorsFn);
code.append(kMiterExtentFn);
code.append(R"(
float cross2d(vec2 a, vec2 b) {
@ -444,7 +444,8 @@ SkString GrStrokeTessellationShader::HardwareImpl::getTessControlShaderGLSL(
// Determine the curve's total rotation. The vertex shader ensures our curve does not rotate
// more than 180 degrees or inflect, so the inverse cosine has enough range.
float cosTheta = cosine_between_vectors(tangents[0], tangents[1]);
float cosTheta = cosine_between_unit_vectors(normalize(tangents[0]),
normalize(tangents[1]));
float rotation = acos(cosTheta);
// Adjust sign of rotation to match the direction the curve turns.
@ -582,6 +583,7 @@ SkString GrStrokeTessellationShader::HardwareImpl::getTessEvaluationShaderGLSL(
uniform vec4 sk_RTAdjust;)");
code.append(kUncheckedMixFn);
code.append(kRobustNormalizeDiffFn);
code.append(R"(
void main() {
@ -628,7 +630,10 @@ SkString GrStrokeTessellationShader::HardwareImpl::getTessEvaluationShaderGLSL(
numParametricSegments = tcsTessArgs[2].y;
radsPerSegment = tcsTessArgs[2].z;
}
float2 tan1 = tcsEndPtEndTan.zw;
// emitTessellationCode() expects unit vectors for tan0 and tan1
tan0 = normalize(tan0);
float2 tan1 = normalize(tcsEndPtEndTan.zw);
bool isFinalEdge = (gl_TessCoord.x == 1);
float w = -1.0; // w<0 means the curve is an integral cubic.
if (isinf(p3.y)) {

View File

@ -26,7 +26,8 @@ void GrStrokeTessellationShader::InstancedImpl::onEmitCode(EmitArgs& args, GrGPA
if (shader.hasDynamicStroke()) {
args.fVertBuilder->insertFunction(kNumRadialSegmentsPerRadianFn);
}
args.fVertBuilder->insertFunction(kCosineBetweenVectorsFn);
args.fVertBuilder->insertFunction(kRobustNormalizeDiffFn);
args.fVertBuilder->insertFunction(kCosineBetweenUnitVectorsFn);
args.fVertBuilder->insertFunction(kMiterExtentFn);
args.fVertBuilder->insertFunction(kUncheckedMixFn);
args.fVertBuilder->insertFunction(GrTessellationShader::WangsFormulaSkSL());
@ -138,8 +139,9 @@ void GrStrokeTessellationShader::InstancedImpl::onEmitCode(EmitArgs& args, GrGPA
args.fVertBuilder->codeAppend(R"(
// Find the starting and ending tangents.
float2 tan0 = ((p0 == p1) ? (p1 == p2) ? p3 : p2 : p1) - p0;
float2 tan1 = p3 - ((p3 == p2) ? (p2 == p1) ? p0 : p1 : p2);
// (p0 == p1) ? ((p1 == p2) ? p3 : p2) : p1
float2 tan0 = robust_normalize_diff((p0 == p1) ? ((p1 == p2) ? p3 : p2) : p1, p0);
float2 tan1 = robust_normalize_diff(p3, (p3 == p2) ? ((p2 == p1) ? p0 : p1) : p2);
if (tan0 == float2(0)) {
// The stroke is a point. This special case tells us to draw a stroke-width circle as a
// 180 degree point stroke instead.
@ -163,7 +165,8 @@ void GrStrokeTessellationShader::InstancedImpl::onEmitCode(EmitArgs& args, GrGPA
// of the join twice: once full width and once restricted to half width. This guarantees
// perfect seaming by matching the vertices from the join as well as from the strokes on
// either side.
float joinRads = acos(cosine_between_vectors(p0 - lastControlPoint, tan0));
float2 prevTan = robust_normalize_diff(p0, lastControlPoint);
float joinRads = acos(cosine_between_unit_vectors(prevTan, tan0));
float numRadialSegmentsInJoin = max(ceil(joinRads * NUM_RADIAL_SEGMENTS_PER_RADIAN), 1);
// +2 because we emit the beginning and ending edges twice (see above comment).
float numEdgesInJoin = numRadialSegmentsInJoin + 2;
@ -205,13 +208,13 @@ void GrStrokeTessellationShader::InstancedImpl::onEmitCode(EmitArgs& args, GrGPA
// means the join is disabled, and to disable it with the existing code we can leave
// tan0 equal to tan1.
if (lastControlPoint != p0) {
tan0 = p0 - lastControlPoint;
tan0 = robust_normalize_diff(p0, lastControlPoint);
}
turn = cross_length_2d(tan0, tan1);
}
// Calculate the curve's starting angle and rotation.
float cosTheta = cosine_between_vectors(tan0, tan1);
float cosTheta = cosine_between_unit_vectors(tan0, tan1);
float rotation = acos(cosTheta);
if (turn < 0) {
// Adjust sign of rotation to match the direction the curve turns.