CultMath: cycles_ramp_index casts its int operands explicitly (GLSL ES 3.00 has no implicit int to float); README and GLSL file-contract tests name the Cycles files

Builds. The oracle fixtures and CultMath.Cycles.glsl are still to come, so the oracle and golden tests fail here.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-12 04:07:08 +02:00
commit 355cfa2a3d
5 changed files with 40 additions and 10 deletions

View file

@ -33,9 +33,20 @@ shader semantics.
- `shaders/CultMath.hlsl`, a canonical HLSL mirror include for shader-side
parity. HLSL already owns `float2`, `float3`, and `float4`; the include
exposes `cultmath_*` functions for shared semantics HLSL does not provide.
It includes `shaders/CultMath.Phacelle.hlsl` (MPL-2.0) and
It includes `shaders/CultMath.Phacelle.hlsl` (MPL-2.0),
`shaders/CultMath.Cycles.hlsl` (Apache-2.0) and
`shaders/CultMath.Interval.hlsl` and `shaders/CultMath.Affine.hlsl` from the same
directory, so ship all four.
directory, so ship all five.
- Cycles node semantics (Apache-2.0): `cycles_*` functions that compute what
Blender's Cycles renderer computes for the Math node operations whose results
differ from a plain intrinsic (`cycles_divide`, `cycles_power`, `cycles_modulo`,
`cycles_wrap`, `cycles_pingpong`, `cycles_smooth_min`, ...), Map Range
(`cycles_map_range_linear`, `_stepped`, `_smoothstep`, `_smootherstep`), Clamp
(`cycles_clamp_minmax`, `cycles_clamp_range`), Mix (`cycles_mix_float`,
`cycles_mix_vector` and every colour blend mode as `cycles_mix_<mode>`), RGB to BW,
and `cycles_ramp_index`, where a Color Ramp reads its table. `cultmath_cycles_*` in
HLSL and GLSL. Operands are finite floats, and the tests hold every function to
the results of Blender 5.2.2's own CPU Cycles (`tools/cycles-oracle`).
- interval arithmetic for culling empty space: `iv_*` functions
over `float2(lo, hi)` (`iv_add`, `iv_mul`, `iv_exp`, `iv_smoothstep`, ...)
and `iv_snoise_ball`/`iv_fbm_ball`, which bound `snoise` and `fbm_grad` over a
@ -58,11 +69,14 @@ shader semantics.
- `shaders/CultMath.glsl` (MIT), the same `cultmath_*` library in GLSL ES 3.00
for WebGL2, generated from `CultMath.hlsl` (never edited by hand), and
`shaders/CultMath.Phacelle.glsl` (MPL-2.0), generated from
`CultMath.Phacelle.hlsl`, which holds `cultmath_phacelle` alone. GLSL has no
`CultMath.Phacelle.hlsl`, which holds `cultmath_phacelle` alone, and
`shaders/CultMath.Cycles.glsl` (Apache-2.0), generated from
`CultMath.Cycles.hlsl`, which holds the `cultmath_cycles_*` functions. GLSL has no
`#include`: a WebGL consumer concatenates `CultMath.glsl` after its own
`#version 300 es` line and `precision highp float; precision highp int;`
(the library declares no precision, and its hashes need 32-bit integers),
then `CultMath.Phacelle.glsl` only if it calls `cultmath_phacelle`, then its
then `CultMath.Phacelle.glsl` only if it calls `cultmath_phacelle` and
`CultMath.Cycles.glsl` only if it calls a `cultmath_cycles_` function, then its
own shader source.
- `Voronoi.SampleTones`, a C# batch surface that calls the Rust
`cultmath-core` native kernel when `cultmath_core` is available and falls back

View file

@ -335,9 +335,10 @@ float cultmath_cycles_rgb_to_bw(float3 color)
// otherwise. The ramp table itself is the caller's.
float2 cultmath_cycles_ramp_index(float t, int size)
{
float f = (t > 0.0 ? (t < 1.0 ? t : 1.0) : 0.0) * (size - 1);
float f = (t > 0.0 ? (t < 1.0 ? t : 1.0) : 0.0) * (float)(size - 1);
int index = clamp((int)f, 0, size - 1);
return float2(index, f - index);
float lower = (float)index;
return float2(lower, f - lower);
}
#endif

View file

@ -361,8 +361,9 @@ public static partial class math
/// </summary>
public static float2 cycles_ramp_index(float t, int size)
{
var f = (t > 0.0f ? (t < 1.0f ? t : 1.0f) : 0.0f) * (size - 1);
var f = (t > 0.0f ? (t < 1.0f ? t : 1.0f) : 0.0f) * (float)(size - 1);
var index = clamp((int)f, 0, size - 1);
return new float2(index, f - index);
var lower = (float)index;
return new float2(lower, f - lower);
}
}

View file

@ -86,6 +86,12 @@ public sealed class GlslMirrorTests
Assert.DoesNotContain(token, library);
Assert.Contains(token, phacelle);
}
// The Apache-2.0 code is in its own file too, and the MPL-2.0 file holds none of it.
var cycles = lowered.Single(output => output.File == "CultMath.Cycles.glsl").Text;
Assert.DoesNotContain("cultmath_cycles_divide(", library);
Assert.DoesNotContain("cultmath_cycles_divide(", phacelle);
Assert.Contains("float cultmath_cycles_divide(", cycles);
}
// The two-file contract a consumer relies on when it concatenates CultMath.Phacelle.glsl after
@ -106,6 +112,13 @@ public sealed class GlslMirrorTests
Assert.Contains("cultmath_phacelle is in CultMath.Phacelle.glsl", library);
Assert.Contains(" MPL-2.0;", phacelle);
Assert.DoesNotContain("MIT", phacelle);
var cycles = lowered.Single(output => output.File == "CultMath.Cycles.glsl").Text.Split('
')[0];
Assert.Contains("cultmath_cycles_ramp_index is in CultMath.Cycles.glsl", library);
Assert.Contains(" Apache-2.0;", cycles);
Assert.DoesNotContain("MIT", cycles);
Assert.DoesNotContain("MPL", cycles);
}
[Fact]

View file

@ -335,9 +335,10 @@ float cultmath_cycles_rgb_to_bw(float3 color)
// otherwise. The ramp table itself is the caller's.
float2 cultmath_cycles_ramp_index(float t, int size)
{
float f = (t > 0.0 ? (t < 1.0 ? t : 1.0) : 0.0) * (size - 1);
float f = (t > 0.0 ? (t < 1.0 ? t : 1.0) : 0.0) * (float)(size - 1);
int index = clamp((int)f, 0, size - 1);
return float2(index, f - index);
float lower = (float)index;
return float2(lower, f - lower);
}
#endif