@ifc-lite/renderer 1.44.0 → 1.45.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/scene.js CHANGED
@@ -10,6 +10,7 @@ import { simplifyIndicesByClustering, lodCellSizeForBounds, LOD_MIN_TRIANGLES }
10
10
  import { quantizeInterleaved } from './quantize.js';
11
11
  import { bucketBaseKeyFor } from './chunk-grid.js';
12
12
  import { VisibilityEpochTracker } from './visibility-epoch.js';
13
+ import { isEntityVisible } from './entity-visibility.js';
13
14
  import { selectEvictions } from './residency.js';
14
15
  import { OPAQUE_ALPHA_CUTOFF } from './overlay-routing.js';
15
16
  import { prepareInstancedRender, foldOccurrenceWorldBox, INSTANCE_STRIDE_BYTES, INSTANCE_COLOR_OFFSET, INSTANCE_FLAGS_OFFSET, INSTANCE_FLAG_SELECTED, INSTANCE_FLAG_HIDDEN, } from './instanced-render.js';
@@ -23,6 +24,55 @@ function destroyGpuResources(m) {
23
24
  }
24
25
  /** Shared empty result for getInstancedTemplates() when the instanced pass is hidden. */
25
26
  const EMPTY_INSTANCED_TEMPLATES = [];
