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>
This commit is contained in:
Chris Dalton 2020-10-16 15:12:10 -06:00 committed by Skia Commit-Bot
parent 36af02d2ef
commit 81b270a659
4 changed files with 182 additions and 74 deletions

View File

@ -476,6 +476,11 @@ SINTU Vec<N,T> max(const Vec<N,T>& x, U y) { return max(x, Vec<N,T>(y)); }
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};

View File

@ -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 @@ int SkFindCubicExtrema(SkScalar a, SkScalar b, SkScalar c, SkScalar d,
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;
}
}
}
}

View File

@ -133,13 +133,19 @@ void SkEvalCubicAt(const SkPoint src[4], SkScalar t, SkPoint* locOrNull,
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[],

View File

@ -46,6 +46,72 @@ static void testChopCubic(skiatest::Reporter* reporter) {
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[],