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 +28 -0
- package/README.md +25 -42
- package/goldens/goldens.json +1 -1
- package/lib/common.hlsl +5 -6
- package/package.json +2 -4
- package/src/body.ts +6 -6
- package/src/compile.ts +5 -5
- package/src/core.ts +2 -2
- package/src/hlsl.ts +11 -18
- package/src/index.ts +2 -4
- package/src/ir.ts +37 -37
- package/src/lang/README.md +10 -14
- package/src/lang/builtins.ts +6 -7
- package/src/lang/check.ts +8 -19
- package/src/lang/index.ts +1 -1
- package/src/lang/lower.ts +9 -11
- package/src/lang/parser.ts +2 -2
- package/src/lang/prelude.ts +2 -2
- package/src/ops.ts +25 -48
- package/src/serial.ts +3 -2
- package/src/sl.ts +23 -60
- package/src/web.ts +8 -9
- 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,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
|
|
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
|
|
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,
|
|
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,
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
215
|
-
|
|
216
|
-
|
|
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`.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
296
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
package/goldens/goldens.json
CHANGED
package/lib/common.hlsl
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Helpers shared by EVERY shader language backend.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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.
|
|
138
|
-
//
|
|
139
|
-
//
|
|
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.
|
|
4
|
-
"description": "The OneJS shader language: a small typed language for per pixel programs, compiled to a portable IR and emitted as HLSL, WGSL
|
|
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
|
|
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
|
|
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
|
|
174
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
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
|
|
52
|
-
*
|
|
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
|
|
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
|
|
115
|
-
//
|
|
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 (
|
|
6
|
-
*
|
|
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,
|
|
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.
|
|
5
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
203
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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 >=
|
|
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
|
-
*
|
|
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
|
package/src/lang/README.md
CHANGED
|
@@ -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
|
|
32
|
-
|
|
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
|
|
36
|
-
|
|
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
|
|
53
|
-
object of
|
|
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 **
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|