@aardworx/wombat.rendering 0.7.2 → 0.8.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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AAAA,oDAAoD;AAEpD,OAAO,EACL,OAAO,GAER,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,iBAAiB,GAElB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EACL,QAAQ,GAGT,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,SAAS,GAEV,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AAAA,oDAAoD;AAEpD,OAAO,EACL,OAAO,GAER,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,iBAAiB,GAElB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EACL,QAAQ,GAGT,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,SAAS,GAEV,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,cAAc,GAOf,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aardworx/wombat.rendering",
3
- "version": "0.7.2",
3
+ "version": "0.8.1",
4
4
  "description": "WebGPU rendering layer for the Wombat TypeScript stack \u2014 RenderObject/RenderTask/runtime + window glue, port of Aardvark.Rendering's lower layers on top of WebGPU and wombat.shader.",
5
5
  "license": "MIT",
6
6
  "author": "krauthaufen",
@@ -0,0 +1,510 @@
1
+ // heapScene — multi-group heap-everything render path.
2
+ //
3
+ // Public-API extraction of the experimental heap-demo (commits in
4
+ // the `experimental/heap-arch` branch document the design). One
5
+ // shared GPURenderPipeline + GPUBindGroup per pipeline-state group,
6
+ // per-draw uniforms in a storage buffer, vertex pulling from packed
7
+ // position / normal slabs, indexed draws via a shared index buffer
8
+ // and `firstInstance` routing into the heap.
9
+ //
10
+ // Per-frame uploads:
11
+ // - Globals (one UBO per group): viewProj + lightLocation. ~80 B.
12
+ // Independent of draw count.
13
+ // - DrawHeader (one storage buffer per group): only slots whose
14
+ // `aval<Trafo3d>` or `aval<V4f>` were marked since last frame
15
+ // are re-uploaded — 160 B per dirty slot.
16
+ //
17
+ // Geometry is uploaded ONCE at construction (positions + normals +
18
+ // indices, packed across all draws in a group).
19
+ //
20
+ // The user supplies a fragment-shader body per group key. The vertex
21
+ // stage is provided by this module and reads from a fixed layout:
22
+ //
23
+ // @group(0) @binding(0) var<uniform> globals: Globals;
24
+ // @group(0) @binding(1) var<storage, read> draws: array<DrawHeader>;
25
+ // @group(0) @binding(2) var<storage, read> positions: array<f32>;
26
+ // @group(0) @binding(3) var<storage, read> normals: array<f32>;
27
+ //
28
+ // struct VsOut {
29
+ // @builtin(position) clipPos: vec4<f32>,
30
+ // @location(0) worldPos: vec3<f32>,
31
+ // @location(1) normal: vec3<f32>,
32
+ // @location(2) color: vec4<f32>,
33
+ // @location(3) lightLoc: vec3<f32>,
34
+ // };
35
+ //
36
+ // User WGSL must declare `@fragment fn fs(in: VsOut) -> @location(0) vec4<f32>`.
37
+ //
38
+ // Per-group texture sets. WebGPU 1.0 has no bindless, so texture-set
39
+ // is a wedge in the group key: groups with different texture sets
40
+ // can't share a bind group. Pass a `textures: { texture, sampler }`
41
+ // per groupKey via `BuildHeapSceneOptions.texturesByGroupKey`. The
42
+ // package adds the texture (binding 4) + sampler (binding 5) to that
43
+ // group's bind-group layout; the user's FS WGSL declares them.
44
+
45
+ import { Trafo3d, V3d, V4f, type M44d } from "@aardworx/wombat.base";
46
+ import { AVal, addMarkingCallback } from "@aardworx/wombat.adaptive";
47
+ import type { aval, IDisposable } from "@aardworx/wombat.adaptive";
48
+ import type { CanvasAttachment } from "../window/index.js";
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Layouts
52
+ // ---------------------------------------------------------------------------
53
+
54
+ const GLOBALS_BYTES = 64 + 16; // mat4 + vec4
55
+ const GLOBALS_FLOATS = GLOBALS_BYTES / 4;
56
+ const DRAW_HEADER_BYTES = 64 * 2 + 16 + 16; // 2 mat4 + vec4 + (u32 + 3*pad)
57
+ const DRAW_HEADER_FLOATS = DRAW_HEADER_BYTES / 4;
58
+
59
+ function packMat44(m: M44d, dst: Float32Array, off: number): void {
60
+ // M44d._data is the row-major Float64Array; not part of the public
61
+ // type surface. toArray() round-trips through a fresh number[] which
62
+ // we copy into the f32 staging buffer below.
63
+ const r = m.toArray();
64
+ for (let i = 0; i < 16; i++) dst[off + i] = r[i]!;
65
+ }
66
+
67
+ function packGlobals(viewProj: Trafo3d, lightLocation: V3d, dst: Float32Array): void {
68
+ packMat44(viewProj.forward, dst, 0);
69
+ dst[16] = lightLocation.x as number;
70
+ dst[17] = lightLocation.y as number;
71
+ dst[18] = lightLocation.z as number;
72
+ dst[19] = 0;
73
+ }
74
+
75
+ function packDrawHeader(
76
+ modelTrafo: Trafo3d,
77
+ color: V4f,
78
+ vertexBase: number,
79
+ dst: Float32Array,
80
+ drawIndex: number,
81
+ ): void {
82
+ const off = drawIndex * DRAW_HEADER_FLOATS;
83
+ packMat44(modelTrafo.forward, dst, off + 0);
84
+ packMat44(modelTrafo.backward, dst, off + 16);
85
+ dst[off + 32] = color.x; dst[off + 33] = color.y;
86
+ dst[off + 34] = color.z; dst[off + 35] = color.w;
87
+ new Uint32Array(dst.buffer, dst.byteOffset, dst.length)[off + 36] = vertexBase;
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Shared WGSL prelude (struct + bindings + VS)
92
+ // ---------------------------------------------------------------------------
93
+
94
+ const SHADER_PRELUDE = /* wgsl */`
95
+ struct Globals {
96
+ viewProj: mat4x4<f32>,
97
+ lightLocation: vec4<f32>,
98
+ };
99
+ struct DrawHeader {
100
+ modelTrafo: mat4x4<f32>,
101
+ modelTrafoInv: mat4x4<f32>,
102
+ color: vec4<f32>,
103
+ vertexBase: u32,
104
+ _pad0: u32, _pad1: u32, _pad2: u32,
105
+ };
106
+ @group(0) @binding(0) var<uniform> globals: Globals;
107
+ @group(0) @binding(1) var<storage, read> draws: array<DrawHeader>;
108
+ @group(0) @binding(2) var<storage, read> positions: array<f32>;
109
+ @group(0) @binding(3) var<storage, read> normals: array<f32>;
110
+
111
+ struct VsOut {
112
+ @builtin(position) clipPos: vec4<f32>,
113
+ @location(0) worldPos: vec3<f32>,
114
+ @location(1) normal: vec3<f32>,
115
+ @location(2) color: vec4<f32>,
116
+ @location(3) lightLoc: vec3<f32>,
117
+ };
118
+
119
+ fn fetchVec3(buf: ptr<storage, array<f32>, read>, base: u32) -> vec3<f32> {
120
+ return vec3<f32>((*buf)[base + 0u], (*buf)[base + 1u], (*buf)[base + 2u]);
121
+ }
122
+
123
+ @vertex
124
+ fn vs(@builtin(vertex_index) vid: u32, @builtin(instance_index) drawIdx: u32) -> VsOut {
125
+ let d = draws[drawIdx];
126
+ let base = (d.vertexBase + vid) * 3u;
127
+ let pos = fetchVec3(&positions, base);
128
+ let nor = fetchVec3(&normals, base);
129
+ // wombat.shader convention: matrices uploaded row-major; WGSL reads
130
+ // them as col-major = transposed. v * M is the row-vec dual of
131
+ // M.mul(v) (= M*v math).
132
+ let wp = vec4<f32>(pos, 1.0) * d.modelTrafo;
133
+ let n = (vec4<f32>(nor, 0.0) * d.modelTrafo).xyz;
134
+ var out: VsOut;
135
+ out.clipPos = wp * globals.viewProj;
136
+ out.worldPos = wp.xyz;
137
+ out.normal = n;
138
+ out.color = d.color;
139
+ out.lightLoc = globals.lightLocation.xyz;
140
+ return out;
141
+ }
142
+ `;
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // Geometry packing
146
+ // ---------------------------------------------------------------------------
147
+
148
+ interface PackedGeometry {
149
+ readonly positionsBuf: GPUBuffer;
150
+ readonly normalsBuf: GPUBuffer;
151
+ readonly indexBuf: GPUBuffer;
152
+ readonly entries: ReadonlyArray<{ vertexBase: number; indexCount: number; firstIndex: number }>;
153
+ readonly bytes: number;
154
+ }
155
+
156
+ /**
157
+ * Geometry triple. Tightly-packed Float32 positions / normals (3
158
+ * floats per vertex) plus Uint32 indices. The heap-scene packer
159
+ * concatenates all draws' geometry into per-group slabs.
160
+ */
161
+ export interface HeapGeometry {
162
+ readonly positions: Float32Array;
163
+ readonly normals: Float32Array;
164
+ readonly indices: Uint32Array;
165
+ }
166
+
167
+ function packGeometry(device: GPUDevice, geos: readonly HeapGeometry[], label: string): PackedGeometry {
168
+ let totalVerts = 0, totalIndices = 0;
169
+ for (const g of geos) {
170
+ totalVerts += g.positions.length / 3;
171
+ totalIndices += g.indices.length;
172
+ }
173
+ const positions = new Float32Array(totalVerts * 3);
174
+ const normals = new Float32Array(totalVerts * 3);
175
+ const indices = new Uint32Array(totalIndices);
176
+ const entries: { vertexBase: number; indexCount: number; firstIndex: number }[] = [];
177
+ let vOff = 0, iOff = 0;
178
+ for (const g of geos) {
179
+ const vCount = g.positions.length / 3;
180
+ positions.set(g.positions, vOff * 3);
181
+ normals.set(g.normals, vOff * 3);
182
+ indices.set(g.indices, iOff);
183
+ entries.push({ vertexBase: vOff, indexCount: g.indices.length, firstIndex: iOff });
184
+ vOff += vCount;
185
+ iOff += g.indices.length;
186
+ }
187
+ const mk = (data: ArrayBufferView, usage: GPUBufferUsageFlags, lbl: string): GPUBuffer => {
188
+ const buf = device.createBuffer({
189
+ size: alignUp(data.byteLength, 4),
190
+ usage: usage | GPUBufferUsage.COPY_DST,
191
+ label: lbl,
192
+ });
193
+ device.queue.writeBuffer(buf, 0, data.buffer, data.byteOffset, data.byteLength);
194
+ return buf;
195
+ };
196
+ return {
197
+ positionsBuf: mk(positions, GPUBufferUsage.STORAGE, `${label}/pos`),
198
+ normalsBuf: mk(normals, GPUBufferUsage.STORAGE, `${label}/nor`),
199
+ indexBuf: mk(indices, GPUBufferUsage.INDEX, `${label}/idx`),
200
+ entries,
201
+ bytes: positions.byteLength + normals.byteLength + indices.byteLength,
202
+ };
203
+ }
204
+
205
+ function alignUp(n: number, a: number): number { return (n + a - 1) & ~(a - 1); }
206
+
207
+ function asAval<T>(v: aval<T> | T): aval<T> {
208
+ return (typeof v === "object" && v !== null && typeof (v as { getValue?: unknown }).getValue === "function")
209
+ ? (v as aval<T>)
210
+ : AVal.constant(v as T);
211
+ }
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // Per-group internal state
215
+ // ---------------------------------------------------------------------------
216
+
217
+ interface Group {
218
+ readonly key: string;
219
+ readonly pipeline: GPURenderPipeline;
220
+ readonly bindGroup: GPUBindGroup;
221
+ readonly globals: GPUBuffer;
222
+ readonly globalsStaging: Float32Array;
223
+ readonly drawHeap: GPUBuffer;
224
+ readonly drawStaging: Float32Array;
225
+ readonly packed: PackedGeometry;
226
+ readonly trafos: aval<Trafo3d>[];
227
+ readonly colors: aval<V4f>[];
228
+ readonly dirty: Set<number>;
229
+ readonly subs: IDisposable[];
230
+ }
231
+
232
+ function buildGroup(
233
+ device: GPUDevice,
234
+ attach: CanvasAttachment,
235
+ key: string,
236
+ fragmentWgsl: string,
237
+ textureSet: HeapTextureSet | undefined,
238
+ draws: readonly { spec: HeapDrawSpec; sourceIndex: number }[],
239
+ modelTrafos: aval<Trafo3d>[],
240
+ colors: aval<V4f>[],
241
+ ): Group {
242
+ const sig = attach.signature;
243
+ const colorAttachmentName = sig.colorNames[0];
244
+ if (colorAttachmentName === undefined) {
245
+ throw new Error("buildHeapScene: framebuffer signature has no color attachment");
246
+ }
247
+ const colorFormat = sig.colors.tryFind(colorAttachmentName)!;
248
+ const depthFormat = sig.depthStencil?.format;
249
+
250
+ const memberIndices = draws.map(d => d.sourceIndex);
251
+ const myTrafos = memberIndices.map(i => modelTrafos[i]!);
252
+ const myColors = memberIndices.map(i => colors[i]!);
253
+
254
+ const packed = packGeometry(device, draws.map(d => d.spec.geo), `heapScene/${key}`);
255
+
256
+ const globals = device.createBuffer({
257
+ size: alignUp(GLOBALS_BYTES, 16),
258
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
259
+ label: `heapScene/${key}/globals`,
260
+ });
261
+ const globalsStaging = new Float32Array(GLOBALS_FLOATS);
262
+
263
+ const heapBytes = DRAW_HEADER_BYTES * draws.length;
264
+ const drawHeap = device.createBuffer({
265
+ size: alignUp(heapBytes, 16),
266
+ usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
267
+ label: `heapScene/${key}/draws`,
268
+ });
269
+ const drawStaging = new Float32Array(DRAW_HEADER_FLOATS * draws.length);
270
+ for (let i = 0; i < draws.length; i++) {
271
+ packDrawHeader(
272
+ myTrafos[i]!.force(/* allow-force */),
273
+ myColors[i]!.force(/* allow-force */),
274
+ packed.entries[i]!.vertexBase, drawStaging, i,
275
+ );
276
+ }
277
+ device.queue.writeBuffer(drawHeap, 0, drawStaging.buffer, drawStaging.byteOffset, heapBytes);
278
+
279
+ const dirty = new Set<number>();
280
+ const subs: IDisposable[] = [];
281
+ for (let i = 0; i < draws.length; i++) {
282
+ subs.push(addMarkingCallback(myTrafos[i] as never, () => dirty.add(i)));
283
+ subs.push(addMarkingCallback(myColors[i] as never, () => dirty.add(i)));
284
+ }
285
+
286
+ const module = device.createShaderModule({
287
+ code: SHADER_PRELUDE + fragmentWgsl,
288
+ label: `heapScene/${key}/shader`,
289
+ });
290
+ const bindLayoutEntries: GPUBindGroupLayoutEntry[] = [
291
+ { binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform" } },
292
+ { binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
293
+ { binding: 2, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
294
+ { binding: 3, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } },
295
+ ];
296
+ if (textureSet !== undefined) {
297
+ bindLayoutEntries.push(
298
+ { binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
299
+ { binding: 5, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
300
+ );
301
+ }
302
+ const bindGroupLayout = device.createBindGroupLayout({
303
+ label: `heapScene/${key}/bgl`,
304
+ entries: bindLayoutEntries,
305
+ });
306
+ const pipelineLayout = device.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] });
307
+ const pipeline = device.createRenderPipeline({
308
+ label: `heapScene/${key}/pipeline`,
309
+ layout: pipelineLayout,
310
+ vertex: { module, entryPoint: "vs", buffers: [] },
311
+ fragment: { module, entryPoint: "fs", targets: [{ format: colorFormat }] },
312
+ primitive: { topology: "triangle-list", cullMode: "back", frontFace: "ccw" },
313
+ ...(depthFormat !== undefined
314
+ ? { depthStencil: { format: depthFormat, depthWriteEnabled: true, depthCompare: "less" as GPUCompareFunction } }
315
+ : {}),
316
+ });
317
+ const bindEntries: GPUBindGroupEntry[] = [
318
+ { binding: 0, resource: { buffer: globals } },
319
+ { binding: 1, resource: { buffer: drawHeap } },
320
+ { binding: 2, resource: { buffer: packed.positionsBuf } },
321
+ { binding: 3, resource: { buffer: packed.normalsBuf } },
322
+ ];
323
+ if (textureSet !== undefined) {
324
+ bindEntries.push(
325
+ { binding: 4, resource: textureSet.textureView ?? textureSet.texture.createView() },
326
+ { binding: 5, resource: textureSet.sampler },
327
+ );
328
+ }
329
+ const bindGroup = device.createBindGroup({
330
+ label: `heapScene/${key}/bg`,
331
+ layout: bindGroupLayout,
332
+ entries: bindEntries,
333
+ });
334
+
335
+ return {
336
+ key, pipeline, bindGroup, globals, globalsStaging, drawHeap, drawStaging,
337
+ packed, trafos: myTrafos, colors: myColors, dirty, subs,
338
+ };
339
+ }
340
+
341
+ // ---------------------------------------------------------------------------
342
+ // Public API
343
+ // ---------------------------------------------------------------------------
344
+
345
+ export interface HeapDrawSpec {
346
+ readonly geo: HeapGeometry;
347
+ readonly modelTrafo: aval<Trafo3d> | Trafo3d;
348
+ readonly color: aval<V4f> | V4f;
349
+ /** Group key — selects which pipeline-state group this draw joins. */
350
+ readonly groupKey: string;
351
+ }
352
+
353
+ export interface HeapSceneStats {
354
+ readonly groups: number;
355
+ readonly totalDraws: number;
356
+ readonly globalsBytes: number;
357
+ drawBytes: number;
358
+ readonly geometryBytes: number;
359
+ }
360
+
361
+ export interface HeapScene {
362
+ /** Render one frame against the given globals. */
363
+ frame(viewProj: Trafo3d, lightLocation: V3d): void;
364
+ readonly stats: HeapSceneStats;
365
+ dispose(): void;
366
+ }
367
+
368
+ /**
369
+ * Per-group texture set. Bindless isn't standard WebGPU, so a group
370
+ * with sampled textures gets a distinct bind-group layout and is
371
+ * keyed separately from groups with no textures or different sets.
372
+ */
373
+ export interface HeapTextureSet {
374
+ readonly texture: GPUTexture;
375
+ readonly textureView?: GPUTextureView;
376
+ readonly sampler: GPUSampler;
377
+ }
378
+
379
+ export interface BuildHeapSceneOptions {
380
+ /** WGSL fragment-stage source per group key — must declare `fs(in: VsOut) -> @location(0) vec4<f32>`. */
381
+ readonly fragmentShaders: ReadonlyMap<string, string>;
382
+ /**
383
+ * Per-group texture set. When present, the group's bind-group
384
+ * layout adds a `texture_2d<f32>` at binding 4 and a sampler at
385
+ * binding 5. The user's FS WGSL must declare them.
386
+ */
387
+ readonly texturesByGroupKey?: ReadonlyMap<string, HeapTextureSet>;
388
+ }
389
+
390
+ /**
391
+ * Build a multi-group heap-backed scene renderer. Buckets `draws` by
392
+ * `groupKey`, builds one pipeline + bind group + heap per bucket,
393
+ * subscribes per-RO avals so dirty marks propagate to per-slot
394
+ * `writeBuffer`s on the next `frame()`.
395
+ */
396
+ export function buildHeapScene(
397
+ device: GPUDevice,
398
+ attach: CanvasAttachment,
399
+ draws: readonly HeapDrawSpec[],
400
+ opts: BuildHeapSceneOptions,
401
+ ): HeapScene {
402
+ const modelTrafos: aval<Trafo3d>[] = draws.map(d => asAval(d.modelTrafo));
403
+ const colors: aval<V4f>[] = draws.map(d => asAval(d.color));
404
+
405
+ // Bucket by group key.
406
+ const buckets = new Map<string, { spec: HeapDrawSpec; sourceIndex: number }[]>();
407
+ for (let i = 0; i < draws.length; i++) {
408
+ const d = draws[i]!;
409
+ let bucket = buckets.get(d.groupKey);
410
+ if (bucket === undefined) {
411
+ bucket = [];
412
+ buckets.set(d.groupKey, bucket);
413
+ }
414
+ bucket.push({ spec: d, sourceIndex: i });
415
+ }
416
+ const groups: Group[] = [];
417
+ for (const [key, members] of buckets) {
418
+ const fs = opts.fragmentShaders.get(key);
419
+ if (fs === undefined) {
420
+ throw new Error(`buildHeapScene: no fragment shader supplied for groupKey '${key}'`);
421
+ }
422
+ const textureSet = opts.texturesByGroupKey?.get(key);
423
+ groups.push(buildGroup(device, attach, key, fs, textureSet, members, modelTrafos, colors));
424
+ }
425
+
426
+ const stats: HeapSceneStats = {
427
+ groups: groups.length,
428
+ totalDraws: draws.length,
429
+ globalsBytes: GLOBALS_BYTES * groups.length,
430
+ drawBytes: 0,
431
+ geometryBytes: groups.reduce((acc, g) => acc + g.packed.bytes, 0),
432
+ };
433
+
434
+ function frame(viewProj: Trafo3d, lightLocation: V3d): void {
435
+ const fb = (attach.framebuffer as aval<import("../core/index.js").IFramebuffer>).force(/* allow-force */);
436
+ const colorAttachmentName = attach.signature.colorNames[0]!;
437
+ const colorView = fb.colors.tryFind(colorAttachmentName)!;
438
+ const depthFormat = attach.signature.depthStencil?.format;
439
+ const depthView = fb.depthStencil;
440
+
441
+ let totalDirtyBytes = 0;
442
+
443
+ const enc = device.createCommandEncoder({ label: "heapScene: encoder" });
444
+ let firstPass = true;
445
+ for (const g of groups) {
446
+ packGlobals(viewProj, lightLocation, g.globalsStaging);
447
+ device.queue.writeBuffer(g.globals, 0, g.globalsStaging.buffer, g.globalsStaging.byteOffset, GLOBALS_BYTES);
448
+
449
+ if (g.dirty.size > 0) {
450
+ for (const i of g.dirty) {
451
+ packDrawHeader(
452
+ g.trafos[i]!.force(/* allow-force */),
453
+ g.colors[i]!.force(/* allow-force */),
454
+ g.packed.entries[i]!.vertexBase, g.drawStaging, i,
455
+ );
456
+ const byteOff = i * DRAW_HEADER_BYTES;
457
+ device.queue.writeBuffer(
458
+ g.drawHeap, byteOff,
459
+ g.drawStaging.buffer, g.drawStaging.byteOffset + byteOff,
460
+ DRAW_HEADER_BYTES,
461
+ );
462
+ totalDirtyBytes += DRAW_HEADER_BYTES;
463
+ }
464
+ g.dirty.clear();
465
+ }
466
+
467
+ const pass = enc.beginRenderPass({
468
+ colorAttachments: [{
469
+ view: colorView,
470
+ clearValue: { r: 0.07, g: 0.07, b: 0.08, a: 1.0 },
471
+ loadOp: firstPass ? "clear" : "load",
472
+ storeOp: "store",
473
+ }],
474
+ ...(depthView !== undefined && depthFormat !== undefined ? {
475
+ depthStencilAttachment: {
476
+ view: depthView,
477
+ depthClearValue: 1.0,
478
+ depthLoadOp: firstPass ? "clear" : "load",
479
+ depthStoreOp: "store",
480
+ } satisfies GPURenderPassDepthStencilAttachment,
481
+ } : {}),
482
+ });
483
+ pass.setPipeline(g.pipeline);
484
+ pass.setBindGroup(0, g.bindGroup);
485
+ pass.setIndexBuffer(g.packed.indexBuf, "uint32");
486
+ for (let i = 0; i < g.packed.entries.length; i++) {
487
+ const e = g.packed.entries[i]!;
488
+ pass.drawIndexed(e.indexCount, 1, e.firstIndex, 0, i);
489
+ }
490
+ pass.end();
491
+ firstPass = false;
492
+ }
493
+
494
+ stats.drawBytes = totalDirtyBytes;
495
+ device.queue.submit([enc.finish()]);
496
+ }
497
+
498
+ function dispose(): void {
499
+ for (const g of groups) {
500
+ for (const s of g.subs) s.dispose();
501
+ g.globals.destroy();
502
+ g.drawHeap.destroy();
503
+ g.packed.positionsBuf.destroy();
504
+ g.packed.normalsBuf.destroy();
505
+ g.packed.indexBuf.destroy();
506
+ }
507
+ }
508
+
509
+ return { frame, stats, dispose };
510
+ }