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/lang/lower.ts CHANGED
@@ -39,8 +39,9 @@
39
39
  * `uv.mul(8)`, not as `uv.mul(sl.float(8))`, because the first broadcasts to a
40
40
  * float2 constant and the second builds a float plus a swizzle. Same picture,
41
41
  * different graph, different hash, so the distinction is load bearing rather
42
- * than an optimisation. An int constant is a bigint and a bool constant a
43
- * boolean for the same reason: folded until something needs a node.
42
+ * than an optimisation. An int constant is a bigint, a uint constant a `Uint`
43
+ * and a bool constant a boolean for the same reason: folded until something
44
+ * needs a node.
44
45
  *
45
46
  * A COMPARISON WAITS to be used (`Test`). Where arithmetic or a `?:` reads it,
46
47
  * it is the float that is 0 or 1 it always was, built the way it always was,
@@ -49,7 +50,7 @@
49
50
  */
50
51
 
51
52
  import {
52
- controlProblem, DERIVED_INPUTS, INPUTS, RAMP_STOP_COMPUTED, truncateHeld, writtenColour,
53
+ controlProblem, DERIVED_INPUTS, INPUTS, PREVIOUS, RAMP_STOP_COMPUTED, STEP_INPUTS, truncateHeld, writtenColour,
53
54
  type Program, type SLKind, type SLType, type UniformControl,
54
55
  } from "../ir"
55
56
  import { SLOP } from "../ops"
@@ -80,17 +81,30 @@ class Test {
80
81
  }
81
82
 
82
83
  /**
83
- * A lowered value. A number is a float, a bigint an int and a boolean a bool,
84
- * none of which has had to become a node yet; a Test is a comparison not yet
85
- * either of its forms.
84
+ * A uint constant, 0 to 2^32 - 1, not yet a node. A bigint is already an int,
85
+ * so a uint needs a form of its own to fold the way an int does.
86
86
  */
87
- type LV = Val | number | bigint | boolean | Test
87
+ class Uint {
88
+ readonly v: bigint
89
+ /**
90
+ * Any whole number, wrapped to 32 bits as a uint wraps. By a mask, not
91
+ * `BigInt.asUintN`, which QuickJS-ng answers with a negative from 2^31 up.
92
+ */
93
+ constructor(v: bigint) { this.v = v & 0xffffffffn }
94
+ }
95
+
96
+ /**
97
+ * A lowered value. A number is a float, a bigint an int, a Uint a uint and a
98
+ * boolean a bool, none of which has had to become a node yet; a Test is a
99
+ * comparison not yet either of its forms.
100
+ */
101
+ type LV = Val | number | bigint | Uint | boolean | Test
88
102
 
89
103
  /** A declared type: its width, and its kind when it is not floats. */
90
104
  interface VType { width: SLType; kind: SLKind | undefined }
91
105
 
92
106
  interface Binding extends VType {
93
- /** Always of the declared type: a Test is resolved and an int is a bigint or an int node. */
107
+ /** Always of the declared type: a Test is resolved and an int is a bigint or an int node, a uint a Uint or a uint node. */
94
108
  value: LV
95
109
  }
96
110
 
@@ -205,7 +219,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
205
219
  const blank = (t: VType, pos: Pos): LV => {
206
220
  if (t.kind === "int") return 0n
207
221
  if (t.kind === "bool") return false
208
- if (t.kind === "uint") return at(pos, () => sl.uint(0))
222
+ if (t.kind === "uint") return new Uint(0n)
209
223
  return t.width === 1 ? 0 : at(pos, () => compose(t.width, new Array<LV>(t.width).fill(0)))
210
224
  }
211
225
 
@@ -232,11 +246,11 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
232
246
  // Read through, so a program that never names one records nothing
233
247
  // for it. The setter only serves a refused assignment, which the
234
248
  // checker has already reported and lowering carries on past.
