@vectojs/core 1.22.0 → 1.23.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/index.js +502 -329
- package/dist/index.mjs +340 -167
- package/dist/tree/ComputeParticleEntity.d.ts +5 -1
- package/dist/tree/Scene.d.ts +89 -0
- package/dist/wasm/anim-backend.d.ts +21 -4
- package/dist/wasm/backend.d.ts +9 -4
- package/dist/wasm/particle-backend.d.ts +12 -1
- package/dist/wasm/vectojs_core.wasm +0 -0
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -307,12 +307,16 @@ var ComputeParticleEntity = class extends Entity {
|
|
|
307
307
|
* The backend holds one resident SoA store, so a Scene with multiple particle
|
|
308
308
|
* entities reuses it sequentially — origin is therefore re-gathered each call
|
|
309
309
|
* (not upload-once), a couple of extra f32 reads per particle.
|
|
310
|
+
*
|
|
311
|
+
* Returns `false` if the kernel declined the call and {@link updateCPU} ran
|
|
312
|
+
* instead, so the Scene can report which path actually simulated this frame
|
|
313
|
+
* rather than assuming an installed backend did the work.
|
|
310
314
|
*/
|
|
311
315
|
stepWithBackend(backend, dt, mouseX, mouseY, width, height) {
|
|
312
316
|
const count = this.maxParticles;
|
|
313
317
|
backend.ensure(count);
|
|
314
318
|
backend.gather(this.particleData, count, true);
|
|
315
|
-
|
|
319
|
+
const pending = backend.step(count, {
|
|
316
320
|
dt,
|
|
317
321
|
mouseX,
|
|
318
322
|
mouseY,
|
|
@@ -324,8 +328,14 @@ var ComputeParticleEntity = class extends Entity {
|
|
|
324
328
|
maxVelocity: this.maxVelocity,
|
|
325
329
|
explosion: this.pendingExplosion
|
|
326
330
|
});
|
|
331
|
+
if (pending === null) {
|
|
332
|
+
this.updateCPU(dt, mouseX, mouseY, width, height);
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
this._wasmPending = pending;
|
|
327
336
|
backend.scatter(this.particleData, count);
|
|
328
337
|
this.pendingExplosion = null;
|
|
338
|
+
return true;
|
|
329
339
|
}
|
|
330
340
|
destroy() {
|
|
331
341
|
this.destroyGPUResources();
|
|
@@ -469,148 +479,6 @@ function buildTreeStore(root) {
|
|
|
469
479
|
return { store, indexOf };
|
|
470
480
|
}
|
|
471
481
|
|
|
472
|
-
// src/wasm/hit-store.ts
|
|
473
|
-
function gatherHitAABBs(root, currentFrame) {
|
|
474
|
-
const slotEntity = [];
|
|
475
|
-
const boundless = [];
|
|
476
|
-
const minxs = [];
|
|
477
|
-
const minys = [];
|
|
478
|
-
const maxxs = [];
|
|
479
|
-
const maxys = [];
|
|
480
|
-
const scratch = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
|
|
481
|
-
const visit = (node) => {
|
|
482
|
-
const index = slotEntity.length;
|
|
483
|
-
slotEntity.push(node);
|
|
484
|
-
const bounds = node.getBounds();
|
|
485
|
-
if (bounds === null) {
|
|
486
|
-
boundless.push({ entity: node, index });
|
|
487
|
-
minxs.push(0);
|
|
488
|
-
minys.push(0);
|
|
489
|
-
maxxs.push(0);
|
|
490
|
-
maxys.push(0);
|
|
491
|
-
} else {
|
|
492
|
-
if (!node._readWorldCache(currentFrame, scratch)) {
|
|
493
|
-
const t = node.getWorldTransform();
|
|
494
|
-
scratch.a = t.a;
|
|
495
|
-
scratch.b = t.b;
|
|
496
|
-
scratch.c = t.c;
|
|
497
|
-
scratch.d = t.d;
|
|
498
|
-
scratch.e = t.e;
|
|
499
|
-
scratch.f = t.f;
|
|
500
|
-
}
|
|
501
|
-
const { a, b, c, d, e, f } = scratch;
|
|
502
|
-
let minX = Infinity;
|
|
503
|
-
let minY = Infinity;
|
|
504
|
-
let maxX = -Infinity;
|
|
505
|
-
let maxY = -Infinity;
|
|
506
|
-
for (let i = 0; i < 4; i++) {
|
|
507
|
-
const lx = i & 1 ? bounds.x + bounds.width : bounds.x;
|
|
508
|
-
const ly = i & 2 ? bounds.y + bounds.height : bounds.y;
|
|
509
|
-
const wx = a * lx + c * ly + e;
|
|
510
|
-
const wy = b * lx + d * ly + f;
|
|
511
|
-
if (wx < minX) minX = wx;
|
|
512
|
-
if (wx > maxX) maxX = wx;
|
|
513
|
-
if (wy < minY) minY = wy;
|
|
514
|
-
if (wy > maxY) maxY = wy;
|
|
515
|
-
}
|
|
516
|
-
minxs.push(minX);
|
|
517
|
-
minys.push(minY);
|
|
518
|
-
maxxs.push(maxX);
|
|
519
|
-
maxys.push(maxY);
|
|
520
|
-
}
|
|
521
|
-
const kids = node.children;
|
|
522
|
-
for (let i = 0; i < kids.length; i++) visit(kids[i]);
|
|
523
|
-
};
|
|
524
|
-
visit(root);
|
|
525
|
-
return {
|
|
526
|
-
count: slotEntity.length,
|
|
527
|
-
slotEntity,
|
|
528
|
-
boundless,
|
|
529
|
-
minx: Float64Array.from(minxs),
|
|
530
|
-
miny: Float64Array.from(minys),
|
|
531
|
-
maxx: Float64Array.from(maxxs),
|
|
532
|
-
maxy: Float64Array.from(maxys)
|
|
533
|
-
};
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// src/wasm/hit-store-fused.ts
|
|
537
|
-
function gatherHitAABBsFromStore(root, aabbs, storeSlotEntity, out) {
|
|
538
|
-
const slotEntity = out.slotEntity;
|
|
539
|
-
const boundless = out.boundless;
|
|
540
|
-
slotEntity.length = 0;
|
|
541
|
-
boundless.length = 0;
|
|
542
|
-
let count = 0;
|
|
543
|
-
let capacity = out.minx.length;
|
|
544
|
-
let minx = out.minx;
|
|
545
|
-
let miny = out.miny;
|
|
546
|
-
let maxx = out.maxx;
|
|
547
|
-
let maxy = out.maxy;
|
|
548
|
-
const grow = (needed) => {
|
|
549
|
-
if (needed <= capacity) return;
|
|
550
|
-
let next = capacity || 256;
|
|
551
|
-
while (next < needed) next *= 2;
|
|
552
|
-
const nminx = new Float64Array(next);
|
|
553
|
-
const nminy = new Float64Array(next);
|
|
554
|
-
const nmaxx = new Float64Array(next);
|
|
555
|
-
const nmaxy = new Float64Array(next);
|
|
556
|
-
nminx.set(minx);
|
|
557
|
-
nminy.set(miny);
|
|
558
|
-
nmaxx.set(maxx);
|
|
559
|
-
nmaxy.set(maxy);
|
|
560
|
-
minx = nminx;
|
|
561
|
-
miny = nminy;
|
|
562
|
-
maxx = nmaxx;
|
|
563
|
-
maxy = nmaxy;
|
|
564
|
-
capacity = next;
|
|
565
|
-
};
|
|
566
|
-
let bailed = false;
|
|
567
|
-
const visit = (node) => {
|
|
568
|
-
if (bailed) return;
|
|
569
|
-
const index = count;
|
|
570
|
-
slotEntity.push(node);
|
|
571
|
-
count++;
|
|
572
|
-
grow(count);
|
|
573
|
-
if (node.getBounds() === null) {
|
|
574
|
-
boundless.push({ entity: node, index });
|
|
575
|
-
minx[index] = 0;
|
|
576
|
-
miny[index] = 0;
|
|
577
|
-
maxx[index] = 0;
|
|
578
|
-
maxy[index] = 0;
|
|
579
|
-
} else {
|
|
580
|
-
const slot = node._storeSlot;
|
|
581
|
-
if (slot < 0 || slot >= aabbs.aminx.length || storeSlotEntity[slot] !== node) {
|
|
582
|
-
bailed = true;
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
minx[index] = aabbs.aminx[slot];
|
|
586
|
-
miny[index] = aabbs.aminy[slot];
|
|
587
|
-
maxx[index] = aabbs.amaxx[slot];
|
|
588
|
-
maxy[index] = aabbs.amaxy[slot];
|
|
589
|
-
}
|
|
590
|
-
const children = node.children;
|
|
591
|
-
for (let i = 0; i < children.length; i++) visit(children[i]);
|
|
592
|
-
};
|
|
593
|
-
visit(root);
|
|
594
|
-
if (bailed) return null;
|
|
595
|
-
out.count = count;
|
|
596
|
-
out.minx = minx;
|
|
597
|
-
out.miny = miny;
|
|
598
|
-
out.maxx = maxx;
|
|
599
|
-
out.maxy = maxy;
|
|
600
|
-
return out;
|
|
601
|
-
}
|
|
602
|
-
function createHitGatherBuffer() {
|
|
603
|
-
return {
|
|
604
|
-
count: 0,
|
|
605
|
-
slotEntity: [],
|
|
606
|
-
boundless: [],
|
|
607
|
-
minx: new Float64Array(256),
|
|
608
|
-
miny: new Float64Array(256),
|
|
609
|
-
maxx: new Float64Array(256),
|
|
610
|
-
maxy: new Float64Array(256)
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
|
|
614
482
|
// src/wasm/backend.ts
|
|
615
483
|
var WASM_STATUS = {
|
|
616
484
|
OK: 0,
|
|
@@ -703,7 +571,10 @@ var WasmTransformBackend = class {
|
|
|
703
571
|
return status;
|
|
704
572
|
}
|
|
705
573
|
/** Upload only the run table + count (topology), leaving per-entity inputs to
|
|
706
|
-
* the resident views. Call when the tree structure changes, not per frame.
|
|
574
|
+
* the resident views. Call when the tree structure changes, not per frame.
|
|
575
|
+
* Returns `false` if the crate rejected the run count, in which case the
|
|
576
|
+
* PREVIOUS topology is still published and any kernel run against it would
|
|
577
|
+
* compose the wrong tree — the caller must not proceed. */
|
|
707
578
|
uploadRuns(store) {
|
|
708
579
|
this.ensure(store.count, store.runCount);
|
|
709
580
|
const rc = store.runCount;
|
|
@@ -711,6 +582,7 @@ var WasmTransformBackend = class {
|
|
|
711
582
|
this.vrs.set(store.runStart.subarray(0, rc));
|
|
712
583
|
this.vrl.set(store.runLen.subarray(0, rc));
|
|
713
584
|
this.lastStatus = this.ex.set_run_count(rc);
|
|
585
|
+
return this.lastStatus === WASM_STATUS.OK;
|
|
714
586
|
}
|
|
715
587
|
/**
|
|
716
588
|
* Status of the most recent kernel or run-table call. `WASM_STATUS.OK` unless
|
|
@@ -759,7 +631,8 @@ var WasmTransformBackend = class {
|
|
|
759
631
|
this.vby.set(store.by.subarray(0, n));
|
|
760
632
|
this.vbw.set(store.bw.subarray(0, n));
|
|
761
633
|
this.vbh.set(store.bh.subarray(0, n));
|
|
762
|
-
this.ex.compute_aabbs(n);
|
|
634
|
+
this.lastStatus = this.ex.compute_aabbs(n);
|
|
635
|
+
if (this.lastStatus !== WASM_STATUS.OK) return;
|
|
763
636
|
store.aminx.set(this.vaminx.subarray(0, n));
|
|
764
637
|
store.aminy.set(this.vaminy.subarray(0, n));
|
|
765
638
|
store.amaxx.set(this.vamaxx.subarray(0, n));
|
|
@@ -767,9 +640,12 @@ var WasmTransformBackend = class {
|
|
|
767
640
|
}
|
|
768
641
|
/** Run the AABB pass only, over `count` entities already resident in wasm
|
|
769
642
|
* memory (bounds written via {@link boundsView}, world matrices already
|
|
770
|
-
* composed). No upload/readback — the per-frame resident path.
|
|
643
|
+
* composed). No upload/readback — the per-frame resident path. Returns
|
|
644
|
+
* `false` if the kernel rejected `count` (beyond capacity, or uninitialized),
|
|
645
|
+
* in which case {@link aabbView} still holds the previous frame's bounds. */
|
|
771
646
|
runAabbs(count) {
|
|
772
|
-
this.ex.compute_aabbs(count);
|
|
647
|
+
this.lastStatus = this.ex.compute_aabbs(count);
|
|
648
|
+
return this.lastStatus === WASM_STATUS.OK;
|
|
773
649
|
}
|
|
774
650
|
/** Resident wasm local-bounds input views (`bx,by,bw,bh`) for the AABB pass. */
|
|
775
651
|
boundsView() {
|
|
@@ -843,6 +719,148 @@ var WasmTransformBackend = class {
|
|
|
843
719
|
}
|
|
844
720
|
};
|
|
845
721
|
|
|
722
|
+
// src/wasm/hit-store.ts
|
|
723
|
+
function gatherHitAABBs(root, currentFrame) {
|
|
724
|
+
const slotEntity = [];
|
|
725
|
+
const boundless = [];
|
|
726
|
+
const minxs = [];
|
|
727
|
+
const minys = [];
|
|
728
|
+
const maxxs = [];
|
|
729
|
+
const maxys = [];
|
|
730
|
+
const scratch = { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
|
|
731
|
+
const visit = (node) => {
|
|
732
|
+
const index = slotEntity.length;
|
|
733
|
+
slotEntity.push(node);
|
|
734
|
+
const bounds = node.getBounds();
|
|
735
|
+
if (bounds === null) {
|
|
736
|
+
boundless.push({ entity: node, index });
|
|
737
|
+
minxs.push(0);
|
|
738
|
+
minys.push(0);
|
|
739
|
+
maxxs.push(0);
|
|
740
|
+
maxys.push(0);
|
|
741
|
+
} else {
|
|
742
|
+
if (!node._readWorldCache(currentFrame, scratch)) {
|
|
743
|
+
const t = node.getWorldTransform();
|
|
744
|
+
scratch.a = t.a;
|
|
745
|
+
scratch.b = t.b;
|
|
746
|
+
scratch.c = t.c;
|
|
747
|
+
scratch.d = t.d;
|
|
748
|
+
scratch.e = t.e;
|
|
749
|
+
scratch.f = t.f;
|
|
750
|
+
}
|
|
751
|
+
const { a, b, c, d, e, f } = scratch;
|
|
752
|
+
let minX = Infinity;
|
|
753
|
+
let minY = Infinity;
|
|
754
|
+
let maxX = -Infinity;
|
|
755
|
+
let maxY = -Infinity;
|
|
756
|
+
for (let i = 0; i < 4; i++) {
|
|
757
|
+
const lx = i & 1 ? bounds.x + bounds.width : bounds.x;
|
|
758
|
+
const ly = i & 2 ? bounds.y + bounds.height : bounds.y;
|
|
759
|
+
const wx = a * lx + c * ly + e;
|
|
760
|
+
const wy = b * lx + d * ly + f;
|
|
761
|
+
if (wx < minX) minX = wx;
|
|
762
|
+
if (wx > maxX) maxX = wx;
|
|
763
|
+
if (wy < minY) minY = wy;
|
|
764
|
+
if (wy > maxY) maxY = wy;
|
|
765
|
+
}
|
|
766
|
+
minxs.push(minX);
|
|
767
|
+
minys.push(minY);
|
|
768
|
+
maxxs.push(maxX);
|
|
769
|
+
maxys.push(maxY);
|
|
770
|
+
}
|
|
771
|
+
const kids = node.children;
|
|
772
|
+
for (let i = 0; i < kids.length; i++) visit(kids[i]);
|
|
773
|
+
};
|
|
774
|
+
visit(root);
|
|
775
|
+
return {
|
|
776
|
+
count: slotEntity.length,
|
|
777
|
+
slotEntity,
|
|
778
|
+
boundless,
|
|
779
|
+
minx: Float64Array.from(minxs),
|
|
780
|
+
miny: Float64Array.from(minys),
|
|
781
|
+
maxx: Float64Array.from(maxxs),
|
|
782
|
+
maxy: Float64Array.from(maxys)
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/wasm/hit-store-fused.ts
|
|
787
|
+
function gatherHitAABBsFromStore(root, aabbs, storeSlotEntity, out) {
|
|
788
|
+
const slotEntity = out.slotEntity;
|
|
789
|
+
const boundless = out.boundless;
|
|
790
|
+
slotEntity.length = 0;
|
|
791
|
+
boundless.length = 0;
|
|
792
|
+
let count = 0;
|
|
793
|
+
let capacity = out.minx.length;
|
|
794
|
+
let minx = out.minx;
|
|
795
|
+
let miny = out.miny;
|
|
796
|
+
let maxx = out.maxx;
|
|
797
|
+
let maxy = out.maxy;
|
|
798
|
+
const grow = (needed) => {
|
|
799
|
+
if (needed <= capacity) return;
|
|
800
|
+
let next = capacity || 256;
|
|
801
|
+
while (next < needed) next *= 2;
|
|
802
|
+
const nminx = new Float64Array(next);
|
|
803
|
+
const nminy = new Float64Array(next);
|
|
804
|
+
const nmaxx = new Float64Array(next);
|
|
805
|
+
const nmaxy = new Float64Array(next);
|
|
806
|
+
nminx.set(minx);
|
|
807
|
+
nminy.set(miny);
|
|
808
|
+
nmaxx.set(maxx);
|
|
809
|
+
nmaxy.set(maxy);
|
|
810
|
+
minx = nminx;
|
|
811
|
+
miny = nminy;
|
|
812
|
+
maxx = nmaxx;
|
|
813
|
+
maxy = nmaxy;
|
|
814
|
+
capacity = next;
|
|
815
|
+
};
|
|
816
|
+
let bailed = false;
|
|
817
|
+
const visit = (node) => {
|
|
818
|
+
if (bailed) return;
|
|
819
|
+
const index = count;
|
|
820
|
+
slotEntity.push(node);
|
|
821
|
+
count++;
|
|
822
|
+
grow(count);
|
|
823
|
+
if (node.getBounds() === null) {
|
|
824
|
+
boundless.push({ entity: node, index });
|
|
825
|
+
minx[index] = 0;
|
|
826
|
+
miny[index] = 0;
|
|
827
|
+
maxx[index] = 0;
|
|
828
|
+
maxy[index] = 0;
|
|
829
|
+
} else {
|
|
830
|
+
const slot = node._storeSlot;
|
|
831
|
+
if (slot < 0 || slot >= aabbs.aminx.length || storeSlotEntity[slot] !== node) {
|
|
832
|
+
bailed = true;
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
minx[index] = aabbs.aminx[slot];
|
|
836
|
+
miny[index] = aabbs.aminy[slot];
|
|
837
|
+
maxx[index] = aabbs.amaxx[slot];
|
|
838
|
+
maxy[index] = aabbs.amaxy[slot];
|
|
839
|
+
}
|
|
840
|
+
const children = node.children;
|
|
841
|
+
for (let i = 0; i < children.length; i++) visit(children[i]);
|
|
842
|
+
};
|
|
843
|
+
visit(root);
|
|
844
|
+
if (bailed) return null;
|
|
845
|
+
out.count = count;
|
|
846
|
+
out.minx = minx;
|
|
847
|
+
out.miny = miny;
|
|
848
|
+
out.maxx = maxx;
|
|
849
|
+
out.maxy = maxy;
|
|
850
|
+
return out;
|
|
851
|
+
}
|
|
852
|
+
function createHitGatherBuffer() {
|
|
853
|
+
return {
|
|
854
|
+
count: 0,
|
|
855
|
+
slotEntity: [],
|
|
856
|
+
boundless: [],
|
|
857
|
+
minx: new Float64Array(256),
|
|
858
|
+
miny: new Float64Array(256),
|
|
859
|
+
maxx: new Float64Array(256),
|
|
860
|
+
maxy: new Float64Array(256)
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
846
864
|
// src/wasm/anim-backend.ts
|
|
847
865
|
var PAD3 = 8;
|
|
848
866
|
var AnimBackend = class {
|
|
@@ -877,14 +895,33 @@ var AnimBackend = class {
|
|
|
877
895
|
this.ex.anim_init(this.springCap, this.tweenCap);
|
|
878
896
|
this.refreshViews();
|
|
879
897
|
}
|
|
880
|
-
/**
|
|
898
|
+
/**
|
|
899
|
+
* Advance `count` springs (from index 0) by `dtMs` milliseconds, in place.
|
|
900
|
+
* Returns `true` when the kernel ran. `false` means it rejected the call
|
|
901
|
+
* (count beyond the capacity {@link ensure} allocated, or no `anim_init` yet)
|
|
902
|
+
* and wrote nothing, so the caller must tick those drivers in JS instead of
|
|
903
|
+
* scattering back a pack the kernel never touched. See {@link lastStatus}.
|
|
904
|
+
*/
|
|
881
905
|
stepSprings(dtMs, count) {
|
|
882
|
-
this.ex.spring_step(dtMs / 1e3, count);
|
|
906
|
+
this.lastStatus = this.ex.spring_step(dtMs / 1e3, count);
|
|
907
|
+
return this.lastStatus === WASM_STATUS.OK;
|
|
883
908
|
}
|
|
884
|
-
/**
|
|
909
|
+
/**
|
|
910
|
+
* Advance `count` tweens (from index 0) by `dtMs` milliseconds, writing `val`.
|
|
911
|
+
* Returns `true` when the kernel ran; `false` means it rejected the call and
|
|
912
|
+
* wrote nothing (see {@link stepSprings}). `elapsed` is kernel-side state, so
|
|
913
|
+
* a rejected tween pack must not be read back — it is unadvanced, not
|
|
914
|
+
* partially advanced.
|
|
915
|
+
*/
|
|
885
916
|
stepTweens(dtMs, count) {
|
|
886
|
-
this.ex.tween_step(dtMs, count);
|
|
917
|
+
this.lastStatus = this.ex.tween_step(dtMs, count);
|
|
918
|
+
return this.lastStatus === WASM_STATUS.OK;
|
|
887
919
|
}
|
|
920
|
+
/**
|
|
921
|
+
* Status of the most recent kernel call — `WASM_STATUS.OK` unless the kernel
|
|
922
|
+
* declined it. Mirrors {@link TransformBackend.lastStatus}.
|
|
923
|
+
*/
|
|
924
|
+
lastStatus = WASM_STATUS.OK;
|
|
888
925
|
refreshViews() {
|
|
889
926
|
const buf = this.ex.memory.buffer;
|
|
890
927
|
const sCap = this.springCap;
|
|
@@ -1035,6 +1072,12 @@ var ParticleBackend = class {
|
|
|
1035
1072
|
* Advance `count` particles one step in place. Returns `true` when at least
|
|
1036
1073
|
* one live particle is still moving or off-origin beyond epsilon (the fused
|
|
1037
1074
|
* `hasPendingAnimations` flag), so the caller need not re-scan the buffer.
|
|
1075
|
+
*
|
|
1076
|
+
* Returns `null` when the kernel REJECTED the call — `count` beyond the
|
|
1077
|
+
* capacity {@link ensure} allocated, or no `particle_init` yet. Nothing was
|
|
1078
|
+
* written, so the caller must NOT {@link scatter} (that would write the
|
|
1079
|
+
* gathered pre-step values back and freeze the simulation) and should fall
|
|
1080
|
+
* back to the JS `updateCPU` path for this frame. See {@link lastStatus}.
|
|
1038
1081
|
*/
|
|
1039
1082
|
step(count, p) {
|
|
1040
1083
|
const e = p.explosion;
|
|
@@ -1054,8 +1097,18 @@ var ParticleBackend = class {
|
|
|
1054
1097
|
e ? e.force : 0,
|
|
1055
1098
|
count
|
|
1056
1099
|
);
|
|
1100
|
+
if (flag < 0) {
|
|
1101
|
+
this.lastStatus = -flag;
|
|
1102
|
+
return null;
|
|
1103
|
+
}
|
|
1104
|
+
this.lastStatus = WASM_STATUS.OK;
|
|
1057
1105
|
return flag !== 0;
|
|
1058
1106
|
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Status of the most recent {@link step} — `WASM_STATUS.OK` unless the kernel
|
|
1109
|
+
* declined it. Mirrors {@link TransformBackend.lastStatus}.
|
|
1110
|
+
*/
|
|
1111
|
+
lastStatus = WASM_STATUS.OK;
|
|
1059
1112
|
/** Transpose the AoS stride-8 buffer into the SoA views (position/velocity/
|
|
1060
1113
|
* life every frame; origin upload-once when `withOrigin`). */
|
|
1061
1114
|
gather(data, count, withOrigin) {
|
|
@@ -1866,10 +1919,13 @@ var Scene = class _Scene {
|
|
|
1866
1919
|
return true;
|
|
1867
1920
|
}
|
|
1868
1921
|
// ── WASM hit-test backend (invisible accelerator, G3) ───────────────────────
|
|
1869
|
-
//
|
|
1870
|
-
//
|
|
1871
|
-
//
|
|
1872
|
-
// AABBs into a dense viewport grid for findEntityAt.
|
|
1922
|
+
// Served by the same instance as every other accelerator (see
|
|
1923
|
+
// `_wasmRuntime`): the crate keeps transform/anim/hit/particle in distinct
|
|
1924
|
+
// statics, so one instance runs them all without aliasing. It indexes the
|
|
1925
|
+
// main tree's world AABBs into a dense viewport grid for findEntityAt. Note
|
|
1926
|
+
// that sharing one linear memory means an allocation here can grow it and
|
|
1927
|
+
// detach views built over the old buffer, so each backend re-checks buffer
|
|
1928
|
+
// identity (`revalidateViews`) before use. The JS depth-first walk
|
|
1873
1929
|
// (findHitRecursively) is the permanent fallback: a null backend, a build
|
|
1874
1930
|
// that overflows its item budget, or the overlay tree (never indexed — small
|
|
1875
1931
|
// and rare, not worth accelerating) all fall through to it, so WASM can only
|
|
@@ -1942,6 +1998,70 @@ var Scene = class _Scene {
|
|
|
1942
1998
|
get hitGatherPath() {
|
|
1943
1999
|
return this._hitFusedGather ? "fused" : "js";
|
|
1944
2000
|
}
|
|
2001
|
+
/**
|
|
2002
|
+
* Why the transform accelerator did or did not run on the most recent frame.
|
|
2003
|
+
* Written by the render walk and `_syncWasmStore`.
|
|
2004
|
+
*/
|
|
2005
|
+
_transformReason = "not-installed";
|
|
2006
|
+
/** Why the batched-driver accelerator did or did not run. */
|
|
2007
|
+
_animReason = "not-installed";
|
|
2008
|
+
/**
|
|
2009
|
+
* Why the hit-test accelerator did or did not serve the last pointer query.
|
|
2010
|
+
* The grid is built lazily on demand, not every frame, so this describes the
|
|
2011
|
+
* most recent BUILD. Starts at `'not-installed'` because that is the truth
|
|
2012
|
+
* before a backend exists; `_ensureHitGrid` moves it to `'not-applicable'`
|
|
2013
|
+
* once one is installed but nothing has queried yet.
|
|
2014
|
+
*/
|
|
2015
|
+
_hitReason = "not-installed";
|
|
2016
|
+
/** Why the particle accelerator did or did not run. */
|
|
2017
|
+
_particleReason = "not-applicable";
|
|
2018
|
+
/** Which particle implementation actually simulated the most recent frame. */
|
|
2019
|
+
_particlePath = "none";
|
|
2020
|
+
/**
|
|
2021
|
+
* Per-frame status of every invisible accelerator: whether each is installed,
|
|
2022
|
+
* whether it actually ran on the most recent frame, and why.
|
|
2023
|
+
*
|
|
2024
|
+
* This exists because the older per-accelerator getters
|
|
2025
|
+
* ({@link transformBackend}, {@link animBackend}, {@link hitTestBackend},
|
|
2026
|
+
* {@link particleBackend}) report only that a backend is INSTALLED. Reading
|
|
2027
|
+
* `'wasm'` from one of those and concluding the accelerator is doing work is
|
|
2028
|
+
* wrong whenever a gate never opens, a kernel rejects its arguments, or a
|
|
2029
|
+
* faster backend takes the pass instead. Read {@link AcceleratorStatus.reason}
|
|
2030
|
+
* for which of those happened.
|
|
2031
|
+
*
|
|
2032
|
+
* Reflects the most recent main-renderer frame; a secondary renderer (SVG
|
|
2033
|
+
* export, offscreen snapshot) does not overwrite it.
|
|
2034
|
+
*/
|
|
2035
|
+
get accelerators() {
|
|
2036
|
+
return {
|
|
2037
|
+
transform: {
|
|
2038
|
+
available: this._wasm !== null && this._transformBackend === "wasm",
|
|
2039
|
+
activeThisFrame: this._transformReason === "active",
|
|
2040
|
+
reason: this._transformReason,
|
|
2041
|
+
path: this._transformReason === "active" ? "wasm" : "js"
|
|
2042
|
+
},
|
|
2043
|
+
animation: {
|
|
2044
|
+
available: this._animWasm !== null,
|
|
2045
|
+
activeThisFrame: this._animBatchedLastFrame,
|
|
2046
|
+
reason: this._animReason,
|
|
2047
|
+
path: this._animBatchedLastFrame ? "wasm" : "js"
|
|
2048
|
+
},
|
|
2049
|
+
hitTest: {
|
|
2050
|
+
available: this._hitWasm !== null,
|
|
2051
|
+
// The grid is built lazily on a pointer query, not every frame, so this
|
|
2052
|
+
// describes the last BUILD rather than the last frame.
|
|
2053
|
+
activeThisFrame: this._hitReason === "active",
|
|
2054
|
+
reason: this._hitReason,
|
|
2055
|
+
path: this._hitReason !== "active" ? "js" : this._hitFusedGather ? "wasm-fused" : "wasm"
|
|
2056
|
+
},
|
|
2057
|
+
particle: {
|
|
2058
|
+
available: this._particleWasm !== null || this.webgpuActive,
|
|
2059
|
+
activeThisFrame: this._particleReason === "active",
|
|
2060
|
+
reason: this._particleReason,
|
|
2061
|
+
path: this._particlePath
|
|
2062
|
+
}
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
1945
2065
|
/** Which backend answers `findEntityAt` for the main tree. */
|
|
1946
2066
|
get hitTestBackend() {
|
|
1947
2067
|
return this._hitWasm ? "wasm" : "js";
|
|
@@ -1951,6 +2071,7 @@ var Scene = class _Scene {
|
|
|
1951
2071
|
setHitTestBackend(backend) {
|
|
1952
2072
|
this._hitWasm = backend;
|
|
1953
2073
|
this._hitGridFrame = -1;
|
|
2074
|
+
this._hitReason = backend ? "not-applicable" : "not-installed";
|
|
1954
2075
|
}
|
|
1955
2076
|
/**
|
|
1956
2077
|
* Asynchronously instantiate the WASM hit-test core and, on success, hot-swap
|
|
@@ -1977,7 +2098,10 @@ var Scene = class _Scene {
|
|
|
1977
2098
|
*/
|
|
1978
2099
|
_ensureHitGrid() {
|
|
1979
2100
|
const backend = this._hitWasm;
|
|
1980
|
-
if (!backend)
|
|
2101
|
+
if (!backend) {
|
|
2102
|
+
this._hitReason = "not-installed";
|
|
2103
|
+
return false;
|
|
2104
|
+
}
|
|
1981
2105
|
if (this._hitGridFrame === this.currentFrame) return this._hitGridOk;
|
|
1982
2106
|
let gathered = null;
|
|
1983
2107
|
if (this._wasm && this._ensureWasmAabbs()) {
|
|
@@ -2006,6 +2130,7 @@ var Scene = class _Scene {
|
|
|
2006
2130
|
this._hitBoundless = gathered.boundless;
|
|
2007
2131
|
this._hitGridFrame = this.currentFrame;
|
|
2008
2132
|
this._hitGridOk = ok;
|
|
2133
|
+
this._hitReason = ok ? "active" : "rejected";
|
|
2009
2134
|
return ok;
|
|
2010
2135
|
}
|
|
2011
2136
|
/**
|
|
@@ -2271,7 +2396,10 @@ var Scene = class _Scene {
|
|
|
2271
2396
|
* pre-pass.
|
|
2272
2397
|
*/
|
|
2273
2398
|
_tickBatchedDrivers(dt) {
|
|
2274
|
-
if (this._activeDriverEntities.size === 0)
|
|
2399
|
+
if (this._activeDriverEntities.size === 0) {
|
|
2400
|
+
this._animReason = "not-applicable";
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2275
2403
|
let springBatchable = 0;
|
|
2276
2404
|
let tweenBatchable = 0;
|
|
2277
2405
|
for (const entity of this._activeDriverEntities) {
|
|
@@ -2288,10 +2416,17 @@ var Scene = class _Scene {
|
|
|
2288
2416
|
const batchable = springBatchable + tweenBatchable;
|
|
2289
2417
|
const backend = this._animWasm;
|
|
2290
2418
|
this._animBatchedLastFrame = false;
|
|
2291
|
-
if (!backend)
|
|
2419
|
+
if (!backend) {
|
|
2420
|
+
this._animReason = "not-installed";
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2292
2423
|
const gate = springBatchable > 0 && tweenBatchable > 0 ? this.animGate.mixed : tweenBatchable > 0 ? this.animGate.tween : this.animGate.spring;
|
|
2293
|
-
if (batchable < gate)
|
|
2424
|
+
if (batchable < gate) {
|
|
2425
|
+
this._animReason = "below-gate";
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2294
2428
|
this._animBatchedLastFrame = true;
|
|
2429
|
+
this._animReason = "active";
|
|
2295
2430
|
const sE = this._springEntities;
|
|
2296
2431
|
const sP = this._springProps;
|
|
2297
2432
|
const sD = this._springDrivers;
|
|
@@ -2338,8 +2473,13 @@ var Scene = class _Scene {
|
|
|
2338
2473
|
sv.damp[i] = phys.damping;
|
|
2339
2474
|
sv.mass[i] = phys.mass;
|
|
2340
2475
|
}
|
|
2341
|
-
backend.stepSprings(dt, springCount)
|
|
2342
|
-
|
|
2476
|
+
if (backend.stepSprings(dt, springCount)) {
|
|
2477
|
+
for (let i = 0; i < springCount; i++) sD[i].syncExternal(sv.val[i], sv.vel[i]);
|
|
2478
|
+
} else {
|
|
2479
|
+
for (let i = 0; i < springCount; i++) sD[i].tick(dt);
|
|
2480
|
+
this._animReason = "rejected";
|
|
2481
|
+
this._animBatchedLastFrame = false;
|
|
2482
|
+
}
|
|
2343
2483
|
}
|
|
2344
2484
|
if (tweenCount > 0) {
|
|
2345
2485
|
const tv = backend.tweenView();
|
|
@@ -2352,8 +2492,13 @@ var Scene = class _Scene {
|
|
|
2352
2492
|
tv.delay[i] = d.delayMs;
|
|
2353
2493
|
tv.ease[i] = d.wasmEasingId;
|
|
2354
2494
|
}
|
|
2355
|
-
backend.stepTweens(dt, tweenCount)
|
|
2356
|
-
|
|
2495
|
+
if (backend.stepTweens(dt, tweenCount)) {
|
|
2496
|
+
for (let i = 0; i < tweenCount; i++) tD[i].syncExternal(tv.val[i], tv.elapsed[i]);
|
|
2497
|
+
} else {
|
|
2498
|
+
for (let i = 0; i < tweenCount; i++) tD[i].tick(dt);
|
|
2499
|
+
this._animReason = "rejected";
|
|
2500
|
+
this._animBatchedLastFrame = false;
|
|
2501
|
+
}
|
|
2357
2502
|
}
|
|
2358
2503
|
for (let i = 0; i < springCount; i++) sE[i]._applyDriverTick(sP[i], sD[i]);
|
|
2359
2504
|
for (let i = 0; i < tweenCount; i++) tE[i]._applyDriverTick(tP[i], tD[i]);
|
|
@@ -2375,7 +2520,10 @@ var Scene = class _Scene {
|
|
|
2375
2520
|
slotEntity2[slot] = entity;
|
|
2376
2521
|
entity._storeSlot = slot;
|
|
2377
2522
|
}
|
|
2378
|
-
backend.uploadRuns(built.store)
|
|
2523
|
+
if (!backend.uploadRuns(built.store)) {
|
|
2524
|
+
this._transformReason = "rejected";
|
|
2525
|
+
return null;
|
|
2526
|
+
}
|
|
2379
2527
|
this._treeStore = built.store;
|
|
2380
2528
|
this._slotEntity = slotEntity2;
|
|
2381
2529
|
this._wasmInputs = backend.inputView();
|
|
@@ -2400,8 +2548,12 @@ var Scene = class _Scene {
|
|
|
2400
2548
|
inp.sin[slot] = trig.sin;
|
|
2401
2549
|
inp.opacity[slot] = e.opacity;
|
|
2402
2550
|
}
|
|
2403
|
-
backend.runKernel("simd")
|
|
2551
|
+
if (backend.runKernel("simd") !== WASM_STATUS.OK) {
|
|
2552
|
+
this._transformReason = "rejected";
|
|
2553
|
+
return null;
|
|
2554
|
+
}
|
|
2404
2555
|
this._wasmAabbsFresh = false;
|
|
2556
|
+
this._transformReason = "active";
|
|
2405
2557
|
return this._wasmWorld;
|
|
2406
2558
|
}
|
|
2407
2559
|
/**
|
|
@@ -2431,7 +2583,7 @@ var Scene = class _Scene {
|
|
|
2431
2583
|
bounds.bw[slot] = b ? b.width : 0;
|
|
2432
2584
|
bounds.bh[slot] = b ? b.height : 0;
|
|
2433
2585
|
}
|
|
2434
|
-
backend.runAabbs(slotEntity.length);
|
|
2586
|
+
if (!backend.runAabbs(slotEntity.length)) return false;
|
|
2435
2587
|
this._wasmAabbsFresh = true;
|
|
2436
2588
|
return true;
|
|
2437
2589
|
}
|
|
@@ -4705,6 +4857,10 @@ var Scene = class _Scene {
|
|
|
4705
4857
|
const computeEntities = this._computeEntitiesFor(this._structureVersion);
|
|
4706
4858
|
if (computeEntities.length > 0) {
|
|
4707
4859
|
const isMainRenderPath = renderer === this.renderer;
|
|
4860
|
+
if (isMainRenderPath) {
|
|
4861
|
+
this._particleReason = this._particleWasm ? "active" : "not-installed";
|
|
4862
|
+
this._particlePath = this._particleWasm ? "wasm" : "js";
|
|
4863
|
+
}
|
|
4708
4864
|
if (isMainRenderPath && !this.device && !this.webgpuDisabled && !this.initializingWebGPU && !this.deviceLost) {
|
|
4709
4865
|
this.initializingWebGPU = true;
|
|
4710
4866
|
this.initWebGPUContext(computeEntities).then((newDevice) => {
|
|
@@ -4780,6 +4936,8 @@ var Scene = class _Scene {
|
|
|
4780
4936
|
}
|
|
4781
4937
|
this.device.queue.submit([commandEncoder.finish()]);
|
|
4782
4938
|
if (this.gpuContext) this.gpuHasContent = true;
|
|
4939
|
+
this._particleReason = "active";
|
|
4940
|
+
this._particlePath = "webgpu";
|
|
4783
4941
|
} catch (e) {
|
|
4784
4942
|
console.error("WebGPU frame execution failed. Falling back.", e);
|
|
4785
4943
|
this.deviceLost = true;
|
|
@@ -4801,13 +4959,25 @@ var Scene = class _Scene {
|
|
|
4801
4959
|
}
|
|
4802
4960
|
}
|
|
4803
4961
|
if (this._particleWasm) {
|
|
4804
|
-
entity.stepWithBackend(
|
|
4962
|
+
if (!entity.stepWithBackend(
|
|
4963
|
+
this._particleWasm,
|
|
4964
|
+
dt / 1e3,
|
|
4965
|
+
mx,
|
|
4966
|
+
my,
|
|
4967
|
+
this.width,
|
|
4968
|
+
this.height
|
|
4969
|
+
)) {
|
|
4970
|
+
this._particleReason = "rejected";
|
|
4971
|
+
this._particlePath = "js";
|
|
4972
|
+
}
|
|
4805
4973
|
} else {
|
|
4806
4974
|
entity.updateCPU(dt / 1e3, mx, my, this.width, this.height);
|
|
4807
4975
|
}
|
|
4808
4976
|
}
|
|
4809
4977
|
}
|
|
4810
4978
|
} else if (isMainRenderer) {
|
|
4979
|
+
this._particleReason = "not-applicable";
|
|
4980
|
+
this._particlePath = "none";
|
|
4811
4981
|
this.clearGPUCanvasIfStale();
|
|
4812
4982
|
}
|
|
4813
4983
|
renderer.clear();
|
|
@@ -4836,6 +5006,9 @@ var Scene = class _Scene {
|
|
|
4836
5006
|
}
|
|
4837
5007
|
};
|
|
4838
5008
|
const wasmMain = isMainRenderer && this._wasm !== null && this._transformBackend === "wasm";
|
|
5009
|
+
if (isMainRenderer) {
|
|
5010
|
+
this._transformReason = wasmMain ? "active" : "not-installed";
|
|
5011
|
+
}
|
|
4839
5012
|
if (wasmMain) {
|
|
4840
5013
|
const updateWalk = (node) => {
|
|
4841
5014
|
runUpdate(node);
|