onejs-sl 0.3.0 → 0.5.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 +29 -0
- package/README.md +70 -28
- package/goldens/goldens.json +147 -63
- package/package.json +1 -1
- package/src/body.ts +132 -26
- package/src/core.ts +1 -1
- package/src/hlsl.ts +7 -3
- package/src/ir.ts +293 -33
- package/src/lang/README.md +2 -2
- package/src/lang/ast.ts +37 -7
- package/src/lang/builtins.ts +5 -17
- package/src/lang/check.ts +179 -70
- package/src/lang/glsl.ts +6 -10
- package/src/lang/lexer.ts +30 -6
- package/src/lang/lower.ts +1034 -289
- package/src/lang/parser.ts +146 -68
- package/src/lang/words.ts +5 -6
- package/src/ops.ts +48 -4
- package/src/params.ts +2 -0
- package/src/serial.ts +100 -17
- package/src/sl.ts +343 -33
- package/src/structure.ts +285 -0
- package/src/web.ts +152 -33
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "onejs-sl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "The OneJS shader language: a small typed language for per pixel programs, compiled to a portable IR and emitted as HLSL, WGSL or GLSL ES",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
package/src/body.ts
CHANGED
|
@@ -21,10 +21,11 @@
|
|
|
21
21
|
* gives common subexpression elimination for free.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { SLError, TYPE, type InputName, type Program, type
|
|
24
|
+
import { INT_BOUNDS, SLError, TYPE, valueAt, type InputName, type NodeRef, type Program, type SLKind, type SLType, type ValueNode } from "./ir"
|
|
25
25
|
import { libClosure, LIB_FUNCTIONS } from "./lib"
|
|
26
26
|
import { LIB_HLSL } from "./lib/hlsl"
|
|
27
27
|
import { SLOP } from "./ops"
|
|
28
|
+
import { inVaryingFlow, local, printBody, structure, type Syntax } from "./structure"
|
|
28
29
|
|
|
29
30
|
export interface BodyTarget {
|
|
30
31
|
/** An expression for each input, e.g. `{ uv: "SL_UV", time: "SL_TIME", ... }`. */
|
|
@@ -51,6 +52,14 @@ export interface BodyTarget {
|
|
|
51
52
|
* as it is, and a host that composites premultiplied multiplies on write.
|
|
52
53
|
*/
|
|
53
54
|
sample: (slot: number, uv: string) => string
|
|
55
|
+
/**
|
|
56
|
+
* A float4 sample of the texture in `slot` at `uv` from mip level `lod`, a
|
|
57
|
+
* float expression: 0 is the full size texture, 1 half, and a fraction
|
|
58
|
+
* blends two levels where the texture's filter does. Otherwise the same
|
|
59
|
+
* contract as `sample`. `tex2Dlod` in a program; HLSL's
|
|
60
|
+
* `tex2Dlod(s, float4(uv, 0, lod))` and Metal's `sample(s, uv, level(lod))`.
|
|
61
|
+
*/
|
|
62
|
+
sampleLevel: (slot: number, uv: string, lod: string) => string
|
|
54
63
|
/**
|
|
55
64
|
* `linear`: `toLinear` calls `sl_toLinear`, for a host whose target holds
|
|
56
65
|
* linear light. `gamma`: `toLinear` is the identity, decided here, so hex
|
|
@@ -59,6 +68,12 @@ export interface BodyTarget {
|
|
|
59
68
|
colour: "gamma" | "linear"
|
|
60
69
|
/** Assign the result to this local instead of returning it. */
|
|
61
70
|
result?: string
|
|
71
|
+
/**
|
|
72
|
+
* Put before every loop the body prints: `[loop]` for HLSL, so a compiler
|
|
73
|
+
* does not try to unroll a loop whose turns are capped only by a counter.
|
|
74
|
+
* Nothing for Metal, which has no such attribute.
|
|
75
|
+
*/
|
|
76
|
+
loopAttribute?: string
|
|
62
77
|
/** Put in front of every line. Default four spaces. */
|
|
63
78
|
indent?: string
|
|
64
79
|
}
|
|
@@ -77,6 +92,25 @@ export interface Body {
|
|
|
77
92
|
|
|
78
93
|
const HLSL_TYPE: Record<SLType, string> = { 1: "float", 2: "float2", 3: "float3", 4: "float4" }
|
|
79
94
|
|
|
95
|
+
/** A value's HLSL type, which Metal spells the same through a host's defines. */
|
|
96
|
+
function typeName(n: { type: SLType; kind?: SLKind }): string {
|
|
97
|
+
if (n.kind === undefined) return HLSL_TYPE[n.type]
|
|
98
|
+
return n.type === 1 ? n.kind : `${n.kind}${n.type}`
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** An int, uint or bool constant as a literal. */
|
|
102
|
+
function kindLit(kind: SLKind, v: number): string {
|
|
103
|
+
if (kind === "bool") return v !== 0 ? "true" : "false"
|
|
104
|
+
if (kind === "uint") return `${v >>> 0}u`
|
|
105
|
+
return v < 0 ? `(${v})` : String(v)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The int range a float is held to on its way to an int, so every backend truncates the same. */
|
|
109
|
+
export const INT_LIMITS = {
|
|
110
|
+
int: INT_BOUNDS.int.map((n) => lit(n)),
|
|
111
|
+
uint: INT_BOUNDS.uint.map((n) => lit(n)),
|
|
112
|
+
} as const
|
|
113
|
+
|
|
80
114
|
/** A literal that survives a float32 round trip and never reads as an int. */
|
|
81
115
|
export function lit(n: number): string {
|
|
82
116
|
if (!Number.isFinite(n)) throw new SLError(`cannot emit ${n} as a shader literal`)
|
|
@@ -92,19 +126,25 @@ const SWZ = "xyzw"
|
|
|
92
126
|
|
|
93
127
|
export function emitBody(p: Program, target: BodyTarget): Body {
|
|
94
128
|
const indent = target.indent ?? " "
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
const emitted = new Set<number>()
|
|
129
|
+
const name = local
|
|
130
|
+
const shape = structure(p)
|
|
98
131
|
const uniforms = new Set<number>()
|
|
99
132
|
const textures = new Set<number>()
|
|
100
133
|
const helpers = new Set<string>()
|
|
101
134
|
const helper = (fn: string) => { helpers.add(fn); return fn }
|
|
102
135
|
/** A node at width `w`: a scalar repeated into a constructor, since HLSL refuses float3(x) and Metal x.xxx. */
|
|
103
|
-
|
|
136
|
+
/** A constant node's value, or null for anything computed. */
|
|
137
|
+
const constantOf = (ref: number): number[] | null => {
|
|
138
|
+
const c = p.nodes[ref]!
|
|
139
|
+
return c.k === "const" ? c.v : null
|
|
140
|
+
}
|
|
141
|
+
const splat = (ref: number, w: SLType) => (valueAt(p.nodes, ref).type === TYPE.FLOAT && w > 1 ? ctor(w, Array(w).fill(name(ref))) : name(ref))
|
|
104
142
|
|
|
105
|
-
const expr = (
|
|
143
|
+
const expr = (ref: NodeRef): string => {
|
|
144
|
+
const n = valueAt(p.nodes, ref)
|
|
106
145
|
switch (n.k) {
|
|
107
146
|
case "const":
|
|
147
|
+
if (n.kind !== undefined) return n.type === 1 ? kindLit(n.kind, n.v[0]!) : `${typeName(n)}(${n.v.map((v) => kindLit(n.kind!, v)).join(", ")})`
|
|
108
148
|
return ctor(n.type, n.v.map(lit))
|
|
109
149
|
case "input":
|
|
110
150
|
return target.inputs[n.name]
|
|
@@ -117,14 +157,18 @@ export function emitBody(p: Program, target: BodyTarget): Body {
|
|
|
117
157
|
}
|
|
118
158
|
case "swizzle":
|
|
119
159
|
// Metal has no swizzle of a scalar, so a scalar widens by constructor.
|
|
120
|
-
if (p.nodes
|
|
160
|
+
if (valueAt(p.nodes, n.src).type === TYPE.FLOAT) {
|
|
161
|
+
return n.type === 1 ? name(n.src) : `${typeName(n)}(${n.chans.map(() => name(n.src)).join(", ")})`
|
|
162
|
+
}
|
|
121
163
|
return `${name(n.src)}.${n.chans.map((c) => SWZ[c]).join("")}`
|
|
122
164
|
case "call":
|
|
123
|
-
return call(n)
|
|
165
|
+
return call(n, ref)
|
|
166
|
+
default:
|
|
167
|
+
throw new SLError(`the HLSL emitter reached a ${n.k} as an expression`)
|
|
124
168
|
}
|
|
125
169
|
}
|
|
126
170
|
|
|
127
|
-
const call = (n: Extract<
|
|
171
|
+
const call = (n: Extract<ValueNode, { k: "call" }>, ref: NodeRef): string => {
|
|
128
172
|
const a = n.args.map(name)
|
|
129
173
|
const t = n.type
|
|
130
174
|
// Every argument of an element wise intrinsic at the result's width,
|
|
@@ -133,10 +177,55 @@ export function emitBody(p: Program, target: BodyTarget): Body {
|
|
|
133
177
|
// max(float, 0) is ambiguous, so the shared subset spells both out.
|
|
134
178
|
const s = n.args.map((r) => splat(r, t))
|
|
135
179
|
const k = (v: number, w: SLType) => ctor(w, Array(w).fill(lit(v)))
|
|
136
|
-
const w0 = n.args.length > 0 ? p.nodes
|
|
180
|
+
const w0 = n.args.length > 0 ? valueAt(p.nodes, n.args[0]!).type : TYPE.FLOAT
|
|
137
181
|
const imm = n.imm ?? []
|
|
182
|
+
const argKind = n.args.length > 0 ? valueAt(p.nodes, n.args[0]!).kind : undefined
|
|
183
|
+
// IR 4: the ops that take ints, uints and bools, by the kinds involved.
|
|
184
|
+
if (n.kind !== undefined || argKind !== undefined) {
|
|
185
|
+
const zero = kindLit(n.kind ?? "int", 0)
|
|
186
|
+
switch (n.op) {
|
|
187
|
+
case SLOP.ADD: return `(${a[0]} + ${a[1]})`
|
|
188
|
+
case SLOP.SUB: return `(${a[0]} - ${a[1]})`
|
|
189
|
+
case SLOP.MUL: return `(${a[0]} * ${a[1]})`
|
|
190
|
+
// Truncating, and 0 for a zero divisor, which HLSL leaves undefined.
|
|
191
|
+
case SLOP.DIV: return `(${a[1]} == ${zero} ? ${zero} : ${a[0]} / ${a[1]})`
|
|
192
|
+
case SLOP.MOD: return `(${a[1]} == ${zero} ? ${zero} : ${a[0]} % ${a[1]})`
|
|
193
|
+
case SLOP.NEG: return `(-${a[0]})`
|
|
194
|
+
case SLOP.MIN: return `min(${a[0]}, ${a[1]})`
|
|
195
|
+
case SLOP.MAX: return `max(${a[0]}, ${a[1]})`
|
|
196
|
+
case SLOP.ABS: return `abs(${a[0]})`
|
|
197
|
+
case SLOP.CLAMP: return `clamp(${a[0]}, ${a[1]}, ${a[2]})`
|
|
198
|
+
}
|
|
199
|
+
}
|
|
138
200
|
switch (n.op) {
|
|
139
|
-
case SLOP.COMPOSE: return ctor(n.type, a)
|
|
201
|
+
case SLOP.COMPOSE: return n.kind === undefined ? ctor(n.type, a) : `${typeName(n)}(${a.join(", ")})`
|
|
202
|
+
case SLOP.CAST: {
|
|
203
|
+
const from = valueAt(p.nodes, n.args[0]!).kind
|
|
204
|
+
if (n.kind === undefined) return `${typeName(n)}(${a[0]})`
|
|
205
|
+
if (n.kind === "bool") return `(${a[0]} != ${from === undefined ? "0.0" : kindLit(from, 0)})`
|
|
206
|
+
if (from === undefined) {
|
|
207
|
+
const [lo, hi] = INT_LIMITS[n.kind]
|
|
208
|
+
return `${n.kind}(clamp(${a[0]}, ${lo}, ${hi}))`
|
|
209
|
+
}
|
|
210
|
+
return `${n.kind}(${a[0]})`
|
|
211
|
+
}
|
|
212
|
+
case SLOP.LT: return `(${a[0]} < ${a[1]})`
|
|
213
|
+
case SLOP.LE: return `(${a[0]} <= ${a[1]})`
|
|
214
|
+
case SLOP.GT: return `(${a[0]} > ${a[1]})`
|
|
215
|
+
case SLOP.GE: return `(${a[0]} >= ${a[1]})`
|
|
216
|
+
case SLOP.EQ: return `(${a[0]} == ${a[1]})`
|
|
217
|
+
case SLOP.NE: return `(${a[0]} != ${a[1]})`
|
|
218
|
+
case SLOP.AND: return `(${a[0]} && ${a[1]})`
|
|
219
|
+
case SLOP.OR: return `(${a[0]} || ${a[1]})`
|
|
220
|
+
case SLOP.NOT: return `(!${a[0]})`
|
|
221
|
+
case SLOP.BIT_AND: return `(${a[0]} & ${a[1]})`
|
|
222
|
+
case SLOP.BIT_OR: return `(${a[0]} | ${a[1]})`
|
|
223
|
+
case SLOP.BIT_XOR: return `(${a[0]} ^ ${a[1]})`
|
|
224
|
+
case SLOP.BIT_NOT: return `(~${a[0]})`
|
|
225
|
+
// The count modulo 32, which HLSL does itself and GLSL leaves undefined past 31.
|
|
226
|
+
case SLOP.SHL: return `(${a[0]} << (${a[1]} & ${kindLit(valueAt(p.nodes, n.args[1]!).kind ?? "int", 31)}))`
|
|
227
|
+
case SLOP.SHR: return `(${a[0]} >> (${a[1]} & ${kindLit(valueAt(p.nodes, n.args[1]!).kind ?? "int", 31)}))`
|
|
228
|
+
case SLOP.CHOOSE: return `(${a[0]} ? ${a[1]} : ${a[2]})`
|
|
140
229
|
|
|
141
230
|
case SLOP.ADD: return `(${a[0]} + ${a[1]})`
|
|
142
231
|
case SLOP.SUB: return `(${a[0]} - ${a[1]})`
|
|
@@ -192,20 +281,32 @@ export function emitBody(p: Program, target: BodyTarget): Body {
|
|
|
192
281
|
case SLOP.HSV2RGB: return `${helper("sl_hsv2rgb")}(${a[0]})`
|
|
193
282
|
case SLOP.NOISE: return `${helper("sl_valueNoise")}(${a[0]})`
|
|
194
283
|
case SLOP.SIMPLEX: return `${helper("sl_simplex")}(${a[0]})`
|
|
195
|
-
case SLOP.FBM:
|
|
196
|
-
case SLOP.TURBULENCE:
|
|
197
|
-
case SLOP.RIDGED:
|
|
284
|
+
case SLOP.FBM:
|
|
285
|
+
case SLOP.TURBULENCE:
|
|
286
|
+
case SLOP.RIDGED: {
|
|
287
|
+
// The count rounded here and held to 1 to 4 by sl_fbm; a constant
|
|
288
|
+
// one is the whole number it is. fbm's kind is the one immediate.
|
|
289
|
+
const c = constantOf(n.args[1]!)
|
|
290
|
+
const count = c !== null ? String(Math.round(c[0]!)) : `int(floor(${a[1]} + 0.5))`
|
|
291
|
+
const kind = n.op === SLOP.TURBULENCE ? 2 : n.op === SLOP.RIDGED ? 3 : Math.round(imm[0] ?? 0)
|
|
292
|
+
return `${helper("sl_fbm")}(${a[0]}, ${count}, ${kind})`
|
|
293
|
+
}
|
|
198
294
|
case SLOP.SDF: {
|
|
199
295
|
const id = Math.round(imm[0] ?? 0)
|
|
200
|
-
|
|
201
|
-
const r = [imm[5] ?? 0, imm[6] ?? 0].map(lit)
|
|
202
|
-
return `${helper("sl_sdfDistance")}(${id}, ${a[0]}, float4(${q.join(", ")}), float2(${r.join(", ")}))`
|
|
296
|
+
return `${helper("sl_sdfDistance")}(${id}, ${a[0]}, ${a[1]}, ${a[2]})`
|
|
203
297
|
}
|
|
204
298
|
case SLOP.VORONOI: return `${helper("sl_voronoi")}(${a[0]})`
|
|
205
299
|
case SLOP.SAMPLE: {
|
|
206
300
|
const slot = Math.round(imm[0] ?? 0)
|
|
207
301
|
textures.add(slot)
|
|
208
|
-
|
|
302
|
+
// Where pixels part ways there are no neighbours to take a mip
|
|
303
|
+
// level from, so every backend samples level 0 there (`structure.ts`).
|
|
304
|
+
return inVaryingFlow(shape, ref) ? target.sampleLevel(slot, a[0]!, "0.0") : target.sample(slot, a[0]!)
|
|
305
|
+
}
|
|
306
|
+
case SLOP.SAMPLE_LOD: {
|
|
307
|
+
const slot = Math.round(imm[0] ?? 0)
|
|
308
|
+
textures.add(slot)
|
|
309
|
+
return target.sampleLevel(slot, a[0]!, a[1]!)
|
|
209
310
|
}
|
|
210
311
|
|
|
211
312
|
default:
|
|
@@ -217,15 +318,20 @@ export function emitBody(p: Program, target: BodyTarget): Body {
|
|
|
217
318
|
}
|
|
218
319
|
}
|
|
219
320
|
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
321
|
+
const typeOf = (ref: NodeRef) => typeName(valueAt(p.nodes, ref))
|
|
322
|
+
const syntax: Syntax = {
|
|
323
|
+
value: (ref, e) => `${typeOf(ref)} ${name(ref)} = ${e};`,
|
|
324
|
+
mutable: (n, like, init) => `${typeOf(like)} ${n}${init === undefined ? "" : ` = ${init}`};`,
|
|
325
|
+
counter: (n) => `int ${n} = 0;`,
|
|
326
|
+
assign: (n, v) => `${n} = ${v};`,
|
|
327
|
+
increment: (n) => `${n}++;`,
|
|
328
|
+
ifOpen: (c) => `if (${c}) {`,
|
|
329
|
+
elseOpen: "} else {",
|
|
330
|
+
close: "}",
|
|
331
|
+
loopOpen: `${target.loopAttribute === undefined ? "" : target.loopAttribute + " "}for (;;) {`,
|
|
332
|
+
breakUnless: (c, turns, max) => `if (!(${c}) || ${turns} >= ${max}) break;`,
|
|
227
333
|
}
|
|
228
|
-
|
|
334
|
+
const lines = printBody(p, shape, syntax, expr, indent)
|
|
229
335
|
lines.push(target.result === undefined
|
|
230
336
|
? `${indent}return ${name(p.result)};`
|
|
231
337
|
: `${indent}${target.result} = ${name(p.result)};`)
|
package/src/core.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
export * as sl from "./sl"
|
|
11
11
|
export type { Float, Vec2, Vec3, Vec4, Num, ProgramInputs, Texture } from "./sl"
|
|
12
12
|
export {
|
|
13
|
-
TYPE, INPUTS, MAX_NODES, SLError, hashProgram, widthName, SL_IR_VERSION, inputsUsed,
|
|
13
|
+
TYPE, INPUTS, MAX_NODES, SLError, hashProgram, widthName, SL_IR_VERSION, SL_HASH_VERSION, inputsUsed,
|
|
14
14
|
CONTROL_FIELDS, controlProblem,
|
|
15
15
|
} from "./ir"
|
|
16
16
|
export type {
|
package/src/hlsl.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* next time it is generated.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { SLError, type Program } from "./ir"
|
|
18
|
+
import { SLError, SL_HASH_VERSION, 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. */
|
|
@@ -74,7 +74,9 @@ export function emitShader(p: Program, options: EmitOptions = {}): string {
|
|
|
74
74
|
//
|
|
75
75
|
// Source of truth is the program this was emitted from; edits here are lost the
|
|
76
76
|
// next time it is generated. The name carries the program hash, which is how the
|
|
77
|
-
// runtime pairs the two. See Specs/SHADER_LANG.md section 6.
|
|
77
|
+
// runtime pairs the two. See Specs/SHADER_LANG.md section 6. The scheme that
|
|
78
|
+
// hash was made under, which a build compares with the apps' manifests:
|
|
79
|
+
// SL_HASH_VERSION ${SL_HASH_VERSION}
|
|
78
80
|
Shader "${name}"
|
|
79
81
|
{
|
|
80
82
|
Properties
|
|
@@ -123,7 +125,7 @@ ${body}
|
|
|
123
125
|
|
|
124
126
|
/**
|
|
125
127
|
* The frame's side of the body: OneJS's names for the inputs, a uniform's
|
|
126
|
-
* material property, and `tex2D` on the slot's sampler. `sl_toLinear` stays a
|
|
128
|
+
* material property, and `tex2D` and `tex2Dlod` on the slot's sampler. `sl_toLinear` stays a
|
|
127
129
|
* call because `SLCommon.cginc` decides Gamma or Linear per project, at the
|
|
128
130
|
* shader compile, so this target always emits it.
|
|
129
131
|
*/
|
|
@@ -148,8 +150,10 @@ function unityTarget(p: Program): BodyTarget {
|
|
|
148
150
|
},
|
|
149
151
|
uniform: (slot) => uniformProperty(p.uniforms[slot]!.name),
|
|
150
152
|
sample: (slot, uv) => `tex2D(_Tex${slot}, ${uv})`,
|
|
153
|
+
sampleLevel: (slot, uv, lod) => `tex2Dlod(_Tex${slot}, float4(${uv}, 0.0, ${lod}))`,
|
|
151
154
|
colour: "linear",
|
|
152
155
|
indent: " ",
|
|
156
|
+
loopAttribute: "[loop]",
|
|
153
157
|
}
|
|
154
158
|
}
|
|
155
159
|
|