Browse Source

Merge pull request #4096 from rstm-sf/bugfix/double_precision

Fix machine epsilon for double
Dariusz Komosiński 5 years ago
parent
commit
adb401afc2

+ 1 - 1
src/Avalonia.Base/Properties/AssemblyInfo.cs

@@ -7,4 +7,4 @@ using Avalonia.Metadata;
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Data.Converters")]
 [assembly: InternalsVisibleTo("Avalonia.Base.UnitTests")]
 [assembly: InternalsVisibleTo("Avalonia.UnitTests")]
-[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] 
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]

+ 91 - 3
src/Avalonia.Base/Utilities/MathUtilities.cs

@@ -8,6 +8,11 @@ namespace Avalonia.Utilities
     /// </summary>
     public static class MathUtilities
     {
+        // smallest such that 1.0+DoubleEpsilon != 1.0
+        private const double DoubleEpsilon = 2.2204460492503131e-016;
+
+        private const float FloatEpsilon = 1.192092896e-07F;
+
         /// <summary>
         /// AreClose - Returns whether or not two doubles are "close".  That is, whether or 
         /// not they are within epsilon of each other.
@@ -18,11 +23,26 @@ namespace Avalonia.Utilities
         {
             //in case they are Infinities (then epsilon check does not work)
             if (value1 == value2) return true;
-            double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * double.Epsilon;
+            double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * DoubleEpsilon;
             double delta = value1 - value2;
             return (-eps < delta) && (eps > delta);
         }
 
+        /// <summary>
+        /// AreClose - Returns whether or not two floats are "close".  That is, whether or 
+        /// not they are within epsilon of each other.
+        /// </summary> 
+        /// <param name="value1"> The first float to compare. </param>
+        /// <param name="value2"> The second float to compare. </param>
+        public static bool AreClose(float value1, float value2)
+        {
+            //in case they are Infinities (then epsilon check does not work)
+            if (value1 == value2) return true;
+            float eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0f) * FloatEpsilon;
+            float delta = value1 - value2;
+            return (-eps < delta) && (eps > delta);
+        }
+
         /// <summary>
         /// LessThan - Returns whether or not the first double is less than the second double.
         /// That is, whether or not the first is strictly less than *and* not within epsilon of
@@ -35,6 +55,18 @@ namespace Avalonia.Utilities
             return (value1 < value2) && !AreClose(value1, value2);
         }
 
+        /// <summary>
+        /// LessThan - Returns whether or not the first float is less than the second float.
+        /// That is, whether or not the first is strictly less than *and* not within epsilon of
+        /// the other number.
+        /// </summary>
+        /// <param name="value1"> The first single float to compare. </param>
+        /// <param name="value2"> The second single float to compare. </param>
+        public static bool LessThan(float value1, float value2)
+        {
+            return (value1 < value2) && !AreClose(value1, value2);
+        }
+
         /// <summary>
         /// GreaterThan - Returns whether or not the first double is greater than the second double.
         /// That is, whether or not the first is strictly greater than *and* not within epsilon of
@@ -47,6 +79,18 @@ namespace Avalonia.Utilities
             return (value1 > value2) && !AreClose(value1, value2);
         }
 
+        /// <summary>
+        /// GreaterThan - Returns whether or not the first float is greater than the second float.
+        /// That is, whether or not the first is strictly greater than *and* not within epsilon of
+        /// the other number.
+        /// </summary>
+        /// <param name="value1"> The first float to compare. </param>
+        /// <param name="value2"> The second float to compare. </param>
+        public static bool GreaterThan(float value1, float value2)
+        {
+            return (value1 > value2) && !AreClose(value1, value2);
+        }
+
         /// <summary>
         /// LessThanOrClose - Returns whether or not the first double is less than or close to
         /// the second double.  That is, whether or not the first is strictly less than or within
@@ -59,6 +103,18 @@ namespace Avalonia.Utilities
             return (value1 < value2) || AreClose(value1, value2);
         }
 
+        /// <summary>
+        /// LessThanOrClose - Returns whether or not the first float is less than or close to
+        /// the second float.  That is, whether or not the first is strictly less than or within
+        /// epsilon of the other number.
+        /// </summary>
+        /// <param name="value1"> The first float to compare. </param>
+        /// <param name="value2"> The second float to compare. </param>
+        public static bool LessThanOrClose(float value1, float value2)
+        {
+            return (value1 < value2) || AreClose(value1, value2);
+        }
+
         /// <summary>
         /// GreaterThanOrClose - Returns whether or not the first double is greater than or close to
         /// the second double.  That is, whether or not the first is strictly greater than or within
@@ -71,6 +127,18 @@ namespace Avalonia.Utilities
             return (value1 > value2) || AreClose(value1, value2);
         }
 
