Add an epsilon to GrPathUtils::findCubicConvex180Chops

Cubic tangents become unstable if we chop too close to 0 or 1. This
adds an epsilon to simply not chop them if it's too close.

Bug: skia:10419
Change-Id: I99e98c5d433e2cb59c767ee564e015d7ec4f7765
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/336280
Commit-Queue: Chris Dalton <csmartdalton@google.com>
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
This commit is contained in:
Chris Dalton 2020-11-19 09:58:21 -07:00 committed by Skia Commit-Bot
parent 4ce110b8bd
commit 9458c8d44a
2 changed files with 66 additions and 45 deletions

View File

@ -585,14 +585,21 @@ void GrPathUtils::convertCubicToQuadsConstrainToTangents(const SkPoint p[4],
} }
} }
static inline bool is_ieee_float_inside_0_1_exclusive(float x) {
constexpr static uint32_t kIEEE_one = 127 << 23;
return sk_bit_cast<uint32_t>(x) - 1 < kIEEE_one - 1;
}
int GrPathUtils::findCubicConvex180Chops(const SkPoint pts[], float T[2]) { int GrPathUtils::findCubicConvex180Chops(const SkPoint pts[], float T[2]) {
using grvx::float2; using grvx::float2;
// If a chop falls within a distance of "kEpsilon" from 0 or 1, throw it out. Tangents become
// unstable when we chop too close to the boundary. This works out because the tessellation
// shaders don't allow more than 2^10 parametric segments, and they snap the beginning and
// ending edges at 0 and 1. So if we overstep an inflection or point of 180-degree rotation by a
// fraction of a tessellation segment, it just gets snapped.
constexpr static float kEpsilon = 1.f / (1 << 12);
// Floating-point representation of "1 - 2*kEpsilon".
constexpr static uint32_t kIEEE_one_minus_2_epsilon = (127 << 23) - 2*(1 << 12);
// Unfortunately we don't have a way to static_assert this, but we can runtime assert that the
// kIEEE_one_minus_2_epsilon bits are correct.
SkASSERT(sk_bit_cast<float>(kIEEE_one_minus_2_epsilon) == 1 - 2*kEpsilon);
float2 p0 = skvx::bit_pun<float2>(pts[0]); float2 p0 = skvx::bit_pun<float2>(pts[0]);
float2 p1 = skvx::bit_pun<float2>(pts[1]); float2 p1 = skvx::bit_pun<float2>(pts[1]);
float2 p2 = skvx::bit_pun<float2>(pts[2]); float2 p2 = skvx::bit_pun<float2>(pts[2]);
@ -642,7 +649,8 @@ int GrPathUtils::findCubicConvex180Chops(const SkPoint pts[], float T[2]) {
// convex-180 if any points are colocated, and T[0] will equal NaN which returns 0 // convex-180 if any points are colocated, and T[0] will equal NaN which returns 0
// chops. // chops.
float root = sk_ieee_float_divide(c, b_over_minus_2); float root = sk_ieee_float_divide(c, b_over_minus_2);
if (is_ieee_float_inside_0_1_exclusive(root)) { // Is "root" inside the range [epsilon, 1 - epsilon)?
if (sk_bit_cast<uint32_t>(root - kEpsilon) < kIEEE_one_minus_2_epsilon) {
T[0] = root; T[0] = root;
return 1; return 1;
} }
@ -673,7 +681,7 @@ int GrPathUtils::findCubicConvex180Chops(const SkPoint pts[], float T[2]) {
q = q + b_over_minus_2; q = q + b_over_minus_2;
float2 roots = float2{q,c} / float2{a,q}; float2 roots = float2{q,c} / float2{a,q};
auto inside = (roots > 0) & (roots < 1); auto inside = (roots > kEpsilon) & (roots < (1 - kEpsilon));
if (inside[0]) { if (inside[0]) {
if (inside[1] && roots[0] != roots[1]) { if (inside[1] && roots[0] != roots[1]) {
if (roots[0] > roots[1]) { if (roots[0] > roots[1]) {

View File

@ -18,50 +18,63 @@ static bool is_linear(const SkPoint p[4]) {
return is_linear(p[0],p[1],p[2]) && is_linear(p[0],p[2],p[3]) && is_linear(p[1],p[2],p[3]); return is_linear(p[0],p[1],p[2]) && is_linear(p[0],p[2],p[3]) && is_linear(p[1],p[2],p[3]);
} }
static void check_cubic_convex_180(skiatest::Reporter* r, const SkPoint p[4]) {
float inflectT[2], convex180T[2];
if (int inflectN = SkFindCubicInflections(p, inflectT)) {
// The curve has inflections. findCubicConvex180Chops should return the inflection
// points.
int convex180N = GrPathUtils::findCubicConvex180Chops(p, convex180T);
REPORTER_ASSERT(r, inflectN == convex180N);
for (int i = 0; i < convex180N; ++i) {
REPORTER_ASSERT(r, SkScalarNearlyEqual(inflectT[i], convex180T[i]));
}
} else {
float totalRotation = SkMeasureNonInflectCubicRotation(p);
int convex180N = GrPathUtils::findCubicConvex180Chops(p, convex180T);
SkPoint chops[10];
SkChopCubicAt(p, chops, convex180T, convex180N);
float radsSum = 0;
for (int i = 0; i <= convex180N; ++i) {
float rads = SkMeasureNonInflectCubicRotation(chops + i*3);
SkASSERT(rads < SK_ScalarPI + SK_ScalarNearlyZero);
radsSum += rads;
}
if (totalRotation < SK_ScalarPI - SK_ScalarNearlyZero) {
// The curve should never chop if rotation is <180 degrees.
REPORTER_ASSERT(r, convex180N == 0);
} else if (!is_linear(p)) {
REPORTER_ASSERT(r, SkScalarNearlyEqual(radsSum, totalRotation));
if (totalRotation > SK_ScalarPI + SK_ScalarNearlyZero) {
REPORTER_ASSERT(r, convex180N == 1);
// This works because cusps take the "inflection" path above, so we don't get
// non-lilnear curves that lose rotation when chopped.
REPORTER_ASSERT(r, SkScalarNearlyEqual(
SkMeasureNonInflectCubicRotation(chops), SK_ScalarPI));
REPORTER_ASSERT(r, SkScalarNearlyEqual(
SkMeasureNonInflectCubicRotation(chops + 3), totalRotation - SK_ScalarPI));
}
}
}
}
DEF_TEST(GrPathUtils_findCubicConvex180Chops, r) { DEF_TEST(GrPathUtils_findCubicConvex180Chops, r) {
// Test all combinations of corners from the square [0,0,1,1]. This gives us all kinds of // Test all combinations of corners from the square [0,0,1,1]. This covers every cubic type as
// special cases for cusps, lines, loops, and inflections. // well as a wide variety of special cases for cusps, lines, loops, and inflections.
for (int i = 0; i < (1 << 8); ++i) { for (int i = 0; i < (1 << 8); ++i) {
SkPoint p[4] = {SkPoint::Make((i>>0)&1, (i>>1)&1), SkPoint p[4] = {SkPoint::Make((i>>0)&1, (i>>1)&1),
SkPoint::Make((i>>2)&1, (i>>3)&1), SkPoint::Make((i>>2)&1, (i>>3)&1),
SkPoint::Make((i>>4)&1, (i>>5)&1), SkPoint::Make((i>>4)&1, (i>>5)&1),
SkPoint::Make((i>>6)&1, (i>>7)&1)}; SkPoint::Make((i>>6)&1, (i>>7)&1)};
float inflectT[2], convex180T[2]; check_cubic_convex_180(r, p);
if (int inflectN = SkFindCubicInflections(p, inflectT)) { }
// The curve has inflections. findCubicConvex180Chops should return the inflection
// points. {
int convex180N = GrPathUtils::findCubicConvex180Chops(p, convex180T); // This cubic has a convex-180 chop at T=1-"epsilon"
REPORTER_ASSERT(r, inflectN == convex180N); static const uint32_t hexPts[] = {0x3ee0ac74, 0x3f1e061a, 0x3e0fc408, 0x3f457230,
for (int i = 0; i < convex180N; ++i) { 0x3f42ac7c, 0x3f70d76c, 0x3f4e6520, 0x3f6acafa};
REPORTER_ASSERT(r, SkScalarNearlyEqual(inflectT[i], convex180T[i])); SkPoint p[4];
} memcpy(p, hexPts, sizeof(p));
} else { check_cubic_convex_180(r, p);
float totalRotation = SkMeasureNonInflectCubicRotation(p);
int convex180N = GrPathUtils::findCubicConvex180Chops(p, convex180T);
SkPoint chops[10];
SkChopCubicAt(p, chops, convex180T, convex180N);
float radsSum = 0;
for (int i = 0; i <= convex180N; ++i) {
float rads = SkMeasureNonInflectCubicRotation(chops + i*3);
SkASSERT(rads < SK_ScalarPI + SK_ScalarNearlyZero);
radsSum += rads;
}
if (totalRotation < SK_ScalarPI - SK_ScalarNearlyZero) {
// The curve should never chop if rotation is <180 degrees.
REPORTER_ASSERT(r, convex180N == 0);
} else if (!is_linear(p)) {
REPORTER_ASSERT(r, SkScalarNearlyEqual(radsSum, totalRotation));
if (totalRotation > SK_ScalarPI + SK_ScalarNearlyZero) {
REPORTER_ASSERT(r, convex180N == 1);
// This works because cusps take the "inflection" path above, so we don't get
// non-lilnear curves that lose rotation when chopped.
REPORTER_ASSERT(r, SkScalarNearlyEqual(
SkMeasureNonInflectCubicRotation(chops), SK_ScalarPI));
REPORTER_ASSERT(r, SkScalarNearlyEqual(
SkMeasureNonInflectCubicRotation(chops + 3), totalRotation - SK_ScalarPI));
}
}
}
} }
// Now test an exact quadratic. // Now test an exact quadratic.