Skip to content

Commit

Permalink
Merge pull request #4096 from rstm-sf/bugfix/double_precision
Browse files Browse the repository at this point in the history
Fix machine epsilon for double
  • Loading branch information
MarchingCube authored Jun 13, 2020
2 parents 4e0dabb + e35e703 commit adb401a
Show file tree
Hide file tree
Showing 9 changed files with 228 additions and 38 deletions.
2 changes: 1 addition & 1 deletion src/Avalonia.Base/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Data.Converters")]
[assembly: InternalsVisibleTo("Avalonia.Base.UnitTests")]
[assembly: InternalsVisibleTo("Avalonia.UnitTests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
94 changes: 91 additions & 3 deletions src/Avalonia.Base/Utilities/MathUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -18,11 +23,26 @@ public static bool AreClose(double value1, double value2)
{
//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
Expand All @@ -35,6 +55,18 @@ public static bool LessThan(double value1, double value2)
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
Expand All @@ -47,6 +79,18 @@ public static bool GreaterThan(double value1, double value2)
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
Expand All @@ -59,6 +103,18 @@ public static bool LessThanOrClose(double value1, double value2)
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
Expand All @@ -71,14 +127,36 @@ public static bool GreaterThanOrClose(double value1, double value2)
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.
/// </summary>
/// <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>
Expand All @@ -88,7 +166,17 @@ public static bool IsOne(double value)
/// <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>
Expand Down
35 changes: 7 additions & 28 deletions src/Avalonia.Controls/Grid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1228,7 +1228,7 @@ private void EnsureMinSizeInDefinitionRange(
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;
Expand Down Expand Up @@ -1306,7 +1306,7 @@ private void EnsureMinSizeInDefinitionRange(
}

// sanity check: requested size must all be distributed
Debug.Assert(_IsZero(sizeToDistribute));
Debug.Assert(MathUtilities.IsZero(sizeToDistribute));
}
else if (requestedSize <= rangeMaxSize)
{
Expand Down Expand Up @@ -1346,7 +1346,7 @@ private void EnsureMinSizeInDefinitionRange(
}

// sanity check: requested size must all be distributed
Debug.Assert(_IsZero(sizeToDistribute));
Debug.Assert(MathUtilities.IsZero(sizeToDistribute));
}
else
{
Expand All @@ -1358,7 +1358,7 @@ private void EnsureMinSizeInDefinitionRange(
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
Expand Down Expand Up @@ -2151,7 +2151,7 @@ private void SetFinalSizeMaxDiscrepancy(
// 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)
Expand All @@ -2168,7 +2168,7 @@ private void SetFinalSizeMaxDiscrepancy(
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;
Expand All @@ -2184,7 +2184,7 @@ private void SetFinalSizeMaxDiscrepancy(
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;
Expand Down Expand Up @@ -2595,27 +2595,6 @@ private bool HasGroup3CellsInAutoRows
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>
Expand Down
3 changes: 2 additions & 1 deletion src/Avalonia.Controls/Slider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,8 @@ private void MoveToPoint(PointerPoint x)
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);
Expand Down
3 changes: 2 additions & 1 deletion src/Avalonia.Controls/Utils/BorderRenderHelper.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Utilities;

namespace Avalonia.Controls.Utils
{
Expand Down Expand Up @@ -119,7 +120,7 @@ void RenderCore(DrawingContext context, IBrush background, IBrush borderBrush, B
}

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);
Expand Down
5 changes: 3 additions & 2 deletions src/Avalonia.Visuals/Media/DrawingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Threading;
using Avalonia.Utilities;
using Avalonia.Visuals.Media.Imaging;

namespace Avalonia.Media
Expand Down Expand Up @@ -154,12 +155,12 @@ public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0,
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);
}
Expand Down
3 changes: 2 additions & 1 deletion src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Avalonia.Media.Immutable;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Utilities;
using Avalonia.Utility;

namespace Avalonia.Media.TextFormatting
Expand Down Expand Up @@ -184,7 +185,7 @@ private TextLine CreateEmptyTextLine(int startingIndex)
/// </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);

Expand Down
2 changes: 1 addition & 1 deletion src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rrect, BoxShadows
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)
{
Expand Down
Loading

0 comments on commit adb401a

Please sign in to comment.