+        /// <summary>
+        /// GreaterThanOrClose - Returns whether or not the first float is greater than or close to
+        /// the second float.  That is, whether or not the first is strictly greater than or within
+        /// epsilon of the other number.
+        /// </summary>
+        /// <param name="value1"> The first float to compare. </param>
+        /// <param name="value2"> The second float to compare. </param>
+        public static bool GreaterThanOrClose(float value1, float value2)
+        {
+            return (value1 > value2) || AreClose(value1, value2);
+        }
+
         /// <summary>
         /// IsOne - Returns whether or not the double is "close" to 1.  Same as AreClose(double, 1),
         /// but this is faster.
@@ -78,7 +146,17 @@ namespace Avalonia.Utilities
         /// <param name="value"> The double to compare to 1. </param>
         public static bool IsOne(double value)
         {
-            return Math.Abs(value - 1.0) < 10.0 * double.Epsilon;
+            return Math.Abs(value - 1.0) < 10.0 * DoubleEpsilon;
+        }
+
+        /// <summary>
+        /// IsOne - Returns whether or not the float is "close" to 1.  Same as AreClose(float, 1),
+        /// but this is faster.
+        /// </summary>
+        /// <param name="value"> The float to compare to 1. </param>
+        public static bool IsOne(float value)
+        {
+            return Math.Abs(value - 1.0f) < 10.0f * FloatEpsilon;
         }
 
         /// <summary>
@@ -88,7 +166,17 @@ namespace Avalonia.Utilities
         /// <param name="value"> The double to compare to 0. </param>
         public static bool IsZero(double value)
         {
-            return Math.Abs(value) < 10.0 * double.Epsilon;
+            return Math.Abs(value) < 10.0 * DoubleEpsilon;
+        }
+
+        /// <summary>
+        /// IsZero - Returns whether or not the float is "close" to 0.  Same as AreClose(float, 0),
+        /// but this is faster.
+        /// </summary>
+        /// <param name="value"> The float to compare to 0. </param>
+        public static bool IsZero(float value)
+        {
+            return Math.Abs(value) < 10.0f * FloatEpsilon;
         }
 
         /// <summary>

+ 7 - 28
src/Avalonia.Controls/Grid.cs

@@ -1228,7 +1228,7 @@ namespace Avalonia.Controls
             Debug.Assert(1 < count && 0 <= start && (start + count) <= definitions.Count);
 
             //  avoid processing when asked to distribute "0"
-            if (!_IsZero(requestedSize))
+            if (!MathUtilities.IsZero(requestedSize))
             {
                 DefinitionBase[] tempDefinitions = TempDefinitions; //  temp array used to remember definitions for sorting
                 int end = start + count;
@@ -1306,7 +1306,7 @@ namespace Avalonia.Controls
                         }
 
                         //  sanity check: requested size must all be distributed
-                        Debug.Assert(_IsZero(sizeToDistribute));
+                        Debug.Assert(MathUtilities.IsZero(sizeToDistribute));
                     }
                     else if (requestedSize <= rangeMaxSize)
                     {
@@ -1346,7 +1346,7 @@ namespace Avalonia.Controls
                         }
 
                         //  sanity check: requested size must all be distributed
