onejs-sl 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/web.ts CHANGED
@@ -24,7 +24,9 @@
24
24
  * vertex index) so one module is one pipeline; the GLSL host supplies its own.
25
25
  */
26
26
 
27
- import { SLError, TYPE, type Program, type SLNode, type SLType } from "./ir"
27
+ import { INT_LIMITS } from "./body"
28
+ import { SLError, TYPE, valueAt, type NodeRef, type Program, type SLKind, type SLType, type ValueNode } from "./ir"
29
+ import { inVaryingFlow, local, printBody, structure, type Syntax } from "./structure"
28
30
  import { libClosure, libIndex, LIB_FUNCTIONS, SDF_CALLS } from "./lib"
29
31
  import { LIB_GLSL } from "./lib/glsl"
30
32
  import { LIB_WGSL } from "./lib/wgsl"
@@ -48,10 +50,22 @@ function emitWeb(p: Program, lang: WebLanguage): string {
48
50
  const need = new Set<number>()
49
51
  const smoothsteps = new Set<SLType>()
50
52
  const sampled = new Set<number>()
51
- const lines: string[] = []
52
- const emitted = new Set<number>()
53
+ const shape = structure(p)
53
54
  const T = (t: SLType) => (W ? (t === 1 ? "f32" : `vec${t}f`) : t === 1 ? "float" : `vec${t}`)
54
- const typeOf = (ref: number) => p.nodes[ref].type
55
+ /** A value's type in this language, its kind included. */
56
+ const typeName = (n: { type: SLType; kind?: SLKind }): string => {
57
+ if (n.kind === undefined) return T(n.type)
58
+ const scalar = W ? { int: "i32", uint: "u32", bool: "bool" }[n.kind] : n.kind
59
+ if (n.type === 1) return scalar
60
+ return W ? `vec${n.type}<${scalar}>` : `${{ int: "i", uint: "u", bool: "b" }[n.kind]}vec${n.type}`
61
+ }
62
+ const kindLit = (kind: SLKind, v: number): string => {
63
+ if (kind === "bool") return v !== 0 ? "true" : "false"
64
+ if (kind === "uint") return `${v >>> 0}u`
65
+ const i = W ? `${v}i` : String(v)
66
+ return v < 0 ? `(${i})` : i
67
+ }
68
+ const typeOf = (ref: number) => valueAt(p.nodes, ref).type
55
69
  /** A constant node's value, or null for anything computed. */
56
70
  const constantOf = (ref: number): number[] | null => {
57
71
  const c = p.nodes[ref]!
@@ -63,19 +77,12 @@ function emitWeb(p: Program, lang: WebLanguage): string {
63
77
  /** A literal at width `t`. */
64
78
  const k = (v: number, t: SLType) => (t > 1 ? `${T(t)}(${lit(v)})` : lit(v))
65
79
 
66
- const walk = (ref: number): void => {
67
- if (emitted.has(ref)) return
68
- const n = p.nodes[ref]
69
- const deps = n.k === "swizzle" ? [n.src] : n.k === "call" ? n.args : []
70
- for (const d of deps) walk(d)
71
- emitted.add(ref)
72
- const e = expr(n)
73
- lines.push(W ? ` let n${ref}: ${T(n.type)} = ${e};` : ` ${T(n.type)} n${ref} = ${e};`)
74
- }
75
-
76
- const expr = (n: SLNode): string => {
80
+ const expr = (ref: NodeRef): string => {
81
+ const n = valueAt(p.nodes, ref)
77
82
  switch (n.k) {
78
- case "const": return n.type === 1 ? lit(n.v[0]) : `${T(n.type)}(${n.v.map(lit).join(", ")})`
83
+ case "const":
84
+ if (n.kind !== undefined) return n.type === 1 ? kindLit(n.kind, n.v[0]!) : `${typeName(n)}(${n.v.map((v) => kindLit(n.kind!, v)).join(", ")})`
85
+ return n.type === 1 ? lit(n.v[0]) : `${T(n.type)}(${n.v.map(lit).join(", ")})`
79
86
  case "input": {
80
87
  const res = W ? "sl.res" : "sl_Res"
81
88
  switch (INPUT_ID[n.name]) {
@@ -92,14 +99,15 @@ function emitWeb(p: Program, lang: WebLanguage): string {
92
99
  }
93
100
  case "swizzle": {
94
101
  // A scalar has one component and no swizzle in either language.
95
- if (typeOf(n.src) === 1) return n.type === 1 ? `n${n.src}` : `${T(n.type)}(n${n.src})`
102
+ if (typeOf(n.src) === 1) return n.type === 1 ? `n${n.src}` : `${typeName(n)}(n${n.src})`
96
103
  return `n${n.src}.${n.chans.map((c) => "xyzw"[c]).join("")}`
97
104
  }
98
- 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`)
99
107
  }
100
108
  }
101
109
 
102
- const call = (n: Extract<SLNode, { k: "call" }>): string => {
110
+ const call = (n: Extract<ValueNode, { k: "call" }>, ref: NodeRef): string => {
103
111
  const t = n.type
104
112
  const a = n.args.map((r) => `n${r}`)
105
113
  // Every argument at the result's width, for the element wise ops. WGSL
@@ -114,8 +122,55 @@ function emitWeb(p: Program, lang: WebLanguage): string {
114
122
  return W ? LIB_FUNCTIONS[i]!.wgsl : name
115
123
  }
116
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
+ }
117
146
  switch (n.op) {
118
- case SLOP.COMPOSE: return t === 1 ? a[0] : `${T(t)}(${a.join(", ")})`
147
+ case SLOP.COMPOSE: return t === 1 ? a[0] : `${typeName(n)}(${a.join(", ")})`
148
+ case SLOP.CAST: {
149
+ const from = valueAt(p.nodes, n.args[0]!).kind
150
+ if (n.kind === "bool") return `(${a[0]} != ${from === undefined ? "0.0" : kindLit(from, 0)})`
151
+ if (n.kind !== undefined && from === undefined) {
152
+ const [lo, hi] = INT_LIMITS[n.kind]
153
+ return `${typeName(n)}(clamp(${a[0]}, ${lo}, ${hi}))`
154
+ }
155
+ return `${typeName(n)}(${a[0]})`
156
+ }
157
+ case SLOP.LT: return `(${a[0]} < ${a[1]})`
158
+ case SLOP.LE: return `(${a[0]} <= ${a[1]})`
159
+ case SLOP.GT: return `(${a[0]} > ${a[1]})`
160
+ case SLOP.GE: return `(${a[0]} >= ${a[1]})`
161
+ case SLOP.EQ: return `(${a[0]} == ${a[1]})`
162
+ case SLOP.NE: return `(${a[0]} != ${a[1]})`
163
+ case SLOP.AND: return `(${a[0]} && ${a[1]})`
164
+ case SLOP.OR: return `(${a[0]} || ${a[1]})`
165
+ case SLOP.NOT: return `(!${a[0]})`
166
+ case SLOP.BIT_AND: return `(${a[0]} & ${a[1]})`
167
+ case SLOP.BIT_OR: return `(${a[0]} | ${a[1]})`
168
+ case SLOP.BIT_XOR: return `(${a[0]} ^ ${a[1]})`
169
+ case SLOP.BIT_NOT: return `(~${a[0]})`
170
+ // The count modulo 32, which GLSL leaves undefined past 31; WGSL shifts by a u32.
171
+ case SLOP.SHL: return W ? `(${a[0]} << (u32(${a[1]}) & 31u))` : `(${a[0]} << (${a[1]} & ${kindLit(valueAt(p.nodes, n.args[1]!).kind ?? "int", 31)}))`
172
+ case SLOP.SHR: return W ? `(${a[0]} >> (u32(${a[1]}) & 31u))` : `(${a[0]} >> (${a[1]} & ${kindLit(valueAt(p.nodes, n.args[1]!).kind ?? "int", 31)}))`
173
+ case SLOP.CHOOSE: return W ? `select(${a[2]}, ${a[1]}, ${a[0]})` : `(${a[0]} ? ${a[1]} : ${a[2]})`
119
174
 
120
175
  case SLOP.ADD: return `(${s[0]} + ${s[1]})`
121
176
  case SLOP.SUB: return `(${s[0]} - ${s[1]})`
@@ -181,6 +236,12 @@ function emitWeb(p: Program, lang: WebLanguage): string {
181
236
  case SLOP.SAMPLE: {
182
237
  const slot = Math.round(imm[0] ?? 0)
183
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
+ }
184
245
  return W ? `textureSample(sl_tex${slot}, sl_samp${slot}, ${a[0]})` : `texture(sl_Tex${slot}, ${a[0]})`
185
246
  }
186
247
  case SLOP.SAMPLE_LOD: {
@@ -245,8 +306,33 @@ function emitWeb(p: Program, lang: WebLanguage): string {
245
306
  }
246
307
  }
247
308
 
248
- walk(p.result)
249
- const body = lines.join("\n")
309
+ const vtype = (ref: NodeRef) => typeName(valueAt(p.nodes, ref))
310
+ const syntax: Syntax = W
311
+ ? {
312
+ value: (ref, e) => `let ${local(ref)}: ${vtype(ref)} = ${e};`,
313
+ mutable: (n, like, init) => `var ${n}: ${vtype(like)}${init === undefined ? "" : ` = ${init}`};`,
314
+ counter: (n) => `var ${n}: i32 = 0;`,
315
+ assign: (n, v) => `${n} = ${v};`,
316
+ increment: (n) => `${n} = ${n} + 1;`,
317
+ ifOpen: (c) => `if (${c}) {`,
318
+ elseOpen: "} else {",
319
+ close: "}",
320
+ loopOpen: "loop {",
321
+ breakUnless: (c, turns, max) => `if (!(${c}) || ${turns} >= ${max}) { break; }`,
322
+ }
323
+ : {
324
+ value: (ref, e) => `${vtype(ref)} ${local(ref)} = ${e};`,
325
+ mutable: (n, like, init) => `${vtype(like)} ${n}${init === undefined ? "" : ` = ${init}`};`,
326
+ counter: (n) => `int ${n} = 0;`,
327
+ assign: (n, v) => `${n} = ${v};`,
328
+ increment: (n) => `${n}++;`,
329
+ ifOpen: (c) => `if (${c}) {`,
330
+ elseOpen: "} else {",
331
+ close: "}",
332
+ loopOpen: "for (;;) {",
333
+ breakUnless: (c, turns, max) => `if (!(${c}) || ${turns} >= ${max}) break;`,
334
+ }
335
+ const body = printBody(p, shape, syntax, expr, " ").join("\n")
250
336
  const library = [...[...smoothsteps].sort().map((w) => smoothstep(w, lang)), ...librarySource(need, lang)].join("\n")
251
337
  const header = `// GENERATED from a shader language program (${p.hash}). Do not edit.`
252
338
  // Only the textures the program samples. WebGPU's automatic layout leaves