From 360176a7dab2c2e0e97845d5db2614b4513bf3f4 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 14 Sep 2026 13:57:03 +0200 Subject: [PATCH] Compile the real HLSL mirror as C# and with dxc; fix its errors HlslSourceCompatibilityTests now reads shaders/CultMath.hlsl, applies only the documented transformations (drop include guards, drop Texture2D/ SamplerState functions, file-scope static const -> const, f literal suffixes, wrap in a class), and compiles it with Roslyn against CultMath. Any compile error fails the test (probed with an undeclared call). The compiled cultmath_snoise(float3/float2) also matches math.snoise. dxc -T lib_6_3 -HV 2021 rejected the mirror: cultmath_snoise(float2) called the undeclared cultmath_simplex_mod289/permute. Renamed to the declared cultmath_snoise_* helpers. Vector const locals became plain locals (C# const is primitives only), and cultmath_step uses value >= edge like HLSL step and the C# mirror. dxc 1.9.0.5402 now compiles the file cleanly. The Unity package's Shaders copy is synced. Co-Authored-By: Claude Opus 5 --- packages/cultmath/shaders/CultMath.hlsl | 12 +- .../CultMath.Tests/CultMath.Tests.csproj | 1 + .../HlslSourceCompatibilityTests.cs | 155 +++++++++++++----- .../Shaders/CultMath.hlsl | 12 +- 4 files changed, 125 insertions(+), 55 deletions(-) diff --git a/packages/cultmath/shaders/CultMath.hlsl b/packages/cultmath/shaders/CultMath.hlsl index f3c398d6..632bb94f 100644 --- a/packages/cultmath/shaders/CultMath.hlsl +++ b/packages/cultmath/shaders/CultMath.hlsl @@ -36,7 +36,7 @@ float2 cultmath_lerp(float2 start, float2 end, float2 amount) { return start + ( float3 cultmath_lerp(float3 start, float3 end, float3 amount) { return start + (end - start) * amount; } float4 cultmath_lerp(float4 start, float4 end, float4 amount) { return start + (end - start) * amount; } -float cultmath_step(float edge, float value) { return value < edge ? 0.0 : 1.0; } +float cultmath_step(float edge, float value) { return value >= edge ? 1.0 : 0.0; } float2 cultmath_step(float2 edge, float2 value) { return float2(cultmath_step(edge.x, value.x), cultmath_step(edge.y, value.y)); } float3 cultmath_step(float3 edge, float3 value) { return float3(cultmath_step(edge.x, value.x), cultmath_step(edge.y, value.y), cultmath_step(edge.z, value.z)); } float4 cultmath_step(float4 edge, float4 value) { return float4(cultmath_step(edge.x, value.x), cultmath_step(edge.y, value.y), cultmath_step(edge.z, value.z), cultmath_step(edge.w, value.w)); } @@ -83,7 +83,7 @@ float4 cultmath_snoise_permute(float4 value) { return cultmath_snoise_mod289(((v // order as the C# math.snoise(float3) mirror. float cultmath_snoise(float3 value) { - const float2 c = float2(1.0 / 6.0, 1.0 / 3.0); + float2 c = float2(1.0 / 6.0, 1.0 / 3.0); float3 i = floor(value + dot(value, c.yyy)); float3 x0 = value - i + dot(i, c.xxx); float3 g = step(x0.yzx, x0.xyz); @@ -132,7 +132,7 @@ float cultmath_snoise(float3 value) float cultmath_snoise(float2 value) { - const float4 c = float4( + float4 c = float4( 0.211324865405187, 0.366025403784439, -0.577350269189626, @@ -142,9 +142,9 @@ float cultmath_snoise(float2 value) float2 i1 = x0.x > x0.y ? float2(1.0, 0.0) : float2(0.0, 1.0); float4 x12 = float4(x0.x + c.x - i1.x, x0.y + c.x - i1.y, x0.x + c.z, x0.y + c.z); - i = cultmath_simplex_mod289(i); - float3 p = cultmath_simplex_permute( - cultmath_simplex_permute(i.y + float3(0.0, i1.y, 1.0)) + i.x + float3(0.0, i1.x, 1.0)); + i = cultmath_snoise_mod289(i); + float3 p = cultmath_snoise_permute( + cultmath_snoise_permute(i.y + float3(0.0, i1.y, 1.0)) + i.x + float3(0.0, i1.x, 1.0)); float3 m = max(0.5 - float3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0); m *= m; m *= m; diff --git a/packages/cultmath/tests/CultMath.Tests/CultMath.Tests.csproj b/packages/cultmath/tests/CultMath.Tests/CultMath.Tests.csproj index 1e77eba1..d77fd05f 100644 --- a/packages/cultmath/tests/CultMath.Tests/CultMath.Tests.csproj +++ b/packages/cultmath/tests/CultMath.Tests/CultMath.Tests.csproj @@ -8,6 +8,7 @@ + diff --git a/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs index 575952c3..3c5aae94 100644 --- a/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs +++ b/packages/cultmath/tests/CultMath.Tests/HlslSourceCompatibilityTests.cs @@ -1,16 +1,111 @@ -using Xunit; +using System.Reflection; +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Xunit; using static CultMath.math; namespace CultMath.Tests; /// -/// HLSL written against CultMath compiles as C# under using static CultMath.math;. -/// The only spelling C# forces is float literal suffixes (0.5f): an unsuffixed -/// literal is a double in C#. +/// The real shader mirror, shaders/CultMath.hlsl, compiles as C# against CultMath under +/// using static CultMath.math; after exactly the source transformations listed in +/// docs/design.md (HLSL Target: Source Transformations). is +/// that list in code; if a body needs anything else, this test fails. /// public sealed class HlslSourceCompatibilityTests { - // ---- HLSL-shaped source: keep this block free of C#-only constructs. ---- + [Fact] + public void ShaderMirrorBodiesCompileAsCSharp() + { + var (assembly, errors) = CompileShaderMirror(); + Assert.True(assembly is not null, "HLSL mirror did not compile as C#:" + Environment.NewLine + string.Join(Environment.NewLine, errors)); + + var shader = assembly!.GetType("CultMathHlsl.HlslShader")!; + Assert.NotNull(ShaderFunction(shader, "cultmath_snoise", typeof(float3))); + Assert.NotNull(ShaderFunction(shader, "cultmath_snoise", typeof(float2))); + Assert.NotNull(ShaderFunction(shader, "cultmath_value_noise_bicubic", typeof(float2))); + } + + // HLSL functions carry no access modifier, so they compile as private instance methods. + private static MethodInfo? ShaderFunction(Type shader, string name, params Type[] parameters) => + shader.GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic, null, parameters, null); + + [Theory] + [InlineData(0.0f, 0.0f, 0.0f)] + [InlineData(0.25f, -0.5f, 1.75f)] + [InlineData(12.25f, -4.5f, 3.125f)] + [InlineData(-7.3f, 2.9f, 101.4f)] + public void CompiledShaderNoiseMatchesCSharpMirror(float x, float y, float z) + { + var (assembly, errors) = CompileShaderMirror(); + Assert.True(assembly is not null, string.Join(Environment.NewLine, errors)); + var shaderType = assembly!.GetType("CultMathHlsl.HlslShader")!; + var shader = Activator.CreateInstance(shaderType); + + var snoise3 = (float)ShaderFunction(shaderType, "cultmath_snoise", typeof(float3))!.Invoke(shader, new object[] { float3(x, y, z) })!; + var snoise2 = (float)ShaderFunction(shaderType, "cultmath_snoise", typeof(float2))!.Invoke(shader, new object[] { float2(x, y) })!; + + Assert.Equal(snoise(float3(x, y, z)), snoise3, precision: 6); + Assert.Equal(snoise(float2(x, y)), snoise2, precision: 6); + } + + /// The documented HLSL-to-C# transformations, and nothing else. + internal static string TransformHlslToCSharp(string hlsl) + { + // 1. Include guards (#ifndef/#define/#endif) are dropped; C# has no textual include. + var source = Regex.Replace(hlsl, @"(?m)^\s*#(ifndef|define|endif)\b.*$", string.Empty); + + // 2. Functions taking Texture2D/SamplerState resources are dropped; GPU resources have + // no CultMath analog. + source = Regex.Replace(source, @"(?ms)^\w+ \w+\([^)]*\b(Texture2D|SamplerState)\b[^)]*\)\s*\{.*?^\}[ \t]*\r?$", string.Empty); + + // 3. File-scope `static const` becomes `const` (C# consts are implicitly static). + source = Regex.Replace(source, @"(?m)^static const ", "const "); + + // 4. Floating literals gain the `f` suffix; an unsuffixed C# literal is a double. + source = Regex.Replace(source, @"(? Errors) CompileShaderMirror() + { + var hlsl = File.ReadAllText(Path.Combine(FindCultMathRoot(), "shaders", "CultMath.hlsl")); + var tree = CSharpSyntaxTree.ParseText(TransformHlslToCSharp(hlsl)); + var references = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!) + .Split(Path.PathSeparator) + .Append(typeof(math).Assembly.Location) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Select(path => MetadataReference.CreateFromFile(path)); + var compilation = CSharpCompilation.Create( + "CultMathHlslMirror", + new[] { tree }, + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + using var stream = new MemoryStream(); + var result = compilation.Emit(stream); + var errors = result.Diagnostics + .Where(diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .Select(diagnostic => diagnostic.ToString()) + .ToList(); + return (result.Success ? Assembly.Load(stream.ToArray()) : null, errors); + } + + private static string FindCultMathRoot() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "packages", "cultmath", "shaders", "CultMath.hlsl"))) + { + directory = directory.Parent; + } + + return Path.Combine(directory?.FullName ?? throw new InvalidOperationException("CultMath package root not found."), "packages", "cultmath"); + } + + // ---- Hand-written HLSL-shaped host code: swizzles, mixed constructors, matrix rows. ---- static float3 shade(float3 position, float3 normal, float2 wind, float time) { float3 p = position; @@ -18,19 +113,20 @@ public sealed class HlslSourceCompatibilityTests p.y = saturate(p.y); float3x3 basis = float3x3(float3(1, 0, 0), float3(0, 1, 0), float3(0, 0, 1)); + basis[1][1] = 1.0f; float3 n = normalize(mul(basis, normal)); float ndotl = saturate(dot(n, normalize(float3(0.3f, 1, 0.2f)))); - float3 color = lerp(float3(0.1f, 0.1f, 0.2f), float3(1, 0.9f, 0.7f), ndotl); + float4 color = float4(lerp(float3(0.1f, 0.1f, 0.2f), float3(1, 0.9f, 0.7f), ndotl), 1); if (any(p < 0.0f)) - color *= 0.5f; + color.rgb *= 0.5f; if (all(abs(n) <= 1.0f)) - color.yzx = color.yzx * float3(1, 1, 1); + color.bgr = color.bgr * float3(1, 1, 1); - color = select(p > 10.0f, float3(0), color); + color.rgb = select(p > 10.0f, float3(0), color.rgb); float2 uv = frac(p.xz * 0.25f); - color.xy = lerp(color.xy, uv, 0.1f); - return color; + color.rg = lerp(color.rg, uv, 0.1f); + return float4(color.xy, color.zw).xyz; } static float2 steer(float2 direction, float torque) @@ -40,46 +136,19 @@ public sealed class HlslSourceCompatibilityTests return normalize(turned) * pow(length(turned), 1.0f); } - static float fbm(float3 p, int octaves) - { - float sum = 0.0f; - float amp = 0.5f; - float freq = 1.0f; - for (int i = 0; i < octaves; i++) - { - sum += abs(snoise(p * freq)) * amp; - freq *= 2.0f; - amp *= 0.5f; - } - return sum; - } - // ---- end HLSL-shaped source ---- - [Fact] - public void ShaderStyleMathCompilesAndEvaluates() + public void HandWrittenShaderStyleMathEvaluates() { - var color = shade(new float3(1.0f, 0.5f, 2.0f), new float3(0.0f, 1.0f, 0.0f), new float2(1.0f, 0.0f), 0.0f); - + var color = shade(float3(1.0f, 0.5f, 2.0f), float3(0.0f, 1.0f, 0.0f), float2(1.0f, 0.0f), 0.0f); Assert.True(color.x > 0.1f && color.x <= 1.0f); Assert.True(float.IsFinite(color.y) && float.IsFinite(color.z)); - var far = shade(new float3(20.0f, 20.0f, 20.0f), new float3(0.0f, 1.0f, 0.0f), float2(0.0f), 0.0f); + var far = shade(float3(20.0f, 20.0f, 20.0f), float3(0.0f, 1.0f, 0.0f), float2(0.0f), 0.0f); Assert.Equal(0.0f, far.z); - } - - [Fact] - public void AetheriaStyleRotationAndNoiseCompile() - { - var steered = steer(new float2(1.0f, 0.0f), 0.5f); - var viaRotate = mul(new float2(1.0f, 0.0f), CultMath.float2x2.Rotate(0.5f)); + var steered = steer(float2(1.0f, 0.0f), 0.5f); + var viaRotate = mul(float2(1.0f, 0.0f), CultMath.float2x2.Rotate(0.5f)); Assert.Equal(viaRotate.x, steered.x, precision: 5); Assert.Equal(viaRotate.y, steered.y, precision: 5); - - var view = mul(float3(0, 0, 1), CultMath.float3x3.Euler(float3(float2(0.3f, -1.1f).yx, 0), RotationOrder.YXZ)); - Assert.Equal(1.0f, length(view), precision: 5); - - var noise = fbm(float3(1.5f, -2.25f, 0.75f), 4); - Assert.InRange(noise, 0.0f, 1.0f); } } diff --git a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl index f3c398d6..632bb94f 100644 --- a/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl +++ b/packages/cultmath/unity/org.gamecult.cultmath/Shaders/CultMath.hlsl @@ -36,7 +36,7 @@ float2 cultmath_lerp(float2 start, float2 end, float2 amount) { return start + ( float3 cultmath_lerp(float3 start, float3 end, float3 amount) { return start + (end - start) * amount; } float4 cultmath_lerp(float4 start, float4 end, float4 amount) { return start + (end - start) * amount; } -float cultmath_step(float edge, float value) { return value < edge ? 0.0 : 1.0; } +float cultmath_step(float edge, float value) { return value >= edge ? 1.0 : 0.0; } float2 cultmath_step(float2 edge, float2 value) { return float2(cultmath_step(edge.x, value.x), cultmath_step(edge.y, value.y)); } float3 cultmath_step(float3 edge, float3 value) { return float3(cultmath_step(edge.x, value.x), cultmath_step(edge.y, value.y), cultmath_step(edge.z, value.z)); } float4 cultmath_step(float4 edge, float4 value) { return float4(cultmath_step(edge.x, value.x), cultmath_step(edge.y, value.y), cultmath_step(edge.z, value.z), cultmath_step(edge.w, value.w)); } @@ -83,7 +83,7 @@ float4 cultmath_snoise_permute(float4 value) { return cultmath_snoise_mod289(((v // order as the C# math.snoise(float3) mirror. float cultmath_snoise(float3 value) { - const float2 c = float2(1.0 / 6.0, 1.0 / 3.0); + float2 c = float2(1.0 / 6.0, 1.0 / 3.0); float3 i = floor(value + dot(value, c.yyy)); float3 x0 = value - i + dot(i, c.xxx); float3 g = step(x0.yzx, x0.xyz); @@ -132,7 +132,7 @@ float cultmath_snoise(float3 value) float cultmath_snoise(float2 value) { - const float4 c = float4( + float4 c = float4( 0.211324865405187, 0.366025403784439, -0.577350269189626, @@ -142,9 +142,9 @@ float cultmath_snoise(float2 value) float2 i1 = x0.x > x0.y ? float2(1.0, 0.0) : float2(0.0, 1.0); float4 x12 = float4(x0.x + c.x - i1.x, x0.y + c.x - i1.y, x0.x + c.z, x0.y + c.z); - i = cultmath_simplex_mod289(i); - float3 p = cultmath_simplex_permute( - cultmath_simplex_permute(i.y + float3(0.0, i1.y, 1.0)) + i.x + float3(0.0, i1.x, 1.0)); + i = cultmath_snoise_mod289(i); + float3 p = cultmath_snoise_permute( + cultmath_snoise_permute(i.y + float3(0.0, i1.y, 1.0)) + i.x + float3(0.0, i1.x, 1.0)); float3 m = max(0.5 - float3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0); m *= m; m *= m;