- Projekt mit Unity 6.3 LTS (6000.3.20f1, Personal) geöffnet: ProjectSettings, .meta-Dateien, URP-Global-Settings, packages-lock.json. - Manifest auf die benötigten Pakete zurückgeführt; Unity hatte beim ersten Öffnen Vorlagenpakete ergänzt (Purchasing, Analytics, Collab, XR, 2D, Navigation). - Test Runner: Die U1-Suiten laufen unverändert als EditMode-Tests; mit LUNE_TEST_REPORT schreiben .NET und Unity dasselbe JSON-Protokoll (SuiteReport). - Golden-Vergleich zusätzlich je Fachbereich (Gold, HP, Positionen, Türme, Ancients, Wellen, Helden, Boss, Gewinner, ...). Nur Bericht; Testwerte und Prüfungsanzahl unverändert. - Diagnose: FloatProbe (Kern-Arithmetik gegen Godot-Bits und Auswertungsart der Laufzeit), DivergenceTrace und tools/port/trace_diff.py (erster abweichender Schritt), CoreBenchmark für .NET und Editor, CoreBatch als -executeMethod-Einstieg. - .NET-Harness baut nach tools/out/, weil Unity Temp/ beim Beenden löscht. - .gitignore ignoriert nur noch die csproj/sln im Projektwurzelverzeichnis; die Projektdateien des .NET-Harness fehlten deshalb in U1 und kommen jetzt dazu. Stand dieses Commits im Unity-Editor (Mono): 4 von 5 Suiten grün; golden 152/157, weil Mono float32-Zwischenwerte doppelt genau rechnet (Teil 2/2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
213 lines
9.4 KiB
C#
213 lines
9.4 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Text;
|
|
|
|
namespace Luneraria.Core.Tests
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public static class FloatProbe
|
|
{
|
|
public sealed class Result
|
|
{
|
|
public int Rows;
|
|
public readonly OrderedMap<string, int> Identical = new OrderedMap<string, int>();
|
|
public readonly OrderedMap<string, double> WorstUlps = new OrderedMap<string, double>();
|
|
public readonly OrderedMap<string, int> Evaluation = new OrderedMap<string, int>();
|
|
public int EvaluationSamples;
|
|
|
|
public int Mismatches
|
|
{
|
|
get
|
|
{
|
|
int total = 0;
|
|
foreach (KeyValuePair<string, int> 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<string, int> 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<string, double> 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<string, int> 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<object> 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;
|
|
}
|
|
|
|
/// <summary>Distance in units of the last place (float32).</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
}
|