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/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[] }
@@ -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 encoded. An author can produce them easily
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
- * encoding them would make the buffer disagree with its own hash about what the
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. Dead nodes do not count: a
458
- * value computed and never used is not in the picture, so a program that does
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): InputName[] {
462
- const read = new Set<InputName>()
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 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
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 = 4
859
+ export const SL_IR_VERSION = 5
764
860
 
765
861
  function fnv1a(s: string): string {
766
862
  let h = 0x811c9dc5
@@ -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, `select`, inlining |
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
- An empty manifest is written only over one that is already there. Deleting the
72
- last `.sl` file has to stop its shaders being generated, and a project that has
73
- never had one should not find a new file beside its bundle.
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, different hash. Write the vector on the left when it matters.
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 `int`, since float would change what
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. So do the calls that need an
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, and an
271
- `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.
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 !== "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"])
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)
@@ -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.at(-1)
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
  /**