MeasureLipschitz on e89fd687 (Yggdrasil): sampled 7.1933103, refined 7.2400064,
x 1.10 = 7.9640074, witness (229.92526, 73.37927, -66.14595). The old 9.181147
sat on the diagonal tie. C#, both Interval.hlsl copies, the pin test's witness
and design.md's provenance numbers move together; CultMath.glsl follows with the
regenerated fixture.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
711 lines
45 KiB
Markdown
711 lines
45 KiB
Markdown
# Design
|
||
|
||
CultMath exists to make renderer math readable in every place it needs to live:
|
||
Rust hot kernels, C# host code, and HLSL shader code.
|
||
|
||
## Parity Target: HLSL
|
||
|
||
HLSL is the semantic authority for every type and intrinsic HLSL defines. The
|
||
goal is source compatibility: HLSL-shaped math compiles as C# under
|
||
`using static CultMath.math;`. C# forces float literal suffixes (`0.5f`); the
|
||
rest of the spelling should survive the move.
|
||
|
||
That makes these HLSL rules, not options:
|
||
|
||
- Every swizzle exists. `float2/3/4`, `int2/3/4`, and `bool2/3/4` expose every
|
||
read swizzle of length 2 to 4, repeats included (`v.wwxy`), in both `xyzw` and
|
||
`rgba` naming (never mixed), plus single-component `r/g/b/a`. `double2/3` get
|
||
lengths 2 and 3 only, because there is no `double4`. Components and swizzles are
|
||
writable: `v.xz = f2;`, `c.rgb *= 0.5f;`, and `v[1] = s;` work. Setters exist
|
||
only for non-repeating masks, because HLSL rejects `v.xx = ...`.
|
||
- Every HLSL mixed constructor exists, as a struct constructor and as a `math`
|
||
function: `float4(float2, float2)`, `float4(float, float2, float)`,
|
||
`float3(float, float2)`, the copy form `float3(float3)`, and the rest, for the
|
||
float, int, and bool families (double gets constructors only; a `math.double3`
|
||
function would hide the `double3` type name).
|
||
- Swizzles and mixed constructors are generated. `tools/generate-swizzles.ps1`
|
||
writes the checked-in `src/CultMath/Swizzles.g.cs` (partial structs plus a
|
||
partial `math`). Edit the generator, rerun it, and commit both; never edit the
|
||
output by hand.
|
||
- Comparison operators (`<`, `>`, `<=`, `>=`, `==`, `!=`) are component-wise and
|
||
return `bool2`/`bool3`/`bool4`. Reduce them with `any` and `all`.
|
||
`Equals`/`GetHashCode` stay value-based so vectors still work as dictionary
|
||
keys.
|
||
- `select(condition, whenTrue, whenFalse)` uses HLSL's argument order.
|
||
- Matrices are row-major: constructors take rows, `m[i]` returns a reference to
|
||
row i (so `m[1][2] = s;` writes one element), `_mRC` is row R column C, and
|
||
products go through `mul` (`mul(m, v)` treats `v` as a column, `mul(v, m)` as a
|
||
row). `*` on matrices would be component-wise in HLSL, so CultMath does not
|
||
define it.
|
||
- `m[r][c] = v` writes through locals, array elements, fields of classes, and
|
||
ref locals. Through a readonly field, an `in` parameter, a property
|
||
(auto-properties included), or a static readonly value such as
|
||
`float3x3.identity`, C# takes a defensive copy: the write compiles and is
|
||
silently lost. Copy the matrix to a local, write to it, and assign it back.
|
||
- Scalar/vector arithmetic and comparisons have explicit overloads on both
|
||
sides rather than leaning on implicit scalar splat.
|
||
- Intrinsics return HLSL's types and edge cases: `sign` returns `int`/`intN`
|
||
(NaN gives 0); `step(y, x)` is `x < y ? 0 : 1` (NaN gives 1), which is how dxc
|
||
lowers it, in C# and in the HLSL mirror; float and double `min` and `max`, and
|
||
float `clamp` and `saturate`, follow the DXIL `FMin`/`FMax`/`Saturate` operations dxc emits for
|
||
them: `min(a, b)` is `a < b ? a : b`, `max(a, b)` is `a >= b ? a : b`, a NaN
|
||
operand returns the other (so `saturate(NaN)` is 0), `clamp(x, a, b)` is
|
||
`min(max(x, a), b)`, and `saturate(x)` is `min(1, max(0, x))`, which maps -0
|
||
to +0; `any`/`all` accept numeric vectors (a component is true when it is
|
||
not 0); `min`, `max`, and `abs` on int vectors return int vectors; int
|
||
`clamp` is `IMin(IMax(x, a), b)`, so inverted bounds return `b`.
|
||
- Same-size vector conversions (`intN(floatN)`, `floatN(intN)`, `boolN(floatN)`,
|
||
`boolN(intN)`, `floatN(boolN)`, `intN(boolN)`) exist as constructors and
|
||
`math` functions and follow dxc: float to int is `fptosi`, truncating toward
|
||
zero; numeric to bool is `!= 0` (`fcmp une`, so NaN is true); bool to numeric
|
||
is 0 or 1. DXIL leaves float-to-int undefined for NaN and out-of-range values,
|
||
and so does C#; CultMath does not pin them.
|
||
- `normalize(x)` is `x * Rsqrt(Dot(x, x))`, which is how dxc lowers it, and
|
||
DXIL.rst defines `Rsqrt` as `1 / sqrt(src)`. A zero vector therefore
|
||
normalizes to NaN (`0 * inf`), and a NaN component makes every component NaN.
|
||
(The special-value table under `Rsqrt` in DXIL.rst is a copy of the `Round`
|
||
table and contradicts that definition, so CultMath follows the definition.)
|
||
`length` and `distance` are `Sqrt` of the summed squares, so a zero vector
|
||
gives 0. Callers that can meet zero-length vectors guard them before calling
|
||
`normalize`.
|
||
- dxc demotes double `frac`, `exp`, `lerp`, and `floor` to float. CultMath's
|
||
double overloads keep double precision instead: `frac(double)` is
|
||
`x - floor(x)` in double. That is a deliberate divergence for CPU simulation
|
||
time, which shaders do not carry.
|
||
|
||
dxc marks float arithmetic and compares `fast` (no NaNs) unless a value is
|
||
`precise`, so drivers may optimize NaN handling away and NaN results on real
|
||
GPUs are not guaranteed. CultMath defines the CPU result by dxc's lowering and
|
||
the DXIL operation spec (DirectXShaderCompiler `docs/DXIL.rst`).
|
||
|
||
NaN does not reliably propagate through CultMath, just as it does not on the
|
||
GPU. Because `min`, `max`, and `saturate` return the non-NaN operand, some
|
||
functions turn NaN or infinity into numbers: `smoothstep(a, a, a)` is 0;
|
||
infinite inputs to `smoothstep` and the intercept helpers give numbers; the
|
||
bezier, `smoothstep01`, and `smootherstep` functions return finite values for
|
||
NaN input. Callers that must detect bad data should check `isnan` before calling these
|
||
functions.
|
||
|
||
Deferred until a consumer needs them: `float4x4`, `transpose`, `determinant`,
|
||
and HLSL's one-based `_11`..`_33` element names.
|
||
|
||
### HLSL Target: Source Transformations
|
||
|
||
`HlslSourceCompatibilityTests` compiles the real `shaders/CultMath.hlsl` with
|
||
Roslyn against CultMath. These transformations are the complete list; if a body
|
||
needs anything else, the test fails:
|
||
|
||
1. Include guards (`#ifndef`/`#define`/`#endif`) are dropped.
|
||
2. Functions taking `Texture2D`/`SamplerState` are dropped; GPU resources have
|
||
no CultMath analog.
|
||
3. File-scope `static const` becomes `const`.
|
||
4. Floating literals gain `f` (`0.5` becomes `0.5f`).
|
||
5. Struct fields gain `public`. HLSL struct members have no access-modifier
|
||
concept; a C# struct's fields default to `private`, which would hide them
|
||
from the mirror test's reflection-based field comparison.
|
||
6. The file body is wrapped in a class, since C# has no free functions; HLSL
|
||
functions become private instance methods.
|
||
|
||
HLSL constructs that stay outside the target and must not appear in the mirror:
|
||
`const` locals of vector type (C# `const` is primitives only; write a plain
|
||
local) and writes through a swizzle component (`v.xy.x = s`, which C# rejects as
|
||
modifying a temporary).
|
||
|
||
The HLSL side is checked too: `tools/get-dxc.ps1` fetches Microsoft's
|
||
DirectXShaderCompiler, and
|
||
`dxc -T lib_6_3 -HV 2021 shaders/CultMath.hlsl` must compile cleanly.
|
||
|
||
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
|
||
C# `math` on the CPU, not that a GPU agrees: driver `sin` precision alone makes
|
||
`cultmath_hash` and value noise differ bit for bit on hardware. The mirror is
|
||
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.
|
||
|
||
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
|
||
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.
|
||
|
||
### GLSL Target: Source Transformations
|
||
|
||
`shaders/CultMath.glsl` is a GLSL ES 3.00 (WebGL2) library generated from
|
||
`shaders/CultMath.hlsl` by `GlslLowering` (in the test project), and
|
||
`shaders/CultMath.Phacelle.glsl` is generated beside it from
|
||
`shaders/CultMath.Phacelle.hlsl`. Both are derived files: never edit them by
|
||
hand. Regenerate them, and the golden fixture
|
||
`tests/CultMath.Tests/fixtures/glsl-parity.json`, with:
|
||
|
||
```powershell
|
||
$env:CULTMATH_WRITE_GLSL = "1"; dotnet test packages/cultmath/tests/CultMath.Tests --filter GlslMirrorTests
|
||
```
|
||
|
||
`GlslMirrorTests.CommittedGlslEqualsLowering` fails on any difference between
|
||
either committed file and the lowering of the committed HLSL. These
|
||
transformations, in order, are the complete list:
|
||
|
||
1. Each `#include "name"` is replaced in place by the included file, by the
|
||
same resolver the C# mirror uses; GLSL has no `#include`. The exception is
|
||
an include under a file-level licence of its own, listed in one table,
|
||
`GlslLowering.SeparateFiles`: its line is dropped and the file is lowered on
|
||
its own, by the same steps, into its own output. The table has one entry,
|
||
`CultMath.Phacelle.hlsl` to `CultMath.Phacelle.glsl`, because Phacelle is
|
||
MPL-2.0 and MPL-2.0 is file-level: kept separate, it leaves
|
||
`CultMath.glsl` MIT. Nothing else in the lowering is keyed by name.
|
||
2. Include guards are dropped, and so are the functions taking
|
||
`Texture2D`/`SamplerState`; the host shader samples its own textures.
|
||
3. Type names: `floatN`, `intN`, `uintN` and `boolN` become `vecN`, `ivecN`,
|
||
`uvecN` and `bvecN`.
|
||
4. File-scope `static const` becomes `const`.
|
||
5. C-style casts `(float)x`, `(int)x` and `(uint)x` become `float(x)`, `int(x)`
|
||
and `uint(x)`. The operand is found by a balanced-bracket scanner:
|
||
`(int)((state >> 28) + 4u)` nests parentheses.
|
||
6. Intrinsics are renamed: `lerp` to `mix`, `frac` to `fract`, `rsqrt` to
|
||
`inversesqrt`, `asuint` to `floatBitsToUint`, `asfloat` to
|
||
`uintBitsToFloat`, and `saturate(x)` becomes `clamp(x, 0.0, 1.0)`.
|
||
7. Float literals pass through unchanged.
|
||
8. Each output is wrapped in a guard named for its file: `CULTMATH_GLSL`,
|
||
`CULTMATH_PHACELLE_GLSL`.
|
||
9. The first line names the file as generated, its sources and its licence,
|
||
and says how to regenerate it; `CultMath.glsl`'s also says which
|
||
functions live in the separate file.
|
||
|
||
GLSL ES 3.00 is stricter than HLSL: no implicit conversions between int, uint
|
||
and float, no C-style casts, and no `step(genType, float)` overload. Those gaps
|
||
are closed in the HLSL, never in the lowering: the HLSL is written in the subset
|
||
both languages share, with edits that are legal HLSL and compute the same bits
|
||
(`step(h, float4(0.0, 0.0, 0.0, 0.0))` rather than `step(h, 0.0)`, `+ 4u` where
|
||
a uint is added, `(float)((uint)hash.x >> 8)` before a float multiply). The C#
|
||
mirror and the FXC and dxc compiles keep those edits honest. A body that needs a
|
||
transformation outside the list fails `GlslMirrorTests.NoHlslOnlyTokensSurvive`
|
||
rather than gaining a special case.
|
||
|
||
The library declares no precision. The host shader does, and it must be
|
||
`precision highp float; precision highp int;`: the PCG hashes need 32-bit
|
||
integers. `tools/compile-glsl.ps1` writes such a wrapper (its own `#version
|
||
300 es` line, the precision, the library by string concatenation, and one call
|
||
of every public function) and compiles it with glslang, which
|
||
`tools/get-glslang.ps1` fetches pinned. It runs twice: over `CultMath.glsl`
|
||
alone, and with `-Phacelle` over `CultMath.glsl` followed by
|
||
`CultMath.Phacelle.glsl`, the order a consumer that calls `cultmath_phacelle`
|
||
concatenates them in. Two negative controls must fail: `-Body` with an HLSL
|
||
token (`float x = lerp(0.0, 1.0, 0.5);`), and `-Body` calling
|
||
`cultmath_phacelle` without `-Phacelle`, which proves the MIT file does not
|
||
carry it. The Unity package ships neither GLSL file.
|
||
|
||
Parity runs in two legs, each with one owner: HLSL against C# by the mirror
|
||
test, bit for bit; GLSL against C# by the golden fixture, which records 256
|
||
seeded cases per function family as float32 bit patterns, evaluated by the
|
||
consumer on WebGL2 and read back. The families `pcg3d` and `pcg4d` are marked
|
||
exact; the float families are ulp-bounded, the bound measured on the device.
|
||
|
||
`iv_frustum_ball` is ulp-bounded, not exact: it calls `sqrt`, which GLSL ES
|
||
3.00 does not require to be correctly rounded, and WebGL2 compilers may
|
||
reassociate. On a GTX 1070 under ANGLE's D3D11 backend its radius differs from
|
||
C# by up to 2 ulp in 43 of 256 cases, sometimes smaller. What the march needs
|
||
is enclosure, and what culling needs is a ball no wider than its rounding
|
||
widening, so the family's fixture entry carries a `check` the consumer applies
|
||
to every case: from the case's arguments `(mx, my, z0, z1, fp, warp)`, compute
|
||
in double `zm = (z0 + z1) / 2`, the centre `c = (mx zm, my zm, zm)`, its
|
||
`|c|_1 = |mx zm| + |my zm| + |zm|`, the radius
|
||
`r = (z1 - z0) / 2 sqrt(mx^2 + my^2 + 1) + z1 fp + warp` and
|
||
`d = |GPU centre - c|`, and require
|
||
`r + d <= GPU radius <= r + d + 2^-19 (r + |c|_1)`. The lower side is
|
||
enclosure; the upper side is the bound `NoiseBoundTests` pins in C#, so a
|
||
lowering that inflates the ball fails too. Only those two bounds fail the
|
||
check; the largest ulp distance is reported as for any ulp-bounded family.
|
||
`GoldenFixtureMatchesCSharp` applies the same check to C#'s own results.
|
||
|
||
`GoldenFixtureMatchesCSharp` evaluates C# on the committed fixture's own
|
||
arguments. Arguments are drawn only when the fixture is regenerated
|
||
(`CULTMATH_WRITE_GLSL=1`), because the draw itself calls `MathF.Pow`, whose
|
||
bits differ by OS. `iv_exp` and `phacelle` take their C# results from the
|
||
platform's `exp`, `sin` and `cos`, which also differ by OS, so their entries
|
||
name the platform that generated them and the test compares them case by case
|
||
against the running platform, printing the largest distance. `iv_exp`
|
||
("ulp-bounded, platform exp") must agree within one ulp. `phacelle`
|
||
("ulp-bounded, platform exp, sin, cos; across platforms |diff| <= 2^-20
|
||
max(|v|, 1)") must agree within `2^-20 max(|v|, 1)` per component, `v` being
|
||
the fixture's value: its sums of `exp`, `sin` and `cos` cancel near zero, where
|
||
ulps mean nothing, and Windows differs from the Linux fixture by up to
|
||
`3.6e-7 max(|v|, 1)`. Every other family is compared as text. `phacelle` is in
|
||
`CultMath.Phacelle.glsl`, so the evaluator concatenates
|
||
`CultMath.Phacelle.glsl` after `CultMath.glsl` for that family, with
|
||
normalization 0.5. The site does not vendor the MPL file and skips the family.
|
||
Its evaluator is the cultmath-tapes follow-up `glsl-browser-parity-run`, owned
|
||
by the next CultMath parity cut: a repeatable CultLib WebGL2 run of every
|
||
fixture family, `phacelle` included. Until it lands, the one GLSL evaluation of
|
||
`phacelle` is a single probe (GTX 1070, ANGLE D3D11: within `3.1e-6 max(|v|, 1)`
|
||
of C#). Asura's `gpu-snoise-parity` is a different check, an FXC and Unity
|
||
readback of `snoise`, and evaluates no GLSL.
|
||
|
||
## Invariant 8: Value-and-Gradient Primitives
|
||
|
||
A primitive tagged invariant 8 returns its value and analytic gradient
|
||
together, laid out as `float4(gradient.xyz, value.w)`, with no value-only twin
|
||
(a value-only form would be a second path with no consumer). `smin_grad` and
|
||
`cellular` are the first primitives in this family:
|
||
|
||
- `smin_grad(float4 a, float4 b, float k)` is Inigo Quilez's quadratic-polynomial
|
||
smooth minimum ("smooth minimum",
|
||
<https://iquilezles.org/articles/smin/>), carried to value-and-gradient
|
||
form. Its blend factor `h` is affine in `b.w - a.w`, so differentiating the
|
||
value with respect to position, the terms carrying `dh/dp` cancel exactly;
|
||
the surviving gradient is `lerp(∇b, ∇a, h)`, the same `h` the value uses.
|
||
This is the analytic gradient, not an approximation, and it is continuous
|
||
across the `|a.w - b.w| = k` seam where `h` saturates to 0 or 1.
|
||
- `cellular(float3 p)` is Worley's cellular texture basis function ("A
|
||
Cellular Texture Basis Function", SIGGRAPH 1996), searched over the
|
||
jittered 5×5×5 neighbourhood of feature points selected by `pcg3d` (never
|
||
the sin-based `hash`), with cells whose position-only lower bound already
|
||
exceeds the running F2 skipped before their hash is even computed. It
|
||
returns `CultCellular { nearest, edge, id }`: `nearest` is `(∇F1, F1)`,
|
||
`edge` is `(∇F2 - ∇F1, F2 - F1)`, and `id` is the nearest cell's `pcg4d`
|
||
hash (over the cell coordinate alone, never the `pcg3d` hash that produced
|
||
its jitter) mapped to `[0, 1)`. `∇F1` and `∇F2` are each undefined exactly
|
||
at their own feature point (`F1 = 0` or `F2 = 0`); `cellular` follows the
|
||
repo's existing degenerate-normal convention
|
||
(`GameCult.Geometry.CultGeometryIsoSurface.EmitOrientedTriangle`, which
|
||
guards a zero-length normal and returns the zero vector instead of the NaN
|
||
a bare `normalize` gives there) and returns the zero vector rather than NaN
|
||
for either. `F1 = 0` is ordinary (`p` sits on a feature point); `F2 = 0`
|
||
needs two distinct cells' `pcg3d`-jittered feature points to land on the
|
||
same float32 value in every component, which is unreachable at ordinary
|
||
magnitudes but confirmed reachable (see "Precision domain" below). The
|
||
identity of the nearest and second-nearest feature point changes
|
||
discontinuously across the `F1 = F2` set (a genuine kink, not a numerical
|
||
artifact), so `∇F1` and `∇F2` are only continuous away from that set.
|
||
|
||
The 5×5×5 radius is exact, not merely safer than the classic 3×3×3 radius:
|
||
for a query at position `u` inside its own cell (`u` in `[0, 1)^3` per
|
||
component) and a candidate feature at integer cell offset `d` with jitter
|
||
`j` in `[0, 1)^3`, the per-axis displacement `(u_i - j_i) - d_i` is always
|
||
strictly greater in magnitude than `max(0, |d_i| - 1)`, because
|
||
`u_i - j_i` is always strictly inside `(-1, 1)`. So any cell with `|d_i| >=
|
||
3` on some axis is strictly farther than 2 from the query. Two real,
|
||
always-present candidates bound `F1` and `F2`: the query's own cell, whose
|
||
distance is always strictly less than `sqrt(3)` (every axis strictly under
|
||
1); and the near-side neighbour on whichever single axis has `u_i` closest
|
||
to a cell boundary. Let `m = min(u_i, 1 - u_i)` on that chosen axis, so
|
||
`m` is in `[0, 1/2]` by construction. Stepping one cell towards that
|
||
nearer boundary puts the query within `1 + m` of the far face on the
|
||
offset axis and within `1 - m` of the near face on each of the other two
|
||
axes, so that neighbour's squared distance is at most
|
||
`(1 + m)^2 + 2*(1 - m)^2 = 3 - 2m + 3m^2 <= 3` for every `m` in `[0, 1/2]`
|
||
(3 at `m = 0`, dipping to a minimum of `8/3 ~= 2.667` at `m = 1/3`, then
|
||
rising back to 2.75 at `m = 1/2`), and strictly below 3 because
|
||
the jitter never reaches exactly 0 or 1. `F2` is at most the larger of
|
||
these two real candidates (any two real candidates upper-bound the
|
||
2nd-smallest value over the full infinite candidate set), so `F1 <
|
||
sqrt(3)` and `F2 < sqrt(3)` always, comfortably under the radius-2
|
||
exclusion distance of 2, with margin `2 - sqrt(3) ~= 0.268` (Soul's
|
||
adversarial check, cut 2a-i: the worst realizable in-box `F2` is exactly
|
||
`sqrt(3)`, reached only in the unattainable limit at a cell corner). 3×3×3's
|
||
matching argument only reaches radius 1, whose excluded cells start at
|
||
distance 1, short of the `sqrt(3)` ceiling — the gap its measured failures
|
||
(9 of 400,000 points in `[-50, 50]^3`, worst error 0.088) live in.
|
||
|
||
An earlier version of this proof picked the near-side neighbour on a fixed
|
||
axis instead of whichever axis sits closest to its own boundary, and that
|
||
version is wrong (Soul, cut 2a-i): `u = (0.5, 0, 0)` with the x-neighbour's
|
||
jittered feature at `(-1, 1 - 2^-24, 1 - 2^-24)` gives a distance of about
|
||
2.06, outside the radius-2 exclusion the proof needs — `u.x = 0.5` is the
|
||
point on the cell farthest from any boundary, so `x` was the wrong axis to
|
||
fix there. Choosing the axis by proximity to its own boundary is what keeps
|
||
`m <= 1/2` and the bound at `sqrt(3)`.
|
||
|
||
`CellularAndSminGradTests` pins both the prune's bit-identical equivalence
|
||
to an unpruned 5×5×5 reference and the search's exact agreement with an
|
||
independent 7×7×7 brute-force oracle.
|
||
|
||
Precision domain: `cellular`'s exactness above is a statement about the
|
||
search radius, not about float32. Past `|p|` of roughly `2^23`, adjacent
|
||
integer cells stop being distinguishable from their neighbours at float32
|
||
precision in some directions, and distinct cells' jittered features can
|
||
round onto the same value, making `F1 = F2` ties (and, per-component,
|
||
`F2 = 0`) increasingly common from about `1e6` and confirmed at `2^24`
|
||
(measured: 4096 of 4096 sampled integer points at `2^24` hit this before
|
||
the guard above). `cellular` stays finite there; it does not stay accurate
|
||
in the sense of matching the idealized real-valued Worley field.
|
||
|
||
The lower-bound prune's bit-identity to the unpruned 5×5×5 search also has
|
||
a domain: it holds for `|p| < 2^25`. Above that, `cell + d` (an integer
|
||
cell coordinate plus a small integer offset) starts to round in float32,
|
||
so a pruned cell's own recomputed offset can land on a different neighbour
|
||
than the unpruned loop visits at that same nominal `(dx, dy, dz)`, and the
|
||
two search paths diverge (Soul, cut 2a-i: measured 180 of 1,100,000 points
|
||
differ at `|p| ~= 3.4e7`). This is a float32-precision limit on the prune's
|
||
equivalence proof, not a bug in the prune itself: below `2^25` the two
|
||
paths are proven bit-identical (`PrunedSearchIsBitIdenticalToTheUnprunedFiveCubedSearch`,
|
||
which samples `[-50, 50]^3`, is comfortably inside that domain).
|
||
|
||
- `snoise(float3)` follows webgl-noise `src/noise3D.glsl` as of
|
||
stegu/webgl-noise `22434e04d7`: kernel radius squared `0.5`, output scale
|
||
`105`, permutation `mod289((34x+10)x)`. Upstream's `21d9fe23d7` (2020-10-14,
|
||
"an age-old bug that caused slight discontinuities along simplex
|
||
boundaries") moved the radius from `0.6` to `0.5`; at `0.6` a lattice vertex
|
||
outside the four summed corners can lie inside the kernel, leaving a value
|
||
jump at the cell boundary (3.1e-3 over the committed probe's 400 lines). `ff3b5d34ea` set the scale
|
||
to `105` and `a3e6d57095` (2021-06-30) the permutation; the 2D `snoise` shares
|
||
that permutation helper, so its values changed too, while its own kernel
|
||
(`0.5`) and scale (`130`) already matched upstream.
|
||
`NoiseGradTests.SnoiseAndItsGradientAreContinuousAcrossSimplexCellBoundaries`
|
||
pins the continuity.
|
||
- `snoise_grad(float3 p)` carries `snoise(float3)` to value-and-gradient form,
|
||
following the differentiation in webgl-noise `src/noise3Dgrad.glsl` (Ashima
|
||
Arts / Ian McEwan, MIT), which uses the same radius, permutation and scale as
|
||
`snoise(float3)`: per corner, `m0 = max(0.5 - dot(x,x), 0)`, so
|
||
`d(m0^4 * dot(p,x))/dx = -8*m0^3*dot(p,x)*x + m0^4*p`, summed over the four
|
||
corners and scaled by the same constant as the value, so `.w` is exactly
|
||
`snoise(float3)`. `fbm_grad` and
|
||
`ridged_grad` are octave sums of `snoise_grad`: each octave samples at
|
||
`p * frequency`, so by the chain rule its gradient scales by that same
|
||
frequency, and both amplitude (`gain`) and frequency (`lacunarity`) compound
|
||
per octave rather than applying once to the whole sum. `ridged_grad` folds
|
||
each octave about zero (`Σ aᵢ(1 − |nᵢ|)`, gradient `−Σ aᵢfᵢ sign(nᵢ)∇nᵢ`,
|
||
Musgrave's ridged multifractal, *Texturing and Modeling* ch. 16); the crease
|
||
at `nᵢ = 0` is deliberate (the operator's dune term, 2026-09-25), not a bug.
|
||
Both clamp `octaves` to `[0, 16]`: `HlslSourceCompatibilityTests`'s bit-parity
|
||
oracle drives every mirrored `int` parameter across the full int32 range,
|
||
which is the right domain for `pcg3d`/`pcg4d`'s O(1) hash inputs but would
|
||
turn an unclamped octave count into a multi-billion-iteration loop for these
|
||
two mirrors.
|
||
- `phacelle(float3 p, float3 side, float offset, float normalization)` is Rune
|
||
Skovbo Johansen's Phacelle noise ("phase + cell": a stripe pattern built by
|
||
blending one cosine/sine wave per jittered cell), generalised to 3D cells. It
|
||
returns `CultPhasor { cos, sin }`, each `(∇, value)`. `side` is the stripe
|
||
wave vector, whose length is 2π times the stripe frequency; in 3D the
|
||
perpendicular to the flow is not unique, so the caller chooses it (on a unit
|
||
sphere, `cross(p̂, flow)`). `offset` is the phase in cycles. It visits 4×4×4
|
||
cells with `pcg3d`-hashed jitter in `[-0.5, 0.5)`, weights each wave by
|
||
`max(0, exp(-2d²) - 0.01111)`, and normalizes the blended phasor as upstream
|
||
does (length 1 where the raw length is at least `1 - normalization`, scaled
|
||
by `1 / (1 - normalization)` below). The gradient is exact, weight
|
||
derivatives and normalization included, rather than upstream's
|
||
`∇cos ≈ -sin·side`, which ignores the weight gradient. It is discontinuous
|
||
where the raw length crosses the normalization threshold and where a cell's
|
||
weight leaves its support (d² = 2.24995, distance 1.49998); the value is
|
||
continuous at both. At the support edge `∇w` jumps from `-4·0.01111·v` to 0,
|
||
a per-cell jump of length `0.0444·|v|` (0.0667 at `|v| = 1.5`); measured over
|
||
random points, the output gradient jumps by a median 1.5% of its length and
|
||
at most about 27%. `normalization` is meant to lie in `[0, 1]`: 1 or more
|
||
saturates (every output is stretched to length 1), a negative value scales
|
||
every output down by `1 / (1 - normalization)`, and the normalized gradient
|
||
scales as `1 / |I|` for the blended phasor `I`, so it grows where the blend
|
||
nearly cancels. The total weight `W` is positive for every `pcg3d` input (the
|
||
smallest found by hill-climbing is 0.056); `W = 0`, and a NaN output, needs
|
||
adversarial jitter no hash yields.
|
||
Cells whose per-axis lower bound `Σ max(0, |local - g| - 0.5)²` is at least
|
||
2.25 lie outside the support and have weight exactly 0, so they are skipped,
|
||
which changes no output bit. Cells whose bound sits just under 2.25 can still
|
||
carry a nonzero weight, so the cut cannot be lowered; `PhacelleTests` pins
|
||
the cut with points next to such cells, against an internal overload with the
|
||
prune disabled, and the HLSL mirror test pins it on the shader side. The prune
|
||
keeps about 45 of the 64 cells on average (a saving of about 30%); about 14
|
||
have a nonzero weight. The port sits in its own MPL-2.0 files
|
||
(`math.Phacelle.cs`, `CultPhasor.cs`, `CultMath.Phacelle.hlsl`); see
|
||
`THIRD-PARTY-NOTICES.md`. No 2D variant exists; nothing consumes one.
|
||
|
||
Integer hashing uses the PCG hashes from Jarzynski and Olano, "Hash Functions
|
||
for GPU Rendering" (JCGT 9(3), 2020): `pcg(uint)` is O'Neill's RXS-M-XS 32/32
|
||
permutation over one LCG step, and `pcg3d`/`pcg4d` are the paper's (3 → 3) and
|
||
(4 → 4) hashes. They use only uint multiply, add, xor, and shifts. dxc wraps
|
||
uint multiply and add modulo 2^32 exactly as C# unchecked arithmetic does, so
|
||
they are integer-exact in HLSL and on the CPU, GPU included, unlike the
|
||
`sin`-based `hash`. CultMath has no uint vectors, so `pcg3d(int3)` and
|
||
`pcg4d(int4)` carry uint bit patterns in int components. The `float2`/`float3`/`float4`
|
||
overloads hash the IEEE-754 bits (`asuint`), so -0 and 0 hash differently; they
|
||
exist for callers that already hold a hash key as float bits, and `pcg3d(float2)`
|
||
holds z at 0, which is the paper's route for (2 → N) inputs. Seeds and other
|
||
scalar uses take one output component.
|
||
|
||
`cellular`'s jitter and id always hash the integer cell coordinate directly
|
||
(`pcg3d(int3(neighbor))`, `pcg4d(int4(int3(cellId), 0))`), never a cell
|
||
coordinate's float32 bit pattern. Hashing the bit pattern of an
|
||
integer-valued float is not the same permutation as hashing the integer: two
|
||
adjacent cells' bit patterns share more structure (both have the same
|
||
exponent and a mantissa that differs by one increment) than two adjacent
|
||
integers do going into `pcg3d`'s own mixing, and that residual structure
|
||
survived into the jitter. Hashing the integer cell coordinate directly is
|
||
exact and runtime-independent, and it is the HLSL mirror's own hash, so C#
|
||
and shader agree bit-for-bit. What pins this is the oracle and mirror tests,
|
||
not a uniformity measurement: reverting to hashing the float bits (or the
|
||
sin-based hash) is caught by `ProductionSearchMatchesTheBruteForceOracleExactly`,
|
||
`EveryMirrorFunctionMatchesCSharpMath`, `F1NeverExceedsF2`, and the other
|
||
tests that key off the integer-hash fixtures (cut 2a-i). `CellularAndSminGradTests`
|
||
(`JitterAndIdPassAChiSquareUniformityTest`) checks the uniformity of the hash
|
||
as `cellular` uses it: the jitter marginals and the id pass a chi-square test
|
||
at a stated significance against `pcg3d(int3(...))`.
|
||
|
||
Where HLSL is silent, CultMath keeps its own decisions and does not defer to
|
||
Unity.Mathematics: `hash` returns float, and `Random` is CultMath's own xorshift32. Engine-shaped
|
||
helpers that HLSL lacks (`float2x2.Rotate`, `float3x3.Euler`, `quaternion`,
|
||
`snoise`) enter only when a consumer needs them, with their semantics stated in
|
||
code.
|
||
|
||
Known C# friction: under `using static CultMath.math;` the constructor
|
||
functions (`float3(...)`, `float3x3(...)`) hide the type names in member access,
|
||
so static members need a qualified type (`CultMath.float3x3.Euler(...)`).
|
||
|
||
## Intervals
|
||
|
||
`math.Interval.cs` and `shaders/CultMath.Interval.hlsl` (included from
|
||
`CultMath.hlsl` after the Phacelle include) carry interval arithmetic for
|
||
culling empty space with a proof. Their invariant, **intervals-enclose**: every
|
||
interval function returns a `[lo, hi]` that contains `f(x)` for every `x` in
|
||
its input region, where `f(x)` is the same function evaluated pointwise in
|
||
float32. A consumer that skips a region because `hi < cutoff` has skipped
|
||
nothing that was there.
|
||
|
||
Representation: an interval is `float2(lo, hi)` with `lo <= hi`, both finite.
|
||
There is no empty interval; a caller that needs "provably empty" tests
|
||
`hi < cutoff`. `iv_point(x)` is `float2(x, x)`.
|
||
|
||
Ops: `iv_point`, `iv_add`, `iv_sub`, `iv_neg`, `iv_mul` (the least and greatest
|
||
of the four corner products), `iv_scale(iv, float)`, `iv_abs` and `iv_sqr`
|
||
(an interval straddling zero starts at 0), `iv_min`, `iv_max`, `iv_sqrt`
|
||
(`lo` clamped at 0), `iv_exp`, `iv_clamp(iv, float, float)`, `iv_saturate`,
|
||
`iv_smoothstep(float, float, iv)` (its ordered endpoints) and
|
||
`iv_lerp(iv, iv, float)`. No others until a consumer names one. Each has an
|
||
HLSL mirror named `cultmath_iv_*`, compared bit for bit by
|
||
`HlslSourceCompatibilityTests`.
|
||
|
||
The ulp-widening rule. Each bound is computed with the pointwise function's
|
||
own float32 operations. IEEE round-to-nearest is monotone, so an op that is
|
||
monotone on its region encloses its float32 evaluation exactly, with no
|
||
widening; that covers every op above except two, which widen by exactly one
|
||
ulp, in the implementation and never in the test:
|
||
|
||
- `iv_exp`: `exp` is not required to be correctly rounded, so a libm need not
|
||
be monotone. .NET's `MathF.Exp` on Linux steps down 0 times over every float
|
||
in [-87, 88.7] (`IntervalTests.MeasureMonotonicity`), so on that CPU the
|
||
widening is a margin; it is there for other libms and GPUs.
|
||
- `iv_smoothstep`: `t * t * (3 - 2t)` in float32 steps down 337,095 times over
|
||
the floats in [0, 1], each time by at most 1 ulp (`MeasureMonotonicity`).
|
||
Since `t = saturate((x - minimum) / (maximum - minimum))` is itself monotone
|
||
in `x`, that covers every `smoothstep`.
|
||
|
||
`iv_lerp` is the natural interval extension of `x + (y - x) * amount`, which is
|
||
how `lerp` evaluates; every step is monotone, so it encloses with no widening
|
||
for any `amount`, and it is tight only when `a` is a point, because `x` appears
|
||
twice.
|
||
|
||
`IntervalTests` runs every op over 10,000 seeded intervals (centres in
|
||
[-20, 20], widths log-uniform in [1e-4, 10], every eighth a point) and 16
|
||
points inside each, endpoints and zero included, with no tolerance; and for
|
||
every op it raises `lo` and lowers `hi` by one ulp and requires the harness to
|
||
catch each. A new op is one entry in that table.
|
||
|
||
Noise bounds. `iv_snoise_ball(c, r)` is `[n - L r, n + L r]` intersected with
|
||
`[-1, 1]`, where `n = snoise(c)` and `L = SNOISE_LIPSCHITZ`: one `snoise`, value
|
||
only. No interval function reads `snoise_grad`, and none has a value-only or
|
||
gradient twin. `iv_fbm_ball(c, r, octaves, lacunarity, gain)` is the octave sum
|
||
of `iv_snoise_ball(c * f_i, r * |f_i|)` scaled by `a_i`, compounding frequency
|
||
and amplitude exactly as `fbm_grad` does, octaves clamped to [0, 16]. A domain
|
||
warp that moves a point by at most `D` is enclosed by the radius `r + D`
|
||
(`NoiseBoundTests.WarpedPointsStayEnclosed`); this is what the gamecult.org
|
||
ground's march rests on.
|
||
|
||
`iv_frustum_ball(m_c, z0, z1, footprintPerDepth, warp)` is the ball one probe
|
||
serves a whole screen tile with. In a pinhole camera frame looking down `+z`,
|
||
where the ray of slope `m` is the points `(m z, z)`, it encloses every point of
|
||
every ray whose slope lies within `footprintPerDepth` of `m_c` (`N / (sqrt(2) f)`
|
||
for an `N x N` tile at focal length `f` pixels: the full pixel footprint, so
|
||
sub-pixel jitter is covered), over depths `[z0, z1]`, each point moved by a warp
|
||
of length at most `warp`. The centre is `(m_c z_m, z_m)`, `z_m` the mid-depth;
|
||
the radius is `((z1 - z0) / 2) |(m_c, 1)| + z1 footprintPerDepth + warp`. A point
|
||
of slope `m_c + e` at depth `z` is the centre plus `(z - z_m)(m_c, 1)` plus
|
||
`z (e, 0)`. The returned ball is float32, and on a degenerate (`z0 == z1`),
|
||
thin or far segment the rounding of the centre alone exceeds the gap the
|
||
triangle inequality leaves, so the radius is widened by
|
||
`(|c|_1 + radius) 2^-20`. With `u = 2^-24`, each centre component carries at most
|
||
two roundings (`z_m`, then the product), so the float centre is within
|
||
`2u |c|_1` of the exact one; the radius carries at most seven (`z1 - z0`, the
|
||
axis length at `3u`, the product, `z1 footprintPerDepth`, the sum, `+ warp`), so
|
||
the exact radius is at most `(1 + 7u)` times the float one; the widening rounds
|
||
once more. `2^-20 = 16u` covers `8u (|c|_1 + radius)` twice over, and fma
|
||
contraction only removes roundings. The axis count assumes a correctly rounded
|
||
`sqrt`, which IEEE gives C#. GLSL ES 3.00 does not: it inherits `sqrt`'s
|
||
precision from `inversesqrt`, and D3D-backed compilers may reassociate. So the
|
||
bound is proven for C# and HLSL on IEEE hardware, and on GLSL it is measured:
|
||
on a GTX 1070 under ANGLE's D3D11 backend the fixture's 256 cases keep a
|
||
minimum relative slack of `1.98e-6` (about `33u`), the same as C#'s. The
|
||
fixture's enclosure check is what measures it on each device. The caller rotates and translates the centre into world space and scales
|
||
centre and radius by its noise frequency; the rounding of that transform is the
|
||
caller's. `NoiseBoundTests.TileBallEnclosesEveryRaySegment` checks 2,000 seeded
|
||
tiles x 64 points of the r2 domain and 2,000 tiles x 8 corner points of each of
|
||
seven extreme families (`z0 == z1`, `z1 - z0 = 1e-6 z1`, `|m_c|` to 1000, `z` to
|
||
`1e7`, `z0 = 0` with `z1` to `1e-3`, `f` down to 1, warp to `1e6`), corner flows
|
||
full-length and outward, distances in double, with no tolerance. It pins the ball
|
||
to the derivation both ways (`r <= ball.w <= r + 2^-19 (r + |c|_1)`), so a ball
|
||
that forgets the widening fails and one grown past it fails too. A moving camera needs nothing
|
||
more: each frame's probes use that frame's camera, and no ball, mask or probe
|
||
result is carried from one frame to the next.
|
||
|
||
The noise ball alone proves little: at `L` near 10 it bounds `snoise` to a width
|
||
of `2 L r`, which covers all of `[-1, 1]` once `L r` reaches 1. Empty space
|
||
becomes provable through an analytic envelope. The consumer's density is a
|
||
pre-distortion SDF (a height fog, a well map, a carved sphere) that noise
|
||
displaces only near its surface, and the consumer composes the bound from `iv_*`
|
||
ops. The envelope is taken over the slice's exact box (`iv_mul` of the depth
|
||
range by each slope range, then the SDF's own monotone pieces), together with its
|
||
fade. The noise is `iv_snoise_ball` over `iv_frustum_ball`, scaled by the noise
|
||
frequency. The warp moves only the noise argument, so it enlarges the ball and
|
||
the envelope's box takes none of it. Where the fade's upper end is 0 the noise
|
||
term is identically 0 and no `snoise` is evaluated. Where the envelope already
|
||
proves the slice empty with the noise at its full range, the ball is not
|
||
evaluated either. A probe is worth attempting only when the best result any
|
||
centre value could give would prove the slice, and a ball with `L r >= 2` is
|
||
`[-1, 1]` whatever `snoise(c)` is, so it costs nothing. CultMath owns none of the
|
||
envelope: the interval files have no envelope function, and each consumer composes
|
||
its own. `NoiseBoundTests.EnvelopeBoundEnclosesDensity` checks that composition,
|
||
the one the site and Aetheria use, over 2,000 seeded slices x 64 warped points
|
||
with no tolerance.
|
||
|
||
`L` has provenance, not a proof. `NoiseBoundTests.MeasureLipschitz` (slow,
|
||
explicit) takes the largest `|snoise_grad|` over 1e6 seeded points in
|
||
[-256, 256]^3 (7.1933103), refines the 1e4 largest by gradient ascent
|
||
(7.2400064), and multiplies by 1.10: `L = 7.9640074`. It also reaches
|
||
`|snoise| = 0.9718335` by the same ascent on the value, which is what lets the
|
||
bound intersect with `[-1, 1]`. `LipschitzConstantPinsSampledGradients`
|
||
checks 1e5 more seeded points against `L` and climbs again from the start
|
||
`MeasureLipschitz` prints as its witness, failing if `L` is not 1.10 times that
|
||
maximum to within half a percent, so a 1% move of `L` fails. The enclosure tests
|
||
over 2,000 balls x 64 points are the defence. A change to the `snoise` kernel
|
||
must re-run `MeasureLipschitz` and re-pin `L` and the witness.
|
||
|
||
What the ball bounds guarantee: enclosure of `snoise` and `fbm_grad(...).w` over
|
||
the ball, in float32. What they do not: anything about tapes, pruning, choice
|
||
tracking or affine forms; those are later steps of
|
||
`docs/cultmath-tape-target.md` (CultLib root). How loose they are is a number:
|
||
`NoiseBoundTests.TightnessReport` prints the mean interval width over the
|
||
sampled range: 5.08 for `iv_snoise_ball` at `r = 0.05`, 1.96 at 0.25 and 1.28
|
||
at 1.0; 6.64 and 2.82 for 4-octave `iv_fbm_ball` at 0.05 and 0.25.
|
||
|
||
The saving is measured where there is empty space to find.
|
||
`NoiseBoundTests.IntervalSkipHalvesEvaluations` draws screen tiles, each with
|
||
its own camera, and runs one pre-pass per tile over the dense grid's cell ranges:
|
||
it starts with the whole grid, skips and doubles a range it proves empty, halves
|
||
one it cannot, and runs to the grid's end. It then draws the tile's `N^2` ray
|
||
slopes, one per pixel, jittered inside it. Each ray is marched three times with
|
||
the same integrator and the same early-out at transmittance 0.02: over every
|
||
cell (dense), over the tile's unproven cells (the tile march), and over the
|
||
oracle mask, the cells whose mid-depth density is nonzero for at least one of
|
||
the tile's rays. The tile march and the oracle march integrate the dense march's
|
||
nonzero cells, so their transmittance agrees with it exactly in every
|
||
configuration. The oracle is a ceiling the pre-pass cannot move: dense cost over
|
||
oracle cost. Against it the test prints the efficiency (oracle cost over the
|
||
tile march's cost with its probes) and the cull fraction (the oracle-empty cells
|
||
the pre-pass proves empty). The probe overhead is the tile march's cost over that
|
||
cost plus the probes'. Cost is counted in `snoise` evaluations, with an envelope
|
||
evaluation or probe weighted 0.2. That weight is an estimate, and both counts are
|
||
printed. The flow is not counted. At `N = 8`, in Aetheria's units on its
|
||
256-cell quadratic grid out to the Main Camera's far plane, 2048:
|
||
|
||
- (a) Height fog, camera above the safety band, rays level and up: the whole
|
||
grid is one envelope probe per tile, against 256 envelope evaluations per ray,
|
||
16384x. Every cell is oracle-empty and the pre-pass proves them all.
|
||
- (b) Inside the fog: 1.15x, and 0.99x with the warp. Most cells are fog, and
|
||
the pre-pass probes each one.
|
||
- (c) The zone bowl and four wells, the camera gazing across. With no warp:
|
||
2.74x, oracle ceiling 7.26x, efficiency 0.378, cull fraction 0.952, probe
|
||
overhead 0.961. With the warp `D = 60`: 1.39x, oracle ceiling 7.17x,
|
||
efficiency 0.194, cull fraction 0.842, probe overhead 0.993. With the warp the
|
||
pre-pass cuts envelope evaluations from 177.46 to 33.49 per ray, but the
|
||
oracle march costs 0.194 of the tile march, so about four fifths of the tile
|
||
march's cost goes to cells no ray samples nonzero: the warp bound swamps the
|
||
noise ball, so the ball cannot prove those cells empty. That gap belongs to
|
||
the tape target's affine forms, not to a better pre-pass.
|
||
- (d) r1's uniform slab, which has no envelope: 1.23x, and 1.48x with no warp.
|
||
- (e) The void at the shipped point (ruling `operator-shipped-void`): `Rh = 198`,
|
||
`S = 50`, `Rc = Rh + S = 248`, `e = -ln 1.5 / ln(1 - (Rh / Rc)^2)`, the
|
||
shipped camera, 200 tiles. The fixed-step reference takes 36.34 steps per
|
||
pixel. The same cells masked by the pre-pass take 20.13, with identical
|
||
transmittance. The footprint-aware march takes 13.54: 2.68x fewer. `snoise`
|
||
per pixel goes from 32.98 to 26.69, and combined cost from 40.25 to 29.52 with
|
||
the probes, 1.36x. Every one of the 200 tiles proves a body. Before the body
|
||
both marches are measured against a converged one (midpoint quadrature at
|
||
`h = 0.5` over every cell, trusting no pre-pass): the relative optical-depth
|
||
error at the body start is at most 0.0608 for the fixed-step march and 0.08639
|
||
for the footprint march, 1.42x. Over the grid of hollow radius, camera offset
|
||
and ramp width the steps ratio runs from 1.57x (`Rh = 100`, low, `S = 150`) to
|
||
27.45x (`Rh = 1000`, centred, `S = 10`), the cost ratio from 1.26x to 3.89x,
|
||
and the depth-error ratio from 0.85 to 4.72 (`Rh = 200`, centred, `S = 150`,
|
||
where the errors are 0.01504 and 0.003188). LOD-far takes 14.19x fewer steps;
|
||
its depth errors, 0.2849 and 0.205, are not comparable, because its
|
||
fixed-step reference keeps the octaves the footprint march truncates.
|
||
|
||
The analytic body (ruling `operator-analytic-wall-body`). When a single dense
|
||
cell's own bound is a positive point, the pre-pass spends one more probe over
|
||
the rest of the grid. If that bound is a positive point too, every point of
|
||
every ray of the tile from there on has exactly that density, so the footprint
|
||
march takes one closed-form step, `exp(-BodyDepth)`, from the body start to the
|
||
grid's end and stops. `BodyDepth` is density x extinction x length x `|dir|`.
|
||
`NoiseBoundTests.AnalyticBodyMatchesFineMarch` pins it: the bound, the mask and
|
||
the pointwise densities agree exactly at the body, and `BodyDepth` equals the
|
||
summed fixed-step depth of the body cells within 1e-4 relative. The comparison
|
||
is in the depth domain, so LOD-far's deep bodies do not underflow.
|
||
|
||
The footprint-truncated void weights its octaves by `w_i(z)`, decreasing in `z`.
|
||
Its bound takes each weight over `[w_i(z1), 1]` rather than
|
||
`[w_i(z1), w_i(z0)]`, because one bound serves both the full field (`w = 1`) and
|
||
the truncated one. `[w_i(z1), 1]` contains both, and `iv_mul` is monotone in its
|
||
interval argument, so the composed interval encloses both fields.
|
||
`NoiseBoundTests.WeightBoundEnclosesOneSignedNoise` checks it on LOD-far over
|
||
2,000 segments: each weight lies in its bound, and `w(z) n` lies in
|
||
`iv_mul(bound, n)` for a one-signed `n`. A lower end of `w(z0)` fails it.
|
||
|
||
The contracts, at `N = 8`, the lower of warp 0 and `D`:
|
||
|
||
- (a) at least 2x combined cost;
|
||
- (c) (ruling `wells-overhead-plus-floor`): probe overhead, the tile march over
|
||
the tile march plus its probes, at least 0.90; and, hard, the pre-pass proves
|
||
at least half of the oracle-empty cells empty, so a pre-pass that skips
|
||
nothing fails. The oracle ceiling, the efficiency against it and the cull
|
||
fraction are printed, not asserted;
|
||
- (e) at the shipped void: the masked fixed-step march agrees with the reference
|
||
exactly; the footprint march's optical depth at the body start is off the
|
||
converged march's by at most twice the fixed-step march's error; and it takes
|
||
at most half the reference's steps per pixel.
|
||
|
||
This run meets every contract: (a) 16384x, (c) probe overhead 0.961 and cull
|
||
fraction 0.842, (e) 2.68x fewer steps and a depth error 1.42x the fixed-step
|
||
march's. The fields, the grids and the 0.2 weight are not tuned toward the
|
||
contracts.
|
||
|
||
Consumers: the gamecult.org ground shader (through the GLSL lowering) and
|
||
Aetheria's nebula raymarch at its CultMath pin bump.
|
||
|
||
## Rules
|
||
|
||
- Keep type names HLSL-shaped: `float2`, `float3`, `float4`, `float3x3`, and `math`.
|
||
- Keep Rust as the owner of native hot-path kernels and parity fixtures.
|
||
- Keep C#, shader, and scripting surfaces as runtime wrappers, not competing
|
||
math authorities.
|
||
- Keep `shaders/CultMath.hlsl` as the canonical shader mirror for shared
|
||
CultMath functions HLSL does not already provide (HLSL intrinsics such as
|
||
`mul`, `any`, and `log` need no mirror). Project-local shader helpers should
|
||
move here once more than one organ needs them or once CPU/GPU parity matters.
|
||
- Implement public shader semantics directly; do not copy Unity.Mathematics.
|
||
- Prefer plain mutable value types with public component fields, as HLSL does.
|
||
- Prefer component-wise overloads over clever generic machinery until the API
|
||
earns that complexity.
|
||
- Keep the surface boring and exact. Fill what a consumer uses, guided by HLSL
|
||
semantics; noise primitives, matrices, quaternions, and packing helpers need an
|
||
owner before they enter.
|
||
- Keep the core assembly engine-free. Unity conversions live in the Unity
|
||
package's `CultMath.UnityBridge` assembly.
|
||
|
||
## Non-Goals
|
||
|
||
- Compiling C# to shaders.
|
||
- Generating HLSL from Rust or Rust from HLSL.
|
||
- Replacing `System.Numerics` for general .NET work.
|
||
- Recreating every Unity.Mathematics type because a spreadsheet somewhere says
|
||
“coverage.” That way lies decorative obesity.
|