Merge origin/main (phasor-field-rename a085c0b3) into hands/cultmath-glsl-build-dxc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
7c47cbaffd
45 changed files with 2385 additions and 534 deletions
|
|
@ -9,7 +9,7 @@ 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). Pass 5 (section "Pass 5") maps
|
||||
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.
|
||||
|
|
@ -126,9 +126,10 @@ nebula input tokens and the seed values of the ground tokens.
|
|||
**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. Whether Aetheria's nebula moves onto the
|
||||
span framework is not decided; until it is, the nebula consumes CultMath noise at
|
||||
its pin bump and nothing else from this line.
|
||||
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.**
|
||||
|
||||
|
|
@ -750,7 +751,7 @@ 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 holds the look question).
|
||||
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
|
||||
|
|
@ -964,7 +965,7 @@ answers the first; the second is content and stays.
|
|||
| --- | --- | --- |
|
||||
| `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 is Q9 |
|
||||
| `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 |
|
||||
|
||||
|
|
@ -1011,7 +1012,7 @@ 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). For `CARVE = 1.5` the ramp width is tied 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
|
||||
|
|
@ -1887,9 +1888,9 @@ Aetheria twin.
|
|||
- `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` (open, from section
|
||||
"lattice-weighted-cellular"): a proven ring search or a fixed measured 7x7
|
||||
window. Gates line 4a's search loop.
|
||||
- `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
|
||||
|
|
@ -1899,8 +1900,7 @@ found) and section 6 (IR placement); Aetheria
|
|||
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 lack that prior art; Self runs Eyes before
|
||||
putting them.
|
||||
lattice Voronoi. Both questions have since been ruled.
|
||||
|
||||
### Cut sequence (Pass 7; one line each, mapped when about to run)
|
||||
|
||||
|
|
@ -2340,6 +2340,65 @@ and 16:50 UTC (18:30 to 18:50 CEST). Logs are in the Imagination scratchpad
|
|||
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
|
||||
|
|
@ -2407,11 +2466,23 @@ and 16:50 UTC (18:30 to 18:50 CEST). Logs are in the Imagination scratchpad
|
|||
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 admitted): the build, not yet the switch. `packages/cultmath/tools/glsl/`
|
||||
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`,
|
||||
|
|
@ -2432,3 +2503,23 @@ and 16:50 UTC (18:30 to 18:50 CEST). Logs are in the Imagination scratchpad
|
|||
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).
|
||||
|
|
|
|||
|
|
@ -108,15 +108,18 @@ source.
|
|||
- Asura does not need this campaign. Its field is noise-dominated, and its
|
||||
`A_max` shell and band-limiting already give the cheap culling.
|
||||
|
||||
## Open before unparking
|
||||
## Decided since the unpark, and what stays open
|
||||
|
||||
- Run the (a) spike first, or go straight to (c)?
|
||||
- Pick the interval representation (plain intervals or affine arithmetic) and
|
||||
the noise bound strategy.
|
||||
Affine forms for noise and warp were unparked on 2026-10-03 (ruling
|
||||
`cultmath-tapes:ruling:affine-forms-unparked`): one shared symbol, mapped in
|
||||
`docs/cultmath-interval-ground-cut.md`, "Pass 5".
|
||||
- Choose the bytecode register model for the HLSL interpreter, given GPU
|
||||
register pressure.
|
||||
- Decide whether the brush-grammar station project lives in Aetheria or in
|
||||
its own repo.
|
||||
- The spike-or-(c) choice was settled by the operator's unpark: the tape
|
||||
unparked for Aetheria's trophic species graphs (ruling
|
||||
`cultmath-tapes:ruling:tape-unparked-for-trophic-graphs`).
|
||||
- Interval representation: affine forms for noise and warp, unparked on
|
||||
2026-10-03 (ruling `cultmath-tapes:ruling:affine-forms-unparked`): one
|
||||
shared symbol, mapped in `docs/cultmath-interval-ground-cut.md`, "Pass 5".
|
||||
- Register model: the HLSL interpreter keeps its registers in groupshared
|
||||
memory, a slice per thread, linear-scan allocated at lowering (ruling
|
||||
`cultmath-tapes:ruling:tape-register-model-ruled`; research in
|
||||
`docs/research/tape-register-model.md`).
|
||||
- Open: whether the brush-grammar station project lives in Aetheria or in
|
||||
its own repo (follow-up `cultmath-tapes:follow_up:brush-grammar-station-repo`,
|
||||
raised as an operator question when a station cut reaches the frontier).
|
||||
|
|
|
|||
11
docs/research/native-shader-probes/strict-form/nc.py
Normal file
11
docs/research/native-shader-probes/strict-form/nc.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import re,sys
|
||||
src,dst=sys.argv[1:3]
|
||||
L=open(src).read().split('\n')
|
||||
ids=[]
|
||||
for l in L:
|
||||
m=re.match(r'\s*(%\w+) = Op(FMul|FAdd|FSub|VectorTimesScalar|Dot)\b',l)
|
||||
if m: ids.append(m.group(1))
|
||||
i=next(k for k,l in enumerate(L) if re.search(r'= OpType',l))
|
||||
L[i:i]=[' OpDecorate %s NoContraction'%x for x in ids]
|
||||
open(dst,'w').write('\n'.join(L))
|
||||
print(src,len(ids))
|
||||
19
docs/research/native-shader-probes/strict-form/probe1.sh
Normal file
19
docs/research/native-shader-probes/strict-form/probe1.sh
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
set -e
|
||||
apt-get update -qq >/dev/null; apt-get install -y -qq --no-install-recommends python3 spirv-tools >/dev/null 2>&1
|
||||
P=packages/cultmath/tools
|
||||
python3 - <<'PY'
|
||||
p='packages/cultmath/tools/device/run-lavapipe.sh'
|
||||
s=open(p).read()
|
||||
k='python3 "$DEV/gen.py" "$W/work" "$CM"\n'
|
||||
s=s.replace(k,k+'sed -i -E "s/[.](cos|sin)([^A-Za-z0-9_])/._\1\2/g" "$W"/work/*.comp\ngrep -E "^f10[34]\t" "$W/work/manifest.txt" >"$W/work/m2"; mv "$W/work/m2" "$W/work/manifest.txt"\n')
|
||||
open(p,'w').write(s)
|
||||
PY
|
||||
bash $P/glsl/build-glsl.sh /tmp/glslout >/dev/null
|
||||
GLSL_DIR=/tmp/glslout bash $P/device/run-lavapipe.sh 2>&1 | tail -4 || true
|
||||
W=/tmp/device/work
|
||||
for t in f103 f104; do for r in h g; do
|
||||
spirv-dis --raw-id $W/$t.$r.spv -o /tmp/$t.$r.dis
|
||||
echo "== $t $r: NoContraction $(grep -c NoContraction /tmp/$t.$r.dis) Fma $(grep -c ' Fma ' /tmp/$t.$r.dis) FMul $(grep -c OpFMul /tmp/$t.$r.dis) FAdd $(grep -c OpFAdd /tmp/$t.$r.dis) FSub $(grep -c OpFSub /tmp/$t.$r.dis) lines $(wc -l </tmp/$t.$r.dis)"
|
||||
done; done
|
||||
mkdir -p /tmp/dump; cp /tmp/f10?.?.dis /tmp/dump/; cp $W/f103.hlsl $W/f103.comp /tmp/dump/; cp /tmp/glslout/*.glsl /tmp/dump/
|
||||
echo DUMP-BEGIN; tar czf - -C /tmp dump | base64 -w0; echo; echo DUMP-END
|
||||
46
docs/research/native-shader-probes/strict-form/probe2.sh
Normal file
46
docs/research/native-shader-probes/strict-form/probe2.sh
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
set -e
|
||||
apt-get update -qq >/dev/null; apt-get install -y -qq --no-install-recommends python3 spirv-tools >/dev/null 2>&1
|
||||
cat >/tmp/nc.py <<'PYEOF'
|
||||
import re,sys
|
||||
src,dst=sys.argv[1:3]
|
||||
L=open(src).read().split('\n')
|
||||
ids=[]
|
||||
for l in L:
|
||||
m=re.match(r'\s*(%\w+) = Op(FMul|FAdd|FSub|VectorTimesScalar|Dot)\b',l)
|
||||
if m: ids.append(m.group(1))
|
||||
i=next(k for k,l in enumerate(L) if re.search(r'= OpType',l))
|
||||
L[i:i]=[' OpDecorate %s NoContraction'%x for x in ids]
|
||||
open(dst,'w').write('\n'.join(L))
|
||||
print(src,len(ids))
|
||||
PYEOF
|
||||
P=packages/cultmath/tools
|
||||
python3 - <<'PY'
|
||||
p='packages/cultmath/tools/device/run-lavapipe.sh'
|
||||
s=open(p).read()
|
||||
k='python3 "$DEV/gen.py" "$W/work" "$CM"\n'
|
||||
s=s.replace(k,k+'sed -i -E "s/[.](cos|sin)([^A-Za-z0-9_])/._\1\2/g" "$W"/work/*.comp\ngrep -E "^f10[34]\t" "$W/work/manifest.txt" >"$W/work/m2"; mv "$W/work/m2" "$W/work/manifest.txt"\n')
|
||||
open(p,'w').write(s)
|
||||
PY
|
||||
bash $P/glsl/build-glsl.sh /tmp/glslout >/dev/null
|
||||
GLSL_DIR=/tmp/glslout bash $P/device/run-lavapipe.sh 2>&1 | tail -4 || true
|
||||
W=/tmp/device/work; R=/tmp/device
|
||||
mkdir -p $R/nc/lavapipe-hlsl $R/nc/lavapipe-glsl
|
||||
while IFS=$'\t' read -r tag _ _ n _ wout; do
|
||||
for r in h g; do
|
||||
spirv-dis --raw-id $W/$tag.$r.spv -o /tmp/$tag.$r.dis
|
||||
python3 /tmp/nc.py /tmp/$tag.$r.dis /tmp/$tag.$r.nc.dis
|
||||
spirv-as --target-env vulkan1.2 /tmp/$tag.$r.nc.dis -o $W/$tag.$r.nc.spv
|
||||
spirv-val --target-env vulkan1.2 $W/$tag.$r.nc.spv
|
||||
d=hlsl; [ $r = g ] && d=glsl
|
||||
$R/vkrun $W/$tag.$r.nc.spv $W/$tag.in $R/nc/lavapipe-$d/$tag.out $n $wout >/dev/null
|
||||
done
|
||||
echo "$tag nc: hlsl==glsl $(cmp -s $R/nc/lavapipe-hlsl/$tag.out $R/nc/lavapipe-glsl/$tag.out && echo YES || echo NO); nc-hlsl==plain-hlsl $(cmp -s $R/nc/lavapipe-hlsl/$tag.out $R/results/lavapipe-hlsl/$tag.out && echo YES || echo NO); nc-glsl==plain-glsl $(cmp -s $R/nc/lavapipe-glsl/$tag.out $R/results/lavapipe-glsl/$tag.out && echo YES || echo NO)"
|
||||
done <$W/manifest.txt
|
||||
cp /tmp/device/vkrun /tmp/ 2>/dev/null || true
|
||||
export CULTMATH_DEVICE_NAME=$(sort -u $R/device.txt | head -1 | sed 's/^device=//') CULTMATH_DEVICE_IDENTITY=x CULTMATH_WRITE_DEVICE_BOUNDS=1
|
||||
for v in results nc; do
|
||||
export CULTMATH_DEVICE_RESULTS=$R/$v
|
||||
dotnet test packages/cultmath/tests/CultMath.Tests --no-build --filter "FullyQualifiedName~GlslMirrorTests.DeviceResultsHoldFixture" >/tmp/judge.$v.log 2>&1 || true
|
||||
echo "== judge $v"; grep -E "^(lavapipe-)" packages/cultmath/tests/CultMath.Tests/fixtures/device-bounds-lavapipe.txt | grep -E " 10[34] " | cut -f1,2,5,6 || tail -5 /tmp/judge.$v.log
|
||||
tail -3 /tmp/judge.$v.log
|
||||
done
|
||||
53
docs/research/native-shader-probes/strict-form/probe3.sh
Normal file
53
docs/research/native-shader-probes/strict-form/probe3.sh
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
set -e
|
||||
apt-get update -qq >/dev/null; apt-get install -y -qq --no-install-recommends python3 spirv-tools >/dev/null 2>&1
|
||||
cat >/tmp/nc.py <<'PYEOF'
|
||||
import re,sys
|
||||
src,dst=sys.argv[1:3]
|
||||
L=open(src).read().split('\n')
|
||||
ids=[]
|
||||
for l in L:
|
||||
m=re.match(r'\s*(%\w+) = Op(FMul|FAdd|FSub|VectorTimesScalar|Dot)\b',l)
|
||||
if m: ids.append(m.group(1))
|
||||
i=next(k for k,l in enumerate(L) if re.search(r'= OpType',l))
|
||||
L[i:i]=[' OpDecorate %s NoContraction'%x for x in ids]
|
||||
open(dst,'w').write('\n'.join(L))
|
||||
print(src,len(ids))
|
||||
PYEOF
|
||||
P=packages/cultmath/tools
|
||||
python3 - <<'PY'
|
||||
p='packages/cultmath/tools/device/run-lavapipe.sh'
|
||||
s=open(p).read()
|
||||
k='python3 "$DEV/gen.py" "$W/work" "$CM"\n'
|
||||
s=s.replace(k,k+'sed -i -E "s/[.](cos|sin)([^A-Za-z0-9_])/._\1\2/g" "$W"/work/*.comp\ngrep -E "^f10[34]\t" "$W/work/manifest.txt" >"$W/work/m2"; mv "$W/work/m2" "$W/work/manifest.txt"\n')
|
||||
open(p,'w').write(s)
|
||||
PY
|
||||
bash $P/glsl/build-glsl.sh /tmp/glslout >/dev/null
|
||||
GLSL_DIR=/tmp/glslout bash $P/device/run-lavapipe.sh 2>&1 | tail -4 || true
|
||||
W=/tmp/device/work; R=/tmp/device
|
||||
DXC=$(find $R/dxc -name dxc -type f | head -1); export LD_LIBRARY_PATH=$(dirname $DXC)/../lib:$(dirname $DXC)
|
||||
CM=packages/cultmath
|
||||
cat >/tmp/cnt.py <<'PYEOF'
|
||||
import sys,struct
|
||||
a=open(sys.argv[1],'rb').read();b=open(sys.argv[2],'rb').read()
|
||||
n=len(a)//4;x=struct.unpack('<%dI'%n,a);y=struct.unpack('<%dI'%n,b)
|
||||
d=[i for i in range(n) if x[i]!=y[i]]
|
||||
print(len(d),'of',n,'cols',sorted(set(i%6 for i in d)))
|
||||
PYEOF
|
||||
run() { # name spv tag n wout
|
||||
$R/vkrun $2 $W/$3.in /tmp/o.$1.$3 $4 $5 >/dev/null; }
|
||||
OPT="--target-env=spv1.5 --eliminate-local-single-store --eliminate-dead-code-aggressive"
|
||||
while IFS=$' ' read -r tag _ _ n _ wout; do
|
||||
for r in h g; do spirv-dis --raw-id $W/$tag.$r.spv -o /tmp/$tag.$r.dis; python3 /tmp/nc.py /tmp/$tag.$r.dis /tmp/$tag.$r.nc.dis >/dev/null; spirv-as --target-env vulkan1.2 /tmp/$tag.$r.nc.dis -o $W/$tag.$r.nc.spv; done
|
||||
# h variants
|
||||
$DXC -T cs_6_0 -E main -HV 2021 -spirv -fspv-target-env=vulkan1.2 -Od -I $CM/shaders $W/$tag.hlsl -Fo $W/$tag.hOd.spv >/dev/null 2>&1
|
||||
spirv-opt $OPT $W/$tag.hOd.spv -o $W/$tag.hOdO.spv
|
||||
for v in hOd hOdO; do spirv-dis --raw-id $W/$tag.$v.spv -o /tmp/$tag.$v.dis; python3 /tmp/nc.py /tmp/$tag.$v.dis /tmp/$tag.$v.nc.dis >/dev/null; spirv-as --target-env vulkan1.2 /tmp/$tag.$v.nc.dis -o $W/$tag.$v.nc.spv; done
|
||||
for v in h g hOd hOdO; do run $v $W/$tag.$v.spv $tag $n $wout; run $v.nc $W/$tag.$v.nc.spv $tag $n $wout; done
|
||||
for v in h hOd hOdO; do
|
||||
echo "$tag $v vs g: $(python3 /tmp/cnt.py /tmp/o.$v.$tag /tmp/o.g.$tag)"
|
||||
echo "$tag $v.nc vs g.nc: $(python3 /tmp/cnt.py /tmp/o.$v.nc.$tag /tmp/o.g.nc.$tag)"
|
||||
done
|
||||
echo "$tag h.nc vs h: $(python3 /tmp/cnt.py /tmp/o.h.nc.$tag /tmp/o.h.$tag)"
|
||||
echo "$tag g.nc vs g: $(python3 /tmp/cnt.py /tmp/o.g.nc.$tag /tmp/o.g.$tag)"
|
||||
echo "$tag hOd.nc vs hOd: $(python3 /tmp/cnt.py /tmp/o.hOd.nc.$tag /tmp/o.hOd.$tag)"
|
||||
done <$W/manifest.txt
|
||||
53
docs/research/native-shader-probes/strict-form/probe4.sh
Normal file
53
docs/research/native-shader-probes/strict-form/probe4.sh
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
set -e
|
||||
apt-get update -qq >/dev/null; apt-get install -y -qq --no-install-recommends python3 spirv-tools >/dev/null 2>&1
|
||||
cat >/tmp/nc.py <<'PYEOF'
|
||||
import re,sys
|
||||
src,dst=sys.argv[1:3]
|
||||
L=open(src).read().split('\n')
|
||||
ids=[]
|
||||
for l in L:
|
||||
m=re.match(r'\s*(%\w+) = Op(FMul|FAdd|FSub|VectorTimesScalar|Dot)\b',l)
|
||||
if m: ids.append(m.group(1))
|
||||
i=next(k for k,l in enumerate(L) if re.search(r'= OpType',l))
|
||||
L[i:i]=[' OpDecorate %s NoContraction'%x for x in ids]
|
||||
open(dst,'w').write('\n'.join(L))
|
||||
print(src,len(ids))
|
||||
PYEOF
|
||||
P=packages/cultmath/tools
|
||||
python3 - <<'PY'
|
||||
p='packages/cultmath/tools/device/run-lavapipe.sh'
|
||||
s=open(p).read()
|
||||
k='python3 "$DEV/gen.py" "$W/work" "$CM"\n'
|
||||
s=s.replace(k,k+'sed -i -E "s/[.](cos|sin)([^A-Za-z0-9_])/._\\1\\2/g" "$W"/work/*.comp\n')
|
||||
open(p,'w').write(s)
|
||||
PY
|
||||
bash $P/glsl/build-glsl.sh /tmp/glslout >/dev/null
|
||||
GLSL_DIR=/tmp/glslout bash $P/device/run-lavapipe.sh 2>&1 | tail -4 || true
|
||||
set +e
|
||||
W=/tmp/device/work; R=/tmp/device
|
||||
DXC=$(find $R/dxc -name dxc -type f | head -1); export LD_LIBRARY_PATH=$(dirname $DXC)/../lib:$(dirname $DXC)
|
||||
CM=packages/cultmath
|
||||
for d in plain ncod gis; do mkdir -p $R/$d/lavapipe-hlsl $R/$d/lavapipe-glsl; done
|
||||
while IFS=$'\t' read -r tag _ _ n _ wout; do
|
||||
spirv-dis --raw-id $W/$tag.g.spv -o /tmp/g.dis; python3 /tmp/nc.py /tmp/g.dis /tmp/g.nc.dis >/dev/null; spirv-as --target-env vulkan1.2 /tmp/g.nc.dis -o /tmp/g.nc.spv
|
||||
$DXC -T cs_6_0 -E main -HV 2021 -spirv -fspv-target-env=vulkan1.2 -Od -I $CM/shaders $W/$tag.hlsl -Fo /tmp/hOd.spv >/dev/null 2>&1
|
||||
spirv-dis --raw-id /tmp/hOd.spv -o /tmp/hOd.dis; python3 /tmp/nc.py /tmp/hOd.dis /tmp/hOd.nc.dis >/dev/null; spirv-as --target-env vulkan1.2 /tmp/hOd.nc.dis -o /tmp/hOd.nc.spv
|
||||
$DXC -T cs_6_0 -E main -HV 2021 -spirv -fspv-target-env=vulkan1.2 -Gis -I $CM/shaders $W/$tag.hlsl -Fo /tmp/hGis.spv >/dev/null 2>&1
|
||||
spirv-dis --raw-id /tmp/hGis.spv -o /tmp/hGis.dis; python3 /tmp/nc.py /tmp/hGis.dis /tmp/hGis.nc.dis >/dev/null; spirv-as --target-env vulkan1.2 /tmp/hGis.nc.dis -o /tmp/hGis.nc.spv
|
||||
cp $R/results/lavapipe-hlsl/$tag.out $R/plain/lavapipe-hlsl/; cp $R/results/lavapipe-glsl/$tag.out $R/plain/lavapipe-glsl/
|
||||
$R/vkrun /tmp/hOd.nc.spv $W/$tag.in $R/ncod/lavapipe-hlsl/$tag.out $n $wout >/dev/null
|
||||
$R/vkrun /tmp/g.nc.spv $W/$tag.in $R/ncod/lavapipe-glsl/$tag.out $n $wout >/dev/null
|
||||
$R/vkrun /tmp/hGis.nc.spv $W/$tag.in $R/gis/lavapipe-hlsl/$tag.out $n $wout >/dev/null
|
||||
cp $R/ncod/lavapipe-glsl/$tag.out $R/gis/lavapipe-glsl/
|
||||
for d in plain ncod gis; do cmp -s $R/$d/lavapipe-hlsl/$tag.out $R/$d/lavapipe-glsl/$tag.out || echo "EQ-FAIL $d $tag"; done
|
||||
done <$W/manifest.txt
|
||||
export CULTMATH_DEVICE_WORK=$W CULTMATH_DEVICE_NAME=$(sort -u $R/device.txt | head -1 | sed 's/^device=//') CULTMATH_DEVICE_IDENTITY=x CULTMATH_WRITE_DEVICE_BOUNDS=1
|
||||
B=packages/cultmath/tests/CultMath.Tests/fixtures/device-bounds-lavapipe.txt
|
||||
for v in plain ncod gis; do
|
||||
export CULTMATH_DEVICE_RESULTS=$R/$v
|
||||
dotnet test packages/cultmath/tests/CultMath.Tests --no-build --filter "FullyQualifiedName~GlslMirrorTests.DeviceResultsHoldFixture" >/tmp/judge.$v.log 2>&1 || true
|
||||
grep -aE "Error Message" -A8 /tmp/judge.$v.log | head -14; ls -la $B; cp $B /tmp/bounds.$v; echo "judge $v: $(tail -3 /tmp/judge.$v.log | tr '\n' ' ')"
|
||||
done
|
||||
for v in plain ncod gis; do awk -F'\t' -v v=$v '{k=$1; u[k]+=$5; if($5+0>m[k]+0)m[k]=$5; c[k]++} END{for(k in c) print "BOUNDS",v,k,"families",c[k],"maxulp",m[k],"sumulp",u[k]}' /tmp/bounds.$v; done
|
||||
for v in plain ncod; do awk -F'\t' '{print $1"/"$2"\t"$5}' /tmp/bounds.$v | sort > /tmp/b.$v; done
|
||||
join -t$'\t' /tmp/b.plain /tmp/b.ncod | awk -F'\t' '$2!=$3{print "ULPDIFF",$1,"plain",$2,"ncod",$3}' | head -60
|
||||
111
docs/research/shader-variable-inputs.md
Normal file
111
docs/research/shader-variable-inputs.md
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# Shader variable-length inputs: how shader libraries take a span, and what CultMath's build route allows
|
||||
|
||||
For `cultmath-tapes:follow_up:shader-weighted-lattice-loop` and `cultmath-tapes:follow_up:lattice-shader-pins` (campaign `cultmath-tapes`).
|
||||
Imagination pass, agent `imagination-ag4-shader-loop`, session `self-2026-10-16-ag4`, 2026-10-17, against CultLib `origin/main` `e2051c5a`
|
||||
and the build branch `hands/cultmath-glsl-build-dxc` `23ac4e89`. Probes are in `docs/research/shader-variable-inputs/`.
|
||||
|
||||
Tags: [src] primary source text read this pass; [web] search summary only; [probe] run this pass, output quoted below.
|
||||
|
||||
## 1. The question
|
||||
|
||||
C#'s `weighted_lattice_cells` reads the body span once per lattice cell (the weight is the exact power border of the
|
||||
cell centre), and the ring search around it is the part a consumer cannot safely write by hand: the exit bound,
|
||||
the two passes and the box-independent fold order. The shader port has the four steps only. A shader function cannot
|
||||
take `ReadOnlySpan`, the library targets GLSL ES 3.00 fragment shaders (WebGL2: no storage buffers), and the GLSL is
|
||||
built from the HLSL by dxc `lib_6_3` SPIR-V plus SPIRV-Cross (ruling `glsl-es-via-dxc-spirv-cross`).
|
||||
So how do bodies reach a shader `weighted_lattice_cells`?
|
||||
|
||||
## 2. Prior art: how shader code that ships as a library takes a variable number of inputs
|
||||
|
||||
- [src] Unity URP (`ShaderLibrary/Input.hlsl`, `RealtimeLights.hlsl`, Graphics `master`): a fixed capacity
|
||||
`MAX_VISIBLE_LIGHTS` chosen per platform (`MAX_VISIBLE_LIGHT_COUNT_LOW_END_MOBILE` under `SHADER_API_GLES30`,
|
||||
`_MOBILE` for GLES3/WebGPU, `_DESKTOP` otherwise), constant-buffer arrays `float4 _AdditionalLightsPosition[MAX_VISIBLE_LIGHTS]`
|
||||
and the like, with `StructuredBuffer<LightData>` only behind `USE_STRUCTURED_BUFFER_FOR_LIGHT_DATA` (defined 0). The
|
||||
count is a uniform read through `GetAdditionalLightsCount()`, and `GetAdditionalLight(uint i, float3 positionWS)` is
|
||||
the accessor; `LIGHT_LOOP_BEGIN(lightCount)` / `LIGHT_LOOP_END` macros hide the cluster versus plain loop. The
|
||||
pipeline owns the storage and the accessor; user shaders call them. Capacity plus count, storage owned by the include.
|
||||
- [src] Filament (`shaders/src/surface_light_punctual.fs`, `main`): the loop is the engine's, over a froxel record
|
||||
(`froxelsUniforms.records`, then `froxelRecordUniforms.records`) giving `recordOffset` and `count`; each light is
|
||||
`lightsUniforms.lights[lightIndex]`, a uniform-block array. Material code never sees the loop. The engine owns both
|
||||
the storage and the loop because it also owns the uniform layout.
|
||||
- [src] three.js (`ShaderChunk/lights_fragment_begin.glsl.js`, `dev`): `for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ )`
|
||||
under `#pragma unroll_loop_start`, reading `pointLights[ i ]`; `NUM_POINT_LIGHTS` is a define the renderer writes,
|
||||
so the shader is regenerated per light count. Count as a compile-time constant; the library is source text.
|
||||
- [web] Bevy: clustered forward needs storage buffers; on WebGL2 it falls back to a uniform buffer with a capped
|
||||
object count (`MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS` 204 in an older `bevy_pbr`, asserted to fit 16384 bytes; the
|
||||
Many Lights example says 256 lights under `WGPU_SETTINGS_PRIO=webgl2`). Capacity plus count where storage is absent.
|
||||
- [web] lygia (README conventions, via search): reuse by `#ifndef` / `#define` options the caller sets before the
|
||||
include (`SAMPLER_FNC(TEX, UV)`, `MYFUNC_TYPE`, per-function sampler overrides). Caller-supplied accessor macros; the
|
||||
library ships as source text, which is what lets a macro reach inside a function body.
|
||||
|
||||
The pattern that splits them: a caller-supplied accessor (lygia, URP's accessor functions, three.js's define) works
|
||||
where the library is source text the caller's preprocessor sees. Where the library is compiled or its storage layout is
|
||||
fixed (URP's GLES path, Bevy's WebGL2 fallback, Filament's uniform blocks), the input is a fixed-capacity array plus a
|
||||
count, and the capacity is a named library constant.
|
||||
|
||||
## 3. What the build route allows (probes)
|
||||
|
||||
| Candidate | HLSL (dxc) | Library SPIR-V and SPIRV-Cross GLSL | C# mirror (`TransformHlslToCSharp`) | Verdict |
|
||||
|---|---|---|---|---|
|
||||
| Fixed-size array parameter plus count | compiles, `lib_6_3` and `cs_6_0` [probe P1, P3] | see P2 | needs one transform (`T x[N]` parameter to `T[] x`) | see section 4 |
|
||||
| Caller accessor macro (`CULTMATH_BODY(i)`) | the library build defines no accessor, so the function is absent [probe P4] | absent from the built GLSL: SPIRV-Cross output carries no consumer macro | the mirror would need the macro too | refused by the route |
|
||||
| Bodies packed into library-owned constants | a library cbuffer or uniform | a uniform block in a function-only library; a binding the consumer does not own | a static field | refused (P5, by construction, not run): a library may not own a consumer's binding |
|
||||
|
||||
Probes (files in `shader-variable-inputs/`):
|
||||
|
||||
- [probe] P1, local, dxc 2026_07_29 (`.tools/dxc`): `lattice-array.hlsl` (CultMath.hlsl, `prototype.hlsl`, one export)
|
||||
compiles with `-T lib_6_3 -HV 2021 -spirv -fspv-target-env=universal1.5 -Od`. The SPIR-V types the array as
|
||||
`OpTypeArray %v4float %uint_16` behind a Function pointer, and `-Od` copies it at each call (`param_var_bodies`).
|
||||
- [probe] P3, local: `entry-cbuffer.hlsl` passes a cbuffer array directly and a `StructuredBuffer` copied into a local
|
||||
array; both compile to SPIR-V (`cs_6_0`) and to DXIL.
|
||||
- [probe] P4, local: `macro-accessor.hlsl` with no `CULTMATH_BODY` defined fails, `use of undeclared identifier`: a
|
||||
macro-parameterised function does not exist in a library compiled without the caller's macros, so the built GLSL
|
||||
cannot carry it. Shipping it would need a hand-kept GLSL text beside the build, a second owner the ruling removed.
|
||||
- [probe] P2, Yggdrasil job (`probe.sh` at `23ac4e89`, sdk:10.0, 2026-10-17): with the prototype appended to
|
||||
`CultMath.hlsl` and two build-tool edits (`wrappers.py` strips `[N]` from an argument name; `partition.py` accepts
|
||||
`static const int`), `build-glsl.sh` is green ("build shape ok", "suffixed ids: 0; inout: 0") and emits
|
||||
`const int CULTMATH_MAX_SITES = 16;`, `vec4 cultmath_power_border(vec3 p, vec4 sites[16], int count)` and
|
||||
`vec4 cultmath_weighted_lattice_cells(vec2 p, float time, vec4 bodies[16], int count, ...)`. A `#version 300 es`
|
||||
fragment consumer passing `uniform vec4 zone[CULTMATH_MAX_SITES]` compiles with glslang.
|
||||
- [probe] Same job, lavapipe (`llvmpipe (LLVM 20.1.2, 256 bits)`), 257 cases from `ref/Program.cs` (the ring-17 point
|
||||
of `TheCapIsReachedAndMatchesTheFullBox` and 7 neighbours, four looks, five normals including (3e-30, -4e-30) and
|
||||
(1e30, 2e30), a zero normal at stretch 1, counts 0, 1, 3, 6, 16), judged against C#'s bits at 2.2e-6 max(|v|, 1):
|
||||
|
||||
| Variant (HLSL route unless named) | bit-exact | over bound | worst |
|
||||
|---|---|---|---|
|
||||
| built GLSL | 237 | 0 | 1.19e-6 |
|
||||
| HLSL (equal to built GLSL bit for bit) | 237 | 0 | 1.19e-6 |
|
||||
| M1 exit at `0.5 k` | 236 | 1 | 4.28e-3 |
|
||||
| M2 exit ignores the smooth band (`>= f2`) | 226 | 13 | 8.33e-2 |
|
||||
| M3 normaliser drops the divide by the largest component | 148 | 96 | 1e30 |
|
||||
| M4 normaliser drops the divide by length | 1 | 208 | 0.259 |
|
||||
| M5 smooth step loses its `min` cap | 153 | 85 | 2.26e-3 |
|
||||
| M6 box pass skips its first row | 228 | 9 | 0.807 |
|
||||
| M7 exit one ring late (`0.5 (k - 2)`) | 237 | 0 | 1.19e-6 |
|
||||
|
||||
M7 is equivalent: the result does not depend on the box (the proof above `weighted_lattice_cells`), so a late exit
|
||||
costs rings and changes nothing. M1 is killed by one case of 257, the ring-17 point: an early exit only matters where
|
||||
a far ring wins, so the device family must carry several such points.
|
||||
|
||||
## 4. Decision
|
||||
|
||||
The fixed-size array plus count is the only candidate the route admits, and it is the shape the compiled-library
|
||||
prior art uses (URP's GLES path, Bevy's WebGL2 fallback, Filament's uniform blocks). The capacity is a named library
|
||||
constant, `CULTMATH_MAX_SITES`, shared by every span-taking shader function. Its value is
|
||||
`cultmath-tapes:question:shader-site-capacity` (Aetheria zones: p50 17, p99 57, max 80 bodies, gravity map B28).
|
||||
|
||||
The shader recomputes each distance in the box pass instead of caching it: C#'s 75 x 75 stack cache is not a shader
|
||||
option, and recomputing gives the same value, as the probe's bit-exact HLSL and built-GLSL agreement shows.
|
||||
Reflection cannot pass a `ReadOnlySpan`, so the mirror and the fixture reach the C# span functions through one
|
||||
explicit adapter table rather than `MethodInfo.Invoke`.
|
||||
|
||||
## 5. lattice-shader-pins
|
||||
|
||||
- The smooth-step family draws m in [f1, f1 + smoothing), so the cap never binds. The cap binds when m = f1 + smoothing,
|
||||
d = f1 + x smoothing with x around 1.3 to 1.9, and smoothing above about 0.25: next - m = (d - m) h - h (1 - h)
|
||||
smoothing / (1 + 3 smoothing) is positive there (for smoothing 1 and d - m = 0.5, h = 0.156 and next - m = +0.045).
|
||||
M5 above is killed by the whole function already; the step family should kill it on its own.
|
||||
- Platform values: `8b31e47e` rewrote 7 platform-family entries of `glsl-parity.json` from "Ubuntu 24.04.5 LTS" to
|
||||
"Microsoft Windows 10.0.26200", while `7ddb1040` recorded the lavapipe bounds on Yggdrasil against that fixture.
|
||||
The bounds, the lavapipe job and the build route all run in Yggdrasil's sdk:10.0 Ubuntu image, so that image
|
||||
owns the fixture's platform values too. A write on another OS is refused rather than reviewed.
|
||||
15
docs/research/shader-variable-inputs/entry-cbuffer.hlsl
Normal file
15
docs/research/shader-variable-inputs/entry-cbuffer.hlsl
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Probe C: a compute entry passing a constant-buffer array, and one passing a StructuredBuffer copied to a local array.
|
||||
#include "lattice-array.hlsl"
|
||||
cbuffer Zone : register(b0) { float4 zoneBodies[CULTMATH_MAX_SITES]; int zoneCount; };
|
||||
StructuredBuffer<float4> bufferBodies : register(t1);
|
||||
RWStructuredBuffer<float4> result : register(u2);
|
||||
[numthreads(64, 1, 1)]
|
||||
void main(uint3 id : SV_DispatchThreadID)
|
||||
{
|
||||
float2 p = float2(id.x * 7.0, id.y * 11.0);
|
||||
float4 local[CULTMATH_MAX_SITES];
|
||||
for (int i = 0; i < CULTMATH_MAX_SITES; i++)
|
||||
local[i] = i < zoneCount ? bufferBodies[i] : float4(0.0, 0.0, 0.0, 1.0);
|
||||
result[id.x] = cultmath_weighted_lattice_cells(p, 1.0, zoneBodies, zoneCount, 260.0, 0.3, 1.6, 120.0, 0.35, float2(1.0, 2.0), 1.5)
|
||||
+ cultmath_weighted_lattice_cells(p, 1.0, local, zoneCount, 260.0, 0.3, 1.6, 120.0, 0.35, float2(1.0, 2.0), 1.5);
|
||||
}
|
||||
16
docs/research/shader-variable-inputs/entry.comp.tail
Normal file
16
docs/research/shader-variable-inputs/entry.comp.tail
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
layout(local_size_x = 64) in;
|
||||
layout(std430, binding = 0) readonly buffer IB { uint a[]; } In;
|
||||
layout(std430, binding = 1) buffer OB { uint a[]; } Out;
|
||||
void main()
|
||||
{
|
||||
uint i = gl_GlobalInvocationID.x; if (i >= NCASES) return; uint b = i * 76u;
|
||||
vec4 bodies[CULTMATH_MAX_SITES];
|
||||
for (int k = 0; k < CULTMATH_MAX_SITES; k++)
|
||||
bodies[k] = uintBitsToFloat(uvec4(In.a[b + 2u + 4u * uint(k)], In.a[b + 3u + 4u * uint(k)], In.a[b + 4u + 4u * uint(k)], In.a[b + 5u + 4u * uint(k)]));
|
||||
uint c = b + 67u;
|
||||
vec4 r = cultmath_weighted_lattice_cells(uintBitsToFloat(uvec2(In.a[b], In.a[b + 1u])), uintBitsToFloat(In.a[c]), bodies, int(In.a[b + 66u]),
|
||||
uintBitsToFloat(In.a[c + 1u]), uintBitsToFloat(In.a[c + 2u]), uintBitsToFloat(In.a[c + 3u]), uintBitsToFloat(In.a[c + 4u]),
|
||||
uintBitsToFloat(In.a[c + 5u]), uintBitsToFloat(uvec2(In.a[c + 6u], In.a[c + 7u])), uintBitsToFloat(In.a[c + 8u]));
|
||||
uvec4 w = floatBitsToUint(r);
|
||||
Out.a[i * 4u] = w.x; Out.a[i * 4u + 1u] = w.y; Out.a[i * 4u + 2u] = w.z; Out.a[i * 4u + 3u] = w.w;
|
||||
}
|
||||
16
docs/research/shader-variable-inputs/entry.hlsl
Normal file
16
docs/research/shader-variable-inputs/entry.hlsl
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Probe compute entry (HLSL route): 76 words in per case (p, 16 bodies, count, 9 parameters), 4 words out.
|
||||
#include "CultMath.hlsl"
|
||||
[[vk::binding(0)]] StructuredBuffer<uint> In;
|
||||
[[vk::binding(1)]] RWStructuredBuffer<uint> Out;
|
||||
[numthreads(64, 1, 1)] void main(uint3 id : SV_DispatchThreadID)
|
||||
{
|
||||
uint i = id.x; if (i >= NCASES) return; uint b = i * 76u;
|
||||
float4 bodies[CULTMATH_MAX_SITES];
|
||||
for (int k = 0; k < CULTMATH_MAX_SITES; k++)
|
||||
bodies[k] = float4(asfloat(In[b + 2 + 4 * k]), asfloat(In[b + 3 + 4 * k]), asfloat(In[b + 4 + 4 * k]), asfloat(In[b + 5 + 4 * k]));
|
||||
uint c = b + 67u;
|
||||
float4 r = cultmath_weighted_lattice_cells(float2(asfloat(In[b]), asfloat(In[b + 1])), asfloat(In[c]), bodies, asint(In[b + 66]),
|
||||
asfloat(In[c + 1]), asfloat(In[c + 2]), asfloat(In[c + 3]), asfloat(In[c + 4]), asfloat(In[c + 5]),
|
||||
float2(asfloat(In[c + 6]), asfloat(In[c + 7])), asfloat(In[c + 8]));
|
||||
Out[i * 4] = asuint(r.x); Out[i * 4 + 1] = asuint(r.y); Out[i * 4 + 2] = asuint(r.z); Out[i * 4 + 3] = asuint(r.w);
|
||||
}
|
||||
14
docs/research/shader-variable-inputs/judge.py
Normal file
14
docs/research/shader-variable-inputs/judge.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Compare a device output with C#'s reference bits: cases over the lavapipe bound 2.2e-6 max(|v|, 1), and the largest.
|
||||
usage: judge.py <ref.bin> <out.bin> <label>"""
|
||||
import math, struct, sys
|
||||
ref, out = open(sys.argv[1], 'rb').read(), open(sys.argv[2], 'rb').read()
|
||||
n = len(ref) // 16
|
||||
r, o = struct.unpack('<%df' % (4 * n), ref), struct.unpack('<%df' % (4 * n), out)
|
||||
over, exact, worst = 0, 0, 0.0
|
||||
for i in range(n):
|
||||
s = max((abs(o[4*i+j] - r[4*i+j]) / max(abs(r[4*i+j]), 1.0) if math.isfinite(o[4*i+j]) else math.inf) for j in range(4))
|
||||
exact += ref[16*i:16*i+16] == out[16*i:16*i+16]
|
||||
over += s > 2.2e-6
|
||||
worst = max(worst, s)
|
||||
print('%s: cases %d, bit-exact %d, over 2.2e-6 %d, worst %.3g' % (sys.argv[3], n, exact, over, worst))
|
||||
7
docs/research/shader-variable-inputs/lattice-array.hlsl
Normal file
7
docs/research/shader-variable-inputs/lattice-array.hlsl
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// Probe A (local dxc): the prototype as a lib_6_3 SPIR-V library export, bodies as a fixed-size array parameter plus a count.
|
||||
#include "CultMath.hlsl"
|
||||
#include "prototype.hlsl"
|
||||
export float4 probe_lattice(float2 p, float4 bodies[CULTMATH_MAX_SITES], int count)
|
||||
{
|
||||
return cultmath_weighted_lattice_cells(p, 1.0, bodies, count, 260.0, 0.3, 1.6, 120.0, 0.35, float2(1.0, 2.0), 1.5);
|
||||
}
|
||||
12
docs/research/shader-variable-inputs/macro-accessor.hlsl
Normal file
12
docs/research/shader-variable-inputs/macro-accessor.hlsl
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Probe B: bodies through a caller-supplied accessor macro. The library build defines no accessor.
|
||||
#include "CultMath.hlsl"
|
||||
#ifdef CULTMATH_BODY
|
||||
float4 cultmath_power_border_m(float3 p)
|
||||
{
|
||||
float4 border = float4(0.0, 0.0, 0.0, 1.0e30);
|
||||
for (int k = 1; k < CULTMATH_BODY_COUNT; k++)
|
||||
border = cultmath_power_border_step(p, CULTMATH_BODY(0), CULTMATH_BODY(k), border);
|
||||
return border;
|
||||
}
|
||||
#endif
|
||||
export float4 probe_macro(float3 p) { return cultmath_power_border_m(p); }
|
||||
55
docs/research/shader-variable-inputs/probe.sh
Normal file
55
docs/research/shader-variable-inputs/probe.sh
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#!/usr/bin/env bash
|
||||
# Probe for shader-weighted-lattice-loop: the prototype through the dxc + SPIRV-Cross build and on lavapipe, against C#.
|
||||
# Run from a CultLib clone at a revision with packages/cultmath/tools/glsl/build-glsl.sh (hands/cultmath-glsl-build-dxc
|
||||
# 23ac4e89), inside mcr.microsoft.com/dotnet/sdk:10.0, with this directory at $P. The clone is disposable: the
|
||||
# prototype is appended to its CultMath.hlsl, and two build-tool lines are patched (array arguments, int constants).
|
||||
set -euo pipefail
|
||||
P=${P:-/tmp/probe}; ROOT=$(pwd); CM=$ROOT/packages/cultmath; W=/tmp/pw; mkdir -p $W
|
||||
apt-get update -qq >/dev/null && apt-get install -y -qq --no-install-recommends python3 >/dev/null
|
||||
cat $P/prototype.hlsl >> $CM/shaders/CultMath.hlsl
|
||||
python3 - $CM/tools/glsl/wrappers.py <<'PY'
|
||||
import sys; f = sys.argv[1]; t = open(f).read()
|
||||
t = t.replace(r"args = [re.split(r'\s+', p)[-1] for p in params]", r"args = [re.split(r'\s+', re.sub(r'\[.*\]', '', p))[-1] for p in params]")
|
||||
open(f, 'w').write(t)
|
||||
PY
|
||||
grep -c "re.sub(r'" $CM/tools/glsl/wrappers.py
|
||||
python3 - $CM/tools/glsl/partition.py <<'PY'
|
||||
import sys; f = sys.argv[1]; t = open(f).read()
|
||||
t = t.replace(r"r'^static const float (CULTMATH_\w+) = (-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?);$'", r"r'^static const (float|int) (CULTMATH_\w+) = (-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?);$'")
|
||||
t = t.replace("consts[o].append('const float %s = %s;' % m.groups())", "consts[o].append('const %s %s = %s;' % m.groups())")
|
||||
open(f, 'w').write(t)
|
||||
PY
|
||||
grep -c "(float|int)" $CM/tools/glsl/partition.py
|
||||
echo "== build"
|
||||
bash $CM/tools/glsl/build-glsl.sh $W/built 2>&1 | tail -12
|
||||
echo "== array functions in the built core.glsl"
|
||||
grep -nE "^(const int CULTMATH_MAX_SITES|vec4 cultmath_power_border\(|float cultmath_lattice_cell_distance\(|vec4 cultmath_weighted_lattice_cells\()" $W/built/core.glsl
|
||||
echo "== ES 3.00 fragment consumer passing a uniform array"
|
||||
{ printf '#version 300 es\nprecision highp float;\nprecision highp int;\n'; cat $W/built/core.glsl
|
||||
printf 'uniform vec4 zone[CULTMATH_MAX_SITES];\nuniform int zoneCount;\nout highp vec4 c;\nvoid main() { c = cultmath_weighted_lattice_cells(gl_FragCoord.xy, 1.0, zone, zoneCount, 260.0, 0.7, 1.6, 120.0, 0.35, vec2(0.6, 0.8), 1.5); }\n'; } > $W/consumer.frag
|
||||
glslangValidator $W/consumer.frag && echo "consumer ES 3.00 ok"
|
||||
echo "== lavapipe"
|
||||
apt-get install -y -qq --no-install-recommends mesa-vulkan-drivers libvulkan-dev gcc libc6-dev >/dev/null
|
||||
gcc -O2 -o $W/vkrun $CM/tools/device/vkrun.c -lvulkan
|
||||
cp -r $P/ref $W/ref && sed -i 's#../../../../packages#'"$CM"'/..#' $W/ref/Ref.csproj
|
||||
N=$(dotnet run --project $W/ref -c Release -- $W/in.bin $W/ref.bin | tail -1); echo "cases $N"
|
||||
DXC=$(find $CM/.tools/glsl/dxc -name dxc -type f | head -1); export LD_LIBRARY_PATH=$(dirname $DXC)/../lib:$(dirname $DXC)
|
||||
{ printf '#version 310 es\nprecision highp float;\nprecision highp int;\n#define NCASES %su\n' $N; cat $W/built/core.glsl $P/entry.comp.tail; } > $W/e.comp
|
||||
glslangValidator -V --target-env vulkan1.2 -o $W/g.spv $W/e.comp >$W/g.log || { head $W/g.log; exit 1; }
|
||||
$W/vkrun $W/g.spv $W/in.bin $W/g.out $N 4
|
||||
python3 $P/judge.py $W/ref.bin $W/g.out built-glsl
|
||||
run_hlsl() { # $1 label, $2 sed expression applied to a copy of the shaders
|
||||
rm -rf $W/s && cp -r $CM/shaders $W/s && sed -i "$2" $W/s/CultMath.hlsl
|
||||
$DXC -T cs_6_0 -E main -HV 2021 -spirv -fspv-target-env=vulkan1.2 -D NCASES=${N}u -I $W/s $P/entry.hlsl -Fo $W/h.spv >$W/h.log 2>&1 || { head -5 $W/h.log; exit 1; }
|
||||
$W/vkrun $W/h.spv $W/in.bin $W/h.out $N 4 >/dev/null
|
||||
python3 $P/judge.py $W/ref.bin $W/h.out "$1"
|
||||
}
|
||||
run_hlsl hlsl 's/^$//'
|
||||
cmp $W/h.out $W/g.out && echo "hlsl equals built glsl bit for bit" || echo "hlsl and built glsl differ (plain form; the gate is strict form)"
|
||||
run_hlsl "M1 exit at 0.5k" 's/0\.5 \* (k - 1) >= max/0.5 * k >= max/'
|
||||
run_hlsl "M2 exit ignores the smooth band" 's/>= max(f2, f1 + 2\.0 \* smoothing))$/>= f2)/'
|
||||
run_hlsl "M3 normaliser drops the pre-scale" 's/normal = stretchNormal \/ scale;/normal = stretchNormal;/'
|
||||
run_hlsl "M4 normaliser drops the length" 's/normal = normal \/ length(normal);//'
|
||||
run_hlsl "M5 smooth step loses its cap" 's/return min(next, f1 + smoothing);/return next;/'
|
||||
run_hlsl "M6 box pass skips its first row" 's/for (int dz = -reach; dz <= reach; dz++)/for (int dz = -reach + 1; dz <= reach; dz++)/'
|
||||
run_hlsl "M7 late exit (equivalent: window-independent)" 's/0\.5 \* (k - 1) >= max/0.5 * (k - 2) >= max/'
|
||||
75
docs/research/shader-variable-inputs/prototype.hlsl
Normal file
75
docs/research/shader-variable-inputs/prototype.hlsl
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// The prototype, appended to CultMath.hlsl by probe.sh: the whole weighted_lattice_cells with bodies as a fixed-size array and a count.
|
||||
static const int CULTMATH_MAX_SITES = 16;
|
||||
|
||||
float4 cultmath_power_border(float3 p, float4 sites[CULTMATH_MAX_SITES], int count)
|
||||
{
|
||||
CultSiteCells cells = cultmath_site_cells_empty();
|
||||
for (int i = 0; i < count; i++)
|
||||
cells = cultmath_site_cells_insert(cells, cultmath_power_score(p, sites[i]), i);
|
||||
float4 border = float4(0.0, 0.0, 0.0, 1.0e30);
|
||||
int owner = (int)cells.nearestSite;
|
||||
if (owner < 0)
|
||||
return border;
|
||||
for (int k = 0; k < count; k++)
|
||||
if (k != owner)
|
||||
border = cultmath_power_border_step(p, sites[owner], sites[k], border);
|
||||
return border;
|
||||
}
|
||||
|
||||
float cultmath_lattice_cell_distance(float2 q, float2 cell, float time, float drift, float4 bodies[CULTMATH_MAX_SITES], int count,
|
||||
float cellSize, float boost, float falloff, float2 normal, float stretch)
|
||||
{
|
||||
float3 site = cultmath_lattice_site(cell, time, drift);
|
||||
float border = cultmath_power_border(float3((cell + 0.5) * cellSize, 0.0), bodies, count).w;
|
||||
return cultmath_lattice_distance(q, site.xy, cultmath_lattice_weight(site.z, border, boost, falloff), normal, stretch);
|
||||
}
|
||||
|
||||
float4 cultmath_weighted_lattice_cells(float2 p, float time, float4 bodies[CULTMATH_MAX_SITES], int count, float cellSize, float drift,
|
||||
float boost, float falloff, float smoothing, float2 stretchNormal, float stretch)
|
||||
{
|
||||
float scale = max(abs(stretchNormal.x), abs(stretchNormal.y));
|
||||
float2 normal = float2(1.0, 0.0);
|
||||
if (scale > 0.0)
|
||||
{
|
||||
normal = stretchNormal / scale;
|
||||
normal = normal / length(normal);
|
||||
}
|
||||
float2 q = p / cellSize;
|
||||
float2 origin = floor(q);
|
||||
int cx0 = (int)origin.x;
|
||||
int cz0 = (int)origin.y;
|
||||
float f1 = 1.0e30;
|
||||
float f2 = 1.0e30;
|
||||
int reach = 0;
|
||||
for (int k = 0; k <= 37; k++)
|
||||
{
|
||||
if (k > 1 && 0.5 * (k - 1) >= max(f2, f1 + 2.0 * smoothing))
|
||||
break;
|
||||
for (int dz = -k; dz <= k; dz++)
|
||||
{
|
||||
int step = dz == -k || dz == k || k == 0 ? 1 : 2 * k;
|
||||
for (int dx = -k; dx <= k; dx += step)
|
||||
{
|
||||
float d = cultmath_lattice_cell_distance(q, float2(cx0 + dx, cz0 + dz), time, drift, bodies, count, cellSize, boost, falloff, normal, stretch);
|
||||
if (d < f1) { f2 = f1; f1 = d; }
|
||||
else if (d < f2) f2 = d;
|
||||
}
|
||||
}
|
||||
reach = k;
|
||||
}
|
||||
float m = f1 + smoothing;
|
||||
float best = 1.0e30;
|
||||
float second = 1.0e30;
|
||||
int winDx = 0;
|
||||
int winDz = 0;
|
||||
for (int dz = -reach; dz <= reach; dz++)
|
||||
for (int dx = -reach; dx <= reach; dx++)
|
||||
{
|
||||
float d = cultmath_lattice_cell_distance(q, float2(cx0 + dx, cz0 + dz), time, drift, bodies, count, cellSize, boost, falloff, normal, stretch);
|
||||
if (d < best) { second = best; best = d; winDx = dx; winDz = dz; }
|
||||
else if (d < second) second = d;
|
||||
m = cultmath_lattice_smooth_step(m, d, f1, smoothing);
|
||||
}
|
||||
int4 id = cultmath_pcg4d(int4(cx0 + winDx, cz0 + winDz, 0, 0));
|
||||
return float4(best, second, m, (float)((uint)id.w >> 8) * (1.0 / 16777216.0));
|
||||
}
|
||||
50
docs/research/shader-variable-inputs/ref/Program.cs
Normal file
50
docs/research/shader-variable-inputs/ref/Program.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// Probe reference: writes the lattice cases (76 words each) and C#'s weighted_lattice_cells bits (4 words each).
|
||||
// usage: Ref <in.bin> <ref.bin>; prints the case count.
|
||||
using CultMath;
|
||||
using static CultMath.math;
|
||||
|
||||
const int Max = 16;
|
||||
var cases = new List<(float2 p, float t, float4[] b, float cell, float drift, float boost, float falloff, float s, float2 n, float st)>();
|
||||
float4[] Bodies(float t, int count)
|
||||
{
|
||||
var b = new float4[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var a = 0.001f * (i + 1) * t + i; var r = 300.0f + 220.0f * i;
|
||||
b[i] = new float4(r * MathF.Cos(a), r * MathF.Sin(a), 0.0f, 40.0f + 40.0f * i);
|
||||
}
|
||||
return b;
|
||||
}
|
||||
// The ring-17 point of LatticeCellsTests.TheCapIsReachedAndMatchesTheFullBox, and its neighbours.
|
||||
for (var j = 0; j < 8; j++)
|
||||
cases.Add((new float2(1446.7345f + 3.0f * j, -680.7637f), 97.69269f, Bodies(97.69269f, 6), 260.0f, 0.7f, 4.0f, 1.0e6f, 1.0f, new float2(0.6f, 0.8f), 2.0f));
|
||||
var looks = new[] { (1.6f, 1.0f, 0.35f), (1.6f, 2.0f, 0.35f), (4.0f, 2.0f, 1.0f), (0.0f, 1.0f, 0.05f) };
|
||||
var normals = new[] { new float2(0.6f, 0.8f), new float2(3.0f, 4.0f), new float2(3.0e-30f, -4.0e-30f), new float2(1.0e30f, 2.0e30f), new float2(-1.0f, 1.0e-3f) };
|
||||
var rng = new System.Random(20261017);
|
||||
float U(float a, float b) => a + (b - a) * (float)rng.NextDouble();
|
||||
for (var i = 0; i < 248; i++)
|
||||
{
|
||||
var (boost, stretch, s) = looks[i % 4];
|
||||
var n = normals[(i / 4) % normals.Length];
|
||||
var count = new[] { 6, 0, 1, 16, 6, 3 }[(i / 20) % 6];
|
||||
var t = U(0.0f, 200.0f);
|
||||
cases.Add((new float2(U(-3000, 3000), U(-3000, 3000)), t, Bodies(t, count), 260.0f, 0.7f, boost, 120.0f, s, n, stretch));
|
||||
}
|
||||
// Zero normal at stretch 1 is (1, 0).
|
||||
cases.Add((new float2(10.0f, 20.0f), 1.0f, Bodies(1.0f, 6), 260.0f, 0.7f, 1.6f, 120.0f, 0.35f, new float2(0.0f, 0.0f), 1.0f));
|
||||
using var input = new BinaryWriter(File.Create(args[0]));
|
||||
using var reference = new BinaryWriter(File.Create(args[1]));
|
||||
foreach (var c in cases)
|
||||
{
|
||||
input.Write(c.p.x); input.Write(c.p.y);
|
||||
for (var k = 0; k < Max; k++)
|
||||
{
|
||||
var v = k < c.b.Length ? c.b[k] : new float4(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
input.Write(v.x); input.Write(v.y); input.Write(v.z); input.Write(v.w);
|
||||
}
|
||||
input.Write(c.b.Length);
|
||||
foreach (var f in new[] { c.t, c.cell, c.drift, c.boost, c.falloff, c.s, c.n.x, c.n.y, c.st }) input.Write(f);
|
||||
var r = weighted_lattice_cells(c.p, c.t, c.b, c.cell, c.drift, c.boost, c.falloff, c.s, c.n, c.st);
|
||||
reference.Write(r.x); reference.Write(r.y); reference.Write(r.z); reference.Write(r.w);
|
||||
}
|
||||
Console.WriteLine(cases.Count);
|
||||
4
docs/research/shader-variable-inputs/ref/Ref.csproj
Normal file
4
docs/research/shader-variable-inputs/ref/Ref.csproj
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net10.0</TargetFramework><Nullable>enable</Nullable><ImplicitUsings>enable</ImplicitUsings></PropertyGroup>
|
||||
<ItemGroup><ProjectReference Include="../../../../packages/cultmath/src/CultMath/CultMath.csproj" /></ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -38,8 +38,9 @@ shader semantics.
|
|||
drifting lattice whose cell weights come from the exact power border of the body span at each cell centre, with a stretched
|
||||
metric and an exact ring search over the stated range (`LatticeMaxBoost`, `LatticeMaxStretch`, `LatticeMaxSmoothing`). It composes the
|
||||
public steps `lattice_site`, `lattice_weight`, `lattice_distance` and `lattice_smooth_step`. The shaders carry the four steps as
|
||||
`cultmath_lattice_site`, `cultmath_lattice_weight`, `cultmath_lattice_distance` and `cultmath_lattice_smooth_step`; the ring and box loops
|
||||
are the shader caller's, written as the comment above them in `CultMath.hlsl` gives them, and the argument checks become its preconditions.
|
||||
`cultmath_lattice_site`, `cultmath_lattice_weight`, `cultmath_lattice_distance` and `cultmath_lattice_smooth_step`, and the whole function as `cultmath_weighted_lattice_cells(p, time, bodies,
|
||||
count, ...)` over a `float4 bodies[CULTMATH_MAX_SITES]` array (128 by default; define the macro before including to change it);
|
||||
`cultmath_power_border(p, sites, count)` is the matching border. The argument checks become the shader caller's preconditions.
|
||||
- `shaders/CultMath.hlsl`, a canonical HLSL mirror include for shader-side
|
||||
parity. HLSL already owns `float2`, `float3`, and `float4`; the include
|
||||
exposes `cultmath_*` functions for shared semantics HLSL does not provide.
|
||||
|
|
|
|||
|
|
@ -95,7 +95,8 @@ and HLSL's one-based `_11`..`_33` element names.
|
|||
Roslyn against CultMath. These transformations are the complete list; if a body
|
||||
needs anything else, the test fails:
|
||||
|
||||
1. Include guards (`#ifndef`/`#define`/`#endif`) are dropped.
|
||||
1. Include guards (`#ifndef`/`#define`/`#endif`) are dropped, except the capacity macro (`#ifndef CULTMATH_MAX_SITES`,
|
||||
`#define CULTMATH_MAX_SITES 128`, `#endif`), which becomes `const int CULTMATH_MAX_SITES = 128;`.
|
||||
2. Functions taking `Texture2D`/`SamplerState` are dropped; GPU resources have
|
||||
no CultMath analog.
|
||||
3. File-scope `static const` becomes `const`.
|
||||
|
|
@ -103,7 +104,8 @@ needs anything else, the test fails:
|
|||
5. Struct fields gain `public`. HLSL struct members have no access-modifier
|
||||
concept; a C# struct's fields default to `private`, which would hide them
|
||||
from the mirror test's reflection-based field comparison.
|
||||
6. The file body is wrapped in a class, since C# has no free functions; HLSL
|
||||
6. An array parameter `T name[CULTMATH_MAX_SITES]` becomes `T[] name`.
|
||||
7. The file body is wrapped in a class, since C# has no free functions; HLSL
|
||||
functions become private instance methods.
|
||||
|
||||
HLSL constructs that stay outside the target and must not appear in the mirror:
|
||||
|
|
@ -156,6 +158,10 @@ hand. Regenerate them, and the golden fixture
|
|||
$env:CULTMATH_WRITE_GLSL = "1"; dotnet test packages/cultmath/tests/CultMath.Tests --filter GlslMirrorTests
|
||||
```
|
||||
|
||||
Run it on Yggdrasil's `mcr.microsoft.com/dotnet/sdk:10.0` image (Ubuntu 24.04) only: that image owns the fixture's platform
|
||||
families and `device-bounds-lavapipe.txt`, and `CULTMATH_WRITE_GLSL=1` fails on any other OS. Re-record the bounds there in
|
||||
the same revision (`WRITE_BOUNDS=1`, `tools/device/run-lavapipe.sh`).
|
||||
|
||||
`GlslMirrorTests.CommittedGlslEqualsLowering` fails on any difference between
|
||||
either committed file and the lowering of the committed HLSL. These
|
||||
transformations, in order, are the complete list:
|
||||
|
|
@ -423,7 +429,7 @@ together, laid out as `float4(gradient.xyz, value.w)`, with no value-only twin
|
|||
- `phacelle(float3 p, float3 side, float offset, float normalization)` is Rune
|
||||
Skovbo Johansen's Phacelle noise ("phase + cell": a stripe pattern built by
|
||||
blending one cosine/sine wave per jittered cell), generalised to 3D cells. It
|
||||
returns `CultPhasor { cos, sin }`, each `(∇, value)`. `side` is the stripe
|
||||
returns `CultPhasor { cosine, sine }`, each `(∇, value)`. `side` is the stripe
|
||||
wave vector, whose length is 2π times the stripe frequency; in 3D the
|
||||
perpendicular to the flow is not unique, so the caller chooses it (on a unit
|
||||
sphere, `cross(p̂, flow)`). `offset` is the phase in cycles. It visits 4×4×4
|
||||
|
|
@ -519,11 +525,12 @@ running `float4(unit normal toward the owner, distance)` and the radical plane o
|
|||
the distance built from the offset `owner - p` and the weight difference `(w1 - w2)(w1 + w2)` so it does not
|
||||
cancel for close sites seen from far; the cell boundary distance is the least over every site except the owner
|
||||
(the fold's nearest), and a site coincident with the owner has no plane. The step trusts the caller's owner: a
|
||||
query outside the owner's cell reports distance 0 with that plane's normal, not an error. `power_border(p, sites)` is that loop.
|
||||
query outside the owner's cell reports distance 0 with that plane's normal, not an error. `power_border(p, sites)` is that loop;
|
||||
the shader's `cultmath_power_border(p, sites, count)` is the same loop over a by-value array (see "Sites in a shader" below).
|
||||
Cell spans refuse a non-finite query, site position or weight, and more than 2^24 sites (indices are floats).
|
||||
`power_cells` and
|
||||
`multiplicative_cells` are that loop over a `ReadOnlySpan<float4>` and nothing else; a shader caller writes the
|
||||
same loop over its own buffer. Every site of an explicit span is visited, so no window and no weight-ratio range
|
||||
same loop over its own array. Every site of an explicit span is visited, so no window and no weight-ratio range
|
||||
apply. `CultSiteCells` holds the two score fields and the two site indices (floats, -1 for none).
|
||||
|
||||
`gabriel_margin(a, b, other)` is `(|other - (a + b) / 2| - r) / r` with `r = |a - b| / 2`: zero on the diameter
|
||||
|
|
@ -557,8 +564,21 @@ The accepted range is boost 0 to `LatticeMaxBoost` (4, a weight ratio of 10:1),
|
|||
and smoothing up to `LatticeMaxSmoothing` (1); outside it the function throws `ArgumentException`. `LatticeMaxRing`
|
||||
(37) is the proven greatest ring at the range edge. Measured over 24,000 points on orbiting bodies (six bodies,
|
||||
sigma ratio 6:1, the ruled look and the range edge), the greatest ring visited has median 3, 99th percentile 7 and
|
||||
maximum 8 (49, 225 and 289 cells); `RingsStayWellInsideTheCap` asserts these. The function is C# only; the shader
|
||||
twins are the next cut.
|
||||
maximum 8 (49, 225 and 289 cells); `RingsStayWellInsideTheCap` asserts these.
|
||||
|
||||
### Sites in a shader
|
||||
|
||||
A shader function cannot take a span, so `cultmath_power_border(p, sites, count)`, `cultmath_lattice_cell_distance(...)`
|
||||
and `cultmath_weighted_lattice_cells(p, time, bodies, count, ...)` take `float4 sites[CULTMATH_MAX_SITES]` by value and the
|
||||
count in use, with `count` in `[0, CULTMATH_MAX_SITES]`. `CULTMATH_MAX_SITES` is 128 unless the consumer defines it before
|
||||
including the library (`#ifndef`): 128 covers every Aetheria zone within GLSL ES 3.00's 224 guaranteed fragment uniform
|
||||
vectors, and loops run to `count`, so the capacity costs uniform space, not iterations. A caller whose sites live in a
|
||||
buffer copies them into a local array first; the library takes no resource, accessor macro or uniform of its own. The preconditions the C#
|
||||
functions throw for are the caller's in a shader (finite arguments, `cellSize > 0`, the stated ranges, `p` within 2^23
|
||||
cells). The ring pass is bounded `k <= 37` and exits as C# does; the box pass recomputes each distance instead of caching
|
||||
75 x 75 of them, which gives the same values. The C# mirror compiles the array as `T[]`, and
|
||||
`HlslSourceCompatibilityTests.SiteArrayMirrorsMatchCSharp` compares the three functions through the span twins in
|
||||
`SiteArrays` (reflection cannot pass a `ReadOnlySpan`); `CULTMATH_MAX_SITES` is a shader-only capacity with no C# twin.
|
||||
|
||||
## Intervals
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@
|
|||
// against CultMath.CultPhasor, same field order.
|
||||
struct CultPhasor
|
||||
{
|
||||
vec4 cos;
|
||||
vec4 sin;
|
||||
vec4 cosine;
|
||||
vec4 sine;
|
||||
};
|
||||
|
||||
// Johansen's Phacelle noise on 3D cells. See math.phacelle's comment (math.Phacelle.cs) for the stripe wave
|
||||
|
|
@ -84,13 +84,13 @@ CultPhasor cultmath_phacelle(vec3 p, vec3 side, float offset, float normalizatio
|
|||
float outCos = rawCos / rawLength;
|
||||
float outSin = rawSin / rawLength;
|
||||
vec3 gradLength = (rawCos * gradRawCos + rawSin * gradRawSin) / rawLength;
|
||||
result.cos = vec4((gradRawCos - outCos * gradLength) / rawLength, outCos);
|
||||
result.sin = vec4((gradRawSin - outSin * gradLength) / rawLength, outSin);
|
||||
result.cosine = vec4((gradRawCos - outCos * gradLength) / rawLength, outCos);
|
||||
result.sine = vec4((gradRawSin - outSin * gradLength) / rawLength, outSin);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.cos = vec4(gradRawCos / floorLength, rawCos / floorLength);
|
||||
result.sin = vec4(gradRawSin / floorLength, rawSin / floorLength);
|
||||
result.cosine = vec4(gradRawCos / floorLength, rawCos / floorLength);
|
||||
result.sine = vec4(gradRawSin / floorLength, rawSin / floorLength);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@
|
|||
// against CultMath.CultPhasor, same field order.
|
||||
struct CultPhasor
|
||||
{
|
||||
float4 cos;
|
||||
float4 sin;
|
||||
float4 cosine;
|
||||
float4 sine;
|
||||
};
|
||||
|
||||
// Johansen's Phacelle noise on 3D cells. See math.phacelle's comment (math.Phacelle.cs) for the stripe wave
|
||||
|
|
@ -84,13 +84,13 @@ CultPhasor cultmath_phacelle(float3 p, float3 side, float offset, float normaliz
|
|||
float outCos = rawCos / rawLength;
|
||||
float outSin = rawSin / rawLength;
|
||||
float3 gradLength = (rawCos * gradRawCos + rawSin * gradRawSin) / rawLength;
|
||||
result.cos = float4((gradRawCos - outCos * gradLength) / rawLength, outCos);
|
||||
result.sin = float4((gradRawSin - outSin * gradLength) / rawLength, outSin);
|
||||
result.cosine = float4((gradRawCos - outCos * gradLength) / rawLength, outCos);
|
||||
result.sine = float4((gradRawSin - outSin * gradLength) / rawLength, outSin);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.cos = float4(gradRawCos / floorLength, rawCos / floorLength);
|
||||
result.sin = float4(gradRawSin / floorLength, rawSin / floorLength);
|
||||
result.cosine = float4(gradRawCos / floorLength, rawCos / floorLength);
|
||||
result.sine = float4(gradRawSin / floorLength, rawSin / floorLength);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -600,14 +600,12 @@ CultCellular cultmath_cellular(vec3 p)
|
|||
return result;
|
||||
}
|
||||
|
||||
// Sites: a site is vec4(position.xyz, weight). A span is a loop over a per-site step, because a shader
|
||||
// function cannot take a span in the mirror (design.md, "Sites"). The consumer's loop, over its own buffer:
|
||||
// Sites: a site is vec4(position.xyz, weight). A shader function cannot take a span (design.md, "Sites"), so a span is a
|
||||
// fixed-capacity array and a count (cultmath_power_border, below), or a loop over a per-site step:
|
||||
// CultSiteCells cells = cultmath_site_cells_empty();
|
||||
// for (int i = 0; i < count; i++)
|
||||
// cells = cultmath_site_cells_insert(cells, cultmath_power_score(p, sites[i]), i);
|
||||
// The exact power cell border is a second loop over the buffer, skipping the owner cells.nearestSite:
|
||||
// vec4 border = vec4(0.0, 0.0, 0.0, 1.0e30); int owner = int(cells.nearestSite);
|
||||
// for (int k = 0; k < count; k++) if (k != owner) border = cultmath_power_border_step(p, sites[owner], sites[k], border);
|
||||
// The exact power cell border is cultmath_power_border, the second loop over the sites that skips the owner cells.nearestSite.
|
||||
// The step trusts the owner: a p outside the owner's cell reports distance 0 with that plane's normal, not an error.
|
||||
// Statement for statement with math.Sites.cs (power_score, multiplicative_score, site_cells_*, gabriel_margin).
|
||||
struct CultSiteCells
|
||||
|
|
@ -688,21 +686,8 @@ float cultmath_gabriel_margin(vec3 a, vec3 b, vec3 other)
|
|||
return (cultmath_distance(other, (a + b) * 0.5) - r) / r;
|
||||
}
|
||||
|
||||
// Weighted lattice cells: the four per-cell steps of math.Lattice.cs, statement for statement. A shader function cannot take a
|
||||
// span (design.md, "Sites"), so weighted_lattice_cells is the consumer's loop over its own body buffer, in two passes:
|
||||
// vec2 q = p / cellSize; ivec2 c0 = (ivec2)floor(q); float f1 = 1.0e30, f2 = 1.0e30; int reach = 0;
|
||||
// for (int k = 0; ; k++) { // ring pass: Chebyshev rings 0, 1, 2, ... around c0, exact exit
|
||||
// if (k > 1 && 0.5 * (k - 1) >= max(f2, f1 + 2.0 * smoothing)) break; // k never reaches 38 in the accepted range
|
||||
// for each cell on ring k: d = cultmath_lattice_distance(q, cultmath_lattice_site(cell, time, drift).xy, w, normal, stretch),
|
||||
// w = cultmath_lattice_weight(site.z, border, boost, falloff), border = the .w of cultmath_power_border_step
|
||||
// folded over the bodies (owner = the nearest body of the cell centre (cell + 0.5) * cellSize, z = 0);
|
||||
// if (d < f1) { f2 = f1; f1 = d; } else if (d < f2) f2 = d; store d for the box pass
|
||||
// reach = k; }
|
||||
// float m = f1 + smoothing; // box pass: every stored cell, rows dz = -reach..reach, then dx = -reach..reach
|
||||
// for each (dz, dx): m = cultmath_lattice_smooth_step(m, d, f1, smoothing); // strict < for the winning id (earlier cell wins ties)
|
||||
// The preconditions the C# function throws for are the caller's in a shader: finite arguments, cellSize > 0, boost in [0, 4],
|
||||
// falloff > 0, smoothing in (0, 1], stretch in [1, 2], p within 2^23 cells. The normal is normalised as C# does: divide by its
|
||||
// largest component, then by its length (a zero normal means (1, 0) and needs stretch == 1). The ring bound is 37.
|
||||
// Weighted lattice cells: the four per-cell steps of math.Lattice.cs, statement for statement; cultmath_weighted_lattice_cells,
|
||||
// after them, is the whole function.
|
||||
vec3 cultmath_lattice_site(vec2 cell, float time, float drift)
|
||||
{
|
||||
ivec3 h = cultmath_pcg3d(ivec3(int(cell.x), int(cell.y), 0));
|
||||
|
|
@ -734,6 +719,90 @@ float cultmath_lattice_smooth_step(float m, float d, float f1, float smoothing)
|
|||
return min(next, f1 + smoothing);
|
||||
}
|
||||
|
||||
// Sites by value: a shader function cannot take a span, so these take vec4 sites[CULTMATH_MAX_SITES] and the count in use, with
|
||||
// count in [0, CULTMATH_MAX_SITES]. A consumer defines CULTMATH_MAX_SITES before including this file to change the capacity; loops
|
||||
// run to count, so the capacity costs uniform space, not iterations. A caller whose sites live in a buffer copies them into a local array first.
|
||||
// The preconditions the C# functions throw for are the caller's in a shader: finite arguments, cellSize > 0, boost in [0, 4],
|
||||
// falloff > 0, smoothing in (0, 1], stretch in [1, 2], p within 2^23 cells. A zero stretchNormal means (1, 0) and needs stretch == 1.
|
||||
// cultmath_weighted_lattice_cells is math.weighted_lattice_cells: the ring pass is bounded by k <= 37 and exits as C# does, and the
|
||||
// box pass recomputes each distance instead of caching it (a 75 x 75 private array is not a shader option), with the same values.
|
||||
#ifndef CULTMATH_MAX_SITES
|
||||
#define CULTMATH_MAX_SITES 128
|
||||
#endif
|
||||
|
||||
vec4 cultmath_power_border(vec3 p, vec4 sites[CULTMATH_MAX_SITES], int count)
|
||||
{
|
||||
CultSiteCells cells = cultmath_site_cells_empty();
|
||||
for (int i = 0; i < count; i++)
|
||||
cells = cultmath_site_cells_insert(cells, cultmath_power_score(p, sites[i]), i);
|
||||
vec4 border = vec4(0.0, 0.0, 0.0, 1.0e30);
|
||||
int owner = int(cells.nearestSite);
|
||||
if (owner < 0)
|
||||
return border;
|
||||
for (int k = 0; k < count; k++)
|
||||
if (k != owner)
|
||||
border = cultmath_power_border_step(p, sites[owner], sites[k], border);
|
||||
return border;
|
||||
}
|
||||
|
||||
float cultmath_lattice_cell_distance(vec2 q, vec2 cell, float time, float drift, vec4 bodies[CULTMATH_MAX_SITES], int count,
|
||||
float cellSize, float boost, float falloff, vec2 normal, float stretch)
|
||||
{
|
||||
vec3 site = cultmath_lattice_site(cell, time, drift);
|
||||
float border = cultmath_power_border(vec3((cell + 0.5) * cellSize, 0.0), bodies, count).w;
|
||||
return cultmath_lattice_distance(q, site.xy, cultmath_lattice_weight(site.z, border, boost, falloff), normal, stretch);
|
||||
}
|
||||
|
||||
vec4 cultmath_weighted_lattice_cells(vec2 p, float time, vec4 bodies[CULTMATH_MAX_SITES], int count, float cellSize, float drift,
|
||||
float boost, float falloff, float smoothing, vec2 stretchNormal, float stretch)
|
||||
{
|
||||
float scale = max(abs(stretchNormal.x), abs(stretchNormal.y));
|
||||
vec2 normal = vec2(1.0, 0.0);
|
||||
if (scale > 0.0)
|
||||
{
|
||||
normal = stretchNormal / scale;
|
||||
normal = normal / length(normal);
|
||||
}
|
||||
vec2 q = p / cellSize;
|
||||
vec2 origin = floor(q);
|
||||
int cx0 = int(origin.x);
|
||||
int cz0 = int(origin.y);
|
||||
float f1 = 1.0e30;
|
||||
float f2 = 1.0e30;
|
||||
int reach = 0;
|
||||
for (int k = 0; k <= 37; k++)
|
||||
{
|
||||
if (k > 1 && 0.5 * float(k - 1) >= max(f2, f1 + 2.0 * smoothing))
|
||||
break;
|
||||
for (int dz = -k; dz <= k; dz++)
|
||||
{
|
||||
int step = dz == -k || dz == k || k == 0 ? 1 : 2 * k;
|
||||
for (int dx = -k; dx <= k; dx += step)
|
||||
{
|
||||
float d = cultmath_lattice_cell_distance(q, vec2(cx0 + dx, cz0 + dz), time, drift, bodies, count, cellSize, boost, falloff, normal, stretch);
|
||||
if (d < f1) { f2 = f1; f1 = d; }
|
||||
else if (d < f2) f2 = d;
|
||||
}
|
||||
}
|
||||
reach = k;
|
||||
}
|
||||
float m = f1 + smoothing;
|
||||
float best = 1.0e30;
|
||||
float second = 1.0e30;
|
||||
int winDx = 0;
|
||||
int winDz = 0;
|
||||
for (int dz = -reach; dz <= reach; dz++)
|
||||
for (int dx = -reach; dx <= reach; dx++)
|
||||
{
|
||||
float d = cultmath_lattice_cell_distance(q, vec2(cx0 + dx, cz0 + dz), time, drift, bodies, count, cellSize, boost, falloff, normal, stretch);
|
||||
if (d < best) { second = best; best = d; winDx = dx; winDz = dz; }
|
||||
else if (d < second) second = d;
|
||||
m = cultmath_lattice_smooth_step(m, d, f1, smoothing);
|
||||
}
|
||||
ivec4 id = cultmath_pcg4d(ivec4(cx0 + winDx, cz0 + winDz, 0, 0));
|
||||
return vec4(best, second, m, float(uint(id.w) >> 8) * (1.0 / 16777216.0));
|
||||
}
|
||||
|
||||
vec2 cultmath_segment_closest(vec2 point_value, vec2 start, vec2 end)
|
||||
{
|
||||
vec2 segment = end - start;
|
||||
|
|
|
|||
|
|
@ -600,14 +600,12 @@ CultCellular cultmath_cellular(float3 p)
|
|||
return result;
|
||||
}
|
||||
|
||||
// Sites: a site is float4(position.xyz, weight). A span is a loop over a per-site step, because a shader
|
||||
// function cannot take a span in the mirror (design.md, "Sites"). The consumer's loop, over its own buffer:
|
||||
// Sites: a site is float4(position.xyz, weight). A shader function cannot take a span (design.md, "Sites"), so a span is a
|
||||
// fixed-capacity array and a count (cultmath_power_border, below), or a loop over a per-site step:
|
||||
// CultSiteCells cells = cultmath_site_cells_empty();
|
||||
// for (int i = 0; i < count; i++)
|
||||
// cells = cultmath_site_cells_insert(cells, cultmath_power_score(p, sites[i]), i);
|
||||
// The exact power cell border is a second loop over the buffer, skipping the owner cells.nearestSite:
|
||||
// float4 border = float4(0.0, 0.0, 0.0, 1.0e30); int owner = (int)cells.nearestSite;
|
||||
// for (int k = 0; k < count; k++) if (k != owner) border = cultmath_power_border_step(p, sites[owner], sites[k], border);
|
||||
// The exact power cell border is cultmath_power_border, the second loop over the sites that skips the owner cells.nearestSite.
|
||||
// The step trusts the owner: a p outside the owner's cell reports distance 0 with that plane's normal, not an error.
|
||||
// Statement for statement with math.Sites.cs (power_score, multiplicative_score, site_cells_*, gabriel_margin).
|
||||
struct CultSiteCells
|
||||
|
|
@ -688,21 +686,8 @@ float cultmath_gabriel_margin(float3 a, float3 b, float3 other)
|
|||
return (cultmath_distance(other, (a + b) * 0.5) - r) / r;
|
||||
}
|
||||
|
||||
// Weighted lattice cells: the four per-cell steps of math.Lattice.cs, statement for statement. A shader function cannot take a
|
||||
// span (design.md, "Sites"), so weighted_lattice_cells is the consumer's loop over its own body buffer, in two passes:
|
||||
// float2 q = p / cellSize; int2 c0 = (int2)floor(q); float f1 = 1.0e30, f2 = 1.0e30; int reach = 0;
|
||||
// for (int k = 0; ; k++) { // ring pass: Chebyshev rings 0, 1, 2, ... around c0, exact exit
|
||||
// if (k > 1 && 0.5 * (k - 1) >= max(f2, f1 + 2.0 * smoothing)) break; // k never reaches 38 in the accepted range
|
||||
// for each cell on ring k: d = cultmath_lattice_distance(q, cultmath_lattice_site(cell, time, drift).xy, w, normal, stretch),
|
||||
// w = cultmath_lattice_weight(site.z, border, boost, falloff), border = the .w of cultmath_power_border_step
|
||||
// folded over the bodies (owner = the nearest body of the cell centre (cell + 0.5) * cellSize, z = 0);
|
||||
// if (d < f1) { f2 = f1; f1 = d; } else if (d < f2) f2 = d; store d for the box pass
|
||||
// reach = k; }
|
||||
// float m = f1 + smoothing; // box pass: every stored cell, rows dz = -reach..reach, then dx = -reach..reach
|
||||
// for each (dz, dx): m = cultmath_lattice_smooth_step(m, d, f1, smoothing); // strict < for the winning id (earlier cell wins ties)
|
||||
// The preconditions the C# function throws for are the caller's in a shader: finite arguments, cellSize > 0, boost in [0, 4],
|
||||
// falloff > 0, smoothing in (0, 1], stretch in [1, 2], p within 2^23 cells. The normal is normalised as C# does: divide by its
|
||||
// largest component, then by its length (a zero normal means (1, 0) and needs stretch == 1). The ring bound is 37.
|
||||
// Weighted lattice cells: the four per-cell steps of math.Lattice.cs, statement for statement; cultmath_weighted_lattice_cells,
|
||||
// after them, is the whole function.
|
||||
float3 cultmath_lattice_site(float2 cell, float time, float drift)
|
||||
{
|
||||
int3 h = cultmath_pcg3d(int3((int)cell.x, (int)cell.y, 0));
|
||||
|
|
@ -734,6 +719,90 @@ float cultmath_lattice_smooth_step(float m, float d, float f1, float smoothing)
|
|||
return min(next, f1 + smoothing);
|
||||
}
|
||||
|
||||
// Sites by value: a shader function cannot take a span, so these take float4 sites[CULTMATH_MAX_SITES] and the count in use, with
|
||||
// count in [0, CULTMATH_MAX_SITES]. A consumer defines CULTMATH_MAX_SITES before including this file to change the capacity; loops
|
||||
// run to count, so the capacity costs uniform space, not iterations. A caller whose sites live in a buffer copies them into a local array first.
|
||||
// The preconditions the C# functions throw for are the caller's in a shader: finite arguments, cellSize > 0, boost in [0, 4],
|
||||
// falloff > 0, smoothing in (0, 1], stretch in [1, 2], p within 2^23 cells. A zero stretchNormal means (1, 0) and needs stretch == 1.
|
||||
// cultmath_weighted_lattice_cells is math.weighted_lattice_cells: the ring pass is bounded by k <= 37 and exits as C# does, and the
|
||||
// box pass recomputes each distance instead of caching it (a 75 x 75 private array is not a shader option), with the same values.
|
||||
#ifndef CULTMATH_MAX_SITES
|
||||
#define CULTMATH_MAX_SITES 128
|
||||
#endif
|
||||
|
||||
float4 cultmath_power_border(float3 p, float4 sites[CULTMATH_MAX_SITES], int count)
|
||||
{
|
||||
CultSiteCells cells = cultmath_site_cells_empty();
|
||||
for (int i = 0; i < count; i++)
|
||||
cells = cultmath_site_cells_insert(cells, cultmath_power_score(p, sites[i]), i);
|
||||
float4 border = float4(0.0, 0.0, 0.0, 1.0e30);
|
||||
int owner = (int)cells.nearestSite;
|
||||
if (owner < 0)
|
||||
return border;
|
||||
for (int k = 0; k < count; k++)
|
||||
if (k != owner)
|
||||
border = cultmath_power_border_step(p, sites[owner], sites[k], border);
|
||||
return border;
|
||||
}
|
||||
|
||||
float cultmath_lattice_cell_distance(float2 q, float2 cell, float time, float drift, float4 bodies[CULTMATH_MAX_SITES], int count,
|
||||
float cellSize, float boost, float falloff, float2 normal, float stretch)
|
||||
{
|
||||
float3 site = cultmath_lattice_site(cell, time, drift);
|
||||
float border = cultmath_power_border(float3((cell + 0.5) * cellSize, 0.0), bodies, count).w;
|
||||
return cultmath_lattice_distance(q, site.xy, cultmath_lattice_weight(site.z, border, boost, falloff), normal, stretch);
|
||||
}
|
||||
|
||||
float4 cultmath_weighted_lattice_cells(float2 p, float time, float4 bodies[CULTMATH_MAX_SITES], int count, float cellSize, float drift,
|
||||
float boost, float falloff, float smoothing, float2 stretchNormal, float stretch)
|
||||
{
|
||||
float scale = max(abs(stretchNormal.x), abs(stretchNormal.y));
|
||||
float2 normal = float2(1.0, 0.0);
|
||||
if (scale > 0.0)
|
||||
{
|
||||
normal = stretchNormal / scale;
|
||||
normal = normal / length(normal);
|
||||
}
|
||||
float2 q = p / cellSize;
|
||||
float2 origin = floor(q);
|
||||
int cx0 = (int)origin.x;
|
||||
int cz0 = (int)origin.y;
|
||||
float f1 = 1.0e30;
|
||||
float f2 = 1.0e30;
|
||||
int reach = 0;
|
||||
for (int k = 0; k <= 37; k++)
|
||||
{
|
||||
if (k > 1 && 0.5 * (float)(k - 1) >= max(f2, f1 + 2.0 * smoothing))
|
||||
break;
|
||||
for (int dz = -k; dz <= k; dz++)
|
||||
{
|
||||
int step = dz == -k || dz == k || k == 0 ? 1 : 2 * k;
|
||||
for (int dx = -k; dx <= k; dx += step)
|
||||
{
|
||||
float d = cultmath_lattice_cell_distance(q, float2(cx0 + dx, cz0 + dz), time, drift, bodies, count, cellSize, boost, falloff, normal, stretch);
|
||||
if (d < f1) { f2 = f1; f1 = d; }
|
||||
else if (d < f2) f2 = d;
|
||||
}
|
||||
}
|
||||
reach = k;
|
||||
}
|
||||
float m = f1 + smoothing;
|
||||
float best = 1.0e30;
|
||||
float second = 1.0e30;
|
||||
int winDx = 0;
|
||||
int winDz = 0;
|
||||
for (int dz = -reach; dz <= reach; dz++)
|
||||
for (int dx = -reach; dx <= reach; dx++)
|
||||
{
|
||||
float d = cultmath_lattice_cell_distance(q, float2(cx0 + dx, cz0 + dz), time, drift, bodies, count, cellSize, boost, falloff, normal, stretch);
|
||||
if (d < best) { second = best; best = d; winDx = dx; winDz = dz; }
|
||||
else if (d < second) second = d;
|
||||
m = cultmath_lattice_smooth_step(m, d, f1, smoothing);
|
||||
}
|
||||
int4 id = cultmath_pcg4d(int4(cx0 + winDx, cz0 + winDz, 0, 0));
|
||||
return float4(best, second, m, (float)((uint)id.w >> 8) * (1.0 / 16777216.0));
|
||||
}
|
||||
|
||||
float2 cultmath_segment_closest(float2 point_value, float2 start, float2 end)
|
||||
{
|
||||
float2 segment = end - start;
|
||||
|
|
|
|||
|
|
@ -14,14 +14,14 @@ namespace CultMath;
|
|||
public struct CultPhasor
|
||||
{
|
||||
/// <summary>(∇cos, cos): the normalized cosine wave and its gradient.</summary>
|
||||
public float4 cos;
|
||||
public float4 cosine;
|
||||
|
||||
/// <summary>(∇sin, sin): the normalized sine wave and its gradient.</summary>
|
||||
public float4 sin;
|
||||
public float4 sine;
|
||||
|
||||
public CultPhasor(float4 cos, float4 sin)
|
||||
public CultPhasor(float4 cosine, float4 sine)
|
||||
{
|
||||
this.cos = cos;
|
||||
this.sin = sin;
|
||||
this.cosine = cosine;
|
||||
this.sine = sine;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ public static partial class math
|
|||
return new float4(best, second, m, cellular_unit(id.w));
|
||||
}
|
||||
|
||||
private static float lattice_cell_distance(
|
||||
internal static float lattice_cell_distance(
|
||||
float2 q, float2 cell, float time, float drift, ReadOnlySpan<float4> bodies, float cellSize, float boost, float falloff,
|
||||
float2 normal, float stretch)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -70,8 +70,9 @@ internal static class GlslLowering
|
|||
var source = hlsl.ReplaceLineEndings("\n");
|
||||
|
||||
// 2. Include guards are dropped, and so are the functions taking Texture2D/SamplerState, exactly
|
||||
// as the C# mirror drops them; the host shader samples its own textures.
|
||||
source = Regex.Replace(source, @"(?m)^[ \t]*#(ifndef|define|endif)\b.*$", string.Empty);
|
||||
// as the C# mirror drops them; the host shader samples its own textures. The capacity macro
|
||||
// (#ifndef CULTMATH_MAX_SITES ... #endif) stays: GLSL has the preprocessor, and a consumer overrides it.
|
||||
source = HlslSourceCompatibilityTests.DropGuards(source, (name, value) => $"#ifndef {name}\n#define {name} {value}\n#endif");
|
||||
source = Regex.Replace(source, @"(?ms)^\w+ \w+\([^)]*\b(Texture2D|SamplerState)\b[^)]*\)\s*\{.*?^\}[ \t]*$", string.Empty);
|
||||
|
||||
// 3. Type names: floatN, intN, uintN and boolN become vecN, ivecN, uvecN and bvecN, and float3x3
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using Xunit;
|
||||
|
||||
namespace CultMath.Tests;
|
||||
|
||||
public sealed partial class GlslMirrorTests
|
||||
{
|
||||
private const string Weighted = "cultmath_weighted_lattice_cells(";
|
||||
private const string SmoothStep = "cultmath_lattice_smooth_step(";
|
||||
|
||||
// Edits of the shader text that each break one rule of the lattice loop. M7 (the exit one ring late) is absent: by the
|
||||
// window-independence proof a later exit visits more cells and changes nothing, so no case can kill it.
|
||||
private static readonly (string Name, string Function, string Old, string New, int Kills)[] LatticeMutants =
|
||||
{
|
||||
("M1 exit one ring early", Weighted, "0.5 * (float)(k - 1) >= max(f2", "0.5 * (float)k >= max(f2", 3),
|
||||
("M2 exit ignores the smoothing band", Weighted, ">= max(f2, f1 + 2.0 * smoothing))", ">= f2)", 1),
|
||||
("M3 no pre-normalisation of the normal", Weighted, "normal = stretchNormal / scale;", "normal = stretchNormal;", 1),
|
||||
("M4 no unit length for the normal", Weighted, "normal = normal / length(normal);", "normal = normal * 1.0;", 1),
|
||||
("M5 smooth step without its clamp", SmoothStep, "return min(next, f1 + smoothing);", "return next;", 1),
|
||||
("M6 box pass skips its first row", Weighted, "for (int dz = -reach; dz <= reach; dz++)", "for (int dz = -reach + 1; dz <= reach; dz++)", 1),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Each rule of the lattice loop is pinned by the committed fixture's own cases: the shader text with one rule broken (a
|
||||
/// mutant mirror, the text a device runs) returns another result than the fixture on at least the stated number of
|
||||
/// cases of the family that exercises it, the exit one ring early on at least three. The device judge holds the same
|
||||
/// cases to their recorded bound, so a mutant these cases separate fails it too.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TheFixtureCasesSeparateEveryLatticeMutantFromTheShader()
|
||||
{
|
||||
var families = FixtureFamilies();
|
||||
foreach (var (name, function, old, replacement, kills) in LatticeMutants)
|
||||
{
|
||||
var (assembly, errors) = HlslSourceCompatibilityTests.CompileShaderMirror(mutate: text =>
|
||||
{
|
||||
Assert.True(text.Split(old).Length == 2, $"{name}: the shader text no longer holds exactly one \"{old}\"");
|
||||
return text.Replace(old, replacement);
|
||||
});
|
||||
Assert.True(assembly is not null, name + ": " + string.Join(Environment.NewLine, errors));
|
||||
var shaderType = assembly!.GetType("CultMathHlsl.HlslShader")!;
|
||||
var shader = Activator.CreateInstance(shaderType);
|
||||
var mirror = shaderType.GetMethod(function[..^1], BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
|
||||
var f = Array.FindIndex(Families, family => family.Glsl.Contains(function));
|
||||
var method = Resolve(Families[f].Glsl);
|
||||
var killed = 0;
|
||||
foreach (var entry in families[f]!["cases"]!.AsArray())
|
||||
{
|
||||
var sides = Sides(entry);
|
||||
if (Hex(new[] { mirror.Invoke(shader, Arguments(method, sides[0]))! }) != sides[1])
|
||||
killed++;
|
||||
}
|
||||
|
||||
output.WriteLine($"{name}: separated on {killed} of {Points} cases of {Families[f].Glsl}");
|
||||
Assert.True(killed >= kills, $"{name}: the {Families[f].Glsl} fixture cases separate it on {killed} of {Points}, fewer than {kills}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,11 @@ public sealed partial class GlslMirrorTests
|
|||
|
||||
public GlslMirrorTests(ITestOutputHelper output) => this.output = output;
|
||||
|
||||
private static bool Write => Environment.GetEnvironmentVariable("CULTMATH_WRITE_GLSL") == "1";
|
||||
// The fixture is written by Yggdrasil's mcr.microsoft.com/dotnet/sdk:10.0 image (Ubuntu 24.04) only: its platform families record
|
||||
// that platform's libm, so a write from another OS would move them.
|
||||
private static bool Write => Environment.GetEnvironmentVariable("CULTMATH_WRITE_GLSL") == "1"
|
||||
? OperatingSystem.IsLinux() ? true : throw new InvalidOperationException("CULTMATH_WRITE_GLSL=1 writes glsl-parity.json, whose platform families belong to Yggdrasil's Ubuntu image (mcr.microsoft.com/dotnet/sdk:10.0): regenerate it there.")
|
||||
: false;
|
||||
|
||||
private static string Root => HlslSourceCompatibilityTests.FindCultMathRoot();
|
||||
|
||||
|
|
@ -497,6 +501,14 @@ static int4 clamp(int4 x, int4 a, int4 b) { if (a.x > b.x || a.y > b.y || a.z >
|
|||
return (int)scalars.Dequeue();
|
||||
if (type == typeof(uint))
|
||||
return scalars.Dequeue();
|
||||
if (type.IsArray)
|
||||
{
|
||||
var array = Array.CreateInstance(type.GetElementType()!, SiteArrays.Capacity);
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
array.SetValue(FromScalars(type.GetElementType()!, scalars), i);
|
||||
return array;
|
||||
}
|
||||
|
||||
var value = Activator.CreateInstance(type)!;
|
||||
foreach (var field in type.GetFields().Where(f => !f.IsStatic))
|
||||
field.SetValue(value, FromScalars(field.FieldType, scalars));
|
||||
|
|
@ -872,14 +884,26 @@ static int4 clamp(int4 x, int4 a, int4 b) { if (a.x > b.x || a.y > b.y || a.z >
|
|||
}),
|
||||
// The smooth step: d exactly at f1 + 2 smoothing one in four (the equal branch), near it one in four, the
|
||||
// smoothing near 0 and at 1, m anywhere up to f1 + smoothing.
|
||||
// One draw in four is the cap point: m = f1 + smoothing exactly and d = f1 + smoothing (1 + U(0.3, 0.9)) with smoothing in
|
||||
// [0.5, 1], where next exceeds f1 + smoothing and the min clamps; a step that returns next reads another function there.
|
||||
new Family("float cultmath_lattice_smooth_step(float, float, float, float)", "ulp-bounded", r =>
|
||||
{
|
||||
if (r.Next(4) == 0)
|
||||
{
|
||||
var cappedSmoothing = Uniform(r, 0.5f, 1.0f);
|
||||
var cappedF1 = Uniform(r, 0.0f, 10.0f);
|
||||
return new object[] { cappedF1 + cappedSmoothing, cappedF1 + cappedSmoothing * (1.0f + Uniform(r, 0.3f, 0.9f)), cappedF1, cappedSmoothing };
|
||||
}
|
||||
|
||||
var smoothing = r.Next(5) switch { 0 => 1.0f, 1 => Uniform(r, 1.0e-3f, 1.0e-2f), _ => Uniform(r, 0.01f, 1.0f) };
|
||||
var f1 = Uniform(r, 0.0f, 10.0f);
|
||||
var edge = f1 + 2.0f * smoothing;
|
||||
var d = r.Next(4) switch { 0 => edge, 1 => edge * Uniform(r, 0.99f, 1.01f), _ => Uniform(r, f1, edge + 1.0f) };
|
||||
return new object[] { Uniform(r, f1, f1 + smoothing), d, f1, smoothing };
|
||||
}),
|
||||
// The site-array functions: a fixed-capacity array and a count, killed on the device (the lattice at rings up to 17).
|
||||
new Family($"vec4 cultmath_power_border(vec3, vec4[{SiteArrays.Capacity}], int)", "ulp-bounded", SiteArrays.PowerBorderArguments),
|
||||
new Family($"vec4 cultmath_weighted_lattice_cells(vec2, float, vec4[{SiteArrays.Capacity}], int, float, float, float, float, float, vec2, float)", "ulp-bounded", SiteArrays.LatticeArguments),
|
||||
}).ToArray();
|
||||
|
||||
// A point of magnitude m, m log-uniform in [lo, hi]: one component at +-m, the other two uniform in [-m, m],
|
||||
|
|
@ -988,6 +1012,7 @@ static int4 clamp(int4 x, int4 a, int4 b) { if (a.x > b.x || a.y > b.y || a.z >
|
|||
["vec4"] = typeof(float4), ["ivec3"] = typeof(int3), ["ivec4"] = typeof(int4),
|
||||
["SpanPath"] = typeof(SpanPath), ["SpanStage"] = typeof(SpanStage),
|
||||
["CultSiteCells"] = typeof(CultSiteCells),
|
||||
[$"vec4[{SiteArrays.Capacity}]"] = typeof(float4[]),
|
||||
};
|
||||
|
||||
// The C# math method a GLSL signature names: cultmath_ dropped, parameter types mapped.
|
||||
|
|
@ -996,6 +1021,7 @@ static int4 clamp(int4 x, int4 a, int4 b) { if (a.x > b.x || a.y > b.y || a.z >
|
|||
var match = Regex.Match(glsl, @"^\w+ cultmath_(\w+)\(([^)]*)\)$");
|
||||
var types = match.Groups[2].Value.Split(", ").Select(t => GlslTypes[t]).ToArray();
|
||||
return typeof(math).GetMethod(match.Groups[1].Value, BindingFlags.Public | BindingFlags.Static, null, types, null)
|
||||
?? typeof(SiteArrays).GetMethod(match.Groups[1].Value, BindingFlags.NonPublic | BindingFlags.Static, null, types, null)
|
||||
?? throw new InvalidOperationException("no C# math counterpart for " + glsl);
|
||||
}
|
||||
|
||||
|
|
@ -1011,6 +1037,8 @@ static int4 clamp(int4 x, int4 a, int4 b) { if (a.x > b.x || a.y > b.y || a.z >
|
|||
// the same read as GLSL's columns.
|
||||
private static IEnumerable<object> Scalars(object value) => value is float or int or uint
|
||||
? new[] { value }
|
||||
: value is Array array
|
||||
? array.Cast<object>().SelectMany(Scalars)
|
||||
: value is float3x3 matrix
|
||||
? new object[] { matrix._m00, matrix._m01, matrix._m02, matrix._m10, matrix._m11, matrix._m12, matrix._m20, matrix._m21, matrix._m22 }
|
||||
: value.GetType().GetFields().Where(f => !f.IsStatic).SelectMany(f => Scalars(f.GetValue(value)!));
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ public sealed class HlslSourceCompatibilityTests
|
|||
shader.GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic, null, parameters, null);
|
||||
|
||||
// Shader constants with no C# twin of the same name (empty when every constant has one).
|
||||
private static readonly string[] ConstantsWithoutTwin = Array.Empty<string>();
|
||||
private static readonly string[] ConstantsWithoutTwin = { "CULTMATH_MAX_SITES" };
|
||||
|
||||
/// <summary>
|
||||
/// Every static const of the compiled HLSL mirror (a static literal CULTMATH_* field, whatever its access) is the
|
||||
|
|
@ -85,7 +85,7 @@ public sealed class HlslSourceCompatibilityTests
|
|||
Assert.True(assembly is not null, string.Join(Environment.NewLine, errors));
|
||||
var shader = assembly!.GetType("CultMathHlsl.HlslShader")!;
|
||||
var constants = shader.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)
|
||||
.Where(f => f.IsLiteral && f.Name.StartsWith("CULTMATH_", StringComparison.Ordinal))
|
||||
.Where(f => f.IsLiteral && f.FieldType == typeof(float) && f.Name.StartsWith("CULTMATH_", StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
Assert.True(constants.Length >= 10, $"only {constants.Length} shader constants were found by reflection");
|
||||
foreach (var constant in constants)
|
||||
|
|
@ -183,7 +183,7 @@ public sealed class HlslSourceCompatibilityTests
|
|||
|
||||
var mismatches = new List<string>();
|
||||
var compared = new HashSet<string>();
|
||||
foreach (var mirror in shaderType.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic).Where(m => m.Name.StartsWith("cultmath_")))
|
||||
foreach (var mirror in shaderType.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic).Where(m => m.Name.StartsWith("cultmath_") && !m.GetParameters().Any(p => p.ParameterType.IsArray)))
|
||||
{
|
||||
var types = mirror.GetParameters().Select(p => p.ParameterType).ToArray();
|
||||
var counterpart = Twin(shaderType, mirror.Name["cultmath_".Length..], types);
|
||||
|
|
@ -204,9 +204,67 @@ public sealed class HlslSourceCompatibilityTests
|
|||
}
|
||||
|
||||
Assert.True(mismatches.Count == 0, $"{mismatches.Count} mismatches:{Environment.NewLine}{string.Join(Environment.NewLine, mismatches.Take(12))}");
|
||||
// The three site-array functions are compared by SiteArrayMirrorsMatchCSharp, which draws their counts and arrays.
|
||||
Assert.Equal(140, compared.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The shader functions that take a site array and a count are bit-equal to C#'s span functions on the same
|
||||
/// arguments: at least 64 compared cases each, counts 0 to the capacity with filler after the count (a loop bound that
|
||||
/// ignores the count reads it), the lattice at rings up to 17. A case C# refuses is skipped and counted.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SiteArrayMirrorsMatchCSharp()
|
||||
{
|
||||
var (assembly, errors) = CompileShaderMirror();
|
||||
Assert.True(assembly is not null, string.Join(Environment.NewLine, errors));
|
||||
var shaderType = assembly!.GetType("CultMathHlsl.HlslShader")!;
|
||||
var shader = Activator.CreateInstance(shaderType);
|
||||
Assert.Equal(17, SiteArrays.FarPoints[0].Ring);
|
||||
Assert.All(SiteArrays.FarPoints, far => Assert.InRange(far.Ring, 8, LatticeMaxRing));
|
||||
|
||||
var draws = new (string Name, Func<System.Random, object[]> Draw)[]
|
||||
{
|
||||
("power_border", SiteArrays.PowerBorderArguments),
|
||||
("lattice_cell_distance", SiteArrays.LatticeCellDistanceArguments),
|
||||
("weighted_lattice_cells", SiteArrays.LatticeArguments),
|
||||
};
|
||||
foreach (var (name, draw) in draws)
|
||||
{
|
||||
var mirror = shaderType.GetMethod("cultmath_" + name, BindingFlags.Instance | BindingFlags.NonPublic)!;
|
||||
var twin = typeof(SiteArrays).GetMethod(name, BindingFlags.Static | BindingFlags.NonPublic)!;
|
||||
Assert.Equal(twin.GetParameters().Select(p => p.ParameterType), mirror.GetParameters().Select(p => p.ParameterType));
|
||||
var random = new System.Random(0x517E);
|
||||
int compared = 0, refused = 0;
|
||||
for (var i = 0; i < 160; i++)
|
||||
{
|
||||
var args = draw(random);
|
||||
object expected;
|
||||
try { expected = twin.Invoke(null, args)!; }
|
||||
catch (TargetInvocationException e) when (e.InnerException is ArgumentException) { refused++; continue; }
|
||||
var actual = Values(mirror.Invoke(shader, args)!);
|
||||
Assert.True(BitwiseEqual(Values(expected), actual),
|
||||
$"{name} case {i}: C# {string.Join(" ", Values(expected).Select(v => v.Value))}, mirror {string.Join(" ", actual.Select(v => v.Value))}");
|
||||
compared++;
|
||||
}
|
||||
|
||||
Assert.True(compared >= 64, $"{name} compared only {compared} cases ({refused} refused)");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>A consumer defines CULTMATH_MAX_SITES before including the library; the mirror compiles with the capacity changed.</summary>
|
||||
[Fact]
|
||||
public void TheSiteCapacityIsOverridable()
|
||||
{
|
||||
var (assembly, errors) = CompileShaderMirror(defines: new Dictionary<string, int> { ["CULTMATH_MAX_SITES"] = 4 });
|
||||
Assert.True(assembly is not null, string.Join(Environment.NewLine, errors));
|
||||
var shaderType = assembly!.GetType("CultMathHlsl.HlslShader")!;
|
||||
Assert.Equal(4, (int)shaderType.GetField("CULTMATH_MAX_SITES", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)!.GetRawConstantValue()!);
|
||||
var sites = new[] { new float4(0, 0, 0, 1), new float4(10, 0, 0, 1), new float4(0, 10, 0, 2), new float4(-10, 0, 0, 1) };
|
||||
var border = (float4)shaderType.GetMethod("cultmath_power_border", BindingFlags.Instance | BindingFlags.NonPublic)!.Invoke(Activator.CreateInstance(shaderType), new object[] { new float3(1, 1, 0), sites, 4 })!;
|
||||
Assert.Equal(power_border(new float3(1, 1, 0), sites), border);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The 1500 arguments (a, b, lo, hi, t0, t1) at which integral_clamp_linear's zero-length rule, its lo
|
||||
/// normalisation and its infinite bounds all bite: a in {-2.5, 0, 1.5}, b in {-1.5, -0, 0, 2}, lo in
|
||||
|
|
@ -682,14 +740,23 @@ public sealed class HlslSourceCompatibilityTests
|
|||
: string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The text without its include-guard lines. A capacity macro (<c>#ifndef X</c>, <c>#define X n</c>, <c>#endif</c>, a
|
||||
/// consumer-overridable default) is not a guard: <paramref name="capacity"/> gives the text it becomes, from its name and value.
|
||||
/// </summary>
|
||||
internal static string DropGuards(string source, Func<string, string, string> capacity) =>
|
||||
Regex.Replace(source, @"(?m)^#ifndef (CULTMATH_MAX_\w+)\r?\n#define \1 (\d+)\r?\n#endif[ \t]*$|^[ \t]*#(ifndef|define|endif)\b.*$",
|
||||
match => match.Groups[1].Success ? capacity(match.Groups[1].Value, match.Groups[2].Value) : string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// The documented HLSL-to-C# transformations, and nothing else. <paramref name="members"/> is C# source a
|
||||
/// test supplies, placed first in the class body: it is not a transformation of the HLSL.
|
||||
/// </summary>
|
||||
internal static string TransformHlslToCSharp(string hlsl, string members = "")
|
||||
internal static string TransformHlslToCSharp(string hlsl, string members = "", IReadOnlyDictionary<string, int>? defines = null)
|
||||
{
|
||||
// 1. Include guards (#ifndef/#define/#endif) are dropped; C# has no textual include.
|
||||
var source = Regex.Replace(hlsl, @"(?m)^\s*#(ifndef|define|endif)\b.*$", string.Empty);
|
||||
// 1. Include guards (#ifndef/#define/#endif) are dropped; C# has no textual include. A capacity macro
|
||||
// (#ifndef X, #define X n, #endif) becomes `const int X = n;`, or the value a test defines for X.
|
||||
var source = DropGuards(hlsl, (name, value) => $"const int {name} = {(defines is not null && defines.TryGetValue(name, out var defined) ? defined : value)};");
|
||||
|
||||
// 2. Functions taking Texture2D/SamplerState resources are dropped; GPU resources have
|
||||
// no CultMath analog.
|
||||
|
|
@ -708,14 +775,19 @@ public sealed class HlslSourceCompatibilityTests
|
|||
source = Regex.Replace(source, @"(?ms)(struct\s+\w+\s*\{)(.*?)(\};)", m =>
|
||||
m.Groups[1].Value + Regex.Replace(m.Groups[2].Value, @"(?m)^(\s*)(\w+\s+\w+;)", "$1public $2") + m.Groups[3].Value);
|
||||
|
||||
// 6. The file body is wrapped in a class; C# has no free functions.
|
||||
// 6. An array parameter `T name[N]` becomes `T[] name`; the mirror takes the array whole and the count beside it.
|
||||
source = Regex.Replace(source, @"\b(\w+) (\w+)\[CULTMATH_MAX_\w+\]", "$1[] $2");
|
||||
|
||||
// 7. The file body is wrapped in a class; C# has no free functions.
|
||||
return "using CultMath;\nusing static CultMath.math;\nnamespace CultMathHlsl;\npublic class HlslShader\n{\n" + members + source + "\n}\n";
|
||||
}
|
||||
|
||||
internal static (Assembly? Assembly, IReadOnlyList<string> Errors) CompileShaderMirror(string members = "")
|
||||
internal static (Assembly? Assembly, IReadOnlyList<string> Errors) CompileShaderMirror(string members = "", IReadOnlyDictionary<string, int>? defines = null, Func<string, string>? mutate = null)
|
||||
{
|
||||
var hlsl = ReadShaderSource(FindCultMathRoot());
|
||||
var tree = CSharpSyntaxTree.ParseText(TransformHlslToCSharp(hlsl, members));
|
||||
if (mutate is not null)
|
||||
hlsl = mutate(hlsl);
|
||||
var tree = CSharpSyntaxTree.ParseText(TransformHlslToCSharp(hlsl, members, defines));
|
||||
var references = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!)
|
||||
.Split(Path.PathSeparator)
|
||||
.Append(typeof(math).Assembly.Location)
|
||||
|
|
|
|||
|
|
@ -9,19 +9,19 @@ namespace CultMath.Tests;
|
|||
/// </summary>
|
||||
public sealed class LatticeCellsTests
|
||||
{
|
||||
private const float Cell = 260.0f;
|
||||
private const float Falloff = 120.0f;
|
||||
private const float Drift = 0.7f;
|
||||
private static readonly float2 Normal = new(0.6f, 0.8f);
|
||||
internal const float Cell = 260.0f;
|
||||
internal const float Falloff = 120.0f;
|
||||
internal const float Drift = 0.7f;
|
||||
internal static readonly float2 Normal = new(0.6f, 0.8f);
|
||||
|
||||
private sealed record Look(float Boost, float Stretch, float Smoothing, float Falloff = LatticeCellsTests.Falloff);
|
||||
internal sealed record Look(float Boost, float Stretch, float Smoothing, float Falloff = LatticeCellsTests.Falloff);
|
||||
|
||||
private static readonly Look[] Looks =
|
||||
internal static readonly Look[] Looks =
|
||||
{
|
||||
new(1.6f, 1.0f, 0.35f), new(1.6f, 2.0f, 0.35f), new(LatticeMaxBoost, LatticeMaxStretch, LatticeMaxSmoothing), new(0.0f, 1.0f, 0.05f),
|
||||
};
|
||||
|
||||
private static float4[] Bodies(float t)
|
||||
internal static float4[] Bodies(float t)
|
||||
{
|
||||
var bodies = new float4[6];
|
||||
for (var i = 0; i < bodies.Length; i++)
|
||||
|
|
@ -41,7 +41,7 @@ public sealed class LatticeCellsTests
|
|||
}
|
||||
|
||||
// Folds F1, F2, id and smooth over the box of the given radius in absolute row-major order.
|
||||
private static float4 BoxFold(float2 p, float time, float4[] bodies, Look look, int radius,
|
||||
internal static float4 BoxFold(float2 p, float time, float4[] bodies, Look look, int radius,
|
||||
Dictionary<(int, int), float> weights, float2? unitNormal = null)
|
||||
{
|
||||
var q = p / Cell;
|
||||
|
|
@ -71,10 +71,10 @@ public sealed class LatticeCellsTests
|
|||
return new float4(f1, f2, m, cellular_unit(id.w));
|
||||
}
|
||||
|
||||
private static float4 Call(float2 p, float time, float4[] bodies, Look look, float2? normal = null) =>
|
||||
internal static float4 Call(float2 p, float time, float4[] bodies, Look look, float2? normal = null) =>
|
||||
weighted_lattice_cells(p, time, bodies, Cell, Drift, look.Boost, look.Falloff, look.Smoothing, normal ?? Normal, look.Stretch);
|
||||
|
||||
private static float2 RandomPoint(System.Random r) =>
|
||||
internal static float2 RandomPoint(System.Random r) =>
|
||||
new((r.NextSingle() * 2.0f - 1.0f) * 1500.0f, (r.NextSingle() * 2.0f - 1.0f) * 1500.0f);
|
||||
|
||||
[Fact]
|
||||
|
|
@ -97,9 +97,9 @@ public sealed class LatticeCellsTests
|
|||
}
|
||||
}
|
||||
|
||||
private delegate bool Stop(int k, float f1, float f2, Look look);
|
||||
internal delegate bool Stop(int k, float f1, float f2, Look look);
|
||||
|
||||
private static readonly (string name, Stop stop)[] Rules =
|
||||
internal static readonly (string name, Stop stop)[] Rules =
|
||||
{
|
||||
("correct", (k, f1, f2, l) => k > 1 && 0.5f * (k - 1) >= max(f2, f1 + 2.0f * l.Smoothing)),
|
||||
("cutoff F1 + k", (k, f1, f2, l) => k > 1 && 0.5f * (k - 1) >= max(f2, f1 + l.Smoothing)),
|
||||
|
|
@ -109,7 +109,7 @@ public sealed class LatticeCellsTests
|
|||
};
|
||||
|
||||
// The radius of the box a search with the given exit rule visits (rings 0 to k - 1 when it stops before ring k).
|
||||
private static int Radius(float2 p, float time, float4[] bodies, Look look, Stop stop,
|
||||
internal static int Radius(float2 p, float time, float4[] bodies, Look look, Stop stop,
|
||||
Dictionary<(int, int), float> weights)
|
||||
{
|
||||
var q = p / Cell;
|
||||
|
|
|
|||
|
|
@ -93,12 +93,12 @@ public sealed class PhacelleTests
|
|||
var actual = math.phacelle(p, side, offset, normalization);
|
||||
var expected = Evaluate(p, side, offset, normalization);
|
||||
|
||||
maxError = Math.Max(maxError, Math.Max(Math.Abs(actual.cos.w - expected.Cos), Math.Abs(actual.sin.w - expected.Sin)));
|
||||
maxError = Math.Max(maxError, Math.Max(Math.Abs(actual.cosine.w - expected.Cos), Math.Abs(actual.sine.w - expected.Sin)));
|
||||
if (expected.RawLength >= 1.0 - normalization + ThresholdBand)
|
||||
{
|
||||
// The output magnitude is 1 wherever the raw magnitude reaches 1 - normalization.
|
||||
above++;
|
||||
var magnitude = Math.Sqrt((double)actual.cos.w * actual.cos.w + (double)actual.sin.w * actual.sin.w);
|
||||
var magnitude = Math.Sqrt((double)actual.cosine.w * actual.cosine.w + (double)actual.sine.w * actual.sine.w);
|
||||
Assert.True(Math.Abs(magnitude - 1.0) < ValueTolerance, $"magnitude {magnitude} above the threshold at {p}");
|
||||
}
|
||||
else if (expected.RawLength <= 1.0 - normalization - ThresholdBand)
|
||||
|
|
@ -130,10 +130,10 @@ public sealed class PhacelleTests
|
|||
var quarter = math.phacelle(p, side, offset + 0.25f, 0.5f);
|
||||
var whole = math.phacelle(p, side, offset + 1.0f, 0.5f);
|
||||
|
||||
Assert.True(MathF.Abs(quarter.cos.w + basePhasor.sin.w) < 2.0e-5f);
|
||||
Assert.True(MathF.Abs(quarter.sin.w - basePhasor.cos.w) < 2.0e-5f);
|
||||
Assert.True(MathF.Abs(whole.cos.w - basePhasor.cos.w) < 2.0e-5f);
|
||||
Assert.True(MathF.Abs(whole.sin.w - basePhasor.sin.w) < 2.0e-5f);
|
||||
Assert.True(MathF.Abs(quarter.cosine.w + basePhasor.sine.w) < 2.0e-5f);
|
||||
Assert.True(MathF.Abs(quarter.sine.w - basePhasor.cosine.w) < 2.0e-5f);
|
||||
Assert.True(MathF.Abs(whole.cosine.w - basePhasor.cosine.w) < 2.0e-5f);
|
||||
Assert.True(MathF.Abs(whole.sine.w - basePhasor.sine.w) < 2.0e-5f);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,9 +166,9 @@ public sealed class PhacelleTests
|
|||
if (distance > 0.0) above++; else below++;
|
||||
|
||||
var analytic = math.phacelle(p, side, offset, normalization);
|
||||
var numericCos = CentralDifference(q => math.phacelle(q, side, offset, normalization).cos.w, p, GradientEps);
|
||||
var numericSin = CentralDifference(q => math.phacelle(q, side, offset, normalization).sin.w, p, GradientEps);
|
||||
maxError = MathF.Max(maxError, MathF.Max(MaxComponentError(numericCos, analytic.cos), MaxComponentError(numericSin, analytic.sin)));
|
||||
var numericCos = CentralDifference(q => math.phacelle(q, side, offset, normalization).cosine.w, p, GradientEps);
|
||||
var numericSin = CentralDifference(q => math.phacelle(q, side, offset, normalization).sine.w, p, GradientEps);
|
||||
maxError = MathF.Max(maxError, MathF.Max(MaxComponentError(numericCos, analytic.cosine), MaxComponentError(numericSin, analytic.sine)));
|
||||
}
|
||||
|
||||
Assert.True(above >= 150 && below >= 150, $"{above} points above and {below} below the threshold");
|
||||
|
|
@ -199,7 +199,7 @@ public sealed class PhacelleTests
|
|||
var flow = math.normalize(tangent);
|
||||
var sideDirection = math.cross(hat, flow);
|
||||
var phasor = math.phacelle(hat * radiusInCells, sideDirection * math.TAU, 0.25f, 0.5f);
|
||||
var gradient = new float3(phasor.cos.x, phasor.cos.y, phasor.cos.z);
|
||||
var gradient = new float3(phasor.cosine.x, phasor.cosine.y, phasor.cosine.z);
|
||||
alongFlow += Math.Abs(math.dot(gradient, flow));
|
||||
acrossFlow += Math.Abs(math.dot(gradient, sideDirection));
|
||||
used++;
|
||||
|
|
@ -232,7 +232,7 @@ public sealed class PhacelleTests
|
|||
{
|
||||
var low = math.phacelle(SetAxis(p, axis, GetAxis(p, axis) - steps[k] * 0.5f), side, offset, 0.5f);
|
||||
var high = math.phacelle(SetAxis(p, axis, GetAxis(p, axis) + steps[k] * 0.5f), side, offset, 0.5f);
|
||||
maxDifference[k] = MathF.Max(maxDifference[k], MathF.Max(MathF.Abs(low.cos.w - high.cos.w), MathF.Abs(low.sin.w - high.sin.w)));
|
||||
maxDifference[k] = MathF.Max(maxDifference[k], MathF.Max(MathF.Abs(low.cosine.w - high.cosine.w), MathF.Abs(low.sine.w - high.sine.w)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -260,8 +260,8 @@ public sealed class PhacelleTests
|
|||
var pruned = math.phacelle(p, side, offset, normalization);
|
||||
var unpruned = math.phacelle(p, side, offset, normalization, false);
|
||||
|
||||
AssertBitEqual(unpruned.cos, pruned.cos);
|
||||
AssertBitEqual(unpruned.sin, pruned.sin);
|
||||
AssertBitEqual(unpruned.cosine, pruned.cosine);
|
||||
AssertBitEqual(unpruned.sine, pruned.sine);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -325,8 +325,8 @@ public sealed class PhacelleTests
|
|||
var pruned = math.phacelle(p, PruneCutSide, PruneCutOffset, PruneCutNormalization);
|
||||
var unpruned = math.phacelle(p, PruneCutSide, PruneCutOffset, PruneCutNormalization, false);
|
||||
|
||||
AssertBitEqual(unpruned.cos, pruned.cos);
|
||||
AssertBitEqual(unpruned.sin, pruned.sin);
|
||||
AssertBitEqual(unpruned.cosine, pruned.cosine);
|
||||
AssertBitEqual(unpruned.sine, pruned.sine);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
160
packages/cultmath/tests/CultMath.Tests/SiteArrays.cs
Normal file
160
packages/cultmath/tests/CultMath.Tests/SiteArrays.cs
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
using CultMath;
|
||||
using static CultMath.math;
|
||||
|
||||
namespace CultMath.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// The shader's site-array functions as C# twins. A shader function cannot take a span, so
|
||||
/// <c>cultmath_power_border</c>, <c>cultmath_lattice_cell_distance</c> and <c>cultmath_weighted_lattice_cells</c> take
|
||||
/// <c>float4 sites[CULTMATH_MAX_SITES]</c> and a count; reflection cannot pass a <see cref="ReadOnlySpan{T}"/>, so each
|
||||
/// twin takes the array and the count and calls the span function on <c>sites[..count]</c>. This table is the one
|
||||
/// place that maps them, read by the mirror test and by the GLSL fixture. The draws are the cases both run.
|
||||
/// </summary>
|
||||
internal static class SiteArrays
|
||||
{
|
||||
/// <summary>The shader's default CULTMATH_MAX_SITES; the device families spell their array with it.</summary>
|
||||
internal const int Capacity = 128;
|
||||
|
||||
internal static float4 power_border(float3 p, float4[] sites, int count) => math.power_border(p, sites.AsSpan(0, count));
|
||||
|
||||
internal static float lattice_cell_distance(
|
||||
float2 q, float2 cell, float time, float drift, float4[] bodies, int count, float cellSize, float boost, float falloff,
|
||||
float2 normal, float stretch) =>
|
||||
math.lattice_cell_distance(q, cell, time, drift, bodies.AsSpan(0, count), cellSize, boost, falloff, normal, stretch);
|
||||
|
||||
internal static float4 weighted_lattice_cells(
|
||||
float2 p, float time, float4[] bodies, int count, float cellSize, float drift, float boost, float falloff,
|
||||
float smoothing, float2 stretchNormal, float stretch) =>
|
||||
math.weighted_lattice_cells(p, time, bodies.AsSpan(0, count), cellSize, drift, boost, falloff, smoothing, stretchNormal, stretch);
|
||||
|
||||
// The array the shader receives: the sites, then filler a loop running past the count would read. The filler is
|
||||
// finite and large, so a loop bound that ignores the count changes the result.
|
||||
private static float4[] Padded(IReadOnlyList<float4> sites)
|
||||
{
|
||||
var array = new float4[Capacity];
|
||||
for (var i = 0; i < Capacity; i++)
|
||||
array[i] = i < sites.Count ? sites[i] : new float4(40.0f * (i + 1), -30.0f * (i + 1), 7.0f, 90.0f + i);
|
||||
return array;
|
||||
}
|
||||
|
||||
private static float Uniform(System.Random r, float lo, float hi) => lo + (hi - lo) * r.NextSingle();
|
||||
|
||||
// Counts 0, 1, 6, 16 and the capacity are drawn often; the rest of [0, Capacity] fills between.
|
||||
private static int Count(System.Random r) => r.Next(3) == 0 ? r.Next(Capacity + 1) : new[] { 0, 1, 6, 16, Capacity }[r.Next(5)];
|
||||
|
||||
// Bodies on slow orbits (float4(position.xy, 0, sigma)), spread over the plane the lattice cells cover.
|
||||
private static float4[] Orbit(System.Random r, int count, float time)
|
||||
{
|
||||
var bodies = new List<float4>();
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var angle = 0.001f * (i + 1) * time + i;
|
||||
var radius = 100.0f + 2400.0f * r.NextSingle();
|
||||
bodies.Add(new float4(radius * MathF.Cos(angle), radius * MathF.Sin(angle), 0.0f, Uniform(r, 20.0f, 300.0f)));
|
||||
}
|
||||
|
||||
return Padded(bodies);
|
||||
}
|
||||
|
||||
internal static object[] PowerBorderArguments(System.Random r)
|
||||
{
|
||||
var count = Count(r);
|
||||
var sites = new List<float4>();
|
||||
for (var i = 0; i < count; i++)
|
||||
sites.Add(new float4(Uniform(r, -1500.0f, 1500.0f), Uniform(r, -1500.0f, 1500.0f), Uniform(r, -200.0f, 200.0f), Uniform(r, 10.0f, 300.0f)));
|
||||
return new object[] { new float3(Uniform(r, -1500.0f, 1500.0f), Uniform(r, -1500.0f, 1500.0f), Uniform(r, -200.0f, 200.0f)), Padded(sites), count };
|
||||
}
|
||||
|
||||
internal static object[] LatticeCellDistanceArguments(System.Random r)
|
||||
{
|
||||
var angle = Uniform(r, 0.0f, 6.2831855f);
|
||||
var look = LatticeCellsTests.Looks[r.Next(LatticeCellsTests.Looks.Length)];
|
||||
var cell = new float2(r.Next(-300, 300), r.Next(-300, 300));
|
||||
var count = Count(r);
|
||||
return new object[]
|
||||
{
|
||||
new float2(Uniform(r, -1500.0f, 1500.0f), Uniform(r, -1500.0f, 1500.0f)) / LatticeCellsTests.Cell, cell, Uniform(r, 0.0f, 200.0f), LatticeCellsTests.Drift,
|
||||
Orbit(r, count, Uniform(r, 0.0f, 200.0f)), count, LatticeCellsTests.Cell, look.Boost, look.Falloff, new float2(MathF.Cos(angle), MathF.Sin(angle)), look.Stretch,
|
||||
};
|
||||
}
|
||||
|
||||
// The unit-less normals the function normalises itself: ordinary, 3-4-5, tiny, huge, and zero (stretch 1 only).
|
||||
private static readonly float2[] Normals = { LatticeCellsTests.Normal, new(3.0f, 4.0f), new(3.0e-30f, -4.0e-30f), new(1.0e30f, 2.0e30f), new(0.0f, 0.0f) };
|
||||
|
||||
internal static readonly LatticeCellsTests.Look WidestLook = new(LatticeMaxBoost, LatticeMaxStretch, LatticeMaxSmoothing, 1.0e6f);
|
||||
|
||||
/// <summary>
|
||||
/// Points whose ring search runs to ring 8 or past it under the widest look (boost, stretch and smoothing at their
|
||||
/// maxima, falloff so wide that every cell is boosted), found once by the radius the exit rule visits, and the
|
||||
/// ring-17 point of LatticeCellsTests.TheCapIsReachedAndMatchesTheFullBox first. Each is (p, time).
|
||||
/// </summary>
|
||||
internal static readonly IReadOnlyList<(float2 P, float Time, int Ring)> FarPoints = FindFarPoints();
|
||||
|
||||
private static List<(float2, float, int)> FindFarPoints()
|
||||
{
|
||||
var found = new List<(float2, float, int)>();
|
||||
var cap = (new float2(1446.7345f, -680.7637f), 97.69269f);
|
||||
found.Add((cap.Item1, cap.Item2, LatticeCellsTests.Radius(cap.Item1, cap.Item2, LatticeCellsTests.Bodies(cap.Item2), WidestLook, LatticeCellsTests.Rules[0].stop, new Dictionary<(int, int), float>())));
|
||||
var r = new System.Random(0x1A77);
|
||||
while (found.Count < 12)
|
||||
{
|
||||
var p = LatticeCellsTests.RandomPoint(r);
|
||||
var time = 13.0f * r.Next(20);
|
||||
var ring = LatticeCellsTests.Radius(p, time, LatticeCellsTests.Bodies(time), WidestLook, LatticeCellsTests.Rules[0].stop, new Dictionary<(int, int), float>());
|
||||
if (ring >= 8)
|
||||
found.Add((p, time, ring));
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Points where an exit one ring early (the rule "bound 0.5*k") returns another result than the exact search, found once by
|
||||
/// running both on the six orbiting bodies under each look: the cases that pin the ring exit to the bit.
|
||||
/// </summary>
|
||||
internal static readonly IReadOnlyList<(float2 P, float Time, LatticeCellsTests.Look Look)> ExitBoundPoints = FindExitBoundPoints();
|
||||
|
||||
private static List<(float2, float, LatticeCellsTests.Look)> FindExitBoundPoints()
|
||||
{
|
||||
var found = new List<(float2, float, LatticeCellsTests.Look)>();
|
||||
var looks = LatticeCellsTests.Looks.Append(WidestLook).ToArray();
|
||||
var r = new System.Random(0xE517);
|
||||
while (found.Count < 24)
|
||||
{
|
||||
var p = LatticeCellsTests.RandomPoint(r);
|
||||
var time = 13.0f * r.Next(20);
|
||||
var look = looks[r.Next(looks.Length)];
|
||||
var bodies = LatticeCellsTests.Bodies(time);
|
||||
var early = LatticeCellsTests.Radius(p, time, bodies, look, LatticeCellsTests.Rules[2].stop, new Dictionary<(int, int), float>());
|
||||
if (early >= LatticeCellsTests.Radius(p, time, bodies, look, LatticeCellsTests.Rules[0].stop, new Dictionary<(int, int), float>()))
|
||||
continue;
|
||||
if (!LatticeCellsTests.BoxFold(p, time, bodies, look, Math.Max(0, early), new Dictionary<(int, int), float>()).Equals(LatticeCellsTests.Call(p, time, bodies, look)))
|
||||
found.Add((p, time, look));
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
internal static object[] LatticeArguments(System.Random r)
|
||||
{
|
||||
switch (r.Next(4))
|
||||
{
|
||||
case 0:
|
||||
var far = FarPoints[r.Next(FarPoints.Count)];
|
||||
return Lattice(far.P, far.Time, Padded(LatticeCellsTests.Bodies(far.Time)), 6, WidestLook, LatticeCellsTests.Normal);
|
||||
case 1:
|
||||
var tight = ExitBoundPoints[r.Next(ExitBoundPoints.Count)];
|
||||
return Lattice(tight.P, tight.Time, Padded(LatticeCellsTests.Bodies(tight.Time)), 6, tight.Look, LatticeCellsTests.Normal);
|
||||
}
|
||||
|
||||
var time = 13.0f * r.Next(20);
|
||||
var normal = Normals[r.Next(Normals.Length)];
|
||||
var look = normal.x == 0.0f ? LatticeCellsTests.Looks[0] : LatticeCellsTests.Looks[r.Next(LatticeCellsTests.Looks.Length)];
|
||||
var count = Count(r);
|
||||
return Lattice(LatticeCellsTests.RandomPoint(r), time, Orbit(r, count, time), count, look, normal);
|
||||
}
|
||||
|
||||
private static object[] Lattice(float2 p, float time, float4[] bodies, int count, LatticeCellsTests.Look look, float2 normal) =>
|
||||
new object[] { p, time, bodies, count, LatticeCellsTests.Cell, LatticeCellsTests.Drift,
|
||||
look.Boost, look.Falloff, look.Smoothing, normal, look.Stretch };
|
||||
}
|
||||
|
|
@ -20,14 +20,14 @@ lavapipe-glsl 15 vec2 cultmath_iv_min(vec2, vec2) ulp-bounded 256 0 0 a324b35cb4
|
|||
lavapipe-glsl 16 vec2 cultmath_iv_max(vec2, vec2) ulp-bounded 256 0 0 d68412262ca234c1da130fc5217ba6856da3e92e435c28c1fbdbf3692a89e1fa
|
||||
lavapipe-glsl 17 vec2 cultmath_iv_sqr(vec2) ulp-bounded 256 0 0 dbbb0e0df015456393452377d689732428a9a8e24d8e2b407ee9620593b4c46b
|
||||
lavapipe-glsl 18 vec2 cultmath_iv_sqrt(vec2) ulp-bounded 256 0 0 95bd15d1b283713fc632ced350f5da1b89f8f73e6618329773db1b52acd29991
|
||||
lavapipe-glsl 19 vec2 cultmath_iv_clamp(vec2, float, float) ulp-bounded 256 0 0 268d61fd6d122e2b85e211c96daf410fb7e2d8ff83ec2082709f640504c852a7
|
||||
lavapipe-glsl 19 vec2 cultmath_iv_clamp(vec2, float, float) ulp-bounded 256 0 0 abe9daf24930a5e3a49ec994c4cae3a0c27b9d019d6d93ec1963ec3daad664a0
|
||||
lavapipe-glsl 20 vec2 cultmath_iv_saturate(vec2) ulp-bounded 256 0 0 0f844ef8c4d9a9f748c80677bbbfc36b5f6e682eb87c2d46e7e20635d5f55c26
|
||||
lavapipe-glsl 21 vec2 cultmath_iv_smoothstep(float, float, vec2) ulp-bounded 256 0 0 3d5944a4c43feb91d6786924e0f5439c05285d28ff1f500b5582893758dd4ce5
|
||||
lavapipe-glsl 22 vec2 cultmath_iv_lerp(vec2, vec2, float) ulp-bounded 256 0 0 96b09e90a3fbbfe81379a29a23b676e490bc7dc6bb731450197707e6cf4c573f
|
||||
lavapipe-glsl 23 vec2 cultmath_iv_snoise_ball(vec3, float) ulp-bounded 242 54 0 570eb031260c8f4aab42edc57a5fa1d0da70e8a4e6f0b4bf68208c15506f7cb5
|
||||
lavapipe-glsl 24 vec2 cultmath_iv_fbm_ball(vec3, float, int, float, float) ulp-bounded 250 56 0 9e7017426314537f84c58dcdd1f85188cbe8f4dd9ababed887f60776cf861ebf
|
||||
lavapipe-glsl 25 vec4 cultmath_iv_frustum_ball(vec2, float, float, float, float) ulp-bounded 198 1.6457063576593662 0 327c306aa4d06aecbb2681d8808c6dbf0567e87b54a6067c68001e36b3e52169
|
||||
lavapipe-glsl 26 vec2 cultmath_iv_exp(vec2) ulp-bounded, platform exp 7 7.526057804152758 0 1a46b0fa116f55e5abd70ec9f2a66de0befe8b3577fe472f9eaa98932aae8040
|
||||
lavapipe-glsl 26 vec2 cultmath_iv_exp(vec2) ulp-bounded, platform exp 7 7.526057804152758 0 b4af8232dc3dd782cc6d790e928ede0f6ba6dfdffd6c71dd9494fb58973e485c
|
||||
lavapipe-glsl 27 CultPhasor cultmath_phacelle(vec3, vec3, float, float) ulp-bounded, platform exp, sin, cos; across platforms |diff| <= 2^-20 max(|v|, 1); on a device |diff| <= 2^-18 max(|v|, 1) 0 18.5 0 705b4c18027a0838218645e9368c3749ae4b5bf75e0485985725e8e72b73ab88
|
||||
lavapipe-glsl 28 float cultmath_snoise(vec3) ulp-bounded, on the simplex tie set 127 112 0 6929326e4f27726d7bbdbac3a7823209e2ecfcd301018ef164527c4c1a9f7ae8
|
||||
lavapipe-glsl 29 vec4 cultmath_snoise_grad(vec3) ulp-bounded, on the simplex tie set 102 546.5 0 6173a812fd4059fa19bce84d34858c6035cdda98aa21a2b210e611cbd58c29d6
|
||||
|
|
@ -36,7 +36,7 @@ lavapipe-glsl 31 vec3 cultmath_af_symbol(float, float) ulp-bounded 256 0 0 04147
|
|||
lavapipe-glsl 32 vec3 cultmath_af_from_iv(vec2) ulp-bounded 256 0 0 306fe58a3268199fc8fd845a32d956f349a790422fa5c87d5a0824636cb55424
|
||||
lavapipe-glsl 33 vec2 cultmath_af_range(vec3) ulp-bounded 256 0 0 6245eeba19473d69d0d8155e20e2e6089a6843140723d23ed5d501064023a44a
|
||||
lavapipe-glsl 34 vec3 cultmath_af_add(vec3, vec3) ulp-bounded 256 0 0 cb1cb3fc69224837342de767525d332687b9d3e17b3de255c28e2f0f65f32640
|
||||
lavapipe-glsl 35 vec3 cultmath_af_sub(vec3, vec3) ulp-bounded 256 0 0 df44c501a8be20dc46a54faac9583da13f4367adc501a33fe14624f10e54b558
|
||||
lavapipe-glsl 35 vec3 cultmath_af_sub(vec3, vec3) ulp-bounded 256 0 0 a8fad8ea86cf61841430293fdd9840a54008160250114040380c9634c63bb17e
|
||||
lavapipe-glsl 36 vec3 cultmath_af_neg(vec3) ulp-bounded 256 0 0 e485acf8ee21c6d880621cc6216b3b2a78318f56f5864b33c5509be27e22a694
|
||||
lavapipe-glsl 37 vec3 cultmath_af_scale(vec3, float) ulp-bounded 256 0 0 8b0c8a540fea82fe058efc42ffa38dbd69850e579cefe5fdbc4c164b715ec15d
|
||||
lavapipe-glsl 38 vec3 cultmath_af_add_iv(vec3, vec2) ulp-bounded 256 0 0 620f747e6ba53014c9714a8dbfaa3ff93f281c80938bd035ef64657e3bdc1a16
|
||||
|
|
@ -50,7 +50,7 @@ lavapipe-glsl 45 vec3 cultmath_af_snoise(vec3, vec3, float) ulp-bounded, reach 1
|
|||
lavapipe-glsl 46 vec3 cultmath_af_fbm(vec3, vec3, float, int, float, float) ulp-bounded, reach 1e-6 to 1e-3 27 102.5 0 42246db4e19a1da20433517cdc7016dda375ea70dc90d1a777679bfd76fb59e8
|
||||
lavapipe-glsl 47 float cultmath_cycles_divide(float, float) ulp-bounded 256 0 0 87f143c57806ad52c3295cde67e97f31e39c8572b8c0a4b8c577e6fcd8ecbebf
|
||||
lavapipe-glsl 48 float cultmath_cycles_power(float, float) ulp-bounded, platform pow 136 4.918926113980705 0 0e62db5ea32fdb9fdd3a9b135a914ddadd3231bdee0ff15c709102b26bb89186
|
||||
lavapipe-glsl 49 float cultmath_cycles_logarithm(float, float) ulp-bounded, platform log 147 4.94622778071426 0 5b2bf1411512c7e5a3f4b335a16fc3ba00ba0784bb87acb14b7d1b9dd551b2f3
|
||||
lavapipe-glsl 49 float cultmath_cycles_logarithm(float, float) ulp-bounded, platform log 146 4.94622778071426 0 5b2bf1411512c7e5a3f4b335a16fc3ba00ba0784bb87acb14b7d1b9dd551b2f3
|
||||
lavapipe-glsl 50 float cultmath_cycles_sqrt(float) ulp-bounded 256 0 0 6d4beb34e93ae304b3d9d30f203fb6c2ba222affc0efc35bb5441528e9062a9a
|
||||
lavapipe-glsl 51 float cultmath_cycles_inverse_sqrt(float) ulp-bounded 256 0 0 71aadd5bd384b9ca2eb89827de53af810a1350b02177de1987e9405e4a6e9ad8
|
||||
lavapipe-glsl 52 float cultmath_cycles_modulo(float, float) ulp-bounded 256 0 0 cf055cafcb5b2d4789a0a2d5cfbc8bc79f52df646b60f5e47db3f994680949e9
|
||||
|
|
@ -96,16 +96,16 @@ lavapipe-glsl 91 vec3 cultmath_cycles_mix_val(float, vec3, vec3) ulp-bounded 256
|
|||
lavapipe-glsl 92 vec3 cultmath_cycles_mix_color(float, vec3, vec3) ulp-bounded 256 0 0 00067b1567bcd6c07a81fa5002f145dfca22437a3f3df5389b2665334d14909b
|
||||
lavapipe-glsl 93 vec3 cultmath_cycles_mix_soft(float, vec3, vec3) ulp-bounded 157 1.9750388068880254 0 ed7c5508ca0543cc20a5f0c5624e75e3ca2a2d6044d481816eeda2edc50c0592
|
||||
lavapipe-glsl 94 vec3 cultmath_cycles_mix_linear(float, vec3, vec3) ulp-bounded 256 0 0 672435cc30bcbbb253cce7316ee0712b1a18f06924bc89cea2d3388dd164a15d
|
||||
lavapipe-glsl 95 float cultmath_cycles_arcsine(float) ulp-bounded, platform asin 155 2734.286809678319 0 f28f3ed0a53ffeebdfa84865d2d3d41590e6b46fc1578d40e1215e2a9e1af585
|
||||
lavapipe-glsl 95 float cultmath_cycles_arcsine(float) ulp-bounded, platform asin 154 2734.286809678319 0 f28f3ed0a53ffeebdfa84865d2d3d41590e6b46fc1578d40e1215e2a9e1af585
|
||||
lavapipe-glsl 96 float cultmath_cycles_arccosine(float) ulp-bounded, platform acos 79 1046.8674409774287 0 22a2c38ab5a232cdbb33cf0c133362581a398f64a4d0d5d6dd89dc5af06bd890
|
||||
lavapipe-glsl 97 float cultmath_cycles_arctan2(float, float) ulp-bounded, platform atan 7 25.5 0 2c60447d80186f6dc9726f19c09fa96ec3f66d8f5688968da533d9195ea463c4
|
||||
lavapipe-glsl 97 float cultmath_cycles_arctan2(float, float) ulp-bounded, platform atan 6 25.5 0 2c60447d80186f6dc9726f19c09fa96ec3f66d8f5688968da533d9195ea463c4
|
||||
lavapipe-glsl 98 mat3 cultmath_snoise_hessian(vec3) ulp-bounded 30 1358.4375 0 62bbafb8420ed5aaa0ce11d1470ca98945e690b186c33a1e7cf4d71478396465
|
||||
lavapipe-glsl 99 mat3 cultmath_snoise_hessian(vec3) ulp-bounded, on the simplex tie set 110 1317.4754224467295 0 0efd9b3a637b46033ed0351f64de274db4d9ce05bb83dd1ce57afb3ceaea26bc
|
||||
lavapipe-glsl 100 float cultmath_integral_clamp_linear(float, float, float, float, float, float) ulp-bounded 256 0 0 84bf9c9a2126b3114a692507cc54f58d313afc33de8a1cac83969cc508433cbe
|
||||
lavapipe-glsl 101 float cultmath_integral_clamp_linear(float, float, float, float, float, float) ulp-bounded, lo above hi and infinite bounds 256 0 0 ff999094d7794aa525e0f9f9a067aa21408a59559aa546cace3b1bb4a8c7f96d
|
||||
lavapipe-glsl 102 float cultmath_integral_clamp_linear(float, float, float, float, float, float) ulp-bounded, zero-length spans and flat infinite clamps 256 0 0 c3cc48f4edcac6801b9223f12d289934916431bdfaea8f68af33a1b833ab9860
|
||||
lavapipe-glsl 103 SpanNoise cultmath_span_fbm(SpanPath, SpanStage, int, float, float) ulp-bounded 7 1012477.3459086476 0 97c531c3a690e056d95e949e243ae74111d4a93a39fd22c0358a103b3bb14bc1
|
||||
lavapipe-glsl 104 SpanNoise cultmath_span_ridged(SpanPath, SpanStage, int, float, float) ulp-bounded 8 948534.9108393334 0 c5d55a1d05403f6a1a7b7946c3a4c9556277f217fc660d338b3894efbb333e43
|
||||
lavapipe-glsl 104 SpanNoise cultmath_span_ridged(SpanPath, SpanStage, int, float, float) ulp-bounded 8 948534.9108393334 0 54fd229d69439ff3318690b7802143f8410dc7ece30ad0d56ccc4b68bf39bec2
|
||||
lavapipe-glsl 105 vec4 cultmath_snoise_grad(vec3) ulp-bounded, coordinate bands 1e-4 to 1e2 116 249.5 0 fff9658a8ec055766c9327ee9b680cf7fb83736c472ae90a11e50d6d2a053fdc
|
||||
lavapipe-glsl 106 vec4 cultmath_snoise_grad(vec3) ulp-bounded, coordinate bands 1e2 to 4e6 70 11866336.625 0 833484714ebcea59d86e0fbc2eaa4ae247c29541703763989e6758547dbf6db8
|
||||
lavapipe-glsl 107 mat3 cultmath_snoise_hessian(vec3) ulp-bounded, coordinate bands 1e-4 to 1e2 120 980.7417469278579 0 3ea43dabe0091ea8e994c1dfc65c7274ed38b9343d4d1d06f7caa09c806c91df
|
||||
|
|
@ -117,13 +117,15 @@ lavapipe-glsl 112 vec3 cultmath_segment_closest(vec3, vec3, vec3) ulp-bounded 21
|
|||
lavapipe-glsl 113 vec4 cultmath_power_border_step(vec3, vec4, vec4, vec4) ulp-bounded 214 43 0 fdfdff73434a146489b002af5e80663658b2dd7b86105a7e65bdab755ab54a5e
|
||||
lavapipe-glsl 114 vec4 cultmath_power_score(vec3, vec4) ulp-bounded 207 1.4619722193127032 0 dc1aa639ae1dc44003a0f5d8c7c99b2e3d7f51b158c96b3c3bd7a9f7df6d927e
|
||||
lavapipe-glsl 115 vec4 cultmath_multiplicative_score(vec3, vec4) ulp-bounded 234 1.5529504197086745 0 ee1af26ac82489e8d363e7a6b6b59da7dacfb397d4bc9a723dd68607a69da41a
|
||||
lavapipe-glsl 116 CultSiteCells cultmath_site_cells_insert(CultSiteCells, vec4, int) ulp-bounded 256 0 0 b8687b139aaa865e9f28fbc84fe37dd4a1d4873d5ff8e2074922c07f56551576
|
||||
lavapipe-glsl 116 CultSiteCells cultmath_site_cells_insert(CultSiteCells, vec4, int) ulp-bounded 256 0 0 bfad8b369011cfbf14644fec68ce65620694fcbf3cd3804f54953d55587313fc
|
||||
lavapipe-glsl 117 vec4 cultmath_site_cells_border(CultSiteCells) ulp-bounded 228 1.069496760346883 0 d7b478bef7e6f80902d0b5b255cb4fca30757ff6b9d25dac2030b037a8d342ce
|
||||
lavapipe-glsl 118 float cultmath_gabriel_margin(vec3, vec3, vec3) ulp-bounded 201 2 0 6a80fbef486de496a08b7c8e7f9f65e2cc0f5b9f93abe50c43e84ad88d1ed034
|
||||
lavapipe-glsl 119 vec3 cultmath_lattice_site(vec2, float, float) ulp-bounded 256 0 0 84df739c336796638dcf0a99c328a4b8c150c25d80e02248d4b6d023dc13161f
|
||||
lavapipe-glsl 120 float cultmath_lattice_weight(float, float, float, float) ulp-bounded 231 2.0671110200639147 0 434cdf5603abc2942aa624b9d2e98d5f1231706c86180a80439bc8b72bdd066e
|
||||
lavapipe-glsl 121 float cultmath_lattice_distance(vec2, vec2, float, vec2, float) ulp-bounded 256 0 0 622d0e1ea0fcf0e2700558dbdaea92b8ce02e44048dbf35944c23b5088c3e40c
|
||||
lavapipe-glsl 122 float cultmath_lattice_smooth_step(float, float, float, float) ulp-bounded 256 0 0 e6c9aeb52c5e63ebf3c8525dd39b5b88d5f890fd4f23f4894a92232807a3d94e
|
||||
lavapipe-glsl 121 float cultmath_lattice_distance(vec2, vec2, float, vec2, float) ulp-bounded 256 0 0 ba3fa3bdb4f5f5b2478a4ddfb2e08f2f425e997778a716884e43b13d6ed050b5
|
||||
lavapipe-glsl 122 float cultmath_lattice_smooth_step(float, float, float, float) ulp-bounded 256 0 0 6cbeddde536b12e83873f2e5ac60d16bef22049d2a7dea61af0ff1e0d5b84740
|
||||
lavapipe-glsl 123 vec4 cultmath_power_border(vec3, vec4[128], int) ulp-bounded 188 65.72121683891783 0 8427c2b99109a4f59ff745fdb87f261deb1cbae34ce99bc1ae05ebb7f9963b4d
|
||||
lavapipe-glsl 124 vec4 cultmath_weighted_lattice_cells(vec2, float, vec4[128], int, float, float, float, float, float, vec2, float) ulp-bounded 179 3 0 fd3b58c4e2ba8a7bf801a1e6cdff82468abcb368442bc98b2583fda9d2e6f823
|
||||
lavapipe-hlsl 0 float cultmath_snoise(vec3) ulp-bounded 94 122.6875 0 5d41c14241a13124f2e1b49f230f5df0c48d0d69fb1be9ef2f33dc1702bf0b0a
|
||||
lavapipe-hlsl 1 float cultmath_snoise(vec2) ulp-bounded 204 1 0 81d9c62726afdccbbe4ffef173b5c92731eb6bf5927b3979d653e50dbd5d4064
|
||||
lavapipe-hlsl 2 vec4 cultmath_snoise_grad(vec3) ulp-bounded 71 617.5 0 f7e9ed1707cdb015efaba0e0150193309da0d4c125349518f8b7c60a17b25790
|
||||
|
|
@ -143,14 +145,14 @@ lavapipe-hlsl 15 vec2 cultmath_iv_min(vec2, vec2) ulp-bounded 256 0 0 a324b35cb4
|
|||
lavapipe-hlsl 16 vec2 cultmath_iv_max(vec2, vec2) ulp-bounded 256 0 0 d68412262ca234c1da130fc5217ba6856da3e92e435c28c1fbdbf3692a89e1fa
|
||||
lavapipe-hlsl 17 vec2 cultmath_iv_sqr(vec2) ulp-bounded 256 0 0 dbbb0e0df015456393452377d689732428a9a8e24d8e2b407ee9620593b4c46b
|
||||
lavapipe-hlsl 18 vec2 cultmath_iv_sqrt(vec2) ulp-bounded 256 0 0 95bd15d1b283713fc632ced350f5da1b89f8f73e6618329773db1b52acd29991
|
||||
lavapipe-hlsl 19 vec2 cultmath_iv_clamp(vec2, float, float) ulp-bounded 256 0 0 268d61fd6d122e2b85e211c96daf410fb7e2d8ff83ec2082709f640504c852a7
|
||||
lavapipe-hlsl 19 vec2 cultmath_iv_clamp(vec2, float, float) ulp-bounded 256 0 0 abe9daf24930a5e3a49ec994c4cae3a0c27b9d019d6d93ec1963ec3daad664a0
|
||||
lavapipe-hlsl 20 vec2 cultmath_iv_saturate(vec2) ulp-bounded 256 0 0 0f844ef8c4d9a9f748c80677bbbfc36b5f6e682eb87c2d46e7e20635d5f55c26
|
||||
lavapipe-hlsl 21 vec2 cultmath_iv_smoothstep(float, float, vec2) ulp-bounded 256 0 0 3d5944a4c43feb91d6786924e0f5439c05285d28ff1f500b5582893758dd4ce5
|
||||
lavapipe-hlsl 22 vec2 cultmath_iv_lerp(vec2, vec2, float) ulp-bounded 256 0 0 96b09e90a3fbbfe81379a29a23b676e490bc7dc6bb731450197707e6cf4c573f
|
||||
lavapipe-hlsl 23 vec2 cultmath_iv_snoise_ball(vec3, float) ulp-bounded 244 54 0 f36530a4cd8c9fdc1c73e7ddb6782cd11cbf8493e2ee7b0656e818812504267e
|
||||
lavapipe-hlsl 24 vec2 cultmath_iv_fbm_ball(vec3, float, int, float, float) ulp-bounded 252 75 0 a1677ea42ce91a5d2b6f83fa7c44879b7a184ed4631108d7a1c6e710bf26ff02
|
||||
lavapipe-hlsl 25 vec4 cultmath_iv_frustum_ball(vec2, float, float, float, float) ulp-bounded 198 1.6457063576593662 0 327c306aa4d06aecbb2681d8808c6dbf0567e87b54a6067c68001e36b3e52169
|
||||
lavapipe-hlsl 26 vec2 cultmath_iv_exp(vec2) ulp-bounded, platform exp 7 7.526057804152758 0 1a46b0fa116f55e5abd70ec9f2a66de0befe8b3577fe472f9eaa98932aae8040
|
||||
lavapipe-hlsl 26 vec2 cultmath_iv_exp(vec2) ulp-bounded, platform exp 7 7.526057804152758 0 b4af8232dc3dd782cc6d790e928ede0f6ba6dfdffd6c71dd9494fb58973e485c
|
||||
lavapipe-hlsl 27 CultPhasor cultmath_phacelle(vec3, vec3, float, float) ulp-bounded, platform exp, sin, cos; across platforms |diff| <= 2^-20 max(|v|, 1); on a device |diff| <= 2^-18 max(|v|, 1) 0 18.5 0 705b4c18027a0838218645e9368c3749ae4b5bf75e0485985725e8e72b73ab88
|
||||
lavapipe-hlsl 28 float cultmath_snoise(vec3) ulp-bounded, on the simplex tie set 144 99 0 be76512b18eb678194dad0c4726c0aef9ba34012b60b79d24c2df27989707145
|
||||
lavapipe-hlsl 29 vec4 cultmath_snoise_grad(vec3) ulp-bounded, on the simplex tie set 111 546.5 0 124d4be07cbb66edab644e649c081cb7c8be5d99b6c1d84fec5d7ef65c76ed0f
|
||||
|
|
@ -159,7 +161,7 @@ lavapipe-hlsl 31 vec3 cultmath_af_symbol(float, float) ulp-bounded 256 0 0 04147
|
|||
lavapipe-hlsl 32 vec3 cultmath_af_from_iv(vec2) ulp-bounded 256 0 0 306fe58a3268199fc8fd845a32d956f349a790422fa5c87d5a0824636cb55424
|
||||
lavapipe-hlsl 33 vec2 cultmath_af_range(vec3) ulp-bounded 256 0 0 6245eeba19473d69d0d8155e20e2e6089a6843140723d23ed5d501064023a44a
|
||||
lavapipe-hlsl 34 vec3 cultmath_af_add(vec3, vec3) ulp-bounded 256 0 0 cb1cb3fc69224837342de767525d332687b9d3e17b3de255c28e2f0f65f32640
|
||||
lavapipe-hlsl 35 vec3 cultmath_af_sub(vec3, vec3) ulp-bounded 256 0 0 df44c501a8be20dc46a54faac9583da13f4367adc501a33fe14624f10e54b558
|
||||
lavapipe-hlsl 35 vec3 cultmath_af_sub(vec3, vec3) ulp-bounded 256 0 0 a8fad8ea86cf61841430293fdd9840a54008160250114040380c9634c63bb17e
|
||||
lavapipe-hlsl 36 vec3 cultmath_af_neg(vec3) ulp-bounded 256 0 0 e485acf8ee21c6d880621cc6216b3b2a78318f56f5864b33c5509be27e22a694
|
||||
lavapipe-hlsl 37 vec3 cultmath_af_scale(vec3, float) ulp-bounded 256 0 0 8b0c8a540fea82fe058efc42ffa38dbd69850e579cefe5fdbc4c164b715ec15d
|
||||
lavapipe-hlsl 38 vec3 cultmath_af_add_iv(vec3, vec2) ulp-bounded 256 0 0 620f747e6ba53014c9714a8dbfaa3ff93f281c80938bd035ef64657e3bdc1a16
|
||||
|
|
@ -173,7 +175,7 @@ lavapipe-hlsl 45 vec3 cultmath_af_snoise(vec3, vec3, float) ulp-bounded, reach 1
|
|||
lavapipe-hlsl 46 vec3 cultmath_af_fbm(vec3, vec3, float, int, float, float) ulp-bounded, reach 1e-6 to 1e-3 24 122.75 0 7f941b1f0c76d9f2121027a555ae7f2a741cc164402f0df75237198ac97f850b
|
||||
lavapipe-hlsl 47 float cultmath_cycles_divide(float, float) ulp-bounded 256 0 0 87f143c57806ad52c3295cde67e97f31e39c8572b8c0a4b8c577e6fcd8ecbebf
|
||||
lavapipe-hlsl 48 float cultmath_cycles_power(float, float) ulp-bounded, platform pow 136 4.918926113980705 0 0e62db5ea32fdb9fdd3a9b135a914ddadd3231bdee0ff15c709102b26bb89186
|
||||
lavapipe-hlsl 49 float cultmath_cycles_logarithm(float, float) ulp-bounded, platform log 147 4.94622778071426 0 5b2bf1411512c7e5a3f4b335a16fc3ba00ba0784bb87acb14b7d1b9dd551b2f3
|
||||
lavapipe-hlsl 49 float cultmath_cycles_logarithm(float, float) ulp-bounded, platform log 146 4.94622778071426 0 5b2bf1411512c7e5a3f4b335a16fc3ba00ba0784bb87acb14b7d1b9dd551b2f3
|
||||
lavapipe-hlsl 50 float cultmath_cycles_sqrt(float) ulp-bounded 256 0 0 6d4beb34e93ae304b3d9d30f203fb6c2ba222affc0efc35bb5441528e9062a9a
|
||||
lavapipe-hlsl 51 float cultmath_cycles_inverse_sqrt(float) ulp-bounded 256 0 0 71aadd5bd384b9ca2eb89827de53af810a1350b02177de1987e9405e4a6e9ad8
|
||||
lavapipe-hlsl 52 float cultmath_cycles_modulo(float, float) ulp-bounded 256 0 0 cf055cafcb5b2d4789a0a2d5cfbc8bc79f52df646b60f5e47db3f994680949e9
|
||||
|
|
@ -219,16 +221,16 @@ lavapipe-hlsl 91 vec3 cultmath_cycles_mix_val(float, vec3, vec3) ulp-bounded 256
|
|||
lavapipe-hlsl 92 vec3 cultmath_cycles_mix_color(float, vec3, vec3) ulp-bounded 256 0 0 00067b1567bcd6c07a81fa5002f145dfca22437a3f3df5389b2665334d14909b
|
||||
lavapipe-hlsl 93 vec3 cultmath_cycles_mix_soft(float, vec3, vec3) ulp-bounded 157 1.9750388068880254 0 ed7c5508ca0543cc20a5f0c5624e75e3ca2a2d6044d481816eeda2edc50c0592
|
||||
lavapipe-hlsl 94 vec3 cultmath_cycles_mix_linear(float, vec3, vec3) ulp-bounded 256 0 0 672435cc30bcbbb253cce7316ee0712b1a18f06924bc89cea2d3388dd164a15d
|
||||
lavapipe-hlsl 95 float cultmath_cycles_arcsine(float) ulp-bounded, platform asin 155 2734.286809678319 0 f28f3ed0a53ffeebdfa84865d2d3d41590e6b46fc1578d40e1215e2a9e1af585
|
||||
lavapipe-hlsl 95 float cultmath_cycles_arcsine(float) ulp-bounded, platform asin 154 2734.286809678319 0 f28f3ed0a53ffeebdfa84865d2d3d41590e6b46fc1578d40e1215e2a9e1af585
|
||||
lavapipe-hlsl 96 float cultmath_cycles_arccosine(float) ulp-bounded, platform acos 79 1046.8674409774287 0 22a2c38ab5a232cdbb33cf0c133362581a398f64a4d0d5d6dd89dc5af06bd890
|
||||
lavapipe-hlsl 97 float cultmath_cycles_arctan2(float, float) ulp-bounded, platform atan 7 25.5 0 2c60447d80186f6dc9726f19c09fa96ec3f66d8f5688968da533d9195ea463c4
|
||||
lavapipe-hlsl 97 float cultmath_cycles_arctan2(float, float) ulp-bounded, platform atan 6 25.5 0 2c60447d80186f6dc9726f19c09fa96ec3f66d8f5688968da533d9195ea463c4
|
||||
lavapipe-hlsl 98 mat3 cultmath_snoise_hessian(vec3) ulp-bounded 33 2377.8799856935966 0 a64853839f394cc7c367ba5e107caa8bb396f07cf202263aa917095dd1c939f7
|
||||
lavapipe-hlsl 99 mat3 cultmath_snoise_hessian(vec3) ulp-bounded, on the simplex tie set 132 1317.4754224467295 0 9066a49e50731881583ffca16c949171bb61578a1dfd497c54dafe773ab49488
|
||||
lavapipe-hlsl 100 float cultmath_integral_clamp_linear(float, float, float, float, float, float) ulp-bounded 256 0 0 84bf9c9a2126b3114a692507cc54f58d313afc33de8a1cac83969cc508433cbe
|
||||
lavapipe-hlsl 101 float cultmath_integral_clamp_linear(float, float, float, float, float, float) ulp-bounded, lo above hi and infinite bounds 256 0 0 ff999094d7794aa525e0f9f9a067aa21408a59559aa546cace3b1bb4a8c7f96d
|
||||
lavapipe-hlsl 102 float cultmath_integral_clamp_linear(float, float, float, float, float, float) ulp-bounded, zero-length spans and flat infinite clamps 256 0 0 c3cc48f4edcac6801b9223f12d289934916431bdfaea8f68af33a1b833ab9860
|
||||
lavapipe-hlsl 103 SpanNoise cultmath_span_fbm(SpanPath, SpanStage, int, float, float) ulp-bounded 3 1024946.5723514018 0 f6395b9f2609d914894c35b442719be3cc3e11d07c5f0bae5a8719651e60cd4e
|
||||
lavapipe-hlsl 104 SpanNoise cultmath_span_ridged(SpanPath, SpanStage, int, float, float) ulp-bounded 7 948534.9108393334 0 bfda3536007373bdbef75a5c7bb63a7192f1c90b082984588f23e5b4eb767867
|
||||
lavapipe-hlsl 104 SpanNoise cultmath_span_ridged(SpanPath, SpanStage, int, float, float) ulp-bounded 7 948534.9108393334 0 2adfdc4e6c81ee0bc7bb7869c55207f5033b4176104bc36dce1528ae0a5fe39b
|
||||
lavapipe-hlsl 105 vec4 cultmath_snoise_grad(vec3) ulp-bounded, coordinate bands 1e-4 to 1e2 119 423.1430718408491 0 a94c38d1baa651a6faa510920cbe9d1f9528c0adb863ba6e35d835588ad00a58
|
||||
lavapipe-hlsl 106 vec4 cultmath_snoise_grad(vec3) ulp-bounded, coordinate bands 1e2 to 4e6 73 13116827.1875 0 9ad0fd577b26aa717534c0693df97ee41bcded0768f442739b98f505e4a2a430
|
||||
lavapipe-hlsl 107 mat3 cultmath_snoise_hessian(vec3) ulp-bounded, coordinate bands 1e-4 to 1e2 122 980.7417469278579 0 0977fa6f446e4f916a9b938ce24ea32df6aa912dc991c182014d309dc0efb1cb
|
||||
|
|
@ -240,10 +242,12 @@ lavapipe-hlsl 112 vec3 cultmath_segment_closest(vec3, vec3, vec3) ulp-bounded 21
|
|||
lavapipe-hlsl 113 vec4 cultmath_power_border_step(vec3, vec4, vec4, vec4) ulp-bounded 214 43 0 fdfdff73434a146489b002af5e80663658b2dd7b86105a7e65bdab755ab54a5e
|
||||
lavapipe-hlsl 114 vec4 cultmath_power_score(vec3, vec4) ulp-bounded 207 1.4619722193127032 0 dc1aa639ae1dc44003a0f5d8c7c99b2e3d7f51b158c96b3c3bd7a9f7df6d927e
|
||||
lavapipe-hlsl 115 vec4 cultmath_multiplicative_score(vec3, vec4) ulp-bounded 234 1.5529504197086745 0 ee1af26ac82489e8d363e7a6b6b59da7dacfb397d4bc9a723dd68607a69da41a
|
||||
lavapipe-hlsl 116 CultSiteCells cultmath_site_cells_insert(CultSiteCells, vec4, int) ulp-bounded 256 0 0 b8687b139aaa865e9f28fbc84fe37dd4a1d4873d5ff8e2074922c07f56551576
|
||||
lavapipe-hlsl 116 CultSiteCells cultmath_site_cells_insert(CultSiteCells, vec4, int) ulp-bounded 256 0 0 bfad8b369011cfbf14644fec68ce65620694fcbf3cd3804f54953d55587313fc
|
||||
lavapipe-hlsl 117 vec4 cultmath_site_cells_border(CultSiteCells) ulp-bounded 228 1.069496760346883 0 d7b478bef7e6f80902d0b5b255cb4fca30757ff6b9d25dac2030b037a8d342ce
|
||||
lavapipe-hlsl 118 float cultmath_gabriel_margin(vec3, vec3, vec3) ulp-bounded 201 2 0 6a80fbef486de496a08b7c8e7f9f65e2cc0f5b9f93abe50c43e84ad88d1ed034
|
||||
lavapipe-hlsl 119 vec3 cultmath_lattice_site(vec2, float, float) ulp-bounded 256 0 0 84df739c336796638dcf0a99c328a4b8c150c25d80e02248d4b6d023dc13161f
|
||||
lavapipe-hlsl 120 float cultmath_lattice_weight(float, float, float, float) ulp-bounded 231 2.0671110200639147 0 434cdf5603abc2942aa624b9d2e98d5f1231706c86180a80439bc8b72bdd066e
|
||||
lavapipe-hlsl 121 float cultmath_lattice_distance(vec2, vec2, float, vec2, float) ulp-bounded 256 0 0 622d0e1ea0fcf0e2700558dbdaea92b8ce02e44048dbf35944c23b5088c3e40c
|
||||
lavapipe-hlsl 122 float cultmath_lattice_smooth_step(float, float, float, float) ulp-bounded 256 0 0 e6c9aeb52c5e63ebf3c8525dd39b5b88d5f890fd4f23f4894a92232807a3d94e
|
||||
lavapipe-hlsl 121 float cultmath_lattice_distance(vec2, vec2, float, vec2, float) ulp-bounded 256 0 0 ba3fa3bdb4f5f5b2478a4ddfb2e08f2f425e997778a716884e43b13d6ed050b5
|
||||
lavapipe-hlsl 122 float cultmath_lattice_smooth_step(float, float, float, float) ulp-bounded 256 0 0 6cbeddde536b12e83873f2e5ac60d16bef22049d2a7dea61af0ff1e0d5b84740
|
||||
lavapipe-hlsl 123 vec4 cultmath_power_border(vec3, vec4[128], int) ulp-bounded 188 65.72121683891783 0 8427c2b99109a4f59ff745fdb87f261deb1cbae34ce99bc1ae05ebb7f9963b4d
|
||||
lavapipe-hlsl 124 vec4 cultmath_weighted_lattice_cells(vec2, float, vec4[128], int, float, float, float, float, float, vec2, float) ulp-bounded 179 3 0 fd3b58c4e2ba8a7bf801a1e6cdff82468abcb368442bc98b2583fda9d2e6f823
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -16,6 +16,7 @@ import sys
|
|||
HLSL_VEC = re.compile(r'(float|int|uint)([234])$')
|
||||
GLSL_VEC = re.compile(r'(i|u|)vec([234])$')
|
||||
KIND = {'float': 'f', 'int': 'i', 'uint': 'u'}
|
||||
ARRAY = re.compile(r'(\w+)\[(\d+)\]$') # an array leaf, vec4[128]: N elements of one leaf type, N x its words
|
||||
|
||||
|
||||
def structs(root):
|
||||
|
|
@ -79,6 +80,19 @@ def body(sig, n, win, wout, defs, glsl):
|
|||
ret, name, params = m.group(1), m.group(2), [p.strip() for p in m.group(3).split(',') if p.strip()]
|
||||
lines, offset, args = [], 0, []
|
||||
for j, p in enumerate(params):
|
||||
array = ARRAY.match(p)
|
||||
if array:
|
||||
elem, size = array.group(1), int(array.group(2))
|
||||
words = leaves(elem, defs)
|
||||
lines.append(' %s a%d[%d];' % (elem if glsl else hlsl_type(elem), j, size))
|
||||
lines.append(' for (uint e = 0u; e < %du; e++) {' % size)
|
||||
for k, leaf in enumerate(words):
|
||||
word = ('In.a[b + %du + e * %du]' if glsl else 'In[b + %du + e * %du]') % (offset + k, len(words))
|
||||
lines.append(' a%d[e]%s = %s;' % (j, access(leaf, glsl), read(leaf[1], glsl, word)))
|
||||
lines.append(' }')
|
||||
offset += size * len(words)
|
||||
args.append('a%d' % j)
|
||||
continue
|
||||
ptype = p if glsl else hlsl_type(p)
|
||||
lines.append(' %s a%d;' % (ptype, j))
|
||||
for leaf in leaves(p, defs):
|
||||
|
|
|
|||
|
|
@ -5,6 +5,15 @@ All notable changes to this package are documented in this file.
|
|||
Earlier releases (0.2.0-0.2.3) predate this package's changelog and are not
|
||||
backfilled here; see `docs/semver-policy.md` for why.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Breaking
|
||||
|
||||
- `CultPhasor.cos` and `.sin` are now `.cosine` and `.sine` in C#, HLSL and GLSL,
|
||||
constructor parameters included, because SPIRV-Cross renames members that match
|
||||
GLSL builtins. This is a minor bump on 0.y.z, so the next cultmath-unity release
|
||||
is 0.5.0.
|
||||
|
||||
## [0.4.1]
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@
|
|||
// against CultMath.CultPhasor, same field order.
|
||||
struct CultPhasor
|
||||
{
|
||||
float4 cos;
|
||||
float4 sin;
|
||||
float4 cosine;
|
||||
float4 sine;
|
||||
};
|
||||
|
||||
// Johansen's Phacelle noise on 3D cells. See math.phacelle's comment (math.Phacelle.cs) for the stripe wave
|
||||
|
|
@ -84,13 +84,13 @@ CultPhasor cultmath_phacelle(float3 p, float3 side, float offset, float normaliz
|
|||
float outCos = rawCos / rawLength;
|
||||
float outSin = rawSin / rawLength;
|
||||
float3 gradLength = (rawCos * gradRawCos + rawSin * gradRawSin) / rawLength;
|
||||
result.cos = float4((gradRawCos - outCos * gradLength) / rawLength, outCos);
|
||||
result.sin = float4((gradRawSin - outSin * gradLength) / rawLength, outSin);
|
||||
result.cosine = float4((gradRawCos - outCos * gradLength) / rawLength, outCos);
|
||||
result.sine = float4((gradRawSin - outSin * gradLength) / rawLength, outSin);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.cos = float4(gradRawCos / floorLength, rawCos / floorLength);
|
||||
result.sin = float4(gradRawSin / floorLength, rawSin / floorLength);
|
||||
result.cosine = float4(gradRawCos / floorLength, rawCos / floorLength);
|
||||
result.sine = float4(gradRawSin / floorLength, rawSin / floorLength);
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -600,14 +600,12 @@ CultCellular cultmath_cellular(float3 p)
|
|||
return result;
|
||||
}
|
||||
|
||||
// Sites: a site is float4(position.xyz, weight). A span is a loop over a per-site step, because a shader
|
||||
// function cannot take a span in the mirror (design.md, "Sites"). The consumer's loop, over its own buffer:
|
||||
// Sites: a site is float4(position.xyz, weight). A shader function cannot take a span (design.md, "Sites"), so a span is a
|
||||
// fixed-capacity array and a count (cultmath_power_border, below), or a loop over a per-site step:
|
||||
// CultSiteCells cells = cultmath_site_cells_empty();
|
||||
// for (int i = 0; i < count; i++)
|
||||
// cells = cultmath_site_cells_insert(cells, cultmath_power_score(p, sites[i]), i);
|
||||
// The exact power cell border is a second loop over the buffer, skipping the owner cells.nearestSite:
|
||||
// float4 border = float4(0.0, 0.0, 0.0, 1.0e30); int owner = (int)cells.nearestSite;
|
||||
// for (int k = 0; k < count; k++) if (k != owner) border = cultmath_power_border_step(p, sites[owner], sites[k], border);
|
||||
// The exact power cell border is cultmath_power_border, the second loop over the sites that skips the owner cells.nearestSite.
|
||||
// The step trusts the owner: a p outside the owner's cell reports distance 0 with that plane's normal, not an error.
|
||||
// Statement for statement with math.Sites.cs (power_score, multiplicative_score, site_cells_*, gabriel_margin).
|
||||
struct CultSiteCells
|
||||
|
|
@ -688,21 +686,8 @@ float cultmath_gabriel_margin(float3 a, float3 b, float3 other)
|
|||
return (cultmath_distance(other, (a + b) * 0.5) - r) / r;
|
||||
}
|
||||
|
||||
// Weighted lattice cells: the four per-cell steps of math.Lattice.cs, statement for statement. A shader function cannot take a
|
||||
// span (design.md, "Sites"), so weighted_lattice_cells is the consumer's loop over its own body buffer, in two passes:
|
||||
// float2 q = p / cellSize; int2 c0 = (int2)floor(q); float f1 = 1.0e30, f2 = 1.0e30; int reach = 0;
|
||||
// for (int k = 0; ; k++) { // ring pass: Chebyshev rings 0, 1, 2, ... around c0, exact exit
|
||||
// if (k > 1 && 0.5 * (k - 1) >= max(f2, f1 + 2.0 * smoothing)) break; // k never reaches 38 in the accepted range
|
||||
// for each cell on ring k: d = cultmath_lattice_distance(q, cultmath_lattice_site(cell, time, drift).xy, w, normal, stretch),
|
||||
// w = cultmath_lattice_weight(site.z, border, boost, falloff), border = the .w of cultmath_power_border_step
|
||||
// folded over the bodies (owner = the nearest body of the cell centre (cell + 0.5) * cellSize, z = 0);
|
||||
// if (d < f1) { f2 = f1; f1 = d; } else if (d < f2) f2 = d; store d for the box pass
|
||||
// reach = k; }
|
||||
// float m = f1 + smoothing; // box pass: every stored cell, rows dz = -reach..reach, then dx = -reach..reach
|
||||
// for each (dz, dx): m = cultmath_lattice_smooth_step(m, d, f1, smoothing); // strict < for the winning id (earlier cell wins ties)
|
||||
// The preconditions the C# function throws for are the caller's in a shader: finite arguments, cellSize > 0, boost in [0, 4],
|
||||
// falloff > 0, smoothing in (0, 1], stretch in [1, 2], p within 2^23 cells. The normal is normalised as C# does: divide by its
|
||||
// largest component, then by its length (a zero normal means (1, 0) and needs stretch == 1). The ring bound is 37.
|
||||
// Weighted lattice cells: the four per-cell steps of math.Lattice.cs, statement for statement; cultmath_weighted_lattice_cells,
|
||||
// after them, is the whole function.
|
||||
float3 cultmath_lattice_site(float2 cell, float time, float drift)
|
||||
{
|
||||
int3 h = cultmath_pcg3d(int3((int)cell.x, (int)cell.y, 0));
|
||||
|
|
@ -734,6 +719,90 @@ float cultmath_lattice_smooth_step(float m, float d, float f1, float smoothing)
|
|||
return min(next, f1 + smoothing);
|
||||
}
|
||||
|
||||
// Sites by value: a shader function cannot take a span, so these take float4 sites[CULTMATH_MAX_SITES] and the count in use, with
|
||||
// count in [0, CULTMATH_MAX_SITES]. A consumer defines CULTMATH_MAX_SITES before including this file to change the capacity; loops
|
||||
// run to count, so the capacity costs uniform space, not iterations. A caller whose sites live in a buffer copies them into a local array first.
|
||||
// The preconditions the C# functions throw for are the caller's in a shader: finite arguments, cellSize > 0, boost in [0, 4],
|
||||
// falloff > 0, smoothing in (0, 1], stretch in [1, 2], p within 2^23 cells. A zero stretchNormal means (1, 0) and needs stretch == 1.
|
||||
// cultmath_weighted_lattice_cells is math.weighted_lattice_cells: the ring pass is bounded by k <= 37 and exits as C# does, and the
|
||||
// box pass recomputes each distance instead of caching it (a 75 x 75 private array is not a shader option), with the same values.
|
||||
#ifndef CULTMATH_MAX_SITES
|
||||
#define CULTMATH_MAX_SITES 128
|
||||
#endif
|
||||
|
||||
float4 cultmath_power_border(float3 p, float4 sites[CULTMATH_MAX_SITES], int count)
|
||||
{
|
||||
CultSiteCells cells = cultmath_site_cells_empty();
|
||||
for (int i = 0; i < count; i++)
|
||||
cells = cultmath_site_cells_insert(cells, cultmath_power_score(p, sites[i]), i);
|
||||
float4 border = float4(0.0, 0.0, 0.0, 1.0e30);
|
||||
int owner = (int)cells.nearestSite;
|
||||
if (owner < 0)
|
||||
return border;
|
||||
for (int k = 0; k < count; k++)
|
||||
if (k != owner)
|
||||
border = cultmath_power_border_step(p, sites[owner], sites[k], border);
|
||||
return border;
|
||||
}
|
||||
|
||||
float cultmath_lattice_cell_distance(float2 q, float2 cell, float time, float drift, float4 bodies[CULTMATH_MAX_SITES], int count,
|
||||
float cellSize, float boost, float falloff, float2 normal, float stretch)
|
||||
{
|
||||
float3 site = cultmath_lattice_site(cell, time, drift);
|
||||
float border = cultmath_power_border(float3((cell + 0.5) * cellSize, 0.0), bodies, count).w;
|
||||
return cultmath_lattice_distance(q, site.xy, cultmath_lattice_weight(site.z, border, boost, falloff), normal, stretch);
|
||||
}
|
||||
|
||||
float4 cultmath_weighted_lattice_cells(float2 p, float time, float4 bodies[CULTMATH_MAX_SITES], int count, float cellSize, float drift,
|
||||
float boost, float falloff, float smoothing, float2 stretchNormal, float stretch)
|
||||
{
|
||||
float scale = max(abs(stretchNormal.x), abs(stretchNormal.y));
|
||||
float2 normal = float2(1.0, 0.0);
|
||||
if (scale > 0.0)
|
||||
{
|
||||
normal = stretchNormal / scale;
|
||||
normal = normal / length(normal);
|
||||
}
|
||||
float2 q = p / cellSize;
|
||||
float2 origin = floor(q);
|
||||
int cx0 = (int)origin.x;
|
||||
int cz0 = (int)origin.y;
|
||||
float f1 = 1.0e30;
|
||||
float f2 = 1.0e30;
|
||||
int reach = 0;
|
||||
for (int k = 0; k <= 37; k++)
|
||||
{
|
||||
if (k > 1 && 0.5 * (float)(k - 1) >= max(f2, f1 + 2.0 * smoothing))
|
||||
break;
|
||||
for (int dz = -k; dz <= k; dz++)
|
||||
{
|
||||
int step = dz == -k || dz == k || k == 0 ? 1 : 2 * k;
|
||||
for (int dx = -k; dx <= k; dx += step)
|
||||
{
|
||||
float d = cultmath_lattice_cell_distance(q, float2(cx0 + dx, cz0 + dz), time, drift, bodies, count, cellSize, boost, falloff, normal, stretch);
|
||||
if (d < f1) { f2 = f1; f1 = d; }
|
||||
else if (d < f2) f2 = d;
|
||||
}
|
||||
}
|
||||
reach = k;
|
||||
}
|
||||
float m = f1 + smoothing;
|
||||
float best = 1.0e30;
|
||||
float second = 1.0e30;
|
||||
int winDx = 0;
|
||||
int winDz = 0;
|
||||
for (int dz = -reach; dz <= reach; dz++)
|
||||
for (int dx = -reach; dx <= reach; dx++)
|
||||
{
|
||||
float d = cultmath_lattice_cell_distance(q, float2(cx0 + dx, cz0 + dz), time, drift, bodies, count, cellSize, boost, falloff, normal, stretch);
|
||||
if (d < best) { second = best; best = d; winDx = dx; winDz = dz; }
|
||||
else if (d < second) second = d;
|
||||
m = cultmath_lattice_smooth_step(m, d, f1, smoothing);
|
||||
}
|
||||
int4 id = cultmath_pcg4d(int4(cx0 + winDx, cz0 + winDz, 0, 0));
|
||||
return float4(best, second, m, (float)((uint)id.w >> 8) * (1.0 / 16777216.0));
|
||||
}
|
||||
|
||||
float2 cultmath_segment_closest(float2 point_value, float2 start, float2 end)
|
||||
{
|
||||
float2 segment = end - start;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ namespace GameCult.Caching.MessagePack;
|
|||
/// The canonical System.Text.Json shape of every public CultMath value type, the JSON twin of
|
||||
/// <see cref="CultMathResolver"/>. Vectors, quaternions and colors are objects keyed by their
|
||||
/// field names (<c>{"x":1,"y":2}</c>), matrices are arrays of row objects, <c>rect</c> is
|
||||
/// <c>{"min":..,"max":..}</c>, <c>CultCellular</c> is <c>{"nearest":..,"edge":..,"id":..}</c>, <c>CultPhasor</c> is <c>{"cos":..,"sin":..}</c>, <c>SpanNoise</c> is <c>{"field":..,"curvature":..,"fold":..}</c>, <c>SpanPath</c> is <c>{"midpoint":..,"velocity":..,"speed":..,"acceleration":..,"halfLength":..}</c>, <c>SpanStage</c> is <c>{"frequency":..,"stretch":..,"axis":..,"shift":..,"offset":..,"octaveStep":..}</c>, and <c>CultSiteCells</c> is <c>{"nearest":..,"second":..,"nearestSite":..,"secondSite":..}</c>.
|
||||
/// <c>{"min":..,"max":..}</c>, <c>CultCellular</c> is <c>{"nearest":..,"edge":..,"id":..}</c>, <c>CultPhasor</c> is <c>{"cosine":..,"sine":..}</c> (reading also accepts the legacy <c>cos</c>/<c>sin</c> names), <c>SpanNoise</c> is <c>{"field":..,"curvature":..,"fold":..}</c>, <c>SpanPath</c> is <c>{"midpoint":..,"velocity":..,"speed":..,"acceleration":..,"halfLength":..}</c>, <c>SpanStage</c> is <c>{"frequency":..,"stretch":..,"axis":..,"shift":..,"offset":..,"octaveStep":..}</c>, and <c>CultSiteCells</c> is <c>{"nearest":..,"second":..,"nearestSite":..,"secondSite":..}</c>.
|
||||
/// Property names are fixed: <c>PropertyNamingPolicy</c> does not rename them, and
|
||||
/// <c>PropertyNameCaseInsensitive</c> makes reading case-insensitive.
|
||||
/// <para>
|
||||
|
|
@ -270,14 +270,24 @@ public static class CultMathJson
|
|||
{
|
||||
private readonly string[] names;
|
||||
private readonly byte[][] utf8Names;
|
||||
private readonly string?[] legacyNames;
|
||||
private readonly byte[]?[] legacyUtf8Names;
|
||||
private readonly JsonEncodedText[] encoded;
|
||||
private readonly Node<S>[] children;
|
||||
private readonly int[] offsets;
|
||||
|
||||
public Fields(params (string Name, Node<S> Child)[] fields)
|
||||
: this(Array.ConvertAll(fields, f => (f.Name, f.Child, (string?)null)))
|
||||
{
|
||||
}
|
||||
|
||||
// A legacy name is matched on read only and maps to the same field as its current name.
|
||||
public Fields(params (string Name, Node<S> Child, string? Legacy)[] fields)
|
||||
{
|
||||
names = new string[fields.Length];
|
||||
utf8Names = new byte[fields.Length][];
|
||||
legacyNames = new string?[fields.Length];
|
||||
legacyUtf8Names = new byte[]?[fields.Length];
|
||||
encoded = new JsonEncodedText[fields.Length];
|
||||
children = new Node<S>[fields.Length];
|
||||
offsets = new int[fields.Length];
|
||||
|
|
@ -285,6 +295,8 @@ public static class CultMathJson
|
|||
{
|
||||
names[i] = fields[i].Name;
|
||||
utf8Names[i] = Encoding.UTF8.GetBytes(fields[i].Name);
|
||||
legacyNames[i] = fields[i].Legacy;
|
||||
legacyUtf8Names[i] = fields[i].Legacy is { } legacy ? Encoding.UTF8.GetBytes(legacy) : null;
|
||||
encoded[i] = JsonEncodedText.Encode(fields[i].Name);
|
||||
children[i] = fields[i].Child;
|
||||
offsets[i] = Count;
|
||||
|
|
@ -339,13 +351,14 @@ public static class CultMathJson
|
|||
if (!ignoreCase)
|
||||
{
|
||||
for (var i = 0; i < utf8Names.Length; i++)
|
||||
if (r.ValueTextEquals(utf8Names[i])) return i;
|
||||
if (r.ValueTextEquals(utf8Names[i]) || (legacyUtf8Names[i] is { } legacy && r.ValueTextEquals(legacy))) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
var name = r.GetString()!;
|
||||
for (var i = 0; i < names.Length; i++)
|
||||
if (string.Equals(names[i], name, StringComparison.OrdinalIgnoreCase)) return i;
|
||||
if (string.Equals(names[i], name, StringComparison.OrdinalIgnoreCase)
|
||||
|| (legacyNames[i] is { } legacy && string.Equals(legacy, name, StringComparison.OrdinalIgnoreCase))) return i;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -481,7 +494,7 @@ public static class CultMathJson
|
|||
Add<Random, uint>(UIntScalar.Instance, new Fields<uint>(("state", Val<uint>())),
|
||||
a => new Random { state = a[0] }, (v, a) => a[0] = v.state),
|
||||
|
||||
// Compositions: matrices are arrays of row objects, rect is {min, max}, CultCellular is {nearest, edge, id}, CultPhasor is {cos, sin}, SpanNoise is {field, curvature, fold}, SpanPath is {midpoint, velocity, speed, acceleration, halfLength}, SpanStage is {frequency, stretch, axis, shift, offset, octaveStep}, CultSiteCells is {nearest, second, nearestSite, secondSite}.
|
||||
// Compositions: matrices are arrays of row objects, rect is {min, max}, CultCellular is {nearest, edge, id}, CultPhasor is {cosine, sine}, SpanNoise is {field, curvature, fold}, SpanPath is {midpoint, velocity, speed, acceleration, halfLength}, SpanStage is {frequency, stretch, axis, shift, offset, octaveStep}, CultSiteCells is {nearest, second, nearestSite, secondSite}.
|
||||
Add<float2x2, float>(f, new Rows<float>(2, Xy<float>()),
|
||||
a => new float2x2(a[0], a[1], a[2], a[3]),
|
||||
(v, a) => { a[0] = v[0].x; a[1] = v[0].y; a[2] = v[1].x; a[3] = v[1].y; }),
|
||||
|
|
@ -507,12 +520,12 @@ public static class CultMathJson
|
|||
a[4] = v.edge.x; a[5] = v.edge.y; a[6] = v.edge.z; a[7] = v.edge.w;
|
||||
a[8] = v.id;
|
||||
}),
|
||||
Add<CultPhasor, float>(f, new Fields<float>(("cos", Xyzw<float>()), ("sin", Xyzw<float>())),
|
||||
Add<CultPhasor, float>(f, new Fields<float>(("cosine", Xyzw<float>(), "cos"), ("sine", Xyzw<float>(), "sin")),
|
||||
a => new CultPhasor(new float4(a[0], a[1], a[2], a[3]), new float4(a[4], a[5], a[6], a[7])),
|
||||
(v, a) =>
|
||||
{
|
||||
a[0] = v.cos.x; a[1] = v.cos.y; a[2] = v.cos.z; a[3] = v.cos.w;
|
||||
a[4] = v.sin.x; a[5] = v.sin.y; a[6] = v.sin.z; a[7] = v.sin.w;
|
||||
a[0] = v.cosine.x; a[1] = v.cosine.y; a[2] = v.cosine.z; a[3] = v.cosine.w;
|
||||
a[4] = v.sine.x; a[5] = v.sine.y; a[6] = v.sine.z; a[7] = v.sine.w;
|
||||
}),
|
||||
Add<SpanNoise, float>(f, new Fields<float>(("field", Xyzw<float>()), ("curvature", Val<float>()), ("fold", Val<float>())),
|
||||
a => new SpanNoise(new float4(a[0], a[1], a[2], a[3]), a[4], a[5]),
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ public sealed class CultMathResolver : IFormatterResolver
|
|||
return value;
|
||||
}),
|
||||
|
||||
// Compositions: matrices are rows, rect is [min, max], CultCellular is [nearest, edge, id], CultPhasor is [cos, sin], SpanNoise is [field, curvature, fold], SpanPath is [midpoint, velocity, speed, acceleration, halfLength], SpanStage is [frequency, stretch, axis, shift, offset, octaveStep], CultSiteCells is [nearest, second, nearestSite, secondSite].
|
||||
// Compositions: matrices are rows, rect is [min, max], CultCellular is [nearest, edge, id], CultPhasor is [cosine, sine], SpanNoise is [field, curvature, fold], SpanPath is [midpoint, velocity, speed, acceleration, halfLength], SpanStage is [frequency, stretch, axis, shift, offset, octaveStep], CultSiteCells is [nearest, second, nearestSite, secondSite].
|
||||
Shape<float2x2>((ref MessagePackWriter w, float2x2 v) => { w.WriteArrayHeader(2); PutFloat2(ref w, v[0]); PutFloat2(ref w, v[1]); },
|
||||
(ref MessagePackReader r) =>
|
||||
{
|
||||
|
|
@ -221,7 +221,7 @@ public sealed class CultMathResolver : IFormatterResolver
|
|||
}
|
||||
return value;
|
||||
}),
|
||||
Shape<CultPhasor>((ref MessagePackWriter w, CultPhasor v) => { w.WriteArrayHeader(2); PutFloat4(ref w, v.cos); PutFloat4(ref w, v.sin); },
|
||||
Shape<CultPhasor>((ref MessagePackWriter w, CultPhasor v) => { w.WriteArrayHeader(2); PutFloat4(ref w, v.cosine); PutFloat4(ref w, v.sine); },
|
||||
(ref MessagePackReader r) =>
|
||||
{
|
||||
var value = default(CultPhasor);
|
||||
|
|
@ -229,8 +229,8 @@ public sealed class CultMathResolver : IFormatterResolver
|
|||
{
|
||||
switch (i)
|
||||
{
|
||||
case 0: value.cos = GetFloat4(ref r); break;
|
||||
case 1: value.sin = GetFloat4(ref r); break;
|
||||
case 0: value.cosine = GetFloat4(ref r); break;
|
||||
case 1: value.sine = GetFloat4(ref r); break;
|
||||
default: r.Skip(); break;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ broken graph whole (an `ExprGraphException` naming the rule), so a consumer
|
|||
| `float2x2`, `float3x3` | array of row vectors (row-major) | array of row objects |
|
||||
| `rect` | `[min, max]` of `float2` | `{"min":{..},"max":{..}}` |
|
||||
| `CultCellular` | `[nearest, edge, id]` | `{"nearest":{..},"edge":{..},"id":..}` |
|
||||
| `CultPhasor` | `[cos, sin]` of `float4` | `{"cos":{..},"sin":{..}}` |
|
||||
| `CultPhasor` | `[cosine, sine]` of `float4` | `{"cosine":{..},"sine":{..}}` (reads legacy `cos`/`sin`) |
|
||||
| `CultSiteCells` | `[nearest, second, nearestSite, secondSite]` | `{"nearest":{..},"second":{..},"nearestSite":..,"secondSite":..}` |
|
||||
|
||||
Floats are MessagePack float32 (`0xCA`), doubles float64 (`0xCB`), integers the
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ namespace GameCult.Caching.Tests
|
|||
Json(new CultCellular(new float4(1f, 2f, 3f, 4f), new float4(5f, 6f, 7f, 8f), 0.25f)).Should().Be(
|
||||
"""{"nearest":{"x":1,"y":2,"z":3,"w":4},"edge":{"x":5,"y":6,"z":7,"w":8},"id":0.25}""");
|
||||
Json(new CultPhasor(new float4(1f, 2f, 3f, 4f), new float4(5f, 6f, 7f, 8f))).Should().Be(
|
||||
"""{"cos":{"x":1,"y":2,"z":3,"w":4},"sin":{"x":5,"y":6,"z":7,"w":8}}""");
|
||||
"""{"cosine":{"x":1,"y":2,"z":3,"w":4},"sine":{"x":5,"y":6,"z":7,"w":8}}""");
|
||||
Json(new SpanNoise(new float4(1f, 2f, 3f, 4f), 0.25f, 0.5f)).Should().Be(
|
||||
"""{"field":{"x":1,"y":2,"z":3,"w":4},"curvature":0.25,"fold":0.5}""");
|
||||
Json(new SpanPath { midpoint = new float3(1f, 2f, 3f), velocity = new float3(4f, 5f, 6f), speed = 7f, acceleration = 8f, halfLength = 9f }).Should().Be(
|
||||
|
|
@ -92,6 +92,87 @@ namespace GameCult.Caching.Tests
|
|||
Parse<float2?>("null").Should().BeNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Reading_AcceptsTheLegacyPhasorNames()
|
||||
{
|
||||
// The fixture is what the 0.4.1-era encoder wrote: keys cos/sin.
|
||||
var path = System.IO.Path.Combine(TestContext.CurrentContext.TestDirectory, "Fixtures", "cultphasor-json-legacy.json");
|
||||
var legacy = Parse<CultPhasor[]>(System.IO.File.ReadAllText(path));
|
||||
var current = new[]
|
||||
{
|
||||
new CultPhasor(new float4(1f, 2f, 3f, 4f), new float4(5f, 6f, 7f, 8f)),
|
||||
math.phacelle(new float3(0.3f, 1.7f, -2.2f), new float3(6.2831853f, 0f, 0f), 0.25f, 0.5f),
|
||||
};
|
||||
legacy.Should().HaveCount(2);
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
BitsOf(legacy[i]).Should().Equal(BitsOf(current[i]));
|
||||
BitsOf(Parse<CultPhasor>(Json(current[i]))).Should().Equal(BitsOf(current[i]));
|
||||
}
|
||||
|
||||
var caseInsensitive = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }.AddCultMathConverters();
|
||||
BitsOf(JsonSerializer.Deserialize<CultPhasor>("""{"COS":{"x":1,"y":2,"z":3,"w":4},"Sin":{"x":5,"y":6,"z":7,"w":8}}""", caseInsensitive))
|
||||
.Should().Equal(BitsOf(current[0]));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Reading_RefusesALegacyAndCurrentNameTogether()
|
||||
{
|
||||
var property = typeof(JsonSerializerOptions).GetProperty("AllowDuplicateProperties");
|
||||
Assert.That(property, Is.Not.Null, "the test runtime's System.Text.Json has AllowDuplicateProperties");
|
||||
var strict = new JsonSerializerOptions().AddCultMathConverters();
|
||||
property!.SetValue(strict, false);
|
||||
Action both = () => JsonSerializer.Deserialize<CultPhasor>("""{"cos":{"x":1},"cosine":{"x":2}}""", strict);
|
||||
both.Should().Throw<JsonException>().WithMessage("*appears more than once*");
|
||||
Action reversed = () => JsonSerializer.Deserialize<CultPhasor>("""{"sine":{"x":1},"sin":{"x":2}}""", strict);
|
||||
reversed.Should().Throw<JsonException>().WithMessage("*appears more than once*");
|
||||
|
||||
// With duplicates allowed the later property replaces the earlier one, as for any repeated name.
|
||||
Parse<CultPhasor>("""{"cos":{"x":1,"y":9},"cosine":{"x":2}}""").cosine.Should().Be(new float4(2f, 0f, 0f, 0f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Reading_AcceptsTheLegacyPhasorNamesUnderDisallow()
|
||||
{
|
||||
var options = new JsonSerializerOptions { UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow }.AddCultMathConverters();
|
||||
var phasor = JsonSerializer.Deserialize<CultPhasor>("""{"cos":{"x":1,"y":2,"z":3,"w":4},"sin":{"x":5,"y":6,"z":7,"w":8}}""", options);
|
||||
BitsOf(phasor).Should().Equal(BitsOf(new CultPhasor(new float4(1f, 2f, 3f, 4f), new float4(5f, 6f, 7f, 8f))));
|
||||
Action unknown = () => JsonSerializer.Deserialize<CultPhasor>("""{"cos":{"x":1},"coss":{"x":2}}""", options);
|
||||
unknown.Should().Throw<JsonException>().WithMessage("*not a component*");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Reading_TreatsAThirdOccurrenceOfAPhasorNameAsADuplicate()
|
||||
{
|
||||
var property = typeof(JsonSerializerOptions).GetProperty("AllowDuplicateProperties");
|
||||
Assert.That(property, Is.Not.Null, "the test runtime's System.Text.Json has AllowDuplicateProperties");
|
||||
var strict = new JsonSerializerOptions().AddCultMathConverters();
|
||||
property!.SetValue(strict, false);
|
||||
Action thrice = () => JsonSerializer.Deserialize<CultPhasor>("""{"cos":{"x":1},"cosine":{"x":2},"cos":{"x":3}}""", strict);
|
||||
thrice.Should().Throw<JsonException>().WithMessage("*appears more than once*");
|
||||
|
||||
// Allowed, the last occurrence replaces the whole value, including after a third.
|
||||
Parse<CultPhasor>("""{"cos":{"x":1,"y":9},"cosine":{"x":2},"cos":{"z":3}}""").cosine.Should().Be(new float4(0f, 0f, 3f, 0f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Reading_SkipsUnknownNamesWhenCaseInsensitive()
|
||||
{
|
||||
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }.AddCultMathConverters();
|
||||
JsonSerializer.Deserialize<CultPhasor>("""{"extra":[1,2],"COSINE":{"x":3},"coss":{"x":4}}""", options).cosine.x.Should().Be(3f);
|
||||
JsonSerializer.Deserialize<float2>("""{"X":1,"q":2,"Y":3}""", options).Should().Be(new float2(1f, 3f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Writing_NeverEmitsLegacyNames()
|
||||
{
|
||||
using var document = JsonDocument.Parse(Json(new CultPhasor(new float4(1f, 1f, 1f, 1f), new float4(2f, 2f, 2f, 2f))));
|
||||
document.RootElement.EnumerateObject().Select(p => p.Name).Should().Equal("cosine", "sine");
|
||||
}
|
||||
|
||||
private static int[] BitsOf(CultPhasor v) => new[] { v.cosine.x, v.cosine.y, v.cosine.z, v.cosine.w, v.sine.x, v.sine.y, v.sine.z, v.sine.w }
|
||||
.Select(BitConverter.SingleToInt32Bits).ToArray();
|
||||
|
||||
[Test]
|
||||
public void Reading_RefusesShapesThatAreNotTheType()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
[{"cos":{"x":1,"y":2,"z":3,"w":4},"sin":{"x":5,"y":6,"z":7,"w":8}},{"cos":{"x":-5.849812,"y":0.051139403,"z":0.14261721,"w":-0.087419346},"sin":{"x":-2.0218675,"y":0.7666904,"z":-0.22879826,"w":0.9240916}}]
|
||||
|
|
@ -6,6 +6,11 @@ All notable changes to this package are documented in this file.
|
|||
|
||||
### Breaking
|
||||
|
||||
- `CultMathJson` writes `CultPhasor` as `{"cosine":..,"sine":..}` and still reads
|
||||
the old `cos`/`sin` keys. A reader from before this change does not know the
|
||||
new keys: with default options it skips them and decodes zeros, and with
|
||||
`UnmappedMemberHandling.Disallow` it throws. Upgrade readers before writers.
|
||||
MessagePack bytes are unchanged.
|
||||
- CultCache refuses an unknown union arm on read and an unnamed union subtype on
|
||||
write. A payload whose arm key the union does not declare used to decode to
|
||||
`null` (and the next save wrote the loss back); a value whose type the union
|
||||
|
|
|
|||
Loading…
Reference in a new issue