onejs-sl 0.8.0 → 0.9.1

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/lang/index.ts CHANGED
@@ -33,8 +33,8 @@ export { TYPE_WIDTH } from "./ast"
33
33
  export { SLParseError }
34
34
  export type { Pos, SLFix, Token } from "./lexer"
35
35
  export { tokenize } from "./lexer"
36
- export { classify } from "./classify"
37
- export type { SLClassifiedToken, SLTokenClass } from "./classify"
36
+ export { classify, classifyLine } from "./classify"
37
+ export type { SLClassifiedToken, SLLineState, SLTokenClass } from "./classify"
38
38
  export { SL_KEYWORDS, SL_TYPES } from "./words"
39
39
  export { parseUnit } from "./parser"
40
40
  export { PRELUDE_SOURCE, preludeFunctions } from "./prelude"
package/src/lang/lower.ts CHANGED
@@ -58,7 +58,7 @@ import type { SlSdfKind } from "../shapes"
58
58
  import * as sl from "../sl"
59
59
  import { Val, type Num } from "../sl"
60
60
  import { TYPE_KIND, TYPE_WIDTH, type BinaryOp, type Expr, type FuncDecl, type Stmt, type TypeName } from "./ast"
61
- import { readAttributes } from "./attributes"
61
+ import { readAttributes, readTextureAttributes } from "./attributes"
62
62
  import { BUILTINS } from "./builtins"
63
63
  import { terminates, type Checked } from "./check"
64
64
  import { SLParseError, type Pos } from "./lexer"
@@ -288,7 +288,10 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
288
288
  global.declare(u.name, { width, kind: TYPE_KIND[u.type], value })
289
289
  }
290
290
 
291
- for (const t of unit.textures) samplers.set(t.name, at(t.pos, () => sl.texture(t.name)))
291
+ // The checker reported any attribute it could not read; what it could goes in.
292
+ for (const t of unit.textures) {
293
+ samplers.set(t.name, at(t.pos, () => sl.texture(t.name, readTextureAttributes(t).sampling)))
294
+ }
292
295
  // Unless the file declared a texture of its own by the name. The
293
296
  // checker refuses tex2Dlod on it, and anything else that took the
294
297
  // name, before lowering could reach either.
@@ -296,6 +299,8 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
296
299
  samplers.set(PREVIOUS, {
297
300
  sample: (uv) => inputs.previous.sample(uv),
298
301
  sampleLevel: () => { throw new Error("tex2Dlod(previous) reached lowering; the checker refuses it") },
302
+ size: () => { throw new Error("textureSize(previous) reached lowering; the checker refuses it") },
303
+ fetch: () => { throw new Error("tex2Dfetch(previous) reached lowering; the checker refuses it") },
299
304
  })
300
305
  }
301
306
 
@@ -1160,6 +1165,16 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
1160
1165
  if (n === "int" || n === "uint" || n === "bool") return convert(n, e, scope)
1161
1166
  if (n in TYPE_WIDTH) return construct(n as TypeName, floats(e.args), e.pos)
1162
1167
 
