Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit df23044

Browse files
authored
[web] Implement ulps for path ops (#19711)
* Implement ulps for path ops * Address review comments * cache abs() * dartfmt and update licenses
1 parent 7a95e32 commit df23044

File tree

4 files changed

+304
-0
lines changed

4 files changed

+304
-0
lines changed

ci/licenses_golden/licenses_flutter

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/text_editing/autofill_hint.dart
524524
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text_editing/input_type.dart
525525
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text_editing/text_capitalization.dart
526526
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart
527+
FILE: ../../../flutter/lib/web_ui/lib/src/engine/ulps.dart
527528
FILE: ../../../flutter/lib/web_ui/lib/src/engine/util.dart
528529
FILE: ../../../flutter/lib/web_ui/lib/src/engine/validators.dart
529530
FILE: ../../../flutter/lib/web_ui/lib/src/engine/vector_math.dart

lib/web_ui/lib/src/engine.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ part 'engine/text_editing/autofill_hint.dart';
131131
part 'engine/text_editing/input_type.dart';
132132
part 'engine/text_editing/text_capitalization.dart';
133133
part 'engine/text_editing/text_editing.dart';
134+
part 'engine/ulps.dart';
134135
part 'engine/util.dart';
135136
part 'engine/validators.dart';
136137
part 'engine/vector_math.dart';
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
part of engine;
6+
7+
// This is a small library to handle stability for floating point operations.
8+
//
9+
// Since we are representing an infinite number of real numbers in finite
10+
// number of bits, when we perform comparisons of coordinates for paths for
11+
// example, we want to make sure that line and curve sections that are too
12+
// close to each other (number of floating point numbers
13+
// representable in bits between two numbers) are handled correctly and
14+
// don't cause algorithms to fail when we perform operations such as
15+
// subtraction or between checks.
16+
//
17+
// Small introduction into floating point comparison:
18+
//
19+
// For some good articles on the topic, see
20+
// https://randomascii.wordpress.com/category/floating-point/page/2/
21+
// Port based on:
22+
// https://github.com/google/skia/blob/master/include/private/SkFloatBits.h
23+
//
24+
// Here is the 32 bit IEEE representation:
25+
// uint32_t mantissa : 23;
26+
// uint32_t exponent : 8;
27+
// uint32_t sign : 1;
28+
// As you can see it was carefully designed to be reinterpreted as an integer.
29+
//
30+
// Ulps stands for unit in the last place. ulp(x) is the gap between two
31+
// floating point numbers nearest x.
32+
33+
/// Converts a sign-bit int (float interpreted as int) into a 2s complement
34+
/// int. Also converts 0x80000000 to 0. Allows result to be compared using
35+
/// int comparison.
36+
int signBitTo2sCompliment(int x) =>
37+
(x & 0x80000000) != 0 ? (-(x & 0x7fffffff)) : x;
38+
39+
/// Convert a 2s complement int to a sign-bit (i.e. int interpreted as float).
40+
int twosComplimentToSignBit(int x) {
41+
if ((x & 0x80000000) == 0) {
42+
return x;
43+
}
44+
x = ~x + 1;
45+
x |= 0x80000000;
46+
return x;
47+
}
48+
49+
class _FloatBitConverter {
50+
final Float32List float32List;
51+
final Int32List int32List;
52+
_FloatBitConverter._(this.float32List, this.int32List);
53+
54+
factory _FloatBitConverter() {
55+
Float32List float32List = Float32List(1);
56+
return _FloatBitConverter._(
57+
float32List, float32List.buffer.asInt32List(0, 1));
58+
}
59+
60+
int toInt(Float32List source, int index) {
61+
float32List[0] = source[index];
62+
return int32List[0];
63+
}
64+
65+
int toBits(double x) {
66+
float32List[0] = x;
67+
return int32List[0];
68+
}
69+
70+
double toDouble(int bits) {
71+
int32List[0] = bits;
72+
return float32List[0];
73+
}
74+
}
75+
76+
// Singleton bit converter to prevent typed array allocations.
77+
final _FloatBitConverter _floatBitConverter = _FloatBitConverter();
78+
79+
// Converts float to bits.
80+
int float2Bits(Float32List source, int index) {
81+
return _floatBitConverter.toInt(source, index);
82+
}
83+
84+
// Converts bits to float.
85+
double bitsToFloat(int bits) {
86+
return _floatBitConverter.toDouble(bits);
87+
}
88+
89+
const int floatBitsExponentMask = 0x7F800000;
90+
const int floatBitsMatissaMask = 0x007FFFFF;
91+
92+
/// Returns a float as 2s complement int to be able to compare floats to each
93+
/// other.
94+
int floatFromListAs2sCompliment(Float32List source, int index) =>
95+
signBitTo2sCompliment(float2Bits(source, index));
96+
97+
int floatAs2sCompliment(double x) =>
98+
signBitTo2sCompliment(_floatBitConverter.toBits(x));
99+
100+
double twosComplimentAsFloat(int x) => bitsToFloat(twosComplimentToSignBit(x));
101+
102+
bool _argumentsDenormalized(double a, double b, int epsilon) {
103+
double denormalizedCheck = kFltEpsilon * epsilon / 2;
104+
return a.abs() <= denormalizedCheck && b.abs() <= denormalizedCheck;
105+
}
106+
107+
bool equalUlps(double a, double b, int epsilon, int depsilon) {
108+
if (_argumentsDenormalized(a, b, depsilon)) {
109+
return true;
110+
}
111+
int aBits = floatAs2sCompliment(a);
112+
int bBits = floatAs2sCompliment(b);
113+
// Find the difference in ULPs.
114+
return aBits < bBits + epsilon && bBits < aBits + epsilon;
115+
}
116+
117+
/// General equality check that covers between, product and division by using
118+
/// ulps epsilon 16.
119+
bool almostEqualUlps(double a, double b) {
120+
const int kUlpsEpsilon = 16;
121+
return equalUlps(a, b, kUlpsEpsilon, kUlpsEpsilon);
122+
}
123+
124+
/// Equality using the same error term for between comparison.
125+
bool almostBequalUlps(double a, double b) {
126+
const int kUlpsEpsilon = 2;
127+
return equalUlps(a, b, kUlpsEpsilon, kUlpsEpsilon);
128+
}
129+
130+
/// Equality check for product.
131+
bool almostPequalUlps(double a, double b) {
132+
const int kUlpsEpsilon = 8;
133+
return equalUlps(a, b, kUlpsEpsilon, kUlpsEpsilon);
134+
}
135+
136+
/// Equality check for division.
137+
bool almostDequalUlps(double a, double b) {
138+
const int kUlpsEpsilon = 16;
139+
return equalUlps(a, b, kUlpsEpsilon, kUlpsEpsilon);
140+
}
141+
142+
/// Checks if 2 points are roughly equal (ulp 256) to each other.
143+
bool approximatelyEqual(double ax, double ay, double bx, double by) {
144+
if (approximatelyEqualT(ax, bx) && approximatelyEqualT(ay, by)) {
145+
return true;
146+
}
147+
if (!roughlyEqualUlps(ax, bx) || !roughlyEqualUlps(ay, by)) {
148+
return false;
149+
}
150+
final double dx = (ax - bx);
151+
final double dy = (ay - by);
152+
double dist = math.sqrt(dx * dx + dy * dy);
153+
double tiniest = math.min(math.min(math.min(ax, bx), ay), by);
154+
double largest = math.max(math.max(math.max(ax, bx), ay), by);
155+
largest = math.max(largest, -tiniest);
156+
return almostDequalUlps(largest, largest + dist);
157+
}
158+
159+
/// Equality check for comparing curve T values in the range of 0 to 1.
160+
///
161+
/// For general numbers (larger and smaller) use
162+
/// AlmostEqualUlps instead.
163+
bool approximatelyEqualT(double t1, double t2) {
164+
return approximatelyZero(t1 - t2);
165+
}
166+
167+
bool approximatelyZero(double value) => value.abs() < kFltEpsilon;
168+
169+
bool roughlyEqualUlps(double a, double b) {
170+
const int kUlpsEpsilon = 256;
171+
const int kDUlpsEpsilon = 1024;
172+
return equalUlps(a, b, kUlpsEpsilon, kDUlpsEpsilon);
173+
}
174+
175+
bool dEqualUlpsEpsilon(double a, double b, int epsilon) {
176+
int aBits = floatAs2sCompliment(a);
177+
int bBits = floatAs2sCompliment(b);
178+
// Find the difference in ULPs.
179+
return aBits < bBits + epsilon && bBits < aBits + epsilon;
180+
}
181+
182+
// Checks equality for division.
183+
bool almostDequalUlpsDouble(double a, double b) {
184+
final double absA = a.abs();
185+
final double absB = b.abs();
186+
if (absA < kScalarMax && absB < kScalarMax) {
187+
return almostDequalUlps(a, b);
188+
}
189+
return (a - b).abs() / math.max(absA, absB) < kDblEpsilonSubdivideErr;
190+
}
191+
192+
const double kFltEpsilon = 1.19209290E-07; // == 1 / (2 ^ 23)
193+
const double kDblEpsilon = 2.22045e-16;
194+
const double kFltEpsilonCubed = kFltEpsilon * kFltEpsilon * kFltEpsilon;
195+
const double kFltEpsilonHalf = kFltEpsilon / 2;
196+
const double kFltEpsilonDouble = kFltEpsilon * 2;
197+
// Epsilon to use when ordering vectors.
198+
const double kFltEpsilonOrderableErr = kFltEpsilon * 16;
199+
const double kFltEpsilonSquared = kFltEpsilon * kFltEpsilon;
200+
// Use a compile-time constant for FLT_EPSILON_SQRT to avoid initializers.
201+
// A 17 digit constant guarantees exact results.
202+
const double kFltEpsilonSqrt = 0.00034526697709225118; // sqrt(kFltEpsilon);
203+
const double kFltEpsilonInverse = 1 / kFltEpsilon;
204+
const double kDblEpsilonErr = kDblEpsilon * 4;
205+
const double kDblEpsilonSubdivideErr = kDblEpsilon * 16;
206+
const double kRoughEpsilon = kFltEpsilon * 64;
207+
const double kMoreRoughEpsilon = kFltEpsilon * 256;
208+
const double kWayRoughEpsilon = kFltEpsilon * 2048;
209+
const double kBumpEpsilon = kFltEpsilon * 4096;
210+
211+
// Scalar max is based on 32 bit float since [PathRef] stores values in
212+
// Float32List.
213+
const double kScalarMax = 3.402823466e+38;
214+
const double kScalarMin = -kScalarMax;
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:typed_data';
6+
import 'package:test/test.dart';
7+
import 'package:ui/src/engine.dart';
8+
9+
void main() {
10+
group('Float Int conversions', (){
11+
test('Should convert signbit to 2\'s compliment', () {
12+
expect(signBitTo2sCompliment(0), 0);
13+
expect(signBitTo2sCompliment(0x7fffffff).toUnsigned(32), 0x7fffffff);
14+
expect(signBitTo2sCompliment(0x80000000), 0);
15+
expect(signBitTo2sCompliment(0x8f000000).toUnsigned(32), 0xf1000000);
16+
expect(signBitTo2sCompliment(0x8fffffff).toUnsigned(32), 0xf0000001);
17+
expect(signBitTo2sCompliment(0xffffffff).toUnsigned(32), 0x80000001);
18+
expect(signBitTo2sCompliment(0x8f000000), -251658240);
19+
expect(signBitTo2sCompliment(0x8fffffff), -268435455);
20+
expect(signBitTo2sCompliment(0xffffffff), -2147483647);
21+
});
22+
23+
test('Should convert 2s compliment to signbit', () {
24+
expect(twosComplimentToSignBit(0), 0);
25+
expect(twosComplimentToSignBit(0x7fffffff), 0x7fffffff);
26+
expect(twosComplimentToSignBit(0), 0);
27+
expect(twosComplimentToSignBit(0xf1000000).toRadixString(16), 0x8f000000.toRadixString(16));
28+
expect(twosComplimentToSignBit(0xf0000001), 0x8fffffff);
29+
expect(twosComplimentToSignBit(0x80000001), 0xffffffff);
30+
expect(twosComplimentToSignBit(0x81234561), 0xfedcba9f);
31+
expect(twosComplimentToSignBit(-5), 0x80000005);
32+
});
33+
34+
test('Should convert float to bits', () {
35+
Float32List floatList = Float32List(1);
36+
floatList[0] = 0;
37+
expect(float2Bits(floatList, 0), 0);
38+
floatList[0] = 0.1;
39+
expect(float2Bits(floatList, 0).toUnsigned(32).toRadixString(16), 0x3dcccccd.toRadixString(16));
40+
floatList[0] = 123456.0;
41+
expect(float2Bits(floatList, 0).toUnsigned(32).toRadixString(16), 0x47f12000.toRadixString(16));
42+
floatList[0] = -0.1;
43+
expect(float2Bits(floatList, 0).toUnsigned(32).toRadixString(16), 0xbdcccccd.toRadixString(16));
44+
floatList[0] = -123456.0;
45+
expect(float2Bits(floatList, 0).toUnsigned(32).toRadixString(16), 0xc7f12000.toRadixString(16));
46+
});
47+
});
48+
group('Comparison', () {
49+
test('Should compare equality based on ulps', () {
50+
// If number of floats between a=1.1 and b are below 16, equals should
51+
// return true.
52+
final double a = 1.1;
53+
int aBits = floatAs2sCompliment(a);
54+
double b = twosComplimentAsFloat(aBits + 1);
55+
expect(almostEqualUlps(a, b), true);
56+
b = twosComplimentAsFloat(aBits + 15);
57+
expect(almostEqualUlps(a, b), true);
58+
b = twosComplimentAsFloat(aBits + 16);
59+
expect(almostEqualUlps(a, b), false);
60+
61+
// Test between variant of equalUlps.
62+
b = twosComplimentAsFloat(aBits + 1);
63+
expect(almostBequalUlps(a, b), true);
64+
b = twosComplimentAsFloat(aBits + 1);
65+
expect(almostBequalUlps(a, b), true);
66+
b = twosComplimentAsFloat(aBits + 2);
67+
expect(almostBequalUlps(a, b), false);
68+
});
69+
70+
test('Should compare 2 coordinates based on ulps', () {
71+
double a = 1.1;
72+
int aBits = floatAs2sCompliment(a);
73+
double b = twosComplimentAsFloat(aBits + 1);
74+
expect(approximatelyEqual(5.0, a, 5.0, b), true);
75+
b = twosComplimentAsFloat(aBits + 16);
76+
expect(approximatelyEqual(5.0, a, 5.0, b), true);
77+
78+
// Increase magnitude which should start checking with ulps rather than
79+
// fltEpsilon.
80+
a = 3000000.1;
81+
aBits = floatAs2sCompliment(a);
82+
b = twosComplimentAsFloat(aBits + 1);
83+
expect(approximatelyEqual(5.0, a, 5.0, b), true);
84+
b = twosComplimentAsFloat(aBits + 16);
85+
expect(approximatelyEqual(5.0, a, 5.0, b), false);
86+
});
87+
});
88+
}

0 commit comments

Comments
 (0)