onejs-sl 0.8.0 → 0.9.1
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 +36 -0
- package/README.md +44 -11
- package/goldens/goldens.json +54 -2
- package/package.json +1 -1
- package/src/body.ts +86 -10
- 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 +67 -17
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
|
}
|
|
@@ -687,9 +750,15 @@ export function hashProgram(nodes: SLNode[], result: NodeRef, uniforms: UniformD
|
|
|
687
750
|
// includes the library's text when it compiles, and none caches a compiled
|
|
688
751
|
// program by hash across a library update, so the new text reaches every
|
|
689
752
|
// shader without a bump.
|
|
690
|
-
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)]
|
|
691
754
|
for (const u of uniforms) parts.push(`U:${u.name}:${u.type}:${u.value.map(fixed).join(",")}`)
|
|
692
|
-
|
|
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
|
+
}
|
|
693
762
|
return fnv1a(parts.join("|"))
|
|
694
763
|
}
|
|
695
764
|
|
|
@@ -751,8 +820,8 @@ export function controlOf(c: UniformControl): UniformControl {
|
|
|
751
820
|
* int constant a fold used up, say), and they draw nothing. `toJSON` writes
|
|
752
821
|
* only the live ones, so an older reader never meets a dead newer node.
|
|
753
822
|
*/
|
|
754
|
-
export function programVersion(nodes: readonly SLNode[], result: NodeRef): number {
|
|
755
|
-
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
|
|
756
825
|
for (const ref of reachable(nodes as SLNode[], result)) v = Math.max(v, nodeVersion(nodes[ref]!))
|
|
757
826
|
return v
|
|
758
827
|
}
|
|
@@ -774,6 +843,9 @@ function nodeVersion(n: SLNode): number {
|
|
|
774
843
|
if (n.k !== "call") return 2
|
|
775
844
|
if (IR4_OPS.has(n.op)) return 4
|
|
776
845
|
switch (n.op) {
|
|
846
|
+
case SLOP.TEXTURE_SIZE:
|
|
847
|
+
case SLOP.SAMPLE_FETCH:
|
|
848
|
+
return 6
|
|
777
849
|
case SLOP.SAMPLE_PREVIOUS: return 5
|
|
778
850
|
case SLOP.SAMPLE_LOD: return 3
|
|
779
851
|
case SLOP.SDF:
|
|
@@ -797,6 +869,8 @@ function nodeVersion(n: SLNode): number {
|
|
|
797
869
|
* one immediate
|
|
798
870
|
* SAMPLE_PREVIOUS a float4 at a float2, and no immediates: there is one
|
|
799
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
|
|
800
874
|
*/
|
|
801
875
|
export function formProblem(n: Extract<SLNode, { k: "call" }>, nodes: readonly SLNode[]): string | null {
|
|
802
876
|
const widths = n.args.map((a) => { const v = nodes[a]!; return v.k === "if" || v.k === "loop" ? "control" : v.type }).join(",")
|
|
@@ -814,6 +888,12 @@ export function formProblem(n: Extract<SLNode, { k: "call" }>, nodes: readonly S
|
|
|
814
888
|
case SLOP.SAMPLE_PREVIOUS:
|
|
815
889
|
if (widths === "2" && n.type === 4 && n.kind === undefined && (n.imm?.length ?? 0) === 0) return null
|
|
816
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"
|
|
817
897
|
default:
|
|
818
898
|
return null
|
|
819
899
|
}
|
|
@@ -855,8 +935,9 @@ export const SL_HASH_VERSION = 2
|
|
|
855
935
|
* operands, where they were immediates
|
|
856
936
|
* 4 control flow (if, loop, their params and projs), ints, uints and bools
|
|
857
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`)
|
|
858
939
|
*/
|
|
859
|
-
export const SL_IR_VERSION =
|
|
940
|
+
export const SL_IR_VERSION = 6
|
|
860
941
|
|
|
861
942
|
function fnv1a(s: string): string {
|
|
862
943
|
let h = 0x811c9dc5
|
package/src/lang/README.md
CHANGED
|
@@ -169,8 +169,8 @@ recorded gap, not an oversight, and each has its own error message rather than
|
|
|
169
169
|
| `rgb2hsv` | The opcode is numbered and **no backend implements it**; every emitter throws on it. |
|
|
170
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. |
|
|
171
171
|
|
|
172
|
-
A `.sl` file and the EDSL are held to the same caps, `UNIFORM_SLOTS` (
|
|
173
|
-
`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
|
|
174
174
|
`tooManyUniforms` and `tooManyTextures`).
|
|
175
175
|
|
|
176
176
|
## Attributes
|
|
@@ -193,6 +193,16 @@ What they produce is metadata on the uniform (`range`, `toggle`, `options`,
|
|
|
193
193
|
`header`, `label`, `hide`), which the hash never reads. `[Color]` is the
|
|
194
194
|
exception, because it changes the reads: it is the same as a hex default.
|
|
195
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
|
+
|
|
196
206
|
## Errors
|
|
197
207
|
|
|
198
208
|
Every one carries file, line, column, offset and length, and `text`, the
|
|
@@ -237,6 +247,13 @@ on the parser's own scanner and word lists, so the highlighting is the parser's.
|
|
|
237
247
|
It never throws, since it runs on every keystroke. It classifies by spelling,
|
|
238
248
|
not scope: a local named `circle` still reads as a builtin where it is used.
|
|
239
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
|
+
|
|
240
257
|
`SL_KEYWORDS` and `SL_TYPES` (`words.ts`) are the lists the parser reads, and a
|
|
241
258
|
name may be neither. `onejs-sl/tables` carries them, and `ATTRIBUTE_NAMES`, with
|
|
242
259
|
a one line description of every builtin, input, attribute and prelude function; a prelude function's is the
|
|
@@ -257,7 +274,9 @@ where they were, so the result reads as the shader it came from. What it does:
|
|
|
257
274
|
the inputs. So do the calls that need an
|
|
258
275
|
expression: `mod` floors (`a - b * floor(a / b)`, never `%`, which
|
|
259
276
|
truncates), and `atan`, `inversesqrt`, `radians`, `degrees`, `exp2` and
|
|
260
|
-
`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.
|
|
261
280
|
- `mainImage` becomes `float4 main()`, with its colour parameter as a local it
|
|
262
281
|
returns; `void main()` writing `gl_FragColor` or a WebGL2 `out vec4` does the
|
|
263
282
|
same. Shadertoy's `uv = fragCoord / iResolution.xy` line is dropped, since
|
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
|
|
package/src/lang/attributes.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* shares.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type
|
|
14
|
-
import { TYPE_WIDTH, type Attribute, type AttributeArg, type UniformDecl } from "./ast"
|
|
13
|
+
import { TEXTURE_FILTERS, TEXTURE_WRAPS, type TextureFilter, type TextureWrap, type UniformControl } from "../ir"
|
|
14
|
+
import { TYPE_WIDTH, type Attribute, type AttributeArg, type TextureDecl, type UniformDecl } from "./ast"
|
|
15
15
|
import type { Pos, SLFix } from "./lexer"
|
|
16
16
|
|
|
17
17
|
export interface AttributeProblem {
|
|
@@ -42,6 +42,75 @@ const ALIASES: Record<string, AttributeName> = {
|
|
|
42
42
|
|
|
43
43
|
const LIST = "Range, Toggle, Enum, Header, Label, Color and Hide"
|
|
44
44
|
|
|
45
|
+
/** A texture's attributes (`Specs/SL_NEXT.md` 8): how the program samples it. */
|
|
46
|
+
export const TEXTURE_ATTRIBUTE_NAMES = ["Wrap", "Filter"] as const
|
|
47
|
+
export type TextureAttributeName = (typeof TEXTURE_ATTRIBUTE_NAMES)[number]
|
|
48
|
+
|
|
49
|
+
/** What each texture attribute takes, as written, from Unity's TextureWrapMode and FilterMode where they agree. */
|
|
50
|
+
const TEXTURE_VALUES: Record<TextureAttributeName, Record<string, TextureWrap | TextureFilter>> = {
|
|
51
|
+
Wrap: Object.fromEntries(TEXTURE_WRAPS.map((w) => [w[0]!.toUpperCase() + w.slice(1), w])),
|
|
52
|
+
Filter: Object.fromEntries(TEXTURE_FILTERS.map((f) => [f[0]!.toUpperCase() + f.slice(1), f])),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Unity's spellings that mean one of ours, and the ones no host can match, with why. */
|
|
56
|
+
const TEXTURE_VALUE_ALIASES: Record<string, string> = { Bilinear: "Linear", Nearest: "Point" }
|
|
57
|
+
const TEXTURE_VALUE_REFUSED: Record<string, string> = {
|
|
58
|
+
Trilinear: "a program samples one mip level at a time, so there is no blend between levels to ask for; " +
|
|
59
|
+
"write [Filter(Linear)], and tex2Dlod with a fractional level blends two levels",
|
|
60
|
+
MirrorOnce: "WebGPU has no mirror once, so a browser could not draw it; write [Wrap(Mirror)] or [Wrap(Clamp)]",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ReadTextureAttributes {
|
|
64
|
+
/** What could be read. Neither set, the texture samples as the bound texture is set. */
|
|
65
|
+
sampling: { wrap?: TextureWrap; filter?: TextureFilter }
|
|
66
|
+
problems: AttributeProblem[]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** `[Wrap(Repeat)]` and `[Filter(Point)]` on a `texture2D`, read into its sampler. */
|
|
70
|
+
export function readTextureAttributes(t: TextureDecl): ReadTextureAttributes {
|
|
71
|
+
const out: ReadTextureAttributes = { sampling: {}, problems: [] }
|
|
72
|
+
const seen = new Set<string>()
|
|
73
|
+
const problem = (at: Attribute | AttributeArg, message: string, fix?: SLFix) => {
|
|
74
|
+
out.problems.push({ message, pos: at.pos, length: at.length, ...(fix === undefined ? {} : { fix }) })
|
|
75
|
+
}
|
|
76
|
+
for (const a of t.attrs) {
|
|
77
|
+
const name = (TEXTURE_ATTRIBUTE_NAMES as readonly string[]).includes(a.name) ? a.name as TextureAttributeName
|
|
78
|
+
: a.name === "WrapMode" ? "Wrap" : a.name === "FilterMode" ? "Filter" : null
|
|
79
|
+
if (name === null) {
|
|
80
|
+
const uniform = (ATTRIBUTE_NAMES as readonly string[]).includes(a.name) || a.name in ALIASES
|
|
81
|
+
problem(a, uniform
|
|
82
|
+
? `[${a.name}] is for a uniform; a texture takes [Wrap(Repeat)], [Wrap(Mirror)], [Wrap(Clamp)] and [Filter(Point)], [Filter(Linear)]`
|
|
83
|
+
: `"${a.name}" is not an attribute of a texture; a texture takes Wrap and Filter`)
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
if (name !== a.name) {
|
|
87
|
+
problem(a, `"${a.name}" is ${name} here: [${name}(...)]`, { title: `Replace ${a.name} with ${name}`, replacement: name })
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
if (seen.has(name)) { problem(a, `this texture already has [${name}]`); continue }
|
|
91
|
+
seen.add(name)
|
|
92
|
+
const values = TEXTURE_VALUES[name]
|
|
93
|
+
const listed = Object.keys(values).join(", ")
|
|
94
|
+
const arg = a.args.length === 1 && a.args[0]!.k === "ident" ? a.args[0]! as Extract<AttributeArg, { k: "ident" }> : null
|
|
95
|
+
if (arg === null) {
|
|
96
|
+
problem(a.args[0] ?? a, `[${name}] takes one of ${listed}, as in [${name}(${Object.keys(values)[0]})]`)
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
const v = values[arg.name]
|
|
100
|
+
if (v !== undefined) {
|
|
101
|
+
if (name === "Wrap") out.sampling.wrap = v as TextureWrap
|
|
102
|
+
else out.sampling.filter = v as TextureFilter
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
const refused = TEXTURE_VALUE_REFUSED[arg.name]
|
|
106
|
+
if (refused !== undefined) { problem(arg, `${name}(${arg.name}) is not offered: ${refused}`); continue }
|
|
107
|
+
const to = TEXTURE_VALUE_ALIASES[arg.name] ?? Object.keys(values).find((k) => k.toLowerCase() === arg.name.toLowerCase())
|
|
108
|
+
problem(arg, `${name} takes ${listed}, and this is ${arg.name}${to === undefined ? "" : `. Did you mean ${to}?`}`,
|
|
109
|
+
to === undefined ? undefined : { title: `Replace ${arg.name} with ${to}`, replacement: to })
|
|
110
|
+
}
|
|
111
|
+
return out
|
|
112
|
+
}
|
|
113
|
+
|
|
45
114
|
export function readAttributes(u: UniformDecl): ReadAttributes {
|
|
46
115
|
const out: ReadAttributes = { control: {}, colour: false, from: {}, problems: [] }
|
|
47
116
|
const seen = new Set<string>()
|
|
@@ -62,6 +131,10 @@ export function readAttributes(u: UniformDecl): ReadAttributes {
|
|
|
62
131
|
|
|
63
132
|
for (const a of u.attrs) {
|
|
64
133
|
const known = (ATTRIBUTE_NAMES as readonly string[]).includes(a.name)
|
|
134
|
+
if (!known && ((TEXTURE_ATTRIBUTE_NAMES as readonly string[]).includes(a.name) || a.name === "WrapMode" || a.name === "FilterMode")) {
|
|
135
|
+
problem(a, `[${a.name}] is for a texture, as in [${a.name.replace("Mode", "")}(${a.name.startsWith("Wrap") ? "Repeat" : "Point"})] texture2D grain;`)
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
65
138
|
if (!known) {
|
|
66
139
|
const to = ALIASES[a.name] ?? ATTRIBUTE_NAMES.find((n) => n.toLowerCase() === a.name.toLowerCase())
|
|
67
140
|
const hint = a.name === "IntRange" ? "; a whole number slider is [Range(min, max, 1)]" : ""
|
package/src/lang/builtins.ts
CHANGED
package/src/lang/check.ts
CHANGED
|
@@ -34,7 +34,7 @@ import { SL_GLSL_HINT } from "../ops"
|
|
|
34
34
|
import { BUILTINS, NOT_YET } from "./builtins"
|
|
35
35
|
import { SL_SDF_SHAPES } from "../shapes"
|
|
36
36
|
import { TYPE_WIDTH, type Expr, type FuncDecl, type Stmt, type Unit } from "./ast"
|
|
37
|
-
import { readAttributes } from "./attributes"
|
|
37
|
+
import { readAttributes, readTextureAttributes } from "./attributes"
|
|
38
38
|
import { SLParseError, type Pos, type SLFix } from "./lexer"
|
|
39
39
|
|
|
40
40
|
const INPUT_NAMES = new Set(Object.keys(INPUTS))
|
|
@@ -68,7 +68,7 @@ const GLSL_RENAMES = new Set([
|
|
|
68
68
|
])
|
|
69
69
|
|
|
70
70
|
/** The builtins whose first argument is a texture, which is not a value anywhere else. */
|
|
71
|
-
const SAMPLES = new Set(["tex2D", "tex2Dlod"])
|
|
71
|
+
const SAMPLES = new Set(["tex2D", "tex2Dlod", "textureSize", "tex2Dfetch"])
|
|
72
72
|
|
|
73
73
|
const rename = (from: string, to: string): SLFix => ({ title: `Replace ${from} with ${to}`, replacement: to })
|
|
74
74
|
|
|
@@ -188,6 +188,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
188
188
|
const clash = valueClash(t.name)
|
|
189
189
|
if (clash !== null) fail(`"${t.name}" already names ${clash}`, t.pos, t.name.length)
|
|
190
190
|
})
|
|
191
|
+
for (const p of readTextureAttributes(t).problems) attempt(() => fail(p.message, p.pos, p.length, p.fix))
|
|
191
192
|
if (!textures.has(t.name)) textures.set(t.name, t)
|
|
192
193
|
}
|
|
193
194
|
attempt(() => {
|
|
@@ -512,7 +513,7 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
512
513
|
if (textures.has(e.name)) {
|
|
513
514
|
fail(
|
|
514
515
|
`"${e.name}" is a texture, and a texture is only ever the first argument of ` +
|
|
515
|
-
`tex2D or
|
|
516
|
+
`tex2D, tex2Dlod, tex2Dfetch or textureSize. Write \`tex2D(${e.name}, uv)\``,
|
|
516
517
|
e.pos, e.name.length,
|
|
517
518
|
)
|
|
518
519
|
}
|
|
@@ -636,11 +637,18 @@ export function check(unit: Unit, prelude: FuncDecl[], options: CheckOptions = {
|
|
|
636
637
|
e.pos,
|
|
637
638
|
)
|
|
638
639
|
}
|
|
640
|
+
if (previous && n === "textureSize") {
|
|
641
|
+
fail("previous is the size this program draws at, which is `resolution`", e.pos)
|
|
642
|
+
}
|
|
643
|
+
if (previous && n === "tex2Dfetch") {
|
|
644
|
+
fail("previous is read at a uv: `tex2D(previous, uv)`, or `tex2D(previous, (pixel + 0.5) / resolution)` for a pixel", e.pos)
|
|
645
|
+
}
|
|
639
646
|
if (!previous && (tex === undefined || tex.k !== "ident" || !textures.has(tex.name))) {
|
|
640
|
-
const
|
|
647
|
+
const rest = n === "tex2Dlod" ? "uv, 0" : n === "textureSize" ? "" : n === "tex2Dfetch" ? "pixel" : "uv"
|
|
648
|
+
const verb = n === "textureSize" ? "measures" : n === "tex2Dfetch" ? "reads" : "samples"
|
|
641
649
|
fail(
|
|
642
|
-
`${n}
|
|
643
|
-
`then \`${n}(grain,
|
|
650
|
+
`${n} ${verb} a texture declared in this file, as in \`texture2D grain;\` ` +
|
|
651
|
+
`then \`${n}(grain${rest === "" ? "" : ", " + rest})\``,
|
|
644
652
|
(tex ?? e).pos,
|
|
645
653
|
)
|
|
646
654
|
}
|
package/src/lang/classify.ts
CHANGED
|
@@ -46,11 +46,54 @@ const INPUT_NAMES: ReadonlySet<string> = new Set([...Object.keys(SOURCE_INPUTS),
|
|
|
46
46
|
|
|
47
47
|
/** The tokens of `source`, comments included, in order, each with its class. */
|
|
48
48
|
export function classify(source: string): SLClassifiedToken[] {
|
|
49
|
+
return run(source, "", "").tokens
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Where a line starts, for `classifyLine`: what the lines before it leave
|
|
54
|
+
* behind that changes how it reads.
|
|
55
|
+
*/
|
|
56
|
+
export interface SLLineState {
|
|
57
|
+
/** The line before ended inside a block comment. */
|
|
58
|
+
readonly comment: boolean
|
|
59
|
+
/** The last two tokens before the line that were not comments, which decide what a name after a dot or a bracket is. */
|
|
60
|
+
readonly before: string
|
|
61
|
+
readonly beforeThat: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const FIRST_LINE: SLLineState = { comment: false, before: "", beforeThat: "" }
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* One line of a file, for an editor that colours a line at a time, as Monaco
|
|
68
|
+
* does: its tokens, with offsets within the line, and the state the next line
|
|
69
|
+
* starts from. Classifying every line of a file this way, each from the state
|
|
70
|
+
* the one before ended in, gives exactly what `classify` gives the whole file,
|
|
71
|
+
* a block comment over several lines coming back as one piece per line.
|
|
72
|
+
*/
|
|
73
|
+
export function classifyLine(line: string, state: SLLineState = FIRST_LINE): { tokens: SLClassifiedToken[]; state: SLLineState } {
|
|
74
|
+
// A line that starts inside a block comment is read with the comment's
|
|
75
|
+
// opening put back in front, so the lexer decides where it closes, and
|
|
76
|
+
// those two characters are taken off the tokens again.
|
|
77
|
+
const shift = state.comment ? 2 : 0
|
|
78
|
+
const read = run(state.comment ? "/*" + line : line, state.before, state.beforeThat)
|
|
79
|
+
const last = read.tokens[read.tokens.length - 1]
|
|
80
|
+
const comment = last !== undefined && last.kind === "comment" && last.text.startsWith("/*")
|
|
81
|
+
&& !(last.text.length >= 4 && last.text.endsWith("*/"))
|
|
82
|
+
const tokens: SLClassifiedToken[] = []
|
|
83
|
+
for (const t of read.tokens) {
|
|
84
|
+
if (shift === 0) { tokens.push(t); continue }
|
|
85
|
+
if (t.offset === 0) {
|
|
86
|
+
if (t.length > shift) tokens.push({ ...t, text: t.text.slice(shift), length: t.length - shift })
|
|
87
|
+
} else {
|
|
88
|
+
tokens.push({ ...t, col: t.col - shift, offset: t.offset - shift })
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { tokens, state: { comment, before: read.before, beforeThat: read.beforeThat } }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function run(source: string, before: string, beforeThat: string): { tokens: SLClassifiedToken[]; before: string; beforeThat: string } {
|
|
49
95
|
const tokens = lex(source, "", true)
|
|
50
96
|
const out: SLClassifiedToken[] = []
|
|
51
|
-
// The last token that was not a comment, which decides what a name after a dot is.
|
|
52
|
-
let before = ""
|
|
53
|
-
let beforeThat = ""
|
|
54
97
|
for (const t of tokens) {
|
|
55
98
|
if (t.kind === "eof") break
|
|
56
99
|
let kind: SLTokenClass
|
|
@@ -68,7 +111,8 @@ export function classify(source: string): SLClassifiedToken[] {
|
|
|
68
111
|
kind = t.kind
|
|
69
112
|
}
|
|
70
113
|
out.push({ kind, text: t.text, line: t.line, col: t.col, offset: t.offset, length: t.length })
|
|
114
|
+
// The last token that was not a comment, which decides what a name after a dot is.
|
|
71
115
|
if (t.kind !== "comment") { beforeThat = before; before = t.text }
|
|
72
116
|
}
|
|
73
|
-
return out
|
|
117
|
+
return { tokens: out, before, beforeThat }
|
|
74
118
|
}
|
package/src/lang/glsl.ts
CHANGED
|
@@ -98,10 +98,8 @@ const MISSING: Record<string, string> = {
|
|
|
98
98
|
iFrameRate: "there is no frame rate",
|
|
99
99
|
iDate: "there is no date",
|
|
100
100
|
iChannelTime: "a texture has no time of its own",
|
|
101
|
-
iChannelResolution: "a
|
|
101
|
+
iChannelResolution: "write textureSize(iChannel0) for a channel's width and height",
|
|
102
102
|
iSampleRate: "there is no audio",
|
|
103
|
-
texelFetch: "a texture cannot be read without filtering yet",
|
|
104
|
-
textureSize: "a texture's size cannot be read yet",
|
|
105
103
|
dFdx: "there are no derivatives", dFdy: "there are no derivatives", fwidth: "there are no derivatives",
|
|
106
104
|
}
|
|
107
105
|
|
|
@@ -490,6 +488,8 @@ export function fromGLSL(glsl: string, options: { file?: string } = {}): FromGLS
|
|
|
490
488
|
// ---------- the rewrite ----------
|
|
491
489
|
|
|
492
490
|
const textures = new Set<string>()
|
|
491
|
+
/** Channels the shader declares itself, as a WebGL port of a Shadertoy shader does, which need no second declaration. */
|
|
492
|
+
const declaredChannels = new Set<string>()
|
|
493
493
|
let mouse = false
|
|
494
494
|
let uvDropped = false
|
|
495
495
|
|
|
@@ -530,6 +530,8 @@ export function fromGLSL(glsl: string, options: { file?: string } = {}): FromGLS
|
|
|
530
530
|
// `uniform sampler2D tex;` is `texture2D tex;`.
|
|
531
531
|
out += "texture2D"
|
|
532
532
|
i = skip(i + 1)
|
|
533
|
+
const declares = toks[skip(i + 1)]?.text ?? ""
|
|
534
|
+
if (/^iChannel\d$/.test(declares)) declaredChannels.add(declares)
|
|
533
535
|
continue
|
|
534
536
|
}
|
|
535
537
|
if (name === "sampler2D") { out += "texture2D"; continue }
|
|
@@ -616,6 +618,13 @@ export function fromGLSL(glsl: string, options: { file?: string } = {}): FromGLS
|
|
|
616
618
|
call = `pow(2.0, ${a[0]})`
|
|
617
619
|
} else if (name === "log2" && a.length === 1) {
|
|
618
620
|
call = `(log(${a[0]}) * 1.4426950408889634)`
|
|
621
|
+
} else if ((name === "texelFetch" && a.length === 3) || (name === "textureSize" && a.length === 2)) {
|
|
622
|
+
// Both read the full size texture here, so the level goes.
|
|
623
|
+
const level = a[a.length - 1]!
|
|
624
|
+
if (!/^0(\.0*)?$/.test(level)) {
|
|
625
|
+
note(t.line, `${name} reads level ${level} in GLSL, and ${name === "texelFetch" ? "tex2Dfetch" : name} reads the full size texture here`)
|
|
626
|
+
}
|
|
627
|
+
call = name === "texelFetch" ? `tex2Dfetch(${a[0]}, ${a[1]})` : `textureSize(${a[0]})`
|
|
619
628
|
}
|
|
620
629
|
if (call !== null) { out += call; i = close; continue }
|
|
621
630
|
}
|
|
@@ -683,7 +692,7 @@ export function fromGLSL(glsl: string, options: { file?: string } = {}): FromGLS
|
|
|
683
692
|
note(at >= 0 ? toks[at]!.line : 1, "the uv line is dropped: fragCoord / resolution is the input uv, y up")
|
|
684
693
|
}
|
|
685
694
|
|
|
686
|
-
const declared = [...textures].sort().map((t) => `texture2D ${t};`)
|
|
695
|
+
const declared = [...textures].filter((t) => !declaredChannels.has(t)).sort().map((t) => `texture2D ${t};`)
|
|
687
696
|
if (mouse) declared.unshift("uniform float4 mouse;")
|
|
688
697
|
const source = (declared.length > 0 ? declared.join("\n") + "\n" : "") + body.replace(/^\n+/, "")
|
|
689
698
|
const lineOf = (n: string) => Number(/^line (\d+)/.exec(n)?.[1] ?? 0)
|