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 +11 -0
- package/README.md +24 -41
- 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 +3 -4
- package/src/core.ts +1 -1
- package/src/hlsl.ts +11 -18
- package/src/index.ts +2 -4
- package/src/ir.ts +8 -32
- package/src/lang/README.md +10 -14
- package/src/lang/builtins.ts +6 -7
- 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 +13 -43
- package/src/serial.ts +1 -1
- package/src/sl.ts +23 -60
- package/src/web.ts +6 -7
- package/src/encode.ts +0 -376
- package/src/limits.ts +0 -50
- package/src/vm.ts +0 -11
package/src/lang/parser.ts
CHANGED
|
@@ -344,8 +344,8 @@ class Parser {
|
|
|
344
344
|
if (t.text === "for") return this.parseFor()
|
|
345
345
|
if (t.text === "while" || t.text === "do") {
|
|
346
346
|
this.fail(
|
|
347
|
-
`there is no ${t.text} loop:
|
|
348
|
-
`
|
|
347
|
+
`there is no ${t.text} loop: a loop unrolls at build time, and only a for loop ` +
|
|
348
|
+
`with constant bounds can. See a for loop`,
|
|
349
349
|
t,
|
|
350
350
|
)
|
|
351
351
|
}
|
package/src/lang/prelude.ts
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* ADDING TO IT COSTS A FUNCTION AND A TEST, NOT AN OPCODE IN THREE PLACES. That
|
|
9
9
|
* is the whole argument for having a text language with inlining functions: a
|
|
10
|
-
* library entry is source, so it lands in
|
|
11
|
-
* hash by being ordinary code, and
|
|
10
|
+
* library entry is source, so it lands in every emitter's output and in the
|
|
11
|
+
* hash by being ordinary code, and no backend learns anything new.
|
|
12
12
|
*
|
|
13
13
|
* WHY THE SOURCE IS A STRING AND NOT A FILE. `onejs-sl` ships raw TypeScript
|
|
14
14
|
* and runs inside a browser worker on play.onejs.com, where
|
package/src/ops.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The shader language opcode table.
|
|
3
3
|
*
|
|
4
|
-
* Phase 1 of `Specs/SHADER_LANG.md`.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* yet, which is exactly why the numbering is fixed now rather than later.
|
|
4
|
+
* Phase 1 of `Specs/SHADER_LANG.md`. The numbers are in every program's hash
|
|
5
|
+
* and in its JSON, so an opcode keeps its number for good, and one that goes
|
|
6
|
+
* leaves its number unused.
|
|
8
7
|
*
|
|
9
8
|
* Numbered in families with gaps, the convention `fx/ops.ts` set, so a reader
|
|
10
9
|
* can tell what an opcode is from its value:
|
|
@@ -22,18 +21,6 @@
|
|
|
22
21
|
* a dense table.
|
|
23
22
|
*/
|
|
24
23
|
|
|
25
|
-
/**
|
|
26
|
-
* The newest VM encoding this encoder writes. The VM accepts 1..its own and
|
|
27
|
-
* refuses newer, where there is a VM: a WebGL player draws compiled and never
|
|
28
|
-
* reads the buffer.
|
|
29
|
-
*
|
|
30
|
-
* A payload records the LOWEST version that can run it (`Encoded.wire`), not
|
|
31
|
-
* this: a program that uses nothing new stays 1, so a new bundle still runs on
|
|
32
|
-
* an older Play container, and only a program that needs a newer instruction
|
|
33
|
-
* is refused by an older one rather than drawn wrong.
|
|
34
|
-
*/
|
|
35
|
-
export const SL_WIRE_VERSION = 2
|
|
36
|
-
|
|
37
24
|
/**
|
|
38
25
|
* How many uniforms and textures one program may declare: the language's caps,
|
|
39
26
|
* for a host to read.
|
|
@@ -50,11 +37,7 @@ export const SL_WIRE_VERSION = 2
|
|
|
50
37
|
export const UNIFORM_SLOTS = 16
|
|
51
38
|
export const TEXTURE_SLOTS = 4
|
|
52
39
|
|
|
53
|
-
/**
|
|
54
|
-
export const VM_UNIFORMS = UNIFORM_SLOTS
|
|
55
|
-
export const VM_TEXTURES = TEXTURE_SLOTS
|
|
56
|
-
|
|
57
|
-
/** Input ids, fixed here because the shader switches on them. */
|
|
40
|
+
/** Input ids, which the web emitter switches on. */
|
|
58
41
|
export const INPUT_ID: Record<string, number> = {
|
|
59
42
|
uv: 0, fragCoord: 1, resolution: 2, time: 3, aspect: 4,
|
|
60
43
|
}
|
|
@@ -136,10 +119,7 @@ export const SLOP = {
|
|
|
136
119
|
// made bright. imm.x carries the octave count, as FBM does.
|
|
137
120
|
TURBULENCE: 133,
|
|
138
121
|
RIDGED: 134,
|
|
139
|
-
//
|
|
140
|
-
// four immediates and id cannot hold. The encoder makes it (`forVm`); the
|
|
141
|
-
// IR and every compiled backend only ever see SDF.
|
|
142
|
-
SDF_WIDE: 135,
|
|
122
|
+
// 135 was SDF_WIDE, the VM's form of an SDF with more than four parameters.
|
|
143
123
|
|
|
144
124
|
// Sampling
|
|
145
125
|
SAMPLE: 144,
|
|
@@ -149,7 +129,7 @@ export const SLOP = {
|
|
|
149
129
|
export type SLOpCode = (typeof SLOP)[keyof typeof SLOP]
|
|
150
130
|
|
|
151
131
|
/**
|
|
152
|
-
* How many arguments each op takes, for validation
|
|
132
|
+
* How many arguments each op takes, for validation.
|
|
153
133
|
*
|
|
154
134
|
* -1 means variadic: COMPOSE takes however many parts add up to its width, RAMP
|
|
155
135
|
* takes a value plus its stops. Everything else is fixed, and a mismatch is a
|
|
@@ -177,7 +157,7 @@ export const SL_ARITY: Record<number, number> = {
|
|
|
177
157
|
[SLOP.RAMP]: -1, [SLOP.HSV2RGB]: 1, [SLOP.RGB2HSV]: 1, [SLOP.LUMINANCE]: 1, [SLOP.TO_LINEAR]: 1,
|
|
178
158
|
|
|
179
159
|
[SLOP.NOISE]: 1, [SLOP.SIMPLEX]: 1, [SLOP.FBM]: 1, [SLOP.SDF]: 1,
|
|
180
|
-
[SLOP.VORONOI]: 1, [SLOP.TURBULENCE]: 1, [SLOP.RIDGED]: 1,
|
|
160
|
+
[SLOP.VORONOI]: 1, [SLOP.TURBULENCE]: 1, [SLOP.RIDGED]: 1,
|
|
181
161
|
|
|
182
162
|
[SLOP.SAMPLE]: 1, [SLOP.SAMPLE_LOD]: 2,
|
|
183
163
|
}
|
|
@@ -187,18 +167,10 @@ export const SL_NAME: Record<number, string> = Object.fromEntries(
|
|
|
187
167
|
Object.entries(SLOP).map(([name, code]) => [code, name.toLowerCase()]),
|
|
188
168
|
)
|
|
189
169
|
|
|
190
|
-
|
|
191
|
-
export const FIRST_MATHS = 48
|
|
192
|
-
export const FIRST_GEOMETRY = 80
|
|
193
|
-
export const FIRST_INTERPOLATE = 96
|
|
194
|
-
export const FIRST_COLOUR = 112
|
|
195
|
-
export const FIRST_PROCEDURAL = 128
|
|
196
|
-
export const FIRST_SAMPLING = 144
|
|
170
|
+
const FIRST_SAMPLING = 144
|
|
197
171
|
|
|
198
172
|
/**
|
|
199
|
-
* Ops that read a texture.
|
|
200
|
-
* backend can widen (16 in a fragment shader on WebGL2), so these are counted
|
|
201
|
-
* at record time.
|
|
173
|
+
* Ops that read a texture.
|
|
202
174
|
*/
|
|
203
175
|
export function isSampling(op: number): boolean {
|
|
204
176
|
return op >= FIRST_SAMPLING
|
|
@@ -292,7 +264,6 @@ export const SL_HLSL: Record<number, SLSurface> = {
|
|
|
292
264
|
[SLOP.VORONOI]: { call: "voronoi" },
|
|
293
265
|
[SLOP.TURBULENCE]: { call: "turbulence" },
|
|
294
266
|
[SLOP.RIDGED]: { call: "ridged" },
|
|
295
|
-
[SLOP.SDF_WIDE]: { syntax: "sdf.<shape>(p, ...) given a fifth or sixth parameter" },
|
|
296
267
|
|
|
297
268
|
[SLOP.SAMPLE]: { call: "tex2D" },
|
|
298
269
|
[SLOP.SAMPLE_LOD]: { call: "tex2Dlod" },
|
|
@@ -303,16 +274,15 @@ export const SL_HLSL: Record<number, SLSurface> = {
|
|
|
303
274
|
*
|
|
304
275
|
* Numbered so the families stay in order, and refused at the surface until
|
|
305
276
|
* somebody writes both halves. A program reaching one would render as whatever
|
|
306
|
-
* the
|
|
307
|
-
*
|
|
308
|
-
* the parser names them rather than treating them as unknown identifiers.
|
|
277
|
+
* nothing: the emitters have no case for them. So the parser names them rather
|
|
278
|
+
* than treating them as unknown identifiers.
|
|
309
279
|
*
|
|
310
280
|
* `REMAP` is here because the opcode is unimplemented; the SURFACE `remap` is a
|
|
311
281
|
* macro over arithmetic and works everywhere.
|
|
312
282
|
*/
|
|
313
283
|
export const SL_UNIMPLEMENTED: Record<number, string> = {
|
|
314
|
-
[SLOP.RGB2HSV]: "
|
|
315
|
-
[SLOP.SAMPLE_LOD]: "
|
|
284
|
+
[SLOP.RGB2HSV]: "no backend has a case for it yet",
|
|
285
|
+
[SLOP.SAMPLE_LOD]: "no backend samples at an explicit LOD yet; use tex2D",
|
|
316
286
|
[SLOP.REMAP]: "the surface form is a macro over arithmetic, so the opcode is unused",
|
|
317
287
|
}
|
|
318
288
|
|
package/src/serial.ts
CHANGED
|
@@ -55,7 +55,7 @@ export function fromJSON(json: unknown): Program {
|
|
|
55
55
|
if (v === SL_IR_VERSION && j.hash !== undefined && j.hash !== hash) {
|
|
56
56
|
fail(`its hash ${j.hash} does not match its nodes (${hash}); it was changed after it was written`)
|
|
57
57
|
}
|
|
58
|
-
return { version: SL_IR_VERSION, nodes, result: result!, uniforms, textures, hash
|
|
58
|
+
return { version: SL_IR_VERSION, nodes, result: result!, uniforms, textures, hash }
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function node(n: unknown, i: number): SLNode {
|
package/src/sl.ts
CHANGED
|
@@ -207,7 +207,7 @@ function align2(a: Val, o: Num): [NodeRef, NodeRef] {
|
|
|
207
207
|
* right for two operands because `bin` takes the max itself; the three and four
|
|
208
208
|
* operand ops used to take the width from one chosen operand, so
|
|
209
209
|
* `mix(0, aVec3, t)` produced a node typed float that held three components.
|
|
210
|
-
* The VM
|
|
210
|
+
* The VM, which kept every value in a float4 register, never noticed, while the
|
|
211
211
|
* HLSL emitter declared `float` and truncated, so the two backends rendered
|
|
212
212
|
* different pictures from one program. Anything component wise with more than
|
|
213
213
|
* two operands goes through here.
|
|
@@ -294,7 +294,6 @@ export function program(fn: (inputs: ProgramInputs) => Vec4): Program {
|
|
|
294
294
|
uniforms: b.uniforms.slice(),
|
|
295
295
|
textures: b.textures.slice(),
|
|
296
296
|
hash: hashProgram(nodes, out.ref, b.uniforms, b.textures),
|
|
297
|
-
loops: b.loops.slice(),
|
|
298
297
|
}
|
|
299
298
|
} finally {
|
|
300
299
|
current = null
|
|
@@ -312,23 +311,14 @@ function compose(width: SLType, parts: Num[]): Val {
|
|
|
312
311
|
// Wide parts are SPLIT into their components here, so COMPOSE only ever
|
|
313
312
|
// sees scalars.
|
|
314
313
|
//
|
|
315
|
-
// The VM
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
// that has no room for them, and would put the complexity in the half that
|
|
320
|
-
// is hardest to test. Splitting at record time costs a few extra swizzle
|
|
321
|
-
// nodes, which the register allocator reclaims immediately, and leaves the
|
|
322
|
-
// VM's COMPOSE trivially correct.
|
|
314
|
+
// The VM (removed in 0.3.0) took the x of each operand, which dropped
|
|
315
|
+
// everything after the first component: vec4(uv, 0, 1) rendered
|
|
316
|
+
// (uv.x, 0, 1, 0). Splitting at record time costs a few extra swizzle
|
|
317
|
+
// nodes and keeps every emitter's COMPOSE trivial.
|
|
323
318
|
// A constructor of literals is ONE constant, not a COMPOSE of four.
|
|
324
319
|
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
// all live. The IR has always been able to say it in one node: a const
|
|
328
|
-
// carries up to four components. A two stop `ramp` used five of the eight
|
|
329
|
-
// registers on nothing but its own stop colours, which put the language's
|
|
330
|
-
// headline colour tool within one or two registers of unusable in any
|
|
331
|
-
// program that also did something.
|
|
320
|
+
// Every part as its own CONST, joined by a COMPOSE, is five nodes where the
|
|
321
|
+
// IR can say it in one: a const carries up to four components.
|
|
332
322
|
if (parts.length === width && parts.every((p) => typeof p === "number")) {
|
|
333
323
|
return mk(b, b.constant(parts as number[]), width)
|
|
334
324
|
}
|
|
@@ -425,9 +415,8 @@ export function hsv2rgb(c: Vec3): Vec3 {
|
|
|
425
415
|
/**
|
|
426
416
|
* Cross and reflect, both vec3 only.
|
|
427
417
|
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
* other widths here is what keeps the two honest.
|
|
418
|
+
* Refusing the other widths here keeps every backend drawing the same thing,
|
|
419
|
+
* rather than each deciding what a vec2 or vec4 cross product means.
|
|
431
420
|
*/
|
|
432
421
|
export function cross(a: Vec3, b: Vec3): Vec3 {
|
|
433
422
|
if (a.width !== TYPE.VEC3 || b.width !== TYPE.VEC3) {
|
|
@@ -452,7 +441,7 @@ export function reflect(incident: Vec3, normal: Vec3): Vec3 {
|
|
|
452
441
|
*
|
|
453
442
|
* A MACRO, like `ramp`, and for the same reason: it expands into arithmetic
|
|
454
443
|
* both backends already have, so it needs no opcode, no second implementation
|
|
455
|
-
* in the
|
|
444
|
+
* in the emitters. `SLOP.REMAP` stays a reserved number.
|
|
456
445
|
*
|
|
457
446
|
* Unclamped on purpose, which is what HLSL authors expect of the one liner they
|
|
458
447
|
* would otherwise write; wrap it in `saturate` when the ends matter.
|
|
@@ -495,11 +484,10 @@ export function select(cond: Num, whenTrue: Num, whenFalse: Num): Val {
|
|
|
495
484
|
const t = typeof whenTrue === "number" ? float(whenTrue) : whenTrue
|
|
496
485
|
const f = typeof whenFalse === "number" ? float(whenFalse) : whenFalse
|
|
497
486
|
// The condition is widened with the branches, not left at its own width.
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
// failure this design cannot tolerate.
|
|
487
|
+
// The VM (removed in 0.3.0) held a scalar condition as (c, 0, 0, 0) and
|
|
488
|
+
// answered 0 for components y, z and w, while HLSL broadcasts a scalar
|
|
489
|
+
// itself; the two disagreed on every vector valued select. Widening here
|
|
490
|
+
// keeps every emitter's SELECT the same simple form.
|
|
503
491
|
const { refs, width } = alignN([c, t, f])
|
|
504
492
|
return mk(b, b.call(SLOP.SELECT, width, refs), width)
|
|
505
493
|
}
|
|
@@ -507,8 +495,8 @@ export function select(cond: Num, whenTrue: Num, whenFalse: Num): Val {
|
|
|
507
495
|
/**
|
|
508
496
|
* The interpolant is broadcast to the operand width before it crosses.
|
|
509
497
|
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
498
|
+
* The VM (removed in 0.3.0) held a scalar `t` as (t, 0, 0, 0) and its lerp
|
|
499
|
+
* ran per component against those zeros:
|
|
512
500
|
* a black to white ramp at its midpoint rendered (0.5, 0, 0, 1) instead of
|
|
513
501
|
* grey. Broadcasting here rather than in the shader keeps `t` free to be a
|
|
514
502
|
* genuine per component vector when an author wants one.
|
|
@@ -597,13 +585,9 @@ export function parseColor(hex: string): [number, number, number, number] {
|
|
|
597
585
|
export function ramp(t: Num, stops: Array<string | [number, number, number, number]>): Vec4 {
|
|
598
586
|
if (stops.length < 2) throw new SLError(`a ramp needs at least 2 stops, got ${stops.length}`)
|
|
599
587
|
const tv = (typeof t === "number" ? float(t) : t).saturate()
|
|
600
|
-
// Each stop is built
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
// register allocator walks the array: built up front, every stop stayed
|
|
604
|
-
// live until the last mix, and a four stop ramp reserved four of the VM's
|
|
605
|
-
// eight registers before the program did anything. That put the language's
|
|
606
|
-
// headline colour tool a couple of registers from unusable.
|
|
588
|
+
// Each stop is built immediately before the mix that consumes it. The graph
|
|
589
|
+
// and the hash are the same either way (the hash is over the shape, not the
|
|
590
|
+
// storage order); the order only mattered to the VM's register allocator.
|
|
607
591
|
const colourAt = (i: number) => {
|
|
608
592
|
const v = typeof stops[i] === "string" ? parseColor(stops[i] as string) : (stops[i] as number[])
|
|
609
593
|
return vec4(v[0], v[1], v[2], v[3])
|
|
@@ -733,34 +717,13 @@ export function texture(name: string): Texture {
|
|
|
733
717
|
*
|
|
734
718
|
* Honest about what it is: a macro, not a loop. It covers fbm, layered noise and
|
|
735
719
|
* small iterated distance fields, which is most of what 2D shaders loop for. A
|
|
736
|
-
*
|
|
737
|
-
* bounded loop with a dynamic trip count while codegen would handle it fine, and
|
|
738
|
-
* the two backends agreeing is the property this whole design protects.
|
|
720
|
+
* loop with a runtime count is `Specs/SL_NEXT.md` proposal 3b.
|
|
739
721
|
*/
|
|
740
722
|
export function repeat<T extends Val>(n: number, body: (i: number, acc: T) => T, seed: T): T {
|
|
741
723
|
if (!Number.isInteger(n) || n < 0 || n > 64) {
|
|
742
724
|
throw new SLError(`repeat count must be a whole number from 0 to 64, got ${n}`)
|
|
743
725
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
return acc
|
|
748
|
-
})
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
/**
|
|
752
|
-
* Records the nodes a body produced as one unrolled span.
|
|
753
|
-
*
|
|
754
|
-
* Diagnostic only: the instruction ceiling error uses the span to say which
|
|
755
|
-
* loop spent the budget, so the fix reads as "lower this count" rather than
|
|
756
|
-
* "fewer instructions". `repeat` is the EDSL's form of it and the text
|
|
757
|
-
* language's `for` is the other, and neither should be reaching into the
|
|
758
|
-
* Builder to push a span itself.
|
|
759
|
-
*/
|
|
760
|
-
export function unrolled<T>(count: number, body: () => T): T {
|
|
761
|
-
const b = ctx()
|
|
762
|
-
const start = b.nodes.length
|
|
763
|
-
const out = body()
|
|
764
|
-
b.loops.push({ count, start, end: b.nodes.length })
|
|
765
|
-
return out
|
|
726
|
+
let acc = seed
|
|
727
|
+
for (let i = 0; i < n; i++) acc = body(i, acc)
|
|
728
|
+
return acc
|
|
766
729
|
}
|
package/src/web.ts
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
* The web backends: prints a program as GLSL ES 3.00 or WGSL, for a browser to
|
|
3
3
|
* compile at runtime on Unity's own graphics device.
|
|
4
4
|
*
|
|
5
|
-
* Unity cannot compile a shader in a built game,
|
|
6
|
-
* A browser can, and the Play container
|
|
7
|
-
* so a program becomes WGSL on one and
|
|
8
|
-
*
|
|
9
|
-
* 600 times cheaper per pixel. The host that compiles and draws these lives in
|
|
5
|
+
* Unity cannot compile a shader in a built game, so a native build ships the
|
|
6
|
+
* shaders the editor generated. A browser can compile, and the Play container
|
|
7
|
+
* runs on WebGPU with WebGL2 behind it, so a program becomes WGSL on one and
|
|
8
|
+
* GLSL ES on the other. The host that compiles and draws these lives in
|
|
10
9
|
* OneJS (`Plugins/WebGL/OneJSSLWeb.jslib`); this file only prints them.
|
|
11
10
|
*
|
|
12
11
|
* Structurally the HLSL emitter again: one local per reachable node, in order,
|
|
@@ -159,7 +158,7 @@ function emitWeb(p: Program, lang: WebLanguage): string {
|
|
|
159
158
|
case SLOP.MIX: return `mix(${s[0]}, ${s[1]}, ${s[2]})`
|
|
160
159
|
case SLOP.STEP: return `step(${s[0]}, ${s[1]})`
|
|
161
160
|
case SLOP.SMOOTHSTEP: smoothsteps.add(t); return `sl_smoothstep${t}(${s[0]}, ${s[1]}, ${s[2]})`
|
|
162
|
-
// As the
|
|
161
|
+
// As the HLSL emitter does it: branchless, so every
|
|
163
162
|
// backend agrees on the edge value.
|
|
164
163
|
case SLOP.SELECT: {
|
|
165
164
|
const c = typeOf(n.args[0])
|
|
@@ -182,7 +181,7 @@ function emitWeb(p: Program, lang: WebLanguage): string {
|
|
|
182
181
|
default:
|
|
183
182
|
throw new SLError(
|
|
184
183
|
`the ${lang.toUpperCase()} emitter has no case for opcode ${n.op}. A program using it would ` +
|
|
185
|
-
`differ between
|
|
184
|
+
`differ between a browser and a Unity build, which is the one failure this design cannot tolerate.`,
|
|
186
185
|
)
|
|
187
186
|
}
|
|
188
187
|
|