CultMath: cycles_modulo is exact (power-of-two subtraction, no quotient form); RGB to BW coefficients are Blender's; oracle.py pins the SSE4.2 CPU kernel
The oracle's first fixtures showed Cycles' AVX2 kernel fuses multiply-adds and its film zeroes non-finite results; the tool now exits 4 off the SSE4.2 kernel (CYCLES_CPU_NO_AVX2=1) and the test skips cases Cycles cannot report. The Unity copy of CultMath.hlsl carries the new include. Builds. Fixtures and CultMath.Cycles.glsl are still to come. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e3c94f8edc
commit
51c5da11c4
7 changed files with 147 additions and 28 deletions
|
|
@ -19,9 +19,9 @@ static const float CULTMATH_CYCLES_FLT_EPSILON = 1.1920929e-07;
|
|||
|
||||
// The scene-linear luminance coefficients Cycles reads from Blender 5.2.2's default OCIO config
|
||||
// (kernel/film linear_rgb_to_gray). The oracle fixture pins them with a case per primary.
|
||||
static const float CULTMATH_CYCLES_RGB_TO_Y_R = 0.2126729;
|
||||
static const float CULTMATH_CYCLES_RGB_TO_Y_G = 0.7151522;
|
||||
static const float CULTMATH_CYCLES_RGB_TO_Y_B = 0.0721750;
|
||||
static const float CULTMATH_CYCLES_RGB_TO_Y_R = 0.212639093;
|
||||
static const float CULTMATH_CYCLES_RGB_TO_Y_G = 0.715169132;
|
||||
static const float CULTMATH_CYCLES_RGB_TO_Y_B = 0.0721927434;
|
||||
|
||||
// ---- Math node (kernel/svm/math_util.h svm_math; util/math_base.h) ----
|
||||
|
||||
|
|
@ -63,13 +63,23 @@ float cultmath_cycles_sqrt(float a) { return sqrt(max(a, 0.0)); }
|
|||
// inversesqrtf
|
||||
float cultmath_cycles_inverse_sqrt(float a) { return a > 0.0 ? 1.0 / sqrt(a) : 0.0; }
|
||||
|
||||
// safe_modulo is fmodf, the remainder with the dividend's sign. GLSL has no fmod and the HLSL one is this
|
||||
// quotient form on a GPU, so this is the form every CultMath target shares; it is not fmodf bit for bit,
|
||||
// and CyclesOracleTests states how far it is.
|
||||
// safe_modulo is fmodf, the remainder with the dividend's sign, exactly. GLSL has no fmod, and a - b trunc(a / b)
|
||||
// is wrong where the quotient rounds up to an integer or the product rounds (3 mod 0.3 is 0.29999995, not 0).
|
||||
// So this subtracts the largest power-of-two multiple of the divisor not above the remainder until the
|
||||
// remainder is below the divisor: every step is an exact subtraction, as in fmod. The loops are bounded by the
|
||||
// binades a float spans.
|
||||
float cultmath_cycles_modulo(float a, float b)
|
||||
{
|
||||
float quotient = a / b;
|
||||
return b != 0.0 ? a - b * (quotient >= 0.0 ? floor(quotient) : ceil(quotient)) : 0.0;
|
||||
float remainder = abs(a);
|
||||
float divisor = abs(b);
|
||||
for (int steps = 0; steps < 300 && remainder >= divisor && divisor > 0.0; steps++)
|
||||
{
|
||||
float multiple = divisor;
|
||||
for (int doublings = 0; doublings < 300 && multiple * 2.0 <= remainder; doublings++)
|
||||
multiple = multiple * 2.0;
|
||||
remainder = remainder - multiple;
|
||||
}
|
||||
return b != 0.0 ? (a < 0.0 ? -remainder : remainder) : 0.0;
|
||||
}
|
||||
|
||||
// safe_floored_modulo
|
||||
|
|
|
|||
|
|
@ -20,13 +20,13 @@ public static partial class math
|
|||
private const float CYCLES_FLT_EPSILON = 1.1920929e-07f;
|
||||
|
||||
/// <summary>The scene-linear red luminance coefficient Cycles' RGB to BW node reads from Blender 5.2.2's default OCIO config.</summary>
|
||||
public const float CYCLES_RGB_TO_Y_R = 0.2126729f;
|
||||
public const float CYCLES_RGB_TO_Y_R = 0.212639093f;
|
||||
|
||||
/// <summary>The scene-linear green luminance coefficient Cycles' RGB to BW node reads from Blender 5.2.2's default OCIO config.</summary>
|
||||
public const float CYCLES_RGB_TO_Y_G = 0.7151522f;
|
||||
public const float CYCLES_RGB_TO_Y_G = 0.715169132f;
|
||||
|
||||
/// <summary>The scene-linear blue luminance coefficient Cycles' RGB to BW node reads from Blender 5.2.2's default OCIO config.</summary>
|
||||
public const float CYCLES_RGB_TO_Y_B = 0.0721750f;
|
||||
public const float CYCLES_RGB_TO_Y_B = 0.0721927434f;
|
||||
|
||||
// ---- Math node (kernel/svm/math_util.h svm_math; util/math_base.h) ----
|
||||
|
||||
|
|
@ -72,14 +72,25 @@ public static partial class math
|
|||
public static float cycles_inverse_sqrt(float a) => a > 0.0f ? 1.0f / sqrt(a) : 0.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Math node Truncated Modulo (safe_modulo), the remainder with the dividend's sign, or 0 when b is 0.
|
||||
/// Cycles computes fmodf. GLSL has no fmod and HLSL's is this quotient form on a GPU, so this is the form
|
||||
/// every CultMath target shares: it is not fmodf bit for bit, and CyclesOracleTests states how far it is.
|
||||
/// Math node Truncated Modulo (safe_modulo), the remainder with the dividend's sign, exactly as fmodf computes
|
||||
/// it, or 0 when b is 0. GLSL has no fmod, and a - b trunc(a / b) is wrong where the quotient rounds up to an
|
||||
/// integer or the product rounds (3 mod 0.3 is 0.29999995, not 0). So this subtracts the largest power-of-two
|
||||
/// multiple of the divisor not above the remainder until the remainder is below the divisor: every step is an
|
||||
/// exact subtraction, as in fmod. The loops are bounded by the binades a float spans.
|
||||
/// </summary>
|
||||
public static float cycles_modulo(float a, float b)
|
||||
{
|
||||
var quotient = a / b;
|
||||
return b != 0.0f ? a - b * (quotient >= 0.0f ? floor(quotient) : ceil(quotient)) : 0.0f;
|
||||
var remainder = abs(a);
|
||||
var divisor = abs(b);
|
||||
for (var steps = 0; steps < 300 && remainder >= divisor && divisor > 0.0f; steps++)
|
||||
{
|
||||
var multiple = divisor;
|
||||
for (var doublings = 0; doublings < 300 && multiple * 2.0f <= remainder; doublings++)
|
||||
multiple = multiple * 2.0f;
|
||||
remainder = remainder - multiple;
|
||||
}
|
||||
|
||||
return b != 0.0f ? (a < 0.0f ? -remainder : remainder) : 0.0f;
|
||||
}
|
||||
|
||||
/// <summary>Math node Floored Modulo (safe_floored_modulo): a - floor(a / b) b, or 0 when b is 0.</summary>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ namespace CultMath.Tests;
|
|||
/// fixtures/cycles-oracle are written by tools/cycles-oracle/oracle.py (never by hand): each case is the
|
||||
/// operands and the node's baked results as float32 bit patterns. Every function is compared at every case
|
||||
/// within the tolerance its family states below; a function not named in <see cref="Tolerances"/> is held to
|
||||
/// 0 ulp. Signed zeros are not distinguished (-0 equals 0), and NaN equals NaN.
|
||||
/// 0 ulp. Signed zeros are not distinguished (-0 equals 0), and NaN equals NaN. The fixtures come from Cycles'
|
||||
/// SSE4.2 CPU kernel, which does not fuse multiply-adds as C# does not (oracle.py checks this before it bakes).
|
||||
/// </summary>
|
||||
public sealed class CyclesOracleTests
|
||||
{
|
||||
|
|
@ -107,6 +108,10 @@ public sealed class CyclesOracleTests
|
|||
Assert.Equal(want.Length, have.Length);
|
||||
for (var k = 0; k < want.Length; k++)
|
||||
{
|
||||
// Cycles' film writes 0 for an infinite or NaN result; the operands are finite, so such a case holds
|
||||
// nothing to compare.
|
||||
if (!float.IsFinite(have[k]) && want[k] == 0.0f)
|
||||
continue;
|
||||
var distance = Distance(want[k], have[k]);
|
||||
if (distance > worst)
|
||||
{
|
||||
|
|
|
|||
52
packages/cultmath/tools/cycles-oracle/README.md
Normal file
52
packages/cultmath/tools/cycles-oracle/README.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# Cycles oracle
|
||||
|
||||
`oracle.py` produces the fixtures that `CyclesOracleTests` holds `math.cycles_*` to: for each Cycles node
|
||||
CultMath ports, the node's results as Blender's own Cycles computes them.
|
||||
|
||||
## Run
|
||||
|
||||
```
|
||||
CYCLES_CPU_NO_AVX2=1 blender --background --factory-startup --python oracle.py -- <fixture directory> [<family> ...]
|
||||
```
|
||||
|
||||
The fixture directory is `tests/CultMath.Tests/fixtures/cycles-oracle`. Families are `math`, `map-range`,
|
||||
`clamp`, `mix`, `rgb-to-bw` and `ramp`; with none named, all are written. The tool is a Blender Python
|
||||
script and runs only under Blender, never under a plain Python.
|
||||
|
||||
## The pin
|
||||
|
||||
It refuses to run under any Blender but **5.2.2 LTS, build hash `d13f752e3b9c`**, exits nonzero and names
|
||||
the version and build it found beside the ones it needs. Blender 5.2.2 is
|
||||
<https://download.blender.org/release/Blender5.2/> (`blender-5.2.2-linux-x64.tar.xz`, or the Windows build
|
||||
from Steam). The fixtures record the version, the hash and the platform they were written on.
|
||||
|
||||
## The CPU kernel
|
||||
|
||||
Cycles' AVX2 CPU kernel is compiled with fused multiply-add, so an expression such as `a - floor(a / b) b` is
|
||||
one fused operation there, and CultMath's C# (which fuses nothing) computes the unfused one. The fixtures pin
|
||||
the SSE4.2 kernel, which has no FMA, by `CYCLES_CPU_NO_AVX2=1`; the tool bakes `10000 mod 0.1` before anything
|
||||
else and exits 4 if the result is the fused one, so a run on the wrong kernel writes nothing.
|
||||
|
||||
## Method
|
||||
|
||||
Operands go into float, Non-Color input images (channel packed, read with Closest interpolation), three
|
||||
operands per image. The node under test reads them. One Emit bake per output channel (Cycles on the CPU, 1
|
||||
sample, margin 0, no denoising) writes the result to a float image; each pixel is one case. The tool first
|
||||
checks that a bake returns an operand image bit for bit, and stops if it does not. The fixtures hold the
|
||||
operands and the results as float32 bit patterns, so a test reads exactly what Cycles computed.
|
||||
|
||||
A Map Range node's Clamp option is baked as the node with Clamp on, which Cycles expands into a Clamp node
|
||||
of type Range on the result. The ramp family bakes a Color Ramp whose table entries are each read at a `t`
|
||||
whose product with 255 is the entry's index, so the first 256 cases are the ramp's own table.
|
||||
|
||||
## Fixtures
|
||||
|
||||
Written by this tool and never edited by hand. Rerunning it under the same Blender build on the same
|
||||
platform rewrites them byte for byte; another platform's libm can differ in the transcendental families
|
||||
(`power`, `logarithm`), which is why `CyclesOracleTests` states a tolerance for them.
|
||||
|
||||
## Licence
|
||||
|
||||
`oracle.py` is GPL-3.0-or-later, as a script that runs inside Blender (`bpy`). It ships in no package: the
|
||||
`GameCult.Math` NuGet package and the Unity package do not contain `tools/`. The fixtures are data it
|
||||
produced. The Cycles functions they test are Apache-2.0 (`math.Cycles.cs`, `CultMath.Cycles.hlsl`).
|
||||
|
|
@ -45,7 +45,10 @@ PLANE = bpy.context.active_object
|
|||
|
||||
def by_identifier(sockets, identifier):
|
||||
"""A node's socket by identifier: several data types share one socket name."""
|
||||
return next(socket for socket in sockets if socket.identifier == identifier)
|
||||
for socket in sockets:
|
||||
if socket.identifier == identifier:
|
||||
return socket
|
||||
raise KeyError("no socket %s among %s" % (identifier, [s.identifier for s in sockets]))
|
||||
|
||||
|
||||
def f32(values):
|
||||
|
|
@ -308,7 +311,7 @@ def family_map_range():
|
|||
node.interpolation_type = kind
|
||||
node.clamp = False
|
||||
sockets = operand_sockets(tree, imgs, 6)
|
||||
for k, identifier in enumerate(["Value", "From_Min", "From_Max", "To_Min", "To_Max", "Steps"]):
|
||||
for k, identifier in enumerate(["Value", "From_Min_FLOAT", "From_Max_FLOAT", "To_Min_FLOAT", "To_Max_FLOAT", "Steps_FLOAT"]):
|
||||
socket = by_identifier(node.inputs, identifier)
|
||||
tree.links.new(sockets[k], socket)
|
||||
return [node.outputs["Result"]]
|
||||
|
|
@ -321,7 +324,7 @@ def family_map_range():
|
|||
node.interpolation_type = kind
|
||||
node.clamp = True
|
||||
sockets = operand_sockets(tree, imgs, 6)
|
||||
for k, identifier in enumerate(["Value", "From_Min", "From_Max", "To_Min", "To_Max", "Steps"]):
|
||||
for k, identifier in enumerate(["Value", "From_Min_FLOAT", "From_Max_FLOAT", "To_Min_FLOAT", "To_Max_FLOAT", "Steps_FLOAT"]):
|
||||
socket = by_identifier(node.inputs, identifier)
|
||||
tree.links.new(sockets[k], socket)
|
||||
return [node.outputs["Result"]]
|
||||
|
|
@ -511,12 +514,39 @@ def identity_check():
|
|||
sys.exit(3)
|
||||
|
||||
|
||||
def kernel_check():
|
||||
"""The CPU kernel must not fuse multiply-adds.
|
||||
|
||||
Cycles' AVX2 CPU kernel is compiled with FMA, so a - floor(a / b) b is one fused operation there and
|
||||
10000 mod 0.1 is -1.5e-4; the SSE4.2 kernel has none, and gives the unfused result 0 that CultMath's C#
|
||||
computes. The fixtures pin the SSE4.2 kernel: run with CYCLES_CPU_NO_AVX2=1.
|
||||
"""
|
||||
a = f32([10000.0] * CASES)
|
||||
b = f32([0.1] * CASES)
|
||||
unfused = a - np.floor(a / b) * b
|
||||
images = pack_operands([a, b])
|
||||
|
||||
def build(tree, imgs):
|
||||
node = tree.nodes.new("ShaderNodeMath")
|
||||
node.operation = "FLOORED_MODULO"
|
||||
sockets = operand_sockets(tree, imgs, 2)
|
||||
tree.links.new(sockets[0], node.inputs[0])
|
||||
tree.links.new(sockets[1], node.inputs[1])
|
||||
return [node.outputs[0]]
|
||||
|
||||
got = bake("kernel", images, build, 1)[0]
|
||||
if not np.array_equal(got.view(np.uint32), unfused.astype(np.float32).view(np.uint32)):
|
||||
sys.stderr.write("cycles-oracle: this Cycles CPU kernel fuses multiply-adds; run with CYCLES_CPU_NO_AVX2=1\n")
|
||||
sys.exit(4)
|
||||
|
||||
|
||||
def main():
|
||||
argv = sys.argv[sys.argv.index("--") + 1:]
|
||||
out = argv[0]
|
||||
wanted = argv[1:] or list(FAMILIES)
|
||||
os.makedirs(out, exist_ok=True)
|
||||
identity_check()
|
||||
kernel_check()
|
||||
header = {
|
||||
"oracle": "tools/cycles-oracle/oracle.py (Blender %s, build %s, Cycles CPU, 1 sample, Emit bake, float Non-Color images)" % (PINNED_VERSION, PINNED_HASH),
|
||||
"blender": PINNED_VERSION,
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ static const float CULTMATH_CYCLES_FLT_EPSILON = 1.1920929e-07;
|
|||
|
||||
// The scene-linear luminance coefficients Cycles reads from Blender 5.2.2's default OCIO config
|
||||
// (kernel/film linear_rgb_to_gray). The oracle fixture pins them with a case per primary.
|
||||
static const float CULTMATH_CYCLES_RGB_TO_Y_R = 0.2126729;
|
||||
static const float CULTMATH_CYCLES_RGB_TO_Y_G = 0.7151522;
|
||||
static const float CULTMATH_CYCLES_RGB_TO_Y_B = 0.0721750;
|
||||
static const float CULTMATH_CYCLES_RGB_TO_Y_R = 0.212639093;
|
||||
static const float CULTMATH_CYCLES_RGB_TO_Y_G = 0.715169132;
|
||||
static const float CULTMATH_CYCLES_RGB_TO_Y_B = 0.0721927434;
|
||||
|
||||
// ---- Math node (kernel/svm/math_util.h svm_math; util/math_base.h) ----
|
||||
|
||||
|
|
@ -63,13 +63,23 @@ float cultmath_cycles_sqrt(float a) { return sqrt(max(a, 0.0)); }
|
|||
// inversesqrtf
|
||||
float cultmath_cycles_inverse_sqrt(float a) { return a > 0.0 ? 1.0 / sqrt(a) : 0.0; }
|
||||
|
||||
// safe_modulo is fmodf, the remainder with the dividend's sign. GLSL has no fmod and the HLSL one is this
|
||||
// quotient form on a GPU, so this is the form every CultMath target shares; it is not fmodf bit for bit,
|
||||
// and CyclesOracleTests states how far it is.
|
||||
// safe_modulo is fmodf, the remainder with the dividend's sign, exactly. GLSL has no fmod, and a - b trunc(a / b)
|
||||
// is wrong where the quotient rounds up to an integer or the product rounds (3 mod 0.3 is 0.29999995, not 0).
|
||||
// So this subtracts the largest power-of-two multiple of the divisor not above the remainder until the
|
||||
// remainder is below the divisor: every step is an exact subtraction, as in fmod. The loops are bounded by the
|
||||
// binades a float spans.
|
||||
float cultmath_cycles_modulo(float a, float b)
|
||||
{
|
||||
float quotient = a / b;
|
||||
return b != 0.0 ? a - b * (quotient >= 0.0 ? floor(quotient) : ceil(quotient)) : 0.0;
|
||||
float remainder = abs(a);
|
||||
float divisor = abs(b);
|
||||
for (int steps = 0; steps < 300 && remainder >= divisor && divisor > 0.0; steps++)
|
||||
{
|
||||
float multiple = divisor;
|
||||
for (int doublings = 0; doublings < 300 && multiple * 2.0 <= remainder; doublings++)
|
||||
multiple = multiple * 2.0;
|
||||
remainder = remainder - multiple;
|
||||
}
|
||||
return b != 0.0 ? (a < 0.0 ? -remainder : remainder) : 0.0;
|
||||
}
|
||||
|
||||
// safe_floored_modulo
|
||||
|
|
|
|||
|
|
@ -509,6 +509,7 @@ CultCellular cultmath_cellular(float3 p)
|
|||
|
||||
#include "CultMath.Phacelle.hlsl"
|
||||
#include "CultMath.Interval.hlsl"
|
||||
#include "CultMath.Cycles.hlsl"
|
||||
#include "CultMath.Affine.hlsl"
|
||||
|
||||
float cultmath_value_noise(float2 position)
|
||||
|
|
|
|||
Loading…
Reference in a new issue