onejs-sl 0.2.1 → 0.4.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,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0
4
+
5
+ Every hash changes once, so a program built in code is recorded again: run the app in the editor once, then build. A program from a `.sl` file needs only the app rebuilt. A host reading programs as JSON reads 0.3.0's files as before.
6
+
7
+ - Every hash changes once: `a + b` and `b + a` share one, as do `8 * uv` and `uv * 8` (`SL_HASH_VERSION` 2)
8
+ - A generated shader names its hash scheme on a line of its own (`// SL_HASH_VERSION 2`); `SL_HASH_VERSION` is exported from `onejs-sl/core`
9
+ - `tex2Dlod(t, uv, lod)` samples a mip level; `sampleLevel` on `sl.texture`
10
+ - `BodyTarget.sampleLevel` is required, for `tex2Dlod`
11
+ - `fromGLSL` turns `textureLod` into `tex2Dlod`
12
+ - A shape's parameters may be any value, a uniform included; a vector counts as its components
13
+ - `fbm`, `turbulence` and `ridged` take an octave count that is a value, rounded and held to 1 to 4
14
+ - IR 3: a shape's parameters and an octave count are always operands; `fromJSON` migrates an older file
15
+ - `toJSON` writes the hash scheme (`h`), so `fromJSON` rehashes a file an older scheme wrote instead of refusing it
16
+ - A ramp's stop may be a colour uniform or a const holding a hex, as well as a hex
17
+
18
+ ## 0.3.0
19
+
20
+ 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`.
21
+
22
+ - `onejs-sl/vm`, `onejs-sl/limits`, `encode`, `forVm`, `vmFit` and `SL_WIRE_VERSION` are removed
23
+ - `VM_UNIFORMS` and `VM_TEXTURES` are removed; `UNIFORM_SLOTS` and `TEXTURE_SLOTS` in `onejs-sl/core` are the same numbers
24
+ - `MAX_TEXTURES` is removed
25
+ - `Program.loops`, `LoopSpan` and `sl.unrolled` are removed
26
+ - `fromJSON` refuses opcode 135, which only the VM's encoder wrote
27
+ - No error, doc or generated shader comment names the VM
28
+
3
29
  ## 0.2.1
4
30
 
5
31
  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 |
@@ -54,7 +51,8 @@ only what it calls.
54
51
  HLSL and Metal shared subset (HLSL spelling, `fmod`, no `mul`, no `static`, no
55
52
  derivatives, no swizzle of a scalar, and every literal and scalar in an
56
53
  intrinsic at its exact type, since Metal overloads where HLSL converts). The `BodyTarget` says what differs between hosts: an expression
57
- for each input, the float4 holding a uniform slot, a texture sample, whether
54
+ for each input, the float4 holding a uniform slot, a texture sample and a
55
+ sample at a mip level (`sampleLevel`, for `tex2Dlod`), whether
58
56
  `toLinear` is real (`colour: "linear"`) or the identity (`"gamma"`), and
59
57
  optionally a local to assign the result to. It returns the uniform and texture
60
58
  slots the body uses and the library functions it calls. The sample's contract
@@ -91,7 +89,7 @@ by `npm run lib`, and nobody edits the copies:
91
89
 
92
90
  - **OneJS's `.cginc` files** (`SDF2D`, `Noise2D`, `SLCommon` in
93
91
  `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.
92
+ header around it. The generated Unity shaders and `fx` include them.
95
93
  `npm run lib` writes them when the package sits in the OneJS container.
