using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; namespace Luneraria.Core.Tests { /// /// Float diagnosis for a runtime (docs/port/U1.5-UNITY.md): evaluates the core's own Vec2 and /// GdMath operations on the inputs of tools/godot-golden/float_probe.gd and compares the raw /// bits with Godot's results (Golden/float_probe.json). A second part classifies how the /// runtime evaluates float32 expressions: strictly per operation, with double-precision /// intermediates (allowed by ECMA-335 I.12.1.3) or with fused multiply-add. /// public static class FloatProbe { public sealed class Result { public int Rows; public readonly OrderedMap Identical = new OrderedMap(); public readonly OrderedMap WorstUlps = new OrderedMap(); public readonly OrderedMap Evaluation = new OrderedMap(); public int EvaluationSamples; public int Mismatches { get { int total = 0; foreach (KeyValuePair pair in Identical) total += Rows - pair.Value; return total; } } public string ToJson(string runtime, string label) { var text = new StringBuilder(); text.Append("{\n"); text.AppendFormat(" \"runtime\": \"{0}\",\n \"label\": \"{1}\",\n \"rows\": {2},\n", runtime, label, Rows); text.Append(" \"identical_to_godot\": {"); bool first = true; foreach (KeyValuePair pair in Identical) { text.AppendFormat(CultureInfo.InvariantCulture, "{0}\n \"{1}\": {2}", first ? "" : ",", pair.Key, pair.Value); first = false; } text.Append("\n },\n \"worst_ulps\": {"); first = true; foreach (KeyValuePair pair in WorstUlps) { text.AppendFormat(CultureInfo.InvariantCulture, "{0}\n \"{1}\": {2}", first ? "" : ",", pair.Key, pair.Value); first = false; } text.AppendFormat("\n }},\n \"evaluation_samples\": {0},\n \"evaluation_matches\": {{", EvaluationSamples); first = true; foreach (KeyValuePair pair in Evaluation) { text.AppendFormat(CultureInfo.InvariantCulture, "{0}\n \"{1}\": {2}", first ? "" : ",", pair.Key, pair.Value); first = false; } text.Append("\n }\n}\n"); return text.ToString(); } } public static Result Run(string goldenDirectory) { var result = new Result(); var probe = MiniJson.Obj(MiniJson.Parse(File.ReadAllText(Path.Combine(goldenDirectory, "float_probe.json")))); foreach (object item in MiniJson.Arr(probe["rows"])) { List row = MiniJson.Arr(item); float[] f = Floats(MiniJson.Str(row[0])); double[] d = Doubles(MiniJson.Str(row[1])); var a = new Vec2(f[0], f[1]); var b = new Vec2(f[2], f[3]); float w = f[4]; float s = f[5]; result.Rows++; Compare(result, "length", a.Length(), f[6]); Compare(result, "length_squared", a.LengthSquared(), f[7]); Compare(result, "distance_to", a.DistanceTo(b), f[8]); Compare(result, "distance_squared_to", a.DistanceSquaredTo(b), f[9]); Compare(result, "dot", a.Dot(b), f[10]); Vec2 n = a.Normalized(); Compare(result, "normalized", n.X, f[11], n.Y, f[12]); Vec2 l = a.Lerp(b, w); Compare(result, "lerp", l.X, f[13], l.Y, f[14]); Vec2 c = a.LimitLength(0.9f); Compare(result, "limit_length", c.X, f[15], c.Y, f[16]); Compare(result, "multiply", (a * s).X, f[17]); Compare(result, "divide", (a / s).Y, f[18]); Compare(result, "sub_normalized", (a - b).Normalized().X, f[19]); double angle = GdMath.Atan2(a.X, a.Y); if (!result.Identical.ContainsKey("atan2")) result.Identical["atan2"] = 0; if (BitConverter.DoubleToInt64Bits(angle) == BitConverter.DoubleToInt64Bits(d[0])) result.Identical["atan2"] += 1; Classify(result, f); } return result; } static void Compare(Result result, string name, float actual, float expected) { Compare(result, name, actual, expected, 0f, 0f); } static void Compare(Result result, string name, float actualX, float expectedX, float actualY, float expectedY) { if (!result.Identical.ContainsKey(name)) { result.Identical[name] = 0; result.WorstUlps[name] = 0.0; } double ulps = Math.Max(Ulps(actualX, expectedX), Ulps(actualY, expectedY)); if (ulps == 0.0) result.Identical[name] += 1; if (ulps > result.WorstUlps[name]) result.WorstUlps[name] = ulps; } /// Distance in units of the last place (float32). public static double Ulps(float a, float b) { if (BitConverter.ToInt32(BitConverter.GetBytes(a), 0) == BitConverter.ToInt32(BitConverter.GetBytes(b), 0)) return 0.0; if (float.IsNaN(a) || float.IsNaN(b)) return double.PositiveInfinity; long ia = Ordered(a); long ib = Ordered(b); return Math.Abs(ia - ib); } static long Ordered(float value) { int bits = BitConverter.ToInt32(BitConverter.GetBytes(value), 0); return bits < 0 ? (long)int.MinValue - bits : bits; } /// /// How does this runtime evaluate `x*x + y*y` and `a + (b - a) * w` with float32 operands? /// The candidates are computed with explicit conversions, which ECMA-335 guarantees to /// narrow: per-operation float32, double intermediates rounded once, fused multiply-add. /// static void Classify(Result result, float[] f) { float x = f[0]; float y = f[1]; float a = f[0]; float b = f[2]; float w = f[4]; float natural = SumOfSquares(x, y); float strict = (float)((float)(x * x) + (float)(y * y)); float wide = (float)((double)x * x + (double)y * y); float fused = (float)((double)x * x + (double)(float)(y * y)); float naturalLerp = LerpNatural(a, b, w); float strictLerp = (float)(a + (float)((float)(b - a) * w)); float wideLerp = (float)(a + ((double)b - a) * w); result.EvaluationSamples++; Count(result, "sum_of_squares_strict_float32", natural == strict); Count(result, "sum_of_squares_double_intermediate", natural == wide); Count(result, "sum_of_squares_fused", natural == fused); Count(result, "sum_of_squares_candidates_differ", strict != wide); Count(result, "lerp_strict_float32", naturalLerp == strictLerp); Count(result, "lerp_double_intermediate", naturalLerp == wideLerp); Count(result, "lerp_candidates_differ", strictLerp != wideLerp); } static void Count(Result result, string key, bool hit) { if (!result.Evaluation.ContainsKey(key)) result.Evaluation[key] = 0; if (hit) result.Evaluation[key] += 1; } // Kept out of line so the expressions are compiled the way the core's own code is. [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] static float SumOfSquares(float x, float y) { return x * x + y * y; } [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] static float LerpNatural(float a, float b, float w) { return a + (b - a) * w; } static float[] Floats(string hex) { byte[] bytes = Bytes(hex); var values = new float[bytes.Length / 4]; for (int i = 0; i < values.Length; i++) values[i] = BitConverter.ToSingle(bytes, i * 4); return values; } static double[] Doubles(string hex) { byte[] bytes = Bytes(hex); var values = new double[bytes.Length / 8]; for (int i = 0; i < values.Length; i++) values[i] = BitConverter.ToDouble(bytes, i * 8); return values; } static byte[] Bytes(string hex) { var bytes = new byte[hex.Length / 2]; for (int i = 0; i < bytes.Length; i++) bytes[i] = byte.Parse(hex.Substring(i * 2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture); return bytes; } } }