# CultMath intervals and the gamecult.org ground: cut map Status, 2026-10-14: the target is r2 (section "Target r2"); snoise-hessian, expr-ir and cycles-converters are on main. Earlier status, 2026-10-04: interval-ops, glsl-lowering, snoise-tie (48aadda0) and affine-forms with affine-shaders (156782da) are on main; the as-built record is the last section. Figures quoted in Passes 1-5 at SNOISE_LIPSCHITZ 10.099261 predate the tie fix (now 7.9640074) and are conservative; design.md carries the current measurements. Map history: Imagination pass 4, 2026-10-03. Pass 1's Q1–Q3 are answered (below); the Self opened the `cultmath-tapes` campaign from pass 2; pass 3 revises the two CultLib cuts after their first Hands reports and the operator's challenge to the march, and changes the site field to envelope-then-noise; pass 4 (section "Pass 4") revises `interval-ops` to r3 after its second Hands report and Soul's verdict, and names the one shipped void (Q9, ruled `operator-shipped-void`). Pass 5 (section "Pass 5") maps affine forms against CultLib `main` at `a7966142`. Where a later pass and an earlier section disagree, the later pass wins. Where this map and the Body disagree, the Body wins and this map is stale. Pinned HEADs (every `file:line` below is against these): | Repo | Ref | SHA | | --- | --- | --- | | CultLib | `main` | `c49c76ee` (CultMath 0.3.0 merged at `614fd445`) | | gamecult-site | `main` | `535d170` (before `wash-home-only` lands) | | Aetheria | local checkout | read only; `Volumetric.cginc`, `Raymarching/CloudShader.shader`, `Compute/Stardust/Stardust.{compute,shader,cs}`, `CustomDoF/DepthOfField.hlsl`, `Zone Display/VolumeCloudRenderer.cs` | ## The operator's words 1. "Can we replace the site's background with a fancy yet unobtrusive and inexpensive shader thing? It's odd that my main site wouldn't have that, considering my specialization. Would it be too much to have some optimized subset of Aetheria's volumetric clouds shader in there?" 2. After the Self proposed a quarter-resolution, 8–16 step, static-on-phones design: "map it as a cut that includes the GPU interval arithmetic we're borrowing from Fidget for a planned Asura campaign. I'd like this shader to be the proving ground for that - with the right math we can absolutely run this fullscreen on mobile, per pixel as long as the sample count is low and we're accumulating samples with TAA (Aetheria's TAA is honestly embarrassing and it still handles smooth 1080p clouds on my tired old 1070)" 3. On pass 1's Q3 (ground tokens as the shader's input): "I like that, but the background should be nice and dynamic and we should sample the wash from the result instead. Is there any point sending you video? I don't think your vision model has anything in it to encode motion. Anyway, this video really blew people away, with its combination of the clouds, the flowing stardust and my hacked DoF shader where I replaced the circle of confusion calculation with an output from the raymarching shader, I think it was final transmittance? Doesn't matter, looks great to have the clouds blur what's behind them. This does remind me we're totally gonna need that stardust too, it really sells the motion of the nebulae" Rulings from pass 1: **Q1** new campaign `cultmath-tapes`. **Q2** the slow ladder is samples, then resolution, then the CSS wash. **Q3** answered outside the options: the shader owns the ground and it moves; the CSS wash is sampled from the shader's result; the ground tokens are seed and fallback only, never the shader's input; stardust and the raymarch-driven DoF are in scope. ## Ownership ### Where the interval math lives **CultLib, `packages/cultmath`.** This is the parked campaign's own first step (`docs/cultmath-tape-target.md`, sketch step 1: "CultMath `interval` in C# and HLSL with parity tests ... It stands alone, and is the first cut to take even if the rest stays parked"). The "planned Asura campaign" in the operator's words is that document; its first named consumer is Aetheria's volumetric raymarching, and Asura itself does not need intervals (`cultmath-tape-target.md:103`). The site speaks WebGL2 GLSL ES 3.00. Three routes were weighed: | Route | What | Why not | | --- | --- | --- | | 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. | Under C, `shaders/CultMath.glsl` is a committed generated artifact, like `Swizzles.g.cs`. One implementation of the lowering lives in the test project; a test pins the committed file equal to the lowering of `CultMath.hlsl` and regenerates it when asked. GLSL's stricter typing is not patched in the lowering: the HLSL is written in the common subset (legal, bit-identical) and glslang compiles the lowered text in CultLib's tooling. Parity runs in two legs, each with one owner: HLSL ⇔ C# by the existing bit-for-bit mirror; GLSL ⇔ C# by a committed golden fixture evaluated on WebGL2 in the browser pane and read back. CultLib has no GPU harness (follow-up `gpu-snoise-parity`); the browser pane is one. The site vendors `CultMath.glsl` with the CultLib commit in its header. ### Campaign `cultmath-tapes`, repos `GameCult/CultLib` and `GameCult/gamecult-site`, in the Eureka Self area (ruling `adopted-by-eureka-self-area`). The tape target doc `docs/cultmath-tape-target.md` has steps 1 (intervals), 2 (graph and C# evaluator) and 4 (HLSL interpreter) unparked, with affine forms beside step 1 (rulings `affine-forms-unparked`, `material-ir-cultmath-now`, `tape-unparked-for-trophic-graphs`). Steps 3 (pruning) and 5 (meshing) stay parked. The volumetric line (the culled march's proofs, the integration-native noise domain, the site ground) leads the campaign (ruling `volumetric-work-prioritized`). Site cuts cite `site-masthead:ruling:operator-chromeless-column`. ### Target r2: owners, consumers, invariants **Owners.** CultMath owns every function a consumer evaluates and every bound on it: the snoise kernel and its derivatives, the noise pins, intervals, affine forms, the warp-chain stage functions and their span bounds, the closed-form span integral, the Cycles converters, the trophic node primitives, and the expression graph with its evaluator and interpreter. C# is the reference; the HLSL mirrors it; the GLSL is generated. `GameCult.Caching.MessagePack` owns the wire shape of CultMath values and graphs. A consumer owns its composition and nothing below it: the site owns its field's brush, ramp, chain order and parameters, its motion, TAA, stardust, DoF, budget controller and sampled tokens; Aetheria owns its species graphs, their settings and when they are evaluated. `custom.scss` owns the four nebula input tokens and the seed values of the ground tokens. **Consumers.** - The gamecult.org ground: the Span Lab framework with the void as its brush (ruling `site-ground-field-ruled`), in WebGL2 GLSL, vendoring CultMath's generated GLSL by CultLib commit. - Aetheria fauna (`aetheria-fauna:target:r4`): trophic species graphs as `CultMath.Expr` documents, evaluated by the C# evaluator in ServerShared and by the HLSL interpreter in its presenters, over CultMath's node primitives. - Aetheria's Cycles-matched bakes (`aetheria-release`): reference bakes compared with CultMath's Cycles converters within the measured tolerance class. **Not consumers.** Asura (its field is noise-dominated and needs no tape). AetheriaEve and AetheriaThing (archived). The material graph line until it returns after the demo (`aetheria-release:ruling:demo-material-scope`). The brush-grammar station, until pruning and meshing unpark. Aetheria's nebula on the span framework is an option to try (ruling `aetheria-nebula-spans-as-option`), not yet a consumer; until a cut makes it one, the nebula consumes CultMath noise at its pin bump and nothing else from this line. **Invariants.** - **`intervals-enclose`**: every interval function returns a `[lo, hi]`, and every affine form's range is an interval, containing `f(x)` for every `x` in its input region, in C#, HLSL and GLSL alike. A domain warp bounded by `D` is enclosed by enlarging the ball radius by `D`. A consumer that skips a region because `hi < cutoff` has skipped nothing that was there. - **`affine-beats-interval-on-wells`**: on the deep-well scenario (wells about an order of magnitude larger than the fog gradient), the affine bound's combined cost ratio exceeds the centred interval bound's at warp 0 and at warp `D`, with the cull floor at 0.5 (rulings `wells-scale-order-of-magnitude`, `affine-wells-contract-cheaper-than-interval`). - **`noise-pins-measured`**: there is one snoise kernel, and its value, gradient and Hessian come from the same tie order. `SNOISE_LIPSCHITZ`, `SNOISE_HESSIAN` and `SNOISE_THIRD` are each measured on that kernel by a committed test (the refined maximum times 1.10), and every runtime's copy is pinned equal to the C# constant by a value test. A kernel change re-measures every pin in the same merge. Consumers read the pins and the derivative functions from CultMath, never from page-local copies. - **`span-bounds-enclose`**: each warp-chain stage (per-component offsets, anisotropic stretch, plain or ridged `1 − 2|n|`, curl if ruled) is a CultMath function in C#, HLSL and GLSL returning value, Jacobian and span bounds (speed, acceleration, curvature, fold deviation) derived only from the noise pins. Each bound holds at every point of its span. The composition helper composes them by the chain rule; a consumer writes its chain and never derives a bound (ruling `span-chain-shape-ruled`). - **`span-integration-closed-form`**: along a span, optical depth is integrated in closed form, the clamped linear ramp by `integral_clamp_linear` (split at the exact clamp crossings, finite for infinite clamp bounds) and a proven constant-density body by Beer–Lambert in one step (ruling `operator-analytic-wall-body`). The linearisation error is bounded by the composed stage bounds, and a span proved empty or saturated by them is so at every point. - **`glsl-derived`**: `CultMath.glsl`, `CultMath.Phacelle.glsl`, `CultMath.Cycles.glsl` and the golden fixture are generated by one enumerated lowering from the HLSL and C# and pinned by tests; the Unity package's shaders are byte-identical copies. Nobody edits them by hand, and a dialect gap is fixed in the HLSL common subset. The site vendors the GLSL by CultLib commit, SHA in the header, with a drift check. - **`three-runtimes-agree`**: HLSL is a bit-for-bit mirror of C#, and the GLSL agrees with C# on the golden fixture within each family's stated tolerance, evaluated on a WebGL2 device. - **`cycles-exact-to-unfused`**: CultMath's `cycles_*` functions are Cycles' formulas with one IEEE single rounding per operation, bit-exact to the Blender 5.2.2 Linux SSE4.2 oracle fixtures. Every other Cycles build and CultMath's own shader lowerings are held to a per-function tolerance class measured on that build and recorded in the oracle README; cancellation-prone functions (floored modulo, wrap) carry an error bound relative to the operand scale and a divergence note (ruling `cycles-kernel-reference-ruled`). - **`one-expression-graph`**: `CultMath.Expr`'s `ExprGraph` is the one IR. One validator admits a graph whole at build and at decode; one op table, append-only, names every op; the graph has no loop construct, and stencils are build-time substitution. Bytecode is derived from the graph, cache-only, and is never stored as truth. The MessagePack resolver is the wire shape; JSON is never load-bearing. - **`evaluator-composes-cultmath`**: the C# evaluator's result equals the direct composition of the same CultMath calls bit for bit, one IEEE single per op; an op's meaning is its CultMath function's, and a function's value change is a Breaking release note. - **`tape-interpreter-compute`**: the HLSL interpreter is one included source, `CultMath.Tape.hlsl`, never a per-graph generated kernel. Its registers are linear-scan allocated at lowering and live in groupshared memory, a slice per thread, the group size shrinking as the register cap grows (64 registers: 128 threads; 128: 64; 255: 32). It and every presenter that uses it run as compute passes writing to a texture (ruling `tape-register-model-ruled`). - **`interpreter-matches-evaluator`**: the HLSL interpreter agrees with the C# evaluator on a graph corpus and on each consumer's own graph within tolerances measured per op, compiled by dxc and run on a device. - **`lattice-node-reads-bodies`**: the weighted lattice cellular node takes the zone body span and a falloff and weights each candidate cell at its cell centre inside the node. It states a supported weight-ratio range, its search window holds every cell that can win within that range, and graph validation refuses parameters outside it (ruling `lattice-weight-from-bodies-ruled`). - **`site-field-span-framework`**: the gamecult.org ground's density is a site-owned composition over CultMath stage functions, integrated span by span: a ramp, a warp chain, and one authored brush, the void, a true 3D spherical cavity of hollow radius 198 and ramp 50 inside a full cloud volume, a sun in its floor and the camera orbiting low (rulings `operator-ground-void-brush`, `operator-void-true-cavity`, `operator-shipped-void`). The site defines no noise, derivative or bound. - **`one-motion-owner`**: the ground has one motion law, owned by the site's field. What moves the density is what TAA reprojection and the stardust read; nothing evaluates motion a second way. - **`shader-owns-ground`**: the ground is the shader's moving render. The CSS wash tokens are sampled from its output by a non-stalling readback, carried across page loads in `sessionStorage`, and are seed or fallback only. The shader reads the four nebula input tokens, never an output token. - **`dof-from-transmittance`**: the stardust's blur is driven by the cloud layer's accumulated opacity at that pixel, so the clouds blur what lies behind them. - **`budget-measured`**: quality steps down by measured GPU or frame time only (samples, then resolution, then the CSS wash), never by user agent. **Not in scope.** - Pruning (tape step 3) and meshing (step 5). Interval, affine and gradient evaluation over `ExprGraph` belong to pruning and stay parked. - Span evaluation over the graph (`tape-span-eval`): the later generalisation of the stage functions once the interpreter exists. - A GLSL or WebGL2 tape interpreter: groupshared memory exists only in compute shaders, and WebGL2 has none. The site's field is written in GLSL against the stage functions. - The material line (cones, frontiers, a per-graph HLSL emitter), until the material graph returns after the demo. - Flow laws, envelopes, brushes and chain order in CultMath: the consumer's. A curl noise primitive waits on the operator's answer to follow-up `curl-warp-primitives`. - The kernel cascade, unless the operator's nebula work picks it. - Per-depth transmittance for the stardust DoF. - A CultMath release for the site; the site consumes a CultLib commit. **Canonical implementations.** Paths are under `packages/cultmath/` unless named. - Kernel, pins and span primitives: `src/CultMath/math.cs`, `math.Interval.cs`, `math.Affine.cs`, `math.Span.cs`; `shaders/CultMath.hlsl`, `CultMath.Interval.hlsl`, `CultMath.Affine.hlsl`. The stage functions land beside them. - Lowering and parity: `tests/CultMath.Tests/GlslLowering.cs`, `GlslMirrorTests.cs`, `HlslMirrorTests.cs`, `fixtures/glsl-parity.json`; generated `shaders/*.glsl`; Unity copies under `unity/org.gamecult.cultmath/Shaders/`. - Cycles: `src/CultMath/math.Cycles.cs`, `shaders/CultMath.Cycles.hlsl`, `tools/cycles-oracle/` (oracle and the tolerance-class table), `tests/CultMath.Tests/fixtures/cycles-oracle/`. - Graph: `src/CultMath/Expr/`; wire shape `src/GameCult.Caching.MessagePack/CultMathExprResolver.cs`; interpreter `shaders/CultMath.Tape.hlsl` (cut `tape-interpreter`). - Ground: gamecult-site, created by `cut-site-ground.r1` (no ground code on `origin/main` `af6e206`); palette input the four `--gamecult-nebula-*` tokens in `custom.scss`. - Research behind the rulings: `docs/research/integration-native-noise-domain.md`, `span-chain-and-site-field.md`, `tape-register-model.md`, `lattice-site-weight.md`. ## The authority map (all four cuts) - Owner: `packages/cultmath` for `iv_*` and the ball bounds; `GlslLowering` (test project) for `shaders/CultMath.glsl`; `custom.scss` for the four nebula input tokens, `--gamecult-wash`, and the seed values of the fifteen ground tokens; `ground.js` for the flow texture, scheduling, budget, TAA, the stardust and DoF passes, and the sampled ground tokens; the site GLSL files for the field, the particles and the resolve. - Inputs: CultMath reads nothing new. The lowering reads the two HLSL files. `ground.js` reads the nebula tokens, `--gamecult-wash`, `prefers-reduced-motion`, `visibilityState`, canvas size, device pixel ratio, the measured GPU frame time and its own 8×8 readback. The field reads uniforms and the flow texture only. - Outputs: `CultMath.glsl`; the interval functions; one canvas under the page; the fifteen ground tokens written on `:root` from the readback; a debug log behind a flag. - Derived state: the ground tokens are derived from the shader's result while it runs and are seed values otherwise; the CSS wash renders them; nothing reads them back into the shader. The flow texture is the one owner of motion: the warp, the TAA motion vectors and the stardust all read it. `CultMath.glsl` is derived from `CultMath.hlsl`. The TAA history is a cache. `S`, `dpr` and the particle count are derived from measurement within the session. - Forbidden writers: no colour literal in `ground.js` or any GLSL; the shader never reads a ground token; `ground.js` never reads the ground tokens except to seed before its first readback; no user-agent, platform or touch-point sniffing; no class on `body` or `html` by script; no second wash token; no second flow function (particles and clouds read one texture); no GLSL edited by hand; no `iv_*` without a C# twin; no noise defined in the site. - Shared paths: every page's ground, the ritual essays included, is one canvas and one token set, in CSS and in the shader. Every reduced budget (slow device, hidden tab, reduced motion, missing WebGL2 or half-float) goes through the one controller. The 8×8 readback is the one path from the shader to CSS. - Deletion line: in the site, the six literal radial colours and three literal stops become token references and the ritual block's own ground goes, in the same commit the tokens are declared. In CultLib nothing is deleted. The subtraction this buys is upstream: Aetheria's `Assets/Plugins/GPU Noise/*.cginc` at its pin bump, and the tapes campaign's step 1 already landed. ## The shader design ### 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:83-104` | transmittance integration | kept | | `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 | ### Density field (site-owned, `cloud.frag.glsl`) Pass 3 wraps this field in an envelope (a nebula floor with authored wells and clear sky above it); `coverage` below becomes the noise that displaces the floor, as in Aetheria. The flow, the phases and the hue stand. ```text camera at the origin, looking +z, static; the slab z in [Z0, Z1] fills the view flow(uv) = A · (snoise(q·Ff + o1), snoise(q·Ff + o2), 0) q = slab mid-plane point; quarter-res texture, once per frame phase_k(t) = fract(t / PERIOD + k/2), w_k = tri2(phase_k) k = 0,1 warp_k(p) = p + flow · (phase_k − 0.5) · PERIOD coverage(p) = Σ_k w_k · snoise(warp_k(p) · F0) two snoise occupancy = max(0, coverage − CUTOFF) · K detail(p) = 1 − |snoise(p · 4F0 + 17)| one snoise; the dune envelope density = occupancy · mix(0.6, 1.0, detail) hue(uv) = mix of the three nebula colours by flow.xy / A free: the flow texture's own channels colour = hue · intensity ``` Three `snoise` per dense sample, one per empty probe. The pattern moves at velocity `−flow` everywhere, because both phases advance at the same rate; the cross-fade only changes content. That is what makes the motion vectors below exact. ### Interval culling along the ray, with the warp Pass 3 supersedes this section's march: the probe is per screen tile in a pre-pass, the field has an analytic envelope the probe tests first, and the loop below becomes the pre-pass's loop over cell ranges. The ball and the warp enlargement stand. `cultmath_iv_snoise_ball(c, r)` returns `[n(c) − L r, n(c) + L r] ∩ [−1, 1]` with `L` the committed Lipschitz constant. Over a segment `[t, t+Δ]` the unwarped ball is centred at `t + Δ/2` with radius `Δ/2`; the warp displaces any point by at most `D = |flow|max · PERIOD / 2`, so the ball `(c, Δ/2 + D)` contains both phases' warped points, and one evaluation bounds the whole coverage term. The march is adaptive subdivision, Fidget's region test on a 1-D region: ```text Δ = Δ0; t = Z0/dir.z + jitter · Δ0 while t < t_exit and evals < S: iv = iv_snoise_ball(p(t + Δ/2) · F0, F0 · (Δ/2 + D)) 1 eval if iv.hi < CUTOFF: t += Δ; Δ = min(2Δ, ΔMAX) provably empty: skip, grow elif Δ > ΔDENSE: Δ = Δ / 2 maybe dense: refine else: integrate density(p(t + Δ/2)) over Δ; t += Δ 3 evals if transmittance < 0.02: break if t < t_exit: integrate one coarse sample over the rest the budget ran out; measured, must be rare ``` `iv.hi < CUTOFF` is the interval proof that the `0` branch of `max(0, coverage − CUTOFF)` wins on the whole segment: Fidget's `min`/`max` pruning at its smallest, applied by hand to a two-clause expression. Tape and automatic pruning stay parked; this proves the arithmetic they rest on. ### Step budget `S` counts `snoise` evaluations per pixel per frame. `S = 16` desktop, `8` phone, `4` floor; the controller sets it from measurement. A debug mode writes `evals / S` and an unfinished flag into the colour. Contract: under 1% of rays unfinished at the chosen `S` on the shipped field. ### TAA, motion-aware The camera is static but the field moves, so history is reprojected by the known motion, which the flow texture owns: - Motion vector: `mv = project(−flow(uv) · dt)` in pixels; history is fetched at `uv − mv`. Exact for the pattern between cross-fades (see above); the cross-fade and the detail term are handled by rejection. - Jitter: Halton(2,3) frame offset on the ray start plus interleaved gradient noise per pixel, rotated per frame. - Accumulation: `α = max(1/frames, 1/32)` on colour and transmittance. - Rejection: a 3×3 min/max box of the current frame clamps the reprojected history (AABB clamp, not variance clipping); out-of-bounds fetches take `α = 1`. - Reset on resize, on the first frame and on a budget step. - Buffers: two RGBA16F history targets at canvas size (8-bit stalls convergence at `α = 1/32` on a ground this dark); missing half-float falls back to the CSS wash through the controller. ### Stardust (cut `site-stardust`) Stateless, as Aetheria's: instance `i` is a cell of an `N×N` grid over the slab's mid-plane, hashed by `cultmath_pcg3d` to a spawn point and a lifetime offset; `lifetime = fract(t/PERIOD_P + offset)`; position = spawn − flow(uv of spawn) · lifetime · PERIOD_P, read from the flow texture in the vertex shader; size = `parabola(lifetime, 2)` × a hashed size; colour = the nebula hue at its screen position × a hashed brightness. Rendered as instanced billboards (one 6-vertex quad, `gl_InstanceID`) with additive blending into a half-resolution RGBA16F stardust layer, with the `powerPulse` falloff of `Stardust.shader:74-78`. Count: 16,384 desktop, 4,096 phone, controller-scaled with `S`. The particles and the clouds move as one because they read the same texture; there is no second flow. ### Depth of field driven by the raymarch (cut `site-stardust`) Aetheria's hack: the CoC is a raymarch output, so clouds blur what is behind them. Web version: in the resolve pass, the stardust layer is gathered with an 8-tap Poisson disk whose radius is `RMAX · opacity(uv)`, where `opacity = 1 − T` is the TAA-accumulated cloud transmittance at that pixel (the stable, converged value, which is why it is read from history and not from the current frame). Composite: `ground + clouds + stardust_blurred · T`. All stardust is treated as behind the whole cloud layer; a per-depth transmittance would cost a second march and is not in scope. ### Sampled wash: the CSS reads the shader After the resolve, a tiny pass downsamples the final frame to 8×8 (RGBA8). `ground.js` reads it back asynchronously: `readPixels` into a `PIXEL_PACK_BUFFER`, a `fenceSync`, then `getBufferSubData` on a later frame once the fence has signalled. Never a synchronous readback. At most twice a second, and only written when a value moved by more than 1/255: - `--gamecult-ground-0/1/2`: the mean of rows 0–1, 3–4 and 6–7. - `--gamecult-light-N`, `-a`: the colour at the seed light's centre texel, split into the hue (normalised) and the excess over the ground stop there. - `--gamecult-light-N-at`, `-r`: the seed values; positions are not fitted. - `--gamecult-ground-mean`: the 8×8 mean, for any tinted UI. These are written to `:root`'s inline style, so they beat the stylesheet's seed declarations. The seeds live on `:root` in `custom.scss` (today's literal wash), with the ritual essays' seeds under `:root:has(body[data-slug="Blog/..."])`. The body background and the ritual ground read the tokens. When WebGL2 is absent or the controller removes the canvas, the seeds are what paints. Across page loads the last sample may be carried as a cache (Q5). ### Palette input The shader reads four input tokens from `custom.scss`, which is where the brand says colour is defined: `--gamecult-nebula-0/1/2` (today orange `255 138 42`, violet `109 96 255`, sky `89 183 255`) and `--gamecult-nebula-base` (`#07111a`), plus `--gamecult-wash` as intensity. Inputs and outputs are different tokens, so there is no cycle (Q4). ### Resolution and the slow ladder (Q2) Full device pixels. The controller's ladder: `S` 16 → 8 → 4, then `dpr → 1` (the stardust layer and DoF follow the canvas), then the CSS wash. Never a step up within a session. ### Pausing and fallbacks - `visibilityState !== "visible"`: no frames; on return, history is kept and the motion vector uses the real elapsed `dt`, clamped to one frame. - `prefers-reduced-motion: reduce`: time is frozen (phase, flow and particle lifetimes fixed), render until 32 accumulated frames, stop; resize re-renders. The clouds and stardust are there, still. - No WebGL2, context lost, no half-float: canvas removed; the seeds paint. - Too slow: measured; see below. - The canvas is `position: fixed; inset: 0; z-index: -1; pointer-events: none; aria-hidden`. The root background paints first, so the canvas covers the CSS wash only once drawn. ### Text contrast The resolve clamps relative luminance at 0.035 (about today's orange hotspot, `#07111a` + `rgba(255,138,42,.18)` ≈ `(53, 42, 34)`, luminance 0.025), after stardust and DoF are composited. Body text `#b7c7d9` (0.55) keeps ≥ 7:1 on any ground pixel. A readback test asserts the max after 32 frames on `/`. ## Budget and how it is measured | Device | Pixels | GPU per frame at 30 fps | Proxy | | --- | --- | --- | --- | | Desktop, GTX 1070 class, 2560×1440 | 3.7 M | ≤ 2.0 ms | 6% GPU duty | | Laptop iGPU, Iris Xe, 1920×1080 at dpr 1.25 | 3.2 M | ≤ 3.5 ms | 10% duty | | Mid-range phone, Pixel 6a / Galaxy A54 class, 1080×2400 | 2.6 M | ≤ 4.0 ms | ≤ 12% duty; no upward drift of the median over 5 minutes | Cost model for the full stack on the phone (one `snoise` ≈ 150 flops, ~1 TFLOP available; estimates, replaced by measurement): | Pass | Work | ms | | --- | --- | --- | | Flow texture, quarter res | 160 k texels × 2 snoise | 0.05 | | 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 | | Stardust layer, half res, 4,096 particles | fill-bound, small | 0.2 | | 8×8 downsample + PBO readback at 2 Hz | tiny | 0.05 | | **Total at `S = 8`, the cavity (pass 3)** | | **≈ 4.8** | | Total at `S = 6` / `S = 4` | | ≈ 3.9 / 3.1 | Said plainly: LOD does not change the per-frame cost, which the controller fixes by `S`; it changes how much a frame's samples are worth. At `S = 8` the cavity does not fit the 4 ms phone budget on the estimate (≈ 4.8); `S = 6` sits at the line and `S = 4` is under it. What gives is `S`, and with the mask and LOD that is nearly free: a converged wall needs about 12 snoise per pixel in total, so `S = 4` converges in about three frames through the TAA instead of thirty, and `S = 8` in two. Then `dpr`, as before. What gives, in order, is the sample count, then the resolution; the stardust and the DoF are never dropped before the clouds' samples; the self-shadowing sample is not taken on any device until measured. On desktop at `S = 16` the stack is ≈ 1.9 ms. Measurement, in this order, all in `ground.js` behind a debug flag and used by the controller: `EXT_disjoint_timer_query_webgl2` where present (Chrome desktop) per pass; else a `fenceSync` after the resolve, polled (universal on WebGL2, phones and Safari included; the queue of a background page is otherwise empty); else `requestAnimationFrame` cadence. Hands proves it in the browser pane: desktop numbers with the timer query, the phone's pixel count from the mobile preset, the phone's GPU time from the operator's phone. ## The proving-ground contract (what the tapes campaign inherits) 1. **Enclosure tests in C#** (and HLSL by the mirror): every `iv_*` op over random intervals and points; `iv_snoise_ball`/`iv_fbm_ball` over 2,000 balls × 64 points. A mutant that shrinks any bound dies. 2. **The warp is enclosed**: for random flows bounded by `D`, every point `warp_k(p)` over the segment lies in the enlarged ball and `snoise` of it lies in `iv_snoise_ball(c, r + D)`. This is the test the site's march rests on. 3. **The Lipschitz constant has provenance**: the max of `|snoise_grad|` over 10⁶ points refined by gradient ascent, × 1.10, measured by a committed test and pinned by another. Empirical with a margin, not a proof; the enclosure tests are the defence. 4. **Tightness is a number**: mean interval width over true range, printed by a committed test, for a later affine cut to beat. 5. **The saving is a number** (amended in pass 3): a C# test marches tiles of rays through four scenarios, an envelope-then-noise field with authored empty space among them, with the dense march and with the tile-amortized interval march, and asserts ≥ 2× lower combined cost at equal transmittance on the authored scenarios; the uniform slab is printed as the worst case. The shipped field's evals per pixel and unfinished fraction come from the browser. 6. **Three runtimes agree**: the GLSL golden fixture passes on WebGL2, with tolerance class and device named in the report. Not proved here, said plainly: tape evaluation, automatic pruning, `min`/`max` choice tracking, affine arithmetic. Pruning and choice tracking stay parked; the graph, its evaluator and interpreter, and affine forms have since unparked (Target r2). ## Cut order ```text glsl-lowering (CultLib) ─┐ interval-ops (CultLib) ─┴─> site-ground (clouds, flow, TAA, sampled wash) ─> site-stardust (particles, DoF) ^ after site-masthead wash-home-only ``` Four cuts. The two CultLib cuts are as in pass 1, except `interval-ops` gains the warp enclosure test. `site-ground` grew (flow texture, reprojection, the readback and token restructure); `site-stardust` is split out because its Soul vocabulary is different (particle statistics, blur quality, the composite) and because the ground must be measured on its own before the stardust is charged against the same budget. No CultMath release is in the sequence; the site consumes a commit. Specs: `docs/cultmath-interval-ground-cut-glsl-lowering.spec.json`, `docs/cultmath-interval-ground-cut-interval-ops.spec.json`, `docs/cultmath-interval-ground-cut-site-ground.spec.json`, `docs/cultmath-interval-ground-cut-site-stardust.spec.json`. Revision 2 of the two CultLib cuts (pass 3): `docs/cultmath-interval-ground-cut-interval-ops.r2.spec.json`, `docs/cultmath-interval-ground-cut-glsl-lowering.r2.spec.json`. Revision 3 of `interval-ops` (pass 4): `docs/cultmath-interval-ground-cut-interval-ops.r3.spec.json`; `glsl-lowering` r2 and `site-ground` r3 now depend on it. ### The Cycles cuts (ruling `cycles-kernel-reference-ruled`) - `cycles-converters` r2 is the fix batch for Soul's s1. Its specs are typed documents in the mind, not files here. - `cycles-noise` r1 waits on `cycles-converters` and reuses its oracle tool. It inherits the ruling: its fixtures are the unfused SSE4.2 kernel's exact witness, and every other build is held to the tolerance-class table that `tools/cycles-oracle/README.md` holds. Body fact, probed 2026-10-14 on Yggdrasil. The job script, log and model are `job.sh`, `probe-avx2.log` and `model.py` in `F:\Projects\eureka-handoff-aux-2026-10-14\imagination-cycles-converters-r2\`. Blender 5.2.2 Linux was run on its default AVX2 kernel, with the oracle's kernel check removed, over the committed `math.json` operands at e48f9488. Results against the SSE4.2 fixture: - `cycles_floored_modulo` differs on 151 of 1024 rows and `cycles_wrap` on 30. - A single-rounding model, `fma(-floor(a / b), b, a)` (wrap: `fma(-range, floor((value - min) / range), value)`), reproduces every one of those rows bit for bit. So the contraction never moved the floor. - The worst `|delta|` is 0.91 x 2^-24 (|a| + |b|) for floored modulo and 0.77 x 2^-24 (|value| + |min| + |range|) for wrap. Both fall within the documented bound of 2^-23 times the same sum. ## Standing design decisions (means; Self may overrule) - D1. Intervals are `float2(lo, hi)`. Affine forms sit beside them (ruling `affine-forms-unparked`); the wells contract says where they pay. - D2. No `iv_*` function has a value-only or gradient twin; the bounds take the point value only. - D3. The site vendors `CultMath.glsl` by CultLib commit, SHA in the header, a drift check in the site cut. - D4. The flow texture is the one owner of motion. Warp, motion vectors, hue and particles read it; nothing evaluates flow a second way. - D5. Routes are page loads (`enableSPA: false`, `quartz.config.ts:15`); if SPA is ever enabled, `ground.js` listens for Quartz's `nav` event and re-reads the input tokens. - D6. The budget verdict is session-only; a stored verdict would outlive a driver update. - D7. Inputs and outputs are different tokens: four nebula inputs, fifteen ground outputs with seeds. The shader never reads an output; CSS never reads an input except through the shader's result. - D8. The ground tokens are written on `:root` inline, so the seed in the stylesheet loses to the sample without a class or a second rule. - D9. The DoF blurs all stardust by the final opacity; per-depth transmittance is out of scope. - D10. Stardust and DoF land after the ground has been measured alone, so the report can say what each costs. ## Pass 3: the envelope, the scenarios, the tile pre-pass, the split Phacelle Pinned: CultLib `main` `d0ea37f8`; `hands/cultmath-interval-ops` at `c9bd003b` (report `cut-interval-ops.h1`); `hands/cultmath-glsl-lowering` at `5ae207e8` (report `cut-glsl-lowering.h1`, branched from interval-ops). Rulings: `operator-tile-amortized-march`, `operator-split-phacelle`, `operator-compiler-downloads`, `self-exp-measure-in-site`, `operator-ground-void-brush` (answers Q6; the void scene below). ### The operator's words 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` (`GetHeight`), `Zone.cs:426` (`PowerPulse`), `Settings.asset` (`DefaultEnvironment`, `PlanetSettings`): ```text s(p) = p.y + h(p.xz) the pre-distortion SDF; h ≥ 0 the well map h(xz) = PowerPulse(|xz| / 2R, 2) · 64 + Σ_b PowerPulse(|xz − c_b| / r_b, 16) · d_b PowerPulse(x, e) = (1 − 4x²)^e on [0, ½], 0 beyond; r_b = 500 M^0.25, d_b = 30 M^0.175 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 | The 2x contract on combined cost is asserted on (a) and (c) at `N = 8`; (b) and (d) are printed. (e) is the headline: its contract is steps per pixel (below), and it prints cost and snoise beside it. ### Footprint LOD, and the cavity cost as a function The operator's words: "Surely this depends on the size of the cavity and the camera placement, no? The slope of the wall's density gradient offset determines the region we have to sample in. How much we actually sample that region depends on how close it is to the camera. A 10-wide gradient 1,000 units away only needs one raymarching step worth of detail, right?" Right. Pass 3b costed every wall at a fixed step. The march is now footprint-aware, in the site and in the saving test: - `θ_px = 2 tan(fov/2) / H`; the footprint at distance `z` is `z θ_px`. - Step `Δ(z) = max(k z θ_px, min(S/4, λ_fine(z)/2))`, `k = 4`: never finer than four footprints, otherwise the finer of the two content limits (four samples across the ramp, two per wavelength of the finest live octave). - Octave `i` of wavelength `λ_i` carries weight `w_i(z) = 1 − smoothstep(λ_i/2m, λ_i/m, z θ_px)`, `m = 4`: one smooth blend per octave, no popping; TAA hides the per-frame sparsity. - The weights are part of the field, so the bound encloses the truncated field exactly: the noise interval over a slice is `Σ_i iv_mul([w_i(z1), w_i(z0)], iv_snoise_ball(c F_i, r F_i))`, `w_i` monotone in `z`. `intervals-enclose` holds for what the shader evaluates. - The pre-pass says where each ray's ramp begins; LOD says how finely to sample it once there. They compound. The reference a march that does not know where the wall is must pay is the fixed step `min(10, S/4)` everywhere. The cavity is parametrised by the hollow radius `Rh` and the ramp width `S`; the brush `(1 − (d/Rc)²)^e` with `CARVE` gives `Rh = Rc √(1 − CARVE^(−1/e))` and `S` from the exponent (`e = 4` gives `S ≈ 1.3 Rh`, `e = 0.25` gives `S ≈ 0.12 Rh`), so `(Rh, S)` are the knobs and `(Rc, e)` derived. Shipped: `Rh = 198`, `S = 50`, so `Rc = Rh + S = 248` and `e = −ln CARVE / ln(1 − (Rh/Rc)²) = 0.40`, `K = 1/30` (pass 4 corrects this line's `e ≈ 0.6, Rc ≈ 220`, which did not satisfy the formula; Q9 asked the look question and is ruled `operator-shipped-void`). Imagination's model (scratchpad `cavity_model.py`; the test confirms or refutes), 1080p, `N = 8`, per pixel for a converged frame; "ref" is the fixed-step reference's steps: | `Rh` | camera | `S = 10` steps / snoise / cost ratio | `S = 50` | `S = 150` | | --- | --- | --- | --- | --- | | 100 | low | 6 / 12 / 2.6x | 6 / 12 / 1.6x | 14 / 36 / 1.3x | | 200 | low (**shipped**, ref ≈ 35 steps) | 6 / 12 / 3.2x | **6 / 12 / 1.7x** | 14 / 36 / 1.4x | | 400 | low | 6 / 12 / 4.0x | 6 / 12 / 1.9x | 14 / 36 / 1.5x | | 1000 | low | 5 / 9 / 8.2x | 6 / 12 / 2.5x | 14 / 36 / 1.7x | | 1000 | centred | 4.3 / 7 / 12.9x | 6 / 12 / 2.9x | 14 / 36 / 1.8x | Centred and mid offsets differ from low by under 10% at every point. What the table says: within the frustum the footprint is under 4 units, so the step is set by the content limits (`S/4`, and the detail octave's 12.5-unit half-wavelength), not by distance; the "10-wide gradient 1,000 units away" row is where the footprint finally bites (4.3 steps, 7 snoise). The sample count is nearly flat in `Rh` and camera offset and rises with `S` only once the detail wavelength, not the ramp, sets the step. What makes the shipped scene cheaper without losing the mood, in order: a lower per-frame sample budget, which LOD and the mask make nearly lossless (`S_budget = 4` converges in about three frames at the walls); a thinner ramp (`S = 10` is 3.2x on cost but reads as a surface; 25–50 keeps the fog); a longer detail wavelength or no detail octave at the far wall; a larger hollow, which changes the composition and costs nothing per pixel. **Q8 retired.** At the shipped point the shipped march takes 5–8x fewer steps than the fixed-step reference and 1.7x less combined cost; the contract for (e) is the number that scales with GPU time, steps per pixel: ≥ 2x fewer than the fixed-step reference at the shipped parameters, with snoise per pixel and combined cost printed beside it and promised nothing. The test asserts that; the browser measures it (`?ground=debug` against `?ground=dense`). A shortfall skips the assertion naming `saving-2x-scenarios`; the fields, grids and `e` are not tuned. The honest summary: at a Lipschitz bound near 10 the noise ball is weak everywhere; what intervals buy on these fields is the envelope's empty space, found exactly, and the tiles make finding it nearly free. ### The site ground: the void (ruling `operator-ground-void-brush`) The operator's words: "There was an old main menu visual with a purely authored field, first thing I did all the way back when the shader was new. I believe it was a single negative pseudo gaussian spherical brush creating a void in the clouds, with a sun gently nestled at the bottom lighting the cavity and orbiting along a track with the camera following low, very moody, gives us lots of options for authoring the flow field and stardust distribution" **The original, found.** Aetheria `e6181c11` ("Main Menu", 2021-03-13): `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 Redux" rewrote it), the tint map's runtime contents, the video. The 2021 brush was radial in `xz` and carved a bowl in a cloud floor; the operator ruled (Q7, `operator-void-true-cavity`) that the site's void is a true 3D hollow inside a cloud volume that surrounds it on every side, including above. Everything else recovered carries over. **The site scene** (`site-ground` r3; the brush parameters in this paragraph, `Rc = 640`, `e = 4`, are superseded by pass 4's one shipped void, `Rh = 198`, `S = 50`, `Rc = 248`, `e = 0.40`, as Q9 ruled): the original's units. Outer volume: an unbounded uniform fill (chosen over a shell: every ray then ends in a wall and the march terminates by opacity, never by a far plane; `Z1 = 2 Rc` bounds the pre-pass range only). The carve: `d = |p − c_v|`, `g(d) = (1 − (d/Rc)²)^4` for `d < Rc`, `Rc = 640`, `CARVE = 1.5`; `density = K · max(0, 1 − CARVE · g(d'))`, exactly 0 inside the hollow radius `Rh = Rc √(1 − CARVE^(−1/4)) = 198`, half at `0.49 Rc`, full at `Rc`; `K = 1/30`, so a wall is opaque within ≈ 150 units of entry. Noise displaces the wall only: `fade(d) = smoothstep(Rh − S, Rh, d)`, `S = 50`, `d' = d + A · fade · n`, `A = 20`, `F0 = 0.01`. Its bound over a tile's slice: `d`'s interval is the box's least and greatest distance to `c_v` (exact); `g`, `fade` and the carve are monotone in it; the slice is provably empty iff `d_hi + A · fade(d_hi) · n_hi ≤ Rh`, and deep inside (`fade = 0`) no noise is evaluated. `Rc` grew from the brush's 256 because a 3D hollow must hold the camera. The sun orbits the origin at radius 175, period 72 s; the cavity centre is `c_v = sun + (0, Rh − 12.5, 0)`, so the sun sphere rests on the hollow's bottom and the hollow moves with it. The camera follows 120 behind the sun along the tangent and 45 above it: 186 from `c_v`, 12 inside the hollow, low, looking at `sun + (0, 60, 0)` so the lit lower wall and the dark upper wall share the frame. The 2021 follow distance 150 and height 25 do not fit a hollow a camera can stand in; 120 and 45 are the re-derivation. There is no sky: behind a fully transmissive pixel the resolve shows the base token, and only an unfinished ray (budget exhausted before the wall, measured, under 1%) reaches it. `worldAt(t)` in `ground.js` is the one owner of sun, cavity and camera; nothing integrates motion. The flow is authored around the void: a swirl `SWIRL · tangent · (1 − (r/Rw)²)²` (largest mid-slope, zero at rim and centre) plus the global noise flow, rendered into a world-space window of side `4 Rw` around the sun, so warp, hue, motion vectors and the stardust read one texture (D4). The stardust (next cut) spawns in the bowl with the original's height rule and is lit by the same pulse. **Lighting, cheapest moody version:** the original's mechanism made 3D. At a sample, `light = SUN · (1 − (d/Rl)²)^3` for `d = |p − sun| < Rl = Rh = 198` (the original's 128 scaled with the hollow), plus the blue ambient; `colour = density · (light + ambient) · hue(flow)`. Zero extra `snoise`; the lower wall within `Rl` of the sun glows and the upper wall stays dark because the pulse is zero beyond `Rl`, which is the moody split; the sun inside the hollow is occluded by nothing, so it is a small additive disc in the resolve, occluded only by the accumulated transmittance along its ray. A self-shadowing step (one envelope-only transmittance sample toward the sun per primary sample, no noise, ≈ +25% on the march) is named, not taken: desktop-only if measurement ever allows. **TAA with a moving camera:** the main pass writes the depth-weighted mean march distance to an `R16F` history channel; the resolve reconstructs the world point at that depth, moves it back by the field motion (`p + flow·dt`), projects it with the previous frame's view-projection and fetches history there. Same 3×3 clamp, same `α`, same resets. Colour, transmittance and depth are reprojected by the one motion vector. **Fit with the tokens:** `--gamecult-nebula-0` (orange) is the sun's colour, `--gamecult-nebula-2` (sky) the ambient, `--gamecult-nebula-1` the cloud hue's second pole, the base the sky. Outputs as pass 2, with one change: `--gamecult-light-0-at` is written from the sun's projected position, so the CSS wash's main light follows the sun; its seed is the sun's mean screen position, so the fallback is a still of the same composition. **Soundness with a moving camera:** each frame's pre-pass evaluates `iv_frustum_ball` from that frame's camera; no ball, mask or probe result is carried across frames. The only cross-frame state is the TAA history, which is colour, not a soundness input. The saving test draws each tile's camera independently and says so. The march: a **low-resolution pre-pass** at `W/8 × H/8` runs the tile loop over the slab's cell ranges (the envelope first, the noise ball only when the envelope leaves `fade.hi > 0` and `L r F0` leaves room) and writes a per-tile skip mask of the dense cells not proven empty into an `RGBA32UI` texel (128 bits; 96 cells at the slab's fine step). The main pass reads one texel (`texelFetch`) and places its `S` samples in unmasked cells only, stratified over the unmasked length; masked cells contribute exactly zero, so this is exact importance sampling, not an approximation. The fragment cannot share a probe across pixels (derivatives reach a 2×2 quad at best), so the pre-pass is the cheaper and the only sound option in WebGL2. Cost at `N = 8` is in the budget table: ≤ 144 probes per tile worst case (nothing skipped, hierarchy descended everywhere), mostly envelope-only, ≈ 0.4–2 snoise-equivalents per pixel; `N = 16` quarters it but makes the ball's lateral term equal the fine half-length at the slab's far plane. **Predicted `N = 8`.** The site's own ratio is measured in the browser against a dense reference with the same envelope gate (the proving-ground contract's second half). ### The split Phacelle and the compile checks (glsl-lowering r2) Pass 4: the glsl branch rebases onto interval-ops **r3**'s head, because r3 changes `cultmath_iv_frustum_ball`'s radius and the fixture family with it. - Transformation step 1 gains one licence-keyed entry: an include under its own licence is lowered into its own file. `CultMath.glsl` (MIT: `CultMath.hlsl` + `CultMath.Interval.hlsl`) and `CultMath.Phacelle.glsl` (MPL-2.0, concatenated after it only by consumers that call `cultmath_phacelle`; the site does not). Both pinned; `THIRD-PARTY-NOTICES.md` lists the MPL files and no longer calls `CultMath.glsl` MPL. - `iv_exp` joins the fixture with a platform tolerance (1 ulp, the generating OS named), because `self-exp-measure-in-site` has the site measure WebGL2 `exp` against it. The fixture is regenerated for `cultmath_iv_frustum_ball`. - glslang 16.6.0 and dxc are downloaded pinned and run: the MIT-only wrapper, the two-file wrapper, a `lerp(` negative control, and a `cultmath_phacelle` call against the MIT-only wrapper that must fail (proves the split). - The glsl branch rebases onto interval-ops r2's head; its first commit is the regeneration alone. Carried, still out of scope: the Unity `CultMath.dll` rebuild (no release in the sequence); the fixture's ulp bounds on WebGL2 (site cut); Stryker's timeouts are reported as timeouts, not kills. ## Pass 4: Soul's five findings, the analytic body, the one shipped void Pinned: CultLib `main` `780d8cfa` (docs only since `d0ea37f8`); `hands/cultmath-interval-ops` at `43183276` (report `cut-interval-ops.h2`, verdict `cut-interval-ops.s1`, worktree `F:\Projects\CultLib-iv`). Rulings new in this pass: `operator-wells-contract-vs-ceiling` ((c) is held to at least 90% of its probe-free ceiling, not 2x: the ceiling is what intervals control; the fog band's cost belongs to the field) and `operator-analytic-wall-body` (where the envelope interval proves the density constant, one closed-form Beer-Lambert step instead of sampling at the content limits). Spec: `docs/cultmath-interval-ground-cut-interval-ops.r3.spec.json`. ### What r2 measured | Scenario | r2 result | Pass 3 prediction | | --- | --- | --- | | (a) height fog | 16384x | > 50x | | (b) inside the fog | 0.99x | 0.8–1.0x | | (c) Aetheria wells | 1.41x at warp D (3.32x at warp 0); probe-free ceiling 1.42x (3.46x) | 2–4x | | (d) uniform slab | 1.23x | 1.3–1.5x | | (e) void, shipped | steps per pixel 36.34 → 16.45 (2.21x fewer), cost 1.34x, snoise 26.6 | 35 → 6 (5–8x), cost 1.7x | 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 both HLSL copies, after the r2 radius: ```text radius += (|c.x| + |c.y| + |c.z| + radius) · 2^-20 (9.5367431640625e-7, exact) ``` Derivation with `u = 2^-24`: the centre `c = (m_c z_m, z_m)` carries at most two roundings per component (`z_m = fl(z0 + z1) · 0.5`, then the product), so `|c_float − c_exact| ≤ 2u |c|₁`; the radius carries at most seven first-order roundings (`z1 − z0`; the axis `sqrt(mx² + my² + 1)` at `3u`; the product; `z1 · footprint`; the sum; `+ warp`), so `r_exact ≤ r_float (1 + 7u)`; the widening's own addition rounds once more. `2^-20 = 16u` covers `8u (|c|₁ + r)` twice over. fma contraction removes roundings and only tightens, so dxc's HLSL and any driver's GLSL are covered by the same bound. Cost at the shipped scales: `|c|₁ ≈ 600`, `r ≈ 5` → `6e-4` units, 0.01% of the radius. The enclosure test computes distances in double from the float ball and the float ray, no tolerance, over the r2 domain and the extreme families (`z0 == z1`; `z1 − z0 = 1e-6 z1`; `|m_c| ≤ 1000`; `z ≤ 1e7`; `z0 = 0, z1 ≥ 1e-3`; `f ≥ 1`; warp to `1e6`), corners at both depth ends with the flow pointed outward. The derivation pin becomes two-sided (`r_exact ≤ ball.w ≤ r_exact + 2^-19 (r_exact + |c|₁)`), so a ball that forgets the widening fails and one that grows past the rule fails. The caller's world transform and its own point rounding remain the caller's; the function says so in its doc instead of a false proof. ### The one shipped void (finding 3) and Q9 The map held two voids: the Footprint-LOD section's `(Rh, S) = (198, 50)` (with an `e ≈ 0.6, Rc ≈ 220` that did not satisfy its own formula) and the site section's `Rc = 640, e = 4` (a 442-wide ramp with a 50-wide fade band). Hands implemented the first with the arithmetic corrected (`Rc = Rh + S = 248`, `e = 0.40`) and measured 2.21x; Soul measured the second at 1.93x. Under the analytic body the second gains nothing: across a 442-wide ramp the optical depth exceeds 4 before the density is provably constant, so rays end inside the ramp and the body is never proven. The map's own rule says `(Rh, S)` are the knobs, so the honest choice is the first; it changes the look relative to the site section, hence Q9 (below; ruled (a), `operator-shipped-void`). For `CARVE = 1.5` the ramp width is tied to the exponent (`S/Rh = 1/√(1 − 1.5^(−1/e)) − 1`: `e = 0.4 → 0.25`, `e = 0.7 → 0.5`, `e = 1 → 0.73`, `e = 4 → 2.2`), so "pseudo-Gaussian" and "narrow" cannot both be had without a third knob, and a narrow wall at `e = 4` needs `CARVE ≈ 58`, which is a hard wall again. The ramp width is the look-cost knob. ### The analytic body (ruling `operator-analytic-wall-body`) Consumer-side, no new CultMath function; the saving test and the site mirror each other. - **When it applies.** The consumer's `Bound` over the whole remaining range `[z, z_end]`, gated, is a point interval with positive value: for the void, the envelope with the free noise interval `[−1.5, 1.5]` proves `fade = 1` and `d_lo + A · (fade n)_lo ≥ Rc`, so every point of every ray of the tile over the rest of the grid has density exactly `K` for any noise value. For (a)–(d) no bound is a positive point (the fog's density varies with `s'`), so the step never applies there, and the test asserts it. - **Pre-pass.** `MarchTile` returns `(dense cells, bodyStart, bodyDensity)`: when a single cell is not provably empty and its own bound is a positive point, one more probe over `[z_j, z_end]`; if that is a positive point too, `bodyStart = j` and every remaining cell is appended dense without probing (always sound; it also drops the body's probes). The site writes the cell in the mask texel's fourth word, `0xFFFFFFFF` when none. - **Main pass.** Unmasked cells before `bodyStart` as before; at `bodyStart`, `T *= exp(−K · (z_end − z) · |dir|)`, one step, stop. The grid end for every void configuration becomes `|camera − c_v| + Rc + 6/K` (was `5/K`): the body starts at `d ≥ Rc + 30` plus box slack, so `6/K` leaves at least 4.9 of optical depth and `T ≤ 0.0075`. No probe in the main pass: the constant is the field's pointwise envelope value at that cell. - **Pin.** `AnalyticBodyMatchesFineMarch`: wherever a body is reported, the bound over the rest of the grid is a positive point and the full and the truncated density at 64 sampled points of the body equal it exactly; the analytic factor equals the fixed-step product over the same cells within `1e-4` relative; no (a)–(d) tile reports a body. The masked fixed-step march stays the mask's soundness pin (exact agreement with the reference); only the LOD march takes the step. Hand mutation that must fail: in `VoidField.Compose`, `CarveRadius → HollowRadius` in the lo test. ### Footprint LOD, exercised (finding 4) At `f = 935` every octave weight is 1 inside every r2 grid (the fine weight drops below 1 past `z = 2922`; the largest grid ends at 2240). `VoidField` takes a focal length, and one configuration reaches the weights: `Rh = 4000`, `S = 50`, low camera, `f = 467` (a 98° vertical FOV at 1080 rows): fine weight below 1 past 1460 and 0 past 2920, coarse below 1 past 5840, grid to ≈ 7990; the footprint term `4 z θ` also exceeds 12.5 past 1460, so `LodStep` is footprint-set there. It joins the enclosure test's field list and is printed by the saving test, not asserted. Deviation recorded: the bound takes each weight over `[w(z1), 1]`, not `[w(z1), w(z0)]`, because one `Bound` serves the full field (`w = 1`) and the truncated one (`w ∈ [w(z1), w(z0)]`); `[w(z1), 1]` contains both and `iv_mul` is monotone in its interval argument, so the composed interval encloses both fields; the slack is only in far tiles whose step the footprint already sets. Hand mutation that must fail the truncated check there: `w(z0)` for `w(z1)`. ### Contracts after r3 (asserted at `N = 8`) - (a): at least 2x combined cost. Measured 16384x. - (c): at least 0.90 of its probe-free ceiling (`Ratio / Ceiling`, the lower of warp 0 and D). Measured 0.99 at warp D, 0.96 at warp 0. - (e): at least 2x fewer LOD steps per pixel than the fixed-step reference at the one shipped void; the masked march exact; the LOD march within 0.02. - (b), (d), the (e) grid and LOD-far: printed only. ### Predictions (Imagination's rule, not a measurement) From r2's printed rows: the LOD steps that evaluated noise are about `snoise / 2.8` plus two pre-band steps; the rest were constant-body steps, which the analytic step makes one. Ref/LOD steps: | `Rh`, camera | `S = 10` r2 → r3 | `S = 50` r2 → r3 | `S = 150` r2 → r3 | | --- | --- | --- | --- | | 100, low | 2.1 → 5.3x | 1.8 → 2.5x | 1.6 → 1.6x | | 200, low | 2.7 → 5.8x | 2.2 → 2.8x | 1.8 → 1.8x | | 400, low | 3.4 → 6.2x | 2.9 → 3.5x | 2.2 → 2.2x | | 1000, low | 6.8 → 10.9x | 4.5 → 5.2x | 3.3 → 3.3x | | 1000, centred | 12.4 → 30x | | | | **shipped** (198, low, 50) | | **2.21 → ≈ 2.9x (12.5 steps), cost 1.34 → 1.38x, snoise 26.6 unchanged** | | At `S = 150` rays end inside the ramp before the body is proven, so nothing changes there. If the shipped point lands under 2.6x, the report says which assumption failed: the pre-band's 0-snoise steps, or a body start later than `Rc + 30` by box slack. ### Authority map delta (against pass 3's) - `iv_frustum_ball` is now the owner of its own float32 rounding: the widening is part of the function. Consumers add nothing for it and remain owners of their world transform. - The body proof is derived from the same `Bound` the mask is derived from, by one more probe; the main pass reads it and never decides it. The Beer-Lambert step is the consumer's integration, not a CultMath function. - `(Rc, e)` are no longer authored; they are derived from `(Rh, S)`. The site section's `Rc = 640, e = 4` is no longer an owner of the look (Q9 ruled (a), `operator-shipped-void`). - (c)'s contract is derived from its own run (0.90 of the ceiling); no absolute target is written for (c). - Deletion line: the false strictness sentence, the Tint camera citation, the (c) 2x assertion and the second shipped void leave before their replacements are written. Net in `src`: one line and a doc comment. ### Dependent specs `glsl-lowering` r2 now depends on `interval-ops` r3 and rebases onto its head (the fixture family for `cultmath_iv_frustum_ball` changes with the widening). `site-ground` r3 now depends on `interval-ops` r3; its field item carries the one shipped void with `(Rc, e)` derived, its pre-pass writes the body-start cell in the texel's fourth word, its main pass takes the one analytic step, and `Z1 = |camera − c_v| + Rc + 6/K` (614 at the shipped point, 62 cells of 10; the mask has 96 bits). Both files are edited in place on `main`; the mind's copies need re-admission by Self. ## Questions Q4-Q9 are all ruled; the options below are kept as the record of what was asked. Q9: (a), ruling `operator-shipped-void` (`rh198-s50`). Q4: (a), ruling `operator-palette-tokens`. Q5: (a), ruling `operator-wash-carry`. **Q9. The one shipped void** (pass 4; finding `shipped-void-two-scenes`). Authored as `(Rh, S)` with `(Rc, e)` derived; `CARVE = 1.5`, `A = 20`, `F0 = 0.01`, `K = 1/30`, the camera as site-ground r3. - (a) `Rh = 198`, `S = 50` → `Rc = 248`, `e = 0.40`. The scene r2 measured (2.21x; predicted ≈ 2.9x with the analytic body). A 50-wide noisy wall whose outer edge has a kink (`e < 1`), softened by the ±20 noise displacement and the TAA. **Recommended**: the map's own rule, the measured scene and the ruling's analytic body all point at it. - (b) `Rc = 640`, `e = 4` (`Rh = 198`, a 442-wide pseudo-Gaussian ramp, fade band 50): the site section's soft wall. Measured 1.93x; the body is never proven, so the analytic step never fires; the (e) contract fails unless it is dropped to printed or the detail octave is removed where the ramp is wide, both of which are the operator's to decide. - (c) `Rh = 198`, `S = 100` → `Rc = 298`, `e = 0.70`: the compromise, a softer 100-wide wall; predicted ≈ 2.4x; not in the r2 grid, so unmeasured. Hands codes (a); another ruling moves only the shipped constants here, in the r3 spec and in the site-ground spec. **Q6. The site ground's envelope look.** Answered outside the options by `cultmath-tapes:ruling:operator-ground-void-brush`: the old main menu's void (pass 3, "The site ground: the void"). **Q7. What "spherical" means for the brush.** Ruled `cultmath-tapes:ruling:operator-void-true-cavity`: a true 3D hollow inside a cloud volume that surrounds it on every side, including above; not the 2021 bowl. **Q8. `cavity-contract`.** Retired in pass 3d (see "Footprint LOD"): the contract for (e) is steps per pixel against the fixed-step reference at the shipped parameters, ≥ 2x fewer, predicted 5–8x; cost and snoise are printed beside it and promised nothing. Specs for pass 3: `docs/cultmath-interval-ground-cut-site-ground.r3.spec.json` beside the two CultLib r2 specs. **Q4. Where the shader's palette input comes from.** - (a) Four new input tokens in `custom.scss` (`--gamecult-nebula-0/1/2`, `--gamecult-nebula-base`), seeded with today's wash colours. **Recommended.** The nebula palette can be tuned without moving the UI accent, and the brand doc's rule (colour is defined in `custom.scss`) holds. - (b) Reuse `--secondary` and `--tertiary` from `quartz.config.ts` and promote the violet to a config token. Fewer tokens; ties the nebula to the UI accent for good. **Q5. Carrying the last sampled wash across page loads.** - (a) `ground.js` keeps the last written token set in `sessionStorage` and applies it before the first paint on the next page; the seeds apply when nothing is stored. A cache, never authority. **Recommended.** Without it, every navigation shows the seed wash for the first frames, then the shader. - (b) No storage; the seed paints first on every page. Simpler; a visible shift on every navigation. ## Unsettled - The Lipschitz constant is empirical with a margin; a per-cell polynomial proof is possible and not in this cut. - `_DoFBlurTex`'s writer is not in the Aetheria checkout; the web DoF takes the operator's recollection (final transmittance) as the driver. If stills of the video arrive, Hands can match the blur radius against them, but the mechanism does not depend on it. - The ritual ground's radii are `rem` lengths and its first gradient stop is unpositioned (`custom.scss:124`); Hands normalises it when tokenising. - Whether GameCult-Quartz's bundler permits a dynamic `import()` from a component's `afterDOMLoaded` or needs a script tag; one line either way. - `:root:has(body[data-slug])` for the ritual seeds: supported in every current browser; a browser without `:has` gets the default seeds, which is acceptable for a fallback of a fallback. ## Pass 5: affine forms (ruling `affine-forms-unparked`) Imagination pass 5, 2026-10-03. It maps the affine-arithmetic piece of the tape target, unparked on its own by ruling `cultmath-tapes:ruling:affine-forms-unparked`. It answers follow-up `cultmath-tapes:follow_up:wells-oracle-shortfall-affine-forms`. No tape, pruning or interpreter is built; tape steps 2-5 stay parked under `tape-target-unparks-when-asura-stable`. Pinned head: CultLib `main` at `a7966142`. Every `file:line` in this section is against that commit. ### What was measured (body facts) The probe is a renamed copy of `NoiseBoundTests.cs` with five more bounds for `FogField`. It lives on the local branch `scratch/imagination-affine-probe` (`5fa8e9ce`). That branch was never pushed to origin and is not for merging. It was run on Yggdrasil through the ygg-verify stopgap (`dotnet/sdk:10.0`, about 3 minutes). It re-runs `Run(FogField.Wells(w), 8, 64, 0x5A7E + 8)`, so the tiles, rays and seeds are those of the landed (c) row at `N = 8`. V0 reproduces the landed figures exactly (0.378 and 0.194). Constants the probe needed, each sampled over 2e5 seeded points with a 1.25 margin (probe grade, not provenance): - the Frobenius norm of the Hessian of `snoise(float3)`, max 55.15, so `M = 68.94`; - the gradient length of `snoise(float2)`, max 7.35, so `L2 = 9.19`; - for comparison, the mean `|snoise_grad|` is 2.59, against `L = 10.10`. The bounds: - **V0**: the landed bound. - **V1**: V0 with the detail octave's ball no longer enlarged by the warp. The detail reads the unwarped point (`NoiseBoundTests.cs:860`, `Noise.Detail(p, ...)` of the unwarped `p`), yet `Bound` hands it `ball.w`, which carries `D` (`:875`, `:890`). - **V2**: V1 with the warp centred. Each phase's coverage ball is centred at `c + flow(c) shift_k`, with radius `r + |shift_k| rho`, where `rho = FlowAmplitude L2 FlowFrequency r sqrt 2` bounds how far the flow moves over the ball. This is the mean-value form of the warp: interval arithmetic only, no affine noise. - **V3**: V2 with the centred form of the noise, `n(c) +- min(L R, |g(c)| R + M R^2 / 2)` from one `snoise_grad`. - **V4**: V3 with a reduced affine form. Three shared symbols (depth `t`, slopes `u` and `v`) carry the envelope's plane term and every noise term's linear part, and one error symbol takes the rest. It applies where the fade is the point 1, and is intersected with V3. - **V5**: V4 with one shared symbol, depth `t`, plus one error symbol: Gamito's `t0 + t1 e1 + t2 e2`. A gradient evaluation is counted as 2 snoise, an estimate. V3 to V5 gate a probe on the best result any centre value and gradient could give. Range ceiling. A cell is range-nonempty if the density is positive at any of 5 x 5 slopes over the tile's slope square (corners included) times 9 depths over the cell, or if it is in the oracle mask. Its efficiency is the oracle march's cost over the cost of marching the range mask, with probes free. The sampling can miss nonzero points, so this figure is an upper estimate. No sound enclosure can beat it. | (c), `N = 8` | efficiency (oracle) | cost ratio | dense cells/ray | probe overhead | cull | | --- | --- | --- | --- | --- | --- | | warp 0, V0 landed interval | 0.378 | 2.74x | 12.40 | 0.961 | 0.952 | | warp 0, V2 centred interval | 0.368 | 2.67x | 12.40 | 0.937 | 0.952 | | warp 0, V3 centred noise | 0.466 | 3.38x | 8.67 | 0.868 | 0.974 | | warp 0, V5 affine, one symbol | **0.511** | **3.71x** | 7.81 | 0.859 | 0.979 | | warp 0, V4 affine, three symbols | 0.534 | 3.88x | 7.37 | 0.853 | 0.981 | | warp 0, range ceiling | 0.859 | | | | 0.995 | | warp 60, V0 landed interval | 0.194 | 1.39x | 31.55 | 0.993 | 0.842 | | warp 60, V1 detail unwarped | 0.205 | 1.47x | 27.74 | 0.963 | 0.864 | | warp 60, V2 centred interval | 0.347 | 2.49x | 13.71 | 0.938 | 0.945 | | warp 60, V3 centred noise | 0.441 | 3.16x | 9.52 | 0.870 | 0.969 | | warp 60, V5 affine, one symbol | **0.477** | **3.42x** | 8.71 | 0.864 | 0.974 | | warp 60, V4 affine, three symbols | 0.502 | 3.60x | 8.16 | 0.856 | 0.977 | | warp 60, range ceiling | 0.854 | | | | 0.995 | 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 `iv_*`); - `af_add`, `af_sub`, `af_neg`, `af_scale`, `af_add_iv`, `af_mul`; - `af_snoise`, `af_fbm`; - `af_frustum_axis`, `af_frustum_ball`. No others until a consumer names one, which is the rule `design.md` already states for intervals. The interval side gains `SNOISE2_LIPSCHITZ` and the overload `iv_snoise_ball(float2, float)`. The (c) field's flow is `snoise(float2)`, and centring the warp needs a bound on it, which belongs to its owner. `af_snoise(centre, axis, radius)` makes one `snoise_grad` call. With `R = |axis| + radius`, it returns the narrower of two sound forms: - the Lipschitz form `(n, 0, L R)`; - the centred form `(n, g · axis, |g| radius + M R² / 2)`. `e` absorbs the op's own float rounding and an allowance for a GPU's gradient error. The allowance is derived in the doc comment and measured on WebGL2 by the fixture's enclosure check, as `iv_frustum_ball`'s is. The warp stays the consumer's, so CultMath owns no flow. The rule, stated in `design.md` and tested for the (c) field: a warp whose value at the centre is known, and whose variation over the ball is at most `ρ`, is enclosed by moving the centre by `w(c)` and adding `ρ` to the radius. Interval balls benefit from it too (V2). ### Authority map - **Owner.** `src/CultMath/math.Affine.cs` owns affine-form semantics on the CPU. `shaders/CultMath.Affine.hlsl` mirrors it bit for bit, and is the shader semantic authority. `shaders/CultMath.glsl` is derived from the HLSL. - **Inputs.** - `snoise_grad`, whose `.w` is bit-equal to `snoise`; - `SNOISE_LIPSCHITZ`; - `SNOISE_HESSIAN`; - the caller's forms and points. - **Outputs.** `float3` forms, and `float2` intervals through `af_range`. - **Derived state.** - The GLSL text is generated by `GlslLowering`, unchanged. - The fixture families are generated. - The (c) efficiency rows are printed measurements, not state. - **Forbidden writers.** - A consumer linearising noise itself: the probe's V4 and V5 did this by hand, and Hands must call `af_snoise`. - A second curvature constant anywhere. - Hand-edited `CultMath.glsl`. - An affine or interval evaluator over `CultMath.Expr` (the parked tape's, see collisions below). - A flow or envelope function in CultMath. Consumers compose those, as for intervals. - **Shared paths.** The test field's `FogField`, the site ground and Aetheria's nebula all reach affine noise through `af_snoise` or `af_fbm`, and a tile slice through `af_frustum_*`. - **Deletion line.** - In `FogField.Bound` (`NoiseBoundTests.cs:875`), the warp-enlarged ball is replaced by the centred warp, and the detail ball stops carrying the warp (`:890`). - The (c) probe-overhead assertion (`:1558`, `:1588`) is replaced as the ruling on `affine-wells-contract` says. - `design.md`'s "What they do not: ... affine forms" (`:584-587`) and "That gap belongs to the tape target's affine forms" (`:623-624`) are rewritten. - The tape target's open item on representation is closed. ### Cut order 1. **`affine-forms`, the one she can see.** - It adds C# `math.Affine.cs` and `AffineTests`, plus `SNOISE_HESSIAN`'s provenance. - The (c) field gains the centred warp and the affine bound. - (c) is re-measured with three bound rows on the same tiles: landed interval, centred interval, affine. The range ceiling is printed beside the oracle. - An explicit, env-gated test writes a cull-map PNG of one (c) frame: dense cells per ray for each bound, and the transmittance. Self publishes it. - `design.md` is updated. - It is blocked on `affine-wells-contract` only for the assertion's shape. 2. **`affine-shaders`.** - It adds `CultMath.Affine.hlsl` and its Unity copy and `.meta`, the include, and the regenerated `CultMath.glsl`. - Fixture families are appended, with an enclosure check for `af_snoise` and `af_frustum_ball`. - The dxc and glslang compiles and the WebGL2 golden-fixture run happen on Starfire. - Both cuts share the branch `hands/cultmath-affine-forms` and merge together after cut 2's Soul pass. Main never carries a C#-only `af_*`, so three-runtimes-agree holds at every merged commit. Writing the C# before the HLSL inverts the usual order, because HLSL is the semantic authority. The cost is small: the ops are plain arithmetic in the common subset. Cut 2 changes the C# if the HLSL dialect forces something, never the reverse. ### Verification venue Heavy work runs on Yggdrasil through the stopgap: `dotnet test`, Stryker scoped to `math.Affine.cs`, the (c) re-measure and the cull-map render. Starfire runs only the Windows and GPU legs, one job at a time: - `dxc -T lib_6_3 -HV 2021` on `CultMath.hlsl`; - `glslangValidator` 16.6.0 on the lowered GLSL (ruling `operator-compiler-downloads`); - the WebGL2 golden-fixture run on the GTX 1070, under ANGLE D3D11 in a browser. It is seeded from the glsl-lowering Soul pass's `gpu.js` (follow-up `glsl-browser-parity-run`). ### Collisions - **`cycles-converters` r1** edits the same seams: - the include list (`CultMath.hlsl:509-510`); - `HlslMirrorTests.cs:64-67`; - `build-unity-package.ps1:82-87`; - the fixture's family list. Whichever merges second rebases, regenerates `CultMath.glsl` and the fixture (`CULTMATH_WRITE_GLSL=1`), and appends its families after the first's. Nothing semantic collides. - **`expr-ir` r1 and question `material-ir-in-cultmath`.** No file collides, and neither cut waits on the other. - Under `cultmath-ir-now`, the parked tape's step 2 will add interval and affine evaluators over `ExprGraph`, and those will call `iv_*` and `af_*` as op semantics. That is one more reason the affine ops are plain value functions over `float3`, with no struct state. - Under `aetheria-local`, nothing here changes. - The `expr-ir` spec's negative check forbids `Interval(Evaluator)?` under `Expr`. Self may add `Affine` to it at its next revision. - **`site-ground` r3** (not dispatched): the void (e) and the site's flow texture are not measured here. Follow-up `site-ground-affine-and-centred-warp` carries both. Centring the warp in the site needs a bound on the flow texture's variation, and the texture stays the one motion owner (it is sampled at the centre, never re-derived). ### Pass 5b: the deep well (ruling `wells-scale-order-of-magnitude`) The operator's words: "There should be a well pushing down with both a radius and depth an order of magnitude larger than the span of the bit that's expensive to sample, the fog gradient, otherwise there's no meaningful skipping being measured". **Her diagnosis holds.** The expensive band is the stretch of the pre-distortion height `s` where only the noise decides whether there is density. - `FogField` has `F = -20` and `A = 20`, and the noise reaches ±1.5. With the fade at 1, a slice is provably empty only where `s >= F + 1.5 A = 10`. - It is dense whatever the noise does where `s < F - 1.5 A = -50`. - So the band is `G = 60` units. Today's (c) wells, from `NoiseBoundTests.cs:753-776`: - **Depths** are `d = 30 M^0.175`: 67, 101, 150 and 101. That is 1.1 to 2.5 times `G`. - **Half-depth radii** are about `0.103 x 500 M^0.25` for a `PowerPulse` exponent of 16: 163, 290, 515 and 290. That is 2.7 to 8.6 times `G`. - **The zone bowl** is 64 deep, 1.07 times `G`. No well is ten times `G` deep. The camera sits just above the band (`y = max(0, S + 10 - h)`) and grazes it, so most of the measured cost is the band itself, and little of it is empty space. **The probe.** The probe added a field, `(c') deep well`, on the scratch branch at `bf0148b8`: - the zone bowl plus one well at its centre, with depth 600 and half-depth radius 600 (`PowerPulse` exponent 2, scale 2220), so each is `10 G`; - the camera's xz within 300 of the centre, at `y = F - h / 2`, halfway between the sunken fog surface and the rim; - slopes as (c), `m_y` in [-0.25, 0.1]. A level ray crosses about 600 units of empty well before the wall, and the band there is about 73 units along the ray. The run used the same run parameters, seeds and constants as Pass 5, on Yggdrasil. | `N = 8` | efficiency vs oracle | cost ratio | dense cells/ray | probe overhead | cull | | --- | --- | --- | --- | --- | --- | | deep well, warp 0, landed interval | 0.591 | 3.10x | 4.14 | 0.943 | 0.986 | | deep well, warp 0, centred interval | 0.579 | 3.04x | 4.14 | 0.924 | 0.986 | | deep well, warp 0, affine (one symbol) | **0.662** | **3.47x** | 3.27 | 0.879 | 0.993 | | deep well, warp 0, range ceiling | 0.859 | (oracle ceiling 5.24x) | | | 0.997 | | deep well, warp 60, landed interval | 0.462 | 2.45x | 6.58 | 0.970 | 0.967 | | deep well, warp 60, centred interval | 0.555 | 2.95x | 4.34 | 0.926 | 0.984 | | deep well, warp 60, affine (one symbol) | **0.644** | **3.42x** | 3.33 | 0.880 | 0.992 | | deep well, warp 60, range ceiling | 0.848 | (oracle ceiling 5.31x) | | | 0.996 | There were no enclosure violations, and transmittance equals the dense march in every row. What changes: - Where the empty space is real, the interval march already skips most of it. It reaches 0.59 at warp 0, and 0.46, or 0.56 with the warp centred, at warp `D`. - Affine's gain over the centred interval shrinks to 1.14x and 1.16x in combined cost, against 1.39x and 1.37x on the band-dominated wells. - Against the landed interval it is 1.12x and 1.40x. - Affine reaches 0.77 and 0.76 of the range ceiling. What it wins is in the wall's band, which is where the shallow wells spend everything. - Probe overhead is 0.88 in both rows, still under the 0.90 rule. Decision for the cut (`affine-forms` r2): **(c) becomes the deep well, which carries the contract.** The four mass-derived wells are kept as a printed row, `(c-band)`. They are not the skipping measurement the ruling asks for, but they are Aetheria's own geometry and the band-dominated case, where affine gains most. Dropping them would hide where affine pays. The cull-map image renders a deep-well camera at warp `D`. The contract question is re-asked against these numbers as `cultmath-tapes:question:affine-wells-contract-r2`. The first question was withdrawn in its favour, and `affine-forms` r2 supersedes r1. ## Pass 6: the snoise diagonal tie (finding `snoise-diagonal-tie-discontinuity`) Soul's affine pass found that `snoise(float3)` and `snoise_grad` jump by up to 0.72 where the three components of `x0` compare equal (`cultmath-tapes:finding:cut-affine-forms.s1.snoise-diagonal-tie-discontinuity`). The defect predates every cut in this campaign. It came with the Ashima kernel and survived the 0.3.0 kernel change. The cut is `cultmath-tapes:cut_spec:cut-snoise-tie.r1`. ### Mechanism (probed) `g = step(x0.yzx, x0.xyz)` makes three pairwise comparisons, all non-strict: x wins its tie with y, y wins its tie with z, and z wins its tie with x. That is a cycle, not an order. When all three components are equal, `g = (1,1,1)`, so `i1 = (0,0,0)` and `i2 = (1,1,1)`, and the four corners are not a simplex. - Corner 1 is then the origin vertex again, with the offset `x0 + 1/6`. Near a lattice vertex that offset is short (`|x1|^2 = 1/12`), so the origin's gradient is counted a second time. - The tie set is the union of the lattice diagonals `v + s(1,1,1)`. It has measure zero in space, but consumers sample it. Every point `(t, t, t)` lies on it exactly in float32, for every `t`, because the three components are computed identically. So does the origin: `snoise(0,0,0)` returns -0.4358730, where the correct value is 0. `HlslSemanticsTests.cs:447` pins the wrong value as golden. A consumer that samples `snoise(float3(t))` or `cultmath_snoise(t.xxx)` reads a different function along the whole line. - With the 0.5 kernel, a lattice vertex outside the four summed corners never contributes on a face, because its squared distance from the face is at least 0.5. So any total order of the three comparisons gives the same exact value on every tie. The fix only has to make the comparisons an order; which order it picks does not matter. ### Prior art - Gustavson's reference C, `simplexnoise1234.c`, picks the 3D simplex with an if-chain on `x0>=y0`, `y0>=z0` and `x0>=z0` and their complements. Ties go x over y, y over z and x over z: a total order. - Gustavson and McEwan's later `psrdnoise3.glsl` (JCGT 2022) replaces Ashima's line with `step(f0.xyx, f0.yzz)` and a re-pairing. Its comment says ties resolve "in priority order": also a total order, z over y over x. - Upstream `webgl-noise` `noise3D.glsl` still has the cyclic `step`. Soul found no fix in its issues or its git history (`log -S` on the line). **The fix** makes one comparison strict, z against x: `g = (step(x0.y, x0.x), step(x0.z, x0.y), 1 - step(x0.z, x0.x))`. Ties then go x over y, y over z and x over z, which is exactly `simplexnoise1234`'s order. The probe's double-precision reference uses that if-chain verbatim, and the patched float kernel agrees with it to within float noise. Other total orders are equivalent mutants: one strict compare placed on x, or two strict compares. Only two spellings are degenerate, all-strict and today's all-non-strict, and the cut's test kills both. ### What the probes measured Both jobs ran on Yggdrasil, on main `a7966142` and on the affine head `4ae986a5`. Each patched C# and both HLSL copies in the container, and regenerated GLSL and the fixture. | Probe | Old rule | Strict-z rule | | --- | --- | --- | | Random points, \|p\| <= 4, 256, 4096 (1e6 each): old and new bit-equal | | 1e6 of 1e6 at every scale | | Random points, \|p\| <= 65536 (1e6) | | 23 differ, each on an exact z = x tie, at ulp level | | Lattice-diagonal points and their +-2 ulp neighbourhoods, \|I\| <= 8, 64, 512, 4096 (500k each, about 10.5k exact triple ties each): max \|f32 - ref64\| | 0.772, 0.768, 0.775, 0.774 | 2.9e-6, 2.2e-5, 1.8e-4, 1.6e-3 (float32 at that magnitude) | | Pairwise faces x0 = (a,a,b) and permutations (108k), \|I\| <= 8 / 512 | 2.5e-6 / 1.3e-4 | the same | | Jump inside a 1e-4 ball at Soul's five witnesses | 0.21 to 0.72 | 0.0016 to 0.0018 (smooth bound 7.6 x 2 sqrt3 x 1e-4 = 0.0026) | | Soul's vertex scan, worst jump, \|L\| <= 8, 32, 128 | 0.72, 0.65, 0.65 | 3.7e-4, 1.5e-3, 5.7e-3; 0 of 400 beyond the smooth bound at every scale | | `af_snoise` around the old Lipschitz spike (2000 forms x 21 points) | 926 misses, worst 38 e | 0 misses, worst -0.72 e | | `MeasureLipschitz` (seed 0x11B5) | refined 9.181147, L = 10.099261 | refined 7.2400064, x 1.10 = **7.9640074**, witness (229.92526, 73.37927, -66.14595) | | `MeasureHessian` (affine head) | 50.954895, giving 56.050385 | unchanged, same witness | | `snoise_grad(p).w == snoise(p)` in a 1e-4 ball about the old witness | | 0 mismatches | The full suite passes once four things change together: the tie fix, `L` re-pinned, the witness moved, and the origin golden set to 0. It passes 288 of 288 on main and 300 of 300 on the affine head. With only the tie fix and nothing re-pinned, exactly two tests fail: - `LipschitzConstantPinsSampledGradients`, the ratio pin, failing as designed; - the origin golden. `HlslSourceCompatibilityTests` passes. Its special values include all-equal triples, so it pins the HLSL tie rule bit for bit against C#. Fixture bytes: - The tie fix alone changes no fixture case. The 256 random points per family never land on a tie. - Re-pinning `L` changes 41 cases, all in `iv_snoise_ball(vec3)` and `iv_fbm_ball`. These are the cases whose bound is not clamped to `[-1, 1]`. - Every other family is byte-identical. - `CultMath.glsl` changes 3 lines: the two tie lines and the constant. ### What follows - **`SNOISE_LIPSCHITZ` narrows by 21%,** from 10.099261 to 7.9640074. Every Lipschitz-form bound narrows with it: `iv_snoise_ball`, `iv_fbm_ball`, the Lipschitz form inside `af_snoise`, and the march's skipping. - The (c) rows in Passes 5 and 5b were measured at the old `L`. They are conservative, not wrong. The affine branch re-measures them after it merges main. - `SNOISE_HESSIAN` does not move. - **Release.** `math.snoise` returns different values on the tie set, which includes the origin and every `(t, t, t)`, points ordinary consumers sample. - The precedent is `asura:ruling:qn1-snoise-upstream`: a public snoise value change ships as a minor bump while the package is below 1.0, filed under Breaking. So the next `cultmath-unity` release after this cut is 0.4.0, not 0.3.x. - The intervals and `L` were never released (`cultmath-unity-v0.3.0` predates `math.Interval.cs`), so re-pinning `L` is not a release change. - The release is not part of this cut. Follow-up `cultmath-tapes:follow_up:snoise-tie-release` carries it, together with Aetheria's pin bump to that release. - **Consumers.** - gamecult-site vendors no CultMath GLSL yet, because `site-ground` has not landed. It gets the fixed GLSL when it does. - Aetheria pins 0.2.4, the old 0.6 kernel, which has the same cyclic `step` line. Its pin bump (`asura:follow_up:aetheria-snoise-pin-bump`) should target the release that carries this cut. - No other runtime under `packages/` (Rust, TS, Python) carries snoise. ### Merge order 1. `snoise-tie` branches from main `a7966142` as `hands/cultmath-snoise-tie`. It gets its own Soul gate and merges to main first: it is small, independent, and fixes a defect in released code. Its two fixture families go after `phacelle`. 2. `hands/cultmath-affine-forms` then merges main with a merge commit. It never rebases, because the branch is shared. The job's edits to `math.Interval.cs`, `NoiseBoundTests.cs` and `design.md` applied cleanly to the affine head. The affine fix batch's (c) re-measure and the affine Soul pass run on the merged tree, so they see the new `L`. 3. `affine-shaders` is revised to r2, which supersedes r1. It depends on `snoise-tie` and starts from the merged tree, so its `af_*` families are appended after the tie families, and its device enclosure checks run on the fixed `cultmath_snoise_grad`. Nothing else in it changes. ## As built: the affine and tie cuts on main (Modeling, 2026-10-04) Body map against CultLib `main` at `14568bf9`, after `48aadda0` (snoise tie) and `156782da` (affine forms, merged as `hands/cultmath-affine-forms`). It records what the Body is, not what Passes 5, 5b and 6 intended. Where it disagrees with those passes, this section wins; where it disagrees with the Body, the Body wins. Cut reports live in the eureka-state mind under `cultmath-tapes`: `cut-snoise-tie.h2`, `cut-affine-forms.h1-h3`, `cut-affine-shaders.h1-h3`. ### Pieces and owners Paths are under `packages/cultmath/`. | Piece | Path | Owner | Role | | --- | --- | --- | --- | | 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. | | Affine HLSL | `shaders/CultMath.Affine.hlsl`, byte-identical `unity/org.gamecult.cultmath/Shaders/CultMath.Affine.hlsl` | library | `cultmath_`-prefixed bit-for-bit mirror of `math.Affine.cs`. | | 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 current output, so a hand edit fails them. - `unity/.../Shaders/CultMath*.hlsl` is a copy. `HlslMirrorTests.UnityPackageShaderIsIdenticalToTheCanonicalShader` pins byte identity per file (`Interval` and `Affine` among them). Edit `shaders/` only. - `SNOISE_LIPSCHITZ` is written in three places (C#, Interval HLSL, generated GLSL) and re-pinned only by running `MeasureLipschitz`. `SNOISE_HESSIAN` has one definition in C# and one in `CultMath.Affine.hlsl`. - Forbidden: a consumer linearising noise itself, a second curvature constant, an evaluator over `CultMath.Expr`, a flow or envelope function in CultMath. - Shared paths a cut touches: the include list in `CultMath.hlsl`, `HlslMirrorTests` InlineData, `scripts/build-unity-package.ps1`'s file list, and the fixture's family list. A cut that adds families appends after the previous cut's and regenerates. ### The mirror chain C# (`math.cs`, `math.Interval.cs`, `math.Affine.cs`) -> HLSL (`shaders/`, Unity copy byte-identical) -> GLSL ES 3.00 (generated by `GlslLowering`) -> fixture (generated from C#). A change starts in C#; the HLSL follows bit for bit; the GLSL and fixture are regenerated. `HlslMirrorTests` sweeps C# against the HLSL semantics on the CPU, including `af_snoise`'s centred branch on both sides of the choice. The tie fix landed in C#, `CultMath.hlsl`, the Unity copy and the regenerated GLSL in one merge. ### Verification layer - Yggdrasil (CPU, through the stopgap): `dotnet test` of `CultMath.Tests` (mirror, GLSL golden-fixture comparison, `AffineTests`, the `NoiseBoundTests` gates, `NoiseGradTests.SnoiseIsContinuousOnTheSimplexDiagonal`), Stryker scoped to `math.Affine.cs`, and the explicit (c) and deep-well re-measures. - Starfire (Windows and GPU, one job at a time): `tools/compile-glsl.ps1` (glslangValidator), the dxc `lib_6_3 -HV 2021` compile of the HLSL, and the WebGL2 golden-fixture run on the GTX 1070 under ANGLE D3D11. The device run checks `af_snoise`, `af_fbm` and `af_frustum_ball` for enclosure against the device's own noise (`design.md`, GLSL target). The device runner is not in the repository. Follow-up `affine-device-runner-seed` carries it; until it lands the device leg is a Starfire-local procedure, not a repeatable test, and `glsl-browser-parity-run` stays open. ### Release state - `unity/org.gamecult.cultmath/package.json` is 0.3.0 and `CHANGELOG.md` has no unreleased section. The snoise value change on the tie set (the origin and every `(t, t, t)`) is Breaking by the `qn1-snoise-upstream` precedent, so the next release is 0.4.0. Follow-up `snoise-tie-release` carries the version bump, the changelog entry (Breaking: snoise; Added: `iv_*`, `af_*`, the two HLSL files and the GLSL library) and Aetheria's pin bump. - Aetheria pins 0.2.4 (the 0.6 kernel with the cyclic `step`). gamecult-site vendors no CultMath GLSL yet. - `SNOISE_LIPSCHITZ` and the intervals were never released, so re-pinning `L` is not itself a release event. ### Stale docs and state found (not edited) - This file's header status line says "pass 4" and pins `c49c76ee`; Pass 5's pin is `a7966142`. Neither is the as-built head. - The (d) row near line 529 ("ceiling at `L = 10.099261`") and Pass 5's "`L = 10.10`" (near line 1037) are measurements at the old `L`, now 7.9640074. Pass 6 says they are conservative; the numbers are not re-printed. - Pass 5 "Cut order" reads as future work and Pass 6 "Merge order" lists the merge as pending. Both merged. Pass 5's deletion line cites `NoiseBoundTests.cs` line numbers that have moved. - `docs/cultmath-tape-target.md`: the header "Status: **parked**" (line 3) and "The operator parked (c)" (line 54) say nothing of affine; only the "Open before unparking" item (lines 109-112) records that affine forms were unparked, and nothing records them as built. The interval-or-affine representation question is closed: both exist. Tape steps 2-5 remain parked. - `docs/cultmath-interval-ground-cut-interval-ops*.spec.json` (r1-r3) pin `SNOISE_LIPSCHITZ` "untouched" and cite 10.099261 as provenance. They are spec history, superseded by the constant in `math.Interval.cs`. - `unity/org.gamecult.cultmath/CHANGELOG.md` has no entry for the tie change, intervals or affine forms. - The `0.77` in `packages/cultmath/docs/design.md:376` and `NoiseGradTests.cs:195` is the upstream tie jump and is accurate; it is a different 0.77 from the range-ceiling ratio at line 1347 of this file. ## Pass 7: the tape unparks (ruling `tape-unparked-for-trophic-graphs`) Written by Imagination (agent `imagination-tape-unpark`, session `self-2026-10-14-eureka`) against CultLib `origin/main` `9ec66d77`. The operator, 2026-10-14: "Unpark the tape". Her choice (`aetheria-fauna:ruling:trophic-graph-tape-ruled`) scopes tape steps 2 and 4 of `docs/cultmath-tape-target.md`: a graph document, a C# evaluator and an HLSL interpreter. Pruning (step 3) and meshing (step 5) stay where the target sequences them. The first consumer is Aetheria's trophic placement graph (`aetheria-fauna:target:r4`; Aetheria map section r4 at `4354ef2c`, rows B28-B36). `aetheria-fauna:ruling:skein-filament-construction-ruled` 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`; HLSL: `StructuredBuffer` 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 cut. `aetheria-fauna:follow_up:flow-parity-under-cultmath` (FlowField's `Tri3D`) resolves the same way: `Tri3D` becomes a CultMath function and op, with no Aetheria twin. ### Decisions (defaults, not forks) - **`CultMath.Expr` is the tape's graph; there is one IR.** `expr-ir` r1 is superseded by r2, the first tape cut, under the same label: ruling `material-ir-cultmath-now` names the owner and the scalar SSA shape, and the tape ruling consumes that graph. r2 drops what only the material line used (cones, frontiers, the HLSL emitter: T2) and the `cycles-converters` dependency (T3). Those return as lines when their consumers do. - **Scalar SSA**, as ruled: vector arguments are decomposed operands, and multi-result ops are read through `Pick`. Fidget's tapes are scalar too. - **The graph is a class; its wire shape is a resolver beside `CultMathResolver`** (T4, T5). CultMath stays reference-free and the coverage tests stay as they are. No JSON shape in the first cut (JSON is debug and xenos export). - **Stencils are substitution at build time** (T7a), not a loop in the tape. Each stencil level multiplies the graph by its tap count, as B36 measured. - **`cycles-noise` r1 is withdrawn**: base `a7966142`, it waits on the `cycles-converters` merge and is not on this session's frontier. It is a line below. ### Questions (admitted in the mind) - `tape-register-model`: the HLSL interpreter's register model. Ruled `tape-register-model-ruled` (groupshared registers). - `lattice-site-weight-source`: how a weighted lattice node gets a per-site weight (T7c). Re-asked as `lattice-weight-from-bodies` and ruled `lattice-weight-from-bodies-ruled` (node-reads-bodies). - `lattice-window-sound-or-measured` (from section "lattice-weighted-cellular"): a proven ring search or a fixed measured 7x7 window. Ruled `lattice-ring-exit-sound` (ring by ring until no farther cell can win). Prior art in hand: `F:\Projects\aetheria-material-path-prior-art.md` section 4 (GPU interpreters: Fidget's crates, Dolphin ubershaders, Laine et al. on megakernel register pressure; no measured interpreter-versus-kernel timing was found) and section 6 (IR placement); Aetheria `docs/research/2026-10-14-trophic-dag-filaments-prior-art.md` (Voronoi, filaments). Neither covers a GPU tape interpreter's register storage (dynamic indexing of local arrays and scratch spill, groupshared memory, Fidget-wgpu's `tape_interpreter.wgsl`, Keeter's MPR 2020) or per-site weight fields in lattice Voronoi. Both questions have since been ruled. ### Cut sequence (Pass 7; one line each, mapped when about to run) Fauna's first cut, `fauna-trophic-habitat`, evaluates the habitat graph in C# at candidates. It needs `expr-ir` r2, gaps (2) and (4) with (3) under the strands, gap (1), and their ops registered (line 5). It does not need the interpreter or gap (5); its presenter does. Lines 1 and 3 and `cycles-converters` are on main (`911fc755`). The critical path is line 4a, after the ruling on `lattice-window-sound-or-measured`, then line 5. Line 4b, the shader twins, is not on the path to fauna's first cut; it is on the path to fauna's presenter, with line 6. 1. `expr-ir` r2 (spec admitted): the graph, builder with substitution, decode validation, wire shape, C# evaluator. Touches no file `cycles-converters` touches (new `Expr` folder, `GameCult.Caching.MessagePack`), so it runs on `origin/main` now and does not wait on that merge. 2. (No sibling spec.) Gap (3) alone was the candidate. It and every node cut append GLSL fixture families, bump the mirror's compared count and edit `CultMath.hlsl`, and so does `cycles-converters` (`53f4c213`: `GlslMirrorTests.cs`, `HlslSourceCompatibilityTests.cs`, `glsl-parity.json`, `CultMath.hlsl`). Families append after the previous cut's, so these cuts map on the merged head, not beside the branch. Gap (3) folds into line 3, whose strand kernel is its only consumer. 3. `trophic-nodes` (CultLib): spec `cut-trophic-nodes.r1` admitted; see "trophic-nodes" below for its body facts and decisions. 4. Gap (1), mapped in section "lattice-weighted-cellular" on 3's merged head (`911fc755`), in two cuts. 4a, `lattice-weighted-cellular` (`cut-lattice-weighted-cellular.r1`), is C#: the node reads the body span and weights each cell at its centre through line 3's exact `power_border`. It waits on question `lattice-window-sound-or-measured`. 4b, `lattice-weighted-shaders` (`cut-lattice-weighted-shaders.r1`), adds the twins of its four steps, the GLSL, the families and the lavapipe bounds. It waits on 4a. 5. `expr-trophic-ops` (CultLib): the span input kind and ops for 2-4 in the op table; the strand reduction over the gated edge span (the graph has no loop, so a span op is a CultMath function: a C# span overload over a step with its shader twin, the convention line 3 sets); follow-up `expr-create-copy-then-validate` (the first cut that touches `ExprGraph`; line 3 does not); a CultMath Unity tag for fauna. Waits on 1, 3 and 4. Fauna's first cut waits on this. 6. `tape-interpreter` (CultLib): bytecode lowering, `CultMath.Tape.hlsl`, and the graph-parity harness (gap 5): C# evaluator against the interpreter on a graph corpus and on a consumer's graph, dxc compile plus a device run, held to tolerances measured per op under `cycles-kernel-reference-ruled`'s stance. Waits on 1 and `tape-register-model`. Fauna's presenter waits on it. 7. `expr-cycles-ops`: `cycles_*` ops, after `cycles-converters` merges. 8. `cycles-noise`, re-mapped from the withdrawn r1, after `cycles-converters` merges. 9. Material line, when the material graph returns after the demo: cones, frontiers and an HLSL emitter over the same graph. 10. `cultmath-unity-math-parity` (follow-up): square, exp2, exp10, log2, log10. 11. Pruning (tape step 3) and meshing (step 5): parked as the target says. ### trophic-nodes Written by Imagination (agent `imagination-ag4-trophic`, session `self-2026-10-16-ag4`) against CultLib `origin/main` `f697e03f`, where `cycles-converters` (`b136ad9b`), `expr-ir` and `stage-functions`/`stage-shaders` (`d7d1bcf3`) are merged. One CultLib cut owns what fauna's map line `cultmath-trophic-nodes` and this map's line 3 both name; fauna's consumer cut (`fauna-trophic-habitat`) waits on line 5 and the Unity tag, not on this cut alone. Spec: `cultmath-tapes:cut_spec:cut-trophic-nodes.r1`. #### Body facts (trophic-nodes) | # | Question | Result | |---|---|---| | 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` (`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`. | #### 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. ### lattice-weighted-cellular 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' convention: `lattice_site`, `lattice_weight`, `lattice_distance`, `lattice_smooth_step`. ## Pass 8: volumetric prioritized (ruling `volumetric-work-prioritized`) Written by Imagination (agent `imagination-volumetric-frontier`, session `self-2026-10-14-eureka`) against CultLib `main` `dfc720e7`. The operator, 2026-10-14, relayed by the Aetheria Self: "I want that volumetric work prioritized, since it's a totally different authoring framework for the nebulae and I'll need to build on it to get it up to par". The ruling names three pieces (the interval- and affine-culled march, the site ground, the integration-native noise domain) and leaves their order to this Self. Later the same day she pointed at "the last nebula playground" as having sophisticated noise. Facts on her two playgrounds were extracted by the Aetheria Self's Eyes into `F:\Projects\eureka-handoff-aux-2026-10-14\eyes-nebula-labs\facts.md`. ### The frontier as found | Piece | Record | State | | --- | --- | --- | | 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 site ground's field was then open between the Span Lab framework, the kernel cascade and r3's marched void. The r3 machinery (the interval pre-pass, the centred warp, the debug-versus-dense readback) is shaped for a marched field. Question `site-ground-field` asked this. It is ruled `site-ground-field-ruled`: the Span Lab framework with the void as its brush. ### `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 behind it. `site-ground-field-ruled` picked the Span Lab framework, so the cascade becomes a cut only if the operator's nebula work picks it. ### Cut sequence (volumetric line) 1. `snoise-hessian` (spec admitted): `snoise_hessian`, `SNOISE_THIRD` re-measured on the tied kernel, `integral_clamp_linear`, in C#, HLSL and GLSL with fixture families. It touches no `expr-ir` file. Its provenance prose goes in code comments and the README, not `design.md`, which `expr-ir` edits. 2. Ruled `stage-functions` (`span-chain-shape-ruled`), split in three for size, on one branch `hands/cultmath-stage-functions`: - `stage-functions` (specs `cut-stage-functions.r1`-`r3`) and `stage-shaders` (spec `cut-stage-shaders.r1`): merged to main 2026-10-16 (verdicts cut-stage-functions.s2-s4, cut-stage-shaders.s1). SpanPath/SpanStage inputs, float-rounding allowances, HLSL and GLSL twins with SpanPath/SpanStage/SpanNoise as shader structs, a struct-aware mirror with no `MirrorOnly` list (125 functions compared), `tools/compile-hlsl.ps1` (x64 dxc, `-WX`). SpanStage.Identity has no shader form (decided in the spec: callers build the struct; a mirror test proves the shader at a C#-built identity equals fbm_grad/ridged_grad). - `stage-shaders-device`: a committed WebGL2 runner on Starfire (ANGLE to D3D11, Unity's driver path; also runnable on a phone) from follow-ups `glsl-browser-parity-run` and `affine-device-runner-seed`; fixture families across coordinate bands 1e2..1e6; a C# test reading the device readback against the double kernel; settles follow-up `span-allowance-gpu-margin`'s four CPU survivors (eg-abs, eh-abs, So-reach, two-en-to-one) and the size of the 2^-14 absolute term (en-abs-halved survives every CPU test). `fold` and the ridged sign jump at zero, so their device check is one-sided enclosure, not ulps. - `cultmath-unity` span release: after `stage-shaders-device`; the smoke shader calls both span twins; its own Soul pass; then Aetheria's pin bump and its spans-option lowering (ruling aetheria-nebula-spans-as-option; a moving nebula also waits on `span-bounds-under-flow`). - `span-compose`: the composition helper (`span_compose`) and `span_range` for the empty and saturated proofs, in all three runtimes, with a two-stage chain enclosure test against dense sampling. `trophic-nodes` (spec admitted on main after this branch merged) and then `lattice-weighted-cellular` (waits on it): Pass 7 lines 3-4. Body fact (probe on Yggdrasil, scratch `edb3c1c3` over `2fd9431a`; log `F:\Projects\eureka-handoff-aux-2026-10-14\imagination-stage-functions\probe2.log`): the HLSL mirror that `HlslSourceCompatibilityTests` compiles exposes every `static const` as a public static literal field of `CultMathHlsl.HlslShader` (`CULTMATH_PI` through `CULTMATH_SNOISE_THIRD` and `CULTMATH_CYCLES_*`). So one reflection test can pin every shader constant to its C# twin. 3. The curl primitive, once the operator rules on curl (the Aetheria flow test). 4. `cut-site-ground.r1` on the Span Lab framework (`site-ground-field-ruled`). Under that option the two marched-only site follow-ups are withdrawn, and only the third carries. 5. The kernel cascade, if picked. ## Pass 9: the shader pipeline (rulings `glsl-es-via-dxc-spirv-cross`, `conforming-run-lavapipe`, `real-device-native-angle`) Imagination `imagination-ag4-shader-pipeline`, session self-2026-10-16-ag4, 2026-10-16, on `origin/main` f65bde6f. The evidence the operator ruled on is `docs/research/native-shader-testing.md` (f697e03f) and `docs/research/native-shader-probes.md` with its scripts (e598c443). This pass adds the body facts below, then sequences the three rulings. ### Body facts (Pass 9) Every probe ran through `ygg-verify.sh` on Yggdrasil at f65bde6f, 2026-10-16 between about 16:30 and 16:50 UTC (18:30 to 18:50 CEST). Logs are in the Imagination scratchpad (`imagination-ag4-shader-pipeline/split-run1.log`, `img-run1.log`, `snap-run1.log`). - **SPIRV-Cross pin.** The newest SDK tag is `vulkan-sdk-1.4.363.0` (commit f11ba9f0, 2026-09-16). Built from that tag (`-DSPIRV_CROSS_CLI=ON`, Release, 4 cores), it takes 70 s and gives the same result as the HEAD build the earlier probe used. `tools/get-spirv-cross.ps1` clones HEAD unpinned. - **dxc pin (Linux).** The asset is `.../v1.9.2607/linux_dxc_2026_07_29.x86_x64.tar.gz`, sha256 `55665c87824051ed4774ff3280a79ccbbb7d39243b9736ca5e98222134112d54`, and it reports libdxcompiler 1.9.0.1. `get-dxc.ps1` pins the Windows zip of the same release. - **Overloads collapse in dxc, not in SPIRV-Cross.** With `export` on the 173 non-texture definitions, the library has 127 `OpFunction`s and 127 exports: one per name. Today's source has 22 overloaded names, 68 definitions and 46 lost. Trophic-nodes (branch `hands/cultmath-trophic-nodes`, unmerged) adds `cultmath_segment_closest` and `cultmath_distance_to_segment` in 2D and 3D, so 24 names and 72 definitions. The loss comes from exporting overloaded names, and build-only wrappers avoid it (next bullet). The source keeps its overloads (ruling `shader-overloads-stay-native`). - **Build-only export wrappers keep every overload (Pass 9b).** Imagination `imagination-ag4-overload-wrappers`, 2026-10-16, on 09940e20, ubuntu:24.04 on Yggdrasil, 17:40 to 17:55 UTC (19:40 to 19:55 CEST), log `imagination-ag4-overload-wrappers/wrap-run2.log` in the session scratchpad. The probe generated one wrapper per non-texture definition (173) in a build-only file. Each wrapper looks like `export float3 cmw036_cultmath_lerp(float3 start, float3 end, float3 amount) { return cultmath_lerp(start, end, amount); }`. The source was unchanged, with no `export` keyword anywhere, and it compiled with dxc `-T lib_6_3 -spirv`. - At default optimisation dxc inlines every wrapper. The 173 wrappers hold the bodies, and no overload survives as its own function. - At `-Od` it keeps the call. That gives 346 functions: 173 wrappers, each with exactly one `OpFunctionCall` to its own distinct callee. Each callee's `OpName` is already the plain source name (`cultmath_lerp` four times), so no rename is needed. - An instruction-level edit of the SPIR-V binary then makes the library. It keeps only the Shader and Linkage capabilities, drops every `OpExtension`, deletes the wrapper functions with their names and decorations, and moves each wrapper's `LinkageAttributes ... Export` onto its callee. `spirv-val --target-env spv1.5` accepts the result. Deleting the export instead of moving it gives a module SPIRV-Cross refuses ("no entry point"). - SPIRV-Cross `vulkan-sdk-1.4.363.0 --es --version 310` emits 173 `cultmath_` definitions, as many per name as the source has. There are no `_N` suffixes and no wrapper names: its own overload handling renames only on a signature collision. The emitted text is 2621 lines, against 1497 for today's three files, because `-Od` leaves temporaries. - Call sites resolve. glslang compiles as a `#version 300 es` fragment shader that calls `cultmath_lerp(float..)`, `cultmath_lerp(vec3..)`, `cultmath_saturate(vec4)` and `cultmath_saturate(float)`. A call with `ivec2`, which has no overload, is refused ("no matching overloaded function"). - Same computation. All 105 families the probe generator covers ran on lavapipe through the restored library, and every one is bit-identical to the committed GLSL route. 85 of the 105 are also bit-identical to the HLSL route, the same 85 where the committed GLSL matches it. - The rest of the pipeline carries over. Partitioned by origin file, the output gives 178 chunks (125 core, 51 Cycles, 2 Phacelle). No origin is unknown and nothing calls across licences. Every overload set lives in one file, and no signature repeats. glslang compiles all four combinations as ES 3.00. The source has 13 `static const` lines, all of the form `static const float CULTMATH_X = ;`. None of them reaches the output, and today's GLSL holds 13. - **What `-Od` leaves, and what cleans it (Pass 9c).** Imagination `imagination-ag4-od-temps`, 2026-10-17, on e626bf26 (188 wrappers, 140 names, 117 families), ubuntu:24.04 on Yggdrasil, 10:05 to 10:35 UTC (12:05 to 12:35 CEST), dxc v1.9.2607, SPIRV-Cross and SPIRV-Tools both built from `vulkan-sdk-1.4.363.0` (spirv-opt v2026.4), log `od-temps/imag-od7.log` in the session scratchpad, probe in `docs/research/native-shader-probes/od-temporaries/`. Pass 9b's route as ruled, with no further step, emits: - 2824 lines, 86129 bytes, 188 `cultmath_` definitions, 140 names, no suffixed names. The committed files are 1663 lines of one-line bodies, so line counts do not compare; SPIRV-Cross prints every body in Allman style. - Named locals survive, so most functions read like the source. Two things do not. First, 472 `param_var_*` copies: dxc passes every parameter as a pointer and the caller copies in. Second, 11 functions assign to a parameter in their body (`smootherstep`, `integral_clamp_linear`, `fbm_grad`, `ridged_grad`, both Bezier pairs, `span_stage`, `iv_fbm_ball`, `af_fbm`) and come out `inout`. The HLSL has no `out` or `inout` anywhere. - That `inout` is a defect in the route, not a blemish. glslang refuses a consumer call with a value, `cultmath_smootherstep(x + 1.0)` ("l-value required"), and a call with a variable rewrites the caller's variable. Pass 9b's bit-identical result held because the families call through their own copies. Overloads that look native must also take values like native ones. - NaN-safe `min` and `max` expansions (`mix(mix(max(a, b), b, isnan(a)), a, isnan(b))`) from `NMin` and `NMax`. - Six `spirv-opt` pass lists on the edited module alone (`--convert-local-access-chains`, `--ssa-rewrite`, `--eliminate-local-single-block`, `--eliminate-local-single-store`, `--eliminate-local-multi-store`, `--simplify-instructions`, `--eliminate-dead-branches`, `--merge-blocks`, `--vector-dce`, `--redundancy-elimination`, `--scalar-replacement`, `--copy-propagate-arrays`, each with `--eliminate-dead-code-aggressive`) are valid and keep 188 functions, but make the text bigger (2870 to 2930 lines) and swap named locals for `_N` ids (218 tokens become 2155 to 2517). `param_var_` and `inout` stay. They are not a cleanup. - A by-value lowering of the SPIR-V (`od-temporaries/byvalue.py`, 50 lines) makes each Function pointer parameter a value parameter, with the old id kept as an entry-block local stored from it, and each call loads what it passed. It changed 187 functions and 456 parameters, validates, and leaves `inout` at 0. On lavapipe all 117 families stay bit-identical to the committed GLSL. - Then `spirv-opt --eliminate-local-single-store --eliminate-dead-code-aggressive` removes every `param_var_` copy (472 to 0): 2604 lines, 79782 bytes, 188 functions, 316 distinct `_N` ids, valid. Adding SPIRV-Cross `--relax-nan-checks` drops the `isnan` expansions: 2556 lines, 74263 bytes, 268 `_N` ids. glslang accepts both as ES 3.00, `cultmath_smootherstep(p.x + 1.0)` included. - That cleaned module is bit-identical to the lavapipe HLSL result on 117 of 117 families, where the committed GLSL is on 97. It differs from the committed GLSL in 20 families. Against the C# fixture the exact, at most 4 ulp, at most 64 ulp and over 64 buckets are 80, 11, 9, 17, where the committed GLSL is 80, 9, 10, 18. `--relax-nan-checks` changes no lavapipe bit. So the cleaned route is closer to the HLSL than today's GLSL is, and "bit-identical to today's lowering" cannot be its gate. With apt's spirv-tools 2025.1~rc1 the figures for this route are the same. - **Why `span_fbm` and `span_ridged` differ by 1 ulp (Pass 9c).** Imagination `imagination-ag4-span-ulp`, 2026-10-17, on 23ac4e89, same image and tools as `cut-glsl-build-dxc.h1`, probes in the session scratchpad `logs/ag4-ulp/` (`probe1.sh` to `probe4.sh`). Only the curvature word (word 4 of 6) differs, in 41 and 42 of 1536 words. Two causes, both measured: - **dxc's entry-point optimiser reassociates.** `cultmath_snoise_hessian` ends every entry in `105.0 * (...)`. In the `-O3` entry module the 105 is hoisted out: nine squares are summed bare and the sum is multiplied once (`OpFMul %11025 %sum`, then `Sqrt`), where the source squares `105 * a` nine times. That is real arithmetic, not float arithmetic. Neither `NoContraction` nor `--relax-nan-checks` touches it. `dxc -Gis` (IEEE strictness) stops it. - **The driver contracts mul+add by use count.** lavapipe (NIR) fuses an `OpFMul` into the `OpFAdd` that consumes it only when the product has one use. dxc's CSE shares the symmetric Hessian's off-diagonal squares (two uses each), so the entry module fuses some of the nine pairs; the `-Od` library module has nine single-use products and fuses all nine. The `OpFAdd` order is the same in both (left fold, as the C# `span_frobenius`). - Decorating every `OpFMul`, `OpFAdd`, `OpFSub`, `OpVectorTimesScalar` and `OpDot` `NoContraction` in both final modules, f103 and f104: `-Od` HLSL equals the built GLSL (0 words differ); `-O3` HLSL still differs in 31 and 44 (the hoist). `-Gis` (with `-O3`) plus `NoContraction` equals the built GLSL on 127 of 127 families, and so does `-Od` plus `NoContraction`. Plain `-Od` HLSL differs from the built GLSL in 664 words (contraction depends on use structure), so `-Od` alone is worse than `-O3`: your suspicion of `-Od` on the HLSL route is not a fix. - **Which is right.** Against the C# fixture, exact cases of 256 for f103 are 3 (HLSL) and 4 (GLSL) plain, and 151 for both under `NoContraction`; for f104, 7 and 10 plain, 141 under it. Of the first 60 families whose count moved, the strict form is nearer the C# in all but one, which moved by 1. Strict IEEE, no contraction, no reassociation is what C# computes, so it is "right"; the plain routes are each one driver's fusion pattern. The built GLSL is source-faithful (nothing in the `-Od` route reassociates). - **What makes them agree.** The equality gate (`GLSL_DIR` mode) compares both routes in the strict form: HLSL entry built with `-Gis`, and a harness pass that adds `NoContraction` to every float op of both modules (about 12 lines of `spirv-dis` and `spirv-as`; `tools/device/nc.py` in the probe). That is 127 of 127. The shipped GLSL and the C# bounds judge are unchanged. See question `glsl-gate-strict-form`. - **The build at main, with the rename and the strict form (Pass 9d).** Imagination `imagination-ag5-glsl`, 2026-10-17, scratch worktree `C:\cl-wt\imag-ag5-probe` (not pushed): `23ac4e89` merged with origin/main `86cd84a3` as `69987d91` (no conflict; README.md and tools/device/gen.py auto-merged), probe commits `575badb6` to `d14bfbbe` (`probe/p1.sh`, `p2.sh`, `spec.py`). Yggdrasil, `mcr.microsoft.com/dotnet/sdk:10.0` and `ubuntu:24.04`; logs in the session scratchpad `imagination-ag5-glsl/p2.log` to `p4.log`. - **Site arrays break the wrappers.** `wrappers.py` passes `sites[CULTMATH_MAX_SITES]` as the argument (its last-token rule), and dxc refuses the three wrappers of `cultmath_power_border`, `cultmath_lattice_cell_distance` and `cultmath_weighted_lattice_cells` ('no known conversion from float4 to float4 [128]'). Stripping the `[..]` suffix from the argument fixes it; then the build is green: core 139 functions over 91 names (1554 lines, 54775 bytes), cycles 51/51 (867, 21250), phacelle 1/1 (82, 3199); zero suffixed ids and zero inout. - **The renamed fields survive SPIRV-Cross.** With `float4 cosine; float4 sine;` in the HLSL (HLSL only, in the job), the built phacelle.glsl declares `vec4 cosine; vec4 sine;`. - **The strict-form gate holds at main.** With `-Gis` on the HLSL entry and NoContraction on both modules (the text `nc.py` of Pass 9c, probe only): lavapipe-glsl equals lavapipe-hlsl on 129 of 129 work items. The work list is 125 fixture families (123 at `774bebb0`; the lattice loop added two) plus one reference item for each of the 4 sampled affine families, so h1's 127 was 123 + 4. - **The built GLSL loses the capacity override.** dxc preprocesses `CULTMATH_MAX_SITES`, so built core.glsl has no mention of it and four literal `[128]` arrays. A consumer with `#define CULTMATH_MAX_SITES 4` passing `vec4 s[4]` fails glslang ('no matching overloaded function'); the same consumer compiles against the committed (lowered) GLSL and through dxc against the HLSL. - **dxc has no spec-constant array size.** `[[vk::constant_id(7)]] const int CAP = 128;` as an array parameter's length is refused ('variable length arrays are not supported in HLSL'). - **A SPIR-V spec constant carries it.** In the built SPIR-V exactly one constant is an array length equal to 128 (id 1297), used by nothing but OpTypeArray. Made an OpSpecConstant with SpecId 0, it passes spirv-val, and SPIRV-Cross ES emits `#ifndef SPIRV_CROSS_CONSTANT_ID_0` / `#define ... 128u` / `const uint _1297 = SPIRV_CROSS_CONSTANT_ID_0;` with `vec4 sites[_1297]` in every site-array signature. The probe's consumer defined the macro as a plain int and was refused for the uint initializer, so the `uint(CULTMATH_MAX_SITES)` mapping that `cut-glsl-build-dxc.r2` writes is the route, but has not been compiled yet. - **Capability strip.** `spirv-opt --trim-capabilities` (spirv-tools 2025.1~rc1) leaves all 14 capabilities and both extensions in place, and SPIRV-Cross still refuses ("RayQuery requires Vulkan GLSL 460"). Stripping every capability except Shader and Linkage and every OpExtension in the disassembly, then running `spirv-as`, gives a module that `spirv-val --target-env spv1.5` accepts. SPIRV-Cross `--es --version 310` lowers it in 54 ms, and dxc takes 322 ms. - **Named constants vanish.** The SPIR-V has no `OpName` for any `CULTMATH_` constant, so the output has none. Today's GLSL has 13: 9 in CultMath.glsl and 4 in CultMath.Cycles.glsl. The HLSL source of every one of them is a `static const float NAME = ;` line, so a build can emit them from the source lines and should refuse any `static const` line of another form. - **Per-file split works.** The SPIRV-Cross output was partitioned by the HLSL file that defines each function and struct (CultMath, Interval and Affine go to core, plus Cycles and Phacelle). That gives 132 chunks: 79 core, 51 Cycles and 2 Phacelle. No chunk's origin is unknown, no chunk is duplicated, and no Cycles or Phacelle function calls a function outside core. The output has no `spv*` helper functions. Its prologue is 3 lines: `#ifdef SPIRV_CROSS_LIBRARY_HEADER`, `#version 310 es` and `#endif`. glslang compiles core, core+Cycles, core+Phacelle and core+Cycles+Phacelle as `#version 300 es` fragment shaders. So the licence split survives the switch as a partition step of about 40 lines in the build. - **One image runs the whole conforming job.** `mcr.microsoft.com/dotnet/sdk:10.0` is Ubuntu 24.04.5 (noble), dotnet 10.0.401. Its apt has mesa-vulkan-drivers 25.2.8-0ubuntu0.24.04.4 (lavapipe, the probe's device), glslang-tools 15.1.0, spirv-tools 2025.1~rc1, libvulkan-dev 1.3.275 and gcc 13.2. `apt-get update --snapshot 20261016T000000Z` (apt 2.8.3) then installs the same versions. That pins the package set to a date and lets the C# judge run in the same container as the device. - **Consumer audit.** No public name changes under the wrapper route, so it shows only who reads the committed GLSL and HLSL. It covers `origin` of Aetheria (master a8586d08 and all of its remote branches), Asura (60fbd19), Odin (8e1de66) and gamecult-site (af6e206), and HEAD of every other git repo under F:\Projects except the CultLib worktrees, the archived AetheriaEve and the retired CultMath history repo. The search was for `cultmath_[a-z]` outside docs. Only one shader calls CultMath: `Muninn` (1d0ea0b, and its worktree `Muninn-next`) `crates/muninn-move-tracker/shaders/MoveSphereCandidate.comp.hlsl:88`, one call to `cultmath_saturate(float)`. Aetheria uses CultMath only from C#. The operator's Span Lab artifact (be6a9e31) embeds its own copy of the GLSL (follow-up `span-lab-pre-tie-kernel`) rather than including the file. ### Decisions (defaults, not forks) - **The fixture owner owns the work list and the verdict.** `GlslMirrorTests` writes what a device must compute: the fixture's families, plus reference families for the four sampled-form checks, whose reference is the device's own `snoise` or fBm at 75 points of each case. It also judges what the device returns. The generator only spells shaders, the host only dispatches, and neither compares anything. The lavapipe job and the later ANGLE tool share this one work list and this one judge. - **Each device has a recorded bound.** Under `cycles-kernel-reference-ruled`'s stance, a device's ulp-bounded and platform families are held to a bound measured on that device and committed with the device's identity. lavapipe re-runs bit-identically, so the bound is a regression gate, and an identity change refuses until it is re-recorded. Exact and integer families must match bit for bit, and `check` families must pass their `Verify`. - **Unique names for dxc live only inside the build** (ruling `shader-overloads-stay-native`). The build owns three things here. - The wrapper list. It is generated from the source's own definitions, one wrapper per non-texture `cultmath_` definition, and the build refuses a definition it cannot read, just as it refuses a `static const` line of another form. The wrappers are text in the build's temp directory and are never committed. The source gains no `export` keyword and no `CULTMATH_EXPORT` macro. - The compile mode. The library compiles at `-Od`, because the default optimiser's inlining is what erases the overloads. The text it leaves is cleaned by the next two items (Pass 9c), not shipped as is: `-Od` alone emits `inout` parameters, which native overloads must not have. - One SPIR-V edit. A single instruction-level pass over the binary, never a regex over text, strips capabilities and extensions, deletes the wrappers and moves their exports onto the callees. It replaces the disassembly grep that the capability-strip probe used. It refuses a wrapper that is not exactly one call. SPIRV-Cross then emits the overloaded names as they are, so the build has no rename step and no name map. - By-value parameters. The same pass makes every Function-pointer parameter a value parameter (`byvalue.py` in the probe), so no emitted function can be `inout`. The build then runs exactly `spirv-opt --eliminate-local-single-store --eliminate-dead-code-aggressive`, never an inliner and never `--simplify-instructions` or `--ssa-rewrite`, which turn named locals into `_N` ids, and SPIRV-Cross runs with `--relax-nan-checks`, so `min` and `max` are the plain intrinsics. - The gate is the HLSL, not the old text. A cleaned route's lavapipe-glsl result must equal the lavapipe-hlsl result bit for bit in every family (117 of 117 measured), and the C# judge's bounds are re-recorded only where a family moved and equals its HLSL result. - **CultPhasor's JSON keys (model page row).** Named by `CultMathJson`; written as `cosine`/`sine` from `phasor-field-rename` on, and read as either those or the legacy `cos`/`sin`, forever unless a later Breaking change removes the legacy names. Decided by ruling `phasor-fields-renamed-at-source`. The MessagePack wire is positional and never named the fields, so `.cc` documents are untouched both ways. A reader from before the rename skips the new keys and decodes zeros (its unknown-property rule), which is why the change is Breaking in both changelogs: upgrade readers before writers. - **The capacity through the build.** dxc cannot keep a macro and refuses spec-constant array sizes, so the build turns the one array-length constant into a SPIR-V spec constant and `partition.py` maps `CULTMATH_MAX_SITES` onto SPIRV-Cross's `SPIRV_CROSS_CONSTANT_ID_0` with source-copied header lines. Rejected: building the GLSL per capacity (a consumer would run the build to change one number), and rewriting `[128]` in the emitted text (the forbidden text post-pass). Pass 9d. ### Cut sequence (Pass 9; one line each, mapped when about to run) 1. `shader-conforming-run`: spec admitted. The lavapipe job with the C# device judge, run against today's committed HLSL and GLSL, so the next cut is verified against a running check. 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/` holds `build-glsl.sh`, which pins dxc v1.9.2607 (the sha above), SPIRV-Cross and SPIRV-Tools `vulkan-sdk-1.4.363.0`, plus one SPIR-V tool in Python. The build generates the export wrappers into its temp directory (never committed), compiles source plus wrappers with `-T lib_6_3 -spirv -Od`, and runs the one SPIR-V edit (strip, wrapper delete, export move, by-value parameters; Pass 9c). It runs the pinned `spirv-opt` pair, `spirv-val`, and SPIRV-Cross ES 3.10 with `--relax-nan-checks`, then partitions by origin file and emits the 13 constants. Output goes to a directory the caller names. The committed GLSL, `GlslLowering.cs` and its tests are untouched, and the lavapipe job gains one switch to run a built directory in place of `shaders/*.glsl`. Waits on 1, which is merged. 2b. `glsl-switch` (mapped when 2 is verified): commits the build's output with the licence lines and guards, deletes `GlslLowering.cs` and the lowering contract tests, makes the lavapipe job check the committed files against the build, re-records the lavapipe bounds for moved families (Pass 9c's gate), and pins `get-spirv-cross.ps1`. If `trophic-nodes` merges first, the build covers its four new overloads with nothing extra. 3. `angle-capture` (stage-shaders-device part 2): the Starfire native-ANGLE tool on the shared work list (ES 3.00 fragment flavour, RGBA32UI readback), loading a pinned ANGLE build rather than Docker Desktop's copy. Captures carry the renderer, ANGLE version and revision, the shader sha and per-family argument hashes and bits, and the ordinary test run judges them. A shader-sha change marks them stale. It settles `span-allowance-gpu-margin`'s six CPU survivors, `iv-exp-gpu-widening`'s measurement and `device-p2-pin-notes`, and closes `glsl-browser-parity-run` and `affine-device-runner-seed`. It waits on 2b, because a capture of the old lowering goes stale at the switch. ### shader-lattice-loop (follow-ups `shader-weighted-lattice-loop`, `lattice-shader-pins`) Written by Imagination (agent `imagination-ag4-shader-loop`, session `self-2026-10-16-ag4`) against `origin/main` `e2051c5a` and the build branch at `23ac4e89`. Research and probes: `docs/research/shader-variable-inputs.md`. Spec: `cultmath-tapes:cut_spec:cut-shader-lattice-loop.r1`. Question: `shader-site-capacity` (gates the spec). - **Bodies reach the shader as `float4 sites[CULTMATH_MAX_SITES]` plus `int count`, by value.** A caller accessor macro cannot survive the library build (the built GLSL has no consumer macros), and library-owned uniforms would take a binding the consumer owns. dxc, the SPIR-V edit, SPIRV-Cross and a WebGL2-style uniform-array consumer all accept the array form (probes P1-P3). - **The shader owns the whole search.** `cultmath_weighted_lattice_cells` and `cultmath_power_border` replace the two comment recipes; the four steps stay public. The box pass recomputes distances (no shader cache), the ring loop is bounded by 37 with C#'s exit. - **The lavapipe run kills the exit and the normaliser** (probe table, research section 3): M1-M6 fail the C# device judge; M7, a late exit, is equivalent by the window-independence proof. M1 needs several far-ring points. - **Smooth-step cap point** joins its family (m = f1 + smoothing, d past it, smoothing >= 0.5). - **Yggdrasil's sdk:10.0 Ubuntu image owns `glsl-parity.json`'s platform values** with the lavapipe bounds; a write on another OS is refused. - Left: `shader-span-functions-array-form` (power_cells, multiplicative_cells, soft_gabriel_gate array forms).