wgsl-play 0.0.2 → 0.0.4
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/README.md +115 -3
- package/package.json +8 -5
- package/playwright-report/index.html +1 -1
- package/src/Config.ts +26 -0
- package/src/FetchingResolver.ts +146 -0
- package/src/HttpPackageLoader.ts +239 -0
- package/src/PackageLoader.ts +140 -29
- package/src/Renderer.ts +32 -13
- package/src/WgslPlay.css +2 -0
- package/src/WgslPlay.ts +144 -64
- package/src/index.ts +2 -0
- package/src/test/BundleHydrator.test.ts +1 -1
- package/src/test/WgslPlay.e2e.ts +181 -13
- package/src/test/WgslPlay.e2e.ts-snapshots/basic-shader-chromium-darwin.png +0 -0
- package/src/test/WgslPlay.e2e.ts-snapshots/conditions-after-red-chromium-darwin.png +0 -0
- package/src/test/WgslPlay.e2e.ts-snapshots/conditions-initial-green-chromium-darwin.png +0 -0
- package/src/test/WgslPlay.e2e.ts-snapshots/link-import-chromium-darwin.png +0 -0
- package/src/test/WgslPlay.e2e.ts-snapshots/npm-cdn-chromium-darwin.png +0 -0
- package/src/test/WgslPlay.e2e.ts-snapshots/shader-root-internal-chromium-darwin.png +0 -0
- package/src/test/WgslPlay.e2e.ts-snapshots/shader-root-src-chromium-darwin.png +0 -0
- package/src/test/WgslPlay.e2e.ts-snapshots/static-import-chromium-darwin.png +0 -0
- package/test-page/index.html +94 -11
- package/test-page/main.ts +87 -8
- package/test-page/shaders/effects/common.wesl +5 -0
- package/test-page/shaders/effects/main.wesl +10 -0
- package/test-page/shaders/utils.wesl +5 -0
- package/test-page/wesl.toml +3 -0
- package/tsconfig.json +1 -1
- package/vite.config.ts +19 -0
package/src/PackageLoader.ts
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module Discovery Phase
|
|
3
|
+
*
|
|
4
|
+
* Before linking, we run a discovery phase to find and fetch all needed modules.
|
|
5
|
+
* This uses a lightweight binding pass (findUnboundIdents) that walks the scope tree
|
|
6
|
+
* to find module references without fully resolving them.
|
|
7
|
+
*
|
|
8
|
+
* Module paths (e.g., foo::bar::baz) come from import statements or inline qualified
|
|
9
|
+
* references. We categorize them as:
|
|
10
|
+
* - internal: package:: and super:: paths, fetched from local URLs (shaderRoot)
|
|
11
|
+
* - external: other packages, fetched from npm
|
|
12
|
+
*
|
|
13
|
+
* The discovery loop uses FetchingResolver which provides:
|
|
14
|
+
* - Sync resolveModule() for findUnboundIdents compatibility
|
|
15
|
+
* - Async resolveModuleAsync() as prototype for future async BindIdents
|
|
16
|
+
*
|
|
17
|
+
* When wesl gets async BindIdents, the shim loop disappears and BindIdents
|
|
18
|
+
* will call resolveModuleAsync directly.
|
|
19
|
+
*/
|
|
20
|
+
|
|
1
21
|
import type { WeslBundle } from "wesl";
|
|
2
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
fileToModulePath,
|
|
24
|
+
findUnboundIdents,
|
|
25
|
+
npmNameVariations,
|
|
26
|
+
partition,
|
|
27
|
+
} from "wesl";
|
|
3
28
|
import type { WeslBundleFile } from "./BundleHydrator.ts";
|
|
4
29
|
import { bundleRegistry, hydrateBundleRegistry } from "./BundleHydrator.ts";
|
|
5
30
|
import {
|
|
@@ -7,40 +32,119 @@ import {
|
|
|
7
32
|
fetchBundleFilesFromUrl,
|
|
8
33
|
lygiaTgzUrl,
|
|
9
34
|
} from "./BundleLoader.ts";
|
|
35
|
+
import { getConfig, type WgslPlayConfig } from "./Config.ts";
|
|
36
|
+
import { FetchingResolver } from "./FetchingResolver.ts";
|
|
10
37
|
|
|
11
|
-
/**
|
|
12
|
-
export interface
|
|
13
|
-
|
|
14
|
-
|
|
38
|
+
/** Resolved sources ready for the linker. */
|
|
39
|
+
export interface ResolvedSources {
|
|
40
|
+
/** All sources keyed by module path. */
|
|
41
|
+
weslSrc: Record<string, string>;
|
|
42
|
+
libs: WeslBundle[];
|
|
43
|
+
/** Module name for the root/main module. */
|
|
44
|
+
rootModuleName?: string;
|
|
15
45
|
}
|
|
16
46
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
47
|
+
const virtualModules = ["constants", "test"];
|
|
48
|
+
|
|
49
|
+
/** Resolve dependencies: internal modules via HTTP, external packages from npm. */
|
|
50
|
+
export async function fetchDependencies(
|
|
51
|
+
rootModuleSource: string,
|
|
52
|
+
configOverrides?: Partial<WgslPlayConfig>,
|
|
53
|
+
currentPath?: string,
|
|
54
|
+
existingSources?: Record<string, string>,
|
|
55
|
+
): Promise<ResolvedSources> {
|
|
56
|
+
const config = getConfig(configOverrides);
|
|
57
|
+
const rootModuleName = currentPath
|
|
58
|
+
? urlToModulePath(currentPath, config.shaderRoot)
|
|
59
|
+
: "package::main";
|
|
60
|
+
const initialSources = {
|
|
61
|
+
...existingSources,
|
|
62
|
+
[rootModuleName]: rootModuleSource,
|
|
63
|
+
};
|
|
64
|
+
const resolverOpts = {
|
|
65
|
+
shaderRoot: config.shaderRoot,
|
|
66
|
+
srcModulePath: currentPath,
|
|
67
|
+
};
|
|
68
|
+
const resolver = new FetchingResolver(initialSources, resolverOpts);
|
|
69
|
+
|
|
70
|
+
const libs: WeslBundle[] = [];
|
|
71
|
+
const fetched = new Set<string>();
|
|
72
|
+
|
|
73
|
+
// Discovery loop (LATER we'll make BindIdents async, and this can go away)
|
|
74
|
+
while (true) {
|
|
75
|
+
findUnboundIdents(resolver);
|
|
76
|
+
|
|
77
|
+
const unresolved = getNonVirtualUnresolved(resolver, fetched);
|
|
78
|
+
if (unresolved.length === 0) break;
|
|
79
|
+
|
|
80
|
+
const [internal, external] = partition(unresolved, isInternal);
|
|
81
|
+
await Promise.all(internal.map(p => resolver.resolveModuleAsync(p)));
|
|
82
|
+
|
|
83
|
+
const newLibs = await fetchExternalBundles(external, fetched);
|
|
84
|
+
libs.push(...newLibs);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const weslSrc = extractWeslSrc(resolver);
|
|
88
|
+
return { libs, weslSrc };
|
|
23
89
|
}
|
|
24
90
|
|
|
25
|
-
/** Load shader
|
|
26
|
-
export async function loadShaderFromUrl(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
91
|
+
/** Load shader from URL, resolving all dependencies. */
|
|
92
|
+
export async function loadShaderFromUrl(
|
|
93
|
+
url: string,
|
|
94
|
+
configOverrides?: Partial<WgslPlayConfig>,
|
|
95
|
+
): Promise<ResolvedSources> {
|
|
96
|
+
const source = await fetchText(url);
|
|
97
|
+
const currentPath = new URL(url, window.location.href).pathname;
|
|
98
|
+
const config = getConfig(configOverrides);
|
|
99
|
+
const rootModuleName = urlToModulePath(currentPath, config.shaderRoot);
|
|
100
|
+
const { weslSrc, libs } = await fetchDependencies(
|
|
101
|
+
source,
|
|
102
|
+
configOverrides,
|
|
103
|
+
currentPath,
|
|
104
|
+
);
|
|
105
|
+
return { weslSrc, libs, rootModuleName };
|
|
31
106
|
}
|
|
32
107
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
108
|
+
function getNonVirtualUnresolved(
|
|
109
|
+
resolver: FetchingResolver,
|
|
110
|
+
fetched: Set<string>,
|
|
111
|
+
): string[] {
|
|
112
|
+
return resolver.getUnresolved().filter(p => {
|
|
113
|
+
const pkg = p.split("::")[0];
|
|
114
|
+
return !virtualModules.includes(pkg) && !fetched.has(pkg);
|
|
115
|
+
});
|
|
40
116
|
}
|
|
41
117
|
|
|
42
|
-
|
|
43
|
-
|
|
118
|
+
function isInternal(modulePath: string): boolean {
|
|
119
|
+
return modulePath.startsWith("package::") || modulePath.startsWith("super::");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Extract all sources from resolver (including main). */
|
|
123
|
+
function extractWeslSrc(resolver: FetchingResolver): Record<string, string> {
|
|
124
|
+
const weslSrc: Record<string, string> = {};
|
|
125
|
+
for (const [path] of resolver.allModules()) {
|
|
126
|
+
if (resolver.sources[path]) {
|
|
127
|
+
weslSrc[path] = resolver.sources[path];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return weslSrc;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Fetch external packages from npm. */
|
|
134
|
+
async function fetchExternalBundles(
|
|
135
|
+
modulePaths: string[],
|
|
136
|
+
fetched: Set<string>,
|
|
137
|
+
): Promise<WeslBundle[]> {
|
|
138
|
+
const pkgNames = [...new Set(modulePaths.map(p => p.split("::")[0]))];
|
|
139
|
+
const newPkgs = pkgNames.filter(p => !fetched.has(p));
|
|
140
|
+
if (newPkgs.length === 0) return [];
|
|
141
|
+
|
|
142
|
+
for (const p of newPkgs) fetched.add(p);
|
|
143
|
+
return fetchPackagesFromNpm(newPkgs);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Fetch WESL bundles from npm, auto-fetching dependencies recursively. */
|
|
147
|
+
async function fetchPackagesFromNpm(pkgIds: string[]): Promise<WeslBundle[]> {
|
|
44
148
|
const loaded = new Set<string>();
|
|
45
149
|
|
|
46
150
|
const promisedBundles = pkgIds.map(id => fetchOnePackage(id, loaded));
|
|
@@ -55,7 +159,7 @@ async function fetchOnePackage(
|
|
|
55
159
|
pkgId: string,
|
|
56
160
|
loaded: Set<string>,
|
|
57
161
|
): Promise<WeslBundleFile[]> {
|
|
58
|
-
if (loaded.has(pkgId)) return [];
|
|
162
|
+
if (loaded.has(pkgId)) return [];
|
|
59
163
|
loaded.add(pkgId);
|
|
60
164
|
|
|
61
165
|
// Special case for lygia - use custom tgz URL (npm package is outdated)
|
|
@@ -73,8 +177,15 @@ async function fetchOnePackage(
|
|
|
73
177
|
throw new Error(`Package not found: ${pkgId}`);
|
|
74
178
|
}
|
|
75
179
|
|
|
76
|
-
/**
|
|
77
|
-
|
|
180
|
+
/** Convert URL path to module path (e.g., "/shaders/effects/main.wesl" -> "package::effects::main"). */
|
|
181
|
+
function urlToModulePath(urlPath: string, shaderRoot: string): string {
|
|
182
|
+
const cleanRoot = shaderRoot.replace(/\/$/, "");
|
|
183
|
+
const relativePath = urlPath.replace(cleanRoot, "").replace(/^\//, "");
|
|
184
|
+
return fileToModulePath(relativePath, "package", false);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Fetch text content from URL. */
|
|
188
|
+
async function fetchText(url: string): Promise<string> {
|
|
78
189
|
const response = await fetch(url);
|
|
79
190
|
if (!response.ok) {
|
|
80
191
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
package/src/Renderer.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
1
|
+
import type { ModuleResolver } from "wesl";
|
|
2
|
+
import {
|
|
3
|
+
BundleResolver,
|
|
4
|
+
CompositeResolver,
|
|
5
|
+
RecordResolver,
|
|
6
|
+
requestWeslDevice,
|
|
7
|
+
} from "wesl";
|
|
3
8
|
import {
|
|
4
9
|
linkAndCreatePipeline,
|
|
5
10
|
renderFrame,
|
|
6
11
|
updateRenderUniforms,
|
|
12
|
+
type WeslOptions,
|
|
7
13
|
} from "wesl-gpu";
|
|
8
14
|
|
|
9
15
|
/** WebGPU state */
|
|
@@ -16,6 +22,7 @@ export interface RenderState {
|
|
|
16
22
|
pipelineLayout: GPUPipelineLayout;
|
|
17
23
|
bindGroup: GPUBindGroup;
|
|
18
24
|
pipeline?: GPURenderPipeline;
|
|
25
|
+
frameCount: number;
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
/** Animation state */
|
|
@@ -25,6 +32,9 @@ export interface PlaybackState {
|
|
|
25
32
|
pausedDuration: number;
|
|
26
33
|
}
|
|
27
34
|
|
|
35
|
+
/** Options for linking shaders - re-exports wesl-gpu's WeslOptions */
|
|
36
|
+
export type LinkOptions = WeslOptions;
|
|
37
|
+
|
|
28
38
|
/** Initialize WebGPU for a canvas element. */
|
|
29
39
|
export async function initWebGPU(
|
|
30
40
|
canvas: HTMLCanvasElement,
|
|
@@ -68,30 +78,39 @@ export async function initWebGPU(
|
|
|
68
78
|
uniformBuffer,
|
|
69
79
|
pipelineLayout,
|
|
70
80
|
bindGroup,
|
|
81
|
+
frameCount: 0,
|
|
71
82
|
};
|
|
72
83
|
}
|
|
73
84
|
|
|
74
|
-
export type LinkOptions = Pick<
|
|
75
|
-
LinkParams,
|
|
76
|
-
"packageName" | "conditions" | "constants"
|
|
77
|
-
>;
|
|
78
|
-
|
|
79
85
|
/** Compile WESL fragment shader and create render pipeline. */
|
|
80
86
|
export async function createPipeline(
|
|
81
87
|
state: RenderState,
|
|
82
88
|
fragmentSource: string,
|
|
83
|
-
bundles: WeslBundle[],
|
|
84
89
|
options?: LinkOptions,
|
|
85
90
|
): Promise<void> {
|
|
91
|
+
const { weslSrc, libs = [], conditions, constants } = options ?? {};
|
|
92
|
+
const { packageName, rootModuleName } = options ?? {};
|
|
93
|
+
|
|
94
|
+
// Build resolver from weslSrc/libs if provided
|
|
95
|
+
let resolver: ModuleResolver | undefined;
|
|
96
|
+
if (weslSrc || libs.length > 0) {
|
|
97
|
+
const resolvers: ModuleResolver[] = [];
|
|
98
|
+
if (weslSrc) resolvers.push(new RecordResolver(weslSrc));
|
|
99
|
+
for (const bundle of libs) resolvers.push(new BundleResolver(bundle));
|
|
100
|
+
resolver =
|
|
101
|
+
resolvers.length === 1 ? resolvers[0] : new CompositeResolver(resolvers);
|
|
102
|
+
}
|
|
103
|
+
|
|
86
104
|
state.pipeline = await linkAndCreatePipeline({
|
|
87
105
|
device: state.device,
|
|
88
106
|
fragmentSource,
|
|
89
|
-
|
|
107
|
+
resolver,
|
|
90
108
|
format: state.presentationFormat,
|
|
91
109
|
layout: state.pipelineLayout,
|
|
92
|
-
conditions
|
|
93
|
-
constants
|
|
94
|
-
packageName
|
|
110
|
+
conditions,
|
|
111
|
+
constants,
|
|
112
|
+
packageName,
|
|
113
|
+
rootModuleName,
|
|
95
114
|
});
|
|
96
115
|
}
|
|
97
116
|
|
|
@@ -128,11 +147,11 @@ export function startRenderLoop(
|
|
|
128
147
|
bindGroup: state.bindGroup,
|
|
129
148
|
targetView: state.context.getCurrentTexture().createView(),
|
|
130
149
|
});
|
|
150
|
+
state.frameCount++;
|
|
131
151
|
animationId = requestAnimationFrame(render);
|
|
132
152
|
}
|
|
133
153
|
|
|
134
154
|
animationId = requestAnimationFrame(render);
|
|
135
|
-
|
|
136
155
|
return () => cancelAnimationFrame(animationId);
|
|
137
156
|
}
|
|
138
157
|
|
package/src/WgslPlay.css
CHANGED
package/src/WgslPlay.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { LinkParams, WeslBundle } from "wesl";
|
|
2
|
+
import { fileToModulePath } from "wesl";
|
|
3
|
+
import type { WgslPlayConfig } from "./Config.ts";
|
|
2
4
|
import { ErrorOverlay } from "./ErrorOverlay.ts";
|
|
3
|
-
import {
|
|
4
|
-
fetchDependenciesForSource,
|
|
5
|
-
loadShaderFromUrl,
|
|
6
|
-
} from "./PackageLoader.ts";
|
|
5
|
+
import { fetchDependencies, loadShaderFromUrl } from "./PackageLoader.ts";
|
|
7
6
|
import {
|
|
8
7
|
createPipeline,
|
|
9
8
|
initWebGPU,
|
|
@@ -14,6 +13,8 @@ import {
|
|
|
14
13
|
} from "./Renderer.ts";
|
|
15
14
|
import cssText from "./WgslPlay.css?inline";
|
|
16
15
|
|
|
16
|
+
export { defaults, getConfig, resetConfig } from "./Config.ts";
|
|
17
|
+
|
|
17
18
|
/** Project configuration for multi-file shaders (subset of wesl link() API). */
|
|
18
19
|
export type WeslProject = Pick<
|
|
19
20
|
LinkParams,
|
|
@@ -49,7 +50,7 @@ let template: HTMLTemplateElement | null = null;
|
|
|
49
50
|
* </wgsl-play>
|
|
50
51
|
*/
|
|
51
52
|
export class WgslPlay extends HTMLElement {
|
|
52
|
-
static observedAttributes = ["src"];
|
|
53
|
+
static observedAttributes = ["src", "shader-root"];
|
|
53
54
|
|
|
54
55
|
private canvas: HTMLCanvasElement;
|
|
55
56
|
private errorOverlay: ErrorOverlay;
|
|
@@ -63,10 +64,20 @@ export class WgslPlay extends HTMLElement {
|
|
|
63
64
|
pausedDuration: 0,
|
|
64
65
|
};
|
|
65
66
|
|
|
66
|
-
private
|
|
67
|
+
private _weslSrc: Record<string, string> = {};
|
|
68
|
+
private _rootModuleName = "package::main";
|
|
69
|
+
private _libs?: WeslBundle[];
|
|
67
70
|
private _linkOptions: LinkOptions = {};
|
|
71
|
+
private _fromFullProject = false;
|
|
68
72
|
private _initialized = false;
|
|
69
73
|
|
|
74
|
+
/** Get config overrides from element attributes. */
|
|
75
|
+
private getConfigOverrides(): Partial<WgslPlayConfig> | undefined {
|
|
76
|
+
const shaderRoot = this.getAttribute("shader-root");
|
|
77
|
+
if (!shaderRoot) return undefined;
|
|
78
|
+
return { shaderRoot };
|
|
79
|
+
}
|
|
80
|
+
|
|
70
81
|
constructor() {
|
|
71
82
|
super();
|
|
72
83
|
const shadow = this.attachShadow({ mode: "open" });
|
|
@@ -110,33 +121,57 @@ export class WgslPlay extends HTMLElement {
|
|
|
110
121
|
}
|
|
111
122
|
}
|
|
112
123
|
|
|
113
|
-
/** Current shader source code. */
|
|
124
|
+
/** Current shader source code (main module). */
|
|
114
125
|
get source(): string {
|
|
115
|
-
return this.
|
|
126
|
+
return this._weslSrc[this._rootModuleName] ?? "";
|
|
116
127
|
}
|
|
117
128
|
|
|
118
129
|
/** Set shader source directly. */
|
|
119
130
|
set source(value: string) {
|
|
120
|
-
this.
|
|
121
|
-
this.
|
|
131
|
+
this._weslSrc = { [this._rootModuleName]: value };
|
|
132
|
+
this._libs = undefined;
|
|
133
|
+
this._fromFullProject = false;
|
|
134
|
+
this.discoverAndRebuild();
|
|
122
135
|
}
|
|
123
136
|
|
|
124
137
|
/** Set project configuration (mirrors wesl link() API). */
|
|
125
138
|
set project(value: WeslProject) {
|
|
126
139
|
const { weslSrc, rootModuleName, libs } = value;
|
|
127
140
|
const { packageName, conditions, constants } = value;
|
|
128
|
-
if (!weslSrc || !rootModuleName) return;
|
|
129
|
-
const mainSource = weslSrc[rootModuleName];
|
|
130
|
-
if (!mainSource) return;
|
|
131
141
|
|
|
132
|
-
|
|
133
|
-
|
|
142
|
+
// Update link options if provided
|
|
143
|
+
if (packageName || conditions || constants) {
|
|
144
|
+
this._linkOptions = { packageName, conditions, constants };
|
|
145
|
+
}
|
|
146
|
+
if (libs) this._libs = libs;
|
|
134
147
|
|
|
135
|
-
if (
|
|
136
|
-
this.
|
|
137
|
-
|
|
138
|
-
this.compileSource(mainSource);
|
|
148
|
+
if (weslSrc) {
|
|
149
|
+
this.setProjectSources(weslSrc, rootModuleName);
|
|
150
|
+
return;
|
|
139
151
|
}
|
|
152
|
+
|
|
153
|
+
// Partial update - may need to refetch if conditions changed
|
|
154
|
+
if (Object.keys(this._weslSrc).length === 0) return;
|
|
155
|
+
if (this._fromFullProject) this.rebuildPipeline();
|
|
156
|
+
else this.discoverAndRebuild();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Set sources from a full project with weslSrc. */
|
|
160
|
+
private setProjectSources(
|
|
161
|
+
weslSrc: Record<string, string>,
|
|
162
|
+
rootModuleName?: string,
|
|
163
|
+
): void {
|
|
164
|
+
// Convert file paths to module paths if needed (for ?link imports)
|
|
165
|
+
const entries = Object.entries(weslSrc).map(([k, v]) => [
|
|
166
|
+
toModulePath(k),
|
|
167
|
+
v,
|
|
168
|
+
]);
|
|
169
|
+
this._weslSrc = Object.fromEntries(entries);
|
|
170
|
+
this._rootModuleName = rootModuleName
|
|
171
|
+
? toModulePath(rootModuleName)
|
|
172
|
+
: "package::main";
|
|
173
|
+
this._fromFullProject = true;
|
|
174
|
+
this.rebuildPipeline();
|
|
140
175
|
}
|
|
141
176
|
|
|
142
177
|
/** Whether the shader is currently playing. */
|
|
@@ -146,10 +181,14 @@ export class WgslPlay extends HTMLElement {
|
|
|
146
181
|
|
|
147
182
|
/** Current animation time in seconds. */
|
|
148
183
|
get time(): number {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
184
|
+
const { isPlaying, startTime, pausedDuration } = this.playback;
|
|
185
|
+
const now = isPlaying ? performance.now() : startTime + pausedDuration;
|
|
186
|
+
return (now - startTime) / 1000;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Number of frames rendered (for testing/debugging). */
|
|
190
|
+
get frameCount(): number {
|
|
191
|
+
return this.renderState?.frameCount ?? 0;
|
|
153
192
|
}
|
|
154
193
|
|
|
155
194
|
/** Whether there's a compilation error. */
|
|
@@ -164,10 +203,9 @@ export class WgslPlay extends HTMLElement {
|
|
|
164
203
|
|
|
165
204
|
/** Start playback. */
|
|
166
205
|
play(): void {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
this.playback.startTime =
|
|
170
|
-
performance.now() - (pauseTime - this.playback.startTime);
|
|
206
|
+
const { isPlaying, pausedDuration } = this.playback;
|
|
207
|
+
if (isPlaying) return;
|
|
208
|
+
this.playback.startTime = performance.now() - pausedDuration;
|
|
171
209
|
this.setPlaying(true);
|
|
172
210
|
}
|
|
173
211
|
|
|
@@ -194,23 +232,24 @@ export class WgslPlay extends HTMLElement {
|
|
|
194
232
|
|
|
195
233
|
/** Display error message in overlay. Pass empty string to clear. */
|
|
196
234
|
showError(message: string): void {
|
|
197
|
-
if (message) {
|
|
198
|
-
this.errorOverlay.show(message);
|
|
199
|
-
this.pause();
|
|
200
|
-
} else {
|
|
235
|
+
if (!message) {
|
|
201
236
|
this.errorOverlay.hide();
|
|
237
|
+
return;
|
|
202
238
|
}
|
|
239
|
+
this.errorOverlay.show(message);
|
|
240
|
+
this.pause();
|
|
203
241
|
}
|
|
204
242
|
|
|
205
|
-
/** Set up WebGPU and load initial shader
|
|
206
|
-
private async initialize(): Promise<
|
|
207
|
-
if (this._initialized) return;
|
|
243
|
+
/** Set up WebGPU and load initial shader. Returns true if successful. */
|
|
244
|
+
private async initialize(): Promise<boolean> {
|
|
245
|
+
if (this._initialized) return !!this.renderState;
|
|
208
246
|
this._initialized = true;
|
|
209
247
|
|
|
210
248
|
try {
|
|
211
249
|
this.renderState = await initWebGPU(this.canvas);
|
|
212
250
|
await this.loadInitialContent();
|
|
213
251
|
this.stopRenderLoop = startRenderLoop(this.renderState, this.playback);
|
|
252
|
+
return true;
|
|
214
253
|
} catch (error) {
|
|
215
254
|
const message = `WebGPU initialization failed: ${error}`;
|
|
216
255
|
this.errorOverlay.show(message);
|
|
@@ -218,18 +257,26 @@ export class WgslPlay extends HTMLElement {
|
|
|
218
257
|
this.dispatchEvent(
|
|
219
258
|
new CustomEvent("init-error", { detail: { message } }),
|
|
220
259
|
);
|
|
260
|
+
return false;
|
|
221
261
|
}
|
|
222
262
|
}
|
|
223
263
|
|
|
224
|
-
/** Load from src attribute or inline textContent. */
|
|
264
|
+
/** Load from src attribute, script child, or inline textContent. */
|
|
225
265
|
private async loadInitialContent(): Promise<void> {
|
|
226
266
|
const src = this.getAttribute("src");
|
|
227
|
-
if (src)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
267
|
+
if (src) return this.loadFromUrl(src);
|
|
268
|
+
|
|
269
|
+
// Prefer <script type="text/wgsl"> or <script type="text/wesl"> (no HTML escaping needed)
|
|
270
|
+
const script = this.querySelector(
|
|
271
|
+
'script[type="text/wgsl"], script[type="text/wesl"]',
|
|
272
|
+
);
|
|
273
|
+
const inlineSource =
|
|
274
|
+
script?.textContent?.trim() ?? this.textContent?.trim();
|
|
275
|
+
if (!inlineSource) return;
|
|
276
|
+
|
|
277
|
+
this._weslSrc = { [this._rootModuleName]: inlineSource };
|
|
278
|
+
this._fromFullProject = false;
|
|
279
|
+
await this.discoverAndRebuild();
|
|
233
280
|
}
|
|
234
281
|
|
|
235
282
|
/** Fetch shader from URL, auto-fetching any imported dependencies. */
|
|
@@ -238,45 +285,73 @@ export class WgslPlay extends HTMLElement {
|
|
|
238
285
|
|
|
239
286
|
try {
|
|
240
287
|
this.errorOverlay.hide();
|
|
241
|
-
const {
|
|
242
|
-
|
|
243
|
-
|
|
288
|
+
const { weslSrc, libs, rootModuleName } = await loadShaderFromUrl(
|
|
289
|
+
url,
|
|
290
|
+
this.getConfigOverrides(),
|
|
291
|
+
);
|
|
292
|
+
this._weslSrc = weslSrc;
|
|
293
|
+
this._libs = libs;
|
|
294
|
+
this._fromFullProject = false;
|
|
295
|
+
if (rootModuleName) this._rootModuleName = rootModuleName;
|
|
296
|
+
|
|
297
|
+
const mainSource = weslSrc[this._rootModuleName];
|
|
298
|
+
if (!mainSource) return;
|
|
299
|
+
|
|
300
|
+
await createPipeline(this.renderState, mainSource, {
|
|
301
|
+
...this._linkOptions,
|
|
302
|
+
weslSrc,
|
|
303
|
+
libs,
|
|
304
|
+
rootModuleName: this._rootModuleName,
|
|
305
|
+
});
|
|
244
306
|
} catch (error) {
|
|
245
307
|
this.handleCompileError(error);
|
|
246
308
|
}
|
|
247
309
|
}
|
|
248
310
|
|
|
249
|
-
/**
|
|
250
|
-
private async
|
|
251
|
-
if (!this.
|
|
311
|
+
/** Rebuild GPU pipeline using stored state. For full projects with all sources. */
|
|
312
|
+
private async rebuildPipeline(): Promise<void> {
|
|
313
|
+
if (!(await this.initialize())) return;
|
|
314
|
+
|
|
315
|
+
const mainSource = this._weslSrc[this._rootModuleName];
|
|
316
|
+
if (!mainSource) return;
|
|
252
317
|
|
|
253
318
|
try {
|
|
254
319
|
this.errorOverlay.hide();
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
this.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
);
|
|
320
|
+
await createPipeline(this.renderState!, mainSource, {
|
|
321
|
+
...this._linkOptions,
|
|
322
|
+
weslSrc: this._weslSrc,
|
|
323
|
+
libs: this._libs,
|
|
324
|
+
rootModuleName: this._rootModuleName,
|
|
325
|
+
});
|
|
262
326
|
} catch (error) {
|
|
263
327
|
this.handleCompileError(error);
|
|
264
328
|
}
|
|
265
329
|
}
|
|
266
330
|
|
|
267
|
-
/**
|
|
268
|
-
private async
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (!
|
|
273
|
-
await this.initialize();
|
|
274
|
-
}
|
|
275
|
-
if (!this.renderState) return;
|
|
331
|
+
/** Discover dependencies and rebuild. For HTTP/inline sources that may need fetching. */
|
|
332
|
+
private async discoverAndRebuild(): Promise<void> {
|
|
333
|
+
if (!(await this.initialize())) return;
|
|
334
|
+
|
|
335
|
+
const mainSource = this._weslSrc[this._rootModuleName];
|
|
336
|
+
if (!mainSource) return;
|
|
276
337
|
|
|
277
338
|
try {
|
|
278
339
|
this.errorOverlay.hide();
|
|
279
|
-
|
|
340
|
+
const { weslSrc, libs } = await fetchDependencies(
|
|
341
|
+
mainSource,
|
|
342
|
+
this.getConfigOverrides(),
|
|
343
|
+
undefined,
|
|
344
|
+
this._weslSrc,
|
|
345
|
+
);
|
|
346
|
+
this._weslSrc = { ...this._weslSrc, ...weslSrc };
|
|
347
|
+
this._libs = [...(this._libs ?? []), ...libs];
|
|
348
|
+
|
|
349
|
+
await createPipeline(this.renderState!, mainSource, {
|
|
350
|
+
...this._linkOptions,
|
|
351
|
+
weslSrc: this._weslSrc,
|
|
352
|
+
libs: this._libs,
|
|
353
|
+
rootModuleName: this._rootModuleName,
|
|
354
|
+
});
|
|
280
355
|
} catch (error) {
|
|
281
356
|
this.handleCompileError(error);
|
|
282
357
|
}
|
|
@@ -306,3 +381,8 @@ function getStyles(): CSSStyleSheet {
|
|
|
306
381
|
}
|
|
307
382
|
return styles;
|
|
308
383
|
}
|
|
384
|
+
|
|
385
|
+
/** Convert file path to module path (e.g., "effects/main.wesl" -> "package::effects::main"). */
|
|
386
|
+
function toModulePath(filePath: string): string {
|
|
387
|
+
return fileToModulePath(filePath, "package", false);
|
|
388
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { loadBundlesFromFiles } from "../BundleHydrator.ts";
|
|
|
9
9
|
|
|
10
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
11
|
const __dirname = dirname(__filename);
|
|
12
|
-
const testPkgDir = join(__dirname, "
|
|
12
|
+
const testPkgDir = join(__dirname, "../../../test_pkg");
|
|
13
13
|
|
|
14
14
|
function loadBundlesFromTgz(tgzPath: string, packageName: string) {
|
|
15
15
|
const gzipData = readFileSync(tgzPath);
|