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.
Files changed (43) hide show
  1. package/README.md +79 -14
  2. package/bench/allocations-1m.js +1 -1
  3. package/bench/component-churn-bench.js +193 -0
  4. package/bench/iterate.wat +95 -0
  5. package/bench/multi-ecs-bench.js +1 -1
  6. package/bench/run-js-vs-go-ts.sh +147 -0
  7. package/bench/typed-vs-bitecs-1m.js +1 -1
  8. package/bench/typed-vs-untyped.js +1 -1
  9. package/bench/vs-bitecs.js +1 -1
  10. package/bench/wasm-iteration-bench.js +289 -0
  11. package/dist/{src/ComponentRegistry.d.ts → ComponentRegistry.d.ts} +8 -3
  12. package/dist/{src/EntityManager.d.ts → EntityManager.d.ts} +15 -10
  13. package/dist/{src/EntityManager.js → EntityManager.js} +196 -95
  14. package/dist/{src/System.d.ts → System.d.ts} +4 -0
  15. package/dist/{src/System.js → System.js} +25 -5
  16. package/dist/WasmArena.d.ts +13 -0
  17. package/dist/WasmArena.js +48 -0
  18. package/dist/{src/index.d.ts → index.d.ts} +7 -9
  19. package/dist/{src/index.js → index.js} +2 -0
  20. package/dist/wasm-kernels.d.ts +10 -0
  21. package/dist/wasm-kernels.js +59 -0
  22. package/package.json +12 -7
  23. package/src/ComponentRegistry.ts +7 -3
  24. package/src/EntityManager.ts +209 -119
  25. package/src/System.ts +34 -9
  26. package/src/WasmArena.ts +83 -0
  27. package/src/index.ts +16 -11
  28. package/src/iterate.wat +135 -0
  29. package/src/wasm-kernels.ts +68 -0
  30. package/tests/EntityManager.test.ts +51 -86
  31. package/tests/System.test.ts +184 -0
  32. package/tests/types.ts +1 -1
  33. package/tsconfig.json +2 -2
  34. package/tsconfig.test.json +13 -0
  35. package/dist/tests/EntityManager.test.d.ts +0 -1
  36. package/dist/tests/EntityManager.test.js +0 -651
  37. package/dist/tests/System.test.d.ts +0 -1
  38. package/dist/tests/System.test.js +0 -630
  39. package/dist/tests/types.d.ts +0 -1
  40. package/dist/tests/types.js +0 -129
  41. /package/dist/{src/ComponentRegistry.js → ComponentRegistry.js} +0 -0
  42. /package/dist/{src/Profiler.d.ts → Profiler.d.ts} +0 -0
  43. /package/dist/{src/Profiler.js → Profiler.js} +0 -0
