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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ The VM is gone: every program is compiled, so there is no instruction buffer, wire version or VM limit left to import. A host on `compile` changes nothing; one still importing `onejs-sl/vm` or `onejs-sl/limits` moves to `onejs-sl/compile` and `onejs-sl/core`.
6
+
7
+ - `onejs-sl/vm`, `onejs-sl/limits`, `encode`, `forVm`, `vmFit` and `SL_WIRE_VERSION` are removed
8
+ - `VM_UNIFORMS` and `VM_TEXTURES` are removed; `UNIFORM_SLOTS` and `TEXTURE_SLOTS` in `onejs-sl/core` are the same numbers
9
+ - `MAX_TEXTURES` is removed
10
+ - `Program.loops`, `LoopSpan` and `sl.unrolled` are removed
11
+ - `fromJSON` refuses opcode 135, which only the VM's encoder wrote
12
+ - No error, doc or generated shader comment names the VM
13
+
14
+ ## 0.2.1
15
+
16
+ A program built in code or read from JSON is refused past 16 uniforms or 4 textures, as a `.sl` file already was, instead of compiling and then drawing differently on different hosts. A host can read both caps.
17
+
18
+ ### Fixed
19
+
20
+ - 0.2.0 regression: `compile()` dropped `encode()`'s texture check, so a program built in code with a fifth texture compiled and then sampled nothing in the editor and a native player
21
+
22
+ ### Changed
23
+
24
+ - `sl.texture` and `sl.uniform` refuse past the caps where they are declared, in the parser's words
25
+ - `compile` and `fromJSON` refuse a program past either cap, however it was made
26
+ - `onejs-sl/core` exports `UNIFORM_SLOTS` (16) and `TEXTURE_SLOTS` (4), which the parser and the builder both read
27
+ - `MAX_TEXTURES` is a deprecated alias of `TEXTURE_SLOTS`, so 4 rather than 15
28
+ - `VM_UNIFORMS` and `VM_TEXTURES` are the same numbers, and go with the VM
29
+ - The parser's cap messages give OneJS's slots as the reason
30
+
3
31
  ## 0.2.0
4
32
 
5
33
  `compile()` replaces `encode()` for every host that draws compiled: it gives a program's hash, names, defaults and sources with no VM buffer and no budget, so a long program or one with many values at once compiles. Program hashes do not change.
package/README.md CHANGED
@@ -3,8 +3,7 @@
3
3
  The OneJS shader language: a small typed language for per pixel programs. A
4
4
  program is written as a `.sl` file (HLSL text) or with the TypeScript form, and
5
5
  recorded as one portable IR, a flat typed graph with a hash. The package emits
6
- that IR as the buffer OneJS's VM evaluates, a Unity ShaderLab shader, WGSL and
7
- GLSL ES.
6
+ that IR as a Unity ShaderLab shader, WGSL and GLSL ES.
8
7
 
9
8
  It is the compiler [OneJS](https://onejs.com) uses, taken out of `onejs-unity`
10
9
  so a host without Unity can run it too. `onejs-unity/sl` and
@@ -36,11 +35,9 @@ only what it calls.
36
35
  | Entry | Contents |
37
36
  |---|---|
38
37
  | `onejs-sl` | `parse` and `analyze`, `diagnose` (every error in a file, not just the first), `classify` (every token with its class, comments kept, never throws), `fromGLSL` (a pasted Shadertoy or WebGL shader as a `.sl` file, with notes on what did not carry over), the TypeScript form `sl`, the IR types, `SL_IR_VERSION`, `toJSON`/`fromJSON`, `SLParseError` (file, line, column, offset, length, the bare `text`, and a `fix` where one is certain) |
39
- | `onejs-sl/core` | the same without the parser: what a game needs at run time |
38
+ | `onejs-sl/core` | the same without the parser: what a game needs at run time. Also the caps on one program, `UNIFORM_SLOTS` (16) and `TEXTURE_SLOTS` (4), which the parser and the builder enforce and a host can read |
40
39
  | `onejs-sl/tables` | `BUILTINS`, `SL_HLSL`, `INPUTS`, `SL_SDF_SHAPES`, `SL_SDF_PARAMS`, `SL_KEYWORDS`, `SL_TYPES`, `TYPE_WIDTH`, `PRELUDE_NAMES`: what completion and highlighting read. `BUILTIN_PARAMS`, `SL_SDF_PARAM_NAMES` and `LIB_SIGNATURES` name every parameter, so an editor can show `lerp(x, y, s)`, and `BUILTIN_DOCS`, `INPUT_DOCS` and `PRELUDE_DOCS` give each a line for its tooltip |
41
40
  | `onejs-sl/compile` | `compile(program)`: what a host draws a program with. Its hash, its uniform and texture names in slot order, their defaults, and `hlsl`, `wgsl` and `glsl` as lazy getters. No budget |
42
- | `onejs-sl/limits` | `vmFit(program)`: whether the VM runs it, and why not. The VM's, until it is deleted |
43
- | `onejs-sl/vm` | `encode`, `SL_WIRE_VERSION`: `compile` plus the VM's buffer. The VM's, until it is deleted |
44
41
  | `onejs-sl/emit/hlsl-body` | `emitBody`: a program as a function body for a host's own frame; `emitLibrary`: the library functions it calls |
45
42
  | `onejs-sl/emit/unity` | `emitShader`: the `.shader` a Unity editor generates, a frame over `emitBody` |
46
43
  | `onejs-sl/emit/web` | `emitWGSL`, `emitGLSL`: OneJS's web frame |
@@ -91,7 +88,7 @@ by `npm run lib`, and nobody edits the copies:
91
88
 
92
89
  - **OneJS's `.cginc` files** (`SDF2D`, `Noise2D`, `SLCommon` in
93
90
  `Resources/OneJS/`) are the source with an include guard and a generated
