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/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, inputsUsed,
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
- fixed4 frag(v2f i) : SV_Target
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. `sl_toLinear` stays a
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 = InputName | DerivedInputName
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: InputName }
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. Dead nodes do not count: a
431
- * value computed and never used is not in the picture, so a program that does
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): InputName[] {
435
- const read = new Set<InputName>()
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 InputName[]).filter((name) => read.has(name))
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 = 4
823
+ export const SL_IR_VERSION = 5
737
824
 
738
825
  function fnv1a(s: string): string {
739
826
  let h = 0x811c9dc5
@@ -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. So do the calls that need an
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 (`iFrame`, `textureLod`, `dFdx`, a function returning
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 !== "a builtin" && why !== "a derived input") return 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 centered",
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) || uniforms.has(e.name) || consts.has(e.name)) return
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
- if (tex === undefined || tex.k !== "ident" || !textures.has(tex.name)) {
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)
@@ -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"])