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/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.4.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
|
*
|
|
@@ -51,6 +51,14 @@ export interface BodyTarget {
|
|
|
51
51
|
* as it is, and a host that composites premultiplied multiplies on write.
|
|
52
52
|
*/
|
|
53
53
|
sample: (slot: number, uv: string) => string
|
|
54
|
+
/**
|
|
55
|
+
* A float4 sample of the texture in `slot` at `uv` from mip level `lod`, a
|
|
56
|
+
* float expression: 0 is the full size texture, 1 half, and a fraction
|
|
57
|
+
* blends two levels where the texture's filter does. Otherwise the same
|
|
58
|
+
* contract as `sample`. `tex2Dlod` in a program; HLSL's
|
|
59
|
+
* `tex2Dlod(s, float4(uv, 0, lod))` and Metal's `sample(s, uv, level(lod))`.
|
|
60
|
+
*/
|
|
61
|
+
sampleLevel: (slot: number, uv: string, lod: string) => string
|
|
54
62
|
/**
|
|
55
63
|
* `linear`: `toLinear` calls `sl_toLinear`, for a host whose target holds
|
|
56
64
|
* linear light. `gamma`: `toLinear` is the identity, decided here, so hex
|
|
@@ -100,6 +108,11 @@ export function emitBody(p: Program, target: BodyTarget): Body {
|
|
|
100
108
|
const helpers = new Set<string>()
|
|
101
109
|
const helper = (fn: string) => { helpers.add(fn); return fn }
|
|
102
110
|
/** A node at width `w`: a scalar repeated into a constructor, since HLSL refuses float3(x) and Metal x.xxx. */
|
|
111
|
+
/** A constant node's value, or null for anything computed. */
|
|
112
|
+
const constantOf = (ref: number): number[] | null => {
|
|
113
|
+
const c = p.nodes[ref]!
|
|
114
|
+
return c.k === "const" ? c.v : null
|
|
115
|
+
}
|
|
103
116
|
const splat = (ref: number, w: SLType) => (p.nodes[ref]!.type === TYPE.FLOAT && w > 1 ? ctor(w, Array(w).fill(name(ref))) : name(ref))
|
|
104
117
|
|
|
105
118
|
const expr = (n: SLNode): string => {
|
|
@@ -143,7 +156,7 @@ export function emitBody(p: Program, target: BodyTarget): Body {
|
|
|
143
156
|
case SLOP.MUL: return `(${a[0]} * ${a[1]})`
|
|
144
157
|
case SLOP.DIV: return `(${a[0]} / ${a[1]})`
|
|
145
158
|
case SLOP.MOD: return `fmod(${s[0]}, ${s[1]})`
|
|
146
|
-
// abs on the base, matching the
|
|
159
|
+
// abs on the base, matching the web emitters. pow of a negative base is
|
|
147
160
|
// undefined in HLSL and the backends must be undefined in the same
|
|
148
161
|
// direction.
|
|
149
162
|
case SLOP.POW: return `pow(abs(${s[0]}), ${s[1]})`
|
|
@@ -170,8 +183,8 @@ export function emitBody(p: Program, target: BodyTarget): Body {
|
|
|
170
183
|
case SLOP.CLAMP: return `clamp(${s[0]}, ${s[1]}, ${s[2]})`
|
|
171
184
|
case SLOP.SATURATE: return `saturate(${a[0]})`
|
|
172
185
|
|
|
173
|
-
// Of a scalar, Metal's are ambiguous, so
|
|
174
|
-
//
|
|
186
|
+
// Of a scalar, Metal's are ambiguous, so the scalar case is written
|
|
187
|
+
// out.
|
|
175
188
|
case SLOP.LENGTH: return w0 === 1 ? `abs(${a[0]})` : `length(${a[0]})`
|
|
176
189
|
case SLOP.DISTANCE: return w0 === 1 ? `abs(${a[0]} - ${a[1]})` : `distance(${a[0]}, ${a[1]})`
|
|
177
190
|
case SLOP.DOT: return w0 === 1 ? `(${a[0]} * ${a[1]})` : `dot(${a[0]}, ${a[1]})`
|
|
@@ -184,7 +197,7 @@ export function emitBody(p: Program, target: BodyTarget): Body {
|
|
|
184
197
|
case SLOP.MIX: return `lerp(${s[0]}, ${s[1]}, ${s[2]})`
|
|
185
198
|
case SLOP.STEP: return `step(${s[0]}, ${s[1]})`
|
|
186
199
|
case SLOP.SMOOTHSTEP: return `smoothstep(${s[0]}, ${s[1]}, ${s[2]})`
|
|
187
|
-
//
|
|
200
|
+
// Branchless, as the web emitters do it, rather than using an if.
|
|
188
201
|
// Two backends that pick differently here disagree on every edge
|
|
189
202
|
// value.
|
|
190
203
|
case SLOP.SELECT: return `lerp(${s[2]}, ${s[1]}, step(${k(0.5, t)}, ${s[0]}))`
|
|
@@ -192,14 +205,19 @@ export function emitBody(p: Program, target: BodyTarget): Body {
|
|
|
192
205
|
case SLOP.HSV2RGB: return `${helper("sl_hsv2rgb")}(${a[0]})`
|
|
193
206
|
case SLOP.NOISE: return `${helper("sl_valueNoise")}(${a[0]})`
|
|
194
207
|
case SLOP.SIMPLEX: return `${helper("sl_simplex")}(${a[0]})`
|
|
195
|
-
case SLOP.FBM:
|
|
196
|
-
case SLOP.TURBULENCE:
|
|
197
|
-
case SLOP.RIDGED:
|
|
208
|
+
case SLOP.FBM:
|
|
209
|
+
case SLOP.TURBULENCE:
|
|
210
|
+
case SLOP.RIDGED: {
|
|
211
|
+
// The count rounded here and held to 1 to 4 by sl_fbm; a constant
|
|
212
|
+
// one is the whole number it is. fbm's kind is the one immediate.
|
|
213
|
+
const c = constantOf(n.args[1]!)
|
|
214
|
+
const count = c !== null ? String(Math.round(c[0]!)) : `int(floor(${a[1]} + 0.5))`
|
|
215
|
+
const kind = n.op === SLOP.TURBULENCE ? 2 : n.op === SLOP.RIDGED ? 3 : Math.round(imm[0] ?? 0)
|
|
216
|
+
return `${helper("sl_fbm")}(${a[0]}, ${count}, ${kind})`
|
|
217
|
+
}
|
|
198
218
|
case SLOP.SDF: {
|
|
199
219
|
const id = Math.round(imm[0] ?? 0)
|
|
200
|
-
|
|
201
|
-
const r = [imm[5] ?? 0, imm[6] ?? 0].map(lit)
|
|
202
|
-
return `${helper("sl_sdfDistance")}(${id}, ${a[0]}, float4(${q.join(", ")}), float2(${r.join(", ")}))`
|
|
220
|
+
return `${helper("sl_sdfDistance")}(${id}, ${a[0]}, ${a[1]}, ${a[2]})`
|
|
203
221
|
}
|
|
204
222
|
case SLOP.VORONOI: return `${helper("sl_voronoi")}(${a[0]})`
|
|
205
223
|
case SLOP.SAMPLE: {
|
|
@@ -207,11 +225,16 @@ export function emitBody(p: Program, target: BodyTarget): Body {
|
|
|
207
225
|
textures.add(slot)
|
|
208
226
|
return target.sample(slot, a[0]!)
|
|
209
227
|
}
|
|
228
|
+
case SLOP.SAMPLE_LOD: {
|
|
229
|
+
const slot = Math.round(imm[0] ?? 0)
|
|
230
|
+
textures.add(slot)
|
|
231
|
+
return target.sampleLevel(slot, a[0]!, a[1]!)
|
|
232
|
+
}
|
|
210
233
|
|
|
211
234
|
default:
|
|
212
235
|
throw new SLError(
|
|
213
236
|
`the HLSL emitter has no case for opcode ${n.op}. A program using it would silently ` +
|
|
214
|
-
`differ between
|
|
237
|
+
`differ between a Unity build and a browser, which is the one failure this design ` +
|
|
215
238
|
`cannot tolerate.`,
|
|
216
239
|
)
|
|
217
240
|
}
|
package/src/compile.ts
CHANGED
|
@@ -6,10 +6,9 @@
|
|
|
6
6
|
* editor to generate a shader from, WGSL and GLSL ES 3.00 for a browser to
|
|
7
7
|
* compile on Unity's own device.
|
|
8
8
|
*
|
|
9
|
-
* There is no budget
|
|
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
14
|
import { checkCaps, type Program } from "./ir"
|
package/src/core.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
export * as sl from "./sl"
|
|
11
11
|
export type { Float, Vec2, Vec3, Vec4, Num, ProgramInputs, Texture } from "./sl"
|
|
12
12
|
export {
|
|
13
|
-
TYPE, INPUTS,
|
|
13
|
+
TYPE, INPUTS, MAX_NODES, SLError, hashProgram, widthName, SL_IR_VERSION, SL_HASH_VERSION, inputsUsed,
|
|
14
14
|
CONTROL_FIELDS, controlProblem,
|
|
15
15
|
} from "./ir"
|
|
16
16
|
export type {
|
package/src/hlsl.ts
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
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
|
*
|
|
@@ -22,7 +15,7 @@
|
|
|
22
15
|
* next time it is generated.
|
|
23
16
|
*/
|
|
24
17
|
|
|
25
|
-
import { SLError, type Program } from "./ir"
|
|
18
|
+
import { SLError, SL_HASH_VERSION, type Program } from "./ir"
|
|
26
19
|
import { emitBody, lit, type BodyTarget } from "./body"
|
|
27
20
|
|
|
28
21
|
/** The property name a uniform gets. Prefixed so it cannot collide with ours. */
|
|
@@ -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}`
|
|
@@ -81,7 +74,9 @@ export function emitShader(p: Program, options: EmitOptions = {}): string {
|
|
|
81
74
|
//
|
|
82
75
|
// Source of truth is the program this was emitted from; edits here are lost the
|
|
83
76
|
// next time it is generated. The name carries the program hash, which is how the
|
|
84
|
-
// runtime pairs the two. See Specs/SHADER_LANG.md section 6.
|
|
77
|
+
// runtime pairs the two. See Specs/SHADER_LANG.md section 6. The scheme that
|
|
78
|
+
// hash was made under, which a build compares with the apps' manifests:
|
|
79
|
+
// SL_HASH_VERSION ${SL_HASH_VERSION}
|
|
85
80
|
Shader "${name}"
|
|
86
81
|
{
|
|
87
82
|
Properties
|
|
@@ -99,7 +94,7 @@ ${props.join("\n")}
|
|
|
99
94
|
#pragma fragment frag
|
|
100
95
|
#pragma target 3.0
|
|
101
96
|
#include "UnityCG.cginc"
|
|
102
|
-
// Shared with the
|
|
97
|
+
// Shared with the web frame, so every backend computes the same noise.
|
|
103
98
|
#include "${include}"
|
|
104
99
|
|
|
105
100
|
${decls.join("\n")}
|
|
@@ -111,8 +106,8 @@ ${decls.join("\n")}
|
|
|
111
106
|
{
|
|
112
107
|
v2f o;
|
|
113
108
|
o.pos = UnityObjectToClipPos(v.vertex);
|
|
114
|
-
// Origin corrected here
|
|
115
|
-
//
|
|
109
|
+
// Origin corrected here, so a Unity build is not upside down
|
|
110
|
+
// relative to the same program in a browser.
|
|
116
111
|
o.uv = float2(v.uv.x, lerp(v.uv.y, 1.0 - v.uv.y, _FlipY));
|
|
117
112
|
return o;
|
|
118
113
|
}
|
|
@@ -130,7 +125,7 @@ ${body}
|
|
|
130
125
|
|
|
131
126
|
/**
|
|
132
127
|
* The frame's side of the body: OneJS's names for the inputs, a uniform's
|
|
133
|
-
* material property, and `tex2D` on the slot's sampler. `sl_toLinear` stays a
|
|
128
|
+
* material property, and `tex2D` and `tex2Dlod` on the slot's sampler. `sl_toLinear` stays a
|
|
134
129
|
* call because `SLCommon.cginc` decides Gamma or Linear per project, at the
|
|
135
130
|
* shader compile, so this target always emits it.
|
|
136
131
|
*/
|
|
@@ -155,6 +150,7 @@ function unityTarget(p: Program): BodyTarget {
|
|
|
155
150
|
},
|
|
156
151
|
uniform: (slot) => uniformProperty(p.uniforms[slot]!.name),
|
|
157
152
|
sample: (slot, uv) => `tex2D(_Tex${slot}, ${uv})`,
|
|
153
|
+
sampleLevel: (slot, uv, lod) => `tex2Dlod(_Tex${slot}, float4(${uv}, 0.0, ${lod}))`,
|
|
158
154
|
colour: "linear",
|
|
159
155
|
indent: " ",
|
|
160
156
|
}
|
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,8 +20,7 @@
|
|
|
21
20
|
* pass. A tree would silently square the cost of the most natural way to write a
|
|
22
21
|
* shader.
|
|
23
22
|
*
|
|
24
|
-
* **Nothing here knows about shaders.** No HLSL, no texture layout, no
|
|
25
|
-
* allocation. Those belong to the backends; this file is the contract between
|
|
23
|
+
* **Nothing here knows about shaders.** No HLSL, no texture layout, no binding. Those belong to the backends; this file is the contract between
|
|
26
24
|
* them, and it is fully testable with no GPU.
|
|
27
25
|
*/
|
|
28
26
|
|
|
@@ -47,7 +45,7 @@ export type InputName = keyof typeof INPUTS
|
|
|
47
45
|
* handed over by the host (`Specs/SL_NEXT.md` 6). Each read records the same
|
|
48
46
|
* few nodes a program would if it wrote the expression out, and the builder's
|
|
49
47
|
* hash consing makes every later read the same node. So there is no new
|
|
50
|
-
*
|
|
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,20 +170,11 @@ 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
|
+
/**
|
|
175
|
+
* The `SL_IR_VERSION` this program's nodes mean what they mean under: the
|
|
176
|
+
* lowest one that has every node it holds. See `programVersion`.
|
|
177
|
+
*/
|
|
189
178
|
version: number
|
|
190
179
|
nodes: SLNode[]
|
|
191
180
|
/** Must be VEC4: a program produces a colour. */
|
|
@@ -194,16 +183,8 @@ export interface Program {
|
|
|
194
183
|
textures: TextureDecl[]
|
|
195
184
|
/** Canonical, stable across machines. See `hashProgram`. */
|
|
196
185
|
hash: string
|
|
197
|
-
/** Not part of the hash: it changes nothing about what the program computes. */
|
|
198
|
-
loops: LoopSpan[]
|
|
199
186
|
}
|
|
200
187
|
|
|
201
|
-
/**
|
|
202
|
-
* @deprecated Use `TEXTURE_SLOTS`. Was 15, the WebGL2 sampler count, which let a
|
|
203
|
-
* program declare textures no native host binds.
|
|
204
|
-
*/
|
|
205
|
-
export const MAX_TEXTURES = TEXTURE_SLOTS
|
|
206
|
-
|
|
207
188
|
/**
|
|
208
189
|
* The refusals for a program past its caps, worded once for the parser (a
|
|
209
190
|
* `file`) and the builder (a `program`) so the two cannot drift apart.
|
|
@@ -232,7 +213,25 @@ export function checkCaps(p: Pick<Program, "uniforms" | "textures">): void {
|
|
|
232
213
|
if (p.textures.length > TEXTURE_SLOTS) throw new SLError(tooManyTextures(p.textures.length, "program"))
|
|
233
214
|
}
|
|
234
215
|
|
|
235
|
-
/**
|
|
216
|
+
/**
|
|
217
|
+
* The colour as written behind a colour value: the operand of the `TO_LINEAR`
|
|
218
|
+
* that reading a hex or a colour uniform applied, or null for a value computed
|
|
219
|
+
* in the working space. What a ramp's stop has to be, since a ramp blends its
|
|
220
|
+
* stops as written (`sl.ramp`).
|
|
221
|
+
*/
|
|
222
|
+
export function writtenColour(nodes: readonly SLNode[], ref: NodeRef): NodeRef | null {
|
|
223
|
+
const n = nodes[ref]!
|
|
224
|
+
if (n.k !== "call" || n.op !== SLOP.TO_LINEAR || (n.type !== TYPE.VEC3 && n.type !== TYPE.VEC4)) return null
|
|
225
|
+
return n.args[0]!
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Why a computed value is not a ramp's stop, in one wording for both surfaces. */
|
|
229
|
+
export const RAMP_STOP_COMPUTED =
|
|
230
|
+
"a ramp's stop is a colour as written: a hex like #ff8040, or a uniform or const that holds one. " +
|
|
231
|
+
"This one is computed, and a ramp blends its stops as written before it converts them, so it has " +
|
|
232
|
+
"no written colour to blend. Blend computed colours with lerp instead"
|
|
233
|
+
|
|
234
|
+
/** Operations a single program may hold. Generous: a guard against a runaway builder, not a budget. */
|
|
236
235
|
export const MAX_NODES = 4096
|
|
237
236
|
|
|
238
237
|
export class SLError extends Error {
|
|
@@ -253,7 +252,6 @@ export class Builder {
|
|
|
253
252
|
readonly nodes: SLNode[] = []
|
|
254
253
|
readonly uniforms: UniformDecl[] = []
|
|
255
254
|
readonly textures: TextureDecl[] = []
|
|
256
|
-
readonly loops: LoopSpan[] = []
|
|
257
255
|
private readonly interned = new Map<string, NodeRef>()
|
|
258
256
|
|
|
259
257
|
add(node: SLNode): NodeRef {
|
|
@@ -401,8 +399,7 @@ export function widthName(t: SLType): string {
|
|
|
401
399
|
*
|
|
402
400
|
* This is the link between a program and its compiled shader. If it differs
|
|
403
401
|
* between the machine that generated the shader and the machine that runs it,
|
|
404
|
-
*
|
|
405
|
-
* worst failure this design can have: correct output, quietly slow, no error.
|
|
402
|
+
* a native player finds no shader for the program and draws nothing.
|
|
406
403
|
*
|
|
407
404
|
* So it hashes the canonicalised node array and nothing else. Never object
|
|
408
405
|
* identity, never insertion order of a Map, never a JSON stringify whose key
|
|
@@ -426,6 +423,13 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
|
|
|
426
423
|
// digests, so two graphs that compute the same thing agree however they were
|
|
427
424
|
// assembled. It also ignores nodes not reachable from the result, which is
|
|
428
425
|
// correct: dead nodes generate no shader code.
|
|
426
|
+
//
|
|
427
|
+
// NORMALISED, so two ways of writing one computation agree (hash version 2).
|
|
428
|
+
// A commutative op's operands are digested in sorted order, so `a + b` is
|
|
429
|
+
// `b + a`; and a swizzle of a constant is digested as the constant it picks,
|
|
430
|
+
// so `8 * uv` (8 broadcast by a swizzle) is `uv * 8` (a float2 constant).
|
|
431
|
+
// Only where every backend computes the same bits either way: min and max
|
|
432
|
+
// are left out, since which of -0 and +0 they return depends on the order.
|
|
429
433
|
const digest = new Map<NodeRef, string>()
|
|
430
434
|
|
|
431
435
|
const of = (ref: NodeRef): string => {
|
|
@@ -441,8 +445,19 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
|
|
|
441
445
|
// produce different shaders and must not share a hash. The names go
|
|
442
446
|
// in separately below.
|
|
443
447
|
case "uniform": body = `u:${n.slot}:${n.type}`; break
|
|
444
|
-
case "swizzle":
|
|
445
|
-
|
|
448
|
+
case "swizzle": {
|
|
449
|
+
const src = nodes[n.src]!
|
|
450
|
+
body = src.k === "const"
|
|
451
|
+
? `c:${n.type}:${n.chans.map((c) => fixed(src.v[c]!)).join(",")}`
|
|
452
|
+
: `s:${of(n.src)}:${n.chans.join("")}`
|
|
453
|
+
break
|
|
454
|
+
}
|
|
455
|
+
case "call": {
|
|
456
|
+
const args = n.args.map(of)
|
|
457
|
+
if (COMMUTATIVE.has(n.op)) args.sort()
|
|
458
|
+
body = `f:${n.op}:${args.join(",")}:${(n.imm ?? []).map(fixed).join(",")}`
|
|
459
|
+
break
|
|
460
|
+
}
|
|
446
461
|
}
|
|
447
462
|
const d = fnv1a(body + "|" + n.type)
|
|
448
463
|
digest.set(ref, d)
|
|
@@ -450,12 +465,15 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
|
|
|
450
465
|
}
|
|
451
466
|
|
|
452
467
|
// The IR version is in the hash, so a change to what an opcode means in the
|
|
453
|
-
// IR changes
|
|
454
|
-
// serving the old maths.
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
// library
|
|
458
|
-
|
|
468
|
+
// IR changes the hash of every program using it, and every cache keyed by
|
|
469
|
+
// one recompiles rather than serving the old maths. It is the PROGRAM's
|
|
470
|
+
// version, the lowest that has its nodes, so a version that adds a node
|
|
471
|
+
// leaves every program without one on the hash it had. A change to the
|
|
472
|
+
// helper library alone (a noise's arithmetic, say) is not one: every host
|
|
473
|
+
// includes the library's text when it compiles, and none caches a compiled
|
|
474
|
+
// program by hash across a library update, so the new text reaches every
|
|
475
|
+
// shader without a bump.
|
|
476
|
+
const parts: string[] = [`v${SL_HASH_VERSION}:${programVersion(nodes)}`, of(result)]
|
|
459
477
|
for (const u of uniforms) parts.push(`U:${u.name}:${u.type}:${u.value.map(fixed).join(",")}`)
|
|
460
478
|
for (const t of textures) parts.push(`T:${t.name}:${t.slot}`)
|
|
461
479
|
return fnv1a(parts.join("|"))
|
|
@@ -473,8 +491,79 @@ export function controlOf(c: UniformControl): UniformControl {
|
|
|
473
491
|
return out
|
|
474
492
|
}
|
|
475
493
|
|
|
476
|
-
/**
|
|
477
|
-
|
|
494
|
+
/**
|
|
495
|
+
* The lowest `SL_IR_VERSION` that has every node here, and never below 2.
|
|
496
|
+
*
|
|
497
|
+
* A version that adds a node, or a form of one, raises only the programs that
|
|
498
|
+
* hold it. Everything else keeps the version, the hash and the JSON it had, so
|
|
499
|
+
* a program recorded before the bump still finds its shader, and a reader that
|
|
500
|
+
* predates the bump still reads it. Every node counts, used or not: the JSON
|
|
501
|
+
* carries them all, and an older reader has to refuse by the version rather
|
|
502
|
+
* than halfway through the nodes.
|
|
503
|
+
*/
|
|
504
|
+
export function programVersion(nodes: readonly SLNode[]): number {
|
|
505
|
+
let v = 2
|
|
506
|
+
for (const n of nodes) v = Math.max(v, nodeVersion(n))
|
|
507
|
+
return v
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* The version that added this node's form. An sdf or octave call with one
|
|
512
|
+
* operand is the form before IR 3, which only a file an older compiler wrote
|
|
513
|
+
* holds, and `fromJSON` moves it to the form IR 3 has.
|
|
514
|
+
*/
|
|
515
|
+
function nodeVersion(n: SLNode): number {
|
|
516
|
+
if (n.k !== "call") return 2
|
|
517
|
+
switch (n.op) {
|
|
518
|
+
case SLOP.SAMPLE_LOD: return 3
|
|
519
|
+
case SLOP.SDF:
|
|
520
|
+
case SLOP.FBM:
|
|
521
|
+
case SLOP.TURBULENCE:
|
|
522
|
+
case SLOP.RIDGED:
|
|
523
|
+
return n.args.length > 1 ? 3 : 2
|
|
524
|
+
default: return 2
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* What is wrong with a call's form, for the ops whose operand list alone does
|
|
530
|
+
* not say it, or null. The builder only makes the right ones; this is for a
|
|
531
|
+
* program read from JSON, which an emitter would otherwise print as something
|
|
532
|
+
* else.
|
|
533
|
+
*
|
|
534
|
+
* SDF the point, a float4 and a float2 holding the six shape parameters,
|
|
535
|
+
* the shape id the one immediate
|
|
536
|
+
* FBM, TURBULENCE, RIDGED the point and the octave count, FBM's kind the
|
|
537
|
+
* one immediate
|
|
538
|
+
*/
|
|
539
|
+
export function formProblem(n: Extract<SLNode, { k: "call" }>, nodes: readonly SLNode[]): string | null {
|
|
540
|
+
const widths = n.args.map((a) => nodes[a]!.type).join(",")
|
|
541
|
+
switch (n.op) {
|
|
542
|
+
case SLOP.SDF:
|
|
543
|
+
if (widths === "2,4,2" && n.imm?.length === 1) return null
|
|
544
|
+
return "an sdf takes a float2, a float4 and a float2, and its shape as the one immediate"
|
|
545
|
+
case SLOP.FBM:
|
|
546
|
+
case SLOP.TURBULENCE:
|
|
547
|
+
case SLOP.RIDGED: {
|
|
548
|
+
const rest = n.op === SLOP.FBM ? 1 : 0
|
|
549
|
+
if (widths === "2,1" && (n.imm?.length ?? 0) === rest) return null
|
|
550
|
+
return `${SL_NAME[n.op]} takes a float2 and its octave count, a float`
|
|
551
|
+
}
|
|
552
|
+
default:
|
|
553
|
+
return null
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** The ops whose two operands the hash takes in either order: exact in IEEE arithmetic on every backend. */
|
|
558
|
+
const COMMUTATIVE = new Set<number>([SLOP.ADD, SLOP.MUL, SLOP.DOT, SLOP.DISTANCE])
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Bumped when the hashing scheme changes, which invalidates generated shaders.
|
|
562
|
+
*
|
|
563
|
+
* 1 the Merkle hash over the reachable graph
|
|
564
|
+
* 2 normalised: commutative operands in either order, a swizzle of a constant as the constant
|
|
565
|
+
*/
|
|
566
|
+
export const SL_HASH_VERSION = 2
|
|
478
567
|
|
|
479
568
|
/**
|
|
480
569
|
* Bumped whenever the IR changes in a way a reader has to know about: a new
|
|
@@ -491,13 +580,16 @@ export const SL_HASH_VERSION = 1
|
|
|
491
580
|
* records it again, for no gain.
|
|
492
581
|
*
|
|
493
582
|
* A reader accepts every version up to its own and refuses a newer one with a
|
|
494
|
-
* message naming both, the rule the particle wire and fx follow.
|
|
495
|
-
*
|
|
583
|
+
* message naming both, the rule the particle wire and fx follow. A program's
|
|
584
|
+
* version is the lowest that has its nodes (`programVersion`), and that is what
|
|
585
|
+
* its hash carries, so a bump rehashes only the programs using what it added.
|
|
496
586
|
*
|
|
497
587
|
* 1 the first versioned IR
|
|
498
588
|
* 2 an SDF call carries up to six shape parameters, not four (#129)
|
|
589
|
+
* 3 SAMPLE_LOD; an SDF's shape parameters and a noise's octave count are
|
|
590
|
+
* operands, where they were immediates
|
|
499
591
|
*/
|
|
500
|
-
export const SL_IR_VERSION =
|
|
592
|
+
export const SL_IR_VERSION = 3
|
|
501
593
|
|
|
502
594
|
function fnv1a(s: string): string {
|
|
503
595
|
let h = 0x811c9dc5
|
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
|
|
|
@@ -152,22 +152,18 @@ one the local does not have, is refused.
|
|
|
152
152
|
|
|
153
153
|
## Not yet spellable
|
|
154
154
|
|
|
155
|
-
|
|
155
|
+
Two things the EDSL or the opcode table has and a file cannot say. Each is a
|
|
156
156
|
recorded gap, not an oversight, and each has its own error message rather than
|
|
157
157
|
"unknown identifier".
|
|
158
158
|
|
|
159
159
|
| | Why |
|
|
160
160
|
|---|---|
|
|
161
|
-
| `rgb2hsv
|
|
161
|
+
| `rgb2hsv` | The opcode is numbered and **no backend implements it**; every emitter throws on it. |
|
|
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
|
|