27
+ /**
28
+ * Pure helper: compute the exclusive end index of the next flushPending()
29
+ * append chunk, starting at `readIndex` and bounded by BOTH mesh count
30
+ * (`hardEnd`, computed by the caller) and index volume (`maxIndicesPerAppend`).
31
+ * Always takes at least one mesh past `readIndex` -- a single oversize mesh is
32
+ * split upstream by splitMeshForStreaming, so the volume cap never blocks the
33
+ * first mesh of a chunk.
34
+ *
35
+ * Non-finite-safe by construction: every non-finite `next` (a malformed mesh
36
+ * reporting NaN, +Infinity, or -Infinity for `indices.length`) closes the
37
+ * chunk explicitly instead of being folded into the running `chunkIndices`
38
+ * total. Only NaN would have made a naive cap check `chunkIndices + next >
39
+ * maxIndicesPerAppend` silently `false` forever (`NaN > cap` is always
40
+ * `false`); +Infinity actually made that same check fire immediately
41
+ * (`chunkIndices + Infinity > cap` is `true`), closing the chunk after a
42
+ * single oversize mesh instead of growing it unbounded. The current
43
+ * `!(chunkIndices + next <= maxIndicesPerAppend)` form below rejects both
44
+ * NaN and +Infinity explicitly rather than relying on that asymmetry.
45
+ * -Infinity needed a separate, explicit check: `-Infinity <= cap` is always
46
+ * `true`, so `!(... <= cap)` lets it straight through, and folding it into
47
+ * `chunkIndices` would poison the running total to -Infinity permanently,
48
+ * keeping the cap vacuous for every mesh after it, not just the malformed
49
+ * one.
50
+ */
51
+ export function computeFlushChunkEnd(getIndicesLength, readIndex, hardEnd, maxIndicesPerAppend) {
52
+ let chunkEnd = readIndex;
53
+ let chunkIndices = 0;
54
+ while (chunkEnd < hardEnd) {
55
+ const next = getIndicesLength(chunkEnd);
56
+ if (!Number.isFinite(next)) {
57
+ // A malformed mesh reporting a non-finite indices.length (NaN, +/-Infinity)
58
+ // must close the chunk here rather than being folded into chunkIndices:
59
+ // `chunkIndices += -Infinity` would poison the running total to -Infinity
60
+ // permanently, making `!(chunkIndices + next <= maxIndicesPerAppend)`
61
+ // false forever and letting the volume cap never fire again for the
62
+ // rest of this chunk. Always take at least the first mesh past
63
+ // readIndex (same progress guarantee as the NaN case below).
64
+ if (chunkEnd === readIndex)
65
+ chunkEnd++;
66
+ break;
67
+ }
68
+ if (chunkEnd > readIndex && !(chunkIndices + next <= maxIndicesPerAppend)) {
69
+ break;
70
+ }
71
+ chunkIndices += next;
72
+ chunkEnd++;
73
+ }
74
+ return chunkEnd;
75
+ }
26
76
  export class Scene {
27
77
  meshes = [];
28
78
  batchedMeshes = []; // flat render array (rebuilt from buckets)
@@ -1637,18 +1687,26 @@ export class Scene {
1637
1687
  break;
1638
1688
  }
1639
1689
  const hardEnd = Math.min(this.meshQueue.length, this.meshQueueReadIndex + MESHES_PER_APPEND, this.meshQueueReadIndex + (MAX_MESHES_PER_FLUSH - processed));
1640
- let chunkEnd = this.meshQueueReadIndex;
1641
- let chunkIndices = 0;
1642
- while (chunkEnd < hardEnd) {
1643
- const next = this.meshQueue[chunkEnd].indices.length;
1644
- // Always take at least one mesh (a single oversize mesh is split upstream
1645
- // by splitMeshForStreaming); otherwise stop before exceeding the cap.
1646
- if (chunkEnd > this.meshQueueReadIndex && chunkIndices + next > MAX_INDICES_PER_APPEND) {
1647
- break;
1648
- }
1649
- chunkIndices += next;
1650
- chunkEnd++;
1651
- }
1690
+ const chunkEnd = computeFlushChunkEnd((i) => this.meshQueue[i].indices.length, this.meshQueueReadIndex, hardEnd, MAX_INDICES_PER_APPEND);
1691
+ // Defensive, not reachable today: chunkEnd is provably > meshQueueReadIndex
1692
+ // here because hardEnd is provably > meshQueueReadIndex whenever this outer
1693
+ // loop iterates, via three invariants that hold simultaneously above:
1694
+ // (1) this.meshQueue.length > this.meshQueueReadIndex -- the outer while
1695
+ // condition that got us into this iteration;
1696
+ // (2) this.meshQueueReadIndex + MESHES_PER_APPEND, and MESHES_PER_APPEND
1697
+ // (512) is a positive constant;
1698
+ // (3) this.meshQueueReadIndex + (MAX_MESHES_PER_FLUSH - processed), and
1699
+ // processed < MAX_MESHES_PER_FLUSH -- the other half of the outer
1700
+ // while condition -- so that term is >= readIndex + 1 too.
1701
+ // hardEnd is the min of all three, so hardEnd >= readIndex + 1, and
1702
+ // computeFlushChunkEnd always advances by at least one past readIndex.
1703
+ // If a future change breaks any one of those three invariants, hardEnd
1704
+ // could collapse to readIndex and the loop would spin the main thread at
1705
+ // 100% CPU doing zero allocation -- the exact signature that made #2379
1706
+ // expensive to diagnose. This break turns that failure mode into "flush
1707
+ // stops early" instead.
1708
+ if (chunkEnd === this.meshQueueReadIndex)
1709
+ break;
1652
1710
  const chunk = this.meshQueue.slice(this.meshQueueReadIndex, chunkEnd);
1653
1711
  this.meshQueueReadIndex = chunkEnd;
1654
1712
  this.appendToBatches(chunk, device, pipeline, true);
@@ -3204,13 +3262,11 @@ export class Scene {
3204
3262
  }
3205
3263
  this.instancedVisibilityDirty = false;
3206
3264
  this.lastInstancedVisibilityVersion = visibilityVersion;
3207
- const isHidden = (eid) => (hiddenIds != null && hiddenIds.has(eid)) ||
3208
- (isolatedIds != null && !isolatedIds.has(eid));
3209
3265
  // Recompute the effective hidden set over all instanced occurrences and diff vs
3210
3266
  // the current one; only flips touch the GPU buffer.
3211
3267
  const next = new Set();
3212
3268
  for (const eid of this.instancedEntityMap.keys()) {
3213
- if (isHidden(eid))
3269
+ if (!isEntityVisible(eid, hiddenIds, isolatedIds))
3214
3270
  next.add(eid);
3215
3271
  }
3216
3272
  // Fast-path: unchanged hidden set → nothing to write.