onejs-sl 0.2.0 → 0.3.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.
@@ -45,13 +45,12 @@ const name = (op: number): string => {
45
45
  const val = (v: Num): Val => (typeof v === "number" ? (sl.float(v) as unknown as Val) : v)
46
46
 
47
47
  /**
48
- * Asserts an operand's width where the two backends would otherwise part ways.
48
+ * Asserts an operand's width where the backends would otherwise part ways.
49
49
  *
50
- * The VM keeps every value in a float4 register and writes `a.xy` or `a.rgb`
51
- * whatever it was handed, so a noise call on a float3 quietly works there. The
52
- * generated HLSL passes the real type to `sl_valueNoise(float2)` and either
53
- * truncates with a warning or fails to compile. Neither outcome is one an
54
- * author should discover after ejecting, so the narrow ops say so here.
50
+ * The generated HLSL passes the real type to `sl_valueNoise(float2)` and either
51
+ * truncates with a warning or fails to compile, while a browser may accept it.
52
+ * Neither outcome is one an author should discover in a build, so the narrow
53
+ * ops say so here.
55
54
  */
56
55
  function widthMustBe(fn: string, v: Num, want: 1 | 2 | 3 | 4, why: string): Val {
57
56
  const got = typeof v === "number" ? 1 : v.width
@@ -146,7 +145,7 @@ function octaves(fn: string, v: Num | undefined): number {
146
145
  if (v === undefined) return 3
147
146
  if (typeof v !== "number") {
148
147
  throw new SLError(
149
- `${fn}'s octave count is baked into the instruction, so it has to be a constant. ` +
148
+ `${fn}'s octave count is part of the operation, so it has to be a constant. ` +
150
149
  `A uniform or a computed value cannot change how many octaves the shader runs.`,
151
150
  )
152
151
  }
package/src/lang/check.ts CHANGED
@@ -28,8 +28,8 @@
28
28
  * is refused with the reason.
29
29
  */
30
30
 
31
- import { DERIVED_INPUTS, INPUTS } from "../ir"
32
- import { VM_TEXTURES, VM_UNIFORMS } from "../ops"
31
+ import { DERIVED_INPUTS, INPUTS, tooManyTextures, tooManyUniforms } from "../ir"
32
+ import { TEXTURE_SLOTS, UNIFORM_SLOTS } from "../ops"
33
33
  import { SL_GLSL_HINT } from "../ops"
34
34
  import { BUILTINS, NOT_YET } from "./builtins"
35
35
  import { SL_SDF_SHAPES } from "../shapes"
@@ -161,15 +161,9 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
161
161
  if (!uniforms.has(u.name)) uniforms.set(u.name, u)
162
162
  }
163
163
  attempt(() => {
164
- if (uniforms.size <= VM_UNIFORMS) return
165
- const over = unit.uniforms[VM_UNIFORMS]!
166
- fail(
167
- `this file declares ${uniforms.size} uniforms and a program may hold ${VM_UNIFORMS}. ` +
168
- `That is the size of the VM's uniform array; past it a slot reads the last one instead ` +
169
- `of its own, so it is refused here rather than rendered wrong. Pack related values into ` +
170
- `a float4.`,
171
- over.pos, over.name.length,
172
- )
164
+ if (uniforms.size <= UNIFORM_SLOTS) return
165
+ const over = unit.uniforms[UNIFORM_SLOTS]!
166
+ fail(tooManyUniforms(uniforms.size, "file"), over.pos, over.name.length)
173
167
  })
174
168
 
175
169
  for (const t of unit.textures) {
@@ -180,14 +174,9 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
180
174
  if (!textures.has(t.name)) textures.set(t.name, t)
181
175
  }
182
176
  attempt(() => {
183
- if (textures.size <= VM_TEXTURES) return
184
- const over = unit.textures[VM_TEXTURES]!
185
- fail(
186
- `this file declares ${textures.size} textures and a program may sample ${VM_TEXTURES}. ` +
187
- `The VM shader declares four samplers; a fifth would sample the fourth in the browser ` +
188
- `and its own after an eject, which is two different pictures from one file.`,
189
- over.pos, over.name.length,
190
- )
177
+ if (textures.size <= TEXTURE_SLOTS) return
178
+ const over = unit.textures[TEXTURE_SLOTS]!
179
+ fail(tooManyTextures(textures.size, "file"), over.pos, over.name.length)
191
180
  })
192
181
 
193
182
  for (const c of unit.consts) {
package/src/lang/index.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * import { parse } from "onejs-sl"
9
9
  *
10
10
  * const plasma = parse(source, { file: "plasma.sl" })
11
- * // ^ a Program: encode() it, emit HLSL from it, hash it
11
+ * // ^ a Program: compile() it, emit HLSL from it, hash it
12
12
  *
13
13
  * The parser is not a second authoring surface with its own semantics. It
14
14
  * lowers through the EDSL, so a `.sl` file and the `sl.program(...)` an author
package/src/lang/lower.ts CHANGED
@@ -15,8 +15,7 @@
15
15
  * existed. Reassignment is free; it is not a store.
16
16
  *
17
17
  * **`for` unrolls.** The bounds are constants, so the body is lowered once per
18
- * iteration with the counter substituted as a number. `sl.unrolled` records the
19
- * span so the instruction ceiling error can name the loop that spent it.
18
+ * iteration with the counter substituted as a number.
20
19
  *
21
20
  * **`if` becomes `select`.** Both sides are lowered and every local assigned in
22
21
  * either is selected at the join. A constant condition folds instead, which is
@@ -421,20 +420,19 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
421
420
  count++
422
421
  if (count > MAX_UNROLL) {
423
422
  fail(
424
- `this loop would unroll to more than ${MAX_UNROLL} iterations. There is ` +
425
- `no loop on either backend, so every iteration is emitted in full and ` +
426
- `the VM runs at most 256 instructions`,
423
+ `this loop would unroll to more than ${MAX_UNROLL} iterations. A for ` +
424
+ `loop unrolls at build time, so every iteration is written out in full`,
427
425
  s.pos,
428
426
  )
429
427
  }
430
428
  }
431
- at(s.pos, () => sl.unrolled(count, () => {
429
+ at(s.pos, () => {
432
430
  for (let i = from; s.inclusive ? i <= to : i < to; i += step) {
433
431
  const inner = new Scope(scope)
434
432
  inner.declare(s.counter, { width: 1, value: i })
435
433
  exec(s.body, inner, ret)
436
434
  }
437
- }))
435
+ })
438
436
  break
439
437
  }
440
438
  default:
@@ -447,7 +445,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
447
445
  if (typeof v !== "number") {
448
446
  fail(
449
447
  `a for loop unrolls at build time, so its ${what} has to be a constant. This one is ` +
450
- `computed while the shader runs, and neither backend has a loop to run it in`,
448
+ `computed while the shader runs`,
451
449
  e.pos,
452
450
  )
453
451
  }
@@ -609,7 +607,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
609
607
  const v = lowerExpr(a, scope)
610
608
  if (typeof v !== "number") {
611
609
  fail(
612
- `sdf.${shape}'s shape parameters ride inside the instruction, so they have to ` +
610
+ `sdf.${shape}'s shape parameters are part of the operation, so they have to ` +
613
611
  `be constants. A size that changes belongs on the point: scale or offset it ` +
614
612
  `before the call`,
615
613
  a.pos,
@@ -730,8 +728,8 @@ const ARITH: Record<ArithOp, (a: number, b: number) => number> = {
730
728
  "-": (a, b) => a - b,
731
729
  "*": (a, b) => a * b,
732
730
  "/": (a, b) => a / b,
733
- // JavaScript's % is the truncated remainder, which is what HLSL's fmod is
734
- // and what the VM's OP_MOD implements. Folding it in JS is the same answer.
731
+ // JavaScript's % is the truncated remainder, which is what HLSL's fmod is.
732
+ // Folding it in JS is the same answer.
735
733
  "%": (a, b) => a % b,
736
734
  }
737
735
 
@@ -344,8 +344,8 @@ class Parser {
344
344
  if (t.text === "for") return this.parseFor()
345
345
  if (t.text === "while" || t.text === "do") {
346
346
  this.fail(
347
- `there is no ${t.text} loop: the VM has no branches, so a loop has to unroll, and ` +
348
- `only a for loop with constant bounds can. See a for loop`,
347
+ `there is no ${t.text} loop: a loop unrolls at build time, and only a for loop ` +
348
+ `with constant bounds can. See a for loop`,
349
349
  t,
350
350
  )
351
351
  }
@@ -7,8 +7,8 @@
7
7
  *
8
8
  * ADDING TO IT COSTS A FUNCTION AND A TEST, NOT AN OPCODE IN THREE PLACES. That
9
9
  * is the whole argument for having a text language with inlining functions: a
10
- * library entry is source, so it lands in the VM, in generated HLSL and in the
11
- * hash by being ordinary code, and neither backend learns anything new.
10
+ * library entry is source, so it lands in every emitter's output and in the
11
+ * hash by being ordinary code, and no backend learns anything new.
12
12
  *
13
13
  * WHY THE SOURCE IS A STRING AND NOT A FILE. `onejs-sl` ships raw TypeScript
14
14
  * and runs inside a browser worker on play.onejs.com, where
package/src/ops.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * The shader language opcode table.
3
3
  *
4
- * Phase 1 of `Specs/SHADER_LANG.md`. This is the contract three things will
5
- * share: the encoder that packs a program into a texture, the VM shader that
6
- * evaluates it, and the HLSL emitter that prints it. Two of those do not exist
7
- * yet, which is exactly why the numbering is fixed now rather than later.
4
+ * Phase 1 of `Specs/SHADER_LANG.md`. The numbers are in every program's hash
5
+ * and in its JSON, so an opcode keeps its number for good, and one that goes
6
+ * leaves its number unused.
8
7
  *
9
8
  * Numbered in families with gaps, the convention `fx/ops.ts` set, so a reader
10
9
  * can tell what an opcode is from its value:
@@ -23,31 +22,22 @@
23
22
  */
24
23
 
25
24
  /**
26
- * The newest VM encoding this encoder writes. The VM accepts 1..its own and
27
- * refuses newer, where there is a VM: a WebGL player draws compiled and never
28
- * reads the buffer.
25
+ * How many uniforms and textures one program may declare: the language's caps,
26
+ * for a host to read.
29
27
  *
30
- * A payload records the LOWEST version that can run it (`Encoded.wire`), not
31
- * this: a program that uses nothing new stays 1, so a new bundle still runs on
32
- * an older Play container, and only a program that needs a newer instruction
33
- * is refused by an older one rather than drawn wrong.
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.
34
36
  */
35
- export const SL_WIRE_VERSION = 2
37
+ export const UNIFORM_SLOTS = 16
38
+ export const TEXTURE_SLOTS = 4
36
39
 
37
- /**
38
- * What the VM shader actually declares, and therefore what a program may use.
39
- *
40
- * `FxProgram.shader` has `float4 _Uniforms[16]` and four `sampler2D`s, and
41
- * `SLProgramBridge.cs` fills exactly those. Past either ceiling the VM does not
42
- * fail, it clamps: uniform 17 reads slot 15 and texture slot 4 samples `_Tex3`,
43
- * while the generated HLSL would declare both correctly. That is a silent
44
- * disagreement between the backends, so the text form reports it at the
45
- * declaration with a file and a line. See `Specs/SL_TEXT.md` 3.7.
46
- */
47
- export const VM_UNIFORMS = 16
48
- export const VM_TEXTURES = 4
49
-
50
- /** Input ids, fixed here because the shader switches on them. */
40
+ /** Input ids, which the web emitter switches on. */
51
41
  export const INPUT_ID: Record<string, number> = {
52
42
  uv: 0, fragCoord: 1, resolution: 2, time: 3, aspect: 4,
53
43
  }
@@ -129,10 +119,7 @@ export const SLOP = {
129
119
  // made bright. imm.x carries the octave count, as FBM does.
130
120
  TURBULENCE: 133,
131
121
  RIDGED: 134,
132
- // VM only: an SDF given a fifth or sixth parameter, which one instruction's
133
- // four immediates and id cannot hold. The encoder makes it (`forVm`); the
134
- // IR and every compiled backend only ever see SDF.
135
- SDF_WIDE: 135,
122
+ // 135 was SDF_WIDE, the VM's form of an SDF with more than four parameters.
136
123
 
137
124
  // Sampling
138
125
  SAMPLE: 144,
@@ -142,7 +129,7 @@ export const SLOP = {
142
129
  export type SLOpCode = (typeof SLOP)[keyof typeof SLOP]
143
130
 
144
131
  /**
145
- * How many arguments each op takes, for validation and for the encoder.
132
+ * How many arguments each op takes, for validation.
146
133
  *
147
134
  * -1 means variadic: COMPOSE takes however many parts add up to its width, RAMP
148
135
  * takes a value plus its stops. Everything else is fixed, and a mismatch is a
@@ -170,7 +157,7 @@ export const SL_ARITY: Record<number, number> = {
170
157
  [SLOP.RAMP]: -1, [SLOP.HSV2RGB]: 1, [SLOP.RGB2HSV]: 1, [SLOP.LUMINANCE]: 1, [SLOP.TO_LINEAR]: 1,
171
158
 
172
159
  [SLOP.NOISE]: 1, [SLOP.SIMPLEX]: 1, [SLOP.FBM]: 1, [SLOP.SDF]: 1,
173
- [SLOP.VORONOI]: 1, [SLOP.TURBULENCE]: 1, [SLOP.RIDGED]: 1, [SLOP.SDF_WIDE]: 2,
160
+ [SLOP.VORONOI]: 1, [SLOP.TURBULENCE]: 1, [SLOP.RIDGED]: 1,
174
161
 
175
162
  [SLOP.SAMPLE]: 1, [SLOP.SAMPLE_LOD]: 2,
176
163
  }
@@ -180,18 +167,10 @@ export const SL_NAME: Record<number, string> = Object.fromEntries(
180
167
  Object.entries(SLOP).map(([name, code]) => [code, name.toLowerCase()]),
181
168
  )
182
169
 
183
- export const FIRST_ARITHMETIC = 16
184
- export const FIRST_MATHS = 48
185
- export const FIRST_GEOMETRY = 80
186
- export const FIRST_INTERPOLATE = 96
187
- export const FIRST_COLOUR = 112
188
- export const FIRST_PROCEDURAL = 128
189
- export const FIRST_SAMPLING = 144
170
+ const FIRST_SAMPLING = 144
190
171
 
191
172
  /**
192
- * Ops that read a texture. Sampler slots are the one resource ceiling neither
193
- * backend can widen (16 in a fragment shader on WebGL2), so these are counted
194
- * at record time.
173
+ * Ops that read a texture.
195
174
  */
196
175
  export function isSampling(op: number): boolean {
197
176
  return op >= FIRST_SAMPLING
@@ -285,7 +264,6 @@ export const SL_HLSL: Record<number, SLSurface> = {
285
264
  [SLOP.VORONOI]: { call: "voronoi" },
286
265
  [SLOP.TURBULENCE]: { call: "turbulence" },
287
266
  [SLOP.RIDGED]: { call: "ridged" },
288
- [SLOP.SDF_WIDE]: { syntax: "sdf.<shape>(p, ...) given a fifth or sixth parameter" },
289
267
 
290
268
  [SLOP.SAMPLE]: { call: "tex2D" },
291
269
  [SLOP.SAMPLE_LOD]: { call: "tex2Dlod" },
@@ -296,16 +274,15 @@ export const SL_HLSL: Record<number, SLSurface> = {
296
274
  *
297
275
  * Numbered so the families stay in order, and refused at the surface until
298
276
  * somebody writes both halves. A program reaching one would render as whatever
299
- * the VM's dispatch falls through to and fail outright in the HLSL emitter,
300
- * which is the silent-disagreement failure this design exists to prevent, so
301
- * the parser names them rather than treating them as unknown identifiers.
277
+ * nothing: the emitters have no case for them. So the parser names them rather
278
+ * than treating them as unknown identifiers.
302
279
  *
303
280
  * `REMAP` is here because the opcode is unimplemented; the SURFACE `remap` is a
304
281
  * macro over arithmetic and works everywhere.
305
282
  */
306
283
  export const SL_UNIMPLEMENTED: Record<number, string> = {
307
- [SLOP.RGB2HSV]: "neither the VM shader nor the HLSL emitter has a case for it",
308
- [SLOP.SAMPLE_LOD]: "the VM samples without an explicit LOD; use tex2D",
284
+ [SLOP.RGB2HSV]: "no backend has a case for it yet",
285
+ [SLOP.SAMPLE_LOD]: "no backend samples at an explicit LOD yet; use tex2D",
309
286
  [SLOP.REMAP]: "the surface form is a macro over arithmetic, so the opcode is unused",
310
287
  }
311
288
 
package/src/serial.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  * older one is migrated to this one and rehashed under it.
10
10
  */
11
11
  import {
12
- INPUTS, SLError, SL_IR_VERSION, TYPE, controlOf, controlProblem, hashProgram,
12
+ INPUTS, SLError, SL_IR_VERSION, TYPE, checkCaps, controlOf, controlProblem, hashProgram,
13
13
  type NodeRef, type Program, type SLNode, type SLType, type TextureDecl, type UniformDecl,
14
14
  } from "./ir"
15
15
  import { SL_ARITY, SL_NAME } from "./ops"
@@ -44,6 +44,7 @@ export function fromJSON(json: unknown): Program {
44
44
  if (nodes[result!]!.type !== TYPE.VEC4) fail("its result is not a float4")
45
45
  const uniforms = (j.uniforms ?? []).map(uniform)
46
46
  const textures = (j.textures ?? []).map(texture)
47
+ checkCaps({ uniforms, textures })
47
48
  for (const n of nodes) {
48
49
  if (n.k === "uniform" && n.slot >= uniforms.length) fail(`a node reads uniform slot ${n.slot}, which is not declared`)
49
50
  }
@@ -54,7 +55,7 @@ export function fromJSON(json: unknown): Program {
54
55
  if (v === SL_IR_VERSION && j.hash !== undefined && j.hash !== hash) {
55
56
  fail(`its hash ${j.hash} does not match its nodes (${hash}); it was changed after it was written`)
56
57
  }
57
- return { version: SL_IR_VERSION, nodes, result: result!, uniforms, textures, hash, loops: [] }
58
+ return { version: SL_IR_VERSION, nodes, result: result!, uniforms, textures, hash }
58
59
  }
59
60
 
60
61
  function node(n: unknown, i: number): SLNode {
package/src/sl.ts CHANGED
@@ -207,7 +207,7 @@ function align2(a: Val, o: Num): [NodeRef, NodeRef] {
207
207
  * right for two operands because `bin` takes the max itself; the three and four
208
208
  * operand ops used to take the width from one chosen operand, so
209
209
  * `mix(0, aVec3, t)` produced a node typed float that held three components.
210
- * The VM keeps every value in a float4 register and never noticed, while the
210
+ * The VM, which kept every value in a float4 register, never noticed, while the
211
211
  * HLSL emitter declared `float` and truncated, so the two backends rendered
212
212
  * different pictures from one program. Anything component wise with more than
213
213
  * two operands goes through here.
@@ -294,7 +294,6 @@ export function program(fn: (inputs: ProgramInputs) => Vec4): Program {
294
294
  uniforms: b.uniforms.slice(),
295
295
  textures: b.textures.slice(),
296
296
  hash: hashProgram(nodes, out.ref, b.uniforms, b.textures),
297
- loops: b.loops.slice(),
298
297
  }
299
298
  } finally {
300
299
  current = null
@@ -312,23 +311,14 @@ function compose(width: SLType, parts: Num[]): Val {
312
311
  // Wide parts are SPLIT into their components here, so COMPOSE only ever
313
312
  // sees scalars.
314
313
  //
315
- // The VM keeps every value in a float4 register and took the x of each
316
- // operand, which silently dropped everything after the first component:
317
- // vec4(uv, 0, 1) rendered (uv.x, 0, 1, 0) instead of (uv.x, uv.y, 0, 1).
318
- // Teaching the shader each part's width would need widths in an encoding
319
- // that has no room for them, and would put the complexity in the half that
320
- // is hardest to test. Splitting at record time costs a few extra swizzle
321
- // nodes, which the register allocator reclaims immediately, and leaves the
322
- // VM's COMPOSE trivially correct.
314
+ // The VM (removed in 0.3.0) took the x of each operand, which dropped
315
+ // everything after the first component: vec4(uv, 0, 1) rendered
316
+ // (uv.x, 0, 1, 0). Splitting at record time costs a few extra swizzle
317
+ // nodes and keeps every emitter's COMPOSE trivial.
323
318
  // A constructor of literals is ONE constant, not a COMPOSE of four.
324
319
  //
325
- // `vec4(0.1, 0.2, 0.3, 0.4)` was five instructions holding four registers
326
- // at once, because every part became its own CONST and COMPOSE kept them
327
- // all live. The IR has always been able to say it in one node: a const
328
- // carries up to four components. A two stop `ramp` used five of the eight
329
- // registers on nothing but its own stop colours, which put the language's
330
- // headline colour tool within one or two registers of unusable in any
331
- // program that also did something.
320
+ // Every part as its own CONST, joined by a COMPOSE, is five nodes where the
321
+ // IR can say it in one: a const carries up to four components.
332
322
  if (parts.length === width && parts.every((p) => typeof p === "number")) {
333
323
  return mk(b, b.constant(parts as number[]), width)
334
324
  }
@@ -425,9 +415,8 @@ export function hsv2rgb(c: Vec3): Vec3 {
425
415
  /**
426
416
  * Cross and reflect, both vec3 only.
427
417
  *
428
- * The VM evaluates them as `cross(a.xyz, b.xyz)`, so a vec2 or vec4 would mean
429
- * one thing in a compiled build and another in the interpreter. Refusing the
430
- * other widths here is what keeps the two honest.
418
+ * Refusing the other widths here keeps every backend drawing the same thing,
419
+ * rather than each deciding what a vec2 or vec4 cross product means.
431
420
  */
432
421
  export function cross(a: Vec3, b: Vec3): Vec3 {
433
422
  if (a.width !== TYPE.VEC3 || b.width !== TYPE.VEC3) {
@@ -452,7 +441,7 @@ export function reflect(incident: Vec3, normal: Vec3): Vec3 {
452
441
  *
453
442
  * A MACRO, like `ramp`, and for the same reason: it expands into arithmetic
454
443
  * both backends already have, so it needs no opcode, no second implementation
455
- * in the emitter and no VM case. `SLOP.REMAP` stays a reserved number.
444
+ * in the emitters. `SLOP.REMAP` stays a reserved number.
456
445
  *
457
446
  * Unclamped on purpose, which is what HLSL authors expect of the one liner they
458
447
  * would otherwise write; wrap it in `saturate` when the ends matter.
@@ -495,11 +484,10 @@ export function select(cond: Num, whenTrue: Num, whenFalse: Num): Val {
495
484
  const t = typeof whenTrue === "number" ? float(whenTrue) : whenTrue
496
485
  const f = typeof whenFalse === "number" ? float(whenFalse) : whenFalse
497
486
  // The condition is widened with the branches, not left at its own width.
498
- // A register is a float4 whatever it holds, so a scalar condition sits
499
- // there as (c, 0, 0, 0) and the VM's `step(0.5, cond)` answered 0 for
500
- // components y, z and w, while the HLSL backend broadcasts a scalar
501
- // itself. The two DISAGREED on every vector valued select, which is the one
502
- // failure this design cannot tolerate.
487
+ // The VM (removed in 0.3.0) held a scalar condition as (c, 0, 0, 0) and
488
+ // answered 0 for components y, z and w, while HLSL broadcasts a scalar
489
+ // itself; the two disagreed on every vector valued select. Widening here
490
+ // keeps every emitter's SELECT the same simple form.
503
491
  const { refs, width } = alignN([c, t, f])
504
492
  return mk(b, b.call(SLOP.SELECT, width, refs), width)
505
493
  }
@@ -507,8 +495,8 @@ export function select(cond: Num, whenTrue: Num, whenFalse: Num): Val {
507
495
  /**
508
496
  * The interpolant is broadcast to the operand width before it crosses.
509
497
  *
510
- * A register is a float4 whatever it holds, so a scalar `t` sits there as
511
- * (t, 0, 0, 0) and the shader's lerp ran per component against those zeros:
498
+ * The VM (removed in 0.3.0) held a scalar `t` as (t, 0, 0, 0) and its lerp
499
+ * ran per component against those zeros:
512
500
  * a black to white ramp at its midpoint rendered (0.5, 0, 0, 1) instead of
513
501
  * grey. Broadcasting here rather than in the shader keeps `t` free to be a
514
502
  * genuine per component vector when an author wants one.
@@ -597,13 +585,9 @@ export function parseColor(hex: string): [number, number, number, number] {
597
585
  export function ramp(t: Num, stops: Array<string | [number, number, number, number]>): Vec4 {
598
586
  if (stops.length < 2) throw new SLError(`a ramp needs at least 2 stops, got ${stops.length}`)
599
587
  const tv = (typeof t === "number" ? float(t) : t).saturate()
600
- // Each stop is built IMMEDIATELY BEFORE the mix that consumes it, not all
601
- // of them up front. The graph is identical either way and so is the hash,
602
- // which is a Merkle hash over the shape and not over storage order, but the
603
- // register allocator walks the array: built up front, every stop stayed
604
- // live until the last mix, and a four stop ramp reserved four of the VM's
605
- // eight registers before the program did anything. That put the language's
606
- // headline colour tool a couple of registers from unusable.
588
+ // Each stop is built immediately before the mix that consumes it. The graph
589
+ // and the hash are the same either way (the hash is over the shape, not the
590
+ // storage order); the order only mattered to the VM's register allocator.
607
591
  const colourAt = (i: number) => {
608
592
  const v = typeof stops[i] === "string" ? parseColor(stops[i] as string) : (stops[i] as number[])
609
593
  return vec4(v[0], v[1], v[2], v[3])
@@ -733,34 +717,13 @@ export function texture(name: string): Texture {
733
717
  *
734
718
  * Honest about what it is: a macro, not a loop. It covers fbm, layered noise and
735
719
  * small iterated distance fields, which is most of what 2D shaders loop for. A
736
- * data dependent loop is out of scope, because in the VM it would need a nested
737
- * bounded loop with a dynamic trip count while codegen would handle it fine, and
738
- * the two backends agreeing is the property this whole design protects.
720
+ * loop with a runtime count is `Specs/SL_NEXT.md` proposal 3b.
739
721
  */
740
722
  export function repeat<T extends Val>(n: number, body: (i: number, acc: T) => T, seed: T): T {
741
723
  if (!Number.isInteger(n) || n < 0 || n > 64) {
742
724
  throw new SLError(`repeat count must be a whole number from 0 to 64, got ${n}`)
743
725
  }
744
- return unrolled(n, () => {
745
- let acc = seed
746
- for (let i = 0; i < n; i++) acc = body(i, acc)
747
- return acc
748
- })
749
- }
750
-
751
- /**
752
- * Records the nodes a body produced as one unrolled span.
753
- *
754
- * Diagnostic only: the instruction ceiling error uses the span to say which
755
- * loop spent the budget, so the fix reads as "lower this count" rather than
756
- * "fewer instructions". `repeat` is the EDSL's form of it and the text
757
- * language's `for` is the other, and neither should be reaching into the
758
- * Builder to push a span itself.
759
- */
760
- export function unrolled<T>(count: number, body: () => T): T {
761
- const b = ctx()
762
- const start = b.nodes.length
763
- const out = body()
764
- b.loops.push({ count, start, end: b.nodes.length })
765
- return out
726
+ let acc = seed
727
+ for (let i = 0; i < n; i++) acc = body(i, acc)
728
+ return acc
766
729
  }
package/src/web.ts CHANGED
@@ -2,11 +2,10 @@
2
2
  * The web backends: prints a program as GLSL ES 3.00 or WGSL, for a browser to
3
3
  * compile at runtime on Unity's own graphics device.
4
4
  *
5
- * Unity cannot compile a shader in a built game, which is why the VM exists.
6
- * A browser can, and the Play container runs on WebGPU with WebGL2 behind it,
7
- * so a program becomes WGSL on one and GLSL ES on the other and runs compiled:
8
- * no register budget, no interpreter, and on the spike's measurements 100 to
9
- * 600 times cheaper per pixel. The host that compiles and draws these lives in
5
+ * Unity cannot compile a shader in a built game, so a native build ships the
6
+ * shaders the editor generated. A browser can compile, and the Play container
7
+ * runs on WebGPU with WebGL2 behind it, so a program becomes WGSL on one and
8
+ * GLSL ES on the other. The host that compiles and draws these lives in
10
9
  * OneJS (`Plugins/WebGL/OneJSSLWeb.jslib`); this file only prints them.
11
10
  *
12
11
  * Structurally the HLSL emitter again: one local per reachable node, in order,
@@ -29,12 +28,12 @@ import { SLError, TYPE, type Program, type SLNode, type SLType } from "./ir"
29
28
  import { libClosure, libIndex, LIB_FUNCTIONS, SDF_CALLS } from "./lib"
30
29
  import { LIB_GLSL } from "./lib/glsl"
31
30
  import { LIB_WGSL } from "./lib/wgsl"
32
- import { INPUT_ID, SLOP, VM_UNIFORMS } from "./ops"
31
+ import { INPUT_ID, SLOP, UNIFORM_SLOTS } from "./ops"
33
32
 
34
33
  export type WebLanguage = "glsl" | "wgsl"
35
34
 
36
35
  /** Uniform slots the host always provides, so every program shares one layout. */
37
- export const WEB_UNIFORM_SLOTS = VM_UNIFORMS
36
+ export const WEB_UNIFORM_SLOTS = UNIFORM_SLOTS
38
37
 
39
38
  export function emitGLSL(p: Program): string {
40
39
  return emitWeb(p, "glsl")
@@ -159,7 +158,7 @@ function emitWeb(p: Program, lang: WebLanguage): string {
159
158
  case SLOP.MIX: return `mix(${s[0]}, ${s[1]}, ${s[2]})`
160
159
  case SLOP.STEP: return `step(${s[0]}, ${s[1]})`
161
160
  case SLOP.SMOOTHSTEP: smoothsteps.add(t); return `sl_smoothstep${t}(${s[0]}, ${s[1]}, ${s[2]})`
162
- // As the VM and the HLSL emitter do it: branchless, so every
161
+ // As the HLSL emitter does it: branchless, so every
163
162
  // backend agrees on the edge value.
164
163
  case SLOP.SELECT: {
165
164
  const c = typeOf(n.args[0])
@@ -182,7 +181,7 @@ function emitWeb(p: Program, lang: WebLanguage): string {
182
181
  default:
183
182
  throw new SLError(
184
183
  `the ${lang.toUpperCase()} emitter has no case for opcode ${n.op}. A program using it would ` +
185
- `differ between the VM and a compiled build, which is the one failure this design cannot tolerate.`,
184
+ `differ between a browser and a Unity build, which is the one failure this design cannot tolerate.`,
186
185
  )
187
186
  }
188
187