Optimize SkChopCubicAt to chop at two points at once
Adds an SkChopCubicAt overload that performs two chops at once in
SIMD. Also updates SkChopCubicAt to accept T values of 0 and 1. This
has been the source of bugs in the past.
Bug: skia:10419
Change-Id: Ic8a482a69192fb1685f3766411cbdceed830f9b7
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/327436
Reviewed-by: Mike Reed <reed@google.com>
Commit-Queue: Chris Dalton <csmartdalton@google.com>
diff --git a/include/private/SkVx.h b/include/private/SkVx.h
index 7e9d79f..c00e6e6 100644
--- a/include/private/SkVx.h
+++ b/include/private/SkVx.h
@@ -476,6 +476,11 @@
SINTU Vec<N,T> min(U x, const Vec<N,T>& y) { return min(Vec<N,T>(x), y); }
SINTU Vec<N,T> max(U x, const Vec<N,T>& y) { return max(Vec<N,T>(x), y); }
+// pin matches the logic of SkTPin, which is important when NaN is involved. It always returns
+// values in the range lo..hi, and if x is NaN, it returns lo.
+SINT Vec<N,T> pin(const Vec<N,T>& x, const Vec<N,T>& lo, const Vec<N,T>& hi) {
+ return max(lo, min(x, hi));
+}
// Shuffle values from a vector pretty arbitrarily:
// skvx::Vec<4,float> rgba = {R,G,B,A};
diff --git a/src/core/SkGeometry.cpp b/src/core/SkGeometry.cpp
index 0aad23a..9e5d39e 100644
--- a/src/core/SkGeometry.cpp
+++ b/src/core/SkGeometry.cpp
@@ -9,6 +9,7 @@
#include "include/core/SkPoint3.h"
#include "include/private/SkNx.h"
#include "include/private/SkTPin.h"
+#include "include/private/SkVx.h"
#include "src/core/SkGeometry.h"
#include "src/core/SkPointPriv.h"
@@ -442,92 +443,122 @@
return SkFindUnitQuadRoots(A, B, C, tValues);
}
-void SkChopCubicAt(const SkPoint src[4], SkPoint dst[7], SkScalar t) {
- SkASSERT(t > 0 && t < SK_Scalar1);
-
- Sk2s p0 = from_point(src[0]);
- Sk2s p1 = from_point(src[1]);
- Sk2s p2 = from_point(src[2]);
- Sk2s p3 = from_point(src[3]);
- Sk2s tt(t);
-
- Sk2s ab = interp(p0, p1, tt);
- Sk2s bc = interp(p1, p2, tt);
- Sk2s cd = interp(p2, p3, tt);
- Sk2s abc = interp(ab, bc, tt);
- Sk2s bcd = interp(bc, cd, tt);
- Sk2s abcd = interp(abc, bcd, tt);
-
- dst[0] = to_point(p0);
- dst[1] = to_point(ab);
- dst[2] = to_point(abc);
- dst[3] = to_point(abcd);
- dst[4] = to_point(bcd);
- dst[5] = to_point(cd);
- dst[6] = to_point(p3);
+// This does not return b when t==1, but it otherwise seems to get better precision than
+// "a*(1 - t) + b*t" for things like chopping cubics on exact cusp points.
+// The responsibility falls on the caller to check that t != 1 before calling.
+template<int N, typename T>
+inline static skvx::Vec<N,T> unchecked_mix(const skvx::Vec<N,T>& a, const skvx::Vec<N,T>& b,
+ const skvx::Vec<N,T>& t) {
+ return (b - a)*t + a;
}
-/* http://code.google.com/p/skia/issues/detail?id=32
+void SkChopCubicAt(const SkPoint src[4], SkPoint dst[7], SkScalar t) {
+ using float2 = skvx::Vec<2,float>;
+ SkASSERT(0 <= t && t <= 1);
- This test code would fail when we didn't check the return result of
- valid_unit_divide in SkChopCubicAt(... tValues[], int roots). The reason is
- that after the first chop, the parameters to valid_unit_divide are equal
- (thanks to finite float precision and rounding in the subtracts). Thus
- even though the 2nd tValue looks < 1.0, after we renormalize it, we end
- up with 1.0, hence the need to check and just return the last cubic as
- a degenerate clump of 4 points in the sampe place.
-
- static void test_cubic() {
- SkPoint src[4] = {
- { 556.25000, 523.03003 },
- { 556.23999, 522.96002 },
- { 556.21997, 522.89001 },
- { 556.21997, 522.82001 }
- };
- SkPoint dst[10];
- SkScalar tval[] = { 0.33333334f, 0.99999994f };
- SkChopCubicAt(src, dst, tval, 2);
+ if (t == 1) {
+ memcpy(dst, src, sizeof(SkPoint) * 4);
+ dst[4] = dst[5] = dst[6] = src[3];
+ return;
}
- */
+
+ float2 p0 = skvx::bit_pun<float2>(src[0]);
+ float2 p1 = skvx::bit_pun<float2>(src[1]);
+ float2 p2 = skvx::bit_pun<float2>(src[2]);
+ float2 p3 = skvx::bit_pun<float2>(src[3]);
+ float2 T = t;
+
+ float2 ab = unchecked_mix(p0, p1, T);
+ float2 bc = unchecked_mix(p1, p2, T);
+ float2 cd = unchecked_mix(p2, p3, T);
+ float2 abc = unchecked_mix(ab, bc, T);
+ float2 bcd = unchecked_mix(bc, cd, T);
+ float2 abcd = unchecked_mix(abc, bcd, T);
+
+ dst[0] = skvx::bit_pun<SkPoint>(p0);
+ dst[1] = skvx::bit_pun<SkPoint>(ab);
+ dst[2] = skvx::bit_pun<SkPoint>(abc);
+ dst[3] = skvx::bit_pun<SkPoint>(abcd);
+ dst[4] = skvx::bit_pun<SkPoint>(bcd);
+ dst[5] = skvx::bit_pun<SkPoint>(cd);
+ dst[6] = skvx::bit_pun<SkPoint>(p3);
+}
+
+void SkChopCubicAt(const SkPoint src[4], SkPoint dst[10], float t0, float t1) {
+ using float4 = skvx::Vec<4,float>;
+ using float2 = skvx::Vec<2,float>;
+ SkASSERT(0 <= t0 && t0 <= t1 && t1 <= 1);
+
+ if (t1 == 1) {
+ SkChopCubicAt(src, dst, t0);
+ dst[7] = dst[8] = dst[9] = src[3];
+ return;
+ }
+
+ // Perform both chops in parallel using 4-lane SIMD.
+ float4 p00, p11, p22, p33, T;
+ p00.lo = p00.hi = skvx::bit_pun<float2>(src[0]);
+ p11.lo = p11.hi = skvx::bit_pun<float2>(src[1]);
+ p22.lo = p22.hi = skvx::bit_pun<float2>(src[2]);
+ p33.lo = p33.hi = skvx::bit_pun<float2>(src[3]);
+ T.lo = t0;
+ T.hi = t1;
+
+ float4 ab = unchecked_mix(p00, p11, T);
+ float4 bc = unchecked_mix(p11, p22, T);
+ float4 cd = unchecked_mix(p22, p33, T);
+ float4 abc = unchecked_mix(ab, bc, T);
+ float4 bcd = unchecked_mix(bc, cd, T);
+ float4 abcd = unchecked_mix(abc, bcd, T);
+ float4 middle = unchecked_mix(abc, bcd, skvx::shuffle<2,3,0,1>(T));
+
+ dst[0] = skvx::bit_pun<SkPoint>(p00.lo);
+ dst[1] = skvx::bit_pun<SkPoint>(ab.lo);
+ dst[2] = skvx::bit_pun<SkPoint>(abc.lo);
+ dst[3] = skvx::bit_pun<SkPoint>(abcd.lo);
+ middle.store(dst + 4);
+ dst[6] = skvx::bit_pun<SkPoint>(abcd.hi);
+ dst[7] = skvx::bit_pun<SkPoint>(bcd.hi);
+ dst[8] = skvx::bit_pun<SkPoint>(cd.hi);
+ dst[9] = skvx::bit_pun<SkPoint>(p33.hi);
+}
void SkChopCubicAt(const SkPoint src[4], SkPoint dst[],
- const SkScalar tValues[], int roots) {
+ const SkScalar tValues[], int tCount) {
+ using float2 = skvx::Vec<2,float>;
+
#ifdef SK_DEBUG
- {
- for (int i = 0; i < roots - 1; i++)
- {
- SkASSERT(0 < tValues[i] && tValues[i] < 1);
- SkASSERT(0 < tValues[i+1] && tValues[i+1] < 1);
- SkASSERT(tValues[i] < tValues[i+1]);
- }
+ float lastT = 0;
+ for (int i = 0; i < tCount; i++) {
+ SkASSERT(lastT <= tValues[i] && tValues[i] <= 1);
+ lastT = tValues[i];
}
#endif
if (dst) {
- if (roots == 0) { // nothing to chop
+ if (tCount == 0) { // nothing to chop
memcpy(dst, src, 4*sizeof(SkPoint));
} else {
- SkScalar t = tValues[0];
- SkPoint tmp[4];
-
- for (int i = 0; i < roots; i++) {
+ int i = 0;
+ for (; i < tCount - 1; i += 2) {
+ // Do two chops at once.
+ float2 tt = float2::Load(tValues + i);
+ if (i != 0) {
+ float lastT = tValues[i - 1];
+ tt = skvx::pin((tt - lastT) / (1 - lastT), float2(0), float2(1));
+ }
+ SkChopCubicAt(src, dst, tt[0], tt[1]);
+ src = dst = dst + 6;
+ }
+ if (i < tCount) {
+ // Chop the final cubic if there was an odd number of chops.
+ SkASSERT(i + 1 == tCount);
+ float t = tValues[i];
+ if (i != 0) {
+ float lastT = tValues[i - 1];
+ t = SkTPin((t - lastT) / (1 - lastT), 0.f, 1.f);
+ }
SkChopCubicAt(src, dst, t);
- if (i == roots - 1) {
- break;
- }
-
- dst += 3;
- // have src point to the remaining cubic (after the chop)
- memcpy(tmp, dst, 4 * sizeof(SkPoint));
- src = tmp;
-
- // watch out in case the renormalized t isn't in range
- if (!valid_unit_divide(tValues[i+1] - tValues[i],
- SK_Scalar1 - tValues[i], &t)) {
- // if we can't, just create a degenerate cubic
- dst[4] = dst[5] = dst[6] = src[3];
- break;
- }
}
}
}
diff --git a/src/core/SkGeometry.h b/src/core/SkGeometry.h
index 92d70fa..a206b64 100644
--- a/src/core/SkGeometry.h
+++ b/src/core/SkGeometry.h
@@ -133,13 +133,19 @@
SkVector* tangentOrNull, SkVector* curvatureOrNull);
/** Given a src cubic bezier, chop it at the specified t value,
- where 0 < t < 1, and return the two new cubics in dst:
+ where 0 <= t <= 1, and return the two new cubics in dst:
dst[0..3] and dst[3..6]
*/
void SkChopCubicAt(const SkPoint src[4], SkPoint dst[7], SkScalar t);
+/** Given a src cubic bezier, chop it at the specified t0 and t1 values,
+ where 0 <= t0 <= t1 <= 1, and return the three new cubics in dst:
+ dst[0..3], dst[3..6], and dst[6..9]
+*/
+void SkChopCubicAt(const SkPoint src[4], SkPoint dst[10], float t0, float t1);
+
/** Given a src cubic bezier, chop it at the specified t values,
- where 0 < t < 1, and return the new cubics in dst:
+ where 0 <= t0 <= t1 <= ... <= 1, and return the new cubics in dst:
dst[0..3],dst[3..6],...,dst[3*t_count..3*(t_count+1)]
*/
void SkChopCubicAt(const SkPoint src[4], SkPoint dst[], const SkScalar t[],
diff --git a/tests/GeometryTest.cpp b/tests/GeometryTest.cpp
index 1b08821..78b1db4 100644
--- a/tests/GeometryTest.cpp
+++ b/tests/GeometryTest.cpp
@@ -46,6 +46,72 @@
REPORTER_ASSERT(reporter, pts[i].fX == pts[i].fY);
REPORTER_ASSERT(reporter, pts[i].fX == i * .5f);
}
+
+ static const float chopTs[] = {
+ 0, 3/83.f, 3/79.f, 3/73.f, 3/71.f, 3/67.f, 3/61.f, 3/59.f, 3/53.f, 3/47.f, 3/43.f, 3/41.f,
+ 3/37.f, 3/31.f, 3/29.f, 3/23.f, 3/19.f, 3/17.f, 3/13.f, 3/11.f, 3/7.f, 3/5.f, 1,
+ };
+ float ones[] = {1,1,1,1,1};
+
+ // Ensure an odd number of T values so we exercise the single chop code at the end of
+ // SkChopCubicAt form multiple T.
+ static_assert(SK_ARRAY_COUNT(chopTs) % 2 == 1);
+ static_assert(SK_ARRAY_COUNT(ones) % 2 == 1);
+
+ SkRandom rand;
+ for (int iterIdx = 0; iterIdx < 5; ++iterIdx) {
+ SkPoint pts[4] = {{rand.nextF(), rand.nextF()}, {rand.nextF(), rand.nextF()},
+ {rand.nextF(), rand.nextF()}, {rand.nextF(), rand.nextF()}};
+
+ SkPoint allChops[4 + SK_ARRAY_COUNT(chopTs)*3];
+ SkChopCubicAt(pts, allChops, chopTs, SK_ARRAY_COUNT(chopTs));
+ int i = 3;
+ for (float chopT : chopTs) {
+ // Ensure we chop at approximately the correct points when we chop an entire list.
+ SkPoint expectedPt;
+ SkEvalCubicAt(pts, chopT, &expectedPt, nullptr, nullptr);
+ REPORTER_ASSERT(reporter, SkScalarNearlyEqual(allChops[i].x(), expectedPt.x()));
+ REPORTER_ASSERT(reporter, SkScalarNearlyEqual(allChops[i].y(), expectedPt.y()));
+ if (chopT == 0) {
+ REPORTER_ASSERT(reporter, allChops[i] == pts[0]);
+ }
+ if (chopT == 1) {
+ REPORTER_ASSERT(reporter, allChops[i] == pts[3]);
+ }
+ i += 3;
+
+ // Ensure the middle is exactly degenerate when we chop at two equal points.
+ SkPoint localChops[10];
+ SkChopCubicAt(pts, localChops, chopT, chopT);
+ REPORTER_ASSERT(reporter, localChops[3] == localChops[4]);
+ REPORTER_ASSERT(reporter, localChops[3] == localChops[5]);
+ REPORTER_ASSERT(reporter, localChops[3] == localChops[6]);
+ if (chopT == 0) {
+ // Also ensure the first curve is exactly p0 when we chop at T=0.
+ REPORTER_ASSERT(reporter, localChops[0] == pts[0]);
+ REPORTER_ASSERT(reporter, localChops[1] == pts[0]);
+ REPORTER_ASSERT(reporter, localChops[2] == pts[0]);
+ REPORTER_ASSERT(reporter, localChops[3] == pts[0]);
+ }
+ if (chopT == 1) {
+ // Also ensure the last curve is exactly p3 when we chop at T=1.
+ REPORTER_ASSERT(reporter, localChops[6] == pts[3]);
+ REPORTER_ASSERT(reporter, localChops[7] == pts[3]);
+ REPORTER_ASSERT(reporter, localChops[8] == pts[3]);
+ REPORTER_ASSERT(reporter, localChops[9] == pts[3]);
+ }
+ }
+
+ // Now test what happens when SkChopCubicAt does 0/0 and gets NaN values.
+ SkPoint oneChops[4 + SK_ARRAY_COUNT(ones)*3];
+ SkChopCubicAt(pts, oneChops, ones, SK_ARRAY_COUNT(ones));
+ REPORTER_ASSERT(reporter, oneChops[0] == pts[0]);
+ REPORTER_ASSERT(reporter, oneChops[1] == pts[1]);
+ REPORTER_ASSERT(reporter, oneChops[2] == pts[2]);
+ for (size_t i = 3; i < SK_ARRAY_COUNT(oneChops); ++i) {
+ REPORTER_ASSERT(reporter, oneChops[i] == pts[3]);
+ }
+ }
}
static void check_pairs(skiatest::Reporter* reporter, int index, SkScalar t, const char name[],