1168
+ if (n === "textureSize" || n === "tex2Dfetch") {
1169
+ const tex = samplers.get((e.args[0] as Extract<Expr, { k: "ident" }>).name)!
1170
+ if (n === "textureSize") return at(e.pos, () => tex.size() as unknown as Val)
1171
+ const pixel = toFloat(lowerExpr(e.args[1]!, scope), e.args[1]!.pos)
1172
+ if (typeof pixel === "number" || pixel.width !== 2) {
1173
+ fail("tex2Dfetch reads at a float2 pixel, as in tex2Dfetch(t, fragCoord)", e.args[1]!.pos)
1174
+ }
1175
+ return at(e.pos, () => tex.fetch(pixel as never) as unknown as Val)
1176
+ }
1177
+
1163
1178
  if (n === "tex2D" || n === "tex2Dlod") {
1164
1179
  const texName = (e.args[0] as Extract<Expr, { k: "ident" }>).name
1165
1180
  const tex = samplers.get(texName)!
@@ -212,17 +212,18 @@ class Parser {
212
212
 
213
213
  if (t.text === "[") {
214
214
  const attrs = this.parseAttributes()
215
+ if (this.at("texture2D")) { unit.textures.push(this.parseTexture(attrs)); return }
215
216
  if (!this.at("uniform")) {
216
217
  this.fail(
217
- `an attribute belongs on a uniform, as in \`[Range(0, 1)] uniform float amount = 0.5;\`, ` +
218
- `and this one is followed by ${describe(this.peek())}`, t,
218
+ `an attribute belongs on a uniform or a texture, as in \`[Range(0, 1)] uniform float amount = 0.5;\` ` +
219
+ `or \`[Wrap(Repeat)] texture2D grain;\`, and this one is followed by ${describe(this.peek())}`, t,
219
220
  )
220
221
  }
221
222
  unit.uniforms.push(this.parseUniform(attrs))
222
223
  return
223
224
  }
224
225
  if (t.text === "uniform") { unit.uniforms.push(this.parseUniform([])); return }
225
- if (t.text === "texture2D") { unit.textures.push(this.parseTexture()); return }
226
+ if (t.text === "texture2D") { unit.textures.push(this.parseTexture([])); return }
226
227
  if (t.text === "const") { unit.consts.push(this.parseConst()); return }
227
228
  if (t.kind === "ident") {
228
229
  const fn = this.parseFunction()
@@ -277,7 +278,7 @@ class Parser {
277
278
  this.fail(`expected a number, a word or a "string" as an attribute's argument, got ${describe(t)}`, t)
278
279
  }
279
280
 
280
- private parseTexture(): TextureDecl {
281
+ private parseTexture(attrs: Attribute[]): TextureDecl {
281
282
  const kw = this.next()
282
283
  const name = this.expectName("a texture name")
283
284
  if (this.at("=")) {
@@ -287,7 +288,7 @@ class Parser {
287
288
  )
288
289
  }
289
290
  this.expect(";", "after a texture declaration")
290
- return { name: name.text, pos: kw }
291
+ return { name: name.text, attrs, pos: kw }
291
292
  }
292
293
 
293
294
  private parseConst(): Extract<Stmt, { k: "const" }> {
package/src/ops.ts CHANGED
@@ -26,16 +26,21 @@
26
26
  * for a host to read.
27
27
  *
28
28
  * They are what OneJS draws a program with. It keeps a program's uniforms in
29
- * 16 slots in the editor, a native player and a browser alike
30
- * (`SLProgramBridge.MaxUniforms`, the web frame's `sl_U`), and binds four
31
- * textures in the editor and a native player (`_Tex0` to `_Tex3`). A program
32
- * past either has something with no slot of its own, which one host would drop
33
- * and another would draw, so the parser and the builder both refuse it at the
34
- * declaration, the parser with a file and a line. `Specs/SL_NEXT.md` section 8
35
- * proposes raising both.
29
+ * 64 slots in the editor, a native player and a browser alike
30
+ * (`SLProgramBridge.MaxUniforms`, the web frame's `sl_U`), and binds eight
31
+ * textures everywhere (`_Tex0` to `_Tex7`). A program past either has
32
+ * something with no slot of its own, which one host would drop and another
33
+ * would draw, so the parser and the builder both refuse it at the declaration,
34
+ * the parser with a file and a line.
35
+ *
36
+ * Both sit inside every backend's minimum (`Specs/SL_NEXT.md` 8). Eight
37
+ * textures and the previous frame are nine sampled textures and at most nine
38
+ * samplers, where WebGPU, WebGL2, OpenGL ES 3 and Direct3D 11 all guarantee 16
39
+ * a fragment stage. 64 float4s are 1 KB of uniforms, where WebGL2 and OpenGL
40
+ * ES 3 guarantee 224 vectors and WebGPU 64 KB.
36
41
  */
37
- export const UNIFORM_SLOTS = 16
38
- export const TEXTURE_SLOTS = 4
42
+ export const UNIFORM_SLOTS = 64
43
+ export const TEXTURE_SLOTS = 8
39
44
 
40
45
  /** Input ids, which the web emitter switches on. */
41
46
  export const INPUT_ID: Record<string, number> = {
@@ -149,6 +154,11 @@ export const SLOP = {
149
154
  // What this program drew the frame before, at a uv (IR 5). No slot: the
150
155
  // host keeps it, and a program that never reads it has none.
151
156
  SAMPLE_PREVIOUS: 146,
157
+ // IR 6. A texture's size in pixels, a float2, its slot the one immediate.
158
+ TEXTURE_SIZE: 147,
159
+ // IR 6. One texel, unfiltered, at a float2 pixel: floored and held inside
160
+ // the texture, so every backend reads the same texel at every pixel.
161
+ SAMPLE_FETCH: 148,
152
162
  } as const
153
163
 
154
164
  export type SLOpCode = (typeof SLOP)[keyof typeof SLOP]
@@ -191,6 +201,7 @@ export const SL_ARITY: Record<number, number> = {
191
201
  [SLOP.VORONOI]: 1, [SLOP.TURBULENCE]: -1, [SLOP.RIDGED]: -1,
192
202
 
193
203
  [SLOP.SAMPLE]: 1, [SLOP.SAMPLE_LOD]: 2, [SLOP.SAMPLE_PREVIOUS]: 1,
204
+ [SLOP.TEXTURE_SIZE]: 0, [SLOP.SAMPLE_FETCH]: 1,
194
205
  }
195
206
 
196
207
  /** Name per opcode, for error messages and for the HLSL emitter's comments. */
@@ -316,6 +327,8 @@ export const SL_HLSL: Record<number, SLSurface> = {
316
327
  [SLOP.SAMPLE]: { call: "tex2D" },
317
328
  [SLOP.SAMPLE_LOD]: { call: "tex2Dlod" },
318
329
  [SLOP.SAMPLE_PREVIOUS]: { syntax: "tex2D(previous, uv)" },
330
+ [SLOP.TEXTURE_SIZE]: { call: "textureSize" },
331
+ [SLOP.SAMPLE_FETCH]: { call: "tex2Dfetch" },
319
332
  }
320
333
 
321
334
  /**
@@ -347,6 +360,7 @@ export const SL_GLSL_HINT: Record<string, string> = {
347
360
  mod: "%",
348
361
  texture: "tex2D",
349
362
  textureLod: "tex2Dlod",
363
+ texelFetch: "tex2Dfetch",
350
364
  vec2: "float2",
351
365
  vec3: "float3",
352
366
  vec4: "float4",
package/src/params.ts CHANGED
@@ -21,7 +21,7 @@ import { LIB_FUNCTIONS, SDF_CALLS } from "./lib/table"
21
21
  import { LIB_PARAM_NAMES } from "./lib/names"
22
22
  import { SL_SDF_PARAMS, SL_SDF_SHAPES, type SlSdfKind } from "./shapes"
23
23
  import type { PREVIOUS, SourceInputName } from "./ir"
24
- import type { AttributeName } from "./lang/attributes"
24
+ import type { AttributeName, TextureAttributeName } from "./lang/attributes"
25
25
 
26
26
  /** One function of the helper library, as its source declares it. */
27
27
  export interface LibSignature {
@@ -55,6 +55,8 @@ const INTRINSIC: Record<string, readonly string[]> = {
55
55
  lerp: ["x", "y", "s"], step: ["y", "x"], smoothstep: ["min", "max", "x"],
56
56
  tex2D: ["s", "t"],
57
57
  tex2Dlod: ["s", "t", "lod"],
58
+ textureSize: ["s"],
59
+ tex2Dfetch: ["s", "pixel"],
58
60
  }
59
61
 
60
62
  /**
@@ -163,6 +165,8 @@ export const BUILTIN_DOCS: Readonly<Record<string, string>> = {
163
165
  ridged: "Turbulence made bright at its creases: ridges, lightning, cracks.",
164
166
  tex2D: "The colour of texture s at the uv t, with straight alpha.",
165
167
  tex2Dlod: "Texture s at uv t from mip level lod: 0 is full size, 1 half, and a fraction blends the two.",
168
+ textureSize: "The width and height of texture s in pixels, at full size, as a float2.",
169
+ tex2Dfetch: "One texel of texture s at pixel, unfiltered; a pixel past an edge reads the edge.",
166
170
  sdf: "The signed distance from p to a shape, negative inside: `sdf.circle(p, r)`.",
167
171
  ramp: "t mapped through evenly spaced colour stops, from the first at 0 to the last at 1.",
168
172
  }
@@ -181,6 +185,12 @@ export const ATTRIBUTE_DOCS: Readonly<Record<AttributeName, string>> = {
181
185
  Hide: "No control: the host sets this uniform from code.",
182
186
  }
183
187
 
188
+ /** One line on each texture attribute, for the same tooltips. */
189
+ export const TEXTURE_ATTRIBUTE_DOCS: Readonly<Record<TextureAttributeName, string>> = {
190
+ Wrap: "How uv outside 0 to 1 reads: [Wrap(Repeat)] tiles, [Wrap(Mirror)] tiles mirrored, [Wrap(Clamp)] holds the edge.",
191
+ Filter: "How the texture reads between texels: [Filter(Point)] the nearest, [Filter(Linear)] a blend of four.",
192
+ }
193
+
184
194
  /**
185
195
  * One line on each input, for the same tooltips. In the words of no one host:
186
196
  * the output is a UI element in OneJS and an image on a Magerie board, and the
package/src/serial.ts CHANGED
@@ -10,10 +10,13 @@
10
10
  */
11
11
  import {
12
12
  INPUTS, STEP_INPUTS, SLError, SL_HASH_VERSION, SL_IR_VERSION, TYPE, checkCaps, controlOf, controlProblem, formProblem, hashProgram, intUniformProblem,
13
- programVersion, type HostInputName, type NodeRef, type Program, type SLNode, type SLType, type TextureDecl, type UniformDecl,
13
+ programVersion, samplingProblem, type HostInputName, type NodeRef, type Program, type SLNode, type SLType, type TextureDecl, type UniformDecl,
14
14
  } from "./ir"
15
15
  import { SLOP, SL_ARITY, SL_NAME } from "./ops"
16
16
 
17
+ /** The ops that name a texture slot in their first immediate. */
18
+ const TEXTURE_OPS = new Set<number>([SLOP.SAMPLE, SLOP.SAMPLE_LOD, SLOP.TEXTURE_SIZE, SLOP.SAMPLE_FETCH])
19
+
17
20
  const HOST_INPUTS: Readonly<Record<HostInputName, SLType>> = { ...INPUTS, ...STEP_INPUTS }
18
21
 
19
22
  export interface ProgramJSON {
@@ -54,7 +57,8 @@ export function fromJSON(json: unknown): Program {
54
57
  // as they are) or was edited. One that claims less than its nodes need
55
58
  // holds something its version never had, and a version 1 file has
56
59
  // nothing IR 3 added.
57
- const needs = programVersion(written, j.result!)
60
+ const textures = (j.textures ?? []).map(texture)
61
+ const needs = programVersion(written, j.result!, textures)
58
62
  if (v < needs && (v >= 2 || needs >= 3)) fail(`it says IR version ${v} and holds nodes version ${needs} added; it was changed after it was written`)
59
63
  const { nodes, result } = v < 3 ? toOperands(written, j.result!) : { nodes: written, result: j.result! }
60
64
  nodes.forEach((n, i) => {
@@ -64,12 +68,14 @@ export function fromJSON(json: unknown): Program {
64
68
  const out = nodes[result]!
65
69
  if (out.k === "if" || out.k === "loop" || out.type !== TYPE.VEC4 || out.kind !== undefined) fail("its result is not a float4")
66
70
  const uniforms = (j.uniforms ?? []).map(uniform)
67
- const textures = (j.textures ?? []).map(texture)
68
71
  checkCaps({ uniforms, textures })
69
72
  for (const n of nodes) {
70
73
  if (n.k === "uniform" && n.slot >= uniforms.length) fail(`a node reads uniform slot ${n.slot}, which is not declared`)
74
+ if (n.k === "call" && TEXTURE_OPS.has(n.op) && !(n.imm !== undefined && n.imm[0]! < textures.length)) {
75
+ fail(`a node reads texture slot ${String(n.imm?.[0])}, which is not declared`)
76
+ }
71
77
  }
72
- const version = programVersion(nodes, result)
78
+ const version = programVersion(nodes, result, textures)
73
79
  const hash = hashProgram(nodes, result, uniforms, textures)
74
80
  // The same IR version and hash scheme, so the same hash; a different one
75
81
  // means the file was edited or damaged, and its cached shader belongs to
@@ -245,7 +251,12 @@ function uniform(u: unknown, i: number): UniformDecl {
245
251
  function texture(t: unknown, i: number): TextureDecl {
246
252
  const x = (t ?? {}) as Partial<TextureDecl>
247
253
  if (typeof x.name !== "string" || x.slot !== i) fail(`texture ${i} needs a name and slot ${i}`)
248
- return { name: x.name!, slot: i }
254
+ const problem = samplingProblem(x)
255
+ if (problem !== null) fail(`texture ${i}: ${problem}`)
256
+ const out: TextureDecl = { name: x.name!, slot: i }
257
+ if (x.wrap !== undefined) out.wrap = x.wrap
258
+ if (x.filter !== undefined) out.filter = x.filter
259
+ return out
249
260
  }
250
261
 
251
262
  function fail(why: string): never {
package/src/sl.ts CHANGED
@@ -25,7 +25,8 @@
25
25
  import { parseColor as parseHex } from "./color"
26
26
  import {
27
27
  Builder, INPUTS, RAMP_STOP_COMPUTED, STEP_INPUTS, SLError, TYPE, hashProgram, programVersion, truncateHeld, widthName, writtenColour,
28
- type HostInputName, type InputName, type NodeRef, type Program, type SLKind, type SLNode, type SLType, type UniformControl,
28
+ type HostInputName, type InputName, type NodeRef, type Program, type SLKind, type SLNode, type SLType, type TextureFilter,
29
+ type TextureWrap, type UniformControl,
29
30
  } from "./ir"
30
31
  import { SLOP, SL_HLSL, SL_NAME, type SLOpCode } from "./ops"
31
32
  import { SL_SDF_PARAMS, SL_SDF_SHAPES, type SlSdfKind } from "./shapes"
@@ -561,7 +562,7 @@ export function program(fn: (inputs: ProgramInputs) => Vec4): Program {
561
562
  }
562
563
  const nodes: SLNode[] = b.nodes.slice()
563
564
  return {
564
- version: programVersion(nodes, out.ref),
565
+ version: programVersion(nodes, out.ref, b.textures),
565
566
  nodes,
566
567
  result: out.ref,
567
568
  uniforms: b.uniforms.slice(),
@@ -1033,7 +1034,10 @@ export const uniform = {
1033
1034
  }
1034
1035
 
1035
1036
  export interface Texture {
1036
- /** The colour at `uv`, filtered as the texture's own settings say. */
1037
+ /**
1038
+ * The colour at `uv`, filtered and wrapped as the texture's own settings
1039
+ * say, or as `sl.texture`'s sampling says when it was given one.
1040
+ */
1037
1041
  sample(uv: Vec2): Vec4
1038
1042
  /**
1039
1043
  * The colour at `uv` from mip level `lod`: 0 is the full size texture, 1
@@ -1041,11 +1045,31 @@ export interface Texture {
1041
1045
  * texture's filter mode. A texture with no mips has only level 0.
1042
1046
  */
1043
1047
  sampleLevel(uv: Vec2, lod: Num): Vec4
1048
+ /** The texture's width and height in pixels, at full size. `textureSize(t)` in a `.sl` file. */
1049
+ size(): Vec2
1050
+ /**
1051
+ * One texel, unfiltered, at a pixel: (0, 0) is the texel `uv` (0, 0)
1052
+ * samples. The pixel is floored and held inside the texture, so a pixel
1053
+ * past an edge reads the edge on every host. Full size only, and the
1054
+ * sampling settings do not apply. `tex2Dfetch(t, pixel)` in a `.sl` file.
1055
+ */
1056
+ fetch(pixel: Vec2): Vec4
1044
1057
  }
1045
1058
 
1046
- export function texture(name: string): Texture {
1059
+ /**
1060
+ * How a program samples a texture, when it should not follow the bound
1061
+ * texture's own settings. Either half set, the program owns the sampler and
1062
+ * the other half is clamp or linear. `[Wrap(Repeat)]` and `[Filter(Point)]`
1063
+ * in a `.sl` file.
1064
+ */
1065
+ export interface TextureSamplingOptions {
1066
+ wrap?: TextureWrap
1067
+ filter?: TextureFilter
1068
+ }
1069
+
1070
+ export function texture(name: string, sampling: TextureSamplingOptions = {}): Texture {
1047
1071
  const b = ctx()
1048
- const slot = b.texture(name)
1072
+ const slot = b.texture(name, sampling)
1049
1073
  return {
1050
1074
  sample(uv: Vec2): Vec4 {
1051
1075
  if (uv.owner !== b) throw new SLError("a value from another program cannot be used in this one")
@@ -1060,6 +1084,16 @@ export function texture(name: string): Texture {
1060
1084
  }
1061
1085
  return mk(b, b.call(SLOP.SAMPLE_LOD, TYPE.VEC4, [uv.ref, typeof level === "number" ? level : level.ref], [slot]), TYPE.VEC4)
1062
1086
  },
1087
+ size(): Vec2 {
1088
+ return mk(b, b.call(SLOP.TEXTURE_SIZE, TYPE.VEC2, [], [slot]), TYPE.VEC2)
1089
+ },
1090
+ fetch(pixel: Vec2): Vec4 {
1091
+ if (pixel.owner !== b) throw new SLError("a value from another program cannot be used in this one")
1092
+ if (pixel.width !== TYPE.VEC2 || pixel.kind !== undefined) {
1093
+ throw new SLError(`a texel is fetched at a float2 pixel, and this is ${pixel.kind === undefined ? `a ${widthName(pixel.width)}` : `an ${pixel.kind}`}`)
1094
+ }
1095
+ return mk(b, b.call(SLOP.SAMPLE_FETCH, TYPE.VEC4, [pixel.ref], [slot]), TYPE.VEC4)
1096
+ },
1063
1097
  }
1064
1098
  }
1065
1099
 
package/src/tables.ts CHANGED
@@ -16,9 +16,11 @@ export type { SLSurface } from "./ops"
16
16
  export { DERIVED_INPUTS, INPUTS, PREVIOUS, SOURCE_INPUTS, STEP_INPUTS } from "./ir"
17
17
  export type { DerivedInputName, HostInputName, InputName, SourceInputName, StepInputName } from "./ir"
18
18
  export { SL_SDF_SHAPES, SL_SDF_PARAMS } from "./shapes"
19
- export { ATTRIBUTE_DOCS, BUILTIN_DOCS, BUILTIN_PARAMS, INPUT_DOCS, LIB_SIGNATURES, SL_SDF_PARAM_NAMES } from "./params"
20
- export { ATTRIBUTE_NAMES } from "./lang/attributes"
21
- export type { AttributeName } from "./lang/attributes"
19
+ export {
20
+ ATTRIBUTE_DOCS, BUILTIN_DOCS, BUILTIN_PARAMS, INPUT_DOCS, LIB_SIGNATURES, SL_SDF_PARAM_NAMES, TEXTURE_ATTRIBUTE_DOCS,
21
+ } from "./params"
22
+ export { ATTRIBUTE_NAMES, TEXTURE_ATTRIBUTE_NAMES } from "./lang/attributes"
23
+ export type { AttributeName, TextureAttributeName } from "./lang/attributes"
22
24
  export { PRELUDE_DOCS, PRELUDE_NAMES } from "./lang/prelude-source"
23
25
  export { SL_KEYWORDS, SL_TYPES } from "./lang/words"
24
26
  export { TYPE_WIDTH } from "./lang/ast"
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, isWhole, 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 => {
@@ -232,11 +259,15 @@ function emitWeb(p: Program, lang: WebLanguage): string {
232
259
  case SLOP.MIX: return `mix(${s[0]}, ${s[1]}, ${s[2]})`
233
260
  case SLOP.STEP: return `step(${s[0]}, ${s[1]})`
234
261
  case SLOP.SMOOTHSTEP: smoothsteps.add(t); return `sl_smoothstep${t}(${s[0]}, ${s[1]}, ${s[2]})`
235
- // As the HLSL emitter does it: branchless, so every
236
- // backend agrees on the edge value.
262
+ // A choice, never a blend. mix(f, t, s) is f + (t - f) * s, which
263
+ // rounds t away beside 1e9 and is NaN beside an infinity, so the
264
+ // side not chosen reached the one that was. The condition holds
265
+ // from 0.5 up, as the step it replaces did, and a NaN condition
266
+ // picks the false side, as the HLSL emitter's does. A bool vector
267
+ // makes GLSL's mix choose rather than blend.
237
268
  case SLOP.SELECT: {
238
- const c = typeOf(n.args[0])
239
- return `mix(${splat(n.args[2], t)}, ${splat(n.args[1], t)}, step(${k(0.5, c)}, ${a[0]}))`
269
+ const on = t === 1 ? `(${s[0]} >= ${lit(0.5)})` : W ? `(${s[0]} >= ${k(0.5, t)})` : `greaterThanEqual(${s[0]}, ${k(0.5, t)})`
270
+ return W ? `select(${s[2]}, ${s[1]}, ${on})` : t === 1 ? `(${on} ? ${s[1]} : ${s[2]})` : `mix(${s[2]}, ${s[1]}, ${on})`
240
271
  }
241
272
 
242
273
  case SLOP.HSV2RGB: return `${lib("sl_hsv2rgb")}(${a[0]})`
@@ -272,6 +303,20 @@ function emitWeb(p: Program, lang: WebLanguage): string {
272
303
  const below = `floor(${a[1]})`
273
304
  return `mix(${at(below)}, ${at(`${below} + 1.0`)}, fract(${a[1]}))`
274
305
  }
306
+ case SLOP.TEXTURE_SIZE: {
307
+ const slot = Math.round(imm[0] ?? 0)
308
+ loaded.add(slot)
309
+ return size(slot)
310
+ }
311
+ case SLOP.SAMPLE_FETCH: {
312
+ const slot = Math.round(imm[0] ?? 0)
313
+ loaded.add(slot)
314
+ // Floored and held inside, as the HLSL body does: a load out of
315
+ // range is undefined in GLSL and implementation defined in WGSL.
316
+ return W
317
+ ? `textureLoad(sl_tex${slot}, vec2i(clamp(floor(${a[0]}), vec2f(0.0), ${size(slot)} - 1.0)), 0)`
318
+ : `texelFetch(sl_Tex${slot}, ivec2(clamp(floor(${a[0]}), vec2(0.0), ${size(slot)} - 1.0)), 0)`
319
+ }
275
320
  case SLOP.SAMPLE_PREVIOUS:
276
321
  previous = true
277
322
  // Flipped as this frame flipped the uv it wrote at, so a read at
@@ -363,21 +408,26 @@ function emitWeb(p: Program, lang: WebLanguage): string {
363
408
  const body = printBody(p, shape, syntax, expr, " ").join("\n")
364
409
  const library = [...[...smoothsteps].sort().map((w) => smoothstep(w, lang)), ...librarySource(need, lang)].join("\n")
365
410
  const header = `// GENERATED from a shader language program (${p.hash}). Do not edit.`
366
- // Only the textures the program samples. WebGPU's automatic layout leaves
367
- // out a binding the shader never reads, and a bind group that supplies one
368
- // anyway does not validate; the host reads these declarations back to know
369
- // which slots to bind.
370
- const textures = p.textures.filter((t) => sampled.has(t.slot))
411
+ // Only the textures the program reads, and in WGSL only the samplers it
412
+ // samples with. WebGPU's automatic layout leaves out a binding the shader
413
+ // never reads, and a bind group that supplies one anyway does not
414
+ // validate; the host reads these declarations back to know what to bind.
415
+ const textures = p.textures.filter((t) => sampled.has(t.slot) || loaded.has(t.slot))
416
+ /** The comment that says a texture's sampler is the program's own, for a slot it samples. */
417
+ const samplerNote = (t: TextureDecl) => {
418
+ const s = sampled.has(t.slot) ? samplingOf(t) : null
419
+ return s === null ? "" : ` // sampler ${s.wrap} ${s.filter}`
420
+ }
371
421
 
372
422
  if (W) {
373
423
  const bindings = [...textures.map((t) =>
374
- `@group(0) @binding(${1 + 2 * t.slot}) var sl_samp${t.slot}: sampler;\n` +
424
+ (sampled.has(t.slot) ? `@group(0) @binding(${1 + 2 * t.slot}) var sl_samp${t.slot}: sampler;${samplerNote(t)}\n` : "") +
375
425
  `@group(0) @binding(${2 + 2 * t.slot}) var sl_tex${t.slot}: texture_2d<f32>;`),
376
426
  ...previous ? [
377
427
  `@group(0) @binding(${1 + 2 * TEXTURE_SLOTS}) var sl_prevSamp: sampler;\n` +
378
428
  `@group(0) @binding(${2 + 2 * TEXTURE_SLOTS}) var sl_prev: texture_2d<f32>;`] : []].join("\n")
379
429
  return `${header}
380
- struct SLFrame { res: vec4f, opt: vec4f, u: array<vec4f, ${WEB_UNIFORM_SLOTS}> }
430
+ struct SLFrame { res: vec4f, opt: vec4f, u: array<vec4f, ${uniformBlock(p)}> }
381
431
  @group(0) @binding(0) var<uniform> sl: SLFrame;
382
432
  ${bindings}
383
433
  ${library}
@@ -393,14 +443,14 @@ ${body}
393
443
  }
394
444
  `
395
445
  }
396
- const samplers = [...textures.map((t) => `uniform sampler2D sl_Tex${t.slot};`), ...previous ? ["uniform sampler2D sl_Prev;"] : []].join("\n")
446
+ const samplers = [...textures.map((t) => `uniform sampler2D sl_Tex${t.slot};${samplerNote(t)}`), ...previous ? ["uniform sampler2D sl_Prev;"] : []].join("\n")
397
447
  return `#version 300 es
398
448
  ${header}
399
449
  precision highp float;
400
450
  precision highp int;
401
451
  uniform vec4 sl_Res;
402
452
  uniform vec4 sl_Opt;
403
- uniform vec4 sl_U[${WEB_UNIFORM_SLOTS}];
453
+ uniform vec4 sl_U[${uniformBlock(p)}];
404
454
  ${samplers}
405
455
  out vec4 sl_Out;
406
456
  ${library}