-                        Debug.Assert(_IsZero(sizeToDistribute));
+                        Debug.Assert(MathUtilities.IsZero(sizeToDistribute));
                     }
                     else
                     {
@@ -1358,7 +1358,7 @@ namespace Avalonia.Controls
                         double equalSize = requestedSize / count;
 
                         if (equalSize < maxMaxSize
-                            && !_AreClose(equalSize, maxMaxSize))
+                            && !MathUtilities.AreClose(equalSize, maxMaxSize))
                         {
                             //  equi-size is less than maximum of maxSizes.
                             //  in this case distribute so that smaller definitions grow faster than
@@ -2151,7 +2151,7 @@ namespace Avalonia.Controls
                 // and precision of floating-point computation.  (However, the resulting
                 // display is subject to anti-aliasing problems.   TANSTAAFL.)
 
-                if (!_AreClose(roundedTakenSize, finalSize))
+                if (!MathUtilities.AreClose(roundedTakenSize, finalSize))
                 {
                     // Compute deltas
                     for (int i = 0; i < definitions.Count; ++i)
@@ -2168,7 +2168,7 @@ namespace Avalonia.Controls
                     if (roundedTakenSize > finalSize)
                     {
                         int i = definitions.Count - 1;
-                        while ((adjustedSize > finalSize && !_AreClose(adjustedSize, finalSize)) && i >= 0)
+                        while ((adjustedSize > finalSize && !MathUtilities.AreClose(adjustedSize, finalSize)) && i >= 0)
                         {
                             DefinitionBase definition = definitions[definitionIndices[i]];
                             double final = definition.SizeCache - dpiIncrement;
@@ -2184,7 +2184,7 @@ namespace Avalonia.Controls
                     else if (roundedTakenSize < finalSize)
                     {
                         int i = 0;
-                        while ((adjustedSize < finalSize && !_AreClose(adjustedSize, finalSize)) && i < definitions.Count)
+                        while ((adjustedSize < finalSize && !MathUtilities.AreClose(adjustedSize, finalSize)) && i < definitions.Count)
                         {
                             DefinitionBase definition = definitions[definitionIndices[i]];
                             double final = definition.SizeCache + dpiIncrement;
@@ -2595,27 +2595,6 @@ namespace Avalonia.Controls
             set { SetFlags(value, Flags.HasGroup3CellsInAutoRows); }
         }
 
-        /// <summary>
-        /// fp version of <c>d == 0</c>.
-        /// </summary>
-        /// <param name="d">Value to check.</param>
-        /// <returns><c>true</c> if d == 0.</returns>
-        private static bool _IsZero(double d)
-        {
-            return (Math.Abs(d) < double.Epsilon);
-        }
-
-        /// <summary>
-        /// fp version of <c>d1 == d2</c>
-        /// </summary>
-        /// <param name="d1">First value to compare</param>
-        /// <param name="d2">Second value to compare</param>
-        /// <returns><c>true</c> if d1 == d2</returns>
-        private static bool _AreClose(double d1, double d2)
-        {
-            return (Math.Abs(d1 - d2) < double.Epsilon);
-        }
-
         /// <summary>
         /// Returns reference to extended data bag.
         /// </summary>

+ 2 - 1
src/Avalonia.Controls/Slider.cs

@@ -193,7 +193,8 @@ namespace Avalonia.Controls
             var orient = Orientation == Orientation.Horizontal;
 
             var pointDen = orient ? _track.Bounds.Width : _track.Bounds.Height;
-            pointDen += double.Epsilon; // Just add epsilon to avoid divide by zero exceptions.
+            // Just add epsilon to avoid NaN in case 0/0
+            pointDen += double.Epsilon;
 
             var pointNum = orient ? x.Position.X : x.Position.Y;
             var logicalPos = MathUtilities.Clamp(pointNum / pointDen, 0.0d, 1.0d);

+ 2 - 1
src/Avalonia.Controls/Utils/BorderRenderHelper.cs

@@ -1,6 +1,7 @@
 using System;
 using Avalonia.Media;
 using Avalonia.Platform;
+using Avalonia.Utilities;
 
 namespace Avalonia.Controls.Utils
 {
@@ -119,7 +120,7 @@ namespace Avalonia.Controls.Utils
                 }
 
                 var rect = new Rect(_size);
-                if (Math.Abs(borderThickness) > double.Epsilon)
+                if (!MathUtilities.IsZero(borderThickness))
                     rect = rect.Deflate(borderThickness * 0.5);
                 var rrect = new RoundedRect(rect, _cornerRadius.TopLeft, _cornerRadius.TopRight,
                     _cornerRadius.BottomRight, _cornerRadius.BottomLeft);

+ 3 - 2
src/Avalonia.Visuals/Media/DrawingContext.cs

@@ -4,6 +4,7 @@ using Avalonia.Media.Imaging;
 using Avalonia.Platform;
 using Avalonia.Rendering.SceneGraph;
 using Avalonia.Threading;
+using Avalonia.Utilities;
 using Avalonia.Visuals.Media.Imaging;
 
 namespace Avalonia.Media
@@ -154,12 +155,12 @@ namespace Avalonia.Media
                 return;
             }
 
-            if (Math.Abs(radiusX) > double.Epsilon)
+            if (!MathUtilities.IsZero(radiusX))
             {
                 radiusX = Math.Min(radiusX, rect.Width / 2);
             }
 
-            if (Math.Abs(radiusY) > double.Epsilon)
+            if (!MathUtilities.IsZero(radiusY))
             {
                 radiusY = Math.Min(radiusY, rect.Height / 2);
             }

+ 2 - 1
src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs

@@ -4,6 +4,7 @@ using System.Linq;
 using Avalonia.Media.Immutable;
 using Avalonia.Media.TextFormatting.Unicode;
 using Avalonia.Platform;
+using Avalonia.Utilities;
 using Avalonia.Utility;
 
 namespace Avalonia.Media.TextFormatting
@@ -184,7 +185,7 @@ namespace Avalonia.Media.TextFormatting
         /// </summary>
         private void UpdateLayout()
         {
-            if (_text.IsEmpty || Math.Abs(MaxWidth) < double.Epsilon || Math.Abs(MaxHeight) < double.Epsilon)
+            if (_text.IsEmpty || MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
             {
                 var textLine = CreateEmptyTextLine(0);
 

+ 1 - 1
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@@ -236,7 +236,7 @@ namespace Avalonia.Direct2D1.Media
                 Math.Max(rrect.RadiiTopRight.X, Math.Max(rrect.RadiiBottomRight.X, rrect.RadiiBottomLeft.X)));
             var radiusY = Math.Max(rrect.RadiiTopLeft.Y,
                 Math.Max(rrect.RadiiTopRight.Y, Math.Max(rrect.RadiiBottomRight.Y, rrect.RadiiBottomLeft.Y)));
-            var isRounded = Math.Abs(radiusX) > double.Epsilon || Math.Abs(radiusY) > double.Epsilon;
+            var isRounded = !MathUtilities.IsZero(radiusX) || !MathUtilities.IsZero(radiusY);
 
             if (brush != null)
             {

+ 119 - 0
tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs

@@ -0,0 +1,119 @@
+using System;
+using Avalonia.Utilities;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests.Utilities
+{
+    public class MathUtilitiesTests
+    {
+        private const double AnyValue = 42.42;
+        private readonly double _calculatedAnyValue;
+        private readonly double _one;
+        private readonly double _zero;
+
+        public MathUtilitiesTests()
+        {
+            _calculatedAnyValue = 0.0;
+            _one = 0.0;
+            _zero = 1.0;
+
+            const int N = 10;
+            var dxAny = AnyValue / N;
+            var dxOne = 1.0 / N;
+            var dxZero = _zero / N;
+
+            for (var i = 0; i < N; ++i)
+            {
+                _calculatedAnyValue += dxAny;
+                _one += dxOne;
+                _zero -= dxZero;
+            }
+        }
+
+        [Fact]
+        public void Two_Equivalent_Double_Values_Are_Close()
+        {
+            var actual = MathUtilities.AreClose(AnyValue, _calculatedAnyValue);
+
+            Assert.True(actual);
+            Assert.Equal(AnyValue, Math.Round(_calculatedAnyValue, 14));
+        }
+
+        [Fact]
+        public void Two_Equivalent_Single_Values_Are_Close()
+        {
+            var expectedValue = (float)AnyValue;
+            var actualValue = (float)_calculatedAnyValue;
+            
+            var actual = MathUtilities.AreClose(expectedValue, actualValue);
+
+            Assert.True(actual);
+            Assert.Equal((float) Math.Round(expectedValue, 5), (float) Math.Round(actualValue, 4));
+        }
+
+        [Fact]
+        public void Calculated_Double_One_Is_One()
+        {
+            var actual = MathUtilities.IsOne(_one);
+
+            Assert.True(actual);
+            Assert.Equal(1.0, Math.Round(_one, 15));
+        }
+
+        [Fact]
+        public void Calculated_Single_One_Is_One()
+        {
+            var actualValue = (float)_one;
+            
+            var actual = MathUtilities.IsOne(actualValue);
+
+            Assert.True(actual);
+            Assert.Equal(1.0f, (float) Math.Round(actualValue, 7));
+        }
+
+        [Fact]
+        public void Calculated_Double_Zero_Is_Zero()
+        {
+            var actual = MathUtilities.IsZero(_zero);
+
+            Assert.True(actual);
+            Assert.Equal(0.0, Math.Round(_zero, 15));
+        }
+
+        [Fact]
+        public void Calculated_Single_Zero_Is_Zero()
+        {
+            var actualValue = (float)_zero;
+
+            var actual = MathUtilities.IsZero(actualValue);
+
+            Assert.True(actual);
+            Assert.Equal(0.0f, (float) Math.Round(actualValue, 7));
+        }
+
+        [Fact]
+        public void Clamp_Input_NaN_Return_NaN()
+        {
+            var clamp = MathUtilities.Clamp(double.NaN, 0.0, 1.0);
+            Assert.True(double.IsNaN(clamp));
+        }
+
+        [Fact]
+        public void Clamp_Input_NegativeInfinity_Return_Min()
+        {
+            const double min = 0.0;
+            const double max = 1.0;
+            var actual = MathUtilities.Clamp(double.NegativeInfinity, min, max);
+            Assert.Equal(min, actual);
+        }
+
+        [Fact]
+        public void Clamp_Input_PositiveInfinity_Return_Max()
+        {
+            const double min = 0.0;
+            const double max = 1.0;
+            var actual = MathUtilities.Clamp(double.PositiveInfinity, min, max);
+            Assert.Equal(max, actual);
+        }
+    }
+}