package/README.md CHANGED
@@ -51,7 +51,7 @@ em.forEach([Position, Velocity], (arch) => {
51
51
  ### Why archetype-ecs?
52
52
 
53
53
  <table>
54
- <tr><td><strong>Fast iteration</strong></td><td>1.7 ms/frame over 1M entities. Faster than bitecs, wolf-ecs, harmony-ecs — see <a href="#benchmarks">benchmarks</a>.</td></tr>
54
+ <tr><td><strong>Fast iteration</strong></td><td>0.5 ms/frame over 1M entities with auto-detected <a href="#wasm-simd">WASM SIMD</a>. Faster than bitecs, wolf-ecs, harmony-ecs — see <a href="#benchmarks">benchmarks</a>.</td></tr>
55
55
  <tr><td><strong>Low memory</strong></td><td>86 MB for 1M entities. Sparse-array ECS libraries use up to 2.4x more.</td></tr>
56
56
  <tr><td><strong>No allocations</strong></td><td><code>get</code>, <code>set</code>, and <code>forEach</code> don't allocate.</td></tr>
57
57
  <tr><td><strong>Typed</strong></td><td>TypeScript generics throughout. Field names autocomplete, wrong fields don't compile.</td></tr>
@@ -228,8 +228,8 @@ A functional API is also available:
228
228
  import { createSystem } from 'archetype-ecs'
229
229
 
230
230
  const deathSystem = createSystem(em, (sys) => {
231
- sys.onAdded(Health, (id: EntityId) => console.log(`${id} spawned`))
232
- sys.onRemoved(Health, (id: EntityId) => console.log(`${id} died`))
231
+ sys.onAdded(Health, (id) => console.log(`${id} spawned`))
232
+ sys.onRemoved(Health, (id) => console.log(`${id} died`))
233
233
  })
234
234
 
235
235
  em.flushHooks()
@@ -254,6 +254,47 @@ em.deserialize(JSON.parse(json), { Position, Velocity, Health })
254
254
 
255
255
  Supports stripping components, skipping entities, and custom serializers.
256
256
 
257
+ ### WASM SIMD
258
+
259
+ When WebAssembly SIMD is available (all modern browsers and Node.js 16+), archetype-ecs automatically allocates numeric TypedArrays on a shared `WebAssembly.Memory`. This enables SIMD-accelerated batch operations like `fieldAdd()` — no manual WASM code needed.
260
+
261
+ ```ts
262
+ em.forEach([Position, Velocity], (arch) => {
263
+ arch.fieldAdd(Position.x, Velocity.vx) // px[i] += vx[i], SIMD-accelerated
264
+ arch.fieldAdd(Position.y, Velocity.vy) // py[i] += vy[i], SIMD-accelerated
265
+ })
266
+ ```
267
+
268
+ `fieldAdd(target, source)` dispatches to a `f32x4.add` SIMD kernel (4 floats per instruction) when available, and falls back to a scalar JS loop otherwise. Your system code stays identical either way — no feature detection needed.
269
+
270
+ To disable WASM mode (e.g. for testing or environments without WebAssembly):
271
+
272
+ ```ts
273
+ const em = createEntityManager({ wasm: false })
274
+ ```
275
+
276
+ For advanced use cases, you can write your own WASM modules and use `fieldOffset()` + `em.wasmMemory` to operate directly on the raw memory:
277
+
278
+ ```ts
279
+ import { instantiateKernels } from 'archetype-ecs'
280
+
281
+ const kernels = await instantiateKernels(em.wasmMemory!)
282
+
283
+ em.forEach([Position, Velocity], (arch) => {
284
+ kernels.iterate_simd(
285
+ arch.fieldOffset(Position.x), arch.fieldOffset(Position.y),
286
+ arch.fieldOffset(Velocity.vx), arch.fieldOffset(Velocity.vy),
287
+ arch.count,
288
+ )
289
+ })
290
+ ```
291
+
292
+ **Details:**
293
+ - The arena reserves 128 MB virtual address space (lazily committed — no physical RAM cost on most OSes)
294
+ - String fields always fall back to regular JS arrays
295
+ - `fieldAdd` uses SIMD for `Float32Array` fields; other types fall back to scalar
296
+ - The bump allocator doesn't reclaim memory — frequent archetype churn may waste space
297
+
257
298
  ---
258
299
 
259
300
  ## TypeScript
@@ -261,7 +302,7 @@ Supports stripping components, skipping entities, and custom serializers.
261
302
  Component types are inferred from their definition. Field names autocomplete, wrong fields are compile errors.
262
303
 
263
304
  ```ts
264
- // Fields are typed as FieldRef — Position becomes ComponentDef & { x: FieldRef; y: FieldRef }
305
+ // Schema is inferred — Position becomes ComponentDef<'x' | 'y'>
265
306
  const Position = component('Position', 'f32', ['x', 'y'])
266
307
 
267
308
  Position.x // FieldRef — autocompletes to .x and .y
@@ -298,9 +339,9 @@ Schema component with mixed field types.
298
339
  const Item = component('Item', { name: 'string', weight: 'f32', armor: 'u8' })
299
340
  ```
300
341
 
301
- ### `createEntityManager()`
342
+ ### `createEntityManager(options?)`
302
343
 
303
- Returns an entity manager with the following methods:
344
+ Returns an entity manager. WASM SIMD is auto-detected and enabled by default. Pass `{ wasm: false }` to force JS-only mode.
304
345
 
305
346
  | Method | Description |
306
347
  |---|---|
@@ -321,6 +362,21 @@ Returns an entity manager with the following methods:
321
362
  | `flushHooks()` | Collect pending add/remove events for registered hooks |
322
363
  | `serialize(symbolToName, strip?, skip?, opts?)` | Serialize world to JSON-friendly object |
323
364
  | `deserialize(data, nameToSymbol, opts?)` | Restore world from serialized data |
365
+ | `wasmMemory` | `WebAssembly.Memory \| null` — the underlying memory (WASM mode only) |
366
+
367
+ The `forEach` callback receives an `ArchetypeView` with:
368
+
369
+ | Method | Description |
370
+ |---|---|
371
+ | `field(ref)` | Get the backing TypedArray for a field |
372
+ | `fieldStride(ref)` | Elements per entity (1 for scalars, N for arrays) |
373
+ | `fieldOffset(ref)` | Byte offset in `wasmMemory` (-1 if not in WASM mode) |
374
+ | `fieldAdd(target, source)` | `target[i] += source[i]` — uses SIMD in WASM mode, scalar loop otherwise |
375
+ | `snapshot(ref)` | Get the snapshot TypedArray (change tracking) |
376
+
377
+ ### `instantiateKernels(memory)`
378
+
379
+ Loads the built-in WASM SIMD module onto the given `WebAssembly.Memory`. Returns `{ iterate_scalar, iterate_simd }` — both take `(pxOffset, pyOffset, vxOffset, vyOffset, count)` as byte offsets.
324
380
 
325
381
  ### `System`
326
382
 
@@ -349,16 +405,16 @@ Creates a pipeline from an array of class-based (`System` subclasses) and/or fun
349
405
 
350
406
  1M entities, Position += Velocity, 5 runs (median), Node.js:
351
407
 
352
- | | archetype-ecs | [bitecs](https://github.com/NateTheGreatt/bitECS) | [wolf-ecs](https://github.com/EnderShadow8/wolf-ecs) | [harmony-ecs](https://github.com/3mcd/harmony-ecs) | [miniplex](https://github.com/hmans/miniplex) |
353
- |---|---:|---:|---:|---:|---:|
354
- | **Iteration** (ms/frame) | **1.7** | 2.2 | 2.2 | 1.8 | 32.5 |
355
- | **Entity creation** (ms) | 401 | 366 | **106** | 248 | 265 |
356
- | **Memory** (MB) | 86 | 204 | 60 | **31** | 166 |
408
+ | | archetype-ecs | archetype-ecs (WASM SIMD) | [bitecs](https://github.com/NateTheGreatt/bitECS) | [wolf-ecs](https://github.com/EnderShadow8/wolf-ecs) | [harmony-ecs](https://github.com/3mcd/harmony-ecs) | [miniplex](https://github.com/hmans/miniplex) |
409
+ |---|---:|---:|---:|---:|---:|---:|
410
+ | **Iteration** (ms/frame) | 1.5 | **0.5** | 2.2 | 2.2 | 1.8 | 32.5 |
411
+ | **Entity creation** (ms) | 401 | 401 | 366 | **106** | 248 | 265 |
412
+ | **Memory** (MB) | 86 | 86+128 | 204 | 60 | **31** | 166 |
357
413
 
358
414
  Each library runs the same test — iterate 1M entities over 500 frames:
359
415
 
360
416
  ```ts
361
- // archetype-ecs
417
+ // archetype-ecs (default) — manual loop
362
418
  em.forEach([Position, Velocity], (arch) => {
363
419
  const px = arch.field(Position.x) // Float32Array, dense
364
420
  const py = arch.field(Position.y)
@@ -369,14 +425,22 @@ em.forEach([Position, Velocity], (arch) => {
369
425
  py[i] += vy[i]
370
426
  }
371
427
  })
428
+
429
+ // archetype-ecs (WASM SIMD) — same code, 2.5x faster
430
+ const em = createEntityManager({ wasm: true })
431
+ em.forEach([Position, Velocity], (arch) => {
432
+ arch.fieldAdd(Position.x, Velocity.vx)
433
+ arch.fieldAdd(Position.y, Velocity.vy)
434
+ })
372
435
  ```
373
436
 
374
- archetype-ecs is fastest at iteration. Harmony-ecs and wolf-ecs are close; miniplex is ~20x slower due to object-based storage.
437
+ The WASM SIMD kernel processes 4 floats per instruction (`f32x4.add`) and avoids V8's `f32→f64→f32` conversion overhead.
375
438
 
376
439
  Run them yourself:
377
440
 
378
441
  ```bash
379
- npm run bench
442
+ npm run bench # vs other ECS libraries
443
+ node --expose-gc bench/wasm-iteration-bench.js # WASM SIMD benchmark
380
444
  ```
381
445
 
382
446
  ---
@@ -389,6 +453,7 @@ Compared against other JS ECS libraries:
389
453
 
390
454
  | Feature | archetype-ecs | bitecs | wolf-ecs | harmony-ecs | miniplex |
391
455
  |---|:---:|:---:|:---:|:---:|:---:|
456
+ | WASM SIMD iteration (opt-in) | ✓ | — | — | — | — |
392
457
  | String SoA storage | ✓ | — | — | — | — |
393
458
  | Mixed string + numeric components | ✓ | — | — | — | — |
394
459
  | `forEach` with dense TypedArray field access | ✓ | — | — | — | — |
@@ -4,7 +4,7 @@
4
4
  import {
5
5
  createWorld, addEntity, addComponent, removeEntity, query
6
6
  } from 'bitecs';
7
- import { createEntityManager, component } from '../dist/src/index.js';
7
+ import { createEntityManager, component } from '../dist/index.js';
8
8
 
9
9
  const COUNT = 1_000_000;
10
10
  const RUNS = 5;
@@ -0,0 +1,193 @@
1
+ // Component Churn Benchmark: 1M entities, component add/remove over 500 frames
2
+ // Vergelijkt runtime van code gecompileerd met tsc (JS) vs tsgo (Go)
3
+ // Run met: node --expose-gc bench/component-churn-bench.js
4
+ // Of via: bash bench/run-js-vs-go-ts.sh
5
+
6
+ import { createEntityManager, component } from '../dist/index.js';
7
+
8
+ const COUNT = 1_000_000;
9
+ const FRAMES = 500;
10
+ const CHURN_BATCH = 1_000;
11
+ const RUNS = 5;
12
+
13
+ // ── Utilities ────────────────────────────────────────────────────────────────
14
+
15
+ const median = (arr) => {
16
+ const s = [...arr].sort((a, b) => a - b);
17
+ const mid = s.length >> 1;
18
+ return s.length & 1 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
19
+ };
20
+
21
+ const memMB = () => {
22
+ if (globalThis.gc) globalThis.gc();
23
+ return process.memoryUsage().heapUsed / 1024 / 1024;
24
+ };
25
+
26
+ // ── Components ───────────────────────────────────────────────────────────────
27
+
28
+ const Position = component('ChurnPos', { x: 'f32', y: 'f32' });
29
+ const Velocity = component('ChurnVel', { vx: 'f32', vy: 'f32' });
30
+ const Active = component('ChurnActive', { frame: 'i32' });
31
+
32
+ // ── Benchmark: iteration only (baseline) ─────────────────────────────────────
33
+
34
+ function benchIterationOnly() {
35
+ const em = createEntityManager();
36
+ for (let i = 0; i < COUNT; i++) {
37
+ em.createEntityWith(
38
+ Position, { x: i * 0.1, y: i * 0.1 },
39
+ Velocity, { vx: 1, vy: 1 },
40
+ );
41
+ }
42
+
43
+ // Warmup
44
+ for (let f = 0; f < 10; f++) {
45
+ em.forEach([Position, Velocity], (arch) => {
46
+ const px = arch.field(Position.x);
47
+ const py = arch.field(Position.y);
48
+ const vx = arch.field(Velocity.vx);
49
+ const vy = arch.field(Velocity.vy);
50
+ for (let i = 0; i < arch.count; i++) {
51
+ px[i] += vx[i];
52
+ py[i] += vy[i];
53
+ }
54
+ });
55
+ }
56
+
57
+ const t0 = performance.now();
58
+ for (let f = 0; f < FRAMES; f++) {
59
+ em.forEach([Position, Velocity], (arch) => {
60
+ const px = arch.field(Position.x);
61
+ const py = arch.field(Position.y);
62
+ const vx = arch.field(Velocity.vx);
63
+ const vy = arch.field(Velocity.vy);
64
+ for (let i = 0; i < arch.count; i++) {
65
+ px[i] += vx[i];
66
+ py[i] += vy[i];
67
+ }
68
+ });
69
+ }
70
+ return (performance.now() - t0) / FRAMES;
71
+ }
72
+
73
+ // ── Benchmark: iteration + component churn ───────────────────────────────────
74
+
75
+ function benchComponentChurn() {
76
+ const em = createEntityManager();
77
+
78
+ const entityIds = new Array(COUNT);
79
+ for (let i = 0; i < COUNT; i++) {
80
+ entityIds[i] = em.createEntityWith(
81
+ Position, { x: i * 0.1, y: i * 0.1 },
82
+ Velocity, { vx: 1, vy: 1 },
83
+ );
84
+ }
85
+
86
+ let activeIds = [];
87
+
88
+ // Warmup
89
+ for (let f = 0; f < 10; f++) {
90
+ em.forEach([Position, Velocity], (arch) => {
91
+ const px = arch.field(Position.x);
92
+ const py = arch.field(Position.y);
93
+ const vx = arch.field(Velocity.vx);
94
+ const vy = arch.field(Velocity.vy);
95
+ for (let i = 0; i < arch.count; i++) {
96
+ px[i] += vx[i];
97
+ py[i] += vy[i];
98
+ }
99
+ });
100
+ }
101
+
102
+ const t0 = performance.now();
103
+ for (let f = 0; f < FRAMES; f++) {
104
+ // 1. Movement system: iterate all entities with Position + Velocity
105
+ em.forEach([Position, Velocity], (arch) => {
106
+ const px = arch.field(Position.x);
107
+ const py = arch.field(Position.y);
108
+ const vx = arch.field(Velocity.vx);
109
+ const vy = arch.field(Velocity.vy);
110
+ for (let i = 0; i < arch.count; i++) {
111
+ px[i] += vx[i];
112
+ py[i] += vy[i];
113
+ }
114
+ });
115
+
116
+ // 2. Remove Active from previous batch (archetype migration)
117
+ for (const id of activeIds) {
118
+ em.removeComponent(id, Active);
119
+ }
120
+
121
+ // 3. Add Active to new batch (archetype migration, rotating through entities)
122
+ activeIds = [];
123
+ const base = (f * CHURN_BATCH) % (COUNT - CHURN_BATCH);
124
+ for (let i = 0; i < CHURN_BATCH; i++) {
125
+ const id = entityIds[base + i];
126
+ em.addComponent(id, Active, { frame: f });
127
+ activeIds.push(id);
128
+ }
129
+
130
+ // 4. Process Active entities (small set, tests multi-archetype iteration)
131
+ em.forEach([Position, Active], (arch) => {
132
+ const px = arch.field(Position.x);
133
+ const frame = arch.field(Active.frame);
134
+ for (let i = 0; i < arch.count; i++) {
135
+ px[i] += frame[i] * 0.001;
136
+ }
137
+ });
138
+ }
139
+ return (performance.now() - t0) / FRAMES;
140
+ }
141
+
142
+ // ── Main ─────────────────────────────────────────────────────────────────────
143
+
144
+ function main() {
145
+ const compiler = process.env.TS_COMPILER || 'unknown';
146
+
147
+ console.log(`=== Component Churn Benchmark ===`);
148
+ console.log(` ${(COUNT / 1e6).toFixed(0)}M entities | ${FRAMES} frames | ${CHURN_BATCH} churn/frame | ${RUNS} runs`);
149
+ console.log(` Compiler: ${compiler}`);
150
+ console.log();
151
+
152
+ const memBefore = memMB();
153
+
154
+ // ── Baseline: iteration only ──────────────────────────────────────────────
155
+ console.log(' [1/2] Iteration only (baseline)');
156
+ const iterResults = [];
157
+ for (let r = 0; r < RUNS; r++) {
158
+ process.stdout.write(` Run ${r + 1}/${RUNS}...`);
159
+ const ms = benchIterationOnly();
160
+ iterResults.push(ms);
161
+ console.log(` ${ms.toFixed(2)} ms/frame`);
162
+ }
163
+ const iterMedian = median(iterResults);
164
+
165
+ // ── Component churn ───────────────────────────────────────────────────────
166
+ console.log(' [2/2] Iteration + component churn');
167
+ const churnResults = [];
168
+ for (let r = 0; r < RUNS; r++) {
169
+ process.stdout.write(` Run ${r + 1}/${RUNS}...`);
170
+ const ms = benchComponentChurn();
171
+ churnResults.push(ms);
172
+ console.log(` ${ms.toFixed(2)} ms/frame`);
173
+ }
174
+ const churnMedian = median(churnResults);
175
+
176
+ const memAfter = memMB();
177
+ const churnOverhead = ((churnMedian / iterMedian - 1) * 100).toFixed(1);
178
+
179
+ // ── Resultaten ────────────────────────────────────────────────────────────
180
+ console.log();
181
+ console.log(` Resultaten (${compiler}):`);
182
+ console.log(` Iteratie baseline: ${iterMedian.toFixed(2)} ms/frame`);
183
+ console.log(` Met component churn: ${churnMedian.toFixed(2)} ms/frame`);
184
+ console.log(` Churn overhead: +${churnOverhead}%`);
185
+ console.log(` Geheugen (heap): ~${(memAfter - memBefore).toFixed(0)} MB`);
186
+ console.log();
187
+
188
+ // Machine-readable output voor runner script
189
+ console.log(`__ITER__=${iterMedian.toFixed(4)}`);
190
+ console.log(`__CHURN__=${churnMedian.toFixed(4)}`);
191
+ }
192
+
193
+ main();
@@ -0,0 +1,95 @@
1
+ (module
2
+ (memory (import "env" "memory") 1)
3
+
4
+ ;; Scalar loop: px[i] += vx[i]; py[i] += vy[i]
5
+ ;; params: byte offsets for px, py, vx, vy arrays + element count
6
+ (func (export "iterate_scalar")
7
+ (param $px i32) (param $py i32) (param $vx i32) (param $vy i32) (param $count i32)
8
+ (local $i i32)
9
+ (local $off i32)
10
+ (local.set $i (i32.const 0))
11
+ (block $break
12
+ (loop $loop
13
+ (br_if $break (i32.ge_u (local.get $i) (local.get $count)))
14
+ (local.set $off (i32.shl (local.get $i) (i32.const 2)))
15
+ ;; px[i] += vx[i]
16
+ (f32.store
17
+ (i32.add (local.get $px) (local.get $off))
18
+ (f32.add
19
+ (f32.load (i32.add (local.get $px) (local.get $off)))
20
+ (f32.load (i32.add (local.get $vx) (local.get $off)))
21
+ )
22
+ )
23
+ ;; py[i] += vy[i]
24
+ (f32.store
25
+ (i32.add (local.get $py) (local.get $off))
26
+ (f32.add
27
+ (f32.load (i32.add (local.get $py) (local.get $off)))
28
+ (f32.load (i32.add (local.get $vy) (local.get $off)))
29
+ )
30
+ )
31
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
32
+ (br $loop)
33
+ )
34
+ )
35
+ )
36
+
37
+ ;; SIMD loop: processes 4 floats at a time using v128 / f32x4
38
+ (func (export "iterate_simd")
39
+ (param $px i32) (param $py i32) (param $vx i32) (param $vy i32) (param $count i32)
40
+ (local $i i32)
41
+ (local $off i32)
42
+ (local $end4 i32)
43
+ ;; end4 = count & ~3 (round down to multiple of 4)
44
+ (local.set $end4 (i32.and (local.get $count) (i32.const -4)))
45
+ ;; SIMD loop: 4 elements per iteration
46
+ (local.set $i (i32.const 0))
47
+ (block $break
48
+ (loop $loop
49
+ (br_if $break (i32.ge_u (local.get $i) (local.get $end4)))
50
+ (local.set $off (i32.shl (local.get $i) (i32.const 2)))
51
+ ;; px[i..i+4] += vx[i..i+4]
52
+ (v128.store
53
+ (i32.add (local.get $px) (local.get $off))
54
+ (f32x4.add
55
+ (v128.load (i32.add (local.get $px) (local.get $off)))
56
+ (v128.load (i32.add (local.get $vx) (local.get $off)))
57
+ )
58
+ )
59
+ ;; py[i..i+4] += vy[i..i+4]
60
+ (v128.store
61
+ (i32.add (local.get $py) (local.get $off))
62
+ (f32x4.add
63
+ (v128.load (i32.add (local.get $py) (local.get $off)))
64
+ (v128.load (i32.add (local.get $vy) (local.get $off)))
65
+ )
66
+ )
67
+ (local.set $i (i32.add (local.get $i) (i32.const 4)))
68
+ (br $loop)
69
+ )
70
+ )
71
+ ;; Scalar remainder
72
+ (block $break2
73
+ (loop $loop2
74
+ (br_if $break2 (i32.ge_u (local.get $i) (local.get $count)))
75
+ (local.set $off (i32.shl (local.get $i) (i32.const 2)))
76
+ (f32.store
77
+ (i32.add (local.get $px) (local.get $off))
78
+ (f32.add
79
+ (f32.load (i32.add (local.get $px) (local.get $off)))
80
+ (f32.load (i32.add (local.get $vx) (local.get $off)))
81
+ )
82
+ )
83
+ (f32.store
84
+ (i32.add (local.get $py) (local.get $off))
85
+ (f32.add
86
+ (f32.load (i32.add (local.get $py) (local.get $off)))
87
+ (f32.load (i32.add (local.get $vy) (local.get $off)))
88
+ )
89
+ )
90
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
91
+ (br $loop2)
92
+ )
93
+ )
94
+ )
95
+ )
@@ -2,7 +2,7 @@
2
2
  // Tests: iteration (Position += Velocity), entity creation, memory usage
3
3
  // Run with: node --expose-gc bench/multi-ecs-bench.js
4
4
 
5
- import { createEntityManager, component } from '../dist/src/index.js';
5
+ import { createEntityManager, component } from '../dist/index.js';
6
6
 
7
7
  const COUNT = 1_000_000;
8
8
  const FRAMES = 500;
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env bash
2
+ # ── JS TypeScript (tsc) vs Go TypeScript (tsgo) Benchmark Runner ──────────────
3
+ # Compileert archetype-ecs met beide compilers en vergelijkt de runtime.
4
+ #
5
+ # Gebruik: bash bench/run-js-vs-go-ts.sh
6
+ # Vereist: npm install @typescript/native-preview
7
+ # ──────────────────────────────────────────────────────────────────────────────
8
+
9
+ set -euo pipefail
10
+ cd "$(dirname "$0")/.."
11
+
12
+ BENCH="bench/component-churn-bench.js"
13
+ NODE_FLAGS="--expose-gc"
14
+
15
+ # Kleuren
16
+ RED='\033[0;31m'
17
+ GREEN='\033[0;32m'
18
+ CYAN='\033[0;36m'
19
+ BOLD='\033[1m'
20
+ NC='\033[0m'
21
+
22
+ echo -e "${BOLD}=== archetype-ecs: tsc vs tsgo ===${NC}"
23
+ echo ""
24
+
25
+ # ── Check of tsgo beschikbaar is ─────────────────────────────────────────────
26
+
27
+ if ! npx tsgo --version &>/dev/null; then
28
+ echo -e "${RED}tsgo niet gevonden. Installeer met:${NC}"
29
+ echo " npm install -D @typescript/native-preview"
30
+ exit 1
31
+ fi
32
+
33
+ TSC_VERSION=$(npx tsc --version 2>/dev/null || echo "unknown")
34
+ TSGO_VERSION=$(npx tsgo --version 2>/dev/null || echo "unknown")
35
+ echo -e " tsc: ${CYAN}${TSC_VERSION}${NC}"
36
+ echo -e " tsgo: ${CYAN}${TSGO_VERSION}${NC}"
37
+ echo ""
38
+
39
+ # ── Backup bestaande dist ───────────────────────────────────────────────────
40
+
41
+ DIST_BACKUP=""
42
+ if [ -d dist ]; then
43
+ DIST_BACKUP=$(mktemp -d)
44
+ cp -r dist/* "$DIST_BACKUP/" 2>/dev/null || true
45
+ fi
46
+
47
+ cleanup() {
48
+ # Herstel originele dist
49
+ if [ -n "$DIST_BACKUP" ] && [ -d "$DIST_BACKUP" ]; then
50
+ rm -rf dist
51
+ mkdir -p dist
52
+ cp -r "$DIST_BACKUP"/* dist/ 2>/dev/null || true
53
+ rm -rf "$DIST_BACKUP"
54
+ fi
55
+ }
56
+ trap cleanup EXIT
57
+
58
+ # ── 1. Compileer en benchmark met tsc (JS TypeScript) ───────────────────────
59
+
60
+ echo -e "${BOLD}--- [1/2] tsc (JS TypeScript) ---${NC}"
61
+ rm -rf dist
62
+
63
+ TSC_START=$(date +%s%N)
64
+ npx tsc
65
+ TSC_END=$(date +%s%N)
66
+ TSC_COMPILE=$(( (TSC_END - TSC_START) / 1000000 ))
67
+ echo -e " Compile time: ${GREEN}${TSC_COMPILE}ms${NC}"
68
+ echo ""
69
+
70
+ TSC_OUTPUT=$(TS_COMPILER="tsc (JS)" node $NODE_FLAGS "$BENCH")
71
+ echo "$TSC_OUTPUT"
72
+
73
+ TSC_ITER=$(echo "$TSC_OUTPUT" | grep '__ITER__=' | cut -d= -f2)
74
+ TSC_CHURN=$(echo "$TSC_OUTPUT" | grep '__CHURN__=' | cut -d= -f2)
75
+
76
+ echo ""
77
+
78
+ # ── 2. Compileer en benchmark met tsgo (Go TypeScript) ──────────────────────
79
+
80
+ echo -e "${BOLD}--- [2/2] tsgo (Go TypeScript) ---${NC}"
81
+ rm -rf dist
82
+
83
+ TSGO_START=$(date +%s%N)
84
+ npx tsgo
85
+ TSGO_END=$(date +%s%N)
86
+ TSGO_COMPILE=$(( (TSGO_END - TSGO_START) / 1000000 ))
87
+ echo -e " Compile time: ${GREEN}${TSGO_COMPILE}ms${NC}"
88
+ echo ""
89
+
90
+ TSGO_OUTPUT=$(TS_COMPILER="tsgo (Go)" node $NODE_FLAGS "$BENCH")
91
+ echo "$TSGO_OUTPUT"
92
+
93
+ TSGO_ITER=$(echo "$TSGO_OUTPUT" | grep '__ITER__=' | cut -d= -f2)
94
+ TSGO_CHURN=$(echo "$TSGO_OUTPUT" | grep '__CHURN__=' | cut -d= -f2)
95
+
96
+ echo ""
97
+
98
+ # ── Samenvatting ─────────────────────────────────────────────────────────────
99
+
100
+ echo -e "${BOLD}=== Samenvatting ===${NC}"
101
+ echo ""
102
+
103
+ # Compile time vergelijking
104
+ if [ "$TSGO_COMPILE" -gt 0 ]; then
105
+ COMPILE_RATIO=$(echo "scale=1; $TSC_COMPILE / $TSGO_COMPILE" | bc 2>/dev/null || echo "?")
106
+ else
107
+ COMPILE_RATIO="?"
108
+ fi
109
+
110
+ echo " Compile time:"
111
+ echo " tsc (JS): ${TSC_COMPILE}ms"
112
+ echo " tsgo (Go): ${TSGO_COMPILE}ms"
113
+ echo -e " ${GREEN}tsgo is ${COMPILE_RATIO}x sneller${NC}"
114
+ echo ""
115
+
116
+ # Runtime vergelijking
117
+ echo " Runtime — iteratie baseline (ms/frame):"
118
+ echo " tsc (JS): ${TSC_ITER}"
119
+ echo " tsgo (Go): ${TSGO_ITER}"
120
+ if [ -n "$TSC_ITER" ] && [ -n "$TSGO_ITER" ]; then
121
+ ITER_RATIO=$(echo "scale=2; $TSC_ITER / $TSGO_ITER" | bc 2>/dev/null || echo "?")
122
+ if (( $(echo "$ITER_RATIO > 1.05" | bc -l 2>/dev/null || echo 0) )); then
123
+ echo -e " ${GREEN}tsgo is ${ITER_RATIO}x sneller${NC}"
124
+ elif (( $(echo "$ITER_RATIO < 0.95" | bc -l 2>/dev/null || echo 0) )); then
125
+ INV=$(echo "scale=2; 1 / $ITER_RATIO" | bc 2>/dev/null || echo "?")
126
+ echo -e " ${RED}tsc is ${INV}x sneller${NC}"
127
+ else
128
+ echo " ~gelijk"
129
+ fi
130
+ fi
131
+ echo ""
132
+
133
+ echo " Runtime — component churn (ms/frame):"
134
+ echo " tsc (JS): ${TSC_CHURN}"
135
+ echo " tsgo (Go): ${TSGO_CHURN}"
136
+ if [ -n "$TSC_CHURN" ] && [ -n "$TSGO_CHURN" ]; then
137
+ CHURN_RATIO=$(echo "scale=2; $TSC_CHURN / $TSGO_CHURN" | bc 2>/dev/null || echo "?")
138
+ if (( $(echo "$CHURN_RATIO > 1.05" | bc -l 2>/dev/null || echo 0) )); then
139
+ echo -e " ${GREEN}tsgo is ${CHURN_RATIO}x sneller${NC}"
140
+ elif (( $(echo "$CHURN_RATIO < 0.95" | bc -l 2>/dev/null || echo 0) )); then
141
+ INV=$(echo "scale=2; 1 / $CHURN_RATIO" | bc 2>/dev/null || echo "?")
142
+ echo -e " ${RED}tsc is ${INV}x sneller${NC}"
143
+ else
144
+ echo " ~gelijk"
145
+ fi
146
+ fi
147
+ echo ""
@@ -3,7 +3,7 @@
3
3
  import {
4
4
  createWorld, addEntity, addComponent, query
5
5
  } from 'bitecs';
6
- import { createEntityManager, component } from '../dist/src/index.js';
6
+ import { createEntityManager, component } from '../dist/index.js';
7
7
 
8
8
  const COUNT = 1_000_000;
9
9
  const FRAMES = 500;
@@ -1,6 +1,6 @@
1
1
  // Benchmark: forEach+field (bulk TypedArray) vs query+get/set (per-entity) vs query+getComponent (allocating)
2
2
 
3
- import { createEntityManager, component } from '../dist/src/index.js';
3
+ import { createEntityManager, component } from '../dist/index.js';
4
4
 
5
5
  const COUNT = 1_000_000;
6
6
  const FRAMES = 200;
@@ -1,7 +1,7 @@
1
1
  // Benchmark: archetype-ecs vs bitECS
2
2
  // Tests: system loop, entity creation, component add/remove churn
3
3
 
4
- import { createEntityManager, component } from '../dist/src/index.js';
4
+ import { createEntityManager, component } from '../dist/index.js';
5
5
  import {
6
6
  createWorld, addEntity,
7
7
  addComponent, removeComponent,