@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/library.d.ts CHANGED
@@ -1,5 +1,7 @@
1
+ import { ShardError } from '@aethervtt/shard-core';
1
2
  import type { GpuContext } from '@aethervtt/shard-gpu';
2
3
  import type { Platform } from '@aethervtt/shard-platform';
4
+ import type { RetargetedBinding } from './baseline/rewrite.js';
3
5
  export interface LinkRequest {
4
6
  /** Module with the entry points, e.g. `shard::pbr::main` or `project::water`. */
5
7
  root: string;
@@ -20,6 +22,8 @@ export interface LinkedShader {
20
22
  code: string;
21
23
  /** Maps a character offset in `code` back to the module that produced it. */
22
24
  locate(offset: number): SourceLocation | undefined;
25
+ /** Baseline variants (0064): the bindings the rewrite retargeted, for the engine's layouts. */
26
+ bindings?: readonly RetargetedBinding[];
23
27
  }
24
28
  /** One linked variant, for `ShaderLibrary.preload`. */
25
29
  export interface BakedShader {
@@ -39,13 +43,18 @@ export interface ShaderBake {
39
43
  * under `shard::`, plugins under their own package, a project's `shaders/` folder under `project::`.
40
44
  */
41
45
  export declare class ShaderLibrary {
46
+ /** Module sources as they link: `@data` marks stripped (0064). */
42
47
  private readonly sources;
48
+ /** Each module's `@data` declarations, for baseline variants. */
49
+ private readonly data;
43
50
  /** Where each module came from, for error messages (a file path, or the module path). */
44
51
  private readonly origins;
45
52
  private readonly linkCache;
46
53
  private readonly variants;
47
54
  private readonly listeners;
48
55
  private version;
56
+ /** Bumps whenever a module is registered or changes: what renderers compare to retry failures. */
57
+ get revision(): number;
49
58
  private readonly preloaded;
50
59
  /** Every variant linked or served from the bake, for `bake()`. */
51
60
  private readonly used;
@@ -92,6 +101,12 @@ export declare class ShaderLibrary {
92
101
  */
93
102
  module(gpu: GpuContext, request: LinkRequest): GPUShaderModule | undefined;
94
103
  private rebuild;
104
+ /**
105
+ * Why a variant has no module to give: its code failed to link or compile, and no earlier version
106
+ * compiled either. Undefined while it compiles, once it has a module, and for unknown variants.
107
+ * Renderers draw with a fallback then instead of skipping the draw forever (0061).
108
+ */
109
+ failure(request: LinkRequest, gpu?: GpuContext): ShardError | undefined;
95
110
  /** Waits until every variant requested through `module()` has finished compiling. */
96
111
  whenIdle(): Promise<void>;
97
112
  /** Module graph and hooks, for agents. */
@@ -105,7 +120,12 @@ export declare class ShaderLibrary {
105
120
  };
106
121
  defines: string[];
107
122
  };
108
- /** Of the source of every module a variant links: its root's imports, and the overrides'. */
123
+ /** Every module a variant links: its root's imports, and the overrides'. */
124
+ private closureOf;
125
+ /**
126
+ * Of the source of every module a variant links. A baseline variant's also covers its `@data`
127
+ * declarations and the rewrite's version, which change its code and not its modules' text.
128
+ */
109
129
  private hashOf;
110
130
  private doLink;
111
131
  }
package/dist/library.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ShardError } from "@aethervtt/shard-core";
2
+ import { BASELINE_REWRITE_VERSION, collectData } from "./data-marks.js";
2
3
  import { applyHooks, findHooks } from "./hooks.js";
3
4
  let wesl;
4
5
  const loadWesl = () => {
@@ -22,13 +23,20 @@ function lineColumn(text, offset) {
22
23
  return { line, column: offset - lineStart + 1 };
23
24
  }
24
25
  class ShaderLibrary {
26
+ /** Module sources as they link: `@data` marks stripped (0064). */
25
27
  sources = /* @__PURE__ */ new Map();
28
+ /** Each module's `@data` declarations, for baseline variants. */
29
+ data = /* @__PURE__ */ new Map();
26
30
  /** Where each module came from, for error messages (a file path, or the module path). */
27
31
  origins = /* @__PURE__ */ new Map();
28
32
  linkCache = /* @__PURE__ */ new Map();
29
33
  variants = /* @__PURE__ */ new Map();
30
34
  listeners = /* @__PURE__ */ new Set();
31
35
  version = 0;
36
+ /** Bumps whenever a module is registered or changes: what renderers compare to retry failures. */
37
+ get revision() {
38
+ return this.version;
39
+ }
32
40
  preloaded = /* @__PURE__ */ new Map();
33
41
  /** Every variant linked or served from the bake, for `bake()`. */
34
42
  used = /* @__PURE__ */ new Map();
@@ -62,8 +70,11 @@ class ShaderLibrary {
62
70
  hint: "Use lowercase `package::dir::name`, e.g. `project::water`."
63
71
  });
64
72
  }
65
- if (this.sources.get(path) === source) return;
66
- this.sources.set(path, source);
73
+ const { code, data } = collectData(source, path);
74
+ if (this.sources.get(path) === code && sameData(this.data.get(path), data)) return;
75
+ this.sources.set(path, code);
76
+ if (data.length > 0) this.data.set(path, data);
77
+ else this.data.delete(path);
67
78
  this.origins.set(path, origin ?? path);
68
79
  this.version++;
69
80
  this.linkCache.clear();
@@ -156,10 +167,13 @@ class ShaderLibrary {
156
167
  * the error is reported through the GPU context and the old module stays in use.
157
168
  */
158
169
  module(gpu, request) {
170
+ if (gpu.tier === "baseline" && !request.defines?.BASELINE) {
171
+ request = { ...request, defines: { ...request.defines, BASELINE: true } };
172
+ }
159
173
  const key = variantKey(request);
160
174
  let state = this.variants.get(key);
161
175
  if (!state) {
162
- state = { good: void 0, pending: void 0, version: -1, gpu, request };
176
+ state = { good: void 0, pending: void 0, version: -1, gpu, request, failed: void 0 };
163
177
  this.variants.set(key, state);
164
178
  }
165
179
  const s = state;
@@ -178,28 +192,43 @@ class ShaderLibrary {
178
192
  if (s.good?.code !== linked.code) {
179
193
  s.pending = linked.code;
180
194
  const error = await compile(gpu, linked);
181
- if (error) gpu.reportError(labelled(error, request.label));
182
- else
195
+ if (error) {
196
+ s.failed = labelled(error, request.label);
197
+ gpu.reportError(s.failed);
198
+ } else {
199
+ s.failed = void 0;
183
200
  s.good = {
184
201
  code: linked.code,
185
202
  module: moduleFor(gpu, linked),
186
203
  linked,
187
204
  generation: gpu.generation
188
205
  };
189
- }
206
+ }
207
+ } else s.failed = void 0;
190
208
  s.pending = void 0;
191
209
  },
192
210
  (err) => {
193
- gpu.reportError(
194
- labelled(
195
- err instanceof ShardError ? err : new ShardError("shader/link", String(err)),
196
- request.label
197
- )
211
+ s.failed = labelled(
212
+ err instanceof ShardError ? err : new ShardError("shader/link", String(err)),
213
+ request.label
198
214
  );
215
+ gpu.reportError(s.failed);
199
216
  s.pending = void 0;
200
217
  }
201
218
  );
202
219
  }
220
+ /**
221
+ * Why a variant has no module to give: its code failed to link or compile, and no earlier version
222
+ * compiled either. Undefined while it compiles, once it has a module, and for unknown variants.
223
+ * Renderers draw with a fallback then instead of skipping the draw forever (0061).
224
+ */
225
+ failure(request, gpu) {
226
+ if (gpu?.tier === "baseline" && !request.defines?.BASELINE) {
227
+ request = { ...request, defines: { ...request.defines, BASELINE: true } };
228
+ }
229
+ const s = this.variants.get(variantKey(request));
230
+ return s && !s.good && s.pending === void 0 ? s.failed : void 0;
231
+ }
203
232
  /** Waits until every variant requested through `module()` has finished compiling. */
204
233
  async whenIdle() {
205
234
  for (let i = 0; i < 200; i++) {
@@ -221,18 +250,28 @@ class ShaderLibrary {
221
250
  defines: [...new Set(reachable.flatMap((m) => definesOf(this.sources.get(m) ?? "")))].sort()
222
251
  };
223
252
  }
224
- /** Of the source of every module a variant links: its root's imports, and the overrides'. */
225
- hashOf(request) {
226
- const imports = (p) => importsOf(this.sources.get(p) ?? "");
253
+ /** Every module a variant links: its root's imports, and the overrides'. */
254
+ closureOf(request) {
255
+ const imports = (p) => importsOf(this.sources.get(p) ?? "", request.defines ?? {});
227
256
  const paths = /* @__PURE__ */ new Set();
228
257
  for (const root of [request.root, ...request.overrides ?? []])
229
258
  for (const p of closure(root, imports)) paths.add(p);
259
+ return [...paths].sort();
260
+ }
261
+ /**
262
+ * Of the source of every module a variant links. A baseline variant's also covers its `@data`
263
+ * declarations and the rewrite's version, which change its code and not its modules' text.
264
+ */
265
+ hashOf(request) {
266
+ const paths = this.closureOf(request);
230
267
  let a = 2166136261;
231
268
  let b = 2538058380;
232
- for (const path of [...paths].sort()) {
233
- const text = `${path}
269
+ const baseline = request.defines?.BASELINE ? `baseline ${BASELINE_REWRITE_VERSION} ${JSON.stringify(paths.map((p) => this.data.get(p) ?? []))}
270
+ ` : "";
271
+ for (const path of [...paths, ...baseline ? [""] : []]) {
272
+ const text = path ? `${path}
234
273
  ${this.sources.get(path) ?? ""}
235
- `;
274
+ ` : baseline;
236
275
  for (let i = 0; i < text.length; i++) {
237
276
  const c = text.charCodeAt(i);
238
277
  a = Math.imul(a ^ c, 16777619);
@@ -254,7 +293,11 @@ ${this.sources.get(path) ?? ""}
254
293
  );
255
294
  }
256
295
  }
257
- for (const path of closure(request.root, (p) => importsOf(this.sources.get(p) ?? ""))) {
296
+ const defines = request.defines ?? {};
297
+ for (const path of closure(
298
+ request.root,
299
+ (p) => importsOf(this.sources.get(p) ?? "", defines)
300
+ )) {
258
301
  if (!this.sources.has(path)) {
259
302
  const importer = [...this.sources].find(([, src]) => importsOf(src).includes(path))?.[0];
260
303
  throw new ShardError("shader/link-unknown-module", `Unknown shader module "${path}"`, {
@@ -302,27 +345,39 @@ ${this.sources.get(path) ?? ""}
302
345
  this.origins.get(request.root)
303
346
  );
304
347
  }
305
- const code = linked.dest;
306
- this.used.set(key, { key, hash, code });
307
348
  const origins = this.origins;
349
+ const locate = (offset) => {
350
+ try {
351
+ const pos = linked.sourceMap.destToSrc(offset);
352
+ const srcPath = pos.src.path ?? "";
353
+ const match = /^\.\/([a-z_][a-z0-9_]*)\/(.*)$/.exec(srcPath);
354
+ const module = fileToModule.get(`${root.pkg}|${srcPath}`) ?? (match ? fileToModule.get(`${match[1]}|./${match[2]}`) : void 0) ?? srcPath;
355
+ const { line, column } = lineColumn(pos.src.text, pos.position);
356
+ return { module: origins.get(module) ?? module, line, column };
357
+ } catch {
358
+ return void 0;
359
+ }
360
+ };
361
+ if (!request.defines?.BASELINE) {
362
+ const code = linked.dest;
363
+ this.used.set(key, { key, hash, code });
364
+ return { key, code, locate };
365
+ }
366
+ const { rewriteForBaseline } = await import("./baseline/rewrite.js");
367
+ const data = this.closureOf(request).flatMap((p) => this.data.get(p) ?? []);
368
+ const rewritten = rewriteForBaseline(linked.dest, data, locate, request.label);
369
+ this.used.set(key, { key, hash, code: rewritten.code });
308
370
  return {
309
371
  key,
310
- code,
311
- locate(offset) {
312
- try {
313
- const pos = linked.sourceMap.destToSrc(offset);
314
- const srcPath = pos.src.path ?? "";
315
- const match = /^\.\/([a-z_][a-z0-9_]*)\/(.*)$/.exec(srcPath);
316
- const module = fileToModule.get(`${root.pkg}|${srcPath}`) ?? (match ? fileToModule.get(`${match[1]}|./${match[2]}`) : void 0) ?? srcPath;
317
- const { line, column } = lineColumn(pos.src.text, pos.position);
318
- return { module: origins.get(module) ?? module, line, column };
319
- } catch {
320
- return void 0;
321
- }
322
- }
372
+ code: rewritten.code,
373
+ locate: (offset) => locate(rewritten.toLinked(offset)),
374
+ bindings: rewritten.bindings
323
375
  };
324
376
  }
325
377
  }
378
+ function sameData(a, b) {
379
+ return JSON.stringify(a ?? []) === JSON.stringify(b);
380
+ }
326
381
  const moduleCache = /* @__PURE__ */ new WeakMap();
327
382
  function moduleFor(gpu, linked) {
328
383
  let cache = moduleCache.get(gpu);
@@ -372,10 +427,13 @@ function variantKey(request) {
372
427
  const defines = Object.entries(request.defines ?? {}).filter(([, on]) => on).map(([name]) => name).sort();
373
428
  return `${request.root}|${defines.join(",")}|${(request.overrides ?? []).join(",")}`;
374
429
  }
375
- function importsOf(source) {
430
+ function importsOf(source, defines) {
376
431
  const out = [];
377
- for (const m of source.matchAll(/^\s*import\s+([^;]+);/gm)) {
378
- const spec = m[1].replace(/\s+/g, "");
432
+ for (const m of source.matchAll(
433
+ /^\s*((?:@\w+\s*\((?:[^()]|\([^()]*\))*\)\s*)*)import\s+([^;]+);/gm
434
+ )) {
435
+ if (defines && m[1] && !attributesHold(m[1], defines)) continue;
436
+ const spec = m[2].replace(/\s+/g, "");
379
437
  const braced = /^(.*?)::\{(.*)\}$/.exec(spec);
380
438
  if (braced) {
381
439
  for (const item of braced[2].split(",")) {
@@ -389,6 +447,44 @@ function importsOf(source) {
389
447
  }
390
448
  return [...new Set(out.filter((p) => p.includes("::") && !p.startsWith("constants")))];
391
449
  }
450
+ function attributesHold(attributes, defines) {
451
+ for (const m of attributes.matchAll(/@if\s*\(((?:[^()]|\([^()]*\))*)\)/g))
452
+ if (!evaluateCondition(m[1], defines)) return false;
453
+ return true;
454
+ }
455
+ function evaluateCondition(expression, defines) {
456
+ const tokens = expression.match(/[A-Za-z_]\w*|&&|\|\||[!()]/g) ?? [];
457
+ let i = 0;
458
+ const or = () => {
459
+ let v = and();
460
+ while (tokens[i] === "||") {
461
+ i++;
462
+ v = and() || v;
463
+ }
464
+ return v;
465
+ };
466
+ const and = () => {
467
+ let v = unary();
468
+ while (tokens[i] === "&&") {
469
+ i++;
470
+ v = unary() && v;
471
+ }
472
+ return v;
473
+ };
474
+ const unary = () => {
475
+ const t = tokens[i++];
476
+ if (t === "!") return !unary();
477
+ if (t === "(") {
478
+ const v = or();
479
+ i++;
480
+ return v;
481
+ }
482
+ if (t === "true") return true;
483
+ if (t === "false" || t === void 0) return false;
484
+ return defines[t] === true;
485
+ };
486
+ return or();
487
+ }
392
488
  function definesOf(source) {
393
489
  const out = [];
394
490
  for (const m of source.matchAll(/@(?:if|elif)\s*\(([^)]*)\)/g)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aethervtt/shard-shader",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -8,6 +8,10 @@
8
8
  ".": {
9
9
  "types": "./dist/index.d.ts",
10
10
  "default": "./dist/index.js"
11
+ },
12
+ "./fragment-kernel": {
13
+ "types": "./dist/baseline/fragment-kernel.d.ts",
14
+ "default": "./dist/baseline/fragment-kernel.js"
11
15
  }
12
16
  },
13
17
  "files": [
@@ -16,9 +20,9 @@
16
20
  "README.md"
17
21
  ],
18
22
  "dependencies": {
19
- "@aethervtt/shard-core": "0.0.1",
20
- "@aethervtt/shard-gpu": "0.0.1",
21
- "@aethervtt/shard-platform": "0.0.1",
23
+ "@aethervtt/shard-core": "0.1.0",
24
+ "@aethervtt/shard-gpu": "0.1.0",
25
+ "@aethervtt/shard-platform": "0.1.0",
22
26
  "wesl": "0.7.31"
23
27
  },
24
28
  "repository": {
@@ -31,6 +35,6 @@
31
35
  "provenance": true
32
36
  },
33
37
  "shard": {
34
- "buildSha": "d6c5c6aef4675575c878b02cf2c8f738b4190229"
38
+ "buildSha": "8e14c9896f82a3554b156f1ffdd68f2cc3e7d78a"
35
39
  }
36
40
  }