@aardworx/wombat.rendering 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +67 -0
- package/src/commands/clear.ts +66 -0
- package/src/commands/index.ts +11 -0
- package/src/commands/render.ts +106 -0
- package/src/core/acquire.ts +22 -0
- package/src/core/adaptiveResource.ts +88 -0
- package/src/core/buffer.ts +45 -0
- package/src/core/bufferView.ts +26 -0
- package/src/core/clear.ts +16 -0
- package/src/core/command.ts +50 -0
- package/src/core/drawCall.ts +22 -0
- package/src/core/framebuffer.ts +37 -0
- package/src/core/framebufferSignature.ts +19 -0
- package/src/core/index.ts +116 -0
- package/src/core/pipelineState.ts +64 -0
- package/src/core/renderContext.ts +48 -0
- package/src/core/renderObject.ts +57 -0
- package/src/core/renderTask.ts +17 -0
- package/src/core/renderTree.ts +29 -0
- package/src/core/sampler.ts +12 -0
- package/src/core/shader.ts +28 -0
- package/src/core/texture.ts +63 -0
- package/src/index.ts +18 -0
- package/src/resources/adaptiveBuffer.ts +135 -0
- package/src/resources/adaptiveSampler.ts +82 -0
- package/src/resources/adaptiveTexture.ts +269 -0
- package/src/resources/framebuffer.ts +163 -0
- package/src/resources/framebufferSignature.ts +36 -0
- package/src/resources/index.ts +82 -0
- package/src/resources/mipGen.ts +148 -0
- package/src/resources/preparedComputeShader.ts +426 -0
- package/src/resources/preparedRenderObject.ts +498 -0
- package/src/resources/renderPipeline.ts +148 -0
- package/src/resources/shaderDiagnostics.ts +52 -0
- package/src/resources/sourceMapDecoder.ts +133 -0
- package/src/resources/uniformBuffer.ts +170 -0
- package/src/resources/webgpuFlags.ts +43 -0
- package/src/runtime/copy.ts +13 -0
- package/src/runtime/index.ts +24 -0
- package/src/runtime/renderTask.ts +137 -0
- package/src/runtime/renderTo.ts +154 -0
- package/src/runtime/runtime.ts +113 -0
- package/src/runtime/scenePass.ts +273 -0
- package/src/window/canvas.ts +252 -0
- package/src/window/index.ts +14 -0
- package/src/window/loop.ts +62 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
// prepareComputeShader — turn a `ComputeShader` (single compute
|
|
2
|
+
// stage, peer of `Effect`) into an imperative dispatch surface.
|
|
3
|
+
//
|
|
4
|
+
// Mirrors Aardvark.GPGPU's `IComputeShader` + mutable
|
|
5
|
+
// `IComputeInputBinding` pattern: callers build a binding by name,
|
|
6
|
+
// `set` values/buffers/textures/samplers into it imperatively, then
|
|
7
|
+
// `dispatch` to encode + submit (or hand an encoder for batching).
|
|
8
|
+
//
|
|
9
|
+
// Unlike `prepareRenderObject`, none of the inputs are `aval<T>`. The
|
|
10
|
+
// binding owns its own mutable state; uniform writes are byte-poked
|
|
11
|
+
// into a staging `ArrayBuffer` and uploaded on dispatch.
|
|
12
|
+
//
|
|
13
|
+
// Supported binding kinds (from the program interface):
|
|
14
|
+
// - One uniform block per group: `setUniform(name, value)`.
|
|
15
|
+
// - Storage buffers: `setBuffer(name, GPUBuffer)`.
|
|
16
|
+
// - Sampled textures: `setTexture(name, GPUTextureView | GPUTexture)`.
|
|
17
|
+
// - Samplers: `setSampler(name, GPUSampler)`.
|
|
18
|
+
//
|
|
19
|
+
// Storage textures aren't covered here yet — separate gap.
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
type ComputeShader,
|
|
23
|
+
type ProgramInterface,
|
|
24
|
+
} from "../core/index.js";
|
|
25
|
+
import type { StorageTextureFormat, Type } from "@aardworx/wombat.shader/ir";
|
|
26
|
+
import { ShaderStage } from "./webgpuFlags.js";
|
|
27
|
+
import { installShaderDiagnostics } from "./shaderDiagnostics.js";
|
|
28
|
+
import { makePackedView, writeField, type PackedView } from "./uniformBuffer.js";
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Bind-group-entry descriptions (mirrors PreparedRenderObject's shape, but
|
|
32
|
+
// over plain GPU handles instead of AdaptiveResource<...>).
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
type EntryDesc =
|
|
36
|
+
| { kind: "ubuf"; binding: number; uniformBlock: import("../core/index.js").UniformBlockInfo }
|
|
37
|
+
| { kind: "sbuf"; binding: number; name: string; access: "read" | "read_write" }
|
|
38
|
+
| { kind: "tex"; binding: number; name: string; sampleType: GPUTextureSampleType }
|
|
39
|
+
| { kind: "sampler";binding: number; name: string }
|
|
40
|
+
| { kind: "stex"; binding: number; name: string; format: GPUTextureFormat; access: GPUStorageTextureAccess };
|
|
41
|
+
|
|
42
|
+
interface GroupDesc {
|
|
43
|
+
readonly group: number;
|
|
44
|
+
readonly layout: GPUBindGroupLayout;
|
|
45
|
+
readonly entries: readonly EntryDesc[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function storageAccessToGPU(a: "read" | "write" | "read_write"): GPUStorageTextureAccess {
|
|
49
|
+
switch (a) {
|
|
50
|
+
case "read": return "read-only";
|
|
51
|
+
case "write": return "write-only";
|
|
52
|
+
case "read_write": return "read-write";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function sampleTypeFor(type: Type): GPUTextureSampleType {
|
|
57
|
+
if (type.kind === "Texture") {
|
|
58
|
+
if (type.comparison === true) return "depth";
|
|
59
|
+
const s = type.sampled;
|
|
60
|
+
if (s.kind === "Int") return s.signed ? "sint" : "uint";
|
|
61
|
+
return "float";
|
|
62
|
+
}
|
|
63
|
+
return "float";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildGroups(device: GPUDevice, descs: readonly EntryDesc[][]): GroupDesc[] {
|
|
67
|
+
const out: GroupDesc[] = [];
|
|
68
|
+
for (let g = 0; g < descs.length; g++) {
|
|
69
|
+
const entries = (descs[g] ?? []).slice().sort((a, b) => a.binding - b.binding);
|
|
70
|
+
const layoutEntries: GPUBindGroupLayoutEntry[] = entries.map(e => {
|
|
71
|
+
const visibility = ShaderStage.COMPUTE;
|
|
72
|
+
switch (e.kind) {
|
|
73
|
+
case "ubuf": return { binding: e.binding, visibility, buffer: { type: "uniform" } };
|
|
74
|
+
case "sbuf": return {
|
|
75
|
+
binding: e.binding, visibility,
|
|
76
|
+
buffer: { type: e.access === "read_write" ? "storage" : "read-only-storage" },
|
|
77
|
+
};
|
|
78
|
+
case "tex": return { binding: e.binding, visibility, texture: { sampleType: e.sampleType } };
|
|
79
|
+
case "sampler": return { binding: e.binding, visibility, sampler: { type: "filtering" } };
|
|
80
|
+
case "stex": return { binding: e.binding, visibility, storageTexture: { access: e.access, format: e.format } };
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
const layout = device.createBindGroupLayout({ entries: layoutEntries });
|
|
84
|
+
out.push({ group: g, layout, entries });
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// ComputeInputBinding — mutable, name-keyed state for one shader.
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
export interface DispatchSize {
|
|
94
|
+
readonly x: number;
|
|
95
|
+
readonly y: number;
|
|
96
|
+
readonly z: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface UboField {
|
|
100
|
+
readonly offset: number;
|
|
101
|
+
readonly type: import("../core/index.js").UniformFieldInfo["type"];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface UboState {
|
|
105
|
+
readonly group: number;
|
|
106
|
+
readonly binding: number;
|
|
107
|
+
readonly view: PackedView;
|
|
108
|
+
readonly fields: ReadonlyMap<string, UboField>;
|
|
109
|
+
buffer: GPUBuffer | undefined;
|
|
110
|
+
dirty: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export class ComputeInputBinding {
|
|
114
|
+
private readonly _ubos = new Map<string, UboState>(); // ubo name → state
|
|
115
|
+
private readonly _ubosByBinding = new Map<string, UboState>(); // "g:b" → state
|
|
116
|
+
private readonly _buffers = new Map<string, GPUBuffer>(); // name → buffer
|
|
117
|
+
private readonly _textures = new Map<string, GPUTextureView>(); // name → view
|
|
118
|
+
private readonly _samplers = new Map<string, GPUSampler>(); // name → sampler
|
|
119
|
+
private readonly _storageTextures = new Map<string, GPUTextureView>(); // name → view (storage)
|
|
120
|
+
/** Names declared by the shader as storage textures (vs sampled). */
|
|
121
|
+
private readonly _storageTextureNames: ReadonlySet<string>;
|
|
122
|
+
|
|
123
|
+
/** @internal — built by `PreparedComputeShader`. */
|
|
124
|
+
constructor(
|
|
125
|
+
private readonly device: GPUDevice,
|
|
126
|
+
private readonly groups: readonly GroupDesc[],
|
|
127
|
+
private readonly iface: ProgramInterface,
|
|
128
|
+
private readonly label: string | undefined,
|
|
129
|
+
) {
|
|
130
|
+
this._storageTextureNames = new Set(
|
|
131
|
+
iface.samplers.filter(s => s.type.kind === "StorageTexture").map(s => s.name),
|
|
132
|
+
);
|
|
133
|
+
for (const ub of iface.uniformBlocks) {
|
|
134
|
+
const view = makePackedView(ub.size);
|
|
135
|
+
const fields = new Map<string, UboField>(
|
|
136
|
+
ub.fields.map(f => [f.name, { offset: f.offset, type: f.type }]),
|
|
137
|
+
);
|
|
138
|
+
const state: UboState = {
|
|
139
|
+
group: ub.group, binding: ub.slot, view, fields,
|
|
140
|
+
buffer: undefined, dirty: true,
|
|
141
|
+
};
|
|
142
|
+
this._ubos.set(ub.name, state);
|
|
143
|
+
this._ubosByBinding.set(`${ub.group}:${ub.slot}`, state);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Write a uniform value by name. Searches all uniform blocks; the
|
|
149
|
+
* first hit wins. Throws if the name isn't a uniform field.
|
|
150
|
+
* Accepted types: `number`, `Float32Array`/`Int32Array`/`Uint32Array`,
|
|
151
|
+
* any wombat.base packed value (`{ _data: TypedArray }`).
|
|
152
|
+
*/
|
|
153
|
+
setUniform(name: string, value: unknown): this {
|
|
154
|
+
for (const ub of this._ubos.values()) {
|
|
155
|
+
const f = ub.fields.get(name);
|
|
156
|
+
if (f === undefined) continue;
|
|
157
|
+
writeField(ub.view, f.offset, value, f.type);
|
|
158
|
+
ub.dirty = true;
|
|
159
|
+
return this;
|
|
160
|
+
}
|
|
161
|
+
throw new Error(`ComputeInputBinding: no uniform field "${name}"`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
setBuffer(name: string, buffer: GPUBuffer): this {
|
|
165
|
+
if (!this.iface.storageBuffers.some(s => s.name === name)) {
|
|
166
|
+
throw new Error(`ComputeInputBinding: no storage buffer "${name}"`);
|
|
167
|
+
}
|
|
168
|
+
this._buffers.set(name, buffer);
|
|
169
|
+
return this;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
setTexture(name: string, source: GPUTexture | GPUTextureView): this {
|
|
173
|
+
if (!this.iface.textures.some(t => t.name === name)) {
|
|
174
|
+
throw new Error(`ComputeInputBinding: no texture "${name}"`);
|
|
175
|
+
}
|
|
176
|
+
const view = "createView" in source ? source.createView() : source;
|
|
177
|
+
this._textures.set(name, view);
|
|
178
|
+
return this;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Bind a storage-texture view (write/read/read_write per the
|
|
183
|
+
* shader declaration). `view` should reference a `GPUTexture`
|
|
184
|
+
* created with `STORAGE_BINDING` usage in the format the shader
|
|
185
|
+
* expects.
|
|
186
|
+
*/
|
|
187
|
+
setStorageTexture(name: string, source: GPUTexture | GPUTextureView): this {
|
|
188
|
+
if (!this._storageTextureNames.has(name)) {
|
|
189
|
+
throw new Error(`ComputeInputBinding: no storage texture "${name}"`);
|
|
190
|
+
}
|
|
191
|
+
const view = "createView" in source ? source.createView() : source;
|
|
192
|
+
this._storageTextures.set(name, view);
|
|
193
|
+
return this;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
setSampler(name: string, sampler: GPUSampler): this {
|
|
197
|
+
if (!this.iface.samplers.some(s => s.name === name)) {
|
|
198
|
+
throw new Error(`ComputeInputBinding: no sampler "${name}"`);
|
|
199
|
+
}
|
|
200
|
+
this._samplers.set(name, sampler);
|
|
201
|
+
return this;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** @internal — flush dirty UBOs and build current bind groups. */
|
|
205
|
+
flushAndBuildBindGroups(): GPUBindGroup[] {
|
|
206
|
+
for (const ub of this._ubos.values()) {
|
|
207
|
+
if (ub.buffer === undefined) {
|
|
208
|
+
ub.buffer = this.device.createBuffer({
|
|
209
|
+
size: ub.view.bytes.byteLength,
|
|
210
|
+
usage: 0x40 | 0x8, // UNIFORM | COPY_DST
|
|
211
|
+
...(this.label !== undefined ? { label: `${this.label}.ubo` } : {}),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
if (ub.dirty) {
|
|
215
|
+
this.device.queue.writeBuffer(ub.buffer, 0, ub.view.bytes);
|
|
216
|
+
ub.dirty = false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const out: GPUBindGroup[] = [];
|
|
221
|
+
for (const g of this.groups) {
|
|
222
|
+
const entries: GPUBindGroupEntry[] = g.entries.map(e => {
|
|
223
|
+
switch (e.kind) {
|
|
224
|
+
case "ubuf": {
|
|
225
|
+
const ub = this._ubosByBinding.get(`${g.group}:${e.binding}`);
|
|
226
|
+
if (!ub || ub.buffer === undefined) {
|
|
227
|
+
throw new Error(`ComputeInputBinding.dispatch: UBO at group=${g.group} binding=${e.binding} not initialised`);
|
|
228
|
+
}
|
|
229
|
+
return { binding: e.binding, resource: { buffer: ub.buffer } };
|
|
230
|
+
}
|
|
231
|
+
case "sbuf": {
|
|
232
|
+
const buf = this._buffers.get(e.name);
|
|
233
|
+
if (buf === undefined) throw new Error(`ComputeInputBinding.dispatch: missing storage buffer "${e.name}"`);
|
|
234
|
+
return { binding: e.binding, resource: { buffer: buf } };
|
|
235
|
+
}
|
|
236
|
+
case "tex": {
|
|
237
|
+
const view = this._textures.get(e.name);
|
|
238
|
+
if (view === undefined) throw new Error(`ComputeInputBinding.dispatch: missing texture "${e.name}"`);
|
|
239
|
+
return { binding: e.binding, resource: view };
|
|
240
|
+
}
|
|
241
|
+
case "sampler": {
|
|
242
|
+
const samp = this._samplers.get(e.name);
|
|
243
|
+
if (samp === undefined) throw new Error(`ComputeInputBinding.dispatch: missing sampler "${e.name}"`);
|
|
244
|
+
return { binding: e.binding, resource: samp };
|
|
245
|
+
}
|
|
246
|
+
case "stex": {
|
|
247
|
+
const view = this._storageTextures.get(e.name);
|
|
248
|
+
if (view === undefined) throw new Error(`ComputeInputBinding.dispatch: missing storage texture "${e.name}"`);
|
|
249
|
+
return { binding: e.binding, resource: view };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
out.push(this.device.createBindGroup({ layout: g.layout, entries }));
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Free the binding's owned UBO buffers. Storage buffers / textures / samplers are caller-owned. */
|
|
259
|
+
dispose(): void {
|
|
260
|
+
for (const ub of this._ubos.values()) {
|
|
261
|
+
if (ub.buffer !== undefined) {
|
|
262
|
+
ub.buffer.destroy();
|
|
263
|
+
ub.buffer = undefined;
|
|
264
|
+
}
|
|
265
|
+
ub.dirty = true;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
// PreparedComputeShader
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
export interface PrepareComputeShaderOptions {
|
|
275
|
+
readonly label?: string;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export class PreparedComputeShader {
|
|
279
|
+
/** @internal */
|
|
280
|
+
constructor(
|
|
281
|
+
private readonly device: GPUDevice,
|
|
282
|
+
public readonly pipeline: GPUComputePipeline,
|
|
283
|
+
public readonly interface_: ProgramInterface,
|
|
284
|
+
private readonly groups: readonly GroupDesc[],
|
|
285
|
+
private readonly label: string | undefined,
|
|
286
|
+
) {}
|
|
287
|
+
|
|
288
|
+
/** Same shape as Aardvark's `IComputeShader.Interface` — useful for tooling. */
|
|
289
|
+
get programInterface(): ProgramInterface { return this.interface_; }
|
|
290
|
+
|
|
291
|
+
/** Allocate a fresh, empty binding. */
|
|
292
|
+
createInputBinding(): ComputeInputBinding {
|
|
293
|
+
return new ComputeInputBinding(this.device, this.groups, this.interface_, this.label);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Encode `Bind + SetInput + Dispatch` into the supplied encoder.
|
|
298
|
+
* Use this when batching compute work alongside other GPU commands
|
|
299
|
+
* — caller submits.
|
|
300
|
+
*/
|
|
301
|
+
encode(encoder: GPUCommandEncoder, binding: ComputeInputBinding, groups: DispatchSize): void {
|
|
302
|
+
const bindGroups = binding.flushAndBuildBindGroups();
|
|
303
|
+
const pass = encoder.beginComputePass({
|
|
304
|
+
...(this.label !== undefined ? { label: this.label } : {}),
|
|
305
|
+
});
|
|
306
|
+
pass.setPipeline(this.pipeline);
|
|
307
|
+
for (let i = 0; i < bindGroups.length; i++) pass.setBindGroup(i, bindGroups[i]!);
|
|
308
|
+
pass.dispatchWorkgroups(groups.x, groups.y, groups.z);
|
|
309
|
+
pass.end();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* One-shot dispatch — builds an encoder, encodes, submits, awaits
|
|
314
|
+
* `queue.onSubmittedWorkDone()`. Aardvark's `IComputeShader.Invoke`
|
|
315
|
+
* shape.
|
|
316
|
+
*/
|
|
317
|
+
async dispatch(binding: ComputeInputBinding, groups: DispatchSize): Promise<void> {
|
|
318
|
+
const encoder = this.device.createCommandEncoder({
|
|
319
|
+
...(this.label !== undefined ? { label: `${this.label}.dispatch` } : {}),
|
|
320
|
+
});
|
|
321
|
+
this.encode(encoder, binding, groups);
|
|
322
|
+
this.device.queue.submit([encoder.finish()]);
|
|
323
|
+
await this.device.queue.onSubmittedWorkDone();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
// Compute pipeline cache
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
interface ComputePipelineCache {
|
|
332
|
+
pipelines: Map<string, GPUComputePipeline>;
|
|
333
|
+
modules: Map<string, GPUShaderModule>;
|
|
334
|
+
}
|
|
335
|
+
const computeCaches = new WeakMap<GPUDevice, ComputePipelineCache>();
|
|
336
|
+
function cacheFor(device: GPUDevice): ComputePipelineCache {
|
|
337
|
+
let c = computeCaches.get(device);
|
|
338
|
+
if (c === undefined) {
|
|
339
|
+
c = { pipelines: new Map(), modules: new Map() };
|
|
340
|
+
computeCaches.set(device, c);
|
|
341
|
+
}
|
|
342
|
+
return c;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
// prepareComputeShader
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
export function prepareComputeShader(
|
|
350
|
+
device: GPUDevice,
|
|
351
|
+
shader: ComputeShader,
|
|
352
|
+
opts: PrepareComputeShaderOptions = {},
|
|
353
|
+
): PreparedComputeShader {
|
|
354
|
+
const compiled = shader.compile({ target: "wgsl" });
|
|
355
|
+
const csStage = compiled.stages.find(s => s.stage === "compute");
|
|
356
|
+
if (csStage === undefined) {
|
|
357
|
+
throw new Error("prepareComputeShader: ComputeShader compiled without a compute stage");
|
|
358
|
+
}
|
|
359
|
+
const iface = compiled.interface;
|
|
360
|
+
|
|
361
|
+
const maxGroup = Math.max(
|
|
362
|
+
-1,
|
|
363
|
+
...iface.uniformBlocks.map(b => b.group),
|
|
364
|
+
...iface.samplers.map(b => b.group),
|
|
365
|
+
...iface.textures.map(b => b.group),
|
|
366
|
+
...iface.storageBuffers.map(b => b.group),
|
|
367
|
+
);
|
|
368
|
+
const perGroup: EntryDesc[][] = [];
|
|
369
|
+
for (let g = 0; g <= maxGroup; g++) perGroup.push([]);
|
|
370
|
+
|
|
371
|
+
for (const ub of iface.uniformBlocks) {
|
|
372
|
+
perGroup[ub.group]!.push({ kind: "ubuf", binding: ub.slot, uniformBlock: ub });
|
|
373
|
+
}
|
|
374
|
+
for (const t of iface.textures) {
|
|
375
|
+
perGroup[t.group]!.push({ kind: "tex", binding: t.slot, name: t.name, sampleType: sampleTypeFor(t.type) });
|
|
376
|
+
}
|
|
377
|
+
for (const s of iface.samplers) {
|
|
378
|
+
if (s.type.kind === "StorageTexture") {
|
|
379
|
+
perGroup[s.group]!.push({
|
|
380
|
+
kind: "stex", binding: s.slot, name: s.name,
|
|
381
|
+
format: s.type.format as GPUTextureFormat,
|
|
382
|
+
access: storageAccessToGPU(s.type.access),
|
|
383
|
+
});
|
|
384
|
+
} else {
|
|
385
|
+
perGroup[s.group]!.push({ kind: "sampler", binding: s.slot, name: s.name });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
for (const sb of iface.storageBuffers) {
|
|
389
|
+
perGroup[sb.group]!.push({ kind: "sbuf", binding: sb.slot, name: sb.name, access: sb.access });
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const groups = buildGroups(device, perGroup);
|
|
393
|
+
|
|
394
|
+
// Compile module + pipeline (cached per device by effect id).
|
|
395
|
+
const cache = cacheFor(device);
|
|
396
|
+
const moduleKey = csStage.source;
|
|
397
|
+
let module = cache.modules.get(moduleKey);
|
|
398
|
+
if (module === undefined) {
|
|
399
|
+
module = device.createShaderModule({
|
|
400
|
+
code: csStage.source,
|
|
401
|
+
...(opts.label !== undefined ? { label: `${opts.label}.cs` } : {}),
|
|
402
|
+
});
|
|
403
|
+
cache.modules.set(moduleKey, module);
|
|
404
|
+
installShaderDiagnostics(module, csStage.source, {
|
|
405
|
+
...(opts.label !== undefined ? { label: opts.label } : {}),
|
|
406
|
+
...(csStage.sourceMap ? { sourceMap: csStage.sourceMap } : {}),
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const pipelineKey = `${shader.id}|${csStage.entryName}|${groups.length}`;
|
|
411
|
+
let pipeline = cache.pipelines.get(pipelineKey);
|
|
412
|
+
if (pipeline === undefined) {
|
|
413
|
+
const pipelineLayout = device.createPipelineLayout({
|
|
414
|
+
bindGroupLayouts: groups.map(g => g.layout),
|
|
415
|
+
...(opts.label !== undefined ? { label: `${opts.label}.layout` } : {}),
|
|
416
|
+
});
|
|
417
|
+
pipeline = device.createComputePipeline({
|
|
418
|
+
layout: pipelineLayout,
|
|
419
|
+
compute: { module, entryPoint: csStage.entryName },
|
|
420
|
+
...(opts.label !== undefined ? { label: opts.label } : {}),
|
|
421
|
+
});
|
|
422
|
+
cache.pipelines.set(pipelineKey, pipeline);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return new PreparedComputeShader(device, pipeline, iface, groups, opts.label);
|
|
426
|
+
}
|