94
- header around it. The VM, the generated Unity shaders and `fx` include them.
91
+ header around it. The generated Unity shaders and `fx` include them.
95
92
  `npm run lib` writes them when the package sits in the OneJS container.
96
93
  - **`src/lib/`**: `table.ts` (every function, its signature, what it calls, and
97
94
  the shape table read from `sl_sdfDistance`'s switch), and the text of each
@@ -169,7 +166,7 @@ and QuickJS (Magerie). Two checks hold it to that:
169
166
  - `npm run test:quickjs` bundles `quickjs/corpus.ts` as one ES2020 IIFE, the
170
167
  way Magerie bundles a script, runs it in QuickJS-ng and in a bare Node
171
168
  context, and fails unless the two results agree byte for byte. The corpus
172
- parses, encodes, fits and emits every program in `corpus/`, every
169
+ parses, compiles and emits every program in `corpus/`, every
173
170
  shape at its full parameter count, and the error paths. `npm test` runs it
174
171
  after vitest.
175
172
 
@@ -206,15 +203,14 @@ container this package is checked out at `JSModules/onejs-sl`, and
206
203
  ## The one idea
207
204
 
208
205
  **One authoring surface, one IR, several backends.** Unity cannot compile a
209
- shader at runtime in a player build, on any graphics API, so the first backend
210
- made a program data that a fixed shader evaluates: a VM. An editor does compile
211
- shaders, at build time, so the second prints HLSL for it.
206
+ shader at runtime in a player build, on any graphics API, but an editor
207
+ compiles shaders at build time, so `hlsl.ts` prints HLSL for it and the build
208
+ ships the result.
212
209
 
213
210
  So the same source draws compiled from generated HLSL in the editor and in a
214
- native player, with no edit in between, and the VM stays behind a switch: OneJS
215
- leaves it out unless a project defines `ONEJS_SL_VM`. `compile.ts` gives a host
216
- everything it draws with and no VM buffer; `encode.ts` adds the buffer for
217
- `FxProgram.shader`, and goes with the VM. `hlsl.ts` feeds OneJS's
211
+ native player, with no edit in between. `compile.ts` gives a host everything it
212
+ draws with. (Until 0.3.0 a VM, one fixed shader evaluating a program as data,
213
+ drew what had no shader yet; OneJS 3.8 deleted it.) `hlsl.ts` feeds OneJS's
218
214
  `Editor/SLShaderGenerator.cs`. Nobody writes a manifest for it: an editor that
219
215
  draws a program asks the compiled program for its
220
216
  `hlsl` (a lazy getter, never read in Play), records it into
@@ -229,7 +225,7 @@ as GLSL ES 3.00 (carrying the library functions it calls, translated from
229
225
  `Plugins/WebGL/OneJSSLWeb.jslib` compiles whichever one Unity's device speaks
230
226
  and draws it into the element's target. A `.sl` import carries both strings,
231
227
  printed at build time; a `compile()` result has them as lazy getters, like
232
- `hlsl`. A WebGL player has no VM: the element draws nothing until the compiled
228
+ `hlsl`. The element draws nothing until the compiled
233
229
  program is ready, and nothing after a compile error, which the page reports.
234
230
  The host contract (the frame block, the
235
231
  16 uniform slots, one binding pair per sampled texture) is written out at the
@@ -237,7 +233,7 @@ top of `web.ts`.
237
233
 
238
234
  The emitters match the HLSL emitter's semantics rather than each language's
239
235
  own: `%` truncates like `fmod`, `pow` takes `abs` of its base, `asin` and
240
- `acos` clamp, `log` and `sqrt` guard their argument, a select is the VM's
236
+ `acos` clamp, `log` and `sqrt` guard their argument, a select is a
241
237
  branchless `lerp`. `web.test.ts` checks the structure (every shape, every
242
238
  opcode, the library order); whether the output matches the goldens is
243
239
  measured in a browser, through the real element, by `Tools/sl-web-parity` in
@@ -274,7 +270,7 @@ references, and writing it out long hand three times costs exactly the same,
274
270
  because nodes are interned as they are built.
275
271
 
276
272
  That reasoning is why the parser, when it came, cost only a parser: it emits
277
- this same IR, so it inherited the encoder, the emitter, the hash and every test
273
+ this same IR, so it inherited the emitters, the hash and every test
278
274
  that runs on a program.
279
275
 
280
276
  ## What is checked, and when
@@ -289,11 +285,12 @@ at module load, not at draw time:
289
285
  | A program returning something other than a `vec4` | "a program must return a vec4. Wrap it: sl.vec4(value, 1)" |
290
286
  | `vec4` given the wrong number of parts | "vec4 needs 4 components, got 3" |
291
287
  | One uniform name at two widths | "declared as both a float and a vec4" |
292
- | More than 15 textures | Names the limit and why it cannot be widened |
288
+ | More than 16 uniforms or 4 textures | "this program declares 5 textures and a program may sample 4", the parser's words |
293
289
  | A value borrowed from another program | "a value from another program cannot be used in this one" |
294
290
 
295
- The texture ceiling is the fragment shader's sampler slots on the WebGL2
296
- baseline, which is the one resource neither backend can widen.
291
+ The caps are `UNIFORM_SLOTS` and `TEXTURE_SLOTS`: what OneJS binds to one
292
+ program. `compile` and `fromJSON` hold a program to them too, however it was
293
+ made.
297
294
 
298
295
  ## The hash is the fragile part
299
296
 
@@ -323,20 +320,9 @@ refuses a newer one with a message naming both.
323
320
  `fromJSON` (`serial.ts`) are the IR as JSON for a host that stores programs;
324
321
  `fromJSON` checks everything an emitter relies on, refuses a newer version,
325
322
  and migrates an older one.
326
- - **`SL_WIRE_VERSION`** (`ops.ts`) is the newest VM encoding, and
327
- `SLProgramBridge.WireVersion` in OneJS must match it (a container test
328
- compares the two). `Encoded.wire`, and the `wire` in a `.sl` import, is the
329
- LOWEST version that can run that program, so a program using nothing new
330
- stays 1 and still runs on an older Play container. The VM refuses a newer
331
- one, only where the VM runs; a WebGL player draws compiled and never reads
332
- the buffer.
333
-
334
- IR 2 and wire 2 came with #129. A shape takes as many parameters as it reads
323
+ IR 2 came with #129. A shape takes as many parameters as it reads
335
324
  (`SL_SDF_PARAMS`, six at most, and never fewer than four accepted), where it
336
- used to take four and lose the rest. One instruction holds a shape id and four
337
- immediates, so the encoder's `forVm` turns a shape given a fifth or sixth into
338
- `SDF_WIDE`, which reads the remaining four from a constant register. Only the
339
- VM sees it, and only those programs are wire 2.
325
+ used to take four and lose the rest.
340
326
 
341
327
  ## Control flow
342
328
 
@@ -346,13 +332,10 @@ unrolls at record time because `n` is a JavaScript number.
346
332
 
347
333
  `repeat` is honest about being a macro rather than a loop. It covers fbm,
348
334
  layered noise and small iterated distance fields, which is most of what 2D
349
- shaders loop for. A data dependent loop is out of scope: the VM would need a
350
- nested bounded loop with a dynamic trip count while codegen would handle it
351
- fine, and the two backends agreeing is the property the whole design protects.
352
- Because it unrolls, the count multiplies the body's operation count. `compile`
353
- has no ceiling, so a long program costs what it costs, as any shader does;
354
- `corpus/long.sl` is over a thousand operations. `encode` still refuses past the
355
- VM's 256, and names any `repeat` that fills a quarter of that.
335
+ shaders loop for. A loop with a runtime count is `Specs/SL_NEXT.md` proposal
336
+ 3b. Because it unrolls, the count multiplies the body's operation count.
337
+ `compile` has no ceiling, so a long program costs what it costs, as any shader
338
+ does; `corpus/long.sl` is over a thousand operations.
356
339
 
357
340
  Every loop that reaches a GPU is therefore bounded by a constant: `repeat` is
358
341
  unrolled, fbm's octaves are a constant 1 to 4, the helper loops in the noise
@@ -364,8 +347,8 @@ reset takes the whole page's device, Unity's included.
364
347
  ## See also
365
348
 
366
349
  - `Specs/SHADER_LANG.md` in the OneJS container, sections 3 and 4, and section 5.4 for the Phase 0
367
- measurements that decided the VM's shape
368
- - `Tools/shader-vm-spike/`, the harness behind those numbers
350
+ measurements that decided the VM's shape; the harness behind those numbers,
351
+ `Tools/shader-vm-spike/`, is in the container's history at `afd2535`
369
352
  - `onejs-unity`'s `fx`, the image pipeline this becomes a source and an operand for
370
353
 
371
354
  ## What a program is given
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "generatedBy": "onejs-sl goldens/run.mjs",
3
- "package": "0.2.0",
3
+ "package": "0.3.0",
4
4
  "ir": 2,
5
5
  "target": "rgba8unorm-srgb: straight alpha, 8 bit, sRGB encoded, the stored bytes of a Linear OneJS game's element",
6
6
  "size": [
package/lib/common.hlsl CHANGED
@@ -1,7 +1,7 @@
1
1
  // Helpers shared by EVERY shader language backend.
2
2
  //
3
- // The VM (OneJS/FxProgram.shader) includes this as SLCommon.cginc, and every
4
- // shader generated from a program by the HLSL emitter includes it too. The web
3
+ // Every shader generated from a program by the HLSL emitter includes this as
4
+ // SLCommon.cginc. The web
5
5
  // emitters and a host's own frame (`emitBody`) get it translated, by
6
6
  // `lib/translate.ts`, from this one text. That sharing is the whole reason this
7
7
  // file exists: the backends have to agree, and the fastest way to make them
@@ -134,10 +134,9 @@ float sl_luminance(float3 c) { return dot(c, float3(0.2126, 0.7152, 0.0722)); }
134
134
  // In a Gamma project the written value is already right. The float4 form keeps
135
135
  // alpha, which is coverage rather than light. Mirrors fxColorToWorking.
136
136
  //
137
- // Every width uses Unity's cubic approximation, lane by lane. The VM has one
138
- // TO_LINEAR for all widths (its registers are float4, so it converts .rgb),
139
- // and a scalar or float2 on the exact curve here drew up to 5/255 apart from
140
- // it in the dark range: the editor and a player disagreed.
137
+ // Every width uses Unity's cubic approximation, lane by lane. A scalar or
138
+ // float2 on the exact curve once drew up to 5/255 apart from the float3 form
139
+ // in the dark range.
141
140
  //
142
141
  // The curve is Unity's GammaToLinearSpace, written out here so that a target
143
142
  // without UnityCG.cginc has it too. The switch below is the one piece of
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "onejs-sl",
3
- "version": "0.2.0",
4
- "description": "The OneJS shader language: a small typed language for per pixel programs, compiled to a portable IR and emitted as HLSL, WGSL, GLSL ES or a VM buffer",
3
+ "version": "0.3.0",
4
+ "description": "The OneJS shader language: a small typed language for per pixel programs, compiled to a portable IR and emitted as HLSL, WGSL or GLSL ES",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
7
7
  "exports": {
@@ -9,8 +9,6 @@
9
9
  "./core": "./src/core.ts",
10
10
  "./compile": "./src/compile.ts",
11
11
  "./tables": "./src/tables.ts",
12
- "./limits": "./src/limits.ts",
13
- "./vm": "./src/vm.ts",
14
12
  "./emit/hlsl-body": "./src/emit/hlsl-body.ts",
15
13
  "./emit/unity": "./src/emit/unity.ts",
16
14
  "./emit/web": "./src/emit/web.ts",
package/src/body.ts CHANGED
@@ -43,7 +43,7 @@ export interface BodyTarget {
43
43
  * an sRGB texture is decoded before it is filtered, as an sRGB sampler
44
44
  * does; with `gamma`, rgb is as stored.
45
45
  * - **Filtered and wrapped by the host.** The body does neither. OneJS's
46
- * hosts, the generated shader, the VM and the web backends alike, use the
46
+ * hosts, the generated shader and the web backends alike, use the
47
47
  * bound texture's own filter and wrap modes, so a host that filters by
48
48
  * hand draws the same picture by following the texture's settings.
49
49
  *
@@ -143,7 +143,7 @@ export function emitBody(p: Program, target: BodyTarget): Body {
143
143
  case SLOP.MUL: return `(${a[0]} * ${a[1]})`
144
144
  case SLOP.DIV: return `(${a[0]} / ${a[1]})`
145
145
  case SLOP.MOD: return `fmod(${s[0]}, ${s[1]})`
146
- // abs on the base, matching the VM. pow of a negative base is
146
+ // abs on the base, matching the web emitters. pow of a negative base is
147
147
  // undefined in HLSL and the backends must be undefined in the same
148
148
  // direction.
149
149
  case SLOP.POW: return `pow(abs(${s[0]}), ${s[1]})`
@@ -170,8 +170,8 @@ export function emitBody(p: Program, target: BodyTarget): Body {
170
170
  case SLOP.CLAMP: return `clamp(${s[0]}, ${s[1]}, ${s[2]})`
171
171
  case SLOP.SATURATE: return `saturate(${a[0]})`
172
172
 
173
- // Of a scalar, Metal's are ambiguous, so what the VM computes for one
174
- // (its lanes past the width hold 0) is written out.
173
+ // Of a scalar, Metal's are ambiguous, so the scalar case is written
174
+ // out.
175
175
  case SLOP.LENGTH: return w0 === 1 ? `abs(${a[0]})` : `length(${a[0]})`
176
176
  case SLOP.DISTANCE: return w0 === 1 ? `abs(${a[0]} - ${a[1]})` : `distance(${a[0]}, ${a[1]})`
177
177
  case SLOP.DOT: return w0 === 1 ? `(${a[0]} * ${a[1]})` : `dot(${a[0]}, ${a[1]})`
@@ -184,7 +184,7 @@ export function emitBody(p: Program, target: BodyTarget): Body {
184
184
  case SLOP.MIX: return `lerp(${s[0]}, ${s[1]}, ${s[2]})`
185
185
  case SLOP.STEP: return `step(${s[0]}, ${s[1]})`
186
186
  case SLOP.SMOOTHSTEP: return `smoothstep(${s[0]}, ${s[1]}, ${s[2]})`
187
- // Matches the VM exactly, branchlessly, rather than using an if.
187
+ // Branchless, as the web emitters do it, rather than using an if.
188
188
  // Two backends that pick differently here disagree on every edge
189
189
  // value.
190
190
  case SLOP.SELECT: return `lerp(${s[2]}, ${s[1]}, step(${k(0.5, t)}, ${s[0]}))`
@@ -211,7 +211,7 @@ export function emitBody(p: Program, target: BodyTarget): Body {
211
211
  default:
212
212
  throw new SLError(
213
213
  `the HLSL emitter has no case for opcode ${n.op}. A program using it would silently ` +
214
- `differ between the VM and a compiled build, which is the one failure this design ` +
214
+ `differ between a Unity build and a browser, which is the one failure this design ` +
215
215
  `cannot tolerate.`,
216
216
  )
217
217
  }
package/src/compile.ts CHANGED
@@ -6,13 +6,12 @@
6
6
  * editor to generate a shader from, WGSL and GLSL ES 3.00 for a browser to
7
7
  * compile on Unity's own device.
8
8
  *
9
- * There is no budget. `encode` refuses a program past the VM's 256
10
- * instructions or 8 registers, because the VM cannot run it; every backend
11
- * here is a real compiler, so a long program costs what it costs, as any
12
- * shader does. `Specs/SL_NEXT.md` section 0.5, step 3.
9
+ * There is no budget: every backend is a real compiler, so a long program
10
+ * costs what it costs, as any shader does. The caps on uniforms and textures
11
+ * are the only refusal (`checkCaps`).
13
12
  */
14
13
 
15
- import type { Program } from "./ir"
14
+ import { checkCaps, type Program } from "./ir"
16
15
  import { emitShader } from "./hlsl"
17
16
  import { emitGLSL, emitWGSL } from "./web"
18
17
  import { uniformDefaults } from "./sl"
@@ -69,6 +68,7 @@ export interface Compiled {
69
68
  }
70
69
 
71
70
  export function compile(program: Program): Compiled {
71
+ checkCaps(program)
72
72
  const compiled = {
73
73
  hash: program.hash,
74
74
  // Slot order, which is declaration order: Builder.uniform pushes and
package/src/core.ts CHANGED
@@ -10,13 +10,13 @@
10
10
  export * as sl from "./sl"
11
11
  export type { Float, Vec2, Vec3, Vec4, Num, ProgramInputs, Texture } from "./sl"
12
12
  export {
13
- TYPE, INPUTS, MAX_TEXTURES, MAX_NODES, SLError, hashProgram, widthName, SL_IR_VERSION, inputsUsed,
13
+ TYPE, INPUTS, MAX_NODES, SLError, hashProgram, widthName, SL_IR_VERSION, inputsUsed,
14
14
  CONTROL_FIELDS, controlProblem,
15
15
  } from "./ir"
16
16
  export type {
17
17
  SLType, InputName, NodeRef, SLNode, Program, UniformDecl, UniformControl, TextureDecl,
18
18
  } from "./ir"
19
- export { SLOP, SL_ARITY, SL_NAME, INPUT_ID, isSampling } from "./ops"
19
+ export { SLOP, SL_ARITY, SL_NAME, INPUT_ID, isSampling, UNIFORM_SLOTS, TEXTURE_SLOTS } from "./ops"
20
20
  export type { SLOpCode } from "./ops"
21
21
  export { SL_SDF_SHAPES, SL_SDF_PARAMS } from "./shapes"
22
22
  export type { SlSdfKind } from "./shapes"
package/src/hlsl.ts CHANGED
@@ -1,19 +1,12 @@
1
1
  /**
2
- * The second backend: prints a program as real HLSL, compiled at build time.
2
+ * The Unity backend: prints a program as real HLSL, compiled at build time.
3
3
  *
4
- * Phase 3 of `Specs/SHADER_LANG.md` section 6, and the half the whole design
5
- * exists for. Unity cannot compile a shader at runtime in a player build, so on
6
- * play.onejs.com a program has to be interpreted. Ejecting to a Unity project
7
- * does not change what the author wrote, it changes what is POSSIBLE, because
8
- * an editor compiles shaders at build time.
4
+ * Phase 3 of `Specs/SHADER_LANG.md` section 6. Unity cannot compile a shader
5
+ * at runtime in a player build, so the editor generates one per program and the
6
+ * build ships it; a browser compiles the web emitters' output instead
7
+ * (`web.ts`). The same source draws both ways with no edit in between.
9
8
  *
10
- * So the same source is interpreted in the browser and compiled after an eject,
11
- * with no edit in between. That is what stops "Play games eject cleanly" from
12
- * quietly acquiring an asterisk.
13
- *
14
- * WHY THIS IS THE EASY BACKEND. The VM keeps every value in a float4 register
15
- * and has to be told how wide each one really is. Here the IR's types become the
16
- * HLSL types directly, so `float2` is a `float2` and nothing needs padding or
9
+ * The IR's types become the HLSL types directly, so `float2` is a `float2` and nothing needs padding or
17
10
  * explaining. One local per node also gives common subexpression elimination for
18
11
  * free: a node is emitted once no matter how many nodes reference it.
19
12
  *
@@ -48,8 +41,8 @@ export interface EmitOptions {
48
41
  * Emits a complete `.shader` for a program.
49
42
  *
50
43
  * The shader's name carries the program hash, which is how the runtime finds it
51
- * again. If that link breaks, the runtime silently falls back to the VM and
52
- * nobody is told: correct output, quietly slow, no error. See `hashProgram`.
44
+ * again. If that link breaks, the program has no shader and draws nothing. See
45
+ * `hashProgram`.
53
46
  */
54
47
  export function emitShader(p: Program, options: EmitOptions = {}): string {
55
48
  const name = options.name ?? `Hidden/SLGenerated/${p.hash}`
@@ -99,7 +92,7 @@ ${props.join("\n")}
99
92
  #pragma fragment frag
100
93
  #pragma target 3.0
101
94
  #include "UnityCG.cginc"
102
- // Shared with the VM, so both backends compute the same noise.
95
+ // Shared with the web frame, so every backend computes the same noise.
103
96
  #include "${include}"
104
97
 
105
98
  ${decls.join("\n")}
@@ -111,8 +104,8 @@ ${decls.join("\n")}
111
104
  {
112
105
  v2f o;
113
106
  o.pos = UnityObjectToClipPos(v.vertex);
114
- // Origin corrected here exactly as the VM does it, so an ejected
115
- // game is not upside down relative to the one on the site.
107
+ // Origin corrected here, so a Unity build is not upside down
108
+ // relative to the same program in a browser.
116
109
  o.uv = float2(v.uv.x, lerp(v.uv.y, 1.0 - v.uv.y, _FlipY));
117
110
  return o;
118
111
  }
package/src/index.ts CHANGED
@@ -2,16 +2,14 @@
2
2
  * `onejs-sl`: the shader language, with no Unity in it.
3
3
  *
4
4
  * Source text or the TypeScript form in, a `Program` out: a flat typed graph
5
- * with a hash. Everything a host does with a program (run it on the VM, emit
6
- * HLSL, WGSL or GLSL ES) is a separate entry, so a host bundles only what it
5
+ * with a hash. Everything a host does with a program (emit HLSL, WGSL or GLSL
6
+ * ES) is a separate entry, so a host bundles only what it
7
7
  * calls:
8
8
  *
9
9
  * onejs-sl this: parse, diagnose, classify, fromGLSL, the TypeScript form, the IR, its JSON
10
10
  * onejs-sl/core the same without the parser, for a game at run time
11
11
  * onejs-sl/tables builtins, opcodes, inputs, shapes: what an editor reads
12
12
  * onejs-sl/compile what a host draws a program with, and no budget
13
- * onejs-sl/limits whether a program fits the VM, until the VM goes
14
- * onejs-sl/vm the VM encoder, until the VM goes
15
13
  * onejs-sl/emit/unity the ShaderLab shader a Unity editor generates
16
14
  * onejs-sl/emit/web WGSL and GLSL ES with OneJS's web frame
17
15
  *
package/src/ir.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  /**
2
- * The shader language IR: one graph, two consumers.
2
+ * The shader language IR: one graph, and every backend a function of it.
3
3
  *
4
- * Phase 1 of `Specs/SHADER_LANG.md` section 4. Everything the VM encoder and the
5
- * HLSL emitter do is a function of this shape, so it is the part worth getting
6
- * right before either exists.
4
+ * Phase 1 of `Specs/SHADER_LANG.md` section 4. Every emitter (HLSL, WGSL, GLSL
5
+ * ES) reads this shape and nothing else.
7
6
  *
8
7
  * Four properties are load bearing:
9
8
  *
@@ -12,7 +11,7 @@
12
11
  * produces the author facing type errors.
13
12
  *
14
13
  * **Nodes are a flat array in topological order.** A node refers to earlier
15
- * nodes by index only, never forwards, so an encoder can walk the array once and
14
+ * nodes by index only, never forwards, so an emitter can walk the array once and
16
15
  * emit in order. The hash deliberately does NOT depend on that order; see
17
16
  * `hashProgram`.
18
17
  *
@@ -21,12 +20,11 @@
21
20
  * pass. A tree would silently square the cost of the most natural way to write a
22
21
  * shader.
23
22
  *
24
- * **Nothing here knows about shaders.** No HLSL, no texture layout, no register
25
- * allocation. Those belong to the backends; this file is the contract between
23
+ * **Nothing here knows about shaders.** No HLSL, no texture layout, no binding. Those belong to the backends; this file is the contract between
26
24
  * them, and it is fully testable with no GPU.
27
25
  */
28
26
 
29
- import { SLOP, SL_ARITY, SL_NAME, type SLOpCode } from "./ops"
27
+ import { SLOP, SL_ARITY, SL_NAME, TEXTURE_SLOTS, UNIFORM_SLOTS, type SLOpCode } from "./ops"
30
28
 
31
29
  /** Component count. The only notion of type the IR has. */
32
30
  export const TYPE = { FLOAT: 1, VEC2: 2, VEC3: 3, VEC4: 4 } as const
@@ -47,7 +45,7 @@ export type InputName = keyof typeof INPUTS
47
45
  * handed over by the host (`Specs/SL_NEXT.md` 6). Each read records the same
48
46
  * few nodes a program would if it wrote the expression out, and the builder's
49
47
  * hash consing makes every later read the same node. So there is no new
50
- * instruction, no VM or host change, and a program that never names one holds
48
+ * operation, no host change, and a program that never names one holds
51
49
  * no extra node, compiling to the bytes it always did.
52
50
  *
53
51
  * texel 1 / resolution: one pixel, in uv
@@ -172,18 +170,6 @@ export interface TextureDecl {
172
170
  slot: number
173
171
  }
174
172
 
175
- /**
176
- * The nodes one `sl.repeat` call unrolled into, as the half open range
177
- * `[start, end)` of `nodes`. Diagnostic only: the instruction ceiling error
178
- * uses it to say which loop the operations came from, since a program that is
179
- * "too long" has usually been made so by one count.
180
- */
181
- export interface LoopSpan {
182
- count: number
183
- start: NodeRef
184
- end: NodeRef
185
- }
186
-
187
173
  export interface Program {
188
174
  /** The `SL_IR_VERSION` this program's nodes mean what they mean under. */
189
175
  version: number
@@ -194,18 +180,37 @@ export interface Program {
194
180
  textures: TextureDecl[]
195
181
  /** Canonical, stable across machines. See `hashProgram`. */
196
182
  hash: string
197
- /** Not part of the hash: it changes nothing about what the program computes. */
198
- loops: LoopSpan[]
199
183
  }
200
184
 
201
185
  /**
202
- * Sampler slots in a fragment shader on the WebGL2 baseline, minus one for the
203
- * program texture itself. Exceeding it is refused when the program is written
204
- * rather than when it is drawn, with a message naming the limit.
186
+ * The refusals for a program past its caps, worded once for the parser (a
187
+ * `file`) and the builder (a `program`) so the two cannot drift apart.
205
188
  */
206
- export const MAX_TEXTURES = 15
189
+ export function tooManyUniforms(declared: number, source: "file" | "program"): string {
190
+ return `this ${source} declares ${declared} uniforms and a program may hold ${UNIFORM_SLOTS}. ` +
191
+ `OneJS keeps a program's uniforms in ${UNIFORM_SLOTS} slots, so this one would have no ` +
192
+ `slot of its own; it is refused here rather than drawn wrong. Pack related values into ` +
193
+ `a ${source === "file" ? "float4" : "vec4"}.`
194
+ }
195
+
196
+ export function tooManyTextures(declared: number, source: "file" | "program"): string {
197
+ return `this ${source} declares ${declared} textures and a program may sample ${TEXTURE_SLOTS}. ` +
198
+ `OneJS binds ${TEXTURE_SLOTS} textures to a program in the editor and a native player, so ` +
199
+ `this one would sample nothing there while a browser drew it: two pictures from one ${source}.`
200
+ }
201
+
202
+ /**
203
+ * Refuses a finished program past either cap. The parser and the builder
204
+ * refuse at the declaration, where the message can point at it; this is for
205
+ * a program that arrived some other way, stored as JSON or put together by
206
+ * hand, and `compile` runs it so nothing reaches a host around it.
207
+ */
208
+ export function checkCaps(p: Pick<Program, "uniforms" | "textures">): void {
209
+ if (p.uniforms.length > UNIFORM_SLOTS) throw new SLError(tooManyUniforms(p.uniforms.length, "program"))
210
+ if (p.textures.length > TEXTURE_SLOTS) throw new SLError(tooManyTextures(p.textures.length, "program"))
211
+ }
207
212
 
208
- /** Instructions a single program may hold. Generous; the ceiling that matters is registers. */
213
+ /** Operations a single program may hold. Generous: a guard against a runaway builder, not a budget. */
209
214
  export const MAX_NODES = 4096
210
215
 
211
216
  export class SLError extends Error {
@@ -226,7 +231,6 @@ export class Builder {
226
231
  readonly nodes: SLNode[] = []
227
232
  readonly uniforms: UniformDecl[] = []
228
233
  readonly textures: TextureDecl[] = []
229
- readonly loops: LoopSpan[] = []
230
234
  private readonly interned = new Map<string, NodeRef>()
231
235
 
232
236
  add(node: SLNode): NodeRef {
@@ -288,6 +292,7 @@ export class Builder {
288
292
  const problem = controlProblem(type, value, control)
289
293
  if (problem !== null) throw new SLError(`uniform "${name}": ${problem.message}`)
290
294
  const slot = this.uniforms.length
295
+ if (slot >= UNIFORM_SLOTS) throw new SLError(tooManyUniforms(slot + 1, "program"))
291
296
  const decl: UniformDecl = { name, type, value: value.slice() }
292
297
  if (colour) decl.colour = true
293
298
  Object.assign(decl, controlOf(control))
@@ -298,12 +303,8 @@ export class Builder {
298
303
  texture(name: string): number {
299
304
  const existing = this.textures.findIndex((t) => t.name === name)
300
305
  if (existing >= 0) return this.textures[existing].slot
301
- if (this.textures.length >= MAX_TEXTURES) {
302
- throw new SLError(
303
- `a program may sample at most ${MAX_TEXTURES} textures, and this one asks for ` +
304
- `${this.textures.length + 1}. That ceiling is the fragment shader's sampler slots ` +
305
- `on the WebGL2 baseline, so it cannot be widened.`,
306
- )
306
+ if (this.textures.length >= TEXTURE_SLOTS) {
307
+ throw new SLError(tooManyTextures(this.textures.length + 1, "program"))
307
308
  }
308
309
  const slot = this.textures.length
309
310
  this.textures.push({ name, slot })
@@ -377,8 +378,7 @@ export function widthName(t: SLType): string {
377
378
  *
378
379
  * This is the link between a program and its compiled shader. If it differs
379
380
  * between the machine that generated the shader and the machine that runs it,
380
- * the runtime silently falls back to the VM and nobody is told, which is the
381
- * worst failure this design can have: correct output, quietly slow, no error.
381
+ * a native player finds no shader for the program and draws nothing.
382
382
  *
383
383
  * So it hashes the canonicalised node array and nothing else. Never object
384
384
  * identity, never insertion order of a Map, never a JSON stringify whose key
@@ -28,12 +28,12 @@ const plasma = parse(source, { file: "plasma.sl" })
28
28
  written, and the two produce the same graph. `parity.test.ts` asserts that as
29
29
  hash equality for every GPU fixture and for the example on play.onejs.com: same
30
30
  hash means same generated shader and same pixels, established without rendering
31
- anything. It also means the existing GPU fixtures, the codegen goldens and the
32
- C# VM tests cover the text form for free, because they run on the IR.
31
+ anything. It also means the existing GPU fixtures and the codegen goldens
32
+ cover the text form for free, because they run on the IR.
33
33
 
34
34
  If that property ever breaks, the failure is silent in the worst way: a program
35
- whose hash does not match its generated shader falls back to the VM and nobody
36
- is told. So parity is the test to keep green, not the parser's unit tests.
35
+ whose hash does not match its generated shader draws nothing in a native
36
+ player. So parity is the test to keep green, not the parser's unit tests.
37
37
 
38
38
  | File | What it does |
39
39
  |---|---|
@@ -49,8 +49,8 @@ is told. So parity is the test to keep green, not the parser's unit tests.
49
49
 
50
50
  ## The loader
51
51
 
52
- `slPlugin()`, in onejs-unity's `onejs-unity/esbuild`, parses and encodes at BUILD TIME, so an import resolves to a small
53
- object of numbers and the bundle carries neither the parser nor the source. A
52
+ `slPlugin()`, in onejs-unity's `onejs-unity/esbuild`, parses and compiles at BUILD TIME, so an import resolves to a small
53
+ object of names, defaults and shader sources and the bundle carries neither the parser nor the source. A
54
54
  parse error becomes an esbuild error with the file, line and column, which the
55
55
  Play editor surfaces and every terminal editor links.
56
56
 
@@ -158,16 +158,12 @@ recorded gap, not an oversight, and each has its own error message rather than
158
158
 
159
159
  | | Why |
160
160
  |---|---|
161
- | `rgb2hsv`, `tex2Dlod` | The opcodes are numbered and **neither backend implements them**. Writing one would render as whatever the VM's dispatch falls through to and fail outright in the HLSL emitter. |
161
+ | `rgb2hsv`, `tex2Dlod` | The opcodes are numbered and **no backend implements them**; every emitter throws on them. |
162
162
  | `fbm`'s simplex base | `sl.fbm(p, octaves, "simplex")` picks the base with a string, and the language has no strings. `fbm(p, octaves)` is the value base, and `turbulence` and `ridged` are the simplex family. |
163
163
 
164
- Related, and worth knowing: **the EDSL lets a program declare 15 textures and
165
- the VM has 4.** `ir.ts` picked its ceiling from the WebGL2 sampler count rather
166
- than from `FxProgram.shader`, which declares `_Tex0` to `_Tex3` and samples
167
- `_Tex3` for every slot past it, so slots 4 and up are silently wrong in the
168
- browser and correct after an eject. A `.sl` file is held to the real number, 4,
169
- reported at the declaration. The EDSL's ceiling is untouched here because
170
- lowering it changes recorded behaviour rather than parser behaviour.
164
+ A `.sl` file and the EDSL are held to the same caps, `UNIFORM_SLOTS` (16) and
165
+ `TEXTURE_SLOTS` (4), reported at the declaration in one wording (`ir.ts`'s
166
+ `tooManyUniforms` and `tooManyTextures`).
171
167
 
172
168
  ## Attributes
173
169