@aethervtt/shard-shader 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/baseline/fragment-kernel.d.ts +15 -0
- package/dist/baseline/fragment-kernel.js +114 -0
- package/dist/baseline/rewrite.d.ts +20 -0
- package/dist/baseline/rewrite.js +436 -0
- package/dist/baseline/wgsl.d.ts +106 -0
- package/dist/baseline/wgsl.js +355 -0
- package/dist/data-marks.d.ts +23 -0
- package/dist/data-marks.js +40 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/layout.d.ts +7 -0
- package/dist/layout.js +100 -64
- package/dist/library.d.ts +21 -1
- package/dist/library.js +132 -36
- package/package.json +9 -5
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The uniform an image kernel's fragment form reads its target from: width, height, depth, layer. */
|
|
2
|
+
export declare const FRAGMENT_KERNEL_TARGET = "shard_target";
|
|
3
|
+
export interface FragmentKernel {
|
|
4
|
+
/** The module's WGSL with the fragment entry point (named as the compute one was). */
|
|
5
|
+
source: string;
|
|
6
|
+
/** The storage texture it wrote: its format, and its (group, binding), now the target uniform. */
|
|
7
|
+
format: string;
|
|
8
|
+
group: number;
|
|
9
|
+
binding: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The fragment form of the image kernel `entry` in a module's source. Throws
|
|
13
|
+
* `shader/fragment-kernel` when the kernel isn't one (no single storage output, stores elsewhere).
|
|
14
|
+
*/
|
|
15
|
+
export declare function fragmentKernel(source: string, entry?: string, label?: string): FragmentKernel;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { ShardError } from "@aethervtt/shard-core";
|
|
2
|
+
import { attribute, declarations, matching, tokenize } from "./wgsl.js";
|
|
3
|
+
const FRAGMENT_KERNEL_TARGET = "shard_target";
|
|
4
|
+
function fragmentKernel(source, entry = "main", label = "kernel") {
|
|
5
|
+
const fail = (message) => {
|
|
6
|
+
throw new ShardError("shader/fragment-kernel", `${label}: ${message}`, {
|
|
7
|
+
hint: "Only image kernels run as fragment passes: one storage texture, stored at the invocation id."
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
const tokens = tokenize(source);
|
|
11
|
+
const decls = declarations(source, tokens);
|
|
12
|
+
const outputs = decls.filter(
|
|
13
|
+
(d) => d.keyword === "var" && d.type.trim().startsWith("texture_storage_")
|
|
14
|
+
);
|
|
15
|
+
if (outputs.length !== 1) fail(`needs exactly one storage texture, found ${outputs.length}`);
|
|
16
|
+
const output = outputs[0];
|
|
17
|
+
const kind = output.type.trim();
|
|
18
|
+
const format = kind.slice(kind.indexOf("<") + 1, kind.indexOf(",")).trim();
|
|
19
|
+
const dims = kind.startsWith("texture_storage_3d") ? "xyz" : "xy";
|
|
20
|
+
const fn = decls.find((d) => d.keyword === "fn" && d.name === entry);
|
|
21
|
+
if (!fn?.attributes.some((a) => a.name === "compute")) fail(`no @compute fn ${entry}`);
|
|
22
|
+
const fnToken = tokens.findIndex((t, k) => k >= fn.first && t.text === "fn");
|
|
23
|
+
const body = fnBody(tokens, fnToken, fn.last);
|
|
24
|
+
const id = invocationId(tokens, body.params, body.open);
|
|
25
|
+
if (!id) fail("its entry point takes no @builtin(global_invocation_id)");
|
|
26
|
+
const edits = [];
|
|
27
|
+
const group = attribute(output, "group");
|
|
28
|
+
const binding = attribute(output, "binding");
|
|
29
|
+
edits.push({
|
|
30
|
+
start: output.start,
|
|
31
|
+
end: output.end,
|
|
32
|
+
text: `@group(${group}) @binding(${binding}) var<uniform> ${FRAGMENT_KERNEL_TARGET}: vec4u;`
|
|
33
|
+
});
|
|
34
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
35
|
+
if (i >= output.first && i <= output.last) continue;
|
|
36
|
+
const t = tokens[i];
|
|
37
|
+
if (t.kind !== "ident" || t.text !== output.name || tokens[i - 1]?.text === ".") continue;
|
|
38
|
+
const call = tokens[i - 2]?.text;
|
|
39
|
+
if (tokens[i - 1]?.text !== "(" || call !== "textureDimensions" && call !== "textureStore") {
|
|
40
|
+
fail(`uses "${output.name}" other than in textureDimensions or textureStore`);
|
|
41
|
+
}
|
|
42
|
+
const open = i - 1;
|
|
43
|
+
const close = matching(tokens, open);
|
|
44
|
+
if (call === "textureDimensions") {
|
|
45
|
+
edits.push({
|
|
46
|
+
start: tokens[i - 2].start,
|
|
47
|
+
end: tokens[close].end,
|
|
48
|
+
text: `${FRAGMENT_KERNEL_TARGET}.${dims}`
|
|
49
|
+
});
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (i < body.open || i > body.close) fail("stores outside its entry point");
|
|
53
|
+
const commas = topLevelCommas(tokens, open, close);
|
|
54
|
+
const value = source.slice(tokens[commas[commas.length - 1]].end, tokens[close].start).trim();
|
|
55
|
+
let end = tokens[close].end;
|
|
56
|
+
let next = close + 1;
|
|
57
|
+
if (tokens[next]?.text === ";") end = tokens[next++].end;
|
|
58
|
+
if (tokens[next]?.text === "return" && tokens[next + 1]?.text === ";")
|
|
59
|
+
end = tokens[next + 1].end;
|
|
60
|
+
edits.push({ start: tokens[i - 2].start, end, text: `return ${value};` });
|
|
61
|
+
}
|
|
62
|
+
for (let i = body.open; i < body.close; i++) {
|
|
63
|
+
if (tokens[i].text !== "return" || tokens[i + 1]?.text !== ";") continue;
|
|
64
|
+
if (edits.some((e) => e.start <= tokens[i].start && tokens[i].end <= e.end)) continue;
|
|
65
|
+
edits.push({ start: tokens[i].start, end: tokens[i + 1].end, text: "discard;" });
|
|
66
|
+
}
|
|
67
|
+
const attrStart = fn.attributes.length > 0 ? tokens[fn.first].start : fn.start;
|
|
68
|
+
edits.push({
|
|
69
|
+
start: attrStart,
|
|
70
|
+
end: tokens[fnToken].start,
|
|
71
|
+
text: "@fragment "
|
|
72
|
+
});
|
|
73
|
+
edits.push({
|
|
74
|
+
start: tokens[body.params].start,
|
|
75
|
+
end: tokens[body.open].end,
|
|
76
|
+
text: `(@builtin(position) shard_position: vec4f) -> @location(0) vec4f {
|
|
77
|
+
let ${id} = vec3u(vec2u(shard_position.xy), ${FRAGMENT_KERNEL_TARGET}.w);`
|
|
78
|
+
});
|
|
79
|
+
edits.sort((a, b) => b.start - a.start);
|
|
80
|
+
let out = source;
|
|
81
|
+
for (const e of edits) out = out.slice(0, e.start) + e.text + out.slice(e.end);
|
|
82
|
+
return { source: out, format, group: Number(group), binding: Number(binding) };
|
|
83
|
+
}
|
|
84
|
+
function fnBody(tokens, first, last) {
|
|
85
|
+
let params = first;
|
|
86
|
+
while (params <= last && tokens[params].text !== "(") params++;
|
|
87
|
+
const paramsClose = matching(tokens, params);
|
|
88
|
+
let open = paramsClose + 1;
|
|
89
|
+
while (open <= last && tokens[open].text !== "{") open++;
|
|
90
|
+
return { params, open, close: matching(tokens, open) };
|
|
91
|
+
}
|
|
92
|
+
function invocationId(tokens, first, open) {
|
|
93
|
+
for (let i = first; i < open; i++) {
|
|
94
|
+
if (tokens[i].text === "global_invocation_id" && tokens[i - 2]?.text === "builtin") {
|
|
95
|
+
return tokens[i + 2]?.kind === "ident" ? tokens[i + 2].text : void 0;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return void 0;
|
|
99
|
+
}
|
|
100
|
+
function topLevelCommas(tokens, open, close) {
|
|
101
|
+
const out = [];
|
|
102
|
+
let depth = 0;
|
|
103
|
+
for (let i = open + 1; i < close; i++) {
|
|
104
|
+
const t = tokens[i].text;
|
|
105
|
+
if (t === "(" || t === "[" || t === "{") depth++;
|
|
106
|
+
else if (t === ")" || t === "]" || t === "}") depth--;
|
|
107
|
+
else if (t === "," && depth === 0) out.push(i);
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
export {
|
|
112
|
+
FRAGMENT_KERNEL_TARGET,
|
|
113
|
+
fragmentKernel
|
|
114
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type DataDeclaration } from '../data-marks.js';
|
|
2
|
+
import type { SourceLocation } from '../library.js';
|
|
3
|
+
/** What a binding becomes on baseline, for the engine's bind group layouts. */
|
|
4
|
+
export interface RetargetedBinding {
|
|
5
|
+
group: number;
|
|
6
|
+
binding: number;
|
|
7
|
+
name: string;
|
|
8
|
+
as: 'data-texture' | 'uniform' | 'unfilterable-float';
|
|
9
|
+
}
|
|
10
|
+
export interface BaselineRewrite {
|
|
11
|
+
code: string;
|
|
12
|
+
bindings: RetargetedBinding[];
|
|
13
|
+
/** The offset in the linked code a rewritten offset came from (for compile errors). */
|
|
14
|
+
toLinked(offset: number): number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Rewrites linked WGSL for the baseline tier. `data` is every `@data` declaration of the modules
|
|
18
|
+
* the variant links; `locate` maps a linked offset to its module and line, for errors.
|
|
19
|
+
*/
|
|
20
|
+
export declare function rewriteForBaseline(code: string, data: readonly DataDeclaration[], locate?: (offset: number) => SourceLocation | undefined, label?: string): BaselineRewrite;
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { ShardError } from "@aethervtt/shard-core";
|
|
2
|
+
import { DATA_TEXTURE_WIDTH } from "../data-marks.js";
|
|
3
|
+
import {
|
|
4
|
+
attribute,
|
|
5
|
+
declarations,
|
|
6
|
+
Layouts,
|
|
7
|
+
matching,
|
|
8
|
+
parseType,
|
|
9
|
+
tokenize
|
|
10
|
+
} from "./wgsl.js";
|
|
11
|
+
const RENDER_STAGES = /* @__PURE__ */ new Set(["vertex", "fragment"]);
|
|
12
|
+
function rewriteForBaseline(code, data, locate = () => void 0, label = "shader") {
|
|
13
|
+
const tokens = tokenize(code);
|
|
14
|
+
const decls = declarations(code, tokens);
|
|
15
|
+
const layouts = new Layouts(code, tokens, decls);
|
|
16
|
+
const edits = [];
|
|
17
|
+
const bindings = [];
|
|
18
|
+
const tail = [];
|
|
19
|
+
const fail = (code2, message, offset, hint) => {
|
|
20
|
+
const at = locate(offset);
|
|
21
|
+
throw new ShardError(code2, `${label}: ${message}`, {
|
|
22
|
+
path: at ? `${at.module}:${at.line}:${at.column}` : void 0,
|
|
23
|
+
hint
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
const globals = /* @__PURE__ */ new Map();
|
|
27
|
+
for (const d of decls) if (d.keyword === "var") globals.set(d.name, d);
|
|
28
|
+
const reachable = renderReachable(tokens, decls, globals);
|
|
29
|
+
let coordNeeded = false;
|
|
30
|
+
for (const d of decls) {
|
|
31
|
+
if (d.keyword !== "var" || !d.template.startsWith("storage")) continue;
|
|
32
|
+
const group = Number(attribute(d, "group"));
|
|
33
|
+
const binding = Number(attribute(d, "binding"));
|
|
34
|
+
const marked = data.find((x) => x.group === group && x.binding === binding);
|
|
35
|
+
if (!marked) {
|
|
36
|
+
if (reachable.has(d.name)) {
|
|
37
|
+
fail(
|
|
38
|
+
"shader/baseline-storage",
|
|
39
|
+
`storage buffer "${d.name}" is read by a vertex or fragment shader, which the baseline tier can't do`,
|
|
40
|
+
d.start,
|
|
41
|
+
"Read engine data through shard::data (a @data declaration), or keep it out of render stages on baseline with @if(!BASELINE)."
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (d.template.includes("read_write")) {
|
|
47
|
+
fail("shader/baseline-data", `@data "${d.name}" must be read-only`, d.start);
|
|
48
|
+
}
|
|
49
|
+
const type = parseType(d.type);
|
|
50
|
+
const varToken = tokens.findIndex((t, i) => i >= d.first && t.text === "var");
|
|
51
|
+
if (marked.kind === "uniform") {
|
|
52
|
+
const runtime = type.kind === "array" && type.count === void 0;
|
|
53
|
+
if (runtime && !marked.count) {
|
|
54
|
+
fail(
|
|
55
|
+
"shader/baseline-data",
|
|
56
|
+
`@data(uniform) "${d.name}" is a runtime-sized array: give it a length, @data(uniform, N)`,
|
|
57
|
+
d.start
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
const sized = runtime && type.kind === "array" ? { ...type, count: marked.count } : type;
|
|
61
|
+
if (!layouts.sameInUniform(sized)) {
|
|
62
|
+
fail(
|
|
63
|
+
"shader/baseline-uniform-layout",
|
|
64
|
+
`@data(uniform) "${d.name}" (${d.type}) lies out differently in uniform memory`,
|
|
65
|
+
d.start,
|
|
66
|
+
"Pad its arrays to 16-byte strides, or declare it @data (a data texture)."
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const element = sized.kind === "array" ? sized.element : void 0;
|
|
70
|
+
if (sized.kind === "array" && sized.count !== void 0 && element && (element.kind === "struct" || element.kind === "matrix" || element.kind === "array")) {
|
|
71
|
+
const words = sized.count * layouts.stride(sized, "storage") / 16;
|
|
72
|
+
edits.push({
|
|
73
|
+
start: tokens[varToken].start,
|
|
74
|
+
end: d.end,
|
|
75
|
+
text: `var<uniform> ${d.name}: array<vec4<u32>, ${words}>;`
|
|
76
|
+
});
|
|
77
|
+
rejectShadowing(tokens, decls, d, fail);
|
|
78
|
+
for (const i of usesOf(tokens, decls, d)) {
|
|
79
|
+
const t = tokens[i];
|
|
80
|
+
if (tokens[i + 1]?.text === "[") {
|
|
81
|
+
const close = matching(tokens, i + 1);
|
|
82
|
+
edits.push({ start: t.start, end: tokens[i + 1].end, text: `${d.name}_at(u32(` });
|
|
83
|
+
edits.push({ start: tokens[close].start, end: tokens[close].end, text: "))" });
|
|
84
|
+
} else if (tokens[i - 1]?.text === "&" && tokens[i - 2]?.text === "(" && tokens[i - 3]?.text === "arrayLength" && tokens[i + 1]?.text === ")") {
|
|
85
|
+
edits.push({
|
|
86
|
+
start: tokens[i - 3].start,
|
|
87
|
+
end: tokens[i + 1].end,
|
|
88
|
+
text: `${sized.count}u`
|
|
89
|
+
});
|
|
90
|
+
} else {
|
|
91
|
+
fail(
|
|
92
|
+
"shader/baseline-data",
|
|
93
|
+
`@data(uniform) "${d.name}" is used other than by index or arrayLength`,
|
|
94
|
+
t.start,
|
|
95
|
+
"Read one element at a time: name[i]."
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
tail.push(
|
|
100
|
+
loader(
|
|
101
|
+
d.name,
|
|
102
|
+
sized,
|
|
103
|
+
layouts,
|
|
104
|
+
false,
|
|
105
|
+
(k) => `${d.name}[${k}]`
|
|
106
|
+
)
|
|
107
|
+
);
|
|
108
|
+
} else if (runtime && type.kind === "array") {
|
|
109
|
+
edits.push({
|
|
110
|
+
start: tokens[varToken].start,
|
|
111
|
+
end: d.end,
|
|
112
|
+
text: `var<uniform> ${d.name}: array<${type.elementText}, ${marked.count}>;`
|
|
113
|
+
});
|
|
114
|
+
for (let i = 3; i < tokens.length - 1; i++) {
|
|
115
|
+
if (tokens[i].text === d.name && tokens[i - 1].text === "&" && tokens[i - 2].text === "(" && tokens[i - 3].text === "arrayLength" && tokens[i + 1].text === ")") {
|
|
116
|
+
edits.push({
|
|
117
|
+
start: tokens[i - 3].start,
|
|
118
|
+
end: tokens[i + 1].end,
|
|
119
|
+
text: `${marked.count}u`
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
const close = matching(tokens, varToken + 1);
|
|
125
|
+
edits.push({
|
|
126
|
+
start: tokens[varToken].start,
|
|
127
|
+
end: tokens[close].end,
|
|
128
|
+
text: "var<uniform>"
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
bindings.push({ group, binding, name: d.name, as: "uniform" });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (type.kind !== "array") {
|
|
135
|
+
fail("shader/baseline-data", `@data "${d.name}" must be an array (${d.type})`, d.start);
|
|
136
|
+
}
|
|
137
|
+
const array = type;
|
|
138
|
+
edits.push({
|
|
139
|
+
start: tokens[varToken].start,
|
|
140
|
+
end: d.end,
|
|
141
|
+
text: `var ${d.name}: texture_2d<u32>;`
|
|
142
|
+
});
|
|
143
|
+
bindings.push({ group, binding, name: d.name, as: "data-texture" });
|
|
144
|
+
rejectShadowing(tokens, decls, d, fail);
|
|
145
|
+
let lengthUsed = false;
|
|
146
|
+
for (const i of usesOf(tokens, decls, d)) {
|
|
147
|
+
const t = tokens[i];
|
|
148
|
+
if (tokens[i + 1]?.text === "[") {
|
|
149
|
+
const close = matching(tokens, i + 1);
|
|
150
|
+
edits.push({ start: t.start, end: tokens[i + 1].end, text: `${d.name}_at(u32(` });
|
|
151
|
+
edits.push({ start: tokens[close].start, end: tokens[close].end, text: "))" });
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (tokens[i - 1]?.text === "&" && tokens[i - 2]?.text === "(" && tokens[i - 3]?.text === "arrayLength" && tokens[i + 1]?.text === ")") {
|
|
155
|
+
edits.push({
|
|
156
|
+
start: tokens[i - 3].start,
|
|
157
|
+
end: tokens[i + 1].end,
|
|
158
|
+
text: `${d.name}_len()`
|
|
159
|
+
});
|
|
160
|
+
lengthUsed = true;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
fail(
|
|
164
|
+
"shader/baseline-data",
|
|
165
|
+
`@data "${d.name}" is used other than by index or arrayLength`,
|
|
166
|
+
t.start,
|
|
167
|
+
"Read one element at a time: name[i]."
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
tail.push(
|
|
171
|
+
loader(
|
|
172
|
+
d.name,
|
|
173
|
+
array,
|
|
174
|
+
layouts,
|
|
175
|
+
lengthUsed,
|
|
176
|
+
(k) => `textureLoad(${d.name}, shard_data_coord(${k}), 0)`
|
|
177
|
+
)
|
|
178
|
+
);
|
|
179
|
+
coordNeeded = true;
|
|
180
|
+
}
|
|
181
|
+
if (coordNeeded) {
|
|
182
|
+
tail.unshift(
|
|
183
|
+
`fn shard_data_coord(k: u32) -> vec2i {
|
|
184
|
+
return vec2i(i32(k % ${DATA_TEXTURE_WIDTH}u), i32(k / ${DATA_TEXTURE_WIDTH}u));
|
|
185
|
+
}`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
for (const d of decls) {
|
|
189
|
+
if (d.keyword !== "var" || !d.type.startsWith("texture_depth_")) continue;
|
|
190
|
+
const kind = d.type.trim();
|
|
191
|
+
const floatType = kind === "texture_depth_2d" ? "texture_2d<f32>" : kind === "texture_depth_2d_array" ? "texture_2d_array<f32>" : kind === "texture_depth_cube" ? "texture_cube<f32>" : void 0;
|
|
192
|
+
if (!floatType) continue;
|
|
193
|
+
const calls = callsOn(tokens, d);
|
|
194
|
+
if (calls.some((c) => /Compare/.test(c.fn))) continue;
|
|
195
|
+
if (usesOf(tokens, decls, d).length !== calls.length) continue;
|
|
196
|
+
const typeStart = code.indexOf(kind, d.start);
|
|
197
|
+
edits.push({ start: typeStart, end: typeStart + kind.length, text: floatType });
|
|
198
|
+
for (const c of calls) {
|
|
199
|
+
if (c.fn === "textureGather") {
|
|
200
|
+
edits.push({ start: c.open, end: c.open, text: "0, " });
|
|
201
|
+
} else if (c.fn !== "textureDimensions" && c.fn !== "textureNumLayers") {
|
|
202
|
+
edits.push({ start: c.close, end: c.close, text: ".x" });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
bindings.push({
|
|
206
|
+
group: Number(attribute(d, "group")),
|
|
207
|
+
binding: Number(attribute(d, "binding")),
|
|
208
|
+
name: d.name,
|
|
209
|
+
as: "unfilterable-float"
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
let reverseBits = false;
|
|
213
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
214
|
+
const t = tokens[i];
|
|
215
|
+
if (t.text === "interpolate" && tokens[i - 1]?.text === "@" && tokens[i + 1]?.text === "(" && tokens[i + 2]?.text === "flat" && tokens[i + 3]?.text === ")") {
|
|
216
|
+
edits.push({ start: tokens[i + 3].start, end: tokens[i + 3].start, text: ", either" });
|
|
217
|
+
}
|
|
218
|
+
if (t.text === "reverseBits" && tokens[i + 1]?.text === "(") {
|
|
219
|
+
edits.push({ start: t.start, end: t.end, text: "shard_reverse_bits" });
|
|
220
|
+
reverseBits = true;
|
|
221
|
+
}
|
|
222
|
+
if ((t.text === "countOneBits" || t.text === "firstLeadingBit" || t.text === "firstTrailingBit" || t.text === "extractBits" || t.text === "insertBits" || t.text === "textureNumLevels") && tokens[i + 1]?.text === "(") {
|
|
223
|
+
fail(
|
|
224
|
+
"shader/baseline-builtin",
|
|
225
|
+
`${t.text} has no GLSL ES 3.00 equivalent`,
|
|
226
|
+
t.start,
|
|
227
|
+
"Compute it differently on baseline (@if(BASELINE))."
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (reverseBits) tail.push(REVERSE_BITS);
|
|
232
|
+
return { ...apply(code, edits, tail), bindings };
|
|
233
|
+
}
|
|
234
|
+
function usesOf(tokens, decls, d) {
|
|
235
|
+
const out = [];
|
|
236
|
+
let s = 0;
|
|
237
|
+
const structs = decls.filter((x) => x.keyword === "struct");
|
|
238
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
239
|
+
const t = tokens[i];
|
|
240
|
+
if (t.kind !== "ident" || t.text !== d.name || tokens[i - 1]?.text === ".") continue;
|
|
241
|
+
if (i >= d.first && i <= d.last) continue;
|
|
242
|
+
while (s < structs.length && structs[s].last < i) s++;
|
|
243
|
+
if (s < structs.length && structs[s].first <= i) continue;
|
|
244
|
+
out.push(i);
|
|
245
|
+
}
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
function renderReachable(tokens, decls, globals) {
|
|
249
|
+
const fns = /* @__PURE__ */ new Map();
|
|
250
|
+
for (const d of decls) if (d.keyword === "fn") fns.set(d.name, d);
|
|
251
|
+
const seen = /* @__PURE__ */ new Set();
|
|
252
|
+
const out = /* @__PURE__ */ new Set();
|
|
253
|
+
const stack = decls.filter((d) => d.keyword === "fn" && d.attributes.some((a) => RENDER_STAGES.has(a.name))).map((d) => d.name);
|
|
254
|
+
while (stack.length > 0) {
|
|
255
|
+
const name = stack.pop();
|
|
256
|
+
if (seen.has(name)) continue;
|
|
257
|
+
seen.add(name);
|
|
258
|
+
const fn = fns.get(name);
|
|
259
|
+
for (let i = fn.first; i <= fn.last; i++) {
|
|
260
|
+
const t = tokens[i];
|
|
261
|
+
if (t.kind !== "ident" || tokens[i - 1]?.text === ".") continue;
|
|
262
|
+
if (fns.has(t.text) && !seen.has(t.text)) stack.push(t.text);
|
|
263
|
+
else if (globals.has(t.text)) out.add(t.text);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return out;
|
|
267
|
+
}
|
|
268
|
+
function rejectShadowing(tokens, decls, data, fail) {
|
|
269
|
+
for (const d of decls) {
|
|
270
|
+
if (d.keyword !== "fn") continue;
|
|
271
|
+
for (let i = d.first; i <= d.last; i++) {
|
|
272
|
+
const t = tokens[i];
|
|
273
|
+
if (t.text !== data.name) continue;
|
|
274
|
+
const before = tokens[i - 1]?.text;
|
|
275
|
+
const after = tokens[i + 1]?.text;
|
|
276
|
+
const declares = before === "let" || before === "var" || before === "const";
|
|
277
|
+
const parameter = after === ":" && (before === "(" || before === ",");
|
|
278
|
+
if (declares || parameter) {
|
|
279
|
+
fail(
|
|
280
|
+
"shader/baseline-shadowed",
|
|
281
|
+
`"${data.name}" is declared again inside ${d.name}, over the @data declaration`,
|
|
282
|
+
t.start,
|
|
283
|
+
"Rename the local."
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function callsOn(tokens, d) {
|
|
290
|
+
const out = [];
|
|
291
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
292
|
+
const t = tokens[i];
|
|
293
|
+
if (t.kind !== "ident" || !t.text.startsWith("texture")) continue;
|
|
294
|
+
if (tokens[i + 1]?.text !== "(" || tokens[i + 2]?.text !== d.name) continue;
|
|
295
|
+
const close = matching(tokens, i + 1);
|
|
296
|
+
out.push({ fn: t.text, open: tokens[i + 2].start, close: tokens[close].end });
|
|
297
|
+
}
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
300
|
+
function loader(name, array, layouts, withLength, fetch) {
|
|
301
|
+
const stride = layouts.stride(array, "storage");
|
|
302
|
+
const element = array.element;
|
|
303
|
+
const lines = [];
|
|
304
|
+
let word;
|
|
305
|
+
if (stride % 16 === 0) {
|
|
306
|
+
const texels = stride / 16;
|
|
307
|
+
lines.push(` let b = i * ${texels}u;`);
|
|
308
|
+
for (let k = 0; k < texels; k++) {
|
|
309
|
+
lines.push(` let t${k} = ${fetch(`b + ${k}u`)};`);
|
|
310
|
+
}
|
|
311
|
+
word = (byte) => `t${byte >> 4}.${"xyzw"[(byte & 15) >> 2]}`;
|
|
312
|
+
} else {
|
|
313
|
+
lines.push(` let b = i * ${stride}u;`);
|
|
314
|
+
word = (byte) => `${name}_word(b + ${byte}u)`;
|
|
315
|
+
}
|
|
316
|
+
const body = `${lines.join("\n")}
|
|
317
|
+
return ${read(element, 0, word, layouts)};`;
|
|
318
|
+
let out = `fn ${name}_at(i: u32) -> ${canonical(element)} {
|
|
319
|
+
${body}
|
|
320
|
+
}`;
|
|
321
|
+
if (stride % 16 !== 0) {
|
|
322
|
+
out += `
|
|
323
|
+
fn ${name}_word(byte: u32) -> u32 {
|
|
324
|
+
return ${fetch("byte / 16u")}[(byte % 16u) / 4u];
|
|
325
|
+
}`;
|
|
326
|
+
}
|
|
327
|
+
if (withLength) {
|
|
328
|
+
out += `
|
|
329
|
+
fn ${name}_len() -> u32 {
|
|
330
|
+
let d = vec2i(textureDimensions(${name}));
|
|
331
|
+
return textureLoad(${name}, d - vec2i(1, 1), 0).x / ${stride}u;
|
|
332
|
+
}`;
|
|
333
|
+
}
|
|
334
|
+
return out;
|
|
335
|
+
}
|
|
336
|
+
function read(t, offset, word, layouts) {
|
|
337
|
+
switch (t.kind) {
|
|
338
|
+
case "scalar":
|
|
339
|
+
return scalar(t.name, word(offset));
|
|
340
|
+
case "vector": {
|
|
341
|
+
const parts = [];
|
|
342
|
+
for (let k = 0; k < t.size; k++) parts.push(scalar(t.element.name, word(offset + k * 4)));
|
|
343
|
+
return `${canonical(t)}(${parts.join(", ")})`;
|
|
344
|
+
}
|
|
345
|
+
case "matrix": {
|
|
346
|
+
const column = { kind: "vector", size: t.rows, element: t.element };
|
|
347
|
+
const step = t.rows === 2 ? 8 : 16;
|
|
348
|
+
const columns = [];
|
|
349
|
+
for (let c = 0; c < t.columns; c++)
|
|
350
|
+
columns.push(read(column, offset + c * step, word, layouts));
|
|
351
|
+
return `${canonical(t)}(${columns.join(", ")})`;
|
|
352
|
+
}
|
|
353
|
+
case "array": {
|
|
354
|
+
if (t.count === void 0)
|
|
355
|
+
throw new ShardError("shader/baseline-data", "Nested runtime array");
|
|
356
|
+
const stride = layouts.stride(t, "storage");
|
|
357
|
+
const items = [];
|
|
358
|
+
for (let k = 0; k < t.count; k++)
|
|
359
|
+
items.push(read(t.element, offset + k * stride, word, layouts));
|
|
360
|
+
return `${canonical(t)}(${items.join(", ")})`;
|
|
361
|
+
}
|
|
362
|
+
case "struct": {
|
|
363
|
+
const s = layouts.struct(t.name, "storage");
|
|
364
|
+
return `${t.name}(${s.members.map((m) => read(m.type, offset + m.offset, word, layouts)).join(", ")})`;
|
|
365
|
+
}
|
|
366
|
+
default:
|
|
367
|
+
throw new ShardError(
|
|
368
|
+
"shader/baseline-data",
|
|
369
|
+
`Can't read ${JSON.stringify(t)} from a data texture`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function scalar(name, w) {
|
|
374
|
+
if (name === "u32") return w;
|
|
375
|
+
if (name === "i32" || name === "f32") return `bitcast<${name}>(${w})`;
|
|
376
|
+
throw new ShardError("shader/baseline-data", `${name} can't live in a data texture`);
|
|
377
|
+
}
|
|
378
|
+
function canonical(t) {
|
|
379
|
+
switch (t.kind) {
|
|
380
|
+
case "scalar":
|
|
381
|
+
return t.name;
|
|
382
|
+
case "vector":
|
|
383
|
+
return `vec${t.size}<${t.element.name}>`;
|
|
384
|
+
case "matrix":
|
|
385
|
+
return `mat${t.columns}x${t.rows}<${t.element.name}>`;
|
|
386
|
+
case "array":
|
|
387
|
+
return t.count === void 0 ? `array<${canonical(t.element)}>` : `array<${canonical(t.element)}, ${t.count}>`;
|
|
388
|
+
case "struct":
|
|
389
|
+
return t.name;
|
|
390
|
+
case "atomic":
|
|
391
|
+
return `atomic<${canonical(t.element)}>`;
|
|
392
|
+
default:
|
|
393
|
+
return t.text;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const REVERSE_BITS = `fn shard_reverse_bits(v: u32) -> u32 {
|
|
397
|
+
var x = v;
|
|
398
|
+
x = ((x >> 1u) & 0x55555555u) | ((x & 0x55555555u) << 1u);
|
|
399
|
+
x = ((x >> 2u) & 0x33333333u) | ((x & 0x33333333u) << 2u);
|
|
400
|
+
x = ((x >> 4u) & 0x0F0F0F0Fu) | ((x & 0x0F0F0F0Fu) << 4u);
|
|
401
|
+
x = ((x >> 8u) & 0x00FF00FFu) | ((x & 0x00FF00FFu) << 8u);
|
|
402
|
+
return (x >> 16u) | (x << 16u);
|
|
403
|
+
}`;
|
|
404
|
+
function apply(code, edits, tail) {
|
|
405
|
+
edits.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
406
|
+
let out = "";
|
|
407
|
+
let at = 0;
|
|
408
|
+
const marks = [];
|
|
409
|
+
for (const e of edits) {
|
|
410
|
+
out += code.slice(at, e.start);
|
|
411
|
+
marks.push({ out: out.length, linked: e.start });
|
|
412
|
+
out += e.text;
|
|
413
|
+
at = e.end;
|
|
414
|
+
marks.push({ out: out.length, linked: e.end });
|
|
415
|
+
}
|
|
416
|
+
out += code.slice(at);
|
|
417
|
+
const end = code.length;
|
|
418
|
+
if (tail.length > 0) out += `
|
|
419
|
+
// baseline tier (0064)
|
|
420
|
+
${tail.join("\n\n")}
|
|
421
|
+
`;
|
|
422
|
+
return {
|
|
423
|
+
code: out,
|
|
424
|
+
toLinked(offset) {
|
|
425
|
+
let base = { out: 0, linked: 0 };
|
|
426
|
+
for (const m of marks) {
|
|
427
|
+
if (m.out > offset) break;
|
|
428
|
+
base = m;
|
|
429
|
+
}
|
|
430
|
+
return Math.min(end, base.linked + (offset - base.out));
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
export {
|
|
435
|
+
rewriteForBaseline
|
|
436
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export interface Token {
|
|
2
|
+
kind: 'ident' | 'number' | 'punct';
|
|
3
|
+
text: string;
|
|
4
|
+
start: number;
|
|
5
|
+
end: number;
|
|
6
|
+
}
|
|
7
|
+
/** Tokens of `code`, comments skipped. Punctuation is one character, except `->`. */
|
|
8
|
+
export declare function tokenize(code: string): Token[];
|
|
9
|
+
/** Index of the token closing the bracket at `open` (`(`, `[`, `{` or `<` of a template). */
|
|
10
|
+
export declare function matching(tokens: readonly Token[], open: number): number;
|
|
11
|
+
export interface Attribute {
|
|
12
|
+
name: string;
|
|
13
|
+
/** Argument text, e.g. `2` for `@group(2)`; '' without arguments. */
|
|
14
|
+
args: string;
|
|
15
|
+
}
|
|
16
|
+
/** A module-scope declaration: its attributes, keyword, name and where it lies in the code. */
|
|
17
|
+
export interface Declaration {
|
|
18
|
+
attributes: Attribute[];
|
|
19
|
+
keyword: 'struct' | 'fn' | 'var' | 'const' | 'override' | 'alias' | 'other';
|
|
20
|
+
name: string;
|
|
21
|
+
/** `var<storage, read>` → `storage, read`. */
|
|
22
|
+
template: string;
|
|
23
|
+
/** The type after `:` (vars), as written. */
|
|
24
|
+
type: string;
|
|
25
|
+
/** Offsets of the whole declaration, attributes included, and of its body or type. */
|
|
26
|
+
start: number;
|
|
27
|
+
end: number;
|
|
28
|
+
/** Token range of the declaration. */
|
|
29
|
+
first: number;
|
|
30
|
+
last: number;
|
|
31
|
+
}
|
|
32
|
+
/** Module-scope declarations of linked WGSL, in order. */
|
|
33
|
+
export declare function declarations(code: string, tokens: readonly Token[]): Declaration[];
|
|
34
|
+
export declare function attribute(d: Declaration, name: string): string | undefined;
|
|
35
|
+
export type WgslType = {
|
|
36
|
+
kind: 'scalar';
|
|
37
|
+
name: 'f32' | 'u32' | 'i32' | 'f16' | 'bool';
|
|
38
|
+
} | {
|
|
39
|
+
kind: 'vector';
|
|
40
|
+
size: 2 | 3 | 4;
|
|
41
|
+
element: WgslType & {
|
|
42
|
+
kind: 'scalar';
|
|
43
|
+
};
|
|
44
|
+
} | {
|
|
45
|
+
kind: 'matrix';
|
|
46
|
+
columns: 2 | 3 | 4;
|
|
47
|
+
rows: 2 | 3 | 4;
|
|
48
|
+
element: WgslType & {
|
|
49
|
+
kind: 'scalar';
|
|
50
|
+
};
|
|
51
|
+
} | {
|
|
52
|
+
kind: 'array';
|
|
53
|
+
element: WgslType;
|
|
54
|
+
elementText: string;
|
|
55
|
+
count: number | undefined;
|
|
56
|
+
text: string;
|
|
57
|
+
} | {
|
|
58
|
+
kind: 'struct';
|
|
59
|
+
name: string;
|
|
60
|
+
} | {
|
|
61
|
+
kind: 'atomic';
|
|
62
|
+
element: WgslType;
|
|
63
|
+
} | {
|
|
64
|
+
kind: 'other';
|
|
65
|
+
text: string;
|
|
66
|
+
};
|
|
67
|
+
/** Parses a type as written (`array<Light>`, `vec3f`, `mat4x4<f32>`, `Instance`). */
|
|
68
|
+
export declare function parseType(text: string): WgslType;
|
|
69
|
+
export interface Member {
|
|
70
|
+
name: string;
|
|
71
|
+
type: WgslType;
|
|
72
|
+
typeText: string;
|
|
73
|
+
offset: number;
|
|
74
|
+
size: number;
|
|
75
|
+
align: number;
|
|
76
|
+
}
|
|
77
|
+
export interface StructLayout {
|
|
78
|
+
name: string;
|
|
79
|
+
members: Member[];
|
|
80
|
+
size: number;
|
|
81
|
+
align: number;
|
|
82
|
+
}
|
|
83
|
+
export type AddressSpace = 'storage' | 'uniform';
|
|
84
|
+
/**
|
|
85
|
+
* Struct layouts by name, under one address space's rules (WGSL §14.4): uniform raises array
|
|
86
|
+
* strides and struct alignments to 16. `@align` and `@size` are honored.
|
|
87
|
+
*/
|
|
88
|
+
export declare class Layouts {
|
|
89
|
+
private readonly structs;
|
|
90
|
+
private readonly cache;
|
|
91
|
+
constructor(code: string, tokens: readonly Token[], decls: readonly Declaration[]);
|
|
92
|
+
has(name: string): boolean;
|
|
93
|
+
struct(name: string, space: AddressSpace): StructLayout;
|
|
94
|
+
align(t: WgslType, space: AddressSpace): number;
|
|
95
|
+
size(t: WgslType, space: AddressSpace): number;
|
|
96
|
+
/** Bytes between an array's elements. */
|
|
97
|
+
stride(t: WgslType & {
|
|
98
|
+
kind: 'array';
|
|
99
|
+
}, space: AddressSpace): number;
|
|
100
|
+
/**
|
|
101
|
+
* Whether a type lies out the same in uniform and storage memory: every member and element at
|
|
102
|
+
* the same offset, so the same bytes read the same through either.
|
|
103
|
+
*/
|
|
104
|
+
sameInUniform(t: WgslType): boolean;
|
|
105
|
+
private sameLayout;
|
|
106
|
+
}
|