cultmath: compile-hlsl.ps1 fails on warnings and on a missing shader without its path; compile-glsl.ps1 passes struct arguments

dxc runs with -WX; a ShaderPath that is not a file throws naming the parameter and entry index,
never the path; the GLSL wrapper builds a struct parameter from its declaration's field zeros.
design.md and README state the struct-argument twin rule, the span twins and the missing shader identity.
No C# change; the compiles run on Starfire after the last commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-16 03:02:13 +02:00
commit 4250dd28d5
4 changed files with 43 additions and 15 deletions

View file

@ -82,9 +82,10 @@ shader semantics.
the analytic Hessian of `snoise(float3)` on the same lattice, hash and tie order as
`snoise_grad` (a symmetric `float3x3`; GLSL `mat3`), and `integral_clamp_linear(a,
b, lo, hi, t0, t1)`, the closed-form integral over `[t0, t1]` of `clamp(a + b t,
lo, hi)`; and, in C# for now (shaders follow), the octave stages `span_fbm(path, stage,
octaves, lacunarity, gain)` and `span_ridged`, over a `SpanPath` and a `SpanStage` (`SpanStage.Identity`
is the plain fbm), which return a `SpanNoise` (`field` as `float4(gradient, value)`,
lo, hi)`; and the octave stages `span_fbm(path, stage,
octaves, lacunarity, gain)` and `span_ridged` (shader twins `cultmath_span_fbm` and `cultmath_span_ridged`,
whose callers build the `SpanStage` themselves), over a `SpanPath` and a `SpanStage` (`SpanStage.Identity`
is the plain fbm in C#), which return a `SpanNoise` (`field` as `float4(gradient, value)`,
`curvature`, `fold`) for a stretched, offset, shifted octave sum over a span. Three measured bounds describe `snoise`: `SNOISE_LIPSCHITZ` (gradient
length, `NoiseBoundTests.MeasureLipschitz`), `SNOISE_HESSIAN` (Hessian spectral
norm, `MeasureHessian`) and `SNOISE_THIRD` (third-derivative operator norm,
@ -178,7 +179,7 @@ CultMath keeps portable DXC outside git under `packages/cultmath/.tools/`:
-OutputPath E:\Projects\Odin\crates\muninn-move-tracker\artifacts\shader\MoveSphereCandidate.comp.metal
```
`compile-hlsl.ps1` compiles both `CultMath.hlsl` copies as a `lib_6_3` library with the x64 DXC.
`compile-hlsl.ps1` compiles both `CultMath.hlsl` copies as a `lib_6_3` library with the x64 DXC; a warning fails it too (`-WX`).
DXC emits Vulkan SPIR-V with `-spirv`. `-TargetEnv vulkan1.2` is exposed by the
script, but the current Windows DXC release compiled this shader successfully

View file

@ -113,8 +113,9 @@ modifying a temporary).
The HLSL side is checked too: `tools/get-dxc.ps1` fetches Microsoft's
DirectXShaderCompiler (x64), and `tools/compile-hlsl.ps1` compiles both copies of
`CultMath.hlsl` with it (`dxc -T lib_6_3 -HV 2021`); the script throws when dxc cannot
launch or exits non-zero, so a stale exit code never reads as a pass.
`CultMath.hlsl` with it (`dxc -T lib_6_3 -HV 2021 -WX`, so a warning fails like an error and the
text must compile cleanly); the script throws when a shader file is missing, when dxc cannot
launch or when it exits non-zero, so a stale exit code never reads as a pass.
The mirror test compares bit patterns (any NaN equals any NaN; -0 differs from
0). It proves the text of `CultMath.hlsl` computes the same float32 results as
@ -124,13 +125,20 @@ compiled under `using static CultMath.math;`, so its intrinsics are C# `math`
itself: the test proves the composition in the text, not the intrinsic rules,
which only `HlslSemanticsTests` pins.
Every mirror function is compared, internals included: its counterpart is the `math`
function of the same name, public or private (`snoise_mod289` and `snoise_permute` are
private), and a function with no counterpart fails by name. A struct parameter maps to the
CultMath type of the same name, and only if both have the same instance fields by name, type
and order (`SpanPath`, `SpanStage`); the argument is built from the case's scalars field by
field and copied to its C# twin by field name.
The comparison also allows exactly one struct return shape: a mirror function
whose HLSL return type is a plain struct of `float`/`floatN`/`int`/`intN`
fields is compared to its C# counterpart field by field, recursing into each
field's own components, down to bit-for-bit scalars, matching each leaf's
declaration path (field names, not just position) and declared type as well
as its value. `CultCellular` (`math.cs`, `cellular`) and `CultPhasor`
(`math.Phacelle.cs`, `phacelle`) are its consumers. A struct return with a mismatched, reordered-by-name, or
as its value. `CultCellular` (`math.cs`, `cellular`), `CultPhasor`
(`math.Phacelle.cs`, `phacelle`) and `SpanNoise` (`math.Span.cs`, `span_fbm`) are its consumers. A struct return with a mismatched, reordered-by-name, or
retyped field does not get a second comparison path or a shape of its own;
it fails the same walk that already handles a bare vector return.
@ -809,7 +817,8 @@ configuration bound. A change to the kernels must re-run the measurements.
### Span stages
`span_fbm` and `span_ridged` (`math.Span.cs`, C# only so far) are the octave stages
`span_fbm` and `span_ridged` (`math.Span.cs`; `cultmath_span_fbm` and `cultmath_span_ridged` in HLSL over
the same struct types, `cultmath_span_stage` behind both, and GLSL derived) are the octave stages
of a warp chain, `span_fbm(path, stage, octaves, lacunarity, gain)`, each returning
one `SpanNoise { float4 field; float curvature; float fold; }`. The inputs are two
structs with named public fields and no positional constructor, so the three floats
@ -831,6 +840,12 @@ Lab's stage is `1 - 2|n|` per octave: with `G` the sum of `gain^o` over the
octaves (signed, so the alternating sum for a negative gain), its value is `2 value
- G`, and its gradient, curvature and fold are twice `span_ridged`'s.
The shaders have no `SpanStage.Identity`: a shader caller builds every stage field itself (in GLSL,
`SpanStage(1.0, 1.0, vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0))` is the identity). A constant would
need a second transformation rule (C# has no const structs, GLSL wants a constructor) and a mirror
exemption, for one value; the mirror test proves instead that the shader text at a C#-built identity
returns `cultmath_fbm_grad` and `cultmath_ridged_grad`'s field bit for bit.
Bounds come from the three pins only. With `|M| = |frequency| max(1, 1/stretch)` and
`nu = f_o |M| speed` the noise-space speed, the Hessian of the noise on the span has
operator norm at most `Hl = min(SNOISE_HESSIAN, ||snoise_hessian(x_mid)||_F + e_h +

View file

@ -29,12 +29,21 @@ if ($Phacelle) {
if ($Cycles) {
$library += Get-Content -LiteralPath (Join-Path $repoRoot "shaders\CultMath.Cycles.glsl") -Raw
}
# A parameter of a struct type the library declares is passed as that struct's constructor with each
# field's zero, read from its declaration (a field may be a struct itself); every other type is its zero.
$structs = @{}
foreach ($declaration in [regex]::Matches($library, '(?s)struct (\w+)\s*\{(.*?)\};')) {
$structs[$declaration.Groups[1].Value] = @([regex]::Matches($declaration.Groups[2].Value, '(?m)^\s*(\w+)\s+\w+\s*;') | ForEach-Object { $_.Groups[1].Value })
}
function Get-Zero([string]$type) {
if ($structs.ContainsKey($type)) { return "$type($((@($structs[$type] | ForEach-Object { Get-Zero $_ })) -join ', '))" }
if ($type -eq 'int' -or $type -like 'ivec*') { return "$type(0)" }
if ($type -eq 'uint' -or $type -like 'uvec*') { return "$type(0u)" }
return "$type(0.0)"
}
$calls = foreach ($match in [regex]::Matches($library, '(?m)^\w+\s+(cultmath_\w+)\s*\(([^)]*)\)')) {
$arguments = foreach ($parameter in ($match.Groups[2].Value -split ',')) {
$type = ($parameter.Trim() -split '\s+')[0]
if ($type -eq 'int' -or $type -like 'ivec*') { "$type(0)" }
elseif ($type -eq 'uint' -or $type -like 'uvec*') { "$type(0u)" }
else { "$type(0.0)" }
Get-Zero (($parameter.Trim() -split '\s+')[0])
}
" $($match.Groups[1].Value)($($arguments -join ', '));"
}

View file

@ -29,11 +29,14 @@ New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null
$index = 0
foreach ($shader in $ShaderPath) {
$index++
if (-not (Test-Path -LiteralPath $shader -PathType Leaf)) {
throw "shader not found (parameter ShaderPath, entry $index)."
}
$shaderFile = Get-Item -LiteralPath $shader
$output = Join-Path $outputDirectory "$index-$($shaderFile.BaseName).dxil"
& $DxcPath -T lib_6_3 -HV 2021 -I $shaderFile.DirectoryName $shaderFile.FullName -Fo $output
& $DxcPath -T lib_6_3 -HV 2021 -WX -I $shaderFile.DirectoryName $shaderFile.FullName -Fo $output
if ($LASTEXITCODE -ne 0) {
throw "dxc failed with exit code $LASTEXITCODE (parameter ShaderPath, entry $index)."
}
Write-Host "compiled shader $index of $(@($ShaderPath).Count) (lib_6_3, -HV 2021)"
Write-Host "compiled shader $index of $(@($ShaderPath).Count) (lib_6_3, -HV 2021, -WX)"
}