onejs-sl 0.1.1 → 0.1.3

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/params.ts ADDED
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Parameter names, for an editor's completions and signature help: so it can
3
+ * offer `lerp(x, y, s)` rather than "lerp takes 3 arguments".
4
+ *
5
+ * Metadata only. Nothing that checks or emits a program reads this file, and
6
+ * only `onejs-sl/tables` imports it, so it cannot change what any program
7
+ * compiles to. Each table is built by a call marked pure, so a bundle that
8
+ * imports `onejs-sl/tables` for BUILTINS alone (the Play editor's parser) drops
9
+ * all of it.
10
+ *
11
+ * The intrinsics carry the names the HLSL documentation gives them. Everything
12
+ * the library implements is named as `lib/*.hlsl` writes it, read by the
13
+ * translator into `src/lib/names.ts` when `npm run lib` runs, so there is no
14
+ * second list of those to drift: a builtin that lowers to a library helper takes
15
+ * that helper's names, and a shape's parameters are named after its function's
16
+ * parameters, in the slots `sl_sdfDistance` passes them in.
17
+ */
18
+
19
+ import { BUILTINS } from "./lang/builtins"
20
+ import { LIB_FUNCTIONS, SDF_CALLS } from "./lib/table"
21
+ import { LIB_PARAM_NAMES } from "./lib/names"
22
+ import { SL_SDF_PARAMS, SL_SDF_SHAPES, type SlSdfKind } from "./shapes"
23
+
24
+ /** One function of the helper library, as its source declares it. */
25
+ export interface LibSignature {
26
+ name: string
27
+ params: ReadonlyArray<{ name: string; type: string }>
28
+ ret: string
29
+ }
30
+
31
+ /** Every function in the helper library, by `LIB_FUNCTIONS` index, overloads included. */
32
+ export const LIB_SIGNATURES: readonly LibSignature[] = /* @__PURE__ */ LIB_FUNCTIONS.map((f, i) => ({
33
+ name: f.name,
34
+ params: f.params.map((type, j) => ({ name: LIB_PARAM_NAMES[i]![j]!, type })),
35
+ ret: f.ret,
36
+ }))
37
+
38
+ /** A library function's parameter names, the same for every overload of it. */
39
+ function namesOf(fn: string): readonly string[] {
40
+ const found = LIB_SIGNATURES.filter((s) => s.name === fn).map((s) => s.params.map((p) => p.name).join(", "))
41
+ if (found.length === 0) throw new Error(`the library has no ${fn}`)
42
+ if (new Set(found).size > 1) throw new Error(`${fn}'s overloads name their parameters differently: ${found.join(" / ")}`)
43
+ return found[0]!.split(", ")
44
+ }
45
+
46
+ /** The HLSL documentation's names. */
47
+ const INTRINSIC: Record<string, readonly string[]> = {
48
+ sin: ["x"], cos: ["x"], tan: ["x"], asin: ["x"], acos: ["x"], exp: ["x"], log: ["x"], sqrt: ["x"],
49
+ sign: ["x"], ceil: ["x"], round: ["x"], abs: ["x"], floor: ["x"], frac: ["x"], saturate: ["x"],
50
+ rcp: ["x"], normalize: ["x"], length: ["x"],
51
+ pow: ["x", "y"], min: ["x", "y"], max: ["x", "y"], clamp: ["x", "min", "max"], atan2: ["y", "x"],
52
+ distance: ["x", "y"], dot: ["x", "y"], cross: ["x", "y"], reflect: ["i", "n"],
53
+ lerp: ["x", "y", "s"], step: ["y", "x"], smoothstep: ["min", "max", "x"],
54
+ tex2D: ["s", "t"],
55
+ }
56
+
57
+ /**
58
+ * The library helper each builtin lowers to (`emitBody` calls it, and a test
59
+ * checks that it does), and how many of its parameters an author writes: fbm's
60
+ * third, the kind, is the opcode's own.
61
+ */
62
+ export const LIBRARY: Readonly<Record<string, [string, number]>> = {
63
+ toLinear: ["sl_toLinear", 1], hsv2rgb: ["sl_hsv2rgb", 1], luminance: ["sl_luminance", 1],
64
+ noise: ["sl_valueNoise", 1], simplex: ["sl_simplex", 1], voronoi: ["sl_voronoi", 1],
65
+ fbm: ["sl_fbm", 2], turbulence: ["sl_fbm", 2], ridged: ["sl_fbm", 2],
66
+ // The point; the shape's own parameters are SL_SDF_PARAM_NAMES.
67
+ sdf: ["sl_sdfDistance", 0],
68
+ }
69
+
70
+ /** The two the language defines itself, named as the EDSL's `sl.remap` and `sl.ramp` name theirs. */
71
+ const MACRO: Record<string, readonly string[]> = {
72
+ remap: ["v", "fromMin", "fromMax", "toMin", "toMax"],
73
+ // At least two stops, and as many more as `max` allows.
74
+ ramp: ["t", "stop", "stop..."],
75
+ }
76
+
77
+ /**
78
+ * Each builtin's parameter names, one per argument up to its `max`; those past
79
+ * its `min` are optional. A name ending in `...` repeats up to `max`. `sdf` is
80
+ * written `sdf.circle(p, r)`: its entry names the point, and the shape's own
81
+ * parameters are `SL_SDF_PARAM_NAMES[shape]`.
82
+ */
83
+ export const BUILTIN_PARAMS: Readonly<Record<string, readonly string[]>> = /* @__PURE__ */ Object.fromEntries(
84
+ Object.keys(BUILTINS).map((name) => {
85
+ const lib = LIBRARY[name]
86
+ if (name === "sdf") return [name, [namesOf(lib![0])[1]!]]
87
+ const names = INTRINSIC[name] ?? MACRO[name] ?? (lib && namesOf(lib[0]).slice(0, lib[1]))
88
+ if (names === undefined) throw new Error(`builtin ${name} has no parameter names in params.ts`)
89
+ return [name, names]
90
+ }),
91
+ )
92
+
93
+ /**
94
+ * Each shape's parameters, in the order `sdf.<shape>(p, ...)` takes them, named
95
+ * after its function's: a vector parameter's components as `b.x`, `b.y`.
96
+ */
97
+ export const SL_SDF_PARAM_NAMES: Readonly<Record<SlSdfKind, readonly string[]>> = /* @__PURE__ */ shapeNames()
98
+
99
+ function shapeNames(): Record<SlSdfKind, readonly string[]> {
100
+ const out = {} as Record<SlSdfKind, readonly string[]>
101
+ for (const shape of Object.keys(SL_SDF_SHAPES) as SlSdfKind[]) {
102
+ const call = SDF_CALLS[SL_SDF_SHAPES[shape]]!
103
+ // The function's first parameter is the point, which `sl_sdfDistance` passes itself.
104
+ const fn = namesOf(call.fn).slice(1)
105
+ const slots = new Array<string>(SL_SDF_PARAMS[shape])
106
+ call.args.forEach((arg, i) => {
107
+ const at = "int" in arg ? [arg.int] : arg
108
+ at.forEach((slot, j) => { slots[slot] = at.length === 1 ? fn[i]! : `${fn[i]}.${"xyzw"[j]}` })
109
+ })
110
+ out[shape] = slots
111
+ }
112
+ return out
113
+ }
package/src/shapes.ts CHANGED
@@ -12,8 +12,8 @@
12
12
  *
13
13
  * Duplicating and pinning is the trade this repository already makes for the
14
14
  * painter and particle opcode tables, and for the SDF dispatcher that exists in
15
- * both FxSources and SLCommon. The ids are a contract with SDF2D.cginc either
16
- * way; what matters is that a test fails when they drift, not that there is
15
+ * both FxSources and SLCommon. The ids are a contract with `lib/sdf2d.hlsl`
16
+ * (SDF2D.cginc in OneJS) either way; what matters is that a test fails when they drift, not that there is
17
17
  * only one copy.
18
18
  */
19
19
  export const SL_SDF_SHAPES = {
@@ -30,8 +30,8 @@ export const SL_SDF_SHAPES = {
30
30
  export type SlSdfKind = keyof typeof SL_SDF_SHAPES
31
31
 
32
32
  /**
33
- * How many parameters each shape reads, in the order SDF2D.cginc takes them
34
- * (its `a` then `b`). Six shapes take more than four: roundedBox's last two
33
+ * How many parameters each shape reads, in the order `sl_sdfDistance` in
34
+ * `lib/common.hlsl` takes them (its `a` then `b`). Six shapes take more than four: roundedBox's last two
35
35
  * corner radii, orientedBox's thickness, triangle's third vertex, horseshoe's
36
36
  * second width, orientedVesica's width and bezier's end point. `sl.sdf` takes
37
37
  * up to this many, and a test derives the same counts from the web emitters'
package/src/sl.ts CHANGED
@@ -461,6 +461,14 @@ function difference(a: Num, b: Num): Num {
461
461
 
462
462
  export function atan2(y: Num, x: Num): Float {
463
463
  const b = ctx()
464
+ // An angle is one number. Given vectors, the node would still be a float
465
+ // and every backend would disagree about which lane it kept: HLSL takes
466
+ // .x silently, GLSL and WGSL refuse to compile.
467
+ for (const v of [y, x]) {
468
+ if (typeof v !== "number" && v.width !== 1) {
469
+ throw new SLError(`atan2 takes two floats, and this is a ${v.width === 2 ? "float2" : v.width === 3 ? "float3" : "float4"}: call it per component`)
470
+ }
471
+ }
464
472
  const yy = typeof y === "number" ? float(y) : y
465
473
  const [a, c] = align2(yy as Val, x)
466
474
  return mk(b, b.call(SLOP.ATAN2, TYPE.FLOAT, [a, c]), TYPE.FLOAT)
@@ -622,7 +630,7 @@ export function ramp(t: Num, stops: Array<string | [number, number, number, numb
622
630
  * fifth and sixth.
623
631
  *
624
632
  * Which parameters a shape takes is the shape's own business; `circle` wants a
625
- * radius, `roundedBox` wants half extents and a corner. See SDF2D.cginc.
633
+ * radius, `roundedBox` wants half extents and a corner. See `lib/sdf2d.hlsl`.
626
634
  */
627
635
  export function sdf(kind: SlSdfKind, p: Vec2, params: number[] = []): Float {
628
636
  const id = SL_SDF_SHAPES[kind]
package/src/tables.ts CHANGED
@@ -2,7 +2,8 @@
2
2
  * `onejs-sl/tables`: what completion and highlighting read.
3
3
  *
4
4
  * The same tables the checker and the emitters use, so an editor offers
5
- * exactly the builtins that compile, with the arities they take.
5
+ * exactly the builtins that compile, with the arities they take and the names
6
+ * of their parameters (`params.ts`), and the helper library's signatures.
6
7
  */
7
8
  export { BUILTINS, NOT_YET } from "./lang/builtins"
8
9
  export type { Builtin } from "./lang/builtins"
@@ -12,4 +13,6 @@ export {
12
13
  export type { SLSurface } from "./ops"
13
14
  export { INPUTS } from "./ir"
14
15
  export { SL_SDF_SHAPES, SL_SDF_PARAMS } from "./shapes"
16
+ export { BUILTIN_PARAMS, LIB_SIGNATURES, SL_SDF_PARAM_NAMES } from "./params"
17
+ export type { LibSignature } from "./params"
15
18
  export type { SlSdfKind } from "./shapes"
package/src/web.ts CHANGED
@@ -26,8 +26,10 @@
26
26
  */
27
27
 
28
28
  import { SLError, TYPE, type Program, type SLNode, type SLType } from "./ir"
29
+ import { libClosure, libIndex, LIB_FUNCTIONS, SDF_CALLS } from "./lib"
30
+ import { LIB_GLSL } from "./lib/glsl"
31
+ import { LIB_WGSL } from "./lib/wgsl"
29
32
  import { INPUT_ID, SLOP, VM_UNIFORMS } from "./ops"
30
- import { SDF_CALLS, WEB_LIB } from "./weblib"
31
33
 
32
34
  export type WebLanguage = "glsl" | "wgsl"
33
35
 
@@ -44,7 +46,8 @@ export function emitWGSL(p: Program): string {
44
46
 
45
47
  function emitWeb(p: Program, lang: WebLanguage): string {
46
48
  const W = lang === "wgsl"
47
- const need = new Set<string>()
49
+ const need = new Set<number>()
50
+ const smoothsteps = new Set<SLType>()
48
51
  const sampled = new Set<number>()
49
52
  const lines: string[] = []
50
53
  const emitted = new Set<number>()
@@ -100,7 +103,13 @@ function emitWeb(p: Program, lang: WebLanguage): string {
100
103
  const s = n.args.map((r) => splat(r, t))
101
104
  const w0 = n.args.length > 0 ? typeOf(n.args[0]) : 1
102
105
  const imm = n.imm ?? []
103
- const lib = (name: string) => { need.add(name); return name }
106
+ /** A library function by name, and by HLSL parameter types where it is overloaded. */
107
+ const lib = (name: string, params?: string[]) => {
108
+ const i = libIndex(name, params)
109
+ need.add(i)
110
+ return W ? LIB_FUNCTIONS[i]!.wgsl : name
111
+ }
112
+ const hlslType = (w: SLType) => (w === 1 ? "float" : `float${w}`)
104
113
  switch (n.op) {
105
114
  case SLOP.COMPOSE: return t === 1 ? a[0] : `${T(t)}(${a.join(", ")})`
106
115
 
@@ -145,11 +154,11 @@ function emitWeb(p: Program, lang: WebLanguage): string {
145
154
  ? `(${a[0]} - 2.0 * ${a[1]} * ${a[0]} * ${a[1]})`
146
155
  : `reflect(${a[0]}, ${a[1]})`
147
156
  case SLOP.LUMINANCE: return `dot(${a[0]}.rgb, ${T(3)}(0.2126, 0.7152, 0.0722))`
148
- case SLOP.TO_LINEAR: return `${lib(`sl_toLinear${t}`)}(${a[0]})`
157
+ case SLOP.TO_LINEAR: return `${lib("sl_toLinear", [hlslType(t)])}(${a[0]})`
149
158
 
150
159
  case SLOP.MIX: return `mix(${s[0]}, ${s[1]}, ${s[2]})`
151
160
  case SLOP.STEP: return `step(${s[0]}, ${s[1]})`
152
- case SLOP.SMOOTHSTEP: return `${lib(`sl_smoothstep${t}`)}(${s[0]}, ${s[1]}, ${s[2]})`
161
+ case SLOP.SMOOTHSTEP: smoothsteps.add(t); return `sl_smoothstep${t}(${s[0]}, ${s[1]}, ${s[2]})`
153
162
  // As the VM and the HLSL emitter do it: branchless, so every
154
163
  // backend agrees on the edge value.
155
164
  case SLOP.SELECT: {
@@ -158,8 +167,8 @@ function emitWeb(p: Program, lang: WebLanguage): string {
158
167
  }
159
168
 
160
169
  case SLOP.HSV2RGB: return `${lib("sl_hsv2rgb")}(${a[0]})`
161
- case SLOP.NOISE: return `${lib("oj_vnoise")}(${a[0]}, 0.0)`
162
- case SLOP.SIMPLEX: return `${lib("oj_simplex")}(${a[0]}, 0.0)`
170
+ case SLOP.NOISE: return `${lib("sl_valueNoise")}(${a[0]})`
171
+ case SLOP.SIMPLEX: return `${lib("sl_simplex")}(${a[0]})`
163
172
  case SLOP.FBM: return octaveCall(Math.round(imm[1] ?? 0), a[0], imm[0])
164
173
  case SLOP.TURBULENCE: return octaveCall(2, a[0], imm[0])
165
174
  case SLOP.RIDGED: return octaveCall(3, a[0], imm[0])
@@ -177,32 +186,36 @@ function emitWeb(p: Program, lang: WebLanguage): string {
177
186
  )
178
187
  }
179
188
 
180
- /** Noise2D.cginc's onejsFbmKind, resolved here since the kind is a constant. */
189
+ /**
190
+ * The library's onejsFbmKind, resolved here since the kind is a
191
+ * constant: calling the dispatcher would print a WGSL `select` that
192
+ * runs two of the four fields to keep one.
193
+ */
181
194
  function octaveCall(kind: number, pt: string, octaves: number | undefined): string {
182
195
  const o = Math.min(4, Math.max(1, Math.round(octaves ?? 3)))
183
- const fn = kind === 2 ? "oj_turbulence" : kind === 3 ? "oj_ridged" : kind === 1 ? "oj_fbmSimplex" : "oj_fbm"
184
- return `${lib(fn)}(${pt}, 0.0, ${o})`
196
+ const fn = kind === 2 ? "onejsTurbulence" : kind === 3 ? "onejsRidged" : kind === 1 ? "onejsFbmSimplex" : "onejsFbm"
197
+ return `${lib(fn, ["float2", "float", "int", "float", "float"])}(${pt}, 0.0, ${o}, 2.0, 0.5)`
185
198
  }
186
199
 
187
- /** The shape is a constant, so this calls it directly instead of SLCommon's switch. */
200
+ /** The shape is a constant, so this calls it directly instead of sl_sdfDistance's switch. */
188
201
  function sdfCall(id: number, pt: string, im: number[]): string {
189
202
  const shape = SDF_CALLS[id]
190
- // SLCommon's dispatcher returns 1e6 for an id it does not know.
203
+ // sl_sdfDistance returns 1e6 for an id it does not know.
191
204
  if (shape === undefined) return lit(1e6)
192
- lib(shape.fn)
205
+ const fn = lib(shape.fn)
193
206
  const v = [im[1] ?? 0, im[2] ?? 0, im[3] ?? 0, im[4] ?? 0, im[5] ?? 0, im[6] ?? 0]
194
207
  const args = shape.args.map((arg) => {
195
- if (!Array.isArray(arg)) return String(Math.trunc(v[arg.int]))
208
+ if ("int" in arg) return String(Math.trunc(v[arg.int]))
196
209
  const parts = arg.map((i) => lit(v[i]))
197
210
  return parts.length === 1 ? parts[0] : `${T(parts.length as SLType)}(${parts.join(", ")})`
198
211
  })
199
- return `${shape.fn}(${[pt, ...args].join(", ")})`
212
+ return `${fn}(${[pt, ...args].join(", ")})`
200
213
  }
201
214
  }
202
215
 
203
216
  walk(p.result)
204
217
  const body = lines.join("\n")
205
- const library = librarySource(need, lang)
218
+ const library = [...[...smoothsteps].sort().map((w) => smoothstep(w, lang)), ...librarySource(need, lang)].join("\n")
206
219
  const header = `// GENERATED from a shader language program (${p.hash}). Do not edit.`
207
220
  // Only the textures the program samples. WebGPU's automatic layout leaves
208
221
  // out a binding the shader never reads, and a bind group that supplies one
@@ -251,19 +264,31 @@ ${body}
251
264
  `
252
265
  }
253
266
 
254
- /** The needed entries and everything they depend on, in library order. */
255
- function librarySource(need: Set<string>, lang: WebLanguage): string {
256
- const want = new Set<string>()
257
- const byName = new Map(WEB_LIB.map((e) => [e.name, e]))
258
- const add = (name: string) => {
259
- if (want.has(name)) return
260
- const entry = byName.get(name)
261
- if (entry === undefined) throw new SLError(`internal: the web library has no entry "${name}"`)
262
- want.add(name)
263
- for (const d of entry.deps) add(d)
267
+ /** The needed library functions and everything they call, in library order. */
268
+ function librarySource(need: Set<number>, lang: WebLanguage): string[] {
269
+ const texts = lang === "wgsl" ? LIB_WGSL : LIB_GLSL
270
+ return libClosure(need).map((i) => texts[i]!.trim())
271
+ }
272
+
273
+ /**
274
+ * HLSL's smoothstep is the formula whatever the edge order; GLSL leaves
275
+ * edge0 >= edge1 undefined, so the web writes the formula out. This belongs
276
+ * to the web frame rather than the library: the HLSL backends call the
277
+ * intrinsic, which already means this.
278
+ */
279
+ function smoothstep(w: SLType, lang: WebLanguage): string {
280
+ if (lang === "glsl") {
281
+ const g = w === 1 ? "float" : `vec${w}`
282
+ return `${g} sl_smoothstep${w}(${g} a, ${g} b, ${g} x) {
283
+ ${g} t = clamp((x - a) / (b - a), 0.0, 1.0);
284
+ return t * t * (3.0 - 2.0 * t);
285
+ }`
264
286
  }
265
- for (const n of need) add(n)
266
- return WEB_LIB.filter((e) => want.has(e.name)).map((e) => (lang === "wgsl" ? e.wgsl : e.glsl).trim()).join("\n")
287
+ const t = w === 1 ? "f32" : `vec${w}f`
288
+ return `fn sl_smoothstep${w}(a: ${t}, b: ${t}, x: ${t}) -> ${t} {
289
+ let t = saturate((x - a) / (b - a));
290
+ return t * t * (3.0 - 2.0 * t);
291
+ }`
267
292
  }
268
293
 
269
294
  /** A literal that survives a float32 round trip and never reads as an int. */