onejs-sl 0.7.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 +33 -0
- package/README.md +61 -15
- package/goldens/goldens.json +56 -5
- package/package.json +1 -1
- package/src/body.ts +87 -8
- package/src/core.ts +3 -2
- package/src/hlsl.ts +59 -4
- package/src/index.ts +2 -2
- package/src/ir.ts +129 -12
- package/src/lang/README.md +41 -12
- 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 +12 -2
- package/src/serial.ts +16 -5
- package/src/sl.ts +41 -7
- package/src/tables.ts +5 -3
- package/src/web.ts +68 -15
package/src/body.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* gives common subexpression elimination for free.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { INT_BOUNDS, SLError, TYPE, valueAt, type InputName, type NodeRef, type StepInputName, type Program, type SLKind, type SLType, type ValueNode } from "./ir"
|
|
24
|
+
import { INT_BOUNDS, SLError, TYPE, isWhole, samplingOf, valueAt, type TextureSampling, type InputName, type NodeRef, type StepInputName, 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"
|
|
@@ -55,7 +55,8 @@ export interface BodyTarget {
|
|
|
55
55
|
* - **Filtered and wrapped by the host.** The body does neither. OneJS's
|
|
56
56
|
* hosts, the generated shader and the web backends alike, use the
|
|
57
57
|
* bound texture's own filter and wrap modes, so a host that filters by
|
|
58
|
-
* hand draws the same picture by following the texture's settings.
|
|
58
|
+
* hand draws the same picture by following the texture's settings. A
|
|
59
|
+
* texture whose program sets its sampler goes to `sampleWith` instead.
|
|
59
60
|
*
|
|
60
61
|
* What becomes of the result is the host's: OneJS writes it to the target
|
|
61
62
|
* as it is, and a host that composites premultiplied multiplies on write.
|
|
@@ -63,12 +64,50 @@ export interface BodyTarget {
|
|
|
63
64
|
sample: (slot: number, uv: string) => string
|
|
64
65
|
/**
|
|
65
66
|
* A float4 sample of the texture in `slot` at `uv` from mip level `lod`, a
|
|
66
|
-
* float expression: 0 is the full size texture
|
|
67
|
-
*
|
|
68
|
-
*
|
|
67
|
+
* float expression: 0 is the full size texture and 1 half. The body only
|
|
68
|
+
* asks for whole levels, or for a level it cannot prove whole, floored and
|
|
69
|
+
* floored plus one, which it blends by the fraction itself; so a fraction
|
|
70
|
+
* draws the same whatever the texture's filter between levels, and a host
|
|
71
|
+
* has no blending to do. A level past the smallest reads the smallest.
|
|
72
|
+
* Otherwise the same contract as `sample`. `tex2Dlod` in a program; HLSL's
|
|
69
73
|
* `tex2Dlod(s, float4(uv, 0, lod))` and Metal's `sample(s, uv, level(lod))`.
|
|
70
74
|
*/
|
|
71
75
|
sampleLevel: (slot: number, uv: string, lod: string) => string
|
|
76
|
+
/**
|
|
77
|
+
* A float4 sample of a texture whose program sets its sampler
|
|
78
|
+
* (`[Wrap(Repeat)]`, `[Filter(Point)]`, IR 6), sampled that way whatever
|
|
79
|
+
* the bound texture's own settings: `sample` when `lod` is null, else
|
|
80
|
+
* `sampleLevel` at `lod`, with the same contract as those two in every
|
|
81
|
+
* other respect. `sampling` always has both halves (`samplingOf`):
|
|
82
|
+
*
|
|
83
|
+
* - `wrap`: `repeat` tiles, `mirror` tiles mirrored every other tile,
|
|
84
|
+
* `clamp` holds the edge texel. The filter's footprint wraps the same
|
|
85
|
+
* way, so a tiled texture has no seam.
|
|
86
|
+
* - `filter`: `point` reads the nearest texel and the nearest level,
|
|
87
|
+
* `linear` blends the four nearest texels within the nearest level, as
|
|
88
|
+
* Unity's Bilinear does.
|
|
89
|
+
*
|
|
90
|
+
* Optional, and `emitBody` refuses a program that samples such a texture
|
|
91
|
+
* when a target has none, rather than print one that samples as the
|
|
92
|
+
* bound texture is set. Unity's inline sampler states, a WebGPU sampler
|
|
93
|
+
* and a WebGL2 sampler object each say both halves.
|
|
94
|
+
*/
|
|
95
|
+
sampleWith?: (slot: number, uv: string, lod: string | null, sampling: TextureSampling) => string
|
|
96
|
+
/**
|
|
97
|
+
* A float2 of the texture in `slot`'s width and height, in pixels, at full
|
|
98
|
+
* size (`textureSize(t)`, IR 6). Optional, as `previous` is.
|
|
99
|
+
*/
|
|
100
|
+
size?: (slot: number) => string
|
|
101
|
+
/**
|
|
102
|
+
* A float4 of one texel of the texture in `slot`, unfiltered, at full size
|
|
103
|
+
* (`tex2Dfetch(t, pixel)`, IR 6). `pixel` is a float2 expression the body
|
|
104
|
+
* has already floored and held inside the texture with `size`, so a host
|
|
105
|
+
* converts it to an int2 and loads: HLSL's `t.Load(int3(pixel, 0))`,
|
|
106
|
+
* Metal's `t.read(uint2(pixel))`. Texel (0, 0) is the one `uv` (0, 0)
|
|
107
|
+
* samples. In the space `colour` names, as `sample` is, so an sRGB texture
|
|
108
|
+
* is decoded. Optional; needs `size` too.
|
|
109
|
+
*/
|
|
110
|
+
fetch?: (slot: number, pixel: string) => string
|
|
72
111
|
/**
|
|
73
112
|
* A float4 of what this program drew the frame before, at `uv`, for a
|
|
74
113
|
* host that keeps it (`tex2D(previous, uv)`, `Specs/SL_NEXT.md` 4).
|
|
@@ -110,7 +149,7 @@ export interface Body {
|
|
|
110
149
|
uses: {
|
|
111
150
|
/** Uniform slots the body reads, ascending. */
|
|
112
151
|
uniforms: number[]
|
|
113
|
-
/** Texture slots the body samples, ascending. */
|
|
152
|
+
/** Texture slots the body samples, measures or fetches from, ascending. */
|
|
114
153
|
textures: number[]
|
|
115
154
|
/** Library functions the body calls, sorted: `sl_fbm`, `sl_sdfDistance`, ... `emitLibrary` prints them. */
|
|
116
155
|
helpers: string[]
|
|
@@ -165,6 +204,26 @@ export function emitBody(p: Program, target: BodyTarget): Body {
|
|
|
165
204
|
const c = p.nodes[ref]!
|
|
166
205
|
return c.k === "const" ? c.v : null
|
|
167
206
|
}
|
|
207
|
+
/** A texture's sampler, when its program sets one, and the target that has to honour it. */
|
|
208
|
+
const sampling = (slot: number): TextureSampling | null => {
|
|
209
|
+
const s = samplingOf(p.textures[slot]!)
|
|
210
|
+
if (s !== null && target.sampleWith === undefined) {
|
|
211
|
+
throw new SLError(`this target cannot set a texture's sampler, and the program samples ${p.textures[slot]!.name} with ${s.wrap} and ${s.filter}`)
|
|
212
|
+
}
|
|
213
|
+
return s
|
|
214
|
+
}
|
|
215
|
+
const plain = (slot: number, uv: string) => {
|
|
216
|
+
const s = sampling(slot)
|
|
217
|
+
return s === null ? target.sample(slot, uv) : target.sampleWith!(slot, uv, null, s)
|
|
218
|
+
}
|
|
219
|
+
const level = (slot: number, uv: string, lod: string) => {
|
|
220
|
+
const s = sampling(slot)
|
|
221
|
+
return s === null ? target.sampleLevel(slot, uv, lod) : target.sampleWith!(slot, uv, lod, s)
|
|
222
|
+
}
|
|
223
|
+
const size = (slot: number) => {
|
|
224
|
+
if (target.size === undefined) throw new SLError(`this target cannot measure a texture, and the program measures ${p.textures[slot]!.name}`)
|
|
225
|
+
return target.size(slot)
|
|
226
|
+
}
|
|
168
227
|
const splat = (ref: number, w: SLType) => (valueAt(p.nodes, ref).type === TYPE.FLOAT && w > 1 ? ctor(w, Array(w).fill(name(ref))) : name(ref))
|
|
169
228
|
|
|
170
229
|
const expr = (ref: NodeRef): string => {
|
|
@@ -331,12 +390,32 @@ export function emitBody(p: Program, target: BodyTarget): Body {
|
|
|
331
390
|
textures.add(slot)
|
|
332
391
|
// Where pixels part ways there are no neighbours to take a mip
|
|
333
392
|
// level from, so every backend samples level 0 there (`structure.ts`).
|
|
334
|
-
return inVaryingFlow(shape, ref) ?
|
|
393
|
+
return inVaryingFlow(shape, ref) ? level(slot, a[0]!, "0.0") : plain(slot, a[0]!)
|
|
335
394
|
}
|
|
336
395
|
case SLOP.SAMPLE_LOD: {
|
|
337
396
|
const slot = Math.round(imm[0] ?? 0)
|
|
338
397
|
textures.add(slot)
|
|
339
|
-
|
|
398
|
+
// A fraction blends the level below and the level above,
|
|
399
|
+
// whatever the texture's filter between levels, as the web
|
|
400
|
+
// emitters do (`isWhole`). Past the smallest level both read the
|
|
401
|
+
// smallest, as a whole level past it does.
|
|
402
|
+
if (isWhole(p.nodes, n.args[1]!)) return level(slot, a[0]!, a[1]!)
|
|
403
|
+
const below = `floor(${a[1]})`
|
|
404
|
+
return `lerp(${level(slot, a[0]!, below)}, ${level(slot, a[0]!, `${below} + 1.0`)}, frac(${a[1]}))`
|
|
405
|
+
}
|
|
406
|
+
case SLOP.TEXTURE_SIZE: {
|
|
407
|
+
const slot = Math.round(imm[0] ?? 0)
|
|
408
|
+
textures.add(slot)
|
|
409
|
+
return size(slot)
|
|
410
|
+
}
|
|
411
|
+
case SLOP.SAMPLE_FETCH: {
|
|
412
|
+
const slot = Math.round(imm[0] ?? 0)
|
|
413
|
+
textures.add(slot)
|
|
414
|
+
if (target.fetch === undefined) throw new SLError(`this target cannot fetch a texel, and the program reads ${p.textures[slot]!.name} with tex2Dfetch`)
|
|
415
|
+
// Floored and held inside, so a pixel past an edge reads the
|
|
416
|
+
// edge on every host rather than whatever a host's load returns
|
|
417
|
+
// out of range.
|
|
418
|
+
return target.fetch(slot, `clamp(floor(${a[0]}), float2(0.0, 0.0), ${size(slot)} - 1.0)`)
|
|
340
419
|
}
|
|
341
420
|
case SLOP.SAMPLE_PREVIOUS:
|
|
342
421
|
if (target.previous === undefined) throw new SLError("this target keeps no previous frame, and the program samples it")
|
package/src/core.ts
CHANGED
|
@@ -8,13 +8,14 @@
|
|
|
8
8
|
* game never executes.
|
|
9
9
|
*/
|
|
10
10
|
export * as sl from "./sl"
|
|
11
|
-
export type { Float, Vec2, Vec3, Vec4, Num, ProgramInputs, PreviousFrame, Texture } from "./sl"
|
|
11
|
+
export type { Float, Vec2, Vec3, Vec4, Num, ProgramInputs, PreviousFrame, Texture, TextureSamplingOptions } from "./sl"
|
|
12
12
|
export {
|
|
13
13
|
TYPE, INPUTS, STEP_INPUTS, PREVIOUS, MAX_NODES, SLError, hashProgram, widthName, SL_IR_VERSION, SL_HASH_VERSION,
|
|
14
|
-
inputsUsed, readsOf, CONTROL_FIELDS, controlProblem,
|
|
14
|
+
inputsUsed, readsOf, CONTROL_FIELDS, controlProblem, samplingOf, TEXTURE_WRAPS, TEXTURE_FILTERS,
|
|
15
15
|
} from "./ir"
|
|
16
16
|
export type {
|
|
17
17
|
SLType, InputName, StepInputName, HostInputName, SLReads, NodeRef, SLNode, Program, UniformDecl, UniformControl, TextureDecl,
|
|
18
|
+
TextureSampling, TextureWrap, TextureFilter,
|
|
18
19
|
} from "./ir"
|
|
19
20
|
export { SLOP, SL_ARITY, SL_NAME, INPUT_ID, isSampling, UNIFORM_SLOTS, TEXTURE_SLOTS } from "./ops"
|
|
20
21
|
export type { SLOpCode } from "./ops"
|
package/src/hlsl.ts
CHANGED
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
* next time it is generated.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { SLError, SL_HASH_VERSION, readsOf, type Program } from "./ir"
|
|
18
|
+
import { SLError, SL_HASH_VERSION, readsOf, reachable, samplingOf, type Program, type TextureSampling } from "./ir"
|
|
19
19
|
import { emitBody, lit, type BodyTarget } from "./body"
|
|
20
|
+
import { SLOP } from "./ops"
|
|
20
21
|
|
|
21
22
|
/** The property name a uniform gets. Prefixed so it cannot collide with ours. */
|
|
22
23
|
export function uniformProperty(name: string): string {
|
|
@@ -49,6 +50,7 @@ export function emitShader(p: Program, options: EmitOptions = {}): string {
|
|
|
49
50
|
const include = options.include ?? "SLCommon.cginc"
|
|
50
51
|
const body = emitFragmentBody(p)
|
|
51
52
|
const reads = readsOf(p)
|
|
53
|
+
const use = textureUse(p)
|
|
52
54
|
|
|
53
55
|
const props: string[] = [
|
|
54
56
|
` _Secs ("Seconds", Float) = 0`,
|
|
@@ -68,7 +70,22 @@ export function emitShader(p: Program, options: EmitOptions = {}): string {
|
|
|
68
70
|
}
|
|
69
71
|
for (const t of p.textures) {
|
|
70
72
|
props.push(` _Tex${t.slot} ("${t.name}", 2D) = "white" {}`)
|
|
71
|
-
|
|
73
|
+
// A texture sampled as it is set is the sampler2D it always was, so
|
|
74
|
+
// every program before IR 6 prints the shader it always did. One the
|
|
75
|
+
// program sets a sampler for, or fetches from, is a Texture2D: the
|
|
76
|
+
// inline sampler state or the Load needs the texture on its own.
|
|
77
|
+
if (!use.object.has(t.slot)) decls.push(` sampler2D _Tex${t.slot};`)
|
|
78
|
+
else {
|
|
79
|
+
decls.push(` Texture2D _Tex${t.slot};`)
|
|
80
|
+
// Its own settings, by Unity's naming rule, for a plain sample.
|
|
81
|
+
if (use.plain.has(t.slot)) decls.push(` SamplerState sampler_Tex${t.slot};`)
|
|
82
|
+
}
|
|
83
|
+
// Unity fills it from the bound texture: (1 / width, 1 / height, width, height).
|
|
84
|
+
if (use.sized.has(t.slot)) decls.push(` float4 _Tex${t.slot}_TexelSize;`)
|
|
85
|
+
}
|
|
86
|
+
// Unity's inline sampler states: the name says the filter and the wrap.
|
|
87
|
+
for (const name of [...new Set(p.textures.map(samplingOf).filter((s) => s !== null).map(inlineSampler))].sort()) {
|
|
88
|
+
decls.push(` SamplerState ${name};`)
|
|
72
89
|
}
|
|
73
90
|
// Only for a program that samples it, so every other program's shader is
|
|
74
91
|
// the text it always was. Transparent black is the previous frame's clear
|
|
@@ -143,6 +160,7 @@ ${body}
|
|
|
143
160
|
* shader compile, so this target always emits it.
|
|
144
161
|
*/
|
|
145
162
|
function unityTarget(p: Program): BodyTarget {
|
|
163
|
+
const use = textureUse(p)
|
|
146
164
|
return {
|
|
147
165
|
// `_Res` is the TARGET's size, set by the host, not `_ScreenParams`.
|
|
148
166
|
//
|
|
@@ -164,8 +182,15 @@ function unityTarget(p: Program): BodyTarget {
|
|
|
164
182
|
deltaTime: "_Res.w",
|
|
165
183
|
},
|
|
166
184
|
uniform: (slot) => uniformProperty(p.uniforms[slot]!.name),
|
|
167
|
-
sample: (slot, uv) => `tex2D(_Tex${slot}, ${uv})`,
|
|
168
|
-
sampleLevel: (slot, uv, lod) =>
|
|
185
|
+
sample: (slot, uv) => use.object.has(slot) ? `_Tex${slot}.Sample(sampler_Tex${slot}, ${uv})` : `tex2D(_Tex${slot}, ${uv})`,
|
|
186
|
+
sampleLevel: (slot, uv, lod) => use.object.has(slot)
|
|
187
|
+
? `_Tex${slot}.SampleLevel(sampler_Tex${slot}, ${uv}, ${lod})`
|
|
188
|
+
: `tex2Dlod(_Tex${slot}, float4(${uv}, 0.0, ${lod}))`,
|
|
189
|
+
sampleWith: (slot, uv, lod, s) => lod === null
|
|
190
|
+
? `_Tex${slot}.Sample(${inlineSampler(s)}, ${uv})`
|
|
191
|
+
: `_Tex${slot}.SampleLevel(${inlineSampler(s)}, ${uv}, ${lod})`,
|
|
192
|
+
size: (slot) => `_Tex${slot}_TexelSize.zw`,
|
|
193
|
+
fetch: (slot, pixel) => `_Tex${slot}.Load(int3(${pixel}, 0))`,
|
|
169
194
|
// Flipped as this frame flipped the uv it wrote at, so a read at a
|
|
170
195
|
// pixel's own uv is that pixel whichever way the host's target runs.
|
|
171
196
|
previous: (uv) => `tex2Dlod(_Prev, float4(${uv} * float2(1.0, 1.0 - 2.0 * _FlipY) + float2(0.0, _FlipY), 0.0, 0.0))`,
|
|
@@ -175,6 +200,36 @@ function unityTarget(p: Program): BodyTarget {
|
|
|
175
200
|
}
|
|
176
201
|
}
|
|
177
202
|
|
|
203
|
+
/**
|
|
204
|
+
* An inline sampler state's name. Unity reads the filter and the wrap from the
|
|
205
|
+
* name itself ("point" or "linear", "clamp", "repeat" or "mirror").
|
|
206
|
+
*/
|
|
207
|
+
function inlineSampler(s: TextureSampling): string {
|
|
208
|
+
return `sl_sampler_${s.filter}_${s.wrap}`
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* How the program reaches each texture slot, which decides how the shader
|
|
213
|
+
* declares it. `object`: declared a Texture2D, because the program sets its
|
|
214
|
+
* sampler or fetches from it. `plain`: an object sampled with its own
|
|
215
|
+
* settings, which needs Unity's `sampler_` state. `sized`: reads `_TexelSize`,
|
|
216
|
+
* as `textureSize` and a fetch's bounds do.
|
|
217
|
+
*/
|
|
218
|
+
function textureUse(p: Program): { object: Set<number>; plain: Set<number>; sized: Set<number> } {
|
|
219
|
+
const fetched = new Set<number>(), sampled = new Set<number>(), sized = new Set<number>()
|
|
220
|
+
for (const ref of reachable(p.nodes, p.result)) {
|
|
221
|
+
const n = p.nodes[ref]!
|
|
222
|
+
if (n.k !== "call") continue
|
|
223
|
+
const slot = Math.round(n.imm?.[0] ?? 0)
|
|
224
|
+
if (n.op === SLOP.SAMPLE || n.op === SLOP.SAMPLE_LOD) sampled.add(slot)
|
|
225
|
+
else if (n.op === SLOP.TEXTURE_SIZE) sized.add(slot)
|
|
226
|
+
else if (n.op === SLOP.SAMPLE_FETCH) { fetched.add(slot); sized.add(slot) }
|
|
227
|
+
}
|
|
228
|
+
const object = new Set(p.textures.filter((t) => samplingOf(t) !== null || fetched.has(t.slot)).map((t) => t.slot))
|
|
229
|
+
const plain = new Set([...sampled].filter((slot) => object.has(slot) && samplingOf(p.textures[slot]!) === null))
|
|
230
|
+
return { object, plain, sized }
|
|
231
|
+
}
|
|
232
|
+
|
|
178
233
|
/** The straight line body: one local per reachable node, in order. */
|
|
179
234
|
export function emitFragmentBody(p: Program): string {
|
|
180
235
|
return emitBody(p, unityTarget(p)).body
|
package/src/index.ts
CHANGED
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
*/
|
|
19
19
|
export * from "./core"
|
|
20
20
|
export {
|
|
21
|
-
analyze, classify, diagnose, parse, parseUnit, preludeFunctions, tokenize, PRELUDE_SOURCE, SLParseError,
|
|
21
|
+
analyze, classify, classifyLine, diagnose, parse, parseUnit, preludeFunctions, tokenize, PRELUDE_SOURCE, SLParseError,
|
|
22
22
|
} from "./lang"
|
|
23
23
|
export type {
|
|
24
|
-
Checked, Expr, FuncDecl, ParseOptions, Pos, SLClassifiedToken, SLFix, SLTokenClass, Stmt, Token, Unit,
|
|
24
|
+
Checked, Expr, FuncDecl, ParseOptions, Pos, SLClassifiedToken, SLFix, SLLineState, SLTokenClass, Stmt, Token, Unit,
|
|
25
25
|
} from "./lang"
|
|
26
26
|
export { fromGLSL } from "./lang/glsl"
|
|
27
27
|
export type { FromGLSL } from "./lang/glsl"
|
package/src/ir.ts
CHANGED
|
@@ -274,10 +274,45 @@ export function intUniformProblem(type: SLType, value: readonly number[]): strin
|
|
|
274
274
|
return null
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
/** How a texture's coordinates outside 0 to 1 read: tiled, held at the edge, or tiled mirrored. */
|
|
278
|
+
export type TextureWrap = "repeat" | "clamp" | "mirror"
|
|
279
|
+
/** How a texture is read between texels: the nearest one, or a blend of the four around. */
|
|
280
|
+
export type TextureFilter = "point" | "linear"
|
|
281
|
+
|
|
282
|
+
/** The sampler a program sets for a texture, both halves filled in (`samplingOf`). */
|
|
283
|
+
export interface TextureSampling {
|
|
284
|
+
wrap: TextureWrap
|
|
285
|
+
filter: TextureFilter
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export const TEXTURE_WRAPS: readonly TextureWrap[] = ["repeat", "clamp", "mirror"]
|
|
289
|
+
export const TEXTURE_FILTERS: readonly TextureFilter[] = ["point", "linear"]
|
|
290
|
+
|
|
277
291
|
export interface TextureDecl {
|
|
278
292
|
name: string
|
|
279
293
|
/** Sampler slot, assigned in declaration order. */
|
|
280
294
|
slot: number
|
|
295
|
+
/**
|
|
296
|
+
* How the program samples it, as written: `[Wrap(Repeat)]` and
|
|
297
|
+
* `[Filter(Point)]` in a `.sl` file, `sl.texture(name, { wrap, filter })`.
|
|
298
|
+
* Neither set, the texture samples as the texture bound to it is set,
|
|
299
|
+
* which is what every program did before IR 6. Either set, the program
|
|
300
|
+
* owns the sampler, and the half it leaves out is clamp or linear
|
|
301
|
+
* (`samplingOf`). Part of the hash, since the shader differs.
|
|
302
|
+
*/
|
|
303
|
+
wrap?: TextureWrap
|
|
304
|
+
filter?: TextureFilter
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* The sampler a texture's program sets, or null when it samples as the bound
|
|
309
|
+
* texture is set. A program that names one half gets clamp or linear for the
|
|
310
|
+
* other, on every host: Unity's inline sampler states and WebGPU's samplers
|
|
311
|
+
* have no way to take one half from the texture and the other from the shader.
|
|
312
|
+
*/
|
|
313
|
+
export function samplingOf(t: Pick<TextureDecl, "wrap" | "filter">): TextureSampling | null {
|
|
314
|
+
if (t.wrap === undefined && t.filter === undefined) return null
|
|
315
|
+
return { wrap: t.wrap ?? "clamp", filter: t.filter ?? "linear" }
|
|
281
316
|
}
|
|
282
317
|
|
|
283
318
|
export interface Program {
|
|
@@ -308,8 +343,9 @@ export function tooManyUniforms(declared: number, source: "file" | "program"): s
|
|
|
308
343
|
|
|
309
344
|
export function tooManyTextures(declared: number, source: "file" | "program"): string {
|
|
310
345
|
return `this ${source} declares ${declared} textures and a program may sample ${TEXTURE_SLOTS}. ` +
|
|
311
|
-
`OneJS binds ${TEXTURE_SLOTS} textures to a program
|
|
312
|
-
`
|
|
346
|
+
`OneJS binds ${TEXTURE_SLOTS} textures to a program, so this one would have no slot to be ` +
|
|
347
|
+
`bound in; it is refused here rather than drawn blank. Pack two images into one texture's ` +
|
|
348
|
+
`channels, or draw in two passes.`
|
|
313
349
|
}
|
|
314
350
|
|
|
315
351
|
/**
|
|
@@ -323,6 +359,17 @@ export function checkCaps(p: Pick<Program, "uniforms" | "textures">): void {
|
|
|
323
359
|
if (p.textures.length > TEXTURE_SLOTS) throw new SLError(tooManyTextures(p.textures.length, "program"))
|
|
324
360
|
}
|
|
325
361
|
|
|
362
|
+
/** What is wrong with a texture's sampler, or null. The one check, for the builder and `fromJSON`. */
|
|
363
|
+
export function samplingProblem(s: { wrap?: unknown; filter?: unknown }): string | null {
|
|
364
|
+
if (s.wrap !== undefined && !TEXTURE_WRAPS.includes(s.wrap as TextureWrap)) {
|
|
365
|
+
return `a wrap is ${TEXTURE_WRAPS.join(", ")}, and this is ${String(s.wrap)}`
|
|
366
|
+
}
|
|
367
|
+
if (s.filter !== undefined && !TEXTURE_FILTERS.includes(s.filter as TextureFilter)) {
|
|
368
|
+
return `a filter is ${TEXTURE_FILTERS.join(" or ")}, and this is ${String(s.filter)}`
|
|
369
|
+
}
|
|
370
|
+
return null
|
|
371
|
+
}
|
|
372
|
+
|
|
326
373
|
/**
|
|
327
374
|
* The colour as written behind a colour value: the operand of the `TO_LINEAR`
|
|
328
375
|
* that reading a hex or a colour uniform applied, or null for a value computed
|
|
@@ -446,14 +493,30 @@ export class Builder {
|
|
|
446
493
|
return this.add({ k: "uniform", type, slot })
|
|
447
494
|
}
|
|
448
495
|
|
|
449
|
-
texture(name: string): number {
|
|
496
|
+
texture(name: string, sampling: Pick<TextureDecl, "wrap" | "filter"> = {}): number {
|
|
497
|
+
const problem = samplingProblem(sampling)
|
|
498
|
+
if (problem !== null) throw new SLError(`texture "${name}": ${problem}`)
|
|
450
499
|
const existing = this.textures.findIndex((t) => t.name === name)
|
|
451
|
-
if (existing >= 0)
|
|
500
|
+
if (existing >= 0) {
|
|
501
|
+
// A second use may say nothing about the sampler, or the same thing again.
|
|
502
|
+
const t = this.textures[existing]!
|
|
503
|
+
for (const f of ["wrap", "filter"] as const) {
|
|
504
|
+
if (sampling[f] === undefined) continue
|
|
505
|
+
if (t[f] !== undefined && t[f] !== sampling[f]) {
|
|
506
|
+
throw new SLError(`texture "${name}" is given two different ${f}s, ${t[f]} and ${sampling[f]}`)
|
|
507
|
+
}
|
|
508
|
+
t[f] = sampling[f] as never
|
|
509
|
+
}
|
|
510
|
+
return t.slot
|
|
511
|
+
}
|
|
452
512
|
if (this.textures.length >= TEXTURE_SLOTS) {
|
|
453
513
|
throw new SLError(tooManyTextures(this.textures.length + 1, "program"))
|
|
454
514
|
}
|
|
455
515
|
const slot = this.textures.length
|
|
456
|
-
|
|
516
|
+
const decl: TextureDecl = { name, slot }
|
|
517
|
+
if (sampling.wrap !== undefined) decl.wrap = sampling.wrap
|
|
518
|
+
if (sampling.filter !== undefined) decl.filter = sampling.filter
|
|
519
|
+
this.textures.push(decl)
|
|
457
520
|
return slot
|
|
458
521
|
}
|
|
459
522
|
}
|
|
@@ -461,9 +524,9 @@ export class Builder {
|
|
|
461
524
|
/**
|
|
462
525
|
* Nodes the result actually depends on, in order.
|
|
463
526
|
*
|
|
464
|
-
* Dead nodes are dropped rather than
|
|
527
|
+
* Dead nodes are dropped rather than emitted. An author can produce them easily
|
|
465
528
|
* by computing something and not using it, and the hash already ignores them, so
|
|
466
|
-
*
|
|
529
|
+
* emitting them would make a shader disagree with its own hash about what the
|
|
467
530
|
* program is.
|
|
468
531
|
*/
|
|
469
532
|
export function reachable(nodes: SLNode[], result: NodeRef): NodeRef[] {
|
|
@@ -519,6 +582,42 @@ export function readsOf(p: Program): SLReads {
|
|
|
519
582
|
return out
|
|
520
583
|
}
|
|
521
584
|
|
|
585
|
+
/** Ops whose result is a whole number whatever they are given. */
|
|
586
|
+
const ALWAYS_WHOLE = new Set<number>([SLOP.FLOOR, SLOP.CEIL, SLOP.ROUND, SLOP.SIGN])
|
|
587
|
+
/** Ops whose result is a whole number when every operand they read is one. */
|
|
588
|
+
const KEEPS_WHOLE = new Set<number>([
|
|
589
|
+
SLOP.CAST, SLOP.ADD, SLOP.SUB, SLOP.MUL, SLOP.NEG, SLOP.ABS, SLOP.MIN, SLOP.MAX, SLOP.CLAMP, SLOP.SATURATE,
|
|
590
|
+
])
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Whether a value is a whole number in every component on every pixel, as far
|
|
594
|
+
* as its node says. An int, uint or bool is; so are a constant written whole,
|
|
595
|
+
* `floor`, `ceil`, `round` and `sign`, and sums, products, minimums and the
|
|
596
|
+
* like of whole numbers (a float32 sum or product of whole numbers is whole).
|
|
597
|
+
* Anything else is not known to be, a uniform float included.
|
|
598
|
+
*
|
|
599
|
+
* The emitters read it for `tex2Dlod`: a level known whole reads that level
|
|
600
|
+
* with one sample, and any other blends the level below and the level above by
|
|
601
|
+
* the fraction, so every backend draws a fraction the same way whatever the
|
|
602
|
+
* texture's filter between levels.
|
|
603
|
+
*/
|
|
604
|
+
export function isWhole(nodes: readonly SLNode[], ref: NodeRef, seen = new Map<NodeRef, boolean>()): boolean {
|
|
605
|
+
const known = seen.get(ref)
|
|
606
|
+
if (known !== undefined) return known
|
|
607
|
+
const n = valueAt(nodes, ref)
|
|
608
|
+
let whole = false
|
|
609
|
+
if (n.kind !== undefined) whole = true
|
|
610
|
+
else if (n.k === "const") whole = n.v.every(Number.isInteger)
|
|
611
|
+
else if (n.k === "swizzle") whole = isWhole(nodes, n.src, seen)
|
|
612
|
+
else if (n.k === "call") {
|
|
613
|
+
if (ALWAYS_WHOLE.has(n.op)) whole = true
|
|
614
|
+
else if (KEEPS_WHOLE.has(n.op)) whole = n.args.every((a) => isWhole(nodes, a, seen))
|
|
615
|
+
else if (n.op === SLOP.CHOOSE) whole = isWhole(nodes, n.args[1]!, seen) && isWhole(nodes, n.args[2]!, seen)
|
|
616
|
+
}
|
|
617
|
+
seen.set(ref, whole)
|
|
618
|
+
return whole
|
|
619
|
+
}
|
|
620
|
+
|
|
522
621
|
/** Structural key for hash consing. Order matters and is fixed by the node shape. */
|
|
523
622
|
function keyOf(n: SLNode): string {
|
|
524
623
|
const kind = "kind" in n && n.kind !== undefined ? `@${n.kind}` : ""
|
|
@@ -651,9 +750,15 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
|
|
|
651
750
|
// includes the library's text when it compiles, and none caches a compiled
|
|
652
751
|
// program by hash across a library update, so the new text reaches every
|
|
653
752
|
// shader without a bump.
|
|
654
|
-
const parts: string[] = [`v${SL_HASH_VERSION}:${programVersion(nodes, result)}`, of(result)]
|
|
753
|
+
const parts: string[] = [`v${SL_HASH_VERSION}:${programVersion(nodes, result, textures)}`, of(result)]
|
|
655
754
|
for (const u of uniforms) parts.push(`U:${u.name}:${u.type}:${u.value.map(fixed).join(",")}`)
|
|
656
|
-
|
|
755
|
+
// A texture's sampler as the shader has it, so `[Wrap(Clamp)]` and
|
|
756
|
+
// `[Filter(Linear)]` share a hash, and a texture with neither has the
|
|
757
|
+
// part it had before IR 6.
|
|
758
|
+
for (const t of textures) {
|
|
759
|
+
const s = samplingOf(t)
|
|
760
|
+
parts.push(`T:${t.name}:${t.slot}${s === null ? "" : `:${s.wrap}:${s.filter}`}`)
|
|
761
|
+
}
|
|
657
762
|
return fnv1a(parts.join("|"))
|
|
658
763
|
}
|
|
659
764
|
|
|
@@ -715,8 +820,8 @@ export function controlOf(c: UniformControl): UniformControl {
|
|
|
715
820
|
* int constant a fold used up, say), and they draw nothing. `toJSON` writes
|
|
716
821
|
* only the live ones, so an older reader never meets a dead newer node.
|
|
717
822
|
*/
|
|
718
|
-
export function programVersion(nodes: readonly SLNode[], result: NodeRef): number {
|
|
719
|
-
let v = 2
|
|
823
|
+
export function programVersion(nodes: readonly SLNode[], result: NodeRef, textures: readonly TextureDecl[] = []): number {
|
|
824
|
+
let v = textures.some((t) => samplingOf(t) !== null) ? 6 : 2
|
|
720
825
|
for (const ref of reachable(nodes as SLNode[], result)) v = Math.max(v, nodeVersion(nodes[ref]!))
|
|
721
826
|
return v
|
|
722
827
|
}
|
|
@@ -738,6 +843,9 @@ function nodeVersion(n: SLNode): number {
|
|
|
738
843
|
if (n.k !== "call") return 2
|
|
739
844
|
if (IR4_OPS.has(n.op)) return 4
|
|
740
845
|
switch (n.op) {
|
|
846
|
+
case SLOP.TEXTURE_SIZE:
|
|
847
|
+
case SLOP.SAMPLE_FETCH:
|
|
848
|
+
return 6
|
|
741
849
|
case SLOP.SAMPLE_PREVIOUS: return 5
|
|
742
850
|
case SLOP.SAMPLE_LOD: return 3
|
|
743
851
|
case SLOP.SDF:
|
|
@@ -761,6 +869,8 @@ function nodeVersion(n: SLNode): number {
|
|
|
761
869
|
* one immediate
|
|
762
870
|
* SAMPLE_PREVIOUS a float4 at a float2, and no immediates: there is one
|
|
763
871
|
* previous frame, so there is no slot to name
|
|
872
|
+
* TEXTURE_SIZE a float2 of nothing, the slot the one immediate
|
|
873
|
+
* SAMPLE_FETCH a float4 at a float2 pixel, the slot the one immediate
|
|
764
874
|
*/
|
|
765
875
|
export function formProblem(n: Extract<SLNode, { k: "call" }>, nodes: readonly SLNode[]): string | null {
|
|
766
876
|
const widths = n.args.map((a) => { const v = nodes[a]!; return v.k === "if" || v.k === "loop" ? "control" : v.type }).join(",")
|
|
@@ -778,6 +888,12 @@ export function formProblem(n: Extract<SLNode, { k: "call" }>, nodes: readonly S
|
|
|
778
888
|
case SLOP.SAMPLE_PREVIOUS:
|
|
779
889
|
if (widths === "2" && n.type === 4 && n.kind === undefined && (n.imm?.length ?? 0) === 0) return null
|
|
780
890
|
return "the previous frame is a float4 read at a float2"
|
|
891
|
+
case SLOP.TEXTURE_SIZE:
|
|
892
|
+
if (widths === "" && n.type === 2 && n.kind === undefined && n.imm?.length === 1) return null
|
|
893
|
+
return "a texture's size is a float2, its slot the one immediate"
|
|
894
|
+
case SLOP.SAMPLE_FETCH:
|
|
895
|
+
if (widths === "2" && n.type === 4 && n.kind === undefined && n.imm?.length === 1) return null
|
|
896
|
+
return "a fetch is a float4 read at a float2 pixel, its slot the one immediate"
|
|
781
897
|
default:
|
|
782
898
|
return null
|
|
783
899
|
}
|
|
@@ -819,8 +935,9 @@ export const SL_HASH_VERSION = 2
|
|
|
819
935
|
* operands, where they were immediates
|
|
820
936
|
* 4 control flow (if, loop, their params and projs), ints, uints and bools
|
|
821
937
|
* 5 the previous frame (SAMPLE_PREVIOUS), and the frame and deltaTime inputs
|
|
938
|
+
* 6 TEXTURE_SIZE and SAMPLE_FETCH, and a texture's sampler (`wrap`, `filter`)
|
|
822
939
|
*/
|
|
823
|
-
export const SL_IR_VERSION =
|
|
940
|
+
export const SL_IR_VERSION = 6
|
|
824
941
|
|
|
825
942
|
function fnv1a(s: string): string {
|
|
826
943
|
let h = 0x811c9dc5
|
package/src/lang/README.md
CHANGED
|
@@ -41,9 +41,14 @@ player. So parity is the test to keep green, not the parser's unit tests.
|
|
|
41
41
|
| `ast.ts` | The node shapes, all of them, which is not many |
|
|
42
42
|
| `parser.ts` | Pratt parser: shape only, HLSL precedence |
|
|
43
43
|
| `check.ts` | Declarations, names, statement shape, budgets, recursion |
|
|
44
|
-
| `lower.ts` | AST to IR through the EDSL: SSA, unrolling,
|
|
44
|
+
| `lower.ts` | AST to IR through the EDSL: SSA, unrolling, branches and loops, inlining |
|
|
45
45
|
| `builtins.ts` | What each name does, keyed by the spelling `ops.ts` gives it |
|
|
46
|
-
| `prelude.ts` | The standard library, written in the language |
|
|
46
|
+
| `prelude.ts`, `prelude-source.ts` | The standard library, written in the language |
|
|
47
|
+
| `attributes.ts` | A uniform's `[Range]`, `[Toggle]` and the rest, read into its control |
|
|
48
|
+
| `words.ts` | The keywords and type names, shared by the parser and `classify` |
|
|
49
|
+
| `classify.ts` | Every token with its class, for an editor's highlighting |
|
|
50
|
+
| `glsl.ts` | `fromGLSL`: a pasted GLSL shader as a `.sl` file |
|
|
51
|
+
| `index.ts` | This folder's exports |
|
|
47
52
|
| `onejs-unity`'s `src/esbuild/sl.mjs` | The loader: `import plasma from "./plasma.sl"` |
|
|
48
53
|
| `../index.ts` and `../core.ts` | The package with the parser, and without it. See below |
|
|
49
54
|
|
|
@@ -68,9 +73,11 @@ It writes two things beside the code:
|
|
|
68
73
|
recording stays for EDSL programs and for a build older than the loader; the
|
|
69
74
|
editor reads every `*.sl.json` it can find.
|
|
70
75
|
|
|
71
|
-
|
|
72
|
-
last `.sl` file has to stop its shaders being
|
|
73
|
-
|
|
76
|
+
With no `.sl` files, an empty manifest is written over one that is already
|
|
77
|
+
there, since deleting the last `.sl` file has to stop its shaders being
|
|
78
|
+
generated, and beside a bundle carrying `onejs-sl`, which has to name its hash
|
|
79
|
+
scheme. A project that uses no shader language finds no new file beside its
|
|
80
|
+
bundle.
|
|
74
81
|
|
|
75
82
|
### Why the parser is compiled rather than imported
|
|
76
83
|
|
|
@@ -110,7 +117,8 @@ here is the one nothing else uses.
|
|
|
110
117
|
**A scalar on the left of an operator broadcasts through a swizzle.** `uv * 8`
|
|
111
118
|
is `uv.mul(8)`, one constant node of two components. `8 * uv` is
|
|
112
119
|
`sl.float(8).mul(uv)`, a one component constant and a broadcast. Same picture,
|
|
113
|
-
different graph,
|
|
120
|
+
different graph, and since 0.4.0 (`SL_HASH_VERSION` 2) the same hash. The
|
|
121
|
+
vector on the left saves a node.
|
|
114
122
|
|
|
115
123
|
**Literal arithmetic folds; a builtin call never does.** `2 * 3 + 1` is the
|
|
116
124
|
constant 7 before the IR sees it. `sin(0.5)` is two instructions, because
|
|
@@ -161,8 +169,8 @@ recorded gap, not an oversight, and each has its own error message rather than
|
|
|
161
169
|
| `rgb2hsv` | The opcode is numbered and **no backend implements it**; every emitter throws on it. |
|
|
162
170
|
| `fbm`'s simplex base | `sl.fbm(p, octaves, "simplex")` picks the base with a string, and the language has no strings. `fbm(p, octaves)` is the value base, and `turbulence` and `ridged` are the simplex family. |
|
|
163
171
|
|
|
164
|
-
A `.sl` file and the EDSL are held to the same caps, `UNIFORM_SLOTS` (
|
|
165
|
-
`TEXTURE_SLOTS` (
|
|
172
|
+
A `.sl` file and the EDSL are held to the same caps, `UNIFORM_SLOTS` (64) and
|
|
173
|
+
`TEXTURE_SLOTS` (8), reported at the declaration in one wording (`ir.ts`'s
|
|
166
174
|
`tooManyUniforms` and `tooManyTextures`).
|
|
167
175
|
|
|
168
176
|
## Attributes
|
|
@@ -185,6 +193,16 @@ What they produce is metadata on the uniform (`range`, `toggle`, `options`,
|
|
|
185
193
|
`header`, `label`, `hide`), which the hash never reads. `[Color]` is the
|
|
186
194
|
exception, because it changes the reads: it is the same as a hex default.
|
|
187
195
|
|
|
196
|
+
A texture takes two of its own, which are not metadata: `[Wrap(Repeat)]`,
|
|
197
|
+
`[Wrap(Mirror)]` or `[Wrap(Clamp)]`, and `[Filter(Point)]` or `[Filter(Linear)]`,
|
|
198
|
+
read by `readTextureAttributes`. They change the shader (a sampler the program
|
|
199
|
+
owns rather than the bound texture's), so they are in the hash and make a
|
|
200
|
+
program IR 6; the half left out is clamp or linear (`samplingOf`). Unity's
|
|
201
|
+
`Bilinear` and `WrapMode` are offered as fixes, and `Trilinear` and
|
|
202
|
+
`MirrorOnce` are refused with why: no backend's per program sampler blends
|
|
203
|
+
levels, and WebGPU has no mirror once. A uniform's attribute on a texture, or
|
|
204
|
+
a texture's on a uniform, says which one it belongs on.
|
|
205
|
+
|
|
188
206
|
## Errors
|
|
189
207
|
|
|
190
208
|
Every one carries file, line, column, offset and length, and `text`, the
|
|
@@ -201,7 +219,7 @@ Where the fix is certain, the error carries it as `fix`: a title and the text
|
|
|
201
219
|
for the marked range, one click in an editor. A GLSL spelling that means the
|
|
202
220
|
same in HLSL (`mix`, `fract`, `vec3`, `gl_FragCoord`, `iTime`, two argument
|
|
203
221
|
`atan`) and a "did you mean" both have one. `mod` does not, since GLSL's floors
|
|
204
|
-
and `%` truncates, and neither does `
|
|
222
|
+
and `%` truncates, and neither does `ivec2`, since `float2` would change what
|
|
205
223
|
dividing it does. That is Decision 1 kept as `Specs/SL_NEXT.md` 5 A has it:
|
|
206
224
|
one spelling, and every hint a fix.
|
|
207
225
|
|
|
@@ -229,6 +247,13 @@ on the parser's own scanner and word lists, so the highlighting is the parser's.
|
|
|
229
247
|
It never throws, since it runs on every keystroke. It classifies by spelling,
|
|
230
248
|
not scope: a local named `circle` still reads as a builtin where it is used.
|
|
231
249
|
|
|
250
|
+
`classifyLine(line, state)` is the same for an editor that colours a line at a
|
|
251
|
+
time, as Monaco does: the line's tokens and the state the next line starts
|
|
252
|
+
from (inside a block comment or not, and the two tokens before it). Every line
|
|
253
|
+
of a file classified that way gives what `classify` gives the whole file, a
|
|
254
|
+
block comment over several lines coming back as one piece per line; the corpus
|
|
255
|
+
holds it to that.
|
|
256
|
+
|
|
232
257
|
`SL_KEYWORDS` and `SL_TYPES` (`words.ts`) are the lists the parser reads, and a
|
|
233
258
|
name may be neither. `onejs-sl/tables` carries them, and `ATTRIBUTE_NAMES`, with
|
|
234
259
|
a one line description of every builtin, input, attribute and prelude function; a prelude function's is the
|
|
@@ -249,7 +274,9 @@ where they were, so the result reads as the shader it came from. What it does:
|
|
|
249
274
|
the inputs. So do the calls that need an
|
|
250
275
|
expression: `mod` floors (`a - b * floor(a / b)`, never `%`, which
|
|
251
276
|
truncates), and `atan`, `inversesqrt`, `radians`, `degrees`, `exp2` and
|
|
252
|
-
`log2` become what they compute.
|
|
277
|
+
`log2` become what they compute. `texelFetch(t, p, 0)` is
|
|
278
|
+
`tex2Dfetch(t, p)` and `textureSize(t, 0)` is `textureSize(t)`; another
|
|
279
|
+
level gets a note, since both read the full size texture.
|
|
253
280
|
- `mainImage` becomes `float4 main()`, with its colour parameter as a local it
|
|
254
281
|
returns; `void main()` writing `gl_FragColor` or a WebGL2 `out vec4` does the
|
|
255
282
|
same. Shadertoy's `uv = fragCoord / iResolution.xy` line is dropped, since
|
|
@@ -268,8 +295,10 @@ where they were, so the result reads as the shader it came from. What it does:
|
|
|
268
295
|
the notes point at `rotate()`. A file with a `rotate` of its own is left
|
|
269
296
|
alone, since the calls would reach it. `#define NAME value`
|
|
270
297
|
becomes a const; any other directive is kept as a comment, with a note.
|
|
271
|
-
- A name the language already means something by gets a `_` suffix,
|
|
272
|
-
`int`
|
|
298
|
+
- A name the language already means something by gets a `_` suffix, with a
|
|
299
|
+
note. `int`, `uint` and `bool` are kept, since they are types here too; their
|
|
300
|
+
vectors (`ivec2`, `uvec3`, `bvec4` and the rest) become float vectors, with a
|
|
301
|
+
note saying what changed.
|
|
273
302
|
- What has no counterpart (`iFrameRate`, `texelFetch`, `dFdx`, a function
|
|
274
303
|
returning nothing) is left in place with a note, for `errors` to point at.
|
|
275
304
|
|
package/src/lang/ast.ts
CHANGED
|
@@ -99,7 +99,10 @@ export type AttributeArg =
|
|
|
99
99
|
| { k: "str"; text: string; pos: Pos; length: number }
|
|
100
100
|
| { k: "ident"; name: string; pos: Pos; length: number }
|
|
101
101
|
|
|
102
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* `[Range(0, 2)]` before a uniform, Unity's spelling of what its control is,
|
|
104
|
+
* or `[Wrap(Repeat)]` before a texture, of how it is sampled.
|
|
105
|
+
*/
|
|
103
106
|
export interface Attribute {
|
|
104
107
|
name: string
|
|
105
108
|
args: AttributeArg[]
|
|
@@ -120,6 +123,8 @@ export interface UniformDecl {
|
|
|
120
123
|
|
|
121
124
|
export interface TextureDecl {
|
|
122
125
|
name: string
|
|
126
|
+
/** `[Wrap(...)]` and `[Filter(...)]`, in the order written. */
|
|
127
|
+
attrs: Attribute[]
|
|
123
128
|
pos: Pos
|
|
124
129
|
}
|
|
125
130
|
|