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/CHANGELOG.md +14 -0
- package/README.md +35 -18
- package/goldens/goldens.json +52 -3
- package/package.json +1 -1
- package/src/body.ts +103 -20
- package/src/hlsl.ts +1 -0
- package/src/ir.ts +170 -26
- package/src/lang/ast.ts +37 -7
- package/src/lang/check.ts +155 -59
- package/src/lang/glsl.ts +5 -8
- package/src/lang/lexer.ts +30 -6
- package/src/lang/lower.ts +1015 -266
- package/src/lang/parser.ts +146 -68
- package/src/lang/words.ts +5 -6
- package/src/ops.ts +44 -1
- package/src/serial.ts +39 -8
- package/src/sl.ts +255 -11
- package/src/structure.ts +285 -0
- package/src/web.ts +108 -22
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
|
-
|
|
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
|
-
|
|
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
|
package/src/structure.ts
ADDED
|
@@ -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
|
+
}
|