235
- for (const [name, width] of Object.entries(DERIVED_INPUTS)) {
249
+ for (const [name, width] of Object.entries({ ...DERIVED_INPUTS, ...STEP_INPUTS })) {
236
250
  let value: LV | undefined
237
251
  global.declare(name, {
238
252
  width: width as SLType,
239
- kind: undefined,
253
+ kind: name === "frame" ? "int" : undefined,
240
254
  get value() { return value ??= (inputs as never)[name] },
241
255
  set value(v: LV) { value = v },
242
256
  })
@@ -275,6 +289,15 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
275
289
  }
276
290
 
277
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
+ }
278
301
 
279
302
  for (const c of unit.consts) {
280
303
  const t = typeOf(c.type)
@@ -298,7 +321,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
298
321
  case "float3": return sl.uniform.vec3(name, [c[0]!, c[1]!, c[2]!], control) as unknown as Val
299
322
  case "float4": return sl.uniform.vec4(name, [c[0]!, c[1]!, c[2]!, c[3]!], control) as unknown as Val
300
323
  // A float slot, as every host binds, read as the whole number it holds.
301
- case "int": return sl.int(sl.uniform.float(name, c[0]!, control)) as unknown as Val
324
+ case "int": return sl.uniform.int(name, c[0]!, control) as unknown as Val
302
325
  case "uint":
303
326
  case "bool": throw new Error(`the checker let a ${type} uniform through`)
304
327
  }
@@ -585,7 +608,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
585
608
  const got = widthOf(v)
