Calculate a very accurate discriminant

Calculate the discriminant in the ordinary manner. Check
for too much cancellation if B^2 and AC are too close.
If they are too close, then track the rounding error while
recalculating the discriminant, and add rounding error
back in. Tests included.

Change-Id: If2a04eb5c663ffe05aa90df9116796bc5f5d7a64
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/703078
Commit-Queue: Herb Derby <herb@google.com>
Reviewed-by: Jim Van Verth <jvanverth@google.com>
diff --git a/src/base/SkQuads.cpp b/src/base/SkQuads.cpp
index a778379..acdfabf 100644
--- a/src/base/SkQuads.cpp
+++ b/src/base/SkQuads.cpp
@@ -40,6 +40,33 @@
     return std::abs(A / B) < 1.0e-16;
 }
 
+double SkQuads::Discriminant(const double a, const double b, const double c) {
+    const double b2 = b * b;
+    const double ac = a * c;
+
+    // Calculate the rough discriminate which may suffer from a loss in precision due to b2 and
+    // ac being too close.
+    const double roughDiscriminant = b2 - ac;
+
+    // Check if b2 and ac were too close, and caused catastrophic cancellation. The
+    // roughDiscriminant should be good enough most of the time. If b2 and ac are very close to
+    // each other, then roughDiscriminant will be very small with most of the bits canceled
+    // making much smaller than roundOffCheck.
+    const double roundOffCheck = b2 + ac;
+    if (3 * std::abs(roughDiscriminant) >= roundOffCheck) {
+        return roughDiscriminant;
+    }
+
+    // Use the extra internal precision afforded by fma to calculate the rounding error for
+    // b^2 and ac.
+    const double b2RoundingError = std::fma(b, b, -b2);
+    const double acRoundingError = std::fma(a, c, -ac);
+
+    // Add the total rounding error back into the discriminant guess.
+    const double discriminant = (b2 - ac) + (b2RoundingError - acRoundingError);
+    return discriminant;
+}
+
 int SkQuads::RootsReal(const double A, const double B, const double C, double solution[2]) {
     if (close_to_linear(A, B)) {
         return solve_linear(B, C, solution);
diff --git a/src/base/SkQuads.h b/src/base/SkQuads.h
index 645d43b..2cdc4ac 100644
--- a/src/base/SkQuads.h
+++ b/src/base/SkQuads.h
@@ -15,12 +15,26 @@
 class SkQuads {
 public:
     /**
+     * Calculate a very accurate discriminant.
+     * Given
+     *    A*t^2 -2*B*t + C = 0,
+     * calculate
+     *    B^2 - AC
+     * accurate to 2 bits.
+     * Note the form of the quadratic is slightly different from the normal formulation.
+     *
+     * The method used to calculate the discriminant is from
+     *    "On the Cost of Floating-Point Computation Without Extra-Precise Arithmetic"
+     * by W. Kahan.
+     */
+    static double Discriminant(double A, double B, double C);
+
+    /**
      * Puts up to 2 real solutions to the equation
      *   A*t^2 + B*t + C = 0
      * in the provided array.
      */
-    static int RootsReal(double A, double B, double C,
-                         double solution[2]);
+    static int RootsReal(double A, double B, double C, double solution[2]);
 
     /**
      * Evaluates the quadratic function with the 3 provided coefficients and the
diff --git a/tests/QuadRootsTest.cpp b/tests/QuadRootsTest.cpp
index 62a18ad..34eac57 100644
--- a/tests/QuadRootsTest.cpp
+++ b/tests/QuadRootsTest.cpp
@@ -13,9 +13,10 @@
 #include "tests/Test.h"
 
 #include <algorithm>
-#include <cstddef>
 #include <cfloat>
 #include <cmath>
+#include <cstddef>
+#include <cstdint>
 #include <iterator>
 #include <string>
 
@@ -194,3 +195,21 @@
         "Nan constant"
     );
 }
+
+// Test the discriminant using
+// Use quadratics of the form F_n * x^2 - 2 * F_(n-1) * x + F_(n-2).
+//   This has a discriminant of F_(n-1)^2 - F_n * F_(n-2) = 1 if n is even else -1.
+DEF_TEST(QuadDiscriminant_Fibonacci, reporter) {
+    //            n,  n-1, n-2
+    int64_t F[] = {1,   1,   0};
+    // F_79 just fits in the 53 significant bits of a double.
+    for (int i = 2; i < 79; ++i) {
+        F[0] = F[1] + F[2];
+
+        const int expectedDiscriminant = i % 2 == 0 ? 1 : -1;
+        REPORTER_ASSERT(reporter, SkQuads::Discriminant(F[0], F[1], F[2]) == expectedDiscriminant);
+
+        F[2] = F[1];
+        F[1] = F[0];
+    }
+}