@aardworx/wombat.rendering 0.9.2 → 0.9.4
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/buffer.d.ts.map +1 -1
- package/dist/core/buffer.js +65 -5
- package/dist/core/buffer.js.map +1 -1
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/provider.d.ts +38 -0
- package/dist/core/provider.d.ts.map +1 -0
- package/dist/core/provider.js +104 -0
- package/dist/core/provider.js.map +1 -0
- package/dist/core/renderObject.d.ts +17 -10
- package/dist/core/renderObject.d.ts.map +1 -1
- package/dist/core/sampler.d.ts.map +1 -1
- package/dist/core/sampler.js +71 -2
- package/dist/core/sampler.js.map +1 -1
- package/dist/core/texture.d.ts +9 -0
- package/dist/core/texture.d.ts.map +1 -1
- package/dist/core/texture.js +0 -0
- package/dist/core/texture.js.map +1 -1
- package/dist/resources/adaptiveTexture.d.ts.map +1 -1
- package/dist/resources/adaptiveTexture.js +7 -0
- package/dist/resources/adaptiveTexture.js.map +1 -1
- package/dist/resources/preparedRenderObject.d.ts.map +1 -1
- package/dist/resources/preparedRenderObject.js +20 -18
- package/dist/resources/preparedRenderObject.js.map +1 -1
- package/dist/resources/uniformBuffer.d.ts.map +1 -1
- package/dist/resources/uniformBuffer.js +23 -0
- package/dist/resources/uniformBuffer.js.map +1 -1
- package/dist/runtime/derivedUniforms/slots.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/slots.js +4 -0
- package/dist/runtime/derivedUniforms/slots.js.map +1 -1
- package/dist/runtime/heapAdapter.d.ts.map +1 -1
- package/dist/runtime/heapAdapter.js +72 -29
- package/dist/runtime/heapAdapter.js.map +1 -1
- package/dist/runtime/heapDecoder.d.ts +52 -0
- package/dist/runtime/heapDecoder.d.ts.map +1 -0
- package/dist/runtime/heapDecoder.js +363 -0
- package/dist/runtime/heapDecoder.js.map +1 -0
- package/dist/runtime/heapEffectIR.d.ts.map +1 -1
- package/dist/runtime/heapEffectIR.js +300 -3
- package/dist/runtime/heapEffectIR.js.map +1 -1
- package/dist/runtime/heapEligibility.d.ts.map +1 -1
- package/dist/runtime/heapEligibility.js +42 -5
- package/dist/runtime/heapEligibility.js.map +1 -1
- package/dist/runtime/heapIrBuilders.d.ts +64 -0
- package/dist/runtime/heapIrBuilders.d.ts.map +1 -0
- package/dist/runtime/heapIrBuilders.js +311 -0
- package/dist/runtime/heapIrBuilders.js.map +1 -0
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +329 -112
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.d.ts +67 -4
- package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.js +152 -18
- package/dist/runtime/textureAtlas/atlasPool.js.map +1 -1
- package/package.json +3 -3
- package/src/core/buffer.ts +58 -6
- package/src/core/index.ts +11 -0
- package/src/core/provider.ts +143 -0
- package/src/core/renderObject.ts +17 -10
- package/src/core/sampler.ts +64 -2
- package/src/core/texture.ts +0 -0
- package/src/resources/adaptiveTexture.ts +7 -0
- package/src/resources/preparedRenderObject.ts +21 -16
- package/src/resources/uniformBuffer.ts +21 -0
- package/src/runtime/derivedUniforms/slots.ts +4 -0
- package/src/runtime/heapAdapter.ts +63 -29
- package/src/runtime/heapDecoder.ts +446 -0
- package/src/runtime/heapEffectIR.ts +314 -3
- package/src/runtime/heapEligibility.ts +34 -5
- package/src/runtime/heapIrBuilders.ts +365 -0
- package/src/runtime/heapScene.ts +338 -96
- package/src/runtime/textureAtlas/atlasPool.ts +152 -16
package/src/runtime/heapScene.ts
CHANGED
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
import { Trafo3d, V3d, V4f, M44d, type V2f } from "@aardworx/wombat.base";
|
|
46
46
|
import type { ITexture } from "../core/texture.js";
|
|
47
47
|
import type { ISampler } from "../core/sampler.js";
|
|
48
|
-
import { AVal, AdaptiveObject, AdaptiveToken } from "@aardworx/wombat.adaptive";
|
|
48
|
+
import { AVal, AdaptiveObject, AdaptiveToken, HashTable } from "@aardworx/wombat.adaptive";
|
|
49
49
|
import type { aval, aset, IAdaptiveObject, IDisposable, IHashSetReader } from "@aardworx/wombat.adaptive";
|
|
50
|
-
import type { Effect } from "@aardworx/wombat.shader";
|
|
50
|
+
import type { Effect, CompileOptions } from "@aardworx/wombat.shader";
|
|
51
51
|
import type { PipelineState } from "../core/pipelineState.js";
|
|
52
52
|
import type { BufferView } from "../core/bufferView.js";
|
|
53
53
|
import type { IBuffer, HostBufferSource } from "../core/buffer.js";
|
|
@@ -174,21 +174,43 @@ const PACKER_F32: WgslPacker = {
|
|
|
174
174
|
dataBytes: 4, typeId: 0,
|
|
175
175
|
pack: (val, dst, off) => { dst[off] = val as number; },
|
|
176
176
|
};
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
177
|
+
// Integer scalars / vectors. The arena is fronted by a `Float32Array`,
|
|
178
|
+
// so writing raw bits has to go through a same-buffer Uint32/Int32 view
|
|
179
|
+
// to avoid the lossy `i32 → f32` coercion you'd get from a direct
|
|
180
|
+
// `dst[off] = ...` assignment.
|
|
181
|
+
function makeIntPacker(
|
|
182
|
+
ctor: typeof Uint32Array | typeof Int32Array,
|
|
183
|
+
dim: 1 | 2 | 3 | 4,
|
|
184
|
+
): WgslPacker {
|
|
185
|
+
const bytes = dim * 4;
|
|
186
|
+
if (dim === 1) {
|
|
187
|
+
return {
|
|
188
|
+
dataBytes: bytes, typeId: 0,
|
|
189
|
+
pack: (val, dst, off) => {
|
|
190
|
+
new ctor(dst.buffer as ArrayBuffer, dst.byteOffset + off * 4, 1)[0] = val as number;
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
// Vector: accept {x,y,z,w} components.
|
|
195
|
+
return {
|
|
196
|
+
dataBytes: bytes, typeId: 0,
|
|
197
|
+
pack: (val, dst, off) => {
|
|
198
|
+
const view = new ctor(dst.buffer as ArrayBuffer, dst.byteOffset + off * 4, dim);
|
|
199
|
+
const v = val as { x: number; y: number; z?: number; w?: number };
|
|
200
|
+
view[0] = v.x; view[1] = v.y;
|
|
201
|
+
if (dim >= 3) view[2] = v.z!;
|
|
202
|
+
if (dim >= 4) view[3] = v.w!;
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const PACKER_U32 = makeIntPacker(Uint32Array, 1);
|
|
207
|
+
const PACKER_UVEC2 = makeIntPacker(Uint32Array, 2);
|
|
208
|
+
const PACKER_UVEC3 = makeIntPacker(Uint32Array, 3);
|
|
209
|
+
const PACKER_UVEC4 = makeIntPacker(Uint32Array, 4);
|
|
210
|
+
const PACKER_I32 = makeIntPacker(Int32Array, 1);
|
|
211
|
+
const PACKER_IVEC2 = makeIntPacker(Int32Array, 2);
|
|
212
|
+
const PACKER_IVEC3 = makeIntPacker(Int32Array, 3);
|
|
213
|
+
const PACKER_IVEC4 = makeIntPacker(Int32Array, 4);
|
|
192
214
|
|
|
193
215
|
function packerForWgslType(wgslType: string): WgslPacker {
|
|
194
216
|
switch (wgslType) {
|
|
@@ -198,7 +220,13 @@ function packerForWgslType(wgslType: string): WgslPacker {
|
|
|
198
220
|
case "vec2<f32>": return PACKER_VEC2;
|
|
199
221
|
case "f32": return PACKER_F32;
|
|
200
222
|
case "u32": return PACKER_U32;
|
|
223
|
+
case "vec2<u32>": return PACKER_UVEC2;
|
|
224
|
+
case "vec3<u32>": return PACKER_UVEC3;
|
|
225
|
+
case "vec4<u32>": return PACKER_UVEC4;
|
|
201
226
|
case "i32": return PACKER_I32;
|
|
227
|
+
case "vec2<i32>": return PACKER_IVEC2;
|
|
228
|
+
case "vec3<i32>": return PACKER_IVEC3;
|
|
229
|
+
case "vec4<i32>": return PACKER_IVEC4;
|
|
202
230
|
default:
|
|
203
231
|
throw new Error(`heapScene: no JS-side packer for WGSL type '${wgslType}'`);
|
|
204
232
|
}
|
|
@@ -330,6 +358,17 @@ interface PoolEntry {
|
|
|
330
358
|
* Same code path either way.
|
|
331
359
|
*/
|
|
332
360
|
class UniformPool {
|
|
361
|
+
// Keyed by `aval<unknown>` *by reference* (a plain JS `Map`). These
|
|
362
|
+
// keys are overwhelmingly reactive `cval`s (per-object trafos,
|
|
363
|
+
// colours, …) and the hot path is `acquire`/`release` ~once per
|
|
364
|
+
// drawHeader field per RO. A content-keyed `HashTable` would buy
|
|
365
|
+
// nothing here — reactive avals never compare content-equal — and
|
|
366
|
+
// would cost: reactive avals have no `equals`/`getHashCode`, so a
|
|
367
|
+
// `HashTable` falls back to a WeakMap-counter identity hash per
|
|
368
|
+
// lookup, measurably slower than `Map`'s native hashing. (Constant-
|
|
369
|
+
// aval dedup matters where keys are *texture* avals — there the
|
|
370
|
+
// `AtlasPool` is content-keyed; constant avals there carry a cached
|
|
371
|
+
// hash and a fast `equals`.)
|
|
333
372
|
private readonly byAval = new Map<aval<unknown>, PoolEntry>();
|
|
334
373
|
|
|
335
374
|
has(aval: aval<unknown>): boolean { return this.byAval.has(aval); }
|
|
@@ -603,19 +642,62 @@ class AttributeArena {
|
|
|
603
642
|
}
|
|
604
643
|
release(ref: number, dataBytes: number): void {
|
|
605
644
|
const allocBytes = ALIGN16(ALLOC_HEADER_PAD_TO + dataBytes);
|
|
606
|
-
this.freeList
|
|
607
|
-
// Coalesce adjacent free entries for cleanliness.
|
|
608
|
-
this.freeList.sort((a, b) => a.off - b.off);
|
|
609
|
-
for (let i = 0; i < this.freeList.length - 1; ) {
|
|
610
|
-
const a = this.freeList[i]!, b = this.freeList[i + 1]!;
|
|
611
|
-
if (a.off + a.size === b.off) { a.size += b.size; this.freeList.splice(i + 1, 1); }
|
|
612
|
-
else i++;
|
|
613
|
-
}
|
|
645
|
+
insertSortedFreeBlock(this.freeList, ref, allocBytes);
|
|
614
646
|
}
|
|
615
647
|
onResize(cb: () => void): IDisposable { return this.buf.onResize(cb); }
|
|
616
648
|
destroy(): void { this.buf.destroy(); }
|
|
617
649
|
}
|
|
618
650
|
|
|
651
|
+
/**
|
|
652
|
+
* Insert `{off, size}` into a free-list kept sorted by `off`, then
|
|
653
|
+
* coalesce with the two immediate neighbours.
|
|
654
|
+
*
|
|
655
|
+
* The list invariant — sorted, non-overlapping, never-adjacent — is
|
|
656
|
+
* preserved across allocs (which take from the front or split a
|
|
657
|
+
* block) and releases (this function). The previous implementation
|
|
658
|
+
* did `push + Array.prototype.sort + linear coalesce scan`, which is
|
|
659
|
+
* O(N log N) per release. Under a 500-RO bulk-remove the sort
|
|
660
|
+
* dominated `removeDraw` (~41 ms of self-time in the heap-demo-sg
|
|
661
|
+
* toggle profile). Binary-search insert + 2-neighbour merge collapses
|
|
662
|
+
* that to O(log N + N-shift), and is principled — the sort never
|
|
663
|
+
* actually mattered since we already had the sorted prefix as an
|
|
664
|
+
* invariant.
|
|
665
|
+
*/
|
|
666
|
+
function insertSortedFreeBlock(
|
|
667
|
+
freeList: { off: number; size: number }[],
|
|
668
|
+
off: number,
|
|
669
|
+
size: number,
|
|
670
|
+
): void {
|
|
671
|
+
// Binary-search for the insertion index (first entry whose off > new).
|
|
672
|
+
let lo = 0, hi = freeList.length;
|
|
673
|
+
while (lo < hi) {
|
|
674
|
+
const mid = (lo + hi) >>> 1;
|
|
675
|
+
if (freeList[mid]!.off <= off) lo = mid + 1;
|
|
676
|
+
else hi = mid;
|
|
677
|
+
}
|
|
678
|
+
// lo is the index where the new entry would be inserted.
|
|
679
|
+
// Try merging with the predecessor first; if successful, the merged
|
|
680
|
+
// block may now be adjacent to its (former) successor too.
|
|
681
|
+
const prev = lo > 0 ? freeList[lo - 1] : undefined;
|
|
682
|
+
if (prev !== undefined && prev.off + prev.size === off) {
|
|
683
|
+
prev.size += size;
|
|
684
|
+
// Check forward-merge with what was freeList[lo].
|
|
685
|
+
const next = freeList[lo];
|
|
686
|
+
if (next !== undefined && prev.off + prev.size === next.off) {
|
|
687
|
+
prev.size += next.size;
|
|
688
|
+
freeList.splice(lo, 1);
|
|
689
|
+
}
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
const next = freeList[lo];
|
|
693
|
+
if (next !== undefined && off + size === next.off) {
|
|
694
|
+
next.off = off;
|
|
695
|
+
next.size += size;
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
freeList.splice(lo, 0, { off, size });
|
|
699
|
+
}
|
|
700
|
+
|
|
619
701
|
/**
|
|
620
702
|
* Element-bump allocator over an index GrowBuffer (units = u32). Each
|
|
621
703
|
* draw's index range is allocated as one block; on release the block
|
|
@@ -674,13 +756,7 @@ class IndexAllocator {
|
|
|
674
756
|
return off;
|
|
675
757
|
}
|
|
676
758
|
release(off: number, elements: number): void {
|
|
677
|
-
this.freeList
|
|
678
|
-
this.freeList.sort((a, b) => a.off - b.off);
|
|
679
|
-
for (let i = 0; i < this.freeList.length - 1; ) {
|
|
680
|
-
const a = this.freeList[i]!, b = this.freeList[i + 1]!;
|
|
681
|
-
if (a.off + a.size === b.off) { a.size += b.size; this.freeList.splice(i + 1, 1); }
|
|
682
|
-
else i++;
|
|
683
|
-
}
|
|
759
|
+
insertSortedFreeBlock(this.freeList, off, elements);
|
|
684
760
|
}
|
|
685
761
|
onResize(cb: () => void): IDisposable { return this.buf.onResize(cb); }
|
|
686
762
|
destroy(): void { this.buf.destroy(); }
|
|
@@ -995,8 +1071,15 @@ interface Bucket {
|
|
|
995
1071
|
*/
|
|
996
1072
|
readonly localLayoutIds: (number | undefined)[];
|
|
997
1073
|
|
|
998
|
-
/**
|
|
999
|
-
|
|
1074
|
+
/**
|
|
1075
|
+
* Live local slots. A Set rather than an array so addDraw/removeDraw
|
|
1076
|
+
* are both O(1): a 500-element bulk-remove on a 1000-element bucket
|
|
1077
|
+
* would otherwise burn 250K shifts in the linear-array splice path.
|
|
1078
|
+
* The render loop iterates this once per frame; iteration order
|
|
1079
|
+
* isn't load-bearing (slot→record indirection happens via
|
|
1080
|
+
* `slotToRecord` / `recordToSlot`).
|
|
1081
|
+
*/
|
|
1082
|
+
readonly drawSlots: Set<number>;
|
|
1000
1083
|
/** Local slots whose DrawHeader needs re-pack + writeBuffer next frame. */
|
|
1001
1084
|
readonly dirty: Set<number>;
|
|
1002
1085
|
|
|
@@ -1039,6 +1122,12 @@ interface Bucket {
|
|
|
1039
1122
|
* GrowBuffer reallocation. Standalone-only buckets keep this empty.
|
|
1040
1123
|
*/
|
|
1041
1124
|
readonly localAtlasTextures: ((HeapTextureSet & { kind: "atlas" }) | undefined)[];
|
|
1125
|
+
/**
|
|
1126
|
+
* Per-local-slot index into `atlasAvalRefs[sourceAval]` — lets
|
|
1127
|
+
* removeDraw do swap-pop without an O(N) findIndex scan. `undefined`
|
|
1128
|
+
* for slots without an atlas source aval.
|
|
1129
|
+
*/
|
|
1130
|
+
readonly localAtlasArrIdx: (number | undefined)[];
|
|
1042
1131
|
}
|
|
1043
1132
|
|
|
1044
1133
|
// ---------------------------------------------------------------------------
|
|
@@ -1310,6 +1399,19 @@ export function buildHeapScene(
|
|
|
1310
1399
|
throw new Error("buildHeapScene: framebuffer signature has no color attachment");
|
|
1311
1400
|
}
|
|
1312
1401
|
const colorFormat = sig.colors.tryFind(colorAttachmentName)!;
|
|
1402
|
+
// All color targets, ordered by the signature's `colorNames`. Each
|
|
1403
|
+
// entry's format flows into the pipeline's `fragment.targets[i].format`.
|
|
1404
|
+
// The fragmentOutputLayout's `locations` map tells the WGSL emit which
|
|
1405
|
+
// output @location maps to which color attachment; this array is the
|
|
1406
|
+
// matching pipeline-side ordering.
|
|
1407
|
+
const colorTargets: { format: GPUTextureFormat }[] = [];
|
|
1408
|
+
for (const name of sig.colorNames) {
|
|
1409
|
+
const fmt = sig.colors.tryFind(name);
|
|
1410
|
+
if (fmt === undefined) {
|
|
1411
|
+
throw new Error(`buildHeapScene: signature missing format for color '${name}'`);
|
|
1412
|
+
}
|
|
1413
|
+
colorTargets.push({ format: fmt });
|
|
1414
|
+
}
|
|
1313
1415
|
const depthFormat = sig.depthStencil?.format;
|
|
1314
1416
|
|
|
1315
1417
|
// ─── Global arena (uniform/attribute data + index buffer) ────────
|
|
@@ -1513,7 +1615,11 @@ export function buildHeapScene(
|
|
|
1513
1615
|
readonly repack: (newTex: ITexture) => import("./textureAtlas/atlasPool.js").AtlasAcquisition;
|
|
1514
1616
|
readonly sampler: ISampler;
|
|
1515
1617
|
}
|
|
1516
|
-
|
|
1618
|
+
// Content-keyed (`HashTable`, not a JS `Map`) so distinct
|
|
1619
|
+
// `AVal.constant(tex)` wrappers sharing the same texture collapse to
|
|
1620
|
+
// one `(bucket, slot)` ref-list — matching `AtlasPool.entriesByAval`,
|
|
1621
|
+
// which is now content-keyed too. Reactive avals key by reference.
|
|
1622
|
+
const atlasAvalRefs = new HashTable<aval<ITexture>, AtlasAvalRef[]>();
|
|
1517
1623
|
/**
|
|
1518
1624
|
* Per-draw bucket dirty (rare in steady state — only fires when
|
|
1519
1625
|
* something forces a header rewrite, e.g. a drawHeap GrowBuffer
|
|
@@ -1750,9 +1856,20 @@ export function buildHeapScene(
|
|
|
1750
1856
|
schema: ShaderFamilySchema;
|
|
1751
1857
|
vsModule: GPUShaderModule;
|
|
1752
1858
|
fsModule: GPUShaderModule;
|
|
1753
|
-
|
|
1859
|
+
/** Actual @vertex entry-point name in `vsModule`. */
|
|
1860
|
+
vsEntryName: string;
|
|
1861
|
+
/** Actual @fragment entry-point name in `fsModule`. */
|
|
1862
|
+
fsEntryName: string;
|
|
1863
|
+
fieldsForEffect: Map<string, Set<string>>;
|
|
1754
1864
|
}
|
|
1755
|
-
|
|
1865
|
+
// Keyed by `effect.id` (content hash), NOT object identity. Two
|
|
1866
|
+
// Effect objects with identical content (e.g. produced by separate
|
|
1867
|
+
// calls to `effect(...)` or the pickChain composer) share one
|
|
1868
|
+
// FamilyState. This is the right correctness/perf knob: building a
|
|
1869
|
+
// family by content means an upstream caller that legitimately
|
|
1870
|
+
// produces different-but-identical Effect objects per leaf still
|
|
1871
|
+
// reuses one pipeline + one bucket.
|
|
1872
|
+
const familyByEffectId = new Map<string, FamilyState>();
|
|
1756
1873
|
let familyBuilt = false;
|
|
1757
1874
|
const enableFamilyMerge = opts.enableFamilyMerge === true;
|
|
1758
1875
|
|
|
@@ -1760,26 +1877,50 @@ export function buildHeapScene(
|
|
|
1760
1877
|
effects: readonly Effect[],
|
|
1761
1878
|
perInstanceByEffect: ReadonlyMap<Effect, { attributes: Set<string>; uniforms: Set<string> }>,
|
|
1762
1879
|
): FamilyState {
|
|
1880
|
+
// Family-merge (multi-effect dispatch via layoutId switch) has been
|
|
1881
|
+
// disabled — it was a perf illusion. Every effect compiles to its
|
|
1882
|
+
// own standalone pipeline; the per-bucket WGSL comes from
|
|
1883
|
+
// `compileHeapEffectIR(effect, layout, opts, "standalone")` and is
|
|
1884
|
+
// used directly as the bucket's shader module (no wrapper, no
|
|
1885
|
+
// dispatch). The schema is still derived from the same
|
|
1886
|
+
// `buildShaderFamily` path so the runtime's drawHeader packer,
|
|
1887
|
+
// bind-group layout, etc. don't change shape — `__layoutId`
|
|
1888
|
+
// remains a u32 slot that's written as 0 per RO (harmless).
|
|
1889
|
+
//
|
|
1890
|
+
// `heapShaderFamily.compileShaderFamily` is kept around but
|
|
1891
|
+
// unused; the function and its tests stay disabled.
|
|
1892
|
+
if (effects.length !== 1) {
|
|
1893
|
+
throw new Error(
|
|
1894
|
+
"heapScene: multi-effect family-merge disabled (was a perf illusion). " +
|
|
1895
|
+
"Build one bucket per effect.",
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
const effect = effects[0]!;
|
|
1763
1899
|
const schema = buildShaderFamily(
|
|
1764
|
-
|
|
1900
|
+
[effect], opts.fragmentOutputLayout, undefined,
|
|
1765
1901
|
{
|
|
1766
1902
|
atlasizeAllTextures: atlasPool !== undefined,
|
|
1767
1903
|
perEffectPerInstance: perInstanceByEffect,
|
|
1768
1904
|
},
|
|
1769
1905
|
);
|
|
1770
|
-
const
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
const
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1906
|
+
const compileOpts: CompileOptions = opts.fragmentOutputLayout !== undefined
|
|
1907
|
+
? { target: "wgsl", fragmentOutputLayout: opts.fragmentOutputLayout }
|
|
1908
|
+
: { target: "wgsl" };
|
|
1909
|
+
const ir = compileHeapEffectIR(effect, schema.drawHeaderUnion, compileOpts, "standalone");
|
|
1910
|
+
const vsModule = device.createShaderModule({ code: ir.vs, label: `heapScene/standalone/${schema.id}/vs` });
|
|
1911
|
+
const fsModule = device.createShaderModule({ code: ir.fs, label: `heapScene/standalone/${schema.id}/fs` });
|
|
1912
|
+
const fieldsForEffect = new Map<string, Set<string>>();
|
|
1913
|
+
const s = schema.perEffectSchema.get(effect)!;
|
|
1914
|
+
const fields = new Set<string>();
|
|
1915
|
+
for (const a of s.attributes) fields.add(a.name);
|
|
1916
|
+
for (const u of s.uniforms) fields.add(u.name);
|
|
1917
|
+
for (const t of s.textures) fields.add(t.name);
|
|
1918
|
+
fieldsForEffect.set(effect.id, fields);
|
|
1919
|
+
return {
|
|
1920
|
+
schema, vsModule, fsModule, fieldsForEffect,
|
|
1921
|
+
vsEntryName: ir.vsEntry,
|
|
1922
|
+
fsEntryName: ir.fsEntry,
|
|
1923
|
+
};
|
|
1783
1924
|
}
|
|
1784
1925
|
|
|
1785
1926
|
function buildFamilyFromSpecs(specs: readonly HeapDrawSpec[]): void {
|
|
@@ -1793,45 +1934,42 @@ export function buildHeapScene(
|
|
|
1793
1934
|
// layout to address per-instance attribute reads via `instId`
|
|
1794
1935
|
// instead of `vertex_index` — without this, instanced effects
|
|
1795
1936
|
// produce broken geometry.
|
|
1796
|
-
|
|
1937
|
+
// De-dupe by effect.id so two Effects with identical content but
|
|
1938
|
+
// distinct object identities collapse to one family + one bucket.
|
|
1939
|
+
const seenIds = new Set<string>();
|
|
1797
1940
|
const unique: Effect[] = [];
|
|
1798
|
-
const
|
|
1941
|
+
const perInstanceByEffectId = new Map<string, {
|
|
1799
1942
|
attributes: Set<string>;
|
|
1800
1943
|
uniforms: Set<string>;
|
|
1801
1944
|
}>();
|
|
1802
1945
|
for (const spec of specs) {
|
|
1803
1946
|
const e = spec.effect;
|
|
1804
|
-
let entry =
|
|
1947
|
+
let entry = perInstanceByEffectId.get(e.id);
|
|
1805
1948
|
if (entry === undefined) {
|
|
1806
1949
|
entry = { attributes: new Set<string>(), uniforms: new Set<string>() };
|
|
1807
|
-
|
|
1950
|
+
perInstanceByEffectId.set(e.id, entry);
|
|
1808
1951
|
}
|
|
1809
1952
|
if (spec.instanceAttributes !== undefined) {
|
|
1810
1953
|
for (const name of Object.keys(spec.instanceAttributes)) entry.attributes.add(name);
|
|
1811
1954
|
}
|
|
1812
|
-
if (!
|
|
1955
|
+
if (!seenIds.has(e.id)) { seenIds.add(e.id); unique.push(e); }
|
|
1813
1956
|
}
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
const perI = perInstanceByEffect.get(e);
|
|
1823
|
-
const singleMap = new Map<Effect, { attributes: Set<string>; uniforms: Set<string> }>();
|
|
1824
|
-
if (perI !== undefined) singleMap.set(e, perI);
|
|
1825
|
-
familyByEffect.set(e, compileFamilyFor([e], singleMap));
|
|
1826
|
-
}
|
|
1957
|
+
// Family-merge disabled: always one bucket per effect.
|
|
1958
|
+
// `enableFamilyMerge` is ignored.
|
|
1959
|
+
void enableFamilyMerge;
|
|
1960
|
+
for (const e of unique) {
|
|
1961
|
+
const perI = perInstanceByEffectId.get(e.id);
|
|
1962
|
+
const singleMap = new Map<Effect, { attributes: Set<string>; uniforms: Set<string> }>();
|
|
1963
|
+
if (perI !== undefined) singleMap.set(e, perI);
|
|
1964
|
+
familyByEffectId.set(e.id, compileFamilyFor([e], singleMap));
|
|
1827
1965
|
}
|
|
1828
1966
|
familyBuilt = true;
|
|
1829
1967
|
}
|
|
1830
1968
|
|
|
1831
1969
|
function familyFor(effect: Effect): FamilyState {
|
|
1832
|
-
const f =
|
|
1970
|
+
const f = familyByEffectId.get(effect.id);
|
|
1833
1971
|
if (f === undefined) {
|
|
1834
|
-
const known = [...
|
|
1972
|
+
const known = [...familyByEffectId.keys()].join(",");
|
|
1835
1973
|
throw new Error(
|
|
1836
1974
|
`heapScene: family is frozen; effect ${effect.id} not in {${known}}; ` +
|
|
1837
1975
|
`reactive family rebuild is v2`,
|
|
@@ -1857,13 +1995,89 @@ export function buildHeapScene(
|
|
|
1857
1995
|
if (id === undefined) { id = `tex#${texCounter++}`; textureIds.set(t, id); }
|
|
1858
1996
|
return id;
|
|
1859
1997
|
};
|
|
1860
|
-
|
|
1861
|
-
|
|
1998
|
+
// PipelineState content key. Hashes by the identities of the inner
|
|
1999
|
+
// aval references rather than the wrapper object itself: callers
|
|
2000
|
+
// (notably `wombat.dom`'s `derivePipelineState`) construct a fresh
|
|
2001
|
+
// PipelineState object per leaf even when every contributing aval
|
|
2002
|
+
// is shared — without a content-aware key, every leaf gets its own
|
|
2003
|
+
// bucket and the heap path's drawIndirect coalescing collapses to
|
|
2004
|
+
// one record per bucket. With this key, two leaves that pulled the
|
|
2005
|
+
// same `state.mode` / `state.cullMode` / `state.depthTest` / … avals
|
|
2006
|
+
// bucket together.
|
|
2007
|
+
//
|
|
2008
|
+
// For CONSTANT avals (`isConstant === true`) we key by *value* — two
|
|
2009
|
+
// distinct `AVal.constant(0)` objects produced at different call
|
|
2010
|
+
// sites must collapse to the same bucket. Reactive avals fall back
|
|
2011
|
+
// to reference identity (the right semantic — value can tick).
|
|
2012
|
+
const avalIds = new WeakMap<object, number>();
|
|
2013
|
+
const valueIds = new Map<string, number>();
|
|
2014
|
+
let avalCounter = 0;
|
|
2015
|
+
const avalIdOf = (av: aval<unknown> | undefined): string => {
|
|
2016
|
+
if (av === undefined) return "_";
|
|
2017
|
+
// Reactive aval — key by reference. (`isConstant` is on the
|
|
2018
|
+
// public IAdaptive surface; guard via runtime check so off-spec
|
|
2019
|
+
// duck-types don't blow up.)
|
|
2020
|
+
const isConst = (av as { isConstant?: unknown }).isConstant === true;
|
|
2021
|
+
if (!isConst) {
|
|
2022
|
+
let id = avalIds.get(av);
|
|
2023
|
+
if (id === undefined) { id = avalCounter++; avalIds.set(av, id); }
|
|
2024
|
+
return `a${id}`;
|
|
2025
|
+
}
|
|
2026
|
+
// Constant: key by value. Force is safe (constant: no upstream
|
|
2027
|
+
// dep) and only runs once per distinct aval-object thanks to the
|
|
2028
|
+
// outer avalIds cache below.
|
|
2029
|
+
let id = avalIds.get(av);
|
|
2030
|
+
if (id !== undefined) return `c${id}`;
|
|
2031
|
+
// Constant avals ignore the token; use AdaptiveToken.top to satisfy
|
|
2032
|
+
// the type and traverse a no-op evaluation.
|
|
2033
|
+
const v = av.getValue(AdaptiveToken.top);
|
|
2034
|
+
// Value-typed (`equals` + `getHashCode`)? Try to intern by hash
|
|
2035
|
+
// bucket + equals so two distinct AVal.constant(M44d.identity)
|
|
2036
|
+
// collapse. Falls back to a per-value string key otherwise.
|
|
2037
|
+
let vKey: string;
|
|
2038
|
+
if (
|
|
2039
|
+
v !== null && typeof v === "object" &&
|
|
2040
|
+
typeof (v as { getHashCode?: unknown }).getHashCode === "function" &&
|
|
2041
|
+
typeof (v as { equals?: unknown }).equals === "function"
|
|
2042
|
+
) {
|
|
2043
|
+
const hc = (v as { getHashCode(): number }).getHashCode() | 0;
|
|
2044
|
+
vKey = `hv:${hc}`;
|
|
2045
|
+
} else if (v === null || typeof v !== "object") {
|
|
2046
|
+
vKey = `pv:${typeof v}:${String(v)}`;
|
|
2047
|
+
} else {
|
|
2048
|
+
// Plain object — fall back to reference identity (matches the
|
|
2049
|
+
// memo runtime's behaviour for unhashable objects).
|
|
2050
|
+
let oid = avalIds.get(v as object);
|
|
2051
|
+
if (oid === undefined) { oid = avalCounter++; avalIds.set(v as object, oid); }
|
|
2052
|
+
vKey = `ov:${oid}`;
|
|
2053
|
+
}
|
|
2054
|
+
let vid = valueIds.get(vKey);
|
|
2055
|
+
if (vid === undefined) { vid = avalCounter++; valueIds.set(vKey, vid); }
|
|
2056
|
+
avalIds.set(av, vid);
|
|
2057
|
+
return `c${vid}`;
|
|
2058
|
+
};
|
|
2059
|
+
const psContentIds = new WeakMap<PipelineState, string>();
|
|
1862
2060
|
const psIdOf = (ps: PipelineState | undefined): string => {
|
|
1863
2061
|
if (ps === undefined) return "ps#default";
|
|
1864
|
-
|
|
1865
|
-
if (
|
|
1866
|
-
|
|
2062
|
+
const cached = psContentIds.get(ps);
|
|
2063
|
+
if (cached !== undefined) return cached;
|
|
2064
|
+
const r = ps.rasterizer;
|
|
2065
|
+
const parts: string[] = [
|
|
2066
|
+
avalIdOf(r.topology),
|
|
2067
|
+
avalIdOf(r.cullMode),
|
|
2068
|
+
avalIdOf(r.frontFace),
|
|
2069
|
+
avalIdOf(r.depthBias),
|
|
2070
|
+
ps.depth !== undefined
|
|
2071
|
+
? `d:${avalIdOf(ps.depth.write)}:${avalIdOf(ps.depth.compare)}:${avalIdOf(ps.depth.clamp)}`
|
|
2072
|
+
: "d:_",
|
|
2073
|
+
ps.stencil !== undefined ? "s:1" : "s:_",
|
|
2074
|
+
avalIdOf(ps.blends),
|
|
2075
|
+
avalIdOf(ps.alphaToCoverage),
|
|
2076
|
+
avalIdOf(ps.blendConstant),
|
|
2077
|
+
];
|
|
2078
|
+
const key = `ps#${parts.join("|")}`;
|
|
2079
|
+
psContentIds.set(ps, key);
|
|
2080
|
+
return key;
|
|
1867
2081
|
};
|
|
1868
2082
|
|
|
1869
2083
|
/** Resolved (forced) snapshot of the user's PipelineState. */
|
|
@@ -2074,15 +2288,15 @@ export function buildHeapScene(
|
|
|
2074
2288
|
const isAtlasBucket = layout.atlasTextureBindings.size > 0;
|
|
2075
2289
|
const vsModule = fam.vsModule;
|
|
2076
2290
|
const fsModule = fam.fsModule;
|
|
2077
|
-
const vsEntry =
|
|
2078
|
-
const fsEntry =
|
|
2291
|
+
const vsEntry = fam.vsEntryName;
|
|
2292
|
+
const fsEntry = fam.fsEntryName;
|
|
2079
2293
|
const { pipelineLayout } = getBgl(layout, isAtlasBucket);
|
|
2080
2294
|
|
|
2081
2295
|
const pipeline = device.createRenderPipeline({
|
|
2082
2296
|
label: `heapScene/${bk}/pipeline`,
|
|
2083
2297
|
layout: pipelineLayout,
|
|
2084
2298
|
vertex: { module: vsModule, entryPoint: vsEntry, buffers: [] },
|
|
2085
|
-
fragment: { module: fsModule, entryPoint: fsEntry, targets:
|
|
2299
|
+
fragment: { module: fsModule, entryPoint: fsEntry, targets: colorTargets },
|
|
2086
2300
|
primitive: { topology: ps.topology, cullMode: ps.cullMode, frontFace: ps.frontFace },
|
|
2087
2301
|
...(depthFormat !== undefined && ps.depth !== undefined
|
|
2088
2302
|
? { depthStencil: { format: depthFormat, depthWriteEnabled: ps.depth.write, depthCompare: ps.depth.compare } }
|
|
@@ -2114,7 +2328,7 @@ export function buildHeapScene(
|
|
|
2114
2328
|
localPosRefs: [], localNorRefs: [],
|
|
2115
2329
|
localEntries: [], localToDrawId: [],
|
|
2116
2330
|
localPerDrawAvals: [], localPerDrawRefs: [], localLayoutIds: [],
|
|
2117
|
-
drawSlots:
|
|
2331
|
+
drawSlots: new Set<number>(), dirty: new Set<number>(),
|
|
2118
2332
|
drawTableDirtyMin: Infinity, drawTableDirtyMax: 0,
|
|
2119
2333
|
recordCount: 0, slotToRecord: [], recordToSlot: [],
|
|
2120
2334
|
totalEmitEstimate: 0,
|
|
@@ -2122,6 +2336,7 @@ export function buildHeapScene(
|
|
|
2122
2336
|
isAtlasBucket,
|
|
2123
2337
|
localAtlasReleases: [],
|
|
2124
2338
|
localAtlasTextures: [],
|
|
2339
|
+
localAtlasArrIdx: [],
|
|
2125
2340
|
};
|
|
2126
2341
|
{
|
|
2127
2342
|
const dtBuf = new GrowBuffer(
|
|
@@ -2343,7 +2558,21 @@ export function buildHeapScene(
|
|
|
2343
2558
|
Object.defineProperty(stats, "groups", { get: () => buckets.length, configurable: true });
|
|
2344
2559
|
|
|
2345
2560
|
// ─── addDraw / removeDraw ─────────────────────────────────────────
|
|
2561
|
+
// Public addDraw wrapper. Establishes a sceneObj.evaluateAlways
|
|
2562
|
+
// scope so external callers (no batched outer eval) still register
|
|
2563
|
+
// sceneObj as an output of any aval the spec touches. Internal call
|
|
2564
|
+
// sites (drainAsetWith, batched initial-population) already run
|
|
2565
|
+
// inside a single outer evaluateAlways and invoke addDrawImpl
|
|
2566
|
+
// directly with their token — collapsing 1000× nested
|
|
2567
|
+
// evaluateAlways into 1× outer.
|
|
2346
2568
|
function addDraw(spec: HeapDrawSpec): number {
|
|
2569
|
+
let id = -1;
|
|
2570
|
+
sceneObj.evaluateAlways(AdaptiveToken.top, (tok) => {
|
|
2571
|
+
id = addDrawImpl(spec, tok);
|
|
2572
|
+
});
|
|
2573
|
+
return id;
|
|
2574
|
+
}
|
|
2575
|
+
function addDrawImpl(spec: HeapDrawSpec, outerTok: AdaptiveToken): number {
|
|
2347
2576
|
const drawId = nextDrawId++;
|
|
2348
2577
|
// Family-merge (slice 3c): build the family lazily from this
|
|
2349
2578
|
// single spec when no batched lazy-build occurred earlier (e.g.
|
|
@@ -2360,7 +2589,7 @@ export function buildHeapScene(
|
|
|
2360
2589
|
: new Set<string>();
|
|
2361
2590
|
const bucket = findOrCreateBucket(spec.effect, spec.textures, spec.pipelineState);
|
|
2362
2591
|
const fam = familyFor(spec.effect);
|
|
2363
|
-
const effectFields = fam.fieldsForEffect.get(spec.effect)!;
|
|
2592
|
+
const effectFields = fam.fieldsForEffect.get(spec.effect.id)!;
|
|
2364
2593
|
|
|
2365
2594
|
// Indices live in their own INDEX-usage buffer (WebGPU constraint).
|
|
2366
2595
|
// Aval-keyed: 19K instanced clones of the same mesh share one
|
|
@@ -2382,7 +2611,8 @@ export function buildHeapScene(
|
|
|
2382
2611
|
// emerges from aval identity either way.
|
|
2383
2612
|
const perDrawAvals: aval<unknown>[] = [];
|
|
2384
2613
|
const perDrawRefs = new Map<string, number>();
|
|
2385
|
-
|
|
2614
|
+
{
|
|
2615
|
+
const tok = outerTok;
|
|
2386
2616
|
for (const f of bucket.layout.drawHeaderFields) {
|
|
2387
2617
|
// Atlas-variant texture bindings carry inline values rather than
|
|
2388
2618
|
// pool refs; packAtlasTextureFields fills them after this loop.
|
|
@@ -2454,7 +2684,7 @@ export function buildHeapScene(
|
|
|
2454
2684
|
perDrawRefs.set(f.name, ref);
|
|
2455
2685
|
perDrawAvals.push(av);
|
|
2456
2686
|
}
|
|
2457
|
-
}
|
|
2687
|
+
}
|
|
2458
2688
|
|
|
2459
2689
|
bucket.localPosRefs[localSlot] = perDrawRefs.get("Positions");
|
|
2460
2690
|
bucket.localNorRefs[localSlot] = perDrawRefs.get("Normals");
|
|
@@ -2462,7 +2692,7 @@ export function buildHeapScene(
|
|
|
2462
2692
|
indexCount: idxAlloc.count, firstIndex: idxAlloc.firstIndex, instanceCount,
|
|
2463
2693
|
};
|
|
2464
2694
|
bucket.localToDrawId[localSlot] = drawId;
|
|
2465
|
-
bucket.drawSlots.
|
|
2695
|
+
bucket.drawSlots.add(localSlot);
|
|
2466
2696
|
bucket.localPerDrawAvals[localSlot] = perDrawAvals;
|
|
2467
2697
|
bucket.localPerDrawRefs[localSlot] = perDrawRefs;
|
|
2468
2698
|
const layoutId = fam.schema.layoutIdOf.get(spec.effect)!;
|
|
@@ -2478,15 +2708,14 @@ export function buildHeapScene(
|
|
|
2478
2708
|
const sourceAval = spec.textures.sourceAval;
|
|
2479
2709
|
const repackFn = spec.textures.repack;
|
|
2480
2710
|
if (sourceAval !== undefined && repackFn !== undefined) {
|
|
2481
|
-
sceneObj
|
|
2482
|
-
|
|
2483
|
-
sourceAval.getValue(tok);
|
|
2484
|
-
});
|
|
2711
|
+
// touch — registers sceneObj as an output of sourceAval.
|
|
2712
|
+
sourceAval.getValue(outerTok);
|
|
2485
2713
|
let arr = atlasAvalRefs.get(sourceAval);
|
|
2486
2714
|
if (arr === undefined) {
|
|
2487
2715
|
arr = [];
|
|
2488
2716
|
atlasAvalRefs.set(sourceAval, arr);
|
|
2489
2717
|
}
|
|
2718
|
+
bucket.localAtlasArrIdx[localSlot] = arr.length;
|
|
2490
2719
|
arr.push({ bucket, localSlot, repack: repackFn, sampler: spec.textures.sampler });
|
|
2491
2720
|
}
|
|
2492
2721
|
}
|
|
@@ -2629,11 +2858,18 @@ export function buildHeapScene(
|
|
|
2629
2858
|
const atlasTex = bucket.localAtlasTextures[localSlot];
|
|
2630
2859
|
if (atlasTex !== undefined) {
|
|
2631
2860
|
const sourceAval = atlasTex.sourceAval;
|
|
2632
|
-
|
|
2861
|
+
const i = bucket.localAtlasArrIdx[localSlot];
|
|
2862
|
+
if (sourceAval !== undefined && i !== undefined) {
|
|
2633
2863
|
const arr = atlasAvalRefs.get(sourceAval);
|
|
2634
2864
|
if (arr !== undefined) {
|
|
2635
|
-
|
|
2636
|
-
|
|
2865
|
+
// Swap-pop: O(1) regardless of array length.
|
|
2866
|
+
const last = arr.length - 1;
|
|
2867
|
+
if (i !== last) {
|
|
2868
|
+
const moved = arr[last]!;
|
|
2869
|
+
arr[i] = moved;
|
|
2870
|
+
moved.bucket.localAtlasArrIdx[moved.localSlot] = i;
|
|
2871
|
+
}
|
|
2872
|
+
arr.pop();
|
|
2637
2873
|
if (arr.length === 0) {
|
|
2638
2874
|
atlasAvalRefs.delete(sourceAval);
|
|
2639
2875
|
atlasAvalDirty.delete(sourceAval);
|
|
@@ -2643,6 +2879,7 @@ export function buildHeapScene(
|
|
|
2643
2879
|
}
|
|
2644
2880
|
bucket.localAtlasReleases[localSlot] = undefined;
|
|
2645
2881
|
bucket.localAtlasTextures[localSlot] = undefined;
|
|
2882
|
+
bucket.localAtlasArrIdx[localSlot] = undefined;
|
|
2646
2883
|
|
|
2647
2884
|
bucket.localPerDrawAvals[localSlot] = undefined;
|
|
2648
2885
|
bucket.localPerDrawRefs[localSlot] = undefined;
|
|
@@ -2651,8 +2888,7 @@ export function buildHeapScene(
|
|
|
2651
2888
|
bucket.localNorRefs[localSlot] = undefined;
|
|
2652
2889
|
bucket.localEntries[localSlot] = undefined;
|
|
2653
2890
|
bucket.localToDrawId[localSlot] = undefined;
|
|
2654
|
-
|
|
2655
|
-
if (idx >= 0) bucket.drawSlots.splice(idx, 1);
|
|
2891
|
+
bucket.drawSlots.delete(localSlot);
|
|
2656
2892
|
bucket.dirty.delete(localSlot);
|
|
2657
2893
|
bucket.drawHeap.release(localSlot);
|
|
2658
2894
|
|
|
@@ -2672,7 +2908,11 @@ export function buildHeapScene(
|
|
|
2672
2908
|
const delta = asetReader.getChanges(tok);
|
|
2673
2909
|
delta.iter((op: { value: HeapDrawSpec; count: number }) => {
|
|
2674
2910
|
if (op.count > 0) {
|
|
2675
|
-
|
|
2911
|
+
// Re-use the outer evaluateAlways scope's token (caller is
|
|
2912
|
+
// already sceneObj) — collapses N nested evaluateAlways into
|
|
2913
|
+
// one for a bulk add. Saves a per-RO setUnsafeEvaluationDepth
|
|
2914
|
+
// + outputs.add(sceneObj) round trip.
|
|
2915
|
+
const id = addDrawImpl(op.value, tok);
|
|
2676
2916
|
specToDrawId.set(op.value, id);
|
|
2677
2917
|
} else {
|
|
2678
2918
|
const id = specToDrawId.get(op.value);
|
|
@@ -2691,7 +2931,9 @@ export function buildHeapScene(
|
|
|
2691
2931
|
// array is empty (family will build lazily when an addDraw fires).
|
|
2692
2932
|
const arr = initialDraws as readonly HeapDrawSpec[];
|
|
2693
2933
|
if (arr.length > 0) buildFamilyFromSpecs(arr);
|
|
2694
|
-
|
|
2934
|
+
sceneObj.evaluateAlways(AdaptiveToken.top, (tok) => {
|
|
2935
|
+
for (const d of arr) addDrawImpl(d, tok);
|
|
2936
|
+
});
|
|
2695
2937
|
} else {
|
|
2696
2938
|
asetReader = (initialDraws as aset<HeapDrawSpec>).getReader();
|
|
2697
2939
|
sceneObj.evaluateAlways(AdaptiveToken.top, (tok) => {
|
|
@@ -2711,7 +2953,7 @@ export function buildHeapScene(
|
|
|
2711
2953
|
if (adds.length > 0) buildFamilyFromSpecs(adds);
|
|
2712
2954
|
for (const op of remaining) {
|
|
2713
2955
|
if (op.count > 0) {
|
|
2714
|
-
const id =
|
|
2956
|
+
const id = addDrawImpl(op.value, tok);
|
|
2715
2957
|
specToDrawId.set(op.value, id);
|
|
2716
2958
|
} else {
|
|
2717
2959
|
const id = specToDrawId.get(op.value);
|