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/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. */
@@ -162,7 +162,7 @@ export const BUILTIN_DOCS: Readonly<Record<string, string>> = {
162
162
  turbulence: "Simplex creases layered in octaves, 1 to 4: fire, smoke, marble.",
163
163
  ridged: "Turbulence made bright at its creases: ridges, lightning, cracks.",
164
164
  tex2D: "The colour of texture s at the uv t, with straight alpha.",
165
- tex2Dlod: "The colour of texture s at the uv t from mip level lod: 0 is the full size, 1 half, and so on.",
165
+ tex2Dlod: "Texture s at uv t from mip level lod: 0 is full size, 1 half, and a fraction blends the two.",
166
166
  sdf: "The signed distance from p to a shape, negative inside: `sdf.circle(p, r)`.",
167
167
  ramp: "t mapped through evenly spaced colour stops, from the first at 0 to the last at 1.",
168
168
  }
@@ -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) {
@@ -1003,8 +1037,8 @@ export interface Texture {
1003
1037
  sample(uv: Vec2): Vec4
1004
1038
  /**
1005
1039
  * The colour at `uv` from mip level `lod`: 0 is the full size texture, 1
1006
- * half, and a fraction blends two levels where the texture's filter does.
1007
- * A texture with no mips has only level 0.
1040
+ * half, and a fraction blends the two levels either side, whatever the
1041
+ * texture's filter mode. A texture with no mips has only level 0.
1008
1042
  */
1009
1043
  sampleLevel(uv: Vec2, lod: Num): Vec4
1010
1044
  }
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"
package/src/web.ts CHANGED
@@ -12,25 +12,34 @@
12
12
  * named `n<index>`. What differs is the contract with the host, which is fixed
13
13
  * and the same for both languages:
14
14
  *
15
- * sl_Res (target width, target height, seconds, 0)
16
- * sl_Opt (1 in a Linear colour space project else 0, 1 to flip uv.y, 0, 0)
15
+ * sl_Res (target width, target height, seconds, frame)
16
+ * sl_Opt (1 in a Linear colour space project else 0, 1 to flip uv.y, deltaTime, 0)
17
17
  * sl_U[16] uniform slots, each a vec4, in the program's slot order
18
18
  * textures by slot: GLSL `sl_Tex<slot>`; WGSL `sl_tex<slot>` at binding
19
19
  * 2 + 2 * slot with its own sampler `sl_samp<slot>` at 1 + 2 * slot,
20
20
  * all in group 0 beside the frame block at binding 0. Only the
21
21
  * slots the program samples are declared.
22
+ * the previous frame, only for a program that samples it: GLSL `sl_Prev`;
23
+ * WGSL `sl_prev` with its sampler `sl_prevSamp`, at the two
24
+ * bindings after the last texture slot's, whatever the program
25
+ * declares. Read at level 0 (`body.ts`, `BodyTarget.previous`),
26
+ * its y flipped when `sl_Opt.y` flips uv, so the texture the host
27
+ * drew last frame reads back the way it was written.
28
+ *
29
+ * `frame` and `deltaTime` ride in components that were 0, and a host sets
30
+ * them only for a program that reads them (`readsOf`).
22
31
  *
23
32
  * WGSL also carries its vertex stage (`sl_vs`, a full-target triangle from the
24
33
  * vertex index) so one module is one pipeline; the GLSL host supplies its own.
25
34
  */
26
35
 
27
36
  import { INT_LIMITS } from "./body"
28
- import { SLError, TYPE, valueAt, type NodeRef, type Program, type SLKind, type SLType, type ValueNode } from "./ir"
37
+ import { SLError, TYPE, isWhole, valueAt, type NodeRef, type Program, type SLKind, type SLType, type ValueNode } from "./ir"
29
38
  import { inVaryingFlow, local, printBody, structure, type Syntax } from "./structure"
30
39
  import { libClosure, libIndex, LIB_FUNCTIONS, SDF_CALLS } from "./lib"
31
40
  import { LIB_GLSL } from "./lib/glsl"
32
41
  import { LIB_WGSL } from "./lib/wgsl"
33
- import { INPUT_ID, SLOP, UNIFORM_SLOTS } from "./ops"
42
+ import { INPUT_ID, SLOP, TEXTURE_SLOTS, UNIFORM_SLOTS } from "./ops"
34
43
 
35
44
  export type WebLanguage = "glsl" | "wgsl"
36
45
 
@@ -50,6 +59,8 @@ function emitWeb(p: Program, lang: WebLanguage): string {
50
59
  const need = new Set<number>()
51
60
  const smoothsteps = new Set<SLType>()
52
61
  const sampled = new Set<number>()
62
+ /** Whether the body samples the previous frame, which declares its binding. */
63
+ let previous = false
53
64
  const shape = structure(p)
54
65
  const T = (t: SLType) => (W ? (t === 1 ? "f32" : `vec${t}f`) : t === 1 ? "float" : `vec${t}`)
55
66
  /** A value's type in this language, its kind included. */
@@ -90,7 +101,10 @@ function emitWeb(p: Program, lang: WebLanguage): string {
90
101
  case 1: return `(sl_uv * ${res}.xy)`
91
102
  case 2: return `${res}.xy`
92
103
  case 3: return `${res}.z`
93
- default: return `(${res}.x / max(${res}.y, 1.0))`
104
+ case 4: return `(${res}.x / max(${res}.y, 1.0))`
105
+ case 5: return `${res}.w`
106
+ case 6: return W ? "sl.opt.z" : "sl_Opt.z"
107
+ default: throw new Error(`no web mapping for the input "${n.name}"`)
94
108
  }
95
109
  }
96
110
  case "uniform": {
@@ -247,10 +261,24 @@ function emitWeb(p: Program, lang: WebLanguage): string {
247
261
  case SLOP.SAMPLE_LOD: {
248
262
  const slot = Math.round(imm[0] ?? 0)
249
263
  sampled.add(slot)
250
- return W
251
- ? `textureSampleLevel(sl_tex${slot}, sl_samp${slot}, ${a[0]}, ${a[1]})`
252
- : `textureLod(sl_Tex${slot}, ${a[0]}, ${a[1]})`
264
+ const at = (level: string) => W
265
+ ? `textureSampleLevel(sl_tex${slot}, sl_samp${slot}, ${a[0]}, ${level})`
266
+ : `textureLod(sl_Tex${slot}, ${a[0]}, ${level})`
267
+ // A fraction blends the level below and the level above,
268
+ // whatever the texture's filter between levels, as the HLSL
269
+ // emitter does (`isWhole`). Past the smallest level both read
270
+ // the smallest, as a whole level past it does.
271
+ if (isWhole(p.nodes, n.args[1]!)) return at(a[1]!)
272
+ const below = `floor(${a[1]})`
273
+ return `mix(${at(below)}, ${at(`${below} + 1.0`)}, fract(${a[1]}))`
253
274
  }
275
+ case SLOP.SAMPLE_PREVIOUS:
276
+ previous = true
277
+ // Flipped as this frame flipped the uv it wrote at, so a read at
278
+ // a pixel's own uv is that pixel on either backend.
279
+ return W
280
+ ? `textureSampleLevel(sl_prev, sl_prevSamp, ${a[0]} * vec2f(1.0, 1.0 - 2.0 * sl.opt.y) + vec2f(0.0, sl.opt.y), 0.0)`
281
+ : `textureLod(sl_Prev, ${a[0]} * vec2(1.0, 1.0 - 2.0 * sl_Opt.y) + vec2(0.0, sl_Opt.y), 0.0)`
254
282
  default:
255
283
  throw new SLError(
256
284
  `the ${lang.toUpperCase()} emitter has no case for opcode ${n.op}. A program using it would ` +
@@ -342,9 +370,12 @@ function emitWeb(p: Program, lang: WebLanguage): string {
342
370
  const textures = p.textures.filter((t) => sampled.has(t.slot))
343
371
 
344
372
  if (W) {
345
- const bindings = textures.map((t) =>
373
+ const bindings = [...textures.map((t) =>
346
374
  `@group(0) @binding(${1 + 2 * t.slot}) var sl_samp${t.slot}: sampler;\n` +
347
- `@group(0) @binding(${2 + 2 * t.slot}) var sl_tex${t.slot}: texture_2d<f32>;`).join("\n")
375
+ `@group(0) @binding(${2 + 2 * t.slot}) var sl_tex${t.slot}: texture_2d<f32>;`),
376
+ ...previous ? [
377
+ `@group(0) @binding(${1 + 2 * TEXTURE_SLOTS}) var sl_prevSamp: sampler;\n` +
378
+ `@group(0) @binding(${2 + 2 * TEXTURE_SLOTS}) var sl_prev: texture_2d<f32>;`] : []].join("\n")
348
379
  return `${header}
349
380
  struct SLFrame { res: vec4f, opt: vec4f, u: array<vec4f, ${WEB_UNIFORM_SLOTS}> }
350
381
  @group(0) @binding(0) var<uniform> sl: SLFrame;
@@ -362,7 +393,7 @@ ${body}
362
393
  }
363
394
  `
364
395
  }
365
- const samplers = textures.map((t) => `uniform sampler2D sl_Tex${t.slot};`).join("\n")
396
+ const samplers = [...textures.map((t) => `uniform sampler2D sl_Tex${t.slot};`), ...previous ? ["uniform sampler2D sl_Prev;"] : []].join("\n")
366
397
  return `#version 300 es
367
398
  ${header}
368
399
  precision highp float;