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/ops.ts
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shader language opcode table.
|
|
3
|
+
*
|
|
4
|
+
* Phase 1 of `Specs/SHADER_LANG.md`. This is the contract three things will
|
|
5
|
+
* share: the encoder that packs a program into a texture, the VM shader that
|
|
6
|
+
* evaluates it, and the HLSL emitter that prints it. Two of those do not exist
|
|
7
|
+
* yet, which is exactly why the numbering is fixed now rather than later.
|
|
8
|
+
*
|
|
9
|
+
* Numbered in families with gaps, the convention `fx/ops.ts` set, so a reader
|
|
10
|
+
* can tell what an opcode is from its value:
|
|
11
|
+
*
|
|
12
|
+
* 0..15 construct and swizzle
|
|
13
|
+
* 16..47 arithmetic
|
|
14
|
+
* 48..79 maths
|
|
15
|
+
* 80..95 geometry
|
|
16
|
+
* 96..111 interpolate
|
|
17
|
+
* 112..127 colour
|
|
18
|
+
* 128..143 procedural
|
|
19
|
+
* 144..159 sampling
|
|
20
|
+
*
|
|
21
|
+
* Leave the gaps. A later opcode landing in the right family is worth more than
|
|
22
|
+
* a dense table.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The newest VM encoding this encoder writes. The VM accepts 1..its own and
|
|
27
|
+
* refuses newer, where there is a VM: a WebGL player draws compiled and never
|
|
28
|
+
* reads the buffer.
|
|
29
|
+
*
|
|
30
|
+
* A payload records the LOWEST version that can run it (`Encoded.wire`), not
|
|
31
|
+
* this: a program that uses nothing new stays 1, so a new bundle still runs on
|
|
32
|
+
* an older Play container, and only a program that needs a newer instruction
|
|
33
|
+
* is refused by an older one rather than drawn wrong.
|
|
34
|
+
*/
|
|
35
|
+
export const SL_WIRE_VERSION = 2
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* What the VM shader actually declares, and therefore what a program may use.
|
|
39
|
+
*
|
|
40
|
+
* `FxProgram.shader` has `float4 _Uniforms[16]` and four `sampler2D`s, and
|
|
41
|
+
* `SLProgramBridge.cs` fills exactly those. Past either ceiling the VM does not
|
|
42
|
+
* fail, it clamps: uniform 17 reads slot 15 and texture slot 4 samples `_Tex3`,
|
|
43
|
+
* while the generated HLSL would declare both correctly. That is a silent
|
|
44
|
+
* disagreement between the backends, so the text form reports it at the
|
|
45
|
+
* declaration with a file and a line. See `Specs/SL_TEXT.md` 3.7.
|
|
46
|
+
*/
|
|
47
|
+
export const VM_UNIFORMS = 16
|
|
48
|
+
export const VM_TEXTURES = 4
|
|
49
|
+
|
|
50
|
+
/** Input ids, fixed here because the shader switches on them. */
|
|
51
|
+
export const INPUT_ID: Record<string, number> = {
|
|
52
|
+
uv: 0, fragCoord: 1, resolution: 2, time: 3, aspect: 4,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const SLOP = {
|
|
56
|
+
// Construct and swizzle
|
|
57
|
+
CONST: 0,
|
|
58
|
+
INPUT: 1,
|
|
59
|
+
UNIFORM: 2,
|
|
60
|
+
COMPOSE: 3, // build a wider value from narrower parts
|
|
61
|
+
SWIZZLE: 4,
|
|
62
|
+
|
|
63
|
+
// Arithmetic
|
|
64
|
+
ADD: 16,
|
|
65
|
+
SUB: 17,
|
|
66
|
+
MUL: 18,
|
|
67
|
+
DIV: 19,
|
|
68
|
+
MOD: 20,
|
|
69
|
+
POW: 21,
|
|
70
|
+
NEG: 22,
|
|
71
|
+
RECIP: 23,
|
|
72
|
+
|
|
73
|
+
// Maths
|
|
74
|
+
SIN: 48,
|
|
75
|
+
COS: 49,
|
|
76
|
+
TAN: 50,
|
|
77
|
+
ASIN: 51,
|
|
78
|
+
ACOS: 52,
|
|
79
|
+
ATAN2: 53,
|
|
80
|
+
EXP: 54,
|
|
81
|
+
LOG: 55,
|
|
82
|
+
SQRT: 56,
|
|
83
|
+
ABS: 57,
|
|
84
|
+
SIGN: 58,
|
|
85
|
+
FLOOR: 59,
|
|
86
|
+
CEIL: 60,
|
|
87
|
+
ROUND: 61,
|
|
88
|
+
FRACT: 62,
|
|
89
|
+
MIN: 63,
|
|
90
|
+
MAX: 64,
|
|
91
|
+
CLAMP: 65,
|
|
92
|
+
SATURATE: 66,
|
|
93
|
+
|
|
94
|
+
// Geometry
|
|
95
|
+
LENGTH: 80,
|
|
96
|
+
DISTANCE: 81,
|
|
97
|
+
DOT: 82,
|
|
98
|
+
CROSS: 83,
|
|
99
|
+
NORMALIZE: 84,
|
|
100
|
+
REFLECT: 85,
|
|
101
|
+
|
|
102
|
+
// Interpolate
|
|
103
|
+
MIX: 96,
|
|
104
|
+
STEP: 97,
|
|
105
|
+
SMOOTHSTEP: 98,
|
|
106
|
+
SELECT: 99,
|
|
107
|
+
REMAP: 100,
|
|
108
|
+
|
|
109
|
+
// Colour
|
|
110
|
+
RAMP: 112,
|
|
111
|
+
HSV2RGB: 113,
|
|
112
|
+
RGB2HSV: 114,
|
|
113
|
+
LUMINANCE: 115,
|
|
114
|
+
// A colour as written (sRGB, like CSS) to the working space the target
|
|
115
|
+
// holds. Gamma-aware in both backends; alpha is left alone.
|
|
116
|
+
TO_LINEAR: 116,
|
|
117
|
+
|
|
118
|
+
// Procedural. These are SUPERINSTRUCTIONS: one opcode expanding to a
|
|
119
|
+
// substantial block in the shader, the way SOURCE_NOISE and SOURCE_SDF
|
|
120
|
+
// already do in FxSources. Making an author build fbm out of thirty
|
|
121
|
+
// primitives would be slower to run and worse to write, and we already own
|
|
122
|
+
// the HLSL.
|
|
123
|
+
NOISE: 128,
|
|
124
|
+
SIMPLEX: 129,
|
|
125
|
+
FBM: 130,
|
|
126
|
+
SDF: 131,
|
|
127
|
+
VORONOI: 132,
|
|
128
|
+
// Same field family as fx noise: |simplex| octaves summed, and the crease
|
|
129
|
+
// made bright. imm.x carries the octave count, as FBM does.
|
|
130
|
+
TURBULENCE: 133,
|
|
131
|
+
RIDGED: 134,
|
|
132
|
+
// VM only: an SDF given a fifth or sixth parameter, which one instruction's
|
|
133
|
+
// four immediates and id cannot hold. The encoder makes it (`forVm`); the
|
|
134
|
+
// IR and every compiled backend only ever see SDF.
|
|
135
|
+
SDF_WIDE: 135,
|
|
136
|
+
|
|
137
|
+
// Sampling
|
|
138
|
+
SAMPLE: 144,
|
|
139
|
+
SAMPLE_LOD: 145,
|
|
140
|
+
} as const
|
|
141
|
+
|
|
142
|
+
export type SLOpCode = (typeof SLOP)[keyof typeof SLOP]
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* How many arguments each op takes, for validation and for the encoder.
|
|
146
|
+
*
|
|
147
|
+
* -1 means variadic: COMPOSE takes however many parts add up to its width, RAMP
|
|
148
|
+
* takes a value plus its stops. Everything else is fixed, and a mismatch is a
|
|
149
|
+
* bug in the builder rather than something an author can cause.
|
|
150
|
+
*/
|
|
151
|
+
export const SL_ARITY: Record<number, number> = {
|
|
152
|
+
[SLOP.CONST]: 0, [SLOP.INPUT]: 0, [SLOP.UNIFORM]: 0,
|
|
153
|
+
[SLOP.COMPOSE]: -1, [SLOP.SWIZZLE]: 1,
|
|
154
|
+
|
|
155
|
+
[SLOP.ADD]: 2, [SLOP.SUB]: 2, [SLOP.MUL]: 2, [SLOP.DIV]: 2,
|
|
156
|
+
[SLOP.MOD]: 2, [SLOP.POW]: 2, [SLOP.NEG]: 1, [SLOP.RECIP]: 1,
|
|
157
|
+
|
|
158
|
+
[SLOP.SIN]: 1, [SLOP.COS]: 1, [SLOP.TAN]: 1, [SLOP.ASIN]: 1, [SLOP.ACOS]: 1,
|
|
159
|
+
[SLOP.ATAN2]: 2, [SLOP.EXP]: 1, [SLOP.LOG]: 1, [SLOP.SQRT]: 1, [SLOP.ABS]: 1,
|
|
160
|
+
[SLOP.SIGN]: 1, [SLOP.FLOOR]: 1, [SLOP.CEIL]: 1, [SLOP.ROUND]: 1,
|
|
161
|
+
[SLOP.FRACT]: 1, [SLOP.MIN]: 2, [SLOP.MAX]: 2, [SLOP.CLAMP]: 3,
|
|
162
|
+
[SLOP.SATURATE]: 1,
|
|
163
|
+
|
|
164
|
+
[SLOP.LENGTH]: 1, [SLOP.DISTANCE]: 2, [SLOP.DOT]: 2, [SLOP.CROSS]: 2,
|
|
165
|
+
[SLOP.NORMALIZE]: 1, [SLOP.REFLECT]: 2,
|
|
166
|
+
|
|
167
|
+
[SLOP.MIX]: 3, [SLOP.STEP]: 2, [SLOP.SMOOTHSTEP]: 3, [SLOP.SELECT]: 3,
|
|
168
|
+
[SLOP.REMAP]: 5,
|
|
169
|
+
|
|
170
|
+
[SLOP.RAMP]: -1, [SLOP.HSV2RGB]: 1, [SLOP.RGB2HSV]: 1, [SLOP.LUMINANCE]: 1, [SLOP.TO_LINEAR]: 1,
|
|
171
|
+
|
|
172
|
+
[SLOP.NOISE]: 1, [SLOP.SIMPLEX]: 1, [SLOP.FBM]: 1, [SLOP.SDF]: 1,
|
|
173
|
+
[SLOP.VORONOI]: 1, [SLOP.TURBULENCE]: 1, [SLOP.RIDGED]: 1, [SLOP.SDF_WIDE]: 2,
|
|
174
|
+
|
|
175
|
+
[SLOP.SAMPLE]: 1, [SLOP.SAMPLE_LOD]: 2,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Name per opcode, for error messages and for the HLSL emitter's comments. */
|
|
179
|
+
export const SL_NAME: Record<number, string> = Object.fromEntries(
|
|
180
|
+
Object.entries(SLOP).map(([name, code]) => [code, name.toLowerCase()]),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
export const FIRST_ARITHMETIC = 16
|
|
184
|
+
export const FIRST_MATHS = 48
|
|
185
|
+
export const FIRST_GEOMETRY = 80
|
|
186
|
+
export const FIRST_INTERPOLATE = 96
|
|
187
|
+
export const FIRST_COLOUR = 112
|
|
188
|
+
export const FIRST_PROCEDURAL = 128
|
|
189
|
+
export const FIRST_SAMPLING = 144
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Ops that read a texture. Sampler slots are the one resource ceiling neither
|
|
193
|
+
* backend can widen (16 in a fragment shader on WebGL2), so these are counted
|
|
194
|
+
* at record time.
|
|
195
|
+
*/
|
|
196
|
+
export function isSampling(op: number): boolean {
|
|
197
|
+
return op >= FIRST_SAMPLING
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* How each opcode is written in the text form, `Specs/SL_TEXT.md` 3.4.
|
|
202
|
+
*
|
|
203
|
+
* ONE COLUMN, READ BY BOTH SURFACES. The EDSL's names and the parser's names
|
|
204
|
+
* are the same set by construction rather than by two lists that have to be
|
|
205
|
+
* kept level; `lang/builtins.ts` attaches a lowering to each `call` entry here
|
|
206
|
+
* and a contract test refuses an opcode that has neither a spelling nor a
|
|
207
|
+
* reason to lack one.
|
|
208
|
+
*
|
|
209
|
+
* `call` is a name an author writes as a function. `syntax` is everything the
|
|
210
|
+
* language produces from a form instead: an operator, a constructor, a swizzle,
|
|
211
|
+
* the conditional. An opcode has exactly one of the two.
|
|
212
|
+
*/
|
|
213
|
+
export interface SLSurface {
|
|
214
|
+
call?: string
|
|
215
|
+
syntax?: string
|
|
216
|
+
/**
|
|
217
|
+
* Set when the surface form does not emit this opcode at all: it expands
|
|
218
|
+
* into ops both backends already have. `ramp` and `remap` are macros for
|
|
219
|
+
* the same reason `sl.ramp` is, and the opcode number stays reserved.
|
|
220
|
+
*/
|
|
221
|
+
macro?: true
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export const SL_HLSL: Record<number, SLSurface> = {
|
|
225
|
+
[SLOP.CONST]: { syntax: "a number literal" },
|
|
226
|
+
[SLOP.INPUT]: { syntax: "uv, fragCoord, resolution, time, aspect" },
|
|
227
|
+
[SLOP.UNIFORM]: { syntax: "uniform <type> name = <default>;" },
|
|
228
|
+
[SLOP.COMPOSE]: { syntax: "float2(), float3(), float4()" },
|
|
229
|
+
[SLOP.SWIZZLE]: { syntax: ".xyzw / .rgba" },
|
|
230
|
+
|
|
231
|
+
[SLOP.ADD]: { syntax: "+" },
|
|
232
|
+
[SLOP.SUB]: { syntax: "-" },
|
|
233
|
+
[SLOP.MUL]: { syntax: "*" },
|
|
234
|
+
[SLOP.DIV]: { syntax: "/" },
|
|
235
|
+
[SLOP.MOD]: { syntax: "%" },
|
|
236
|
+
[SLOP.POW]: { call: "pow" },
|
|
237
|
+
[SLOP.NEG]: { syntax: "unary -" },
|
|
238
|
+
[SLOP.RECIP]: { call: "rcp" },
|
|
239
|
+
|
|
240
|
+
[SLOP.SIN]: { call: "sin" },
|
|
241
|
+
[SLOP.COS]: { call: "cos" },
|
|
242
|
+
[SLOP.TAN]: { call: "tan" },
|
|
243
|
+
[SLOP.ASIN]: { call: "asin" },
|
|
244
|
+
[SLOP.ACOS]: { call: "acos" },
|
|
245
|
+
[SLOP.ATAN2]: { call: "atan2" },
|
|
246
|
+
[SLOP.EXP]: { call: "exp" },
|
|
247
|
+
[SLOP.LOG]: { call: "log" },
|
|
248
|
+
[SLOP.SQRT]: { call: "sqrt" },
|
|
249
|
+
[SLOP.ABS]: { call: "abs" },
|
|
250
|
+
[SLOP.SIGN]: { call: "sign" },
|
|
251
|
+
[SLOP.FLOOR]: { call: "floor" },
|
|
252
|
+
[SLOP.CEIL]: { call: "ceil" },
|
|
253
|
+
[SLOP.ROUND]: { call: "round" },
|
|
254
|
+
[SLOP.FRACT]: { call: "frac" },
|
|
255
|
+
[SLOP.MIN]: { call: "min" },
|
|
256
|
+
[SLOP.MAX]: { call: "max" },
|
|
257
|
+
[SLOP.CLAMP]: { call: "clamp" },
|
|
258
|
+
[SLOP.SATURATE]: { call: "saturate" },
|
|
259
|
+
|
|
260
|
+
[SLOP.LENGTH]: { call: "length" },
|
|
261
|
+
[SLOP.DISTANCE]: { call: "distance" },
|
|
262
|
+
[SLOP.DOT]: { call: "dot" },
|
|
263
|
+
[SLOP.CROSS]: { call: "cross" },
|
|
264
|
+
[SLOP.NORMALIZE]: { call: "normalize" },
|
|
265
|
+
[SLOP.REFLECT]: { call: "reflect" },
|
|
266
|
+
|
|
267
|
+
[SLOP.MIX]: { call: "lerp" },
|
|
268
|
+
[SLOP.STEP]: { call: "step" },
|
|
269
|
+
[SLOP.SMOOTHSTEP]: { call: "smoothstep" },
|
|
270
|
+
// `select` exists as a name too, because `sl.select` does and a contract
|
|
271
|
+
// test holds the two surfaces level, but `?:` is how anyone writes it.
|
|
272
|
+
[SLOP.SELECT]: { syntax: "?:" },
|
|
273
|
+
[SLOP.REMAP]: { call: "remap", macro: true },
|
|
274
|
+
|
|
275
|
+
[SLOP.RAMP]: { call: "ramp", macro: true },
|
|
276
|
+
[SLOP.HSV2RGB]: { call: "hsv2rgb" },
|
|
277
|
+
[SLOP.RGB2HSV]: { call: "rgb2hsv" },
|
|
278
|
+
[SLOP.LUMINANCE]: { call: "luminance" },
|
|
279
|
+
[SLOP.TO_LINEAR]: { call: "toLinear" },
|
|
280
|
+
|
|
281
|
+
[SLOP.NOISE]: { call: "noise" },
|
|
282
|
+
[SLOP.SIMPLEX]: { call: "simplex" },
|
|
283
|
+
[SLOP.FBM]: { call: "fbm" },
|
|
284
|
+
[SLOP.SDF]: { call: "sdf" },
|
|
285
|
+
[SLOP.VORONOI]: { call: "voronoi" },
|
|
286
|
+
[SLOP.TURBULENCE]: { call: "turbulence" },
|
|
287
|
+
[SLOP.RIDGED]: { call: "ridged" },
|
|
288
|
+
[SLOP.SDF_WIDE]: { syntax: "sdf.<shape>(p, ...) given a fifth or sixth parameter" },
|
|
289
|
+
|
|
290
|
+
[SLOP.SAMPLE]: { call: "tex2D" },
|
|
291
|
+
[SLOP.SAMPLE_LOD]: { call: "tex2Dlod" },
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Opcodes no backend implements.
|
|
296
|
+
*
|
|
297
|
+
* Numbered so the families stay in order, and refused at the surface until
|
|
298
|
+
* somebody writes both halves. A program reaching one would render as whatever
|
|
299
|
+
* the VM's dispatch falls through to and fail outright in the HLSL emitter,
|
|
300
|
+
* which is the silent-disagreement failure this design exists to prevent, so
|
|
301
|
+
* the parser names them rather than treating them as unknown identifiers.
|
|
302
|
+
*
|
|
303
|
+
* `REMAP` is here because the opcode is unimplemented; the SURFACE `remap` is a
|
|
304
|
+
* macro over arithmetic and works everywhere.
|
|
305
|
+
*/
|
|
306
|
+
export const SL_UNIMPLEMENTED: Record<number, string> = {
|
|
307
|
+
[SLOP.RGB2HSV]: "neither the VM shader nor the HLSL emitter has a case for it",
|
|
308
|
+
[SLOP.SAMPLE_LOD]: "the VM samples without an explicit LOD; use tex2D",
|
|
309
|
+
[SLOP.REMAP]: "the surface form is a macro over arithmetic, so the opcode is unused",
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* GLSL spellings, recognised only so they can be refused by their HLSL name.
|
|
314
|
+
*
|
|
315
|
+
* Accepting both would be two ways to write everything and a table that has to
|
|
316
|
+
* stay complete in two columns forever (`Specs/SL_TEXT.md` 9.1). Recognising
|
|
317
|
+
* them costs one entry each and turns "unknown identifier mix" into the answer.
|
|
318
|
+
*/
|
|
319
|
+
export const SL_GLSL_HINT: Record<string, string> = {
|
|
320
|
+
mix: "lerp",
|
|
321
|
+
fract: "frac",
|
|
322
|
+
mod: "%",
|
|
323
|
+
texture: "tex2D",
|
|
324
|
+
textureLod: "tex2Dlod",
|
|
325
|
+
vec2: "float2",
|
|
326
|
+
vec3: "float3",
|
|
327
|
+
vec4: "float4",
|
|
328
|
+
ivec2: "float2",
|
|
329
|
+
ivec3: "float3",
|
|
330
|
+
ivec4: "float4",
|
|
331
|
+
inversesqrt: "1 / sqrt(x); there is no rsqrt opcode",
|
|
332
|
+
atan: "atan2",
|
|
333
|
+
sampler2D: "texture2D",
|
|
334
|
+
gl_FragCoord: "fragCoord",
|
|
335
|
+
gl_FragColor: "the value main returns",
|
|
336
|
+
iTime: "time",
|
|
337
|
+
iResolution: "resolution",
|
|
338
|
+
iMouse: "a uniform you declare",
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Every opcode that an author can write as a call, by the name they write. */
|
|
342
|
+
export const SL_CALL_NAMES: Record<string, number> = Object.fromEntries(
|
|
343
|
+
Object.entries(SL_HLSL)
|
|
344
|
+
.filter(([, s]) => s.call !== undefined)
|
|
345
|
+
.map(([code, s]) => [s.call!, Number(code)]),
|
|
346
|
+
)
|
package/src/serial.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The IR as JSON, for a host that stores or ships a program rather than its
|
|
3
|
+
* source (`Specs/SL_PACKAGE.md` section 2).
|
|
4
|
+
*
|
|
5
|
+
* `toJSON` writes the version beside the graph. `fromJSON` is the only way
|
|
6
|
+
* back in, and it checks everything an emitter relies on, because an emitter
|
|
7
|
+
* handed a malformed graph does not fail: it prints a shader that computes
|
|
8
|
+
* something else. A newer version is refused with a message naming both; an
|
|
9
|
+
* older one is migrated to this one and rehashed under it.
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
INPUTS, SLError, SL_IR_VERSION, TYPE, hashProgram,
|
|
13
|
+
type NodeRef, type Program, type SLNode, type SLType, type TextureDecl, type UniformDecl,
|
|
14
|
+
} from "./ir"
|
|
15
|
+
import { SL_ARITY, SL_NAME } from "./ops"
|
|
16
|
+
|
|
17
|
+
export interface ProgramJSON {
|
|
18
|
+
/** `SL_IR_VERSION` of the writer. Absent means 1. */
|
|
19
|
+
v: number
|
|
20
|
+
nodes: SLNode[]
|
|
21
|
+
result: NodeRef
|
|
22
|
+
uniforms: UniformDecl[]
|
|
23
|
+
textures: TextureDecl[]
|
|
24
|
+
hash: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function toJSON(p: Program): ProgramJSON {
|
|
28
|
+
return { v: p.version, nodes: p.nodes, result: p.result, uniforms: p.uniforms, textures: p.textures, hash: p.hash }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function fromJSON(json: unknown): Program {
|
|
32
|
+
if (typeof json !== "object" || json === null) fail("a program must be an object")
|
|
33
|
+
const j = json as Partial<ProgramJSON>
|
|
34
|
+
const v = j.v ?? 1
|
|
35
|
+
if (!Number.isInteger(v) || v < 1) fail(`its version must be a whole number from 1, got ${String(j.v)}`)
|
|
36
|
+
if (v > SL_IR_VERSION) {
|
|
37
|
+
fail(`it is IR version ${v}, and this compiler reads up to ${SL_IR_VERSION}. ` +
|
|
38
|
+
`Update onejs-sl to read it, or rebuild it from its .sl source.`)
|
|
39
|
+
}
|
|
40
|
+
if (!Array.isArray(j.nodes) || j.nodes.length === 0) fail("it has no nodes")
|
|
41
|
+
const nodes = j.nodes.map((n, i) => node(n, i))
|
|
42
|
+
const result = j.result
|
|
43
|
+
if (!Number.isInteger(result) || result! < 0 || result! >= nodes.length) fail(`its result ${String(result)} is not a node`)
|
|
44
|
+
if (nodes[result!]!.type !== TYPE.VEC4) fail("its result is not a float4")
|
|
45
|
+
const uniforms = (j.uniforms ?? []).map(uniform)
|
|
46
|
+
const textures = (j.textures ?? []).map(texture)
|
|
47
|
+
for (const n of nodes) {
|
|
48
|
+
if (n.k === "uniform" && n.slot >= uniforms.length) fail(`a node reads uniform slot ${n.slot}, which is not declared`)
|
|
49
|
+
}
|
|
50
|
+
const hash = hashProgram(nodes, result!, uniforms, textures)
|
|
51
|
+
// Same version, so the same maths and the same hash; a different one means
|
|
52
|
+
// the file was edited or damaged, and its cached shader belongs to
|
|
53
|
+
// something else. An older version is rehashed under this one on purpose.
|
|
54
|
+
if (v === SL_IR_VERSION && j.hash !== undefined && j.hash !== hash) {
|
|
55
|
+
fail(`its hash ${j.hash} does not match its nodes (${hash}); it was changed after it was written`)
|
|
56
|
+
}
|
|
57
|
+
return { version: SL_IR_VERSION, nodes, result: result!, uniforms, textures, hash, loops: [] }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function node(n: unknown, i: number): SLNode {
|
|
61
|
+
if (typeof n !== "object" || n === null) fail(`node ${i} is not an object`)
|
|
62
|
+
const x = n as Record<string, unknown>
|
|
63
|
+
const type = x.type
|
|
64
|
+
if (type !== 1 && type !== 2 && type !== 3 && type !== 4) fail(`node ${i} has no width 1 to 4`)
|
|
65
|
+
const t = type as SLType
|
|
66
|
+
const ref = (r: unknown, what: string): NodeRef => {
|
|
67
|
+
if (!Number.isInteger(r) || (r as number) < 0 || (r as number) >= i) {
|
|
68
|
+
fail(`node ${i}'s ${what} ${String(r)} does not refer to an earlier node`)
|
|
69
|
+
}
|
|
70
|
+
return r as number
|
|
71
|
+
}
|
|
72
|
+
const nums = (a: unknown, what: string): number[] => {
|
|
73
|
+
if (!Array.isArray(a) || !a.every((e) => typeof e === "number" && Number.isFinite(e))) {
|
|
74
|
+
fail(`node ${i}'s ${what} must be finite numbers`)
|
|
75
|
+
}
|
|
76
|
+
return a as number[]
|
|
77
|
+
}
|
|
78
|
+
switch (x.k) {
|
|
79
|
+
case "const": {
|
|
80
|
+
const v = nums(x.v, "value")
|
|
81
|
+
if (v.length !== t) fail(`node ${i} is a float${t === 1 ? "" : t} constant with ${v.length} values`)
|
|
82
|
+
return { k: "const", type: t, v }
|
|
83
|
+
}
|
|
84
|
+
case "input": {
|
|
85
|
+
const name = x.name as keyof typeof INPUTS
|
|
86
|
+
if (!(name in INPUTS) || INPUTS[name] !== t) fail(`node ${i} reads an input "${String(x.name)}" that does not exist at that width`)
|
|
87
|
+
return { k: "input", type: t, name }
|
|
88
|
+
}
|
|
89
|
+
case "uniform": {
|
|
90
|
+
if (!Number.isInteger(x.slot) || (x.slot as number) < 0) fail(`node ${i} has no uniform slot`)
|
|
91
|
+
return { k: "uniform", type: t, slot: x.slot as number }
|
|
92
|
+
}
|
|
93
|
+
case "swizzle": {
|
|
94
|
+
const chans = nums(x.chans, "channels")
|
|
95
|
+
if (chans.length !== t || !chans.every((c) => Number.isInteger(c) && c >= 0 && c <= 3)) {
|
|
96
|
+
fail(`node ${i}'s swizzle does not pick ${t} channels`)
|
|
97
|
+
}
|
|
98
|
+
return { k: "swizzle", type: t, src: ref(x.src, "source"), chans }
|
|
99
|
+
}
|
|
100
|
+
case "call": {
|
|
101
|
+
const op = x.op as number
|
|
102
|
+
if (SL_NAME[op] === undefined) {
|
|
103
|
+
fail(`node ${i} calls opcode ${String(x.op)}, which this compiler does not know. ` +
|
|
104
|
+
`A newer compiler wrote it; its version says otherwise, so the file is damaged.`)
|
|
105
|
+
}
|
|
106
|
+
if (!Array.isArray(x.args)) fail(`node ${i} has no arguments list`)
|
|
107
|
+
const args = (x.args as unknown[]).map((a) => ref(a, "argument"))
|
|
108
|
+
const arity = SL_ARITY[op]
|
|
109
|
+
if (arity !== undefined && arity >= 0 && args.length !== arity) {
|
|
110
|
+
fail(`node ${i} calls ${SL_NAME[op]} with ${args.length} arguments; it takes ${arity}`)
|
|
111
|
+
}
|
|
112
|
+
const out: SLNode = { k: "call", type: t, op: op as never, args }
|
|
113
|
+
if (x.imm !== undefined) out.imm = nums(x.imm, "immediates")
|
|
114
|
+
return out
|
|
115
|
+
}
|
|
116
|
+
default:
|
|
117
|
+
fail(`node ${i} is a "${String(x.k)}", which is not a kind of node`)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function uniform(u: unknown, i: number): UniformDecl {
|
|
122
|
+
const x = (u ?? {}) as Partial<UniformDecl>
|
|
123
|
+
if (typeof x.name !== "string" || ![1, 2, 3, 4].includes(x.type as number) || !Array.isArray(x.value)) {
|
|
124
|
+
fail(`uniform ${i} needs a name, a width and a value`)
|
|
125
|
+
}
|
|
126
|
+
return { name: x.name!, type: x.type as SLType, value: x.value!.slice() }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function texture(t: unknown, i: number): TextureDecl {
|
|
130
|
+
const x = (t ?? {}) as Partial<TextureDecl>
|
|
131
|
+
if (typeof x.name !== "string" || x.slot !== i) fail(`texture ${i} needs a name and slot ${i}`)
|
|
132
|
+
return { name: x.name!, slot: i }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function fail(why: string): never {
|
|
136
|
+
throw new SLError(`this program's IR cannot be read: ${why}.`)
|
|
137
|
+
}
|
package/src/shapes.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shape names and ids for `sl.sdf`.
|
|
3
|
+
*
|
|
4
|
+
* A SECOND COPY of `fx/sdf.ts`'s table, deliberately, and guarded by a test
|
|
5
|
+
* that compares the two.
|
|
6
|
+
*
|
|
7
|
+
* `fx` lives in onejs-unity and this package has no Unity in it, so the table
|
|
8
|
+
* cannot be imported from there. It was a copy before the move too: OneJS's
|
|
9
|
+
* Play eject scaffold vendors each module into its own folder and does not
|
|
10
|
+
* rewrite relative paths across modules. onejs-unity's `sl/shapes.test.ts`
|
|
11
|
+
* compares the two.
|
|
12
|
+
*
|
|
13
|
+
* Duplicating and pinning is the trade this repository already makes for the
|
|
14
|
+
* painter and particle opcode tables, and for the SDF dispatcher that exists in
|
|
15
|
+
* both FxSources and SLCommon. The ids are a contract with SDF2D.cginc either
|
|
16
|
+
* way; what matters is that a test fails when they drift, not that there is
|
|
17
|
+
* only one copy.
|
|
18
|
+
*/
|
|
19
|
+
export const SL_SDF_SHAPES = {
|
|
20
|
+
circle: 0, roundedBox: 1, box: 2, orientedBox: 3, segment: 4, rhombus: 5,
|
|
21
|
+
trapezoid: 6, parallelogram: 7, equilateralTriangle: 8, triangleIsosceles: 9,
|
|
22
|
+
triangle: 10, unevenCapsule: 11, pentagon: 12, hexagon: 13, octagon: 14,
|
|
23
|
+
hexagram: 15, star5: 16, star: 17, pie: 18, cutDisk: 19, arc: 20, ring: 21,
|
|
24
|
+
horseshoe: 22, vesica: 23, orientedVesica: 24, moon: 25, roundedCross: 26,
|
|
25
|
+
egg: 27, heart: 28, cross: 29, roundedX: 30, ellipse: 31, parabola: 32,
|
|
26
|
+
parabolaSegment: 33, bezier: 34, blobbyCross: 35, tunnel: 36, stairs: 37,
|
|
27
|
+
quadraticCircle: 38, hyperbola: 39, coolS: 40, circleWave: 41,
|
|
28
|
+
} as const
|
|
29
|
+
|
|
30
|
+
export type SlSdfKind = keyof typeof SL_SDF_SHAPES
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* How many parameters each shape reads, in the order SDF2D.cginc takes them
|
|
34
|
+
* (its `a` then `b`). Six shapes take more than four: roundedBox's last two
|
|
35
|
+
* corner radii, orientedBox's thickness, triangle's third vertex, horseshoe's
|
|
36
|
+
* second width, orientedVesica's width and bezier's end point. `sl.sdf` takes
|
|
37
|
+
* up to this many, and a test derives the same counts from the web emitters'
|
|
38
|
+
* call table, so the two cannot drift.
|
|
39
|
+
*/
|
|
40
|
+
export const SL_SDF_PARAMS: Record<SlSdfKind, number> = {
|
|
41
|
+
circle: 1, roundedBox: 6, box: 2, orientedBox: 5, segment: 4, rhombus: 2,
|
|
42
|
+
trapezoid: 3, parallelogram: 3, equilateralTriangle: 1, triangleIsosceles: 2,
|
|
43
|
+
triangle: 6, unevenCapsule: 3, pentagon: 1, hexagon: 1, octagon: 1,
|
|
44
|
+
hexagram: 1, star5: 2, star: 3, pie: 3, cutDisk: 2, arc: 4, ring: 4,
|
|
45
|
+
horseshoe: 5, vesica: 2, orientedVesica: 5, moon: 3, roundedCross: 1,
|
|
46
|
+
egg: 4, heart: 0, cross: 3, roundedX: 2, ellipse: 2, parabola: 1,
|
|
47
|
+
parabolaSegment: 2, bezier: 6, blobbyCross: 1, tunnel: 2, stairs: 3,
|
|
48
|
+
quadraticCircle: 0, hyperbola: 2, coolS: 0, circleWave: 2,
|
|
49
|
+
}
|