96
94
  - **`src/lib/`**: `table.ts` (every function, its signature, what it calls, and
97
95
  the shape table read from `sl_sdfDistance`'s switch), and the text of each
@@ -134,9 +132,13 @@ what a WebGL player draws.
134
132
 
135
133
  `npm run goldens` draws it on WebGPU and WebGL2 in a Chrome with its own
136
134
  profile (set `CHROME` to choose one). The two backends must agree within 1/255
137
- over every pixel, and three anchors must match arithmetic, not each other:
135
+ over every pixel, and seven anchors must match arithmetic, not each other:
138
136
  `orient.sl` (orientation, and the linear to sRGB store), `hex.sl` (a hex colour
139
- stores as written) and `texture.sl` (a texture's orientation and sRGB decode).
137
+ stores as written), `texture.sl` (a texture's orientation and sRGB decode) and
138
+ `lod.sl` (each mip level `tex2Dlod` reads, the texture's smaller levels being
139
+ solid colours). `sdf-values.sl`, `fbm-values.sl` and `ramp-values.sl` are black unless a
140
+ shape, a noise or a ramp draws differently with its parameters as values than
141
+ as constants.
140
142
  The whole translated library must also compile on both backends, including
141
143
  the functions no fixture reaches. The file describes the sampling grid, the
142
144
  times and the texture every sampled slot gets. A host imports it as
@@ -169,7 +171,7 @@ and QuickJS (Magerie). Two checks hold it to that:
169
171
  - `npm run test:quickjs` bundles `quickjs/corpus.ts` as one ES2020 IIFE, the
170
172
  way Magerie bundles a script, runs it in QuickJS-ng and in a bare Node
171
173
  context, and fails unless the two results agree byte for byte. The corpus
172
- parses, encodes, fits and emits every program in `corpus/`, every
174
+ parses, compiles and emits every program in `corpus/`, every
173
175
  shape at its full parameter count, and the error paths. `npm test` runs it
174
176
  after vitest.
175
177
 
@@ -206,15 +208,14 @@ container this package is checked out at `JSModules/onejs-sl`, and
206
208
  ## The one idea
207
209
 
208
210
  **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.
211
+ shader at runtime in a player build, on any graphics API, but an editor
212
+ compiles shaders at build time, so `hlsl.ts` prints HLSL for it and the build
213
+ ships the result.
212
214
 
213
215
  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
216
+ native player, with no edit in between. `compile.ts` gives a host everything it
217
+ draws with. (Until 0.3.0 a VM, one fixed shader evaluating a program as data,
218
+ drew what had no shader yet; OneJS 3.8 deleted it.) `hlsl.ts` feeds OneJS's
218
219
  `Editor/SLShaderGenerator.cs`. Nobody writes a manifest for it: an editor that
219
220
  draws a program asks the compiled program for its
220
221
  `hlsl` (a lazy getter, never read in Play), records it into
@@ -229,7 +230,7 @@ as GLSL ES 3.00 (carrying the library functions it calls, translated from
229
230
  `Plugins/WebGL/OneJSSLWeb.jslib` compiles whichever one Unity's device speaks
230
231
  and draws it into the element's target. A `.sl` import carries both strings,
231
232
  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
233
+ `hlsl`. The element draws nothing until the compiled
233
234
  program is ready, and nothing after a compile error, which the page reports.
234
235
  The host contract (the frame block, the
235
236
  16 uniform slots, one binding pair per sampled texture) is written out at the
@@ -237,7 +238,7 @@ top of `web.ts`.
237
238
 
238
239
  The emitters match the HLSL emitter's semantics rather than each language's
239
240
  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
241
+ `acos` clamp, `log` and `sqrt` guard their argument, a select is a
241
242
  branchless `lerp`. `web.test.ts` checks the structure (every shape, every
242
243
  opcode, the library order); whether the output matches the goldens is
243
244
  measured in a browser, through the real element, by `Tools/sl-web-parity` in
@@ -274,7 +275,7 @@ references, and writing it out long hand three times costs exactly the same,
274
275
  because nodes are interned as they are built.
275
276
 
276
277
  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
278
+ this same IR, so it inherited the emitters, the hash and every test
278
279
  that runs on a program.
279
280
 
280
281
  ## What is checked, and when
@@ -289,11 +290,12 @@ at module load, not at draw time:
289
290
  | A program returning something other than a `vec4` | "a program must return a vec4. Wrap it: sl.vec4(value, 1)" |
290
291
  | `vec4` given the wrong number of parts | "vec4 needs 4 components, got 3" |
291
292
  | 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 |
293
+ | More than 16 uniforms or 4 textures | "this program declares 5 textures and a program may sample 4", the parser's words |
293
294
  | A value borrowed from another program | "a value from another program cannot be used in this one" |
294
295
 
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.
296
+ The caps are `UNIFORM_SLOTS` and `TEXTURE_SLOTS`: what OneJS binds to one
297
+ program. `compile` and `fromJSON` hold a program to them too, however it was
298
+ made.
297
299
 
298
300
  ## The hash is the fragile part
299
301
 
@@ -308,7 +310,11 @@ So it is a Merkle hash over the graph reachable from the result, not a walk of
308
310
  the node array. An earlier version hashed storage order, which meant hoisting a
309
311
  shared subexpression into a `const` changed the hash without changing what the
310
312
  program computed. Constants go through a fixed precision, so `0.1 + 0.2` and
311
- `0.3` do not produce different shaders. It is eight lowercase hex characters
313
+ `0.3` do not produce different shaders. It is normalised, so two ways of
314
+ writing one computation share a hash: `a + b` and `b + a` (and `*`, `dot`,
315
+ `distance`), and `8 * uv`, whose 8 is a broadcast, and `uv * 8`, whose 8 is a
316
+ float2. `min` and `max` keep their order, since which of -0 and +0 they return
317
+ depends on it. It is eight lowercase hex characters
312
318
  from FNV-1a, chosen so a C# implementation can produce the same string rather
313
319
  than for any cryptographic reason.
314
320
 
@@ -317,26 +323,25 @@ than for any cryptographic reason.
317
323
  Two numbers, with one rule: a reader accepts every version up to its own and
318
324
  refuses a newer one with a message naming both.
319
325
 
320
- - **`SL_IR_VERSION`** (`ir.ts`) is on every `Program` and in the hash, and is
321
- bumped whenever an opcode, a shape or what one computes changes. Because it
322
- is in the hash, a bump recompiles every cached shader. `toJSON` and
323
- `fromJSON` (`serial.ts`) are the IR as JSON for a host that stores programs;
324
- `fromJSON` checks everything an emitter relies on, refuses a newer version,
325
- 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
326
+ - **`SL_HASH_VERSION`** (`ir.ts`) is bumped when the hashing scheme changes,
327
+ which changes every hash. 2 is the normalised hash above. `toJSON` writes it
328
+ beside the hash, and `fromJSON` checks a file's hash only when it was written
329
+ under the same scheme and IR version, and otherwise rehashes it.
330
+
331
+ - **`SL_IR_VERSION`** (`ir.ts`) is bumped whenever an opcode, a shape or what
332
+ one computes changes. A program carries the lowest version that has all its
333
+ nodes (`programVersion`), and that is what its hash carries, so a bump
334
+ rehashes only the programs using what it added: every other program keeps
335
+ the hash its shader was recorded under. `toJSON` and `fromJSON`
336
+ (`serial.ts`) are the IR as JSON for a host that stores programs; `fromJSON`
337
+ checks everything an emitter relies on, refuses a newer version, and
338
+ migrates an older one.
339
+ IR 2 came with #129. A shape takes as many parameters as it reads
335
340
  (`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.
341
+ used to take four and lose the rest. IR 3 added `SAMPLE_LOD`, and made an
342
+ SDF's shape parameters and a noise's octave count operands, where they were
343
+ immediates, so they can be any value; a constant one still prints as its
344
+ number. `fromJSON` moves an older file's immediates to operands.
340
345
 
341
346
  ## Control flow
342
347
 
@@ -346,17 +351,14 @@ unrolls at record time because `n` is a JavaScript number.
346
351
 
347
352
  `repeat` is honest about being a macro rather than a loop. It covers fbm,
348
353
  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.
354
+ shaders loop for. A loop with a runtime count is `Specs/SL_NEXT.md` proposal
355
+ 3b. Because it unrolls, the count multiplies the body's operation count.
356
+ `compile` has no ceiling, so a long program costs what it costs, as any shader
357
+ does; `corpus/long.sl` is over a thousand operations.
356
358
 
357
359
  Every loop that reaches a GPU is therefore bounded by a constant: `repeat` is
358
- unrolled, fbm's octaves are a constant 1 to 4, the helper loops in the noise
359
- and Voronoi functions have fixed trip counts. No program can hang a GPU today,
360
+ unrolled, fbm's octaves are held to 1 to 4 even when the count is a value, the
361
+ helper loops in the noise and Voronoi functions have fixed trip counts. No program can hang a GPU today,
360
362
  so the compiled backends carry no loop cap. A data dependent loop (the
361
363
  raymarching tier) would need one emitted into every loop it prints, since a GPU
362
364
  reset takes the whole page's device, Unity's included.
@@ -364,8 +366,8 @@ reset takes the whole page's device, Unity's included.
364
366
  ## See also
365
367
 
366
368
  - `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
369
+ measurements that decided the VM's shape; the harness behind those numbers,
370
+ `Tools/shader-vm-spike/`, is in the container's history at `afd2535`
369
371
  - `onejs-unity`'s `fx`, the image pipeline this becomes a source and an operand for
370
372
 
371
373
  ## What a program is given
@@ -425,6 +427,12 @@ own for a vec4 built from raw components. Both backends implement it gamma
425
427
  aware (`sl_toLinear` in `lib/common.hlsl`), so a Gamma project gets the value
426
428
  as written. Alpha is coverage and is never converted. Same rule as `fx`.
427
429
 
430
+ A ramp's stop can also be a colour value that was written as one: a colour
431
+ uniform, or a const holding a hex. The ramp takes the value from under the
432
+ conversion that reading it applied (`writtenColour` in `ir.ts`), so it blends
433
+ as written like a hex stop. A value computed in linear light has no written
434
+ form to take, so it is refused, with lerp as the way to blend it.
435
+
428
436
  ## Noise
429
437
 
430
438
  `sl.noise`, `sl.simplex`, `sl.fbm(p, octaves, base)`, `sl.turbulence` and