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/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
|
+
}
|
package/src/web.ts
CHANGED
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
* vertex index) so one module is one pipeline; the GLSL host supplies its own.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import {
|
|
27
|
+
import { INT_LIMITS } from "./body"
|
|
28
|
+
import { SLError, TYPE, valueAt, type NodeRef, type Program, type SLKind, type SLType, type ValueNode } from "./ir"
|
|
29
|
+
import { inVaryingFlow, local, printBody, structure, type Syntax } from "./structure"
|
|
28
30
|
import { libClosure, libIndex, LIB_FUNCTIONS, SDF_CALLS } from "./lib"
|
|
29
31
|
import { LIB_GLSL } from "./lib/glsl"
|
|
30
32
|
import { LIB_WGSL } from "./lib/wgsl"
|
|
@@ -48,29 +50,39 @@ function emitWeb(p: Program, lang: WebLanguage): string {
|
|
|
48
50
|
const need = new Set<number>()
|
|
49
51
|
const smoothsteps = new Set<SLType>()
|
|
50
52
|
const sampled = new Set<number>()
|
|
51
|
-
const
|
|
52
|
-
const emitted = new Set<number>()
|
|
53
|
+
const shape = structure(p)
|
|
53
54
|
const T = (t: SLType) => (W ? (t === 1 ? "f32" : `vec${t}f`) : t === 1 ? "float" : `vec${t}`)
|
|
54
|
-
|
|
55
|
+
/** A value's type in this language, its kind included. */
|
|
56
|
+
const typeName = (n: { type: SLType; kind?: SLKind }): string => {
|
|
57
|
+
if (n.kind === undefined) return T(n.type)
|
|
58
|
+
const scalar = W ? { int: "i32", uint: "u32", bool: "bool" }[n.kind] : n.kind
|
|
59
|
+
if (n.type === 1) return scalar
|
|
60
|
+
return W ? `vec${n.type}<${scalar}>` : `${{ int: "i", uint: "u", bool: "b" }[n.kind]}vec${n.type}`
|
|
61
|
+
}
|
|
62
|
+
const kindLit = (kind: SLKind, v: number): string => {
|
|
63
|
+
if (kind === "bool") return v !== 0 ? "true" : "false"
|
|
64
|
+
if (kind === "uint") return `${v >>> 0}u`
|
|
65
|
+
const i = W ? `${v}i` : String(v)
|
|
66
|
+
return v < 0 ? `(${i})` : i
|
|
67
|
+
}
|
|
68
|
+
const typeOf = (ref: number) => valueAt(p.nodes, ref).type
|
|
69
|
+
/** A constant node's value, or null for anything computed. */
|
|
70
|
+
const constantOf = (ref: number): number[] | null => {
|
|
71
|
+
const c = p.nodes[ref]!
|
|
72
|
+
return c.k === "const" ? c.v : null
|
|
73
|
+
}
|
|
55
74
|
|
|
56
75
|
/** A node's value widened to `t` when it is a scalar and `t` is not. */
|
|
57
76
|
const splat = (ref: number, t: SLType) => (typeOf(ref) === 1 && t > 1 ? `${T(t)}(n${ref})` : `n${ref}`)
|
|
58
77
|
/** A literal at width `t`. */
|
|
59
78
|
const k = (v: number, t: SLType) => (t > 1 ? `${T(t)}(${lit(v)})` : lit(v))
|
|
60
79
|
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
const n = p.nodes[ref]
|
|
64
|
-
const deps = n.k === "swizzle" ? [n.src] : n.k === "call" ? n.args : []
|
|
65
|
-
for (const d of deps) walk(d)
|
|
66
|
-
emitted.add(ref)
|
|
67
|
-
const e = expr(n)
|
|
68
|
-
lines.push(W ? ` let n${ref}: ${T(n.type)} = ${e};` : ` ${T(n.type)} n${ref} = ${e};`)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const expr = (n: SLNode): string => {
|
|
80
|
+
const expr = (ref: NodeRef): string => {
|
|
81
|
+
const n = valueAt(p.nodes, ref)
|
|
72
82
|
switch (n.k) {
|
|
73
|
-
case "const":
|
|
83
|
+
case "const":
|
|
84
|
+
if (n.kind !== undefined) return n.type === 1 ? kindLit(n.kind, n.v[0]!) : `${typeName(n)}(${n.v.map((v) => kindLit(n.kind!, v)).join(", ")})`
|
|
85
|
+
return n.type === 1 ? lit(n.v[0]) : `${T(n.type)}(${n.v.map(lit).join(", ")})`
|
|
74
86
|
case "input": {
|
|
75
87
|
const res = W ? "sl.res" : "sl_Res"
|
|
76
88
|
switch (INPUT_ID[n.name]) {
|
|
@@ -87,14 +99,15 @@ function emitWeb(p: Program, lang: WebLanguage): string {
|
|
|
87
99
|
}
|
|
88
100
|
case "swizzle": {
|
|
89
101
|
// A scalar has one component and no swizzle in either language.
|
|
90
|
-
if (typeOf(n.src) === 1) return n.type === 1 ? `n${n.src}` : `${
|
|
102
|
+
if (typeOf(n.src) === 1) return n.type === 1 ? `n${n.src}` : `${typeName(n)}(n${n.src})`
|
|
91
103
|
return `n${n.src}.${n.chans.map((c) => "xyzw"[c]).join("")}`
|
|
92
104
|
}
|
|
93
|
-
case "call": return call(n)
|
|
105
|
+
case "call": return call(n, ref)
|
|
106
|
+
default: throw new SLError(`the ${lang.toUpperCase()} emitter reached a ${n.k} as an expression`)
|
|
94
107
|
}
|
|
95
108
|
}
|
|
96
109
|
|
|
97
|
-
const call = (n: Extract<
|
|
110
|
+
const call = (n: Extract<ValueNode, { k: "call" }>, ref: NodeRef): string => {
|
|
98
111
|
const t = n.type
|
|
99
112
|
const a = n.args.map((r) => `n${r}`)
|
|
100
113
|
// Every argument at the result's width, for the element wise ops. WGSL
|
|
@@ -109,8 +122,55 @@ function emitWeb(p: Program, lang: WebLanguage): string {
|
|
|
109
122
|
return W ? LIB_FUNCTIONS[i]!.wgsl : name
|
|
110
123
|
}
|
|
111
124
|
const hlslType = (w: SLType) => (w === 1 ? "float" : `float${w}`)
|
|
125
|
+
const argKind = n.args.length > 0 ? valueAt(p.nodes, n.args[0]!).kind : undefined
|
|
126
|
+
// IR 4: the ops that take ints, uints and bools, by the kinds involved.
|
|
127
|
+
if (n.kind !== undefined || argKind !== undefined) {
|
|
128
|
+
const zero = kindLit(n.kind ?? "int", 0)
|
|
129
|
+
/** `c ? x : y`, which WGSL spells select(y, x, c). */
|
|
130
|
+
const pick = (c: string, x: string, y: string) => (W ? `select(${y}, ${x}, ${c})` : `(${c} ? ${x} : ${y})`)
|
|
131
|
+
switch (n.op) {
|
|
132
|
+
case SLOP.ADD: return `(${a[0]} + ${a[1]})`
|
|
133
|
+
case SLOP.SUB: return `(${a[0]} - ${a[1]})`
|
|
134
|
+
case SLOP.MUL: return `(${a[0]} * ${a[1]})`
|
|
135
|
+
// Truncating, and 0 for a zero divisor. GLSL ES leaves % of a
|
|
136
|
+
// negative undefined, so it is written out from the division.
|
|
137
|
+
case SLOP.DIV: return pick(`${a[1]} == ${zero}`, zero, `${a[0]} / ${a[1]}`)
|
|
138
|
+
case SLOP.MOD: return pick(`${a[1]} == ${zero}`, zero, W ? `${a[0]} % ${a[1]}` : `${a[0]} - ${a[1]} * (${a[0]} / ${a[1]})`)
|
|
139
|
+
case SLOP.NEG: return `(-${a[0]})`
|
|
140
|
+
case SLOP.MIN: return `min(${a[0]}, ${a[1]})`
|
|
141
|
+
case SLOP.MAX: return `max(${a[0]}, ${a[1]})`
|
|
142
|
+
case SLOP.ABS: return `abs(${a[0]})`
|
|
143
|
+
case SLOP.CLAMP: return `clamp(${a[0]}, ${a[1]}, ${a[2]})`
|
|
144
|
+
}
|
|
145
|
+
}
|
|
112
146
|
switch (n.op) {
|
|
113
|
-
case SLOP.COMPOSE: return t === 1 ? a[0] : `${
|
|
147
|
+
case SLOP.COMPOSE: return t === 1 ? a[0] : `${typeName(n)}(${a.join(", ")})`
|
|
148
|
+
case SLOP.CAST: {
|
|
149
|
+
const from = valueAt(p.nodes, n.args[0]!).kind
|
|
150
|
+
if (n.kind === "bool") return `(${a[0]} != ${from === undefined ? "0.0" : kindLit(from, 0)})`
|
|
151
|
+
if (n.kind !== undefined && from === undefined) {
|
|
152
|
+
const [lo, hi] = INT_LIMITS[n.kind]
|
|
153
|
+
return `${typeName(n)}(clamp(${a[0]}, ${lo}, ${hi}))`
|
|
154
|
+
}
|
|
155
|
+
return `${typeName(n)}(${a[0]})`
|
|
156
|
+
}
|
|
157
|
+
case SLOP.LT: return `(${a[0]} < ${a[1]})`
|
|
158
|
+
case SLOP.LE: return `(${a[0]} <= ${a[1]})`
|
|
159
|
+
case SLOP.GT: return `(${a[0]} > ${a[1]})`
|
|
160
|
+
case SLOP.GE: return `(${a[0]} >= ${a[1]})`
|
|
161
|
+
case SLOP.EQ: return `(${a[0]} == ${a[1]})`
|
|
162
|
+
case SLOP.NE: return `(${a[0]} != ${a[1]})`
|
|
163
|
+
case SLOP.AND: return `(${a[0]} && ${a[1]})`
|
|
164
|
+
case SLOP.OR: return `(${a[0]} || ${a[1]})`
|
|
165
|
+
case SLOP.NOT: return `(!${a[0]})`
|
|
166
|
+
case SLOP.BIT_AND: return `(${a[0]} & ${a[1]})`
|
|
167
|
+
case SLOP.BIT_OR: return `(${a[0]} | ${a[1]})`
|
|
168
|
+
case SLOP.BIT_XOR: return `(${a[0]} ^ ${a[1]})`
|
|
169
|
+
case SLOP.BIT_NOT: return `(~${a[0]})`
|
|
170
|
+
// The count modulo 32, which GLSL leaves undefined past 31; WGSL shifts by a u32.
|
|
171
|
+
case SLOP.SHL: return W ? `(${a[0]} << (u32(${a[1]}) & 31u))` : `(${a[0]} << (${a[1]} & ${kindLit(valueAt(p.nodes, n.args[1]!).kind ?? "int", 31)}))`
|
|
172
|
+
case SLOP.SHR: return W ? `(${a[0]} >> (u32(${a[1]}) & 31u))` : `(${a[0]} >> (${a[1]} & ${kindLit(valueAt(p.nodes, n.args[1]!).kind ?? "int", 31)}))`
|
|
173
|
+
case SLOP.CHOOSE: return W ? `select(${a[2]}, ${a[1]}, ${a[0]})` : `(${a[0]} ? ${a[1]} : ${a[2]})`
|
|
114
174
|
|
|
115
175
|
case SLOP.ADD: return `(${s[0]} + ${s[1]})`
|
|
116
176
|
case SLOP.SUB: return `(${s[0]} - ${s[1]})`
|
|
@@ -168,16 +228,29 @@ function emitWeb(p: Program, lang: WebLanguage): string {
|
|
|
168
228
|
case SLOP.HSV2RGB: return `${lib("sl_hsv2rgb")}(${a[0]})`
|
|
169
229
|
case SLOP.NOISE: return `${lib("sl_valueNoise")}(${a[0]})`
|
|
170
230
|
case SLOP.SIMPLEX: return `${lib("sl_simplex")}(${a[0]})`
|
|
171
|
-
case SLOP.FBM: return octaveCall(Math.round(imm[1] ?? 0), a[0]
|
|
172
|
-
case SLOP.TURBULENCE: return octaveCall(2, a[0]
|
|
173
|
-
case SLOP.RIDGED: return octaveCall(3, a[0]
|
|
231
|
+
case SLOP.FBM: return octaveCall(Math.round((n.args.length > 1 ? imm[0] : imm[1]) ?? 0), a[0])
|
|
232
|
+
case SLOP.TURBULENCE: return octaveCall(2, a[0])
|
|
233
|
+
case SLOP.RIDGED: return octaveCall(3, a[0])
|
|
174
234
|
case SLOP.VORONOI: return `${lib("sl_voronoi")}(${a[0]})`
|
|
175
|
-
case SLOP.SDF: return sdfCall(Math.round(imm[0] ?? 0), a[0],
|
|
235
|
+
case SLOP.SDF: return sdfCall(Math.round(imm[0] ?? 0), a[0], sdfParams())
|
|
176
236
|
case SLOP.SAMPLE: {
|
|
177
237
|
const slot = Math.round(imm[0] ?? 0)
|
|
178
238
|
sampled.add(slot)
|
|
239
|
+
// Where pixels part ways there are no neighbours to take a mip
|
|
240
|
+
// level from, and WGSL refuses the implicit sample outright, so
|
|
241
|
+
// every backend samples level 0 there (`structure.ts`).
|
|
242
|
+
if (inVaryingFlow(shape, ref)) {
|
|
243
|
+
return W ? `textureSampleLevel(sl_tex${slot}, sl_samp${slot}, ${a[0]}, 0.0)` : `textureLod(sl_Tex${slot}, ${a[0]}, 0.0)`
|
|
244
|
+
}
|
|
179
245
|
return W ? `textureSample(sl_tex${slot}, sl_samp${slot}, ${a[0]})` : `texture(sl_Tex${slot}, ${a[0]})`
|
|
180
246
|
}
|
|
247
|
+
case SLOP.SAMPLE_LOD: {
|
|
248
|
+
const slot = Math.round(imm[0] ?? 0)
|
|
249
|
+
sampled.add(slot)
|
|
250
|
+
return W
|
|
251
|
+
? `textureSampleLevel(sl_tex${slot}, sl_samp${slot}, ${a[0]}, ${a[1]})`
|
|
252
|
+
: `textureLod(sl_Tex${slot}, ${a[0]}, ${a[1]})`
|
|
253
|
+
}
|
|
181
254
|
default:
|
|
182
255
|
throw new SLError(
|
|
183
256
|
`the ${lang.toUpperCase()} emitter has no case for opcode ${n.op}. A program using it would ` +
|
|
@@ -188,32 +261,78 @@ function emitWeb(p: Program, lang: WebLanguage): string {
|
|
|
188
261
|
/**
|
|
189
262
|
* The library's onejsFbmKind, resolved here since the kind is a
|
|
190
263
|
* constant: calling the dispatcher would print a WGSL `select` that
|
|
191
|
-
* runs two of the four fields to keep one.
|
|
264
|
+
* runs two of the four fields to keep one. The count is a literal when
|
|
265
|
+
* it is a constant, and otherwise rounded and held to 1 to 4 as
|
|
266
|
+
* `sl_fbm` holds it.
|
|
192
267
|
*/
|
|
193
|
-
function octaveCall(kind: number, pt: string
|
|
194
|
-
const
|
|
268
|
+
function octaveCall(kind: number, pt: string): string {
|
|
269
|
+
const c = constantOf(n.args[1]!)
|
|
270
|
+
const o = c !== null
|
|
271
|
+
? String(Math.min(4, Math.max(1, Math.round(c[0]!))))
|
|
272
|
+
: `clamp(${W ? "i32" : "int"}(floor(${a[1]} + 0.5)), 1, 4)`
|
|
195
273
|
const fn = kind === 2 ? "onejsTurbulence" : kind === 3 ? "onejsRidged" : kind === 1 ? "onejsFbmSimplex" : "onejsFbm"
|
|
196
274
|
return `${lib(fn, ["float2", "float", "int", "float", "float"])}(${pt}, 0.0, ${o}, 2.0, 0.5)`
|
|
197
275
|
}
|
|
198
276
|
|
|
277
|
+
/**
|
|
278
|
+
* The six shape parameters, each as a float expression and, when it
|
|
279
|
+
* is a constant, its number: literals from a constant float4 or
|
|
280
|
+
* float2, and their components otherwise.
|
|
281
|
+
*/
|
|
282
|
+
function sdfParams(): Array<{ text: string; value?: number }> {
|
|
283
|
+
const part = (ref: number, chans: string[]) => {
|
|
284
|
+
const c = constantOf(ref)
|
|
285
|
+
return chans.map((ch, i) => c !== null ? { text: lit(c[i]!), value: c[i]! } : { text: `${a[n.args.indexOf(ref)]}.${ch}` })
|
|
286
|
+
}
|
|
287
|
+
return [...part(n.args[1]!, ["x", "y", "z", "w"]), ...part(n.args[2]!, ["x", "y"])]
|
|
288
|
+
}
|
|
289
|
+
|
|
199
290
|
/** The shape is a constant, so this calls it directly instead of sl_sdfDistance's switch. */
|
|
200
|
-
function sdfCall(id: number, pt: string,
|
|
291
|
+
function sdfCall(id: number, pt: string, v: Array<{ text: string; value?: number }>): string {
|
|
201
292
|
const shape = SDF_CALLS[id]
|
|
202
293
|
// sl_sdfDistance returns 1e6 for an id it does not know.
|
|
203
294
|
if (shape === undefined) return lit(1e6)
|
|
204
295
|
const fn = lib(shape.fn)
|
|
205
|
-
const v = [im[1] ?? 0, im[2] ?? 0, im[3] ?? 0, im[4] ?? 0, im[5] ?? 0, im[6] ?? 0]
|
|
206
296
|
const args = shape.args.map((arg) => {
|
|
207
|
-
|
|
208
|
-
|
|
297
|
+
// Truncated toward zero, as HLSL's int() of the float4 does.
|
|
298
|
+
if ("int" in arg) {
|
|
299
|
+
const p = v[arg.int]!
|
|
300
|
+
return p.value !== undefined ? String(Math.trunc(p.value)) : W ? `i32(${p.text})` : `int(${p.text})`
|
|
301
|
+
}
|
|
302
|
+
const parts = arg.map((i) => v[i]!.text)
|
|
209
303
|
return parts.length === 1 ? parts[0] : `${T(parts.length as SLType)}(${parts.join(", ")})`
|
|
210
304
|
})
|
|
211
305
|
return `${fn}(${[pt, ...args].join(", ")})`
|
|
212
306
|
}
|
|
213
307
|
}
|
|
214
308
|
|
|
215
|
-
|
|
216
|
-
const
|
|
309
|
+
const vtype = (ref: NodeRef) => typeName(valueAt(p.nodes, ref))
|
|
310
|
+
const syntax: Syntax = W
|
|
311
|
+
? {
|
|
312
|
+
value: (ref, e) => `let ${local(ref)}: ${vtype(ref)} = ${e};`,
|
|
313
|
+
mutable: (n, like, init) => `var ${n}: ${vtype(like)}${init === undefined ? "" : ` = ${init}`};`,
|
|
314
|
+
counter: (n) => `var ${n}: i32 = 0;`,
|
|
315
|
+
assign: (n, v) => `${n} = ${v};`,
|
|
316
|
+
increment: (n) => `${n} = ${n} + 1;`,
|
|
317
|
+
ifOpen: (c) => `if (${c}) {`,
|
|
318
|
+
elseOpen: "} else {",
|
|
319
|
+
close: "}",
|
|
320
|
+
loopOpen: "loop {",
|
|
321
|
+
breakUnless: (c, turns, max) => `if (!(${c}) || ${turns} >= ${max}) { break; }`,
|
|
322
|
+
}
|
|
323
|
+
: {
|
|
324
|
+
value: (ref, e) => `${vtype(ref)} ${local(ref)} = ${e};`,
|
|
325
|
+
mutable: (n, like, init) => `${vtype(like)} ${n}${init === undefined ? "" : ` = ${init}`};`,
|
|
326
|
+
counter: (n) => `int ${n} = 0;`,
|
|
327
|
+
assign: (n, v) => `${n} = ${v};`,
|
|
328
|
+
increment: (n) => `${n}++;`,
|
|
329
|
+
ifOpen: (c) => `if (${c}) {`,
|
|
330
|
+
elseOpen: "} else {",
|
|
331
|
+
close: "}",
|
|
332
|
+
loopOpen: "for (;;) {",
|
|
333
|
+
breakUnless: (c, turns, max) => `if (!(${c}) || ${turns} >= ${max}) break;`,
|
|
334
|
+
}
|
|
335
|
+
const body = printBody(p, shape, syntax, expr, " ").join("\n")
|
|
217
336
|
const library = [...[...smoothsteps].sort().map((w) => smoothstep(w, lang)), ...librarySource(need, lang)].join("\n")
|
|
218
337
|
const header = `// GENERATED from a shader language program (${p.hash}). Do not edit.`
|
|
219
338
|
// Only the textures the program samples. WebGPU's automatic layout leaves
|