| A | Hand-port the needed functions to GLSL inside gamecult-site | A second noise authority with no parity test; the exact thing Aetheria's `GPU Noise` copies are (follow-up `aetheria-snoise-pin-bump`). |
| B | `dxc -spirv` then SPIRV-Cross to GLSL ES, using the existing `tools/compile-hlsl-spirv.ps1` chain | SPIRV-Cross emits one whole shader with mangled names; there is no library a site shader can call `cultmath_snoise` from. |
| C | **A textual lowering HLSL → GLSL in CultLib with a complete, enumerated transformation list, the discipline of the HLSL → C# mirror (`packages/cultmath/docs/design.md:92-134`)** | Chosen. |
### What Aetheria contributes, and what the web version keeps
| Aetheria | Mechanism | Web subset |
| --- | --- | --- |
| `Volumetric.cginc:165-221` density | fill + coverage, domain-warped by `flow()` in two half-period phases cross-faded with `tri2` (a flow map; the advection never accumulates) | kept, one coverage octave and one detail octave, CultMath noise |
| `Volumetric.cginc:130-135``globalFlow` | `Tri3D(p/scale − scroll) · amplitude` | replaced by two `snoise` at low frequency, rendered once per frame into a quarter-res flow texture |
| `CloudShader.shader:261-314` TAA | reprojection by `_PrevVP`, γ=0.5 variance clip, fixed 5% blend, packed depth, the `density2` bug at `:311` | not kept; see TAA |
| `Stardust.compute:118-147` | stateless particles: grid cell hashed to a spawn point, `position −= flow · lifetime · period`, size `parabola(lifetime, 2)`, colour from a hue ramp × nebula tint | kept as an instanced vertex shader reading the flow texture; `gl_InstanceID` is the cell |
| `DepthOfField.hlsl:29-32``FragCoC` | the CoC is read straight from a global `_DoFBlurTex` (its writer is not in this checkout; the operator recalls final transmittance), then the bokeh gather `FragBlur``:150-201` | a gather on the stardust layer with radius ∝ cloud opacity at that pixel |
| Tile pre-pass (pass 3), 1/64 res, `N = 8` | 40 k tiles × ≈ 30 probes in the cavity (interior retired in a few, the wall's ≈ 15 cells at 1.5 probes each), envelope-only because the noise ball cannot prove anything at this scale; ≈ 0.1 snoise-equivalents per pixel; worst case ≤ 192 probes per tile | 0.1 (≤ 0.8) |
| March at `S = 8` | 2.6 M × 8 snoise per frame, every pixel marches a wall; LOD and the mask decide where the frame's samples land, not how many (the controller does) | 3.3 |
| TAA resolve + DoF gather | ~19 texel reads per pixel, plus the depth-history fetch and one matrix multiply for the camera reprojection | 1.1 |
| Sun self-shadowing sample (named, not taken) | one envelope-only transmittance sample per primary sample, ≈ +25% of the march | +0.8, not on the phone |
On the march: "I'm skeptical about the interval march, both in implementation
and measurement. Are we testing it in scenarios where we're actually giving
it empty space to skip? Surely if the pre distortion SDF represents a clear
sky with the camera above the fog and aimed up, the interval math can quickly
tell us not to bother marching very far, no? In Aetheria the levels are
authored to place deep gravity wells for the player to gaze across, with
dynamic action spaced around the well, and finding the empty space there is
the whole point of using intervals, no?"
He is right. r1's saving test marched one `snoise` octave against a cutoff
through a slab with coverage everywhere: the worst case for intervals, and
0.93x said nothing about real scenes.
### What r1 measured, and what was wrong in the march
| Finding (`NoiseBoundTests.cs:344-376` at `c9bd003b`) | Effect | r2 |
| --- | --- | --- |
| Every probe charged one `snoise` per ray | a failed probe cost a third of a dense sample | one probe per tile (the ruling) |
| Coarse probes attempted where the ball could never prove emptiness: `hi = n(c) + L r` clamps to 1, so `L r F0 ≥ 1 + cutoff` is a guaranteed failure; at `StepMax = 2`, `L r F0 = 3.7` | every coarse probe wasted; the 0.74x row | a noise probe is attempted only when `L r F0` leaves room to prove `hi < cutoff`; otherwise the level is "maybe" for free |
| Growth capped at `StepMax = 8 Step0` | clear space cost `Depth / StepMax` probes | ranges double to the grid's end |
| The dense sample evaluated the detail octave even at zero occupancy | the baseline was dearer than a sane shader | coverage first, detail only when density can be nonzero (Aetheria's `if (dist < _SafetyDistance)`) |
| The finest-level probe before each dense cell | fine; amortized it is `1/N²` for a chance at 3 snoise | kept |
| No field structure | nothing to skip | envelope-then-noise, below |
### The field, from Aetheria
`Volumetric.cginc:165-221` with `:91-102`, `Zone.cs:369-397`
fade(s) = 1 − smoothstep(0.75 S, S, s) S = SafetyDistance 30: no noise above it
s'(p) = s + A · fade(s) · n(warp(p)) n = the two-phase warped coverage + ½ detail
density = max(0, (F − s') / B) F = FloorOffset −20, B = FloorBlend 10
```
Above the safety distance the density is exactly 0 and the noise is never
evaluated, by the dense march too. The warp applies to the noise argument
only; the height is read at the unwarped `xz` (Aetheria reads
`_NebulaSurfaceHeight` at `pos.xz` and warps inside `triNoise3d`).
### The composed bound (invariant `intervals-enclose`, kept)
Over a tile's frustum slice `[z0, z1]`, every step an existing `iv_*` op:
- Envelope over the slice's exact box, not the ball: `y = iv_mul([z0, z1],
m_y)`, `x = iv_mul([z0, z1], m_x)` with the tile's slope intervals;
`iv_h` = Σ over bowls of `[pulse(far), pulse(near)]`, `near`/`far` the
least and greatest distance from the box to the bowl centre (monotone, so
exact per bowl). `iv_s = iv_add(y, iv_h)`; `iv_fade` its ordered endpoints.
- Noise over the ball: `iv_n = iv_snoise_ball(ball.xyz · F0, ball.w · F0)`,
`ball = iv_frustum_ball(m_c, z0, z1, N / (√2 f), D)`: centre on the tile's
central ray at mid-depth, radius `(Δ/2)·|(m_c, 1)| + z1·N/(√2 f) + D`.
The warp enlarges only this radius; the envelope's box takes none.
-`iv_s' = iv_add(iv_s, iv_scale(iv_mul(iv_fade, iv_n), A))`; the slice is
provably empty iff `iv_s'.lo ≥ F`. When `iv_fade.hi = 0` the noise term is
identically zero and `iv_snoise_ball` is not called: the envelope proves
the slice empty at no `snoise` cost. This is why tiles compound: one
envelope probe per tile can retire a whole ray length.
Two tests pin it: `TileBallEnclosesEveryRaySegment` (every ray's points,
sub-pixel jitter and warp included, inside the ball) and
`EnvelopeBoundEnclosesDensity` (the composed bound encloses `density` at
warped points of 2,000 slices, no tolerance).
### Scenarios and predictions (combined cost, `e = 0.2` snoise per envelope evaluation, `N = 8`)
| Scenario | Setup | Prediction |
| --- | --- | --- |
| (a) height fog | `h = 0`, camera at `y = S + 30`, rays level and up | > 50x: dense pays 256 envelope evaluations per ray; the tile about ten probes shared by 64 rays; 0 snoise either way |
| (b) inside the fog | `h = 0`, camera at `y = F − 10`, rays level | 0.8–1.0x: every ray saturates in a few cells; the pre-pass still probes the whole grid |
| (c) Aetheria-like | zone bowl `R = 2000` + four wells (masses 100, 1000, 10000, 1000), camera above the fog in the bowl, rays gazing across (`m_y ∈ [−0.25, 0.1]`), Aetheria's units and 256-sample quadratic grid | 2–4x: the clear cells before the fog surface collapse to a few probes per tile; the fog cells cost the same on both sides. The noise ball proves nothing at Aetheria's scale (`L r F0 ≥ 1.6` at the finest cell with `D = 60`), so it is not evaluated; the saving is the envelope's |
| (d) uniform slab | r1's field, kept as the worst case | 1.3–1.5x: the ceiling at `L = 10.099261` with the cheaper baseline |
| (e) the void, the shipped scene | the true cavity, hollow radius `Rh = 198`, ramp `S = 50`, camera 186 from the centre, low; every ray crosses interior and ends in a wall; three marches (fixed-step reference, masked fixed-step, masked LOD) | **steps per pixel ≈ 35 → 6 (5–8x fewer), combined cost 1.7x, snoise alone 1.25x**, from the model below. The noise ball proves nothing (`L r F0 ≥ 1.5`); the interior is retired by the envelope, the ramp is sampled at the content limit |
`Assets/Scenes/Main Menu.unity`, the `Sun` prefab instance under a `Center`
pivot, `Materials/Brushes/Gravity Well.mat` and
`Main Menu Sun Fog Tint.mat`, `Fog Marching Main Menu.mat` on
`VolumeRender.shader` of that commit. What survives, in its units:
| Element | Original value |
| --- | --- |
| The brush | `PowerBrush``(1 − x²)^16`, depth 40, radius 256 (half depth at r = 52; a Gaussian of σ ≈ 45); the sun's own gravity-well child. A boundary bowl (depth 75, power 0.25, radius 1500) is flat across the scene: a constant 75 |
| The floor | offset −16.46, blend 39.6: the fog top is `y = −91` outside the void, `−131` at the sun |
| The sun | sphere radius 12.5 at `y = −130`: the bottom of the void |
| The orbit | `Center` rotates 5°/s about y (72 s period); the sun at radius 175 |
| The camera | Cinemachine vcam following the sun (framing transposer, distance 150, damping 10, handheld noise amplitude 1 at 0.25 Hz), looking at `Look Target``(0, −130, 0)`, the orbit centre at the sun's depth; saved at `y = −27` |
| The light | authored, not marched: the sun's tint brush `(1 − x²)^3`, depth 2, radius 128, orange `(1, .4, .1)`, plus a scene-wide blue ambient tint `(0.18, 0.59, 1)`× 0.01 |
| Stardust | 256² particles, spacing 2, floor −60, ceiling 0, height exponent `800 / (ceiling + wellDepth)`: more headroom where the well is deeper, so the void fills with stardust |
| Noise | strength 150, frequency 0.01, safety 50 |
Not surviving: the 2021 `d()` displacement details (the 2021-05-30 "Volumetric
Two things the pass 3 model got wrong, both now owned by r3: the footprint
step kept its content limits (`S/4`, the detail half-wavelength 12.5) inside
the wall's constant-density body, which the model had costed at two steps; and
the noise band does not end at `Rc`: with `A = 20` and the free noise interval
`[−1.5, 1.5]`, the density is provably `K` only from `d ≥ Rc + 30`. The ruling
answers the first; the second is content and stays.
### Soul's findings and what r3 does with each
| Finding | Severity | r3 |
| --- | --- | --- |
| `frustum-ball-float-crossing` | Medium | the radius is widened by a derived float32 bound (below); seven extreme families join the enclosure test; the strictness sentence leaves the C#, the HLSL and `design.md` |
| `aetheria-far-plane-miscited` | Low | `far = 2048`, the Main Camera's (`ARPG.unity:31777`, `:31830`); the Tint camera's 1000 was the wrong camera |
| `shipped-void-two-scenes` | Medium | one shipped void, `(Rh, S) = (198, 50)`, `(Rc, e) = (248, 0.40)` derived; the map, the site-ground spec and the test agree; the look question was Q9, ruled `operator-shipped-void` |
| `lod-weights-never-exercised` | Low | configuration "LOD far" (`Rh = 4000`, `S = 50`, low camera, `f = 467`) reaches weights in `(0, 1)` and at 0; the `[w(z1), 1]` bound is recorded as a deviation with its soundness argument |
| `stryker-timeout-survivor` | Low | 2525 named equivalent; ball radii to `1e3` so `L/r` dies to an enclosure test; every timeout hand-run; the report claims only demonstrated kills |
### The float widening rule (finding 1)
`iv_frustum_ball` returns a float32 ball; its promise is that every exact
point `(m z, z)` of every ray of the tile, `m` in the footprint, `z ∈ [z0,
z1]`, moved by at most the warp, lies inside it. Soul showed the r2 ball misses
such points by up to `5.7e-4 r` when `z0 == z1`, `3.3e-4 r` at `z ≈ 1e7` and
19% of `r` at `|m_c| ≈ 1000`: the triangle inequality is an equality on the
degenerate slice and the centre's own rounding crosses it. The rule, in C# and
Every variant's transmittance equals the dense march's (max |dT| 0). No
variant's pre-pass proved a cell empty where any of the 225 samples per cell
was nonzero (0 enclosure violations).
What this establishes:
1.**The oracle cannot be reached by any sound bound.** The oracle mask
samples one depth per ray. A bound must cover the whole slice of the tile,
so the best any exact enclosure could do is about 0.86 of the oracle. That
matches the 0.57-0.85 that conservative visibility methods reach against
exact sets (`docs/research/culling-efficiency-oracle-prior-art.md`). An
oracle target of 0.90 is unreachable by construction.
2.**The warp diagnosis was half right.** At warp `D` most of the gap is the
warp enlarging the radius by its maximum (V0 to V2: 0.194 to 0.347). That
is fixed by evaluating the flow at the ball's centre and enlarging the
radius only by how far the flow can move across the ball. It takes no
affine noise, and it costs one more snoise per probe, for the second phase
ball. The detail octave's spurious warp (V1) is worth little on its own.
3.**Affine noise buys the rest:** the centred form, then the shared depth
symbol. Against the centred interval, affine with one symbol closes about a fifth
of the oracle gap at warp 0 and at warp `D` (efficiency 0.368 to 0.511 and
0.347 to 0.477). Its combined cost ratio is 1.39x and 1.37x the centred
interval's, and 1.35x and 2.46x the landed interval's. It reaches 0.59 and
0.56 of the range ceiling.
4.**Three symbols buy about 5% over one** (0.534 against 0.511, 0.502
against 0.477). One symbol fits a `float3`. Three need a struct or a
`float4` plus a `float`, and the HLSL mirror's harness compares struct
returns but has no struct parameters (`packages/cultmath/docs/design.md:126-135`).
5.**The probe overhead falls** from 0.96 to 0.86 under every centred or
affine bound. Each probe costs a gradient and proves more, so the total
cost still drops by 1.35x to 2.46x. Today's (c) assertion, overhead at
least 0.90 (`NoiseBoundTests.cs:1558`, `:1588`), would reject the cheaper
march. That is question `affine-wells-contract`.
### The model
An affine form is a transient value, not a persistent kind. The persistent
things this pass adds or changes:
| Thing | Identity | Lifecycle | Authority |
| --- | --- | --- | --- |
| `SNOISE_HESSIAN` (C#), `CULTMATH_SNOISE_HESSIAN` (HLSL, lowered to GLSL) | The constant's name; one value in `math.Affine.cs`, mirrored | Measured once by `NoiseBoundTests.MeasureHessian` (slow, explicit), as `SNOISE_LIPSCHITZ` is. It is re-measured and re-pinned whenever the `snoise` kernel changes. | `MeasureHessian` sets it; `HessianConstantPinsSampledCurvature` pins it; no consumer carries its own |
| `af_*` families in `fixtures/glsl-parity.json` | Family signature string, appended after the last existing family | Regenerated only by `CULTMATH_WRITE_GLSL=1`; earlier cases stay byte-identical | `GlslMirrorTests` generator |
| The (c) contracts | `IntervalSkipHalvesEvaluations` assertions | Changed only by a ruling | Operator (question `affine-wells-contract`) |
| `SNOISE2_LIPSCHITZ` (C#), mirrored in HLSL | The constant's name in `math.Interval.cs`, beside `SNOISE_LIPSCHITZ`, with an `iv_snoise_ball(float2, float)` overload | Measured by `MeasureLipschitz2` (slow, explicit) and pinned, as `L` is | CultMath. The (c) field's flow is `snoise(float2)`, and a bound on a CultMath function belongs to CultMath, not to a test-local helper. A consumer's own flow, such as the site's texture, keeps its own variation bound |
### Representation
**A reduced affine form with one shared symbol: `float3(x0, a, e)`, meaning
`x0 + a·ε + e·δ`.** Here `ε ∈ [-1, 1]` is shared by every form over the same
region, and `δ ∈ [-1, 1]` is a private error, with `e ≥ 0`.
- This is Messine's AF1 as Gamito and Maddock use it for gradient noise,
`t0 + t1 e1 + t2 e2`.
- Sharp and Jacobson's measurements show that this fixed-symbol form is the
fast one on GPUs: 1.0x against 8.4x for full affine.
- It fits one `float3` register, as an interval fits a `float2`. It needs no
struct parameter, so the mirror, lowering and fixture machinery take it
unchanged.
- V4 against V5 prices the two extra symbols at about 5% efficiency.
For a tile probe `ε` is the depth parameter: `z = z_m + h ε` with
`h = (z1 - z0) / 2`. The footprint goes into the error. For a single ray it is
Gamito's form exactly.
A point in space is not a new type. It is the three arguments
`(centre: float3, axis: float3, radius: float)`, the set
`{centre + axis ε + w : |w| ≤ radius}`. `af_snoise` and `af_fbm` take a point
in that form, and `af_frustum_axis` and `af_frustum_ball` produce one for a
tile slice.
Ops, named by the (c) consumer, plus `af_mul` because affine arithmetic is
defined by its product, as `iv_mul` is for intervals:
-`af_point`, `af_symbol(x0, a)`, `af_from_iv`, `af_range` (the bridge to
| Snoise kernel | `src/CultMath/math.cs` (`snoise`, `snoise_grad`) | library, C# | Ties in `x0` break in Gustavson order (x over y, y over z, x over z), a total order. Mirrored in `shaders/CultMath.hlsl` and the Unity copy. |
| Lipschitz constant | `math.Interval.cs``SNOISE_LIPSCHITZ = 7.9640074f`; `CultMath.Interval.hlsl`; generated GLSL | library | Derived from the kernel by `NoiseBoundTests.MeasureLipschitz` (seed 0x11B5): refined 7.2400064 x 1.10. A kernel change re-pins it. |
| Hessian constant | `math.Affine.cs``SNOISE_HESSIAN = 56.050385f` | library | Unchanged by the tie cut. Provenance in `design.md` "Affine forms". |
| Affine ops | `math.Affine.cs`: `af_point`, `af_symbol`, `af_from_iv`, `af_range`, `af_add`, `af_sub`, `af_neg`, `af_scale`, `af_add_iv`, `af_mul`, `af_snoise`, `af_fbm`, `af_frustum_axis`, `af_frustum_ball`; plus 2D `iv_snoise_ball(float2, float)` in `math.Interval.cs` | library, C# | Plain value functions over `float3` (centre, coefficient of the one shared symbol, radius of the independent remainder). No struct state, no `Expr` coupling. |
| GLSL | `shaders/CultMath.glsl` (and `CultMath.Phacelle.glsl`) | derived | Generated by `GlslLowering` (test project) from the HLSL files. |
| Fixture | `tests/CultMath.Tests/fixtures/glsl-parity.json` | derived | Generated by `GlslMirrorTests.GoldenFixtureMatchesCSharp`. Families: the earlier ones, the tie families (after `phacelle`), `iv_snoise_ball` for vec3 and vec2, every `af_*`, and small-reach `af_snoise` and `af_fbm` families that exercise the centred branch. |
| Test harness | `tests/CultMath.Tests/AffineTests.cs`, `NoiseBoundTests.cs`, `GlslMirrorTests.cs`, `HlslMirrorTests.cs`, `NoiseGradTests.cs` | test project | `FogField` (with `DeepWell`, `Wells`, `InsideFog`, `AetheriaWarp`), `NoiseArg`, `AffineSlice`, `BoundMode` and the gates live here, not in the library. They stand in for the site ground and Aetheria. |
| Explicit measurements | `NoiseBoundTests``[Fact(Explicit = true)]` facts: the (c) rows and `DeepWellCullMap` (PNG into the directory named by `CULTMATH_WRITE_CULLMAP`) | test project | Printed measurements, not state. Their numbers are copied into `design.md` by hand. |
### Inputs and outputs
-`af_snoise(centre, axis, radius)` takes the noise argument as a centre, the
direction of the one shared symbol and a ball radius. It reads `snoise_grad`
(`.w` equals `snoise` bitwise), `SNOISE_LIPSCHITZ` and `SNOISE_HESSIAN`, and
returns a `float3` form: the Lipschitz form or the centred Taylor form,
whichever is narrower (`design.md` "Affine forms"). `af_fbm` composes it per
octave. `af_range` turns a form into a `float2` interval and finds a NaN
endpoint by bit pattern, because dxc's fast-math compare folds `x != x`.
-`af_frustum_axis` and `af_frustum_ball` build the symbol and ball for a tile
slice of the view frustum. A consumer rotates and translates the centre into
world space and scales by its frequencies.
- Interval consumers keep `iv_*`; the two representations meet at `af_from_iv`
and `af_range`.
### Derived state and forbidden writers
-`CultMath.glsl`, `CultMath.Phacelle.glsl` and `glsl-parity.json` are never
hand-edited. The only writer is `CULTMATH_WRITE_GLSL=1 dotnet test --filter
GlslMirrorTests` from `packages/cultmath/tests/CultMath.Tests`. The tests
compare committed text with the lowering and committed families with C#'s
picked `skein-web`, so fauna needs all five CultMath gaps that section names.
The as-built forbidden writer "an evaluator over `CultMath.Expr`" (Pass 5 and
the as-built section above) is lifted for the point evaluator and the HLSL
interpreter only. Interval and affine evaluation over the graph belong to pruning
and stay parked.
### Body facts (Pass 7)
| # | Question | Result |
|---|---|---|
| T1 | Does `CultMath.Expr` exist? | No. `git log --all -S "CultMath.Expr"` in CultLib finds only two map commits (`32ba1bfa`, `4b34b12b`); in Aetheria, nothing. `packages/cultmath/src/CultMath/` has no `Expr` folder. It exists as ruling `material-ir-cultmath-now` and spec `cut-expr-ir.r1` (base `a7966142`), which no report cites. |
| T2 | Is the material consumer that shaped `expr-ir` r1 live? | Not now. `aetheria-release:ruling:demo-material-scope` (2026-10-06): demo hulls ship baked maps, "the material graph comes after the demo". The cones, frontiers and HLSL emitter of r1 served incremental per-ship bakes (`materials-baked-per-ship-incremental`) and generated kernels (`material-mod-graphs-same-lowering`). |
| T3 | Does `expr-ir` need `cycles-converters`? | Only for `cycles_*` ops. The trophic graph uses none (Aetheria `trophic_probe.py` at `4354ef2c`: arithmetic, smoothstep, exp, hypot, floor, sin, hashing, Voronoi, segment distance). |
| T4 | Who owns CultMath values' wire shape? | `GameCult.Caching.MessagePack`: `CultMathResolver` is the canonical MessagePack shape of every public CultMath value type, in the default chain (`CultDocumentMessagePackSerialization.cs:61`). CultMath references nothing (`CultMath.asmdef``references: []`). |
| T5 | What do the coverage tests demand of a new public type? | `CultMathSerializationTests.cs:187-196` and `CultMathJsonTests.cs:93-99,210-218`: every exported non-enum struct in the CultMath assembly has a MessagePack formatter, a JSON converter and a dictionary-key round trip, and `CultMathResolver` formats nothing else. A public struct in `CultMath.Expr` would have to be formatted; a class or enum is not covered, and `CultMathResolver` must not list a class. |
| T6 | Can the HLSL mirror cover `out` parameters or buffers? | No. `HlslSourceCompatibilityTests.cs:120-131` builds each argument with `Activator.CreateInstance` over the parameter type, which a by-ref type refuses; `design.md:96-99` drops functions taking GPU resources. So an HLSL twin of `distance_to_segment` needs an `out`-free overload, and the interpreter's buffer reads are outside the mirror. |
| T7 | What does the trophic graph need beyond scalar ops? | (a) Stencils: a predator is a mean over 8 taps of its prey at offset positions (`stencil`, B36); in a graph that is input substitution, as Fidget's `remap_xyz`. (b) Site spans: the slack layer and the strands read every well (`well_slack`, `pairs`, `strands`): a per-batch array input. (c) A per-site weight: the lattice node raises each cell's weight by its proximity to the wells (`lattice_web`, `prox`), a function evaluated at the site, not at the query point. |
### The model (SKILL step 0b)
| Kind | What names it | What happens to it over time | Who decides |
|---|---|---|---|
| Expression graph (`ExprGraph`, a sealed immutable class) | Nothing of its own: it is a value. The consumer record that holds it names it (an Aetheria species record's key). Its content hash is derived, for caches only. | Built by the builder or decoded; validated on both paths by one validator; never mutated. An edit builds a new graph. The persisted shape carries a format version; a graph naming an unknown op, a newer format, or a forward operand is refused whole at decode, never partly loaded. | `CultMath.Expr` owns shape, validation and op vocabulary. `GameCult.Caching.MessagePack` owns the wire shape (`CultMathExprResolver`, beside `CultMathResolver`: T4). The consumer's store owns the record, its key and when it changes. Sub-graph references are the consumer store's and are inlined before Build, so the graph stays flat (precedent `aetheria-release:ruling:material-graph-home-modular`). |
| Node | Its index in the graph's topological order; an operand references only a lower index. | Exists only inside one graph value; any rebuild may renumber it. The builder hash-conses identical nodes and drops nodes no output reaches, so one expression always builds to one byte string. An editor that needs stable node ids keeps its own map. | The builder. |
| Port | Input ports: the operand positions of the op's signature in the op table. Output: a node is one scalar; a multi-result op (cellular's fields) is read only through `Pick(node, k)` nodes and evaluated once. Graph ports: named inputs (vary per point: position, time), named params (uniform per batch, each with a default), named outputs; names unique per kind, `[A-Za-z_][A-Za-z0-9_]*`. | Graph port names are the graph's contract; renaming one makes a new graph. | The author, through the builder; the evaluator binds names to indices once per bind. |
| Op vocabulary | A stable string per op: the CultMath function's name, with an arity suffix where overloads differ (`snoise2`, `snoise3`). Written into persisted graphs. | Append-only. A released op's meaning follows its CultMath function under semver (a value change, as the snoise tie, is a Breaking release note); removing an op is a major bump. | `CultMath.Expr`: one op table, read by the C# evaluator and later by the bytecode lowering. No second table. |
| Parameter set (bindings) | The consumer's record (fauna: per-species settings); inside it, param name to value. | Edited freely by the consumer. At bind, a missing name takes the graph's default; an unknown name is refused, typed. CultMath persists no parameter-set type. | The consumer's store. |
| Site span (input kind) | A span-input name in the graph. | Bound per evaluation batch by the caller (wells per tick); never persisted in the graph. C#:`ReadOnlySpan<float4>`; HLSL: `StructuredBuffer<float4>` plus a count. Read only by span-taking ops; no op produces one. Lands with `expr-trophic-ops` as an append-only format addition. | The caller binds the content; `CultMath.Expr` owns the kind. |
| Bytecode (the lowered tape) | (graph content hash, bytecode format version). | Derived on demand, cache-only; never stored as truth or shipped as a document; rebuilt when the graph or CultMath changes. | `CultMath.Expr`'s lowering (the interpreter cut). Its register model is question `tape-register-model`. |
| HLSL interpreter source (`shaders/CultMath.Tape.hlsl`) | The file, versioned by the CultMath release tag; it declares the bytecode format version it reads. | Changes only in releases under semver; a format change bumps the version and the lowering together. Copied byte-identical into the Unity package (`UnityPackageShaderIsIdenticalToTheCanonicalShader`). | CultMath. |
| Compiled HLSL artifact | Under the interpreter there is no per-graph compiled HLSL. The one compiled artifact is the consumer's compute kernel that includes `CultMath.Tape.hlsl`, named by the consumer's build. | Compiled by the consumer's build (Unity), never generated per graph. Per-graph generated kernels (the r1 emitter) return only with the material line, as a derived artifact pinned against the C# evaluator. | The consumer's build; CultMath owns only the included source. |
| Evaluation results | Derived. | Cache-only, the consumer's. | The consumer. |
Gap (5) of the fauna map, "CultMath-owned parity for a consumer's combined HLSL",
changes shape under the tape: a consumer's composition is a graph, not HLSL, so
the parity CultMath owns is graph parity, the C# evaluator against the HLSL
interpreter on the consumer's own graph. That harness belongs to the interpreter
| TN1 | What does the probe compute, exactly? (Aetheria `docs/research/2026-10-14-trophic-dag-probe/trophic_probe.py:49-71`, `2026-10-14-body-voronoi-probe/body_voronoi_probe.py:36-67`, master `6b6e7c7c`) | Body diagram: every body visited, scores sorted, the two winners' `(F2 - F1) / |grad(F2 - F1)|` and its unit normal (the lattice web reads both). Power score `d^2 - sigma^2`, multiplicative `d / sigma` with `sigma` the well's Gaussian width. Gate: per pair `(i, j)`, margin = min over the other sites `k` of `(|k - mid| - r) / r`, `r = |ab| / 2`, unweighted positions; gate `smoothstep(-.15, .15, margin)`; no other site gives margin 9 (gate 1). Strands: max over gated edges of `gate * exp(-(segdist / width)^2)`, `width = 28 + .03 L`. |
| TN2 | Is the first-order power border exact? (scratch `border_probe.py`, double, 3,000 spans of 2-8 sites) | Yes: `F2 - F1` is affine in the query for power scores, so the first-order distance is the distance to the power bisector plane; worst relative error against that plane 2.2e-13. A one-pass fold (strict `<`, F1 then F2) gives the sorted order on every draw; an exact tie keeps the lower index first. Gabriel margin: a third site on the diameter circle gives 0, inside at half radius -0.5. |
| TN3 | Can the shader side take a span? | No resource in the mirror: `HlslSourceCompatibilityTests` compiles the HLSL text as C#, and `CultMath.hlsl` has no `StructuredBuffer` (0 in every `shaders/*.hlsl`). The struct-aware twin rule (`:153-161` at stage-shaders, now `:162-181`) compares struct and `int` arguments and private twins; `compared.Count` is 127 at `:181`. `GlslMirrorTests.FromScalars` (`:488-500`) decodes `int` and structs; `GlslTypes` (`:874-879`) maps names; `Resolve` (`:882-888`) binds public `math` methods only and cannot parse an empty parameter list. |
| TN4 | Wire coverage of a new public struct (`CultMathSerializationTests`, `CultMathJsonTests`, T5) | Every exported struct needs a `CultMathResolver` shape and a `CultMathJson` converter; JSON `Fields<S>` (`CultMathJson.cs:269`) holds one scalar type per struct, so a struct mixing `float` and `int` fields needs new JSON machinery. Pattern: `52d25740` (SpanPath, SpanStage: resolver, JSON, a `Json`, a `RoundTrip` and a `Hex` pin). |
| TN5 | Who calls `distance_to_segment(.., out closest)`? | `MathTests.cs` and Aetheria `Assets/Scripts/ServerShared/Galaxy.cs`; it stays, unchanged in value. |
| TN6 | Is `CultMath` free to take `ReadOnlySpan`? | Yes: `net8.0;netstandard2.1` (`CultMath.csproj:4`); `BatchMath` already takes `ReadOnlySpan<float2>`. |
#### Decisions (defaults, not forks)
- **A span function is a C# loop over a step that has a shader twin.** The
shader cannot take a span in the mirror (TN3), and a buffer-taking HLSL
function would be a second, untested path. So each span function is one fold:
`site_cells_empty()`, then `site_cells_insert(cells, score, index)` per site,
read by `site_cells_border(cells)`; the per-site score is `power_score` or
`multiplicative_score`. C#'s `power_cells(p, sites)` and
`multiplicative_cells(p, sites)` are that loop and nothing else. A shader
caller (and later the tape interpreter) writes the same loop over its own
buffer. One rule, one path; the per-site functions are mirrored bit for bit.
- **Sites are `float4(position.xyz, weight)`**, the model page's span element; a
2D consumer uses z = 0. For an explicit span every site is visited, so no
window and no weight-ratio range apply (the lattice node's range is line 4's).
- **Indices are `float` fields** (`nearestSite`, `secondSite`, -1 for none), as
`CultCellular.id` is: one scalar type keeps the MessagePack and JSON shapes on
the existing helpers (TN4), and an index is exact below 2^24, far past any
span a per-point O(N) search serves.
- **The gate is pointwise per pair**: `gabriel_margin(a, b, other)` with a
shader twin, and C#'s `soft_gabriel_gate(sites, first, second, softness)`
folding it over the span (TN1's unweighted positions; a power-weighted
Gabriel graph is not requested by any ruling).
- **The strand reduction is not in this cut.** Its Gaussian profile and width
law are fauna's composition until the graph needs it as an op; line 5 owns
that op, with the same step-and-twin convention.
- **Shader part separable.** The C# functions, struct and wire shapes land
first; the HLSL twins, the regenerated GLSL and the fixture families land in
the last commits under today's `GlslLowering`. Pass `imagination-ag4-shader-runner`
is re-posing the lowering and shader testing (question `device-run-driver`);
if that lands first, only the shader commits are re-mapped.
Written by Imagination (agent `imagination-ag4-lattice-weighted`, session
`self-2026-10-16-ag4`) against CultLib `origin/main``911fc755`, where
`trophic-nodes` is merged (`7e6881af`). Ruling `lattice-weight-from-bodies-ruled`
(node-reads-bodies). Line 4 splits into two cuts because of Hands' budget:
`cut-lattice-weighted-cellular.r1` (C#) and then `cut-lattice-weighted-shaders.r1`
(twins, GLSL, families, lavapipe bounds). One question,
`lattice-window-sound-or-measured`, gates the first cut's search loop.
#### Body facts (lattice-weighted-cellular)
The probes are numpy float64 scripts in
`docs/research/lattice-site-weight/weighted-node/` (`probe.py` is the node, and
each other script is one measure). They use 6 bodies orbiting with period equal to
distance and a sigma ratio of 5:1, `cellSize 260`, `falloff 120` (the body probe's
`3 * EW_U`), `KW .35` and boost 1.6, the ruled look.
| # | Question | Result |
|---|---|---|
| LW1 | What weight does the ruled look use? (Aetheria `body_voronoi_probe.py``lattice_web_edges`, master `4e620671`) | `base(.5..1) * (1 + 1.6 * exp(-(border(cell centre) / 120)^2))`. The border is the body diagram's first-order distance under the ruled power metric (`body-diagram-metric-ruled`). Distance is stretched by `1 + near` (up to 2x) across the body border's normal at the query point. Output `max(soft^2, exp(-((F2 - F1)/.1)^2))`, with soft from her smooth-min fold. |
| LW2 | Window sufficiency (`a.py`, 67,600 points against a 19x19 box) | 7x7 has no F1/F2 misses at ratio 5.2, 10 and 20 with stretch 1 or 2. 5x5 misses .1-.3% of points with stretch 2 (output error up to .92) and .04% at 20:1. A worst-case proof covers 7x7 only for ratio x stretch up to about 1.5. |
| LW3 | Are the weights continuous in time? (`c.py`, 576 cell centres, 300 s at 1/60 s) | First-order border: max step .68, and 113 steps over .01, where the second-nearest body changes. Exact `power_border`: max .0032, none over .01. |
| LW4 | Does her fold (from 8, row-major) depend on the window? (`d3.py`, 40,000 straddles of lattice cell boundaries) | Yes. With F1 and F2 exact, soft jumps .114 at 5x5 and .056 at 7x7 with stretch 2. It vanishes at 13x13 on this corpus, but nothing proves that. |
| LW5 | Which fold start is window-independent and continuous? (`d3.py`, `g.py`) | Start at F1 (F1 cell skipped): no window jump, but at F1 identity swaps m jumps .011 and soft .25, because the polynomial smooth min is not associative. Start at F1 + k and fold every cell: swaps jump 4e-15 and window jumps are 0 once the box holds every `d < F1 + 2k`. Its overshoot above m is at most .017k per step (computed), hence the clamp to F1 + k. |
| LW6 | Look of start F1 + k against hers (`h.py`, 40,000 points, 13x13) | Mean output difference 0.0000, max .032 (stretch 1) and .030 (stretch 2). |
| LW7 | Ring-exit cost (`e.py`) | Cells visited: at the ruled look median 25, max 49 (stretch 1) and 81 (stretch 2); at boost 4 with stretch 2, max 121; at boost 9, max 81. The worst-case sound fixed box is 19x19 to 67x67. |
#### Decisions (defaults, not forks)
- **The weight reads the exact power border** (`power_border`, trophic-nodes),
not the first-order one the ruled look's probe used: LW3, under
`aetheria-fauna:ruling:habitat-no-pop-ruled`. The look moves where the
first-order distance was wrong (LW3's jumps are the same places).
- **Her fold starts at F1 + k and is clamped to F1 + k**, in absolute row-major
order (LW4-LW6). The look is unchanged to within .03, and the node is
window-independent by construction, which the search's exactness needs.
- **The alignment stretch is in the node.** It is a query-point anisotropy, and
it does change winners (B41). Only the node sees the candidate sites, so a
caller cannot apply it. Stretch 1 is isotropic.
- **float4(F1, F2, smooth, id), no gradients, no struct.** fauna's composition
reads values only, and a struct would owe a wire shape (TN4). Gradients can be
added later as their own function, if a consumer asks.
- **The range is public constants**, so line 5's graph validator reads the range
from CultMath rather than restating it: boost up to 4 (10:1), stretch up to 2,
smoothing up to 1.
- **The base weight takes its own hash lane** (`pcg3d(cell).z`), not the jitter
phase the probe reused.
- **Steps for shaders, the span function in C# only**, by trophic-nodes'
| Interval ops | `cut-interval-ops.r4`, reports h1-h4, Soul s1-s3 (s3 all Holds) | Merged to main (`fb567472`). Open Soul findings: `cut-interval-ops.s1.lod-weights-never-exercised` (Low), `cut-interval-ops.s1.shipped-void-two-scenes` (Medium, pre-existing; Pass 4 picked the one shipped void, but the finding was never closed), `cut-interval-ops.s3.lodstep-footprint-branch-unpinned` (Low). |
| GLSL lowering | `cut-glsl-lowering.r2`, h1-h4, s1-s3 (s3 all Holds) | Merged (`bb583e89`). Follow-ups `iv-exp-gpu-widening`, `glsl-fixture-uncovered` and `glsl-browser-parity-run` are open. |
| Affine forms and shaders | `cut-affine-forms.r2` (h1-h3, s1-s3), `cut-affine-shaders.r2` (h1-h3, s1-s3) | Merged (`156782da`). Soul's s3 on affine-forms has one Falsified claim and no open finding; Self carried it as follow-up `gate-detail-scale-sensitivity`. |
| Snoise tie | `cut-snoise-tie.r1`, h1-h2, s1 (all Holds) | Merged (`48aadda0`). It re-pinned `SNOISE_LIPSCHITZ` from 10.099261 to 7.9640074 (Pass 6). |
| Site ground | No document in the mind. The file-era spec `docs/cultmath-interval-ground-cut-site-ground.r3.spec.json` (repo `GameCult/gamecult-site`, base `535d170`) and the stardust spec | Never run. The gamecult-site `origin/main``af6e206` has no ground shader and vendors no CultMath GLSL. Follow-ups `site-ground-affine-and-centred-warp`, `site-ground-dense-check-blind-past-body` and `wells-oracle-shortfall-affine-forms` wait on its next revision. No site-ground spec was ever admitted to the mind, so its first spec there is `cut-site-ground.r1`, whatever the file era numbered it. |
| Noise domain | Ruling `integration-native-noise-domain`; `docs/research/integration-native-noise-domain.md` (`114bb4f3`, on main) | The probe ran on 2026-10-03. There is no cut. Its source (`DomainProbe/Program.cs`, scratch commit `5ec9f529`, never merged) survived only in a temp scratchpad. It is now copied, with its images and log, to `F:\Projects\eureka-handoff-aux-2026-10-14\imagination-volumetric-frontier\`. |
| Operator's playgrounds | Artifacts "Kernel Nebula Lab" (`c09d2762`, version 1791066352) and "Nebula Span Lab" (`be6a9e31`, version 1791074878, the later one) | Facts file above. The Kernel Lab is the probe's kernel cascade in a page and has no noise function. The Span Lab is the "last nebula playground". |
No volumetric spec is in force without a report, so none is stale on an old
base. Ruling `wells-overhead-plus-floor`, which the brief named as in force, is
superseded by `affine-wells-contract-cheaper-than-interval`. The next spec does
not touch (c), so it cites neither. Target `r1` is stale for this line: its
`not_in_scope` still parks affine forms, and no invariant covers the noise
domain. The target revision is Self's.
### The Span Lab is the framework
The Span Lab's density is a ramp, `ext · clamp((F − s)/B, 0, cap)`. Here `s` is a
height pushed down by power brushes. It is then domain-warped twice by octave sums
of `snoise`, each plain, ridged (`1 − 2|n|`) or curl, with an anisotropic stretch
along a flow axis. The lab's own words: "The span integrator linearises the whole
warp chain per span, bounds the error with a curvature bound, and integrates
optical depth in closed form". Its bound is `0.5 K h²` from a hierarchical chain of
speed, acceleration, Jacobian and second-derivative bounds. Its empty and saturated
proofs use the same model, or the later warps' ranges alone.
That is ruling `integration-native-noise-domain`'s domain, "built for interval
integration", made with the noise she likes. It is also the "totally different
authoring framework" she will build on. The kernel cascade (the probe and the Kernel
Lab) is the earlier exploration, and it stays a line below.
**Correction to the brief's expectation.** The next cut is not site-ground. The
### `SNOISE_LIPSCHITZ`: 10.099 in the lab, 7.964 in CultMath
- **Current: 7.9640074.** It is set in `math.Interval.cs:21`, `CultMath.Interval.hlsl`
and the generated GLSL. `MeasureLipschitz` (seed 0x11B5) refined 7.2400064, and
7.2400064 x 1.10 = 7.9640074. That measurement is on the tied kernel, re-pinned by
`cut-snoise-tie` (Pass 6 table; Soul's `cut-snoise-tie.s1` all Holds).
- **Stale: 10.099261.** The same measurement on the pre-tie kernel refined 9.181147.
On that kernel snoise jumps by 0.21 to 0.72 inside a 1e-4 ball near lattice
diagonals, so gradient ascent climbed a near-discontinuity. Pass 6 recorded 926
`af_snoise` misses around that spike before the fix and 0 after.
- **The lab embeds the pre-tie kernel.** Its `cultmath_snoise` uses
`step(x0.yzx, x0.xyz)`, the 0.3.0 cyclic tie (`SPAN` line 244). CultMath's has the
three-component Gustavson order (`CultMath.glsl:90`). Its pins
`SN_L = 10.099261`, `SN_H = 56.050385` and `SN_T = 540` were measured on that
kernel. `SN_T` came from the lab's own hill climb, 487.1 x 1.11.
- So the lab's constant agrees with the lab's kernel.
- But the kernel is discontinuous, so no derivative bound holds across the tie
set. The lab's "proved" span bounds and its bound audit can miss there.
-`SN_H` survived the tie fix unchanged (`MeasureHessian`, same witness). `SN_T`
has never been measured on the tied kernel.
- **Nothing the interval march or the site ground reads is stale.**
- CultLib's march and every `iv_*` and `af_*` function read 7.9640074 in all three
runtimes.
- The site ground has no code.
- The file-era specs and Passes 1-5 cite 10.099261 only as history (as-built
section).
- The stale copy lives only in the lab page. It is recorded as follow-up
`span-lab-pre-tie-kernel`.
### What of the Span Lab belongs in CultMath (the noise domain)
One owner each, and the one rule each protects. CultMath owns noise primitives and
their bounds. The consumer owns its composition: brushes, the ramp, which warps
and in what order. The as-built negative "a flow or envelope function in CultMath"
stands.
| Piece (lab) | Owner | Rule it protects | Line |
| --- | --- | --- | --- |
| `cultmath_snoise_gh`, the analytic Hessian (`makeGH`, string-spliced from the pre-tie grad kernel) | `math.snoise_hessian` in C#, HLSL and GLSL | One snoise kernel. Value, gradient and Hessian come from the same tie order, so no consumer splices its own derivative from an old kernel | Cut `snoise-hessian` (admitted) |
| `SN_T`, the third-derivative tensor norm pin | `SNOISE_THIRD` beside `SNOISE_HESSIAN` | One measured pin per derivative order, re-measured with the kernel | Cut `snoise-hessian` |
| Closed-form optical depth of a clamped linear ramp over a span (`compField` clamp split) | `math.integral_clamp_linear` | One closed form, with the clamp split at the exact crossings | Cut `snoise-hessian` (generic math; no fork) |
| Curl of a three-potential octave sum (`dispVec` mode 2, `curlK`), with its Jacobian from the Hessian | A CultMath curl primitive | Divergence-free by construction. Its bounds come from `L` and `H` | Waits on the curl ruling (not ruled); needs `snoise_hessian` |
| Warp-stage octave field: per-component offsets, per-octave offset, anisotropic stretch `Mop`, ridged `1 − 2|n|` with its fold deviation | CultMath stage functions returning value, Jacobian and span bounds | A stage's bounds compose from the noise pins by the chain rule; a consumer never re-derives them | Waits on question `span-chain-shape` |
| Span evaluator: linearise a composed chain, the curvature bound `K`, the empty and saturated proofs | Per `span-chain-shape` | One composition rule for bounds | Waits on `span-chain-shape` |
| Brushes, the ramp, `ampG`, `brushPoint`, the chain's order, the scroll animation | The consumer (site, Aetheria) | The authored field is the consumer's | Never CultMath |
`1 − 2|n|` is the affine image `2(1 − |n|) − 1` of `ridged_grad`'s octave term.
Adding it as a variant is a naming choice inside the stage-function cut, not a fork.
### The noise domain and a curl node (not decided: no ruling)
The Aetheria Self is testing curl of simplex (`snoise_grad`) as a replacement for
`Tri3D` flow noise. The Span Lab already has curl as a warp mode. The domain's
owner would serve a curl node like this:
- It is one CultMath primitive: the curl of three offset `snoise` (or octave-sum)
potentials from analytic gradients, with the `cultmath_` HLSL twin, the lowered
GLSL and a fixture family. Aetheria's flow law and the nebula's warp both call
it. Each consumer keeps its own composition and scaling: the lab's
`curlK = 0.25/F`, or Aetheria's amplitude and scroll.
- Its Jacobian, which the span evaluator and the kernel cascade's per-kernel affine
warp both need, is a difference of Hessian entries. That is why `snoise-hessian`
comes first, and why the curl node is not in it.
- Bounds without new constants:
- Each curl component is a difference of two partials, so its magnitude per
octave is at most `2 L` times the amplitude.
- Its variation over a ball is bounded through `SNOISE_HESSIAN`.
- Its curvature, which the span bound needs, is bounded through `SNOISE_THIRD`.
- The as-built line "a flow or envelope function in CultMath" was written against
the site's flow law. A curl basis is a noise primitive like `snoise_grad`. If the
curl node is ruled, the line needs that clarification, not a waiver.
### Kernel cascade (kept as a line)
The probe found the cascade exact, and 0.48x the shipped march's cost at 4.2x its
msf on the void. But it "cannot be marched", its look is fibres, and its GPU cost
is traversal, which is unmeasured. Optical depth through it is a sum of per-kernel
segment integrals, so it is order-independent. Additive blending of each kernel's
screen footprint would give exact transmittance in WebGL2, with no compute and no
sorting. Emission is not order-independent. This is a hypothesis with no probe
2.`glsl-build-dxc` (spec `cut-glsl-build-dxc.r2` admitted; r1's h1 left the gate red, Pass 9d): the build, not yet the switch. r2 merges main, carries site arrays and their capacity (a spec constant mapped to `CULTMATH_MAX_SITES`), the strict-form gate and the renamed phasor fields, and waits on `phasor-field-rename` (spec admitted: `cosine`/`sine` in C#, HLSL, GLSL and CultMathJson, legacy keys read; CultMath's next release is 0.5.0, a release cut of its own). `packages/cultmath/tools/glsl/`