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.
- package/CHANGELOG.md +29 -0
- package/README.md +70 -28
- package/goldens/goldens.json +147 -63
- package/package.json +1 -1
- package/src/body.ts +132 -26
- package/src/core.ts +1 -1
- package/src/hlsl.ts +7 -3
- package/src/ir.ts +293 -33
- package/src/lang/README.md +2 -2
- package/src/lang/ast.ts +37 -7
- package/src/lang/builtins.ts +5 -17
- package/src/lang/check.ts +179 -70
- package/src/lang/glsl.ts +6 -10
- package/src/lang/lexer.ts +30 -6
- package/src/lang/lower.ts +1034 -289
- package/src/lang/parser.ts +146 -68
- package/src/lang/words.ts +5 -6
- package/src/ops.ts +48 -4
- package/src/params.ts +2 -0
- package/src/serial.ts +100 -17
- package/src/sl.ts +343 -33
- package/src/structure.ts +285 -0
- package/src/web.ts +152 -33
package/src/ir.ts
CHANGED
|
@@ -64,12 +64,75 @@ export const SOURCE_INPUTS: Readonly<Record<SourceInputName, SLType>> = { ...INP
|
|
|
64
64
|
/** Index into `Program.nodes`. Always refers backwards. */
|
|
65
65
|
export type NodeRef = number
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
/**
|
|
68
|
+
* What a value holds, beside its width. A node without one holds floats, which
|
|
69
|
+
* is every node before IR 4, so a program of floats is the graph and the hash
|
|
70
|
+
* it always was.
|
|
71
|
+
*/
|
|
72
|
+
export type SLKind = "int" | "uint" | "bool"
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A node that is a value. `param` is a loop's carried value at the top of an
|
|
76
|
+
* iteration, `proj` one result of an `if` or a `loop`.
|
|
77
|
+
*/
|
|
78
|
+
export type ValueNode =
|
|
79
|
+
| { k: "const"; type: SLType; kind?: SLKind; v: number[] }
|
|
80
|
+
| { k: "input"; type: SLType; kind?: undefined; name: InputName }
|
|
81
|
+
| { k: "uniform"; type: SLType; kind?: undefined; slot: number }
|
|
82
|
+
| { k: "swizzle"; type: SLType; kind?: SLKind; src: NodeRef; chans: number[] }
|
|
83
|
+
| { k: "call"; type: SLType; kind?: SLKind; op: SLOpCode; args: NodeRef[]; imm?: number[] }
|
|
84
|
+
| { k: "param"; type: SLType; kind?: SLKind; loop: number; index: number }
|
|
85
|
+
| { k: "proj"; type: SLType; kind?: SLKind; src: NodeRef; index: number }
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Structured control flow (IR 4), as regions of the one graph rather than as
|
|
89
|
+
* statements: the graph stays a pure DAG, hash consed and hashed as before, and
|
|
90
|
+
* an emitter places each node in the innermost region that uses it
|
|
91
|
+
* (`structure.ts`), which is what makes a branch skip what only it needs.
|
|
92
|
+
*
|
|
93
|
+
* if `then` or `else` are the results, per the bool `cond`; its `proj`s read them
|
|
94
|
+
* loop the `param`s of loop `id` start as `init`; while `cond` holds and fewer
|
|
95
|
+
* than `max` turns have run, they become `next`. Its `proj`s are the params
|
|
96
|
+
* when it stops. `cond` and `next` are computed from the params.
|
|
97
|
+
*/
|
|
98
|
+
export type ControlNode =
|
|
99
|
+
| { k: "if"; cond: NodeRef; then: NodeRef[]; else: NodeRef[] }
|
|
100
|
+
| { k: "loop"; id: number; init: NodeRef[]; cond: NodeRef; next: NodeRef[]; max: number }
|
|
101
|
+
|
|
102
|
+
export type SLNode = ValueNode | ControlNode
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A float to an int or a uint as the GPU converts one: as a 32 bit float,
|
|
106
|
+
* held to the widest such float the kind can hold, then truncated toward zero.
|
|
107
|
+
* The emitters clamp to these bounds, so a folded conversion
|
|
108
|
+
* and a computed one agree.
|
|
109
|
+
*/
|
|
110
|
+
export function truncateHeld(v: number, kind: "int" | "uint"): number {
|
|
111
|
+
const [lo, hi] = INT_BOUNDS[kind]
|
|
112
|
+
return Math.trunc(Math.min(hi, Math.max(lo, Math.fround(v))))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The widest 32 bit floats an int and a uint hold, which a conversion clamps to first. */
|
|
116
|
+
export const INT_BOUNDS = { int: [-2147483648, 2147483520], uint: [0, 4294967040] } as const
|
|
117
|
+
|
|
118
|
+
/** The value node at `ref`. A control node is only ever read through its projs. */
|
|
119
|
+
export function valueAt(nodes: readonly SLNode[], ref: NodeRef): ValueNode {
|
|
120
|
+
const n = nodes[ref]
|
|
121
|
+
if (n === undefined || n.k === "if" || n.k === "loop") throw new SLError(`node ${ref} is not a value`)
|
|
122
|
+
return n
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The refs a node reads, in order. */
|
|
126
|
+
export function operands(n: SLNode): NodeRef[] {
|
|
127
|
+
switch (n.k) {
|
|
128
|
+
case "swizzle": return [n.src]
|
|
129
|
+
case "call": return n.args
|
|
130
|
+
case "proj": return [n.src]
|
|
131
|
+
case "if": return [n.cond, ...n.then, ...n.else]
|
|
132
|
+
case "loop": return [...n.init, n.cond, ...n.next]
|
|
133
|
+
default: return []
|
|
134
|
+
}
|
|
135
|
+
}
|
|
73
136
|
|
|
74
137
|
/**
|
|
75
138
|
* How a host presents a uniform: a slider, a checkbox, a dropdown, a heading
|
|
@@ -171,7 +234,10 @@ export interface TextureDecl {
|
|
|
171
234
|
}
|
|
172
235
|
|
|
173
236
|
export interface Program {
|
|
174
|
-
/**
|
|
237
|
+
/**
|
|
238
|
+
* The `SL_IR_VERSION` this program's nodes mean what they mean under: the
|
|
239
|
+
* lowest one that has every node it holds. See `programVersion`.
|
|
240
|
+
*/
|
|
175
241
|
version: number
|
|
176
242
|
nodes: SLNode[]
|
|
177
243
|
/** Must be VEC4: a program produces a colour. */
|
|
@@ -210,6 +276,24 @@ export function checkCaps(p: Pick<Program, "uniforms" | "textures">): void {
|
|
|
210
276
|
if (p.textures.length > TEXTURE_SLOTS) throw new SLError(tooManyTextures(p.textures.length, "program"))
|
|
211
277
|
}
|
|
212
278
|
|
|
279
|
+
/**
|
|
280
|
+
* The colour as written behind a colour value: the operand of the `TO_LINEAR`
|
|
281
|
+
* that reading a hex or a colour uniform applied, or null for a value computed
|
|
282
|
+
* in the working space. What a ramp's stop has to be, since a ramp blends its
|
|
283
|
+
* stops as written (`sl.ramp`).
|
|
284
|
+
*/
|
|
285
|
+
export function writtenColour(nodes: readonly SLNode[], ref: NodeRef): NodeRef | null {
|
|
286
|
+
const n = nodes[ref]!
|
|
287
|
+
if (n.k !== "call" || n.op !== SLOP.TO_LINEAR || (n.type !== TYPE.VEC3 && n.type !== TYPE.VEC4)) return null
|
|
288
|
+
return n.args[0]!
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Why a computed value is not a ramp's stop, in one wording for both surfaces. */
|
|
292
|
+
export const RAMP_STOP_COMPUTED =
|
|
293
|
+
"a ramp's stop is a colour as written: a hex like #ff8040, or a uniform or const that holds one. " +
|
|
294
|
+
"This one is computed, and a ramp blends its stops as written before it converts them, so it has " +
|
|
295
|
+
"no written colour to blend. Blend computed colours with lerp instead"
|
|
296
|
+
|
|
213
297
|
/** Operations a single program may hold. Generous: a guard against a runaway builder, not a budget. */
|
|
214
298
|
export const MAX_NODES = 4096
|
|
215
299
|
|
|
@@ -233,6 +317,10 @@ export class Builder {
|
|
|
233
317
|
readonly textures: TextureDecl[] = []
|
|
234
318
|
private readonly interned = new Map<string, NodeRef>()
|
|
235
319
|
|
|
320
|
+
/** The next loop's id: unique within the program, so two loops' params never intern to one node. */
|
|
321
|
+
private loops = 0
|
|
322
|
+
newLoop(): number { return this.loops++ }
|
|
323
|
+
|
|
236
324
|
add(node: SLNode): NodeRef {
|
|
237
325
|
if (this.nodes.length >= MAX_NODES) {
|
|
238
326
|
throw new SLError(`a program may hold at most ${MAX_NODES} operations`)
|
|
@@ -245,7 +333,7 @@ export class Builder {
|
|
|
245
333
|
return ref
|
|
246
334
|
}
|
|
247
335
|
|
|
248
|
-
call(op: SLOpCode, type: SLType, args: NodeRef[], imm?: number[]): NodeRef {
|
|
336
|
+
call(op: SLOpCode, type: SLType, args: NodeRef[], imm?: number[], kind?: SLKind): NodeRef {
|
|
249
337
|
const arity = SL_ARITY[op]
|
|
250
338
|
if (arity >= 0 && args.length !== arity) {
|
|
251
339
|
throw new SLError(`${SL_NAME[op]} takes ${arity} argument(s), got ${args.length}`)
|
|
@@ -255,14 +343,19 @@ export class Builder {
|
|
|
255
343
|
throw new SLError(`${SL_NAME[op]} refers to a node that does not exist yet`)
|
|
256
344
|
}
|
|
257
345
|
}
|
|
258
|
-
|
|
346
|
+
const node: SLNode = imm === undefined ? { k: "call", type, op, args } : { k: "call", type, op, args, imm }
|
|
347
|
+
if (kind !== undefined) node.kind = kind
|
|
348
|
+
return this.add(node)
|
|
259
349
|
}
|
|
260
350
|
|
|
261
|
-
constant(v: number[]): NodeRef {
|
|
351
|
+
constant(v: number[], kind?: SLKind): NodeRef {
|
|
262
352
|
for (const n of v) {
|
|
263
353
|
if (!Number.isFinite(n)) throw new SLError(`a constant must be finite, got ${n}`)
|
|
354
|
+
if (kind !== undefined && !Number.isInteger(n)) throw new SLError(`an ${kind} constant must be a whole number, got ${n}`)
|
|
264
355
|
}
|
|
265
|
-
|
|
356
|
+
const node: SLNode = { k: "const", type: v.length as SLType, v: v.slice() }
|
|
357
|
+
if (kind !== undefined) node.kind = kind
|
|
358
|
+
return this.add(node)
|
|
266
359
|
}
|
|
267
360
|
|
|
268
361
|
uniform(name: string, type: SLType, value: number[], colour = false, control: UniformControl = {}): NodeRef {
|
|
@@ -327,9 +420,7 @@ export function reachable(nodes: SLNode[], result: NodeRef): NodeRef[] {
|
|
|
327
420
|
const ref = stack.pop()!
|
|
328
421
|
if (keep.has(ref)) continue
|
|
329
422
|
keep.add(ref)
|
|
330
|
-
const
|
|
331
|
-
if (n.k === "swizzle") stack.push(n.src)
|
|
332
|
-
else if (n.k === "call") for (const a of n.args) stack.push(a)
|
|
423
|
+
for (const a of operands(nodes[ref]!)) stack.push(a)
|
|
333
424
|
}
|
|
334
425
|
// Ascending, which is still topological because a node only refers backwards.
|
|
335
426
|
return [...keep].sort((x, y) => x - y)
|
|
@@ -351,12 +442,17 @@ export function inputsUsed(p: Program): InputName[] {
|
|
|
351
442
|
|
|
352
443
|
/** Structural key for hash consing. Order matters and is fixed by the node shape. */
|
|
353
444
|
function keyOf(n: SLNode): string {
|
|
445
|
+
const kind = "kind" in n && n.kind !== undefined ? `@${n.kind}` : ""
|
|
354
446
|
switch (n.k) {
|
|
355
|
-
case "const": return `c:${n.type}:${n.v.map(
|
|
447
|
+
case "const": return `c:${n.type}:${n.v.map((v) => constText(v, n.kind)).join(",")}${kind}`
|
|
356
448
|
case "input": return `i:${n.name}`
|
|
357
449
|
case "uniform": return `u:${n.slot}`
|
|
358
|
-
case "swizzle": return `s:${n.src}:${n.chans.join("")}`
|
|
359
|
-
case "call": return `f:${n.op}:${n.args.join(",")}:${(n.imm ?? []).map(fixed).join(",")}`
|
|
450
|
+
case "swizzle": return `s:${n.src}:${n.chans.join("")}${kind}`
|
|
451
|
+
case "call": return `f:${n.op}:${n.args.join(",")}:${(n.imm ?? []).map(fixed).join(",")}${kind}`
|
|
452
|
+
case "param": return `p:${n.loop}:${n.index}`
|
|
453
|
+
case "proj": return `j:${n.src}:${n.index}`
|
|
454
|
+
case "if": return `?:${n.cond}:${n.then.join(",")}:${n.else.join(",")}`
|
|
455
|
+
case "loop": return `L:${n.max}:${n.init.join(",")}:${n.cond}:${n.next.join(",")}`
|
|
360
456
|
}
|
|
361
457
|
}
|
|
362
458
|
|
|
@@ -369,6 +465,11 @@ export function fixed(n: number): string {
|
|
|
369
465
|
return Object.is(n, -0) ? "0" : n.toPrecision(9)
|
|
370
466
|
}
|
|
371
467
|
|
|
468
|
+
/** A constant's value as keyed and hashed: a float at `fixed`'s precision, an int, a uint or a bool exactly. */
|
|
469
|
+
function constText(n: number, kind: SLKind | undefined): string {
|
|
470
|
+
return kind === undefined ? fixed(n) : String(n)
|
|
471
|
+
}
|
|
472
|
+
|
|
372
473
|
export function widthName(t: SLType): string {
|
|
373
474
|
return t === 1 ? "float" : `vec${t}`
|
|
374
475
|
}
|
|
@@ -402,41 +503,113 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
|
|
|
402
503
|
// digests, so two graphs that compute the same thing agree however they were
|
|
403
504
|
// assembled. It also ignores nodes not reachable from the result, which is
|
|
404
505
|
// correct: dead nodes generate no shader code.
|
|
506
|
+
//
|
|
507
|
+
// NORMALISED, so two ways of writing one computation agree (hash version 2).
|
|
508
|
+
// A commutative op's operands are digested in sorted order, so `a + b` is
|
|
509
|
+
// `b + a`; and a swizzle of a constant is digested as the constant it picks,
|
|
510
|
+
// so `8 * uv` (8 broadcast by a swizzle) is `uv * 8` (a float2 constant).
|
|
511
|
+
// Only where every backend computes the same bits either way: min and max
|
|
512
|
+
// are left out, since which of -0 and +0 they return depends on the order.
|
|
405
513
|
const digest = new Map<NodeRef, string>()
|
|
514
|
+
const depth = loopDepths(nodes, result)
|
|
406
515
|
|
|
407
516
|
const of = (ref: NodeRef): string => {
|
|
408
517
|
const seen = digest.get(ref)
|
|
409
518
|
if (seen !== undefined) return seen
|
|
410
|
-
const n = nodes[ref]
|
|
519
|
+
const n = nodes[ref]!
|
|
411
520
|
let body: string
|
|
412
521
|
switch (n.k) {
|
|
413
|
-
|
|
522
|
+
// Control nodes have no type of their own: their projs carry it.
|
|
523
|
+
case "if": {
|
|
524
|
+
body = `?:${of(n.cond)}:${n.then.map(of).join(",")}:${n.else.map(of).join(",")}`
|
|
525
|
+
const d = fnv1a(body)
|
|
526
|
+
digest.set(ref, d)
|
|
527
|
+
return d
|
|
528
|
+
}
|
|
529
|
+
case "loop": {
|
|
530
|
+
body = `L:${n.max}:${n.init.map(of).join(",")}:${of(n.cond)}:${n.next.map(of).join(",")}`
|
|
531
|
+
const d = fnv1a(body)
|
|
532
|
+
digest.set(ref, d)
|
|
533
|
+
return d
|
|
534
|
+
}
|
|
535
|
+
// By nesting depth, not by id: two programs that nest the same
|
|
536
|
+
// loops the same way agree however their ids were handed out, and
|
|
537
|
+
// two loops nested in one another never share a digest for a param.
|
|
538
|
+
case "param": body = `p:${depth.get(n.loop) ?? 1}:${n.index}`; break
|
|
539
|
+
case "proj": body = `j:${of(n.src)}:${n.index}`; break
|
|
540
|
+
case "const": body = `c:${n.type}:${n.v.map((v) => constText(v, n.kind)).join(",")}`; break
|
|
414
541
|
case "input": body = `i:${n.name}`; break
|
|
415
542
|
// By slot rather than by name: the slot is what the generated shader
|
|
416
543
|
// lays out, so two programs whose uniforms differ only in name still
|
|
417
544
|
// produce different shaders and must not share a hash. The names go
|
|
418
545
|
// in separately below.
|
|
419
546
|
case "uniform": body = `u:${n.slot}:${n.type}`; break
|
|
420
|
-
case "swizzle":
|
|
421
|
-
|
|
547
|
+
case "swizzle": {
|
|
548
|
+
const src = nodes[n.src]!
|
|
549
|
+
body = src.k === "const" && src.kind === n.kind
|
|
550
|
+
? `c:${n.type}:${n.chans.map((c) => constText(src.v[c]!, src.kind)).join(",")}`
|
|
551
|
+
: `s:${of(n.src)}:${n.chans.join("")}`
|
|
552
|
+
break
|
|
553
|
+
}
|
|
554
|
+
case "call": {
|
|
555
|
+
const args = n.args.map(of)
|
|
556
|
+
if (COMMUTATIVE.has(n.op)) args.sort()
|
|
557
|
+
body = `f:${n.op}:${args.join(",")}:${(n.imm ?? []).map(fixed).join(",")}`
|
|
558
|
+
break
|
|
559
|
+
}
|
|
422
560
|
}
|
|
423
|
-
const d = fnv1a(body + "|" + n.type)
|
|
561
|
+
const d = fnv1a(body + "|" + n.type + (n.kind === undefined ? "" : n.kind))
|
|
424
562
|
digest.set(ref, d)
|
|
425
563
|
return d
|
|
426
564
|
}
|
|
427
565
|
|
|
428
566
|
// The IR version is in the hash, so a change to what an opcode means in the
|
|
429
|
-
// IR changes
|
|
430
|
-
// serving the old maths.
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
// library
|
|
434
|
-
|
|
567
|
+
// IR changes the hash of every program using it, and every cache keyed by
|
|
568
|
+
// one recompiles rather than serving the old maths. It is the PROGRAM's
|
|
569
|
+
// version, the lowest that has its nodes, so a version that adds a node
|
|
570
|
+
// leaves every program without one on the hash it had. A change to the
|
|
571
|
+
// helper library alone (a noise's arithmetic, say) is not one: every host
|
|
572
|
+
// includes the library's text when it compiles, and none caches a compiled
|
|
573
|
+
// program by hash across a library update, so the new text reaches every
|
|
574
|
+
// shader without a bump.
|
|
575
|
+
const parts: string[] = [`v${SL_HASH_VERSION}:${programVersion(nodes, result)}`, of(result)]
|
|
435
576
|
for (const u of uniforms) parts.push(`U:${u.name}:${u.type}:${u.value.map(fixed).join(",")}`)
|
|
436
577
|
for (const t of textures) parts.push(`T:${t.name}:${t.slot}`)
|
|
437
578
|
return fnv1a(parts.join("|"))
|
|
438
579
|
}
|
|
439
580
|
|
|
581
|
+
/**
|
|
582
|
+
* Each reachable loop's nesting depth, 1 for a loop inside no other: 1 more than
|
|
583
|
+
* the deepest loop whose params it reads. Read from the graph, so it does not
|
|
584
|
+
* depend on the order anything was built or walked in.
|
|
585
|
+
*/
|
|
586
|
+
function loopDepths(nodes: readonly SLNode[], result: NodeRef): Map<number, number> {
|
|
587
|
+
const free = new Map<NodeRef, Set<number>>()
|
|
588
|
+
const loops = new Map<number, Extract<SLNode, { k: "loop" }>>()
|
|
589
|
+
const freeOf = (ref: NodeRef): Set<number> => free.get(ref) ?? new Set()
|
|
590
|
+
for (const ref of reachable(nodes as SLNode[], result)) {
|
|
591
|
+
const n = nodes[ref]!
|
|
592
|
+
const out = new Set<number>()
|
|
593
|
+
if (n.k === "param") out.add(n.loop)
|
|
594
|
+
for (const a of operands(n)) for (const l of freeOf(a)) out.add(l)
|
|
595
|
+
if (n.k === "loop") { out.delete(n.id); loops.set(n.id, n) }
|
|
596
|
+
free.set(ref, out)
|
|
597
|
+
}
|
|
598
|
+
const loopFree = new Map<number, Set<number>>()
|
|
599
|
+
for (const [ref, n] of nodes.entries()) if (n.k === "loop" && free.has(ref)) loopFree.set(n.id, free.get(ref)!)
|
|
600
|
+
const depth = new Map<number, number>()
|
|
601
|
+
const of = (id: number): number => {
|
|
602
|
+
const seen = depth.get(id)
|
|
603
|
+
if (seen !== undefined) return seen
|
|
604
|
+
let d = 1
|
|
605
|
+
for (const outer of loopFree.get(id) ?? []) d = Math.max(d, of(outer) + 1)
|
|
606
|
+
depth.set(id, d)
|
|
607
|
+
return d
|
|
608
|
+
}
|
|
609
|
+
for (const id of loops.keys()) of(id)
|
|
610
|
+
return depth
|
|
611
|
+
}
|
|
612
|
+
|
|
440
613
|
/** The control fields of `c` that are set, copied, in `CONTROL_FIELDS` order. */
|
|
441
614
|
export function controlOf(c: UniformControl): UniformControl {
|
|
442
615
|
const out: UniformControl = {}
|
|
@@ -449,8 +622,91 @@ export function controlOf(c: UniformControl): UniformControl {
|
|
|
449
622
|
return out
|
|
450
623
|
}
|
|
451
624
|
|
|
452
|
-
/**
|
|
453
|
-
|
|
625
|
+
/**
|
|
626
|
+
* The lowest `SL_IR_VERSION` that has every node here, and never below 2.
|
|
627
|
+
*
|
|
628
|
+
* A version that adds a node, or a form of one, raises only the programs that
|
|
629
|
+
* hold it. Everything else keeps the version, the hash and the JSON it had, so
|
|
630
|
+
* a program recorded before the bump still finds its shader, and a reader that
|
|
631
|
+
* predates the bump still reads it. Every node counts, used or not: the JSON
|
|
632
|
+
* carries them all, and an older reader has to refuse by the version rather
|
|
633
|
+
* than halfway through the nodes.
|
|
634
|
+
*
|
|
635
|
+
* Only the nodes the result reads count: lowering leaves dead nodes behind (an
|
|
636
|
+
* int constant a fold used up, say), and they draw nothing. `toJSON` writes
|
|
637
|
+
* only the live ones, so an older reader never meets a dead newer node.
|
|
638
|
+
*/
|
|
639
|
+
export function programVersion(nodes: readonly SLNode[], result: NodeRef): number {
|
|
640
|
+
let v = 2
|
|
641
|
+
for (const ref of reachable(nodes as SLNode[], result)) v = Math.max(v, nodeVersion(nodes[ref]!))
|
|
642
|
+
return v
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** The ops IR 4 added. */
|
|
646
|
+
const IR4_OPS = new Set<number>([
|
|
647
|
+
SLOP.CAST, SLOP.LT, SLOP.LE, SLOP.GT, SLOP.GE, SLOP.EQ, SLOP.NE, SLOP.AND, SLOP.OR, SLOP.NOT,
|
|
648
|
+
SLOP.BIT_AND, SLOP.BIT_OR, SLOP.BIT_XOR, SLOP.BIT_NOT, SLOP.SHL, SLOP.SHR, SLOP.CHOOSE,
|
|
649
|
+
])
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* The version that added this node's form. An sdf or octave call with one
|
|
653
|
+
* operand is the form before IR 3, which only a file an older compiler wrote
|
|
654
|
+
* holds, and `fromJSON` moves it to the form IR 3 has.
|
|
655
|
+
*/
|
|
656
|
+
function nodeVersion(n: SLNode): number {
|
|
657
|
+
if (n.k === "if" || n.k === "loop" || n.k === "param" || n.k === "proj" || n.kind !== undefined) return 4
|
|
658
|
+
if (n.k !== "call") return 2
|
|
659
|
+
if (IR4_OPS.has(n.op)) return 4
|
|
660
|
+
switch (n.op) {
|
|
661
|
+
case SLOP.SAMPLE_LOD: return 3
|
|
662
|
+
case SLOP.SDF:
|
|
663
|
+
case SLOP.FBM:
|
|
664
|
+
case SLOP.TURBULENCE:
|
|
665
|
+
case SLOP.RIDGED:
|
|
666
|
+
return n.args.length > 1 ? 3 : 2
|
|
667
|
+
default: return 2
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* What is wrong with a call's form, for the ops whose operand list alone does
|
|
673
|
+
* not say it, or null. The builder only makes the right ones; this is for a
|
|
674
|
+
* program read from JSON, which an emitter would otherwise print as something
|
|
675
|
+
* else.
|
|
676
|
+
*
|
|
677
|
+
* SDF the point, a float4 and a float2 holding the six shape parameters,
|
|
678
|
+
* the shape id the one immediate
|
|
679
|
+
* FBM, TURBULENCE, RIDGED the point and the octave count, FBM's kind the
|
|
680
|
+
* one immediate
|
|
681
|
+
*/
|
|
682
|
+
export function formProblem(n: Extract<SLNode, { k: "call" }>, nodes: readonly SLNode[]): string | null {
|
|
683
|
+
const widths = n.args.map((a) => { const v = nodes[a]!; return v.k === "if" || v.k === "loop" ? "control" : v.type }).join(",")
|
|
684
|
+
switch (n.op) {
|
|
685
|
+
case SLOP.SDF:
|
|
686
|
+
if (widths === "2,4,2" && n.imm?.length === 1) return null
|
|
687
|
+
return "an sdf takes a float2, a float4 and a float2, and its shape as the one immediate"
|
|
688
|
+
case SLOP.FBM:
|
|
689
|
+
case SLOP.TURBULENCE:
|
|
690
|
+
case SLOP.RIDGED: {
|
|
691
|
+
const rest = n.op === SLOP.FBM ? 1 : 0
|
|
692
|
+
if (widths === "2,1" && (n.imm?.length ?? 0) === rest) return null
|
|
693
|
+
return `${SL_NAME[n.op]} takes a float2 and its octave count, a float`
|
|
694
|
+
}
|
|
695
|
+
default:
|
|
696
|
+
return null
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/** The ops whose two operands the hash takes in either order: exact in IEEE arithmetic on every backend. */
|
|
701
|
+
const COMMUTATIVE = new Set<number>([SLOP.ADD, SLOP.MUL, SLOP.DOT, SLOP.DISTANCE])
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Bumped when the hashing scheme changes, which invalidates generated shaders.
|
|
705
|
+
*
|
|
706
|
+
* 1 the Merkle hash over the reachable graph
|
|
707
|
+
* 2 normalised: commutative operands in either order, a swizzle of a constant as the constant
|
|
708
|
+
*/
|
|
709
|
+
export const SL_HASH_VERSION = 2
|
|
454
710
|
|
|
455
711
|
/**
|
|
456
712
|
* Bumped whenever the IR changes in a way a reader has to know about: a new
|
|
@@ -467,13 +723,17 @@ export const SL_HASH_VERSION = 1
|
|
|
467
723
|
* records it again, for no gain.
|
|
468
724
|
*
|
|
469
725
|
* A reader accepts every version up to its own and refuses a newer one with a
|
|
470
|
-
* message naming both, the rule the particle wire and fx follow.
|
|
471
|
-
*
|
|
726
|
+
* message naming both, the rule the particle wire and fx follow. A program's
|
|
727
|
+
* version is the lowest that has its nodes (`programVersion`), and that is what
|
|
728
|
+
* its hash carries, so a bump rehashes only the programs using what it added.
|
|
472
729
|
*
|
|
473
730
|
* 1 the first versioned IR
|
|
474
731
|
* 2 an SDF call carries up to six shape parameters, not four (#129)
|
|
732
|
+
* 3 SAMPLE_LOD; an SDF's shape parameters and a noise's octave count are
|
|
733
|
+
* operands, where they were immediates
|
|
734
|
+
* 4 control flow (if, loop, their params and projs), ints, uints and bools
|
|
475
735
|
*/
|
|
476
|
-
export const SL_IR_VERSION =
|
|
736
|
+
export const SL_IR_VERSION = 4
|
|
477
737
|
|
|
478
738
|
function fnv1a(s: string): string {
|
|
479
739
|
let h = 0x811c9dc5
|
package/src/lang/README.md
CHANGED
|
@@ -152,13 +152,13 @@ one the local does not have, is refused.
|
|
|
152
152
|
|
|
153
153
|
## Not yet spellable
|
|
154
154
|
|
|
155
|
-
|
|
155
|
+
Two things the EDSL or the opcode table has and a file cannot say. Each is a
|
|
156
156
|
recorded gap, not an oversight, and each has its own error message rather than
|
|
157
157
|
"unknown identifier".
|
|
158
158
|
|
|
159
159
|
| | Why |
|
|
160
160
|
|---|---|
|
|
161
|
-
| `rgb2hsv
|
|
161
|
+
| `rgb2hsv` | The opcode is numbered and **no backend implements it**; every emitter throws on it. |
|
|
162
162
|
| `fbm`'s simplex base | `sl.fbm(p, octaves, "simplex")` picks the base with a string, and the language has no strings. `fbm(p, octaves)` is the value base, and `turbulence` and `ridged` are the simplex family. |
|
|
163
163
|
|
|
164
164
|
A `.sl` file and the EDSL are held to the same caps, `UNIFORM_SLOTS` (16) and
|
package/src/lang/ast.ts
CHANGED
|
@@ -11,41 +11,71 @@
|
|
|
11
11
|
* pass over the source.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import type { SLKind } from "../ir"
|
|
14
15
|
import type { Pos } from "./lexer"
|
|
15
16
|
|
|
16
|
-
/** The
|
|
17
|
-
export type TypeName = "float" | "float2" | "float3" | "float4"
|
|
17
|
+
/** The value types, under their HLSL names. `texture2D` is not one: it declares a slot. */
|
|
18
|
+
export type TypeName = "float" | "float2" | "float3" | "float4" | "int" | "uint" | "bool"
|
|
18
19
|
|
|
19
20
|
export const TYPE_WIDTH: Record<TypeName, 1 | 2 | 3 | 4> = {
|
|
20
|
-
float: 1, float2: 2, float3: 3, float4: 4,
|
|
21
|
+
float: 1, float2: 2, float3: 3, float4: 4, int: 1, uint: 1, bool: 1,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** What a type holds beside its width; the floats hold nothing else. */
|
|
25
|
+
export const TYPE_KIND: Record<TypeName, SLKind | undefined> = {
|
|
26
|
+
float: undefined, float2: undefined, float3: undefined, float4: undefined, int: "int", uint: "uint", bool: "bool",
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
export type BinaryOp =
|
|
24
30
|
| "+" | "-" | "*" | "/" | "%"
|
|
25
31
|
| "<" | "<=" | ">" | ">=" | "==" | "!="
|
|
26
32
|
| "&&" | "||"
|
|
33
|
+
| "&" | "|" | "^" | "<<" | ">>"
|
|
27
34
|
|
|
28
35
|
export type Expr =
|
|
29
|
-
|
|
36
|
+
/**
|
|
37
|
+
* `whole` is a number written with no point and no exponent, `3` not `3.0`:
|
|
38
|
+
* a float, except beside an int, where it is an int. `unsigned` is `3u`.
|
|
39
|
+
*/
|
|
40
|
+
| { k: "num"; value: number; whole: boolean; unsigned?: boolean; pos: Pos }
|
|
41
|
+
| { k: "bool"; value: boolean; pos: Pos }
|
|
30
42
|
| { k: "hex"; hex: string; pos: Pos }
|
|
31
43
|
| { k: "ident"; name: string; pos: Pos }
|
|
32
44
|
/** A swizzle, or the shape half of `sdf.circle`. Which one is decided later. */
|
|
33
45
|
| { k: "member"; obj: Expr; name: string; pos: Pos }
|
|
34
46
|
| { k: "call"; callee: Expr; args: Expr[]; pos: Pos }
|
|
35
|
-
| { k: "unary"; op: "-" | "+" | "!"; arg: Expr; pos: Pos }
|
|
47
|
+
| { k: "unary"; op: "-" | "+" | "!" | "~"; arg: Expr; pos: Pos }
|
|
36
48
|
| { k: "binary"; op: BinaryOp; a: Expr; b: Expr; pos: Pos }
|
|
37
49
|
| { k: "cond"; cond: Expr; then: Expr; else: Expr; pos: Pos }
|
|
38
50
|
|
|
39
|
-
export type AssignOp = "=" | "+=" | "-=" | "*=" | "/=" | "%="
|
|
51
|
+
export type AssignOp = "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "&=" | "|=" | "^=" | "<<=" | ">>="
|
|
52
|
+
|
|
53
|
+
/** One `case` group of a switch: its labels, `null` for `default`, and its body without the closing break. */
|
|
54
|
+
export interface SwitchCase {
|
|
55
|
+
labels: Array<Expr | null>
|
|
56
|
+
body: Stmt[]
|
|
57
|
+
/** Whether it ended in a break, which is dropped from `body`. */
|
|
58
|
+
closed: boolean
|
|
59
|
+
pos: Pos
|
|
60
|
+
}
|
|
40
61
|
|
|
41
62
|
export type Stmt =
|
|
42
63
|
| { k: "var"; type: TypeName; name: string; init: Expr; pos: Pos }
|
|
43
64
|
/** A compile time constant: usable as a value and as a `for` bound. */
|
|
44
65
|
| { k: "const"; type: TypeName; name: string; init: Expr; pos: Pos }
|
|
66
|
+
/** `x++` is `x += 1`, with the 1 whole. */
|
|
45
67
|
| { k: "assign"; target: Expr; op: AssignOp; value: Expr; pos: Pos }
|
|
46
68
|
| { k: "if"; cond: Expr; then: Stmt[]; else: Stmt[]; pos: Pos }
|
|
47
|
-
|
|
69
|
+
/** `for (type counter = from; cond; update)`: the update changes the counter and nothing else. */
|
|
70
|
+
| { k: "for"; type: TypeName; counter: string; from: Expr; cond: Expr; update: Extract<Stmt, { k: "assign" }>; body: Stmt[]; pos: Pos }
|
|
71
|
+
| { k: "while"; cond: Expr; body: Stmt[]; pos: Pos }
|
|
72
|
+
| { k: "break"; pos: Pos }
|
|
73
|
+
| { k: "continue"; pos: Pos }
|
|
74
|
+
/** Cases never fall through: each ends in break, return or continue, and the break is dropped here. */
|
|
75
|
+
| { k: "switch"; value: Expr; cases: SwitchCase[]; pos: Pos }
|
|
48
76
|
| { k: "return"; value: Expr; pos: Pos }
|
|
77
|
+
/** `{ ... }` on its own: a body with its own scope, as a case's braces are. */
|
|
78
|
+
| { k: "block"; body: Stmt[]; pos: Pos }
|
|
49
79
|
|
|
50
80
|
export interface Param {
|
|
51
81
|
type: TypeName
|
package/src/lang/builtins.ts
CHANGED
|
@@ -125,12 +125,10 @@ value(SLOP.LUMINANCE, 1, 1, (a) => sl.luminance(colourOperand("luminance", a[0]!
|
|
|
125
125
|
value(SLOP.NOISE, 1, 1, (a) => sl.noise(point("noise", a[0]!) as never))
|
|
126
126
|
value(SLOP.SIMPLEX, 1, 1, (a) => sl.simplex(point("simplex", a[0]!) as never))
|
|
127
127
|
value(SLOP.VORONOI, 1, 1, (a) => sl.voronoi(point("voronoi", a[0]!) as never))
|
|
128
|
-
// The octave count is
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
value(SLOP.
|
|
132
|
-
value(SLOP.TURBULENCE, 1, 2, (a) => sl.turbulence(point("turbulence", a[0]!) as never, octaves("turbulence", a[1])))
|
|
133
|
-
value(SLOP.RIDGED, 1, 2, (a) => sl.ridged(point("ridged", a[0]!) as never, octaves("ridged", a[1])))
|
|
128
|
+
// The octave count is 3 when left out, and may be a value (`sl.fbm` says how).
|
|
129
|
+
value(SLOP.FBM, 1, 2, (a) => sl.fbm(point("fbm", a[0]!) as never, a[1] ?? 3))
|
|
130
|
+
value(SLOP.TURBULENCE, 1, 2, (a) => sl.turbulence(point("turbulence", a[0]!) as never, a[1] ?? 3))
|
|
131
|
+
value(SLOP.RIDGED, 1, 2, (a) => sl.ridged(point("ridged", a[0]!) as never, a[1] ?? 3))
|
|
134
132
|
|
|
135
133
|
/** Every field is sampled at a float2. */
|
|
136
134
|
function point(fn: string, v: Num): Val {
|
|
@@ -138,20 +136,10 @@ function point(fn: string, v: Num): Val {
|
|
|
138
136
|
}
|
|
139
137
|
|
|
140
138
|
special(SLOP.SAMPLE, 2, 2)
|
|
139
|
+
special(SLOP.SAMPLE_LOD, 3, 3)
|
|
141
140
|
special(SLOP.SDF, 1, 7)
|
|
142
141
|
special(SLOP.RAMP, 3, 64)
|
|
143
142
|
|
|
144
|
-
function octaves(fn: string, v: Num | undefined): number {
|
|
145
|
-
if (v === undefined) return 3
|
|
146
|
-
if (typeof v !== "number") {
|
|
147
|
-
throw new SLError(
|
|
148
|
-
`${fn}'s octave count is part of the operation, so it has to be a constant. ` +
|
|
149
|
-
`A uniform or a computed value cannot change how many octaves the shader runs.`,
|
|
150
|
-
)
|
|
151
|
-
}
|
|
152
|
-
return v
|
|
153
|
-
}
|
|
154
|
-
|
|
155
143
|
export const BUILTINS: Readonly<Record<string, Builtin>> = table
|
|
156
144
|
|
|
157
145
|
/**
|