onejs-sl 0.7.0 → 0.9.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
@@ -14,11 +14,23 @@
14
14
  *
15
15
  * sl_Res (target width, target height, seconds, frame)
16
16
  * sl_Opt (1 in a Linear colour space project else 0, 1 to flip uv.y, deltaTime, 0)
17
- * sl_U[16] uniform slots, each a vec4, in the program's slot order
17
+ * sl_U uniform slots, each a vec4, in the program's slot order: 16 of
18
+ * them, or WEB_UNIFORM_SLOTS for a program with more than 16
19
+ * uniforms. A host fills WEB_UNIFORM_SLOTS whatever the program
20
+ * declares, which both languages allow.
18
21
  * textures by slot: GLSL `sl_Tex<slot>`; WGSL `sl_tex<slot>` at binding
19
22
  * 2 + 2 * slot with its own sampler `sl_samp<slot>` at 1 + 2 * slot,
20
23
  * all in group 0 beside the frame block at binding 0. Only the
21
- * slots the program samples are declared.
24
+ * slots the program reads are declared, and in WGSL a slot's
25
+ * sampler only when the program samples it, rather than only
26
+ * measuring it (`textureSize`) or fetching from it (`tex2Dfetch`).
27
+ * a texture's sampler, when its program sets one (`[Wrap(Repeat)]`,
28
+ * `[Filter(Point)]`, IR 6): its declaration line ends
29
+ * `// sampler <wrap> <filter>`, the sampler's in WGSL and the
30
+ * sampler2D's in GLSL, wrap one of repeat, clamp, mirror and filter
31
+ * point or linear. The host samples that slot so, whatever the
32
+ * bound texture's own settings; with no such comment, as those
33
+ * settings say. `samplingOf` in ir.ts says what each word means.
22
34
  * the previous frame, only for a program that samples it: GLSL `sl_Prev`;
23
35
  * WGSL `sl_prev` with its sampler `sl_prevSamp`, at the two
24
36
  * bindings after the last texture slot's, whatever the program
@@ -34,7 +46,7 @@
34
46
  */
35
47
 
36
48
  import { INT_LIMITS } from "./body"
37
- import { SLError, TYPE, valueAt, type NodeRef, type Program, type SLKind, type SLType, type ValueNode } from "./ir"
49
+ import { SLError, TYPE, isWhole, samplingOf, valueAt, type NodeRef, type Program, type TextureDecl, type SLKind, type SLType, type ValueNode } from "./ir"
38
50
  import { inVaryingFlow, local, printBody, structure, type Syntax } from "./structure"
39
51
  import { libClosure, libIndex, LIB_FUNCTIONS, SDF_CALLS } from "./lib"
40
52
  import { LIB_GLSL } from "./lib/glsl"
@@ -43,9 +55,21 @@ import { INPUT_ID, SLOP, TEXTURE_SLOTS, UNIFORM_SLOTS } from "./ops"
43
55
 
44
56
  export type WebLanguage = "glsl" | "wgsl"
45
57
 
46
- /** Uniform slots the host always provides, so every program shares one layout. */
58
+ /** Uniform slots a host provides, so one buffer fits every program. */
47
59
  export const WEB_UNIFORM_SLOTS = UNIFORM_SLOTS
48
60
 
61
+ /**
62
+ * The block a program with 16 uniforms or fewer declares: the size it had
63
+ * before onejs-sl 0.9 raised the cap. A WebGPU host before then (OneJS 3.9.8)
64
+ * binds a buffer of exactly this many slots, which a larger block fails to
65
+ * validate against, so a program that fits keeps drawing there.
66
+ */
67
+ const BASE_UNIFORM_SLOTS = 16
68
+
69
+ function uniformBlock(p: Program): number {
70
+ return p.uniforms.length > BASE_UNIFORM_SLOTS ? WEB_UNIFORM_SLOTS : BASE_UNIFORM_SLOTS
71
+ }
72
+
49
73
  export function emitGLSL(p: Program): string {
50
74
  return emitWeb(p, "glsl")
51
75
  }
