onejs-sl 0.6.0 → 0.8.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 +22 -0
- package/README.md +65 -13
- package/goldens/goldens.json +80 -5
- package/package.json +2 -2
- package/src/body.ts +51 -9
- package/src/compile.ts +11 -1
- package/src/core.ts +4 -4
- package/src/hlsl.ts +21 -3
- package/src/ir.ts +108 -12
- package/src/lang/README.md +23 -12
- package/src/lang/check.ts +39 -7
- package/src/lang/classify.ts +2 -2
- package/src/lang/glsl.ts +2 -2
- package/src/lang/lower.ts +12 -3
- package/src/lang/parser.ts +1 -1
- package/src/ops.ts +7 -3
- package/src/params.ts +6 -3
- package/src/serial.ts +8 -4
- package/src/sl.ts +39 -5
- package/src/structure.ts +2 -2
- package/src/tables.ts +2 -2
- package/src/web.ts +42 -11
package/src/compile.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* are the only refusal (`checkCaps`).
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { checkCaps, type Program } from "./ir"
|
|
14
|
+
import { checkCaps, readsOf, type Program, type SLReads } from "./ir"
|
|
15
15
|
import { emitShader } from "./hlsl"
|
|
16
16
|
import { emitGLSL, emitWGSL } from "./web"
|
|
17
17
|
import { uniformDefaults } from "./sl"
|
|
@@ -46,6 +46,15 @@ export interface Compiled {
|
|
|
46
46
|
* Setting a material property called `grain` bound nothing, silently.
|
|
47
47
|
*/
|
|
48
48
|
textures: string[]
|
|
49
|
+
/**
|
|
50
|
+
* What the program needs its host to keep from one frame to the next: the
|
|
51
|
+
* previous frame, the frame count, the step (`Specs/SL_NEXT.md` 4). A host
|
|
52
|
+
* keeps a history pair only for a program that reads `previous`, and one
|
|
53
|
+
* that cannot provide something here refuses the program rather than
|
|
54
|
+
* drawing it as if nothing came before. A host that carries a compiled
|
|
55
|
+
* program elsewhere, as OneJS's `.sl` import does, has to carry this too.
|
|
56
|
+
*/
|
|
57
|
+
reads: SLReads
|
|
49
58
|
/**
|
|
50
59
|
* The program as HLSL, for a host that can compile it.
|
|
51
60
|
*
|
|
@@ -76,6 +85,7 @@ export function compile(program: Program): Compiled {
|
|
|
76
85
|
uniforms: program.uniforms.map((u) => u.name),
|
|
77
86
|
defaults: uniformDefaults(program),
|
|
78
87
|
textures: program.textures.map((t) => t.name),
|
|
88
|
+
reads: readsOf(program),
|
|
79
89
|
} as Compiled
|
|
80
90
|
let hlsl: string | undefined
|
|
81
91
|
let wgsl: string | undefined
|
package/src/core.ts
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
* game never executes.
|
|
9
9
|
*/
|
|
10
10
|
export * as sl from "./sl"
|
|
11
|
-
export type { Float, Vec2, Vec3, Vec4, Num, ProgramInputs, Texture } from "./sl"
|
|
11
|
+
export type { Float, Vec2, Vec3, Vec4, Num, ProgramInputs, PreviousFrame, Texture } from "./sl"
|
|
12
12
|
export {
|
|
13
|
-
TYPE, INPUTS, MAX_NODES, SLError, hashProgram, widthName, SL_IR_VERSION, SL_HASH_VERSION,
|
|
14
|
-
CONTROL_FIELDS, controlProblem,
|
|
13
|
+
TYPE, INPUTS, STEP_INPUTS, PREVIOUS, MAX_NODES, SLError, hashProgram, widthName, SL_IR_VERSION, SL_HASH_VERSION,
|
|
14
|
+
inputsUsed, readsOf, CONTROL_FIELDS, controlProblem,
|
|
15
15
|
} from "./ir"
|
|
16
16
|
export type {
|
|
17
|
-
SLType, InputName, NodeRef, SLNode, Program, UniformDecl, UniformControl, TextureDecl,
|
|
17
|
+
SLType, InputName, StepInputName, HostInputName, SLReads, NodeRef, SLNode, Program, UniformDecl, UniformControl, TextureDecl,
|
|
18
18
|
} from "./ir"
|
|
19
19
|
export { SLOP, SL_ARITY, SL_NAME, INPUT_ID, isSampling, UNIFORM_SLOTS, TEXTURE_SLOTS } from "./ops"
|
|
20
20
|
export type { SLOpCode } from "./ops"
|
package/src/hlsl.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* next time it is generated.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { SLError, SL_HASH_VERSION, type Program } from "./ir"
|
|
18
|
+
import { SLError, SL_HASH_VERSION, readsOf, type Program } from "./ir"
|
|
19
19
|
import { emitBody, lit, type BodyTarget } from "./body"
|
|
20
20
|
|
|
21
21
|
/** The property name a uniform gets. Prefixed so it cannot collide with ours. */
|
|
@@ -48,6 +48,7 @@ export function emitShader(p: Program, options: EmitOptions = {}): string {
|
|
|
48
48
|
const name = options.name ?? `Hidden/SLGenerated/${p.hash}`
|
|
49
49
|
const include = options.include ?? "SLCommon.cginc"
|
|
50
50
|
const body = emitFragmentBody(p)
|
|
51
|
+
const reads = readsOf(p)
|
|
51
52
|
|
|
52
53
|
const props: string[] = [
|
|
53
54
|
` _Secs ("Seconds", Float) = 0`,
|
|
@@ -69,6 +70,16 @@ export function emitShader(p: Program, options: EmitOptions = {}): string {
|
|
|
69
70
|
props.push(` _Tex${t.slot} ("${t.name}", 2D) = "white" {}`)
|
|
70
71
|
decls.push(` sampler2D _Tex${t.slot};`)
|
|
71
72
|
}
|
|
73
|
+
// Only for a program that samples it, so every other program's shader is
|
|
74
|
+
// the text it always was. Transparent black is the previous frame's clear
|
|
75
|
+
// value, so a host that binds nothing draws the first frame.
|
|
76
|
+
if (reads.previous) {
|
|
77
|
+
props.push(` _Prev ("Previous frame", 2D) = "black" {}`)
|
|
78
|
+
decls.push(` sampler2D _Prev;`)
|
|
79
|
+
}
|
|
80
|
+
// What the program returns is the next frame's previous, kept at 16 bits
|
|
81
|
+
// a channel, so it leaves at full precision rather than as a fixed4.
|
|
82
|
+
const out = reads.previous ? "float4" : "fixed4"
|
|
72
83
|
|
|
73
84
|
return `// GENERATED from a shader language program. Do not edit.
|
|
74
85
|
//
|
|
@@ -112,7 +123,7 @@ ${decls.join("\n")}
|
|
|
112
123
|
return o;
|
|
113
124
|
}
|
|
114
125
|
|
|
115
|
-
|
|
126
|
+
${out} frag(v2f i) : SV_Target
|
|
116
127
|
{
|
|
117
128
|
${body}
|
|
118
129
|
}
|
|
@@ -125,7 +136,9 @@ ${body}
|
|
|
125
136
|
|
|
126
137
|
/**
|
|
127
138
|
* The frame's side of the body: OneJS's names for the inputs, a uniform's
|
|
128
|
-
* material property, and `tex2D` and `tex2Dlod` on the slot's sampler.
|
|
139
|
+
* material property, and `tex2D` and `tex2Dlod` on the slot's sampler. The
|
|
140
|
+
* host puts the frame count and the step in `_Res`'s spare zw, and binds the
|
|
141
|
+
* previous frame as `_Prev`. `sl_toLinear` stays a
|
|
129
142
|
* call because `SLCommon.cginc` decides Gamma or Linear per project, at the
|
|
130
143
|
* shader compile, so this target always emits it.
|
|
131
144
|
*/
|
|
@@ -147,10 +160,15 @@ function unityTarget(p: Program): BodyTarget {
|
|
|
147
160
|
resolution: "_Res.xy",
|
|
148
161
|
time: "_Secs",
|
|
149
162
|
aspect: "(_Res.x / max(_Res.y, 1.0))",
|
|
163
|
+
frame: "_Res.z",
|
|
164
|
+
deltaTime: "_Res.w",
|
|
150
165
|
},
|
|
151
166
|
uniform: (slot) => uniformProperty(p.uniforms[slot]!.name),
|
|
152
167
|
sample: (slot, uv) => `tex2D(_Tex${slot}, ${uv})`,
|
|
153
168
|
sampleLevel: (slot, uv, lod) => `tex2Dlod(_Tex${slot}, float4(${uv}, 0.0, ${lod}))`,
|
|
169
|
+
// Flipped as this frame flipped the uv it wrote at, so a read at a
|
|
170
|
+
// pixel's own uv is that pixel whichever way the host's target runs.
|
|
171
|
+
previous: (uv) => `tex2Dlod(_Prev, float4(${uv} * float2(1.0, 1.0 - 2.0 * _FlipY) + float2(0.0, _FlipY), 0.0, 0.0))`,
|
|
154
172
|
colour: "linear",
|
|
155
173
|
indent: " ",
|
|
156
174
|
loopAttribute: "[loop]",
|
package/src/ir.ts
CHANGED
|
@@ -57,9 +57,35 @@ export const DERIVED_INPUTS = {
|
|
|
57
57
|
} as const
|
|
58
58
|
export type DerivedInputName = keyof typeof DERIVED_INPUTS
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Inputs the host hands over only to a program that reads one (IR 5,
|
|
62
|
+
* `Specs/SL_NEXT.md` 4). They are not recorded up front as `INPUTS` are, so a
|
|
63
|
+
* program that never names one holds no node for it and keeps its IR version,
|
|
64
|
+
* its hash and its bytes. `compile(p).reads` says which a program reads.
|
|
65
|
+
*
|
|
66
|
+
* frame frames drawn since the previous frame was last cleared, from 0.
|
|
67
|
+
* An int in the language; the host hands over a float holding it
|
|
68
|
+
* exactly, and the program converts
|
|
69
|
+
* deltaTime seconds from the frame before to this one; 0 on a frame that
|
|
70
|
+
* redraws the same time
|
|
71
|
+
*
|
|
72
|
+
* The previous frame itself is SLOP.SAMPLE_PREVIOUS, an op rather than an input.
|
|
73
|
+
*/
|
|
74
|
+
export const STEP_INPUTS = {
|
|
75
|
+
frame: TYPE.FLOAT,
|
|
76
|
+
deltaTime: TYPE.FLOAT,
|
|
77
|
+
} as const
|
|
78
|
+
export type StepInputName = keyof typeof STEP_INPUTS
|
|
79
|
+
|
|
80
|
+
/** The name `.sl` reads the previous frame by, as a texture: `tex2D(previous, uv)`. */
|
|
81
|
+
export const PREVIOUS = "previous"
|
|
82
|
+
|
|
83
|
+
/** Every input a host hands over: the ones every program has, then the ones a program asks for. */
|
|
84
|
+
export type HostInputName = InputName | StepInputName
|
|
85
|
+
|
|
60
86
|
/** Every input a program can name: the host's, then the derived ones. */
|
|
61
|
-
export type SourceInputName =
|
|
62
|
-
export const SOURCE_INPUTS: Readonly<Record<SourceInputName, SLType>> = { ...INPUTS, ...DERIVED_INPUTS }
|
|
87
|
+
export type SourceInputName = HostInputName | DerivedInputName
|
|
88
|
+
export const SOURCE_INPUTS: Readonly<Record<SourceInputName, SLType>> = { ...INPUTS, ...STEP_INPUTS, ...DERIVED_INPUTS }
|
|
63
89
|
|
|
64
90
|
/** Index into `Program.nodes`. Always refers backwards. */
|
|
65
91
|
export type NodeRef = number
|
|
@@ -77,7 +103,7 @@ export type SLKind = "int" | "uint" | "bool"
|
|
|
77
103
|
*/
|
|
78
104
|
export type ValueNode =
|
|
79
105
|
| { k: "const"; type: SLType; kind?: SLKind; v: number[] }
|
|
80
|
-
| { k: "input"; type: SLType; kind?: undefined; name:
|
|
106
|
+
| { k: "input"; type: SLType; kind?: undefined; name: HostInputName }
|
|
81
107
|
| { k: "uniform"; type: SLType; kind?: undefined; slot: number }
|
|
82
108
|
| { k: "swizzle"; type: SLType; kind?: SLKind; src: NodeRef; chans: number[] }
|
|
83
109
|
| { k: "call"; type: SLType; kind?: SLKind; op: SLOpCode; args: NodeRef[]; imm?: number[] }
|
|
@@ -435,9 +461,9 @@ export class Builder {
|
|
|
435
461
|
/**
|
|
436
462
|
* Nodes the result actually depends on, in order.
|
|
437
463
|
*
|
|
438
|
-
* Dead nodes are dropped rather than
|
|
464
|
+
* Dead nodes are dropped rather than emitted. An author can produce them easily
|
|
439
465
|
* by computing something and not using it, and the hash already ignores them, so
|
|
440
|
-
*
|
|
466
|
+
* emitting them would make a shader disagree with its own hash about what the
|
|
441
467
|
* program is.
|
|
442
468
|
*/
|
|
443
469
|
export function reachable(nodes: SLNode[], result: NodeRef): NodeRef[] {
|
|
@@ -454,17 +480,79 @@ export function reachable(nodes: SLNode[], result: NodeRef): NodeRef[] {
|
|
|
454
480
|
}
|
|
455
481
|
|
|
456
482
|
/**
|
|
457
|
-
* The inputs a program reads, in `INPUTS` order
|
|
458
|
-
* value computed and never used is not in the
|
|
459
|
-
* that with `time` is still not animated.
|
|
483
|
+
* The inputs a program reads, in `INPUTS` order and then `STEP_INPUTS` order.
|
|
484
|
+
* Dead nodes do not count: a value computed and never used is not in the
|
|
485
|
+
* picture, so a program that does that with `time` is still not animated.
|
|
460
486
|
*/
|
|
461
|
-
export function inputsUsed(p: Program):
|
|
462
|
-
const read = new Set<
|
|
487
|
+
export function inputsUsed(p: Program): HostInputName[] {
|
|
488
|
+
const read = new Set<HostInputName>()
|
|
463
489
|
for (const ref of reachable(p.nodes, p.result)) {
|
|
464
490
|
const n = p.nodes[ref]
|
|
465
491
|
if (n.k === "input") read.add(n.name)
|
|
466
492
|
}
|
|
467
|
-
return (Object.keys(INPUTS) as
|
|
493
|
+
return ([...Object.keys(INPUTS), ...Object.keys(STEP_INPUTS)] as HostInputName[]).filter((name) => read.has(name))
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* What a program needs its host to keep from one frame to the next
|
|
498
|
+
* (`Specs/SL_NEXT.md` 4). A host that draws a program reading any of these
|
|
499
|
+
* has to hand it over, or refuse the program: nothing here has a value that
|
|
500
|
+
* would pass for right.
|
|
501
|
+
*
|
|
502
|
+
* previous the host keeps what the program drew last and binds it
|
|
503
|
+
* frame frames drawn since `previous` was last cleared
|
|
504
|
+
* deltaTime seconds since the frame before
|
|
505
|
+
*/
|
|
506
|
+
export interface SLReads {
|
|
507
|
+
previous: boolean
|
|
508
|
+
frame: boolean
|
|
509
|
+
deltaTime: boolean
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export function readsOf(p: Program): SLReads {
|
|
513
|
+
const out: SLReads = { previous: false, frame: false, deltaTime: false }
|
|
514
|
+
for (const ref of reachable(p.nodes, p.result)) {
|
|
515
|
+
const n = p.nodes[ref]!
|
|
516
|
+
if (n.k === "call" && n.op === SLOP.SAMPLE_PREVIOUS) out.previous = true
|
|
517
|
+
else if (n.k === "input" && n.name in STEP_INPUTS) out[n.name as StepInputName] = true
|
|
518
|
+
}
|
|
519
|
+
return out
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Ops whose result is a whole number whatever they are given. */
|
|
523
|
+
const ALWAYS_WHOLE = new Set<number>([SLOP.FLOOR, SLOP.CEIL, SLOP.ROUND, SLOP.SIGN])
|
|
524
|
+
/** Ops whose result is a whole number when every operand they read is one. */
|
|
525
|
+
const KEEPS_WHOLE = new Set<number>([
|
|
526
|
+
SLOP.CAST, SLOP.ADD, SLOP.SUB, SLOP.MUL, SLOP.NEG, SLOP.ABS, SLOP.MIN, SLOP.MAX, SLOP.CLAMP, SLOP.SATURATE,
|
|
527
|
+
])
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Whether a value is a whole number in every component on every pixel, as far
|
|
531
|
+
* as its node says. An int, uint or bool is; so are a constant written whole,
|
|
532
|
+
* `floor`, `ceil`, `round` and `sign`, and sums, products, minimums and the
|
|
533
|
+
* like of whole numbers (a float32 sum or product of whole numbers is whole).
|
|
534
|
+
* Anything else is not known to be, a uniform float included.
|
|
535
|
+
*
|
|
536
|
+
* The emitters read it for `tex2Dlod`: a level known whole reads that level
|
|
537
|
+
* with one sample, and any other blends the level below and the level above by
|
|
538
|
+
* the fraction, so every backend draws a fraction the same way whatever the
|
|
539
|
+
* texture's filter between levels.
|
|
540
|
+
*/
|
|
541
|
+
export function isWhole(nodes: readonly SLNode[], ref: NodeRef, seen = new Map<NodeRef, boolean>()): boolean {
|
|
542
|
+
const known = seen.get(ref)
|
|
543
|
+
if (known !== undefined) return known
|
|
544
|
+
const n = valueAt(nodes, ref)
|
|
545
|
+
let whole = false
|
|
546
|
+
if (n.kind !== undefined) whole = true
|
|
547
|
+
else if (n.k === "const") whole = n.v.every(Number.isInteger)
|
|
548
|
+
else if (n.k === "swizzle") whole = isWhole(nodes, n.src, seen)
|
|
549
|
+
else if (n.k === "call") {
|
|
550
|
+
if (ALWAYS_WHOLE.has(n.op)) whole = true
|
|
551
|
+
else if (KEEPS_WHOLE.has(n.op)) whole = n.args.every((a) => isWhole(nodes, a, seen))
|
|
552
|
+
else if (n.op === SLOP.CHOOSE) whole = isWhole(nodes, n.args[1]!, seen) && isWhole(nodes, n.args[2]!, seen)
|
|
553
|
+
}
|
|
554
|
+
seen.set(ref, whole)
|
|
555
|
+
return whole
|
|
468
556
|
}
|
|
469
557
|
|
|
470
558
|
/** Structural key for hash consing. Order matters and is fixed by the node shape. */
|
|
@@ -681,10 +769,12 @@ const IR4_OPS = new Set<number>([
|
|
|
681
769
|
* holds, and `fromJSON` moves it to the form IR 3 has.
|
|
682
770
|
*/
|
|
683
771
|
function nodeVersion(n: SLNode): number {
|
|
772
|
+
if (n.k === "input" && n.name in STEP_INPUTS) return 5
|
|
684
773
|
if (n.k === "if" || n.k === "loop" || n.k === "param" || n.k === "proj" || n.kind !== undefined) return 4
|
|
685
774
|
if (n.k !== "call") return 2
|
|
686
775
|
if (IR4_OPS.has(n.op)) return 4
|
|
687
776
|
switch (n.op) {
|
|
777
|
+
case SLOP.SAMPLE_PREVIOUS: return 5
|
|
688
778
|
case SLOP.SAMPLE_LOD: return 3
|
|
689
779
|
case SLOP.SDF:
|
|
690
780
|
case SLOP.FBM:
|
|
@@ -705,6 +795,8 @@ function nodeVersion(n: SLNode): number {
|
|
|
705
795
|
* the shape id the one immediate
|
|
706
796
|
* FBM, TURBULENCE, RIDGED the point and the octave count, FBM's kind the
|
|
707
797
|
* one immediate
|
|
798
|
+
* SAMPLE_PREVIOUS a float4 at a float2, and no immediates: there is one
|
|
799
|
+
* previous frame, so there is no slot to name
|
|
708
800
|
*/
|
|
709
801
|
export function formProblem(n: Extract<SLNode, { k: "call" }>, nodes: readonly SLNode[]): string | null {
|
|
710
802
|
const widths = n.args.map((a) => { const v = nodes[a]!; return v.k === "if" || v.k === "loop" ? "control" : v.type }).join(",")
|
|
@@ -719,6 +811,9 @@ export function formProblem(n: Extract<SLNode, { k: "call" }>, nodes: readonly S
|
|
|
719
811
|
if (widths === "2,1" && (n.imm?.length ?? 0) === rest) return null
|
|
720
812
|
return `${SL_NAME[n.op]} takes a float2 and its octave count, a float`
|
|
721
813
|
}
|
|
814
|
+
case SLOP.SAMPLE_PREVIOUS:
|
|
815
|
+
if (widths === "2" && n.type === 4 && n.kind === undefined && (n.imm?.length ?? 0) === 0) return null
|
|
816
|
+
return "the previous frame is a float4 read at a float2"
|
|
722
817
|
default:
|
|
723
818
|
return null
|
|
724
819
|
}
|
|
@@ -759,8 +854,9 @@ export const SL_HASH_VERSION = 2
|
|
|
759
854
|
* 3 SAMPLE_LOD; an SDF's shape parameters and a noise's octave count are
|
|
760
855
|
* operands, where they were immediates
|
|
761
856
|
* 4 control flow (if, loop, their params and projs), ints, uints and bools
|
|
857
|
+
* 5 the previous frame (SAMPLE_PREVIOUS), and the frame and deltaTime inputs
|
|
762
858
|
*/
|
|
763
|
-
export const SL_IR_VERSION =
|
|
859
|
+
export const SL_IR_VERSION = 5
|
|
764
860
|
|
|
765
861
|
function fnv1a(s: string): string {
|
|
766
862
|
let h = 0x811c9dc5
|
package/src/lang/README.md
CHANGED
|
@@ -41,9 +41,14 @@ player. So parity is the test to keep green, not the parser's unit tests.
|
|
|
41
41
|
| `ast.ts` | The node shapes, all of them, which is not many |
|
|
42
42
|
| `parser.ts` | Pratt parser: shape only, HLSL precedence |
|
|
43
43
|
| `check.ts` | Declarations, names, statement shape, budgets, recursion |
|
|
44
|
-
| `lower.ts` | AST to IR through the EDSL: SSA, unrolling,
|
|
44
|
+
| `lower.ts` | AST to IR through the EDSL: SSA, unrolling, branches and loops, inlining |
|
|
45
45
|
| `builtins.ts` | What each name does, keyed by the spelling `ops.ts` gives it |
|
|
46
|
-
| `prelude.ts` | The standard library, written in the language |
|
|
46
|
+
| `prelude.ts`, `prelude-source.ts` | The standard library, written in the language |
|
|
47
|
+
| `attributes.ts` | A uniform's `[Range]`, `[Toggle]` and the rest, read into its control |
|
|
48
|
+
| `words.ts` | The keywords and type names, shared by the parser and `classify` |
|
|
49
|
+
| `classify.ts` | Every token with its class, for an editor's highlighting |
|
|
50
|
+
| `glsl.ts` | `fromGLSL`: a pasted GLSL shader as a `.sl` file |
|
|
51
|
+
| `index.ts` | This folder's exports |
|
|
47
52
|
| `onejs-unity`'s `src/esbuild/sl.mjs` | The loader: `import plasma from "./plasma.sl"` |
|
|
48
53
|
| `../index.ts` and `../core.ts` | The package with the parser, and without it. See below |
|
|
49
54
|
|
|
@@ -68,9 +73,11 @@ It writes two things beside the code:
|
|
|
68
73
|
recording stays for EDSL programs and for a build older than the loader; the
|
|
69
74
|
editor reads every `*.sl.json` it can find.
|
|
70
75
|
|
|
71
|
-
|
|
72
|
-
last `.sl` file has to stop its shaders being
|
|
73
|
-
|
|
76
|
+
With no `.sl` files, an empty manifest is written over one that is already
|
|
77
|
+
there, since deleting the last `.sl` file has to stop its shaders being
|
|
78
|
+
generated, and beside a bundle carrying `onejs-sl`, which has to name its hash
|
|
79
|
+
scheme. A project that uses no shader language finds no new file beside its
|
|
80
|
+
bundle.
|
|
74
81
|
|
|
75
82
|
### Why the parser is compiled rather than imported
|
|
76
83
|
|
|
@@ -110,7 +117,8 @@ here is the one nothing else uses.
|
|
|
110
117
|
**A scalar on the left of an operator broadcasts through a swizzle.** `uv * 8`
|
|
111
118
|
is `uv.mul(8)`, one constant node of two components. `8 * uv` is
|
|
112
119
|
`sl.float(8).mul(uv)`, a one component constant and a broadcast. Same picture,
|
|
113
|
-
different graph,
|
|
120
|
+
different graph, and since 0.4.0 (`SL_HASH_VERSION` 2) the same hash. The
|
|
121
|
+
vector on the left saves a node.
|
|
114
122
|
|
|
115
123
|
**Literal arithmetic folds; a builtin call never does.** `2 * 3 + 1` is the
|
|
116
124
|
constant 7 before the IR sees it. `sin(0.5)` is two instructions, because
|
|
@@ -201,7 +209,7 @@ Where the fix is certain, the error carries it as `fix`: a title and the text
|
|
|
201
209
|
for the marked range, one click in an editor. A GLSL spelling that means the
|
|
202
210
|
same in HLSL (`mix`, `fract`, `vec3`, `gl_FragCoord`, `iTime`, two argument
|
|
203
211
|
`atan`) and a "did you mean" both have one. `mod` does not, since GLSL's floors
|
|
204
|
-
and `%` truncates, and neither does `
|
|
212
|
+
and `%` truncates, and neither does `ivec2`, since `float2` would change what
|
|
205
213
|
dividing it does. That is Decision 1 kept as `Specs/SL_NEXT.md` 5 A has it:
|
|
206
214
|
one spelling, and every hint a fix.
|
|
207
215
|
|
|
@@ -245,7 +253,8 @@ It rewrites tokens and keeps everything else: comments, layout and names stay
|
|
|
245
253
|
where they were, so the result reads as the shader it came from. What it does:
|
|
246
254
|
|
|
247
255
|
- The spellings that mean the same (`vec3`, `mix`, `fract`, `texture`,
|
|
248
|
-
`iTime`, `gl_FragCoord`) become the HLSL ones
|
|
256
|
+
`iTime`, `iFrame`, `iTimeDelta`, `gl_FragCoord`) become the HLSL ones and
|
|
257
|
+
the inputs. So do the calls that need an
|
|
249
258
|
expression: `mod` floors (`a - b * floor(a / b)`, never `%`, which
|
|
250
259
|
truncates), and `atan`, `inversesqrt`, `radians`, `degrees`, `exp2` and
|
|
251
260
|
`log2` become what they compute.
|
|
@@ -267,10 +276,12 @@ where they were, so the result reads as the shader it came from. What it does:
|
|
|
267
276
|
the notes point at `rotate()`. A file with a `rotate` of its own is left
|
|
268
277
|
alone, since the calls would reach it. `#define NAME value`
|
|
269
278
|
becomes a const; any other directive is kept as a comment, with a note.
|
|
270
|
-
- A name the language already means something by gets a `_` suffix,
|
|
271
|
-
`int`
|
|
272
|
-
|
|
273
|
-
|
|
279
|
+
- A name the language already means something by gets a `_` suffix, with a
|
|
280
|
+
note. `int`, `uint` and `bool` are kept, since they are types here too; their
|
|
281
|
+
vectors (`ivec2`, `uvec3`, `bvec4` and the rest) become float vectors, with a
|
|
282
|
+
note saying what changed.
|
|
283
|
+
- What has no counterpart (`iFrameRate`, `texelFetch`, `dFdx`, a function
|
|
284
|
+
returning nothing) is left in place with a note, for `errors` to point at.
|
|
274
285
|
|
|
275
286
|
It is not a GLSL parser, and is not meant to be: a shader it cannot follow comes
|
|
276
287
|
out with errors on the lines to look at, which is where a person pasting one
|
package/src/lang/check.ts
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* is refused with the reason.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
import { DERIVED_INPUTS, INPUTS, tooManyTextures, tooManyUniforms } from "../ir"
|
|
31
|
+
import { DERIVED_INPUTS, INPUTS, PREVIOUS, STEP_INPUTS, tooManyTextures, tooManyUniforms } from "../ir"
|
|
32
32
|
import { TEXTURE_SLOTS, UNIFORM_SLOTS } from "../ops"
|
|
33
33
|
import { SL_GLSL_HINT } from "../ops"
|
|
34
34
|
import { BUILTINS, NOT_YET } from "./builtins"
|
|
@@ -44,6 +44,17 @@ const INPUT_NAMES = new Set(Object.keys(INPUTS))
|
|
|
44
44
|
* long before the language had one, and that must keep compiling.
|
|
45
45
|
*/
|
|
46
46
|
const DERIVED_NAMES = new Set(Object.keys(DERIVED_INPUTS))
|
|
47
|
+
/**
|
|
48
|
+
* `frame` and `deltaTime`, and `previous`, the texture that is the frame drawn
|
|
49
|
+
* before (IR 5). The language had none of them before 0.7.0, so a program may
|
|
50
|
+
* already use the name for its own value, texture or function, and keeps it.
|
|
51
|
+
* A function by the name is called by it; the bare name is still the input.
|
|
52
|
+
*/
|
|
53
|
+
const STEP_NAMES = new Set(Object.keys(STEP_INPUTS))
|
|
54
|
+
/** What a declaration may take the name of, with the words for it. */
|
|
55
|
+
const SHADOWABLE = new Set(["a builtin", "a derived input", "a built in input", "the previous frame"])
|
|
56
|
+
/** What a function may take the name of: only the names that came after files could have used them. */
|
|
57
|
+
const FUNCTION_MAY_TAKE = new Set(["a built in input", "the previous frame"])
|
|
47
58
|
|
|
48
59
|
/**
|
|
49
60
|
* The GLSL spellings whose HLSL name means the same wherever it is written, so
|
|
@@ -145,13 +156,15 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
145
156
|
if (textures.has(n)) return "a texture"
|
|
146
157
|
if (consts.has(n)) return "a const"
|
|
147
158
|
if (DERIVED_NAMES.has(n)) return "a derived input"
|
|
159
|
+
if (STEP_NAMES.has(n)) return "a built in input"
|
|
160
|
+
if (n === PREVIOUS) return "the previous frame"
|
|
148
161
|
return null
|
|
149
162
|
}
|
|
150
163
|
|
|
151
164
|
/** What a value may not be called: anything spoken for, except a builtin's or a prelude function's name. */
|
|
152
165
|
const valueClash = (n: string): string | null => {
|
|
153
166
|
const why = taken(n)
|
|
154
|
-
if (why !== null && why
|
|
167
|
+
if (why !== null && !SHADOWABLE.has(why)) return why
|
|
155
168
|
const fn = funcs.get(n)
|
|
156
169
|
return fn !== undefined && !fn.prelude ? "a function" : null
|
|
157
170
|
}
|
|
@@ -194,7 +207,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
194
207
|
for (const fn of unit.funcs) {
|
|
195
208
|
attempt(() => {
|
|
196
209
|
const clash = taken(fn.name)
|
|
197
|
-
if (clash !== null) fail(`"${fn.name}" already names ${clash}`, fn.pos, fn.name.length)
|
|
210
|
+
if (clash !== null && !FUNCTION_MAY_TAKE.has(clash)) fail(`"${fn.name}" already names ${clash}`, fn.pos, fn.name.length)
|
|
198
211
|
if (unit.funcs.filter((f) => f.name === fn.name).length > 1) {
|
|
199
212
|
fail(
|
|
200
213
|
`this file declares ${fn.name} more than once. There is no overloading: a function ` +
|
|
@@ -222,7 +235,8 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
222
235
|
if (main.params.length > 0) {
|
|
223
236
|
fail(
|
|
224
237
|
"main takes no parameters: what a program is given are the free identifiers uv, " +
|
|
225
|
-
"fragCoord, resolution, time, aspect, texel and
|
|
238
|
+
"fragCoord, resolution, time, aspect, texel, centered, frame and deltaTime, and the " +
|
|
239
|
+
"texture previous",
|
|
226
240
|
main.params[0]!.pos,
|
|
227
241
|
)
|
|
228
242
|
}
|
|
@@ -486,7 +500,15 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
486
500
|
case "bool":
|
|
487
501
|
return
|
|
488
502
|
case "ident": {
|
|
489
|
-
if (scope.has(e.name) || INPUT_NAMES.has(e.name) || DERIVED_NAMES.has(e.name) ||
|
|
503
|
+
if (scope.has(e.name) || INPUT_NAMES.has(e.name) || DERIVED_NAMES.has(e.name) || STEP_NAMES.has(e.name) ||
|
|
504
|
+
uniforms.has(e.name) || consts.has(e.name)) return
|
|
505
|
+
if (e.name === PREVIOUS && !textures.has(e.name)) {
|
|
506
|
+
fail(
|
|
507
|
+
"previous is the frame this program drew before, a texture, and a texture is only " +
|
|
508
|
+
"ever the first argument of tex2D. Write `tex2D(previous, uv)`",
|
|
509
|
+
e.pos, e.name.length,
|
|
510
|
+
)
|
|
511
|
+
}
|
|
490
512
|
if (textures.has(e.name)) {
|
|
491
513
|
fail(
|
|
492
514
|
`"${e.name}" is a texture, and a texture is only ever the first argument of ` +
|
|
@@ -604,7 +626,17 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
604
626
|
)
|
|
605
627
|
}
|
|
606
628
|
arity(e, n, builtin.min, builtin.max)
|
|
607
|
-
|
|
629
|
+
// The previous frame, unless something in this file took its name.
|
|
630
|
+
const previous = tex?.k === "ident" && tex.name === PREVIOUS && !textures.has(PREVIOUS) &&
|
|
631
|
+
!scope.has(PREVIOUS) && !uniforms.has(PREVIOUS) && !consts.has(PREVIOUS)
|
|
632
|
+
if (previous && n === "tex2Dlod") {
|
|
633
|
+
fail(
|
|
634
|
+
"previous has one level, the frame as drawn, so there is no mip level to pick. " +
|
|
635
|
+
"Read it with `tex2D(previous, uv)`",
|
|
636
|
+
e.pos,
|
|
637
|
+
)
|
|
638
|
+
}
|
|
639
|
+
if (!previous && (tex === undefined || tex.k !== "ident" || !textures.has(tex.name))) {
|
|
608
640
|
const lod = n === "tex2Dlod" ? ", 0" : ""
|
|
609
641
|
fail(
|
|
610
642
|
`${n} samples a texture declared in this file, as in \`texture2D grain;\` ` +
|
|
@@ -654,7 +686,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
654
686
|
/** Refuses a name nothing declares, offering the nearest one spelled almost like it. */
|
|
655
687
|
function unknown(n: string, scope: Set<string>, pos: Pos): never {
|
|
656
688
|
const near = nearest(n, [
|
|
657
|
-
...scope, ...INPUT_NAMES, ...DERIVED_NAMES, ...uniforms.keys(), ...textures.keys(), ...consts.keys(),
|
|
689
|
+
...scope, ...INPUT_NAMES, ...DERIVED_NAMES, ...STEP_NAMES, ...uniforms.keys(), ...textures.keys(), ...consts.keys(),
|
|
658
690
|
...funcs.keys(), ...Object.keys(BUILTINS),
|
|
659
691
|
])
|
|
660
692
|
if (near === null) fail(`"${n}" is not declared`, pos, n.length)
|
package/src/lang/classify.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* wants that should ask it.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { SOURCE_INPUTS } from "../ir"
|
|
16
|
+
import { PREVIOUS, SOURCE_INPUTS } from "../ir"
|
|
17
17
|
import { BUILTINS } from "./builtins"
|
|
18
18
|
import { lex } from "./lexer"
|
|
19
19
|
import { PRELUDE_NAMES } from "./prelude-source"
|
|
@@ -42,7 +42,7 @@ export interface SLClassifiedToken {
|
|
|
42
42
|
length: number
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
const INPUT_NAMES: ReadonlySet<string> = new Set(Object.keys(SOURCE_INPUTS))
|
|
45
|
+
const INPUT_NAMES: ReadonlySet<string> = new Set([...Object.keys(SOURCE_INPUTS), PREVIOUS])
|
|
46
46
|
|
|
47
47
|
/** The tokens of `source`, comments included, in order, each with its class. */
|
|
48
48
|
export function classify(source: string): SLClassifiedToken[] {
|
package/src/lang/glsl.ts
CHANGED
|
@@ -76,6 +76,7 @@ const GLSL_TYPES = new Set(["float", "int", "uint", "bool", "vec2", "vec3", "vec
|
|
|
76
76
|
const RENAME: Record<string, string> = {
|
|
77
77
|
vec2: "float2", vec3: "float3", vec4: "float4", mix: "lerp", fract: "frac",
|
|
78
78
|
texture: "tex2D", texture2D: "tex2D", textureLod: "tex2Dlod", iTime: "time", iGlobalTime: "time", gl_FragCoord: "fragCoord",
|
|
79
|
+
iFrame: "frame", iTimeDelta: "deltaTime",
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
/**
|
|
@@ -94,8 +95,6 @@ const NARROW: Record<string, [string, string]> = {
|
|
|
94
95
|
|
|
95
96
|
/** Shadertoy's inputs with no counterpart yet, and why. */
|
|
96
97
|
const MISSING: Record<string, string> = {
|
|
97
|
-
iFrame: "there is no frame counter yet",
|
|
98
|
-
iTimeDelta: "there is no delta time yet",
|
|
99
98
|
iFrameRate: "there is no frame rate",
|
|
100
99
|
iDate: "there is no date",
|
|
101
100
|
iChannelTime: "a texture has no time of its own",
|
|
@@ -115,6 +114,7 @@ const MISSING: Record<string, string> = {
|
|
|
115
114
|
const HOST_UNIFORMS: Record<string, string> = {
|
|
116
115
|
u_time: "time", u_resolution: "resolution", time: "time", resolution: "resolution",
|
|
117
116
|
iTime: "time", iGlobalTime: "time", iResolution: "resolution", iMouse: "mouse",
|
|
117
|
+
iFrame: "frame", iTimeDelta: "deltaTime",
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
const QUALIFIERS = new Set(["highp", "mediump", "lowp"])
|
package/src/lang/lower.ts
CHANGED
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
52
|
import {
|
|
53
|
-
controlProblem, DERIVED_INPUTS, INPUTS, RAMP_STOP_COMPUTED, truncateHeld, writtenColour,
|
|
53
|
+
controlProblem, DERIVED_INPUTS, INPUTS, PREVIOUS, RAMP_STOP_COMPUTED, STEP_INPUTS, truncateHeld, writtenColour,
|
|
54
54
|
type Program, type SLKind, type SLType, type UniformControl,
|
|
55
55
|
} from "../ir"
|
|
56
56
|
import { SLOP } from "../ops"
|
|
@@ -246,11 +246,11 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
246
246
|
// Read through, so a program that never names one records nothing
|
|
247
247
|
// for it. The setter only serves a refused assignment, which the
|
|
248
248
|
// checker has already reported and lowering carries on past.
|
|
249
|
-
for (const [name, width] of Object.entries(DERIVED_INPUTS)) {
|
|
249
|
+
for (const [name, width] of Object.entries({ ...DERIVED_INPUTS, ...STEP_INPUTS })) {
|
|
250
250
|
let value: LV | undefined
|
|
251
251
|
global.declare(name, {
|
|
252
252
|
width: width as SLType,
|
|
253
|
-
kind: undefined,
|
|
253
|
+
kind: name === "frame" ? "int" : undefined,
|
|
254
254
|
get value() { return value ??= (inputs as never)[name] },
|
|
255
255
|
set value(v: LV) { value = v },
|
|
256
256
|
})
|
|
@@ -289,6 +289,15 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
|
|
|
289
289
|
}
|
|
290
290
|
|
|
291
291
|
for (const t of unit.textures) samplers.set(t.name, at(t.pos, () => sl.texture(t.name)))
|
|
292
|
+
// Unless the file declared a texture of its own by the name. The
|
|
293
|
+
// checker refuses tex2Dlod on it, and anything else that took the
|
|
294
|
+
// name, before lowering could reach either.
|
|
295
|
+
if (!samplers.has(PREVIOUS)) {
|
|
296
|
+
samplers.set(PREVIOUS, {
|
|
297
|
+
sample: (uv) => inputs.previous.sample(uv),
|
|
298
|
+
sampleLevel: () => { throw new Error("tex2Dlod(previous) reached lowering; the checker refuses it") },
|
|
299
|
+
})
|
|
300
|
+
}
|
|
292
301
|
|
|
293
302
|
for (const c of unit.consts) {
|
|
294
303
|
const t = typeOf(c.type)
|
package/src/lang/parser.ts
CHANGED
|
@@ -636,7 +636,7 @@ function zeroOf(type: TypeName, at: Pos): Expr {
|
|
|
636
636
|
* inside braces that are the case's last statement, as in `case 0: { ...; break; }`.
|
|
637
637
|
*/
|
|
638
638
|
function dropClosingBreak(body: Stmt[]): boolean {
|
|
639
|
-
const last = body.
|
|
639
|
+
const last = body[body.length - 1]
|
|
640
640
|
if (last?.k === "break") {
|
|
641
641
|
body.pop()
|
|
642
642
|
return true
|
package/src/ops.ts
CHANGED
|
@@ -39,7 +39,7 @@ export const TEXTURE_SLOTS = 4
|
|
|
39
39
|
|
|
40
40
|
/** Input ids, which the web emitter switches on. */
|
|
41
41
|
export const INPUT_ID: Record<string, number> = {
|
|
42
|
-
uv: 0, fragCoord: 1, resolution: 2, time: 3, aspect: 4,
|
|
42
|
+
uv: 0, fragCoord: 1, resolution: 2, time: 3, aspect: 4, frame: 5, deltaTime: 6,
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export const SLOP = {
|
|
@@ -146,6 +146,9 @@ export const SLOP = {
|
|
|
146
146
|
// Sampling
|
|
147
147
|
SAMPLE: 144,
|
|
148
148
|
SAMPLE_LOD: 145,
|
|
149
|
+
// What this program drew the frame before, at a uv (IR 5). No slot: the
|
|
150
|
+
// host keeps it, and a program that never reads it has none.
|
|
151
|
+
SAMPLE_PREVIOUS: 146,
|
|
149
152
|
} as const
|
|
150
153
|
|
|
151
154
|
export type SLOpCode = (typeof SLOP)[keyof typeof SLOP]
|
|
@@ -187,7 +190,7 @@ export const SL_ARITY: Record<number, number> = {
|
|
|
187
190
|
[SLOP.NOISE]: 1, [SLOP.SIMPLEX]: 1, [SLOP.FBM]: -1, [SLOP.SDF]: -1,
|
|
188
191
|
[SLOP.VORONOI]: 1, [SLOP.TURBULENCE]: -1, [SLOP.RIDGED]: -1,
|
|
189
192
|
|
|
190
|
-
[SLOP.SAMPLE]: 1, [SLOP.SAMPLE_LOD]: 2,
|
|
193
|
+
[SLOP.SAMPLE]: 1, [SLOP.SAMPLE_LOD]: 2, [SLOP.SAMPLE_PREVIOUS]: 1,
|
|
191
194
|
}
|
|
192
195
|
|
|
193
196
|
/** Name per opcode, for error messages and for the HLSL emitter's comments. */
|
|
@@ -230,7 +233,7 @@ export interface SLSurface {
|
|
|
230
233
|
|
|
231
234
|
export const SL_HLSL: Record<number, SLSurface> = {
|
|
232
235
|
[SLOP.CONST]: { syntax: "a number literal" },
|
|
233
|
-
[SLOP.INPUT]: { syntax: "uv, fragCoord, resolution, time, aspect" },
|
|
236
|
+
[SLOP.INPUT]: { syntax: "uv, fragCoord, resolution, time, aspect, frame, deltaTime" },
|
|
234
237
|
[SLOP.UNIFORM]: { syntax: "uniform <type> name = <default>;" },
|
|
235
238
|
[SLOP.COMPOSE]: { syntax: "float2(), float3(), float4()" },
|
|
236
239
|
[SLOP.SWIZZLE]: { syntax: ".xyzw / .rgba" },
|
|
@@ -312,6 +315,7 @@ export const SL_HLSL: Record<number, SLSurface> = {
|
|
|
312
315
|
|
|
313
316
|
[SLOP.SAMPLE]: { call: "tex2D" },
|
|
314
317
|
[SLOP.SAMPLE_LOD]: { call: "tex2Dlod" },
|
|
318
|
+
[SLOP.SAMPLE_PREVIOUS]: { syntax: "tex2D(previous, uv)" },
|
|
315
319
|
}
|
|
316
320
|
|
|
317
321
|
/**
|