From df2304489783e04cd093cd0e72b57d534b3c1040 Mon Sep 17 00:00:00 2001 From: Ferhat Date: Mon, 13 Jul 2020 13:43:11 -0700 Subject: [PATCH] [web] Implement ulps for path ops (#19711) * Implement ulps for path ops * Address review comments * cache abs() * dartfmt and update licenses --- ci/licenses_golden/licenses_flutter | 1 + lib/web_ui/lib/src/engine.dart | 1 + lib/web_ui/lib/src/engine/ulps.dart | 214 ++++++++++++++++++++++++++ lib/web_ui/test/engine/ulps_test.dart | 88 +++++++++++ 4 files changed, 304 insertions(+) create mode 100644 lib/web_ui/lib/src/engine/ulps.dart create mode 100644 lib/web_ui/test/engine/ulps_test.dart diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 5559d74271419..0134ce99514dd 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -524,6 +524,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/text_editing/autofill_hint.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/text_editing/input_type.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/text_editing/text_capitalization.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/ulps.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/util.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/validators.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/vector_math.dart diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index f4ca2d230c2a4..edc390ee3915f 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -131,6 +131,7 @@ part 'engine/text_editing/autofill_hint.dart'; part 'engine/text_editing/input_type.dart'; part 'engine/text_editing/text_capitalization.dart'; part 'engine/text_editing/text_editing.dart'; +part 'engine/ulps.dart'; part 'engine/util.dart'; part 'engine/validators.dart'; part 'engine/vector_math.dart'; diff --git a/lib/web_ui/lib/src/engine/ulps.dart b/lib/web_ui/lib/src/engine/ulps.dart new file mode 100644 index 0000000000000..678537ef16408 --- /dev/null +++ b/lib/web_ui/lib/src/engine/ulps.dart @@ -0,0 +1,214 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of engine; + +// This is a small library to handle stability for floating point operations. +// +// Since we are representing an infinite number of real numbers in finite +// number of bits, when we perform comparisons of coordinates for paths for +// example, we want to make sure that line and curve sections that are too +// close to each other (number of floating point numbers +// representable in bits between two numbers) are handled correctly and +// don't cause algorithms to fail when we perform operations such as +// subtraction or between checks. +// +// Small introduction into floating point comparison: +// +// For some good articles on the topic, see +// https://randomascii.wordpress.com/category/floating-point/page/2/ +// Port based on: +// https://github.com/google/skia/blob/master/include/private/SkFloatBits.h +// +// Here is the 32 bit IEEE representation: +// uint32_t mantissa : 23; +// uint32_t exponent : 8; +// uint32_t sign : 1; +// As you can see it was carefully designed to be reinterpreted as an integer. +// +// Ulps stands for unit in the last place. ulp(x) is the gap between two +// floating point numbers nearest x. + +/// Converts a sign-bit int (float interpreted as int) into a 2s complement +/// int. Also converts 0x80000000 to 0. Allows result to be compared using +/// int comparison. +int signBitTo2sCompliment(int x) => + (x & 0x80000000) != 0 ? (-(x & 0x7fffffff)) : x; + +/// Convert a 2s complement int to a sign-bit (i.e. int interpreted as float). +int twosComplimentToSignBit(int x) { + if ((x & 0x80000000) == 0) { + return x; + } + x = ~x + 1; + x |= 0x80000000; + return x; +} + +class _FloatBitConverter { + final Float32List float32List; + final Int32List int32List; + _FloatBitConverter._(this.float32List, this.int32List); + + factory _FloatBitConverter() { + Float32List float32List = Float32List(1); + return _FloatBitConverter._( + float32List, float32List.buffer.asInt32List(0, 1)); + } + + int toInt(Float32List source, int index) { + float32List[0] = source[index]; + return int32List[0]; + } + + int toBits(double x) { + float32List[0] = x; + return int32List[0]; + } + + double toDouble(int bits) { + int32List[0] = bits; + return float32List[0]; + } +} + +// Singleton bit converter to prevent typed array allocations. +final _FloatBitConverter _floatBitConverter = _FloatBitConverter(); + +// Converts float to bits. +int float2Bits(Float32List source, int index) { + return _floatBitConverter.toInt(source, index); +} + +// Converts bits to float. +double bitsToFloat(int bits) { + return _floatBitConverter.toDouble(bits); +} + +const int floatBitsExponentMask = 0x7F800000; +const int floatBitsMatissaMask = 0x007FFFFF; + +/// Returns a float as 2s complement int to be able to compare floats to each +/// other. +int floatFromListAs2sCompliment(Float32List source, int index) => + signBitTo2sCompliment(float2Bits(source, index)); + +int floatAs2sCompliment(double x) => + signBitTo2sCompliment(_floatBitConverter.toBits(x)); + +double twosComplimentAsFloat(int x) => bitsToFloat(twosComplimentToSignBit(x)); + +bool _argumentsDenormalized(double a, double b, int epsilon) { + double denormalizedCheck = kFltEpsilon * epsilon / 2; + return a.abs() <= denormalizedCheck && b.abs() <= denormalizedCheck; +} + +bool equalUlps(double a, double b, int epsilon, int depsilon) { + if (_argumentsDenormalized(a, b, depsilon)) { + return true; + } + int aBits = floatAs2sCompliment(a); + int bBits = floatAs2sCompliment(b); + // Find the difference in ULPs. + return aBits < bBits + epsilon && bBits < aBits + epsilon; +} + +/// General equality check that covers between, product and division by using +/// ulps epsilon 16. +bool almostEqualUlps(double a, double b) { + const int kUlpsEpsilon = 16; + return equalUlps(a, b, kUlpsEpsilon, kUlpsEpsilon); +} + +/// Equality using the same error term for between comparison. +bool almostBequalUlps(double a, double b) { + const int kUlpsEpsilon = 2; + return equalUlps(a, b, kUlpsEpsilon, kUlpsEpsilon); +} + +/// Equality check for product. +bool almostPequalUlps(double a, double b) { + const int kUlpsEpsilon = 8; + return equalUlps(a, b, kUlpsEpsilon, kUlpsEpsilon); +} + +/// Equality check for division. +bool almostDequalUlps(double a, double b) { + const int kUlpsEpsilon = 16; + return equalUlps(a, b, kUlpsEpsilon, kUlpsEpsilon); +} + +/// Checks if 2 points are roughly equal (ulp 256) to each other. +bool approximatelyEqual(double ax, double ay, double bx, double by) { + if (approximatelyEqualT(ax, bx) && approximatelyEqualT(ay, by)) { + return true; + } + if (!roughlyEqualUlps(ax, bx) || !roughlyEqualUlps(ay, by)) { + return false; + } + final double dx = (ax - bx); + final double dy = (ay - by); + double dist = math.sqrt(dx * dx + dy * dy); + double tiniest = math.min(math.min(math.min(ax, bx), ay), by); + double largest = math.max(math.max(math.max(ax, bx), ay), by); + largest = math.max(largest, -tiniest); + return almostDequalUlps(largest, largest + dist); +} + +/// Equality check for comparing curve T values in the range of 0 to 1. +/// +/// For general numbers (larger and smaller) use +/// AlmostEqualUlps instead. +bool approximatelyEqualT(double t1, double t2) { + return approximatelyZero(t1 - t2); +} + +bool approximatelyZero(double value) => value.abs() < kFltEpsilon; + +bool roughlyEqualUlps(double a, double b) { + const int kUlpsEpsilon = 256; + const int kDUlpsEpsilon = 1024; + return equalUlps(a, b, kUlpsEpsilon, kDUlpsEpsilon); +} + +bool dEqualUlpsEpsilon(double a, double b, int epsilon) { + int aBits = floatAs2sCompliment(a); + int bBits = floatAs2sCompliment(b); + // Find the difference in ULPs. + return aBits < bBits + epsilon && bBits < aBits + epsilon; +} + +// Checks equality for division. +bool almostDequalUlpsDouble(double a, double b) { + final double absA = a.abs(); + final double absB = b.abs(); + if (absA < kScalarMax && absB < kScalarMax) { + return almostDequalUlps(a, b); + } + return (a - b).abs() / math.max(absA, absB) < kDblEpsilonSubdivideErr; +} + +const double kFltEpsilon = 1.19209290E-07; // == 1 / (2 ^ 23) +const double kDblEpsilon = 2.22045e-16; +const double kFltEpsilonCubed = kFltEpsilon * kFltEpsilon * kFltEpsilon; +const double kFltEpsilonHalf = kFltEpsilon / 2; +const double kFltEpsilonDouble = kFltEpsilon * 2; +// Epsilon to use when ordering vectors. +const double kFltEpsilonOrderableErr = kFltEpsilon * 16; +const double kFltEpsilonSquared = kFltEpsilon * kFltEpsilon; +// Use a compile-time constant for FLT_EPSILON_SQRT to avoid initializers. +// A 17 digit constant guarantees exact results. +const double kFltEpsilonSqrt = 0.00034526697709225118; // sqrt(kFltEpsilon); +const double kFltEpsilonInverse = 1 / kFltEpsilon; +const double kDblEpsilonErr = kDblEpsilon * 4; +const double kDblEpsilonSubdivideErr = kDblEpsilon * 16; +const double kRoughEpsilon = kFltEpsilon * 64; +const double kMoreRoughEpsilon = kFltEpsilon * 256; +const double kWayRoughEpsilon = kFltEpsilon * 2048; +const double kBumpEpsilon = kFltEpsilon * 4096; + +// Scalar max is based on 32 bit float since [PathRef] stores values in +// Float32List. +const double kScalarMax = 3.402823466e+38; +const double kScalarMin = -kScalarMax; diff --git a/lib/web_ui/test/engine/ulps_test.dart b/lib/web_ui/test/engine/ulps_test.dart new file mode 100644 index 0000000000000..9d993e92eb262 --- /dev/null +++ b/lib/web_ui/test/engine/ulps_test.dart @@ -0,0 +1,88 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; +import 'package:test/test.dart'; +import 'package:ui/src/engine.dart'; + +void main() { + group('Float Int conversions', (){ + test('Should convert signbit to 2\'s compliment', () { + expect(signBitTo2sCompliment(0), 0); + expect(signBitTo2sCompliment(0x7fffffff).toUnsigned(32), 0x7fffffff); + expect(signBitTo2sCompliment(0x80000000), 0); + expect(signBitTo2sCompliment(0x8f000000).toUnsigned(32), 0xf1000000); + expect(signBitTo2sCompliment(0x8fffffff).toUnsigned(32), 0xf0000001); + expect(signBitTo2sCompliment(0xffffffff).toUnsigned(32), 0x80000001); + expect(signBitTo2sCompliment(0x8f000000), -251658240); + expect(signBitTo2sCompliment(0x8fffffff), -268435455); + expect(signBitTo2sCompliment(0xffffffff), -2147483647); + }); + + test('Should convert 2s compliment to signbit', () { + expect(twosComplimentToSignBit(0), 0); + expect(twosComplimentToSignBit(0x7fffffff), 0x7fffffff); + expect(twosComplimentToSignBit(0), 0); + expect(twosComplimentToSignBit(0xf1000000).toRadixString(16), 0x8f000000.toRadixString(16)); + expect(twosComplimentToSignBit(0xf0000001), 0x8fffffff); + expect(twosComplimentToSignBit(0x80000001), 0xffffffff); + expect(twosComplimentToSignBit(0x81234561), 0xfedcba9f); + expect(twosComplimentToSignBit(-5), 0x80000005); + }); + + test('Should convert float to bits', () { + Float32List floatList = Float32List(1); + floatList[0] = 0; + expect(float2Bits(floatList, 0), 0); + floatList[0] = 0.1; + expect(float2Bits(floatList, 0).toUnsigned(32).toRadixString(16), 0x3dcccccd.toRadixString(16)); + floatList[0] = 123456.0; + expect(float2Bits(floatList, 0).toUnsigned(32).toRadixString(16), 0x47f12000.toRadixString(16)); + floatList[0] = -0.1; + expect(float2Bits(floatList, 0).toUnsigned(32).toRadixString(16), 0xbdcccccd.toRadixString(16)); + floatList[0] = -123456.0; + expect(float2Bits(floatList, 0).toUnsigned(32).toRadixString(16), 0xc7f12000.toRadixString(16)); + }); + }); + group('Comparison', () { + test('Should compare equality based on ulps', () { + // If number of floats between a=1.1 and b are below 16, equals should + // return true. + final double a = 1.1; + int aBits = floatAs2sCompliment(a); + double b = twosComplimentAsFloat(aBits + 1); + expect(almostEqualUlps(a, b), true); + b = twosComplimentAsFloat(aBits + 15); + expect(almostEqualUlps(a, b), true); + b = twosComplimentAsFloat(aBits + 16); + expect(almostEqualUlps(a, b), false); + + // Test between variant of equalUlps. + b = twosComplimentAsFloat(aBits + 1); + expect(almostBequalUlps(a, b), true); + b = twosComplimentAsFloat(aBits + 1); + expect(almostBequalUlps(a, b), true); + b = twosComplimentAsFloat(aBits + 2); + expect(almostBequalUlps(a, b), false); + }); + + test('Should compare 2 coordinates based on ulps', () { + double a = 1.1; + int aBits = floatAs2sCompliment(a); + double b = twosComplimentAsFloat(aBits + 1); + expect(approximatelyEqual(5.0, a, 5.0, b), true); + b = twosComplimentAsFloat(aBits + 16); + expect(approximatelyEqual(5.0, a, 5.0, b), true); + + // Increase magnitude which should start checking with ulps rather than + // fltEpsilon. + a = 3000000.1; + aBits = floatAs2sCompliment(a); + b = twosComplimentAsFloat(aBits + 1); + expect(approximatelyEqual(5.0, a, 5.0, b), true); + b = twosComplimentAsFloat(aBits + 16); + expect(approximatelyEqual(5.0, a, 5.0, b), false); + }); + }); +}