@aardworx/wombat.rendering 0.9.33 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/renderObject.d.ts +16 -0
- package/dist/core/renderObject.d.ts.map +1 -1
- package/dist/runtime/derivedModes/kernelCodegen.d.ts +90 -0
- package/dist/runtime/derivedModes/kernelCodegen.d.ts.map +1 -0
- package/dist/runtime/derivedModes/kernelCodegen.js +623 -0
- package/dist/runtime/derivedModes/kernelCodegen.js.map +1 -0
- package/dist/runtime/derivedModes/modeKeyCpu.d.ts.map +1 -1
- package/dist/runtime/derivedModes/modeKeyCpu.js +23 -67
- package/dist/runtime/derivedModes/modeKeyCpu.js.map +1 -1
- package/dist/runtime/derivedModes/partitionDispatcher.d.ts +79 -0
- package/dist/runtime/derivedModes/partitionDispatcher.d.ts.map +1 -0
- package/dist/runtime/derivedModes/partitionDispatcher.js +303 -0
- package/dist/runtime/derivedModes/partitionDispatcher.js.map +1 -0
- package/dist/runtime/derivedModes/partitionKernel.d.ts +12 -0
- package/dist/runtime/derivedModes/partitionKernel.d.ts.map +1 -0
- package/dist/runtime/derivedModes/partitionKernel.js +127 -0
- package/dist/runtime/derivedModes/partitionKernel.js.map +1 -0
- package/dist/runtime/derivedModes/partitionKernelLayout.d.ts +4 -0
- package/dist/runtime/derivedModes/partitionKernelLayout.d.ts.map +1 -0
- package/dist/runtime/derivedModes/partitionKernelLayout.js +24 -0
- package/dist/runtime/derivedModes/partitionKernelLayout.js.map +1 -0
- package/dist/runtime/derivedModes/rule.d.ts +38 -86
- package/dist/runtime/derivedModes/rule.d.ts.map +1 -1
- package/dist/runtime/derivedModes/rule.js +34 -74
- package/dist/runtime/derivedModes/rule.js.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.d.ts +10 -8
- package/dist/runtime/derivedUniforms/dispatch.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/dispatch.js +31 -18
- package/dist/runtime/derivedUniforms/dispatch.js.map +1 -1
- package/dist/runtime/derivedUniforms/marker.d.ts +90 -11
- package/dist/runtime/derivedUniforms/marker.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/marker.js +161 -15
- package/dist/runtime/derivedUniforms/marker.js.map +1 -1
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts +25 -6
- package/dist/runtime/derivedUniforms/sceneIntegration.d.ts.map +1 -1
- package/dist/runtime/derivedUniforms/sceneIntegration.js +61 -20
- package/dist/runtime/derivedUniforms/sceneIntegration.js.map +1 -1
- package/dist/runtime/heapAdapter.d.ts.map +1 -1
- package/dist/runtime/heapAdapter.js +17 -0
- package/dist/runtime/heapAdapter.js.map +1 -1
- package/dist/runtime/heapEligibility.d.ts.map +1 -1
- package/dist/runtime/heapEligibility.js +22 -2
- package/dist/runtime/heapEligibility.js.map +1 -1
- package/dist/runtime/heapScene/chunkedArena.d.ts +76 -0
- package/dist/runtime/heapScene/chunkedArena.d.ts.map +1 -0
- package/dist/runtime/heapScene/chunkedArena.js +223 -0
- package/dist/runtime/heapScene/chunkedArena.js.map +1 -0
- package/dist/runtime/heapScene/freelist.d.ts +55 -0
- package/dist/runtime/heapScene/freelist.d.ts.map +1 -0
- package/dist/runtime/heapScene/freelist.js +188 -0
- package/dist/runtime/heapScene/freelist.js.map +1 -0
- package/dist/runtime/heapScene/growBuffer.d.ts +23 -2
- package/dist/runtime/heapScene/growBuffer.d.ts.map +1 -1
- package/dist/runtime/heapScene/growBuffer.js +35 -3
- package/dist/runtime/heapScene/growBuffer.js.map +1 -1
- package/dist/runtime/heapScene/pools.d.ts +84 -42
- package/dist/runtime/heapScene/pools.d.ts.map +1 -1
- package/dist/runtime/heapScene/pools.js +309 -134
- package/dist/runtime/heapScene/pools.js.map +1 -1
- package/dist/runtime/heapScene.d.ts +24 -0
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +1158 -92
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/hybridScene.d.ts +15 -0
- package/dist/runtime/hybridScene.d.ts.map +1 -1
- package/dist/runtime/hybridScene.js +16 -0
- package/dist/runtime/hybridScene.js.map +1 -1
- package/dist/runtime/index.d.ts +4 -3
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +4 -3
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/renderTask.d.ts +5 -0
- package/dist/runtime/renderTask.d.ts.map +1 -1
- package/dist/runtime/renderTask.js +1 -0
- package/dist/runtime/renderTask.js.map +1 -1
- package/dist/runtime/runtime.d.ts +6 -0
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +1 -0
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.d.ts +12 -0
- package/dist/runtime/textureAtlas/atlasPool.d.ts.map +1 -1
- package/dist/runtime/textureAtlas/atlasPool.js +20 -0
- package/dist/runtime/textureAtlas/atlasPool.js.map +1 -1
- package/package.json +1 -1
- package/src/core/renderObject.ts +16 -0
- package/src/runtime/derivedModes/kernelCodegen.ts +673 -0
- package/src/runtime/derivedModes/modeKeyCpu.ts +22 -60
- package/src/runtime/derivedModes/partitionDispatcher.ts +349 -0
- package/src/runtime/derivedModes/partitionKernelLayout.ts +25 -0
- package/src/runtime/derivedModes/rule.ts +66 -145
- package/src/runtime/derivedUniforms/dispatch.ts +48 -21
- package/src/runtime/derivedUniforms/marker.ts +210 -16
- package/src/runtime/derivedUniforms/sceneIntegration.ts +67 -20
- package/src/runtime/heapAdapter.ts +17 -0
- package/src/runtime/heapEligibility.ts +22 -1
- package/src/runtime/heapScene/chunkedArena.ts +238 -0
- package/src/runtime/heapScene/freelist.ts +196 -0
- package/src/runtime/heapScene/growBuffer.ts +39 -2
- package/src/runtime/heapScene/pools.ts +350 -143
- package/src/runtime/heapScene.ts +1350 -88
- package/src/runtime/hybridScene.ts +31 -0
- package/src/runtime/index.ts +12 -9
- package/src/runtime/renderTask.ts +6 -0
- package/src/runtime/runtime.ts +7 -0
- package/src/runtime/textureAtlas/atlasPool.ts +19 -0
- package/src/runtime/derivedModes/gpuDispatcher.ts +0 -226
- package/src/runtime/derivedModes/gpuKernel.ts +0 -98
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pools.d.ts","sourceRoot":"","sources":["../../../src/runtime/heapScene/pools.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"pools.d.ts","sourceRoot":"","sources":["../../../src/runtime/heapScene/pools.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAqC,MAAM,iBAAiB,CAAC;AAEhF,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAIjF;;iCAEiC;AACjC,eAAO,MAAM,kBAAkB,IAAM,CAAC;AACtC,eAAO,MAAM,mBAAmB,KAAM,CAAC;AAEvC,iDAAiD;AACjD,eAAO,MAAM,aAAa,IAAI,CAAC;AAE/B;yCACyC;AACzC,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,WAAW,IAAM,CAAC;AAI/B;;;;;GAKG;AACH,UAAU,SAAS;IACjB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACtE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoD;IAE3E,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO;IAGjD;;oEAEgE;IAChE,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,OAAO;IAGlC,KAAK,CAAC,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAGjE;;0DAEsD;IACtD,oBAAoB,CAAC,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,MAAM,GAAG,SAAS;IAM3D;;;;;OAKG;IACH,OAAO,CACL,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,qBAAqB,EAC5B,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,EACjB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,GAC3D,MAAM;IA8CT,OAAO,CAAC,KAAK,EAAE,qBAAqB,EAAE,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAqBhF;;;4DAGwD;IACxD,MAAM,CACJ,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,qBAAqB,EAC5B,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,EACjB,GAAG,EAAE,OAAO,GACX,IAAI;IAiBP;uDACmD;IACnD,cAAc,CAAC,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,MAAM;CAO1C;AAID;;;;;;;;;;;GAWG;AACH,qBAAa,SAAS;IACpB;6DACyD;IACzD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAGnB;IACJ,0DAA0D;IAC1D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqC;IAChE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0C;IACpE,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,UAAU;IASlB,oBAAoB,CAAC,EAAE,EAAE,IAAI,CAAC,WAAW,CAAC,GAAG,MAAM,GAAG,SAAS;IAM/D,OAAO,CACL,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,qBAAqB,EAC9B,EAAE,EAAE,IAAI,CAAC,WAAW,CAAC,EACrB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,WAAW,GACf;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE;IA6C1D,OAAO,CAAC,OAAO,EAAE,qBAAqB,EAAE,EAAE,EAAE,IAAI,CAAC,WAAW,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;CAgBvF;AAYD;;;;;GAKG;AACH,qBAAa,QAAQ;IAKP,OAAO,CAAC,QAAQ,CAAC,GAAG;IAAc,OAAO,CAAC,QAAQ,CAAC,SAAS;IAJxE,OAAO,CAAC,IAAI,CAAgB;IAC5B,OAAO,CAAC,QAAQ,CAAK;IACrB,gEAAgE;IAChE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAqB;gBACb,GAAG,EAAE,UAAU,EAAmB,SAAS,EAAE,MAAM;IAChF,IAAI,MAAM,IAAI,SAAS,CAA4B;IACnD,yEAAyE;IACzE,IAAI,YAAY,IAAI,MAAM,CAA2B;IACrD,2EAA2E;IAC3E,IAAI,SAAS,IAAI,MAAM,CAA2C;IAClE,KAAK,IAAI,MAAM;IAUf,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAO3B,QAAQ,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,WAAW;IACrC,OAAO,IAAI,IAAI;CAChB;AAID;;;;;GAKG;AACH,qBAAa,cAAc;IAab,OAAO,CAAC,QAAQ,CAAC,GAAG;IAZhC,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkB;IAC3C;;;gEAG4D;IAC5D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;IACxD;oEACgE;IAChE,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,QAAQ,CAAY;IAC5B,OAAO,CAAC,QAAQ,CAAK;gBACQ,GAAG,EAAE,UAAU;IAQ5C,IAAI,MAAM,IAAI,SAAS,CAA4B;IACnD,IAAI,QAAQ,IAAI,MAAM,CAA8B;IACpD,IAAI,SAAS,IAAI,MAAM,CAAwB;IAC/C;;;;;qCAKiC;IACjC,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW;IAGtD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,IAAI;IAM1C,KAAK,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI;IAU9B;;;OAGG;IACH;;;0BAGsB;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAgB/C,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAShC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IA8B7C,sEAAsE;IACtE,OAAO,CAAC,WAAW;IAgBnB,QAAQ,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,WAAW;IACrC,OAAO,IAAI,IAAI;CAChB;AAID;;;;GAIG;AACH,qBAAa,cAAc;IAQb,OAAO,CAAC,QAAQ,CAAC,GAAG;IAPhC,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkB;IAC3C,8DAA8D;IAC9D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;IACxD,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,QAAQ,CAAY;IAC5B,OAAO,CAAC,QAAQ,CAAK;gBACQ,GAAG,EAAE,UAAU;IAQ5C,IAAI,MAAM,IAAI,SAAS,CAA4B;IACnD,IAAI,YAAY,IAAI,MAAM,CAAwB;IAClD,KAAK,CAAC,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,IAAI;IAMpD,KAAK,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI;IAU9B;;oEAEgE;IAChE,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAe9C,OAAO,CAAC,WAAW;IAYnB,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;IAO/B,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAwB5C,QAAQ,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,WAAW;IACrC,OAAO,IAAI,IAAI;CAChB;AAID;;;;;;;;GAQG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAK,qBAAqB,CAAC;IACzC,QAAQ,CAAC,OAAO,EAAG,qBAAqB,CAAC;CAC1C;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,SAAS,EACjB,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,aAAa,GAAE,mBAAuB,EACtC,aAAa,GAAE,MAAM,GAAG,SAAqB,GAC5C,UAAU,CAkBZ;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAEpD;AAED,wGAAwG;AACxG,wBAAgB,cAAc,CAC5B,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,GACjG,IAAI,CASN;AAED,wBAAgB,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAIjD;AAED,kFAAkF;AAClF,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,UAAU,CAMxD;AAED;iEACiE;AACjE,wBAAgB,SAAS,CAAC,IAAI,EAAE,gBAAgB,GAAG,YAAY,CAM9D"}
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
// / `isBufferView` / `asFloat32` helpers that the heap-scene factory
|
|
16
16
|
// uses to assemble the arena.
|
|
17
17
|
import { AVal } from "@aardworx/wombat.adaptive";
|
|
18
|
-
import { GrowBuffer, ALIGN16 } from "./growBuffer.js";
|
|
18
|
+
import { GrowBuffer, ALIGN16, DEFAULT_MAX_BUFFER_BYTES } from "./growBuffer.js";
|
|
19
|
+
import { Freelist } from "./freelist.js";
|
|
20
|
+
import { ChunkedAttributeArena, ChunkedIndexAllocator } from "./chunkedArena.js";
|
|
19
21
|
// ─── Per-allocation header layout ──────────────────────────────────────
|
|
20
22
|
/** Per-allocation header: (u32 typeId, u32 length). Data follows
|
|
21
23
|
* the header aligned up to 16 bytes (so positions/normals/etc. line
|
|
@@ -29,76 +31,137 @@ export const ENC_V3F_TIGHT = 1; // tightly-packed array of vec3<f32> (12 B/elt)
|
|
|
29
31
|
export const SEM_POSITIONS = 1;
|
|
30
32
|
export const SEM_NORMALS = 2;
|
|
31
33
|
/**
|
|
32
|
-
*
|
|
33
|
-
* aval
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* (
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* Sharing emerges from aval identity — no separate "frequency"
|
|
40
|
-
* declaration needed. A `cval` shared by all draws → 1 alloc.
|
|
41
|
-
* A static positions array shared across instanced draws → 1 alloc.
|
|
42
|
-
* Same code path either way.
|
|
34
|
+
* `(aval, chunkIdx)`-keyed pool of chunked-arena allocations. An
|
|
35
|
+
* aval that's referenced from multiple chunks (e.g. two ROs sharing
|
|
36
|
+
* a uniform but landing in different chunks) gets one entry per
|
|
37
|
+
* chunk — accepted duplication cost of the multi-draw-call
|
|
38
|
+
* per-chunk render path (§3). Within a single chunk, ROs sharing
|
|
39
|
+
* an aval still share the underlying allocation.
|
|
43
40
|
*/
|
|
44
41
|
export class UniformPool {
|
|
45
|
-
// Keyed by `aval<unknown>` *by reference* (a plain JS `Map`). These
|
|
46
|
-
// keys are overwhelmingly reactive `cval`s (per-object trafos,
|
|
47
|
-
// colours, …) and the hot path is `acquire`/`release` ~once per
|
|
48
|
-
// drawHeader field per RO.
|
|
49
42
|
byAval = new Map();
|
|
50
|
-
has(av) {
|
|
51
|
-
|
|
43
|
+
has(av, chunkIdx) {
|
|
44
|
+
return this.byAval.get(av)?.has(chunkIdx) ?? false;
|
|
45
|
+
}
|
|
46
|
+
/** True when the pool holds at least one entry for `av` in any
|
|
47
|
+
* chunk. Used by the scene's `inputChanged` dispatch to decide
|
|
48
|
+
* whether a marking aval is one of our tracked allocations. */
|
|
49
|
+
hasAny(av) {
|
|
50
|
+
return (this.byAval.get(av)?.size ?? 0) > 0;
|
|
51
|
+
}
|
|
52
|
+
entry(av, chunkIdx) {
|
|
53
|
+
return this.byAval.get(av)?.get(chunkIdx);
|
|
54
|
+
}
|
|
55
|
+
/** First chunk this aval is allocated in, if any. Used by `addRO`
|
|
56
|
+
* to prefer co-locating a new RO with its already-allocated
|
|
57
|
+
* shared inputs (avoids unnecessary duplication). */
|
|
58
|
+
firstChunkContaining(av) {
|
|
59
|
+
const byChunk = this.byAval.get(av);
|
|
60
|
+
if (byChunk === undefined || byChunk.size === 0)
|
|
61
|
+
return undefined;
|
|
62
|
+
return byChunk.keys().next().value;
|
|
63
|
+
}
|
|
52
64
|
/**
|
|
53
|
-
* Acquire (or share) an allocation for `aval
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* uploaded immediately.
|
|
65
|
+
* Acquire (or share) an allocation for `aval` in `chunkIdx`.
|
|
66
|
+
* Returns the byte offset within that chunk's GPUBuffer. If a
|
|
67
|
+
* fresh allocation is made, the value is packed + uploaded
|
|
68
|
+
* immediately into the chunk's CPU shadow.
|
|
58
69
|
*/
|
|
59
|
-
acquire(device, arena, av, value, dataBytes, typeId, length, pack) {
|
|
60
|
-
|
|
70
|
+
acquire(device, arena, av, chunkIdx, value, dataBytes, typeId, length, pack) {
|
|
71
|
+
let byChunk = this.byAval.get(av);
|
|
72
|
+
const existing = byChunk?.get(chunkIdx);
|
|
61
73
|
if (existing !== undefined) {
|
|
62
74
|
existing.refcount++;
|
|
63
75
|
return existing.ref;
|
|
64
76
|
}
|
|
65
|
-
const
|
|
77
|
+
const r = arena.alloc(dataBytes, chunkIdx);
|
|
78
|
+
// `arena.alloc` may spill to a different chunk if `chunkIdx`'s
|
|
79
|
+
// GrowBuffer is at its maxCapacity. Callers (heapScene.addRO)
|
|
80
|
+
// bind a bucket's bind groups to a single chunk's buffer — a
|
|
81
|
+
// silent spill makes the bucket's drawHeader refs point into
|
|
82
|
+
// the wrong chunk's buffer (garbage reads / typeId corruption).
|
|
83
|
+
//
|
|
84
|
+
// Hard-fail with diagnostic info instead of returning quietly.
|
|
85
|
+
// The right long-term fix is for addRO to pre-probe the chunk,
|
|
86
|
+
// open a new bucket bound to the spill chunk, and re-route; but
|
|
87
|
+
// until that lands, a clear error beats unexplained artefacts.
|
|
88
|
+
if (r.chunkIdx !== chunkIdx) {
|
|
89
|
+
throw new Error(`UniformPool.acquire: allocator spilled from chunk ${chunkIdx} to chunk ${r.chunkIdx} ` +
|
|
90
|
+
`(${dataBytes} bytes). Caller's bucket is bound to chunk ${chunkIdx}; this would silently ` +
|
|
91
|
+
`corrupt the drawHeader→arena reads. Open a new bucket bound to chunk ${r.chunkIdx} instead.`);
|
|
92
|
+
}
|
|
93
|
+
const finalChunk = r.chunkIdx;
|
|
66
94
|
const allocBytes = ALIGN16(ALLOC_HEADER_PAD_TO + dataBytes);
|
|
67
95
|
const buf = new ArrayBuffer(allocBytes);
|
|
68
96
|
const u32 = new Uint32Array(buf);
|
|
69
97
|
const f32 = new Float32Array(buf);
|
|
70
98
|
u32[0] = typeId;
|
|
71
99
|
u32[1] = length;
|
|
72
|
-
// stride_bytes (offset 8): bytes per element. Lets the VS decode
|
|
73
|
-
// pick V3- vs V4-tight load expressions for vec4 attributes.
|
|
74
100
|
u32[2] = length > 0 ? Math.floor(dataBytes / length) : 0;
|
|
75
101
|
pack(value, f32, ALLOC_HEADER_PAD_TO / 4);
|
|
76
|
-
arena.write(
|
|
102
|
+
arena.write(finalChunk, r.off, new Uint8Array(buf));
|
|
77
103
|
void device;
|
|
78
|
-
|
|
79
|
-
|
|
104
|
+
if (byChunk === undefined) {
|
|
105
|
+
byChunk = new Map();
|
|
106
|
+
this.byAval.set(av, byChunk);
|
|
107
|
+
}
|
|
108
|
+
byChunk.set(finalChunk, {
|
|
109
|
+
chunkIdx: finalChunk, ref: r.off, dataBytes, typeId, pack, refcount: 1,
|
|
110
|
+
});
|
|
111
|
+
return r.off;
|
|
80
112
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const e =
|
|
113
|
+
release(arena, av, chunkIdx) {
|
|
114
|
+
const byChunk = this.byAval.get(av);
|
|
115
|
+
const e = byChunk?.get(chunkIdx);
|
|
84
116
|
if (e === undefined)
|
|
85
117
|
return;
|
|
86
118
|
e.refcount--;
|
|
87
119
|
if (e.refcount > 0)
|
|
88
120
|
return;
|
|
89
|
-
arena.release
|
|
90
|
-
|
|
121
|
+
// BUG FIX: arena.release's third arg is `dataBytes` (raw), and the
|
|
122
|
+
// chunk's AttributeArena.release internally adds the header padding
|
|
123
|
+
// via `ALIGN16(ALLOC_HEADER_PAD_TO + dataBytes)`. We MUST pass the
|
|
124
|
+
// raw data size here — passing pre-aligned `allocBytes` (the old
|
|
125
|
+
// code) double-pads, freeing 16 extra bytes per release. The freed
|
|
126
|
+
// surplus then gets coalesced with the neighbouring scalar-uniform
|
|
127
|
+
// allocs (any 32 B u32/f32) into a single combined block, and the
|
|
128
|
+
// next attribute alloc handed out from that pool ends up STARTING
|
|
129
|
+
// 16 B inside the previous alloc → overlapping allocations →
|
|
130
|
+
// garbage `typeId`/`length` in the attribute header.
|
|
131
|
+
arena.release(e.chunkIdx, e.ref, e.dataBytes);
|
|
132
|
+
byChunk.delete(chunkIdx);
|
|
133
|
+
if (byChunk.size === 0)
|
|
134
|
+
this.byAval.delete(av);
|
|
91
135
|
}
|
|
92
|
-
/** Re-pack one entry's data region into
|
|
136
|
+
/** Re-pack one entry's data region into every chunk that holds
|
|
137
|
+
* this aval. When an aval is shared across N chunks (each chunk
|
|
138
|
+
* has its own duplicate alloc), every duplicate needs the
|
|
139
|
+
* refresh so all the chunks' ROs see the new value. */
|
|
93
140
|
repack(device, arena, av, val) {
|
|
94
|
-
const
|
|
95
|
-
if (
|
|
141
|
+
const byChunk = this.byAval.get(av);
|
|
142
|
+
if (byChunk === undefined)
|
|
96
143
|
return;
|
|
97
|
-
|
|
98
|
-
e.
|
|
99
|
-
|
|
144
|
+
let dst;
|
|
145
|
+
for (const e of byChunk.values()) {
|
|
146
|
+
if (dst === undefined || dst.length !== e.dataBytes / 4) {
|
|
147
|
+
dst = new Float32Array(e.dataBytes / 4);
|
|
148
|
+
}
|
|
149
|
+
e.pack(val, dst, 0);
|
|
150
|
+
arena.write(e.chunkIdx, e.ref + ALLOC_HEADER_PAD_TO, new Uint8Array(dst.buffer, dst.byteOffset, e.dataBytes));
|
|
151
|
+
}
|
|
100
152
|
void device;
|
|
101
153
|
}
|
|
154
|
+
/** Total bytes touched by `repack` for this aval — sum across
|
|
155
|
+
* chunks. Used by the dirty-bytes diagnostics. */
|
|
156
|
+
totalDataBytes(av) {
|
|
157
|
+
const byChunk = this.byAval.get(av);
|
|
158
|
+
if (byChunk === undefined)
|
|
159
|
+
return 0;
|
|
160
|
+
let s = 0;
|
|
161
|
+
for (const e of byChunk.values())
|
|
162
|
+
s += e.dataBytes;
|
|
163
|
+
return s;
|
|
164
|
+
}
|
|
102
165
|
}
|
|
103
166
|
// ─── IndexPool ─────────────────────────────────────────────────────────
|
|
104
167
|
/**
|
|
@@ -114,7 +177,10 @@ export class UniformPool {
|
|
|
114
177
|
* `Uint32Array` view collapse to one allocation.
|
|
115
178
|
*/
|
|
116
179
|
export class IndexPool {
|
|
180
|
+
/** Per-aval, per-chunk bookkeeping. Same aval acquired in two
|
|
181
|
+
* chunks gets two entries (accepted §3 duplication). */
|
|
117
182
|
byAval = new Map();
|
|
183
|
+
/** Per-chunk constant-aval value-dedup (§5b in-chunk). */
|
|
118
184
|
byValueKey = new Map();
|
|
119
185
|
bufferIds = new WeakMap();
|
|
120
186
|
nextBufferId = 1;
|
|
@@ -126,43 +192,69 @@ export class IndexPool {
|
|
|
126
192
|
}
|
|
127
193
|
return id;
|
|
128
194
|
}
|
|
129
|
-
|
|
130
|
-
const
|
|
195
|
+
firstChunkContaining(av) {
|
|
196
|
+
const byChunk = this.byAval.get(av);
|
|
197
|
+
if (byChunk === undefined || byChunk.size === 0)
|
|
198
|
+
return undefined;
|
|
199
|
+
return byChunk.keys().next().value;
|
|
200
|
+
}
|
|
201
|
+
acquire(device, indices, av, chunkIdx, arr) {
|
|
202
|
+
let byChunk = this.byAval.get(av);
|
|
203
|
+
const bound = byChunk?.get(chunkIdx);
|
|
131
204
|
if (bound !== undefined) {
|
|
132
205
|
bound.perAvalCount++;
|
|
133
206
|
bound.entry.totalRefcount++;
|
|
134
|
-
return { firstIndex: bound.entry.firstIndex, count: bound.entry.count };
|
|
207
|
+
return { chunkIdx: bound.entry.chunkIdx, firstIndex: bound.entry.firstIndex, count: bound.entry.count };
|
|
135
208
|
}
|
|
136
209
|
let valueKey;
|
|
137
210
|
if (av.isConstant) {
|
|
138
|
-
|
|
211
|
+
// Value-dedup is scoped per chunk — two constant avals in the
|
|
212
|
+
// same chunk collapse to one allocation; two constant avals in
|
|
213
|
+
// DIFFERENT chunks each get their own (we can't share across
|
|
214
|
+
// chunks since the index range lives in the chunk's GPUBuffer).
|
|
215
|
+
valueKey = `${chunkIdx}:${this.bufferIdOf(arr.buffer)}:${arr.byteOffset}:${arr.byteLength}`;
|
|
139
216
|
const shared = this.byValueKey.get(valueKey);
|
|
140
217
|
if (shared !== undefined) {
|
|
141
218
|
shared.totalRefcount++;
|
|
142
|
-
|
|
143
|
-
|
|
219
|
+
if (byChunk === undefined) {
|
|
220
|
+
byChunk = new Map();
|
|
221
|
+
this.byAval.set(av, byChunk);
|
|
222
|
+
}
|
|
223
|
+
byChunk.set(chunkIdx, { entry: shared, perAvalCount: 1 });
|
|
224
|
+
return { chunkIdx: shared.chunkIdx, firstIndex: shared.firstIndex, count: shared.count };
|
|
144
225
|
}
|
|
145
226
|
}
|
|
146
|
-
const
|
|
147
|
-
indices.write(
|
|
227
|
+
const r = indices.alloc(arr.length, chunkIdx);
|
|
228
|
+
indices.write(r.chunkIdx, r.off * 4, new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength));
|
|
148
229
|
void device;
|
|
149
|
-
const entry = {
|
|
150
|
-
|
|
230
|
+
const entry = {
|
|
231
|
+
chunkIdx: r.chunkIdx, firstIndex: r.off, count: arr.length,
|
|
232
|
+
totalRefcount: 1, valueKey,
|
|
233
|
+
};
|
|
234
|
+
if (byChunk === undefined) {
|
|
235
|
+
byChunk = new Map();
|
|
236
|
+
this.byAval.set(av, byChunk);
|
|
237
|
+
}
|
|
238
|
+
byChunk.set(r.chunkIdx, { entry, perAvalCount: 1 });
|
|
151
239
|
if (valueKey !== undefined)
|
|
152
240
|
this.byValueKey.set(valueKey, entry);
|
|
153
|
-
return { firstIndex, count: arr.length };
|
|
241
|
+
return { chunkIdx: r.chunkIdx, firstIndex: r.off, count: arr.length };
|
|
154
242
|
}
|
|
155
|
-
release(indices, av) {
|
|
156
|
-
const
|
|
243
|
+
release(indices, av, chunkIdx) {
|
|
244
|
+
const byChunk = this.byAval.get(av);
|
|
245
|
+
const bound = byChunk?.get(chunkIdx);
|
|
157
246
|
if (bound === undefined)
|
|
158
247
|
return;
|
|
159
248
|
bound.perAvalCount--;
|
|
160
249
|
bound.entry.totalRefcount--;
|
|
161
|
-
if (bound.perAvalCount === 0)
|
|
162
|
-
|
|
250
|
+
if (bound.perAvalCount === 0) {
|
|
251
|
+
byChunk.delete(chunkIdx);
|
|
252
|
+
if (byChunk.size === 0)
|
|
253
|
+
this.byAval.delete(av);
|
|
254
|
+
}
|
|
163
255
|
if (bound.entry.totalRefcount > 0)
|
|
164
256
|
return;
|
|
165
|
-
indices.release(bound.entry.firstIndex, bound.entry.count);
|
|
257
|
+
indices.release(bound.entry.chunkIdx, bound.entry.firstIndex, bound.entry.count);
|
|
166
258
|
if (bound.entry.valueKey !== undefined) {
|
|
167
259
|
this.byValueKey.delete(bound.entry.valueKey);
|
|
168
260
|
}
|
|
@@ -180,6 +272,8 @@ export class DrawHeap {
|
|
|
180
272
|
slotBytes;
|
|
181
273
|
free = [];
|
|
182
274
|
nextSlot = 0;
|
|
275
|
+
/** DEBUG: tracks slot live/free state. true = live (in use). */
|
|
276
|
+
live = new Set();
|
|
183
277
|
constructor(buf, slotBytes) {
|
|
184
278
|
this.buf = buf;
|
|
185
279
|
this.slotBytes = slotBytes;
|
|
@@ -191,11 +285,21 @@ export class DrawHeap {
|
|
|
191
285
|
get usedBytes() { return this.nextSlot * this.slotBytes; }
|
|
192
286
|
alloc() {
|
|
193
287
|
const slot = this.free.length > 0 ? this.free.pop() : this.nextSlot++;
|
|
288
|
+
if (this.live.has(slot)) {
|
|
289
|
+
throw new Error(`DrawHeap.alloc: returned already-live slot ${slot}`);
|
|
290
|
+
}
|
|
291
|
+
this.live.add(slot);
|
|
194
292
|
this.buf.ensureCapacity((slot + 1) * this.slotBytes);
|
|
195
293
|
this.buf.setUsed(Math.max(this.buf.usedBytes, (slot + 1) * this.slotBytes));
|
|
196
294
|
return slot;
|
|
197
295
|
}
|
|
198
|
-
release(slot) {
|
|
296
|
+
release(slot) {
|
|
297
|
+
if (!this.live.has(slot)) {
|
|
298
|
+
throw new Error(`DrawHeap.release: slot ${slot} was not live — double-release or release-of-never-allocated.`);
|
|
299
|
+
}
|
|
300
|
+
this.live.delete(slot);
|
|
301
|
+
this.free.push(slot);
|
|
302
|
+
}
|
|
199
303
|
onResize(cb) { return this.buf.onResize(cb); }
|
|
200
304
|
destroy() { this.buf.destroy(); }
|
|
201
305
|
}
|
|
@@ -209,7 +313,12 @@ export class DrawHeap {
|
|
|
209
313
|
export class AttributeArena {
|
|
210
314
|
buf;
|
|
211
315
|
cursor = 0;
|
|
212
|
-
|
|
316
|
+
freelist = new Freelist();
|
|
317
|
+
/** DEBUG: tracks every live allocation by start byte → size. Set
|
|
318
|
+
* on alloc, cleared on release. Used by `assertNoOverlap()` to
|
|
319
|
+
* detect allocator returning overlapping ranges. Enabled in
|
|
320
|
+
* development; cheap (one Map insert/delete per alloc). */
|
|
321
|
+
liveAllocs = new Map();
|
|
213
322
|
/** CPU shadow of the entire GPU buffer; writes go here first then
|
|
214
323
|
* flush() emits one writeBuffer per dirty contiguous range. */
|
|
215
324
|
shadow;
|
|
@@ -227,6 +336,15 @@ export class AttributeArena {
|
|
|
227
336
|
get buffer() { return this.buf.buffer; }
|
|
228
337
|
get capacity() { return this.buf.capacity; }
|
|
229
338
|
get usedBytes() { return this.cursor; }
|
|
339
|
+
/** Read 4 u32s from the CPU shadow starting at byte `off`. Used by
|
|
340
|
+
* the validator to compare against the GPU's read-back values:
|
|
341
|
+
* if CPU shadow is correct but GPU shows garbage → the corruption
|
|
342
|
+
* came from a GPU-side write (a compute kernel writing to a wrong
|
|
343
|
+
* byte). If both differ from what pool.acquire wrote → CPU-side
|
|
344
|
+
* write path is the culprit. */
|
|
345
|
+
peekShadowU32(off, count) {
|
|
346
|
+
return new Uint32Array(this.shadow.buffer, this.shadow.byteOffset + off, count);
|
|
347
|
+
}
|
|
230
348
|
write(dst, data) {
|
|
231
349
|
this.shadow.set(data, dst);
|
|
232
350
|
if (dst < this.dirtyMin)
|
|
@@ -246,76 +364,91 @@ export class AttributeArena {
|
|
|
246
364
|
* Allocate space for one attribute. Returns the byte ref (offset
|
|
247
365
|
* to the header — data lives at ref + 16).
|
|
248
366
|
*/
|
|
249
|
-
alloc
|
|
367
|
+
/** Non-throwing alloc — returns `undefined` when the request
|
|
368
|
+
* would exceed the underlying GrowBuffer's `maxCapacity` cap.
|
|
369
|
+
* Used by `ChunkedAttributeArena` to probe before spilling to
|
|
370
|
+
* the next chunk. */
|
|
371
|
+
tryAlloc(dataBytes) {
|
|
250
372
|
const allocBytes = ALIGN16(ALLOC_HEADER_PAD_TO + dataBytes);
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const ref = f.off;
|
|
256
|
-
if (f.size === allocBytes)
|
|
257
|
-
this.freeList.splice(i, 1);
|
|
258
|
-
else {
|
|
259
|
-
f.off += allocBytes;
|
|
260
|
-
f.size -= allocBytes;
|
|
261
|
-
}
|
|
262
|
-
return ref;
|
|
263
|
-
}
|
|
373
|
+
const reused = this.freelist.alloc(allocBytes);
|
|
374
|
+
if (reused !== undefined) {
|
|
375
|
+
this.recordAlloc(reused, allocBytes, "freelist");
|
|
376
|
+
return reused;
|
|
264
377
|
}
|
|
265
|
-
const
|
|
266
|
-
this.
|
|
267
|
-
|
|
268
|
-
this.
|
|
378
|
+
const next = this.cursor + allocBytes;
|
|
379
|
+
if (next > this.buf.maxCapacity)
|
|
380
|
+
return undefined;
|
|
381
|
+
this.cursor = next;
|
|
382
|
+
this.buf.ensureCapacity(next);
|
|
383
|
+
this.buf.setUsed(next);
|
|
384
|
+
const ref = next - allocBytes;
|
|
385
|
+
this.recordAlloc(ref, allocBytes, "bump");
|
|
269
386
|
return ref;
|
|
270
387
|
}
|
|
388
|
+
alloc(dataBytes) {
|
|
389
|
+
const r = this.tryAlloc(dataBytes);
|
|
390
|
+
if (r === undefined) {
|
|
391
|
+
throw new Error(`AttributeArena: allocation of ${dataBytes}B (with header alignment) exceeds chunk cap`);
|
|
392
|
+
}
|
|
393
|
+
return r;
|
|
394
|
+
}
|
|
271
395
|
release(ref, dataBytes) {
|
|
272
396
|
const allocBytes = ALIGN16(ALLOC_HEADER_PAD_TO + dataBytes);
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
prev.size += next.size;
|
|
297
|
-
freeList.splice(lo, 1);
|
|
397
|
+
const tracked = this.liveAllocs.get(ref);
|
|
398
|
+
if (tracked === undefined) {
|
|
399
|
+
throw new Error(`AttributeArena.release: ref=${ref} (size=${allocBytes}) was not in liveAllocs — ` +
|
|
400
|
+
`double-free or release for an alloc that never happened. ` +
|
|
401
|
+
`Live alloc count=${this.liveAllocs.size}.`);
|
|
402
|
+
}
|
|
403
|
+
if (tracked !== allocBytes) {
|
|
404
|
+
throw new Error(`AttributeArena.release: size mismatch at ref=${ref} — recorded=${tracked} but ` +
|
|
405
|
+
`release args→ allocBytes=${allocBytes} (dataBytes=${dataBytes}). ` +
|
|
406
|
+
`Caller is releasing the wrong size; this WILL corrupt the freelist via over- or under-shrinking.`);
|
|
407
|
+
}
|
|
408
|
+
this.liveAllocs.delete(ref);
|
|
409
|
+
this.freelist.release(ref, allocBytes);
|
|
410
|
+
// §5: shrink the bump cursor back if release exposed a free
|
|
411
|
+
// tail touching it (cascading — `freelist.release` may have
|
|
412
|
+
// coalesced into a single block ending at `cursor`). Reclaims
|
|
413
|
+
// the high-watermark over time in long-lived high-churn scenes.
|
|
414
|
+
while (true) {
|
|
415
|
+
const top = this.freelist.takeBlockEndingAt(this.cursor);
|
|
416
|
+
if (top === undefined)
|
|
417
|
+
break;
|
|
418
|
+
this.cursor = top.off;
|
|
419
|
+
this.buf.setUsed(this.cursor);
|
|
298
420
|
}
|
|
299
|
-
return;
|
|
300
421
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
422
|
+
/** Throws if the just-returned alloc overlaps any live allocation. */
|
|
423
|
+
recordAlloc(off, size, source) {
|
|
424
|
+
// Check against every existing live alloc — O(N). Cheap while N
|
|
425
|
+
// is small (per-tile demos run with ~hundreds of allocs). Wire
|
|
426
|
+
// off behind an env-flag if it ever becomes a hot path.
|
|
427
|
+
for (const [liveOff, liveSize] of this.liveAllocs) {
|
|
428
|
+
// [a, a+sa) vs [b, b+sb) overlap iff a < b+sb && b < a+sa
|
|
429
|
+
if (off < liveOff + liveSize && liveOff < off + size) {
|
|
430
|
+
throw new Error(`AttributeArena.tryAlloc(${source}): returned [${off},${off + size}) ` +
|
|
431
|
+
`(size=${size}) overlaps live alloc at [${liveOff},${liveOff + liveSize}) ` +
|
|
432
|
+
`(size=${liveSize}). Allocator is handing out shared memory!`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
this.liveAllocs.set(off, size);
|
|
306
436
|
}
|
|
307
|
-
|
|
437
|
+
onResize(cb) { return this.buf.onResize(cb); }
|
|
438
|
+
destroy() { this.buf.destroy(); }
|
|
308
439
|
}
|
|
309
440
|
// ─── IndexAllocator (element-bump) ─────────────────────────────────────
|
|
310
441
|
/**
|
|
311
442
|
* Element-bump allocator over an index GrowBuffer (units = u32). Each
|
|
312
443
|
* draw's index range is allocated as one block; on release the block
|
|
313
|
-
* is returned to a
|
|
444
|
+
* is returned to a Freelist and can be reused best-fit.
|
|
314
445
|
*/
|
|
315
446
|
export class IndexAllocator {
|
|
316
447
|
buf;
|
|
317
448
|
cursor = 0; // in u32s, not bytes
|
|
318
|
-
|
|
449
|
+
freelist = new Freelist();
|
|
450
|
+
/** DEBUG: live index allocations tracked by start element. */
|
|
451
|
+
liveAllocs = new Map();
|
|
319
452
|
shadow;
|
|
320
453
|
dirtyMin = Infinity;
|
|
321
454
|
dirtyMax = 0;
|
|
@@ -345,39 +478,81 @@ export class IndexAllocator {
|
|
|
345
478
|
this.dirtyMin = Infinity;
|
|
346
479
|
this.dirtyMax = 0;
|
|
347
480
|
}
|
|
348
|
-
alloc
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
f.off += elements;
|
|
357
|
-
f.size -= elements;
|
|
358
|
-
}
|
|
359
|
-
return off;
|
|
360
|
-
}
|
|
481
|
+
/** Non-throwing alloc — returns `undefined` when the request
|
|
482
|
+
* would exceed the underlying GrowBuffer's `maxCapacity` cap.
|
|
483
|
+
* Used by `ChunkedIndexAllocator` to probe before spilling. */
|
|
484
|
+
tryAlloc(elements) {
|
|
485
|
+
const reused = this.freelist.alloc(elements);
|
|
486
|
+
if (reused !== undefined) {
|
|
487
|
+
this.recordAlloc(reused, elements, "freelist");
|
|
488
|
+
return reused;
|
|
361
489
|
}
|
|
362
|
-
const
|
|
363
|
-
this.
|
|
364
|
-
|
|
365
|
-
this.
|
|
490
|
+
const nextElts = this.cursor + elements;
|
|
491
|
+
if (nextElts * 4 > this.buf.maxCapacity)
|
|
492
|
+
return undefined;
|
|
493
|
+
this.cursor = nextElts;
|
|
494
|
+
this.buf.ensureCapacity(nextElts * 4);
|
|
495
|
+
this.buf.setUsed(nextElts * 4);
|
|
496
|
+
const off = nextElts - elements;
|
|
497
|
+
this.recordAlloc(off, elements, "bump");
|
|
366
498
|
return off;
|
|
367
499
|
}
|
|
500
|
+
recordAlloc(off, size, source) {
|
|
501
|
+
for (const [liveOff, liveSize] of this.liveAllocs) {
|
|
502
|
+
if (off < liveOff + liveSize && liveOff < off + size) {
|
|
503
|
+
throw new Error(`IndexAllocator.tryAlloc(${source}): returned [${off},${off + size}) ` +
|
|
504
|
+
`(size=${size}) overlaps live alloc at [${liveOff},${liveOff + liveSize}) ` +
|
|
505
|
+
`(size=${liveSize}). Index allocator is handing out shared memory!`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
this.liveAllocs.set(off, size);
|
|
509
|
+
}
|
|
510
|
+
alloc(elements) {
|
|
511
|
+
const r = this.tryAlloc(elements);
|
|
512
|
+
if (r === undefined) {
|
|
513
|
+
throw new Error(`IndexAllocator: allocation of ${elements} elements exceeds chunk cap`);
|
|
514
|
+
}
|
|
515
|
+
return r;
|
|
516
|
+
}
|
|
368
517
|
release(off, elements) {
|
|
369
|
-
|
|
518
|
+
const tracked = this.liveAllocs.get(off);
|
|
519
|
+
if (tracked === undefined) {
|
|
520
|
+
throw new Error(`IndexAllocator.release: off=${off} (elements=${elements}) was not in ` +
|
|
521
|
+
`liveAllocs — double-free or release for an alloc that never happened. ` +
|
|
522
|
+
`Live alloc count=${this.liveAllocs.size}.`);
|
|
523
|
+
}
|
|
524
|
+
if (tracked !== elements) {
|
|
525
|
+
throw new Error(`IndexAllocator.release: size mismatch at off=${off} — recorded=${tracked} ` +
|
|
526
|
+
`but release elements=${elements}. Will corrupt the freelist.`);
|
|
527
|
+
}
|
|
528
|
+
this.liveAllocs.delete(off);
|
|
529
|
+
this.freelist.release(off, elements);
|
|
530
|
+
while (true) {
|
|
531
|
+
const top = this.freelist.takeBlockEndingAt(this.cursor);
|
|
532
|
+
if (top === undefined)
|
|
533
|
+
break;
|
|
534
|
+
this.cursor = top.off;
|
|
535
|
+
this.buf.setUsed(this.cursor * 4);
|
|
536
|
+
}
|
|
370
537
|
}
|
|
371
538
|
onResize(cb) { return this.buf.onResize(cb); }
|
|
372
539
|
destroy() { this.buf.destroy(); }
|
|
373
540
|
}
|
|
374
|
-
export function buildArenaState(device, attrBytesHint, idxBytesHint, label, idxExtraUsage = 0) {
|
|
375
|
-
|
|
376
|
-
|
|
541
|
+
export function buildArenaState(device, attrBytesHint, idxBytesHint, label, idxExtraUsage = 0, maxChunkBytes = undefined) {
|
|
542
|
+
// §3: cap chunk size at min(adapter's maxStorageBufferBindingSize,
|
|
543
|
+
// GrowBuffer's DEFAULT_MAX_BUFFER_BYTES). Lets chunks grow as far
|
|
544
|
+
// as the hardware lets us (typically 2 GiB+ on desktop, ≥ 256 MB
|
|
545
|
+
// on mobile/integrated) while keeping a sensible internal ceiling.
|
|
546
|
+
// Caller can override (the heap-demo uses a tiny 4 MB cap to
|
|
547
|
+
// exercise multi-chunk routing).
|
|
548
|
+
const adapterCap = device.limits.maxStorageBufferBindingSize;
|
|
549
|
+
const cap = maxChunkBytes ?? Math.min(adapterCap, DEFAULT_MAX_BUFFER_BYTES);
|
|
550
|
+
const attrs = new ChunkedAttributeArena(device, `${label}/attrs`, GPUBufferUsage.STORAGE, attrBytesHint, cap);
|
|
551
|
+
const indices = new ChunkedIndexAllocator(device, `${label}/idx`, GPUBufferUsage.INDEX | idxExtraUsage, idxBytesHint, cap);
|
|
377
552
|
return { attrs, indices };
|
|
378
553
|
}
|
|
379
554
|
export function arenaBytes(arena) {
|
|
380
|
-
return arena.attrs.
|
|
555
|
+
return arena.attrs.totalUsedBytes() + arena.indices.totalUsedElements() * 4;
|
|
381
556
|
}
|
|
382
557
|
/** Upload a single attribute — header (typeId, length) + data — into the arena at byte offset `ref`. */
|
|
383
558
|
export function writeAttribute(device, buf, ref, typeId, length, data) {
|