@aardworx/wombat.rendering 0.19.24 → 0.20.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/dist/core/elementType.d.ts +2 -0
- package/dist/core/elementType.d.ts.map +1 -1
- package/dist/core/elementType.js +10 -0
- package/dist/core/elementType.js.map +1 -1
- package/dist/core/provider.d.ts.map +1 -1
- package/dist/core/provider.js +65 -32
- package/dist/core/provider.js.map +1 -1
- package/dist/core/renderObject.d.ts +18 -0
- package/dist/core/renderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.js +28 -5
- package/dist/resources/preparedRenderObject.js.map +1 -1
- package/dist/resources/uniformBuffer.d.ts.map +1 -1
- package/dist/resources/uniformBuffer.js +3 -1
- package/dist/resources/uniformBuffer.js.map +1 -1
- package/dist/runtime/derivedModes/modeKeyCpu.d.ts +2 -12
- package/dist/runtime/derivedModes/modeKeyCpu.d.ts.map +1 -1
- package/dist/runtime/derivedModes/modeKeyCpu.js +30 -13
- package/dist/runtime/derivedModes/modeKeyCpu.js.map +1 -1
- package/dist/runtime/derivedModes/partitionDispatcher.d.ts +11 -0
- package/dist/runtime/derivedModes/partitionDispatcher.d.ts.map +1 -1
- package/dist/runtime/derivedModes/partitionDispatcher.js +31 -0
- package/dist/runtime/derivedModes/partitionDispatcher.js.map +1 -1
- package/dist/runtime/derivedUniforms/codegen.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/codegen.js +34 -13
- package/dist/runtime/derivedUniforms/codegen.js.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.js +6 -0
- package/dist/runtime/derivedUniforms/dispatch.js.map +1 -1
- package/dist/runtime/derivedUniforms/records.d.ts +15 -0
- package/dist/runtime/derivedUniforms/records.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/records.js +81 -14
- package/dist/runtime/derivedUniforms/records.js.map +1 -1
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts +6 -0
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/sceneIntegration.js +18 -4
- package/dist/runtime/derivedUniforms/sceneIntegration.js.map +1 -1
- package/dist/runtime/flattenTree.d.ts.map +1 -1
- package/dist/runtime/flattenTree.js +46 -2
- package/dist/runtime/flattenTree.js.map +1 -1
- package/dist/runtime/heapAdapter.d.ts +3 -1
- package/dist/runtime/heapAdapter.d.ts.map +1 -1
- package/dist/runtime/heapAdapter.js +45 -4
- package/dist/runtime/heapAdapter.js.map +1 -1
- package/dist/runtime/heapDecoder.d.ts.map +1 -1
- package/dist/runtime/heapDecoder.js +8 -3
- package/dist/runtime/heapDecoder.js.map +1 -1
- package/dist/runtime/heapEffect.d.ts +36 -1
- package/dist/runtime/heapEffect.d.ts.map +1 -1
- package/dist/runtime/heapEffect.js +29 -2
- package/dist/runtime/heapEffect.js.map +1 -1
- package/dist/runtime/heapEffectIR.d.ts.map +1 -1
- package/dist/runtime/heapEffectIR.js +51 -11
- package/dist/runtime/heapEffectIR.js.map +1 -1
- package/dist/runtime/heapEligibility.d.ts.map +1 -1
- package/dist/runtime/heapEligibility.js +64 -12
- package/dist/runtime/heapEligibility.js.map +1 -1
- package/dist/runtime/heapIrBuilders.d.ts +8 -0
- package/dist/runtime/heapIrBuilders.d.ts.map +1 -1
- package/dist/runtime/heapIrBuilders.js +47 -2
- package/dist/runtime/heapIrBuilders.js.map +1 -1
- package/dist/runtime/heapScene/chunkedArena.d.ts +20 -28
- package/dist/runtime/heapScene/chunkedArena.d.ts.map +1 -1
- package/dist/runtime/heapScene/chunkedArena.js +28 -91
- package/dist/runtime/heapScene/chunkedArena.js.map +1 -1
- package/dist/runtime/heapScene/freelist.d.ts +1 -1
- package/dist/runtime/heapScene/freelist.js +3 -3
- package/dist/runtime/heapScene/freelist.js.map +1 -1
- package/dist/runtime/heapScene/pools.d.ts +103 -59
- package/dist/runtime/heapScene/pools.d.ts.map +1 -1
- package/dist/runtime/heapScene/pools.js +335 -194
- package/dist/runtime/heapScene/pools.js.map +1 -1
- package/dist/runtime/heapScene.d.ts +125 -1
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +671 -126
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/heapShaderFamily.d.ts.map +1 -1
- package/dist/runtime/heapShaderFamily.js +19 -0
- package/dist/runtime/heapShaderFamily.js.map +1 -1
- package/dist/runtime/hybridScene.d.ts +6 -0
- package/dist/runtime/hybridScene.d.ts.map +1 -1
- package/dist/runtime/hybridScene.js +36 -3
- package/dist/runtime/hybridScene.js.map +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/renderTask.d.ts +6 -0
- package/dist/runtime/renderTask.d.ts.map +1 -1
- package/dist/runtime/renderTask.js +3 -0
- package/dist/runtime/renderTask.js.map +1 -1
- package/dist/runtime/runtime.d.ts +15 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +3 -0
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts +19 -2
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.d.ts.map +1 -1
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.js +37 -6
- package/dist/runtime/textureAtlas/atlasMipGutterKernel.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.d.ts +5 -0
- package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.js +25 -3
- package/dist/runtime/textureAtlas/atlasPool.js.map +1 -1
- package/dist/window/loop.d.ts.map +1 -1
- package/dist/window/loop.js +31 -4
- package/dist/window/loop.js.map +1 -1
- package/package.json +3 -3
- package/src/core/elementType.ts +10 -0
- package/src/core/provider.ts +56 -28
- package/src/core/renderObject.ts +18 -0
- package/src/resources/preparedRenderObject.ts +29 -6
- package/src/resources/uniformBuffer.ts +3 -1
- package/src/runtime/derivedModes/modeKeyCpu.ts +26 -11
- package/src/runtime/derivedModes/partitionDispatcher.ts +30 -0
- package/src/runtime/derivedUniforms/codegen.ts +34 -13
- package/src/runtime/derivedUniforms/dispatch.ts +6 -0
- package/src/runtime/derivedUniforms/records.ts +70 -15
- package/src/runtime/derivedUniforms/sceneIntegration.ts +19 -4
- package/src/runtime/flattenTree.ts +47 -2
- package/src/runtime/heapAdapter.ts +43 -5
- package/src/runtime/heapDecoder.ts +8 -3
- package/src/runtime/heapEffect.ts +44 -2
- package/src/runtime/heapEffectIR.ts +54 -11
- package/src/runtime/heapEligibility.ts +39 -12
- package/src/runtime/heapIrBuilders.ts +50 -2
- package/src/runtime/heapScene/chunkedArena.ts +29 -94
- package/src/runtime/heapScene/freelist.ts +3 -3
- package/src/runtime/heapScene/pools.ts +340 -218
- package/src/runtime/heapScene.ts +826 -142
- package/src/runtime/heapShaderFamily.ts +21 -0
- package/src/runtime/hybridScene.ts +43 -3
- package/src/runtime/index.ts +5 -0
- package/src/runtime/renderTask.ts +9 -0
- package/src/runtime/runtime.ts +18 -0
- package/src/runtime/textureAtlas/atlasMipGutterKernel.ts +52 -3
- package/src/runtime/textureAtlas/atlasPool.ts +28 -2
- package/src/window/loop.ts +32 -4
|
@@ -481,6 +481,26 @@ function unionDrawHeaders(
|
|
|
481
481
|
// descriptor-only purpose of this slice, the per-effect-allocated
|
|
482
482
|
// numbers are retained as-is on each entry.
|
|
483
483
|
|
|
484
|
+
// Union user storage-buffer bindings by name (type/access agreement),
|
|
485
|
+
// keeping the per-effect-allocated binding numbers (all effects allocate
|
|
486
|
+
// from the same fixed base in the same sorted-by-name order, so shared
|
|
487
|
+
// names agree; family-merge is disabled in practice anyway).
|
|
488
|
+
const storageByName = new Map<string, { name: string; access: "read" | "read_write"; binding: number }>();
|
|
489
|
+
for (const e of sortedEffects) {
|
|
490
|
+
const layout = perEffectLayout.get(e)!;
|
|
491
|
+
for (const sb of layout.storageBindings) {
|
|
492
|
+
const existing = storageByName.get(sb.name);
|
|
493
|
+
if (existing === undefined) storageByName.set(sb.name, { ...sb });
|
|
494
|
+
else if (existing.access !== sb.access) {
|
|
495
|
+
throw new Error(
|
|
496
|
+
`buildShaderFamily: storage buffer '${sb.name}' has conflicting access ` +
|
|
497
|
+
`across effects: '${existing.access}' vs '${sb.access}'.`,
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
const storageBindings = [...storageByName.values()].sort((a, b) => a.binding - b.binding);
|
|
503
|
+
|
|
484
504
|
const base = {
|
|
485
505
|
drawHeaderFields: fields,
|
|
486
506
|
drawHeaderBytes,
|
|
@@ -491,6 +511,7 @@ function unionDrawHeaders(
|
|
|
491
511
|
textureBindings: orderedTextures,
|
|
492
512
|
samplerBindings: orderedSamplers,
|
|
493
513
|
atlasTextureBindings: atlasUnion,
|
|
514
|
+
storageBindings,
|
|
494
515
|
};
|
|
495
516
|
return { id: bucketLayoutId(base), ...base };
|
|
496
517
|
}
|
|
@@ -92,6 +92,12 @@ export interface CompileHybridSceneOptions {
|
|
|
92
92
|
* massive workloads.
|
|
93
93
|
*/
|
|
94
94
|
readonly maxChunkBytes?: number;
|
|
95
|
+
/** Pre-size heap arena chunks (bytes) — pass-through to `BuildHeapSceneOptions.initialArenaBytes`. */
|
|
96
|
+
readonly initialArenaBytes?: number;
|
|
97
|
+
/** Heap arena compaction toggle — pass-through to `BuildHeapSceneOptions.enableCompaction`. */
|
|
98
|
+
readonly enableCompaction?: boolean;
|
|
99
|
+
/** Compaction waste floor (bytes) — pass-through to `BuildHeapSceneOptions.compactionWasteFloorBytes`. */
|
|
100
|
+
readonly compactionWasteFloorBytes?: number;
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
export interface HybridScene {
|
|
@@ -197,19 +203,33 @@ export function compileHybridScene(
|
|
|
197
203
|
// toggle — flipping that off forces every RO to legacy.
|
|
198
204
|
const heapEnabled = opts.heapEnabled ?? AVal.constant(true);
|
|
199
205
|
const eligCache = new WeakMap<RenderObject, aval<boolean>>();
|
|
206
|
+
const notEligCache = new WeakMap<RenderObject, aval<boolean>>();
|
|
200
207
|
const elig = (ro: RenderObject): aval<boolean> => {
|
|
201
208
|
let av = eligCache.get(ro);
|
|
202
209
|
if (av === undefined) {
|
|
203
210
|
const perRO = isHeapEligible(ro);
|
|
204
|
-
|
|
211
|
+
// Constant collapse (static RO + constant heapEnabled): a plain
|
|
212
|
+
// constant instead of a custom aval + two subscriptions per RO.
|
|
213
|
+
av = heapEnabled.isConstant && perRO.isConstant
|
|
214
|
+
? AVal.constant(heapEnabled.force(/* allow-force */) && perRO.force(/* allow-force */))
|
|
215
|
+
: AVal.custom(t => heapEnabled.getValue(t) && perRO.getValue(t));
|
|
205
216
|
eligCache.set(ro, av);
|
|
206
217
|
}
|
|
207
218
|
return av;
|
|
208
219
|
};
|
|
220
|
+
const notElig = (ro: RenderObject): aval<boolean> => {
|
|
221
|
+
let av = notEligCache.get(ro);
|
|
222
|
+
if (av === undefined) {
|
|
223
|
+
const e = elig(ro);
|
|
224
|
+
av = e.isConstant ? AVal.constant(!e.force(/* allow-force */)) : e.map(b => !b);
|
|
225
|
+
notEligCache.set(ro, av);
|
|
226
|
+
}
|
|
227
|
+
return av;
|
|
228
|
+
};
|
|
209
229
|
|
|
210
230
|
const flat = flattenRenderTree(tree);
|
|
211
231
|
const heapAset = flat.filterA(ro => elig(ro));
|
|
212
|
-
const legacyAset = flat.filterA(ro =>
|
|
232
|
+
const legacyAset = flat.filterA(ro => notElig(ro));
|
|
213
233
|
|
|
214
234
|
// ─── Atlas pool ──────────────────────────────────────────────────
|
|
215
235
|
// Per-scene Tier-S atlas pool. Owned by this hybrid scene; disposed
|
|
@@ -231,7 +251,9 @@ export function compileHybridScene(
|
|
|
231
251
|
const heapSpecAset = heapAset.map((ro: RenderObject) => {
|
|
232
252
|
let spec = specCache.get(ro);
|
|
233
253
|
if (spec === undefined) {
|
|
234
|
-
spec = renderObjectToHeapSpec(ro, AdaptiveToken.top, atlasPool
|
|
254
|
+
spec = renderObjectToHeapSpec(ro, AdaptiveToken.top, atlasPool, {
|
|
255
|
+
enableDerivedUniforms: opts.enableDerivedUniforms !== false,
|
|
256
|
+
});
|
|
235
257
|
specCache.set(ro, spec);
|
|
236
258
|
}
|
|
237
259
|
return spec;
|
|
@@ -243,6 +265,9 @@ export function compileHybridScene(
|
|
|
243
265
|
...(opts.enableFamilyMerge === true ? { enableFamilyMerge: true } : {}),
|
|
244
266
|
...(opts.enableDerivedUniforms === false ? { enableDerivedUniforms: false } : {}),
|
|
245
267
|
...(opts.maxChunkBytes !== undefined ? { maxChunkBytes: opts.maxChunkBytes } : {}),
|
|
268
|
+
...(opts.initialArenaBytes !== undefined ? { initialArenaBytes: opts.initialArenaBytes } : {}),
|
|
269
|
+
...(opts.enableCompaction !== undefined ? { enableCompaction: opts.enableCompaction } : {}),
|
|
270
|
+
...(opts.compactionWasteFloorBytes !== undefined ? { compactionWasteFloorBytes: opts.compactionWasteFloorBytes } : {}),
|
|
246
271
|
});
|
|
247
272
|
|
|
248
273
|
// ─── Legacy subset → RenderTree → ScenePass ──────────────────────
|
|
@@ -253,10 +278,24 @@ export function compileHybridScene(
|
|
|
253
278
|
const legacyTree: RenderTree =
|
|
254
279
|
RenderTree.unorderedFromSet(legacyAset.map(ro => RenderTree.leaf(ro)));
|
|
255
280
|
const scenePass = new ScenePass(device, signature, legacyTree, compileEffect);
|
|
281
|
+
// DEBUG registry: per-hybrid-scene live draw counts (heap vs legacy) so
|
|
282
|
+
// multi-task pipelines (OIT) can be inspected from the console.
|
|
283
|
+
const dbgEntry = {
|
|
284
|
+
sig: signature.colorNames.join("+"),
|
|
285
|
+
heap: () => heapScene.stats.totalDraws,
|
|
286
|
+
legacy: () => scenePass.collect().length,
|
|
287
|
+
};
|
|
288
|
+
const dbgArr = ((globalThis as any).__hybridScenes ??= []) as (typeof dbgEntry)[];
|
|
289
|
+
dbgArr.push(dbgEntry);
|
|
256
290
|
|
|
257
291
|
return {
|
|
258
292
|
update(token: AdaptiveToken): void {
|
|
293
|
+
// Batch all of this frame's atlas uploads (tile ingest during
|
|
294
|
+
// heapScene.update → pool.acquire) into ONE queue submission —
|
|
295
|
+
// per-tile submits stall multi-frame when several tiles land at once.
|
|
296
|
+
atlasPool.beginUploadBatch();
|
|
259
297
|
heapScene.update(token);
|
|
298
|
+
atlasPool.flushUploadBatch();
|
|
260
299
|
scenePass.update(token);
|
|
261
300
|
},
|
|
262
301
|
encodeComputePrep(enc: GPUCommandEncoder, token: AdaptiveToken): void {
|
|
@@ -291,6 +330,7 @@ export function compileHybridScene(
|
|
|
291
330
|
__legacyCount(): number {
|
|
292
331
|
return scenePass.collect().length;
|
|
293
332
|
},
|
|
333
|
+
|
|
294
334
|
heapDerivedTimings() {
|
|
295
335
|
const s = heapScene.stats;
|
|
296
336
|
return {
|
package/src/runtime/index.ts
CHANGED
|
@@ -59,6 +59,12 @@ export interface RuntimeContext {
|
|
|
59
59
|
* compiled against this context. Default unset → adapter limit.
|
|
60
60
|
*/
|
|
61
61
|
readonly maxChunkBytes?: number;
|
|
62
|
+
/** Pre-size heap arena chunks (bytes) — see `BuildHeapSceneOptions.initialArenaBytes`. */
|
|
63
|
+
readonly initialArenaBytes?: number;
|
|
64
|
+
/** Heap arena compaction toggle — see `BuildHeapSceneOptions.enableCompaction`. */
|
|
65
|
+
readonly enableCompaction?: boolean;
|
|
66
|
+
/** Compaction waste floor (bytes) — see `BuildHeapSceneOptions.compactionWasteFloorBytes`. */
|
|
67
|
+
readonly compactionWasteFloorBytes?: number;
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
class RenderTask implements IRenderTask {
|
|
@@ -262,6 +268,9 @@ class RenderTask implements IRenderTask {
|
|
|
262
268
|
...(this.ctx.enableFamilyMerge === true ? { enableFamilyMerge: true } : {}),
|
|
263
269
|
...(this.ctx.enableDerivedUniforms === false ? { enableDerivedUniforms: false } : {}),
|
|
264
270
|
...(this.ctx.maxChunkBytes !== undefined ? { maxChunkBytes: this.ctx.maxChunkBytes } : {}),
|
|
271
|
+
...(this.ctx.initialArenaBytes !== undefined ? { initialArenaBytes: this.ctx.initialArenaBytes } : {}),
|
|
272
|
+
...(this.ctx.enableCompaction !== undefined ? { enableCompaction: this.ctx.enableCompaction } : {}),
|
|
273
|
+
...(this.ctx.compactionWasteFloorBytes !== undefined ? { compactionWasteFloorBytes: this.ctx.compactionWasteFloorBytes } : {}),
|
|
265
274
|
});
|
|
266
275
|
this._scenes.set(cmd, s);
|
|
267
276
|
}
|
package/src/runtime/runtime.ts
CHANGED
|
@@ -47,6 +47,21 @@ export interface RuntimeOptions {
|
|
|
47
47
|
* from a demo / test to exercise multi-chunk routing.
|
|
48
48
|
*/
|
|
49
49
|
readonly maxChunkBytes?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Pre-size heap arena chunks (bytes) — threaded into
|
|
52
|
+
* `BuildHeapSceneOptions.initialArenaBytes`. Streaming apps should pass
|
|
53
|
+
* their expected working-set size to avoid pow2 grow-copies (full-arena
|
|
54
|
+
* GPU copy + bind-group rebuilds) mid-navigation.
|
|
55
|
+
*/
|
|
56
|
+
readonly initialArenaBytes?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Heap arena compaction toggle — threaded into
|
|
59
|
+
* `BuildHeapSceneOptions.enableCompaction`. Streaming apps with steady
|
|
60
|
+
* churn may prefer `false` to guarantee no mid-frame compaction bounce.
|
|
61
|
+
*/
|
|
62
|
+
readonly enableCompaction?: boolean;
|
|
63
|
+
/** Waste floor (bytes) before compaction triggers — see `BuildHeapSceneOptions`. */
|
|
64
|
+
readonly compactionWasteFloorBytes?: number;
|
|
50
65
|
}
|
|
51
66
|
|
|
52
67
|
/**
|
|
@@ -84,6 +99,9 @@ export class Runtime {
|
|
|
84
99
|
...(opts.enableFamilyMerge === true ? { enableFamilyMerge: true } : {}),
|
|
85
100
|
...(opts.enableDerivedUniforms === false ? { enableDerivedUniforms: false } : {}),
|
|
86
101
|
...(opts.maxChunkBytes !== undefined ? { maxChunkBytes: opts.maxChunkBytes } : {}),
|
|
102
|
+
...(opts.initialArenaBytes !== undefined ? { initialArenaBytes: opts.initialArenaBytes } : {}),
|
|
103
|
+
...(opts.enableCompaction !== undefined ? { enableCompaction: opts.enableCompaction } : {}),
|
|
104
|
+
...(opts.compactionWasteFloorBytes !== undefined ? { compactionWasteFloorBytes: opts.compactionWasteFloorBytes } : {}),
|
|
87
105
|
};
|
|
88
106
|
// `device.lost` is a real-WebGPU promise; mock devices may not
|
|
89
107
|
// expose it. Treat as "never lost" in that case.
|
|
@@ -185,6 +185,44 @@ export interface MipSlot {
|
|
|
185
185
|
readonly size: { w: number; h: number };
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Shared-encoder batch for atlas uploads: many tiles' mip/gutter kernels
|
|
190
|
+
* encode into ONE command encoder and submit ONCE — per-tile submits (2 per
|
|
191
|
+
* tile: the implicit copyExternalImageToTexture + a dedicated kernel submit)
|
|
192
|
+
* were measured as multi-frame stalls when several tiles land per frame on
|
|
193
|
+
* mobile GPUs. Resources pushed to `trash`/`trashTextures` are destroyed
|
|
194
|
+
* once the single submission completes.
|
|
195
|
+
*/
|
|
196
|
+
export interface MipGutterBatch {
|
|
197
|
+
readonly enc: GPUCommandEncoder;
|
|
198
|
+
readonly trash: GPUBuffer[];
|
|
199
|
+
readonly trashTextures: GPUTexture[];
|
|
200
|
+
/** encoded job count (empty batches skip the submit entirely) */
|
|
201
|
+
jobs: number;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function beginMipGutterBatch(device: GPUDevice): MipGutterBatch {
|
|
205
|
+
return {
|
|
206
|
+
enc: device.createCommandEncoder({ label: "atlas/mipGutterBatch" }),
|
|
207
|
+
trash: [], trashTextures: [], jobs: 0,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function submitMipGutterBatch(device: GPUDevice, batch: MipGutterBatch): void {
|
|
212
|
+
if (batch.jobs === 0) {
|
|
213
|
+
// nothing encoded — still finish the encoder to release it
|
|
214
|
+
batch.enc.finish();
|
|
215
|
+
for (const t of batch.trashTextures) t.destroy();
|
|
216
|
+
for (const b of batch.trash) b.destroy();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
device.queue.submit([batch.enc.finish()]);
|
|
220
|
+
void device.queue.onSubmittedWorkDone().then(() => {
|
|
221
|
+
for (const t of batch.trashTextures) t.destroy();
|
|
222
|
+
for (const b of batch.trash) b.destroy();
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
188
226
|
/** How mip-0's interior pixels get into the scratch buffer. */
|
|
189
227
|
type Mip0Source =
|
|
190
228
|
// The caller already filled the buffer's mip-0 region at creation
|
|
@@ -216,12 +254,15 @@ function runMipGutter(
|
|
|
216
254
|
srcW: number, srcH: number,
|
|
217
255
|
mips: readonly MipSlot[],
|
|
218
256
|
mip0Src: Mip0Source,
|
|
257
|
+
batch?: MipGutterBatch,
|
|
219
258
|
): void {
|
|
220
259
|
const k = getKernel(device);
|
|
221
260
|
const bufStrideU32 = rowBytes / 4;
|
|
222
261
|
const mip0 = mips[0]!;
|
|
223
262
|
|
|
224
|
-
const enc =
|
|
263
|
+
const enc = batch !== undefined
|
|
264
|
+
? batch.enc
|
|
265
|
+
: device.createCommandEncoder({ label: "atlas/mipGutterKernel" });
|
|
225
266
|
|
|
226
267
|
const ubos: GPUBuffer[] = [];
|
|
227
268
|
const makeUbo = (
|
|
@@ -312,6 +353,12 @@ function runMipGutter(
|
|
|
312
353
|
{ width: boundsW, height: boundsH, depthOrArrayLayers: 1 },
|
|
313
354
|
);
|
|
314
355
|
|
|
356
|
+
if (batch !== undefined) {
|
|
357
|
+
batch.jobs++;
|
|
358
|
+
batch.trash.push(scratch, ...ubos);
|
|
359
|
+
return; // caller submits the shared encoder once per frame
|
|
360
|
+
}
|
|
361
|
+
|
|
315
362
|
device.queue.submit([enc.finish()]);
|
|
316
363
|
|
|
317
364
|
// Lifetime: destroy scratch + per-pass UBOs once submitted work is
|
|
@@ -348,6 +395,7 @@ export function buildMipsAndGutterOnGpu(
|
|
|
348
395
|
srcPixels: Uint8Array,
|
|
349
396
|
srcW: number, srcH: number,
|
|
350
397
|
mips: readonly MipSlot[],
|
|
398
|
+
batch?: MipGutterBatch,
|
|
351
399
|
): void {
|
|
352
400
|
if (mips.length === 0) return;
|
|
353
401
|
const { rowBytes, bufSize } = scratchGeom(boundsW, boundsH);
|
|
@@ -382,7 +430,7 @@ export function buildMipsAndGutterOnGpu(
|
|
|
382
430
|
|
|
383
431
|
runMipGutter(
|
|
384
432
|
device, page, subRectX, subRectY, boundsW, boundsH,
|
|
385
|
-
rowBytes, scratch, srcW, srcH, mips, { kind: "prefilled" },
|
|
433
|
+
rowBytes, scratch, srcW, srcH, mips, { kind: "prefilled" }, batch,
|
|
386
434
|
);
|
|
387
435
|
}
|
|
388
436
|
|
|
@@ -408,6 +456,7 @@ export function buildMipsAndGutterFromTexture(
|
|
|
408
456
|
srcTex: GPUTexture,
|
|
409
457
|
srcW: number, srcH: number,
|
|
410
458
|
mips: readonly MipSlot[],
|
|
459
|
+
batch?: MipGutterBatch,
|
|
411
460
|
): void {
|
|
412
461
|
if (mips.length === 0) return;
|
|
413
462
|
const { rowBytes, bufSize } = scratchGeom(boundsW, boundsH);
|
|
@@ -418,6 +467,6 @@ export function buildMipsAndGutterFromTexture(
|
|
|
418
467
|
});
|
|
419
468
|
runMipGutter(
|
|
420
469
|
device, page, subRectX, subRectY, boundsW, boundsH,
|
|
421
|
-
rowBytes, scratch, srcW, srcH, mips, { kind: "texture", tex: srcTex },
|
|
470
|
+
rowBytes, scratch, srcW, srcH, mips, { kind: "texture", tex: srcTex }, batch,
|
|
422
471
|
);
|
|
423
472
|
}
|
|
@@ -33,7 +33,10 @@ import { V2f, V2i } from "@aardworx/wombat.base";
|
|
|
33
33
|
import { type aval, HashTable } from "@aardworx/wombat.adaptive";
|
|
34
34
|
import type { ITexture, HostTextureSource } from "../../core/texture.js";
|
|
35
35
|
import { TexturePacking } from "./packer.js";
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
buildMipsAndGutterOnGpu, buildMipsAndGutterFromTexture, type MipSlot,
|
|
38
|
+
beginMipGutterBatch, submitMipGutterBatch, type MipGutterBatch,
|
|
39
|
+
} from "./atlasMipGutterKernel.js";
|
|
37
40
|
|
|
38
41
|
export const ATLAS_PAGE_SIZE = 4096;
|
|
39
42
|
/** Tier S/M source-side dimension cap. */
|
|
@@ -675,6 +678,27 @@ export class AtlasPool {
|
|
|
675
678
|
this.lru.clear();
|
|
676
679
|
}
|
|
677
680
|
|
|
681
|
+
// ── shared-encoder upload batching ─────────────────────────────────
|
|
682
|
+
// While a batch is open, per-tile kernel work encodes into ONE encoder
|
|
683
|
+
// and `flushUploadBatch` submits once — per-tile submits were measured
|
|
684
|
+
// as multi-frame stalls when several tiles land per frame (mobile GPUs,
|
|
685
|
+
// large photogrammetry textures). Callers that never open a batch keep
|
|
686
|
+
// the old immediate-submit behavior.
|
|
687
|
+
private uploadBatch: MipGutterBatch | null = null;
|
|
688
|
+
|
|
689
|
+
/** Open (or keep) a shared upload batch. Pair with `flushUploadBatch`. */
|
|
690
|
+
beginUploadBatch(): void {
|
|
691
|
+
if (this.uploadBatch === null) this.uploadBatch = beginMipGutterBatch(this.device);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/** Submit all batched uploads in one queue submission. */
|
|
695
|
+
flushUploadBatch(): void {
|
|
696
|
+
if (this.uploadBatch !== null) {
|
|
697
|
+
submitMipGutterBatch(this.device, this.uploadBatch);
|
|
698
|
+
this.uploadBatch = null;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
678
702
|
private finalize(
|
|
679
703
|
aval: aval<ITexture>,
|
|
680
704
|
format: AtlasPageFormat,
|
|
@@ -804,8 +828,10 @@ export class AtlasPool {
|
|
|
804
828
|
this.device, page.texture,
|
|
805
829
|
boundsX, boundsY, reservedW, reservedH,
|
|
806
830
|
staging, w, h, slots,
|
|
831
|
+
this.uploadBatch ?? undefined,
|
|
807
832
|
);
|
|
808
|
-
|
|
833
|
+
if (this.uploadBatch !== null) this.uploadBatch.trashTextures.push(staging);
|
|
834
|
+
else void this.device.queue.onSubmittedWorkDone().then(() => staging.destroy());
|
|
809
835
|
return;
|
|
810
836
|
} catch {
|
|
811
837
|
// Source not in a copyable state (e.g. a not-ready video) —
|
package/src/window/loop.ts
CHANGED
|
@@ -131,6 +131,8 @@ export function runFrame(
|
|
|
131
131
|
let lastTime = performance.now();
|
|
132
132
|
let rafId = 0;
|
|
133
133
|
let pending = false;
|
|
134
|
+
let consecutiveErrors = 0;
|
|
135
|
+
const MAX_CONSECUTIVE_ERRORS = 30;
|
|
134
136
|
const UNCAPPED = isUncapped();
|
|
135
137
|
|
|
136
138
|
// Wrap the frame callback in an AVal so the adaptive system tracks
|
|
@@ -143,10 +145,34 @@ export function runFrame(
|
|
|
143
145
|
attachment.markFrame();
|
|
144
146
|
try {
|
|
145
147
|
frame(token, { frame: frameNo, timestampMs: now, deltaMs: delta });
|
|
148
|
+
consecutiveErrors = 0;
|
|
146
149
|
} catch (e) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
+
// A throwing frame used to kill the loop for good. That is far too harsh
|
|
151
|
+
// for a transient fault (a resource disposed mid-frame, a one-off device
|
|
152
|
+
// hiccup): the page then looks alive — DOM, input and workers all keep
|
|
153
|
+
// running — while nothing renders again and any animation driven from the
|
|
154
|
+
// frame callback freezes. Retry instead, and only give up if the fault
|
|
155
|
+
// repeats every frame (a genuinely broken scene, which would otherwise
|
|
156
|
+
// spin the error path forever).
|
|
157
|
+
consecutiveErrors++;
|
|
158
|
+
console.error(
|
|
159
|
+
`runFrame: error in frame callback (${consecutiveErrors}/${MAX_CONSECUTIVE_ERRORS})`, e,
|
|
160
|
+
);
|
|
161
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
162
|
+
stopped = true;
|
|
163
|
+
console.error("runFrame: giving up after repeated frame errors — render loop stopped");
|
|
164
|
+
throw e;
|
|
165
|
+
}
|
|
166
|
+
// Keep the loop breathing: schedule the next frame ourselves, since a
|
|
167
|
+
// frame that died early may not have read the inputs it would be marked
|
|
168
|
+
// by. Swallow the error rather than rethrowing — an exception escaping
|
|
169
|
+
// the aval eval leaves the wrapper's dirty state ambiguous, and we have
|
|
170
|
+
// already reported it.
|
|
171
|
+
queueMicrotask(() => {
|
|
172
|
+
if (stopped || pending) return;
|
|
173
|
+
pending = true;
|
|
174
|
+
rafId = schedule(tick);
|
|
175
|
+
});
|
|
150
176
|
}
|
|
151
177
|
frameNo++;
|
|
152
178
|
return frameNo;
|
|
@@ -188,8 +214,10 @@ export function runFrame(
|
|
|
188
214
|
// frame.
|
|
189
215
|
if (opts.pacer !== undefined) {
|
|
190
216
|
opts.pacer().then(finalizeFrame, (e) => {
|
|
191
|
-
|
|
217
|
+
// A rejected pacer (GPU hiccup) must not brick the loop either — the
|
|
218
|
+
// frame is done as far as we're concerned; carry on.
|
|
192
219
|
console.error("runFrame: pacer rejected", e);
|
|
220
|
+
finalizeFrame();
|
|
193
221
|
});
|
|
194
222
|
} else {
|
|
195
223
|
finalizeFrame();
|