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/CHANGELOG.md +22 -0
- package/README.md +65 -13
- package/goldens/goldens.json +80 -5
- package/package.json +2 -2
- package/src/body.ts +51 -9
- package/src/compile.ts +11 -1
- package/src/core.ts +4 -4
- package/src/hlsl.ts +21 -3
- package/src/ir.ts +108 -12
- package/src/lang/README.md +23 -12
- package/src/lang/check.ts +39 -7
- package/src/lang/classify.ts +2 -2
- package/src/lang/glsl.ts +2 -2
- package/src/lang/lower.ts +12 -3
- package/src/lang/parser.ts +1 -1
- package/src/ops.ts +7 -3
- package/src/params.ts +6 -3
- package/src/serial.ts +8 -4
- package/src/sl.ts +39 -5
- package/src/structure.ts +2 -2
- package/src/tables.ts +2 -2
- package/src/web.ts +42 -11
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: "
|
|
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
|
|
170
|
-
if (!(name
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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,
|
|
16
|
-
* sl_Opt (1 in a Linear colour space project else 0, 1 to flip uv.y,
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
? `textureSampleLevel(sl_tex${slot}, sl_samp${slot}, ${a[0]}, ${
|
|
252
|
-
: `textureLod(sl_Tex${slot}, ${a[0]}, ${
|
|
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>;`)
|
|
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;
|