The code that operates on matrices produces different results in .Net Core and .Net Framework.
This issue raises tricky bugs in visual components like charts and maps that use heavy matrix calculations.
OS Version: Microsoft Windows [Version 10.0.18362.535]
.Net Framework Version: Microsoft (R) .NET CLR Version Tool Version 4.8.3928.0
.Net Core Version: 5.0.100-rc.2.20479.15
Here is the minimal synthetic code that demonstrates the diference:
float rt2_div2 = (float)(Math.Sqrt(2.0) / 2.0);
Matrix matrix = new Matrix(rt2_div2, -rt2_div2, rt2_div2, rt2_div2, -94.97475f, 370.7107f);
matrix.RotateAt(75f, new PointF(229f, 498f));
// to demostrate the real binary difference rather than formatting difference due to
// https://devblogs.microsoft.com/dotnet/floating-point-parsing-and-formatting-improvements-in-net-core-3-0/
for(int i = 0; i < matrix.Elements.Length; i++) {
Debug.WriteLine(
matrix.Elements[i].ToString().PadRight(12) +
BitConverter.ToString(BitConverter.GetBytes(matrix.Elements[i]))
);
}
Here is the output:
// .Net FW .Net 5
// 0.8660253 (D6-B3-5D-3F) 0.8660253 (D6-B3-5D-3F)
// 0.49999997 (FF-FF-FF-3E) 0.49999994 (FE-FF-FF-3E)
//-0.5 (00-00-00-BF) -0.49999997 (FF-FF-FF-BE)
// 0.8660253 (D6-B3-5D-3F) 0.8660254 (D7-B3-5D-3F)
// 469.772034 (D2-E2-EA-43) 469.772 (D1-E2-EA-43)
// 15.1417561 (A2-44-72-41) 15.1416626 (40-44-72-41)
The errors are growing cumulatively in a real application.
Is this difference documented somewhere?
Why do you think it is a Windows Forms issue?
Why do you think it is a Windows Forms issue?
That's my bad, - it is the System.Drawing.Drawing2D area which is not in WinForms, of course.
Digging deeper, I have found that the result depends on the compiler target platform (x86\x64) rather than on the framework version.
You can close this issue. I'm very sorry for the concern I've caused.
No worries. We'll get this transferred over.
Tagging subscribers to this area: @tannergooding, @pgovind, @jeffhandley
See info in area-owners.md if you want to be subscribed.
Tagging subscribers to this area: @safern, @tannergooding, @jeffhandley
See info in area-owners.md if you want to be subscribed.
@DmitryGaravsky where you able to compare the results vs a .NET Core 3.1 app to see if this was a regression from 3.1 to 5.0?
Here's what I get:
64 bit
C:\Program Files\dotnet\shared\Microsoft.NETCore.App\5.0.0-rc.2.20462.5\System.Private.CoreLib.dll
0.8660253 D6-B3-5D-3F
0.49999994 FE-FF-FF-3E
-0.49999997 FF-FF-FF-BE
0.8660254 D7-B3-5D-3F
469.772 D1-E2-EA-43
15.141663 40-44-72-41
C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.1.7\System.Private.CoreLib.dll
0.8660253 D6-B3-5D-3F
0.49999994 FE-FF-FF-3E
-0.49999997 FF-FF-FF-BE
0.8660254 D7-B3-5D-3F
469.772 D1-E2-EA-43
15.141663 40-44-72-41
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\mscorlib.dll
0.8660253 D6-B3-5D-3F
0.4999999 FE-FF-FF-3E
-0.5 FF-FF-FF-BE
0.8660254 D7-B3-5D-3F
469.772 D1-E2-EA-43
15.14166 40-44-72-41
32 bit
```
C:\Program Files (x86)\dotnet\shared\Microsoft.NETCore.App\5.0.0\System.Private.CoreLib.dll
0.8660253 D6-B3-5D-3F
0.49999997 FF-FF-FF-3E
-0.5 00-00-00-BF
0.8660253 D6-B3-5D-3F
469.77203 D2-E2-EA-43
15.141756 A2-44-72-41
C:\Program Files (x86)\dotnet\shared\Microsoft.NETCore.App\3.1.7\System.Private.CoreLib.dll
0.8660253 D6-B3-5D-3F
0.49999997 FF-FF-FF-3E
-0.5 00-00-00-BF
0.8660253 D6-B3-5D-3F
469.77203 D2-E2-EA-43
15.141756 A2-44-72-41
C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscorlib.dll
0.8660253 D6-B3-5D-3F
0.5 FF-FF-FF-3E
-0.5 00-00-00-BF
0.8660253 D6-B3-5D-3F
469.772 D2-E2-EA-43
15.14176 A2-44-72-41
Thanks @danmosemsft, I will then leave it as 6.0.0 as it doesn't seem like a regression from 3.1 to 5.0.
@DmitryGaravsky were you able to compare the results vs a .NET Core 3.1 app to see if this was a regression from 3.1 to 5.0?
As I have already mentioned (and as @danmosemsft confirmed), this is not a regression. It only depends on the compiler target platform (x86 vs x64) that have different default settings under .Net Core and .Net Framework.
Thanks for participating, guys, and sorry for the inconvenience.
This isn't something we can readily fix in .NET as System.Drawing is effectively a thin wrapper over the GDI+ implementation in native.
The underlying native implementation uses the C runtime math implementations of things like sin and cos for performing the rotation and so the result will differ (even in native) based on that.
There can be differences for all valid combinations between The architecture (x86, x64, Arm32, Arm64, etc) and the OS (windows, linux, macos, freebsd, etc).
Our own System.Math and System.MathF types have the same limitations as they are also just thin wrappers over the underlying C runtime implementations.
@tannergooding but why the difference in between full framework and .NET Core? They are using the same GDI+.
Most apps on full framework run as 32-bit by default (and will have prefer 32-bit checked in the project settings), where-as you are more likely to run as 64-bit by default on .NET Core.
You can see this above by the folder being C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscorlib.dll, rather than C:\Windows\Microsoft.NET\Framework64\v4.0.30319\mscorlib.dll, but you can also change the program to something like:
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
class Program
{
static void Main(string[] args)
{
Console.WriteLine($"Corelib: {typeof(object).Assembly.Location}");
Console.WriteLine($"Environment.Is64BitProcess: {Environment.Is64BitProcess}");
float rt2_div2 = (float)(Math.Sqrt(2.0) / 2.0);
Matrix matrix = new Matrix(rt2_div2, -rt2_div2, rt2_div2, rt2_div2, -94.97475f, 370.7107f);
matrix.RotateAt(75f, new PointF(229f, 498f));
// to demostrate the real binary difference rather than formatting difference due to
// https://devblogs.microsoft.com/dotnet/floating-point-parsing-and-formatting-improvements-in-net-core-3-0/
for (int i = 0; i < matrix.Elements.Length; i++)
{
Console.WriteLine($"{matrix.Elements[i],12} {BitConverter.DoubleToInt64Bits(matrix.Elements[i]):X16}");
}
}
}
and you'll get:
.NET 5.0 64-bit
Corelib: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\5.0.0-rc.2.20475.5\System.Private.CoreLib.dll
Environment.Is64BitProcess: True
0.8660253 3FEBB67AC0000000
0.49999994 3FDFFFFFC0000000
-0.49999997 BFDFFFFFE0000000
0.8660254 3FEBB67AE0000000
469.772 407D5C5A20000000
15.141663 402E488800000000
.NET 5.0 32-bit
Corelib: C:\Program Files (x86)\dotnet\shared\Microsoft.NETCore.App\5.0.0-rc.2.20475.5\System.Private.CoreLib.dll
Environment.Is64BitProcess: False
0.8660253 3FEBB67AC0000000
0.49999997 3FDFFFFFE0000000
-0.5 BFE0000000000000
0.8660253 3FEBB67AC0000000
469.77203 407D5C5A40000000
15.141756 402E489440000000
.NET 4.8 64-bit
Corelib: C:\Windows\Microsoft.NET\Framework64\v4.0.30319\mscorlib.dll
Environment.Is64BitProcess: True
0.8660253 3FEBB67AC0000000
0.4999999 3FDFFFFFC0000000
-0.5 BFDFFFFFE0000000
0.8660254 3FEBB67AE0000000
469.772 407D5C5A20000000
15.14166 402E488800000000
.NET 4.8 32-bit
Corelib: C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscorlib.dll
Environment.Is64BitProcess: False
0.8660253 3FEBB67AC0000000
0.5 3FDFFFFFE0000000
-0.5 BFE0000000000000
0.8660253 3FEBB67AC0000000
469.772 407D5C5A40000000
15.14176 402E489440000000
I see... I didn't know that. Interesting. So then I think there is nothing we can do about this issue, right?
Right, we would need to use a single GDI+ implementation everywhere and have it compiled with the same libm implementation.
Thanks, @tannergooding -- closing the issue as per discussion above.