onejs-sl 0.5.0 → 0.7.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 +27 -0
- package/README.md +50 -6
- package/goldens/goldens.json +69 -2
- package/package.json +2 -2
- package/src/body.ts +38 -5
- package/src/compile.ts +11 -1
- package/src/core.ts +4 -4
- package/src/hlsl.ts +21 -3
- package/src/ir.ts +98 -11
- package/src/lang/README.md +4 -3
- 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 +108 -41
- package/src/lang/parser.ts +1 -1
- package/src/ops.ts +7 -3
- package/src/params.ts +5 -2
- package/src/serial.ts +14 -4
- package/src/sl.ts +46 -3
- package/src/structure.ts +2 -2
- package/src/tables.ts +2 -2
- package/src/web.ts +31 -7
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[] }
|
|
@@ -225,6 +251,27 @@ export interface UniformDecl extends UniformControl {
|
|
|
225
251
|
* conversion is already in the graph, so it is not part of the hash.
|
|
226
252
|
*/
|
|
227
253
|
colour?: true
|
|
254
|
+
/**
|
|
255
|
+
* An int: `value` is a whole number and the program reads it as one, so a
|
|
256
|
+
* host shows a whole number field rather than a float's. The slot is a
|
|
257
|
+
* float, as every host binds it, and `type` is FLOAT. Set by `uniform int`
|
|
258
|
+
* in a `.sl` file or `sl.uniform.int`. Metadata for a host like `colour`:
|
|
259
|
+
* the reads' conversion to an int is already in the graph, so it is not
|
|
260
|
+
* part of the hash. A float uniform has none.
|
|
261
|
+
*/
|
|
262
|
+
kind?: "int"
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** What is wrong with an int uniform of `type` and default `value`, or null. The one check, for the builder and `fromJSON`. */
|
|
266
|
+
export function intUniformProblem(type: SLType, value: readonly number[]): string | null {
|
|
267
|
+
if (type !== TYPE.FLOAT) return `an int uniform is one number, and this is a ${widthName(type)}`
|
|
268
|
+
const v = value[0]!
|
|
269
|
+
// Its slot is a float, and a read is held to INT_BOUNDS: a default the
|
|
270
|
+
// program would read as another number is refused rather than shown.
|
|
271
|
+
if (!Number.isInteger(v) || Math.fround(v) !== v || v < INT_BOUNDS.int[0] || v > INT_BOUNDS.int[1]) {
|
|
272
|
+
return `an int uniform's default is a whole number its float slot holds exactly, and this is ${v}`
|
|
273
|
+
}
|
|
274
|
+
return null
|
|
228
275
|
}
|
|
229
276
|
|
|
230
277
|
export interface TextureDecl {
|
|
@@ -358,7 +405,7 @@ export class Builder {
|
|
|
358
405
|
return this.add(node)
|
|
359
406
|
}
|
|
360
407
|
|
|
361
|
-
uniform(name: string, type: SLType, value: number[], colour = false, control: UniformControl = {}): NodeRef {
|
|
408
|
+
uniform(name: string, type: SLType, value: number[], colour = false, control: UniformControl = {}, kind?: "int"): NodeRef {
|
|
362
409
|
const existing = this.uniforms.findIndex((u) => u.name === name)
|
|
363
410
|
if (existing >= 0) {
|
|
364
411
|
const u = this.uniforms[existing]
|
|
@@ -368,6 +415,9 @@ export class Builder {
|
|
|
368
415
|
if ((u.colour === true) !== colour) {
|
|
369
416
|
throw new SLError(`uniform "${name}" is declared both as a colour and as plain numbers`)
|
|
370
417
|
}
|
|
418
|
+
if (u.kind !== kind) {
|
|
419
|
+
throw new SLError(`uniform "${name}" is declared as both an int and a float`)
|
|
420
|
+
}
|
|
371
421
|
// A second read may say nothing about the control, or the same thing again.
|
|
372
422
|
const merged = { ...controlOf(u) }
|
|
373
423
|
for (const f of CONTROL_FIELDS) {
|
|
@@ -384,10 +434,13 @@ export class Builder {
|
|
|
384
434
|
}
|
|
385
435
|
const problem = controlProblem(type, value, control)
|
|
386
436
|
if (problem !== null) throw new SLError(`uniform "${name}": ${problem.message}`)
|
|
437
|
+
const kindProblem = kind === undefined ? null : intUniformProblem(type, value)
|
|
438
|
+
if (kindProblem !== null) throw new SLError(`uniform "${name}": ${kindProblem}`)
|
|
387
439
|
const slot = this.uniforms.length
|
|
388
440
|
if (slot >= UNIFORM_SLOTS) throw new SLError(tooManyUniforms(slot + 1, "program"))
|
|
389
441
|
const decl: UniformDecl = { name, type, value: value.slice() }
|
|
390
442
|
if (colour) decl.colour = true
|
|
443
|
+
if (kind !== undefined) decl.kind = kind
|
|
391
444
|
Object.assign(decl, controlOf(control))
|
|
392
445
|
this.uniforms.push(decl)
|
|
393
446
|
return this.add({ k: "uniform", type, slot })
|
|
@@ -427,17 +480,43 @@ export function reachable(nodes: SLNode[], result: NodeRef): NodeRef[] {
|
|
|
427
480
|
}
|
|
428
481
|
|
|
429
482
|
/**
|
|
430
|
-
* The inputs a program reads, in `INPUTS` order
|
|
431
|
-
* value computed and never used is not in the
|
|
432
|
-
* 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.
|
|
433
486
|
*/
|
|
434
|
-
export function inputsUsed(p: Program):
|
|
435
|
-
const read = new Set<
|
|
487
|
+
export function inputsUsed(p: Program): HostInputName[] {
|
|
488
|
+
const read = new Set<HostInputName>()
|
|
436
489
|
for (const ref of reachable(p.nodes, p.result)) {
|
|
437
490
|
const n = p.nodes[ref]
|
|
438
491
|
if (n.k === "input") read.add(n.name)
|
|
439
492
|
}
|
|
440
|
-
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
|
|
441
520
|
}
|
|
442
521
|
|
|
443
522
|
/** Structural key for hash consing. Order matters and is fixed by the node shape. */
|
|
@@ -654,10 +733,12 @@ const IR4_OPS = new Set<number>([
|
|
|
654
733
|
* holds, and `fromJSON` moves it to the form IR 3 has.
|
|
655
734
|
*/
|
|
656
735
|
function nodeVersion(n: SLNode): number {
|
|
736
|
+
if (n.k === "input" && n.name in STEP_INPUTS) return 5
|
|
657
737
|
if (n.k === "if" || n.k === "loop" || n.k === "param" || n.k === "proj" || n.kind !== undefined) return 4
|
|
658
738
|
if (n.k !== "call") return 2
|
|
659
739
|
if (IR4_OPS.has(n.op)) return 4
|
|
660
740
|
switch (n.op) {
|
|
741
|
+
case SLOP.SAMPLE_PREVIOUS: return 5
|
|
661
742
|
case SLOP.SAMPLE_LOD: return 3
|
|
662
743
|
case SLOP.SDF:
|
|
663
744
|
case SLOP.FBM:
|
|
@@ -678,6 +759,8 @@ function nodeVersion(n: SLNode): number {
|
|
|
678
759
|
* the shape id the one immediate
|
|
679
760
|
* FBM, TURBULENCE, RIDGED the point and the octave count, FBM's kind the
|
|
680
761
|
* one immediate
|
|
762
|
+
* SAMPLE_PREVIOUS a float4 at a float2, and no immediates: there is one
|
|
763
|
+
* previous frame, so there is no slot to name
|
|
681
764
|
*/
|
|
682
765
|
export function formProblem(n: Extract<SLNode, { k: "call" }>, nodes: readonly SLNode[]): string | null {
|
|
683
766
|
const widths = n.args.map((a) => { const v = nodes[a]!; return v.k === "if" || v.k === "loop" ? "control" : v.type }).join(",")
|
|
@@ -692,6 +775,9 @@ export function formProblem(n: Extract<SLNode, { k: "call" }>, nodes: readonly S
|
|
|
692
775
|
if (widths === "2,1" && (n.imm?.length ?? 0) === rest) return null
|
|
693
776
|
return `${SL_NAME[n.op]} takes a float2 and its octave count, a float`
|
|
694
777
|
}
|
|
778
|
+
case SLOP.SAMPLE_PREVIOUS:
|
|
779
|
+
if (widths === "2" && n.type === 4 && n.kind === undefined && (n.imm?.length ?? 0) === 0) return null
|
|
780
|
+
return "the previous frame is a float4 read at a float2"
|
|
695
781
|
default:
|
|
696
782
|
return null
|
|
697
783
|
}
|
|
@@ -732,8 +818,9 @@ export const SL_HASH_VERSION = 2
|
|
|
732
818
|
* 3 SAMPLE_LOD; an SDF's shape parameters and a noise's octave count are
|
|
733
819
|
* operands, where they were immediates
|
|
734
820
|
* 4 control flow (if, loop, their params and projs), ints, uints and bools
|
|
821
|
+
* 5 the previous frame (SAMPLE_PREVIOUS), and the frame and deltaTime inputs
|
|
735
822
|
*/
|
|
736
|
-
export const SL_IR_VERSION =
|
|
823
|
+
export const SL_IR_VERSION = 5
|
|
737
824
|
|
|
738
825
|
function fnv1a(s: string): string {
|
|
739
826
|
let h = 0x811c9dc5
|
package/src/lang/README.md
CHANGED
|
@@ -245,7 +245,8 @@ It rewrites tokens and keeps everything else: comments, layout and names stay
|
|
|
245
245
|
where they were, so the result reads as the shader it came from. What it does:
|
|
246
246
|
|
|
247
247
|
- The spellings that mean the same (`vec3`, `mix`, `fract`, `texture`,
|
|
248
|
-
`iTime`, `gl_FragCoord`) become the HLSL ones
|
|
248
|
+
`iTime`, `iFrame`, `iTimeDelta`, `gl_FragCoord`) become the HLSL ones and
|
|
249
|
+
the inputs. So do the calls that need an
|
|
249
250
|
expression: `mod` floors (`a - b * floor(a / b)`, never `%`, which
|
|
250
251
|
truncates), and `atan`, `inversesqrt`, `radians`, `degrees`, `exp2` and
|
|
251
252
|
`log2` become what they compute.
|
|
@@ -269,8 +270,8 @@ where they were, so the result reads as the shader it came from. What it does:
|
|
|
269
270
|
becomes a const; any other directive is kept as a comment, with a note.
|
|
270
271
|
- A name the language already means something by gets a `_` suffix, and an
|
|
271
272
|
`int` becomes a float (a for-loop counter stays an int), each with a note.
|
|
272
|
-
- What has no counterpart (`
|
|
273
|
-
nothing) is left in place with a note, for `errors` to point at.
|
|
273
|
+
- What has no counterpart (`iFrameRate`, `texelFetch`, `dFdx`, a function
|
|
274
|
+
returning nothing) is left in place with a note, for `errors` to point at.
|
|
274
275
|
|
|
275
276
|
It is not a GLSL parser, and is not meant to be: a shader it cannot follow comes
|
|
276
277
|
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"])
|