@aardworx/wombat.rendering 0.21.11 → 0.21.13
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/runtime/heapAdapter.d.ts.map +1 -1
- package/dist/runtime/heapAdapter.js +15 -5
- package/dist/runtime/heapAdapter.js.map +1 -1
- package/dist/runtime/heapScene/pools.d.ts +30 -6
- package/dist/runtime/heapScene/pools.d.ts.map +1 -1
- package/dist/runtime/heapScene/pools.js +38 -1
- package/dist/runtime/heapScene/pools.js.map +1 -1
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +195 -3
- package/dist/runtime/heapScene.js.map +1 -1
- package/package.json +1 -1
- package/src/runtime/heapAdapter.ts +16 -5
- package/src/runtime/heapScene/pools.ts +70 -5
- package/src/runtime/heapScene.ts +189 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aardworx/wombat.rendering",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.13",
|
|
4
4
|
"description": "WebGPU rendering layer for the Wombat TypeScript stack — RenderObject/RenderTask/runtime + window glue, port of Aardvark.Rendering's lower layers on top of WebGPU and wombat.shader.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "krauthaufen",
|
|
@@ -621,11 +621,22 @@ export function renderObjectToHeapSpec(
|
|
|
621
621
|
pipelineState: ro.pipelineState,
|
|
622
622
|
inputs,
|
|
623
623
|
...(instanceAttributes !== undefined ? { instanceAttributes } : {}),
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
|
|
624
|
+
// Count binding, in preference order:
|
|
625
|
+
// 1. The scene layer's shape-invariant drawCall marker carries
|
|
626
|
+
// the LIVE count aval (`__sgHeapSafeDraw.count`) — bind it so
|
|
627
|
+
// count ticks flow to the drawTable in place (epoch morphing,
|
|
628
|
+
// point add/remove) with no per-RO wrapper aval.
|
|
629
|
+
// 2. Snapshot ≠ 1 (not > 1): instanceCount 0 must flow through —
|
|
630
|
+
// the scan kernel emits indexCount × 0 = nothing, matching
|
|
631
|
+
// what the legacy path draws for zero instances. Omitting it
|
|
632
|
+
// would default to 1 and pack a garbage instance.
|
|
633
|
+
...((): { instanceCount?: aval<number> | number } => {
|
|
634
|
+
const marker = (ro.drawCall as {
|
|
635
|
+
__sgHeapSafeDraw?: { kind: string; count?: aval<number> };
|
|
636
|
+
}).__sgHeapSafeDraw;
|
|
637
|
+
if (marker?.count !== undefined) return { instanceCount: marker.count };
|
|
638
|
+
return dc.instanceCount !== 1 ? { instanceCount: dc.instanceCount } : {};
|
|
639
|
+
})(),
|
|
629
640
|
...geom,
|
|
630
641
|
...(textures !== undefined ? { textures } : {}),
|
|
631
642
|
...(ro.injectedStorage !== undefined && ro.injectedStorage.count > 0
|
|
@@ -67,16 +67,42 @@ export const SEM_NORMALS = 2;
|
|
|
67
67
|
* in which chunk's bind group is active at draw time (shaders
|
|
68
68
|
* never decode it).
|
|
69
69
|
*/
|
|
70
|
+
/** Fresh placement for a changed value — everything `repack` needs to
|
|
71
|
+
* decide whether the allocation still fits and how to re-encode. */
|
|
72
|
+
export interface PoolPlacement {
|
|
73
|
+
readonly dataBytes: number;
|
|
74
|
+
readonly typeId: number;
|
|
75
|
+
readonly length: number;
|
|
76
|
+
readonly pack: (val: unknown, dst: Float32Array, off: number) => void;
|
|
77
|
+
}
|
|
78
|
+
|
|
70
79
|
interface PoolEntry {
|
|
71
80
|
readonly chunkIdx: number;
|
|
72
81
|
/** Byte offset within the chunk's GPUBuffer. Mutated by
|
|
73
82
|
* `UniformPool.remapRefs` when a waste-triggered arena compaction
|
|
74
|
-
* relocates the allocation. */
|
|
83
|
+
* relocates the allocation, and by the sized-repack realloc path. */
|
|
75
84
|
ref: number;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
85
|
+
/** Raw data bytes of the CURRENT allocation. Mutated by the
|
|
86
|
+
* sized-repack realloc path. */
|
|
87
|
+
dataBytes: number;
|
|
88
|
+
typeId: number;
|
|
89
|
+
pack: (val: unknown, dst: Float32Array, off: number) => void;
|
|
79
90
|
refcount: number;
|
|
91
|
+
/** Present for size-variable payloads (attribute buffers): re-derive
|
|
92
|
+
* the placement from the CURRENT value so `repack` can realloc when
|
|
93
|
+
* the payload grew or shrank (geometry morphing — epoch switches,
|
|
94
|
+
* point add/remove). Absent = fixed-size (plain uniforms). */
|
|
95
|
+
replan?: (val: unknown) => PoolPlacement;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A pool allocation that moved during a sized repack — every
|
|
99
|
+
* drawHeader field referencing (aval, chunkIdx, oldRef) must be
|
|
100
|
+
* rewritten to newRef by the caller. */
|
|
101
|
+
export interface MovedRef {
|
|
102
|
+
readonly av: aval<unknown>;
|
|
103
|
+
readonly chunkIdx: number;
|
|
104
|
+
readonly oldRef: number;
|
|
105
|
+
readonly newRef: number;
|
|
80
106
|
}
|
|
81
107
|
|
|
82
108
|
/**
|
|
@@ -200,10 +226,12 @@ export class UniformPool {
|
|
|
200
226
|
typeId: number,
|
|
201
227
|
length: number,
|
|
202
228
|
pack: (val: unknown, dst: Float32Array, off: number) => void,
|
|
229
|
+
replan?: (val: unknown) => PoolPlacement,
|
|
203
230
|
): number {
|
|
204
231
|
const existing = this.getEntry(av, chunkIdx);
|
|
205
232
|
if (existing !== undefined) {
|
|
206
233
|
existing.refcount++;
|
|
234
|
+
if (replan !== undefined && existing.replan === undefined) existing.replan = replan;
|
|
207
235
|
return existing.ref;
|
|
208
236
|
}
|
|
209
237
|
const r = arena.alloc(dataBytes, chunkIdx);
|
|
@@ -237,6 +265,7 @@ export class UniformPool {
|
|
|
237
265
|
void device;
|
|
238
266
|
this.setEntry(av, {
|
|
239
267
|
chunkIdx: finalChunk, ref: r.off, dataBytes, typeId, pack, refcount: 1,
|
|
268
|
+
...(replan !== undefined ? { replan } : {}),
|
|
240
269
|
});
|
|
241
270
|
return r.off;
|
|
242
271
|
}
|
|
@@ -282,9 +311,44 @@ export class UniformPool {
|
|
|
282
311
|
arena: ChunkedAttributeArena,
|
|
283
312
|
av: aval<unknown>,
|
|
284
313
|
val: unknown,
|
|
285
|
-
):
|
|
314
|
+
): MovedRef[] | undefined {
|
|
286
315
|
let dst: Float32Array | undefined;
|
|
316
|
+
let moved: MovedRef[] | undefined;
|
|
287
317
|
for (const e of this.entriesOf(av)) {
|
|
318
|
+
if (e.replan !== undefined) {
|
|
319
|
+
// Size-variable payload: re-derive the placement from the new
|
|
320
|
+
// value. Same size → refresh the encoder and fall through to
|
|
321
|
+
// the in-place write; different size → realloc within the
|
|
322
|
+
// same chunk, rewrite header + data, and report the move so
|
|
323
|
+
// the caller re-seats every drawHeader field.
|
|
324
|
+
const p = e.replan(val);
|
|
325
|
+
if (p.dataBytes !== e.dataBytes) {
|
|
326
|
+
arena.release(e.chunkIdx, e.ref, e.dataBytes);
|
|
327
|
+
const r = arena.alloc(p.dataBytes, e.chunkIdx);
|
|
328
|
+
if (r.chunkIdx !== e.chunkIdx) {
|
|
329
|
+
throw new Error(
|
|
330
|
+
`UniformPool.repack: realloc spilled from chunk ${e.chunkIdx} to ${r.chunkIdx} ` +
|
|
331
|
+
`(${p.dataBytes} bytes) — the referencing bucket is bound to chunk ${e.chunkIdx}.`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
const allocBytes = ALIGN16(ALLOC_HEADER_PAD_TO + p.dataBytes);
|
|
335
|
+
const buf = new ArrayBuffer(allocBytes);
|
|
336
|
+
const u32 = new Uint32Array(buf);
|
|
337
|
+
const f32 = new Float32Array(buf);
|
|
338
|
+
u32[0] = p.typeId;
|
|
339
|
+
u32[1] = p.length;
|
|
340
|
+
u32[2] = p.length > 0 ? Math.floor(p.dataBytes / p.length) : 0;
|
|
341
|
+
p.pack(val, f32, ALLOC_HEADER_PAD_TO / 4);
|
|
342
|
+
arena.write(e.chunkIdx, r.off, new Uint8Array(buf));
|
|
343
|
+
(moved ??= []).push({ av, chunkIdx: e.chunkIdx, oldRef: e.ref, newRef: r.off });
|
|
344
|
+
e.ref = r.off;
|
|
345
|
+
e.dataBytes = p.dataBytes;
|
|
346
|
+
e.typeId = p.typeId;
|
|
347
|
+
e.pack = p.pack;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
e.pack = p.pack;
|
|
351
|
+
}
|
|
288
352
|
if (dst === undefined || dst.length !== e.dataBytes / 4) {
|
|
289
353
|
dst = new Float32Array(e.dataBytes / 4);
|
|
290
354
|
}
|
|
@@ -296,6 +360,7 @@ export class UniformPool {
|
|
|
296
360
|
);
|
|
297
361
|
}
|
|
298
362
|
void device;
|
|
363
|
+
return moved;
|
|
299
364
|
}
|
|
300
365
|
/** Total bytes touched by `repack` for this aval — sum across
|
|
301
366
|
* chunks. Used by the dirty-bytes diagnostics. */
|
package/src/runtime/heapScene.ts
CHANGED
|
@@ -91,6 +91,7 @@ import {
|
|
|
91
91
|
ENC_V3F_TIGHT, ENC_OCT32, ENC_C4B, SEM_POSITIONS, SEM_NORMALS,
|
|
92
92
|
COMPACTION_WASTE_FLOOR_BYTES,
|
|
93
93
|
type ArenaState,
|
|
94
|
+
type PoolPlacement,
|
|
94
95
|
} from "./heapScene/pools.js";
|
|
95
96
|
import { encodeModeKey, type PipelineStateDescriptor } from "./pipelineCache/index.js";
|
|
96
97
|
import { snapshotDescriptor, ModeKeyTracker } from "./derivedModes/modeKeyCpu.js";
|
|
@@ -385,6 +386,12 @@ interface Bucket {
|
|
|
385
386
|
* keyed by aval identity.
|
|
386
387
|
*/
|
|
387
388
|
readonly localPerDrawAvals: (aval<unknown>[] | undefined)[];
|
|
389
|
+
/** drawHeader field index per pooled aval — PARALLEL to
|
|
390
|
+
* `localPerDrawAvals[slot]`. Lets the sized-repack drain rewrite
|
|
391
|
+
* exactly the fields whose aval's allocation moved (matching by
|
|
392
|
+
* aval identity, never by numeric offset — a released offset can
|
|
393
|
+
* be re-issued to a different aval within the same drain). */
|
|
394
|
+
readonly localPerDrawAvalFieldIdx: (number[] | undefined)[];
|
|
388
395
|
/** Field indices (into `layout.drawHeaderFields`) whose drawHeader
|
|
389
396
|
* cells hold RAW arena refs (§7 output slots, no pool entry). */
|
|
390
397
|
readonly localPerDrawRawFieldIdx: (number[] | undefined)[];
|
|
@@ -1150,6 +1157,79 @@ export function buildHeapScene(
|
|
|
1150
1157
|
}
|
|
1151
1158
|
}
|
|
1152
1159
|
|
|
1160
|
+
// ─── HeapDrawSpec.instanceCount as an aval ────────────────────────
|
|
1161
|
+
// The geometry-morphing / collection-count path (epoch switches,
|
|
1162
|
+
// point add/remove): a count tick is ONE drawTable write (record
|
|
1163
|
+
// word 4) + emit re-estimate + re-scan — no add/remove churn, no
|
|
1164
|
+
// arena activity. Instance ATTRIBUTE payloads ride the sized-repack
|
|
1165
|
+
// path independently; per-instance UNIFORM arrays (packed per
|
|
1166
|
+
// count) are rejected with an adaptive count at addDraw.
|
|
1167
|
+
const drawIdToCountAval = new Map<number, aval<number>>();
|
|
1168
|
+
const drawIdToCountCallback = new Map<number, IDisposable>();
|
|
1169
|
+
const countDirty = new Set<number>();
|
|
1170
|
+
|
|
1171
|
+
function subscribeCount(av: aval<number>, drawId: number): void {
|
|
1172
|
+
drawIdToCountAval.set(drawId, av);
|
|
1173
|
+
drawIdToCountCallback.set(
|
|
1174
|
+
drawId,
|
|
1175
|
+
addMarkingCallback(av, () => { countDirty.add(drawId); }),
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function unsubscribeCount(drawId: number): void {
|
|
1180
|
+
const cb = drawIdToCountCallback.get(drawId);
|
|
1181
|
+
if (cb === undefined) return;
|
|
1182
|
+
cb.dispose();
|
|
1183
|
+
drawIdToCountCallback.delete(drawId);
|
|
1184
|
+
drawIdToCountAval.delete(drawId);
|
|
1185
|
+
countDirty.delete(drawId);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/** Update the drawTable record's instanceCount in place — the
|
|
1189
|
+
* word-4 sibling of `setEffectiveIndexCount`. */
|
|
1190
|
+
function setEffectiveInstanceCount(drawId: number, newCount: number): void {
|
|
1191
|
+
const bucket = drawIdToBucket[drawId];
|
|
1192
|
+
if (bucket === undefined) return;
|
|
1193
|
+
if (bucket.gpuRouted) {
|
|
1194
|
+
const partition = bucket.partitionScene!;
|
|
1195
|
+
const recIdx = bucket.drawIdToRecord!.get(drawId);
|
|
1196
|
+
if (recIdx === undefined) return;
|
|
1197
|
+
const ru32 = partition.recordU32;
|
|
1198
|
+
const indexCount = partition.masterShadow[recIdx * ru32 + 3]!;
|
|
1199
|
+
const oldCount = partition.masterShadow[recIdx * ru32 + 4]!;
|
|
1200
|
+
partition.masterShadow[recIdx * ru32 + 4] = newCount >>> 0;
|
|
1201
|
+
const delta = indexCount * (newCount - oldCount);
|
|
1202
|
+
bucket.partitionDirty = true;
|
|
1203
|
+
for (const sl of bucket.slots) {
|
|
1204
|
+
sl.totalEmitEstimate = Math.max(0, sl.totalEmitEstimate + delta);
|
|
1205
|
+
sl.scanDirty = true;
|
|
1206
|
+
}
|
|
1207
|
+
} else {
|
|
1208
|
+
const slotIdx = drawIdToSlotIdx[drawId];
|
|
1209
|
+
const slot = slotIdx !== undefined ? bucket.slots[slotIdx] : bucket.slots[0];
|
|
1210
|
+
if (slot === undefined) return;
|
|
1211
|
+
const localSlot = drawIdToLocalSlot[drawId];
|
|
1212
|
+
if (localSlot === undefined) return;
|
|
1213
|
+
const recIdx = slot.slotToRecord[localSlot];
|
|
1214
|
+
if (recIdx === undefined || recIdx < 0) return;
|
|
1215
|
+
const shadow = slot.drawTableShadow!;
|
|
1216
|
+
const indexCount = shadow[recIdx * RECORD_U32 + 3]!;
|
|
1217
|
+
const oldCount = shadow[recIdx * RECORD_U32 + 4]!;
|
|
1218
|
+
shadow[recIdx * RECORD_U32 + 4] = newCount >>> 0;
|
|
1219
|
+
const byteOff = recIdx * RECORD_BYTES + 4 * 4;
|
|
1220
|
+
if (byteOff < slot.drawTableDirtyMin) slot.drawTableDirtyMin = byteOff;
|
|
1221
|
+
if (byteOff + 4 > slot.drawTableDirtyMax) slot.drawTableDirtyMax = byteOff + 4;
|
|
1222
|
+
const delta = indexCount * (newCount - oldCount);
|
|
1223
|
+
slot.totalEmitEstimate = Math.max(0, slot.totalEmitEstimate + delta);
|
|
1224
|
+
slot.scanDirty = true;
|
|
1225
|
+
}
|
|
1226
|
+
const localSlot = drawIdToLocalSlot[drawId];
|
|
1227
|
+
if (localSlot !== undefined) {
|
|
1228
|
+
const e = bucket.localEntries[localSlot];
|
|
1229
|
+
if (e !== undefined) e.instanceCount = newCount;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1153
1233
|
function subscribeModeLeaf(av: aval<unknown>, drawId: number): void {
|
|
1154
1234
|
let set = modeAvalToDrawIds.get(av);
|
|
1155
1235
|
if (set === undefined) {
|
|
@@ -1732,7 +1812,18 @@ export function buildHeapScene(
|
|
|
1732
1812
|
// `colorTargets` here silently dropped blends/write-masks from every
|
|
1733
1813
|
// heap pipeline: transparent heap leaves painted opaque, and the OIT
|
|
1734
1814
|
// pick passes' write-mask-0 "Colors" leaked color onto the composite.
|
|
1815
|
+
// Locations the family's fragment shader actually writes — a
|
|
1816
|
+
// signature target the shader has NO output for must set
|
|
1817
|
+
// writeMask 0 (WebGPU: non-zero writeMask without a fragment
|
|
1818
|
+
// output is a validation error; e.g. a non-picking leaf in a
|
|
1819
|
+
// Colors+pickId pass writes only Colors).
|
|
1820
|
+
const writtenLocations = new Set<number>();
|
|
1821
|
+
for (const e of fam.schema.effects) {
|
|
1822
|
+
const sc = fam.schema.perEffectSchema.get(e)!;
|
|
1823
|
+
for (const fo of sc.fragmentOutputs) writtenLocations.add(fo.location);
|
|
1824
|
+
}
|
|
1735
1825
|
const targets: GPUColorTargetState[] = colorTargets.map((t, i) => {
|
|
1826
|
+
if (!writtenLocations.has(i)) return { format: t.format, writeMask: 0 };
|
|
1736
1827
|
const a = desc.attachments[i];
|
|
1737
1828
|
if (a === undefined) return { format: t.format };
|
|
1738
1829
|
return {
|
|
@@ -3091,7 +3182,7 @@ export function buildHeapScene(
|
|
|
3091
3182
|
headerDirtyMin: Infinity, headerDirtyMax: 0,
|
|
3092
3183
|
localPosRefs: [], localNorRefs: [],
|
|
3093
3184
|
localEntries: [], localToDrawId: [],
|
|
3094
|
-
localPerDrawAvals: [], localPerDrawRawFieldIdx: [], localPerDrawRefs: [], localLayoutIds: [],
|
|
3185
|
+
localPerDrawAvals: [], localPerDrawAvalFieldIdx: [], localPerDrawRawFieldIdx: [], localPerDrawRefs: [], localLayoutIds: [],
|
|
3095
3186
|
fieldIdx,
|
|
3096
3187
|
posFieldIdx: fieldIdx.get("Positions") ?? -1,
|
|
3097
3188
|
norFieldIdx: fieldIdx.get("Normals") ?? -1,
|
|
@@ -3365,8 +3456,24 @@ export function buildHeapScene(
|
|
|
3365
3456
|
const hasIndices = spec.indices !== undefined;
|
|
3366
3457
|
const indicesAval = hasIndices ? (asAval(spec.indices!) as aval<Uint32Array>) : undefined;
|
|
3367
3458
|
const indexArr = hasIndices ? (readPlain(spec.indices!) as Uint32Array) : undefined;
|
|
3459
|
+
const icAval =
|
|
3460
|
+
spec.instanceCount !== undefined && typeof spec.instanceCount === "object"
|
|
3461
|
+
&& typeof (spec.instanceCount as { getValue?: unknown }).getValue === "function"
|
|
3462
|
+
? (spec.instanceCount as aval<number>)
|
|
3463
|
+
: undefined;
|
|
3368
3464
|
const instanceCount = spec.instanceCount !== undefined
|
|
3369
3465
|
? (readPlain(spec.instanceCount) as number) : 1;
|
|
3466
|
+
if (icAval !== undefined && perInstanceUniforms.size > 0) {
|
|
3467
|
+
// Per-instance UNIFORM arrays are packed `instanceCount` deep at
|
|
3468
|
+
// addDraw — a changing count would need a repack protocol those
|
|
3469
|
+
// packers don't have. Attribute-backed instancing (BufferViews /
|
|
3470
|
+
// raw arrays) is unaffected: the payload rides the sized-repack
|
|
3471
|
+
// path and the count merely selects a prefix.
|
|
3472
|
+
throw new Error(
|
|
3473
|
+
"heapScene: adaptive instanceCount is not supported together with per-instance UNIFORM values " +
|
|
3474
|
+
`(fields: ${[...perInstanceUniforms].join(", ")}) — use instance attributes instead`,
|
|
3475
|
+
);
|
|
3476
|
+
}
|
|
3370
3477
|
|
|
3371
3478
|
let estBytes = indexArr !== undefined ? ALIGN16(ALLOC_HEADER_PAD_TO + indexArr.byteLength) : 0;
|
|
3372
3479
|
for (const f of layout.drawHeaderFields) {
|
|
@@ -3489,6 +3596,7 @@ export function buildHeapScene(
|
|
|
3489
3596
|
// packs as a single value. Both go through the same pool — sharing
|
|
3490
3597
|
// emerges from aval identity either way.
|
|
3491
3598
|
const perDrawAvals: aval<unknown>[] = [];
|
|
3599
|
+
const perDrawAvalFieldIdx: number[] = [];
|
|
3492
3600
|
let perDrawRawFieldIdx: number[] | undefined;
|
|
3493
3601
|
const perDrawRefs = new Int32Array(bucket.layout.drawHeaderFields.length).fill(-1);
|
|
3494
3602
|
const setRef = (name: string, ref: number): void => {
|
|
@@ -3583,9 +3691,13 @@ export function buildHeapScene(
|
|
|
3583
3691
|
let value: unknown;
|
|
3584
3692
|
let placement: ReturnType<typeof poolPlacementFor>;
|
|
3585
3693
|
let releasable: { _v: unknown } | undefined;
|
|
3694
|
+
let replanFn: ((v: unknown) => PoolPlacement) | undefined;
|
|
3586
3695
|
if (f.kind === "attribute-ref" && !isPerInstanceUniformField && isBufferView(provided)) {
|
|
3587
3696
|
const bv = provided;
|
|
3588
3697
|
placement = bufferViewPlacement(f, bv);
|
|
3698
|
+
// Size-variable payload: geometry morphing re-derives the
|
|
3699
|
+
// placement from the view's CURRENT buffer value.
|
|
3700
|
+
replanFn = () => bufferViewPlacement(f, bv);
|
|
3589
3701
|
av = bv.buffer as aval<unknown>;
|
|
3590
3702
|
value = bv.buffer.getValue(tok);
|
|
3591
3703
|
} else if (
|
|
@@ -3619,14 +3731,19 @@ export function buildHeapScene(
|
|
|
3619
3731
|
placement = isPerInstanceUniformField
|
|
3620
3732
|
? perInstancePlacementFor(f, value, instanceCount)
|
|
3621
3733
|
: poolPlacementFor(f, value);
|
|
3734
|
+
if (f.kind === "attribute-ref" && !isPerInstanceUniformField) {
|
|
3735
|
+
replanFn = (v) => poolPlacementFor(f, v);
|
|
3736
|
+
}
|
|
3622
3737
|
}
|
|
3623
3738
|
const ref = pool.acquire(
|
|
3624
3739
|
device, arena.attrs, av, bucket.chunkIdx, value,
|
|
3625
3740
|
placement.dataBytes, placement.typeId, placement.length, placement.pack,
|
|
3741
|
+
replanFn,
|
|
3626
3742
|
);
|
|
3627
3743
|
if (releasable !== undefined) releasable._v = null; // staged — drop the CPU copy
|
|
3628
3744
|
setRef(f.name, ref);
|
|
3629
3745
|
perDrawAvals.push(av);
|
|
3746
|
+
perDrawAvalFieldIdx.push(bucket.fieldIdx.get(f.name)!);
|
|
3630
3747
|
}
|
|
3631
3748
|
}
|
|
3632
3749
|
|
|
@@ -3638,6 +3755,7 @@ export function buildHeapScene(
|
|
|
3638
3755
|
bucket.localToDrawId[localSlot] = drawId;
|
|
3639
3756
|
bucket.drawSlots.add(localSlot);
|
|
3640
3757
|
bucket.localPerDrawAvals[localSlot] = perDrawAvals;
|
|
3758
|
+
bucket.localPerDrawAvalFieldIdx[localSlot] = perDrawAvalFieldIdx;
|
|
3641
3759
|
bucket.localPerDrawRawFieldIdx[localSlot] = perDrawRawFieldIdx;
|
|
3642
3760
|
bucket.localPerDrawRefs[localSlot] = perDrawRefs;
|
|
3643
3761
|
const layoutId = fam.schema.layoutIdOf.get(spec.effect)!;
|
|
@@ -3839,6 +3957,9 @@ export function buildHeapScene(
|
|
|
3839
3957
|
setEffectiveIndexCount(drawId, 0);
|
|
3840
3958
|
}
|
|
3841
3959
|
}
|
|
3960
|
+
if (icAval !== undefined) {
|
|
3961
|
+
subscribeCount(icAval, drawId);
|
|
3962
|
+
}
|
|
3842
3963
|
|
|
3843
3964
|
// ─── Reactive rebucket: track PS-modeKey changes ────────────────
|
|
3844
3965
|
// When any mode-axis aval marks (e.g. cullCval.value = 'front'),
|
|
@@ -3955,6 +4076,7 @@ export function buildHeapScene(
|
|
|
3955
4076
|
// Tear down `active` subscription (if any) so its callback won't
|
|
3956
4077
|
// fire after the underlying drawTable record is gone.
|
|
3957
4078
|
unsubscribeActive(drawId);
|
|
4079
|
+
unsubscribeCount(drawId);
|
|
3958
4080
|
// §7: deregister this RO's derivation records and release slots.
|
|
3959
4081
|
if (derivedScene !== undefined) {
|
|
3960
4082
|
const reg = derivedByDrawId.get(drawId);
|
|
@@ -4089,6 +4211,7 @@ export function buildHeapScene(
|
|
|
4089
4211
|
bucket.localAtlasArrIdx[localSlot] = undefined;
|
|
4090
4212
|
|
|
4091
4213
|
bucket.localPerDrawAvals[localSlot] = undefined;
|
|
4214
|
+
bucket.localPerDrawAvalFieldIdx[localSlot] = undefined;
|
|
4092
4215
|
bucket.localPerDrawRefs[localSlot] = undefined;
|
|
4093
4216
|
bucket.localLayoutIds[localSlot] = undefined;
|
|
4094
4217
|
bucket.localPosRefs[localSlot] = undefined;
|
|
@@ -4479,11 +4602,63 @@ export function buildHeapScene(
|
|
|
4479
4602
|
// One writeBuffer per dirty aval, regardless of how many draws
|
|
4480
4603
|
// reference it — sharing pays off here.
|
|
4481
4604
|
if (allocDirty.size > 0) {
|
|
4605
|
+
// aval → (chunkIdx → newRef) for allocations the sized repack
|
|
4606
|
+
// moved (payload grew/shrank → realloc'd within the chunk).
|
|
4607
|
+
let movedByAval: Map<aval<unknown>, Map<number, number>> | undefined;
|
|
4482
4608
|
for (const av of allocDirty) {
|
|
4483
|
-
pool.repack(device, arena.attrs, av, av.getValue(tok));
|
|
4609
|
+
const moved = pool.repack(device, arena.attrs, av, av.getValue(tok));
|
|
4484
4610
|
totalDirtyBytes += pool.totalDataBytes(av);
|
|
4611
|
+
if (moved !== undefined) {
|
|
4612
|
+
for (const m of moved) {
|
|
4613
|
+
let byChunk = (movedByAval ??= new Map()).get(m.av);
|
|
4614
|
+
if (byChunk === undefined) { byChunk = new Map(); movedByAval.set(m.av, byChunk); }
|
|
4615
|
+
byChunk.set(m.chunkIdx, m.newRef);
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4485
4618
|
}
|
|
4486
4619
|
allocDirty.clear();
|
|
4620
|
+
if (movedByAval !== undefined) {
|
|
4621
|
+
// Re-seat every drawHeader field whose aval's allocation
|
|
4622
|
+
// moved. Matched by AVAL IDENTITY via the parallel
|
|
4623
|
+
// `localPerDrawAvalFieldIdx` — never by numeric offset (a
|
|
4624
|
+
// released offset can be re-issued to a DIFFERENT aval in
|
|
4625
|
+
// this same drain). Mirrors the compaction remap loop.
|
|
4626
|
+
for (const bucket of buckets) {
|
|
4627
|
+
for (const localSlot of bucket.drawSlots) {
|
|
4628
|
+
const avals = bucket.localPerDrawAvals[localSlot];
|
|
4629
|
+
const fidx = bucket.localPerDrawAvalFieldIdx[localSlot];
|
|
4630
|
+
const refs = bucket.localPerDrawRefs[localSlot];
|
|
4631
|
+
if (avals === undefined || fidx === undefined || refs === undefined) continue;
|
|
4632
|
+
let changed = false;
|
|
4633
|
+
for (let i = 0; i < avals.length; i++) {
|
|
4634
|
+
const byChunk = movedByAval.get(avals[i]!);
|
|
4635
|
+
if (byChunk === undefined) continue;
|
|
4636
|
+
const nn = byChunk.get(bucket.chunkIdx);
|
|
4637
|
+
if (nn === undefined) continue;
|
|
4638
|
+
if (bucket.gpuRouted) {
|
|
4639
|
+
throw new Error(
|
|
4640
|
+
"heapScene: payload realloc under a GPU-routed (derived-modes) bucket is not supported yet",
|
|
4641
|
+
);
|
|
4642
|
+
}
|
|
4643
|
+
if (refs[fidx[i]!]! !== nn) { refs[fidx[i]!] = nn; changed = true; }
|
|
4644
|
+
}
|
|
4645
|
+
if (!changed) continue;
|
|
4646
|
+
const pr = bucket.posFieldIdx >= 0 ? refs[bucket.posFieldIdx]! : -1;
|
|
4647
|
+
const nr = bucket.norFieldIdx >= 0 ? refs[bucket.norFieldIdx]! : -1;
|
|
4648
|
+
bucket.localPosRefs[localSlot] = pr < 0 ? undefined : pr;
|
|
4649
|
+
bucket.localNorRefs[localSlot] = nr < 0 ? undefined : nr;
|
|
4650
|
+
packBucketHeader(bucket, localSlot, refs, bucket.localLayoutIds[localSlot] ?? 0);
|
|
4651
|
+
if (bucket.isAtlasBucket) {
|
|
4652
|
+
const ts = bucket.localAtlasTextures[localSlot];
|
|
4653
|
+
if (ts !== undefined) packAtlasTextureFields(bucket, localSlot, ts);
|
|
4654
|
+
}
|
|
4655
|
+
const byteOff = localSlot * bucket.layout.drawHeaderBytes;
|
|
4656
|
+
if (byteOff < bucket.headerDirtyMin) bucket.headerDirtyMin = byteOff;
|
|
4657
|
+
const end = byteOff + bucket.layout.drawHeaderBytes;
|
|
4658
|
+
if (end > bucket.headerDirtyMax) bucket.headerDirtyMax = end;
|
|
4659
|
+
}
|
|
4660
|
+
}
|
|
4661
|
+
}
|
|
4487
4662
|
}
|
|
4488
4663
|
|
|
4489
4664
|
// 1c. HeapDrawSpec.active drain — flip drawTable.indexCount
|
|
@@ -4502,6 +4677,18 @@ export function buildHeapScene(
|
|
|
4502
4677
|
activeDirty.clear();
|
|
4503
4678
|
}
|
|
4504
4679
|
|
|
4680
|
+
// 1d. HeapDrawSpec.instanceCount aval drain — write the new
|
|
4681
|
+
// count into drawTable word 4 (emit = indexCount × count;
|
|
4682
|
+
// 0 draws nothing). One record write + re-scan per tick.
|
|
4683
|
+
if (countDirty.size > 0) {
|
|
4684
|
+
for (const did of countDirty) {
|
|
4685
|
+
const av = drawIdToCountAval.get(did);
|
|
4686
|
+
if (av === undefined) continue;
|
|
4687
|
+
setEffectiveInstanceCount(did, Math.max(0, av.getValue(tok) | 0));
|
|
4688
|
+
}
|
|
4689
|
+
countDirty.clear();
|
|
4690
|
+
}
|
|
4691
|
+
|
|
4505
4692
|
// 1b. Atlas-texture aval reactivity: an `aval<ITexture>` that
|
|
4506
4693
|
// drives an atlas placement was marked. Repack the pool entry
|
|
4507
4694
|
// and rewrite the drawHeader fields of every (bucket, slot)
|