@aardworx/wombat.rendering 0.19.25 → 0.20.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.
- package/dist/core/renderObject.d.ts +9 -0
- package/dist/core/renderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.js +9 -3
- 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/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/sceneIntegration.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/sceneIntegration.js +9 -4
- package/dist/runtime/derivedUniforms/sceneIntegration.js.map +1 -1
- package/dist/runtime/heapAdapter.d.ts.map +1 -1
- package/dist/runtime/heapAdapter.js +3 -0
- package/dist/runtime/heapAdapter.js.map +1 -1
- package/dist/runtime/heapEffect.d.ts +23 -1
- package/dist/runtime/heapEffect.d.ts.map +1 -1
- package/dist/runtime/heapEffect.js +17 -2
- package/dist/runtime/heapEffect.js.map +1 -1
- package/dist/runtime/heapEffectIR.d.ts.map +1 -1
- package/dist/runtime/heapEffectIR.js +33 -2
- package/dist/runtime/heapEffectIR.js.map +1 -1
- package/dist/runtime/heapScene.d.ts +9 -0
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +90 -1
- 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 +17 -0
- package/dist/runtime/hybridScene.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/renderObject.ts +9 -0
- package/src/resources/preparedRenderObject.ts +7 -3
- package/src/resources/uniformBuffer.ts +3 -1
- package/src/runtime/derivedUniforms/codegen.ts +34 -13
- package/src/runtime/derivedUniforms/dispatch.ts +6 -0
- package/src/runtime/derivedUniforms/sceneIntegration.ts +9 -4
- package/src/runtime/heapAdapter.ts +3 -0
- package/src/runtime/heapEffect.ts +30 -2
- package/src/runtime/heapEffectIR.ts +36 -2
- package/src/runtime/heapScene.ts +109 -1
- package/src/runtime/heapShaderFamily.ts +21 -0
- package/src/runtime/hybridScene.ts +23 -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
|
@@ -75,6 +75,10 @@ export interface HeapEffectSchema {
|
|
|
75
75
|
readonly fragmentOutputs: readonly { readonly name: string; readonly location: number; readonly wgslType: string }[];
|
|
76
76
|
readonly textures: readonly HeapTextureBinding[];
|
|
77
77
|
readonly samplers: readonly HeapSamplerBinding[];
|
|
78
|
+
/** User-declared storage buffers (the OIT node pool & co) — bound at
|
|
79
|
+
* bucket level from scene-shared avals (`HeapDrawSpec.storageBuffers`).
|
|
80
|
+
* Absent/empty for ordinary effects. */
|
|
81
|
+
readonly storageBuffers?: readonly { readonly name: string; readonly access: "read" | "read_write" }[];
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
export interface CompiledHeapEffect {
|
|
@@ -104,7 +108,7 @@ export interface FragmentOutputLayout {
|
|
|
104
108
|
// and on quota; every failure simply degrades to "recompute".
|
|
105
109
|
|
|
106
110
|
/** Cache-generation stamp for the persistent (localStorage) tier. */
|
|
107
|
-
export const HEAP_PERSIST_VERSION = "
|
|
111
|
+
export const HEAP_PERSIST_VERSION = "h7"; // h7: OIT resolve early-exit
|
|
108
112
|
|
|
109
113
|
/**
|
|
110
114
|
* Uniforms whose drawHeader word IS the u32 value (no arena
|
|
@@ -275,7 +279,11 @@ function buildSchema(iface: ProgramInterface): HeapEffectSchema {
|
|
|
275
279
|
name: s.name, wgslType: isComparison(s.type) ? "sampler_comparison" : "sampler",
|
|
276
280
|
...(s.state !== undefined ? { state: s.state } : {}),
|
|
277
281
|
}));
|
|
278
|
-
|
|
282
|
+
const storageBuffers = iface.storageBuffers.map(sb => ({ name: sb.name, access: sb.access }));
|
|
283
|
+
return {
|
|
284
|
+
attributes, uniforms, varyings, fragmentOutputs, textures, samplers,
|
|
285
|
+
...(storageBuffers.length > 0 ? { storageBuffers } : {}),
|
|
286
|
+
};
|
|
279
287
|
}
|
|
280
288
|
|
|
281
289
|
function irTypeToWgsl(t: IrType): string {
|
|
@@ -389,6 +397,15 @@ export interface BucketLayout {
|
|
|
389
397
|
* `atlasSample(...)` helper for these names only.
|
|
390
398
|
*/
|
|
391
399
|
readonly atlasTextureBindings: ReadonlySet<string>;
|
|
400
|
+
/**
|
|
401
|
+
* User storage-buffer bindings (allocated from
|
|
402
|
+
* HEAP_USER_STORAGE_BINDING_BASE, above the atlas range). Bound at
|
|
403
|
+
* bucket level from `HeapDrawSpec.storageBuffers` — every RO in the
|
|
404
|
+
* bucket shares the same buffers (they are scene-level injections
|
|
405
|
+
* like the OIT node pool). FRAGMENT visibility only (WebGPU forbids
|
|
406
|
+
* writable storage in vertex stages).
|
|
407
|
+
*/
|
|
408
|
+
readonly storageBindings: readonly { readonly name: string; readonly access: "read" | "read_write"; readonly binding: number }[];
|
|
392
409
|
}
|
|
393
410
|
|
|
394
411
|
/**
|
|
@@ -412,6 +429,8 @@ export const ATLAS_LINEAR_BINDING_BASE = 11;
|
|
|
412
429
|
export const ATLAS_SRGB_BINDING_BASE = ATLAS_LINEAR_BINDING_BASE + ATLAS_ARRAY_SIZE;
|
|
413
430
|
/** Atlas sampler binding (= linear base + 2N). */
|
|
414
431
|
export const ATLAS_SAMPLER_BINDING = ATLAS_LINEAR_BINDING_BASE + 2 * ATLAS_ARRAY_SIZE;
|
|
432
|
+
/** First user storage-buffer binding (above the whole atlas range). */
|
|
433
|
+
export const HEAP_USER_STORAGE_BINDING_BASE = ATLAS_SAMPLER_BINDING + 1;
|
|
415
434
|
|
|
416
435
|
export function buildBucketLayout(
|
|
417
436
|
schema: HeapEffectSchema,
|
|
@@ -536,10 +555,18 @@ export function buildBucketLayout(
|
|
|
536
555
|
isAtlasBucket,
|
|
537
556
|
);
|
|
538
557
|
|
|
558
|
+
// User storage buffers: deterministic order (by name) starting above
|
|
559
|
+
// the atlas binding range.
|
|
560
|
+
let nextStorageBinding = HEAP_USER_STORAGE_BINDING_BASE;
|
|
561
|
+
const storageBindings = [...(schema.storageBuffers ?? [])]
|
|
562
|
+
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
|
|
563
|
+
.map(sb => ({ name: sb.name, access: sb.access, binding: nextStorageBinding++ }));
|
|
564
|
+
|
|
539
565
|
const base = {
|
|
540
566
|
drawHeaderFields, drawHeaderBytes, preludeWgsl, strideU32,
|
|
541
567
|
perInstanceUniforms, perInstanceAttributes,
|
|
542
568
|
textureBindings, samplerBindings, atlasTextureBindings,
|
|
569
|
+
storageBindings,
|
|
543
570
|
};
|
|
544
571
|
return { id: bucketLayoutId(base), ...base };
|
|
545
572
|
}
|
|
@@ -567,6 +594,7 @@ export function bucketLayoutId(l: Omit<BucketLayout, "id">): string {
|
|
|
567
594
|
parts.push("PIA:" + [...l.perInstanceAttributes].sort().join(","));
|
|
568
595
|
for (const t of l.textureBindings) parts.push(`T:${t.name}|${t.wgslType}|${t.binding}`);
|
|
569
596
|
for (const s of l.samplerBindings) parts.push(`S:${s.name}|${s.wgslType}|${s.binding}`);
|
|
597
|
+
for (const sb of l.storageBindings) parts.push(`SB:${sb.name}|${sb.access}|${sb.binding}`);
|
|
570
598
|
parts.push("ATB:" + [...l.atlasTextureBindings].sort().join(","));
|
|
571
599
|
// The prelude is derived from the above, but fold its hash in too —
|
|
572
600
|
// cheap insurance against any generator path the field list misses.
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
import { compileModule, stage as makeStage, effect as makeEffect } from "@aardworx/wombat.shader";
|
|
21
21
|
import { isInlineHeaderField } from "./heapEffect.js";
|
|
22
22
|
import type { Effect, CompileOptions } from "@aardworx/wombat.shader";
|
|
23
|
-
import { substituteInputsInStage, readInputs, mapExpr, mapStmt, liftReturns, uniformsToInputs } from "@aardworx/wombat.shader/passes";
|
|
23
|
+
import { substituteInputsInStage, readInputs, mapExpr, mapStmt, liftReturns, uniformsToInputs, resolveHoles } from "@aardworx/wombat.shader/passes";
|
|
24
|
+
import type { HoleValue } from "@aardworx/wombat.shader/passes";
|
|
24
25
|
import { synthesizeHeapDecoderModule } from "./heapDecoder.js";
|
|
25
26
|
import {
|
|
26
27
|
type Module, type Expr, type Stmt, type Type, type ValueDef,
|
|
@@ -1020,6 +1021,22 @@ function compileHeapEffectIRViaDecoder(
|
|
|
1020
1021
|
// from the carrier varyings the decoder wrote).
|
|
1021
1022
|
combined = rewriteFsAtlasTexturesViaCarrier(combined, layout);
|
|
1022
1023
|
|
|
1024
|
+
// 6b. USER storage buffers (the OIT node pool & co): pin each to the
|
|
1025
|
+
// slot the bucket's BGL committed to. `keep: true` makes the emitter
|
|
1026
|
+
// use it verbatim (and stops `reduceUniforms` dropping an unreferenced
|
|
1027
|
+
// one). Without this the emitter auto-assigns sequentially and collides
|
|
1028
|
+
// with the heap's own reserved bindings.
|
|
1029
|
+
if (layout.storageBindings.length > 0) {
|
|
1030
|
+
const userStorageSlot = new Map(layout.storageBindings.map(b => [b.name, b.binding]));
|
|
1031
|
+
combined = {
|
|
1032
|
+
...combined,
|
|
1033
|
+
values: combined.values.map(v =>
|
|
1034
|
+
v.kind === "StorageBuffer" && userStorageSlot.has(v.name)
|
|
1035
|
+
? { ...v, binding: { group: 0, slot: userStorageSlot.get(v.name)! }, keep: true }
|
|
1036
|
+
: v),
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1023
1040
|
// 7. Compile.
|
|
1024
1041
|
const compiled = compileModule(combined, compileOptions);
|
|
1025
1042
|
if ((globalThis as { __HEAP_DEBUG_FULL__?: boolean }).__HEAP_DEBUG_FULL__) {
|
|
@@ -1566,7 +1583,24 @@ function mergeStages(eff: Effect): Module {
|
|
|
1566
1583
|
// Combine all stages' templates into one module by concatenating
|
|
1567
1584
|
// their values + types arrays. The compile pipeline's composeStages
|
|
1568
1585
|
// pass will fuse same-stage entries afterwards.
|
|
1586
|
+
//
|
|
1587
|
+
// Closure holes are resolved here (per stage, with that stage's
|
|
1588
|
+
// getters) — Effect.compile does this on the classic path, but the
|
|
1589
|
+
// heap pipeline compiles the raw templates directly, and an
|
|
1590
|
+
// unresolved `ReadInput("Closure", …)` emits as a bare identifier
|
|
1591
|
+
// ("unresolved value" at pipeline compile). Constant captures like
|
|
1592
|
+
// an OIT writer's node-pool capacity are the norm in composed
|
|
1593
|
+
// effects.
|
|
1569
1594
|
const types = eff.stages.flatMap(s => [...s.template.types]);
|
|
1570
|
-
const values = eff.stages.flatMap(s =>
|
|
1595
|
+
const values = eff.stages.flatMap(s => {
|
|
1596
|
+
let hasHoles = false;
|
|
1597
|
+
const sampled: Record<string, HoleValue> = {};
|
|
1598
|
+
for (const [name, getter] of Object.entries(s.holes ?? {})) {
|
|
1599
|
+
sampled[name] = getter();
|
|
1600
|
+
hasHoles = true;
|
|
1601
|
+
}
|
|
1602
|
+
const filled = hasHoles ? resolveHoles(s.template, sampled) : s.template;
|
|
1603
|
+
return [...filled.values];
|
|
1604
|
+
});
|
|
1571
1605
|
return { types, values };
|
|
1572
1606
|
}
|
package/src/runtime/heapScene.ts
CHANGED
|
@@ -350,6 +350,14 @@ interface Bucket {
|
|
|
350
350
|
readonly slots: BucketSlot[];
|
|
351
351
|
/** Repointed by `rebuildBindGroups` whenever any backing GrowBuffer reallocates. */
|
|
352
352
|
bindGroup: GPUBindGroup;
|
|
353
|
+
/** Scene-shared user storage buffers (layout.storageBindings), set on the
|
|
354
|
+
* first addDraw that supplies them. */
|
|
355
|
+
storageBuffers: ReadonlyMap<string, aval<IBuffer>> | undefined;
|
|
356
|
+
/** Set when a storage aval marks (e.g. the OIT head buffer reallocating on
|
|
357
|
+
* resize) — update() rebuilds this bucket's slot bind groups. */
|
|
358
|
+
storageDirty: boolean;
|
|
359
|
+
/** Disposers for the storage avals' marking subscriptions. */
|
|
360
|
+
storageSubs: (() => void)[];
|
|
353
361
|
|
|
354
362
|
// Per-bucket buffers
|
|
355
363
|
readonly drawHeap: DrawHeap;
|
|
@@ -579,6 +587,14 @@ export interface HeapDrawSpec {
|
|
|
579
587
|
* alongside the effect id.
|
|
580
588
|
*/
|
|
581
589
|
readonly textures?: HeapTextureSet;
|
|
590
|
+
/**
|
|
591
|
+
* Scene-shared storage buffers for effects that declare
|
|
592
|
+
* `var<storage>` bindings (e.g. the OIT node pool injected by the
|
|
593
|
+
* transparency task). Bound ONCE at bucket level — every RO in a
|
|
594
|
+
* bucket must supply the SAME avals (they are scene-level by
|
|
595
|
+
* construction). Buffers must be GPU-backed IBuffers.
|
|
596
|
+
*/
|
|
597
|
+
readonly storageBuffers?: ReadonlyMap<string, aval<IBuffer>>;
|
|
582
598
|
/**
|
|
583
599
|
* Optional pipeline state (rasterizer / depth / blend). Two specs
|
|
584
600
|
* sharing the SAME `PipelineState` object share a bucket; distinct
|
|
@@ -1516,6 +1532,19 @@ export function buildHeapScene(
|
|
|
1516
1532
|
// that don't yet point at an allocated AtlasPage. WebGPU rejects
|
|
1517
1533
|
// bind-group entries with `undefined` resources, so every slot in
|
|
1518
1534
|
// the N-wide ladder must hold a valid view at all times.
|
|
1535
|
+
// 4-byte placeholder storage buffer — binds user-storage slots on the
|
|
1536
|
+
// seed bind group built before the first addDraw delivers the real
|
|
1537
|
+
// buffers (rebuilt immediately after).
|
|
1538
|
+
let storagePlaceholder: GPUBuffer | undefined;
|
|
1539
|
+
function getStoragePlaceholder(): GPUBuffer {
|
|
1540
|
+
if (storagePlaceholder === undefined) {
|
|
1541
|
+
storagePlaceholder = device.createBuffer({
|
|
1542
|
+
label: "heapScene/storagePlaceholder", size: 4, usage: GPUBufferUsage.STORAGE,
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
return storagePlaceholder;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1519
1548
|
let atlasPlaceholders: Map<AtlasPageFormat, GPUTextureView> | undefined;
|
|
1520
1549
|
function getAtlasPlaceholder(format: AtlasPageFormat): GPUTextureView {
|
|
1521
1550
|
if (atlasPlaceholders === undefined) {
|
|
@@ -1546,6 +1575,7 @@ export function buildHeapScene(
|
|
|
1546
1575
|
const key =
|
|
1547
1576
|
`t${layout.textureBindings.map(t => t.wgslType).join(",")}` +
|
|
1548
1577
|
`|s${layout.samplerBindings.map(s => s.wgslType).join(",")}` +
|
|
1578
|
+
`|sb${layout.storageBindings.map(b => `${b.binding}${b.access === "read_write" ? "w" : "r"}`).join(",")}` +
|
|
1549
1579
|
`|a${withAtlasArrays ? 1 : 0}`;
|
|
1550
1580
|
let e = bglCache.get(key);
|
|
1551
1581
|
if (e !== undefined) return e;
|
|
@@ -1576,6 +1606,14 @@ export function buildHeapScene(
|
|
|
1576
1606
|
sampler: { type: s.wgslType === "sampler_comparison" ? "comparison" : "filtering" },
|
|
1577
1607
|
});
|
|
1578
1608
|
}
|
|
1609
|
+
for (const sb of layout.storageBindings) {
|
|
1610
|
+
// FRAGMENT only: WebGPU forbids writable storage in vertex stages,
|
|
1611
|
+
// and the OIT writers are fragment-side anyway.
|
|
1612
|
+
entries.push({
|
|
1613
|
+
binding: sb.binding, visibility: GPUShaderStage.FRAGMENT,
|
|
1614
|
+
buffer: { type: sb.access === "read_write" ? "storage" : "read-only-storage" },
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1579
1617
|
if (withAtlasArrays) {
|
|
1580
1618
|
// 2N + 1 entries: N linear textures, N srgb textures, 1 sampler.
|
|
1581
1619
|
for (let i = 0; i < ATLAS_ARRAY_SIZE; i++) {
|
|
@@ -1688,11 +1726,31 @@ export function buildHeapScene(
|
|
|
1688
1726
|
const cached = pipelineByModeKey.get(cacheKey);
|
|
1689
1727
|
if (cached !== undefined) return cached;
|
|
1690
1728
|
const { pipelineLayout } = getBgl(layout, isAtlasBucket);
|
|
1729
|
+
// Per-descriptor color targets: blend + write mask come from the RO's
|
|
1730
|
+
// PipelineState (`desc.attachments`, ordered by signature colorNames —
|
|
1731
|
+
// the same values the modeKey encodes). Using the bare scene-level
|
|
1732
|
+
// `colorTargets` here silently dropped blends/write-masks from every
|
|
1733
|
+
// heap pipeline: transparent heap leaves painted opaque, and the OIT
|
|
1734
|
+
// pick passes' write-mask-0 "Colors" leaked color onto the composite.
|
|
1735
|
+
const targets: GPUColorTargetState[] = colorTargets.map((t, i) => {
|
|
1736
|
+
const a = desc.attachments[i];
|
|
1737
|
+
if (a === undefined) return { format: t.format };
|
|
1738
|
+
return {
|
|
1739
|
+
format: t.format,
|
|
1740
|
+
writeMask: a.writeMask,
|
|
1741
|
+
...(a.enabled
|
|
1742
|
+
? { blend: {
|
|
1743
|
+
color: { operation: a.color.operation, srcFactor: a.color.srcFactor, dstFactor: a.color.dstFactor },
|
|
1744
|
+
alpha: { operation: a.alpha.operation, srcFactor: a.alpha.srcFactor, dstFactor: a.alpha.dstFactor },
|
|
1745
|
+
} }
|
|
1746
|
+
: {}),
|
|
1747
|
+
};
|
|
1748
|
+
});
|
|
1691
1749
|
const pipeline = device.createRenderPipeline({
|
|
1692
1750
|
label: `heapScene/cached/${cacheKey}`,
|
|
1693
1751
|
layout: pipelineLayout,
|
|
1694
1752
|
vertex: { module: fam.vsModule, entryPoint: fam.vsEntryName, buffers: [] },
|
|
1695
|
-
fragment: { module: fam.fsModule, entryPoint: fam.fsEntryName, targets
|
|
1753
|
+
fragment: { module: fam.fsModule, entryPoint: fam.fsEntryName, targets },
|
|
1696
1754
|
primitive: { topology: desc.topology, cullMode: desc.cullMode, frontFace: desc.frontFace },
|
|
1697
1755
|
...(depthFormat !== undefined && desc.depth !== undefined
|
|
1698
1756
|
? { depthStencil: { format: depthFormat, depthWriteEnabled: desc.depth.write, depthCompare: desc.depth.compare } }
|
|
@@ -2066,6 +2124,23 @@ export function buildHeapScene(
|
|
|
2066
2124
|
// here — the atlas slots come from the per-format page tracking
|
|
2067
2125
|
// below.
|
|
2068
2126
|
}
|
|
2127
|
+
for (const sb of bucket.layout.storageBindings) {
|
|
2128
|
+
const av = bucket.storageBuffers?.get(sb.name);
|
|
2129
|
+
if (av === undefined) {
|
|
2130
|
+
// Buffers arrive with the first addDraw; the seed slot built at
|
|
2131
|
+
// bucket creation is rebuilt right after they land. Bind a
|
|
2132
|
+
// 4-byte placeholder so the seed bind group validates.
|
|
2133
|
+
entries.push({ binding: sb.binding, resource: { buffer: getStoragePlaceholder() } });
|
|
2134
|
+
continue;
|
|
2135
|
+
}
|
|
2136
|
+
const ib = av.force(/* allow-force */);
|
|
2137
|
+
if (ib.kind !== "gpu") {
|
|
2138
|
+
throw new Error(
|
|
2139
|
+
`heapScene: user storage buffer '${sb.name}' must be a GPU-backed IBuffer`,
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
2142
|
+
entries.push({ binding: sb.binding, resource: { buffer: ib.buffer } });
|
|
2143
|
+
}
|
|
2069
2144
|
if (bucket.isAtlasBucket) {
|
|
2070
2145
|
if (atlasPool === undefined) {
|
|
2071
2146
|
throw new Error(
|
|
@@ -3008,6 +3083,7 @@ export function buildHeapScene(
|
|
|
3008
3083
|
layout.drawHeaderFields.forEach((f, i) => fieldIdx.set(f.name, i));
|
|
3009
3084
|
const bucket: Bucket = {
|
|
3010
3085
|
label: bk, textures: undefined, layout, chunkIdx,
|
|
3086
|
+
storageBuffers: undefined, storageDirty: false, storageSubs: [],
|
|
3011
3087
|
slots: [], // populated lazily by ensureSlot per modeKey
|
|
3012
3088
|
bindGroup: null as unknown as GPUBindGroup, // legacy field; slot.bindGroup is the real one
|
|
3013
3089
|
drawHeap,
|
|
@@ -3334,6 +3410,28 @@ export function buildHeapScene(
|
|
|
3334
3410
|
: arena.attrs.openPage();
|
|
3335
3411
|
}
|
|
3336
3412
|
const bucket = findOrCreateBucket(spec.effect, spec.textures, spec.pipelineState, chunkIdx, precomputedDescriptor);
|
|
3413
|
+
// User storage buffers (layout.storageBindings): bucket-level, set once.
|
|
3414
|
+
if (bucket.layout.storageBindings.length > 0 && bucket.storageBuffers === undefined) {
|
|
3415
|
+
if (spec.storageBuffers === undefined) {
|
|
3416
|
+
throw new Error(
|
|
3417
|
+
`heapScene: bucket '${bucket.label}' needs storage buffers ` +
|
|
3418
|
+
`(${bucket.layout.storageBindings.map(b => b.name).join(", ")}) but spec.storageBuffers is undefined`,
|
|
3419
|
+
);
|
|
3420
|
+
}
|
|
3421
|
+
bucket.storageBuffers = spec.storageBuffers;
|
|
3422
|
+
// A storage aval marking (e.g. the OIT head buffer reallocating on a
|
|
3423
|
+
// canvas resize) invalidates every slot bind group of this bucket.
|
|
3424
|
+
for (const av of spec.storageBuffers.values()) {
|
|
3425
|
+
const disposer = (av as unknown as { addMarkingCallback?: (cb: () => void) => () => void })
|
|
3426
|
+
.addMarkingCallback?.(() => { bucket.storageDirty = true; });
|
|
3427
|
+
if (disposer !== undefined) bucket.storageSubs.push(disposer);
|
|
3428
|
+
}
|
|
3429
|
+
// Slots seeded before the buffers were known carry incomplete bind
|
|
3430
|
+
// groups — rebuild them now.
|
|
3431
|
+
for (let i = 0; i < bucket.slots.length; i++) {
|
|
3432
|
+
bucket.slots[i]!.bindGroup = buildBucketBindGroup(bucket, i);
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3337
3435
|
// Phase 5c.3 — every RO carrying a derived-mode rule is registered
|
|
3338
3436
|
// on the bucket (lazy-promoting it to GPU-routed on the first
|
|
3339
3437
|
// call). ROs may carry DIFFERENT rules even when they share the
|
|
@@ -4239,6 +4337,16 @@ export function buildHeapScene(
|
|
|
4239
4337
|
sceneObj.evaluateAlways(token, (tok) => {
|
|
4240
4338
|
drainAsetWith(tok);
|
|
4241
4339
|
|
|
4340
|
+
// User storage buffers that marked (e.g. the OIT head buffer
|
|
4341
|
+
// reallocating on a resize) invalidate the bucket's bind groups.
|
|
4342
|
+
for (const b of buckets) {
|
|
4343
|
+
if (!b.storageDirty) continue;
|
|
4344
|
+
b.storageDirty = false;
|
|
4345
|
+
for (let i = 0; i < b.slots.length; i++) {
|
|
4346
|
+
b.slots[i]!.bindGroup = buildBucketBindGroup(b, i);
|
|
4347
|
+
}
|
|
4348
|
+
}
|
|
4349
|
+
|
|
4242
4350
|
// 0. Reactive rebucket: any RO whose PipelineState modeKey
|
|
4243
4351
|
// changed since last frame moves to its new bucket.
|
|
4244
4352
|
// `dirtyModeKeyDrawIds` was populated by ModeKeyTracker's
|
|
@@ -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 {
|
|
@@ -259,6 +265,9 @@ export function compileHybridScene(
|
|
|
259
265
|
...(opts.enableFamilyMerge === true ? { enableFamilyMerge: true } : {}),
|
|
260
266
|
...(opts.enableDerivedUniforms === false ? { enableDerivedUniforms: false } : {}),
|
|
261
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 } : {}),
|
|
262
271
|
});
|
|
263
272
|
|
|
264
273
|
// ─── Legacy subset → RenderTree → ScenePass ──────────────────────
|
|
@@ -269,10 +278,24 @@ export function compileHybridScene(
|
|
|
269
278
|
const legacyTree: RenderTree =
|
|
270
279
|
RenderTree.unorderedFromSet(legacyAset.map(ro => RenderTree.leaf(ro)));
|
|
271
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);
|
|
272
290
|
|
|
273
291
|
return {
|
|
274
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();
|
|
275
297
|
heapScene.update(token);
|
|
298
|
+
atlasPool.flushUploadBatch();
|
|
276
299
|
scenePass.update(token);
|
|
277
300
|
},
|
|
278
301
|
encodeComputePrep(enc: GPUCommandEncoder, token: AdaptiveToken): void {
|
|
@@ -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) —
|