@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,355 @@
|
|
|
1
|
+
function tokenize(code) {
|
|
2
|
+
const out = [];
|
|
3
|
+
let i = 0;
|
|
4
|
+
const n = code.length;
|
|
5
|
+
while (i < n) {
|
|
6
|
+
const c = code.charCodeAt(i);
|
|
7
|
+
if (c === 32 || c === 9 || c === 10 || c === 13) {
|
|
8
|
+
i++;
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
if (c === 47 && code.charCodeAt(i + 1) === 47) {
|
|
12
|
+
while (i < n && code.charCodeAt(i) !== 10) i++;
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (c === 47 && code.charCodeAt(i + 1) === 42) {
|
|
16
|
+
let depth = 1;
|
|
17
|
+
i += 2;
|
|
18
|
+
while (i < n && depth > 0) {
|
|
19
|
+
if (code.charCodeAt(i) === 47 && code.charCodeAt(i + 1) === 42) {
|
|
20
|
+
depth++;
|
|
21
|
+
i += 2;
|
|
22
|
+
} else if (code.charCodeAt(i) === 42 && code.charCodeAt(i + 1) === 47) {
|
|
23
|
+
depth--;
|
|
24
|
+
i += 2;
|
|
25
|
+
} else i++;
|
|
26
|
+
}
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const start = i;
|
|
30
|
+
if (isIdentStart(c)) {
|
|
31
|
+
while (i < n && isIdentPart(code.charCodeAt(i))) i++;
|
|
32
|
+
out.push({ kind: "ident", text: code.slice(start, i), start, end: i });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (c >= 48 && c <= 57) {
|
|
36
|
+
while (i < n && /[0-9a-fA-FxXuif.eEpP+-]/.test(code[i])) {
|
|
37
|
+
const ch = code[i];
|
|
38
|
+
if ((ch === "+" || ch === "-") && !/[eEpP]/.test(code[i - 1])) break;
|
|
39
|
+
i++;
|
|
40
|
+
}
|
|
41
|
+
out.push({ kind: "number", text: code.slice(start, i), start, end: i });
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (c === 45 && code.charCodeAt(i + 1) === 62) {
|
|
45
|
+
out.push({ kind: "punct", text: "->", start, end: i + 2 });
|
|
46
|
+
i += 2;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
out.push({ kind: "punct", text: code[i], start, end: i + 1 });
|
|
50
|
+
i++;
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
function isIdentStart(c) {
|
|
55
|
+
return c >= 65 && c <= 90 || c >= 97 && c <= 122 || c === 95;
|
|
56
|
+
}
|
|
57
|
+
function isIdentPart(c) {
|
|
58
|
+
return isIdentStart(c) || c >= 48 && c <= 57;
|
|
59
|
+
}
|
|
60
|
+
function matching(tokens, open) {
|
|
61
|
+
const o = tokens[open].text;
|
|
62
|
+
const c = o === "(" ? ")" : o === "[" ? "]" : o === "{" ? "}" : ">";
|
|
63
|
+
let depth = 0;
|
|
64
|
+
for (let i = open; i < tokens.length; i++) {
|
|
65
|
+
const t = tokens[i].text;
|
|
66
|
+
if (t === o) depth++;
|
|
67
|
+
else if (t === c) {
|
|
68
|
+
depth--;
|
|
69
|
+
if (depth === 0) return i;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return -1;
|
|
73
|
+
}
|
|
74
|
+
function declarations(code, tokens) {
|
|
75
|
+
const out = [];
|
|
76
|
+
let i = 0;
|
|
77
|
+
while (i < tokens.length) {
|
|
78
|
+
const first = i;
|
|
79
|
+
const attributes = [];
|
|
80
|
+
while (tokens[i]?.text === "@") {
|
|
81
|
+
const name2 = tokens[i + 1].text;
|
|
82
|
+
i += 2;
|
|
83
|
+
let args = "";
|
|
84
|
+
if (tokens[i]?.text === "(") {
|
|
85
|
+
const close = matching(tokens, i);
|
|
86
|
+
args = code.slice(tokens[i].end, tokens[close].start).trim();
|
|
87
|
+
i = close + 1;
|
|
88
|
+
}
|
|
89
|
+
attributes.push({ name: name2, args });
|
|
90
|
+
}
|
|
91
|
+
const head = tokens[i];
|
|
92
|
+
if (!head) break;
|
|
93
|
+
const kw = head.text;
|
|
94
|
+
const keyword = kw === "struct" || kw === "fn" || kw === "var" || kw === "const" || kw === "override" || kw === "alias" ? kw : "other";
|
|
95
|
+
i++;
|
|
96
|
+
let template = "";
|
|
97
|
+
if (keyword === "var" && tokens[i]?.text === "<") {
|
|
98
|
+
const close = matching(tokens, i);
|
|
99
|
+
template = code.slice(tokens[i].end, tokens[close].start).replace(/\s+/g, " ").trim();
|
|
100
|
+
i = close + 1;
|
|
101
|
+
}
|
|
102
|
+
const name = tokens[i]?.kind === "ident" ? tokens[i].text : "";
|
|
103
|
+
let type = "";
|
|
104
|
+
let last = i;
|
|
105
|
+
if (keyword === "struct" || keyword === "fn") {
|
|
106
|
+
while (last < tokens.length && tokens[last].text !== "{") last++;
|
|
107
|
+
last = matching(tokens, last);
|
|
108
|
+
if (tokens[last + 1]?.text === ";") last++;
|
|
109
|
+
} else {
|
|
110
|
+
let depth = 0;
|
|
111
|
+
let colon = -1;
|
|
112
|
+
while (last < tokens.length) {
|
|
113
|
+
const t = tokens[last].text;
|
|
114
|
+
if (t === "(" || t === "[" || t === "{") depth++;
|
|
115
|
+
else if (t === ")" || t === "]" || t === "}") depth--;
|
|
116
|
+
else if (t === ":" && depth === 0 && colon === -1) colon = last;
|
|
117
|
+
else if (t === ";" && depth === 0) break;
|
|
118
|
+
else if (t === "=" && depth === 0 && colon !== -1 && !type) {
|
|
119
|
+
type = code.slice(tokens[colon].end, tokens[last].start).trim();
|
|
120
|
+
}
|
|
121
|
+
last++;
|
|
122
|
+
}
|
|
123
|
+
if (colon !== -1 && !type) type = code.slice(tokens[colon].end, tokens[last].start).trim();
|
|
124
|
+
}
|
|
125
|
+
const end = tokens[Math.min(last, tokens.length - 1)].end;
|
|
126
|
+
out.push({
|
|
127
|
+
attributes,
|
|
128
|
+
keyword,
|
|
129
|
+
name,
|
|
130
|
+
template,
|
|
131
|
+
type,
|
|
132
|
+
start: tokens[first].start,
|
|
133
|
+
end,
|
|
134
|
+
first,
|
|
135
|
+
last: Math.min(last, tokens.length - 1)
|
|
136
|
+
});
|
|
137
|
+
i = last + 1;
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
function attribute(d, name) {
|
|
142
|
+
return d.attributes.find((a) => a.name === name)?.args;
|
|
143
|
+
}
|
|
144
|
+
const SHORT = { f: "f32", u: "u32", i: "i32", h: "f16" };
|
|
145
|
+
function parseType(text) {
|
|
146
|
+
const t = text.replace(/\s+/g, "");
|
|
147
|
+
if (t === "f32" || t === "u32" || t === "i32" || t === "f16" || t === "bool") {
|
|
148
|
+
return { kind: "scalar", name: t };
|
|
149
|
+
}
|
|
150
|
+
let m = /^vec([234])(?:([fuih])|<(\w+)>)$/.exec(t);
|
|
151
|
+
if (m) {
|
|
152
|
+
const element = parseType(m[2] ? SHORT[m[2]] : m[3]);
|
|
153
|
+
return { kind: "vector", size: Number(m[1]), element };
|
|
154
|
+
}
|
|
155
|
+
m = /^mat([234])x([234])(?:([fh])|<(\w+)>)$/.exec(t);
|
|
156
|
+
if (m) {
|
|
157
|
+
const element = parseType(m[3] ? SHORT[m[3]] : m[4]);
|
|
158
|
+
return {
|
|
159
|
+
kind: "matrix",
|
|
160
|
+
columns: Number(m[1]),
|
|
161
|
+
rows: Number(m[2]),
|
|
162
|
+
element
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (t.startsWith("array<") && t.endsWith(">")) {
|
|
166
|
+
const inner = t.slice(6, -1);
|
|
167
|
+
let depth = 0;
|
|
168
|
+
let split = -1;
|
|
169
|
+
for (let i = 0; i < inner.length; i++) {
|
|
170
|
+
const c = inner[i];
|
|
171
|
+
if (c === "<") depth++;
|
|
172
|
+
else if (c === ">") depth--;
|
|
173
|
+
else if (c === "," && depth === 0) split = i;
|
|
174
|
+
}
|
|
175
|
+
const element = parseType(split === -1 ? inner : inner.slice(0, split));
|
|
176
|
+
const countText = split === -1 ? void 0 : inner.slice(split + 1).replace(/[ui]$/, "");
|
|
177
|
+
const count = countText === void 0 ? void 0 : Number(countText);
|
|
178
|
+
return {
|
|
179
|
+
kind: "array",
|
|
180
|
+
element,
|
|
181
|
+
elementText: split === -1 ? inner : inner.slice(0, split),
|
|
182
|
+
count: Number.isFinite(count) ? count : void 0,
|
|
183
|
+
text
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (t.startsWith("atomic<") && t.endsWith(">")) {
|
|
187
|
+
return { kind: "atomic", element: parseType(t.slice(7, -1)) };
|
|
188
|
+
}
|
|
189
|
+
if (/^\w+$/.test(t)) return { kind: "struct", name: t };
|
|
190
|
+
return { kind: "other", text };
|
|
191
|
+
}
|
|
192
|
+
const roundUp = (n, a) => Math.ceil(n / a) * a;
|
|
193
|
+
class Layouts {
|
|
194
|
+
structs = /* @__PURE__ */ new Map();
|
|
195
|
+
cache = /* @__PURE__ */ new Map();
|
|
196
|
+
constructor(code, tokens, decls) {
|
|
197
|
+
for (const d of decls) {
|
|
198
|
+
if (d.keyword !== "struct") continue;
|
|
199
|
+
let i = d.first;
|
|
200
|
+
while (tokens[i].text !== "{") i++;
|
|
201
|
+
const close = matching(tokens, i);
|
|
202
|
+
const members = [];
|
|
203
|
+
i++;
|
|
204
|
+
while (i < close) {
|
|
205
|
+
let align;
|
|
206
|
+
let size;
|
|
207
|
+
while (tokens[i].text === "@") {
|
|
208
|
+
const attr = tokens[i + 1].text;
|
|
209
|
+
i += 2;
|
|
210
|
+
if (tokens[i].text === "(") {
|
|
211
|
+
const end = matching(tokens, i);
|
|
212
|
+
const value = Number(
|
|
213
|
+
code.slice(tokens[i].end, tokens[end].start).trim().replace(/[ui]$/, "")
|
|
214
|
+
);
|
|
215
|
+
if (attr === "align") align = value;
|
|
216
|
+
if (attr === "size") size = value;
|
|
217
|
+
i = end + 1;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const name = tokens[i].text;
|
|
221
|
+
i += 2;
|
|
222
|
+
let depth = 0;
|
|
223
|
+
const from = i;
|
|
224
|
+
while (i < close) {
|
|
225
|
+
const t = tokens[i].text;
|
|
226
|
+
if (t === "<" || t === "(") depth++;
|
|
227
|
+
else if (t === ">" || t === ")") depth--;
|
|
228
|
+
else if (t === "," && depth === 0) break;
|
|
229
|
+
i++;
|
|
230
|
+
}
|
|
231
|
+
members.push({
|
|
232
|
+
name,
|
|
233
|
+
typeText: code.slice(tokens[from].start, tokens[i - 1].end),
|
|
234
|
+
align,
|
|
235
|
+
size
|
|
236
|
+
});
|
|
237
|
+
if (tokens[i]?.text === ",") i++;
|
|
238
|
+
}
|
|
239
|
+
this.structs.set(d.name, { name: d.name, members });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
has(name) {
|
|
243
|
+
return this.structs.has(name);
|
|
244
|
+
}
|
|
245
|
+
struct(name, space) {
|
|
246
|
+
const key = `${space}:${name}`;
|
|
247
|
+
const cached = this.cache.get(key);
|
|
248
|
+
if (cached) return cached;
|
|
249
|
+
const s = this.structs.get(name);
|
|
250
|
+
if (!s) throw new Error(`No struct ${name}`);
|
|
251
|
+
let offset = 0;
|
|
252
|
+
let align = 1;
|
|
253
|
+
const members = [];
|
|
254
|
+
for (const m of s.members) {
|
|
255
|
+
const type = parseType(m.typeText);
|
|
256
|
+
const a = m.align ?? this.align(type, space);
|
|
257
|
+
const size = m.size ?? this.size(type, space);
|
|
258
|
+
offset = roundUp(offset, a);
|
|
259
|
+
members.push({ name: m.name, type, typeText: m.typeText, offset, size, align: a });
|
|
260
|
+
offset += size;
|
|
261
|
+
align = Math.max(align, a);
|
|
262
|
+
}
|
|
263
|
+
if (space === "uniform") align = roundUp(align, 16);
|
|
264
|
+
const layout = { name, members, size: roundUp(offset, align), align };
|
|
265
|
+
this.cache.set(key, layout);
|
|
266
|
+
return layout;
|
|
267
|
+
}
|
|
268
|
+
align(t, space) {
|
|
269
|
+
switch (t.kind) {
|
|
270
|
+
case "scalar":
|
|
271
|
+
return t.name === "f16" ? 2 : 4;
|
|
272
|
+
case "atomic":
|
|
273
|
+
return 4;
|
|
274
|
+
case "vector": {
|
|
275
|
+
const e = t.element.name === "f16" ? 2 : 4;
|
|
276
|
+
return t.size === 2 ? 2 * e : 4 * e;
|
|
277
|
+
}
|
|
278
|
+
case "matrix":
|
|
279
|
+
return this.align({ kind: "vector", size: t.rows, element: t.element }, space);
|
|
280
|
+
case "array": {
|
|
281
|
+
const a = this.align(t.element, space);
|
|
282
|
+
return space === "uniform" ? roundUp(a, 16) : a;
|
|
283
|
+
}
|
|
284
|
+
case "struct":
|
|
285
|
+
return this.struct(t.name, space).align;
|
|
286
|
+
default:
|
|
287
|
+
throw new Error(`No layout for ${t.text}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
size(t, space) {
|
|
291
|
+
switch (t.kind) {
|
|
292
|
+
case "scalar":
|
|
293
|
+
return t.name === "f16" ? 2 : 4;
|
|
294
|
+
case "atomic":
|
|
295
|
+
return 4;
|
|
296
|
+
case "vector":
|
|
297
|
+
return t.size * (t.element.name === "f16" ? 2 : 4);
|
|
298
|
+
case "matrix": {
|
|
299
|
+
const column = { kind: "vector", size: t.rows, element: t.element };
|
|
300
|
+
return t.columns * roundUp(this.size(column, space), this.align(column, space));
|
|
301
|
+
}
|
|
302
|
+
case "array":
|
|
303
|
+
if (t.count === void 0) return this.stride(t, space);
|
|
304
|
+
return t.count * this.stride(t, space);
|
|
305
|
+
case "struct":
|
|
306
|
+
return this.struct(t.name, space).size;
|
|
307
|
+
default:
|
|
308
|
+
throw new Error(`No layout for ${t.text}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/** Bytes between an array's elements. */
|
|
312
|
+
stride(t, space) {
|
|
313
|
+
const e = t.element;
|
|
314
|
+
const stride = roundUp(this.size(e, space), this.align(e, space));
|
|
315
|
+
return space === "uniform" ? roundUp(stride, 16) : stride;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Whether a type lies out the same in uniform and storage memory: every member and element at
|
|
319
|
+
* the same offset, so the same bytes read the same through either.
|
|
320
|
+
*/
|
|
321
|
+
sameInUniform(t) {
|
|
322
|
+
try {
|
|
323
|
+
return this.sameLayout(t);
|
|
324
|
+
} catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
sameLayout(t) {
|
|
329
|
+
switch (t.kind) {
|
|
330
|
+
case "scalar":
|
|
331
|
+
case "vector":
|
|
332
|
+
case "matrix":
|
|
333
|
+
return true;
|
|
334
|
+
case "array":
|
|
335
|
+
return t.count !== void 0 && this.stride(t, "storage") === this.stride(t, "uniform") && this.sameLayout(t.element);
|
|
336
|
+
case "struct": {
|
|
337
|
+
const s = this.struct(t.name, "storage");
|
|
338
|
+
const u = this.struct(t.name, "uniform");
|
|
339
|
+
return s.members.every(
|
|
340
|
+
(m, i) => m.offset === u.members[i].offset && this.sameLayout(m.type)
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
default:
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
export {
|
|
349
|
+
Layouts,
|
|
350
|
+
attribute,
|
|
351
|
+
declarations,
|
|
352
|
+
matching,
|
|
353
|
+
parseType,
|
|
354
|
+
tokenize
|
|
355
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Bumps when the baseline rewrite's output changes: baked baseline code from before goes stale. */
|
|
2
|
+
export declare const BASELINE_REWRITE_VERSION = 1;
|
|
3
|
+
/**
|
|
4
|
+
* Width of a data texture in texels (0064): element bytes at byte offset b sit in texel b / 16, a
|
|
5
|
+
* row holding this many. The rewrite's loaders and the engine's data textures agree on it.
|
|
6
|
+
*/
|
|
7
|
+
export declare const DATA_TEXTURE_WIDTH = 1024;
|
|
8
|
+
/** A storage declaration marked `@data`, as collected before linking. */
|
|
9
|
+
export interface DataDeclaration {
|
|
10
|
+
/** The module that declared it, for errors. */
|
|
11
|
+
module: string;
|
|
12
|
+
name: string;
|
|
13
|
+
group: number;
|
|
14
|
+
binding: number;
|
|
15
|
+
kind: 'texture' | 'uniform';
|
|
16
|
+
/** `@data(uniform, N)`: the uniform array length of a runtime-sized array. */
|
|
17
|
+
count?: number;
|
|
18
|
+
}
|
|
19
|
+
/** The source without its `@data` marks, and the declarations they marked. */
|
|
20
|
+
export declare function collectData(source: string, module: string): {
|
|
21
|
+
code: string;
|
|
22
|
+
data: DataDeclaration[];
|
|
23
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ShardError } from "@aethervtt/shard-core";
|
|
2
|
+
const BASELINE_REWRITE_VERSION = 1;
|
|
3
|
+
const DATA_TEXTURE_WIDTH = 1024;
|
|
4
|
+
const MARK = /@data(?:\((texture|uniform)(?:\s*,\s*(\d+))?\))?\s/g;
|
|
5
|
+
const DECLARATION = /^((?:@\w+\s*(?:\([^)]*\))?\s*)*)var\s*<\s*storage[^>]*>\s*([A-Za-z_]\w*)/;
|
|
6
|
+
function collectData(source, module) {
|
|
7
|
+
if (!source.includes("@data")) return { code: source, data: [] };
|
|
8
|
+
const data = [];
|
|
9
|
+
let code = "";
|
|
10
|
+
let at = 0;
|
|
11
|
+
for (const m of source.matchAll(MARK)) {
|
|
12
|
+
const after = source.slice(m.index + m[0].length);
|
|
13
|
+
const d = DECLARATION.exec(after);
|
|
14
|
+
const group = d ? /@group\s*\(\s*(\d+)/.exec(d[1]) : null;
|
|
15
|
+
const binding = d ? /@binding\s*\(\s*(\d+)/.exec(d[1]) : null;
|
|
16
|
+
if (!d || !group || !binding) {
|
|
17
|
+
throw new ShardError(
|
|
18
|
+
"shader/data-misplaced",
|
|
19
|
+
`@data in ${module} must mark a storage declaration with @group and @binding`,
|
|
20
|
+
{ hint: "@data @group(2) @binding(0) var<storage, read> name: array<T>;" }
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
data.push({
|
|
24
|
+
module,
|
|
25
|
+
name: d[2],
|
|
26
|
+
group: Number(group[1]),
|
|
27
|
+
binding: Number(binding[1]),
|
|
28
|
+
kind: m[1] === "uniform" ? "uniform" : "texture",
|
|
29
|
+
...m[2] ? { count: Number(m[2]) } : {}
|
|
30
|
+
});
|
|
31
|
+
code += source.slice(at, m.index);
|
|
32
|
+
at = m.index + m[0].length;
|
|
33
|
+
}
|
|
34
|
+
return { code: code + source.slice(at), data };
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
BASELINE_REWRITE_VERSION,
|
|
38
|
+
DATA_TEXTURE_WIDTH,
|
|
39
|
+
collectData
|
|
40
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { DATA_TEXTURE_WIDTH } from './data-marks.js';
|
|
1
2
|
export { applyHooks, findHooks, type HookSignature } from './hooks.js';
|
|
2
3
|
export { type WgslLayout, wgslLayout } from './layout.js';
|
|
3
4
|
export { type BakedShader, type LinkedShader, type LinkRequest, type ShaderBake, ShaderLibrary, type SourceLocation, } from './library.js';
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { DATA_TEXTURE_WIDTH } from "./data-marks.js";
|
|
1
2
|
import { applyHooks, findHooks } from "./hooks.js";
|
|
2
3
|
import { wgslLayout } from "./layout.js";
|
|
3
4
|
import {
|
|
4
5
|
ShaderLibrary
|
|
5
6
|
} from "./library.js";
|
|
6
7
|
export {
|
|
8
|
+
DATA_TEXTURE_WIDTH,
|
|
7
9
|
ShaderLibrary,
|
|
8
10
|
applyHooks,
|
|
9
11
|
findHooks,
|
package/dist/layout.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ interface FieldLayout {
|
|
|
6
6
|
offset: number;
|
|
7
7
|
size: number;
|
|
8
8
|
align: number;
|
|
9
|
+
/** Struct fields: the nested struct's members. */
|
|
10
|
+
members?: readonly FieldLayout[];
|
|
9
11
|
}
|
|
10
12
|
export interface WgslLayout<F extends Fields> {
|
|
11
13
|
/** WGSL struct declaration, e.g. `struct ToonParams { bands: u32, rim: vec4f, width: f32, }`. */
|
|
@@ -15,6 +17,11 @@ export interface WgslLayout<F extends Fields> {
|
|
|
15
17
|
readonly size: number;
|
|
16
18
|
readonly align: number;
|
|
17
19
|
readonly fields: readonly FieldLayout[];
|
|
20
|
+
/**
|
|
21
|
+
* The nested structs it uses (struct fields with a `wgsl` option), as `module::path::Name`: a
|
|
22
|
+
* module declaring this struct imports them.
|
|
23
|
+
*/
|
|
24
|
+
readonly imports: readonly string[];
|
|
18
25
|
/** Writes a value at a byte offset using WGSL's layout rules. */
|
|
19
26
|
write(view: DataView, offset: number, value: InferFields<F>): void;
|
|
20
27
|
}
|
package/dist/layout.js
CHANGED
|
@@ -21,82 +21,118 @@ const TYPES = {
|
|
|
21
21
|
affine3x4: { type: "array<vec4f, 3>", size: 48, align: 16 }
|
|
22
22
|
};
|
|
23
23
|
const alignTo = (n, a) => Math.ceil(n / a) * a;
|
|
24
|
-
function
|
|
25
|
-
const structName = def.name.split("/").pop();
|
|
24
|
+
function pack(owner, entries, imports) {
|
|
26
25
|
const fields = [];
|
|
27
26
|
let offset = 0;
|
|
28
27
|
let structAlign = 4;
|
|
29
|
-
for (const
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
28
|
+
for (const [name, field] of entries) {
|
|
29
|
+
if (field.options.gpu === false) continue;
|
|
30
|
+
let layout;
|
|
31
|
+
if (field.kind === "struct" && field.options.wgsl) {
|
|
32
|
+
const inner = pack(`${owner}.${name}`, Object.entries(field.fields ?? {}), imports);
|
|
33
|
+
const path = field.options.wgsl;
|
|
34
|
+
imports.add(path);
|
|
35
|
+
const align = Math.max(16, inner.align);
|
|
36
|
+
offset = alignTo(offset, align);
|
|
37
|
+
layout = {
|
|
38
|
+
name,
|
|
39
|
+
kind: "struct",
|
|
40
|
+
wgslType: path.slice(path.lastIndexOf("::") + 2),
|
|
41
|
+
offset,
|
|
42
|
+
size: alignTo(inner.size, align),
|
|
43
|
+
align,
|
|
44
|
+
members: inner.fields
|
|
45
|
+
};
|
|
46
|
+
} else {
|
|
47
|
+
if (field.storage === "object") continue;
|
|
48
|
+
const t = TYPES[field.kind];
|
|
49
|
+
if (!t) {
|
|
50
|
+
throw new ShardError(
|
|
51
|
+
"shader/unsupported-field",
|
|
52
|
+
`Field "${name}" of ${owner} (${field.kind}) can't be a GPU struct member`,
|
|
53
|
+
{
|
|
54
|
+
hint: "GPU structs take numbers, bools, enums, vectors, colors, matrices, and structs with a `wgsl` option. f64 fields are not allowed; other object fields are skipped."
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
offset = alignTo(offset, t.align);
|
|
59
|
+
layout = { name, kind: field.kind, wgslType: t.type, offset, size: t.size, align: t.align };
|
|
40
60
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
structAlign = Math.max(structAlign, t.align);
|
|
61
|
+
fields.push(layout);
|
|
62
|
+
offset += layout.size;
|
|
63
|
+
structAlign = Math.max(structAlign, layout.align);
|
|
45
64
|
}
|
|
46
|
-
|
|
65
|
+
return { fields, size: alignTo(Math.max(offset, 4), structAlign), align: structAlign, imports };
|
|
66
|
+
}
|
|
67
|
+
function enumsOf(entries, out) {
|
|
68
|
+
for (const [, field] of entries) {
|
|
69
|
+
if (field.kind === "enum") out.set(field, field.jsonSchema().enum);
|
|
70
|
+
else if (field.kind === "struct" && field.fields) enumsOf(Object.entries(field.fields), out);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
function wgslLayout(def) {
|
|
75
|
+
const structName = def.name.split("/").pop();
|
|
76
|
+
const entries = def.layout.map(({ name, field }) => [name, field]);
|
|
77
|
+
const packed = pack(def.name, entries, /* @__PURE__ */ new Set());
|
|
47
78
|
const wgsl = `struct ${structName} {
|
|
48
|
-
${fields.map((f) => ` ${f.name}: ${f.wgslType},`).join("\n")}
|
|
79
|
+
${packed.fields.map((f) => ` ${f.name}: ${f.wgslType},`).join("\n")}
|
|
49
80
|
}`;
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
81
|
+
const schema = Object.fromEntries(entries);
|
|
82
|
+
const enumValues = enumsOf(entries, /* @__PURE__ */ new Map());
|
|
83
|
+
const writeFields = (view, base, layouts, record, schema2) => {
|
|
84
|
+
for (const f of layouts) {
|
|
85
|
+
const o = base + f.offset;
|
|
86
|
+
const v = record[f.name];
|
|
87
|
+
switch (f.kind) {
|
|
88
|
+
case "struct": {
|
|
89
|
+
writeFields(view, o, f.members, v, schema2[f.name].fields);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case "f32":
|
|
93
|
+
view.setFloat32(o, v, true);
|
|
94
|
+
break;
|
|
95
|
+
case "i8":
|
|
96
|
+
case "i16":
|
|
97
|
+
case "i32":
|
|
98
|
+
view.setInt32(o, v, true);
|
|
99
|
+
break;
|
|
100
|
+
case "u8":
|
|
101
|
+
case "u16":
|
|
102
|
+
case "u32":
|
|
103
|
+
view.setUint32(o, v, true);
|
|
104
|
+
break;
|
|
105
|
+
case "bool":
|
|
106
|
+
view.setUint32(o, v ? 1 : 0, true);
|
|
107
|
+
break;
|
|
108
|
+
case "enum": {
|
|
109
|
+
const values = enumValues.get(schema2[f.name]);
|
|
110
|
+
view.setUint32(o, Math.max(0, values.indexOf(v)), true);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case "mat3": {
|
|
114
|
+
const m = v;
|
|
115
|
+
for (let c = 0; c < 3; c++) {
|
|
116
|
+
for (let r = 0; r < 3; r++) view.setFloat32(o + c * 16 + r * 4, m[c * 3 + r], true);
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
default: {
|
|
121
|
+
const arr = v;
|
|
122
|
+
for (let i = 0; i < arr.length; i++) view.setFloat32(o + i * 4, arr[i], true);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
54
125
|
}
|
|
55
|
-
}
|
|
126
|
+
};
|
|
56
127
|
return {
|
|
57
128
|
wgsl,
|
|
58
129
|
structName,
|
|
59
|
-
size,
|
|
60
|
-
align:
|
|
61
|
-
fields,
|
|
130
|
+
size: packed.size,
|
|
131
|
+
align: packed.align,
|
|
132
|
+
fields: packed.fields,
|
|
133
|
+
imports: [...packed.imports],
|
|
62
134
|
write(view, base, value) {
|
|
63
|
-
|
|
64
|
-
for (const f of fields) {
|
|
65
|
-
const o = base + f.offset;
|
|
66
|
-
const v = record[f.name];
|
|
67
|
-
switch (f.kind) {
|
|
68
|
-
case "f32":
|
|
69
|
-
view.setFloat32(o, v, true);
|
|
70
|
-
break;
|
|
71
|
-
case "i8":
|
|
72
|
-
case "i16":
|
|
73
|
-
case "i32":
|
|
74
|
-
view.setInt32(o, v, true);
|
|
75
|
-
break;
|
|
76
|
-
case "u8":
|
|
77
|
-
case "u16":
|
|
78
|
-
case "u32":
|
|
79
|
-
view.setUint32(o, v, true);
|
|
80
|
-
break;
|
|
81
|
-
case "bool":
|
|
82
|
-
view.setUint32(o, v ? 1 : 0, true);
|
|
83
|
-
break;
|
|
84
|
-
case "enum":
|
|
85
|
-
view.setUint32(o, Math.max(0, enumValues.get(f.name).indexOf(v)), true);
|
|
86
|
-
break;
|
|
87
|
-
case "mat3": {
|
|
88
|
-
const m = v;
|
|
89
|
-
for (let c = 0; c < 3; c++) {
|
|
90
|
-
for (let r = 0; r < 3; r++) view.setFloat32(o + c * 16 + r * 4, m[c * 3 + r], true);
|
|
91
|
-
}
|
|
92
|
-
break;
|
|
93
|
-
}
|
|
94
|
-
default: {
|
|
95
|
-
const arr = v;
|
|
96
|
-
for (let i = 0; i < arr.length; i++) view.setFloat32(o + i * 4, arr[i], true);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
135
|
+
writeFields(view, base, packed.fields, value, schema);
|
|
100
136
|
}
|
|
101
137
|
};
|
|
102
138
|
}
|