onejs-sl 0.6.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/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[] }
@@ -454,17 +480,43 @@ 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
468
520
  }
469
521
 
470
522
  /** Structural key for hash consing. Order matters and is fixed by the node shape. */
@@ -681,10 +733,12 @@ const IR4_OPS = new Set<number>([
681
733
  * holds, and `fromJSON` moves it to the form IR 3 has.
682
734
  */
683
735
  function nodeVersion(n: SLNode): number {
736
+ if (n.k === "input" && n.name in STEP_INPUTS) return 5
684
737
  if (n.k === "if" || n.k === "loop" || n.k === "param" || n.k === "proj" || n.kind !== undefined) return 4
685
738
  if (n.k !== "call") return 2
686
739
  if (IR4_OPS.has(n.op)) return 4
687
740
  switch (n.op) {
741
+ case SLOP.SAMPLE_PREVIOUS: return 5
688
742
  case SLOP.SAMPLE_LOD: return 3
689
743
  case SLOP.SDF:
690
744
  case SLOP.FBM:
@@ -705,6 +759,8 @@ function nodeVersion(n: SLNode): number {
705
759
  * the shape id the one immediate
706
760
  * FBM, TURBULENCE, RIDGED the point and the octave count, FBM's kind the
707
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
708
764
  */
709
765
  export function formProblem(n: Extract<SLNode, { k: "call" }>, nodes: readonly SLNode[]): string | null {
710
766
  const widths = n.args.map((a) => { const v = nodes[a]!; return v.k === "if" || v.k === "loop" ? "control" : v.type }).join(",")
@@ -719,6 +775,9 @@ export function formProblem(n: Extract<SLNode, { k: "call" }>, nodes: readonly S
719
775
  if (widths === "2,1" && (n.imm?.length ?? 0) === rest) return null
720
776
  return `${SL_NAME[n.op]} takes a float2 and its octave count, a float`
721
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"
722
781
  default:
723
782
  return null
724
783
  }
@@ -759,8 +818,9 @@ export const SL_HASH_VERSION = 2
759
818
  * 3 SAMPLE_LOD; an SDF's shape parameters and a noise's octave count are
760
819
  * operands, where they were immediates
761
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
762
822
  */
763
- export const SL_IR_VERSION = 4
823
+ export const SL_IR_VERSION = 5
764
824
 
765
825
  function fnv1a(s: string): string {
766
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"])
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
  /**
package/src/params.ts CHANGED
@@ -20,7 +20,7 @@ import { BUILTINS } from "./lang/builtins"
20
20
  import { LIB_FUNCTIONS, SDF_CALLS } from "./lib/table"
21
21
  import { LIB_PARAM_NAMES } from "./lib/names"
22
22
  import { SL_SDF_PARAMS, SL_SDF_SHAPES, type SlSdfKind } from "./shapes"
23
- import type { SourceInputName } from "./ir"
23
+ import type { PREVIOUS, SourceInputName } from "./ir"
24
24
  import type { AttributeName } from "./lang/attributes"
25
25
 
26
26
  /** One function of the helper library, as its source declares it. */
@@ -186,12 +186,15 @@ export const ATTRIBUTE_DOCS: Readonly<Record<AttributeName, string>> = {
186
186
  * the output is a UI element in OneJS and an image on a Magerie board, and the
187
187
  * clock is whichever one the host runs.
188
188
  */
189
- export const INPUT_DOCS: Readonly<Record<SourceInputName, string>> = {
189
+ export const INPUT_DOCS: Readonly<Record<SourceInputName | typeof PREVIOUS, string>> = {
190
190
  uv: "The centre of this pixel in the output, 0 to 1 on each axis, with y up.",
191
191
  fragCoord: "The centre of this pixel in the output, in pixels, with y up: the first column is at 0.5.",
192
192
  resolution: "The output's size in pixels.",
193
193
  time: "The clock, in seconds.",
194
194
  aspect: "The output's width divided by its height.",
195
+ frame: "Frames drawn since previous was last cleared, from 0: an int, 0 exactly when previous is clear.",
196
+ deltaTime: "Seconds since the frame before; 0 when a frame redraws the same time.",
197
+ previous: "What this program drew the frame before, as a texture: tex2D(previous, uv). Clear on the first.",
195
198
  texel: "One pixel, in uv: 1 / resolution. Step by it to read a neighbouring pixel.",
196
199
  centered: "uv with 0 at the centre and x scaled by aspect, so a circle drawn in it stays round.",
197
200
  }
package/src/serial.ts CHANGED
@@ -9,11 +9,13 @@
9
9
  * older one is migrated to this one and rehashed under it.
10
10
  */
11
11
  import {
12
- INPUTS, SLError, SL_HASH_VERSION, SL_IR_VERSION, TYPE, checkCaps, controlOf, controlProblem, formProblem, hashProgram, intUniformProblem,
13
- programVersion, type NodeRef, type Program, type SLNode, type SLType, type TextureDecl, type UniformDecl,
12
+ INPUTS, STEP_INPUTS, SLError, SL_HASH_VERSION, SL_IR_VERSION, TYPE, checkCaps, controlOf, controlProblem, formProblem, hashProgram, intUniformProblem,
13
+ programVersion, type HostInputName, type NodeRef, type Program, type SLNode, type SLType, type TextureDecl, type UniformDecl,
14
14
  } from "./ir"
15
15
  import { SLOP, SL_ARITY, SL_NAME } from "./ops"
16
16
 
17
+ const HOST_INPUTS: Readonly<Record<HostInputName, SLType>> = { ...INPUTS, ...STEP_INPUTS }
18
+
17
19
  export interface ProgramJSON {
18
20
  /** `SL_IR_VERSION` of the writer. Absent means 1. */
19
21
  v: number
@@ -166,8 +168,10 @@ function node(n: unknown, i: number): SLNode {
166
168
  return withKind({ k: "const", type: t, v })
167
169
  }
168
170
  case "input": {
169
- const name = x.name as keyof typeof INPUTS
170
- if (!(name in INPUTS) || INPUTS[name] !== t) fail(`node ${i} reads an input "${String(x.name)}" that does not exist at that width`)
171
+ const name = x.name as HostInputName
172
+ if (!Object.prototype.hasOwnProperty.call(HOST_INPUTS, name) || HOST_INPUTS[name] !== t) {
173
+ fail(`node ${i} reads an input "${String(x.name)}" that does not exist at that width`)
174
+ }
171
175
  return { k: "input", type: t, name }
172
176
  }
173
177
  case "uniform": {
package/src/sl.ts CHANGED
@@ -24,8 +24,8 @@
24
24
 
25
25
  import { parseColor as parseHex } from "./color"
26
26
  import {
27
- Builder, INPUTS, RAMP_STOP_COMPUTED, SLError, TYPE, hashProgram, programVersion, truncateHeld, widthName, writtenColour,
28
- type InputName, type NodeRef, type Program, type SLKind, type SLNode, type SLType, type UniformControl,
27
+ Builder, INPUTS, RAMP_STOP_COMPUTED, STEP_INPUTS, SLError, TYPE, hashProgram, programVersion, truncateHeld, widthName, writtenColour,
28
+ type HostInputName, type InputName, type NodeRef, type Program, type SLKind, type SLNode, type SLType, type UniformControl,
29
29
  } from "./ir"
30
30
  import { SLOP, SL_HLSL, SL_NAME, type SLOpCode } from "./ops"
31
31
  import { SL_SDF_PARAMS, SL_SDF_SHAPES, type SlSdfKind } from "./shapes"
@@ -497,6 +497,26 @@ export interface ProgramInputs {
497
497
  texel: Vec2
498
498
  /** `(uv - 0.5) * vec2(aspect, 1)`: 0 at the centre, and a circle stays round. Recorded only if read. */
499
499
  centered: Vec2
500
+ /**
501
+ * Frames drawn since `previous` was last cleared, from 0: so `frame` is 0
502
+ * exactly when `previous` is clear. Recorded only if read, and the host
503
+ * counts frames only for a program that reads it (`compile(p).reads`).
504
+ */
505
+ frame: Int
506
+ /** Seconds from the frame before to this one; 0 when a frame redraws the same time. Recorded only if read. */
507
+ deltaTime: Float
508
+ /**
509
+ * What this program drew the frame before: transparent black on the first
510
+ * frame and after anything that clears it (a new size, a seek, time going
511
+ * back). Linear and straight alpha, as `main` returned it, bilinear and
512
+ * clamped at the edges. A host keeps it only for a program that samples it.
513
+ */
514
+ previous: PreviousFrame
515
+ }
516
+
517
+ /** The frame a program drew before this one. It has one level, so there is no `sampleLevel`. */
518
+ export interface PreviousFrame {
519
+ sample(uv: Vec2): Vec4
500
520
  }
501
521
 
502
522
  /**
@@ -515,11 +535,25 @@ export function program(fn: (inputs: ProgramInputs) => Vec4): Program {
515
535
  ;(inputs as any)[name] = mk(b, ref, width as SLType)
516
536
  }
517
537
  // Getters, so a program that never reads one records nothing for it
518
- // (DERIVED_INPUTS in ir.ts). A second read is hash consed to the first.
538
+ // (DERIVED_INPUTS and STEP_INPUTS in ir.ts). A second read is hash
539
+ // consed to the first.
540
+ const step = (name: keyof typeof STEP_INPUTS): Val =>
541
+ mk(b, b.add({ k: "input", type: STEP_INPUTS[name], name: name as HostInputName }), STEP_INPUTS[name])
519
542
  Object.defineProperties(inputs, {
520
543
  texel: { enumerable: true, get: () => vec2(1, 1).div(inputs.resolution) },
521
544
  centered: { enumerable: true, get: () => inputs.uv.sub(0.5).mul(vec2(inputs.aspect, 1)) },
545
+ // The host hands the count over as a float, which holds it exactly
546
+ // for as long as anything will run.
547
+ frame: { enumerable: true, get: () => convert(step("frame"), "int") },
548
+ deltaTime: { enumerable: true, get: () => step("deltaTime") },
522
549
  })
550
+ inputs.previous = {
551
+ sample(uv: Vec2): Vec4 {
552
+ if (uv.owner !== b) throw new SLError("a value from another program cannot be used in this one")
553
+ if (uv.width !== TYPE.VEC2 || uv.kind !== undefined) throw new SLError(`the previous frame is read at a float2, and this is ${uv.kind === undefined ? `a ${widthName(uv.width)}` : `an ${uv.kind}`}`)
554
+ return mk(b, b.call(SLOP.SAMPLE_PREVIOUS, TYPE.VEC4, [uv.ref]), TYPE.VEC4)
555
+ },
556
+ }
523
557
  const out = fn(inputs)
524
558
  if (!(out instanceof Val)) throw new SLError("a program must return an sl value, not " + typeof out)
525
559
  if (out.width !== TYPE.VEC4) {
package/src/structure.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  import { INPUTS, operands, reachable, type NodeRef, type Program, type SLNode } from "./ir"
19
- import { SLOP } from "./ops"
19
+ import { isSampling } from "./ops"
20
20
 
21
21
  export interface Region {
22
22
  id: number
@@ -171,7 +171,7 @@ function varyingValues(nodes: readonly SLNode[], reach: NodeRef[]): Set<NodeRef>
171
171
  switch (n.k) {
172
172
  case "input": if (PER_PIXEL.has(n.name)) mark(ref); break
173
173
  case "call":
174
- if (n.op === SLOP.SAMPLE || n.op === SLOP.SAMPLE_LOD || n.args.some((a) => out.has(a))) mark(ref)
174
+ if (isSampling(n.op) || n.args.some((a) => out.has(a))) mark(ref)
175
175
  break
176
176
  case "swizzle": if (out.has(n.src)) mark(ref); break
177
177
  case "proj": {
package/src/tables.ts CHANGED
@@ -13,8 +13,8 @@ export {
13
13
  SLOP, SL_ARITY, SL_NAME, SL_HLSL, SL_CALL_NAMES, SL_GLSL_HINT, SL_UNIMPLEMENTED,
14
14
  } from "./ops"
15
15
  export type { SLSurface } from "./ops"
16
- export { DERIVED_INPUTS, INPUTS, SOURCE_INPUTS } from "./ir"
17
- export type { DerivedInputName, InputName, SourceInputName } from "./ir"
16
+ export { DERIVED_INPUTS, INPUTS, PREVIOUS, SOURCE_INPUTS, STEP_INPUTS } from "./ir"
17
+ export type { DerivedInputName, HostInputName, InputName, SourceInputName, StepInputName } from "./ir"
18
18
  export { SL_SDF_SHAPES, SL_SDF_PARAMS } from "./shapes"
19
19
  export { ATTRIBUTE_DOCS, BUILTIN_DOCS, BUILTIN_PARAMS, INPUT_DOCS, LIB_SIGNATURES, SL_SDF_PARAM_NAMES } from "./params"
20
20
  export { ATTRIBUTE_NAMES } from "./lang/attributes"