586
609
  if (got !== t.width) {
587
610
  if (fn === main) {
588
- const what = typeof v === "number" || typeof v === "bigint" ? "a single number" : `a ${widthType(got)}`
611
+ const what = isConstant(v) ? "a single number" : `a ${widthType(got)}`
589
612
  fail(`main returns a float4, a colour with alpha, and this returns ${what}. Wrap it: float4(value, 1)`, pos, 6)
590
613
  }
591
614
  fail(`${fn.name} is declared to return a ${fn.ret} and returns a ${widthType(got)}`, fn.pos)
@@ -755,8 +778,8 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
755
778
  if (c.labels.includes(null)) { otherwise = c.body; continue }
756
779
  let cond: Expr | null = null
757
780
  for (const l of c.labels as Expr[]) {
758
- const k = lowerExpr(l, scope, v instanceof Val && v.kind === "uint" ? "uint" : "int")
759
- const key = typeof k === "bigint" ? k : k instanceof Val ? constValue(k) : null
781
+ const k = lowerExpr(l, scope, kindWant(v))
782
+ const key = typeof k === "bigint" ? k : k instanceof Uint ? k.v : k instanceof Val ? constValue(k) : null
760
783
  if (key === null) fail("a case label is a whole number, or a const int", l.pos)
761
784
  if (seen.has(key)) fail(`this switch already has a case ${key}`, l.pos)
762
785
  seen.add(key)
@@ -795,13 +818,13 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
795
818
  }
796
819
  case "int":
797
820
  if (isInt(v)) return v
798
- if (v instanceof Val && v.kind === "uint") fail(`${name} is an int and this is a uint; convert it with int(...)`, pos)
821
+ if (isUint(v)) fail(`${name} is an int and this is a uint; convert it with int(...)`, pos)
799
822
  if (isBoolish(v)) fail(`${name} is an int and this is a bool; convert it with int(...)`, pos)
800
823
  fail(`${name} is an int and this is a ${widthType(widthOf(v))}; convert it with int(...), which truncates toward zero`, pos)
801
824
  break
802
825
  case "uint":
803
- if (v instanceof Val && v.kind === "uint") return v
804
- if (typeof v === "bigint" && v >= 0n) return at(pos, () => sl.uint(Number(v)) as unknown as Val)
826
+ if (isUint(v)) return v
827
+ if (typeof v === "bigint" && v >= 0n) return new Uint(v)
805
828
  fail(`${name} is a uint and this is ${describeLV(v)}; convert it with uint(...)`, pos)
806
829
  break
807
830
  case "bool": {
@@ -820,6 +843,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
820
843
  function toFloat(v: LV, pos: Pos): number | Val {
821
844
  if (typeof v === "number") return v
822
845
  if (typeof v === "bigint") return Number(v)
846
+ if (v instanceof Uint) return Number(v.v)
823
847
  if (typeof v === "boolean") return v ? 1 : 0
824
848
  if (v instanceof Test) return v.float
825
849
  return v.kind === undefined ? v : at(pos, () => sl.float(v) as unknown as Val)
@@ -839,6 +863,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
839
863
  if (typeof v === "boolean") return v
840
864
  if (typeof v === "number") return v >= 0.5
841
865
  if (typeof v === "bigint") return v !== 0n
866
+ if (v instanceof Uint) return v.v !== 0n
842
867
  if (v instanceof Test) return v.bool
843
868
  if (v.kind === "bool") return v
844
869
  return at(pos, () => (v.kind === undefined ? v.ge(0.5) : v.ne(0)) as unknown as Val)
@@ -898,9 +923,8 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
898
923
  function lowerExpr(e: Expr, scope: Scope, want?: "int" | "uint"): LV {
899
924
  switch (e.k) {
900
925
  case "num":
901
- if (e.unsigned === true) return at(e.pos, () => sl.uint(e.value) as unknown as Val)
926
+ if (e.unsigned === true || (e.whole && want === "uint")) return new Uint(BigInt(e.value))
902
927
  if (e.whole && want === "int") return BigInt.asIntN(32, BigInt(e.value))
903
- if (e.whole && want === "uint") return at(e.pos, () => sl.uint(e.value) as unknown as Val)
904
928
  return e.value
905
929
  case "bool": return e.value
906
930
  case "hex": return at(e.pos, () => sl.color(e.hex) as unknown as Val)
@@ -927,18 +951,22 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
927
951
  case "-":
928
952
  if (typeof v === "number") return -v
929
953
  if (typeof v === "bigint") return BigInt.asIntN(32, -v)
954
+ // As the node would: a uint negated is taken as a float.
955
+ if (v instanceof Uint) return -Number(v.v)
930
956
  if (v instanceof Val && v.kind !== "bool") return at(e.pos, () => v.neg())
931
957
  return at(e.pos, () => asVal(toFloat(v, e.pos)).neg())
932
958
  case "!":
933
959
  if (typeof v === "boolean") return !v
934
960
  if (typeof v === "number") return 1 - v
935
961
  if (typeof v === "bigint") return v === 0n
962
+ if (v instanceof Uint) return v.v === 0n
936
963
  if (v instanceof Test) return new Test(v.width, () => oneMinus(v.float, e.pos), () => notBool(v.bool))
937
964
  if (v.kind === "bool") return at(e.pos, () => v.not())
938
965
  if (v.kind === undefined) return oneMinus(v, e.pos)
939
966
  return at(e.pos, () => v.eq(0))
940
967
  case "~":
941
968
  if (typeof v === "bigint") return BigInt.asIntN(32, ~v)
969
+ if (v instanceof Uint) return new Uint(~v.v)
942
970
  if (v instanceof Val && (v.kind === "int" || v.kind === "uint")) return at(e.pos, () => v.bnot())
943
971
  fail(`~ flips the bits of an int or a uint, and this is ${describeLV(v)}`, e.pos)
944
972
  }
@@ -978,7 +1006,11 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
978
1006
  }
979
1007
  if (isUint(a) || isUint(b)) {
980
1008
  if (!isIntLike(a) || !isIntLike(b)) fail(`a uint only meets another uint here; convert with uint(...) or float(...)`, pos)
981
- return at(pos, () => intVal(a)[METHOD[op]](typeof b === "bigint" ? Number(b) : b as Val))
1009
+ if (a instanceof Uint && b instanceof Uint) {
1010
+ if ((op === "/" || op === "%") && b.v === 0n) return new Uint(0n)
1011
+ return new Uint(INT_ARITH[op](a.v, b.v))
1012
+ }
1013
+ return at(pos, () => intVal(a)[METHOD[op]](other(b)))
982
1014
  }
983
1015
  return floatArithmetic(op, toFloat(a, pos), toFloat(b, pos), pos)
984
1016
  }
@@ -1002,8 +1034,13 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
1002
1034
  if (!isIntLike(v)) fail(`${op} works on the bits of an int or a uint, and this is ${describeLV(v)}${isBoolish(v) ? "; use && or ||" : ""}`, pos)
1003
1035
  }
1004
1036
  if (typeof a === "bigint" && typeof b === "bigint") return BigInt.asIntN(32, INT_BITS[op](a, b))
1005
- const x = intVal(a as bigint | Val)
1006
- const y = typeof b === "bigint" ? Number(b) : b as Val
1037
+ // A shift's count may be either kind, and the value shifted keeps its own.
1038
+ const shift = op === "<<" || op === ">>"
1039
+ const count = typeof b === "bigint" ? b : b instanceof Uint ? b.v : null
1040
+ if (a instanceof Uint && count !== null && (shift || b instanceof Uint)) return new Uint(INT_BITS[op](a.v, count))
1041
+ if (typeof a === "bigint" && shift && b instanceof Uint) return BigInt.asIntN(32, INT_BITS[op](a, b.v))
1042
+ const x = intVal(a as bigint | Uint | Val)
1043
+ const y = other(b as bigint | Uint | Val)
1007
1044
  return at(pos, () => {
1008
1045
  switch (op) {
1009
1046
  case "&": return x.band(y)
@@ -1025,7 +1062,8 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
1025
1062
  return at(pos, () => intVal(a)[CMP_METHOD[op]!](typeof b === "bigint" ? Number(b) : b as Val) as unknown as Val)
1026
1063
  }
1027
1064
  if ((isUint(a) || isUint(b)) && isIntLike(a) && isIntLike(b)) {
1028
- return at(pos, () => intVal(a)[CMP_METHOD[op]!](typeof b === "bigint" ? Number(b) : b as Val) as unknown as Val)
1065
+ if (a instanceof Uint && b instanceof Uint) return INT_CMP[op]!(a.v, b.v)
1066
+ return at(pos, () => intVal(a)[CMP_METHOD[op]!](other(b)) as unknown as Val)
1029
1067
  }
1030
1068
  const fa = toFloat(a, pos)
1031
1069
  const fb = toFloat(b, pos)
@@ -1082,7 +1120,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
1082
1120
  /** `c ? a : b`: always a select, both sides computed. A float condition is the select it always was. */
1083
1121
  function lowerChoice(e: Extract<Expr, { k: "cond" }>, scope: Scope, want?: "int" | "uint"): LV {
1084
1122
  const cond = lowerExpr(e.cond, scope)
1085
- if (typeof cond === "boolean" || typeof cond === "number" || typeof cond === "bigint") {
1123
+ if (typeof cond === "boolean" || typeof cond === "number" || typeof cond === "bigint" || cond instanceof Uint) {
1086
1124
  return lowerExpr(boolOf(cond, e.pos) ? e.then : e.else, scope, want)
1087
1125
  }
1088
1126
  // Built before the sides, as it always was, so a program's nodes keep
@@ -1093,13 +1131,13 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
1093
1131
  let f = lowerExpr(e.else, scope, want)
1094
1132
  if (isIntLike(t) && typeof f === "number" && wholeExpr(e.else)) f = lowerExpr(e.else, scope, kindWant(t))
1095
1133
  if (isIntLike(f) && typeof t === "number" && wholeExpr(e.then)) t = lowerExpr(e.then, scope, kindWant(f))
1096
- const kinded = [t, f].some((v) => typeof v === "bigint" || typeof v === "boolean" || (v instanceof Val && v.kind !== undefined))
1134
+ const kinded = [t, f].some((v) => typeof v === "bigint" || v instanceof Uint || typeof v === "boolean" || (v instanceof Val && v.kind !== undefined))
1097
1135
  if (!kinded) {
1098
1136
  const c = cond instanceof Test ? cond.float : cond
1099
1137
  return at(e.pos, () => sl.select(c, toFloat(t, e.pos), toFloat(f, e.pos)))
1100
1138
  }
1101
1139
  const c = boolOf(cond, e.pos)
1102
- const side = (v: LV): Num => (typeof v === "bigint" ? sl.int(Number(v)) : typeof v === "boolean" ? sl.bool(v) : v instanceof Test ? v.float : v) as Num
1140
+ const side = (v: LV): Num => (isIntLike(v) ? intVal(v) : typeof v === "boolean" ? sl.bool(v) : v instanceof Test ? v.float : v) as Num
1103
1141
  return at(e.pos, () => sl.select(c, side(t), side(f)))
1104
1142
  }
1105
1143
 
@@ -1160,7 +1198,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
1160
1198
  // min, max, abs and clamp keep ints ints; everything else takes floats.
1161
1199
  if (INT_BUILTINS.has(n) && args.some(isIntLike) && args.every((v, i) => isIntLike(v) || (typeof v === "number" && wholeExpr(e.args[i]!)))) {
1162
1200
  const k = kindWant(args.find(isIntLike)!)
1163
- args = e.args.map((a) => lowerExpr(a, scope, k)).map((v) => (typeof v === "bigint" ? sl.int(Number(v)) : v) as LV)
1201
+ args = e.args.map((a) => lowerExpr(a, scope, k)).map((v) => (isIntLike(v) ? intVal(v) : v) as LV)
1164
1202
  return at(e.pos, () => builtin.lower!(args as Num[]))
1165
1203
  }
1166
1204
  const fs = args.map((v, i) => toFloat(v, e.args[i]!.pos))
@@ -1177,17 +1215,26 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
1177
1215
  const v = lowerExpr(e.args[0]!, scope, n === "bool" ? undefined : n)
1178
1216
  if (widthOf(v) !== 1) fail(`${n}(x) takes one component, and this is a ${widthType(widthOf(v))}; convert each one`, e.pos)
1179
1217
  const x = v instanceof Test ? v.bool : v
1218
+ // Between an int and a uint the bits are kept, as the GPU keeps them.
1180
1219
  if (n === "int") {
1181
1220
  if (typeof x === "bigint") return x
1221
+ if (x instanceof Uint) return BigInt.asIntN(32, x.v)
1182
1222
  if (typeof x === "number") return BigInt(truncateHeld(x, "int"))
1183
1223
  if (typeof x === "boolean") return x ? 1n : 0n
1184
1224
  }
1225
+ if (n === "uint") {
1226
+ if (x instanceof Uint) return x
1227
+ if (typeof x === "bigint") return new Uint(x)
1228
+ if (typeof x === "number") return new Uint(BigInt(truncateHeld(x, "uint")))
1229
+ if (typeof x === "boolean") return new Uint(x ? 1n : 0n)
1230
+ }
1185
1231
  if (n === "bool") {
1186
1232
  if (typeof x === "boolean") return x
1187
1233
  if (typeof x === "number") return x !== 0
1188
1234
  if (typeof x === "bigint") return x !== 0n
1235
+ if (x instanceof Uint) return x.v !== 0n
1189
1236
  }
1190
- const c = typeof x === "bigint" ? Number(x) : x
1237
+ const c = isIntLike(x) ? intVal(x) : x
1191
1238
  return at(e.pos, () => (n === "int" ? sl.int(c) : n === "uint" ? sl.uint(c) : sl.bool(c)) as unknown as Val)
1192
1239
  }
1193
1240
 
@@ -1238,7 +1285,7 @@ export function lower(checked: Checked, errors?: SLParseError[]): Program {
1238
1285
  }
1239
1286
 
1240
1287
  function compose(width: SLType, parts: LV[]): Val {
1241
- const ps = parts.map((p) => (p instanceof Test ? p.float : typeof p === "bigint" ? Number(p) : typeof p === "boolean" ? (p ? 1 : 0) : p)) as Num[]
1288
+ const ps = parts.map((p) => (p instanceof Test ? p.float : typeof p === "bigint" ? Number(p) : p instanceof Uint ? Number(p.v) : typeof p === "boolean" ? (p ? 1 : 0) : p)) as Num[]
1242
1289
  if (width === 2) return sl.vec2(...ps) as unknown as Val
1243
1290
  if (width === 3) return sl.vec3(...ps) as unknown as Val
1244
1291
  return sl.vec4(...ps) as unknown as Val
@@ -1284,6 +1331,12 @@ const INT_BITS: Record<BitOp, (a: bigint, b: bigint) => bigint> = {
1284
1331
  ">>": (a, b) => a >> (b & 31n),
1285
1332
  }
1286
1333
 
1334
+ /** Comparisons of two uints, whose bigints compare as the values do. */
1335
+ const INT_CMP: Record<string, (a: bigint, b: bigint) => boolean> = {
1336
+ "<": (a, b) => a < b, "<=": (a, b) => a <= b, ">": (a, b) => a > b,
1337
+ ">=": (a, b) => a >= b, "==": (a, b) => a === b, "!=": (a, b) => a !== b,
1338
+ }
1339
+
1287
1340
  const FOLD_CMP: Record<string, (a: number, b: number) => boolean> = {
1288
1341
  "<": (a, b) => a < b, "<=": (a, b) => a <= b, ">": (a, b) => a > b,
1289
1342
  ">=": (a, b) => a >= b, "==": (a, b) => a === b, "!=": (a, b) => a !== b,
@@ -1313,11 +1366,11 @@ function isInt(v: LV): v is bigint | Val {
1313
1366
  return typeof v === "bigint" || (v instanceof Val && v.kind === "int")
1314
1367
  }
1315
1368
 
1316
- function isUint(v: LV): v is Val {
1317
- return v instanceof Val && v.kind === "uint"
1369
+ function isUint(v: LV): v is Uint | Val {
1370
+ return v instanceof Uint || (v instanceof Val && v.kind === "uint")
1318
1371
  }
1319
1372
 
1320
- function isIntLike(v: LV): v is bigint | Val {
1373
+ function isIntLike(v: LV): v is bigint | Uint | Val {
1321
1374
  return isInt(v) || isUint(v)
1322
1375
  }
1323
1376
 
@@ -1325,25 +1378,37 @@ function isBoolish(v: LV): boolean {
1325
1378
  return typeof v === "boolean" || v instanceof Test || (v instanceof Val && v.kind === "bool")
1326
1379
  }
1327
1380
 
1328
- function isConstant(v: LV): v is number | bigint {
1329
- return typeof v === "number" || typeof v === "bigint"
1381
+ function isConstant(v: LV): v is number | bigint | Uint {
1382
+ return typeof v === "number" || typeof v === "bigint" || v instanceof Uint
1330
1383
  }
1331
1384
 
1332
- function kindWant(v: bigint | Val): "int" | "uint" {
1333
- return v instanceof Val && v.kind === "uint" ? "uint" : "int"
1385
+ function kindWant(v: LV): "int" | "uint" {
1386
+ return isUint(v) ? "uint" : "int"
1334
1387
  }
1335
1388
 
1336
1389
  function num(v: LV): number {
1337
- return typeof v === "bigint" ? Number(v) : v as number
1390
+ return typeof v === "bigint" ? Number(v) : v instanceof Uint ? Number(v.v) : v as number
1338
1391
  }
1339
1392
 
1340
- /** An int operand as a node the EDSL's int methods take. */
1341
- function intVal(v: bigint | Val): Val {
1342
- return typeof v === "bigint" ? sl.int(Number(v)) as unknown as Val : v
1393
+ /** An int or uint operand as a node the EDSL's int methods take. */
1394
+ function intVal(v: bigint | Uint | Val): Val {
1395
+ if (typeof v === "bigint") return sl.int(Number(v)) as unknown as Val
1396
+ if (v instanceof Uint) return sl.uint(Number(v.v)) as unknown as Val
1397
+ return v
1398
+ }
1399
+
1400
+ /**
1401
+ * The second operand of an int method: an int constant as a number, which
1402
+ * takes the first operand's kind, and a uint one as a node, so an int meeting
1403
+ * it is refused rather than made a uint.
1404
+ */
1405
+ function other(v: bigint | Uint | Val): number | Val {
1406
+ return typeof v === "bigint" ? Number(v) : intVal(v)
1343
1407
  }
1344
1408
 
1345
1409
  function asVal(v: LV): Val {
1346
1410
  if (v instanceof Val) return v
1411
+ if (v instanceof Uint) return intVal(v)
1347
1412
  return sl.float(typeof v === "number" ? v : num(v)) as unknown as Val
1348
1413
  }
1349
1414
 
@@ -1366,6 +1431,7 @@ function article(t: VType): string {
1366
1431
 
1367
1432
  function describeLV(v: LV): string {
1368
1433
  if (typeof v === "bigint") return "an int"
1434
+ if (v instanceof Uint) return "a uint"
1369
1435
  if (typeof v === "boolean" || v instanceof Test) return "a bool"
1370
1436
  if (typeof v === "number") return "a float"
1371
1437
  return v.kind === undefined ? `a ${widthType(v.width)}` : v.kind === "int" ? "an int" : `a ${v.kind}`
@@ -1380,6 +1446,7 @@ function constValue(v: Val): bigint | null {
1380
1446
  /** Two lowered values that are certainly the same, so a join can leave them out. */
1381
1447
  function same(a: LV, b: LV): boolean {
1382
1448
  if (a === b) return true
1449
+ if (a instanceof Uint && b instanceof Uint) return a.v === b.v
1383
1450
  return a instanceof Val && b instanceof Val && a.ref === b.ref && a.width === b.width && a.kind === b.kind
1384
1451
  }
1385
1452
 
@@ -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,
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": {
@@ -209,6 +213,12 @@ function uniform(u: unknown, i: number): UniformDecl {
209
213
  }
210
214
  const out: UniformDecl = { name: x.name!, type: x.type as SLType, value: x.value!.slice() }
211
215
  if (x.colour === true) out.colour = true
216
+ if (x.kind !== undefined) {
217
+ if (x.kind !== "int") fail(`uniform ${i} holds a "${String(x.kind)}"; a uniform is a float or an int`)
218
+ const problem = intUniformProblem(out.type, out.value)
219
+ if (problem !== null) fail(`uniform ${i}: ${problem}`)
220
+ out.kind = "int"
221
+ }
212
222
  // Its control: shapes checked here, sense checked by the one check the
213
223
  // compiler uses, so a hand edited file cannot give a host a range it would
214
224
  // choke on.
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) {
@@ -956,6 +990,15 @@ export const uniform = {
956
990
  const b = ctx()
957
991
  return mk(b, b.uniform(name, TYPE.FLOAT, [value], false, control), TYPE.FLOAT)
958
992
  },
993
+ /**
994
+ * An int, read as the whole number it holds. Its slot is a float, as every
995
+ * host binds one; the declaration says `kind: "int"`, so a host shows a
996
+ * whole number field for it.
997
+ */
998
+ int(name: string, value = 0, control: UniformControl = {}): Int {
999
+ const b = ctx()
1000
+ return int(mk(b, b.uniform(name, TYPE.FLOAT, [value], false, control, "int"), TYPE.FLOAT) as Float)
1001
+ },
959
1002
  vec2(name: string, value: [number, number] = [0, 0], control: UniformControl = {}): Vec2 {
960
1003
  const b = ctx()
961
1004
  return mk(b, b.uniform(name, TYPE.VEC2, value, false, control), TYPE.VEC2)
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"