Skip to content

Commit

Permalink
[web] Implement ulps for path ops (#19711)
Browse files Browse the repository at this point in the history
* Implement ulps for path ops
* Address review comments
* cache abs()
* dartfmt and update licenses
  • Loading branch information
ferhatb authored Jul 13, 2020
1 parent 7a95e32 commit df23044
Show file tree
Hide file tree
Showing 4 changed files with 304 additions and 0 deletions.
1 change: 1 addition & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/web_ui/lib/src/engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
214 changes: 214 additions & 0 deletions lib/web_ui/lib/src/engine/ulps.dart
Original file line number Diff line number Diff line change
@@ -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;
88 changes: 88 additions & 0 deletions lib/web_ui/test/engine/ulps_test.dart
Original file line number Diff line number Diff line change
@@ -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);
});
});
}

0 comments on commit df23044

Please sign in to comment.