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 +26 -0
- package/README.md +61 -53
- package/goldens/goldens.json +98 -63
- package/lib/common.hlsl +5 -6
- package/package.json +2 -4
- package/src/body.ts +35 -12
- package/src/compile.ts +3 -4
- package/src/core.ts +1 -1
- package/src/hlsl.ts +17 -21
- package/src/index.ts +2 -4
- package/src/ir.ts +138 -46
- package/src/lang/README.md +11 -15
- package/src/lang/builtins.ts +10 -23
- package/src/lang/check.ts +24 -11
- package/src/lang/glsl.ts +1 -2
- package/src/lang/index.ts +1 -1
- package/src/lang/lower.ts +31 -37
- package/src/lang/parser.ts +2 -2
- package/src/lang/prelude.ts +2 -2
- package/src/ops.ts +15 -44
- package/src/params.ts +2 -0
- package/src/serial.ts +66 -14
- package/src/sl.ts +113 -84
- package/src/web.ts +50 -18
- package/src/encode.ts +0 -376
- package/src/limits.ts +0 -50
- package/src/vm.ts +0 -11
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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,
|
|
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,
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
215
|
-
|
|
216
|
-
|
|
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`.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
296
|
-
|
|
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
|
|
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
|
-
- **`
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
`
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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.
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
|
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
|
-
|
|
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
|