archetype-ecs 1.4.2 → 1.5.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/README.md +79 -14
- package/bench/allocations-1m.js +1 -1
- package/bench/component-churn-bench.js +193 -0
- package/bench/iterate.wat +95 -0
- package/bench/multi-ecs-bench.js +1 -1
- package/bench/run-js-vs-go-ts.sh +147 -0
- package/bench/typed-vs-bitecs-1m.js +1 -1
- package/bench/typed-vs-untyped.js +1 -1
- package/bench/vs-bitecs.js +1 -1
- package/bench/wasm-iteration-bench.js +289 -0
- package/dist/{src/ComponentRegistry.d.ts → ComponentRegistry.d.ts} +8 -3
- package/dist/{src/EntityManager.d.ts → EntityManager.d.ts} +15 -10
- package/dist/{src/EntityManager.js → EntityManager.js} +196 -95
- package/dist/{src/System.d.ts → System.d.ts} +4 -0
- package/dist/{src/System.js → System.js} +25 -5
- package/dist/WasmArena.d.ts +13 -0
- package/dist/WasmArena.js +48 -0
- package/dist/{src/index.d.ts → index.d.ts} +7 -9
- package/dist/{src/index.js → index.js} +2 -0
- package/dist/wasm-kernels.d.ts +10 -0
- package/dist/wasm-kernels.js +59 -0
- package/package.json +12 -7
- package/src/ComponentRegistry.ts +7 -3
- package/src/EntityManager.ts +209 -119
- package/src/System.ts +34 -9
- package/src/WasmArena.ts +83 -0
- package/src/index.ts +16 -11
- package/src/iterate.wat +135 -0
- package/src/wasm-kernels.ts +68 -0
- package/tests/EntityManager.test.ts +51 -86
- package/tests/System.test.ts +184 -0
- package/tests/types.ts +1 -1
- package/tsconfig.json +2 -2
- package/tsconfig.test.json +13 -0
- package/dist/tests/EntityManager.test.d.ts +0 -1
- package/dist/tests/EntityManager.test.js +0 -651
- package/dist/tests/System.test.d.ts +0 -1
- package/dist/tests/System.test.js +0 -630
- package/dist/tests/types.d.ts +0 -1
- package/dist/tests/types.js +0 -129
- /package/dist/{src/ComponentRegistry.js → ComponentRegistry.js} +0 -0
- /package/dist/{src/Profiler.d.ts → Profiler.d.ts} +0 -0
- /package/dist/{src/Profiler.js → Profiler.js} +0 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { TYPED, componentSchemas, toSym } from './ComponentRegistry.js';
|
|
2
|
+
import { WasmArena } from './WasmArena.js';
|
|
3
|
+
import { instantiateKernelsSync, isWasmSimdAvailable } from './wasm-kernels.js';
|
|
2
4
|
const INITIAL_CAPACITY = 64;
|
|
3
5
|
// ── Array-based bitmask helpers ──────────────────────────
|
|
4
6
|
function slotsNeeded(bitCount) {
|
|
@@ -57,84 +59,101 @@ function unpackSpec(spec) {
|
|
|
57
59
|
return spec;
|
|
58
60
|
return [spec, 0];
|
|
59
61
|
}
|
|
60
|
-
function createSoAStore(schema, capacity) {
|
|
61
|
-
const
|
|
62
|
+
function createSoAStore(schema, capacity, arena) {
|
|
63
|
+
const fields = {};
|
|
64
|
+
const arraySizes = {};
|
|
62
65
|
for (const [field, spec] of Object.entries(schema)) {
|
|
63
66
|
const [Ctor, size] = unpackSpec(spec);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
const count = size > 0 ? capacity * size : capacity;
|
|
68
|
+
if (size > 0)
|
|
69
|
+
arraySizes[field] = size;
|
|
70
|
+
if (arena && Ctor !== Array) {
|
|
71
|
+
const NumCtor = Ctor;
|
|
72
|
+
const offset = arena.alloc(count * NumCtor.BYTES_PER_ELEMENT);
|
|
73
|
+
arena.createView(NumCtor, offset, count, fields, field);
|
|
67
74
|
}
|
|
68
75
|
else {
|
|
69
|
-
|
|
76
|
+
fields[field] = new Ctor(count);
|
|
70
77
|
}
|
|
71
78
|
}
|
|
72
|
-
return
|
|
79
|
+
return { [TYPED]: true, _schema: schema, _capacity: capacity, _arraySizes: arraySizes, _fields: fields };
|
|
73
80
|
}
|
|
74
|
-
function growSoAStore(store, newCapacity) {
|
|
81
|
+
function growSoAStore(store, newCapacity, arena) {
|
|
75
82
|
store._capacity = newCapacity;
|
|
76
83
|
for (const [field, spec] of Object.entries(store._schema)) {
|
|
77
84
|
const [Ctor, size] = unpackSpec(spec);
|
|
78
|
-
const old = store[field];
|
|
85
|
+
const old = store._fields[field];
|
|
79
86
|
const allocSize = size > 0 ? newCapacity * size : newCapacity;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
if (arena && Ctor !== Array) {
|
|
88
|
+
const NumCtor = Ctor;
|
|
89
|
+
const offset = arena.alloc(allocSize * NumCtor.BYTES_PER_ELEMENT);
|
|
90
|
+
arena.updateView(store._fields, field, offset, NumCtor, allocSize);
|
|
91
|
+
store._fields[field].set(old);
|
|
84
92
|
}
|
|
85
93
|
else {
|
|
86
|
-
|
|
94
|
+
const grown = new Ctor(allocSize);
|
|
95
|
+
if (Ctor === Array) {
|
|
96
|
+
for (let i = 0; i < old.length; i++)
|
|
97
|
+
grown[i] = old[i];
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
grown.set(old);
|
|
101
|
+
}
|
|
102
|
+
store._fields[field] = grown;
|
|
87
103
|
}
|
|
88
104
|
}
|
|
89
105
|
}
|
|
90
106
|
function soaWrite(store, idx, data) {
|
|
91
107
|
if (!data) {
|
|
92
108
|
for (const field in store._schema) {
|
|
109
|
+
const arr = store._fields[field];
|
|
93
110
|
const size = store._arraySizes[field] || 0;
|
|
94
111
|
if (size > 0) {
|
|
95
112
|
const base = idx * size;
|
|
96
113
|
for (let j = 0; j < size; j++)
|
|
97
|
-
|
|
114
|
+
arr[base + j] = 0;
|
|
98
115
|
}
|
|
99
116
|
else {
|
|
100
|
-
|
|
117
|
+
arr[idx] = 0;
|
|
101
118
|
}
|
|
102
119
|
}
|
|
103
120
|
return;
|
|
104
121
|
}
|
|
105
122
|
for (const field in store._schema) {
|
|
123
|
+
const arr = store._fields[field];
|
|
106
124
|
const size = store._arraySizes[field] || 0;
|
|
107
125
|
if (size > 0) {
|
|
108
126
|
const base = idx * size;
|
|
109
127
|
const src = data[field];
|
|
110
128
|
if (src) {
|
|
111
129
|
for (let j = 0; j < size; j++) {
|
|
112
|
-
|
|
130
|
+
arr[base + j] = (src[j] ?? 0);
|
|
113
131
|
}
|
|
114
132
|
}
|
|
115
133
|
}
|
|
116
134
|
else {
|
|
117
|
-
|
|
135
|
+
arr[idx] = data[field];
|
|
118
136
|
}
|
|
119
137
|
}
|
|
120
138
|
}
|
|
121
139
|
function soaRead(store, idx) {
|
|
122
140
|
const obj = {};
|
|
123
141
|
for (const field in store._schema) {
|
|
142
|
+
const arr = store._fields[field];
|
|
124
143
|
const size = store._arraySizes[field] || 0;
|
|
125
144
|
if (size > 0) {
|
|
126
145
|
const base = idx * size;
|
|
127
|
-
obj[field] = Array.from(
|
|
146
|
+
obj[field] = Array.from(arr.subarray(base, base + size));
|
|
128
147
|
}
|
|
129
148
|
else {
|
|
130
|
-
obj[field] =
|
|
149
|
+
obj[field] = arr[idx];
|
|
131
150
|
}
|
|
132
151
|
}
|
|
133
152
|
return obj;
|
|
134
153
|
}
|
|
135
154
|
function soaSwap(store, idxA, idxB) {
|
|
136
155
|
for (const field in store._schema) {
|
|
137
|
-
const arr = store[field];
|
|
156
|
+
const arr = store._fields[field];
|
|
138
157
|
const size = store._arraySizes[field] || 0;
|
|
139
158
|
if (size > 0) {
|
|
140
159
|
const baseA = idxA * size;
|
|
@@ -152,29 +171,51 @@ function soaSwap(store, idxA, idxB) {
|
|
|
152
171
|
}
|
|
153
172
|
}
|
|
154
173
|
}
|
|
155
|
-
function createSnapshotStore(schema, capacity) {
|
|
174
|
+
function createSnapshotStore(schema, capacity, arena) {
|
|
156
175
|
const snap = {};
|
|
157
176
|
for (const [field, spec] of Object.entries(schema)) {
|
|
158
177
|
const [Ctor, size] = unpackSpec(spec);
|
|
159
|
-
|
|
178
|
+
const count = size > 0 ? capacity * size : capacity;
|
|
179
|
+
if (arena && Ctor !== Array) {
|
|
180
|
+
const NumCtor = Ctor;
|
|
181
|
+
const offset = arena.alloc(count * NumCtor.BYTES_PER_ELEMENT);
|
|
182
|
+
arena.createView(NumCtor, offset, count, snap, field);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
snap[field] = new Ctor(count);
|
|
186
|
+
}
|
|
160
187
|
}
|
|
161
188
|
return snap;
|
|
162
189
|
}
|
|
163
|
-
function growSnapshotStore(snap, schema, newCapacity) {
|
|
190
|
+
function growSnapshotStore(snap, schema, newCapacity, arena) {
|
|
164
191
|
for (const [field, spec] of Object.entries(schema)) {
|
|
165
192
|
const [Ctor, size] = unpackSpec(spec);
|
|
166
193
|
const old = snap[field];
|
|
167
|
-
|
|
168
|
-
if (Ctor
|
|
169
|
-
|
|
170
|
-
|
|
194
|
+
const allocSize = size > 0 ? newCapacity * size : newCapacity;
|
|
195
|
+
if (arena && Ctor !== Array) {
|
|
196
|
+
const NumCtor = Ctor;
|
|
197
|
+
const offset = arena.alloc(allocSize * NumCtor.BYTES_PER_ELEMENT);
|
|
198
|
+
arena.updateView(snap, field, offset, NumCtor, allocSize);
|
|
199
|
+
snap[field].set(old);
|
|
171
200
|
}
|
|
172
201
|
else {
|
|
173
|
-
|
|
202
|
+
const grown = new Ctor(allocSize);
|
|
203
|
+
if (Ctor === Array) {
|
|
204
|
+
for (let i = 0; i < old.length; i++)
|
|
205
|
+
grown[i] = old[i];
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
grown.set(old);
|
|
209
|
+
}
|
|
210
|
+
snap[field] = grown;
|
|
174
211
|
}
|
|
175
212
|
}
|
|
176
213
|
}
|
|
177
|
-
|
|
214
|
+
// ── Entity Manager ───────────────────────────────────────
|
|
215
|
+
export function createEntityManager(options) {
|
|
216
|
+
const useWasm = options?.wasm ?? isWasmSimdAvailable();
|
|
217
|
+
const arena = useWasm ? new WasmArena() : undefined;
|
|
218
|
+
const kernels = arena ? instantiateKernelsSync(arena.memory) : null;
|
|
178
219
|
let nextId = 1;
|
|
179
220
|
let nextArchId = 1;
|
|
180
221
|
const allEntityIds = new Set();
|
|
@@ -183,7 +224,7 @@ export function createEntityManager() {
|
|
|
183
224
|
let destroyedSet = null;
|
|
184
225
|
const trackedArchetypes = [];
|
|
185
226
|
let hooks = null;
|
|
186
|
-
|
|
227
|
+
const removedData = new Map();
|
|
187
228
|
let iterating = 0;
|
|
188
229
|
const deferred = [];
|
|
189
230
|
const componentBitIndex = new Map();
|
|
@@ -230,10 +271,10 @@ export function createEntityManager() {
|
|
|
230
271
|
};
|
|
231
272
|
for (const t of types) {
|
|
232
273
|
const schema = componentSchemas.get(t);
|
|
233
|
-
const store = schema ? createSoAStore(schema, INITIAL_CAPACITY) : null;
|
|
274
|
+
const store = schema ? createSoAStore(schema, INITIAL_CAPACITY, arena) : null;
|
|
234
275
|
arch.components.set(t, store);
|
|
235
276
|
if (tracked && store) {
|
|
236
|
-
arch.snapshots.set(t, createSnapshotStore(schema, INITIAL_CAPACITY));
|
|
277
|
+
arch.snapshots.set(t, createSnapshotStore(schema, INITIAL_CAPACITY, arena));
|
|
237
278
|
}
|
|
238
279
|
}
|
|
239
280
|
archetypes.set(key, arch);
|
|
@@ -250,11 +291,11 @@ export function createEntityManager() {
|
|
|
250
291
|
arch.capacity = newCap;
|
|
251
292
|
for (const [type, store] of arch.components) {
|
|
252
293
|
if (store) {
|
|
253
|
-
growSoAStore(store, newCap);
|
|
294
|
+
growSoAStore(store, newCap, arena);
|
|
254
295
|
if (arch.snapshots) {
|
|
255
296
|
const snap = arch.snapshots.get(type);
|
|
256
297
|
if (snap)
|
|
257
|
-
growSnapshotStore(snap, store._schema, newCap);
|
|
298
|
+
growSnapshotStore(snap, store._schema, newCap, arena);
|
|
258
299
|
}
|
|
259
300
|
}
|
|
260
301
|
}
|
|
@@ -266,7 +307,7 @@ export function createEntityManager() {
|
|
|
266
307
|
for (const t of arch.types) {
|
|
267
308
|
const store = arch.components.get(t);
|
|
268
309
|
if (store)
|
|
269
|
-
soaWrite(store, idx, componentMap
|
|
310
|
+
soaWrite(store, idx, componentMap.get(t));
|
|
270
311
|
}
|
|
271
312
|
arch.entityToIndex.set(entityId, idx);
|
|
272
313
|
arch.count++;
|
|
@@ -313,7 +354,6 @@ export function createEntityManager() {
|
|
|
313
354
|
queryCache.set(queryStr, { version: queryCacheVersion, archetypes: matching });
|
|
314
355
|
return matching;
|
|
315
356
|
}
|
|
316
|
-
// ── Internal structural change implementations ──────────
|
|
317
357
|
function doDestroyEntity(id) {
|
|
318
358
|
const arch = entityArchetype.get(id);
|
|
319
359
|
if (arch) {
|
|
@@ -323,6 +363,22 @@ export function createEntityManager() {
|
|
|
323
363
|
if (pending)
|
|
324
364
|
pending.push(id);
|
|
325
365
|
}
|
|
366
|
+
if (hooks.removeCbs.size > 0) {
|
|
367
|
+
const idx = arch.entityToIndex.get(id);
|
|
368
|
+
let entitySnap;
|
|
369
|
+
for (const type of arch.types) {
|
|
370
|
+
if (hooks.removeCbs.has(type)) {
|
|
371
|
+
const store = arch.components.get(type);
|
|
372
|
+
if (store) {
|
|
373
|
+
if (!entitySnap) {
|
|
374
|
+
entitySnap = new Map();
|
|
375
|
+
removedData.set(id, entitySnap);
|
|
376
|
+
}
|
|
377
|
+
entitySnap.set(type, soaRead(store, idx));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
326
382
|
}
|
|
327
383
|
if (destroyedSet && trackFilter && maskOverlaps(arch.key, trackFilter))
|
|
328
384
|
destroyedSet.add(id);
|
|
@@ -335,7 +391,7 @@ export function createEntityManager() {
|
|
|
335
391
|
const arch = entityArchetype.get(entityId);
|
|
336
392
|
if (!arch) {
|
|
337
393
|
const newArch = getOrCreateArchetype([type]);
|
|
338
|
-
addToArchetype(newArch, entityId,
|
|
394
|
+
addToArchetype(newArch, entityId, new Map([[type, data]]));
|
|
339
395
|
if (hooks) {
|
|
340
396
|
const pending = hooks.pendingAdd.get(type);
|
|
341
397
|
if (pending)
|
|
@@ -354,9 +410,9 @@ export function createEntityManager() {
|
|
|
354
410
|
const newTypes = [...arch.types, type];
|
|
355
411
|
const newArch = getOrCreateArchetype(newTypes);
|
|
356
412
|
const idx = arch.entityToIndex.get(entityId);
|
|
357
|
-
const map =
|
|
413
|
+
const map = new Map([[type, data]]);
|
|
358
414
|
for (const t of arch.types) {
|
|
359
|
-
map
|
|
415
|
+
map.set(t, readComponentData(arch, t, idx));
|
|
360
416
|
}
|
|
361
417
|
removeFromArchetype(arch, entityId);
|
|
362
418
|
addToArchetype(newArch, entityId, map);
|
|
@@ -375,6 +431,15 @@ export function createEntityManager() {
|
|
|
375
431
|
const pending = hooks.pendingRemove.get(type);
|
|
376
432
|
if (pending)
|
|
377
433
|
pending.push(entityId);
|
|
434
|
+
if (hooks.removeCbs.has(type)) {
|
|
435
|
+
const store = arch.components.get(type);
|
|
436
|
+
if (store) {
|
|
437
|
+
const idx = arch.entityToIndex.get(entityId);
|
|
438
|
+
if (!removedData.has(entityId))
|
|
439
|
+
removedData.set(entityId, new Map());
|
|
440
|
+
removedData.get(entityId).set(type, soaRead(store, idx));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
378
443
|
}
|
|
379
444
|
if (destroyedSet && trackFilter && maskOverlaps(arch.key, trackFilter))
|
|
380
445
|
destroyedSet.add(entityId);
|
|
@@ -389,35 +454,31 @@ export function createEntityManager() {
|
|
|
389
454
|
}
|
|
390
455
|
const newArch = getOrCreateArchetype(newTypes);
|
|
391
456
|
const idx = arch.entityToIndex.get(entityId);
|
|
392
|
-
const map =
|
|
457
|
+
const map = new Map();
|
|
393
458
|
for (const t of newTypes) {
|
|
394
|
-
map
|
|
459
|
+
map.set(t, readComponentData(arch, t, idx));
|
|
395
460
|
}
|
|
396
461
|
removeFromArchetype(arch, entityId);
|
|
397
462
|
addToArchetype(newArch, entityId, map);
|
|
398
463
|
}
|
|
399
464
|
function flushDeferred() {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
break;
|
|
413
|
-
case 'destroy':
|
|
414
|
-
doDestroyEntity(op.entityId);
|
|
415
|
-
break;
|
|
416
|
-
}
|
|
465
|
+
const ops = deferred.splice(0);
|
|
466
|
+
for (const op of ops) {
|
|
467
|
+
switch (op.kind) {
|
|
468
|
+
case 'add':
|
|
469
|
+
doAddComponent(op.entityId, op.comp, op.data);
|
|
470
|
+
break;
|
|
471
|
+
case 'remove':
|
|
472
|
+
doRemoveComponent(op.entityId, op.comp);
|
|
473
|
+
break;
|
|
474
|
+
case 'destroy':
|
|
475
|
+
doDestroyEntity(op.entityId);
|
|
476
|
+
break;
|
|
417
477
|
}
|
|
418
478
|
}
|
|
419
479
|
}
|
|
420
480
|
return {
|
|
481
|
+
wasmMemory: arena ? arena.memory : null,
|
|
421
482
|
createEntity() {
|
|
422
483
|
const id = nextId++;
|
|
423
484
|
allEntityIds.add(id);
|
|
@@ -459,27 +520,39 @@ export function createEntityManager() {
|
|
|
459
520
|
getComponent(entityId, comp) {
|
|
460
521
|
const type = toSym(comp);
|
|
461
522
|
const arch = entityArchetype.get(entityId);
|
|
462
|
-
if (
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
523
|
+
if (arch) {
|
|
524
|
+
const idx = arch.entityToIndex.get(entityId);
|
|
525
|
+
if (idx !== undefined)
|
|
526
|
+
return readComponentData(arch, type, idx);
|
|
527
|
+
}
|
|
528
|
+
// Fallback: check recently-removed data (accessible during @OnRemoved hooks)
|
|
529
|
+
const removed = removedData.get(entityId);
|
|
530
|
+
if (removed)
|
|
531
|
+
return removed.get(type);
|
|
532
|
+
return undefined;
|
|
468
533
|
},
|
|
469
534
|
get(entityId, fieldRef) {
|
|
470
535
|
const arch = entityArchetype.get(entityId);
|
|
471
|
-
if (
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
536
|
+
if (arch) {
|
|
537
|
+
const store = arch.components.get(fieldRef._sym);
|
|
538
|
+
if (store) {
|
|
539
|
+
const idx = arch.entityToIndex.get(entityId);
|
|
540
|
+
const size = store._arraySizes[fieldRef._field] || 0;
|
|
541
|
+
if (size > 0) {
|
|
542
|
+
const base = idx * size;
|
|
543
|
+
return store._fields[fieldRef._field].subarray(base, base + size);
|
|
544
|
+
}
|
|
545
|
+
return store._fields[fieldRef._field][idx];
|
|
546
|
+
}
|
|
481
547
|
}
|
|
482
|
-
|
|
548
|
+
// Fallback: check recently-removed data (accessible during @OnRemoved hooks)
|
|
549
|
+
const removed = removedData.get(entityId);
|
|
550
|
+
if (removed) {
|
|
551
|
+
const compData = removed.get(fieldRef._sym);
|
|
552
|
+
if (compData)
|
|
553
|
+
return compData[fieldRef._field];
|
|
554
|
+
}
|
|
555
|
+
return undefined;
|
|
483
556
|
},
|
|
484
557
|
set(entityId, fieldRef, value) {
|
|
485
558
|
const arch = entityArchetype.get(entityId);
|
|
@@ -491,10 +564,10 @@ export function createEntityManager() {
|
|
|
491
564
|
const idx = arch.entityToIndex.get(entityId);
|
|
492
565
|
const size = store._arraySizes[fieldRef._field] || 0;
|
|
493
566
|
if (size > 0) {
|
|
494
|
-
store[fieldRef._field].set(value, idx * size);
|
|
567
|
+
store._fields[fieldRef._field].set(value, idx * size);
|
|
495
568
|
}
|
|
496
569
|
else {
|
|
497
|
-
store[fieldRef._field][idx] = value;
|
|
570
|
+
store._fields[fieldRef._field][idx] = value;
|
|
498
571
|
}
|
|
499
572
|
},
|
|
500
573
|
hasComponent(entityId, comp) {
|
|
@@ -521,11 +594,11 @@ export function createEntityManager() {
|
|
|
521
594
|
const id = nextId++;
|
|
522
595
|
allEntityIds.add(id);
|
|
523
596
|
const types = [];
|
|
524
|
-
const map =
|
|
597
|
+
const map = new Map();
|
|
525
598
|
for (let i = 0; i < args.length; i += 2) {
|
|
526
599
|
const sym = toSym(args[i]);
|
|
527
600
|
types.push(sym);
|
|
528
|
-
map
|
|
601
|
+
map.set(sym, args[i + 1]);
|
|
529
602
|
}
|
|
530
603
|
const arch = getOrCreateArchetype(types);
|
|
531
604
|
addToArchetype(arch, id, map);
|
|
@@ -564,24 +637,49 @@ export function createEntityManager() {
|
|
|
564
637
|
snapshotEntityIds: arch.snapshotEntityIds,
|
|
565
638
|
snapshotCount: arch.snapshotCount,
|
|
566
639
|
field(ref) {
|
|
567
|
-
const
|
|
568
|
-
const store = arch.components.get(sym);
|
|
640
|
+
const store = arch.components.get(ref._sym);
|
|
569
641
|
if (!store)
|
|
570
642
|
return undefined;
|
|
571
|
-
return store[ref._field];
|
|
643
|
+
return store._fields[ref._field];
|
|
572
644
|
},
|
|
573
645
|
fieldStride(ref) {
|
|
574
|
-
const
|
|
575
|
-
const store = arch.components.get(sym);
|
|
646
|
+
const store = arch.components.get(ref._sym);
|
|
576
647
|
if (!store)
|
|
577
648
|
return 1;
|
|
578
649
|
return store._arraySizes[ref._field] || 1;
|
|
579
650
|
},
|
|
651
|
+
fieldOffset(ref) {
|
|
652
|
+
if (!arena)
|
|
653
|
+
return -1;
|
|
654
|
+
const store = arch.components.get(ref._sym);
|
|
655
|
+
if (!store)
|
|
656
|
+
return -1;
|
|
657
|
+
const arr = store._fields[ref._field];
|
|
658
|
+
if (!arr || arr instanceof Array)
|
|
659
|
+
return -1;
|
|
660
|
+
return arr.byteOffset;
|
|
661
|
+
},
|
|
662
|
+
fieldAdd(target, source) {
|
|
663
|
+
const tStore = arch.components.get(target._sym);
|
|
664
|
+
const sStore = arch.components.get(source._sym);
|
|
665
|
+
if (!tStore || !sStore)
|
|
666
|
+
return;
|
|
667
|
+
const dst = tStore._fields[target._field];
|
|
668
|
+
const src = sStore._fields[source._field];
|
|
669
|
+
const stride = tStore._arraySizes[target._field] || 1;
|
|
670
|
+
const n = arch.count * stride;
|
|
671
|
+
if (kernels && dst instanceof Float32Array && src instanceof Float32Array) {
|
|
672
|
+
kernels.add_f32(dst.byteOffset, src.byteOffset, n);
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
for (let i = 0; i < n; i++)
|
|
676
|
+
dst[i] = (dst[i] + src[i]);
|
|
677
|
+
}
|
|
678
|
+
},
|
|
580
679
|
snapshot(ref) {
|
|
581
680
|
if (!snaps)
|
|
582
681
|
return undefined;
|
|
583
|
-
const
|
|
584
|
-
const snap = snaps.get(sym);
|
|
682
|
+
const snap = snaps.get(ref._sym);
|
|
585
683
|
if (!snap)
|
|
586
684
|
return undefined;
|
|
587
685
|
return snap[ref._field];
|
|
@@ -611,7 +709,7 @@ export function createEntityManager() {
|
|
|
611
709
|
arch.snapshotCount = 0;
|
|
612
710
|
for (const [t, store] of arch.components) {
|
|
613
711
|
if (store) {
|
|
614
|
-
arch.snapshots.set(t, createSnapshotStore(store._schema, arch.capacity));
|
|
712
|
+
arch.snapshots.set(t, createSnapshotStore(store._schema, arch.capacity, arena));
|
|
615
713
|
}
|
|
616
714
|
}
|
|
617
715
|
trackedArchetypes.push(arch);
|
|
@@ -640,11 +738,11 @@ export function createEntityManager() {
|
|
|
640
738
|
if (!snap)
|
|
641
739
|
continue;
|
|
642
740
|
for (const field in store._schema) {
|
|
643
|
-
const src = store[field];
|
|
741
|
+
const src = store._fields[field];
|
|
644
742
|
const dst = snap[field];
|
|
645
743
|
const size = store._arraySizes[field] || 0;
|
|
646
744
|
const len = size > 0 ? count * size : count;
|
|
647
|
-
if (src
|
|
745
|
+
if ('set' in src) {
|
|
648
746
|
dst.set(src.subarray(0, len));
|
|
649
747
|
}
|
|
650
748
|
else {
|
|
@@ -739,6 +837,9 @@ export function createEntityManager() {
|
|
|
739
837
|
pending.length = 0;
|
|
740
838
|
}
|
|
741
839
|
},
|
|
840
|
+
commitRemovals() {
|
|
841
|
+
removedData.clear();
|
|
842
|
+
},
|
|
742
843
|
serialize(symbolToName, stripComponents = [], skipEntitiesWith = [], { serializers } = {}) {
|
|
743
844
|
const stripSymbols = new Set(stripComponents.map(toSym));
|
|
744
845
|
const skipSymbols = new Set(skipEntitiesWith.map(toSym));
|
|
@@ -809,7 +910,7 @@ export function createEntityManager() {
|
|
|
809
910
|
const entityComponents = new Map();
|
|
810
911
|
for (const id of data.entities) {
|
|
811
912
|
allEntityIds.add(id);
|
|
812
|
-
entityComponents.set(id,
|
|
913
|
+
entityComponents.set(id, new Map());
|
|
813
914
|
}
|
|
814
915
|
for (const [name, store] of Object.entries(data.components)) {
|
|
815
916
|
const entry = nameToSymbol[name];
|
|
@@ -823,16 +924,16 @@ export function createEntityManager() {
|
|
|
823
924
|
if (!obj)
|
|
824
925
|
continue;
|
|
825
926
|
if (customDeserializer) {
|
|
826
|
-
obj
|
|
927
|
+
obj.set(sym, customDeserializer(compData));
|
|
827
928
|
}
|
|
828
929
|
else {
|
|
829
|
-
obj
|
|
930
|
+
obj.set(sym, compData);
|
|
830
931
|
}
|
|
831
932
|
}
|
|
832
933
|
}
|
|
833
934
|
const groupedByKey = new Map();
|
|
834
935
|
for (const [entityId, compMap] of entityComponents) {
|
|
835
|
-
const types =
|
|
936
|
+
const types = [...compMap.keys()];
|
|
836
937
|
if (types.length === 0)
|
|
837
938
|
continue;
|
|
838
939
|
const key = maskKey(computeMask(types));
|
|
@@ -842,7 +943,7 @@ export function createEntityManager() {
|
|
|
842
943
|
groupedByKey.get(key).push({ entityId, compMap });
|
|
843
944
|
}
|
|
844
945
|
for (const [, entries] of groupedByKey) {
|
|
845
|
-
const types =
|
|
946
|
+
const types = [...entries[0].compMap.keys()];
|
|
846
947
|
const arch = getOrCreateArchetype(types);
|
|
847
948
|
for (const { entityId, compMap } of entries) {
|
|
848
949
|
addToArchetype(arch, entityId, compMap);
|
|
@@ -14,6 +14,8 @@ export declare class System {
|
|
|
14
14
|
_registerHook(kind: 'add' | 'remove', types: ComponentDef[], handler: (id: EntityId) => void): void;
|
|
15
15
|
forEach(types: ComponentDef[], callback: (view: ArchetypeView) => void, exclude?: ComponentDef[]): void;
|
|
16
16
|
tick?(): void;
|
|
17
|
+
/** Process hooks and tick without clearing removed-data snapshots. */
|
|
18
|
+
_runCore(): void;
|
|
17
19
|
run(): void;
|
|
18
20
|
dispose(): void;
|
|
19
21
|
}
|
|
@@ -26,6 +28,8 @@ export interface SystemContext {
|
|
|
26
28
|
export type FunctionalSystemConstructor = (sys: SystemContext) => (() => void) | void;
|
|
27
29
|
export interface FunctionalSystem {
|
|
28
30
|
(): void;
|
|
31
|
+
/** @internal Process hooks and tick without clearing removed-data snapshots. */
|
|
32
|
+
_runCore(): void;
|
|
29
33
|
dispose(): void;
|
|
30
34
|
}
|
|
31
35
|
export declare function createSystem(em: EntityManager, constructor: FunctionalSystemConstructor): FunctionalSystem;
|
|
@@ -47,7 +47,8 @@ export class System {
|
|
|
47
47
|
forEach(types, callback, exclude) {
|
|
48
48
|
this.em.forEach(types, callback, exclude);
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
/** Process hooks and tick without clearing removed-data snapshots. */
|
|
51
|
+
_runCore() {
|
|
51
52
|
for (const hook of this._hooks) {
|
|
52
53
|
for (const id of hook.buffer)
|
|
53
54
|
hook.handler(id);
|
|
@@ -56,6 +57,10 @@ export class System {
|
|
|
56
57
|
if (this.tick)
|
|
57
58
|
this.tick();
|
|
58
59
|
}
|
|
60
|
+
run() {
|
|
61
|
+
this._runCore();
|
|
62
|
+
this.em.commitRemovals();
|
|
63
|
+
}
|
|
59
64
|
dispose() {
|
|
60
65
|
for (const unsub of this._unsubs)
|
|
61
66
|
unsub();
|
|
@@ -101,7 +106,7 @@ export function createSystem(em, constructor) {
|
|
|
101
106
|
},
|
|
102
107
|
};
|
|
103
108
|
const tick = constructor(sys);
|
|
104
|
-
function
|
|
109
|
+
function runCore() {
|
|
105
110
|
for (const hook of hooks) {
|
|
106
111
|
for (const id of hook.buffer)
|
|
107
112
|
hook.callback(id);
|
|
@@ -110,6 +115,11 @@ export function createSystem(em, constructor) {
|
|
|
110
115
|
if (tick)
|
|
111
116
|
tick();
|
|
112
117
|
}
|
|
118
|
+
function system() {
|
|
119
|
+
runCore();
|
|
120
|
+
em.commitRemovals();
|
|
121
|
+
}
|
|
122
|
+
system._runCore = runCore;
|
|
113
123
|
system.dispose = function () {
|
|
114
124
|
for (const hook of hooks) {
|
|
115
125
|
for (const unsub of hook.unsubs)
|
|
@@ -119,17 +129,27 @@ export function createSystem(em, constructor) {
|
|
|
119
129
|
};
|
|
120
130
|
return system;
|
|
121
131
|
}
|
|
132
|
+
function isSystemClass(entry) {
|
|
133
|
+
let proto = entry.prototype;
|
|
134
|
+
while (proto) {
|
|
135
|
+
proto = Object.getPrototypeOf(proto);
|
|
136
|
+
if (proto === System.prototype)
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
122
141
|
export function createSystems(em, entries) {
|
|
123
142
|
const systems = entries.map(Entry => {
|
|
124
|
-
if (
|
|
143
|
+
if (isSystemClass(Entry)) {
|
|
125
144
|
return new Entry(em);
|
|
126
145
|
}
|
|
127
146
|
const sys = createSystem(em, Entry);
|
|
128
|
-
return {
|
|
147
|
+
return { _runCore: sys._runCore, dispose: sys.dispose };
|
|
129
148
|
});
|
|
130
149
|
function pipeline() {
|
|
131
150
|
for (let i = 0; i < systems.length; i++)
|
|
132
|
-
systems[i].
|
|
151
|
+
systems[i]._runCore();
|
|
152
|
+
em.commitRemovals();
|
|
133
153
|
}
|
|
134
154
|
pipeline.dispose = function () {
|
|
135
155
|
for (let i = 0; i < systems.length; i++)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SoAArrayValue } from './EntityManager.js';
|
|
2
|
+
export type NumericTypedArrayConstructor = typeof Float32Array | typeof Float64Array | typeof Int8Array | typeof Int16Array | typeof Int32Array | typeof Uint8Array | typeof Uint16Array | typeof Uint32Array;
|
|
3
|
+
export declare class WasmArena {
|
|
4
|
+
memory: WebAssembly.Memory;
|
|
5
|
+
private nextOffset;
|
|
6
|
+
private views;
|
|
7
|
+
constructor(initialPages?: number, maxPages?: number);
|
|
8
|
+
alloc(byteLength: number): number;
|
|
9
|
+
createView<T extends SoAArrayValue>(Ctor: NumericTypedArrayConstructor, offset: number, count: number, fields: Record<string, SoAArrayValue>, field: string): T;
|
|
10
|
+
private handleGrow;
|
|
11
|
+
/** Update a registered view to point to a new (larger) allocation. */
|
|
12
|
+
updateView(fields: Record<string, SoAArrayValue>, field: string, offset: number, Ctor: NumericTypedArrayConstructor, count: number): void;
|
|
13
|
+
}
|