@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
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
// package adds the texture (binding 4) + sampler (binding 5) to that
|
|
43
43
|
// group's bind-group layout; the user's FS WGSL declares them.
|
|
44
44
|
import { Trafo3d, V3d, V4f, M44d } from "@aardworx/wombat.base";
|
|
45
|
-
import { AVal, AdaptiveObject, AdaptiveToken } from "@aardworx/wombat.adaptive";
|
|
45
|
+
import { AVal, AdaptiveObject, AdaptiveToken, HashTable } from "@aardworx/wombat.adaptive";
|
|
46
46
|
import { buildBucketLayout, compileHeapEffect, } from "./heapEffect.js";
|
|
47
47
|
import { compileHeapEffectIR } from "./heapEffectIR.js";
|
|
48
48
|
import { buildShaderFamily, compileShaderFamily, } from "./heapShaderFamily.js";
|
|
@@ -115,21 +115,43 @@ const PACKER_F32 = {
|
|
|
115
115
|
dataBytes: 4, typeId: 0,
|
|
116
116
|
pack: (val, dst, off) => { dst[off] = val; },
|
|
117
117
|
};
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
}
|
|
118
|
+
// Integer scalars / vectors. The arena is fronted by a `Float32Array`,
|
|
119
|
+
// so writing raw bits has to go through a same-buffer Uint32/Int32 view
|
|
120
|
+
// to avoid the lossy `i32 → f32` coercion you'd get from a direct
|
|
121
|
+
// `dst[off] = ...` assignment.
|
|
122
|
+
function makeIntPacker(ctor, dim) {
|
|
123
|
+
const bytes = dim * 4;
|
|
124
|
+
if (dim === 1) {
|
|
125
|
+
return {
|
|
126
|
+
dataBytes: bytes, typeId: 0,
|
|
127
|
+
pack: (val, dst, off) => {
|
|
128
|
+
new ctor(dst.buffer, dst.byteOffset + off * 4, 1)[0] = val;
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// Vector: accept {x,y,z,w} components.
|
|
133
|
+
return {
|
|
134
|
+
dataBytes: bytes, typeId: 0,
|
|
135
|
+
pack: (val, dst, off) => {
|
|
136
|
+
const view = new ctor(dst.buffer, dst.byteOffset + off * 4, dim);
|
|
137
|
+
const v = val;
|
|
138
|
+
view[0] = v.x;
|
|
139
|
+
view[1] = v.y;
|
|
140
|
+
if (dim >= 3)
|
|
141
|
+
view[2] = v.z;
|
|
142
|
+
if (dim >= 4)
|
|
143
|
+
view[3] = v.w;
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const PACKER_U32 = makeIntPacker(Uint32Array, 1);
|
|
148
|
+
const PACKER_UVEC2 = makeIntPacker(Uint32Array, 2);
|
|
149
|
+
const PACKER_UVEC3 = makeIntPacker(Uint32Array, 3);
|
|
150
|
+
const PACKER_UVEC4 = makeIntPacker(Uint32Array, 4);
|
|
151
|
+
const PACKER_I32 = makeIntPacker(Int32Array, 1);
|
|
152
|
+
const PACKER_IVEC2 = makeIntPacker(Int32Array, 2);
|
|
153
|
+
const PACKER_IVEC3 = makeIntPacker(Int32Array, 3);
|
|
154
|
+
const PACKER_IVEC4 = makeIntPacker(Int32Array, 4);
|
|
133
155
|
function packerForWgslType(wgslType) {
|
|
134
156
|
switch (wgslType) {
|
|
135
157
|
case "mat4x4<f32>": return PACKER_MAT4;
|
|
@@ -138,7 +160,13 @@ function packerForWgslType(wgslType) {
|
|
|
138
160
|
case "vec2<f32>": return PACKER_VEC2;
|
|
139
161
|
case "f32": return PACKER_F32;
|
|
140
162
|
case "u32": return PACKER_U32;
|
|
163
|
+
case "vec2<u32>": return PACKER_UVEC2;
|
|
164
|
+
case "vec3<u32>": return PACKER_UVEC3;
|
|
165
|
+
case "vec4<u32>": return PACKER_UVEC4;
|
|
141
166
|
case "i32": return PACKER_I32;
|
|
167
|
+
case "vec2<i32>": return PACKER_IVEC2;
|
|
168
|
+
case "vec3<i32>": return PACKER_IVEC3;
|
|
169
|
+
case "vec4<i32>": return PACKER_IVEC4;
|
|
142
170
|
default:
|
|
143
171
|
throw new Error(`heapScene: no JS-side packer for WGSL type '${wgslType}'`);
|
|
144
172
|
}
|
|
@@ -221,6 +249,17 @@ class GrowBuffer {
|
|
|
221
249
|
* Same code path either way.
|
|
222
250
|
*/
|
|
223
251
|
class UniformPool {
|
|
252
|
+
// Keyed by `aval<unknown>` *by reference* (a plain JS `Map`). These
|
|
253
|
+
// keys are overwhelmingly reactive `cval`s (per-object trafos,
|
|
254
|
+
// colours, …) and the hot path is `acquire`/`release` ~once per
|
|
255
|
+
// drawHeader field per RO. A content-keyed `HashTable` would buy
|
|
256
|
+
// nothing here — reactive avals never compare content-equal — and
|
|
257
|
+
// would cost: reactive avals have no `equals`/`getHashCode`, so a
|
|
258
|
+
// `HashTable` falls back to a WeakMap-counter identity hash per
|
|
259
|
+
// lookup, measurably slower than `Map`'s native hashing. (Constant-
|
|
260
|
+
// aval dedup matters where keys are *texture* avals — there the
|
|
261
|
+
// `AtlasPool` is content-keyed; constant avals there carry a cached
|
|
262
|
+
// hash and a fast `equals`.)
|
|
224
263
|
byAval = new Map();
|
|
225
264
|
has(aval) { return this.byAval.has(aval); }
|
|
226
265
|
entry(aval) { return this.byAval.get(aval); }
|
|
@@ -471,22 +510,58 @@ class AttributeArena {
|
|
|
471
510
|
}
|
|
472
511
|
release(ref, dataBytes) {
|
|
473
512
|
const allocBytes = ALIGN16(ALLOC_HEADER_PAD_TO + dataBytes);
|
|
474
|
-
this.freeList
|
|
475
|
-
// Coalesce adjacent free entries for cleanliness.
|
|
476
|
-
this.freeList.sort((a, b) => a.off - b.off);
|
|
477
|
-
for (let i = 0; i < this.freeList.length - 1;) {
|
|
478
|
-
const a = this.freeList[i], b = this.freeList[i + 1];
|
|
479
|
-
if (a.off + a.size === b.off) {
|
|
480
|
-
a.size += b.size;
|
|
481
|
-
this.freeList.splice(i + 1, 1);
|
|
482
|
-
}
|
|
483
|
-
else
|
|
484
|
-
i++;
|
|
485
|
-
}
|
|
513
|
+
insertSortedFreeBlock(this.freeList, ref, allocBytes);
|
|
486
514
|
}
|
|
487
515
|
onResize(cb) { return this.buf.onResize(cb); }
|
|
488
516
|
destroy() { this.buf.destroy(); }
|
|
489
517
|
}
|
|
518
|
+
/**
|
|
519
|
+
* Insert `{off, size}` into a free-list kept sorted by `off`, then
|
|
520
|
+
* coalesce with the two immediate neighbours.
|
|
521
|
+
*
|
|
522
|
+
* The list invariant — sorted, non-overlapping, never-adjacent — is
|
|
523
|
+
* preserved across allocs (which take from the front or split a
|
|
524
|
+
* block) and releases (this function). The previous implementation
|
|
525
|
+
* did `push + Array.prototype.sort + linear coalesce scan`, which is
|
|
526
|
+
* O(N log N) per release. Under a 500-RO bulk-remove the sort
|
|
527
|
+
* dominated `removeDraw` (~41 ms of self-time in the heap-demo-sg
|
|
528
|
+
* toggle profile). Binary-search insert + 2-neighbour merge collapses
|
|
529
|
+
* that to O(log N + N-shift), and is principled — the sort never
|
|
530
|
+
* actually mattered since we already had the sorted prefix as an
|
|
531
|
+
* invariant.
|
|
532
|
+
*/
|
|
533
|
+
function insertSortedFreeBlock(freeList, off, size) {
|
|
534
|
+
// Binary-search for the insertion index (first entry whose off > new).
|
|
535
|
+
let lo = 0, hi = freeList.length;
|
|
536
|
+
while (lo < hi) {
|
|
537
|
+
const mid = (lo + hi) >>> 1;
|
|
538
|
+
if (freeList[mid].off <= off)
|
|
539
|
+
lo = mid + 1;
|
|
540
|
+
else
|
|
541
|
+
hi = mid;
|
|
542
|
+
}
|
|
543
|
+
// lo is the index where the new entry would be inserted.
|
|
544
|
+
// Try merging with the predecessor first; if successful, the merged
|
|
545
|
+
// block may now be adjacent to its (former) successor too.
|
|
546
|
+
const prev = lo > 0 ? freeList[lo - 1] : undefined;
|
|
547
|
+
if (prev !== undefined && prev.off + prev.size === off) {
|
|
548
|
+
prev.size += size;
|
|
549
|
+
// Check forward-merge with what was freeList[lo].
|
|
550
|
+
const next = freeList[lo];
|
|
551
|
+
if (next !== undefined && prev.off + prev.size === next.off) {
|
|
552
|
+
prev.size += next.size;
|
|
553
|
+
freeList.splice(lo, 1);
|
|
554
|
+
}
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const next = freeList[lo];
|
|
558
|
+
if (next !== undefined && off + size === next.off) {
|
|
559
|
+
next.off = off;
|
|
560
|
+
next.size += size;
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
freeList.splice(lo, 0, { off, size });
|
|
564
|
+
}
|
|
490
565
|
/**
|
|
491
566
|
* Element-bump allocator over an index GrowBuffer (units = u32). Each
|
|
492
567
|
* draw's index range is allocated as one block; on release the block
|
|
@@ -550,17 +625,7 @@ class IndexAllocator {
|
|
|
550
625
|
return off;
|
|
551
626
|
}
|
|
552
627
|
release(off, elements) {
|
|
553
|
-
this.freeList
|
|
554
|
-
this.freeList.sort((a, b) => a.off - b.off);
|
|
555
|
-
for (let i = 0; i < this.freeList.length - 1;) {
|
|
556
|
-
const a = this.freeList[i], b = this.freeList[i + 1];
|
|
557
|
-
if (a.off + a.size === b.off) {
|
|
558
|
-
a.size += b.size;
|
|
559
|
-
this.freeList.splice(i + 1, 1);
|
|
560
|
-
}
|
|
561
|
-
else
|
|
562
|
-
i++;
|
|
563
|
-
}
|
|
628
|
+
insertSortedFreeBlock(this.freeList, off, elements);
|
|
564
629
|
}
|
|
565
630
|
onResize(cb) { return this.buf.onResize(cb); }
|
|
566
631
|
destroy() { this.buf.destroy(); }
|
|
@@ -784,6 +849,19 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
784
849
|
throw new Error("buildHeapScene: framebuffer signature has no color attachment");
|
|
785
850
|
}
|
|
786
851
|
const colorFormat = sig.colors.tryFind(colorAttachmentName);
|
|
852
|
+
// All color targets, ordered by the signature's `colorNames`. Each
|
|
853
|
+
// entry's format flows into the pipeline's `fragment.targets[i].format`.
|
|
854
|
+
// The fragmentOutputLayout's `locations` map tells the WGSL emit which
|
|
855
|
+
// output @location maps to which color attachment; this array is the
|
|
856
|
+
// matching pipeline-side ordering.
|
|
857
|
+
const colorTargets = [];
|
|
858
|
+
for (const name of sig.colorNames) {
|
|
859
|
+
const fmt = sig.colors.tryFind(name);
|
|
860
|
+
if (fmt === undefined) {
|
|
861
|
+
throw new Error(`buildHeapScene: signature missing format for color '${name}'`);
|
|
862
|
+
}
|
|
863
|
+
colorTargets.push({ format: fmt });
|
|
864
|
+
}
|
|
787
865
|
const depthFormat = sig.depthStencil?.format;
|
|
788
866
|
// ─── Global arena (uniform/attribute data + index buffer) ────────
|
|
789
867
|
// Initial capacities are just hints; both buffers pow2-grow on
|
|
@@ -946,7 +1024,11 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
946
1024
|
// buckets) — `atlasAvalRefs` tracks all (bucket, localSlot) pairs so
|
|
947
1025
|
// one swap rewrites N drawHeaders in one drain.
|
|
948
1026
|
const atlasAvalDirty = new Set();
|
|
949
|
-
|
|
1027
|
+
// Content-keyed (`HashTable`, not a JS `Map`) so distinct
|
|
1028
|
+
// `AVal.constant(tex)` wrappers sharing the same texture collapse to
|
|
1029
|
+
// one `(bucket, slot)` ref-list — matching `AtlasPool.entriesByAval`,
|
|
1030
|
+
// which is now content-keyed too. Reactive avals key by reference.
|
|
1031
|
+
const atlasAvalRefs = new HashTable();
|
|
950
1032
|
/**
|
|
951
1033
|
* Per-draw bucket dirty (rare in steady state — only fires when
|
|
952
1034
|
* something forces a header rewrite, e.g. a drawHeap GrowBuffer
|
|
@@ -1147,30 +1229,59 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1147
1229
|
// ─── Buckets ──────────────────────────────────────────────────────
|
|
1148
1230
|
const buckets = [];
|
|
1149
1231
|
const bucketByKey = new Map();
|
|
1150
|
-
|
|
1232
|
+
// Keyed by `effect.id` (content hash), NOT object identity. Two
|
|
1233
|
+
// Effect objects with identical content (e.g. produced by separate
|
|
1234
|
+
// calls to `effect(...)` or the pickChain composer) share one
|
|
1235
|
+
// FamilyState. This is the right correctness/perf knob: building a
|
|
1236
|
+
// family by content means an upstream caller that legitimately
|
|
1237
|
+
// produces different-but-identical Effect objects per leaf still
|
|
1238
|
+
// reuses one pipeline + one bucket.
|
|
1239
|
+
const familyByEffectId = new Map();
|
|
1151
1240
|
let familyBuilt = false;
|
|
1152
1241
|
const enableFamilyMerge = opts.enableFamilyMerge === true;
|
|
1153
1242
|
function compileFamilyFor(effects, perInstanceByEffect) {
|
|
1154
|
-
|
|
1243
|
+
// Family-merge (multi-effect dispatch via layoutId switch) has been
|
|
1244
|
+
// disabled — it was a perf illusion. Every effect compiles to its
|
|
1245
|
+
// own standalone pipeline; the per-bucket WGSL comes from
|
|
1246
|
+
// `compileHeapEffectIR(effect, layout, opts, "standalone")` and is
|
|
1247
|
+
// used directly as the bucket's shader module (no wrapper, no
|
|
1248
|
+
// dispatch). The schema is still derived from the same
|
|
1249
|
+
// `buildShaderFamily` path so the runtime's drawHeader packer,
|
|
1250
|
+
// bind-group layout, etc. don't change shape — `__layoutId`
|
|
1251
|
+
// remains a u32 slot that's written as 0 per RO (harmless).
|
|
1252
|
+
//
|
|
1253
|
+
// `heapShaderFamily.compileShaderFamily` is kept around but
|
|
1254
|
+
// unused; the function and its tests stay disabled.
|
|
1255
|
+
if (effects.length !== 1) {
|
|
1256
|
+
throw new Error("heapScene: multi-effect family-merge disabled (was a perf illusion). " +
|
|
1257
|
+
"Build one bucket per effect.");
|
|
1258
|
+
}
|
|
1259
|
+
const effect = effects[0];
|
|
1260
|
+
const schema = buildShaderFamily([effect], opts.fragmentOutputLayout, undefined, {
|
|
1155
1261
|
atlasizeAllTextures: atlasPool !== undefined,
|
|
1156
1262
|
perEffectPerInstance: perInstanceByEffect,
|
|
1157
1263
|
});
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1160
|
-
|
|
1264
|
+
const compileOpts = opts.fragmentOutputLayout !== undefined
|
|
1265
|
+
? { target: "wgsl", fragmentOutputLayout: opts.fragmentOutputLayout }
|
|
1266
|
+
: { target: "wgsl" };
|
|
1267
|
+
const ir = compileHeapEffectIR(effect, schema.drawHeaderUnion, compileOpts, "standalone");
|
|
1268
|
+
const vsModule = device.createShaderModule({ code: ir.vs, label: `heapScene/standalone/${schema.id}/vs` });
|
|
1269
|
+
const fsModule = device.createShaderModule({ code: ir.fs, label: `heapScene/standalone/${schema.id}/fs` });
|
|
1161
1270
|
const fieldsForEffect = new Map();
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1271
|
+
const s = schema.perEffectSchema.get(effect);
|
|
1272
|
+
const fields = new Set();
|
|
1273
|
+
for (const a of s.attributes)
|
|
1274
|
+
fields.add(a.name);
|
|
1275
|
+
for (const u of s.uniforms)
|
|
1276
|
+
fields.add(u.name);
|
|
1277
|
+
for (const t of s.textures)
|
|
1278
|
+
fields.add(t.name);
|
|
1279
|
+
fieldsForEffect.set(effect.id, fields);
|
|
1280
|
+
return {
|
|
1281
|
+
schema, vsModule, fsModule, fieldsForEffect,
|
|
1282
|
+
vsEntryName: ir.vsEntry,
|
|
1283
|
+
fsEntryName: ir.fsEntry,
|
|
1284
|
+
};
|
|
1174
1285
|
}
|
|
1175
1286
|
function buildFamilyFromSpecs(specs) {
|
|
1176
1287
|
if (familyBuilt)
|
|
@@ -1184,48 +1295,43 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1184
1295
|
// layout to address per-instance attribute reads via `instId`
|
|
1185
1296
|
// instead of `vertex_index` — without this, instanced effects
|
|
1186
1297
|
// produce broken geometry.
|
|
1187
|
-
|
|
1298
|
+
// De-dupe by effect.id so two Effects with identical content but
|
|
1299
|
+
// distinct object identities collapse to one family + one bucket.
|
|
1300
|
+
const seenIds = new Set();
|
|
1188
1301
|
const unique = [];
|
|
1189
|
-
const
|
|
1302
|
+
const perInstanceByEffectId = new Map();
|
|
1190
1303
|
for (const spec of specs) {
|
|
1191
1304
|
const e = spec.effect;
|
|
1192
|
-
let entry =
|
|
1305
|
+
let entry = perInstanceByEffectId.get(e.id);
|
|
1193
1306
|
if (entry === undefined) {
|
|
1194
1307
|
entry = { attributes: new Set(), uniforms: new Set() };
|
|
1195
|
-
|
|
1308
|
+
perInstanceByEffectId.set(e.id, entry);
|
|
1196
1309
|
}
|
|
1197
1310
|
if (spec.instanceAttributes !== undefined) {
|
|
1198
1311
|
for (const name of Object.keys(spec.instanceAttributes))
|
|
1199
1312
|
entry.attributes.add(name);
|
|
1200
1313
|
}
|
|
1201
|
-
if (!
|
|
1202
|
-
|
|
1314
|
+
if (!seenIds.has(e.id)) {
|
|
1315
|
+
seenIds.add(e.id);
|
|
1203
1316
|
unique.push(e);
|
|
1204
1317
|
}
|
|
1205
1318
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
for (const e of unique) {
|
|
1216
|
-
const perI = perInstanceByEffect.get(e);
|
|
1217
|
-
const singleMap = new Map();
|
|
1218
|
-
if (perI !== undefined)
|
|
1219
|
-
singleMap.set(e, perI);
|
|
1220
|
-
familyByEffect.set(e, compileFamilyFor([e], singleMap));
|
|
1221
|
-
}
|
|
1319
|
+
// Family-merge disabled: always one bucket per effect.
|
|
1320
|
+
// `enableFamilyMerge` is ignored.
|
|
1321
|
+
void enableFamilyMerge;
|
|
1322
|
+
for (const e of unique) {
|
|
1323
|
+
const perI = perInstanceByEffectId.get(e.id);
|
|
1324
|
+
const singleMap = new Map();
|
|
1325
|
+
if (perI !== undefined)
|
|
1326
|
+
singleMap.set(e, perI);
|
|
1327
|
+
familyByEffectId.set(e.id, compileFamilyFor([e], singleMap));
|
|
1222
1328
|
}
|
|
1223
1329
|
familyBuilt = true;
|
|
1224
1330
|
}
|
|
1225
1331
|
function familyFor(effect) {
|
|
1226
|
-
const f =
|
|
1332
|
+
const f = familyByEffectId.get(effect.id);
|
|
1227
1333
|
if (f === undefined) {
|
|
1228
|
-
const known = [...
|
|
1334
|
+
const known = [...familyByEffectId.keys()].join(",");
|
|
1229
1335
|
throw new Error(`heapScene: family is frozen; effect ${effect.id} not in {${known}}; ` +
|
|
1230
1336
|
`reactive family rebuild is v2`);
|
|
1231
1337
|
}
|
|
@@ -1251,17 +1357,102 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1251
1357
|
}
|
|
1252
1358
|
return id;
|
|
1253
1359
|
};
|
|
1254
|
-
|
|
1255
|
-
|
|
1360
|
+
// PipelineState content key. Hashes by the identities of the inner
|
|
1361
|
+
// aval references rather than the wrapper object itself: callers
|
|
1362
|
+
// (notably `wombat.dom`'s `derivePipelineState`) construct a fresh
|
|
1363
|
+
// PipelineState object per leaf even when every contributing aval
|
|
1364
|
+
// is shared — without a content-aware key, every leaf gets its own
|
|
1365
|
+
// bucket and the heap path's drawIndirect coalescing collapses to
|
|
1366
|
+
// one record per bucket. With this key, two leaves that pulled the
|
|
1367
|
+
// same `state.mode` / `state.cullMode` / `state.depthTest` / … avals
|
|
1368
|
+
// bucket together.
|
|
1369
|
+
//
|
|
1370
|
+
// For CONSTANT avals (`isConstant === true`) we key by *value* — two
|
|
1371
|
+
// distinct `AVal.constant(0)` objects produced at different call
|
|
1372
|
+
// sites must collapse to the same bucket. Reactive avals fall back
|
|
1373
|
+
// to reference identity (the right semantic — value can tick).
|
|
1374
|
+
const avalIds = new WeakMap();
|
|
1375
|
+
const valueIds = new Map();
|
|
1376
|
+
let avalCounter = 0;
|
|
1377
|
+
const avalIdOf = (av) => {
|
|
1378
|
+
if (av === undefined)
|
|
1379
|
+
return "_";
|
|
1380
|
+
// Reactive aval — key by reference. (`isConstant` is on the
|
|
1381
|
+
// public IAdaptive surface; guard via runtime check so off-spec
|
|
1382
|
+
// duck-types don't blow up.)
|
|
1383
|
+
const isConst = av.isConstant === true;
|
|
1384
|
+
if (!isConst) {
|
|
1385
|
+
let id = avalIds.get(av);
|
|
1386
|
+
if (id === undefined) {
|
|
1387
|
+
id = avalCounter++;
|
|
1388
|
+
avalIds.set(av, id);
|
|
1389
|
+
}
|
|
1390
|
+
return `a${id}`;
|
|
1391
|
+
}
|
|
1392
|
+
// Constant: key by value. Force is safe (constant: no upstream
|
|
1393
|
+
// dep) and only runs once per distinct aval-object thanks to the
|
|
1394
|
+
// outer avalIds cache below.
|
|
1395
|
+
let id = avalIds.get(av);
|
|
1396
|
+
if (id !== undefined)
|
|
1397
|
+
return `c${id}`;
|
|
1398
|
+
// Constant avals ignore the token; use AdaptiveToken.top to satisfy
|
|
1399
|
+
// the type and traverse a no-op evaluation.
|
|
1400
|
+
const v = av.getValue(AdaptiveToken.top);
|
|
1401
|
+
// Value-typed (`equals` + `getHashCode`)? Try to intern by hash
|
|
1402
|
+
// bucket + equals so two distinct AVal.constant(M44d.identity)
|
|
1403
|
+
// collapse. Falls back to a per-value string key otherwise.
|
|
1404
|
+
let vKey;
|
|
1405
|
+
if (v !== null && typeof v === "object" &&
|
|
1406
|
+
typeof v.getHashCode === "function" &&
|
|
1407
|
+
typeof v.equals === "function") {
|
|
1408
|
+
const hc = v.getHashCode() | 0;
|
|
1409
|
+
vKey = `hv:${hc}`;
|
|
1410
|
+
}
|
|
1411
|
+
else if (v === null || typeof v !== "object") {
|
|
1412
|
+
vKey = `pv:${typeof v}:${String(v)}`;
|
|
1413
|
+
}
|
|
1414
|
+
else {
|
|
1415
|
+
// Plain object — fall back to reference identity (matches the
|
|
1416
|
+
// memo runtime's behaviour for unhashable objects).
|
|
1417
|
+
let oid = avalIds.get(v);
|
|
1418
|
+
if (oid === undefined) {
|
|
1419
|
+
oid = avalCounter++;
|
|
1420
|
+
avalIds.set(v, oid);
|
|
1421
|
+
}
|
|
1422
|
+
vKey = `ov:${oid}`;
|
|
1423
|
+
}
|
|
1424
|
+
let vid = valueIds.get(vKey);
|
|
1425
|
+
if (vid === undefined) {
|
|
1426
|
+
vid = avalCounter++;
|
|
1427
|
+
valueIds.set(vKey, vid);
|
|
1428
|
+
}
|
|
1429
|
+
avalIds.set(av, vid);
|
|
1430
|
+
return `c${vid}`;
|
|
1431
|
+
};
|
|
1432
|
+
const psContentIds = new WeakMap();
|
|
1256
1433
|
const psIdOf = (ps) => {
|
|
1257
1434
|
if (ps === undefined)
|
|
1258
1435
|
return "ps#default";
|
|
1259
|
-
|
|
1260
|
-
if (
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1436
|
+
const cached = psContentIds.get(ps);
|
|
1437
|
+
if (cached !== undefined)
|
|
1438
|
+
return cached;
|
|
1439
|
+
const r = ps.rasterizer;
|
|
1440
|
+
const parts = [
|
|
1441
|
+
avalIdOf(r.topology),
|
|
1442
|
+
avalIdOf(r.cullMode),
|
|
1443
|
+
avalIdOf(r.frontFace),
|
|
1444
|
+
avalIdOf(r.depthBias),
|
|
1445
|
+
ps.depth !== undefined
|
|
1446
|
+
? `d:${avalIdOf(ps.depth.write)}:${avalIdOf(ps.depth.compare)}:${avalIdOf(ps.depth.clamp)}`
|
|
1447
|
+
: "d:_",
|
|
1448
|
+
ps.stencil !== undefined ? "s:1" : "s:_",
|
|
1449
|
+
avalIdOf(ps.blends),
|
|
1450
|
+
avalIdOf(ps.alphaToCoverage),
|
|
1451
|
+
avalIdOf(ps.blendConstant),
|
|
1452
|
+
];
|
|
1453
|
+
const key = `ps#${parts.join("|")}`;
|
|
1454
|
+
psContentIds.set(ps, key);
|
|
1455
|
+
return key;
|
|
1265
1456
|
};
|
|
1266
1457
|
function resolvePipelineState(ps) {
|
|
1267
1458
|
if (ps === undefined) {
|
|
@@ -1446,14 +1637,14 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1446
1637
|
const isAtlasBucket = layout.atlasTextureBindings.size > 0;
|
|
1447
1638
|
const vsModule = fam.vsModule;
|
|
1448
1639
|
const fsModule = fam.fsModule;
|
|
1449
|
-
const vsEntry =
|
|
1450
|
-
const fsEntry =
|
|
1640
|
+
const vsEntry = fam.vsEntryName;
|
|
1641
|
+
const fsEntry = fam.fsEntryName;
|
|
1451
1642
|
const { pipelineLayout } = getBgl(layout, isAtlasBucket);
|
|
1452
1643
|
const pipeline = device.createRenderPipeline({
|
|
1453
1644
|
label: `heapScene/${bk}/pipeline`,
|
|
1454
1645
|
layout: pipelineLayout,
|
|
1455
1646
|
vertex: { module: vsModule, entryPoint: vsEntry, buffers: [] },
|
|
1456
|
-
fragment: { module: fsModule, entryPoint: fsEntry, targets:
|
|
1647
|
+
fragment: { module: fsModule, entryPoint: fsEntry, targets: colorTargets },
|
|
1457
1648
|
primitive: { topology: ps.topology, cullMode: ps.cullMode, frontFace: ps.frontFace },
|
|
1458
1649
|
...(depthFormat !== undefined && ps.depth !== undefined
|
|
1459
1650
|
? { depthStencil: { format: depthFormat, depthWriteEnabled: ps.depth.write, depthCompare: ps.depth.compare } }
|
|
@@ -1480,7 +1671,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1480
1671
|
localPosRefs: [], localNorRefs: [],
|
|
1481
1672
|
localEntries: [], localToDrawId: [],
|
|
1482
1673
|
localPerDrawAvals: [], localPerDrawRefs: [], localLayoutIds: [],
|
|
1483
|
-
drawSlots:
|
|
1674
|
+
drawSlots: new Set(), dirty: new Set(),
|
|
1484
1675
|
drawTableDirtyMin: Infinity, drawTableDirtyMax: 0,
|
|
1485
1676
|
recordCount: 0, slotToRecord: [], recordToSlot: [],
|
|
1486
1677
|
totalEmitEstimate: 0,
|
|
@@ -1488,6 +1679,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1488
1679
|
isAtlasBucket,
|
|
1489
1680
|
localAtlasReleases: [],
|
|
1490
1681
|
localAtlasTextures: [],
|
|
1682
|
+
localAtlasArrIdx: [],
|
|
1491
1683
|
};
|
|
1492
1684
|
{
|
|
1493
1685
|
const dtBuf = new GrowBuffer(device, `heapScene/${bk}/drawTable`, GPUBufferUsage.STORAGE, 1024);
|
|
@@ -1681,7 +1873,21 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1681
1873
|
};
|
|
1682
1874
|
Object.defineProperty(stats, "groups", { get: () => buckets.length, configurable: true });
|
|
1683
1875
|
// ─── addDraw / removeDraw ─────────────────────────────────────────
|
|
1876
|
+
// Public addDraw wrapper. Establishes a sceneObj.evaluateAlways
|
|
1877
|
+
// scope so external callers (no batched outer eval) still register
|
|
1878
|
+
// sceneObj as an output of any aval the spec touches. Internal call
|
|
1879
|
+
// sites (drainAsetWith, batched initial-population) already run
|
|
1880
|
+
// inside a single outer evaluateAlways and invoke addDrawImpl
|
|
1881
|
+
// directly with their token — collapsing 1000× nested
|
|
1882
|
+
// evaluateAlways into 1× outer.
|
|
1684
1883
|
function addDraw(spec) {
|
|
1884
|
+
let id = -1;
|
|
1885
|
+
sceneObj.evaluateAlways(AdaptiveToken.top, (tok) => {
|
|
1886
|
+
id = addDrawImpl(spec, tok);
|
|
1887
|
+
});
|
|
1888
|
+
return id;
|
|
1889
|
+
}
|
|
1890
|
+
function addDrawImpl(spec, outerTok) {
|
|
1685
1891
|
const drawId = nextDrawId++;
|
|
1686
1892
|
// Family-merge (slice 3c): build the family lazily from this
|
|
1687
1893
|
// single spec when no batched lazy-build occurred earlier (e.g.
|
|
@@ -1699,7 +1905,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1699
1905
|
: new Set();
|
|
1700
1906
|
const bucket = findOrCreateBucket(spec.effect, spec.textures, spec.pipelineState);
|
|
1701
1907
|
const fam = familyFor(spec.effect);
|
|
1702
|
-
const effectFields = fam.fieldsForEffect.get(spec.effect);
|
|
1908
|
+
const effectFields = fam.fieldsForEffect.get(spec.effect.id);
|
|
1703
1909
|
// Indices live in their own INDEX-usage buffer (WebGPU constraint).
|
|
1704
1910
|
// Aval-keyed: 19K instanced clones of the same mesh share one
|
|
1705
1911
|
// index allocation + one upload.
|
|
@@ -1718,7 +1924,8 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1718
1924
|
// emerges from aval identity either way.
|
|
1719
1925
|
const perDrawAvals = [];
|
|
1720
1926
|
const perDrawRefs = new Map();
|
|
1721
|
-
|
|
1927
|
+
{
|
|
1928
|
+
const tok = outerTok;
|
|
1722
1929
|
for (const f of bucket.layout.drawHeaderFields) {
|
|
1723
1930
|
// Atlas-variant texture bindings carry inline values rather than
|
|
1724
1931
|
// pool refs; packAtlasTextureFields fills them after this loop.
|
|
@@ -1783,14 +1990,14 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1783
1990
|
perDrawRefs.set(f.name, ref);
|
|
1784
1991
|
perDrawAvals.push(av);
|
|
1785
1992
|
}
|
|
1786
|
-
}
|
|
1993
|
+
}
|
|
1787
1994
|
bucket.localPosRefs[localSlot] = perDrawRefs.get("Positions");
|
|
1788
1995
|
bucket.localNorRefs[localSlot] = perDrawRefs.get("Normals");
|
|
1789
1996
|
bucket.localEntries[localSlot] = {
|
|
1790
1997
|
indexCount: idxAlloc.count, firstIndex: idxAlloc.firstIndex, instanceCount,
|
|
1791
1998
|
};
|
|
1792
1999
|
bucket.localToDrawId[localSlot] = drawId;
|
|
1793
|
-
bucket.drawSlots.
|
|
2000
|
+
bucket.drawSlots.add(localSlot);
|
|
1794
2001
|
bucket.localPerDrawAvals[localSlot] = perDrawAvals;
|
|
1795
2002
|
bucket.localPerDrawRefs[localSlot] = perDrawRefs;
|
|
1796
2003
|
const layoutId = fam.schema.layoutIdOf.get(spec.effect);
|
|
@@ -1805,15 +2012,14 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1805
2012
|
const sourceAval = spec.textures.sourceAval;
|
|
1806
2013
|
const repackFn = spec.textures.repack;
|
|
1807
2014
|
if (sourceAval !== undefined && repackFn !== undefined) {
|
|
1808
|
-
sceneObj
|
|
1809
|
-
|
|
1810
|
-
sourceAval.getValue(tok);
|
|
1811
|
-
});
|
|
2015
|
+
// touch — registers sceneObj as an output of sourceAval.
|
|
2016
|
+
sourceAval.getValue(outerTok);
|
|
1812
2017
|
let arr = atlasAvalRefs.get(sourceAval);
|
|
1813
2018
|
if (arr === undefined) {
|
|
1814
2019
|
arr = [];
|
|
1815
2020
|
atlasAvalRefs.set(sourceAval, arr);
|
|
1816
2021
|
}
|
|
2022
|
+
bucket.localAtlasArrIdx[localSlot] = arr.length;
|
|
1817
2023
|
arr.push({ bucket, localSlot, repack: repackFn, sampler: spec.textures.sampler });
|
|
1818
2024
|
}
|
|
1819
2025
|
}
|
|
@@ -1963,12 +2169,18 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1963
2169
|
const atlasTex = bucket.localAtlasTextures[localSlot];
|
|
1964
2170
|
if (atlasTex !== undefined) {
|
|
1965
2171
|
const sourceAval = atlasTex.sourceAval;
|
|
1966
|
-
|
|
2172
|
+
const i = bucket.localAtlasArrIdx[localSlot];
|
|
2173
|
+
if (sourceAval !== undefined && i !== undefined) {
|
|
1967
2174
|
const arr = atlasAvalRefs.get(sourceAval);
|
|
1968
2175
|
if (arr !== undefined) {
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
2176
|
+
// Swap-pop: O(1) regardless of array length.
|
|
2177
|
+
const last = arr.length - 1;
|
|
2178
|
+
if (i !== last) {
|
|
2179
|
+
const moved = arr[last];
|
|
2180
|
+
arr[i] = moved;
|
|
2181
|
+
moved.bucket.localAtlasArrIdx[moved.localSlot] = i;
|
|
2182
|
+
}
|
|
2183
|
+
arr.pop();
|
|
1972
2184
|
if (arr.length === 0) {
|
|
1973
2185
|
atlasAvalRefs.delete(sourceAval);
|
|
1974
2186
|
atlasAvalDirty.delete(sourceAval);
|
|
@@ -1978,6 +2190,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1978
2190
|
}
|
|
1979
2191
|
bucket.localAtlasReleases[localSlot] = undefined;
|
|
1980
2192
|
bucket.localAtlasTextures[localSlot] = undefined;
|
|
2193
|
+
bucket.localAtlasArrIdx[localSlot] = undefined;
|
|
1981
2194
|
bucket.localPerDrawAvals[localSlot] = undefined;
|
|
1982
2195
|
bucket.localPerDrawRefs[localSlot] = undefined;
|
|
1983
2196
|
bucket.localLayoutIds[localSlot] = undefined;
|
|
@@ -1985,9 +2198,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
1985
2198
|
bucket.localNorRefs[localSlot] = undefined;
|
|
1986
2199
|
bucket.localEntries[localSlot] = undefined;
|
|
1987
2200
|
bucket.localToDrawId[localSlot] = undefined;
|
|
1988
|
-
|
|
1989
|
-
if (idx >= 0)
|
|
1990
|
-
bucket.drawSlots.splice(idx, 1);
|
|
2201
|
+
bucket.drawSlots.delete(localSlot);
|
|
1991
2202
|
bucket.dirty.delete(localSlot);
|
|
1992
2203
|
bucket.drawHeap.release(localSlot);
|
|
1993
2204
|
drawIdToBucket[drawId] = undefined;
|
|
@@ -2005,7 +2216,11 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2005
2216
|
const delta = asetReader.getChanges(tok);
|
|
2006
2217
|
delta.iter((op) => {
|
|
2007
2218
|
if (op.count > 0) {
|
|
2008
|
-
|
|
2219
|
+
// Re-use the outer evaluateAlways scope's token (caller is
|
|
2220
|
+
// already sceneObj) — collapses N nested evaluateAlways into
|
|
2221
|
+
// one for a bulk add. Saves a per-RO setUnsafeEvaluationDepth
|
|
2222
|
+
// + outputs.add(sceneObj) round trip.
|
|
2223
|
+
const id = addDrawImpl(op.value, tok);
|
|
2009
2224
|
specToDrawId.set(op.value, id);
|
|
2010
2225
|
}
|
|
2011
2226
|
else {
|
|
@@ -2025,8 +2240,10 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2025
2240
|
const arr = initialDraws;
|
|
2026
2241
|
if (arr.length > 0)
|
|
2027
2242
|
buildFamilyFromSpecs(arr);
|
|
2028
|
-
|
|
2029
|
-
|
|
2243
|
+
sceneObj.evaluateAlways(AdaptiveToken.top, (tok) => {
|
|
2244
|
+
for (const d of arr)
|
|
2245
|
+
addDrawImpl(d, tok);
|
|
2246
|
+
});
|
|
2030
2247
|
}
|
|
2031
2248
|
else {
|
|
2032
2249
|
asetReader = initialDraws.getReader();
|
|
@@ -2049,7 +2266,7 @@ export function buildHeapScene(device, sig, initialDraws, opts = {}) {
|
|
|
2049
2266
|
buildFamilyFromSpecs(adds);
|
|
2050
2267
|
for (const op of remaining) {
|
|
2051
2268
|
if (op.count > 0) {
|
|
2052
|
-
const id =
|
|
2269
|
+
const id = addDrawImpl(op.value, tok);
|
|
2053
2270
|
specToDrawId.set(op.value, id);
|
|
2054
2271
|
}
|
|
2055
2272
|
else {
|