onejs-sl 0.2.1 → 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,16 @@
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
+
3
14
  ## 0.2.1
4
15
 
5
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.
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
@@ -39,8 +38,6 @@ only what it calls.
39
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.1",
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.1",
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,10 +6,9 @@
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
14
  import { checkCaps, type Program } from "./ir"
package/src/core.ts CHANGED
@@ -10,7 +10,7 @@
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 {
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,8 +20,7 @@
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
 
@@ -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,16 +180,8 @@ 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
- /**
202
- * @deprecated Use `TEXTURE_SLOTS`. Was 15, the WebGL2 sampler count, which let a
203
- * program declare textures no native host binds.
204
- */
205
- export const MAX_TEXTURES = TEXTURE_SLOTS
206
-
207
185
  /**
208
186
  * The refusals for a program past its caps, worded once for the parser (a
209
187
  * `file`) and the builder (a `program`) so the two cannot drift apart.
@@ -232,7 +210,7 @@ export function checkCaps(p: Pick<Program, "uniforms" | "textures">): void {
232
210
  if (p.textures.length > TEXTURE_SLOTS) throw new SLError(tooManyTextures(p.textures.length, "program"))
233
211
  }
234
212
 
235
- /** 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. */
236
214
  export const MAX_NODES = 4096
237
215
 
238
216
  export class SLError extends Error {
@@ -253,7 +231,6 @@ export class Builder {
253
231
  readonly nodes: SLNode[] = []
254
232
  readonly uniforms: UniformDecl[] = []
255
233
  readonly textures: TextureDecl[] = []
256
- readonly loops: LoopSpan[] = []
257
234
  private readonly interned = new Map<string, NodeRef>()
258
235
 
259
236
  add(node: SLNode): NodeRef {
@@ -401,8 +378,7 @@ export function widthName(t: SLType): string {
401
378
  *
402
379
  * This is the link between a program and its compiled shader. If it differs
403
380
  * between the machine that generated the shader and the machine that runs it,
404
- * the runtime silently falls back to the VM and nobody is told, which is the
405
- * 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.
406
382
  *
407
383
  * So it hashes the canonicalised node array and nothing else. Never object
408
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
 
@@ -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/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