wgsl-play 0.0.39 → 0.0.40
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 +59 -2
- package/dist/{WgslPlay-CSjRo-5Z.d.ts → WgslPlay-ouKx64GN.d.ts} +52 -3
- package/dist/WgslPlay-zK9mE5rV.js +1544 -0
- package/dist/WgslPlay.d.ts +2 -2
- package/dist/WgslPlay.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/jsx-preact.d.ts +12 -0
- package/dist/jsx-preact.js +0 -0
- package/dist/wgsl-play.js +1834 -754
- package/package.json +12 -8
- package/src/CanvasSize.ts +27 -0
- package/src/ComputeBuild.ts +151 -0
- package/src/FragmentRender.ts +147 -0
- package/src/PlaybackControls.ts +36 -13
- package/src/RenderResources.ts +132 -0
- package/src/Renderer.ts +112 -138
- package/src/ResultsPanel.ts +261 -0
- package/src/WgslPlay.css +80 -8
- package/src/WgslPlay.ts +346 -103
- package/src/icons/refresh.svg +1 -0
- package/src/jsx-preact.ts +15 -0
- package/src/test/BundleHydrator.test.ts +8 -11
- package/src/test/CanvasSize.test.ts +48 -0
- package/src/test/E2eHelpers.ts +117 -0
- package/src/test/RenderFrame.e2e.ts +33 -0
- package/src/test/ResultsPanel.test.ts +164 -0
- package/src/test/WgslPlay.basics.e2e.ts +87 -0
- package/src/test/WgslPlay.compute.e2e.ts +147 -0
- package/src/test/WgslPlay.connect.e2e.ts +101 -0
- package/src/test/WgslPlay.loading.e2e.ts +68 -0
- package/src/test/WgslPlay.resize.e2e.ts +199 -0
- package/src/test/WgslPlay.resources.e2e.ts +67 -0
- package/src/test/WgslPlay.resources.e2e.ts-snapshots/buffer-zero-init-chromium-darwin.png +0 -0
- package/src/test/WgslPlay.resources.e2e.ts-snapshots/texture-grid-chromium-darwin.png +0 -0
- package/src/test/WgslPlay.resources.e2e.ts-snapshots/texture-img-green-chromium-darwin.png +0 -0
- package/src/test/WgslPlay.resources.e2e.ts-snapshots/texture-img-magenta-chromium-darwin.png +0 -0
- package/src/test/WgslPlay.resources.e2e.ts-snapshots/texture-multi-chromium-darwin.png +0 -0
- package/src/test/WgslPlay.uniforms.e2e.ts +57 -0
- package/dist/WgslPlay-LsU6XE09.js +0 -933
- package/src/test/WgslPlay.e2e.ts +0 -327
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.basics.e2e.ts-snapshots}/basic-shader-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.basics.e2e.ts-snapshots}/conditions-after-red-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.basics.e2e.ts-snapshots}/conditions-initial-green-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.connect.e2e.ts-snapshots}/connect-conditions-green-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.connect.e2e.ts-snapshots}/connect-conditions-red-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.connect.e2e.ts-snapshots}/connect-dynamic-npm-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.connect.e2e.ts-snapshots}/connect-source-external-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.connect.e2e.ts-snapshots}/connect-source-multifile-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.loading.e2e.ts-snapshots}/link-import-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.loading.e2e.ts-snapshots}/npm-cdn-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.loading.e2e.ts-snapshots}/shader-root-internal-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.loading.e2e.ts-snapshots}/shader-root-src-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.loading.e2e.ts-snapshots}/static-import-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.uniforms.e2e.ts-snapshots}/uniforms-initial-chromium-darwin.png +0 -0
- /package/src/test/{WgslPlay.e2e.ts-snapshots → WgslPlay.uniforms.e2e.ts-snapshots}/uniforms-slider-changed-chromium-darwin.png +0 -0
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wgsl-play",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.40",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
7
7
|
"src"
|
|
8
8
|
],
|
|
9
|
-
"repository": "github:
|
|
9
|
+
"repository": "github:webgpu-tools/wesl-js",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
12
12
|
"types": "./dist/index.d.ts",
|
|
@@ -19,18 +19,22 @@
|
|
|
19
19
|
"./bundle": {
|
|
20
20
|
"types": "./dist/WgslPlay.d.ts",
|
|
21
21
|
"import": "./dist/wgsl-play.js"
|
|
22
|
+
},
|
|
23
|
+
"./jsx-preact": {
|
|
24
|
+
"types": "./dist/jsx-preact.d.ts",
|
|
25
|
+
"import": "./dist/jsx-preact.js"
|
|
22
26
|
}
|
|
23
27
|
},
|
|
24
28
|
"dependencies": {
|
|
25
|
-
"wesl": "0.7.
|
|
26
|
-
"wesl-
|
|
27
|
-
"wesl-reflect": "0.0.
|
|
28
|
-
"wesl-
|
|
29
|
+
"wesl": "0.7.27",
|
|
30
|
+
"wesl-gpu": "0.1.29",
|
|
31
|
+
"wesl-reflect": "0.0.6",
|
|
32
|
+
"wesl-fetch": "0.0.15"
|
|
29
33
|
},
|
|
30
34
|
"devDependencies": {
|
|
31
35
|
"@playwright/test": "^1.53.2",
|
|
32
|
-
"fflate": "^0.8.2",
|
|
33
36
|
"nanotar": "^0.2.0",
|
|
37
|
+
"preact": "^10.27.2",
|
|
34
38
|
"wesl-plugin": "x"
|
|
35
39
|
},
|
|
36
40
|
"scripts": {
|
|
@@ -38,7 +42,7 @@
|
|
|
38
42
|
"dev": "vite test-page --config vite.config.ts",
|
|
39
43
|
"test": "vitest",
|
|
40
44
|
"test:e2e": "playwright test",
|
|
41
|
-
"typecheck": "tsgo"
|
|
45
|
+
"typecheck": "tsgo && tsgo -p tsconfig.e2e.json"
|
|
42
46
|
},
|
|
43
47
|
"main": "./dist/index.js"
|
|
44
48
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Device-pixel size of an observed element.
|
|
2
|
+
* Prefers `devicePixelContentBoxSize` (Chromium/Firefox) — integer, pixel-exact.
|
|
3
|
+
* Falls back to `contentBoxSize × ratio` on Safari and when a `pixel-ratio`
|
|
4
|
+
* override is set (the override is CSS-pixel-based by intent). */
|
|
5
|
+
export function entrySize(
|
|
6
|
+
entry: ResizeObserverEntry,
|
|
7
|
+
pixelRatioOverride: string | null,
|
|
8
|
+
): [width: number, height: number] {
|
|
9
|
+
if (pixelRatioOverride === null) {
|
|
10
|
+
const dp = entry.devicePixelContentBoxSize?.[0];
|
|
11
|
+
if (dp) return [dp.inlineSize, dp.blockSize];
|
|
12
|
+
}
|
|
13
|
+
const ratio =
|
|
14
|
+
pixelRatioOverride !== null ? Number(pixelRatioOverride) : devicePixelRatio;
|
|
15
|
+
const css = entry.contentBoxSize?.[0];
|
|
16
|
+
if (css) return [css.inlineSize * ratio, css.blockSize * ratio];
|
|
17
|
+
const { width, height } = entry.contentRect;
|
|
18
|
+
return [width * ratio, height * ratio];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Floor and clamp to `[1, max]` so the canvas never exceeds the GPU's 2D limit.
|
|
22
|
+
* Pass `max = undefined` to skip the upper clamp (e.g. before the GPU device
|
|
23
|
+
* is available; device init will surface limit errors itself). */
|
|
24
|
+
export function clampCanvas(size: number, max: number | undefined): number {
|
|
25
|
+
const floored = Math.max(1, Math.floor(size));
|
|
26
|
+
return max !== undefined ? Math.min(floored, max) : floored;
|
|
27
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { WeslAST } from "wesl";
|
|
2
|
+
import {
|
|
3
|
+
type AutoValues,
|
|
4
|
+
clearBuffers,
|
|
5
|
+
linkComputeShader,
|
|
6
|
+
type PlayResources,
|
|
7
|
+
runCompute,
|
|
8
|
+
withErrorScopes,
|
|
9
|
+
writeUniforms,
|
|
10
|
+
} from "wesl-gpu";
|
|
11
|
+
import {
|
|
12
|
+
type DiscoveredResource,
|
|
13
|
+
type VarReflection,
|
|
14
|
+
varReflection,
|
|
15
|
+
} from "wesl-reflect";
|
|
16
|
+
import type { BuildResult, LinkOptions, RenderState } from "./Renderer.ts";
|
|
17
|
+
import {
|
|
18
|
+
type BuildBranchParams,
|
|
19
|
+
destroyPlayResources,
|
|
20
|
+
disposeResources,
|
|
21
|
+
mergeResourcePlugins,
|
|
22
|
+
} from "./RenderResources.ts";
|
|
23
|
+
import type { BufferEntry } from "./ResultsPanel.ts";
|
|
24
|
+
|
|
25
|
+
export interface BuildComputeParams extends BuildBranchParams {
|
|
26
|
+
ast: WeslAST;
|
|
27
|
+
entryPoint: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface LinkComputeForParams {
|
|
31
|
+
device: GPUDevice;
|
|
32
|
+
shaderSource: string;
|
|
33
|
+
resources: DiscoveredResource[];
|
|
34
|
+
options?: LinkOptions;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Build the compute pipeline, dispatch once, and return initial readback. */
|
|
38
|
+
export async function buildCompute(
|
|
39
|
+
p: BuildComputeParams,
|
|
40
|
+
): Promise<BuildResult> {
|
|
41
|
+
const { state, resources, playResources, ast, shaderSource } = p;
|
|
42
|
+
const { device } = state;
|
|
43
|
+
const compute = await linkComputeFor({
|
|
44
|
+
device,
|
|
45
|
+
shaderSource,
|
|
46
|
+
resources,
|
|
47
|
+
options: p.options,
|
|
48
|
+
}).catch(err => {
|
|
49
|
+
destroyPlayResources(playResources);
|
|
50
|
+
throw err;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const readBuffers = mapBuffersByName(resources, playResources);
|
|
54
|
+
const reflections = mapBufferReflections(ast, resources);
|
|
55
|
+
|
|
56
|
+
disposeResources(state);
|
|
57
|
+
state.pipeline = undefined;
|
|
58
|
+
state.bindGroup = p.bindGroup;
|
|
59
|
+
state.resourceTextures = playResources.textures;
|
|
60
|
+
state.resourceBuffers = playResources.buffers;
|
|
61
|
+
state.compute = {
|
|
62
|
+
pipelineLayout: p.pipelineLayout,
|
|
63
|
+
bindGroup: p.bindGroup,
|
|
64
|
+
module: compute.module,
|
|
65
|
+
entryPoint: p.entryPoint,
|
|
66
|
+
readBuffers,
|
|
67
|
+
reflections,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const computeReadback = await dispatchComputeAndReadback(state);
|
|
71
|
+
return { layout: p.layout, mode: "compute", computeReadback };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Re-dispatch the compute pipeline against the current uniform state and
|
|
75
|
+
* return fresh BufferEntry[] for the panel. */
|
|
76
|
+
export async function rerunCompute(state: RenderState): Promise<BufferEntry[]> {
|
|
77
|
+
return dispatchComputeAndReadback(state);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Re-zero each readback buffer, dispatch, and read back. Used for both initial
|
|
81
|
+
* build and slider/refresh-driven re-runs in compute mode. */
|
|
82
|
+
async function dispatchComputeAndReadback(
|
|
83
|
+
state: RenderState,
|
|
84
|
+
): Promise<BufferEntry[]> {
|
|
85
|
+
const compute = state.compute;
|
|
86
|
+
if (!compute) return [];
|
|
87
|
+
// Auto fields (time/frame/delta_time) stay zero in compute mode by design.
|
|
88
|
+
writeUniforms(state.device, state.uniformState, computeAutoValues(state));
|
|
89
|
+
clearBuffers(state.device, compute.readBuffers.values());
|
|
90
|
+
const { readbacks } = await runCompute({
|
|
91
|
+
device: state.device,
|
|
92
|
+
module: compute.module,
|
|
93
|
+
entryPoint: compute.entryPoint,
|
|
94
|
+
bindGroup: compute.bindGroup,
|
|
95
|
+
pipelineLayout: compute.pipelineLayout,
|
|
96
|
+
readBuffers: compute.readBuffers,
|
|
97
|
+
});
|
|
98
|
+
return [...readbacks].map(([varName, data]) => ({
|
|
99
|
+
reflection: compute.reflections.get(varName)!,
|
|
100
|
+
data,
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function linkComputeFor(
|
|
105
|
+
p: LinkComputeForParams,
|
|
106
|
+
): Promise<{ module: GPUShaderModule }> {
|
|
107
|
+
const config = mergeResourcePlugins(p.options?.config, p.resources);
|
|
108
|
+
return withErrorScopes(p.device, () =>
|
|
109
|
+
linkComputeShader({
|
|
110
|
+
device: p.device,
|
|
111
|
+
computeSource: p.shaderSource,
|
|
112
|
+
...p.options,
|
|
113
|
+
config,
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Map @buffer var name to its allocated GPU buffer (positional alignment). */
|
|
119
|
+
function mapBuffersByName(
|
|
120
|
+
resources: DiscoveredResource[],
|
|
121
|
+
playResources: PlayResources,
|
|
122
|
+
): Map<string, GPUBuffer> {
|
|
123
|
+
const bufferVars = resources.filter(r => r.kind === "buffer");
|
|
124
|
+
return new Map(
|
|
125
|
+
bufferVars.map((r, i) => [r.varName, playResources.buffers[i]]),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Map @buffer var name to its VarReflection (type tree, address space, etc.). */
|
|
130
|
+
function mapBufferReflections(
|
|
131
|
+
ast: WeslAST,
|
|
132
|
+
resources: DiscoveredResource[],
|
|
133
|
+
): Map<string, VarReflection> {
|
|
134
|
+
return new Map(
|
|
135
|
+
resources
|
|
136
|
+
.filter(r => r.kind === "buffer")
|
|
137
|
+
.map(r => [r.varName, varReflection(ast, r.varName)]),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function computeAutoValues(state: RenderState): AutoValues {
|
|
142
|
+
return {
|
|
143
|
+
resolution: [state.canvas.width, state.canvas.height],
|
|
144
|
+
time: 0,
|
|
145
|
+
delta_time: 0,
|
|
146
|
+
frame: 0,
|
|
147
|
+
mouse_pos: [0, 0],
|
|
148
|
+
mouse_delta: [0, 0],
|
|
149
|
+
mouse_button: 0,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AutoValues,
|
|
3
|
+
linkAndCreatePipeline,
|
|
4
|
+
renderFrame,
|
|
5
|
+
withErrorScopes,
|
|
6
|
+
writeUniforms,
|
|
7
|
+
} from "wesl-gpu";
|
|
8
|
+
import type { DiscoveredResource } from "wesl-reflect";
|
|
9
|
+
import type { BuildResult, LinkOptions, RenderState } from "./Renderer.ts";
|
|
10
|
+
import {
|
|
11
|
+
type BuildBranchParams,
|
|
12
|
+
destroyPlayResources,
|
|
13
|
+
disposeResources,
|
|
14
|
+
mergeResourcePlugins,
|
|
15
|
+
} from "./RenderResources.ts";
|
|
16
|
+
|
|
17
|
+
/** Animation playback state. */
|
|
18
|
+
export interface PlaybackState {
|
|
19
|
+
isPlaying: boolean;
|
|
20
|
+
startTime: number;
|
|
21
|
+
pausedDuration: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface BuildRenderPipelineParams {
|
|
25
|
+
device: GPUDevice;
|
|
26
|
+
fragmentSource: string;
|
|
27
|
+
presentationFormat: GPUTextureFormat;
|
|
28
|
+
pipelineLayout: GPUPipelineLayout;
|
|
29
|
+
resources: DiscoveredResource[];
|
|
30
|
+
options?: LinkOptions;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Build the render pipeline and install it on state. */
|
|
34
|
+
export async function buildFragment(
|
|
35
|
+
p: BuildBranchParams,
|
|
36
|
+
): Promise<BuildResult> {
|
|
37
|
+
const newPipeline = await buildRenderPipeline({
|
|
38
|
+
device: p.state.device,
|
|
39
|
+
fragmentSource: p.shaderSource,
|
|
40
|
+
presentationFormat: p.state.presentationFormat,
|
|
41
|
+
pipelineLayout: p.pipelineLayout,
|
|
42
|
+
resources: p.resources,
|
|
43
|
+
options: p.options,
|
|
44
|
+
}).catch(err => {
|
|
45
|
+
destroyPlayResources(p.playResources);
|
|
46
|
+
p.state.pipeline = undefined;
|
|
47
|
+
throw err;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
disposeResources(p.state);
|
|
51
|
+
p.state.pipeline = newPipeline;
|
|
52
|
+
p.state.bindGroup = p.bindGroup;
|
|
53
|
+
p.state.compute = undefined;
|
|
54
|
+
p.state.resourceTextures = p.playResources.textures;
|
|
55
|
+
p.state.resourceBuffers = p.playResources.buffers;
|
|
56
|
+
return { layout: p.layout, mode: "fragment" };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Render a single frame (used when paused). */
|
|
60
|
+
export function renderOnce(state: RenderState, playback: PlaybackState): void {
|
|
61
|
+
if (!state.pipeline) return;
|
|
62
|
+
const time = calculateTime(playback);
|
|
63
|
+
const { mouse, canvas } = state;
|
|
64
|
+
submitFrame(state, {
|
|
65
|
+
resolution: [canvas.width, canvas.height],
|
|
66
|
+
time,
|
|
67
|
+
delta_time: 0,
|
|
68
|
+
frame: state.frameCount,
|
|
69
|
+
mouse_pos: mouse.pos,
|
|
70
|
+
mouse_delta: [0, 0],
|
|
71
|
+
mouse_button: mouse.button,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Start the render loop. Returns a stop function. */
|
|
76
|
+
export function startRenderLoop(
|
|
77
|
+
state: RenderState,
|
|
78
|
+
playback: PlaybackState,
|
|
79
|
+
): () => void {
|
|
80
|
+
let animationId: number;
|
|
81
|
+
let lastTime = 0;
|
|
82
|
+
|
|
83
|
+
function render(): void {
|
|
84
|
+
if (!state.pipeline) {
|
|
85
|
+
animationId = requestAnimationFrame(render);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const time = calculateTime(playback);
|
|
90
|
+
const delta_time = time - lastTime;
|
|
91
|
+
lastTime = time;
|
|
92
|
+
|
|
93
|
+
const { mouse, canvas } = state;
|
|
94
|
+
const auto: AutoValues = {
|
|
95
|
+
resolution: [canvas.width, canvas.height],
|
|
96
|
+
time,
|
|
97
|
+
delta_time,
|
|
98
|
+
frame: state.frameCount,
|
|
99
|
+
mouse_pos: mouse.pos,
|
|
100
|
+
mouse_delta: mouse.delta,
|
|
101
|
+
mouse_button: mouse.button,
|
|
102
|
+
};
|
|
103
|
+
// Reset per-frame deltas after reading
|
|
104
|
+
mouse.delta = [0, 0];
|
|
105
|
+
|
|
106
|
+
submitFrame(state, auto);
|
|
107
|
+
animationId = requestAnimationFrame(render);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
animationId = requestAnimationFrame(render);
|
|
111
|
+
return () => cancelAnimationFrame(animationId);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Seconds elapsed since playback start, frozen while paused. */
|
|
115
|
+
export function calculateTime(playback: PlaybackState): number {
|
|
116
|
+
const currentTime = playback.isPlaying
|
|
117
|
+
? performance.now()
|
|
118
|
+
: playback.startTime + playback.pausedDuration;
|
|
119
|
+
return (currentTime - playback.startTime) / 1000;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Link the shader and create the render pipeline, surfacing both JS and GPU
|
|
123
|
+
* validation errors as a thrown rejection. */
|
|
124
|
+
async function buildRenderPipeline(
|
|
125
|
+
p: BuildRenderPipelineParams,
|
|
126
|
+
): Promise<GPURenderPipeline> {
|
|
127
|
+
const config = mergeResourcePlugins(p.options?.config, p.resources);
|
|
128
|
+
return withErrorScopes(p.device, () =>
|
|
129
|
+
linkAndCreatePipeline({
|
|
130
|
+
device: p.device,
|
|
131
|
+
fragmentSource: p.fragmentSource,
|
|
132
|
+
format: p.presentationFormat,
|
|
133
|
+
layout: p.pipelineLayout,
|
|
134
|
+
...p.options,
|
|
135
|
+
config,
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Write uniforms, render to the current swap-chain texture, and tick frameCount. */
|
|
141
|
+
function submitFrame(state: RenderState, auto: AutoValues): void {
|
|
142
|
+
const { device, context, bindGroup } = state;
|
|
143
|
+
writeUniforms(device, state.uniformState, auto);
|
|
144
|
+
const targetView = context.getCurrentTexture().createView();
|
|
145
|
+
renderFrame({ device, pipeline: state.pipeline!, bindGroup, targetView });
|
|
146
|
+
state.frameCount++;
|
|
147
|
+
}
|
package/src/PlaybackControls.ts
CHANGED
|
@@ -2,12 +2,21 @@ import backToStartSvg from "./icons/backToStart.svg?raw";
|
|
|
2
2
|
import expandSvg from "./icons/expand.svg?raw";
|
|
3
3
|
import pauseSvg from "./icons/pause.svg?raw";
|
|
4
4
|
import playSvg from "./icons/play.svg?raw";
|
|
5
|
+
import refreshSvg from "./icons/refresh.svg?raw";
|
|
5
6
|
import shrinkSvg from "./icons/shrink.svg?raw";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
export type ControlsMode = "render" | "compute";
|
|
9
|
+
|
|
10
|
+
/** Playback controls overlay for wgsl-play.
|
|
11
|
+
*
|
|
12
|
+
* In "render" mode (default): play/pause + rewind + fullscreen.
|
|
13
|
+
* In "compute" mode: refresh (re-dispatch) + fullscreen — play/pause and
|
|
14
|
+
* rewind are hidden because compute mode is not animation-driven. */
|
|
8
15
|
export class PlaybackControls {
|
|
9
16
|
private container: HTMLDivElement;
|
|
10
17
|
private playPauseBtn: HTMLButtonElement;
|
|
18
|
+
private rewindBtn: HTMLButtonElement;
|
|
19
|
+
private refreshBtn: HTMLButtonElement;
|
|
11
20
|
private fullscreenBtn: HTMLButtonElement;
|
|
12
21
|
private playing = true;
|
|
13
22
|
|
|
@@ -17,25 +26,25 @@ export class PlaybackControls {
|
|
|
17
26
|
onPause: () => void,
|
|
18
27
|
onRewind: () => void,
|
|
19
28
|
onFullscreen: () => void,
|
|
29
|
+
onRefresh: () => void,
|
|
20
30
|
) {
|
|
21
31
|
this.container = document.createElement("div");
|
|
22
32
|
this.container.className = "controls";
|
|
23
33
|
|
|
24
|
-
this.fullscreenBtn =
|
|
25
|
-
this.
|
|
26
|
-
this.
|
|
27
|
-
|
|
28
|
-
const rewindBtn = document.createElement("button");
|
|
29
|
-
rewindBtn.innerHTML = backToStartSvg;
|
|
30
|
-
rewindBtn.addEventListener("click", onRewind);
|
|
31
|
-
|
|
32
|
-
this.playPauseBtn = document.createElement("button");
|
|
33
|
-
this.playPauseBtn.innerHTML = pauseSvg;
|
|
34
|
-
this.playPauseBtn.addEventListener("click", () =>
|
|
34
|
+
this.fullscreenBtn = makeBtn(expandSvg, onFullscreen);
|
|
35
|
+
this.rewindBtn = makeBtn(backToStartSvg, onRewind);
|
|
36
|
+
this.playPauseBtn = makeBtn(pauseSvg, () =>
|
|
35
37
|
this.playing ? onPause() : onPlay(),
|
|
36
38
|
);
|
|
39
|
+
this.refreshBtn = makeBtn(refreshSvg, onRefresh);
|
|
40
|
+
this.refreshBtn.hidden = true;
|
|
37
41
|
|
|
38
|
-
this.container.append(
|
|
42
|
+
this.container.append(
|
|
43
|
+
this.fullscreenBtn,
|
|
44
|
+
this.rewindBtn,
|
|
45
|
+
this.playPauseBtn,
|
|
46
|
+
this.refreshBtn,
|
|
47
|
+
);
|
|
39
48
|
shadow.appendChild(this.container);
|
|
40
49
|
}
|
|
41
50
|
|
|
@@ -48,6 +57,13 @@ export class PlaybackControls {
|
|
|
48
57
|
this.fullscreenBtn.innerHTML = isFullscreen ? shrinkSvg : expandSvg;
|
|
49
58
|
}
|
|
50
59
|
|
|
60
|
+
setMode(mode: ControlsMode): void {
|
|
61
|
+
const isCompute = mode === "compute";
|
|
62
|
+
this.rewindBtn.hidden = isCompute;
|
|
63
|
+
this.playPauseBtn.hidden = isCompute;
|
|
64
|
+
this.refreshBtn.hidden = !isCompute;
|
|
65
|
+
}
|
|
66
|
+
|
|
51
67
|
show(): void {
|
|
52
68
|
this.container.style.display = "";
|
|
53
69
|
}
|
|
@@ -56,3 +72,10 @@ export class PlaybackControls {
|
|
|
56
72
|
this.container.style.display = "none";
|
|
57
73
|
}
|
|
58
74
|
}
|
|
75
|
+
|
|
76
|
+
function makeBtn(html: string, handler: () => void): HTMLButtonElement {
|
|
77
|
+
const btn = document.createElement("button");
|
|
78
|
+
btn.innerHTML = html;
|
|
79
|
+
btn.addEventListener("click", handler);
|
|
80
|
+
return btn;
|
|
81
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createPlayResources,
|
|
3
|
+
createUniformBuffer,
|
|
4
|
+
type PlayResources,
|
|
5
|
+
type ResolveUserTexture,
|
|
6
|
+
scanUniforms,
|
|
7
|
+
type UniformBufferState,
|
|
8
|
+
} from "wesl-gpu";
|
|
9
|
+
import {
|
|
10
|
+
type AnnotatedLayout,
|
|
11
|
+
annotatedResourcesPlugin,
|
|
12
|
+
type DiscoveredResource,
|
|
13
|
+
} from "wesl-reflect";
|
|
14
|
+
import type { LinkOptions, RenderState } from "./Renderer.ts";
|
|
15
|
+
|
|
16
|
+
export interface PrepareResourcesParams {
|
|
17
|
+
state: RenderState;
|
|
18
|
+
shaderSource: string;
|
|
19
|
+
pkg: string;
|
|
20
|
+
root: string;
|
|
21
|
+
resources: DiscoveredResource[];
|
|
22
|
+
resolveTexture: ResolveUserTexture;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ResourceSetup {
|
|
26
|
+
playResources: PlayResources;
|
|
27
|
+
pipelineLayout: GPUPipelineLayout;
|
|
28
|
+
bindGroup: GPUBindGroup;
|
|
29
|
+
layout: AnnotatedLayout | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Common params shared by both fragment and compute build paths. */
|
|
33
|
+
export interface BuildBranchParams {
|
|
34
|
+
state: RenderState;
|
|
35
|
+
resources: DiscoveredResource[];
|
|
36
|
+
shaderSource: string;
|
|
37
|
+
pipelineLayout: GPUPipelineLayout;
|
|
38
|
+
bindGroup: GPUBindGroup;
|
|
39
|
+
playResources: PlayResources;
|
|
40
|
+
layout: AnnotatedLayout | null;
|
|
41
|
+
options?: LinkOptions;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Default allocation size (bytes) for runtime-sized `@buffer` arrays. The
|
|
45
|
+
* WGSL gives no element count, so the playground picks one. 1024 bytes covers
|
|
46
|
+
* 256 f32s or 64 vec4f, enough for casual playground use. */
|
|
47
|
+
const defaultRuntimeArrayBytes = 1024;
|
|
48
|
+
|
|
49
|
+
/** Allocate user-declared resources, rebuild the uniform buffer to match the
|
|
50
|
+
* shader's uniform layout, and assemble the pipeline layout + bind group. */
|
|
51
|
+
export async function prepareResources(
|
|
52
|
+
p: PrepareResourcesParams,
|
|
53
|
+
): Promise<ResourceSetup> {
|
|
54
|
+
const { state, resources, resolveTexture } = p;
|
|
55
|
+
const { device } = state;
|
|
56
|
+
const scan = scanUniforms(p.shaderSource, `${p.pkg}::${p.root}`);
|
|
57
|
+
const playResources = await createPlayResources({
|
|
58
|
+
device,
|
|
59
|
+
resources,
|
|
60
|
+
startBinding: 1,
|
|
61
|
+
resolveTexture,
|
|
62
|
+
minBufferBytes: defaultRuntimeArrayBytes,
|
|
63
|
+
});
|
|
64
|
+
state.uniformState.buffer.destroy();
|
|
65
|
+
state.uniformState = createUniformBuffer(device, scan.layout);
|
|
66
|
+
const bindGroupLayout = buildBindGroupLayout(device, playResources);
|
|
67
|
+
const pipelineLayout = device.createPipelineLayout({
|
|
68
|
+
bindGroupLayouts: [bindGroupLayout],
|
|
69
|
+
});
|
|
70
|
+
const bindGroup = buildBindGroup(
|
|
71
|
+
device,
|
|
72
|
+
bindGroupLayout,
|
|
73
|
+
state.uniformState,
|
|
74
|
+
playResources,
|
|
75
|
+
);
|
|
76
|
+
return { playResources, pipelineLayout, bindGroup, layout: scan.layout };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Destroy all textures and buffers owned by a PlayResources bundle. */
|
|
80
|
+
export function destroyPlayResources(r: PlayResources): void {
|
|
81
|
+
for (const t of r.textures) t.destroy();
|
|
82
|
+
for (const b of r.buffers) b.destroy();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Dispose GPU resources owned by the current compile (textures + storage buffers). */
|
|
86
|
+
export function disposeResources(state: RenderState): void {
|
|
87
|
+
for (const t of state.resourceTextures) t.destroy();
|
|
88
|
+
for (const b of state.resourceBuffers) b.destroy();
|
|
89
|
+
state.resourceTextures = [];
|
|
90
|
+
state.resourceBuffers = [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Add the @buffer/@texture/@sampler plugin to the user-supplied config. */
|
|
94
|
+
export function mergeResourcePlugins(
|
|
95
|
+
userConfig: LinkOptions["config"],
|
|
96
|
+
resources: DiscoveredResource[],
|
|
97
|
+
): LinkOptions["config"] {
|
|
98
|
+
if (resources.length === 0) return userConfig;
|
|
99
|
+
const plugins = [
|
|
100
|
+
...(userConfig?.plugins ?? []),
|
|
101
|
+
annotatedResourcesPlugin(resources, 1),
|
|
102
|
+
];
|
|
103
|
+
return { ...userConfig, plugins };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildBindGroupLayout(
|
|
107
|
+
device: GPUDevice,
|
|
108
|
+
resources: PlayResources,
|
|
109
|
+
): GPUBindGroupLayout {
|
|
110
|
+
const visibility = GPUShaderStage.COMPUTE | GPUShaderStage.FRAGMENT;
|
|
111
|
+
return device.createBindGroupLayout({
|
|
112
|
+
entries: [
|
|
113
|
+
{ binding: 0, visibility, buffer: {} },
|
|
114
|
+
...resources.layoutEntries,
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function buildBindGroup(
|
|
120
|
+
device: GPUDevice,
|
|
121
|
+
layout: GPUBindGroupLayout,
|
|
122
|
+
uniformState: UniformBufferState,
|
|
123
|
+
resources: PlayResources,
|
|
124
|
+
): GPUBindGroup {
|
|
125
|
+
return device.createBindGroup({
|
|
126
|
+
layout,
|
|
127
|
+
entries: [
|
|
128
|
+
{ binding: 0, resource: { buffer: uniformState.buffer } },
|
|
129
|
+
...resources.entries,
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
}
|