Improve conic Wang's formula correctness tests

- Treat randomly generated control points as pre-projected
- Use double-precision conic evaluation for "within tolerance" test.
  This allows us to test with larger magnitude control points (the
  SkConic/SkGeometry single-precision functions lose too much accuracy).

Bug: skia:10419
Change-Id: Iba0915dccb50131e1a1b28a7d556863497f636e9
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/367057
Commit-Queue: Tyler Denniston <tdenniston@google.com>
Reviewed-by: Chris Dalton <csmartdalton@google.com>
This commit is contained in:
Tyler Denniston 2021-02-08 11:06:49 -05:00 committed by Skia Commit-Bot
parent c740d93628
commit 4c0a35f9d6

View File

@ -411,36 +411,61 @@ DEF_TEST(WangsFormula_rational_quad_reduces, r) {
// Ensure the rational quad version (used for conics) produces max error within tolerance.
DEF_TEST(WangsFormula_conic_within_tol, r) {
constexpr int maxExponent = 15;
constexpr int maxExponent = 24;
// Single-precision functions in SkConic/SkGeometry lose too much accuracy with
// large-magnitude curves and large weights for this test to pass.
using Sk2d = skvx::Vec<2, double>;
const auto eval_conic = [](const SkPoint pts[3], float w, float t) -> Sk2d {
const auto eval = [](Sk2d A, Sk2d B, Sk2d C, float t) -> Sk2d {
return (A * t + B) * t + C;
};
const Sk2d p0 = {pts[0].fX, pts[0].fY};
const Sk2d p1 = {pts[1].fX, pts[1].fY};
const Sk2d p1w = p1 * w;
const Sk2d p2 = {pts[2].fX, pts[2].fY};
Sk2d numer = eval(p2 - p1w * 2 + p0, (p1w - p0) * 2, p0, t);
Sk2d denomC = {1, 1};
Sk2d denomB = {2 * (w - 1), 2 * (w - 1)};
Sk2d denomA = {-2 * (w - 1), -2 * (w - 1)};
Sk2d denom = eval(denomA, denomB, denomC, t);
return numer / denom;
};
const auto dot = [](const Sk2d& a, const Sk2d& b) -> double {
return a[0] * b[0] + a[1] * b[1];
};
const auto length = [](const Sk2d& p) -> double { return sqrt(p[0] * p[0] + p[1] * p[1]); };
SkRandom rand;
for (int i = -10; i <= 10; ++i) {
const float w = std::ldexp(1 + rand.nextF(), i);
for_random_beziers(
3, &rand,
[&r, w](const SkPoint pts[]) {
const SkPoint projPts[3] = {pts[0], pts[1] * (1.f / w), pts[2]};
const int nsegs = static_cast<int>(std::ceil(
wangs_formula_conic_reference_impl(kIntolerance, projPts, w)));
[&](const SkPoint pts[]) {
const int nsegs = static_cast<int>(
std::ceil(wangs_formula_conic_reference_impl(kIntolerance, pts, w)));
const SkConic conic(projPts[0], projPts[1], projPts[2], w);
const float tdelta = 1.f / nsegs;
for (int j = 0; j < nsegs; ++j) {
const float tmin = j * tdelta, tmax = (j + 1) * tdelta,
tmid = 0.5f * (tmin + tmax);
SkPoint p0, p1, p2;
conic.evalAt(tmin, &p0);
conic.evalAt(tmid, &p1);
conic.evalAt(tmax, &p2);
Sk2d p0, p1, p2;
p0 = eval_conic(pts, w, tmin);
p1 = eval_conic(pts, w, tmid);
p2 = eval_conic(pts, w, tmax);
// Get distance of p1 to baseline (p0, p2).
const SkPoint n = {p2.fY - p0.fY, p0.fX - p2.fX};
SkASSERT(n.length() != 0);
const float d = std::abs((p1 - p0).dot(n)) / n.length();
const Sk2d n = {p2[1] - p0[1], p0[0] - p2[0]};
SkASSERT(length(n) != 0);
const double d = std::abs(dot(p1 - p0, n)) / length(n);
// Check distance is within tolerance
REPORTER_ASSERT(r, d <= (1.f / kIntolerance) + SK_ScalarNearlyZero);
REPORTER_ASSERT(r, d <= (1.0 / kIntolerance) + SK_ScalarNearlyZero);
}
},
maxExponent);
@ -455,9 +480,8 @@ DEF_TEST(WangsFormula_conic_matches_reference, r) {
for (int i = -10; i <= 10; ++i) {
const float w = std::ldexp(1 + rand.nextF(), i);
for_random_beziers(3, &rand, [&r, w](const SkPoint pts[]) {
const SkPoint projPts[3] = {pts[0], pts[1] * (1.f / w), pts[2]};
const float ref_nsegs = wangs_formula_conic_reference_impl(kIntolerance, projPts, w);
const float nsegs = GrWangsFormula::conic(kTolerance, projPts, w);
const float ref_nsegs = wangs_formula_conic_reference_impl(kIntolerance, pts, w);
const float nsegs = GrWangsFormula::conic(kTolerance, pts, w);
// Because the Gr version may implement the math differently for performance,
// allow different slack in the comparison based on the rough scale of the answer.
@ -472,11 +496,10 @@ DEF_TEST(WangsFormula_conic_vectorXforms, r) {
constexpr static float kTolerance = 1.f / kIntolerance;
auto check_conic_with_transform = [&](const SkPoint* pts, float w, const SkMatrix& m) {
const SkPoint projPts[3] = {pts[0], pts[1] * (1.f / w), pts[2]};
SkPoint ptsXformed[3];
m.mapPoints(ptsXformed, projPts, 3);
m.mapPoints(ptsXformed, pts, 3);
float expected = GrWangsFormula::conic(kTolerance, ptsXformed, w);
float actual = GrWangsFormula::conic(kTolerance, projPts, w, GrVectorXform(m));
float actual = GrWangsFormula::conic(kTolerance, pts, w, GrVectorXform(m));
REPORTER_ASSERT(r, SkScalarNearlyEqual(actual, expected));
};