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/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
|
|
@@ -126,12 +125,10 @@ value(SLOP.LUMINANCE, 1, 1, (a) => sl.luminance(colourOperand("luminance", a[0]!
|
|
|
126
125
|
value(SLOP.NOISE, 1, 1, (a) => sl.noise(point("noise", a[0]!) as never))
|
|
127
126
|
value(SLOP.SIMPLEX, 1, 1, (a) => sl.simplex(point("simplex", a[0]!) as never))
|
|
128
127
|
value(SLOP.VORONOI, 1, 1, (a) => sl.voronoi(point("voronoi", a[0]!) as never))
|
|
129
|
-
// The octave count is
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
value(SLOP.
|
|
133
|
-
value(SLOP.TURBULENCE, 1, 2, (a) => sl.turbulence(point("turbulence", a[0]!) as never, octaves("turbulence", a[1])))
|
|
134
|
-
value(SLOP.RIDGED, 1, 2, (a) => sl.ridged(point("ridged", a[0]!) as never, octaves("ridged", a[1])))
|
|
128
|
+
// The octave count is 3 when left out, and may be a value (`sl.fbm` says how).
|
|
129
|
+
value(SLOP.FBM, 1, 2, (a) => sl.fbm(point("fbm", a[0]!) as never, a[1] ?? 3))
|
|
130
|
+
value(SLOP.TURBULENCE, 1, 2, (a) => sl.turbulence(point("turbulence", a[0]!) as never, a[1] ?? 3))
|
|
131
|
+
value(SLOP.RIDGED, 1, 2, (a) => sl.ridged(point("ridged", a[0]!) as never, a[1] ?? 3))
|
|
135
132
|
|
|
136
133
|
/** Every field is sampled at a float2. */
|
|
137
134
|
function point(fn: string, v: Num): Val {
|
|
@@ -139,20 +136,10 @@ function point(fn: string, v: Num): Val {
|
|
|
139
136
|
}
|
|
140
137
|
|
|
141
138
|
special(SLOP.SAMPLE, 2, 2)
|
|
139
|
+
special(SLOP.SAMPLE_LOD, 3, 3)
|
|
142
140
|
special(SLOP.SDF, 1, 7)
|
|
143
141
|
special(SLOP.RAMP, 3, 64)
|
|
144
142
|
|
|
145
|
-
function octaves(fn: string, v: Num | undefined): number {
|
|
146
|
-
if (v === undefined) return 3
|
|
147
|
-
if (typeof v !== "number") {
|
|
148
|
-
throw new SLError(
|
|
149
|
-
`${fn}'s octave count is baked into the instruction, so it has to be a constant. ` +
|
|
150
|
-
`A uniform or a computed value cannot change how many octaves the shader runs.`,
|
|
151
|
-
)
|
|
152
|
-
}
|
|
153
|
-
return v
|
|
154
|
-
}
|
|
155
|
-
|
|
156
143
|
export const BUILTINS: Readonly<Record<string, Builtin>> = table
|
|
157
144
|
|
|
158
145
|
/**
|
package/src/lang/check.ts
CHANGED
|
@@ -49,11 +49,15 @@ const DERIVED_NAMES = new Set(Object.keys(DERIVED_INPUTS))
|
|
|
49
49
|
* The GLSL spellings whose HLSL name means the same wherever it is written, so
|
|
50
50
|
* the hint can be a one click fix (`Specs/SL_NEXT.md` 5, Decision 1 A). The
|
|
51
51
|
* rest of `SL_GLSL_HINT` gets the hint alone: `mod` floors where `%` truncates,
|
|
52
|
-
* `ivec2` truncates where `float2` does not, `atan` is `atan2` only with two
|
|
53
|
-
* arguments (handled at the call)
|
|
54
|
-
* implemented yet.
|
|
52
|
+
* `ivec2` truncates where `float2` does not, and `atan` is `atan2` only with two
|
|
53
|
+
* arguments (handled at the call).
|
|
55
54
|
*/
|
|
56
|
-
const GLSL_RENAMES = new Set([
|
|
55
|
+
const GLSL_RENAMES = new Set([
|
|
56
|
+
"mix", "fract", "texture", "textureLod", "vec2", "vec3", "vec4", "gl_FragCoord", "iTime", "iResolution",
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
/** The builtins whose first argument is a texture, which is not a value anywhere else. */
|
|
60
|
+
const SAMPLES = new Set(["tex2D", "tex2Dlod"])
|
|
57
61
|
|
|
58
62
|
const rename = (from: string, to: string): SLFix => ({ title: `Replace ${from} with ${to}`, replacement: to })
|
|
59
63
|
|
|
@@ -427,7 +431,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
427
431
|
if (textures.has(e.name)) {
|
|
428
432
|
fail(
|
|
429
433
|
`"${e.name}" is a texture, and a texture is only ever the first argument of ` +
|
|
430
|
-
`tex2D. Write \`tex2D(${e.name}, uv)\``,
|
|
434
|
+
`tex2D or tex2Dlod. Write \`tex2D(${e.name}, uv)\``,
|
|
431
435
|
e.pos, e.name.length,
|
|
432
436
|
)
|
|
433
437
|
}
|
|
@@ -458,9 +462,9 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
458
462
|
// Only reached while collecting: the call was refused, and its
|
|
459
463
|
// arguments are separate questions, so a mistake inside one is
|
|
460
464
|
// not hidden by the call's own. `fract(uv * wrap)` is two
|
|
461
|
-
// mistakes.
|
|
462
|
-
//
|
|
463
|
-
const tex = e.callee.k === "ident" && e.callee.name
|
|
465
|
+
// mistakes. A sample's first argument is the texture, which is
|
|
466
|
+
// not a value.
|
|
467
|
+
const tex = e.callee.k === "ident" && SAMPLES.has(e.callee.name)
|
|
464
468
|
for (const a of tex ? e.args.slice(1) : e.args) attempt(() => checkExpr(fn, a, scope))
|
|
465
469
|
return
|
|
466
470
|
}
|
|
@@ -529,13 +533,22 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
529
533
|
|
|
530
534
|
const builtin = BUILTINS[n]
|
|
531
535
|
if (builtin !== undefined) {
|
|
532
|
-
if (n
|
|
536
|
+
if (SAMPLES.has(n)) {
|
|
533
537
|
const [tex, ...rest] = e.args
|
|
538
|
+
// HLSL's own tex2Dlod packs the level into a float4 with the uv.
|
|
539
|
+
if (n === "tex2Dlod" && e.args.length === 2) {
|
|
540
|
+
fail(
|
|
541
|
+
"tex2Dlod takes the uv and the mip level separately, `tex2Dlod(t, uv, lod)`, " +
|
|
542
|
+
"rather than HLSL's float4(uv, 0, lod)",
|
|
543
|
+
e.pos,
|
|
544
|
+
)
|
|
545
|
+
}
|
|
534
546
|
arity(e, n, builtin.min, builtin.max)
|
|
535
547
|
if (tex === undefined || tex.k !== "ident" || !textures.has(tex.name)) {
|
|
548
|
+
const lod = n === "tex2Dlod" ? ", 0" : ""
|
|
536
549
|
fail(
|
|
537
|
-
|
|
538
|
-
`then
|
|
550
|
+
`${n} samples a texture declared in this file, as in \`texture2D grain;\` ` +
|
|
551
|
+
`then \`${n}(grain, uv${lod})\``,
|
|
539
552
|
(tex ?? e).pos,
|
|
540
553
|
)
|
|
541
554
|
}
|
package/src/lang/glsl.ts
CHANGED
|
@@ -75,7 +75,7 @@ const GLSL_TYPES = new Set(["float", "int", "uint", "bool", "vec2", "vec3", "vec
|
|
|
75
75
|
/** A straight rename, the meaning unchanged. */
|
|
76
76
|
const RENAME: Record<string, string> = {
|
|
77
77
|
vec2: "float2", vec3: "float3", vec4: "float4", mix: "lerp", fract: "frac",
|
|
78
|
-
texture: "tex2D", texture2D: "tex2D", iTime: "time", iGlobalTime: "time", gl_FragCoord: "fragCoord",
|
|
78
|
+
texture: "tex2D", texture2D: "tex2D", textureLod: "tex2Dlod", iTime: "time", iGlobalTime: "time", gl_FragCoord: "fragCoord",
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/** A rename that changes what the value can hold, so it is said. */
|
|
@@ -101,7 +101,6 @@ const MISSING: Record<string, string> = {
|
|
|
101
101
|
iChannelTime: "a texture has no time of its own",
|
|
102
102
|
iChannelResolution: "a texture's size cannot be read yet",
|
|
103
103
|
iSampleRate: "there is no audio",
|
|
104
|
-
textureLod: "tex2Dlod is not implemented yet; tex2D samples with the texture's own filtering",
|
|
105
104
|
texelFetch: "a texture cannot be read without filtering yet",
|
|
106
105
|
textureSize: "a texture's size cannot be read yet",
|
|
107
106
|
dFdx: "there are no derivatives", dFdy: "there are no derivatives", fwidth: "there are no derivatives",
|
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
|
|
@@ -33,7 +32,9 @@
|
|
|
33
32
|
* than an optimisation.
|
|
34
33
|
*/
|
|
35
34
|
|
|
36
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
controlProblem, DERIVED_INPUTS, INPUTS, RAMP_STOP_COMPUTED, writtenColour, type Program, type SLType, type UniformControl,
|
|
37
|
+
} from "../ir"
|
|
37
38
|
import type { SlSdfKind } from "../shapes"
|
|
38
39
|
import * as sl from "../sl"
|
|
39
40
|
import type { Num, Val } from "../sl"
|
|
@@ -421,20 +422,19 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
421
422
|
count++
|
|
422
423
|
if (count > MAX_UNROLL) {
|
|
423
424
|
fail(
|
|
424
|
-
`this loop would unroll to more than ${MAX_UNROLL} iterations.
|
|
425
|
-
`
|
|
426
|
-
`the VM runs at most 256 instructions`,
|
|
425
|
+
`this loop would unroll to more than ${MAX_UNROLL} iterations. A for ` +
|
|
426
|
+
`loop unrolls at build time, so every iteration is written out in full`,
|
|
427
427
|
s.pos,
|
|
428
428
|
)
|
|
429
429
|
}
|
|
430
430
|
}
|
|
431
|
-
at(s.pos, () =>
|
|
431
|
+
at(s.pos, () => {
|
|
432
432
|
for (let i = from; s.inclusive ? i <= to : i < to; i += step) {
|
|
433
433
|
const inner = new Scope(scope)
|
|
434
434
|
inner.declare(s.counter, { width: 1, value: i })
|
|
435
435
|
exec(s.body, inner, ret)
|
|
436
436
|
}
|
|
437
|
-
})
|
|
437
|
+
})
|
|
438
438
|
break
|
|
439
439
|
}
|
|
440
440
|
default:
|
|
@@ -447,7 +447,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
447
447
|
if (typeof v !== "number") {
|
|
448
448
|
fail(
|
|
449
449
|
`a for loop unrolls at build time, so its ${what} has to be a constant. This one is ` +
|
|
450
|
-
`computed while the shader runs
|
|
450
|
+
`computed while the shader runs`,
|
|
451
451
|
e.pos,
|
|
452
452
|
)
|
|
453
453
|
}
|
|
@@ -604,19 +604,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
604
604
|
if (typeof p === "number" || p.width !== 2) {
|
|
605
605
|
fail(`sdf.${shape} measures the distance to a point, so it takes a float2`, e.args[0]!.pos)
|
|
606
606
|
}
|
|
607
|
-
const params
|
|
608
|
-
for (const a of e.args.slice(1)) {
|
|
609
|
-
const v = lowerExpr(a, scope)
|
|
610
|
-
if (typeof v !== "number") {
|
|
611
|
-
fail(
|
|
612
|
-
`sdf.${shape}'s shape parameters ride inside the instruction, so they have to ` +
|
|
613
|
-
`be constants. A size that changes belongs on the point: scale or offset it ` +
|
|
614
|
-
`before the call`,
|
|
615
|
-
a.pos,
|
|
616
|
-
)
|
|
617
|
-
}
|
|
618
|
-
params.push(v)
|
|
619
|
-
}
|
|
607
|
+
const params = e.args.slice(1).map((a) => lowerExpr(a, scope))
|
|
620
608
|
return at(e.pos, () => sl.sdf(shape, p as never, params))
|
|
621
609
|
}
|
|
622
610
|
|
|
@@ -626,29 +614,35 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
626
614
|
return construct(n as TypeName, e.args.map((a) => lowerExpr(a, scope)), e.pos)
|
|
627
615
|
}
|
|
628
616
|
|
|
629
|
-
if (n === "tex2D") {
|
|
617
|
+
if (n === "tex2D" || n === "tex2Dlod") {
|
|
630
618
|
const texName = (e.args[0] as Extract<Expr, { k: "ident" }>).name
|
|
631
619
|
const tex = samplers.get(texName)!
|
|
632
620
|
const uv = lowerExpr(e.args[1]!, scope)
|
|
633
621
|
if (typeof uv === "number" || uv.width !== 2) {
|
|
634
|
-
fail(
|
|
622
|
+
fail(`${n} samples at a float2`, e.args[1]!.pos)
|
|
635
623
|
}
|
|
636
|
-
return at(e.pos, () => tex.sample(uv as never) as unknown as Val)
|
|
624
|
+
if (n === "tex2D") return at(e.pos, () => tex.sample(uv as never) as unknown as Val)
|
|
625
|
+
const lod = lowerExpr(e.args[2]!, scope)
|
|
626
|
+
if (typeof lod !== "number" && lod.width !== 1) {
|
|
627
|
+
fail(`a mip level is one number, and this is a ${widthType(lod.width)}`, e.args[2]!.pos)
|
|
628
|
+
}
|
|
629
|
+
return at(e.pos, () => tex.sampleLevel(uv as never, lod) as unknown as Val)
|
|
637
630
|
}
|
|
638
631
|
|
|
639
632
|
if (n === "ramp") {
|
|
640
633
|
const t = lowerExpr(e.args[0]!, scope)
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
)
|
|
634
|
+
// A hex stays the string it was, so a ramp of hexes is the program
|
|
635
|
+
// it always was. Anything else is a value, which has to be a colour
|
|
636
|
+
// as written (`sl.ramp` says why), a colour uniform say.
|
|
637
|
+
const stops = e.args.slice(1).map((a) => {
|
|
638
|
+
if (a.k === "hex") return a.hex
|
|
639
|
+
const v = lowerExpr(a, scope)
|
|
640
|
+
if (typeof v === "number" || v.width < 3) {
|
|
641
|
+
fail(`a ramp's stop is a colour, a float3 or a float4, and this is a ${typeof v === "number" ? "float" : widthType(v.width)}`, a.pos)
|
|
649
642
|
}
|
|
650
|
-
|
|
651
|
-
|
|
643
|
+
if (writtenColour(v.owner.nodes, v.ref) === null) fail(RAMP_STOP_COMPUTED, a.pos)
|
|
644
|
+
return v as never
|
|
645
|
+
})
|
|
652
646
|
return at(e.pos, () => sl.ramp(t, stops) as unknown as Val)
|
|
653
647
|
}
|
|
654
648
|
|
|
@@ -730,8 +724,8 @@ const ARITH: Record<ArithOp, (a: number, b: number) => number> = {
|
|
|
730
724
|
"-": (a, b) => a - b,
|
|
731
725
|
"*": (a, b) => a * b,
|
|
732
726
|
"/": (a, b) => a / b,
|
|
733
|
-
// JavaScript's % is the truncated remainder, which is what HLSL's fmod is
|
|
734
|
-
//
|
|
727
|
+
// JavaScript's % is the truncated remainder, which is what HLSL's fmod is.
|
|
728
|
+
// Folding it in JS is the same answer.
|
|
735
729
|
"%": (a, b) => a % b,
|
|
736
730
|
}
|
|
737
731
|
|
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
|
|
@@ -176,8 +156,10 @@ export const SL_ARITY: Record<number, number> = {
|
|
|
176
156
|
|
|
177
157
|
[SLOP.RAMP]: -1, [SLOP.HSV2RGB]: 1, [SLOP.RGB2HSV]: 1, [SLOP.LUMINANCE]: 1, [SLOP.TO_LINEAR]: 1,
|
|
178
158
|
|
|
179
|
-
|
|
180
|
-
|
|
159
|
+
// An SDF takes the point, or the point and its parameters as operands; an
|
|
160
|
+
// octave op the point, or the point and its count (`formProblem` in ir.ts).
|
|
161
|
+
[SLOP.NOISE]: 1, [SLOP.SIMPLEX]: 1, [SLOP.FBM]: -1, [SLOP.SDF]: -1,
|
|
162
|
+
[SLOP.VORONOI]: 1, [SLOP.TURBULENCE]: -1, [SLOP.RIDGED]: -1,
|
|
181
163
|
|
|
182
164
|
[SLOP.SAMPLE]: 1, [SLOP.SAMPLE_LOD]: 2,
|
|
183
165
|
}
|
|
@@ -187,18 +169,10 @@ export const SL_NAME: Record<number, string> = Object.fromEntries(
|
|
|
187
169
|
Object.entries(SLOP).map(([name, code]) => [code, name.toLowerCase()]),
|
|
188
170
|
)
|
|
189
171
|
|
|
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
|
|
172
|
+
const FIRST_SAMPLING = 144
|
|
197
173
|
|
|
198
174
|
/**
|
|
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.
|
|
175
|
+
* Ops that read a texture.
|
|
202
176
|
*/
|
|
203
177
|
export function isSampling(op: number): boolean {
|
|
204
178
|
return op >= FIRST_SAMPLING
|
|
@@ -292,7 +266,6 @@ export const SL_HLSL: Record<number, SLSurface> = {
|
|
|
292
266
|
[SLOP.VORONOI]: { call: "voronoi" },
|
|
293
267
|
[SLOP.TURBULENCE]: { call: "turbulence" },
|
|
294
268
|
[SLOP.RIDGED]: { call: "ridged" },
|
|
295
|
-
[SLOP.SDF_WIDE]: { syntax: "sdf.<shape>(p, ...) given a fifth or sixth parameter" },
|
|
296
269
|
|
|
297
270
|
[SLOP.SAMPLE]: { call: "tex2D" },
|
|
298
271
|
[SLOP.SAMPLE_LOD]: { call: "tex2Dlod" },
|
|
@@ -303,16 +276,14 @@ export const SL_HLSL: Record<number, SLSurface> = {
|
|
|
303
276
|
*
|
|
304
277
|
* Numbered so the families stay in order, and refused at the surface until
|
|
305
278
|
* 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.
|
|
279
|
+
* nothing: the emitters have no case for them. So the parser names them rather
|
|
280
|
+
* than treating them as unknown identifiers.
|
|
309
281
|
*
|
|
310
282
|
* `REMAP` is here because the opcode is unimplemented; the SURFACE `remap` is a
|
|
311
283
|
* macro over arithmetic and works everywhere.
|
|
312
284
|
*/
|
|
313
285
|
export const SL_UNIMPLEMENTED: Record<number, string> = {
|
|
314
|
-
[SLOP.RGB2HSV]: "
|
|
315
|
-
[SLOP.SAMPLE_LOD]: "the VM samples without an explicit LOD; use tex2D",
|
|
286
|
+
[SLOP.RGB2HSV]: "no backend has a case for it yet",
|
|
316
287
|
[SLOP.REMAP]: "the surface form is a macro over arithmetic, so the opcode is unused",
|
|
317
288
|
}
|
|
318
289
|
|
package/src/params.ts
CHANGED
|
@@ -54,6 +54,7 @@ const INTRINSIC: Record<string, readonly string[]> = {
|
|
|
54
54
|
distance: ["x", "y"], dot: ["x", "y"], cross: ["x", "y"], reflect: ["i", "n"],
|
|
55
55
|
lerp: ["x", "y", "s"], step: ["y", "x"], smoothstep: ["min", "max", "x"],
|
|
56
56
|
tex2D: ["s", "t"],
|
|
57
|
+
tex2Dlod: ["s", "t", "lod"],
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
/**
|
|
@@ -161,6 +162,7 @@ export const BUILTIN_DOCS: Readonly<Record<string, string>> = {
|
|
|
161
162
|
turbulence: "Simplex creases layered in octaves, 1 to 4: fire, smoke, marble.",
|
|
162
163
|
ridged: "Turbulence made bright at its creases: ridges, lightning, cracks.",
|
|
163
164
|
tex2D: "The colour of texture s at the uv t, with straight alpha.",
|
|
165
|
+
tex2Dlod: "The colour of texture s at the uv t from mip level lod: 0 is the full size, 1 half, and so on.",
|
|
164
166
|
sdf: "The signed distance from p to a shape, negative inside: `sdf.circle(p, r)`.",
|
|
165
167
|
ramp: "t mapped through evenly spaced colour stops, from the first at 0 to the last at 1.",
|
|
166
168
|
}
|
package/src/serial.ts
CHANGED
|
@@ -9,14 +9,16 @@
|
|
|
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, checkCaps, controlOf, controlProblem, hashProgram,
|
|
13
|
-
type NodeRef, type Program, type SLNode, type SLType, type TextureDecl, type UniformDecl,
|
|
12
|
+
INPUTS, SLError, SL_HASH_VERSION, SL_IR_VERSION, TYPE, checkCaps, controlOf, controlProblem, formProblem, hashProgram,
|
|
13
|
+
programVersion, type NodeRef, type Program, type SLNode, type SLType, type TextureDecl, type UniformDecl,
|
|
14
14
|
} from "./ir"
|
|
15
|
-
import { SL_ARITY, SL_NAME } from "./ops"
|
|
15
|
+
import { SLOP, SL_ARITY, SL_NAME } from "./ops"
|
|
16
16
|
|
|
17
17
|
export interface ProgramJSON {
|
|
18
18
|
/** `SL_IR_VERSION` of the writer. Absent means 1. */
|
|
19
19
|
v: number
|
|
20
|
+
/** `SL_HASH_VERSION` of the writer, which `hash` was computed under. Absent means 1. */
|
|
21
|
+
h?: number
|
|
20
22
|
nodes: SLNode[]
|
|
21
23
|
result: NodeRef
|
|
22
24
|
uniforms: UniformDecl[]
|
|
@@ -25,7 +27,10 @@ export interface ProgramJSON {
|
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export function toJSON(p: Program): ProgramJSON {
|
|
28
|
-
return {
|
|
30
|
+
return {
|
|
31
|
+
v: p.version, h: SL_HASH_VERSION, nodes: p.nodes, result: p.result, uniforms: p.uniforms, textures: p.textures,
|
|
32
|
+
hash: p.hash,
|
|
33
|
+
}
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
export function fromJSON(json: unknown): Program {
|
|
@@ -38,24 +43,71 @@ export function fromJSON(json: unknown): Program {
|
|
|
38
43
|
`Update onejs-sl to read it, or rebuild it from its .sl source.`)
|
|
39
44
|
}
|
|
40
45
|
if (!Array.isArray(j.nodes) || j.nodes.length === 0) fail("it has no nodes")
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
const written = j.nodes.map((n, i) => node(n, i))
|
|
47
|
+
if (!Number.isInteger(j.result) || j.result! < 0 || j.result! >= written.length) {
|
|
48
|
+
fail(`its result ${String(j.result)} is not a node`)
|
|
49
|
+
}
|
|
50
|
+
// The lowest version that has these nodes, which is what the writer
|
|
51
|
+
// recorded unless the file predates version 2 (whose nodes version 2 reads
|
|
52
|
+
// as they are) or was edited. One that claims less than its nodes need
|
|
53
|
+
// holds something its version never had.
|
|
54
|
+
const needs = programVersion(written)
|
|
55
|
+
if (v < needs && v >= 2) fail(`it says IR version ${v} and holds nodes version ${needs} added; it was changed after it was written`)
|
|
56
|
+
const { nodes, result } = v < 3 ? toOperands(written, j.result!) : { nodes: written, result: j.result! }
|
|
57
|
+
nodes.forEach((n, i) => {
|
|
58
|
+
const problem = n.k === "call" ? formProblem(n, nodes) : null
|
|
59
|
+
if (problem !== null) fail(`node ${i} is not a form its op has: ${problem}`)
|
|
60
|
+
})
|
|
61
|
+
if (nodes[result]!.type !== TYPE.VEC4) fail("its result is not a float4")
|
|
45
62
|
const uniforms = (j.uniforms ?? []).map(uniform)
|
|
46
63
|
const textures = (j.textures ?? []).map(texture)
|
|
47
64
|
checkCaps({ uniforms, textures })
|
|
48
65
|
for (const n of nodes) {
|
|
49
66
|
if (n.k === "uniform" && n.slot >= uniforms.length) fail(`a node reads uniform slot ${n.slot}, which is not declared`)
|
|
50
67
|
}
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
68
|
+
const version = programVersion(nodes)
|
|
69
|
+
const hash = hashProgram(nodes, result, uniforms, textures)
|
|
70
|
+
// The same IR version and hash scheme, so the same hash; a different one
|
|
71
|
+
// means the file was edited or damaged, and its cached shader belongs to
|
|
72
|
+
// something else. A file an older version or scheme wrote is rehashed under
|
|
73
|
+
// this one on purpose.
|
|
74
|
+
if (v === version && (j.h ?? 1) === SL_HASH_VERSION && j.hash !== undefined && j.hash !== hash) {
|
|
56
75
|
fail(`its hash ${j.hash} does not match its nodes (${hash}); it was changed after it was written`)
|
|
57
76
|
}
|
|
58
|
-
return { version
|
|
77
|
+
return { version, nodes, result, uniforms, textures, hash }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A graph written before IR 3, with the forms it had then moved to the one each
|
|
82
|
+
* op has now: an sdf's shape parameters after its id and an octave op's count
|
|
83
|
+
* were immediates, and are operands. Each constant goes in just before the
|
|
84
|
+
* call that reads it, and every later reference moves along.
|
|
85
|
+
*/
|
|
86
|
+
function toOperands(written: readonly SLNode[], writtenResult: NodeRef): { nodes: SLNode[]; result: NodeRef } {
|
|
87
|
+
const nodes: SLNode[] = []
|
|
88
|
+
const at: NodeRef[] = []
|
|
89
|
+
const constant = (type: SLType, v: number[]): NodeRef => nodes.push({ k: "const", type, v }) - 1
|
|
90
|
+
written.forEach((n, i) => {
|
|
91
|
+
const imm = n.k === "call" ? n.imm ?? [] : []
|
|
92
|
+
if (n.k === "call" && n.op === SLOP.SDF && n.args.length === 1) {
|
|
93
|
+
if (imm.length < 5) fail(`node ${i} is an sdf with ${imm.length} immediates; one written before IR 3 has at least 5`)
|
|
94
|
+
const q = constant(TYPE.VEC4, [imm[1]!, imm[2]!, imm[3]!, imm[4]!])
|
|
95
|
+
const r = constant(TYPE.VEC2, [imm[5] ?? 0, imm[6] ?? 0])
|
|
96
|
+
at[i] = nodes.push({ ...n, args: [at[n.args[0]!]!, q, r], imm: [imm[0]!] }) - 1
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
const octaves = n.k === "call" && (n.op === SLOP.FBM || n.op === SLOP.TURBULENCE || n.op === SLOP.RIDGED)
|
|
100
|
+
if (octaves && n.args.length === 1) {
|
|
101
|
+
if (imm.length < 1) fail(`node ${i} is ${SL_NAME[n.op]} with no octave count`)
|
|
102
|
+
const count = constant(TYPE.FLOAT, [imm[0]!])
|
|
103
|
+
at[i] = nodes.push({ ...n, args: [at[n.args[0]!]!, count], imm: imm.slice(1) }) - 1
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
const moved: SLNode = n.k === "call" ? { ...n, args: n.args.map((a) => at[a]!) }
|
|
107
|
+
: n.k === "swizzle" ? { ...n, src: at[n.src]! } : n
|
|
108
|
+
at[i] = nodes.push(moved) - 1
|
|
109
|
+
})
|
|
110
|
+
return { nodes, result: at[writtenResult]! }
|
|
59
111
|
}
|
|
60
112
|
|
|
61
113
|
function node(n: unknown, i: number): SLNode {
|