@@ -59,9 +83,12 @@ function emitWeb(p: Program, lang: WebLanguage): string {
59
83
  const need = new Set<number>()
60
84
  const smoothsteps = new Set<SLType>()
61
85
  const sampled = new Set<number>()
86
+ /** Slots read without a sampler: measured or fetched from. */
87
+ const loaded = new Set<number>()
62
88
  /** Whether the body samples the previous frame, which declares its binding. */
63
89
  let previous = false
64
90
  const shape = structure(p)
91
+ const size = (slot: number) => W ? `vec2f(textureDimensions(sl_tex${slot}))` : `vec2(textureSize(sl_Tex${slot}, 0))`
65
92
  const T = (t: SLType) => (W ? (t === 1 ? "f32" : `vec${t}f`) : t === 1 ? "float" : `vec${t}`)
66
93
  /** A value's type in this language, its kind included. */
67
94
  const typeName = (n: { type: SLType; kind?: SLKind }): string => {
@@ -261,9 +288,30 @@ function emitWeb(p: Program, lang: WebLanguage): string {
261
288
  case SLOP.SAMPLE_LOD: {
262
289
  const slot = Math.round(imm[0] ?? 0)
263
290
  sampled.add(slot)
291
+ const at = (level: string) => W
292
+ ? `textureSampleLevel(sl_tex${slot}, sl_samp${slot}, ${a[0]}, ${level})`
293
+ : `textureLod(sl_Tex${slot}, ${a[0]}, ${level})`
294
+ // A fraction blends the level below and the level above,
295
+ // whatever the texture's filter between levels, as the HLSL
296
+ // emitter does (`isWhole`). Past the smallest level both read
297
+ // the smallest, as a whole level past it does.
298
+ if (isWhole(p.nodes, n.args[1]!)) return at(a[1]!)
299
+ const below = `floor(${a[1]})`
300
+ return `mix(${at(below)}, ${at(`${below} + 1.0`)}, fract(${a[1]}))`
301
+ }
302
+ case SLOP.TEXTURE_SIZE: {
303
+ const slot = Math.round(imm[0] ?? 0)
304
+ loaded.add(slot)
305
+ return size(slot)
306
+ }
307
+ case SLOP.SAMPLE_FETCH: {
308
+ const slot = Math.round(imm[0] ?? 0)
309
+ loaded.add(slot)
310
+ // Floored and held inside, as the HLSL body does: a load out of
311
+ // range is undefined in GLSL and implementation defined in WGSL.
264
312
  return W
265
- ? `textureSampleLevel(sl_tex${slot}, sl_samp${slot}, ${a[0]}, ${a[1]})`
266
- : `textureLod(sl_Tex${slot}, ${a[0]}, ${a[1]})`
313
+ ? `textureLoad(sl_tex${slot}, vec2i(clamp(floor(${a[0]}), vec2f(0.0), ${size(slot)} - 1.0)), 0)`
314
+ : `texelFetch(sl_Tex${slot}, ivec2(clamp(floor(${a[0]}), vec2(0.0), ${size(slot)} - 1.0)), 0)`
267
315
  }
268
316
  case SLOP.SAMPLE_PREVIOUS:
269
317
  previous = true
@@ -356,21 +404,26 @@ function emitWeb(p: Program, lang: WebLanguage): string {
356
404
  const body = printBody(p, shape, syntax, expr, " ").join("\n")
357
405
  const library = [...[...smoothsteps].sort().map((w) => smoothstep(w, lang)), ...librarySource(need, lang)].join("\n")
358
406
  const header = `// GENERATED from a shader language program (${p.hash}). Do not edit.`
359
- // Only the textures the program samples. WebGPU's automatic layout leaves
360
- // out a binding the shader never reads, and a bind group that supplies one
361
- // anyway does not validate; the host reads these declarations back to know
362
- // which slots to bind.
363
- const textures = p.textures.filter((t) => sampled.has(t.slot))
407
+ // Only the textures the program reads, and in WGSL only the samplers it
408
+ // samples with. WebGPU's automatic layout leaves out a binding the shader
409
+ // never reads, and a bind group that supplies one anyway does not
410
+ // validate; the host reads these declarations back to know what to bind.
411
+ const textures = p.textures.filter((t) => sampled.has(t.slot) || loaded.has(t.slot))
412
+ /** The comment that says a texture's sampler is the program's own, for a slot it samples. */
413
+ const samplerNote = (t: TextureDecl) => {
414
+ const s = sampled.has(t.slot) ? samplingOf(t) : null
415
+ return s === null ? "" : ` // sampler ${s.wrap} ${s.filter}`
416
+ }
364
417
 
365
418
  if (W) {
366
419
  const bindings = [...textures.map((t) =>
367
- `@group(0) @binding(${1 + 2 * t.slot}) var sl_samp${t.slot}: sampler;\n` +
420
+ (sampled.has(t.slot) ? `@group(0) @binding(${1 + 2 * t.slot}) var sl_samp${t.slot}: sampler;${samplerNote(t)}\n` : "") +
368
421
  `@group(0) @binding(${2 + 2 * t.slot}) var sl_tex${t.slot}: texture_2d<f32>;`),
369
422
  ...previous ? [
370
423
  `@group(0) @binding(${1 + 2 * TEXTURE_SLOTS}) var sl_prevSamp: sampler;\n` +
371
424
  `@group(0) @binding(${2 + 2 * TEXTURE_SLOTS}) var sl_prev: texture_2d<f32>;`] : []].join("\n")
372
425
  return `${header}
373
- struct SLFrame { res: vec4f, opt: vec4f, u: array<vec4f, ${WEB_UNIFORM_SLOTS}> }
426
+ struct SLFrame { res: vec4f, opt: vec4f, u: array<vec4f, ${uniformBlock(p)}> }
374
427
  @group(0) @binding(0) var<uniform> sl: SLFrame;
375
428
  ${bindings}
376
429
  ${library}
@@ -386,14 +439,14 @@ ${body}
386
439
  }
387
440
  `
388
441
  }
389
- const samplers = [...textures.map((t) => `uniform sampler2D sl_Tex${t.slot};`), ...previous ? ["uniform sampler2D sl_Prev;"] : []].join("\n")
442
+ const samplers = [...textures.map((t) => `uniform sampler2D sl_Tex${t.slot};${samplerNote(t)}`), ...previous ? ["uniform sampler2D sl_Prev;"] : []].join("\n")
390
443
  return `#version 300 es
391
444
  ${header}
392
445
  precision highp float;
393
446
  precision highp int;
394
447
  uniform vec4 sl_Res;
395
448
  uniform vec4 sl_Opt;
396
- uniform vec4 sl_U[${WEB_UNIFORM_SLOTS}];
449
+ uniform vec4 sl_U[${uniformBlock(p)}];
397
450
  ${samplers}
398
451
  out vec4 sl_Out;
399
452
  ${library}