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/src/lang/builtins.ts
CHANGED
|
@@ -45,13 +45,12 @@ const name = (op: number): string => {
|
|
|
45
45
|
const val = (v: Num): Val => (typeof v === "number" ? (sl.float(v) as unknown as Val) : v)
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
* Asserts an operand's width where the
|
|
48
|
+
* Asserts an operand's width where the backends would otherwise part ways.
|
|
49
49
|
*
|
|
50
|
-
* The
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* author should discover after ejecting, so the narrow ops say so here.
|
|
50
|
+
* The generated HLSL passes the real type to `sl_valueNoise(float2)` and either
|
|
51
|
+
* truncates with a warning or fails to compile, while a browser may accept it.
|
|
52
|
+
* Neither outcome is one an author should discover in a build, so the narrow
|
|
53
|
+
* ops say so here.
|
|
55
54
|
*/
|
|
56
55
|
function widthMustBe(fn: string, v: Num, want: 1 | 2 | 3 | 4, why: string): Val {
|
|
57
56
|
const got = typeof v === "number" ? 1 : v.width
|
|
@@ -146,7 +145,7 @@ function octaves(fn: string, v: Num | undefined): number {
|
|
|
146
145
|
if (v === undefined) return 3
|
|
147
146
|
if (typeof v !== "number") {
|
|
148
147
|
throw new SLError(
|
|
149
|
-
`${fn}'s octave count is
|
|
148
|
+
`${fn}'s octave count is part of the operation, so it has to be a constant. ` +
|
|
150
149
|
`A uniform or a computed value cannot change how many octaves the shader runs.`,
|
|
151
150
|
)
|
|
152
151
|
}
|
package/src/lang/check.ts
CHANGED
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
* is refused with the reason.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
import { DERIVED_INPUTS, INPUTS } from "../ir"
|
|
32
|
-
import {
|
|
31
|
+
import { DERIVED_INPUTS, INPUTS, tooManyTextures, tooManyUniforms } from "../ir"
|
|
32
|
+
import { TEXTURE_SLOTS, UNIFORM_SLOTS } from "../ops"
|
|
33
33
|
import { SL_GLSL_HINT } from "../ops"
|
|
34
34
|
import { BUILTINS, NOT_YET } from "./builtins"
|
|
35
35
|
import { SL_SDF_SHAPES } from "../shapes"
|
|
@@ -161,15 +161,9 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
161
161
|
if (!uniforms.has(u.name)) uniforms.set(u.name, u)
|
|
162
162
|
}
|
|
163
163
|
attempt(() => {
|
|
164
|
-
if (uniforms.size <=
|
|
165
|
-
const over = unit.uniforms[
|
|
166
|
-
fail(
|
|
167
|
-
`this file declares ${uniforms.size} uniforms and a program may hold ${VM_UNIFORMS}. ` +
|
|
168
|
-
`That is the size of the VM's uniform array; past it a slot reads the last one instead ` +
|
|
169
|
-
`of its own, so it is refused here rather than rendered wrong. Pack related values into ` +
|
|
170
|
-
`a float4.`,
|
|
171
|
-
over.pos, over.name.length,
|
|
172
|
-
)
|
|
164
|
+
if (uniforms.size <= UNIFORM_SLOTS) return
|
|
165
|
+
const over = unit.uniforms[UNIFORM_SLOTS]!
|
|
166
|
+
fail(tooManyUniforms(uniforms.size, "file"), over.pos, over.name.length)
|
|
173
167
|
})
|
|
174
168
|
|
|
175
169
|
for (const t of unit.textures) {
|
|
@@ -180,14 +174,9 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
180
174
|
if (!textures.has(t.name)) textures.set(t.name, t)
|
|
181
175
|
}
|
|
182
176
|
attempt(() => {
|
|
183
|
-
if (textures.size <=
|
|
184
|
-
const over = unit.textures[
|
|
185
|
-
fail(
|
|
186
|
-
`this file declares ${textures.size} textures and a program may sample ${VM_TEXTURES}. ` +
|
|
187
|
-
`The VM shader declares four samplers; a fifth would sample the fourth in the browser ` +
|
|
188
|
-
`and its own after an eject, which is two different pictures from one file.`,
|
|
189
|
-
over.pos, over.name.length,
|
|
190
|
-
)
|
|
177
|
+
if (textures.size <= TEXTURE_SLOTS) return
|
|
178
|
+
const over = unit.textures[TEXTURE_SLOTS]!
|
|
179
|
+
fail(tooManyTextures(textures.size, "file"), over.pos, over.name.length)
|
|
191
180
|
})
|
|
192
181
|
|
|
193
182
|
for (const c of unit.consts) {
|
package/src/lang/index.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* import { parse } from "onejs-sl"
|
|
9
9
|
*
|
|
10
10
|
* const plasma = parse(source, { file: "plasma.sl" })
|
|
11
|
-
* // ^ a Program:
|
|
11
|
+
* // ^ a Program: compile() it, emit HLSL from it, hash it
|
|
12
12
|
*
|
|
13
13
|
* The parser is not a second authoring surface with its own semantics. It
|
|
14
14
|
* lowers through the EDSL, so a `.sl` file and the `sl.program(...)` an author
|
package/src/lang/lower.ts
CHANGED
|
@@ -15,8 +15,7 @@
|
|
|
15
15
|
* existed. Reassignment is free; it is not a store.
|
|
16
16
|
*
|
|
17
17
|
* **`for` unrolls.** The bounds are constants, so the body is lowered once per
|
|
18
|
-
* iteration with the counter substituted as a number.
|
|
19
|
-
* span so the instruction ceiling error can name the loop that spent it.
|
|
18
|
+
* iteration with the counter substituted as a number.
|
|
20
19
|
*
|
|
21
20
|
* **`if` becomes `select`.** Both sides are lowered and every local assigned in
|
|
22
21
|
* either is selected at the join. A constant condition folds instead, which is
|
|
@@ -421,20 +420,19 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
421
420
|
count++
|
|
422
421
|
if (count > MAX_UNROLL) {
|
|
423
422
|
fail(
|
|
424
|
-
`this loop would unroll to more than ${MAX_UNROLL} iterations.
|
|
425
|
-
`
|
|
426
|
-
`the VM runs at most 256 instructions`,
|
|
423
|
+
`this loop would unroll to more than ${MAX_UNROLL} iterations. A for ` +
|
|
424
|
+
`loop unrolls at build time, so every iteration is written out in full`,
|
|
427
425
|
s.pos,
|
|
428
426
|
)
|
|
429
427
|
}
|
|
430
428
|
}
|
|
431
|
-
at(s.pos, () =>
|
|
429
|
+
at(s.pos, () => {
|
|
432
430
|
for (let i = from; s.inclusive ? i <= to : i < to; i += step) {
|
|
433
431
|
const inner = new Scope(scope)
|
|
434
432
|
inner.declare(s.counter, { width: 1, value: i })
|
|
435
433
|
exec(s.body, inner, ret)
|
|
436
434
|
}
|
|
437
|
-
})
|
|
435
|
+
})
|
|
438
436
|
break
|
|
439
437
|
}
|
|
440
438
|
default:
|
|
@@ -447,7 +445,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
447
445
|
if (typeof v !== "number") {
|
|
448
446
|
fail(
|
|
449
447
|
`a for loop unrolls at build time, so its ${what} has to be a constant. This one is ` +
|
|
450
|
-
`computed while the shader runs
|
|
448
|
+
`computed while the shader runs`,
|
|
451
449
|
e.pos,
|
|
452
450
|
)
|
|
453
451
|
}
|
|
@@ -609,7 +607,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
609
607
|
const v = lowerExpr(a, scope)
|
|
610
608
|
if (typeof v !== "number") {
|
|
611
609
|
fail(
|
|
612
|
-
`sdf.${shape}'s shape parameters
|
|
610
|
+
`sdf.${shape}'s shape parameters are part of the operation, so they have to ` +
|
|
613
611
|
`be constants. A size that changes belongs on the point: scale or offset it ` +
|
|
614
612
|
`before the call`,
|
|
615
613
|
a.pos,
|
|
@@ -730,8 +728,8 @@ const ARITH: Record<ArithOp, (a: number, b: number) => number> = {
|
|
|
730
728
|
"-": (a, b) => a - b,
|
|
731
729
|
"*": (a, b) => a * b,
|
|
732
730
|
"/": (a, b) => a / b,
|
|
733
|
-
// JavaScript's % is the truncated remainder, which is what HLSL's fmod is
|
|
734
|
-
//
|
|
731
|
+
// JavaScript's % is the truncated remainder, which is what HLSL's fmod is.
|
|
732
|
+
// Folding it in JS is the same answer.
|
|
735
733
|
"%": (a, b) => a % b,
|
|
736
734
|
}
|
|
737
735
|
|
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:
|
|
@@ -23,31 +22,22 @@
|
|
|
23
22
|
*/
|
|
24
23
|
|
|
25
24
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* reads the buffer.
|
|
25
|
+
* How many uniforms and textures one program may declare: the language's caps,
|
|
26
|
+
* for a host to read.
|
|
29
27
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
28
|
+
* They are what OneJS draws a program with. It keeps a program's uniforms in
|
|
29
|
+
* 16 slots in the editor, a native player and a browser alike
|
|
30
|
+
* (`SLProgramBridge.MaxUniforms`, the web frame's `sl_U`), and binds four
|
|
31
|
+
* textures in the editor and a native player (`_Tex0` to `_Tex3`). A program
|
|
32
|
+
* past either has something with no slot of its own, which one host would drop
|
|
33
|
+
* and another would draw, so the parser and the builder both refuse it at the
|
|
34
|
+
* declaration, the parser with a file and a line. `Specs/SL_NEXT.md` section 8
|
|
35
|
+
* proposes raising both.
|
|
34
36
|
*/
|
|
35
|
-
export const
|
|
37
|
+
export const UNIFORM_SLOTS = 16
|
|
38
|
+
export const TEXTURE_SLOTS = 4
|
|
36
39
|
|
|
37
|
-
/**
|
|
38
|
-
* What the VM shader actually declares, and therefore what a program may use.
|
|
39
|
-
*
|
|
40
|
-
* `FxProgram.shader` has `float4 _Uniforms[16]` and four `sampler2D`s, and
|
|
41
|
-
* `SLProgramBridge.cs` fills exactly those. Past either ceiling the VM does not
|
|
42
|
-
* fail, it clamps: uniform 17 reads slot 15 and texture slot 4 samples `_Tex3`,
|
|
43
|
-
* while the generated HLSL would declare both correctly. That is a silent
|
|
44
|
-
* disagreement between the backends, so the text form reports it at the
|
|
45
|
-
* declaration with a file and a line. See `Specs/SL_TEXT.md` 3.7.
|
|
46
|
-
*/
|
|
47
|
-
export const VM_UNIFORMS = 16
|
|
48
|
-
export const VM_TEXTURES = 4
|
|
49
|
-
|
|
50
|
-
/** Input ids, fixed here because the shader switches on them. */
|
|
40
|
+
/** Input ids, which the web emitter switches on. */
|
|
51
41
|
export const INPUT_ID: Record<string, number> = {
|
|
52
42
|
uv: 0, fragCoord: 1, resolution: 2, time: 3, aspect: 4,
|
|
53
43
|
}
|
|
@@ -129,10 +119,7 @@ export const SLOP = {
|
|
|
129
119
|
// made bright. imm.x carries the octave count, as FBM does.
|
|
130
120
|
TURBULENCE: 133,
|
|
131
121
|
RIDGED: 134,
|
|
132
|
-
//
|
|
133
|
-
// four immediates and id cannot hold. The encoder makes it (`forVm`); the
|
|
134
|
-
// IR and every compiled backend only ever see SDF.
|
|
135
|
-
SDF_WIDE: 135,
|
|
122
|
+
// 135 was SDF_WIDE, the VM's form of an SDF with more than four parameters.
|
|
136
123
|
|
|
137
124
|
// Sampling
|
|
138
125
|
SAMPLE: 144,
|
|
@@ -142,7 +129,7 @@ export const SLOP = {
|
|
|
142
129
|
export type SLOpCode = (typeof SLOP)[keyof typeof SLOP]
|
|
143
130
|
|
|
144
131
|
/**
|
|
145
|
-
* How many arguments each op takes, for validation
|
|
132
|
+
* How many arguments each op takes, for validation.
|
|
146
133
|
*
|
|
147
134
|
* -1 means variadic: COMPOSE takes however many parts add up to its width, RAMP
|
|
148
135
|
* takes a value plus its stops. Everything else is fixed, and a mismatch is a
|
|
@@ -170,7 +157,7 @@ export const SL_ARITY: Record<number, number> = {
|
|
|
170
157
|
[SLOP.RAMP]: -1, [SLOP.HSV2RGB]: 1, [SLOP.RGB2HSV]: 1, [SLOP.LUMINANCE]: 1, [SLOP.TO_LINEAR]: 1,
|
|
171
158
|
|
|
172
159
|
[SLOP.NOISE]: 1, [SLOP.SIMPLEX]: 1, [SLOP.FBM]: 1, [SLOP.SDF]: 1,
|
|
173
|
-
[SLOP.VORONOI]: 1, [SLOP.TURBULENCE]: 1, [SLOP.RIDGED]: 1,
|
|
160
|
+
[SLOP.VORONOI]: 1, [SLOP.TURBULENCE]: 1, [SLOP.RIDGED]: 1,
|
|
174
161
|
|
|
175
162
|
[SLOP.SAMPLE]: 1, [SLOP.SAMPLE_LOD]: 2,
|
|
176
163
|
}
|
|
@@ -180,18 +167,10 @@ export const SL_NAME: Record<number, string> = Object.fromEntries(
|
|
|
180
167
|
Object.entries(SLOP).map(([name, code]) => [code, name.toLowerCase()]),
|
|
181
168
|
)
|
|
182
169
|
|
|
183
|
-
|
|
184
|
-
export const FIRST_MATHS = 48
|
|
185
|
-
export const FIRST_GEOMETRY = 80
|
|
186
|
-
export const FIRST_INTERPOLATE = 96
|
|
187
|
-
export const FIRST_COLOUR = 112
|
|
188
|
-
export const FIRST_PROCEDURAL = 128
|
|
189
|
-
export const FIRST_SAMPLING = 144
|
|
170
|
+
const FIRST_SAMPLING = 144
|
|
190
171
|
|
|
191
172
|
/**
|
|
192
|
-
* Ops that read a texture.
|
|
193
|
-
* backend can widen (16 in a fragment shader on WebGL2), so these are counted
|
|
194
|
-
* at record time.
|
|
173
|
+
* Ops that read a texture.
|
|
195
174
|
*/
|
|
196
175
|
export function isSampling(op: number): boolean {
|
|
197
176
|
return op >= FIRST_SAMPLING
|
|
@@ -285,7 +264,6 @@ export const SL_HLSL: Record<number, SLSurface> = {
|
|
|
285
264
|
[SLOP.VORONOI]: { call: "voronoi" },
|
|
286
265
|
[SLOP.TURBULENCE]: { call: "turbulence" },
|
|
287
266
|
[SLOP.RIDGED]: { call: "ridged" },
|
|
288
|
-
[SLOP.SDF_WIDE]: { syntax: "sdf.<shape>(p, ...) given a fifth or sixth parameter" },
|
|
289
267
|
|
|
290
268
|
[SLOP.SAMPLE]: { call: "tex2D" },
|
|
291
269
|
[SLOP.SAMPLE_LOD]: { call: "tex2Dlod" },
|
|
@@ -296,16 +274,15 @@ export const SL_HLSL: Record<number, SLSurface> = {
|
|
|
296
274
|
*
|
|
297
275
|
* Numbered so the families stay in order, and refused at the surface until
|
|
298
276
|
* somebody writes both halves. A program reaching one would render as whatever
|
|
299
|
-
* the
|
|
300
|
-
*
|
|
301
|
-
* 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.
|
|
302
279
|
*
|
|
303
280
|
* `REMAP` is here because the opcode is unimplemented; the SURFACE `remap` is a
|
|
304
281
|
* macro over arithmetic and works everywhere.
|
|
305
282
|
*/
|
|
306
283
|
export const SL_UNIMPLEMENTED: Record<number, string> = {
|
|
307
|
-
[SLOP.RGB2HSV]: "
|
|
308
|
-
[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",
|
|
309
286
|
[SLOP.REMAP]: "the surface form is a macro over arithmetic, so the opcode is unused",
|
|
310
287
|
}
|
|
311
288
|
|
package/src/serial.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* older one is migrated to this one and rehashed under it.
|
|
10
10
|
*/
|
|
11
11
|
import {
|
|
12
|
-
INPUTS, SLError, SL_IR_VERSION, TYPE, controlOf, controlProblem, hashProgram,
|
|
12
|
+
INPUTS, SLError, SL_IR_VERSION, TYPE, checkCaps, controlOf, controlProblem, hashProgram,
|
|
13
13
|
type NodeRef, type Program, type SLNode, type SLType, type TextureDecl, type UniformDecl,
|
|
14
14
|
} from "./ir"
|
|
15
15
|
import { SL_ARITY, SL_NAME } from "./ops"
|
|
@@ -44,6 +44,7 @@ export function fromJSON(json: unknown): Program {
|
|
|
44
44
|
if (nodes[result!]!.type !== TYPE.VEC4) fail("its result is not a float4")
|
|
45
45
|
const uniforms = (j.uniforms ?? []).map(uniform)
|
|
46
46
|
const textures = (j.textures ?? []).map(texture)
|
|
47
|
+
checkCaps({ uniforms, textures })
|
|
47
48
|
for (const n of nodes) {
|
|
48
49
|
if (n.k === "uniform" && n.slot >= uniforms.length) fail(`a node reads uniform slot ${n.slot}, which is not declared`)
|
|
49
50
|
}
|
|
@@ -54,7 +55,7 @@ export function fromJSON(json: unknown): Program {
|
|
|
54
55
|
if (v === SL_IR_VERSION && j.hash !== undefined && j.hash !== hash) {
|
|
55
56
|
fail(`its hash ${j.hash} does not match its nodes (${hash}); it was changed after it was written`)
|
|
56
57
|
}
|
|
57
|
-
return { version: SL_IR_VERSION, nodes, result: result!, uniforms, textures, hash
|
|
58
|
+
return { version: SL_IR_VERSION, nodes, result: result!, uniforms, textures, hash }
|
|
58
59
|
}
|
|
59
60
|
|
|
60
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,
|
|
@@ -29,12 +28,12 @@ import { SLError, TYPE, type Program, type SLNode, type SLType } from "./ir"
|
|
|
29
28
|
import { libClosure, libIndex, LIB_FUNCTIONS, SDF_CALLS } from "./lib"
|
|
30
29
|
import { LIB_GLSL } from "./lib/glsl"
|
|
31
30
|
import { LIB_WGSL } from "./lib/wgsl"
|
|
32
|
-
import { INPUT_ID, SLOP,
|
|
31
|
+
import { INPUT_ID, SLOP, UNIFORM_SLOTS } from "./ops"
|
|
33
32
|
|
|
34
33
|
export type WebLanguage = "glsl" | "wgsl"
|
|
35
34
|
|
|
36
35
|
/** Uniform slots the host always provides, so every program shares one layout. */
|
|
37
|
-
export const WEB_UNIFORM_SLOTS =
|
|
36
|
+
export const WEB_UNIFORM_SLOTS = UNIFORM_SLOTS
|
|
38
37
|
|
|
39
38
|
export function emitGLSL(p: Program): string {
|
|
40
39
|
return emitWeb(p, "glsl")
|
|
@@ -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
|
|