@aethervtt/shard-shader 0.0.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/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/hooks.d.ts +10 -0
- package/dist/hooks.js +132 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +11 -0
- package/dist/layout.d.ts +26 -0
- package/dist/layout.js +105 -0
- package/dist/library.d.ts +111 -0
- package/dist/library.js +413 -0
- package/package.json +36 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Patrick Dias
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface HookSignature {
|
|
2
|
+
params: string;
|
|
3
|
+
returns: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function findHooks(source: string): Map<string, HookSignature>;
|
|
6
|
+
/**
|
|
7
|
+
* Applies hooks and overrides to module sources (keyed by module path like `shard::pbr::material`).
|
|
8
|
+
* Returns new sources; inputs are not modified.
|
|
9
|
+
*/
|
|
10
|
+
export declare function applyHooks(sources: ReadonlyMap<string, string>, overrides: readonly string[]): Map<string, string>;
|
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { ShardError } from "@aethervtt/shard-core";
|
|
2
|
+
const HOOK = /@hook\s+fn\s+([A-Za-z_]\w*)\s*\(([^)]*)\)\s*(?:->\s*([^{]+?))?\s*\{/g;
|
|
3
|
+
const OVERRIDE = /override\s+fn\s+((?:[A-Za-z_]\w*::)*)([A-Za-z_]\w*)\s*\(([^)]*)\)\s*(?:->\s*([^{]+?))?\s*\{/g;
|
|
4
|
+
const normalize = (s) => (s ?? "").replace(/\s+/g, " ").replace(/\s*([,:<>()])\s*/g, "$1").trim();
|
|
5
|
+
function findHooks(source) {
|
|
6
|
+
const hooks = /* @__PURE__ */ new Map();
|
|
7
|
+
for (const m of source.matchAll(HOOK)) hooks.set(m[1], { params: m[2], returns: m[3] ?? "" });
|
|
8
|
+
return hooks;
|
|
9
|
+
}
|
|
10
|
+
function paramNames(params) {
|
|
11
|
+
const names = [];
|
|
12
|
+
let depth = 0;
|
|
13
|
+
let current = "";
|
|
14
|
+
for (const ch of params) {
|
|
15
|
+
if (ch === "<" || ch === "(") depth++;
|
|
16
|
+
if (ch === ">" || ch === ")") depth--;
|
|
17
|
+
if (ch === "," && depth === 0) {
|
|
18
|
+
names.push(current);
|
|
19
|
+
current = "";
|
|
20
|
+
} else current += ch;
|
|
21
|
+
}
|
|
22
|
+
if (current.trim()) names.push(current);
|
|
23
|
+
return names.map(
|
|
24
|
+
(p) => p.replace(/@\w+(\([^)]*\))?/g, "").split(":")[0].trim()
|
|
25
|
+
).filter(Boolean);
|
|
26
|
+
}
|
|
27
|
+
function insertAfterImports(source, lines) {
|
|
28
|
+
const importRe = /^\s*import\s[^;]*;/gm;
|
|
29
|
+
let end = 0;
|
|
30
|
+
for (const m of source.matchAll(importRe)) end = m.index + m[0].length;
|
|
31
|
+
return `${source.slice(0, end)}
|
|
32
|
+
${lines}
|
|
33
|
+
${source.slice(end)}`;
|
|
34
|
+
}
|
|
35
|
+
function applyHooks(sources, overrides) {
|
|
36
|
+
const out = new Map(sources);
|
|
37
|
+
const hooksByModule = /* @__PURE__ */ new Map();
|
|
38
|
+
for (const [path, source] of sources) {
|
|
39
|
+
const hooks = findHooks(source);
|
|
40
|
+
if (hooks.size > 0) hooksByModule.set(path, hooks);
|
|
41
|
+
}
|
|
42
|
+
const winners = /* @__PURE__ */ new Map();
|
|
43
|
+
overrides.forEach((overrideModule, index) => {
|
|
44
|
+
const source = out.get(overrideModule);
|
|
45
|
+
if (source === void 0) {
|
|
46
|
+
throw new ShardError(
|
|
47
|
+
"shader/link-unknown-module",
|
|
48
|
+
`Override module "${overrideModule}" is not registered`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
let rewritten = source;
|
|
52
|
+
for (const m of source.matchAll(OVERRIDE)) {
|
|
53
|
+
const name = m[2];
|
|
54
|
+
let target = m[1].slice(0, -2);
|
|
55
|
+
if (!target) {
|
|
56
|
+
const owners = [...hooksByModule].filter(([, hooks]) => hooks.has(name)).map(([p]) => p);
|
|
57
|
+
if (owners.length !== 1) {
|
|
58
|
+
throw new ShardError(
|
|
59
|
+
owners.length === 0 ? "shader/unknown-hook" : "shader/ambiguous-hook",
|
|
60
|
+
owners.length === 0 ? `"${name}" is not a hook` : `"${name}" is a hook in ${owners.join(" and ")}`,
|
|
61
|
+
{
|
|
62
|
+
path: overrideModule,
|
|
63
|
+
hint: owners.length === 0 ? "Only functions marked @hook can be overridden." : `Qualify it: override fn ${owners[0]}::${name}(...).`
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
target = owners[0];
|
|
68
|
+
}
|
|
69
|
+
const hook = hooksByModule.get(target)?.get(name);
|
|
70
|
+
if (!hook) {
|
|
71
|
+
throw new ShardError("shader/unknown-hook", `"${target}::${name}" is not a hook`, {
|
|
72
|
+
path: overrideModule,
|
|
73
|
+
hint: "Only functions marked @hook can be overridden."
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (normalize(hook.params) !== normalize(m[3]) || normalize(hook.returns) !== normalize(m[4])) {
|
|
77
|
+
throw new ShardError(
|
|
78
|
+
"shader/hook-signature-mismatch",
|
|
79
|
+
`Override of ${target}::${name} in ${overrideModule} has a different signature`,
|
|
80
|
+
{
|
|
81
|
+
path: overrideModule,
|
|
82
|
+
hint: `Expected fn ${name}(${hook.params.trim()})${hook.returns ? ` -> ${hook.returns.trim()}` : ""}.`
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const fn = `${name}__override_${index}`;
|
|
87
|
+
rewritten = rewritten.replace(m[0], `fn ${fn}(${m[3]})${m[4] ? ` -> ${m[4].trim()}` : ""} {`);
|
|
88
|
+
const importPattern = new RegExp(
|
|
89
|
+
`(import\\s+${target.replace(/::/g, "::")}::[^;]*?\\b)${name}\\b`,
|
|
90
|
+
"g"
|
|
91
|
+
);
|
|
92
|
+
rewritten = rewritten.replace(importPattern, `$1${name}__default`);
|
|
93
|
+
winners.set(`${target}::${name}`, {
|
|
94
|
+
module: overrideModule,
|
|
95
|
+
fn,
|
|
96
|
+
params: m[3],
|
|
97
|
+
returns: m[4] ?? ""
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
out.set(overrideModule, rewritten);
|
|
101
|
+
});
|
|
102
|
+
for (const [path, hooks] of hooksByModule) {
|
|
103
|
+
let source = out.get(path);
|
|
104
|
+
const imports = [];
|
|
105
|
+
const dispatchers = [];
|
|
106
|
+
for (const [name, sig] of hooks) {
|
|
107
|
+
const winner = winners.get(`${path}::${name}`);
|
|
108
|
+
const header = new RegExp(`@hook\\s+fn\\s+${name}\\s*\\(`);
|
|
109
|
+
if (!winner) {
|
|
110
|
+
source = source.replace(header, `fn ${name}(`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
source = source.replace(header, `fn ${name}__default(`);
|
|
114
|
+
imports.push(`import ${winner.module}::${winner.fn};`);
|
|
115
|
+
const ret = sig.returns.trim();
|
|
116
|
+
const call = `${winner.fn}(${paramNames(sig.params).join(", ")})`;
|
|
117
|
+
dispatchers.push(
|
|
118
|
+
`fn ${name}(${sig.params})${ret ? ` -> ${ret}` : ""} { ${ret ? "return " : ""}${call}; }`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (imports.length > 0)
|
|
122
|
+
source = `${insertAfterImports(source, imports.join("\n"))}
|
|
123
|
+
${dispatchers.join("\n")}
|
|
124
|
+
`;
|
|
125
|
+
out.set(path, source);
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
export {
|
|
130
|
+
applyHooks,
|
|
131
|
+
findHooks
|
|
132
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/layout.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type ComponentDef, type FieldKind, type Fields, type InferFields } from '@aethervtt/shard-core';
|
|
2
|
+
interface FieldLayout {
|
|
3
|
+
name: string;
|
|
4
|
+
kind: FieldKind;
|
|
5
|
+
wgslType: string;
|
|
6
|
+
offset: number;
|
|
7
|
+
size: number;
|
|
8
|
+
align: number;
|
|
9
|
+
}
|
|
10
|
+
export interface WgslLayout<F extends Fields> {
|
|
11
|
+
/** WGSL struct declaration, e.g. `struct ToonParams { bands: u32, rim: vec4f, width: f32, }`. */
|
|
12
|
+
readonly wgsl: string;
|
|
13
|
+
readonly structName: string;
|
|
14
|
+
/** Size in bytes, rounded up to the struct's alignment (what a uniform/storage binding needs). */
|
|
15
|
+
readonly size: number;
|
|
16
|
+
readonly align: number;
|
|
17
|
+
readonly fields: readonly FieldLayout[];
|
|
18
|
+
/** Writes a value at a byte offset using WGSL's layout rules. */
|
|
19
|
+
write(view: DataView, offset: number, value: InferFields<F>): void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* WGSL struct and byte packer generated from a component schema, so the CPU value, the GPU struct,
|
|
23
|
+
* and the byte layout come from one definition and can't drift apart.
|
|
24
|
+
*/
|
|
25
|
+
export declare function wgslLayout<F extends Fields>(def: ComponentDef<F>): WgslLayout<F>;
|
|
26
|
+
export {};
|
package/dist/layout.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ShardError
|
|
3
|
+
} from "@aethervtt/shard-core";
|
|
4
|
+
const TYPES = {
|
|
5
|
+
f32: { type: "f32", size: 4, align: 4 },
|
|
6
|
+
i8: { type: "i32", size: 4, align: 4 },
|
|
7
|
+
i16: { type: "i32", size: 4, align: 4 },
|
|
8
|
+
i32: { type: "i32", size: 4, align: 4 },
|
|
9
|
+
u8: { type: "u32", size: 4, align: 4 },
|
|
10
|
+
u16: { type: "u32", size: 4, align: 4 },
|
|
11
|
+
u32: { type: "u32", size: 4, align: 4 },
|
|
12
|
+
bool: { type: "u32", size: 4, align: 4 },
|
|
13
|
+
enum: { type: "u32", size: 4, align: 4 },
|
|
14
|
+
vec2: { type: "vec2f", size: 8, align: 8 },
|
|
15
|
+
vec3: { type: "vec3f", size: 12, align: 16 },
|
|
16
|
+
vec4: { type: "vec4f", size: 16, align: 16 },
|
|
17
|
+
quat: { type: "vec4f", size: 16, align: 16 },
|
|
18
|
+
color: { type: "vec4f", size: 16, align: 16 },
|
|
19
|
+
mat3: { type: "mat3x3f", size: 48, align: 16 },
|
|
20
|
+
mat4: { type: "mat4x4f", size: 64, align: 16 },
|
|
21
|
+
affine3x4: { type: "array<vec4f, 3>", size: 48, align: 16 }
|
|
22
|
+
};
|
|
23
|
+
const alignTo = (n, a) => Math.ceil(n / a) * a;
|
|
24
|
+
function wgslLayout(def) {
|
|
25
|
+
const structName = def.name.split("/").pop();
|
|
26
|
+
const fields = [];
|
|
27
|
+
let offset = 0;
|
|
28
|
+
let structAlign = 4;
|
|
29
|
+
for (const { name, field, storage } of def.layout) {
|
|
30
|
+
if (storage === "object") continue;
|
|
31
|
+
const t = TYPES[field.kind];
|
|
32
|
+
if (!t) {
|
|
33
|
+
throw new ShardError(
|
|
34
|
+
"shader/unsupported-field",
|
|
35
|
+
`Field "${name}" of ${def.name} (${field.kind}) can't be a GPU struct member`,
|
|
36
|
+
{
|
|
37
|
+
hint: "GPU structs take numbers, bools, enums, vectors, colors, and matrices. f64 fields are not allowed; object fields are skipped."
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
offset = alignTo(offset, t.align);
|
|
42
|
+
fields.push({ name, kind: field.kind, wgslType: t.type, offset, size: t.size, align: t.align });
|
|
43
|
+
offset += t.size;
|
|
44
|
+
structAlign = Math.max(structAlign, t.align);
|
|
45
|
+
}
|
|
46
|
+
const size = alignTo(Math.max(offset, 4), structAlign);
|
|
47
|
+
const wgsl = `struct ${structName} {
|
|
48
|
+
${fields.map((f) => ` ${f.name}: ${f.wgslType},`).join("\n")}
|
|
49
|
+
}`;
|
|
50
|
+
const enumValues = /* @__PURE__ */ new Map();
|
|
51
|
+
for (const { name, field } of def.layout) {
|
|
52
|
+
if (field.kind === "enum") {
|
|
53
|
+
enumValues.set(name, field.jsonSchema().enum);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
wgsl,
|
|
58
|
+
structName,
|
|
59
|
+
size,
|
|
60
|
+
align: structAlign,
|
|
61
|
+
fields,
|
|
62
|
+
write(view, base, value) {
|
|
63
|
+
const record = value;
|
|
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
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export {
|
|
104
|
+
wgslLayout
|
|
105
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { GpuContext } from '@aethervtt/shard-gpu';
|
|
2
|
+
import type { Platform } from '@aethervtt/shard-platform';
|
|
3
|
+
export interface LinkRequest {
|
|
4
|
+
/** Module with the entry points, e.g. `shard::pbr::main` or `project::water`. */
|
|
5
|
+
root: string;
|
|
6
|
+
/** Conditions for `@if(...)`. */
|
|
7
|
+
defines?: Readonly<Record<string, boolean>>;
|
|
8
|
+
/** Override modules, in order; the last one wins per hook. */
|
|
9
|
+
overrides?: readonly string[];
|
|
10
|
+
/** Names what the variant is for in errors, e.g. `material my-game/Lava`. */
|
|
11
|
+
label?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface SourceLocation {
|
|
14
|
+
module: string;
|
|
15
|
+
line: number;
|
|
16
|
+
column: number;
|
|
17
|
+
}
|
|
18
|
+
export interface LinkedShader {
|
|
19
|
+
key: string;
|
|
20
|
+
code: string;
|
|
21
|
+
/** Maps a character offset in `code` back to the module that produced it. */
|
|
22
|
+
locate(offset: number): SourceLocation | undefined;
|
|
23
|
+
}
|
|
24
|
+
/** One linked variant, for `ShaderLibrary.preload`. */
|
|
25
|
+
export interface BakedShader {
|
|
26
|
+
/** The variant: root module, defines, overrides. */
|
|
27
|
+
key: string;
|
|
28
|
+
/** Of every module the variant links; a changed module makes the entry stale. */
|
|
29
|
+
hash: string;
|
|
30
|
+
code: string;
|
|
31
|
+
}
|
|
32
|
+
/** Linked variants saved from a run (`ShaderLibrary.bake()`), loaded with `preload`. */
|
|
33
|
+
export interface ShaderBake {
|
|
34
|
+
version: 1;
|
|
35
|
+
shaders: BakedShader[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Shader modules by path (`package::dir::file`), linked on demand with WESL. Engine shaders live
|
|
39
|
+
* under `shard::`, plugins under their own package, a project's `shaders/` folder under `project::`.
|
|
40
|
+
*/
|
|
41
|
+
export declare class ShaderLibrary {
|
|
42
|
+
private readonly sources;
|
|
43
|
+
/** Where each module came from, for error messages (a file path, or the module path). */
|
|
44
|
+
private readonly origins;
|
|
45
|
+
private readonly linkCache;
|
|
46
|
+
private readonly variants;
|
|
47
|
+
private readonly listeners;
|
|
48
|
+
private version;
|
|
49
|
+
private readonly preloaded;
|
|
50
|
+
/** Every variant linked or served from the bake, for `bake()`. */
|
|
51
|
+
private readonly used;
|
|
52
|
+
/** How many variants the bake didn't cover and WESL linked. */
|
|
53
|
+
linked: number;
|
|
54
|
+
/**
|
|
55
|
+
* Serves these variants without linking, as long as the modules they came from haven't changed.
|
|
56
|
+
* A stale or missing variant is linked as usual (loading WESL if it hasn't loaded yet).
|
|
57
|
+
*/
|
|
58
|
+
preload(bake: ShaderBake): void;
|
|
59
|
+
/**
|
|
60
|
+
* The variants this library has used so far, to save and `preload` next time. Bake after a run
|
|
61
|
+
* that draws everything the app draws: a variant that wasn't used isn't in it.
|
|
62
|
+
*/
|
|
63
|
+
bake(): ShaderBake;
|
|
64
|
+
/** Adds or replaces a module. `origin` is shown in errors (e.g. `shaders/toon.wesl`). */
|
|
65
|
+
register(path: string, source: string, origin?: string): void;
|
|
66
|
+
private rebuildScheduled;
|
|
67
|
+
/**
|
|
68
|
+
* After a change, rebuilds every variant already in use at once (in a microtask, so a batch of
|
|
69
|
+
* registrations rebuilds once), instead of waiting for its next `module()` call: an edited
|
|
70
|
+
* shader has usually compiled by the frame that draws it.
|
|
71
|
+
*/
|
|
72
|
+
private scheduleRebuild;
|
|
73
|
+
has(path: string): boolean;
|
|
74
|
+
/** Called with the changed module paths whenever a module is registered or replaced. */
|
|
75
|
+
onChange(listener: (paths: readonly string[]) => void): () => void;
|
|
76
|
+
/**
|
|
77
|
+
* Reloads modules from files when they change. `dir` maps to `pkg`: with `('shaders', 'project')`,
|
|
78
|
+
* `shaders/water/foam.wesl` is `project::water::foam`.
|
|
79
|
+
*/
|
|
80
|
+
watch(platform: Platform, dir: string, pkg: string): Promise<() => void>;
|
|
81
|
+
/**
|
|
82
|
+
* Registers every `.wesl`/`.wgsl` file under `dir` (recursively) as `pkg::path::name`. Hosts load a
|
|
83
|
+
* project's `shaders/` folder with it, then `watch` for edits.
|
|
84
|
+
*/
|
|
85
|
+
loadDir(platform: Platform, dir: string, pkg: string): Promise<string[]>;
|
|
86
|
+
/** Links a variant. Results are cached until any module changes. */
|
|
87
|
+
link(request: LinkRequest): Promise<LinkedShader>;
|
|
88
|
+
/**
|
|
89
|
+
* A compiled shader module for a variant, or undefined while the first version compiles. After an
|
|
90
|
+
* edit, keeps returning the previous module until the new code compiles cleanly; if it doesn't,
|
|
91
|
+
* the error is reported through the GPU context and the old module stays in use.
|
|
92
|
+
*/
|
|
93
|
+
module(gpu: GpuContext, request: LinkRequest): GPUShaderModule | undefined;
|
|
94
|
+
private rebuild;
|
|
95
|
+
/** Waits until every variant requested through `module()` has finished compiling. */
|
|
96
|
+
whenIdle(): Promise<void>;
|
|
97
|
+
/** Module graph and hooks, for agents. */
|
|
98
|
+
describe(root?: string): {
|
|
99
|
+
modules: string[];
|
|
100
|
+
imports: {
|
|
101
|
+
[k: string]: string[];
|
|
102
|
+
};
|
|
103
|
+
hooks: {
|
|
104
|
+
[k: string]: readonly string[];
|
|
105
|
+
};
|
|
106
|
+
defines: string[];
|
|
107
|
+
};
|
|
108
|
+
/** Of the source of every module a variant links: its root's imports, and the overrides'. */
|
|
109
|
+
private hashOf;
|
|
110
|
+
private doLink;
|
|
111
|
+
}
|
package/dist/library.js
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { ShardError } from "@aethervtt/shard-core";
|
|
2
|
+
import { applyHooks, findHooks } from "./hooks.js";
|
|
3
|
+
let wesl;
|
|
4
|
+
const loadWesl = () => {
|
|
5
|
+
wesl ??= import("wesl");
|
|
6
|
+
return wesl;
|
|
7
|
+
};
|
|
8
|
+
const PATH = /^[a-z_][a-z0-9_]*(::[a-z_][a-z0-9_]*)+$/;
|
|
9
|
+
function splitPath(path) {
|
|
10
|
+
const [pkg, ...rest] = path.split("::");
|
|
11
|
+
return { pkg, file: `./${rest.join("/")}.wesl` };
|
|
12
|
+
}
|
|
13
|
+
function lineColumn(text, offset) {
|
|
14
|
+
let line = 1;
|
|
15
|
+
let lineStart = 0;
|
|
16
|
+
for (let i = 0; i < offset && i < text.length; i++) {
|
|
17
|
+
if (text.charCodeAt(i) === 10) {
|
|
18
|
+
line++;
|
|
19
|
+
lineStart = i + 1;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return { line, column: offset - lineStart + 1 };
|
|
23
|
+
}
|
|
24
|
+
class ShaderLibrary {
|
|
25
|
+
sources = /* @__PURE__ */ new Map();
|
|
26
|
+
/** Where each module came from, for error messages (a file path, or the module path). */
|
|
27
|
+
origins = /* @__PURE__ */ new Map();
|
|
28
|
+
linkCache = /* @__PURE__ */ new Map();
|
|
29
|
+
variants = /* @__PURE__ */ new Map();
|
|
30
|
+
listeners = /* @__PURE__ */ new Set();
|
|
31
|
+
version = 0;
|
|
32
|
+
preloaded = /* @__PURE__ */ new Map();
|
|
33
|
+
/** Every variant linked or served from the bake, for `bake()`. */
|
|
34
|
+
used = /* @__PURE__ */ new Map();
|
|
35
|
+
/** How many variants the bake didn't cover and WESL linked. */
|
|
36
|
+
linked = 0;
|
|
37
|
+
/**
|
|
38
|
+
* Serves these variants without linking, as long as the modules they came from haven't changed.
|
|
39
|
+
* A stale or missing variant is linked as usual (loading WESL if it hasn't loaded yet).
|
|
40
|
+
*/
|
|
41
|
+
preload(bake) {
|
|
42
|
+
if (bake.version !== 1) {
|
|
43
|
+
throw new ShardError("shader/bake-version", `Unknown shader bake version ${bake.version}`, {
|
|
44
|
+
hint: "Bake again with this version of Shard."
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
for (const entry of bake.shaders) this.preloaded.set(entry.key, entry);
|
|
48
|
+
this.linkCache.clear();
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The variants this library has used so far, to save and `preload` next time. Bake after a run
|
|
52
|
+
* that draws everything the app draws: a variant that wasn't used isn't in it.
|
|
53
|
+
*/
|
|
54
|
+
bake() {
|
|
55
|
+
const shaders = [...this.used.values()].sort((a, b) => a.key < b.key ? -1 : 1);
|
|
56
|
+
return { version: 1, shaders };
|
|
57
|
+
}
|
|
58
|
+
/** Adds or replaces a module. `origin` is shown in errors (e.g. `shaders/toon.wesl`). */
|
|
59
|
+
register(path, source, origin) {
|
|
60
|
+
if (!PATH.test(path)) {
|
|
61
|
+
throw new ShardError("shader/invalid-path", `Invalid shader module path "${path}"`, {
|
|
62
|
+
hint: "Use lowercase `package::dir::name`, e.g. `project::water`."
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (this.sources.get(path) === source) return;
|
|
66
|
+
this.sources.set(path, source);
|
|
67
|
+
this.origins.set(path, origin ?? path);
|
|
68
|
+
this.version++;
|
|
69
|
+
this.linkCache.clear();
|
|
70
|
+
for (const listener of this.listeners) listener([path]);
|
|
71
|
+
this.scheduleRebuild();
|
|
72
|
+
}
|
|
73
|
+
rebuildScheduled = false;
|
|
74
|
+
/**
|
|
75
|
+
* After a change, rebuilds every variant already in use at once (in a microtask, so a batch of
|
|
76
|
+
* registrations rebuilds once), instead of waiting for its next `module()` call: an edited
|
|
77
|
+
* shader has usually compiled by the frame that draws it.
|
|
78
|
+
*/
|
|
79
|
+
scheduleRebuild() {
|
|
80
|
+
if (this.rebuildScheduled || this.variants.size === 0) return;
|
|
81
|
+
this.rebuildScheduled = true;
|
|
82
|
+
queueMicrotask(() => {
|
|
83
|
+
this.rebuildScheduled = false;
|
|
84
|
+
for (const s of this.variants.values()) {
|
|
85
|
+
if (s.gpu && s.request && s.version !== this.version && s.pending === void 0) {
|
|
86
|
+
this.rebuild(s.gpu, s.request, s);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
has(path) {
|
|
92
|
+
return this.sources.has(path);
|
|
93
|
+
}
|
|
94
|
+
/** Called with the changed module paths whenever a module is registered or replaced. */
|
|
95
|
+
onChange(listener) {
|
|
96
|
+
this.listeners.add(listener);
|
|
97
|
+
return () => this.listeners.delete(listener);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Reloads modules from files when they change. `dir` maps to `pkg`: with `('shaders', 'project')`,
|
|
101
|
+
* `shaders/water/foam.wesl` is `project::water::foam`.
|
|
102
|
+
*/
|
|
103
|
+
async watch(platform, dir, pkg) {
|
|
104
|
+
if (!platform.fs.watch) {
|
|
105
|
+
throw new ShardError(
|
|
106
|
+
"shader/watch-unsupported",
|
|
107
|
+
`The ${platform.name} platform can't watch files`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const prefix = dir.replace(/\/+$/, "");
|
|
111
|
+
return platform.fs.watch(prefix, (event) => {
|
|
112
|
+
if (event.kind === "remove" || !/\.(wesl|wgsl)$/.test(event.path)) return;
|
|
113
|
+
const rel = event.path.startsWith(`${prefix}/`) ? event.path.slice(prefix.length + 1) : event.path;
|
|
114
|
+
const path = `${pkg}::${rel.replace(/\.(wesl|wgsl)$/, "").split("/").join("::")}`;
|
|
115
|
+
void platform.fs.readText(event.path).then((text) => this.register(path, text, event.path));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Registers every `.wesl`/`.wgsl` file under `dir` (recursively) as `pkg::path::name`. Hosts load a
|
|
120
|
+
* project's `shaders/` folder with it, then `watch` for edits.
|
|
121
|
+
*/
|
|
122
|
+
async loadDir(platform, dir, pkg) {
|
|
123
|
+
const list = platform.fs.list;
|
|
124
|
+
if (!list) return [];
|
|
125
|
+
const loaded = [];
|
|
126
|
+
const prefix = dir.replace(/\/+$/, "");
|
|
127
|
+
const walk = async (rel) => {
|
|
128
|
+
for (const entry of await list.call(platform.fs, rel ? `${prefix}/${rel}` : prefix)) {
|
|
129
|
+
const path = rel ? `${rel}/${entry.name}` : entry.name;
|
|
130
|
+
if (entry.kind === "dir") await walk(path);
|
|
131
|
+
else if (/\.(wesl|wgsl)$/.test(entry.name)) {
|
|
132
|
+
const module = `${pkg}::${path.replace(/\.(wesl|wgsl)$/, "").split("/").join("::")}`;
|
|
133
|
+
const file = `${prefix}/${path}`;
|
|
134
|
+
this.register(module, await platform.fs.readText(file), file);
|
|
135
|
+
loaded.push(module);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
await walk("");
|
|
140
|
+
return loaded;
|
|
141
|
+
}
|
|
142
|
+
/** Links a variant. Results are cached until any module changes. */
|
|
143
|
+
link(request) {
|
|
144
|
+
const key = variantKey(request);
|
|
145
|
+
let result = this.linkCache.get(key);
|
|
146
|
+
if (!result) {
|
|
147
|
+
result = this.doLink(request, key);
|
|
148
|
+
this.linkCache.set(key, result);
|
|
149
|
+
result.catch(() => this.linkCache.delete(key));
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* A compiled shader module for a variant, or undefined while the first version compiles. After an
|
|
155
|
+
* edit, keeps returning the previous module until the new code compiles cleanly; if it doesn't,
|
|
156
|
+
* the error is reported through the GPU context and the old module stays in use.
|
|
157
|
+
*/
|
|
158
|
+
module(gpu, request) {
|
|
159
|
+
const key = variantKey(request);
|
|
160
|
+
let state = this.variants.get(key);
|
|
161
|
+
if (!state) {
|
|
162
|
+
state = { good: void 0, pending: void 0, version: -1, gpu, request };
|
|
163
|
+
this.variants.set(key, state);
|
|
164
|
+
}
|
|
165
|
+
const s = state;
|
|
166
|
+
s.gpu = gpu;
|
|
167
|
+
if (s.version !== this.version && s.pending === void 0) this.rebuild(gpu, request, s);
|
|
168
|
+
if (s.good && s.good.generation !== gpu.generation) {
|
|
169
|
+
s.good = { ...s.good, module: moduleFor(gpu, s.good.linked), generation: gpu.generation };
|
|
170
|
+
}
|
|
171
|
+
return s.good?.module;
|
|
172
|
+
}
|
|
173
|
+
rebuild(gpu, request, s) {
|
|
174
|
+
s.version = this.version;
|
|
175
|
+
s.pending = "linking";
|
|
176
|
+
this.link(request).then(
|
|
177
|
+
async (linked) => {
|
|
178
|
+
if (s.good?.code !== linked.code) {
|
|
179
|
+
s.pending = linked.code;
|
|
180
|
+
const error = await compile(gpu, linked);
|
|
181
|
+
if (error) gpu.reportError(labelled(error, request.label));
|
|
182
|
+
else
|
|
183
|
+
s.good = {
|
|
184
|
+
code: linked.code,
|
|
185
|
+
module: moduleFor(gpu, linked),
|
|
186
|
+
linked,
|
|
187
|
+
generation: gpu.generation
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
s.pending = void 0;
|
|
191
|
+
},
|
|
192
|
+
(err) => {
|
|
193
|
+
gpu.reportError(
|
|
194
|
+
labelled(
|
|
195
|
+
err instanceof ShardError ? err : new ShardError("shader/link", String(err)),
|
|
196
|
+
request.label
|
|
197
|
+
)
|
|
198
|
+
);
|
|
199
|
+
s.pending = void 0;
|
|
200
|
+
}
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
/** Waits until every variant requested through `module()` has finished compiling. */
|
|
204
|
+
async whenIdle() {
|
|
205
|
+
for (let i = 0; i < 200; i++) {
|
|
206
|
+
if (![...this.variants.values()].some((v) => v.pending !== void 0)) return;
|
|
207
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** Module graph and hooks, for agents. */
|
|
211
|
+
describe(root) {
|
|
212
|
+
const modules = [...this.sources.keys()].sort();
|
|
213
|
+
const imports = (path) => importsOf(this.sources.get(path) ?? "");
|
|
214
|
+
const reachable = root ? [...closure(root, imports)] : modules;
|
|
215
|
+
return {
|
|
216
|
+
modules: reachable,
|
|
217
|
+
imports: Object.fromEntries(reachable.map((m) => [m, imports(m)])),
|
|
218
|
+
hooks: Object.fromEntries(
|
|
219
|
+
reachable.map((m) => [m, [...findHooks(this.sources.get(m) ?? "").keys()]]).filter(([, h]) => h.length > 0)
|
|
220
|
+
),
|
|
221
|
+
defines: [...new Set(reachable.flatMap((m) => definesOf(this.sources.get(m) ?? "")))].sort()
|
|
222
|
+
};
|
|
223
|
+
}
|
|
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) ?? "");
|
|
227
|
+
const paths = /* @__PURE__ */ new Set();
|
|
228
|
+
for (const root of [request.root, ...request.overrides ?? []])
|
|
229
|
+
for (const p of closure(root, imports)) paths.add(p);
|
|
230
|
+
let a = 2166136261;
|
|
231
|
+
let b = 2538058380;
|
|
232
|
+
for (const path of [...paths].sort()) {
|
|
233
|
+
const text = `${path}
|
|
234
|
+
${this.sources.get(path) ?? ""}
|
|
235
|
+
`;
|
|
236
|
+
for (let i = 0; i < text.length; i++) {
|
|
237
|
+
const c = text.charCodeAt(i);
|
|
238
|
+
a = Math.imul(a ^ c, 16777619);
|
|
239
|
+
b = Math.imul(b ^ c, 1540483477);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return `${(a >>> 0).toString(16).padStart(8, "0")}${(b >>> 0).toString(16).padStart(8, "0")}`;
|
|
243
|
+
}
|
|
244
|
+
async doLink(request, key) {
|
|
245
|
+
const overrides = request.overrides ?? [];
|
|
246
|
+
for (const path of [request.root, ...overrides]) {
|
|
247
|
+
if (!this.sources.has(path)) {
|
|
248
|
+
throw new ShardError(
|
|
249
|
+
"shader/link-unknown-module",
|
|
250
|
+
`Shader module "${path}" is not registered`,
|
|
251
|
+
{
|
|
252
|
+
hint: `Registered modules: ${[...this.sources.keys()].sort().join(", ") || "none"}.`
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
for (const path of closure(request.root, (p) => importsOf(this.sources.get(p) ?? ""))) {
|
|
258
|
+
if (!this.sources.has(path)) {
|
|
259
|
+
const importer = [...this.sources].find(([, src]) => importsOf(src).includes(path))?.[0];
|
|
260
|
+
throw new ShardError("shader/link-unknown-module", `Unknown shader module "${path}"`, {
|
|
261
|
+
path: importer ? this.origins.get(importer) : void 0,
|
|
262
|
+
hint: "Check the import path, or register the module."
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const hash = this.hashOf(request);
|
|
267
|
+
const baked = this.preloaded.get(key);
|
|
268
|
+
if (baked && baked.hash === hash) {
|
|
269
|
+
this.used.set(key, baked);
|
|
270
|
+
return { key, code: baked.code, locate: () => void 0 };
|
|
271
|
+
}
|
|
272
|
+
const { link } = await loadWesl();
|
|
273
|
+
this.linked++;
|
|
274
|
+
const hooked = applyHooks(this.sources, overrides);
|
|
275
|
+
const root = splitPath(request.root);
|
|
276
|
+
const packages = /* @__PURE__ */ new Map();
|
|
277
|
+
const fileToModule = /* @__PURE__ */ new Map();
|
|
278
|
+
for (const [path, source] of hooked) {
|
|
279
|
+
const { pkg, file } = splitPath(path);
|
|
280
|
+
if (!packages.has(pkg)) packages.set(pkg, {});
|
|
281
|
+
packages.get(pkg)[file] = source;
|
|
282
|
+
fileToModule.set(`${pkg}|${file}`, path);
|
|
283
|
+
}
|
|
284
|
+
const libs = [...packages].filter(([pkg]) => pkg !== root.pkg).map(([pkg, modules]) => ({ name: pkg, edition: "unstable_2025", modules }));
|
|
285
|
+
let linked;
|
|
286
|
+
try {
|
|
287
|
+
linked = await link({
|
|
288
|
+
weslSrc: packages.get(root.pkg),
|
|
289
|
+
rootModuleName: root.file,
|
|
290
|
+
packageName: root.pkg,
|
|
291
|
+
libs,
|
|
292
|
+
conditions: { ...request.defines }
|
|
293
|
+
});
|
|
294
|
+
} catch (err) {
|
|
295
|
+
throw linkError(
|
|
296
|
+
err,
|
|
297
|
+
(file) => {
|
|
298
|
+
const lib = /^\.\/([a-z_][a-z0-9_]*)\/(.*)$/.exec(file);
|
|
299
|
+
const module = fileToModule.get(`${root.pkg}|${file}`) ?? (lib ? fileToModule.get(`${lib[1]}|./${lib[2]}`) : void 0);
|
|
300
|
+
return module ? this.origins.get(module) : void 0;
|
|
301
|
+
},
|
|
302
|
+
this.origins.get(request.root)
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
const code = linked.dest;
|
|
306
|
+
this.used.set(key, { key, hash, code });
|
|
307
|
+
const origins = this.origins;
|
|
308
|
+
return {
|
|
309
|
+
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
|
+
}
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const moduleCache = /* @__PURE__ */ new WeakMap();
|
|
327
|
+
function moduleFor(gpu, linked) {
|
|
328
|
+
let cache = moduleCache.get(gpu);
|
|
329
|
+
if (!cache || cache.generation !== gpu.generation) {
|
|
330
|
+
cache = /* @__PURE__ */ new Map();
|
|
331
|
+
cache.generation = gpu.generation;
|
|
332
|
+
moduleCache.set(gpu, cache);
|
|
333
|
+
}
|
|
334
|
+
let module = cache.get(linked.code);
|
|
335
|
+
if (!module) {
|
|
336
|
+
module = gpu.device.createShaderModule({ label: linked.key, code: linked.code });
|
|
337
|
+
cache.set(linked.code, module);
|
|
338
|
+
}
|
|
339
|
+
return module;
|
|
340
|
+
}
|
|
341
|
+
function linkError(err, originOf, fallback) {
|
|
342
|
+
let message = (err instanceof Error ? err.message : String(err)).split("\n")[0];
|
|
343
|
+
let loc = err.weslLocation;
|
|
344
|
+
const prefix = /^(\.\/\S+?):(\d+):(\d+)\s+error:\s*(.*)$/.exec(message);
|
|
345
|
+
if (!loc && prefix) {
|
|
346
|
+
loc = { file: prefix[1], line: Number(prefix[2]), column: Number(prefix[3]) };
|
|
347
|
+
message = prefix[4];
|
|
348
|
+
}
|
|
349
|
+
const origin = (loc && originOf(loc.file)) ?? fallback;
|
|
350
|
+
const path = origin && loc ? `${origin}:${loc.line}:${loc.column}` : origin;
|
|
351
|
+
return new ShardError("shader/link-unresolved", message.replace(/ in file: \S+$/, ""), { path });
|
|
352
|
+
}
|
|
353
|
+
async function compile(gpu, linked) {
|
|
354
|
+
gpu.device.pushErrorScope("validation");
|
|
355
|
+
const probe = gpu.device.createShaderModule({ label: `${linked.key} (check)`, code: linked.code });
|
|
356
|
+
void gpu.device.popErrorScope();
|
|
357
|
+
const info = await probe.getCompilationInfo();
|
|
358
|
+
const first = info.messages.find((m) => m.type === "error");
|
|
359
|
+
if (!first) return void 0;
|
|
360
|
+
const where = linked.locate(first.offset);
|
|
361
|
+
const path = where ? `${where.module}:${where.line}:${where.column}` : void 0;
|
|
362
|
+
return new ShardError("shader/compile", `${first.message}${path ? ` (${path})` : ""}`, { path });
|
|
363
|
+
}
|
|
364
|
+
function labelled(error, label) {
|
|
365
|
+
if (!label) return error;
|
|
366
|
+
return new ShardError(error.code, `${label}: ${error.message}`, {
|
|
367
|
+
path: error.path,
|
|
368
|
+
hint: error.hint
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
function variantKey(request) {
|
|
372
|
+
const defines = Object.entries(request.defines ?? {}).filter(([, on]) => on).map(([name]) => name).sort();
|
|
373
|
+
return `${request.root}|${defines.join(",")}|${(request.overrides ?? []).join(",")}`;
|
|
374
|
+
}
|
|
375
|
+
function importsOf(source) {
|
|
376
|
+
const out = [];
|
|
377
|
+
for (const m of source.matchAll(/^\s*import\s+([^;]+);/gm)) {
|
|
378
|
+
const spec = m[1].replace(/\s+/g, "");
|
|
379
|
+
const braced = /^(.*?)::\{(.*)\}$/.exec(spec);
|
|
380
|
+
if (braced) {
|
|
381
|
+
for (const item of braced[2].split(",")) {
|
|
382
|
+
const name = item.split(/as/)[0];
|
|
383
|
+
const parts = `${braced[1]}::${name}`.split("::");
|
|
384
|
+
out.push(parts.slice(0, -1).join("::"));
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
out.push(spec.split("::").slice(0, -1).join("::"));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return [...new Set(out.filter((p) => p.includes("::") && !p.startsWith("constants")))];
|
|
391
|
+
}
|
|
392
|
+
function definesOf(source) {
|
|
393
|
+
const out = [];
|
|
394
|
+
for (const m of source.matchAll(/@(?:if|elif)\s*\(([^)]*)\)/g)) {
|
|
395
|
+
for (const id of m[1].matchAll(/[A-Za-z_]\w*/g))
|
|
396
|
+
if (id[0] !== "true" && id[0] !== "false") out.push(id[0]);
|
|
397
|
+
}
|
|
398
|
+
return out;
|
|
399
|
+
}
|
|
400
|
+
function closure(root, next) {
|
|
401
|
+
const seen = /* @__PURE__ */ new Set();
|
|
402
|
+
const stack = [root];
|
|
403
|
+
while (stack.length > 0) {
|
|
404
|
+
const path = stack.pop();
|
|
405
|
+
if (seen.has(path)) continue;
|
|
406
|
+
seen.add(path);
|
|
407
|
+
stack.push(...next(path));
|
|
408
|
+
}
|
|
409
|
+
return seen;
|
|
410
|
+
}
|
|
411
|
+
export {
|
|
412
|
+
ShaderLibrary
|
|
413
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aethervtt/shard-shader",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"LICENSE",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@aethervtt/shard-core": "0.0.1",
|
|
20
|
+
"@aethervtt/shard-gpu": "0.0.1",
|
|
21
|
+
"@aethervtt/shard-platform": "0.0.1",
|
|
22
|
+
"wesl": "0.7.31"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/aethervtt/shard.git",
|
|
27
|
+
"directory": "packages/shader"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public",
|
|
31
|
+
"provenance": true
|
|
32
|
+
},
|
|
33
|
+
"shard": {
|
|
34
|
+
"buildSha": "d6c5c6aef4675575c878b02cf2c8f738b4190229"
|
|
35
|
+
}
|
|
36
|
+
}
|