onejs-sl 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Where each node of a program is computed: the region tree an emitter prints.
3
+ *
4
+ * The IR is a graph, and an `if` or a `loop` (IR 4) is a node whose regions are
5
+ * the parts of the graph it runs (`ir.ts`, `ControlNode`). A node belongs to no
6
+ * region by itself; this places it in the innermost one that every use of it is
7
+ * inside. So a value only a branch needs is computed only when that branch
8
+ * runs, one a loop's body needs is computed each turn, and one that is also
9
+ * needed outside is computed once, before either. Nothing is hoisted out of the
10
+ * region it is used in, which is what an author who wrote it there expects.
11
+ *
12
+ * It also says which regions run in control flow that differs from one pixel
13
+ * to the next. A texture sample there has no neighbours to take its mip level
14
+ * from (WGSL refuses one outright), so every emitter samples at level 0 in such
15
+ * a region, and every backend draws the same picture.
16
+ */
17
+
18
+ import { INPUTS, operands, reachable, type NodeRef, type Program, type SLNode } from "./ir"
19
+ import { SLOP } from "./ops"
20
+
21
+ export interface Region {
22
+ id: number
23
+ /** The region this one is inside; null for the root, the whole function. */
24
+ parent: number | null
25
+ /** The `if` or `loop` whose region this is; null for the root. */
26
+ owner: NodeRef | null
27
+ role: "root" | "then" | "else" | "cond" | "body"
28
+ depth: number
29
+ /** Runs in control flow that differs between pixels. */
30
+ varying: boolean
31
+ /**
32
+ * The nodes placed here, each after what it reads: depth first from the
33
+ * result, operands in order, which is the order a program with no control
34
+ * flow has always been printed in.
35
+ */
36
+ nodes: NodeRef[]
37
+ }
38
+
39
+ export interface Structure {
40
+ regions: Region[]
41
+ /** The region each reachable node is placed in. */
42
+ placed: Map<NodeRef, number>
43
+ /** An `if`'s then and else regions; a `loop`'s cond and body regions (the body inside the cond). */
44
+ children: Map<NodeRef, [number, number]>
45
+ /** The values that can differ from one pixel to the next. */
46
+ varying: Set<NodeRef>
47
+ }
48
+
49
+ /** Inputs that differ per pixel. The others are one value for the whole draw. */
50
+ const PER_PIXEL = new Set<string>(["uv", "fragCoord"] satisfies Array<keyof typeof INPUTS>)
51
+
52
+ export function structure(p: Program): Structure {
53
+ const nodes = p.nodes
54
+ const reach = reachable(nodes, p.result)
55
+ const regions: Region[] = []
56
+ const placed = new Map<NodeRef, number>()
57
+ const children = new Map<NodeRef, [number, number]>()
58
+ const uses = new Map<NodeRef, number[]>()
59
+
60
+ const region = (parent: number | null, owner: NodeRef | null, role: Region["role"]): number => {
61
+ const id = regions.length
62
+ regions.push({ id, parent, owner, role, depth: parent === null ? 0 : regions[parent]!.depth + 1, varying: false, nodes: [] })
63
+ return id
64
+ }
65
+ const use = (ref: NodeRef, at: number) => {
66
+ const list = uses.get(ref)
67
+ if (list === undefined) uses.set(ref, [at])
68
+ else list.push(at)
69
+ }
70
+ const lca = (a: number, b: number): number => {
71
+ let x = a, y = b
72
+ while (regions[x]!.depth > regions[y]!.depth) x = regions[x]!.parent!
73
+ while (regions[y]!.depth > regions[x]!.depth) y = regions[y]!.parent!
74
+ while (x !== y) { x = regions[x]!.parent!; y = regions[y]!.parent! }
75
+ return x
76
+ }
77
+ const inside = (inner: number, outer: number): boolean => {
78
+ let r: number | null = inner
79
+ while (r !== null) { if (r === outer) return true; r = regions[r]!.parent }
80
+ return false
81
+ }
82
+
83
+ const root = region(null, null, "root")
84
+ use(p.result, root)
85
+ const loopRegion = new Map<number, number>()
86
+
87
+ // Latest first: every use of a node is a later node, so it is placed by then.
88
+ for (let i = reach.length - 1; i >= 0; i--) {
89
+ const ref = reach[i]!
90
+ const n = nodes[ref]!
91
+ let at: number
92
+ if (n.k === "param") {
93
+ const cond = loopRegion.get(n.loop)
94
+ if (cond === undefined) throw new Error(`node ${ref} is a param of loop ${n.loop}, which nothing reaches`)
95
+ at = cond
96
+ for (const u of uses.get(ref) ?? []) {
97
+ if (!inside(u, cond)) throw new Error(`node ${ref}, a param of loop ${n.loop}, is read outside that loop`)
98
+ }
99
+ } else {
100
+ const list = uses.get(ref)
101
+ if (list === undefined) throw new Error(`node ${ref} is reachable and has no use`)
102
+ at = list.reduce(lca)
103
+ }
104
+ placed.set(ref, at)
105
+ regions[at]!.nodes.push(ref)
106
+ if (n.k === "if") {
107
+ const then = region(at, ref, "then")
108
+ const otherwise = region(at, ref, "else")
109
+ children.set(ref, [then, otherwise])
110
+ use(n.cond, at)
111
+ for (const r of n.then) use(r, then)
112
+ for (const r of n.else) use(r, otherwise)
113
+ } else if (n.k === "loop") {
114
+ const cond = region(at, ref, "cond")
115
+ const body = region(cond, ref, "body")
116
+ children.set(ref, [cond, body])
117
+ loopRegion.set(n.id, cond)
118
+ for (const r of n.init) use(r, at)
119
+ use(n.cond, cond)
120
+ for (const r of n.next) use(r, body)
121
+ } else {
122
+ for (const a of operands(n)) use(a, at)
123
+ }
124
+ }
125
+ const order = postOrder(nodes, p.result)
126
+ for (const r of regions) r.nodes.sort((x, y) => order.get(x)! - order.get(y)!)
127
+
128
+ const varying = varyingValues(nodes, reach)
129
+ // A region varies if the one it is in does, or if its own condition does.
130
+ for (const r of regions) {
131
+ if (r.parent === null) continue
132
+ const owner = nodes[r.owner!]!
133
+ const cond = owner.k === "if" || owner.k === "loop" ? owner.cond : -1
134
+ r.varying = regions[r.parent]!.varying || varying.has(cond)
135
+ }
136
+ return { regions, placed, children, varying }
137
+ }
138
+
139
+ /** Each reachable node's place in a depth first walk from the result, operands in order, each after its operands. */
140
+ function postOrder(nodes: readonly SLNode[], result: NodeRef): Map<NodeRef, number> {
141
+ const order = new Map<NodeRef, number>()
142
+ let next = 0
143
+ const walk = (ref: NodeRef): void => {
144
+ if (order.has(ref)) return
145
+ order.set(ref, -1)
146
+ for (const a of operands(nodes[ref]!)) walk(a)
147
+ order.set(ref, next++)
148
+ }
149
+ walk(result)
150
+ return order
151
+ }
152
+
153
+ /**
154
+ * The values that can differ between pixels: the per pixel inputs, texture
155
+ * samples, and anything computed from one. A loop's params differ when their
156
+ * start, their next value or the loop's condition does, which depends on the
157
+ * params, so it is found by going round until nothing changes.
158
+ */
159
+ function varyingValues(nodes: readonly SLNode[], reach: NodeRef[]): Set<NodeRef> {
160
+ const out = new Set<NodeRef>()
161
+ const params = new Map<number, NodeRef[]>()
162
+ for (const ref of reach) {
163
+ const n = nodes[ref]!
164
+ if (n.k === "param") params.set(n.loop, [...(params.get(n.loop) ?? []), ref])
165
+ }
166
+ for (let changed = true; changed;) {
167
+ changed = false
168
+ const mark = (ref: NodeRef) => { if (!out.has(ref)) { out.add(ref); changed = true } }
169
+ for (const ref of reach) {
170
+ const n = nodes[ref]!
171
+ switch (n.k) {
172
+ case "input": if (PER_PIXEL.has(n.name)) mark(ref); break
173
+ case "call":
174
+ if (n.op === SLOP.SAMPLE || n.op === SLOP.SAMPLE_LOD || n.args.some((a) => out.has(a))) mark(ref)
175
+ break
176
+ case "swizzle": if (out.has(n.src)) mark(ref); break
177
+ case "proj": {
178
+ const src = nodes[n.src]!
179
+ if (src.k === "if" && (out.has(src.cond) || out.has(src.then[n.index]!) || out.has(src.else[n.index]!))) mark(ref)
180
+ if (src.k === "loop") {
181
+ const param = (params.get(src.id) ?? []).find((q) => (nodes[q] as { index: number }).index === n.index)
182
+ if (out.has(src.cond) || out.has(src.init[n.index]!) || out.has(src.next[n.index]!) || (param !== undefined && out.has(param))) mark(ref)
183
+ }
184
+ break
185
+ }
186
+ case "loop":
187
+ for (const q of params.get(n.id) ?? []) {
188
+ const i = (nodes[q] as { index: number }).index
189
+ if (out.has(n.cond) || out.has(n.init[i]!) || out.has(n.next[i]!)) mark(q)
190
+ }
191
+ break
192
+ default: break
193
+ }
194
+ }
195
+ }
196
+ return out
197
+ }
198
+
199
+ /**
200
+ * How a language spells the statements a structured body is printed with. The
201
+ * expressions are the emitter's own; this is only the frame around them.
202
+ */
203
+ export interface Syntax {
204
+ /** A value node's local, initialised: `float n3 = expr;` */
205
+ value(ref: NodeRef, expr: string): string
206
+ /** A local that is assigned later, typed as `like` is: an if's or a loop's result. */
207
+ mutable(name: string, like: NodeRef, init?: string): string
208
+ /** A loop's turn counter, from 0. */
209
+ counter(name: string): string
210
+ assign(name: string, value: string): string
211
+ increment(name: string): string
212
+ ifOpen(cond: string): string
213
+ elseOpen: string
214
+ close: string
215
+ loopOpen: string
216
+ /** Leaves the loop unless `cond` holds and `turns` is under `max`. */
217
+ breakUnless(cond: string, turns: string, max: number): string
218
+ }
219
+
220
+ /** A node's local. */
221
+ export const local = (ref: NodeRef): string => `n${ref}`
222
+
223
+ /**
224
+ * Prints the body: every placed node in its region, each `if` and `loop` with
225
+ * its regions inside it. `expr` prints a value node's expression; the result is
226
+ * `local(p.result)` for the caller to return.
227
+ */
228
+ export function printBody(p: Program, s: Structure, syntax: Syntax, expr: (ref: NodeRef) => string, indent: string): string[] {
229
+ const lines: string[] = []
230
+ const loopRef = new Map<number, NodeRef>()
231
+ for (const [ref, n] of p.nodes.entries()) if (n.k === "loop") loopRef.set(n.id, ref)
232
+ const result = (ref: NodeRef, i: number) => `r${ref}_${i}`
233
+ const carried = (ref: NodeRef, i: number) => `l${ref}_${i}`
234
+
235
+ const region = (id: number, ind: string): void => {
236
+ for (const ref of s.regions[id]!.nodes) {
237
+ const n = p.nodes[ref]!
238
+ switch (n.k) {
239
+ case "if": {
240
+ const [then, otherwise] = s.children.get(ref)!
241
+ n.then.forEach((r, i) => lines.push(ind + syntax.mutable(result(ref, i), r)))
242
+ lines.push(ind + syntax.ifOpen(local(n.cond)))
243
+ region(then, ind + " ")
244
+ n.then.forEach((r, i) => lines.push(ind + " " + syntax.assign(result(ref, i), local(r))))
245
+ lines.push(ind + syntax.elseOpen)
246
+ region(otherwise, ind + " ")
247
+ n.else.forEach((r, i) => lines.push(ind + " " + syntax.assign(result(ref, i), local(r))))
248
+ lines.push(ind + syntax.close)
249
+ break
250
+ }
251
+ case "loop": {
252
+ const [cond, body] = s.children.get(ref)!
253
+ const turns = `l${ref}_turns`
254
+ n.init.forEach((r, i) => lines.push(ind + syntax.mutable(carried(ref, i), r, local(r))))
255
+ lines.push(ind + syntax.counter(turns))
256
+ lines.push(ind + syntax.loopOpen)
257
+ region(cond, ind + " ")
258
+ lines.push(ind + " " + syntax.breakUnless(local(n.cond), turns, n.max))
259
+ lines.push(ind + " " + syntax.increment(turns))
260
+ region(body, ind + " ")
261
+ n.next.forEach((r, i) => lines.push(ind + " " + syntax.assign(carried(ref, i), local(r))))
262
+ lines.push(ind + syntax.close)
263
+ break
264
+ }
265
+ case "param":
266
+ lines.push(ind + syntax.value(ref, carried(loopRef.get(n.loop)!, n.index)))
267
+ break
268
+ case "proj": {
269
+ const src = p.nodes[n.src]!
270
+ lines.push(ind + syntax.value(ref, src.k === "loop" ? carried(n.src, n.index) : result(n.src, n.index)))
271
+ break
272
+ }
273
+ default:
274
+ lines.push(ind + syntax.value(ref, expr(ref)))
275
+ }
276
+ }
277
+ }
278
+ region(0, indent)
279
+ return lines
280
+ }
281
+
282
+ /** Whether a node is computed where a sample has no neighbours to take a mip level from. */
283
+ export function inVaryingFlow(s: Structure, ref: NodeRef): boolean {
284
+ return s.regions[s.placed.get(ref)!]!.varying
285
+ }
package/src/web.ts CHANGED
@@ -24,7 +24,9 @@
24
24
  * vertex index) so one module is one pipeline; the GLSL host supplies its own.
