-
Notifications
You must be signed in to change notification settings - Fork 6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[web] Implement ulps for path ops #19711
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
// 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/ | ||
// | ||
// 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. compliment => complement (applies throughout the file) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
|
||
/// Convert a 2s compliment int to a sign-bit (i.e. int interpreted as float). | ||
int twosComplimentToSignBit(int x) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this work on arbitrary ranges of values? Same question for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added tests to cover ranges. |
||
if ((x & 0x80000000) == 0) { | ||
return x; | ||
} | ||
x = ~x + 1; | ||
x |= 0x80000000; | ||
return x; | ||
} | ||
|
||
class _FloatBitConverter { | ||
final Float32List float32List = Float32List(1); | ||
late Int32List int32List; | ||
_FloatBitConverter() { | ||
int32List = float32List.buffer.asInt32List(0, 1); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
} | ||
int toInt(Float32List source, int index) { | ||
float32List[0] = source[index]; | ||
return int32List[0]; | ||
} | ||
int toBits(double x) { | ||
float32List[0] = x; | ||
return int32List[0]; | ||
} | ||
double toFloat(int bits) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
int32List[0] = bits; | ||
return float32List[0]; | ||
} | ||
} | ||
|
||
// Singleton bit converter to prevent typed array allocations. | ||
_FloatBitConverter _floatBitConverter = _FloatBitConverter(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. final? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
|
||
// Converts float to bits. | ||
int float2Bits(Float32List source, int index) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not take a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will pass pathRef.points array directly to this api. See toBits that uses double as input. |
||
return _floatBitConverter.toInt(source, index); | ||
} | ||
|
||
// Converts bits to float. | ||
double bitsToFloat(int bits) { | ||
return _floatBitConverter.toFloat(bits); | ||
} | ||
|
||
const int floatBitsExponentMask = 0x7F800000; | ||
const int floatBitsMatissaMask = 0x007FFFFF; | ||
|
||
/// Returns a float as 2s compliment 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; | ||
} | ||
|
||
bool almostEqualUlps(double a, double b) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Docs? (applies here and other undocumented functions) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
const int kUlpsEpsilon = 16; | ||
return equalUlps(a, b, kUlpsEpsilon, kUlpsEpsilon); | ||
} | ||
|
||
// Equality using the same error term as between | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this doc comment need an ending? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
bool almostBequalUlps(double a, double b) { | ||
const int kUlpsEpsilon = 2; | ||
return equalUlps(a, b, kUlpsEpsilon, kUlpsEpsilon); | ||
} | ||
|
||
bool almostPequalUlps(double a, double b) { | ||
const int kUlpsEpsilon = 8; | ||
return equalUlps(a, b, kUlpsEpsilon, kUlpsEpsilon); | ||
} | ||
|
||
bool almostDequalUlps(double a, double b) { | ||
const int kUlpsEpsilon = 16; | ||
return equalUlps(a, b, kUlpsEpsilon, kUlpsEpsilon); | ||
} | ||
|
||
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); | ||
} | ||
|
||
// Use this for comparing Ts 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 UlpsEpsilon = 256; | ||
const int DUlpsEpsilon = 1024; | ||
return equalUlps(a, b, UlpsEpsilon, DUlpsEpsilon); | ||
} | ||
|
||
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; | ||
} | ||
|
||
bool almostDequalUlpsDouble(double a, double b) { | ||
if (a.abs() < kScalarMax && b.abs() < kScalarMax) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe cache There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
return almostDequalUlps(a, b); | ||
} | ||
return (a - b).abs() / math.max(a.abs(), b.abs()) < kFltEpsilon * 16; | ||
ditman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
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; | ||
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; | ||
ditman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// Scalar max is based on 32 bit float since [PathRef] stores values in | ||
// Float32List. | ||
const double kScalarMax = 3.402823466e+38; | ||
const double kScalarMin = -kScalarMax; |
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); | ||
}); | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a Skia file(s) we can point to where this was ported from?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added link.