@aardworx/wombat.rendering 0.9.14 → 0.9.15
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/derivedModes/modeKeyCpu.d.ts +45 -0
- package/dist/runtime/derivedModes/modeKeyCpu.d.ts.map +1 -0
- package/dist/runtime/derivedModes/modeKeyCpu.js +179 -0
- package/dist/runtime/derivedModes/modeKeyCpu.js.map +1 -0
- package/dist/runtime/derivedModes/partition.d.ts +36 -0
- package/dist/runtime/derivedModes/partition.d.ts.map +1 -0
- package/dist/runtime/derivedModes/partition.js +170 -0
- package/dist/runtime/derivedModes/partition.js.map +1 -0
- package/dist/runtime/derivedModes/slotTable.d.ts +70 -0
- package/dist/runtime/derivedModes/slotTable.d.ts.map +1 -0
- package/dist/runtime/derivedModes/slotTable.js +130 -0
- package/dist/runtime/derivedModes/slotTable.js.map +1 -0
- package/dist/runtime/heapScene/growBuffer.d.ts +33 -0
- package/dist/runtime/heapScene/growBuffer.d.ts.map +1 -0
- package/dist/runtime/heapScene/growBuffer.js +76 -0
- package/dist/runtime/heapScene/growBuffer.js.map +1 -0
- package/dist/runtime/heapScene/packers.d.ts +25 -0
- package/dist/runtime/heapScene/packers.d.ts.map +1 -0
- package/dist/runtime/heapScene/packers.js +111 -0
- package/dist/runtime/heapScene/packers.js.map +1 -0
- package/dist/runtime/heapScene/pools.d.ts +181 -0
- package/dist/runtime/heapScene/pools.d.ts.map +1 -0
- package/dist/runtime/heapScene/pools.js +417 -0
- package/dist/runtime/heapScene/pools.js.map +1 -0
- package/dist/runtime/heapScene/scanKernel.d.ts +11 -0
- package/dist/runtime/heapScene/scanKernel.d.ts.map +1 -0
- package/dist/runtime/heapScene/scanKernel.js +181 -0
- package/dist/runtime/heapScene/scanKernel.js.map +1 -0
- package/dist/runtime/heapScene.d.ts.map +1 -1
- package/dist/runtime/heapScene.js +228 -937
- package/dist/runtime/heapScene.js.map +1 -1
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +5 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/pipelineCache/bitfield.d.ts +5 -0
- package/dist/runtime/pipelineCache/bitfield.d.ts.map +1 -0
- package/dist/runtime/pipelineCache/bitfield.js +201 -0
- package/dist/runtime/pipelineCache/bitfield.js.map +1 -0
- package/dist/runtime/pipelineCache/cache.d.ts +36 -0
- package/dist/runtime/pipelineCache/cache.d.ts.map +1 -0
- package/dist/runtime/pipelineCache/cache.js +108 -0
- package/dist/runtime/pipelineCache/cache.js.map +1 -0
- package/dist/runtime/pipelineCache/descriptor.d.ts +58 -0
- package/dist/runtime/pipelineCache/descriptor.d.ts.map +1 -0
- package/dist/runtime/pipelineCache/descriptor.js +100 -0
- package/dist/runtime/pipelineCache/descriptor.js.map +1 -0
- package/dist/runtime/pipelineCache/index.d.ts +5 -0
- package/dist/runtime/pipelineCache/index.d.ts.map +1 -0
- package/dist/runtime/pipelineCache/index.js +12 -0
- package/dist/runtime/pipelineCache/index.js.map +1 -0
- package/package.json +1 -1
- package/src/runtime/derivedModes/modeKeyCpu.ts +213 -0
- package/src/runtime/derivedModes/partition.ts +206 -0
- package/src/runtime/derivedModes/slotTable.ts +153 -0
- package/src/runtime/heapScene/growBuffer.ts +75 -0
- package/src/runtime/heapScene/packers.ts +127 -0
- package/src/runtime/heapScene/pools.ts +492 -0
- package/src/runtime/heapScene/scanKernel.ts +184 -0
- package/src/runtime/heapScene.ts +283 -1064
- package/src/runtime/index.ts +40 -0
- package/src/runtime/pipelineCache/bitfield.ts +225 -0
- package/src/runtime/pipelineCache/cache.ts +129 -0
- package/src/runtime/pipelineCache/descriptor.ts +150 -0
- package/src/runtime/pipelineCache/index.ts +37 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// WGSL-type → JS-value packers used by heapScene to write per-draw
|
|
2
|
+
// uniforms into the arena's Float32Array staging mirror.
|
|
3
|
+
//
|
|
4
|
+
// One packer per supported WGSL type. The `pack` callback takes the
|
|
5
|
+
// aval's `.getValue(tok)` result and writes its bytes to `dst` at
|
|
6
|
+
// float offset `off`. Coerces between Trafo3d / M44d / V4f / V3d /
|
|
7
|
+
// V3f / number as needed.
|
|
8
|
+
//
|
|
9
|
+
// Integer scalar / vector packers go through a same-buffer Uint32/
|
|
10
|
+
// Int32 view to avoid the lossy i32 → f32 coercion you'd get from a
|
|
11
|
+
// direct `dst[off] = ...` assignment.
|
|
12
|
+
|
|
13
|
+
import type { M44d, V4f } from "@aardworx/wombat.base";
|
|
14
|
+
|
|
15
|
+
export interface WgslPacker {
|
|
16
|
+
/** Tightly-packed size in bytes of one value (mat4 = 64, vec3 = 12, …). */
|
|
17
|
+
readonly dataBytes: number;
|
|
18
|
+
readonly typeId: number;
|
|
19
|
+
/**
|
|
20
|
+
* Pack `val` (the aval's `.getValue(tok)` result) into `dst` at
|
|
21
|
+
* float offset `off`.
|
|
22
|
+
*/
|
|
23
|
+
readonly pack: (val: unknown, dst: Float32Array, off: number) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function packMat44(m: M44d, dst: Float32Array, off: number): void {
|
|
27
|
+
// Zero-alloc flat copy (row-major) straight into the f32 staging
|
|
28
|
+
// buffer — `copyTo` does `dst.set(m._data, off)` which narrows f64→f32
|
|
29
|
+
// on store, no throwaway `number[]` per call.
|
|
30
|
+
m.copyTo(dst, off);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const PACKER_MAT4: WgslPacker = {
|
|
34
|
+
dataBytes: 64, typeId: 0,
|
|
35
|
+
pack: (val, dst, off) => {
|
|
36
|
+
// Accept Trafo3d (uses .forward) or M44d directly.
|
|
37
|
+
const m = (val as { forward?: M44d }).forward !== undefined
|
|
38
|
+
? (val as { forward: M44d }).forward
|
|
39
|
+
: (val as M44d);
|
|
40
|
+
packMat44(m, dst, off);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const PACKER_VEC4: WgslPacker = {
|
|
45
|
+
dataBytes: 16, typeId: 0,
|
|
46
|
+
pack: (val, dst, off) => {
|
|
47
|
+
const v = val as V4f;
|
|
48
|
+
dst[off + 0] = v.x; dst[off + 1] = v.y;
|
|
49
|
+
dst[off + 2] = v.z; dst[off + 3] = v.w;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const PACKER_VEC3: WgslPacker = {
|
|
54
|
+
dataBytes: 12, typeId: 0,
|
|
55
|
+
pack: (val, dst, off) => {
|
|
56
|
+
// V3f or V3d both expose .x/.y/.z; cast through a common shape.
|
|
57
|
+
const v = val as { x: number; y: number; z: number };
|
|
58
|
+
dst[off + 0] = v.x; dst[off + 1] = v.y; dst[off + 2] = v.z;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const PACKER_VEC2: WgslPacker = {
|
|
63
|
+
dataBytes: 8, typeId: 0,
|
|
64
|
+
pack: (val, dst, off) => {
|
|
65
|
+
const v = val as { x: number; y: number };
|
|
66
|
+
dst[off + 0] = v.x; dst[off + 1] = v.y;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const PACKER_F32: WgslPacker = {
|
|
71
|
+
dataBytes: 4, typeId: 0,
|
|
72
|
+
pack: (val, dst, off) => { dst[off] = val as number; },
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
function makeIntPacker(
|
|
76
|
+
ctor: typeof Uint32Array | typeof Int32Array,
|
|
77
|
+
dim: 1 | 2 | 3 | 4,
|
|
78
|
+
): WgslPacker {
|
|
79
|
+
const bytes = dim * 4;
|
|
80
|
+
if (dim === 1) {
|
|
81
|
+
return {
|
|
82
|
+
dataBytes: bytes, typeId: 0,
|
|
83
|
+
pack: (val, dst, off) => {
|
|
84
|
+
new ctor(dst.buffer as ArrayBuffer, dst.byteOffset + off * 4, 1)[0] = val as number;
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
dataBytes: bytes, typeId: 0,
|
|
90
|
+
pack: (val, dst, off) => {
|
|
91
|
+
const view = new ctor(dst.buffer as ArrayBuffer, dst.byteOffset + off * 4, dim);
|
|
92
|
+
const v = val as { x: number; y: number; z?: number; w?: number };
|
|
93
|
+
view[0] = v.x; view[1] = v.y;
|
|
94
|
+
if (dim >= 3) view[2] = v.z!;
|
|
95
|
+
if (dim >= 4) view[3] = v.w!;
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const PACKER_U32 = makeIntPacker(Uint32Array, 1);
|
|
101
|
+
export const PACKER_UVEC2 = makeIntPacker(Uint32Array, 2);
|
|
102
|
+
export const PACKER_UVEC3 = makeIntPacker(Uint32Array, 3);
|
|
103
|
+
export const PACKER_UVEC4 = makeIntPacker(Uint32Array, 4);
|
|
104
|
+
export const PACKER_I32 = makeIntPacker(Int32Array, 1);
|
|
105
|
+
export const PACKER_IVEC2 = makeIntPacker(Int32Array, 2);
|
|
106
|
+
export const PACKER_IVEC3 = makeIntPacker(Int32Array, 3);
|
|
107
|
+
export const PACKER_IVEC4 = makeIntPacker(Int32Array, 4);
|
|
108
|
+
|
|
109
|
+
export function packerForWgslType(wgslType: string): WgslPacker {
|
|
110
|
+
switch (wgslType) {
|
|
111
|
+
case "mat4x4<f32>": return PACKER_MAT4;
|
|
112
|
+
case "vec4<f32>": return PACKER_VEC4;
|
|
113
|
+
case "vec3<f32>": return PACKER_VEC3;
|
|
114
|
+
case "vec2<f32>": return PACKER_VEC2;
|
|
115
|
+
case "f32": return PACKER_F32;
|
|
116
|
+
case "u32": return PACKER_U32;
|
|
117
|
+
case "vec2<u32>": return PACKER_UVEC2;
|
|
118
|
+
case "vec3<u32>": return PACKER_UVEC3;
|
|
119
|
+
case "vec4<u32>": return PACKER_UVEC4;
|
|
120
|
+
case "i32": return PACKER_I32;
|
|
121
|
+
case "vec2<i32>": return PACKER_IVEC2;
|
|
122
|
+
case "vec3<i32>": return PACKER_IVEC3;
|
|
123
|
+
case "vec4<i32>": return PACKER_IVEC4;
|
|
124
|
+
default:
|
|
125
|
+
throw new Error(`heapScene: no JS-side packer for WGSL type '${wgslType}'`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
// Arena + pool allocators that back the heap renderer's per-RO
|
|
2
|
+
// storage. Pulled out of heapScene.ts as a unit because the five
|
|
3
|
+
// classes here are tightly interleaved:
|
|
4
|
+
//
|
|
5
|
+
// AttributeArena — byte-bump over a STORAGE GrowBuffer
|
|
6
|
+
// IndexAllocator — element-bump over an INDEX GrowBuffer
|
|
7
|
+
// DrawHeap — slot-indexed allocator over a STORAGE GrowBuffer
|
|
8
|
+
// (one slot per RO drawHeader)
|
|
9
|
+
// UniformPool — aval-keyed refcounted alloc on top of AttributeArena
|
|
10
|
+
// IndexPool — aval-keyed refcounted alloc on top of IndexAllocator,
|
|
11
|
+
// with value-tuple dedup for constant avals (§5b).
|
|
12
|
+
//
|
|
13
|
+
// Plus the per-allocation header constants used by all of them and
|
|
14
|
+
// the `buildArenaState` / `arenaBytes` / `writeAttribute` / `asAval`
|
|
15
|
+
// / `isBufferView` / `asFloat32` helpers that the heap-scene factory
|
|
16
|
+
// uses to assemble the arena.
|
|
17
|
+
|
|
18
|
+
import { AVal } from "@aardworx/wombat.adaptive";
|
|
19
|
+
import type { aval, IDisposable } from "@aardworx/wombat.adaptive";
|
|
20
|
+
import type { BufferView } from "../../core/bufferView.js";
|
|
21
|
+
import type { HostBufferSource } from "../../core/buffer.js";
|
|
22
|
+
import { GrowBuffer, ALIGN16 } from "./growBuffer.js";
|
|
23
|
+
|
|
24
|
+
// ─── Per-allocation header layout ──────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/** Per-allocation header: (u32 typeId, u32 length). Data follows
|
|
27
|
+
* the header aligned up to 16 bytes (so positions/normals/etc. line
|
|
28
|
+
* up for future vec4 reads). */
|
|
29
|
+
export const ALLOC_HEADER_BYTES = 8;
|
|
30
|
+
export const ALLOC_HEADER_PAD_TO = 16;
|
|
31
|
+
|
|
32
|
+
/** Encoding-tag enum (low 16 bits of typeId). */
|
|
33
|
+
export const ENC_V3F_TIGHT = 1; // tightly-packed array of vec3<f32> (12 B/elt)
|
|
34
|
+
|
|
35
|
+
/** Semantic-tag enum (high 16 bits of typeId). Optional metadata —
|
|
36
|
+
* the shader doesn't branch on this. */
|
|
37
|
+
export const SEM_POSITIONS = 1;
|
|
38
|
+
export const SEM_NORMALS = 2;
|
|
39
|
+
|
|
40
|
+
// ─── UniformPool ───────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
interface PoolEntry {
|
|
43
|
+
/** Byte offset into the arena. Data starts at ref + ALLOC_HEADER_PAD_TO. */
|
|
44
|
+
readonly ref: number;
|
|
45
|
+
readonly dataBytes: number;
|
|
46
|
+
readonly typeId: number;
|
|
47
|
+
/** Packer used to refresh the data region on aval marks. */
|
|
48
|
+
readonly pack: (val: unknown, dst: Float32Array, off: number) => void;
|
|
49
|
+
refcount: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Aval-keyed pool of arena allocations. One allocation per unique
|
|
54
|
+
* aval (object identity). Two draws referencing the same aval share
|
|
55
|
+
* the allocation; their DrawHeaders carry the same u32 ref. Holds
|
|
56
|
+
* uniforms (fixed-size scalars/vectors/matrices) AND attribute arrays
|
|
57
|
+
* (variable-size). The caller decides `dataBytes` + `length` per
|
|
58
|
+
* acquisition — the pool just keys on aval identity and refcounts.
|
|
59
|
+
*
|
|
60
|
+
* Sharing emerges from aval identity — no separate "frequency"
|
|
61
|
+
* declaration needed. A `cval` shared by all draws → 1 alloc.
|
|
62
|
+
* A static positions array shared across instanced draws → 1 alloc.
|
|
63
|
+
* Same code path either way.
|
|
64
|
+
*/
|
|
65
|
+
export class UniformPool {
|
|
66
|
+
// Keyed by `aval<unknown>` *by reference* (a plain JS `Map`). These
|
|
67
|
+
// keys are overwhelmingly reactive `cval`s (per-object trafos,
|
|
68
|
+
// colours, …) and the hot path is `acquire`/`release` ~once per
|
|
69
|
+
// drawHeader field per RO.
|
|
70
|
+
private readonly byAval = new Map<aval<unknown>, PoolEntry>();
|
|
71
|
+
|
|
72
|
+
has(av: aval<unknown>): boolean { return this.byAval.has(av); }
|
|
73
|
+
entry(av: aval<unknown>): PoolEntry | undefined { return this.byAval.get(av); }
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Acquire (or share) an allocation for `aval`. Caller passes the
|
|
77
|
+
* pre-read `value` (so the pool doesn't need a token) plus the
|
|
78
|
+
* (`dataBytes`, `typeId`, `length`, `pack`) describing how to lay
|
|
79
|
+
* it out. If a new allocation is made, the value is packed and
|
|
80
|
+
* uploaded immediately.
|
|
81
|
+
*/
|
|
82
|
+
acquire(
|
|
83
|
+
device: GPUDevice,
|
|
84
|
+
arena: AttributeArena,
|
|
85
|
+
av: aval<unknown>,
|
|
86
|
+
value: unknown,
|
|
87
|
+
dataBytes: number,
|
|
88
|
+
typeId: number,
|
|
89
|
+
length: number,
|
|
90
|
+
pack: (val: unknown, dst: Float32Array, off: number) => void,
|
|
91
|
+
): number {
|
|
92
|
+
const existing = this.byAval.get(av);
|
|
93
|
+
if (existing !== undefined) {
|
|
94
|
+
existing.refcount++;
|
|
95
|
+
return existing.ref;
|
|
96
|
+
}
|
|
97
|
+
const ref = arena.alloc(dataBytes);
|
|
98
|
+
const allocBytes = ALIGN16(ALLOC_HEADER_PAD_TO + dataBytes);
|
|
99
|
+
const buf = new ArrayBuffer(allocBytes);
|
|
100
|
+
const u32 = new Uint32Array(buf);
|
|
101
|
+
const f32 = new Float32Array(buf);
|
|
102
|
+
u32[0] = typeId;
|
|
103
|
+
u32[1] = length;
|
|
104
|
+
// stride_bytes (offset 8): bytes per element. Lets the VS decode
|
|
105
|
+
// pick V3- vs V4-tight load expressions for vec4 attributes.
|
|
106
|
+
u32[2] = length > 0 ? Math.floor(dataBytes / length) : 0;
|
|
107
|
+
pack(value, f32, ALLOC_HEADER_PAD_TO / 4);
|
|
108
|
+
arena.write(ref, new Uint8Array(buf));
|
|
109
|
+
void device;
|
|
110
|
+
this.byAval.set(av, { ref, dataBytes, typeId, pack, refcount: 1 });
|
|
111
|
+
return ref;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Decrement refcount; if zero, free the arena allocation. */
|
|
115
|
+
release(arena: AttributeArena, av: aval<unknown>): void {
|
|
116
|
+
const e = this.byAval.get(av);
|
|
117
|
+
if (e === undefined) return;
|
|
118
|
+
e.refcount--;
|
|
119
|
+
if (e.refcount > 0) return;
|
|
120
|
+
arena.release(e.ref, ALIGN16(ALLOC_HEADER_PAD_TO + e.dataBytes));
|
|
121
|
+
this.byAval.delete(av);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Re-pack one entry's data region into the arena's CPU shadow. */
|
|
125
|
+
repack(device: GPUDevice, arena: AttributeArena, av: aval<unknown>, val: unknown): void {
|
|
126
|
+
const e = this.byAval.get(av);
|
|
127
|
+
if (e === undefined) return;
|
|
128
|
+
const dst = new Float32Array(e.dataBytes / 4);
|
|
129
|
+
e.pack(val, dst, 0);
|
|
130
|
+
arena.write(
|
|
131
|
+
e.ref + ALLOC_HEADER_PAD_TO,
|
|
132
|
+
new Uint8Array(dst.buffer, dst.byteOffset, e.dataBytes),
|
|
133
|
+
);
|
|
134
|
+
void device;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── IndexPool ─────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Aval-keyed pool over the `IndexAllocator`. Two draws referencing
|
|
142
|
+
* the same `Uint32Array` (or aval thereof) share an index range —
|
|
143
|
+
* 19K instanced clones of the same mesh share one allocation, one
|
|
144
|
+
* upload.
|
|
145
|
+
*
|
|
146
|
+
* **Value-equality dedup for constant avals (§5b):** when an
|
|
147
|
+
* incoming aval has `isConstant === true`, the pool also keys by
|
|
148
|
+
* the underlying `ArrayBuffer` tuple `(buffer, byteOffset,
|
|
149
|
+
* byteLength)`. Two distinct constant avals wrapping the same
|
|
150
|
+
* `Uint32Array` view collapse to one allocation.
|
|
151
|
+
*/
|
|
152
|
+
export class IndexPool {
|
|
153
|
+
private readonly byAval = new Map<
|
|
154
|
+
aval<Uint32Array>,
|
|
155
|
+
{ entry: IndexPoolEntry; perAvalCount: number }
|
|
156
|
+
>();
|
|
157
|
+
private readonly byValueKey = new Map<string, IndexPoolEntry>();
|
|
158
|
+
private readonly bufferIds = new WeakMap<ArrayBufferLike, number>();
|
|
159
|
+
private nextBufferId = 1;
|
|
160
|
+
private bufferIdOf(buf: ArrayBufferLike): number {
|
|
161
|
+
let id = this.bufferIds.get(buf);
|
|
162
|
+
if (id === undefined) {
|
|
163
|
+
id = this.nextBufferId++;
|
|
164
|
+
this.bufferIds.set(buf, id);
|
|
165
|
+
}
|
|
166
|
+
return id;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
acquire(
|
|
170
|
+
device: GPUDevice,
|
|
171
|
+
indices: IndexAllocator,
|
|
172
|
+
av: aval<Uint32Array>,
|
|
173
|
+
arr: Uint32Array,
|
|
174
|
+
): { firstIndex: number; count: number } {
|
|
175
|
+
const bound = this.byAval.get(av);
|
|
176
|
+
if (bound !== undefined) {
|
|
177
|
+
bound.perAvalCount++;
|
|
178
|
+
bound.entry.totalRefcount++;
|
|
179
|
+
return { firstIndex: bound.entry.firstIndex, count: bound.entry.count };
|
|
180
|
+
}
|
|
181
|
+
let valueKey: string | undefined;
|
|
182
|
+
if (av.isConstant) {
|
|
183
|
+
valueKey = `${this.bufferIdOf(arr.buffer)}:${arr.byteOffset}:${arr.byteLength}`;
|
|
184
|
+
const shared = this.byValueKey.get(valueKey);
|
|
185
|
+
if (shared !== undefined) {
|
|
186
|
+
shared.totalRefcount++;
|
|
187
|
+
this.byAval.set(av, { entry: shared, perAvalCount: 1 });
|
|
188
|
+
return { firstIndex: shared.firstIndex, count: shared.count };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const firstIndex = indices.alloc(arr.length);
|
|
192
|
+
indices.write(
|
|
193
|
+
firstIndex * 4,
|
|
194
|
+
new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength),
|
|
195
|
+
);
|
|
196
|
+
void device;
|
|
197
|
+
const entry: IndexPoolEntry = { firstIndex, count: arr.length, totalRefcount: 1, valueKey };
|
|
198
|
+
this.byAval.set(av, { entry, perAvalCount: 1 });
|
|
199
|
+
if (valueKey !== undefined) this.byValueKey.set(valueKey, entry);
|
|
200
|
+
return { firstIndex, count: arr.length };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
release(indices: IndexAllocator, av: aval<Uint32Array>): void {
|
|
204
|
+
const bound = this.byAval.get(av);
|
|
205
|
+
if (bound === undefined) return;
|
|
206
|
+
bound.perAvalCount--;
|
|
207
|
+
bound.entry.totalRefcount--;
|
|
208
|
+
if (bound.perAvalCount === 0) this.byAval.delete(av);
|
|
209
|
+
if (bound.entry.totalRefcount > 0) return;
|
|
210
|
+
indices.release(bound.entry.firstIndex, bound.entry.count);
|
|
211
|
+
if (bound.entry.valueKey !== undefined) {
|
|
212
|
+
this.byValueKey.delete(bound.entry.valueKey);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
interface IndexPoolEntry {
|
|
218
|
+
firstIndex: number;
|
|
219
|
+
count: number;
|
|
220
|
+
totalRefcount: number;
|
|
221
|
+
valueKey: string | undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ─── DrawHeap (slot-indexed) ───────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Slot-indexed allocator over a GrowBuffer. `slotBytes` is set per-
|
|
228
|
+
* instance — each bucket sizes its DrawHeader from its effect's
|
|
229
|
+
* schema, so a bucket whose layout is e.g. 96 B / slot uses a
|
|
230
|
+
* DrawHeap with `slotBytes=96`.
|
|
231
|
+
*/
|
|
232
|
+
export class DrawHeap {
|
|
233
|
+
private free: number[] = [];
|
|
234
|
+
private nextSlot = 0;
|
|
235
|
+
constructor(private readonly buf: GrowBuffer, private readonly slotBytes: number) {}
|
|
236
|
+
get buffer(): GPUBuffer { return this.buf.buffer; }
|
|
237
|
+
/** Bytes per slot — caller multiplies by slot index for byte offsets. */
|
|
238
|
+
get bytesPerSlot(): number { return this.slotBytes; }
|
|
239
|
+
/** High-water mark in bytes (used to size bind-group entry on rebuild). */
|
|
240
|
+
get usedBytes(): number { return this.nextSlot * this.slotBytes; }
|
|
241
|
+
alloc(): number {
|
|
242
|
+
const slot = this.free.length > 0 ? this.free.pop()! : this.nextSlot++;
|
|
243
|
+
this.buf.ensureCapacity((slot + 1) * this.slotBytes);
|
|
244
|
+
this.buf.setUsed(Math.max(this.buf.usedBytes, (slot + 1) * this.slotBytes));
|
|
245
|
+
return slot;
|
|
246
|
+
}
|
|
247
|
+
release(slot: number): void { this.free.push(slot); }
|
|
248
|
+
onResize(cb: () => void): IDisposable { return this.buf.onResize(cb); }
|
|
249
|
+
destroy(): void { this.buf.destroy(); }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ─── AttributeArena (byte-bump) ────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Byte-bump allocator over a GrowBuffer for variable-size attribute
|
|
256
|
+
* allocations. Each allocation gets a 16-byte aligned start (8-byte
|
|
257
|
+
* (typeId, length) header at the start, data 16 bytes in). Frees go
|
|
258
|
+
* onto a sorted free list with coalesce on insert.
|
|
259
|
+
*/
|
|
260
|
+
export class AttributeArena {
|
|
261
|
+
private cursor = 0;
|
|
262
|
+
private freeList: { off: number; size: number }[] = [];
|
|
263
|
+
/** CPU shadow of the entire GPU buffer; writes go here first then
|
|
264
|
+
* flush() emits one writeBuffer per dirty contiguous range. */
|
|
265
|
+
private shadow: Uint8Array;
|
|
266
|
+
private dirtyMin = Infinity;
|
|
267
|
+
private dirtyMax = 0;
|
|
268
|
+
constructor(private readonly buf: GrowBuffer) {
|
|
269
|
+
this.shadow = new Uint8Array(buf.capacity);
|
|
270
|
+
buf.onResize(() => {
|
|
271
|
+
const grown = new Uint8Array(buf.capacity);
|
|
272
|
+
grown.set(this.shadow);
|
|
273
|
+
this.shadow = grown;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
get buffer(): GPUBuffer { return this.buf.buffer; }
|
|
277
|
+
get capacity(): number { return this.buf.capacity; }
|
|
278
|
+
get usedBytes(): number { return this.cursor; }
|
|
279
|
+
write(dst: number, data: Uint8Array): void {
|
|
280
|
+
this.shadow.set(data, dst);
|
|
281
|
+
if (dst < this.dirtyMin) this.dirtyMin = dst;
|
|
282
|
+
const end = dst + data.byteLength;
|
|
283
|
+
if (end > this.dirtyMax) this.dirtyMax = end;
|
|
284
|
+
}
|
|
285
|
+
flush(device: GPUDevice): void {
|
|
286
|
+
if (this.dirtyMax <= this.dirtyMin) return;
|
|
287
|
+
device.queue.writeBuffer(
|
|
288
|
+
this.buf.buffer, this.dirtyMin,
|
|
289
|
+
this.shadow.buffer, this.shadow.byteOffset + this.dirtyMin,
|
|
290
|
+
this.dirtyMax - this.dirtyMin,
|
|
291
|
+
);
|
|
292
|
+
this.dirtyMin = Infinity;
|
|
293
|
+
this.dirtyMax = 0;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Allocate space for one attribute. Returns the byte ref (offset
|
|
297
|
+
* to the header — data lives at ref + 16).
|
|
298
|
+
*/
|
|
299
|
+
alloc(dataBytes: number): number {
|
|
300
|
+
const allocBytes = ALIGN16(ALLOC_HEADER_PAD_TO + dataBytes);
|
|
301
|
+
// First-fit reuse from free list.
|
|
302
|
+
for (let i = 0; i < this.freeList.length; i++) {
|
|
303
|
+
const f = this.freeList[i]!;
|
|
304
|
+
if (f.size >= allocBytes) {
|
|
305
|
+
const ref = f.off;
|
|
306
|
+
if (f.size === allocBytes) this.freeList.splice(i, 1);
|
|
307
|
+
else { f.off += allocBytes; f.size -= allocBytes; }
|
|
308
|
+
return ref;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const ref = this.cursor;
|
|
312
|
+
this.cursor += allocBytes;
|
|
313
|
+
this.buf.ensureCapacity(this.cursor);
|
|
314
|
+
this.buf.setUsed(this.cursor);
|
|
315
|
+
return ref;
|
|
316
|
+
}
|
|
317
|
+
release(ref: number, dataBytes: number): void {
|
|
318
|
+
const allocBytes = ALIGN16(ALLOC_HEADER_PAD_TO + dataBytes);
|
|
319
|
+
insertSortedFreeBlock(this.freeList, ref, allocBytes);
|
|
320
|
+
}
|
|
321
|
+
onResize(cb: () => void): IDisposable { return this.buf.onResize(cb); }
|
|
322
|
+
destroy(): void { this.buf.destroy(); }
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Insert `{off, size}` into a free-list kept sorted by `off`, then
|
|
327
|
+
* coalesce with the two immediate neighbours.
|
|
328
|
+
*/
|
|
329
|
+
export function insertSortedFreeBlock(
|
|
330
|
+
freeList: { off: number; size: number }[],
|
|
331
|
+
off: number,
|
|
332
|
+
size: number,
|
|
333
|
+
): void {
|
|
334
|
+
let lo = 0, hi = freeList.length;
|
|
335
|
+
while (lo < hi) {
|
|
336
|
+
const mid = (lo + hi) >>> 1;
|
|
337
|
+
if (freeList[mid]!.off <= off) lo = mid + 1;
|
|
338
|
+
else hi = mid;
|
|
339
|
+
}
|
|
340
|
+
const prev = lo > 0 ? freeList[lo - 1] : undefined;
|
|
341
|
+
if (prev !== undefined && prev.off + prev.size === off) {
|
|
342
|
+
prev.size += size;
|
|
343
|
+
const next = freeList[lo];
|
|
344
|
+
if (next !== undefined && prev.off + prev.size === next.off) {
|
|
345
|
+
prev.size += next.size;
|
|
346
|
+
freeList.splice(lo, 1);
|
|
347
|
+
}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const next = freeList[lo];
|
|
351
|
+
if (next !== undefined && off + size === next.off) {
|
|
352
|
+
next.off = off;
|
|
353
|
+
next.size += size;
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
freeList.splice(lo, 0, { off, size });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ─── IndexAllocator (element-bump) ─────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Element-bump allocator over an index GrowBuffer (units = u32). Each
|
|
363
|
+
* draw's index range is allocated as one block; on release the block
|
|
364
|
+
* is returned to a free list and can be reused first-fit.
|
|
365
|
+
*/
|
|
366
|
+
export class IndexAllocator {
|
|
367
|
+
private cursor = 0; // in u32s, not bytes
|
|
368
|
+
private freeList: { off: number; size: number }[] = [];
|
|
369
|
+
private shadow: Uint8Array;
|
|
370
|
+
private dirtyMin = Infinity;
|
|
371
|
+
private dirtyMax = 0;
|
|
372
|
+
constructor(private readonly buf: GrowBuffer) {
|
|
373
|
+
this.shadow = new Uint8Array(buf.capacity);
|
|
374
|
+
buf.onResize(() => {
|
|
375
|
+
const grown = new Uint8Array(buf.capacity);
|
|
376
|
+
grown.set(this.shadow);
|
|
377
|
+
this.shadow = grown;
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
get buffer(): GPUBuffer { return this.buf.buffer; }
|
|
381
|
+
get usedElements(): number { return this.cursor; }
|
|
382
|
+
write(dstByteOffset: number, data: Uint8Array): void {
|
|
383
|
+
this.shadow.set(data, dstByteOffset);
|
|
384
|
+
if (dstByteOffset < this.dirtyMin) this.dirtyMin = dstByteOffset;
|
|
385
|
+
const end = dstByteOffset + data.byteLength;
|
|
386
|
+
if (end > this.dirtyMax) this.dirtyMax = end;
|
|
387
|
+
}
|
|
388
|
+
flush(device: GPUDevice): void {
|
|
389
|
+
if (this.dirtyMax <= this.dirtyMin) return;
|
|
390
|
+
device.queue.writeBuffer(
|
|
391
|
+
this.buf.buffer, this.dirtyMin,
|
|
392
|
+
this.shadow.buffer, this.shadow.byteOffset + this.dirtyMin,
|
|
393
|
+
this.dirtyMax - this.dirtyMin,
|
|
394
|
+
);
|
|
395
|
+
this.dirtyMin = Infinity;
|
|
396
|
+
this.dirtyMax = 0;
|
|
397
|
+
}
|
|
398
|
+
alloc(elements: number): number {
|
|
399
|
+
for (let i = 0; i < this.freeList.length; i++) {
|
|
400
|
+
const f = this.freeList[i]!;
|
|
401
|
+
if (f.size >= elements) {
|
|
402
|
+
const off = f.off;
|
|
403
|
+
if (f.size === elements) this.freeList.splice(i, 1);
|
|
404
|
+
else { f.off += elements; f.size -= elements; }
|
|
405
|
+
return off;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const off = this.cursor;
|
|
409
|
+
this.cursor += elements;
|
|
410
|
+
this.buf.ensureCapacity(this.cursor * 4);
|
|
411
|
+
this.buf.setUsed(this.cursor * 4);
|
|
412
|
+
return off;
|
|
413
|
+
}
|
|
414
|
+
release(off: number, elements: number): void {
|
|
415
|
+
insertSortedFreeBlock(this.freeList, off, elements);
|
|
416
|
+
}
|
|
417
|
+
onResize(cb: () => void): IDisposable { return this.buf.onResize(cb); }
|
|
418
|
+
destroy(): void { this.buf.destroy(); }
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ─── ArenaState + helpers ──────────────────────────────────────────────
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Global arena state: attribute / uniform data lives in `attrs`
|
|
425
|
+
* (multi-typed-view storage); indices live in `indices` (separate
|
|
426
|
+
* INDEX-usage buffer).
|
|
427
|
+
*/
|
|
428
|
+
export interface ArenaState {
|
|
429
|
+
readonly attrs: AttributeArena;
|
|
430
|
+
readonly indices: IndexAllocator;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function buildArenaState(
|
|
434
|
+
device: GPUDevice,
|
|
435
|
+
attrBytesHint: number,
|
|
436
|
+
idxBytesHint: number,
|
|
437
|
+
label: string,
|
|
438
|
+
idxExtraUsage: GPUBufferUsageFlags = 0,
|
|
439
|
+
): ArenaState {
|
|
440
|
+
const attrs = new AttributeArena(new GrowBuffer(
|
|
441
|
+
device, `${label}/attrs`, GPUBufferUsage.STORAGE,
|
|
442
|
+
attrBytesHint,
|
|
443
|
+
));
|
|
444
|
+
const indices = new IndexAllocator(new GrowBuffer(
|
|
445
|
+
device, `${label}/idx`, GPUBufferUsage.INDEX | idxExtraUsage,
|
|
446
|
+
idxBytesHint,
|
|
447
|
+
));
|
|
448
|
+
return { attrs, indices };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function arenaBytes(arena: ArenaState): number {
|
|
452
|
+
return arena.attrs.usedBytes + arena.indices.usedElements * 4;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Upload a single attribute — header (typeId, length) + data — into the arena at byte offset `ref`. */
|
|
456
|
+
export function writeAttribute(
|
|
457
|
+
device: GPUDevice, buf: GPUBuffer, ref: number, typeId: number, length: number, data: Float32Array,
|
|
458
|
+
): void {
|
|
459
|
+
const allocBytes = ALIGN16(ALLOC_HEADER_PAD_TO + data.byteLength);
|
|
460
|
+
const staging = new ArrayBuffer(allocBytes);
|
|
461
|
+
const u32 = new Uint32Array(staging);
|
|
462
|
+
const f32 = new Float32Array(staging);
|
|
463
|
+
u32[0] = typeId;
|
|
464
|
+
u32[1] = length;
|
|
465
|
+
f32.set(data, ALLOC_HEADER_PAD_TO / 4);
|
|
466
|
+
device.queue.writeBuffer(buf, ref, staging, 0, allocBytes);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function asAval<T>(v: aval<T> | T): aval<T> {
|
|
470
|
+
return (typeof v === "object" && v !== null && typeof (v as { getValue?: unknown }).getValue === "function")
|
|
471
|
+
? (v as aval<T>)
|
|
472
|
+
: AVal.constant(v as T);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Heuristic predicate — BufferView has `buffer: aval<IBuffer>` + elementType. */
|
|
476
|
+
export function isBufferView(v: unknown): v is BufferView {
|
|
477
|
+
if (typeof v !== "object" || v === null) return false;
|
|
478
|
+
const o = v as { buffer?: unknown; elementType?: unknown };
|
|
479
|
+
return typeof o.buffer === "object" && o.buffer !== null
|
|
480
|
+
&& typeof (o.buffer as { getValue?: unknown }).getValue === "function"
|
|
481
|
+
&& typeof o.elementType === "object" && o.elementType !== null;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Float32 view over a host-side buffer source. Used by the BufferView
|
|
485
|
+
* packer to hand the pool a typed array it can `set()` from. */
|
|
486
|
+
export function asFloat32(data: HostBufferSource): Float32Array {
|
|
487
|
+
if (data instanceof Float32Array) return data;
|
|
488
|
+
if (ArrayBuffer.isView(data)) {
|
|
489
|
+
return new Float32Array(data.buffer, data.byteOffset, data.byteLength / 4);
|
|
490
|
+
}
|
|
491
|
+
return new Float32Array(data); // ArrayBuffer
|
|
492
|
+
}
|