25
25
  */
26
26
 
27
- import { SLError, TYPE, type Program, type SLNode, type SLType } from "./ir"
27
+ import { INT_LIMITS } from "./body"
28
+ import { SLError, TYPE, valueAt, type NodeRef, type Program, type SLKind, type SLType, type ValueNode } from "./ir"
29
+ import { inVaryingFlow, local, printBody, structure, type Syntax } from "./structure"
28
30
  import { libClosure, libIndex, LIB_FUNCTIONS, SDF_CALLS } from "./lib"
29
31
  import { LIB_GLSL } from "./lib/glsl"
30
32
  import { LIB_WGSL } from "./lib/wgsl"
@@ -48,29 +50,39 @@ function emitWeb(p: Program, lang: WebLanguage): string {
48
50
  const need = new Set<number>()
49
51
  const smoothsteps = new Set<SLType>()
50
52
  const sampled = new Set<number>()
51
- const lines: string[] = []
52
- const emitted = new Set<number>()
53
+ const shape = structure(p)
53
54
  const T = (t: SLType) => (W ? (t === 1 ? "f32" : `vec${t}f`) : t === 1 ? "float" : `vec${t}`)
54
- const typeOf = (ref: number) => p.nodes[ref].type
55
+ /** A value's type in this language, its kind included. */
56
+ const typeName = (n: { type: SLType; kind?: SLKind }): string => {
57
+ if (n.kind === undefined) return T(n.type)
58
+ const scalar = W ? { int: "i32", uint: "u32", bool: "bool" }[n.kind] : n.kind
59
+ if (n.type === 1) return scalar
60
+ return W ? `vec${n.type}<${scalar}>` : `${{ int: "i", uint: "u", bool: "b" }[n.kind]}vec${n.type}`
61
+ }
62
+ const kindLit = (kind: SLKind, v: number): string => {
63
+ if (kind === "bool") return v !== 0 ? "true" : "false"
64
+ if (kind === "uint") return `${v >>> 0}u`
65
+ const i = W ? `${v}i` : String(v)
66
+ return v < 0 ? `(${i})` : i
67
+ }
68
+ const typeOf = (ref: number) => valueAt(p.nodes, ref).type
69
+ /** A constant node's value, or null for anything computed. */
70
+ const constantOf = (ref: number): number[] | null => {
71
+ const c = p.nodes[ref]!
72
+ return c.k === "const" ? c.v : null
73
+ }
55
74
 
56
75
  /** A node's value widened to `t` when it is a scalar and `t` is not. */
57
76
  const splat = (ref: number, t: SLType) => (typeOf(ref) === 1 && t > 1 ? `${T(t)}(n${ref})` : `n${ref}`)
58
77
  /** A literal at width `t`. */
59
78
  const k = (v: number, t: SLType) => (t > 1 ? `${T(t)}(${lit(v)})` : lit(v))
60
79
 
61
- const walk = (ref: number): void => {
62
- if (emitted.has(ref)) return
63
- const n = p.nodes[ref]
64
- const deps = n.k === "swizzle" ? [n.src] : n.k === "call" ? n.args : []
65
- for (const d of deps) walk(d)
66
- emitted.add(ref)
67
- const e = expr(n)
68
- lines.push(W ? ` let n${ref}: ${T(n.type)} = ${e};` : ` ${T(n.type)} n${ref} = ${e};`)
69
- }
70
-
71
- const expr = (n: SLNode): string => {
80
+ const expr = (ref: NodeRef): string => {
81
+ const n = valueAt(p.nodes, ref)
72
82
  switch (n.k) {
73
- case "const": return n.type === 1 ? lit(n.v[0]) : `${T(n.type)}(${n.v.map(lit).join(", ")})`
83
+ case "const":
84
+ if (n.kind !== undefined) return n.type === 1 ? kindLit(n.kind, n.v[0]!) : `${typeName(n)}(${n.v.map((v) => kindLit(n.kind!, v)).join(", ")})`
85
+ return n.type === 1 ? lit(n.v[0]) : `${T(n.type)}(${n.v.map(lit).join(", ")})`
74
86
  case "input": {
75
87
  const res = W ? "sl.res" : "sl_Res"
76
88
  switch (INPUT_ID[n.name]) {
@@ -87,14 +99,15 @@ function emitWeb(p: Program, lang: WebLanguage): string {
87
99
  }
88
100
  case "swizzle": {
89
101
  // A scalar has one component and no swizzle in either language.
90
- if (typeOf(n.src) === 1) return n.type === 1 ? `n${n.src}` : `${T(n.type)}(n${n.src})`
102
+ if (typeOf(n.src) === 1) return n.type === 1 ? `n${n.src}` : `${typeName(n)}(n${n.src})`
91
103
  return `n${n.src}.${n.chans.map((c) => "xyzw"[c]).join("")}`
92
104
  }
93
- case "call": return call(n)
105
+ case "call": return call(n, ref)
106
+ default: throw new SLError(`the ${lang.toUpperCase()} emitter reached a ${n.k} as an expression`)
94
107
  }
95
108
  }
96
109
 
97
- const call = (n: Extract<SLNode, { k: "call" }>): string => {
110
+ const call = (n: Extract<ValueNode, { k: "call" }>, ref: NodeRef): string => {
98
111
  const t = n.type
99
112
  const a = n.args.map((r) => `n${r}`)
100
113
  // Every argument at the result's width, for the element wise ops. WGSL
@@ -109,8 +122,55 @@ function emitWeb(p: Program, lang: WebLanguage): string {
109
122
  return W ? LIB_FUNCTIONS[i]!.wgsl : name
110
123
  }
111
124
  const hlslType = (w: SLType) => (w === 1 ? "float" : `float${w}`)
125
+ const argKind = n.args.length > 0 ? valueAt(p.nodes, n.args[0]!).kind : undefined
126
+ // IR 4: the ops that take ints, uints and bools, by the kinds involved.
127
+ if (n.kind !== undefined || argKind !== undefined) {
128
+ const zero = kindLit(n.kind ?? "int", 0)
129
+ /** `c ? x : y`, which WGSL spells select(y, x, c). */
130
+ const pick = (c: string, x: string, y: string) => (W ? `select(${y}, ${x}, ${c})` : `(${c} ? ${x} : ${y})`)
131
+ switch (n.op) {
132
+ case SLOP.ADD: return `(${a[0]} + ${a[1]})`
133
+ case SLOP.SUB: return `(${a[0]} - ${a[1]})`
134
+ case SLOP.MUL: return `(${a[0]} * ${a[1]})`
135
+ // Truncating, and 0 for a zero divisor. GLSL ES leaves % of a
136
+ // negative undefined, so it is written out from the division.
137
+ case SLOP.DIV: return pick(`${a[1]} == ${zero}`, zero, `${a[0]} / ${a[1]}`)
138
+ case SLOP.MOD: return pick(`${a[1]} == ${zero}`, zero, W ? `${a[0]} % ${a[1]}` : `${a[0]} - ${a[1]} * (${a[0]} / ${a[1]})`)
139
+ case SLOP.NEG: return `(-${a[0]})`
140
+ case SLOP.MIN: return `min(${a[0]}, ${a[1]})`
141
+ case SLOP.MAX: return `max(${a[0]}, ${a[1]})`
142
+ case SLOP.ABS: return `abs(${a[0]})`
143
+ case SLOP.CLAMP: return `clamp(${a[0]}, ${a[1]}, ${a[2]})`
144
+ }
145
+ }
112
146
  switch (n.op) {
113
- case SLOP.COMPOSE: return t === 1 ? a[0] : `${T(t)}(${a.join(", ")})`
147
+ case SLOP.COMPOSE: return t === 1 ? a[0] : `${typeName(n)}(${a.join(", ")})`
148
+ case SLOP.CAST: {
149
+ const from = valueAt(p.nodes, n.args[0]!).kind
150
+ if (n.kind === "bool") return `(${a[0]} != ${from === undefined ? "0.0" : kindLit(from, 0)})`
151
+ if (n.kind !== undefined && from === undefined) {
152
+ const [lo, hi] = INT_LIMITS[n.kind]
153
+ return `${typeName(n)}(clamp(${a[0]}, ${lo}, ${hi}))`
154
+ }
155
+ return `${typeName(n)}(${a[0]})`
156
+ }
157
+ case SLOP.LT: return `(${a[0]} < ${a[1]})`
158
+ case SLOP.LE: return `(${a[0]} <= ${a[1]})`
159
+ case SLOP.GT: return `(${a[0]} > ${a[1]})`
160
+ case SLOP.GE: return `(${a[0]} >= ${a[1]})`
161
+ case SLOP.EQ: return `(${a[0]} == ${a[1]})`
162
+ case SLOP.NE: return `(${a[0]} != ${a[1]})`
163
+ case SLOP.AND: return `(${a[0]} && ${a[1]})`
164
+ case SLOP.OR: return `(${a[0]} || ${a[1]})`
165
+ case SLOP.NOT: return `(!${a[0]})`
166
+ case SLOP.BIT_AND: return `(${a[0]} & ${a[1]})`
167
+ case SLOP.BIT_OR: return `(${a[0]} | ${a[1]})`
168
+ case SLOP.BIT_XOR: return `(${a[0]} ^ ${a[1]})`
169
+ case SLOP.BIT_NOT: return `(~${a[0]})`
170
+ // The count modulo 32, which GLSL leaves undefined past 31; WGSL shifts by a u32.
171
+ case SLOP.SHL: return W ? `(${a[0]} << (u32(${a[1]}) & 31u))` : `(${a[0]} << (${a[1]} & ${kindLit(valueAt(p.nodes, n.args[1]!).kind ?? "int", 31)}))`
172
+ case SLOP.SHR: return W ? `(${a[0]} >> (u32(${a[1]}) & 31u))` : `(${a[0]} >> (${a[1]} & ${kindLit(valueAt(p.nodes, n.args[1]!).kind ?? "int", 31)}))`
173
+ case SLOP.CHOOSE: return W ? `select(${a[2]}, ${a[1]}, ${a[0]})` : `(${a[0]} ? ${a[1]} : ${a[2]})`
114
174
 
115
175
  case SLOP.ADD: return `(${s[0]} + ${s[1]})`
116
176
  case SLOP.SUB: return `(${s[0]} - ${s[1]})`
@@ -168,16 +228,29 @@ function emitWeb(p: Program, lang: WebLanguage): string {
168
228
  case SLOP.HSV2RGB: return `${lib("sl_hsv2rgb")}(${a[0]})`
169
229
  case SLOP.NOISE: return `${lib("sl_valueNoise")}(${a[0]})`
170
230
  case SLOP.SIMPLEX: return `${lib("sl_simplex")}(${a[0]})`
171
- case SLOP.FBM: return octaveCall(Math.round(imm[1] ?? 0), a[0], imm[0])
172
- case SLOP.TURBULENCE: return octaveCall(2, a[0], imm[0])
173
- case SLOP.RIDGED: return octaveCall(3, a[0], imm[0])
231
+ case SLOP.FBM: return octaveCall(Math.round((n.args.length > 1 ? imm[0] : imm[1]) ?? 0), a[0])
232
+ case SLOP.TURBULENCE: return octaveCall(2, a[0])
233
+ case SLOP.RIDGED: return octaveCall(3, a[0])
174
234
  case SLOP.VORONOI: return `${lib("sl_voronoi")}(${a[0]})`
175
- case SLOP.SDF: return sdfCall(Math.round(imm[0] ?? 0), a[0], imm)
235
+ case SLOP.SDF: return sdfCall(Math.round(imm[0] ?? 0), a[0], sdfParams())
176
236
  case SLOP.SAMPLE: {
177
237
  const slot = Math.round(imm[0] ?? 0)
178
238
  sampled.add(slot)
239
+ // Where pixels part ways there are no neighbours to take a mip
240
+ // level from, and WGSL refuses the implicit sample outright, so
241
+ // every backend samples level 0 there (`structure.ts`).
242
+ if (inVaryingFlow(shape, ref)) {
243
+ return W ? `textureSampleLevel(sl_tex${slot}, sl_samp${slot}, ${a[0]}, 0.0)` : `textureLod(sl_Tex${slot}, ${a[0]}, 0.0)`
244
+ }
179
245
  return W ? `textureSample(sl_tex${slot}, sl_samp${slot}, ${a[0]})` : `texture(sl_Tex${slot}, ${a[0]})`
180
246
  }
247
+ case SLOP.SAMPLE_LOD: {
248
+ const slot = Math.round(imm[0] ?? 0)
249
+ sampled.add(slot)
250
+ return W
251
+ ? `textureSampleLevel(sl_tex${slot}, sl_samp${slot}, ${a[0]}, ${a[1]})`
252
+ : `textureLod(sl_Tex${slot}, ${a[0]}, ${a[1]})`
253
+ }
181
254
  default:
182
255
  throw new SLError(
183
256
  `the ${lang.toUpperCase()} emitter has no case for opcode ${n.op}. A program using it would ` +
@@ -188,32 +261,78 @@ function emitWeb(p: Program, lang: WebLanguage): string {
188
261
  /**
189
262
  * The library's onejsFbmKind, resolved here since the kind is a
190
263
  * constant: calling the dispatcher would print a WGSL `select` that
191
- * runs two of the four fields to keep one.
264
+ * runs two of the four fields to keep one. The count is a literal when
265
+ * it is a constant, and otherwise rounded and held to 1 to 4 as
266
+ * `sl_fbm` holds it.
192
267
  */
193
- function octaveCall(kind: number, pt: string, octaves: number | undefined): string {
194
- const o = Math.min(4, Math.max(1, Math.round(octaves ?? 3)))
268
+ function octaveCall(kind: number, pt: string): string {
269
+ const c = constantOf(n.args[1]!)
270
+ const o = c !== null
271
+ ? String(Math.min(4, Math.max(1, Math.round(c[0]!))))
272
+ : `clamp(${W ? "i32" : "int"}(floor(${a[1]} + 0.5)), 1, 4)`
195
273
  const fn = kind === 2 ? "onejsTurbulence" : kind === 3 ? "onejsRidged" : kind === 1 ? "onejsFbmSimplex" : "onejsFbm"
196
274
  return `${lib(fn, ["float2", "float", "int", "float", "float"])}(${pt}, 0.0, ${o}, 2.0, 0.5)`
197
275
  }
198
276
 
277
+ /**
278
+ * The six shape parameters, each as a float expression and, when it
279
+ * is a constant, its number: literals from a constant float4 or
280
+ * float2, and their components otherwise.
281
+ */
282
+ function sdfParams(): Array<{ text: string; value?: number }> {
283
+ const part = (ref: number, chans: string[]) => {
284
+ const c = constantOf(ref)
285
+ return chans.map((ch, i) => c !== null ? { text: lit(c[i]!), value: c[i]! } : { text: `${a[n.args.indexOf(ref)]}.${ch}` })
286
+ }
287
+ return [...part(n.args[1]!, ["x", "y", "z", "w"]), ...part(n.args[2]!, ["x", "y"])]
288
+ }
289
+
199
290
  /** The shape is a constant, so this calls it directly instead of sl_sdfDistance's switch. */
200
- function sdfCall(id: number, pt: string, im: number[]): string {
291
+ function sdfCall(id: number, pt: string, v: Array<{ text: string; value?: number }>): string {
201
292
  const shape = SDF_CALLS[id]
202
293
  // sl_sdfDistance returns 1e6 for an id it does not know.
203
294
  if (shape === undefined) return lit(1e6)
204
295
  const fn = lib(shape.fn)
205
- const v = [im[1] ?? 0, im[2] ?? 0, im[3] ?? 0, im[4] ?? 0, im[5] ?? 0, im[6] ?? 0]
206
296
  const args = shape.args.map((arg) => {
207
- if ("int" in arg) return String(Math.trunc(v[arg.int]))
208
- const parts = arg.map((i) => lit(v[i]))
297
+ // Truncated toward zero, as HLSL's int() of the float4 does.
298
+ if ("int" in arg) {
299
+ const p = v[arg.int]!
300
+ return p.value !== undefined ? String(Math.trunc(p.value)) : W ? `i32(${p.text})` : `int(${p.text})`
301
+ }
302
+ const parts = arg.map((i) => v[i]!.text)
209
303
  return parts.length === 1 ? parts[0] : `${T(parts.length as SLType)}(${parts.join(", ")})`
210
304
  })
211
305
  return `${fn}(${[pt, ...args].join(", ")})`
212
306
  }
213
307
  }
214
308
 
215
- walk(p.result)
216
- const body = lines.join("\n")
309
+ const vtype = (ref: NodeRef) => typeName(valueAt(p.nodes, ref))
310
+ const syntax: Syntax = W
311
+ ? {
312
+ value: (ref, e) => `let ${local(ref)}: ${vtype(ref)} = ${e};`,
313
+ mutable: (n, like, init) => `var ${n}: ${vtype(like)}${init === undefined ? "" : ` = ${init}`};`,
314
+ counter: (n) => `var ${n}: i32 = 0;`,
315
+ assign: (n, v) => `${n} = ${v};`,
316
+ increment: (n) => `${n} = ${n} + 1;`,
317
+ ifOpen: (c) => `if (${c}) {`,
318
+ elseOpen: "} else {",
319
+ close: "}",
320
+ loopOpen: "loop {",
321
+ breakUnless: (c, turns, max) => `if (!(${c}) || ${turns} >= ${max}) { break; }`,
322
+ }
323
+ : {
324
+ value: (ref, e) => `${vtype(ref)} ${local(ref)} = ${e};`,
325
+ mutable: (n, like, init) => `${vtype(like)} ${n}${init === undefined ? "" : ` = ${init}`};`,
326
+ counter: (n) => `int ${n} = 0;`,
327
+ assign: (n, v) => `${n} = ${v};`,
328
+ increment: (n) => `${n}++;`,
329
+ ifOpen: (c) => `if (${c}) {`,
330
+ elseOpen: "} else {",
331
+ close: "}",
332
+ loopOpen: "for (;;) {",
333
+ breakUnless: (c, turns, max) => `if (!(${c}) || ${turns} >= ${max}) break;`,
334
+ }
335
+ const body = printBody(p, shape, syntax, expr, " ").join("\n")
217
336
  const library = [...[...smoothsteps].sort().map((w) => smoothstep(w, lang)), ...librarySource(need, lang)].join("\n")
218
337
  const header = `// GENERATED from a shader language program (${p.hash}). Do not edit.`
219
338
  // Only the textures the program samples. WebGPU's automatic layout leaves