2525 lines
172 KiB
Markdown
2525 lines
172 KiB
Markdown
# 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<float4>`; HLSL: `StructuredBuffer<float4>` plus a count. Read only by span-taking ops; no op produces one. Lands with `expr-trophic-ops` as an append-only format addition. | The caller binds the content; `CultMath.Expr` owns the kind. |
|
||
| Bytecode (the lowered tape) | (graph content hash, bytecode format version). | Derived on demand, cache-only; never stored as truth or shipped as a document; rebuilt when the graph or CultMath changes. | `CultMath.Expr`'s lowering (the interpreter cut). Its register model is question `tape-register-model`. |
|
||
| HLSL interpreter source (`shaders/CultMath.Tape.hlsl`) | The file, versioned by the CultMath release tag; it declares the bytecode format version it reads. | Changes only in releases under semver; a format change bumps the version and the lowering together. Copied byte-identical into the Unity package (`UnityPackageShaderIsIdenticalToTheCanonicalShader`). | CultMath. |
|
||
| Compiled HLSL artifact | Under the interpreter there is no per-graph compiled HLSL. The one compiled artifact is the consumer's compute kernel that includes `CultMath.Tape.hlsl`, named by the consumer's build. | Compiled by the consumer's build (Unity), never generated per graph. Per-graph generated kernels (the r1 emitter) return only with the material line, as a derived artifact pinned against the C# evaluator. | The consumer's build; CultMath owns only the included source. |
|
||
| Evaluation results | Derived. | Cache-only, the consumer's. | The consumer. |
|
||
|
||
Gap (5) of the fauna map, "CultMath-owned parity for a consumer's combined HLSL",
|
||
changes shape under the tape: a consumer's composition is a graph, not HLSL, so
|
||
the parity CultMath owns is graph parity, the C# evaluator against the HLSL
|
||
interpreter on the consumer's own graph. That harness belongs to the interpreter
|
||
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<S>` (`CultMathJson.cs:269`) holds one scalar type per struct, so a struct mixing `float` and `int` fields needs new JSON machinery. Pattern: `52d25740` (SpanPath, SpanStage: resolver, JSON, a `Json`, a `RoundTrip` and a `Hex` pin). |
|
||
| TN5 | Who calls `distance_to_segment(.., out closest)`? | `MathTests.cs` and Aetheria `Assets/Scripts/ServerShared/Galaxy.cs`; it stays, unchanged in value. |
|
||
| TN6 | Is `CultMath` free to take `ReadOnlySpan`? | Yes: `net8.0;netstandard2.1` (`CultMath.csproj:4`); `BatchMath` already takes `ReadOnlySpan<float2>`. |
|
||
|
||
#### Decisions (defaults, not forks)
|
||
|
||
- **A span function is a C# loop over a step that has a shader twin.** The
|
||
shader cannot take a span in the mirror (TN3), and a buffer-taking HLSL
|
||
function would be a second, untested path. So each span function is one fold:
|
||
`site_cells_empty()`, then `site_cells_insert(cells, score, index)` per site,
|
||
read by `site_cells_border(cells)`; the per-site score is `power_score` or
|
||
`multiplicative_score`. C#'s `power_cells(p, sites)` and
|
||
`multiplicative_cells(p, sites)` are that loop and nothing else. A shader
|
||
caller (and later the tape interpreter) writes the same loop over its own
|
||
buffer. One rule, one path; the per-site functions are mirrored bit for bit.
|
||
- **Sites are `float4(position.xyz, weight)`**, the model page's span element; a
|
||
2D consumer uses z = 0. For an explicit span every site is visited, so no
|
||
window and no weight-ratio range apply (the lattice node's range is line 4's).
|
||
- **Indices are `float` fields** (`nearestSite`, `secondSite`, -1 for none), as
|
||
`CultCellular.id` is: one scalar type keeps the MessagePack and JSON shapes on
|
||
the existing helpers (TN4), and an index is exact below 2^24, far past any
|
||
span a per-point O(N) search serves.
|
||
- **The gate is pointwise per pair**: `gabriel_margin(a, b, other)` with a
|
||
shader twin, and C#'s `soft_gabriel_gate(sites, first, second, softness)`
|
||
folding it over the span (TN1's unweighted positions; a power-weighted
|
||
Gabriel graph is not requested by any ruling).
|
||
- **The strand reduction is not in this cut.** Its Gaussian profile and width
|
||
law are fauna's composition until the graph needs it as an op; line 5 owns
|
||
that op, with the same step-and-twin convention.
|
||
- **Shader part separable.** The C# functions, struct and wire shapes land
|
||
first; the HLSL twins, the regenerated GLSL and the fixture families land in
|
||
the last commits under today's `GlslLowering`. Pass `imagination-ag4-shader-runner`
|
||
is re-posing the lowering and shader testing (question `device-run-driver`);
|
||
if that lands first, only the shader commits are re-mapped.
|
||
|
||
### 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 = <literal>;`. 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 = <literal>;` 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).
|