From da082e7e7c66b1171a2287a92605aa0bd5845c71 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 17 Oct 2026 12:49:12 +0200 Subject: [PATCH 01/21] cultmath glsl build: wrappers.py, build-only export wrappers (188) with a count refusal Co-Authored-By: Claude Sonnet 5.5 --- packages/cultmath/tools/glsl/wrappers.py | 41 ++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 packages/cultmath/tools/glsl/wrappers.py diff --git a/packages/cultmath/tools/glsl/wrappers.py b/packages/cultmath/tools/glsl/wrappers.py new file mode 100644 index 00000000..0b5af871 --- /dev/null +++ b/packages/cultmath/tools/glsl/wrappers.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +"""Build-only export wrappers: one exported, uniquely named wrapper per top-level cultmath_ definition, so dxc keeps +every overload as its own function. The shaders are copied unchanged; the unique names exist only in . + +usage: wrappers.py +Writes /cm_wrappers.hlsl and /wrappers.tsv (wrapper, name, file, params). Definitions with a Texture +or Sampler parameter get no wrapper. Refuses a file where the definition-looking lines differ from the parsed ones.""" +import os, re, shutil, sys + +FILES = ['CultMath.hlsl', 'CultMath.Interval.hlsl', 'CultMath.Affine.hlsl', 'CultMath.Cycles.hlsl', 'CultMath.Phacelle.hlsl'] +SIG = re.compile(r'(?m)^(\w+)[ \t]+(cultmath_\w+)[ \t]*\(([^)]*)\)') +LOOKS = re.compile(r'(?m)^\w+ cultmath_\w+\(') + + +def main(shaders, out): + shutil.copytree(shaders, out, dirs_exist_ok=True) + wrappers = ['#include "CultMath.hlsl"'] + rows = [] + for fn in FILES: + text = open(os.path.join(shaders, fn)).read() + parsed = list(SIG.finditer(text)) + looks = len(LOOKS.findall(text)) + if looks != len(parsed): + sys.exit('refuse: %s has %d definition-looking lines but %d parsed' % (fn, looks, len(parsed))) + for m in parsed: + ret, name, ps = m.groups() + if re.search(r'Texture|Sampler', ps): + continue + params = [p.strip() for p in ps.split(',') if p.strip()] + args = [re.split(r'\s+', p)[-1] for p in params] + wn = 'cmw%03d_%s' % (len(rows), name) + call = '%s(%s)' % (name, ', '.join(args)) + wrappers.append('export %s %s(%s) { %s%s; }' % (ret, wn, ', '.join(params), '' if ret == 'void' else 'return ', call)) + rows.append('\t'.join([wn, name, fn, ', '.join(params)])) + open(os.path.join(out, 'cm_wrappers.hlsl'), 'w').write('\n'.join(wrappers) + '\n') + open(os.path.join(out, 'wrappers.tsv'), 'w').write('\n'.join(rows) + '\n') + print('wrappers', len(rows)) + + +if __name__ == '__main__': + main(*sys.argv[1:3]) From b10f4530d0276ed95f465a2760c5b85063621a17 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 17 Oct 2026 12:49:50 +0200 Subject: [PATCH 02/21] cultmath glsl build: spvedit.py, the one SPIR-V edit with by-value parameters Co-Authored-By: Claude Sonnet 5.5 --- packages/cultmath/tools/glsl/spvedit.py | 189 ++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 packages/cultmath/tools/glsl/spvedit.py diff --git a/packages/cultmath/tools/glsl/spvedit.py b/packages/cultmath/tools/glsl/spvedit.py new file mode 100644 index 00000000..b22db70d --- /dev/null +++ b/packages/cultmath/tools/glsl/spvedit.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +"""The one instruction-level SPIR-V edit of the GLSL build. Binary, never text. + +usage: spvedit.py +1. Keeps only the capabilities Shader and Linkage and drops every OpExtension. +2. Deletes each wrapper function with its OpName and decorations, moves its LinkageAttributes Export to its callee, and + sets the callee's OpName to the source name. Refuses a wrapper that is not exactly one OpFunctionCall. +3. By-value lowering: dxc -Od passes every parameter as a Function pointer and the caller copies in, so a callee that + assigns its parameter comes out of SPIRV-Cross as `inout`. Each Function-pointer OpFunctionParameter becomes a value + parameter with a new id; the old id stays as an entry-block OpVariable stored from it (OpName copied to the new id); + OpTypeFunction is reused if the signature exists, else added; each OpFunctionCall loads the pointer it passed. +The header bound is last id + 1.""" +import struct, sys + +OP_NAME, OP_MEMBER_NAME, OP_CAPABILITY, OP_EXTENSION, OP_DECORATE = 5, 6, 17, 10, 71 +OP_TYPE_POINTER, OP_TYPE_FUNCTION, OP_FUNCTION, OP_PARAMETER, OP_FUNCTION_END = 32, 33, 54, 55, 56 +OP_CALL, OP_LABEL, OP_VARIABLE, OP_LOAD, OP_STORE = 57, 248, 59, 61, 62 +CAP_SHADER, CAP_LINKAGE, DECORATION_LINKAGE, LINKAGE_EXPORT, FUNCTION_STORAGE = 1, 5, 41, 41, 7 +TERMINATORS = (8, 317, 62, 246, 247, 249, 250, 253, 254, 252) + + +def load(path): + raw = open(path, 'rb').read() + words = list(struct.unpack('<%dI' % (len(raw) // 4), raw)) + ins, i = [], 5 + while i < len(words): + n = words[i] >> 16 + ins.append(words[i:i + n]) + i += n + return words[:5], ins + + +def op(x): return x[0] & 0xffff +def mk(o, *ws): return [((len(ws) + 1) << 16) | o, *ws] + + +def decode(ws): + b = struct.pack('<%dI' % len(ws), *ws) + return b[:b.index(0)].decode() + + +def encode(s): + b = s.encode() + b'\0' + b += b'\0' * (-len(b) % 4) + return list(struct.unpack('<%dI' % (len(b) // 4), b)) + + +def functions(ins): + """function id -> [first index, last index, callee ids, ids defined inside]""" + fs, cur = {}, None + for k, x in enumerate(ins): + o = op(x) + if o == OP_FUNCTION: + cur = x[2] + fs[cur] = [k, None, [], set()] + if cur is None: + continue + if o == OP_CALL: + fs[cur][2].append(x[3]) + if o == OP_LABEL: + fs[cur][3].add(x[1]) + elif o not in TERMINATORS and len(x) > 2: + fs[cur][3].add(x[2]) + if o == OP_FUNCTION_END: + fs[cur][1] = k + cur = None + return fs + + +def export_edit(h, ins, rows): + names = {x[1]: decode(x[2:]) for x in ins if op(x) == OP_NAME} + fs = functions(ins) + by_name = {v: k for k, v in names.items() if k in fs} + drop_k, drop_ids, set_name, retarget = set(), set(), {}, {} + for wn, name, _, _ in rows: + fid = by_name.get(wn) + callees = fs[fid][2] if fid else None + if not callees or len(callees) != 1: + sys.exit('refuse: wrapper %s is not exactly one OpFunctionCall' % wn) + a, b, _, ids = fs[fid] + drop_k.update(range(a, b + 1)) + drop_ids |= ids | {fid} + set_name[callees[0]] = name + retarget[fid] = callees[0] + out, named = [], set() + for k, x in enumerate(ins): + if k in drop_k: + continue + o = op(x) + if o == OP_CAPABILITY and x[1] not in (CAP_SHADER, CAP_LINKAGE): + continue + if o == OP_EXTENSION: + continue + if o == OP_NAME and x[1] in set_name: + x = mk(OP_NAME, x[1], *encode(set_name[x[1]])) + named.add(x[1]) + elif o in (OP_NAME, OP_MEMBER_NAME) and x[1] in drop_ids: + continue + if o == OP_DECORATE and x[1] in drop_ids: + if x[1] in retarget and x[2] == DECORATION_LINKAGE: + x = [x[0], retarget[x[1]]] + x[2:] + else: + continue + out.append(x) + for fid, name in set_name.items(): + if fid not in named: + out.insert(next(i for i, x in enumerate(out) if op(x) == OP_NAME), mk(OP_NAME, fid, *encode(name))) + return out, len(rows) + + +def by_value(h, ins): + bound = h[3] + ptr = {x[1]: (x[2], x[3]) for x in ins if op(x) == OP_TYPE_POINTER} # id -> (storage, pointee) + ftype = {x[1]: x for x in ins if op(x) == OP_TYPE_FUNCTION} + fnparams, fntype, cur = {}, {}, None + for x in ins: # per function: which parameters are Function pointers + if op(x) == OP_FUNCTION: + cur = x[2] + fnparams[cur], fntype[cur] = [], x[4] + elif op(x) == OP_PARAMETER and cur is not None: + t = x[1] + conv = t in ptr and ptr[t][0] == FUNCTION_STORAGE + fnparams[cur].append((x[2], t, ptr[t][1] if conv else None, bound if conv else None)) + bound += conv + newid = {p[0]: p[3] for ps in fnparams.values() for p in ps if p[2]} + newtype, typeins = {}, [] + sig = {tuple(x[2:]): x[1] for x in ins if op(x) == OP_TYPE_FUNCTION} + for f, ps in fnparams.items(): + if any(p[2] for p in ps): + key = (ftype[fntype[f]][2], *[p[2] if p[2] else p[1] for p in ps]) + if key not in sig: + bound += 1 + sig[key] = bound + typeins.append(mk(OP_TYPE_FUNCTION, bound, *key)) + newtype[f] = sig[key] + out, cur, pend, need_stores, first, pc = [], None, [], False, True, 0 + for x in ins: + o = op(x) + if o == OP_FUNCTION and first: + out += typeins + first = False + if need_stores and o != OP_VARIABLE: + out += [mk(OP_STORE, p, q) for p, _, q in pend] + pend, need_stores = [], False + if o == OP_FUNCTION: + cur, pc = x[2], 0 + if cur in newtype: + x = x[:4] + [newtype[cur]] + x[5:] + elif o == OP_PARAMETER: + _, t, pointee, q = fnparams[cur][pc] + pc += 1 + if pointee: + out.append(mk(OP_PARAMETER, pointee, q)) + pend.append((x[2], t, q)) + continue + elif o == OP_LABEL and pend: + out.append(x) + out += [mk(OP_VARIABLE, t, p, FUNCTION_STORAGE) for p, t, _ in pend] + need_stores = True + continue + elif o == OP_CALL: + args = [] + for a, (_, _, pointee, _) in zip(x[4:], fnparams[x[3]]): + if pointee: + bound += 1 + out.append(mk(OP_LOAD, pointee, bound, a)) + args.append(bound) + else: + args.append(a) + x = mk(OP_CALL, *x[1:4], *args) + out.append(x) + if o == OP_NAME and x[1] in newid: + out.append([x[0], newid[x[1]]] + x[2:]) + h[3] = bound + 1 + return out, len(newtype), len(newid) + + +def main(src, dst, tsv): + h, ins = load(src) + rows = [l.split('\t') for l in open(tsv).read().splitlines() if l] + ins, wrappers = export_edit(h, ins, rows) + ins, changed, params = by_value(h, ins) + words = h + [v for x in ins for v in x] + open(dst, 'wb').write(struct.pack('<%dI' % len(words), *words)) + print('spvedit: %d wrappers dropped; by-value: %d functions changed, %d parameters' % (wrappers, changed, params)) + + +if __name__ == '__main__': + main(*sys.argv[1:4]) From 951bd15635e383ab298996d7d36acef74d354707 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 17 Oct 2026 12:53:48 +0200 Subject: [PATCH 03/21] cultmath glsl build: partition.py, origin partition and constants with refusals Co-Authored-By: Claude Sonnet 5.5 --- packages/cultmath/tools/glsl/partition.py | 84 +++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 packages/cultmath/tools/glsl/partition.py diff --git a/packages/cultmath/tools/glsl/partition.py b/packages/cultmath/tools/glsl/partition.py new file mode 100644 index 00000000..3574ad63 --- /dev/null +++ b/packages/cultmath/tools/glsl/partition.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Partition the SPIRV-Cross GLSL of the whole library into the three files a consumer includes. + +usage: partition.py +Each top-level struct and function chunk goes to the file whose HLSL defines it (CultMath, Interval and Affine to core; +Cycles; Phacelle). The library-header prologue and any main are dropped. Writes core.glsl, cycles.glsl and +phacelle.glsl bodies, with the 13 `static const float` constants of the HLSL as `const float NAME = ;` at the +head of the file that defines them. Licence lines, include guards and the #version contract belong to the caller. +Refuses: a static const line that is not `static const float CULTMATH_X = ;`, an unknown-origin chunk, a +duplicate chunk, an unexpected prologue line, and a call from a non-core chunk into a different non-core origin.""" +import collections, os, re, sys + +OWNER = {'CultMath.hlsl': 'core', 'CultMath.Interval.hlsl': 'core', 'CultMath.Affine.hlsl': 'core', + 'CultMath.Cycles.hlsl': 'cycles', 'CultMath.Phacelle.hlsl': 'phacelle'} +CONST = re.compile(r'^static const float (CULTMATH_\w+) = (-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?);$') +PROLOGUE = re.compile(r'^(#version |#extension |precision |#ifdef SPIRV_CROSS|#define SPIRV_CROSS|#endif)') +STRUCT, FUNC = re.compile(r'^struct (\w+)$'), re.compile(r'^\w+ (\w+)\(.*\)$') + + +def origins(shaders): + origin, consts = {}, collections.defaultdict(list) + for fn, o in OWNER.items(): + text = open(os.path.join(shaders, fn)).read() + for m in re.finditer(r'(?m)^\w+\s+(cultmath_\w+)\s*\(', text): + origin.setdefault(m.group(1), o) + for m in re.finditer(r'(?m)^struct\s+(\w+)', text): + origin.setdefault(m.group(1), o) + for line in text.splitlines(): + if line.startswith('static const'): + m = CONST.match(line) + if not m: + sys.exit('refuse: %s has a static const line that is not `static const float CULTMATH_X = ;`' % fn) + consts[o].append('const float %s = %s;' % m.groups()) + return origin, consts + + +def chunks(lines): + out, prologue, i = [], [], 0 + while i < len(lines): + m = STRUCT.match(lines[i]) or FUNC.match(lines[i]) + if not m: + if lines[i].strip(): + prologue.append(lines[i]) + i += 1 + continue + j = i + while lines[j] not in ('};', '}'): + j += 1 + if m.group(1) != 'main': + out.append((m.group(1), '\n'.join(lines[i:j + 1]))) + i = j + 1 + return out, prologue + + +def main(shaders, glsl, outdir): + origin, consts = origins(shaders) + parts, prologue = chunks(open(glsl).read().split('\n')) + odd = [l for l in prologue if not PROLOGUE.match(l)] + if odd: + sys.exit('refuse: %d unexpected prologue lines, first at %r' % (len(odd), odd[0][:60])) + seen = collections.Counter(n for n, _ in parts) + dup = sorted(n for n, c in seen.items() if c > 1) + if dup: + sys.exit('refuse: duplicate chunks %s' % dup[:6]) + unknown = sorted(n for n, _ in parts if n not in origin) + if unknown: + sys.exit('refuse: %d unknown-origin chunks %s' % (len(unknown), unknown[:8])) + files = collections.defaultdict(list) + for name, body in parts: + o = origin[name] + for callee in set(re.findall(r'\b(cultmath_\w+|[A-Z]\w+)\b', body)) - {name}: + co = origin.get(callee) + if o != 'core' and co and co != 'core' and co != o: + sys.exit('refuse: %s (%s) calls %s (%s)' % (name, o, callee, co)) + files[o].append(body) + os.makedirs(outdir, exist_ok=True) + for o in OWNER.values(): + text = '\n'.join(consts[o] + files[o]) + '\n' + open(os.path.join(outdir, o + '.glsl'), 'w').write(text) + print('partition:', {o: len(files[o]) for o in sorted(files)}, 'constants', sum(len(v) for v in consts.values())) + + +if __name__ == '__main__': + main(*sys.argv[1:4]) From bcb263f2cc742ff5d9df22dcb9bb61028133b4bd Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 17 Oct 2026 12:54:26 +0200 Subject: [PATCH 04/21] cultmath glsl build: build-glsl.sh, pinned tools, pipeline, ES 3.00 and consumer checks Co-Authored-By: Claude Sonnet 5.5 --- packages/cultmath/tools/glsl/build-glsl.sh | 74 ++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 packages/cultmath/tools/glsl/build-glsl.sh diff --git a/packages/cultmath/tools/glsl/build-glsl.sh b/packages/cultmath/tools/glsl/build-glsl.sh new file mode 100644 index 00000000..e018fc96 --- /dev/null +++ b/packages/cultmath/tools/glsl/build-glsl.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Builds the library GLSL from the HLSL: dxc lib_6_3 -spirv -Od, one SPIR-V edit, a fixed spirv-opt pair, SPIRV-Cross, +# partition. Linux, run from the repository root: build-glsl.sh +# Writes core.glsl, cycles.glsl and phacelle.glsl bodies into ; the committed GLSL is untouched. +# Tools are cached in GLSL_TOOLS (default packages/cultmath/.tools/glsl, gitignored): the pinned dxc tarball and +# SPIRV-Cross, SPIRV-Tools, SPIRV-Headers at TAG built with cmake. +set -euo pipefail +OUT=$(realpath -m "${1:?usage: build-glsl.sh }") +ROOT=$(pwd) +CM=$ROOT/packages/cultmath +G=$CM/tools/glsl +TOOLS=$(realpath -m "${GLSL_TOOLS:-$CM/.tools/glsl}") +TAG=vulkan-sdk-1.4.363.0 +DXC_URL=https://github.com/microsoft/DirectXShaderCompiler/releases/download/v1.9.2607/linux_dxc_2026_07_29.x86_x64.tar.gz +DXC_SHA=55665c87824051ed4774ff3280a79ccbbb7d39243b9736ca5e98222134112d54 +W=$(mktemp -d) +mkdir -p "$TOOLS" "$OUT" + +export DEBIAN_FRONTEND=noninteractive +apt-get update -qq >/dev/null +apt-get install -y -qq --no-install-recommends ca-certificates curl git cmake g++ make python3 glslang-tools >/dev/null + +if [ ! -x "$TOOLS/dxc.ok" ]; then + curl -sL --fail -o "$TOOLS/dxc.tgz" "$DXC_URL" + echo "$DXC_SHA $TOOLS/dxc.tgz" | sha256sum -c - >/dev/null || { echo "dxc tarball sha256 differs from the pinned one"; rm -f "$TOOLS/dxc.tgz"; exit 1; } + rm -rf "$TOOLS/dxc"; mkdir -p "$TOOLS/dxc"; tar xzf "$TOOLS/dxc.tgz" -C "$TOOLS/dxc"; : >"$TOOLS/dxc.ok" +fi +DXC=$(find "$TOOLS/dxc" -name dxc -type f | head -1) +export LD_LIBRARY_PATH=$(dirname "$DXC")/../lib:$(dirname "$DXC")${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH} + +if [ ! -x "$TOOLS/bin/spirv-cross" ]; then + git clone -q --depth 1 --branch $TAG https://github.com/KhronosGroup/SPIRV-Cross.git "$TOOLS/sc-src" + cmake -S "$TOOLS/sc-src" -B "$TOOLS/sc-build" -DCMAKE_BUILD_TYPE=Release -DSPIRV_CROSS_ENABLE_TESTS=OFF -DSPIRV_CROSS_CLI=ON >/dev/null + cmake --build "$TOOLS/sc-build" --target spirv-cross -j8 >/dev/null + mkdir -p "$TOOLS/bin"; cp "$TOOLS/sc-build/spirv-cross" "$TOOLS/bin/" +fi +if [ ! -x "$TOOLS/bin/spirv-opt" ]; then + git clone -q --depth 1 --branch $TAG https://github.com/KhronosGroup/SPIRV-Tools.git "$TOOLS/st-src" + git clone -q --depth 1 --branch $TAG https://github.com/KhronosGroup/SPIRV-Headers.git "$TOOLS/st-src/external/spirv-headers" + cmake -S "$TOOLS/st-src" -B "$TOOLS/st-build" -DCMAKE_BUILD_TYPE=Release -DSPIRV_SKIP_TESTS=ON -DSPIRV_WERROR=OFF >/dev/null + cmake --build "$TOOLS/st-build" --target spirv-opt spirv-val -j8 >/dev/null + mkdir -p "$TOOLS/bin"; cp "$TOOLS/st-build/tools/spirv-opt" "$TOOLS/st-build/tools/spirv-val" "$TOOLS/bin/" +fi +export PATH=$TOOLS/bin:$PATH +echo "dxc: $("$DXC" --version | head -1)"; echo "spirv-opt: $(spirv-opt --version | head -1)" + +python3 "$G/wrappers.py" "$CM/shaders" "$W/shw" +"$DXC" -T lib_6_3 -HV 2021 -spirv -fspv-target-env=universal1.5 -Od -I "$W/shw" "$W/shw/cm_wrappers.hlsl" -Fo "$W/lib.spv" >"$W/dxc.log" 2>&1 || { head -20 "$W/dxc.log"; exit 1; } +python3 "$G/spvedit.py" "$W/lib.spv" "$W/edited.spv" "$W/shw/wrappers.tsv" +spirv-val --target-env spv1.5 "$W/edited.spv" +spirv-opt --target-env=spv1.5 --eliminate-local-single-store --eliminate-dead-code-aggressive "$W/edited.spv" -o "$W/opt.spv" +spirv-val --target-env spv1.5 "$W/opt.spv" +spirv-cross "$W/opt.spv" --es --version 310 --relax-nan-checks --output "$W/cross.glsl" +python3 "$G/partition.py" "$CM/shaders" "$W/cross.glsl" "$OUT" + +# Consumer checks: glslang ES 3.00 fragment compiles of every include combination, and a consumer calling overloads +# with an r-value argument. +CONSUMER='out highp vec4 c; +void main() { vec3 p = gl_FragCoord.xyz; c = vec4(cultmath_lerp(p, p.yzx, vec3(0.5)), cultmath_lerp(1.0, 2.0, p.x)) + cultmath_saturate(vec4(p, 1.0)) + vec4(cultmath_smootherstep(gl_FragCoord.x + 1.0)); }' +for combo in "core" "core cycles" "core phacelle" "core cycles phacelle"; do + f=$W/check.frag; printf '#version 300 es\nprecision highp float;\nprecision highp int;\n' >"$f" + for part in $combo; do cat "$OUT/$part.glsl" >>"$f"; done + printf '%s\n' "$CONSUMER" >>"$f" + glslangValidator "$f" >"$W/check.log" 2>&1 || { echo "ES 3.00 compile failed for [$combo]"; head -8 "$W/check.log"; exit 1; } + echo "ES 3.00 ok: $combo" +done + +for part in core cycles phacelle; do + g=$OUT/$part.glsl + fn=$(grep -cE '^[A-Za-z_]\w* [A-Za-z_]\w*\(.*\)$' "$g" || true) + names=$(grep -oE '^[A-Za-z_]\w* [A-Za-z_]\w*\(' "$g" | awk '{print $2}' | sort -u | wc -l) + echo "$part: functions $fn, distinct names $names, lines $(wc -l <"$g"), bytes $(wc -c <"$g"), sha256 $(sha256sum "$g" | cut -c1-64)" +done +echo "suffixed ids: $(cat "$OUT"/*.glsl | grep -oE '\bcultmath_\w+_[0-9]+\(' | sort -u | wc -l); inout: $(cat "$OUT"/*.glsl | grep -c inout || true)" From cd94a2185dd406dfd04ae61186dfd46560ac3b18 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 17 Oct 2026 12:58:55 +0200 Subject: [PATCH 05/21] partition.py: duplicate chunks are duplicate signatures, not names (overloads) Co-Authored-By: Claude Sonnet 5.5 --- packages/cultmath/tools/glsl/partition.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/cultmath/tools/glsl/partition.py b/packages/cultmath/tools/glsl/partition.py index 3574ad63..0f69d8a7 100644 --- a/packages/cultmath/tools/glsl/partition.py +++ b/packages/cultmath/tools/glsl/partition.py @@ -58,7 +58,8 @@ def main(shaders, glsl, outdir): odd = [l for l in prologue if not PROLOGUE.match(l)] if odd: sys.exit('refuse: %d unexpected prologue lines, first at %r' % (len(odd), odd[0][:60])) - seen = collections.Counter(n for n, _ in parts) + seen = collections.Counter(b.split(' +')[0] for _, b in parts) # overloads share a name, never a signature dup = sorted(n for n, c in seen.items() if c > 1) if dup: sys.exit('refuse: duplicate chunks %s' % dup[:6]) From 330290dbd3d5582ab99ab4da14be464d994a8b5c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 17 Oct 2026 12:59:52 +0200 Subject: [PATCH 06/21] partition.py: fix duplicate-signature key Co-Authored-By: Claude Sonnet 5.5 --- packages/cultmath/tools/glsl/partition.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/cultmath/tools/glsl/partition.py b/packages/cultmath/tools/glsl/partition.py index 0f69d8a7..88121c92 100644 --- a/packages/cultmath/tools/glsl/partition.py +++ b/packages/cultmath/tools/glsl/partition.py @@ -58,8 +58,7 @@ def main(shaders, glsl, outdir): odd = [l for l in prologue if not PROLOGUE.match(l)] if odd: sys.exit('refuse: %d unexpected prologue lines, first at %r' % (len(odd), odd[0][:60])) - seen = collections.Counter(b.split(' -')[0] for _, b in parts) # overloads share a name, never a signature + seen = collections.Counter(b.split(chr(10))[0] for _, b in parts) # overloads share a name, never a signature dup = sorted(n for n, c in seen.items() if c > 1) if dup: sys.exit('refuse: duplicate chunks %s' % dup[:6]) From 9f0c20929cec90bc5343a383cf06bb1bfe2ddb48 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 17 Oct 2026 13:04:16 +0200 Subject: [PATCH 07/21] cultmath device run: GLSL_DIR runs the built route and compares lavapipe-glsl to lavapipe-hlsl bit for bit Co-Authored-By: Claude Sonnet 5.5 --- packages/cultmath/tools/device/gen.py | 4 +++- packages/cultmath/tools/device/run-lavapipe.sh | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/cultmath/tools/device/gen.py b/packages/cultmath/tools/device/gen.py index a2ae8104..44caf1d4 100644 --- a/packages/cultmath/tools/device/gen.py +++ b/packages/cultmath/tools/device/gen.py @@ -101,7 +101,9 @@ def body(sig, n, win, wout, defs, glsl): def main(work, root): defs = structs(root) shaders = os.path.join(root, 'shaders') - library = ''.join(open(os.path.join(shaders, f)).read() for f in ('CultMath.glsl', 'CultMath.Cycles.glsl', 'CultMath.Phacelle.glsl')) + built = os.environ.get('GLSL_DIR') + parts = [os.path.join(built, f) for f in ('core.glsl', 'cycles.glsl', 'phacelle.glsl')] if built else [os.path.join(shaders, f) for f in ('CultMath.glsl', 'CultMath.Cycles.glsl', 'CultMath.Phacelle.glsl')] + library = ''.join(open(p).read() for p in parts) count = 0 for line in open(os.path.join(work, 'manifest.txt')): tag, _, sig, n, win, wout = line.rstrip('\n').split('\t') diff --git a/packages/cultmath/tools/device/run-lavapipe.sh b/packages/cultmath/tools/device/run-lavapipe.sh index 22b04ece..4505aca7 100644 --- a/packages/cultmath/tools/device/run-lavapipe.sh +++ b/packages/cultmath/tools/device/run-lavapipe.sh @@ -5,6 +5,9 @@ # # Run from the CultLib root inside mcr.microsoft.com/dotnet/sdk:10.0 (Ubuntu 24.04), for example # SUMMARY= tools/stopgap/ygg-verify.sh mcr.microsoft.com/dotnet/sdk:10.0 'bash packages/cultmath/tools/device/run-lavapipe.sh' +# GLSL_DIR= (the output of tools/glsl/build-glsl.sh) runs the built +# route instead of the committed one and skips the bounds judge: every lavapipe-glsl/.out must equal its +# lavapipe-hlsl/.out byte for byte, and a difference exits 1. It records no bounds. # WRITE_BOUNDS=1 re-records packages/cultmath/tests/CultMath.Tests/fixtures/device-bounds-lavapipe.txt and prints it # between BOUNDS-BEGIN and BOUNDS-END, because the job's clone is discarded. set -euo pipefail @@ -30,6 +33,7 @@ export LD_LIBRARY_PATH=$(dirname "$DXC")/../lib:$(dirname "$DXC")${LD_LIBRARY_PA gcc -O2 -o "$W/vkrun" "$DEV/vkrun.c" -lvulkan dotnet build "$TESTS" -nologo -v q >/dev/null +GLSL_DIR=${GLSL_DIR:+$(realpath "$GLSL_DIR")}; export GLSL_DIR export CULTMATH_DEVICE_WORK=$W/work dotnet test "$TESTS" --no-build --filter "FullyQualifiedName~GlslMirrorTests.WriteDeviceWork" python3 "$DEV/gen.py" "$W/work" "$CM" @@ -44,6 +48,17 @@ while IFS=$'\t' read -r tag _ _ n _ wout; do "$W/vkrun" "$W/work/$tag.g.spv" "$in" "$W/results/lavapipe-glsl/$tag.out" "$n" "$wout" >>"$W/device.txt" done <"$W/work/manifest.txt" +if [ -n "${GLSL_DIR:-}" ]; then + total=0; equal=0 + for h in "$W"/results/lavapipe-hlsl/*.out; do + tag=$(basename "$h" .out); total=$((total + 1)) + if cmp -s "$h" "$W/results/lavapipe-glsl/$tag.out"; then equal=$((equal + 1)); else echo "differs: $tag"; fi + done + echo "lavapipe-glsl equals lavapipe-hlsl: $equal/$total" + [ "$equal" = "$total" ] && [ "$total" -gt 0 ] + exit +fi + RENDERER=$(sort -u "$W/device.txt") [ "$(echo "$RENDERER" | wc -l)" = 1 ] || { echo "the runs saw more than one device: $RENDERER"; exit 1; } export CULTMATH_DEVICE_NAME=${RENDERER#device=} From b7f18ff5b1ef5575d84f2d4ec1dde91fe0c23259 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 17 Oct 2026 13:24:11 +0200 Subject: [PATCH 08/21] build-glsl.sh: fail on inout, param_var_, wrapper names, export and mix(mix( in the output Co-Authored-By: Claude Sonnet 5.5 --- packages/cultmath/tools/glsl/build-glsl.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/cultmath/tools/glsl/build-glsl.sh b/packages/cultmath/tools/glsl/build-glsl.sh index e018fc96..8897cc80 100644 --- a/packages/cultmath/tools/glsl/build-glsl.sh +++ b/packages/cultmath/tools/glsl/build-glsl.sh @@ -72,3 +72,8 @@ for part in core cycles phacelle; do echo "$part: functions $fn, distinct names $names, lines $(wc -l <"$g"), bytes $(wc -c <"$g"), sha256 $(sha256sum "$g" | cut -c1-64)" done echo "suffixed ids: $(cat "$OUT"/*.glsl | grep -oE '\bcultmath_\w+_[0-9]+\(' | sort -u | wc -l); inout: $(cat "$OUT"/*.glsl | grep -c inout || true)" +# Build shape: the lowering leaves none of these, and the NaN-check option leaves no mix(mix( expansions. +if cat "$OUT"/*.glsl | grep -Eq 'inout|param_var_|cmw[0-9]{3}_|export |mix\(mix\('; then + echo "build shape failed:"; cat "$OUT"/*.glsl | grep -Eo 'inout|param_var_|cmw[0-9]{3}_|export |mix\(mix\(' | sort | uniq -c; exit 1 +fi +echo "build shape ok" From 23ac4e895be9ded3757128c19a1a6b596256dbbd Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 17 Oct 2026 13:32:37 +0200 Subject: [PATCH 09/21] cultmath README: name tools/glsl/build-glsl.sh and GLSL_DIR, not yet the owner of the committed GLSL Co-Authored-By: Claude Sonnet 5.5 --- packages/cultmath/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/cultmath/README.md b/packages/cultmath/README.md index 29113254..1e50645d 100644 --- a/packages/cultmath/README.md +++ b/packages/cultmath/README.md @@ -189,6 +189,8 @@ CultMath keeps portable DXC outside git under `packages/cultmath/.tools/`: -OutputPath E:\Projects\Odin\crates\muninn-move-tracker\artifacts\shader\MoveSphereCandidate.comp.metal ``` +`tools/glsl/build-glsl.sh ` (Linux, repository root) builds the library GLSL from the HLSL with dxc, SPIRV-Cross and SPIRV-Tools and writes `core.glsl`, `cycles.glsl` and `phacelle.glsl` bodies into ``. It does not yet own the committed GLSL; `GlslLowering` still does. `GLSL_DIR= tools/device/run-lavapipe.sh` runs that output on lavapipe and compares it with the HLSL results bit for bit instead of judging the committed files. + `compile-hlsl.ps1` compiles both `CultMath.hlsl` copies as a `lib_6_3` library with the x64 DXC; a warning fails it too (`-WX`). DXC emits Vulkan SPIR-V with `-spirv`. `-TargetEnv vulkan1.2` is exposed by the From 51f284ac6c2d94264b02d285bac430f5e7d5f432 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 17 Oct 2026 21:36:31 +0200 Subject: [PATCH 10/21] wrappers.py: array arguments by declarator name, a capacity column, a refusal for CULTMATH_MAX_SITES outside its guard Does not build alone: spvedit.py still reads four columns until the next commit. Co-Authored-By: Claude Opus 5.5 --- packages/cultmath/tools/glsl/wrappers.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/cultmath/tools/glsl/wrappers.py b/packages/cultmath/tools/glsl/wrappers.py index 0b5af871..96c1c7b5 100644 --- a/packages/cultmath/tools/glsl/wrappers.py +++ b/packages/cultmath/tools/glsl/wrappers.py @@ -3,13 +3,19 @@ every overload as its own function. The shaders are copied unchanged; the unique names exist only in . usage: wrappers.py -Writes /cm_wrappers.hlsl and /wrappers.tsv (wrapper, name, file, params). Definitions with a Texture -or Sampler parameter get no wrapper. Refuses a file where the definition-looking lines differ from the parsed ones.""" +Writes /cm_wrappers.hlsl and /wrappers.tsv (wrapper, name, file, params, capacity). An argument is the +parameter's declarator name with any [..] suffix removed; capacity lists the indices of the parameters declared +[CULTMATH_MAX_SITES]. Definitions with a Texture or Sampler parameter get no wrapper. Refuses a file where the +definition-looking lines differ from the parsed ones, and a file that names CULTMATH_MAX_SITES anywhere but its +#ifndef/#define/#endif guard and array declarators.""" import os, re, shutil, sys FILES = ['CultMath.hlsl', 'CultMath.Interval.hlsl', 'CultMath.Affine.hlsl', 'CultMath.Cycles.hlsl', 'CultMath.Phacelle.hlsl'] SIG = re.compile(r'(?m)^(\w+)[ \t]+(cultmath_\w+)[ \t]*\(([^)]*)\)') LOOKS = re.compile(r'(?m)^\w+ cultmath_\w+\(') +GUARD = re.compile(r'#ifndef CULTMATH_MAX_SITES\n#define CULTMATH_MAX_SITES \d+\n#endif\n') +COMMENT = re.compile(r'//[^\n]*|/\*.*?\*/', re.S) +CAP = '[CULTMATH_MAX_SITES]' def main(shaders, out): @@ -17,7 +23,10 @@ def main(shaders, out): wrappers = ['#include "CultMath.hlsl"'] rows = [] for fn in FILES: - text = open(os.path.join(shaders, fn)).read() + text = open(os.path.join(shaders, fn)).read().replace('\r\n', '\n') + stray = GUARD.sub('', COMMENT.sub('', text)).replace(CAP, '') + if 'CULTMATH_MAX_SITES' in stray: + sys.exit('refuse: %s names CULTMATH_MAX_SITES outside its guard and array declarators' % fn) parsed = list(SIG.finditer(text)) looks = len(LOOKS.findall(text)) if looks != len(parsed): @@ -27,11 +36,12 @@ def main(shaders, out): if re.search(r'Texture|Sampler', ps): continue params = [p.strip() for p in ps.split(',') if p.strip()] - args = [re.split(r'\s+', p)[-1] for p in params] + args = [re.split(r'\s+', p)[-1].split('[')[0] for p in params] + cap = ','.join(str(i) for i, p in enumerate(params) if p.endswith(CAP)) wn = 'cmw%03d_%s' % (len(rows), name) call = '%s(%s)' % (name, ', '.join(args)) wrappers.append('export %s %s(%s) { %s%s; }' % (ret, wn, ', '.join(params), '' if ret == 'void' else 'return ', call)) - rows.append('\t'.join([wn, name, fn, ', '.join(params)])) + rows.append('\t'.join([wn, name, fn, ', '.join(params), cap])) open(os.path.join(out, 'cm_wrappers.hlsl'), 'w').write('\n'.join(wrappers) + '\n') open(os.path.join(out, 'wrappers.tsv'), 'w').write('\n'.join(rows) + '\n') print('wrappers', len(rows)) From 40ab7a20c5a08bf3cebdc8f8750b531d130f1866 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 17 Oct 2026 21:36:34 +0200 Subject: [PATCH 11/21] spvedit.py: the capacity edit, one array length id becomes a spec constant (SpecId 0) Does not build alone: partition.py refuses the spec-constant block until the next commit. Co-Authored-By: Claude Opus 5.5 --- packages/cultmath/tools/glsl/spvedit.py | 55 ++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/packages/cultmath/tools/glsl/spvedit.py b/packages/cultmath/tools/glsl/spvedit.py index b22db70d..560ca870 100644 --- a/packages/cultmath/tools/glsl/spvedit.py +++ b/packages/cultmath/tools/glsl/spvedit.py @@ -12,7 +12,8 @@ usage: spvedit.py The header bound is last id + 1.""" import struct, sys -OP_NAME, OP_MEMBER_NAME, OP_CAPABILITY, OP_EXTENSION, OP_DECORATE = 5, 6, 17, 10, 71 +OP_NAME, OP_MEMBER_NAME, OP_CAPABILITY, OP_EXTENSION, OP_DECORATE, OP_MEMBER_DECORATE = 5, 6, 17, 10, 71, 72 +OP_TYPE_ARRAY, OP_CONSTANT, OP_SPEC_CONSTANT, DECORATION_SPEC_ID = 28, 43, 50, 1 OP_TYPE_POINTER, OP_TYPE_FUNCTION, OP_FUNCTION, OP_PARAMETER, OP_FUNCTION_END = 32, 33, 54, 55, 56 OP_CALL, OP_LABEL, OP_VARIABLE, OP_LOAD, OP_STORE = 57, 248, 59, 61, 62 CAP_SHADER, CAP_LINKAGE, DECORATION_LINKAGE, LINKAGE_EXPORT, FUNCTION_STORAGE = 1, 5, 41, 41, 7 @@ -72,7 +73,8 @@ def export_edit(h, ins, rows): fs = functions(ins) by_name = {v: k for k, v in names.items() if k in fs} drop_k, drop_ids, set_name, retarget = set(), set(), {}, {} - for wn, name, _, _ in rows: + callee_of = [] + for wn, name, _, _, _ in rows: fid = by_name.get(wn) callees = fs[fid][2] if fid else None if not callees or len(callees) != 1: @@ -82,6 +84,7 @@ def export_edit(h, ins, rows): drop_ids |= ids | {fid} set_name[callees[0]] = name retarget[fid] = callees[0] + callee_of.append(callees[0]) out, named = [], set() for k, x in enumerate(ins): if k in drop_k: @@ -105,7 +108,7 @@ def export_edit(h, ins, rows): for fid, name in set_name.items(): if fid not in named: out.insert(next(i for i, x in enumerate(out) if op(x) == OP_NAME), mk(OP_NAME, fid, *encode(name))) - return out, len(rows) + return out, callee_of def by_value(h, ins): @@ -175,14 +178,56 @@ def by_value(h, ins): return out, len(newtype), len(newid) +def capacity_edit(h, ins, rows, callee_of): + """Each capacity parameter (wrappers.tsv column 5) of each callee has an OpTypeArray type. Exactly one length id may + serve all of them, and nothing but OpTypeArray, OpName and decorations may use it. That OpConstant becomes an + OpSpecConstant with SpecId 0 named cultmath_max_sites. Returns the instructions and the spec constant id.""" + ftype = {x[1]: x for x in ins if op(x) == OP_TYPE_FUNCTION} + arrays = {x[1]: x for x in ins if op(x) == OP_TYPE_ARRAY} + params, cur = {}, None + for x in ins: + if op(x) == OP_FUNCTION: + cur = x[2] + params[cur] = [] + elif op(x) == OP_PARAMETER: + params[cur].append(x[1]) + lengths = set() + for row, fid in zip(rows, callee_of): + for i in [int(c) for c in row[4].split(',') if c]: + t = params[fid][i] + if t not in arrays: + sys.exit('refuse: capacity parameter %d of %s is not an array type' % (i, row[1])) + lengths.add(arrays[t][3]) + if len(lengths) != 1: + sys.exit('refuse: %d array length ids serve the capacity parameters, expected exactly one' % len(lengths)) + cap = lengths.pop() + for x in ins: + o = op(x) + defines = o in (OP_CONSTANT, OP_SPEC_CONSTANT) and x[2] == cap + allowed = o in (OP_TYPE_ARRAY, OP_NAME, OP_DECORATE, OP_MEMBER_DECORATE) or defines + if cap in x[1:] and not allowed: + sys.exit('refuse: the capacity length id %d is also used by opcode %d' % (cap, o)) + out = [] + for x in ins: + if op(x) == OP_CONSTANT and x[2] == cap: + x = [(x[0] & 0xffff0000) | OP_SPEC_CONSTANT] + x[1:] + out.append(x) + first = next((k for k, x in enumerate(out) if op(x) in (OP_DECORATE, OP_MEMBER_DECORATE)), None) + if first is None: + sys.exit('refuse: no annotation section to decorate the capacity constant in') + out[first:first] = [mk(OP_NAME, cap, *encode('cultmath_max_sites')), mk(OP_DECORATE, cap, DECORATION_SPEC_ID, 0)] + return out, cap + + def main(src, dst, tsv): h, ins = load(src) rows = [l.split('\t') for l in open(tsv).read().splitlines() if l] - ins, wrappers = export_edit(h, ins, rows) + ins, callee_of = export_edit(h, ins, rows) ins, changed, params = by_value(h, ins) + ins, spec = capacity_edit(h, ins, rows, callee_of) words = h + [v for x in ins for v in x] open(dst, 'wb').write(struct.pack('<%dI' % len(words), *words)) - print('spvedit: %d wrappers dropped; by-value: %d functions changed, %d parameters' % (wrappers, changed, params)) + print('spvedit: %d wrappers dropped; by-value: %d functions changed, %d parameters; capacity spec constant id %d' % (len(rows), changed, params, spec)) if __name__ == '__main__': From c2658fa0b28781215ffa8a3e2408390d1e260382 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 17 Oct 2026 21:36:34 +0200 Subject: [PATCH 12/21] partition.py: the capacity header at the head of core.glsl, the one spec-constant block accepted Co-Authored-By: Claude Opus 5.5 --- packages/cultmath/tools/glsl/partition.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/cultmath/tools/glsl/partition.py b/packages/cultmath/tools/glsl/partition.py index 88121c92..b4d25312 100644 --- a/packages/cultmath/tools/glsl/partition.py +++ b/packages/cultmath/tools/glsl/partition.py @@ -5,15 +5,19 @@ usage: partition.py Each top-level struct and function chunk goes to the file whose HLSL defines it (CultMath, Interval and Affine to core; Cycles; Phacelle). The library-header prologue and any main are dropped. Writes core.glsl, cycles.glsl and phacelle.glsl bodies, with the 13 `static const float` constants of the HLSL as `const float NAME = ;` at the -head of the file that defines them. Licence lines, include guards and the #version contract belong to the caller. +head of the file that defines them. core.glsl opens with the HLSL's own CULTMATH_MAX_SITES guard lines, one line mapping +SPIRV_CROSS_CONSTANT_ID_0 to uint(CULTMATH_MAX_SITES), and SPIRV-Cross's one spec-constant block. Licence lines, include guards and the #version contract belong to the caller. Refuses: a static const line that is not `static const float CULTMATH_X = ;`, an unknown-origin chunk, a -duplicate chunk, an unexpected prologue line, and a call from a non-core chunk into a different non-core origin.""" +duplicate chunk, an unexpected prologue line, a missing or other SPIRV_CROSS_CONSTANT_ID block, and a call from a non-core chunk into a different non-core origin.""" import collections, os, re, sys OWNER = {'CultMath.hlsl': 'core', 'CultMath.Interval.hlsl': 'core', 'CultMath.Affine.hlsl': 'core', 'CultMath.Cycles.hlsl': 'cycles', 'CultMath.Phacelle.hlsl': 'phacelle'} CONST = re.compile(r'^static const float (CULTMATH_\w+) = (-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?);$') PROLOGUE = re.compile(r'^(#version |#extension |precision |#ifdef SPIRV_CROSS|#define SPIRV_CROSS|#endif)') +GUARD = re.compile(r'#ifndef CULTMATH_MAX_SITES\n#define CULTMATH_MAX_SITES (\d+)\n#endif\n') +SPEC = re.compile(r'#ifndef SPIRV_CROSS_CONSTANT_ID_0\n#define SPIRV_CROSS_CONSTANT_ID_0 (\d+)u\n#endif\n' + r'const uint cultmath_max_sites = SPIRV_CROSS_CONSTANT_ID_0;\n?') STRUCT, FUNC = re.compile(r'^struct (\w+)$'), re.compile(r'^\w+ (\w+)\(.*\)$') @@ -54,8 +58,16 @@ def chunks(lines): def main(shaders, glsl, outdir): origin, consts = origins(shaders) + guard = GUARD.search(open(os.path.join(shaders, 'CultMath.hlsl')).read().replace('\r\n', '\n')) + if not guard: + sys.exit('refuse: CultMath.hlsl has no CULTMATH_MAX_SITES guard') parts, prologue = chunks(open(glsl).read().split('\n')) - odd = [l for l in prologue if not PROLOGUE.match(l)] + joined = '\n'.join(prologue) + '\n' + spec = SPEC.search(joined) + text, found = SPEC.subn('', joined) + if found != 1 or spec.group(1) != guard.group(1): + sys.exit('refuse: expected exactly one spec-constant block defaulting to the guard value, found %d' % found) + odd = [l for l in text.split('\n') if l and (not PROLOGUE.match(l) or 'SPIRV_CROSS_CONSTANT_ID' in l)] if odd: sys.exit('refuse: %d unexpected prologue lines, first at %r' % (len(odd), odd[0][:60])) seen = collections.Counter(b.split(chr(10))[0] for _, b in parts) # overloads share a name, never a signature @@ -75,7 +87,8 @@ def main(shaders, glsl, outdir): files[o].append(body) os.makedirs(outdir, exist_ok=True) for o in OWNER.values(): - text = '\n'.join(consts[o] + files[o]) + '\n' + head = [guard.group(0).rstrip('\n'), '#define SPIRV_CROSS_CONSTANT_ID_0 uint(CULTMATH_MAX_SITES)', spec.group(0).rstrip('\n')] if o == 'core' else [] + text = '\n'.join(head + consts[o] + files[o]) + '\n' open(os.path.join(outdir, o + '.glsl'), 'w').write(text) print('partition:', {o: len(files[o]) for o in sorted(files)}, 'constants', sum(len(v) for v in consts.values())) From 7b121f6ab2ca5edfb8921641e48f13597219f7f8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 17 Oct 2026 21:36:35 +0200 Subject: [PATCH 13/21] build-glsl.sh: capacity checks (a) to (e), and a [128], _cos, _sin shape refusal Co-Authored-By: Claude Opus 5.5 --- packages/cultmath/tools/glsl/build-glsl.sh | 25 ++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/cultmath/tools/glsl/build-glsl.sh b/packages/cultmath/tools/glsl/build-glsl.sh index 8897cc80..0bef11c2 100644 --- a/packages/cultmath/tools/glsl/build-glsl.sh +++ b/packages/cultmath/tools/glsl/build-glsl.sh @@ -65,6 +65,26 @@ for combo in "core" "core cycles" "core phacelle" "core cycles phacelle"; do echo "ES 3.00 ok: $combo" done +# Capacity checks: a consumer's prior #define CULTMATH_MAX_SITES changes the array type through dxc, the committed GLSL +# and the built GLSL, and 128 still works with no define. +printf '#define CULTMATH_MAX_SITES 4\n#include "CultMath.hlsl"\nfloat4 cap4(float3 p, float4 s[4]) { return cultmath_power_border(p, s, 4); }\n' >"$W/shw/cap4.hlsl" +"$DXC" -T lib_6_3 -HV 2021 -Od -I "$W/shw" "$W/shw/cap4.hlsl" -Fo "$W/cap4.dxil" >"$W/cap4.log" 2>&1 || { echo "capacity (a) failed: dxc with CULTMATH_MAX_SITES 4"; head -8 "$W/cap4.log"; exit 1; } +echo "capacity (a) ok: dxc lib_6_3 with a prior define 4 takes float4 s[4]" +cap_check() { # label, expected (ok|fail), define lines, GLSL library file, array length + local f=$W/cap.frag + printf '#version 300 es\nprecision highp float;\nprecision highp int;\n%s\n' "$3" >"$f"; cat "$4" >>"$f" + printf 'out highp vec4 c;\nvoid main() { vec4 s[%s]; for (int i = 0; i < %s; i++) s[i] = vec4(float(i)); c = cultmath_power_border(gl_FragCoord.xyz, s, 4); }\n' "$5" "$5" >>"$f" + local got=fail; glslangValidator "$f" >"$W/cap.log" 2>&1 && got=ok + [ "$got" = "$2" ] || { echo "capacity $1 failed: expected $2, got $got"; head -6 "$W/cap.log"; exit 1; } + echo "capacity $1 ok: $2" +} +D4='#define CULTMATH_MAX_SITES 4' +cap_check "(b) built, define 4, vec4 s[4]" ok "$D4" "$OUT/core.glsl" 4 +cap_check "(c) built, define 4, vec4 s[128]" fail "$D4" "$OUT/core.glsl" 128 +cap_check "(d) built, no define, vec4 s[128]" ok "" "$OUT/core.glsl" 128 +cap_check "(e) committed, define 4, vec4 s[4]" ok "$D4" "$CM/shaders/CultMath.glsl" 4 +cap_check "(e) committed, define 4, vec4 s[128]" fail "$D4" "$CM/shaders/CultMath.glsl" 128 + for part in core cycles phacelle; do g=$OUT/$part.glsl fn=$(grep -cE '^[A-Za-z_]\w* [A-Za-z_]\w*\(.*\)$' "$g" || true) @@ -73,7 +93,8 @@ for part in core cycles phacelle; do done echo "suffixed ids: $(cat "$OUT"/*.glsl | grep -oE '\bcultmath_\w+_[0-9]+\(' | sort -u | wc -l); inout: $(cat "$OUT"/*.glsl | grep -c inout || true)" # Build shape: the lowering leaves none of these, and the NaN-check option leaves no mix(mix( expansions. -if cat "$OUT"/*.glsl | grep -Eq 'inout|param_var_|cmw[0-9]{3}_|export |mix\(mix\('; then - echo "build shape failed:"; cat "$OUT"/*.glsl | grep -Eo 'inout|param_var_|cmw[0-9]{3}_|export |mix\(mix\(' | sort | uniq -c; exit 1 +SHAPE='inout|param_var_|cmw[0-9]{3}_|export |mix\(mix\(|\[128\]|\b_cos\b|\b_sin\b' +if cat "$OUT"/*.glsl | grep -Eq "$SHAPE"; then + echo "build shape failed:"; cat "$OUT"/*.glsl | grep -Eo "$SHAPE" | sort | uniq -c; exit 1 fi echo "build shape ok" From 38668b6ec0d585fababdf2f61183913adf2f14cf Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 17 Oct 2026 21:41:56 +0200 Subject: [PATCH 14/21] nocontract.py and run-lavapipe.sh: the strict-form gate under GLSL_DIR (-Gis, NoContraction on both modules) Co-Authored-By: Claude Opus 5.5 --- packages/cultmath/tools/device/nocontract.py | 34 +++++++++++++++++++ .../cultmath/tools/device/run-lavapipe.sh | 14 ++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 packages/cultmath/tools/device/nocontract.py diff --git a/packages/cultmath/tools/device/nocontract.py b/packages/cultmath/tools/device/nocontract.py new file mode 100644 index 00000000..26a200a7 --- /dev/null +++ b/packages/cultmath/tools/device/nocontract.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""The strict-form harness pass of the GLSL equality gate: decorates the result of every OpFMul, OpFAdd, OpFSub, +OpVectorTimesScalar and OpDot NoContraction, so the driver may not fuse them into a multiply-add. Binary, instruction +level, reusing spvedit.py's load and mk. It lives in the gate's job-local modules only, never in shipped GLSL. + +usage: nocontract.py prints the count of decorated results""" +import importlib.util, os, struct, sys + +here = os.path.dirname(os.path.abspath(__file__)) +spec = importlib.util.spec_from_file_location('spvedit', os.path.join(here, '..', 'glsl', 'spvedit.py')) +spvedit = importlib.util.module_from_spec(spec) +spec.loader.exec_module(spvedit) + +OP_DOT, OP_FADD, OP_FSUB, OP_FMUL, OP_VECTOR_TIMES_SCALAR = 148, 129, 131, 133, 142 +FLOAT_OPS = (OP_FADD, OP_FSUB, OP_FMUL, OP_VECTOR_TIMES_SCALAR, OP_DOT) +DECORATION_NO_CONTRACTION = 42 +FIRST_TYPE_OP, LAST_TYPE_OP = 19, 39 # OpTypeVoid .. OpTypeForwardPointer: the annotations end where types begin + + +def main(src, dst): + h, ins = spvedit.load(src) + done = {x[1] for x in ins if spvedit.op(x) == spvedit.OP_DECORATE and x[2] == DECORATION_NO_CONTRACTION} + ids = [x[2] for x in ins if spvedit.op(x) in FLOAT_OPS and x[2] not in done] + at = next((k for k, x in enumerate(ins) if FIRST_TYPE_OP <= spvedit.op(x) <= LAST_TYPE_OP), None) + if at is None: + sys.exit('refuse: no type section to place the decorations before') + ins[at:at] = [spvedit.mk(spvedit.OP_DECORATE, i, DECORATION_NO_CONTRACTION) for i in ids] + words = h + [v for x in ins for v in x] + open(dst, 'wb').write(struct.pack('<%dI' % len(words), *words)) + print('nocontract: %d results decorated' % len(ids)) + + +if __name__ == '__main__': + main(*sys.argv[1:3]) diff --git a/packages/cultmath/tools/device/run-lavapipe.sh b/packages/cultmath/tools/device/run-lavapipe.sh index 4505aca7..2cbb1aaa 100644 --- a/packages/cultmath/tools/device/run-lavapipe.sh +++ b/packages/cultmath/tools/device/run-lavapipe.sh @@ -7,7 +7,9 @@ # SUMMARY= tools/stopgap/ygg-verify.sh mcr.microsoft.com/dotnet/sdk:10.0 'bash packages/cultmath/tools/device/run-lavapipe.sh' # GLSL_DIR= (the output of tools/glsl/build-glsl.sh) runs the built # route instead of the committed one and skips the bounds judge: every lavapipe-glsl/.out must equal its -# lavapipe-hlsl/.out byte for byte, and a difference exits 1. It records no bounds. +# lavapipe-hlsl/.out byte for byte, and a difference exits 1. It records no bounds. That compare is in strict form: +# dxc gets -Gis and nocontract.py decorates every float result of both modules NoContraction, so the two routes cannot +# differ by a fused multiply-add. # WRITE_BOUNDS=1 re-records packages/cultmath/tests/CultMath.Tests/fixtures/device-bounds-lavapipe.txt and prints it # between BOUNDS-BEGIN and BOUNDS-END, because the job's clone is discarded. set -euo pipefail @@ -38,12 +40,19 @@ export CULTMATH_DEVICE_WORK=$W/work dotnet test "$TESTS" --no-build --filter "FullyQualifiedName~GlslMirrorTests.WriteDeviceWork" python3 "$DEV/gen.py" "$W/work" "$CM" +STRICT=(); [ -z "${GLSL_DIR:-}" ] || STRICT=(-Gis) while IFS=$'\t' read -r tag _ _ n _ wout; do in=$W/work/$tag.in - "$DXC" -T cs_6_0 -E main -HV 2021 -spirv -fspv-target-env=vulkan1.2 -I "$CM/shaders" "$W/work/$tag.hlsl" -Fo "$W/work/$tag.h.spv" >"$W/work/$tag.h.log" 2>&1 \ + "$DXC" -T cs_6_0 -E main -HV 2021 "${STRICT[@]}" -spirv -fspv-target-env=vulkan1.2 -I "$CM/shaders" "$W/work/$tag.hlsl" -Fo "$W/work/$tag.h.spv" >"$W/work/$tag.h.log" 2>&1 \ || { echo "dxc failed for $tag"; head -5 "$W/work/$tag.h.log"; exit 1; } glslangValidator -V --target-env vulkan1.2 -o "$W/work/$tag.g.spv" "$W/work/$tag.comp" >"$W/work/$tag.g.log" 2>&1 \ || { echo "glslang failed for $tag"; head -5 "$W/work/$tag.g.log"; exit 1; } + if [ -n "${GLSL_DIR:-}" ]; then + for m in h g; do + python3 "$DEV/nocontract.py" "$W/work/$tag.$m.spv" "$W/work/$tag.$m.nc.spv" >>"$W/nocontract.txt" + mv "$W/work/$tag.$m.nc.spv" "$W/work/$tag.$m.spv" + done + fi "$W/vkrun" "$W/work/$tag.h.spv" "$in" "$W/results/lavapipe-hlsl/$tag.out" "$n" "$wout" >>"$W/device.txt" "$W/vkrun" "$W/work/$tag.g.spv" "$in" "$W/results/lavapipe-glsl/$tag.out" "$n" "$wout" >>"$W/device.txt" done <"$W/work/manifest.txt" @@ -54,6 +63,7 @@ if [ -n "${GLSL_DIR:-}" ]; then tag=$(basename "$h" .out); total=$((total + 1)) if cmp -s "$h" "$W/results/lavapipe-glsl/$tag.out"; then equal=$((equal + 1)); else echo "differs: $tag"; fi done + echo "strict form: $(wc -l <"$W/nocontract.txt") modules rewritten, $(awk '{n += $2} END {print n}' "$W/nocontract.txt") results decorated NoContraction" echo "lavapipe-glsl equals lavapipe-hlsl: $equal/$total" [ "$equal" = "$total" ] && [ "$total" -gt 0 ] exit From 68b74eaee3b4e2e03497b0bd2bc584b7b86d531e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 17 Oct 2026 21:58:59 +0200 Subject: [PATCH 15/21] cultmath README: Shader Tooling names the strict-form gate and the overridable site capacity Co-Authored-By: Claude Opus 5.5 --- packages/cultmath/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cultmath/README.md b/packages/cultmath/README.md index b5ba974d..381b194a 100644 --- a/packages/cultmath/README.md +++ b/packages/cultmath/README.md @@ -190,7 +190,7 @@ CultMath keeps portable DXC outside git under `packages/cultmath/.tools/`: -OutputPath E:\Projects\Odin\crates\muninn-move-tracker\artifacts\shader\MoveSphereCandidate.comp.metal ``` -`tools/glsl/build-glsl.sh ` (Linux, repository root) builds the library GLSL from the HLSL with dxc, SPIRV-Cross and SPIRV-Tools and writes `core.glsl`, `cycles.glsl` and `phacelle.glsl` bodies into ``. It does not yet own the committed GLSL; `GlslLowering` still does. `GLSL_DIR= tools/device/run-lavapipe.sh` runs that output on lavapipe and compares it with the HLSL results bit for bit instead of judging the committed files. +`tools/glsl/build-glsl.sh ` (Linux, repository root) builds the library GLSL from the HLSL with dxc, SPIRV-Cross and SPIRV-Tools and writes `core.glsl`, `cycles.glsl` and `phacelle.glsl` bodies into ``. It does not yet own the committed GLSL; `GlslLowering` still does. `GLSL_DIR= tools/device/run-lavapipe.sh` runs that output on lavapipe and compares it with the HLSL results bit for bit instead of judging the committed files. That comparison is in strict form (dxc `-Gis` and a `NoContraction` decoration on every float result of both modules), and a consumer overrides `CULTMATH_MAX_SITES` in the built GLSL the same way as in HLSL: define it before including `core.glsl`. `compile-hlsl.ps1` compiles both `CultMath.hlsl` copies as a `lib_6_3` library with the x64 DXC; a warning fails it too (`-WX`). From ae838b60fd7b9325d2905d2ca9836769156a99a6 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 17 Oct 2026 22:59:55 +0200 Subject: [PATCH 16/21] glsl build: the capacity edit names the site arrays by a sentinel length, with tests for the tool refusals wrappers.py compiles the library with CULTMATH_MAX_SITES defined as a sentinel no shader file spells, so the three site arrays get an array length id of their own and spvedit.py converts exactly that constant to the overridable specialization constant (default from the HLSL guard). A library array of literal length 128 keeps its own id and stays a plain constant. build-glsl.sh gains the pipeline function and scratch variants: a macro outside a declarator, a folded expression reaching the capacity id, and a non-site float4 x[128] parameter that a consumer's override must not resize. The [128] shape arm goes: a non-site 128 array is legitimate output. Co-Authored-By: Claude Opus 5.5 --- packages/cultmath/tools/glsl/build-glsl.sh | 59 ++++++++++++++++++---- packages/cultmath/tools/glsl/partition.py | 10 ++-- packages/cultmath/tools/glsl/spvedit.py | 26 +++++++--- packages/cultmath/tools/glsl/wrappers.py | 18 +++++-- 4 files changed, 86 insertions(+), 27 deletions(-) diff --git a/packages/cultmath/tools/glsl/build-glsl.sh b/packages/cultmath/tools/glsl/build-glsl.sh index 0bef11c2..4f2a1a66 100644 --- a/packages/cultmath/tools/glsl/build-glsl.sh +++ b/packages/cultmath/tools/glsl/build-glsl.sh @@ -44,14 +44,19 @@ fi export PATH=$TOOLS/bin:$PATH echo "dxc: $("$DXC" --version | head -1)"; echo "spirv-opt: $(spirv-opt --version | head -1)" -python3 "$G/wrappers.py" "$CM/shaders" "$W/shw" -"$DXC" -T lib_6_3 -HV 2021 -spirv -fspv-target-env=universal1.5 -Od -I "$W/shw" "$W/shw/cm_wrappers.hlsl" -Fo "$W/lib.spv" >"$W/dxc.log" 2>&1 || { head -20 "$W/dxc.log"; exit 1; } -python3 "$G/spvedit.py" "$W/lib.spv" "$W/edited.spv" "$W/shw/wrappers.tsv" -spirv-val --target-env spv1.5 "$W/edited.spv" -spirv-opt --target-env=spv1.5 --eliminate-local-single-store --eliminate-dead-code-aggressive "$W/edited.spv" -o "$W/opt.spv" -spirv-val --target-env spv1.5 "$W/opt.spv" -spirv-cross "$W/opt.spv" --es --version 310 --relax-nan-checks --output "$W/cross.glsl" -python3 "$G/partition.py" "$CM/shaders" "$W/cross.glsl" "$OUT" +pipeline() { # shaders dir, work dir, out dir: HLSL to partitioned GLSL; returns 1 at the first refusal + local sh=$1 w=$2 out=$3 + mkdir -p "$w" "$out" + python3 "$G/wrappers.py" "$sh" "$w/shw" || return 1 + "$DXC" -T lib_6_3 -HV 2021 -spirv -fspv-target-env=universal1.5 -Od -I "$w/shw" "$w/shw/cm_wrappers.hlsl" -Fo "$w/lib.spv" >"$w/dxc.log" 2>&1 || { head -20 "$w/dxc.log"; return 1; } + python3 "$G/spvedit.py" "$w/lib.spv" "$w/edited.spv" "$w/shw/wrappers.tsv" "$w/shw/capacity.tsv" || return 1 + spirv-val --target-env spv1.5 "$w/edited.spv" || return 1 + spirv-opt --target-env=spv1.5 --eliminate-local-single-store --eliminate-dead-code-aggressive "$w/edited.spv" -o "$w/opt.spv" || return 1 + spirv-val --target-env spv1.5 "$w/opt.spv" || return 1 + spirv-cross "$w/opt.spv" --es --version 310 --relax-nan-checks --output "$w/cross.glsl" || return 1 + python3 "$G/partition.py" "$sh" "$w/cross.glsl" "$out" || return 1 +} +pipeline "$CM/shaders" "$W" "$OUT" || exit 1 # Consumer checks: glslang ES 3.00 fragment compiles of every include combination, and a consumer calling overloads # with an r-value argument. @@ -70,14 +75,23 @@ done printf '#define CULTMATH_MAX_SITES 4\n#include "CultMath.hlsl"\nfloat4 cap4(float3 p, float4 s[4]) { return cultmath_power_border(p, s, 4); }\n' >"$W/shw/cap4.hlsl" "$DXC" -T lib_6_3 -HV 2021 -Od -I "$W/shw" "$W/shw/cap4.hlsl" -Fo "$W/cap4.dxil" >"$W/cap4.log" 2>&1 || { echo "capacity (a) failed: dxc with CULTMATH_MAX_SITES 4"; head -8 "$W/cap4.log"; exit 1; } echo "capacity (a) ok: dxc lib_6_3 with a prior define 4 takes float4 s[4]" -cap_check() { # label, expected (ok|fail), define lines, GLSL library file, array length +consume() { # label, expected (ok|fail), define lines, GLSL library file, main body local f=$W/cap.frag - printf '#version 300 es\nprecision highp float;\nprecision highp int;\n%s\n' "$3" >"$f"; cat "$4" >>"$f" - printf 'out highp vec4 c;\nvoid main() { vec4 s[%s]; for (int i = 0; i < %s; i++) s[i] = vec4(float(i)); c = cultmath_power_border(gl_FragCoord.xyz, s, 4); }\n' "$5" "$5" >>"$f" + printf '#version 300 es +precision highp float; +precision highp int; +%s +' "$3" >"$f"; cat "$4" >>"$f" + printf 'out highp vec4 c; +void main() { %s } +' "$5" >>"$f" local got=fail; glslangValidator "$f" >"$W/cap.log" 2>&1 && got=ok [ "$got" = "$2" ] || { echo "capacity $1 failed: expected $2, got $got"; head -6 "$W/cap.log"; exit 1; } echo "capacity $1 ok: $2" } +cap_check() { # label, expected, define lines, GLSL library file, site array length + consume "$1" "$2" "$3" "$4" "vec4 s[$5]; for (int i = 0; i < $5; i++) s[i] = vec4(float(i)); c = cultmath_power_border(gl_FragCoord.xyz, s, 4);" +} D4='#define CULTMATH_MAX_SITES 4' cap_check "(b) built, define 4, vec4 s[4]" ok "$D4" "$OUT/core.glsl" 4 cap_check "(c) built, define 4, vec4 s[128]" fail "$D4" "$OUT/core.glsl" 128 @@ -98,3 +112,26 @@ if cat "$OUT"/*.glsl | grep -Eq "$SHAPE"; then echo "build shape failed:"; cat "$OUT"/*.glsl | grep -Eo "$SHAPE" | sort | uniq -c; exit 1 fi echo "build shape ok" + +# Tool refusals, and the capacity's reach: each variant appends one probe function to a scratch copy of CultMath.hlsl and +# runs the whole pipeline on it. +variant() { # label, probe HLSL; leaves the variant directory in VD + VD=$W/v/$1; rm -rf "$VD"; mkdir -p "$VD"; cp -r "$CM/shaders" "$VD/sh" + printf ' +%s +' "$2" >>"$VD/sh/CultMath.hlsl" + pipeline "$VD/sh" "$VD/w" "$VD/out" >"$VD/log" 2>&1 +} +expect_refused() { # label, probe HLSL, pattern the refusal prints + if variant "$1" "$2"; then echo "refusal $1 failed: the build passed"; exit 1; fi + grep -q "$3" "$VD/log" || { echo "refusal $1 failed: no '$3' in the log"; tail -5 "$VD/log"; exit 1; } + echo "refusal $1 ok" +} +SENTINEL=$(cut -f2 "$W/shw/capacity.tsv") +expect_refused macro-in-expression 'int cultmath_zz_probe(int n) { return n + CULTMATH_MAX_SITES; }' 'outside its guard' +expect_refused length-in-expression "uint cultmath_zz_probe(uint n) { return n + (100u + $((SENTINEL - 100))u); }" 'also used by opcode' +variant array-128 'float4 cultmath_zz_probe(float4 x[128], int n) { return x[n]; }' || { echo "array-128 failed: a library array of length 128 broke the build"; tail -5 "$VD/log"; exit 1; } +grep -Eq 'cultmath_zz_probe\(.*\[128\]' "$VD/out/core.glsl" || { echo "array-128 failed: the 128 array is not literal in core.glsl"; exit 1; } +PROBE='vec4 x[128]; c = cultmath_zz_probe(x, 1);' +consume "(f) non-site array, define 4, vec4 x[128]" ok "$D4" "$VD/out/core.glsl" "$PROBE" +consume "(f) non-site array, define 4, vec4 x[4]" fail "$D4" "$VD/out/core.glsl" 'vec4 x[4]; c = cultmath_zz_probe(x, 1);' diff --git a/packages/cultmath/tools/glsl/partition.py b/packages/cultmath/tools/glsl/partition.py index b4d25312..415c9b2b 100644 --- a/packages/cultmath/tools/glsl/partition.py +++ b/packages/cultmath/tools/glsl/partition.py @@ -6,18 +6,20 @@ Each top-level struct and function chunk goes to the file whose HLSL defines it Cycles; Phacelle). The library-header prologue and any main are dropped. Writes core.glsl, cycles.glsl and phacelle.glsl bodies, with the 13 `static const float` constants of the HLSL as `const float NAME = ;` at the head of the file that defines them. core.glsl opens with the HLSL's own CULTMATH_MAX_SITES guard lines, one line mapping -SPIRV_CROSS_CONSTANT_ID_0 to uint(CULTMATH_MAX_SITES), and SPIRV-Cross's one spec-constant block. Licence lines, include guards and the #version contract belong to the caller. +SPIRV_CROSS_CONSTANT_ID_ (CultMath's own, 'CMS1') to uint(CULTMATH_MAX_SITES), and SPIRV-Cross's one spec-constant block. Licence lines, include guards and the #version contract belong to the caller. Refuses: a static const line that is not `static const float CULTMATH_X = ;`, an unknown-origin chunk, a duplicate chunk, an unexpected prologue line, a missing or other SPIRV_CROSS_CONSTANT_ID block, and a call from a non-core chunk into a different non-core origin.""" import collections, os, re, sys +from spvedit import SPEC_ID OWNER = {'CultMath.hlsl': 'core', 'CultMath.Interval.hlsl': 'core', 'CultMath.Affine.hlsl': 'core', 'CultMath.Cycles.hlsl': 'cycles', 'CultMath.Phacelle.hlsl': 'phacelle'} CONST = re.compile(r'^static const float (CULTMATH_\w+) = (-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?);$') PROLOGUE = re.compile(r'^(#version |#extension |precision |#ifdef SPIRV_CROSS|#define SPIRV_CROSS|#endif)') GUARD = re.compile(r'#ifndef CULTMATH_MAX_SITES\n#define CULTMATH_MAX_SITES (\d+)\n#endif\n') -SPEC = re.compile(r'#ifndef SPIRV_CROSS_CONSTANT_ID_0\n#define SPIRV_CROSS_CONSTANT_ID_0 (\d+)u\n#endif\n' - r'const uint cultmath_max_sites = SPIRV_CROSS_CONSTANT_ID_0;\n?') +ID = 'SPIRV_CROSS_CONSTANT_ID_%d' % SPEC_ID +SPEC = re.compile(r'#ifndef %s\n#define %s (\d+)u\n#endif\n' + r'const uint cultmath_max_sites = %s;\n?' % (ID, ID, ID)) STRUCT, FUNC = re.compile(r'^struct (\w+)$'), re.compile(r'^\w+ (\w+)\(.*\)$') @@ -87,7 +89,7 @@ def main(shaders, glsl, outdir): files[o].append(body) os.makedirs(outdir, exist_ok=True) for o in OWNER.values(): - head = [guard.group(0).rstrip('\n'), '#define SPIRV_CROSS_CONSTANT_ID_0 uint(CULTMATH_MAX_SITES)', spec.group(0).rstrip('\n')] if o == 'core' else [] + head = [guard.group(0).rstrip('\n'), '#define %s uint(CULTMATH_MAX_SITES)' % ID, spec.group(0).rstrip('\n')] if o == 'core' else [] text = '\n'.join(head + consts[o] + files[o]) + '\n' open(os.path.join(outdir, o + '.glsl'), 'w').write(text) print('partition:', {o: len(files[o]) for o in sorted(files)}, 'constants', sum(len(v) for v in consts.values())) diff --git a/packages/cultmath/tools/glsl/spvedit.py b/packages/cultmath/tools/glsl/spvedit.py index 560ca870..d1b3e169 100644 --- a/packages/cultmath/tools/glsl/spvedit.py +++ b/packages/cultmath/tools/glsl/spvedit.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """The one instruction-level SPIR-V edit of the GLSL build. Binary, never text. -usage: spvedit.py +usage: spvedit.py 1. Keeps only the capabilities Shader and Linkage and drops every OpExtension. 2. Deletes each wrapper function with its OpName and decorations, moves its LinkageAttributes Export to its callee, and sets the callee's OpName to the source name. Refuses a wrapper that is not exactly one OpFunctionCall. @@ -14,6 +14,7 @@ import struct, sys OP_NAME, OP_MEMBER_NAME, OP_CAPABILITY, OP_EXTENSION, OP_DECORATE, OP_MEMBER_DECORATE = 5, 6, 17, 10, 71, 72 OP_TYPE_ARRAY, OP_CONSTANT, OP_SPEC_CONSTANT, DECORATION_SPEC_ID = 28, 43, 50, 1 +SPEC_ID = 0 OP_TYPE_POINTER, OP_TYPE_FUNCTION, OP_FUNCTION, OP_PARAMETER, OP_FUNCTION_END = 32, 33, 54, 55, 56 OP_CALL, OP_LABEL, OP_VARIABLE, OP_LOAD, OP_STORE = 57, 248, 59, 61, 62 CAP_SHADER, CAP_LINKAGE, DECORATION_LINKAGE, LINKAGE_EXPORT, FUNCTION_STORAGE = 1, 5, 41, 41, 7 @@ -178,10 +179,12 @@ def by_value(h, ins): return out, len(newtype), len(newid) -def capacity_edit(h, ins, rows, callee_of): +def capacity_edit(h, ins, rows, callee_of, default, sentinel): """Each capacity parameter (wrappers.tsv column 5) of each callee has an OpTypeArray type. Exactly one length id may serve all of them, and nothing but OpTypeArray, OpName and decorations may use it. That OpConstant becomes an - OpSpecConstant with SpecId 0 named cultmath_max_sites. Returns the instructions and the spec constant id.""" + OpSpecConstant with SpecId SPEC_ID named cultmath_max_sites, defaulting to the HLSL guard value. The constant is the + sentinel wrappers.py compiled the library with, so no array outside the capacity parameters shares its length id. + Returns the instructions and the spec constant id.""" ftype = {x[1]: x for x in ins if op(x) == OP_TYPE_FUNCTION} arrays = {x[1]: x for x in ins if op(x) == OP_TYPE_ARRAY} params, cur = {}, None @@ -201,6 +204,12 @@ def capacity_edit(h, ins, rows, callee_of): if len(lengths) != 1: sys.exit('refuse: %d array length ids serve the capacity parameters, expected exactly one' % len(lengths)) cap = lengths.pop() + value = next(x[3] for x in ins if op(x) == OP_CONSTANT and x[2] == cap) + if value != sentinel: + sys.exit('refuse: the capacity length constant is %d, expected the sentinel %d' % (value, sentinel)) + site_types = {params[fid][int(c)] for row, fid in zip(rows, callee_of) for c in row[4].split(',') if c} + if {t for t, a in arrays.items() if a[3] == cap} != site_types: + sys.exit('refuse: an array type of the capacity length is not a capacity parameter type') for x in ins: o = op(x) defines = o in (OP_CONSTANT, OP_SPEC_CONSTANT) and x[2] == cap @@ -210,25 +219,26 @@ def capacity_edit(h, ins, rows, callee_of): out = [] for x in ins: if op(x) == OP_CONSTANT and x[2] == cap: - x = [(x[0] & 0xffff0000) | OP_SPEC_CONSTANT] + x[1:] + x = [(x[0] & 0xffff0000) | OP_SPEC_CONSTANT, x[1], x[2], default] out.append(x) first = next((k for k, x in enumerate(out) if op(x) in (OP_DECORATE, OP_MEMBER_DECORATE)), None) if first is None: sys.exit('refuse: no annotation section to decorate the capacity constant in') - out[first:first] = [mk(OP_NAME, cap, *encode('cultmath_max_sites')), mk(OP_DECORATE, cap, DECORATION_SPEC_ID, 0)] + out[first:first] = [mk(OP_NAME, cap, *encode('cultmath_max_sites')), mk(OP_DECORATE, cap, DECORATION_SPEC_ID, SPEC_ID)] return out, cap -def main(src, dst, tsv): +def main(src, dst, tsv, capacity): h, ins = load(src) rows = [l.split('\t') for l in open(tsv).read().splitlines() if l] ins, callee_of = export_edit(h, ins, rows) ins, changed, params = by_value(h, ins) - ins, spec = capacity_edit(h, ins, rows, callee_of) + default, sentinel = [int(v) for v in open(capacity).read().split()] + ins, spec = capacity_edit(h, ins, rows, callee_of, default, sentinel) words = h + [v for x in ins for v in x] open(dst, 'wb').write(struct.pack('<%dI' % len(words), *words)) print('spvedit: %d wrappers dropped; by-value: %d functions changed, %d parameters; capacity spec constant id %d' % (len(rows), changed, params, spec)) if __name__ == '__main__': - main(*sys.argv[1:4]) + main(*sys.argv[1:5]) diff --git a/packages/cultmath/tools/glsl/wrappers.py b/packages/cultmath/tools/glsl/wrappers.py index 96c1c7b5..742881ac 100644 --- a/packages/cultmath/tools/glsl/wrappers.py +++ b/packages/cultmath/tools/glsl/wrappers.py @@ -3,7 +3,9 @@ every overload as its own function. The shaders are copied unchanged; the unique names exist only in . usage: wrappers.py -Writes /cm_wrappers.hlsl and /wrappers.tsv (wrapper, name, file, params, capacity). An argument is the +Writes /cm_wrappers.hlsl, /wrappers.tsv (wrapper, name, file, params, capacity) and +/capacity.tsv (the guard default, the sentinel). cm_wrappers.hlsl defines CULTMATH_MAX_SITES as the sentinel, +a number no shader file spells, so the site arrays get an array length id of their own and no other array shares it. An argument is the parameter's declarator name with any [..] suffix removed; capacity lists the indices of the parameters declared [CULTMATH_MAX_SITES]. Definitions with a Texture or Sampler parameter get no wrapper. Refuses a file where the definition-looking lines differ from the parsed ones, and a file that names CULTMATH_MAX_SITES anywhere but its @@ -13,17 +15,18 @@ import os, re, shutil, sys FILES = ['CultMath.hlsl', 'CultMath.Interval.hlsl', 'CultMath.Affine.hlsl', 'CultMath.Cycles.hlsl', 'CultMath.Phacelle.hlsl'] SIG = re.compile(r'(?m)^(\w+)[ \t]+(cultmath_\w+)[ \t]*\(([^)]*)\)') LOOKS = re.compile(r'(?m)^\w+ cultmath_\w+\(') -GUARD = re.compile(r'#ifndef CULTMATH_MAX_SITES\n#define CULTMATH_MAX_SITES \d+\n#endif\n') +GUARD = re.compile(r'#ifndef CULTMATH_MAX_SITES\n#define CULTMATH_MAX_SITES (\d+)\n#endif\n') COMMENT = re.compile(r'//[^\n]*|/\*.*?\*/', re.S) CAP = '[CULTMATH_MAX_SITES]' def main(shaders, out): shutil.copytree(shaders, out, dirs_exist_ok=True) - wrappers = ['#include "CultMath.hlsl"'] - rows = [] + wrappers, rows, spelled, default = [], [], '', None for fn in FILES: text = open(os.path.join(shaders, fn)).read().replace('\r\n', '\n') + spelled += COMMENT.sub('', text) + default = default or GUARD.search(text) stray = GUARD.sub('', COMMENT.sub('', text)).replace(CAP, '') if 'CULTMATH_MAX_SITES' in stray: sys.exit('refuse: %s names CULTMATH_MAX_SITES outside its guard and array declarators' % fn) @@ -42,6 +45,13 @@ def main(shaders, out): call = '%s(%s)' % (name, ', '.join(args)) wrappers.append('export %s %s(%s) { %s%s; }' % (ret, wn, ', '.join(params), '' if ret == 'void' else 'return ', call)) rows.append('\t'.join([wn, name, fn, ', '.join(params), cap])) + if not default: + sys.exit('refuse: no shader file carries the CULTMATH_MAX_SITES guard') + sentinel = int(default.group(1)) + 1 + while re.search(r'(? Date: Sat, 17 Oct 2026 23:00:04 +0200 Subject: [PATCH 17/21] glsl build: the capacity constant carries CultMath's own SpecId, so a consumer's SPIRV_CROSS_CONSTANT_ID_0 cannot clash SPIRV-Cross names the macro from the SpecId and offers no option to rename it, so the SpecId is CultMath's own ('CMS1'); partition.py reads it from spvedit.py. Co-Authored-By: Claude Opus 5.5 --- packages/cultmath/tools/glsl/spvedit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cultmath/tools/glsl/spvedit.py b/packages/cultmath/tools/glsl/spvedit.py index d1b3e169..5d882b13 100644 --- a/packages/cultmath/tools/glsl/spvedit.py +++ b/packages/cultmath/tools/glsl/spvedit.py @@ -14,7 +14,7 @@ import struct, sys OP_NAME, OP_MEMBER_NAME, OP_CAPABILITY, OP_EXTENSION, OP_DECORATE, OP_MEMBER_DECORATE = 5, 6, 17, 10, 71, 72 OP_TYPE_ARRAY, OP_CONSTANT, OP_SPEC_CONSTANT, DECORATION_SPEC_ID = 28, 43, 50, 1 -SPEC_ID = 0 +SPEC_ID = int.from_bytes(b'CMS1', 'big') # CultMath's own id: a consumer's SPIRV_CROSS_CONSTANT_ID_n macro cannot meet it OP_TYPE_POINTER, OP_TYPE_FUNCTION, OP_FUNCTION, OP_PARAMETER, OP_FUNCTION_END = 32, 33, 54, 55, 56 OP_CALL, OP_LABEL, OP_VARIABLE, OP_LOAD, OP_STORE = 57, 248, 59, 61, 62 CAP_SHADER, CAP_LINKAGE, DECORATION_LINKAGE, LINKAGE_EXPORT, FUNCTION_STORAGE = 1, 5, 41, 41, 7 From 72af377a05e14dc57ecc6ab1e726550bc8730854 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 17 Oct 2026 23:20:48 +0200 Subject: [PATCH 18/21] build-glsl.sh: the length-in-expression refusal test spells the sentinel in hex, which dxc keeps as the capacity constant Co-Authored-By: Claude Opus 5.5 --- packages/cultmath/tools/glsl/build-glsl.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/cultmath/tools/glsl/build-glsl.sh b/packages/cultmath/tools/glsl/build-glsl.sh index 4f2a1a66..e2d07cd4 100644 --- a/packages/cultmath/tools/glsl/build-glsl.sh +++ b/packages/cultmath/tools/glsl/build-glsl.sh @@ -129,7 +129,8 @@ expect_refused() { # label, probe HLSL, pattern the refusal prints } SENTINEL=$(cut -f2 "$W/shw/capacity.tsv") expect_refused macro-in-expression 'int cultmath_zz_probe(int n) { return n + CULTMATH_MAX_SITES; }' 'outside its guard' -expect_refused length-in-expression "uint cultmath_zz_probe(uint n) { return n + (100u + $((SENTINEL - 100))u); }" 'also used by opcode' +# the sentinel spelled in hex: wrappers.py's scan reads decimal tokens, so only the length check can see it +expect_refused length-in-expression "uint cultmath_zz_probe(uint n) { return n + $(printf '0x%xu' "$SENTINEL"); }" 'also used by opcode' variant array-128 'float4 cultmath_zz_probe(float4 x[128], int n) { return x[n]; }' || { echo "array-128 failed: a library array of length 128 broke the build"; tail -5 "$VD/log"; exit 1; } grep -Eq 'cultmath_zz_probe\(.*\[128\]' "$VD/out/core.glsl" || { echo "array-128 failed: the 128 array is not literal in core.glsl"; exit 1; } PROBE='vec4 x[128]; c = cultmath_zz_probe(x, 1);' From f7369949427afccbbd83c55e21e39ad9be9cedf8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 17 Oct 2026 23:30:35 +0200 Subject: [PATCH 19/21] build-glsl.sh: test that a consumer's own SPIRV_CROSS_CONSTANT_ID_0 and the built core.glsl coexist Co-Authored-By: Claude Opus 5.5 --- packages/cultmath/tools/glsl/build-glsl.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cultmath/tools/glsl/build-glsl.sh b/packages/cultmath/tools/glsl/build-glsl.sh index e2d07cd4..270199d6 100644 --- a/packages/cultmath/tools/glsl/build-glsl.sh +++ b/packages/cultmath/tools/glsl/build-glsl.sh @@ -136,3 +136,4 @@ grep -Eq 'cultmath_zz_probe\(.*\[128\]' "$VD/out/core.glsl" || { echo "array-128 PROBE='vec4 x[128]; c = cultmath_zz_probe(x, 1);' consume "(f) non-site array, define 4, vec4 x[128]" ok "$D4" "$VD/out/core.glsl" "$PROBE" consume "(f) non-site array, define 4, vec4 x[4]" fail "$D4" "$VD/out/core.glsl" 'vec4 x[4]; c = cultmath_zz_probe(x, 1);' +consume "(g) consumer's own SPIRV_CROSS_CONSTANT_ID_0" ok '#define SPIRV_CROSS_CONSTANT_ID_0 7u' "$OUT/core.glsl" 'c = vec4(float(cultmath_max_sites));' From 263fc4b4357138b7fd3fe4f5754e443e63da845d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 17 Oct 2026 23:42:01 +0200 Subject: [PATCH 20/21] spvedit.py: drop the sentinel value check (the wrapper owns the sentinel); test the element-type check with a float3 array of the sentinel length Co-Authored-By: Claude Opus 5.5 --- packages/cultmath/tools/glsl/build-glsl.sh | 2 ++ packages/cultmath/tools/glsl/spvedit.py | 9 +++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/cultmath/tools/glsl/build-glsl.sh b/packages/cultmath/tools/glsl/build-glsl.sh index 270199d6..659ea930 100644 --- a/packages/cultmath/tools/glsl/build-glsl.sh +++ b/packages/cultmath/tools/glsl/build-glsl.sh @@ -131,6 +131,8 @@ SENTINEL=$(cut -f2 "$W/shw/capacity.tsv") expect_refused macro-in-expression 'int cultmath_zz_probe(int n) { return n + CULTMATH_MAX_SITES; }' 'outside its guard' # the sentinel spelled in hex: wrappers.py's scan reads decimal tokens, so only the length check can see it expect_refused length-in-expression "uint cultmath_zz_probe(uint n) { return n + $(printf '0x%xu' "$SENTINEL"); }" 'also used by opcode' +# the sentinel as another element type's array length: not a capacity parameter type +expect_refused other-element-same-length "float3 cultmath_zz_probe(float3 x[$(printf '0x%x' "$SENTINEL")], int n) { return x[n]; }" 'not a capacity parameter type' variant array-128 'float4 cultmath_zz_probe(float4 x[128], int n) { return x[n]; }' || { echo "array-128 failed: a library array of length 128 broke the build"; tail -5 "$VD/log"; exit 1; } grep -Eq 'cultmath_zz_probe\(.*\[128\]' "$VD/out/core.glsl" || { echo "array-128 failed: the 128 array is not literal in core.glsl"; exit 1; } PROBE='vec4 x[128]; c = cultmath_zz_probe(x, 1);' diff --git a/packages/cultmath/tools/glsl/spvedit.py b/packages/cultmath/tools/glsl/spvedit.py index 5d882b13..a5707518 100644 --- a/packages/cultmath/tools/glsl/spvedit.py +++ b/packages/cultmath/tools/glsl/spvedit.py @@ -179,7 +179,7 @@ def by_value(h, ins): return out, len(newtype), len(newid) -def capacity_edit(h, ins, rows, callee_of, default, sentinel): +def capacity_edit(h, ins, rows, callee_of, default): """Each capacity parameter (wrappers.tsv column 5) of each callee has an OpTypeArray type. Exactly one length id may serve all of them, and nothing but OpTypeArray, OpName and decorations may use it. That OpConstant becomes an OpSpecConstant with SpecId SPEC_ID named cultmath_max_sites, defaulting to the HLSL guard value. The constant is the @@ -204,9 +204,6 @@ def capacity_edit(h, ins, rows, callee_of, default, sentinel): if len(lengths) != 1: sys.exit('refuse: %d array length ids serve the capacity parameters, expected exactly one' % len(lengths)) cap = lengths.pop() - value = next(x[3] for x in ins if op(x) == OP_CONSTANT and x[2] == cap) - if value != sentinel: - sys.exit('refuse: the capacity length constant is %d, expected the sentinel %d' % (value, sentinel)) site_types = {params[fid][int(c)] for row, fid in zip(rows, callee_of) for c in row[4].split(',') if c} if {t for t, a in arrays.items() if a[3] == cap} != site_types: sys.exit('refuse: an array type of the capacity length is not a capacity parameter type') @@ -233,8 +230,8 @@ def main(src, dst, tsv, capacity): rows = [l.split('\t') for l in open(tsv).read().splitlines() if l] ins, callee_of = export_edit(h, ins, rows) ins, changed, params = by_value(h, ins) - default, sentinel = [int(v) for v in open(capacity).read().split()] - ins, spec = capacity_edit(h, ins, rows, callee_of, default, sentinel) + default = int(open(capacity).read().split()[0]) + ins, spec = capacity_edit(h, ins, rows, callee_of, default) words = h + [v for x in ins for v in x] open(dst, 'wb').write(struct.pack('<%dI' % len(words), *words)) print('spvedit: %d wrappers dropped; by-value: %d functions changed, %d parameters; capacity spec constant id %d' % (len(rows), changed, params, spec)) From ab86876362a0f251ddfa416688ff9bae7e26cdcb Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 18 Oct 2026 00:37:44 +0200 Subject: [PATCH 21/21] build-glsl.sh: delete the [128] shape arm; the shape check runs on a library array of length 128 through the real script Selection is by name, so a non-site 128 array is legitimate output. The shape check is a function and the array-128 variant passes it. Co-Authored-By: Claude Opus 5.5 --- packages/cultmath/tools/glsl/build-glsl.sh | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/cultmath/tools/glsl/build-glsl.sh b/packages/cultmath/tools/glsl/build-glsl.sh index 659ea930..3281d545 100644 --- a/packages/cultmath/tools/glsl/build-glsl.sh +++ b/packages/cultmath/tools/glsl/build-glsl.sh @@ -106,11 +106,15 @@ for part in core cycles phacelle; do echo "$part: functions $fn, distinct names $names, lines $(wc -l <"$g"), bytes $(wc -c <"$g"), sha256 $(sha256sum "$g" | cut -c1-64)" done echo "suffixed ids: $(cat "$OUT"/*.glsl | grep -oE '\bcultmath_\w+_[0-9]+\(' | sort -u | wc -l); inout: $(cat "$OUT"/*.glsl | grep -c inout || true)" -# Build shape: the lowering leaves none of these, and the NaN-check option leaves no mix(mix( expansions. -SHAPE='inout|param_var_|cmw[0-9]{3}_|export |mix\(mix\(|\[128\]|\b_cos\b|\b_sin\b' -if cat "$OUT"/*.glsl | grep -Eq "$SHAPE"; then - echo "build shape failed:"; cat "$OUT"/*.glsl | grep -Eo "$SHAPE" | sort | uniq -c; exit 1 -fi +# Build shape: the lowering leaves none of these, and the NaN-check option leaves no mix(mix( expansions. A literal +# [128] is not among them: the capacity is selected by name, so a library array of length 128 is legitimate output. +SHAPE='inout|param_var_|cmw[0-9]{3}_|export |mix\(mix\(|\b_cos\b|\b_sin\b' +shape_check() { # output directory; returns 1 when the shape fails + if cat "$1"/*.glsl | grep -Eq "$SHAPE"; then + echo "build shape failed:"; cat "$1"/*.glsl | grep -Eo "$SHAPE" | sort | uniq -c; return 1 + fi +} +shape_check "$OUT" || exit 1 echo "build shape ok" # Tool refusals, and the capacity's reach: each variant appends one probe function to a scratch copy of CultMath.hlsl and @@ -135,6 +139,7 @@ expect_refused length-in-expression "uint cultmath_zz_probe(uint n) { return n + expect_refused other-element-same-length "float3 cultmath_zz_probe(float3 x[$(printf '0x%x' "$SENTINEL")], int n) { return x[n]; }" 'not a capacity parameter type' variant array-128 'float4 cultmath_zz_probe(float4 x[128], int n) { return x[n]; }' || { echo "array-128 failed: a library array of length 128 broke the build"; tail -5 "$VD/log"; exit 1; } grep -Eq 'cultmath_zz_probe\(.*\[128\]' "$VD/out/core.glsl" || { echo "array-128 failed: the 128 array is not literal in core.glsl"; exit 1; } +shape_check "$VD/out" || { echo "array-128 failed: the shape check refused a library array of length 128"; exit 1; } PROBE='vec4 x[128]; c = cultmath_zz_probe(x, 1);' consume "(f) non-site array, define 4, vec4 x[128]" ok "$D4" "$VD/out/core.glsl" "$PROBE" consume "(f) non-site array, define 4, vec4 x[4]" fail "$D4" "$VD/out/core.glsl" 'vec4 x[4]; c = cultmath_zz_probe(x, 1);'