onejs-sl 0.1.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 +10 -0
- package/README.md +266 -0
- package/package.json +60 -0
- package/src/color.ts +16 -0
- package/src/core.ts +24 -0
- package/src/emit/unity.ts +3 -0
- package/src/emit/web.ts +3 -0
- package/src/encode.ts +462 -0
- package/src/hlsl.ts +275 -0
- package/src/index.ts +24 -0
- package/src/ir.ts +301 -0
- package/src/lang/README.md +167 -0
- package/src/lang/ast.ts +87 -0
- package/src/lang/builtins.ts +168 -0
- package/src/lang/check.ts +541 -0
- package/src/lang/index.ts +55 -0
- package/src/lang/lexer.ts +176 -0
- package/src/lang/lower.ts +628 -0
- package/src/lang/parser.ts +460 -0
- package/src/lang/prelude.ts +75 -0
- package/src/limits.ts +50 -0
- package/src/ops.ts +346 -0
- package/src/serial.ts +137 -0
- package/src/shapes.ts +49 -0
- package/src/sl.ts +721 -0
- package/src/tables.ts +15 -0
- package/src/vm.ts +11 -0
- package/src/web.ts +273 -0
- package/src/weblib.ts +1303 -0
package/src/tables.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `onejs-sl/tables`: what completion and highlighting read.
|
|
3
|
+
*
|
|
4
|
+
* The same tables the checker and the emitters use, so an editor offers
|
|
5
|
+
* exactly the builtins that compile, with the arities they take.
|
|
6
|
+
*/
|
|
7
|
+
export { BUILTINS, NOT_YET } from "./lang/builtins"
|
|
8
|
+
export type { Builtin } from "./lang/builtins"
|
|
9
|
+
export {
|
|
10
|
+
SLOP, SL_ARITY, SL_NAME, SL_HLSL, SL_CALL_NAMES, SL_GLSL_HINT, SL_UNIMPLEMENTED,
|
|
11
|
+
} from "./ops"
|
|
12
|
+
export type { SLSurface } from "./ops"
|
|
13
|
+
export { INPUTS } from "./ir"
|
|
14
|
+
export { SL_SDF_SHAPES, SL_SDF_PARAMS } from "./shapes"
|
|
15
|
+
export type { SlSdfKind } from "./shapes"
|
package/src/vm.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `onejs-sl/vm`: the buffer OneJS's shader language VM evaluates.
|
|
3
|
+
*
|
|
4
|
+
* Only a host that runs the VM needs this. `SL_WIRE_VERSION` is the newest
|
|
5
|
+
* encoding it writes; each result's `wire` is the oldest VM that can run it.
|
|
6
|
+
*/
|
|
7
|
+
export {
|
|
8
|
+
encode, forVm, reachable, liveRanges, REGISTERS, MAX_INSTRUCTIONS, TEXELS_PER_INSTRUCTION,
|
|
9
|
+
} from "./encode"
|
|
10
|
+
export type { Encoded } from "./encode"
|
|
11
|
+
export { SL_WIRE_VERSION, INPUT_ID, VM_UNIFORMS, VM_TEXTURES } from "./ops"
|
package/src/web.ts
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The web backends: prints a program as GLSL ES 3.00 or WGSL, for a browser to
|
|
3
|
+
* compile at runtime on Unity's own graphics device.
|
|
4
|
+
*
|
|
5
|
+
* Unity cannot compile a shader in a built game, which is why the VM exists.
|
|
6
|
+
* A browser can, and the Play container runs on WebGPU with WebGL2 behind it,
|
|
7
|
+
* so a program becomes WGSL on one and GLSL ES on the other and runs compiled:
|
|
8
|
+
* no register budget, no interpreter, and on the spike's measurements 100 to
|
|
9
|
+
* 600 times cheaper per pixel. The host that compiles and draws these lives in
|
|
10
|
+
* OneJS (`Plugins/WebGL/OneJSSLWeb.jslib`); this file only prints them.
|
|
11
|
+
*
|
|
12
|
+
* Structurally the HLSL emitter again: one local per reachable node, in order,
|
|
13
|
+
* named `n<index>`. What differs is the contract with the host, which is fixed
|
|
14
|
+
* and the same for both languages:
|
|
15
|
+
*
|
|
16
|
+
* sl_Res (target width, target height, seconds, 0)
|
|
17
|
+
* sl_Opt (1 in a Linear colour space project else 0, 1 to flip uv.y, 0, 0)
|
|
18
|
+
* sl_U[16] uniform slots, each a vec4, in the program's slot order
|
|
19
|
+
* textures by slot: GLSL `sl_Tex<slot>`; WGSL `sl_tex<slot>` at binding
|
|
20
|
+
* 2 + 2 * slot with its own sampler `sl_samp<slot>` at 1 + 2 * slot,
|
|
21
|
+
* all in group 0 beside the frame block at binding 0. Only the
|
|
22
|
+
* slots the program samples are declared.
|
|
23
|
+
*
|
|
24
|
+
* WGSL also carries its vertex stage (`sl_vs`, a full-target triangle from the
|
|
25
|
+
* vertex index) so one module is one pipeline; the GLSL host supplies its own.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { SLError, TYPE, type Program, type SLNode, type SLType } from "./ir"
|
|
29
|
+
import { INPUT_ID, SLOP, VM_UNIFORMS } from "./ops"
|
|
30
|
+
import { SDF_CALLS, WEB_LIB } from "./weblib"
|
|
31
|
+
|
|
32
|
+
export type WebLanguage = "glsl" | "wgsl"
|
|
33
|
+
|
|
34
|
+
/** Uniform slots the host always provides, so every program shares one layout. */
|
|
35
|
+
export const WEB_UNIFORM_SLOTS = VM_UNIFORMS
|
|
36
|
+
|
|
37
|
+
export function emitGLSL(p: Program): string {
|
|
38
|
+
return emitWeb(p, "glsl")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function emitWGSL(p: Program): string {
|
|
42
|
+
return emitWeb(p, "wgsl")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function emitWeb(p: Program, lang: WebLanguage): string {
|
|
46
|
+
const W = lang === "wgsl"
|
|
47
|
+
const need = new Set<string>()
|
|
48
|
+
const sampled = new Set<number>()
|
|
49
|
+
const lines: string[] = []
|
|
50
|
+
const emitted = new Set<number>()
|
|
51
|
+
const T = (t: SLType) => (W ? (t === 1 ? "f32" : `vec${t}f`) : t === 1 ? "float" : `vec${t}`)
|
|
52
|
+
const typeOf = (ref: number) => p.nodes[ref].type
|
|
53
|
+
|
|
54
|
+
/** A node's value widened to `t` when it is a scalar and `t` is not. */
|
|
55
|
+
const splat = (ref: number, t: SLType) => (typeOf(ref) === 1 && t > 1 ? `${T(t)}(n${ref})` : `n${ref}`)
|
|
56
|
+
/** A literal at width `t`. */
|
|
57
|
+
const k = (v: number, t: SLType) => (t > 1 ? `${T(t)}(${lit(v)})` : lit(v))
|
|
58
|
+
|
|
59
|
+
const walk = (ref: number): void => {
|
|
60
|
+
if (emitted.has(ref)) return
|
|
61
|
+
const n = p.nodes[ref]
|
|
62
|
+
const deps = n.k === "swizzle" ? [n.src] : n.k === "call" ? n.args : []
|
|
63
|
+
for (const d of deps) walk(d)
|
|
64
|
+
emitted.add(ref)
|
|
65
|
+
const e = expr(n)
|
|
66
|
+
lines.push(W ? ` let n${ref}: ${T(n.type)} = ${e};` : ` ${T(n.type)} n${ref} = ${e};`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const expr = (n: SLNode): string => {
|
|
70
|
+
switch (n.k) {
|
|
71
|
+
case "const": return n.type === 1 ? lit(n.v[0]) : `${T(n.type)}(${n.v.map(lit).join(", ")})`
|
|
72
|
+
case "input": {
|
|
73
|
+
const res = W ? "sl.res" : "sl_Res"
|
|
74
|
+
switch (INPUT_ID[n.name]) {
|
|
75
|
+
case 0: return "sl_uv"
|
|
76
|
+
case 1: return `(sl_uv * ${res}.xy)`
|
|
77
|
+
case 2: return `${res}.xy`
|
|
78
|
+
case 3: return `${res}.z`
|
|
79
|
+
default: return `(${res}.x / max(${res}.y, 1.0))`
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
case "uniform": {
|
|
83
|
+
const u = W ? `sl.u[${n.slot}]` : `sl_U[${n.slot}]`
|
|
84
|
+
return n.type === TYPE.VEC4 ? u : `${u}.${"xyzw".slice(0, n.type)}`
|
|
85
|
+
}
|
|
86
|
+
case "swizzle": {
|
|
87
|
+
// A scalar has one component and no swizzle in either language.
|
|
88
|
+
if (typeOf(n.src) === 1) return n.type === 1 ? `n${n.src}` : `${T(n.type)}(n${n.src})`
|
|
89
|
+
return `n${n.src}.${n.chans.map((c) => "xyzw"[c]).join("")}`
|
|
90
|
+
}
|
|
91
|
+
case "call": return call(n)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const call = (n: Extract<SLNode, { k: "call" }>): string => {
|
|
96
|
+
const t = n.type
|
|
97
|
+
const a = n.args.map((r) => `n${r}`)
|
|
98
|
+
// Every argument at the result's width, for the element wise ops. WGSL
|
|
99
|
+
// refuses min(vec2f, f32) where HLSL and GLSL broadcast.
|
|
100
|
+
const s = n.args.map((r) => splat(r, t))
|
|
101
|
+
const w0 = n.args.length > 0 ? typeOf(n.args[0]) : 1
|
|
102
|
+
const imm = n.imm ?? []
|
|
103
|
+
const lib = (name: string) => { need.add(name); return name }
|
|
104
|
+
switch (n.op) {
|
|
105
|
+
case SLOP.COMPOSE: return t === 1 ? a[0] : `${T(t)}(${a.join(", ")})`
|
|
106
|
+
|
|
107
|
+
case SLOP.ADD: return `(${s[0]} + ${s[1]})`
|
|
108
|
+
case SLOP.SUB: return `(${s[0]} - ${s[1]})`
|
|
109
|
+
case SLOP.MUL: return `(${s[0]} * ${s[1]})`
|
|
110
|
+
case SLOP.DIV: return `(${s[0]} / ${s[1]})`
|
|
111
|
+
// HLSL fmod: truncating, sign of the dividend.
|
|
112
|
+
case SLOP.MOD: return `(${s[0]} - ${s[1]} * trunc(${s[0]} / ${s[1]}))`
|
|
113
|
+
case SLOP.POW: return `pow(abs(${s[0]}), ${s[1]})`
|
|
114
|
+
case SLOP.NEG: return `(-${a[0]})`
|
|
115
|
+
case SLOP.RECIP: return `(${k(1, t)} / ${a[0]})`
|
|
116
|
+
|
|
117
|
+
case SLOP.SIN: return `sin(${a[0]})`
|
|
118
|
+
case SLOP.COS: return `cos(${a[0]})`
|
|
119
|
+
case SLOP.TAN: return `tan(${a[0]})`
|
|
120
|
+
case SLOP.ASIN: return `asin(clamp(${a[0]}, ${k(-1, t)}, ${k(1, t)}))`
|
|
121
|
+
case SLOP.ACOS: return `acos(clamp(${a[0]}, ${k(-1, t)}, ${k(1, t)}))`
|
|
122
|
+
case SLOP.ATAN2: return W ? `atan2(${s[0]}, ${s[1]})` : `atan(${s[0]}, ${s[1]})`
|
|
123
|
+
case SLOP.EXP: return `exp(${a[0]})`
|
|
124
|
+
case SLOP.LOG: return `log(max(${a[0]}, ${k(1e-8, t)}))`
|
|
125
|
+
case SLOP.SQRT: return `sqrt(max(${a[0]}, ${k(0, t)}))`
|
|
126
|
+
case SLOP.ABS: return `abs(${a[0]})`
|
|
127
|
+
case SLOP.SIGN: return `sign(${a[0]})`
|
|
128
|
+
case SLOP.FLOOR: return `floor(${a[0]})`
|
|
129
|
+
case SLOP.CEIL: return `ceil(${a[0]})`
|
|
130
|
+
case SLOP.ROUND: return `round(${a[0]})`
|
|
131
|
+
case SLOP.FRACT: return `fract(${a[0]})`
|
|
132
|
+
case SLOP.MIN: return `min(${s[0]}, ${s[1]})`
|
|
133
|
+
case SLOP.MAX: return `max(${s[0]}, ${s[1]})`
|
|
134
|
+
case SLOP.CLAMP: return `clamp(${s[0]}, ${s[1]}, ${s[2]})`
|
|
135
|
+
case SLOP.SATURATE: return `clamp(${a[0]}, ${k(0, t)}, ${k(1, t)})`
|
|
136
|
+
|
|
137
|
+
// HLSL takes these of a scalar; WGSL does not, so the scalar case is
|
|
138
|
+
// written out as what HLSL computes for it.
|
|
139
|
+
case SLOP.LENGTH: return w0 === 1 ? `abs(${a[0]})` : `length(${a[0]})`
|
|
140
|
+
case SLOP.DISTANCE: return w0 === 1 ? `abs(${a[0]} - ${a[1]})` : `distance(${a[0]}, ${a[1]})`
|
|
141
|
+
case SLOP.DOT: return w0 === 1 ? `(${a[0]} * ${a[1]})` : `dot(${a[0]}, ${a[1]})`
|
|
142
|
+
case SLOP.NORMALIZE: return w0 === 1 ? `sign(${a[0]})` : `normalize(${a[0]})`
|
|
143
|
+
case SLOP.CROSS: return `cross(${a[0]}, ${a[1]})`
|
|
144
|
+
case SLOP.REFLECT: return w0 === 1
|
|
145
|
+
? `(${a[0]} - 2.0 * ${a[1]} * ${a[0]} * ${a[1]})`
|
|
146
|
+
: `reflect(${a[0]}, ${a[1]})`
|
|
147
|
+
case SLOP.LUMINANCE: return `dot(${a[0]}.rgb, ${T(3)}(0.2126, 0.7152, 0.0722))`
|
|
148
|
+
case SLOP.TO_LINEAR: return `${lib(`sl_toLinear${t}`)}(${a[0]})`
|
|
149
|
+
|
|
150
|
+
case SLOP.MIX: return `mix(${s[0]}, ${s[1]}, ${s[2]})`
|
|
151
|
+
case SLOP.STEP: return `step(${s[0]}, ${s[1]})`
|
|
152
|
+
case SLOP.SMOOTHSTEP: return `${lib(`sl_smoothstep${t}`)}(${s[0]}, ${s[1]}, ${s[2]})`
|
|
153
|
+
// As the VM and the HLSL emitter do it: branchless, so every
|
|
154
|
+
// backend agrees on the edge value.
|
|
155
|
+
case SLOP.SELECT: {
|
|
156
|
+
const c = typeOf(n.args[0])
|
|
157
|
+
return `mix(${splat(n.args[2], t)}, ${splat(n.args[1], t)}, step(${k(0.5, c)}, ${a[0]}))`
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
case SLOP.HSV2RGB: return `${lib("sl_hsv2rgb")}(${a[0]})`
|
|
161
|
+
case SLOP.NOISE: return `${lib("oj_vnoise")}(${a[0]}, 0.0)`
|
|
162
|
+
case SLOP.SIMPLEX: return `${lib("oj_simplex")}(${a[0]}, 0.0)`
|
|
163
|
+
case SLOP.FBM: return octaveCall(Math.round(imm[1] ?? 0), a[0], imm[0])
|
|
164
|
+
case SLOP.TURBULENCE: return octaveCall(2, a[0], imm[0])
|
|
165
|
+
case SLOP.RIDGED: return octaveCall(3, a[0], imm[0])
|
|
166
|
+
case SLOP.VORONOI: return `${lib("sl_voronoi")}(${a[0]})`
|
|
167
|
+
case SLOP.SDF: return sdfCall(Math.round(imm[0] ?? 0), a[0], imm)
|
|
168
|
+
case SLOP.SAMPLE: {
|
|
169
|
+
const slot = Math.round(imm[0] ?? 0)
|
|
170
|
+
sampled.add(slot)
|
|
171
|
+
return W ? `textureSample(sl_tex${slot}, sl_samp${slot}, ${a[0]})` : `texture(sl_Tex${slot}, ${a[0]})`
|
|
172
|
+
}
|
|
173
|
+
default:
|
|
174
|
+
throw new SLError(
|
|
175
|
+
`the ${lang.toUpperCase()} emitter has no case for opcode ${n.op}. A program using it would ` +
|
|
176
|
+
`differ between the VM and a compiled build, which is the one failure this design cannot tolerate.`,
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Noise2D.cginc's onejsFbmKind, resolved here since the kind is a constant. */
|
|
181
|
+
function octaveCall(kind: number, pt: string, octaves: number | undefined): string {
|
|
182
|
+
const o = Math.min(4, Math.max(1, Math.round(octaves ?? 3)))
|
|
183
|
+
const fn = kind === 2 ? "oj_turbulence" : kind === 3 ? "oj_ridged" : kind === 1 ? "oj_fbmSimplex" : "oj_fbm"
|
|
184
|
+
return `${lib(fn)}(${pt}, 0.0, ${o})`
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The shape is a constant, so this calls it directly instead of SLCommon's switch. */
|
|
188
|
+
function sdfCall(id: number, pt: string, im: number[]): string {
|
|
189
|
+
const shape = SDF_CALLS[id]
|
|
190
|
+
// SLCommon's dispatcher returns 1e6 for an id it does not know.
|
|
191
|
+
if (shape === undefined) return lit(1e6)
|
|
192
|
+
lib(shape.fn)
|
|
193
|
+
const v = [im[1] ?? 0, im[2] ?? 0, im[3] ?? 0, im[4] ?? 0, im[5] ?? 0, im[6] ?? 0]
|
|
194
|
+
const args = shape.args.map((arg) => {
|
|
195
|
+
if (!Array.isArray(arg)) return String(Math.trunc(v[arg.int]))
|
|
196
|
+
const parts = arg.map((i) => lit(v[i]))
|
|
197
|
+
return parts.length === 1 ? parts[0] : `${T(parts.length as SLType)}(${parts.join(", ")})`
|
|
198
|
+
})
|
|
199
|
+
return `${shape.fn}(${[pt, ...args].join(", ")})`
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
walk(p.result)
|
|
204
|
+
const body = lines.join("\n")
|
|
205
|
+
const library = librarySource(need, lang)
|
|
206
|
+
const header = `// GENERATED from a shader language program (${p.hash}). Do not edit.`
|
|
207
|
+
// Only the textures the program samples. WebGPU's automatic layout leaves
|
|
208
|
+
// out a binding the shader never reads, and a bind group that supplies one
|
|
209
|
+
// anyway does not validate; the host reads these declarations back to know
|
|
210
|
+
// which slots to bind.
|
|
211
|
+
const textures = p.textures.filter((t) => sampled.has(t.slot))
|
|
212
|
+
|
|
213
|
+
if (W) {
|
|
214
|
+
const bindings = textures.map((t) =>
|
|
215
|
+
`@group(0) @binding(${1 + 2 * t.slot}) var sl_samp${t.slot}: sampler;\n` +
|
|
216
|
+
`@group(0) @binding(${2 + 2 * t.slot}) var sl_tex${t.slot}: texture_2d<f32>;`).join("\n")
|
|
217
|
+
return `${header}
|
|
218
|
+
struct SLFrame { res: vec4f, opt: vec4f, u: array<vec4f, ${WEB_UNIFORM_SLOTS}> }
|
|
219
|
+
@group(0) @binding(0) var<uniform> sl: SLFrame;
|
|
220
|
+
${bindings}
|
|
221
|
+
${library}
|
|
222
|
+
@vertex fn sl_vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4f {
|
|
223
|
+
let c = vec2f(f32((i << 1u) & 2u), f32(i & 2u));
|
|
224
|
+
return vec4f(c * 2.0 - 1.0, 0.0, 1.0);
|
|
225
|
+
}
|
|
226
|
+
@fragment fn sl_fs(@builtin(position) sl_pos: vec4f) -> @location(0) vec4f {
|
|
227
|
+
var sl_uv = sl_pos.xy / sl.res.xy;
|
|
228
|
+
if (sl.opt.y > 0.5) { sl_uv.y = 1.0 - sl_uv.y; }
|
|
229
|
+
${body}
|
|
230
|
+
return n${p.result};
|
|
231
|
+
}
|
|
232
|
+
`
|
|
233
|
+
}
|
|
234
|
+
const samplers = textures.map((t) => `uniform sampler2D sl_Tex${t.slot};`).join("\n")
|
|
235
|
+
return `#version 300 es
|
|
236
|
+
${header}
|
|
237
|
+
precision highp float;
|
|
238
|
+
precision highp int;
|
|
239
|
+
uniform vec4 sl_Res;
|
|
240
|
+
uniform vec4 sl_Opt;
|
|
241
|
+
uniform vec4 sl_U[${WEB_UNIFORM_SLOTS}];
|
|
242
|
+
${samplers}
|
|
243
|
+
out vec4 sl_Out;
|
|
244
|
+
${library}
|
|
245
|
+
void main() {
|
|
246
|
+
vec2 sl_uv = gl_FragCoord.xy / sl_Res.xy;
|
|
247
|
+
if (sl_Opt.y > 0.5) sl_uv.y = 1.0 - sl_uv.y;
|
|
248
|
+
${body}
|
|
249
|
+
sl_Out = n${p.result};
|
|
250
|
+
}
|
|
251
|
+
`
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** The needed entries and everything they depend on, in library order. */
|
|
255
|
+
function librarySource(need: Set<string>, lang: WebLanguage): string {
|
|
256
|
+
const want = new Set<string>()
|
|
257
|
+
const byName = new Map(WEB_LIB.map((e) => [e.name, e]))
|
|
258
|
+
const add = (name: string) => {
|
|
259
|
+
if (want.has(name)) return
|
|
260
|
+
const entry = byName.get(name)
|
|
261
|
+
if (entry === undefined) throw new SLError(`internal: the web library has no entry "${name}"`)
|
|
262
|
+
want.add(name)
|
|
263
|
+
for (const d of entry.deps) add(d)
|
|
264
|
+
}
|
|
265
|
+
for (const n of need) add(n)
|
|
266
|
+
return WEB_LIB.filter((e) => want.has(e.name)).map((e) => (lang === "wgsl" ? e.wgsl : e.glsl).trim()).join("\n")
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** A literal that survives a float32 round trip and never reads as an int. */
|
|
270
|
+
function lit(n: number): string {
|
|
271
|
+
if (!Number.isFinite(n)) throw new SLError(`cannot emit ${n} as a shader literal`)
|
|
272
|
+
return Number.isInteger(n) ? n.toFixed(1) : String(n)
|
|
273
|
+
}
|