onejs-sl 0.8.0 → 0.9.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 +27 -0
- package/README.md +44 -11
- package/goldens/goldens.json +45 -2
- package/package.json +1 -1
- package/src/body.ts +76 -6
- package/src/core.ts +3 -2
- package/src/hlsl.ts +59 -4
- package/src/index.ts +2 -2
- package/src/ir.ts +91 -10
- package/src/lang/README.md +22 -3
- package/src/lang/ast.ts +6 -1
- package/src/lang/attributes.ts +75 -2
- package/src/lang/builtins.ts +2 -0
- package/src/lang/check.ts +14 -6
- package/src/lang/classify.ts +48 -4
- package/src/lang/glsl.ts +13 -4
- package/src/lang/index.ts +2 -2
- package/src/lang/lower.ts +17 -2
- package/src/lang/parser.ts +6 -5
- package/src/ops.ts +23 -9
- package/src/params.ts +11 -1
- package/src/serial.ts +16 -5
- package/src/sl.ts +39 -5
- package/src/tables.ts +5 -3
- package/src/web.ts +59 -13
package/src/ops.ts
CHANGED
|
@@ -26,16 +26,21 @@
|
|
|
26
26
|
* for a host to read.
|
|
27
27
|
*
|
|
28
28
|
* They are what OneJS draws a program with. It keeps a program's uniforms in
|
|
29
|
-
*
|
|
30
|
-
* (`SLProgramBridge.MaxUniforms`, the web frame's `sl_U`), and binds
|
|
31
|
-
* textures
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
29
|
+
* 64 slots in the editor, a native player and a browser alike
|
|
30
|
+
* (`SLProgramBridge.MaxUniforms`, the web frame's `sl_U`), and binds eight
|
|
31
|
+
* textures everywhere (`_Tex0` to `_Tex7`). A program past either has
|
|
32
|
+
* something with no slot of its own, which one host would drop and another
|
|
33
|
+
* would draw, so the parser and the builder both refuse it at the declaration,
|
|
34
|
+
* the parser with a file and a line.
|
|
35
|
+
*
|
|
36
|
+
* Both sit inside every backend's minimum (`Specs/SL_NEXT.md` 8). Eight
|
|
37
|
+
* textures and the previous frame are nine sampled textures and at most nine
|
|
38
|
+
* samplers, where WebGPU, WebGL2, OpenGL ES 3 and Direct3D 11 all guarantee 16
|
|
39
|
+
* a fragment stage. 64 float4s are 1 KB of uniforms, where WebGL2 and OpenGL
|
|
40
|
+
* ES 3 guarantee 224 vectors and WebGPU 64 KB.
|
|
36
41
|
*/
|
|
37
|
-
export const UNIFORM_SLOTS =
|
|
38
|
-
export const TEXTURE_SLOTS =
|
|
42
|
+
export const UNIFORM_SLOTS = 64
|
|
43
|
+
export const TEXTURE_SLOTS = 8
|
|
39
44
|
|
|
40
45
|
/** Input ids, which the web emitter switches on. */
|
|
41
46
|
export const INPUT_ID: Record<string, number> = {
|
|
@@ -149,6 +154,11 @@ export const SLOP = {
|
|
|
149
154
|
// What this program drew the frame before, at a uv (IR 5). No slot: the
|
|
150
155
|
// host keeps it, and a program that never reads it has none.
|
|
151
156
|
SAMPLE_PREVIOUS: 146,
|
|
157
|
+
// IR 6. A texture's size in pixels, a float2, its slot the one immediate.
|
|
158
|
+
TEXTURE_SIZE: 147,
|
|
159
|
+
// IR 6. One texel, unfiltered, at a float2 pixel: floored and held inside
|
|
160
|
+
// the texture, so every backend reads the same texel at every pixel.
|
|
161
|
+
SAMPLE_FETCH: 148,
|
|
152
162
|
} as const
|
|
153
163
|
|
|
154
164
|
export type SLOpCode = (typeof SLOP)[keyof typeof SLOP]
|
|
@@ -191,6 +201,7 @@ export const SL_ARITY: Record<number, number> = {
|
|
|
191
201
|
[SLOP.VORONOI]: 1, [SLOP.TURBULENCE]: -1, [SLOP.RIDGED]: -1,
|
|
192
202
|
|
|
193
203
|
[SLOP.SAMPLE]: 1, [SLOP.SAMPLE_LOD]: 2, [SLOP.SAMPLE_PREVIOUS]: 1,
|
|
204
|
+
[SLOP.TEXTURE_SIZE]: 0, [SLOP.SAMPLE_FETCH]: 1,
|
|
194
205
|
}
|
|
195
206
|
|
|
196
207
|
/** Name per opcode, for error messages and for the HLSL emitter's comments. */
|
|
@@ -316,6 +327,8 @@ export const SL_HLSL: Record<number, SLSurface> = {
|
|
|
316
327
|
[SLOP.SAMPLE]: { call: "tex2D" },
|
|
317
328
|
[SLOP.SAMPLE_LOD]: { call: "tex2Dlod" },
|
|
318
329
|
[SLOP.SAMPLE_PREVIOUS]: { syntax: "tex2D(previous, uv)" },
|
|
330
|
+
[SLOP.TEXTURE_SIZE]: { call: "textureSize" },
|
|
331
|
+
[SLOP.SAMPLE_FETCH]: { call: "tex2Dfetch" },
|
|
319
332
|
}
|
|
320
333
|
|
|
321
334
|
/**
|
|
@@ -347,6 +360,7 @@ export const SL_GLSL_HINT: Record<string, string> = {
|
|
|
347
360
|
mod: "%",
|
|
348
361
|
texture: "tex2D",
|
|
349
362
|
textureLod: "tex2Dlod",
|
|
363
|
+
texelFetch: "tex2Dfetch",
|
|
350
364
|
vec2: "float2",
|
|
351
365
|
vec3: "float3",
|
|
352
366
|
vec4: "float4",
|
package/src/params.ts
CHANGED
|
@@ -21,7 +21,7 @@ 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
23
|
import type { PREVIOUS, SourceInputName } from "./ir"
|
|
24
|
-
import type { AttributeName } from "./lang/attributes"
|
|
24
|
+
import type { AttributeName, TextureAttributeName } from "./lang/attributes"
|
|
25
25
|
|
|
26
26
|
/** One function of the helper library, as its source declares it. */
|
|
27
27
|
export interface LibSignature {
|
|
@@ -55,6 +55,8 @@ const INTRINSIC: Record<string, readonly string[]> = {
|
|
|
55
55
|
lerp: ["x", "y", "s"], step: ["y", "x"], smoothstep: ["min", "max", "x"],
|
|
56
56
|
tex2D: ["s", "t"],
|
|
57
57
|
tex2Dlod: ["s", "t", "lod"],
|
|
58
|
+
textureSize: ["s"],
|
|
59
|
+
tex2Dfetch: ["s", "pixel"],
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
/**
|
|
@@ -163,6 +165,8 @@ export const BUILTIN_DOCS: Readonly<Record<string, string>> = {
|
|
|
163
165
|
ridged: "Turbulence made bright at its creases: ridges, lightning, cracks.",
|
|
164
166
|
tex2D: "The colour of texture s at the uv t, with straight alpha.",
|
|
165
167
|
tex2Dlod: "Texture s at uv t from mip level lod: 0 is full size, 1 half, and a fraction blends the two.",
|
|
168
|
+
textureSize: "The width and height of texture s in pixels, at full size, as a float2.",
|
|
169
|
+
tex2Dfetch: "One texel of texture s at pixel, unfiltered; a pixel past an edge reads the edge.",
|
|
166
170
|
sdf: "The signed distance from p to a shape, negative inside: `sdf.circle(p, r)`.",
|
|
167
171
|
ramp: "t mapped through evenly spaced colour stops, from the first at 0 to the last at 1.",
|
|
168
172
|
}
|
|
@@ -181,6 +185,12 @@ export const ATTRIBUTE_DOCS: Readonly<Record<AttributeName, string>> = {
|
|
|
181
185
|
Hide: "No control: the host sets this uniform from code.",
|
|
182
186
|
}
|
|
183
187
|
|
|
188
|
+
/** One line on each texture attribute, for the same tooltips. */
|
|
189
|
+
export const TEXTURE_ATTRIBUTE_DOCS: Readonly<Record<TextureAttributeName, string>> = {
|
|
190
|
+
Wrap: "How uv outside 0 to 1 reads: [Wrap(Repeat)] tiles, [Wrap(Mirror)] tiles mirrored, [Wrap(Clamp)] holds the edge.",
|
|
191
|
+
Filter: "How the texture reads between texels: [Filter(Point)] the nearest, [Filter(Linear)] a blend of four.",
|
|
192
|
+
}
|
|
193
|
+
|
|
184
194
|
/**
|
|
185
195
|
* One line on each input, for the same tooltips. In the words of no one host:
|
|
186
196
|
* the output is a UI element in OneJS and an image on a Magerie board, and the
|
package/src/serial.ts
CHANGED
|
@@ -10,10 +10,13 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import {
|
|
12
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,
|
|
13
|
+
programVersion, samplingProblem, 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
|
+
/** The ops that name a texture slot in their first immediate. */
|
|
18
|
+
const TEXTURE_OPS = new Set<number>([SLOP.SAMPLE, SLOP.SAMPLE_LOD, SLOP.TEXTURE_SIZE, SLOP.SAMPLE_FETCH])
|
|
19
|
+
|
|
17
20
|
const HOST_INPUTS: Readonly<Record<HostInputName, SLType>> = { ...INPUTS, ...STEP_INPUTS }
|
|
18
21
|
|
|
19
22
|
export interface ProgramJSON {
|
|
@@ -54,7 +57,8 @@ export function fromJSON(json: unknown): Program {
|
|
|
54
57
|
// as they are) or was edited. One that claims less than its nodes need
|
|
55
58
|
// holds something its version never had, and a version 1 file has
|
|
56
59
|
// nothing IR 3 added.
|
|
57
|
-
const
|
|
60
|
+
const textures = (j.textures ?? []).map(texture)
|
|
61
|
+
const needs = programVersion(written, j.result!, textures)
|
|
58
62
|
if (v < needs && (v >= 2 || needs >= 3)) fail(`it says IR version ${v} and holds nodes version ${needs} added; it was changed after it was written`)
|
|
59
63
|
const { nodes, result } = v < 3 ? toOperands(written, j.result!) : { nodes: written, result: j.result! }
|
|
60
64
|
nodes.forEach((n, i) => {
|
|
@@ -64,12 +68,14 @@ export function fromJSON(json: unknown): Program {
|
|
|
64
68
|
const out = nodes[result]!
|
|
65
69
|
if (out.k === "if" || out.k === "loop" || out.type !== TYPE.VEC4 || out.kind !== undefined) fail("its result is not a float4")
|
|
66
70
|
const uniforms = (j.uniforms ?? []).map(uniform)
|
|
67
|
-
const textures = (j.textures ?? []).map(texture)
|
|
68
71
|
checkCaps({ uniforms, textures })
|
|
69
72
|
for (const n of nodes) {
|
|
70
73
|
if (n.k === "uniform" && n.slot >= uniforms.length) fail(`a node reads uniform slot ${n.slot}, which is not declared`)
|
|
74
|
+
if (n.k === "call" && TEXTURE_OPS.has(n.op) && !(n.imm !== undefined && n.imm[0]! < textures.length)) {
|
|
75
|
+
fail(`a node reads texture slot ${String(n.imm?.[0])}, which is not declared`)
|
|
76
|
+
}
|
|
71
77
|
}
|
|
72
|
-
const version = programVersion(nodes, result)
|
|
78
|
+
const version = programVersion(nodes, result, textures)
|
|
73
79
|
const hash = hashProgram(nodes, result, uniforms, textures)
|
|
74
80
|
// The same IR version and hash scheme, so the same hash; a different one
|
|
75
81
|
// means the file was edited or damaged, and its cached shader belongs to
|
|
@@ -245,7 +251,12 @@ function uniform(u: unknown, i: number): UniformDecl {
|
|
|
245
251
|
function texture(t: unknown, i: number): TextureDecl {
|
|
246
252
|
const x = (t ?? {}) as Partial<TextureDecl>
|
|
247
253
|
if (typeof x.name !== "string" || x.slot !== i) fail(`texture ${i} needs a name and slot ${i}`)
|
|
248
|
-
|
|
254
|
+
const problem = samplingProblem(x)
|
|
255
|
+
if (problem !== null) fail(`texture ${i}: ${problem}`)
|
|
256
|
+
const out: TextureDecl = { name: x.name!, slot: i }
|
|
257
|
+
if (x.wrap !== undefined) out.wrap = x.wrap
|
|
258
|
+
if (x.filter !== undefined) out.filter = x.filter
|
|
259
|
+
return out
|
|
249
260
|
}
|
|
250
261
|
|
|
251
262
|
function fail(why: string): never {
|
package/src/sl.ts
CHANGED
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
import { parseColor as parseHex } from "./color"
|
|
26
26
|
import {
|
|
27
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
|
|
28
|
+
type HostInputName, type InputName, type NodeRef, type Program, type SLKind, type SLNode, type SLType, type TextureFilter,
|
|
29
|
+
type TextureWrap, type UniformControl,
|
|
29
30
|
} from "./ir"
|
|
30
31
|
import { SLOP, SL_HLSL, SL_NAME, type SLOpCode } from "./ops"
|
|
31
32
|
import { SL_SDF_PARAMS, SL_SDF_SHAPES, type SlSdfKind } from "./shapes"
|
|
@@ -561,7 +562,7 @@ export function program(fn: (inputs: ProgramInputs) => Vec4): Program {
|
|
|
561
562
|
}
|
|
562
563
|
const nodes: SLNode[] = b.nodes.slice()
|
|
563
564
|
return {
|
|
564
|
-
version: programVersion(nodes, out.ref),
|
|
565
|
+
version: programVersion(nodes, out.ref, b.textures),
|
|
565
566
|
nodes,
|
|
566
567
|
result: out.ref,
|
|
567
568
|
uniforms: b.uniforms.slice(),
|
|
@@ -1033,7 +1034,10 @@ export const uniform = {
|
|
|
1033
1034
|
}
|
|
1034
1035
|
|
|
1035
1036
|
export interface Texture {
|
|
1036
|
-
/**
|
|
1037
|
+
/**
|
|
1038
|
+
* The colour at `uv`, filtered and wrapped as the texture's own settings
|
|
1039
|
+
* say, or as `sl.texture`'s sampling says when it was given one.
|
|
1040
|
+
*/
|
|
1037
1041
|
sample(uv: Vec2): Vec4
|
|
1038
1042
|
/**
|
|
1039
1043
|
* The colour at `uv` from mip level `lod`: 0 is the full size texture, 1
|
|
@@ -1041,11 +1045,31 @@ export interface Texture {
|
|
|
1041
1045
|
* texture's filter mode. A texture with no mips has only level 0.
|
|
1042
1046
|
*/
|
|
1043
1047
|
sampleLevel(uv: Vec2, lod: Num): Vec4
|
|
1048
|
+
/** The texture's width and height in pixels, at full size. `textureSize(t)` in a `.sl` file. */
|
|
1049
|
+
size(): Vec2
|
|
1050
|
+
/**
|
|
1051
|
+
* One texel, unfiltered, at a pixel: (0, 0) is the texel `uv` (0, 0)
|
|
1052
|
+
* samples. The pixel is floored and held inside the texture, so a pixel
|
|
1053
|
+
* past an edge reads the edge on every host. Full size only, and the
|
|
1054
|
+
* sampling settings do not apply. `tex2Dfetch(t, pixel)` in a `.sl` file.
|
|
1055
|
+
*/
|
|
1056
|
+
fetch(pixel: Vec2): Vec4
|
|
1044
1057
|
}
|
|
1045
1058
|
|
|
1046
|
-
|
|
1059
|
+
/**
|
|
1060
|
+
* How a program samples a texture, when it should not follow the bound
|
|
1061
|
+
* texture's own settings. Either half set, the program owns the sampler and
|
|
1062
|
+
* the other half is clamp or linear. `[Wrap(Repeat)]` and `[Filter(Point)]`
|
|
1063
|
+
* in a `.sl` file.
|
|
1064
|
+
*/
|
|
1065
|
+
export interface TextureSamplingOptions {
|
|
1066
|
+
wrap?: TextureWrap
|
|
1067
|
+
filter?: TextureFilter
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
export function texture(name: string, sampling: TextureSamplingOptions = {}): Texture {
|
|
1047
1071
|
const b = ctx()
|
|
1048
|
-
const slot = b.texture(name)
|
|
1072
|
+
const slot = b.texture(name, sampling)
|
|
1049
1073
|
return {
|
|
1050
1074
|
sample(uv: Vec2): Vec4 {
|
|
1051
1075
|
if (uv.owner !== b) throw new SLError("a value from another program cannot be used in this one")
|
|
@@ -1060,6 +1084,16 @@ export function texture(name: string): Texture {
|
|
|
1060
1084
|
}
|
|
1061
1085
|
return mk(b, b.call(SLOP.SAMPLE_LOD, TYPE.VEC4, [uv.ref, typeof level === "number" ? level : level.ref], [slot]), TYPE.VEC4)
|
|
1062
1086
|
},
|
|
1087
|
+
size(): Vec2 {
|
|
1088
|
+
return mk(b, b.call(SLOP.TEXTURE_SIZE, TYPE.VEC2, [], [slot]), TYPE.VEC2)
|
|
1089
|
+
},
|
|
1090
|
+
fetch(pixel: Vec2): Vec4 {
|
|
1091
|
+
if (pixel.owner !== b) throw new SLError("a value from another program cannot be used in this one")
|
|
1092
|
+
if (pixel.width !== TYPE.VEC2 || pixel.kind !== undefined) {
|
|
1093
|
+
throw new SLError(`a texel is fetched at a float2 pixel, and this is ${pixel.kind === undefined ? `a ${widthName(pixel.width)}` : `an ${pixel.kind}`}`)
|
|
1094
|
+
}
|
|
1095
|
+
return mk(b, b.call(SLOP.SAMPLE_FETCH, TYPE.VEC4, [pixel.ref], [slot]), TYPE.VEC4)
|
|
1096
|
+
},
|
|
1063
1097
|
}
|
|
1064
1098
|
}
|
|
1065
1099
|
|
package/src/tables.ts
CHANGED
|
@@ -16,9 +16,11 @@ export type { SLSurface } from "./ops"
|
|
|
16
16
|
export { DERIVED_INPUTS, INPUTS, PREVIOUS, SOURCE_INPUTS, STEP_INPUTS } from "./ir"
|
|
17
17
|
export type { DerivedInputName, HostInputName, InputName, SourceInputName, StepInputName } from "./ir"
|
|
18
18
|
export { SL_SDF_SHAPES, SL_SDF_PARAMS } from "./shapes"
|
|
19
|
-
export {
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
export {
|
|
20
|
+
ATTRIBUTE_DOCS, BUILTIN_DOCS, BUILTIN_PARAMS, INPUT_DOCS, LIB_SIGNATURES, SL_SDF_PARAM_NAMES, TEXTURE_ATTRIBUTE_DOCS,
|
|
21
|
+
} from "./params"
|
|
22
|
+
export { ATTRIBUTE_NAMES, TEXTURE_ATTRIBUTE_NAMES } from "./lang/attributes"
|
|
23
|
+
export type { AttributeName, TextureAttributeName } from "./lang/attributes"
|
|
22
24
|
export { PRELUDE_DOCS, PRELUDE_NAMES } from "./lang/prelude-source"
|
|
23
25
|
export { SL_KEYWORDS, SL_TYPES } from "./lang/words"
|
|
24
26
|
export { TYPE_WIDTH } from "./lang/ast"
|
package/src/web.ts
CHANGED
|
@@ -14,11 +14,23 @@
|
|
|
14
14
|
*
|
|
15
15
|
* sl_Res (target width, target height, seconds, frame)
|
|
16
16
|
* sl_Opt (1 in a Linear colour space project else 0, 1 to flip uv.y, deltaTime, 0)
|
|
17
|
-
* sl_U
|
|
17
|
+
* sl_U uniform slots, each a vec4, in the program's slot order: 16 of
|
|
18
|
+
* them, or WEB_UNIFORM_SLOTS for a program with more than 16
|
|
19
|
+
* uniforms. A host fills WEB_UNIFORM_SLOTS whatever the program
|
|
20
|
+
* declares, which both languages allow.
|
|
18
21
|
* textures by slot: GLSL `sl_Tex<slot>`; WGSL `sl_tex<slot>` at binding
|
|
19
22
|
* 2 + 2 * slot with its own sampler `sl_samp<slot>` at 1 + 2 * slot,
|
|
20
23
|
* all in group 0 beside the frame block at binding 0. Only the
|
|
21
|
-
* slots the program
|
|
24
|
+
* slots the program reads are declared, and in WGSL a slot's
|
|
25
|
+
* sampler only when the program samples it, rather than only
|
|
26
|
+
* measuring it (`textureSize`) or fetching from it (`tex2Dfetch`).
|
|
27
|
+
* a texture's sampler, when its program sets one (`[Wrap(Repeat)]`,
|
|
28
|
+
* `[Filter(Point)]`, IR 6): its declaration line ends
|
|
29
|
+
* `// sampler <wrap> <filter>`, the sampler's in WGSL and the
|
|
30
|
+
* sampler2D's in GLSL, wrap one of repeat, clamp, mirror and filter
|
|
31
|
+
* point or linear. The host samples that slot so, whatever the
|
|
32
|
+
* bound texture's own settings; with no such comment, as those
|
|
33
|
+
* settings say. `samplingOf` in ir.ts says what each word means.
|
|
22
34
|
* the previous frame, only for a program that samples it: GLSL `sl_Prev`;
|
|
23
35
|
* WGSL `sl_prev` with its sampler `sl_prevSamp`, at the two
|
|
24
36
|
* bindings after the last texture slot's, whatever the program
|
|
@@ -34,7 +46,7 @@
|
|
|
34
46
|
*/
|
|
35
47
|
|
|
36
48
|
import { INT_LIMITS } from "./body"
|
|
37
|
-
import { SLError, TYPE, isWhole, valueAt, type NodeRef, type Program, type SLKind, type SLType, type ValueNode } from "./ir"
|
|
49
|
+
import { SLError, TYPE, isWhole, samplingOf, valueAt, type NodeRef, type Program, type TextureDecl, type SLKind, type SLType, type ValueNode } from "./ir"
|
|
38
50
|
import { inVaryingFlow, local, printBody, structure, type Syntax } from "./structure"
|
|
39
51
|
import { libClosure, libIndex, LIB_FUNCTIONS, SDF_CALLS } from "./lib"
|
|
40
52
|
import { LIB_GLSL } from "./lib/glsl"
|
|
@@ -43,9 +55,21 @@ import { INPUT_ID, SLOP, TEXTURE_SLOTS, UNIFORM_SLOTS } from "./ops"
|
|
|
43
55
|
|
|
44
56
|
export type WebLanguage = "glsl" | "wgsl"
|
|
45
57
|
|
|
46
|
-
/** Uniform slots
|
|
58
|
+
/** Uniform slots a host provides, so one buffer fits every program. */
|
|
47
59
|
export const WEB_UNIFORM_SLOTS = UNIFORM_SLOTS
|
|
48
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The block a program with 16 uniforms or fewer declares: the size it had
|
|
63
|
+
* before onejs-sl 0.9 raised the cap. A WebGPU host before then (OneJS 3.9.8)
|
|
64
|
+
* binds a buffer of exactly this many slots, which a larger block fails to
|
|
65
|
+
* validate against, so a program that fits keeps drawing there.
|
|
66
|
+
*/
|
|
67
|
+
const BASE_UNIFORM_SLOTS = 16
|
|
68
|
+
|
|
69
|
+
function uniformBlock(p: Program): number {
|
|
70
|
+
return p.uniforms.length > BASE_UNIFORM_SLOTS ? WEB_UNIFORM_SLOTS : BASE_UNIFORM_SLOTS
|
|
71
|
+
}
|
|
72
|
+
|
|
49
73
|
export function emitGLSL(p: Program): string {
|
|
50
74
|
return emitWeb(p, "glsl")
|
|
51
75
|
}
|
|
@@ -59,9 +83,12 @@ function emitWeb(p: Program, lang: WebLanguage): string {
|
|
|
59
83
|
const need = new Set<number>()
|
|
60
84
|
const smoothsteps = new Set<SLType>()
|
|
61
85
|
const sampled = new Set<number>()
|
|
86
|
+
/** Slots read without a sampler: measured or fetched from. */
|
|
87
|
+
const loaded = new Set<number>()
|
|
62
88
|
/** Whether the body samples the previous frame, which declares its binding. */
|
|
63
89
|
let previous = false
|
|
64
90
|
const shape = structure(p)
|
|
91
|
+
const size = (slot: number) => W ? `vec2f(textureDimensions(sl_tex${slot}))` : `vec2(textureSize(sl_Tex${slot}, 0))`
|
|
65
92
|
const T = (t: SLType) => (W ? (t === 1 ? "f32" : `vec${t}f`) : t === 1 ? "float" : `vec${t}`)
|
|
66
93
|
/** A value's type in this language, its kind included. */
|
|
67
94
|
const typeName = (n: { type: SLType; kind?: SLKind }): string => {
|
|
@@ -272,6 +299,20 @@ function emitWeb(p: Program, lang: WebLanguage): string {
|
|
|
272
299
|
const below = `floor(${a[1]})`
|
|
273
300
|
return `mix(${at(below)}, ${at(`${below} + 1.0`)}, fract(${a[1]}))`
|
|
274
301
|
}
|
|
302
|
+
case SLOP.TEXTURE_SIZE: {
|
|
303
|
+
const slot = Math.round(imm[0] ?? 0)
|
|
304
|
+
loaded.add(slot)
|
|
305
|
+
return size(slot)
|
|
306
|
+
}
|
|
307
|
+
case SLOP.SAMPLE_FETCH: {
|
|
308
|
+
const slot = Math.round(imm[0] ?? 0)
|
|
309
|
+
loaded.add(slot)
|
|
310
|
+
// Floored and held inside, as the HLSL body does: a load out of
|
|
311
|
+
// range is undefined in GLSL and implementation defined in WGSL.
|
|
312
|
+
return W
|
|
313
|
+
? `textureLoad(sl_tex${slot}, vec2i(clamp(floor(${a[0]}), vec2f(0.0), ${size(slot)} - 1.0)), 0)`
|
|
314
|
+
: `texelFetch(sl_Tex${slot}, ivec2(clamp(floor(${a[0]}), vec2(0.0), ${size(slot)} - 1.0)), 0)`
|
|
315
|
+
}
|
|
275
316
|
case SLOP.SAMPLE_PREVIOUS:
|
|
276
317
|
previous = true
|
|
277
318
|
// Flipped as this frame flipped the uv it wrote at, so a read at
|
|
@@ -363,21 +404,26 @@ function emitWeb(p: Program, lang: WebLanguage): string {
|
|
|
363
404
|
const body = printBody(p, shape, syntax, expr, " ").join("\n")
|
|
364
405
|
const library = [...[...smoothsteps].sort().map((w) => smoothstep(w, lang)), ...librarySource(need, lang)].join("\n")
|
|
365
406
|
const header = `// GENERATED from a shader language program (${p.hash}). Do not edit.`
|
|
366
|
-
// Only the textures the program
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
const textures = p.textures.filter((t) => sampled.has(t.slot))
|
|
407
|
+
// Only the textures the program reads, and in WGSL only the samplers it
|
|
408
|
+
// samples with. WebGPU's automatic layout leaves out a binding the shader
|
|
409
|
+
// never reads, and a bind group that supplies one anyway does not
|
|
410
|
+
// validate; the host reads these declarations back to know what to bind.
|
|
411
|
+
const textures = p.textures.filter((t) => sampled.has(t.slot) || loaded.has(t.slot))
|
|
412
|
+
/** The comment that says a texture's sampler is the program's own, for a slot it samples. */
|
|
413
|
+
const samplerNote = (t: TextureDecl) => {
|
|
414
|
+
const s = sampled.has(t.slot) ? samplingOf(t) : null
|
|
415
|
+
return s === null ? "" : ` // sampler ${s.wrap} ${s.filter}`
|
|
416
|
+
}
|
|
371
417
|
|
|
372
418
|
if (W) {
|
|
373
419
|
const bindings = [...textures.map((t) =>
|
|
374
|
-
`@group(0) @binding(${1 + 2 * t.slot}) var sl_samp${t.slot}: sampler
|
|
420
|
+
(sampled.has(t.slot) ? `@group(0) @binding(${1 + 2 * t.slot}) var sl_samp${t.slot}: sampler;${samplerNote(t)}\n` : "") +
|
|
375
421
|
`@group(0) @binding(${2 + 2 * t.slot}) var sl_tex${t.slot}: texture_2d<f32>;`),
|
|
376
422
|
...previous ? [
|
|
377
423
|
`@group(0) @binding(${1 + 2 * TEXTURE_SLOTS}) var sl_prevSamp: sampler;\n` +
|
|
378
424
|
`@group(0) @binding(${2 + 2 * TEXTURE_SLOTS}) var sl_prev: texture_2d<f32>;`] : []].join("\n")
|
|
379
425
|
return `${header}
|
|
380
|
-
struct SLFrame { res: vec4f, opt: vec4f, u: array<vec4f, ${
|
|
426
|
+
struct SLFrame { res: vec4f, opt: vec4f, u: array<vec4f, ${uniformBlock(p)}> }
|
|
381
427
|
@group(0) @binding(0) var<uniform> sl: SLFrame;
|
|
382
428
|
${bindings}
|
|
383
429
|
${library}
|
|
@@ -393,14 +439,14 @@ ${body}
|
|
|
393
439
|
}
|
|
394
440
|
`
|
|
395
441
|
}
|
|
396
|
-
const samplers = [...textures.map((t) => `uniform sampler2D sl_Tex${t.slot}
|
|
442
|
+
const samplers = [...textures.map((t) => `uniform sampler2D sl_Tex${t.slot};${samplerNote(t)}`), ...previous ? ["uniform sampler2D sl_Prev;"] : []].join("\n")
|
|
397
443
|
return `#version 300 es
|
|
398
444
|
${header}
|
|
399
445
|
precision highp float;
|
|
400
446
|
precision highp int;
|
|
401
447
|
uniform vec4 sl_Res;
|
|
402
448
|
uniform vec4 sl_Opt;
|
|
403
|
-
uniform vec4 sl_U[${
|
|
449
|
+
uniform vec4 sl_U[${uniformBlock(p)}];
|
|
404
450
|
${samplers}
|
|
405
451
|
out vec4 sl_Out;
|
|
406
452
|
${library}
|