onejs-sl 0.4.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.
package/src/sl.ts CHANGED
@@ -24,10 +24,10 @@
24
24
 
25
25
  import { parseColor as parseHex } from "./color"
26
26
  import {
27
- Builder, INPUTS, RAMP_STOP_COMPUTED, SLError, TYPE, hashProgram, programVersion, widthName, writtenColour,
28
- type InputName, type NodeRef, type Program, type SLNode, type SLType, type UniformControl,
27
+ Builder, INPUTS, RAMP_STOP_COMPUTED, SLError, TYPE, hashProgram, programVersion, truncateHeld, widthName, writtenColour,
28
+ type InputName, type NodeRef, type Program, type SLKind, type SLNode, type SLType, type UniformControl,
29
29
  } from "./ir"
30
- import { SLOP, type SLOpCode } from "./ops"
30
+ import { SLOP, SL_HLSL, SL_NAME, type SLOpCode } from "./ops"
31
31
  import { SL_SDF_PARAMS, SL_SDF_SHAPES, type SlSdfKind } from "./shapes"
32
32
 
33
33
  // MARK: values
@@ -55,7 +55,8 @@ export type Num = Val | number
55
55
  // point rather than a hazard; see the comment above that loop.
56
56
  // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
57
57
  export class Val {
58
- constructor(readonly owner: Builder, readonly ref: NodeRef, readonly width: SLType) {}
58
+ /** `kind` is what the value holds beside its width: an int, a uint or a bool. None means floats. */
59
+ constructor(readonly owner: Builder, readonly ref: NodeRef, readonly width: SLType, readonly kind?: SLKind) {}
59
60
 
60
61
  // Arithmetic. Mixing a vector with a Float broadcasts, as in HLSL.
61
62
  add(o: Num): this { return bin(SLOP.ADD, this, o) as this }
@@ -73,6 +74,11 @@ export class Val {
73
74
  min(o: Num): this { return bin(SLOP.MIN, this, o) as this }
74
75
  max(o: Num): this { return bin(SLOP.MAX, this, o) as this }
75
76
  clamp(lo: Num, hi: Num): this {
77
+ if ([this, lo, hi].some((v) => typeof v !== "number" && v.kind !== undefined)) {
78
+ const u = unify([this, lo, hi])
79
+ if (u.kind !== undefined) return mk(u.owner, u.owner.call(SLOP.CLAMP, u.width, u.refs, undefined, u.kind), u.width, u.kind) as this
80
+ return (mk(u.owner, u.refs[0]!, u.width) as Val).clamp(mk(u.owner, u.refs[1]!, u.width) as Val, mk(u.owner, u.refs[2]!, u.width) as Val) as this
81
+ }
76
82
  const { refs, width } = alignN([this, lo, hi])
77
83
  return mk(this.owner, this.owner.call(SLOP.CLAMP, width, refs), width) as this
78
84
  }
@@ -89,6 +95,27 @@ export class Val {
89
95
  }
90
96
  normalize(): this { return un(SLOP.NORMALIZE, this) as this }
91
97
 
98
+ // Comparisons give a bool per component; an int meeting a float is compared as a float.
99
+ lt(o: Num): Bool { return compare(SLOP.LT, this, o) }
100
+ le(o: Num): Bool { return compare(SLOP.LE, this, o) }
101
+ gt(o: Num): Bool { return compare(SLOP.GT, this, o) }
102
+ ge(o: Num): Bool { return compare(SLOP.GE, this, o) }
103
+ eq(o: Num): Bool { return compare(SLOP.EQ, this, o) }
104
+ ne(o: Num): Bool { return compare(SLOP.NE, this, o) }
105
+
106
+ // Logic, on bools.
107
+ and(o: Val | boolean): Bool { return logic(SLOP.AND, this, o) }
108
+ or(o: Val | boolean): Bool { return logic(SLOP.OR, this, o) }
109
+ not(): Bool { return logic(SLOP.NOT, this) }
110
+
111
+ // Bits, on ints and uints. A shift takes its count modulo 32 on every backend.
112
+ band(o: Num): this { return bits(SLOP.BIT_AND, this, o) as this }
113
+ bor(o: Num): this { return bits(SLOP.BIT_OR, this, o) as this }
114
+ bxor(o: Num): this { return bits(SLOP.BIT_XOR, this, o) as this }
115
+ shl(o: Num): this { return bits(SLOP.SHL, this, o) as this }
116
+ shr(o: Num): this { return bits(SLOP.SHR, this, o) as this }
117
+ bnot(): this { return bits(SLOP.BIT_NOT, this) as this }
118
+
92
119
  /**
93
120
  * Arbitrary swizzle. The common single and full width ones are also getters
94
121
  * (`p.x`, `c.rgb`); this covers everything else and is typed by its argument,
@@ -106,7 +133,7 @@ export class Val {
106
133
  chans.push(c)
107
134
  }
108
135
  const t = chans.length as SLType
109
- return mk(this.owner, this.owner.add({ k: "swizzle", type: t, src: this.ref, chans }), t) as Widen<S>
136
+ return mk(this.owner, this.owner.add(withKind({ k: "swizzle", type: t, src: this.ref, chans }, this.kind)), t, this.kind) as Widen<S>
110
137
  }
111
138
 
112
139
  get x(): Float { return this.swz("x") }
@@ -160,9 +187,18 @@ export interface Float extends Val { readonly width: 1 }
160
187
  export interface Vec2 extends Val { readonly width: 2 }
161
188
  export interface Vec3 extends Val { readonly width: 3 }
162
189
  export interface Vec4 extends Val { readonly width: 4 }
190
+ /** A value holding a bool, an int or a uint rather than floats. */
191
+ export interface Bool extends Val { readonly width: 1; readonly kind: "bool" }
192
+ export interface Int extends Val { readonly width: 1; readonly kind: "int" }
193
+ export interface UInt extends Val { readonly width: 1; readonly kind: "uint" }
163
194
 
164
- function mk(b: Builder, ref: NodeRef, width: SLType): any {
165
- return new Val(b, ref, width)
195
+ function mk(b: Builder, ref: NodeRef, width: SLType, kind?: SLKind): any {
196
+ return new Val(b, ref, width, kind)
197
+ }
198
+
199
+ /** A node with its kind, which a float node leaves out so it keys and hashes as it always did. */
200
+ function withKind<T extends SLNode>(n: T, kind: SLKind | undefined): T {
201
+ return kind === undefined ? n : { ...n, kind }
166
202
  }
167
203
 
168
204
  // MARK: recording context
@@ -233,19 +269,222 @@ function alignN(vs: Num[]): { refs: NodeRef[]; width: SLType } {
233
269
 
234
270
  /** float -> vecN by repeating the component. */
235
271
  function broadcast(v: Val, width: SLType): NodeRef {
236
- return v.owner.add({ k: "swizzle", type: width, src: v.ref, chans: new Array(width).fill(0) })
272
+ return v.owner.add(withKind({ k: "swizzle", type: width, src: v.ref, chans: new Array(width).fill(0) }, v.kind))
237
273
  }
238
274
 
239
275
  function bin(op: SLOpCode, a: Val, o: Num): Val {
276
+ if (a.kind !== undefined || (typeof o !== "number" && o.kind !== undefined)) return kindedBin(op, a, o)
240
277
  const [x, y] = align2(a, o)
241
278
  const width = typeof o === "number" ? a.width : (Math.max(a.width, o.width) as SLType)
242
279
  return mk(a.owner, a.owner.call(op, width, [x, y]), width)
243
280
  }
244
281
 
245
282
  function un(op: SLOpCode, a: Val): Val {
283
+ if (a.kind !== undefined) {
284
+ if ((op === SLOP.NEG || op === SLOP.ABS) && a.kind === "int") return mk(a.owner, a.owner.call(op, a.width, [a.ref], undefined, a.kind), a.width, a.kind)
285
+ a = toFloat(a)
286
+ }
246
287
  return mk(a.owner, a.owner.call(op, a.width, [a.ref]), a.width)
247
288
  }
248
289
 
290
+ // MARK: kinds (IR 4)
291
+
292
+ /** The ops an int or a uint keeps its kind through. Anything else takes it as a float. */
293
+ const INT_OPS = new Set<number>([SLOP.ADD, SLOP.SUB, SLOP.MUL, SLOP.DIV, SLOP.MOD, SLOP.MIN, SLOP.MAX, SLOP.CLAMP])
294
+
295
+ /** A value as floats: itself when it already is, a conversion otherwise (a bool is 0 or 1). */
296
+ export function toFloat(v: Val): Val {
297
+ if (v.kind === undefined) return v
298
+ return mk(v.owner, v.owner.call(SLOP.CAST, v.width, [v.ref]), v.width)
299
+ }
300
+
301
+ /**
302
+ * The operands of an op brought to one kind and one width.
303
+ *
304
+ * Two ints stay ints, and a whole number beside one is an int too. Anything
305
+ * else meeting a float, a bool among them, becomes a float, as HLSL converts
306
+ * it. An int and a uint are refused, since which one wins is a guess.
307
+ */
308
+ function unify(vs: Num[]): { refs: NodeRef[]; width: SLType; kind: SLKind | undefined; owner: Builder } {
309
+ let owner: Builder | null = null
310
+ const kinds = new Set<SLKind | undefined>()
311
+ let width: SLType = 1
312
+ for (const v of vs) {
313
+ if (typeof v === "number") continue
314
+ if (owner === null) owner = v.owner
315
+ else if (v.owner !== owner) throw new SLError("a value from another program cannot be used in this one")
316
+ kinds.add(v.kind)
317
+ if (v.width > width) width = v.width
318
+ }
319
+ const b = owner ?? ctx()
320
+ if (kinds.has("int") && kinds.has("uint")) {
321
+ throw new SLError("cannot combine an int with a uint; convert one with int(...) or uint(...)")
322
+ }
323
+ let kind = kinds.size === 1 ? [...kinds][0] : undefined
324
+ if (kind === "bool") kind = undefined
325
+ if (kind !== undefined && vs.some((v) => typeof v === "number" && !Number.isInteger(v))) kind = undefined
326
+ const refs = vs.map((v) => {
327
+ if (typeof v === "number") return b.constant(new Array(width).fill(v), kind)
328
+ const x = kind === undefined ? toFloat(v) : v
329
+ if (x.width === width) return x.ref
330
+ if (x.width === 1) return broadcast(x, width)
331
+ throw new SLError(`cannot combine a ${widthName(x.width)} with a ${widthName(width)}`)
332
+ })
333
+ return { refs, width, kind, owner: b }
334
+ }
335
+
336
+ function kindedBin(op: SLOpCode, a: Val, o: Num): Val {
337
+ const { refs, width, kind, owner } = unify([a, o])
338
+ if (kind === undefined || !INT_OPS.has(op)) {
339
+ const x = mk(owner, refs[0]!, width) as Val
340
+ return bin(op, x, mk(owner, refs[1]!, width) as Val)
341
+ }
342
+ return mk(owner, owner.call(op, width, refs, undefined, kind), width, kind)
343
+ }
344
+
345
+ function compare(op: SLOpCode, a: Val, o: Num): Bool {
346
+ const { refs, width, kind, owner } = unify([a, o])
347
+ void kind
348
+ return mk(owner, owner.call(op, width, refs, undefined, "bool"), width, "bool")
349
+ }
350
+
351
+ /** A bool operand, a JS boolean made a constant. */
352
+ function asBool(b: Builder, v: Val | boolean, what: string): Val {
353
+ if (typeof v === "boolean") return mk(b, b.constant([v ? 1 : 0], "bool"), 1, "bool")
354
+ if (v.kind !== "bool") throw new SLError(`${what} takes bools, and this is ${v.kind === undefined ? `a ${widthName(v.width)}` : `an ${v.kind}`}; compare it, as in x > 0`)
355
+ return v
356
+ }
357
+
358
+ function logic(op: SLOpCode, a: Val, o?: Val | boolean): Bool {
359
+ const name = op === SLOP.AND ? "&&" : op === SLOP.OR ? "||" : "!"
360
+ const x = asBool(a.owner, a, name)
361
+ if (o === undefined) return mk(a.owner, a.owner.call(op, x.width, [x.ref], undefined, "bool"), x.width, "bool")
362
+ const y = asBool(a.owner, o, name)
363
+ if (x.width !== y.width) throw new SLError(`cannot combine a bool${x.width} with a bool${y.width}`)
364
+ return mk(a.owner, a.owner.call(op, x.width, [x.ref, y.ref], undefined, "bool"), x.width, "bool")
365
+ }
366
+
367
+ function bits(op: SLOpCode, a: Val, o?: Num): Val {
368
+ const word = SL_HLSL[op]?.syntax ?? SL_NAME[op]
369
+ const intOnly = (v: Num) => {
370
+ if (typeof v === "number" ? !Number.isInteger(v) : v.kind !== "int" && v.kind !== "uint") {
371
+ throw new SLError(`${word} works on the bits of an int or a uint, and this is ${typeof v === "number" ? "a fraction" : v.kind === "bool" ? "a bool; use && or ||" : `a ${widthName(v.width)}`}`)
372
+ }
373
+ }
374
+ intOnly(a)
375
+ if (o === undefined) return mk(a.owner, a.owner.call(op, a.width, [a.ref], undefined, a.kind), a.width, a.kind)
376
+ intOnly(o)
377
+ // A shift's count is its own kind; the value shifted keeps its own.
378
+ if (op === SLOP.SHL || op === SLOP.SHR) {
379
+ const count = typeof o === "number" ? a.owner.constant([o], a.kind) : o.ref
380
+ return mk(a.owner, a.owner.call(op, a.width, [a.ref, count], undefined, a.kind), a.width, a.kind)
381
+ }
382
+ const { refs, width, kind, owner } = unify([a, o])
383
+ return mk(owner, owner.call(op, width, refs, undefined, kind), width, kind)
384
+ }
385
+
386
+ /** A conversion to `kind`, or to floats when it is undefined. A float to an int truncates toward zero, held to the int's range. */
387
+ function convert(v: Num | boolean, kind: SLKind | undefined): Val {
388
+ const b = typeof v === "object" ? v.owner : ctx()
389
+ if (typeof v === "boolean") v = mk(b, b.constant([v ? 1 : 0], "bool"), 1, "bool") as Val
390
+ if (typeof v === "number") {
391
+ if (kind === undefined) return mk(b, b.constant([v]), 1)
392
+ if (kind === "bool") return mk(b, b.constant([v !== 0 ? 1 : 0], "bool"), 1, "bool")
393
+ // A whole number the kind holds is that number exactly (`747796405u`);
394
+ // anything else is a float converted, as the GPU would.
395
+ const exact = Number.isInteger(v) && (kind === "int" ? v >= -2147483648 && v <= 2147483647 : v >= 0 && v <= 4294967295)
396
+ return mk(b, b.constant([exact ? v : truncateHeld(v, kind)], kind), 1, kind)
397
+ }
398
+ if (v.kind === kind) return v
399
+ return mk(b, b.call(SLOP.CAST, v.width, [v.ref], undefined, kind), v.width, kind)
400
+ }
401
+
402
+ /** An int: a whole number, or a value converted, a float truncating toward zero. */
403
+ export function int(v: Num | boolean): Int { return convert(v, "int") as Int }
404
+ export function uint(v: Num | boolean): UInt { return convert(v, "uint") as UInt }
405
+ /** A bool: true or false, or a value converted, where anything but zero is true. */
406
+ export function bool(v: Num | boolean): Bool { return convert(v, "bool") as Bool }
407
+
408
+ function choose(b: Builder, cond: Num | boolean, whenTrue: Num, whenFalse: Num): Val {
409
+ if (typeof cond === "boolean") return convertTo(cond ? whenTrue : whenFalse, whenTrue, whenFalse)
410
+ // What SELECT reads a float condition as, so the two agree.
411
+ const c = typeof cond === "number" ? asBool(b, cond >= 0.5, "a choice")
412
+ : cond.kind === "bool" ? cond : cond.kind === undefined ? cond.ge(0.5) : cond.ne(0)
413
+ if (c.width !== 1) throw new SLError("a choice takes one bool, and this is several; combine them with && or ||")
414
+ const { refs, width, kind, owner } = unify([whenTrue, whenFalse])
415
+ return mk(owner, owner.call(SLOP.CHOOSE, width, [c.ref, ...refs], undefined, kind), width, kind)
416
+ }
417
+
418
+ /** `v` as the type the two would unify to, for a choice whose condition is known. */
419
+ function convertTo(v: Num, a: Num, c: Num): Val {
420
+ const { refs, width, kind, owner } = unify([a, c])
421
+ return mk(owner, v === a ? refs[0]! : refs[1]!, width, kind)
422
+ }
423
+
424
+ /**
425
+ * A real branch: only the side `cond` picks runs, and its results come back.
426
+ * Both sides give the same number of results, pairwise of one type; a plain
427
+ * number takes its partner's. A value only one side needs is computed only when
428
+ * that side runs.
429
+ */
430
+ export function branch(cond: Val | boolean, whenTrue: () => Num[], whenFalse: () => Num[]): Val[] {
431
+ const b = ctx()
432
+ if (typeof cond === "boolean") return (cond ? whenTrue() : whenFalse()).map((v) => (typeof v === "number" ? float(v) : v))
433
+ const c = asBool(b, cond, "a branch")
434
+ if (c.width !== 1) throw new SLError("a branch takes one bool, and this is several; combine them with && or ||")
435
+ const t = whenTrue()
436
+ const f = whenFalse()
437
+ if (t.length !== f.length) throw new SLError(`a branch's sides give ${t.length} and ${f.length} results; they must give the same`)
438
+ const types = t.map((x, i) => pairType(x, f[i]!, i))
439
+ if (types.length === 0) return []
440
+ const node = b.add({ k: "if", cond: c.ref, then: t.map((v, i) => typed(b, v, types[i]!)), else: f.map((v, i) => typed(b, v, types[i]!)) })
441
+ return types.map((ty, i) => mk(b, b.add(withKind({ k: "proj", type: ty.width, src: node, index: i }, ty.kind)), ty.width, ty.kind))
442
+ }
443
+
444
+ /**
445
+ * A real loop. The values start as `init`; while `cond` of them holds, and
446
+ * fewer than `max` turns have run, they become `body` of them. What comes back
447
+ * is the values when it stops. `max` is what guarantees it stops: a loop that
448
+ * reaches it leaves as if its condition had failed.
449
+ */
450
+ export function loop(init: Num[], cond: (v: Val[]) => Val | boolean, body: (v: Val[]) => Num[], max = 1024): Val[] {
451
+ const b = ctx()
452
+ if (!Number.isInteger(max) || max < 1) throw new SLError(`a loop's turn limit is a whole number from 1, got ${max}`)
453
+ const start = init.map((v) => (typeof v === "number" ? (float(v) as Val) : v))
454
+ const id = b.newLoop()
455
+ const params = start.map((v, i) => mk(b, b.add(withKind({ k: "param", type: v.width, loop: id, index: i }, v.kind)), v.width, v.kind) as Val)
456
+ const c = asBool(b, cond(params), "a loop's condition")
457
+ if (c.width !== 1) throw new SLError("a loop's condition is one bool, and this is several; combine them with && or ||")
458
+ const next = body(params)
459
+ if (next.length !== params.length) throw new SLError(`a loop carries ${params.length} values and its body gave ${next.length}`)
460
+ const ty = params.map((p) => ({ width: p.width, kind: p.kind }))
461
+ const node = b.add({
462
+ k: "loop", id, init: start.map((v) => v.ref), cond: c.ref,
463
+ next: next.map((v, i) => { pairType(params[i]!, v, i); return typed(b, v, ty[i]!) }), max,
464
+ })
465
+ return ty.map((t, i) => mk(b, b.add(withKind({ k: "proj", type: t.width, src: node, index: i }, t.kind)), t.width, t.kind))
466
+ }
467
+
468
+ interface ValType { width: SLType; kind: SLKind | undefined }
469
+
470
+ /** The one type two results share, or why they do not. */
471
+ function pairType(a: Num, c: Num, i: number): ValType {
472
+ if (typeof a === "number" && typeof c === "number") return { width: 1, kind: undefined }
473
+ if (typeof a === "number") return { width: (c as Val).width, kind: (c as Val).kind }
474
+ if (typeof c === "number") return { width: a.width, kind: a.kind }
475
+ if (a.width !== c.width || a.kind !== c.kind) {
476
+ const name = (v: Val) => (v.kind === undefined ? widthName(v.width) : v.width === 1 ? v.kind : `${v.kind}${v.width}`)
477
+ throw new SLError(`result ${i} is a ${name(a)} one way and a ${name(c)} the other`)
478
+ }
479
+ return { width: a.width, kind: a.kind }
480
+ }
481
+
482
+ /** A result as a node of its type, a plain number becoming a constant. */
483
+ function typed(b: Builder, v: Num, t: ValType): NodeRef {
484
+ if (typeof v === "number") return b.constant(new Array(t.width).fill(v), t.kind)
485
+ return v.ref
486
+ }
487
+
249
488
  // MARK: the public surface
250
489
 
251
490
  export interface ProgramInputs {
@@ -288,7 +527,7 @@ export function program(fn: (inputs: ProgramInputs) => Vec4): Program {
288
527
  }
289
528
  const nodes: SLNode[] = b.nodes.slice()
290
529
  return {
291
- version: programVersion(nodes),
530
+ version: programVersion(nodes, out.ref),
292
531
  nodes,
293
532
  result: out.ref,
294
533
  uniforms: b.uniforms.slice(),
@@ -302,7 +541,8 @@ export function program(fn: (inputs: ProgramInputs) => Vec4): Program {
302
541
 
303
542
  export function float(v: Num): Float {
304
543
  const b = ctx()
305
- return typeof v === "number" ? mk(b, b.constant([v]), TYPE.FLOAT) : (v as Float)
544
+ if (typeof v !== "number") return (v.kind === undefined ? v : toFloat(v)) as Float
545
+ return mk(b, b.constant([v]), TYPE.FLOAT)
306
546
  }
307
547
 
308
548
  /** Builds a wider value from narrower parts, which must add up exactly. */
@@ -478,8 +718,12 @@ export function atan2(y: Num, x: Num): Float {
478
718
  * rather than an `if`: a real branch would have to survive both backends
479
719
  * identically, and this does not.
480
720
  */
481
- export function select(cond: Num, whenTrue: Num, whenFalse: Num): Val {
721
+ export function select(cond: Num | boolean, whenTrue: Num, whenFalse: Num): Val {
482
722
  const b = ctx()
723
+ const kinded = (v: Num | boolean) => typeof v === "object" && v.kind !== undefined
724
+ // A bool condition, or results that are not floats: a choice, which carries
725
+ // any kind, where SELECT's arithmetic carries only floats (IR 4).
726
+ if (typeof cond === "boolean" || kinded(cond) || kinded(whenTrue) || kinded(whenFalse)) return choose(b, cond, whenTrue, whenFalse)
483
727
  const c = typeof cond === "number" ? float(cond) : cond
484
728
  const t = typeof whenTrue === "number" ? float(whenTrue) : whenTrue
485
729
  const f = typeof whenFalse === "number" ? float(whenFalse) : whenFalse
@@ -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
+ }