@ifc-lite/renderer 6.0.1 → 6.3.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 (189) hide show
  1. package/README.md +21 -0
  2. package/dist/ao-pass.d.ts +0 -16
  3. package/dist/ao-pass.d.ts.map +1 -1
  4. package/dist/ao-pass.js +2 -0
  5. package/dist/ao-pass.js.map +1 -1
  6. package/dist/camera-animation.d.ts +1 -1
  7. package/dist/camera-animation.d.ts.map +1 -1
  8. package/dist/camera-animation.js +11 -11
  9. package/dist/camera-animation.js.map +1 -1
  10. package/dist/camera-inertia.d.ts +27 -0
  11. package/dist/camera-inertia.d.ts.map +1 -0
  12. package/dist/camera-inertia.js +33 -0
  13. package/dist/camera-inertia.js.map +1 -0
  14. package/dist/camera.d.ts.map +1 -1
  15. package/dist/camera.js +3 -0
  16. package/dist/camera.js.map +1 -1
  17. package/dist/deviation/deviation-computer.d.ts +5 -1
  18. package/dist/deviation/deviation-computer.d.ts.map +1 -1
  19. package/dist/deviation/deviation-computer.js +13 -6
  20. package/dist/deviation/deviation-computer.js.map +1 -1
  21. package/dist/deviation/deviation-readback.d.ts +16 -3
  22. package/dist/deviation/deviation-readback.d.ts.map +1 -1
  23. package/dist/deviation/deviation-readback.js +46 -36
  24. package/dist/deviation/deviation-readback.js.map +1 -1
  25. package/dist/deviation/deviation-statistics-kernel.d.ts +111 -0
  26. package/dist/deviation/deviation-statistics-kernel.d.ts.map +1 -0
  27. package/dist/deviation/deviation-statistics-kernel.js +303 -0
  28. package/dist/deviation/deviation-statistics-kernel.js.map +1 -0
  29. package/dist/deviation/deviation-statistics.d.ts +100 -0
  30. package/dist/deviation/deviation-statistics.d.ts.map +1 -0
  31. package/dist/deviation/deviation-statistics.js +100 -0
  32. package/dist/deviation/deviation-statistics.js.map +1 -0
  33. package/dist/device-recovery.d.ts.map +1 -1
  34. package/dist/device-recovery.js +5 -0
  35. package/dist/device-recovery.js.map +1 -1
  36. package/dist/device.d.ts +0 -3
  37. package/dist/device.d.ts.map +1 -1
  38. package/dist/device.js +5 -1
  39. package/dist/device.js.map +1 -1
  40. package/dist/edge-pass.d.ts +0 -12
  41. package/dist/edge-pass.d.ts.map +1 -1
  42. package/dist/edge-pass.js +3 -0
  43. package/dist/edge-pass.js.map +1 -1
  44. package/dist/edl-pass.d.ts +0 -21
  45. package/dist/edl-pass.d.ts.map +1 -1
  46. package/dist/edl-pass.js +23 -0
  47. package/dist/edl-pass.js.map +1 -1
  48. package/dist/frame-timing-gpu.d.ts +11 -15
  49. package/dist/frame-timing-gpu.d.ts.map +1 -1
  50. package/dist/frame-timing-gpu.js +57 -26
  51. package/dist/frame-timing-gpu.js.map +1 -1
  52. package/dist/gpu-counters.d.ts +30 -0
  53. package/dist/gpu-counters.d.ts.map +1 -0
  54. package/dist/gpu-counters.js +84 -0
  55. package/dist/gpu-counters.js.map +1 -0
  56. package/dist/gpu-static-upload.d.ts.map +1 -1
  57. package/dist/gpu-static-upload.js +2 -0
  58. package/dist/gpu-static-upload.js.map +1 -1
  59. package/dist/index.d.ts +33 -21
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +70 -59
  62. package/dist/index.js.map +1 -1
  63. package/dist/picker-depth-sample.d.ts +9 -0
  64. package/dist/picker-depth-sample.d.ts.map +1 -0
  65. package/dist/picker-depth-sample.js +63 -0
  66. package/dist/picker-depth-sample.js.map +1 -0
  67. package/dist/picker-readbacks.d.ts +8 -1
  68. package/dist/picker-readbacks.d.ts.map +1 -1
  69. package/dist/picker-readbacks.js +18 -1
  70. package/dist/picker-readbacks.js.map +1 -1
  71. package/dist/picker.d.ts +2 -0
  72. package/dist/picker.d.ts.map +1 -1
  73. package/dist/picker.js +70 -94
  74. package/dist/picker.js.map +1 -1
  75. package/dist/picking-manager.d.ts +5 -2
  76. package/dist/picking-manager.d.ts.map +1 -1
  77. package/dist/picking-manager.js +25 -8
  78. package/dist/picking-manager.js.map +1 -1
  79. package/dist/pipeline.d.ts +8 -0
  80. package/dist/pipeline.d.ts.map +1 -1
  81. package/dist/pipeline.js +22 -0
  82. package/dist/pipeline.js.map +1 -1
  83. package/dist/pointcloud/point-cloud-keyed-chunks.d.ts +23 -0
  84. package/dist/pointcloud/point-cloud-keyed-chunks.d.ts.map +1 -0
  85. package/dist/pointcloud/point-cloud-keyed-chunks.js +44 -0
  86. package/dist/pointcloud/point-cloud-keyed-chunks.js.map +1 -0
  87. package/dist/pointcloud/point-cloud-node.d.ts +5 -1
  88. package/dist/pointcloud/point-cloud-node.d.ts.map +1 -1
  89. package/dist/pointcloud/point-cloud-node.js +8 -4
  90. package/dist/pointcloud/point-cloud-node.js.map +1 -1
  91. package/dist/pointcloud/point-cloud-placement.d.ts +6 -0
  92. package/dist/pointcloud/point-cloud-placement.d.ts.map +1 -1
  93. package/dist/pointcloud/point-cloud-placement.js +8 -0
  94. package/dist/pointcloud/point-cloud-placement.js.map +1 -1
  95. package/dist/pointcloud/point-cloud-ray-transform.d.ts +17 -8
  96. package/dist/pointcloud/point-cloud-ray-transform.d.ts.map +1 -1
  97. package/dist/pointcloud/point-cloud-ray-transform.js +17 -13
  98. package/dist/pointcloud/point-cloud-ray-transform.js.map +1 -1
  99. package/dist/pointcloud/point-cloud-renderer.d.ts +10 -8
  100. package/dist/pointcloud/point-cloud-renderer.d.ts.map +1 -1
  101. package/dist/pointcloud/point-cloud-renderer.js +20 -31
  102. package/dist/pointcloud/point-cloud-renderer.js.map +1 -1
  103. package/dist/pointcloud/point-cloud-stream-lifecycle.d.ts +8 -1
  104. package/dist/pointcloud/point-cloud-stream-lifecycle.d.ts.map +1 -1
  105. package/dist/pointcloud/point-cloud-stream-lifecycle.js +17 -2
  106. package/dist/pointcloud/point-cloud-stream-lifecycle.js.map +1 -1
  107. package/dist/pointcloud/point-cloud-uniforms.d.ts +11 -0
  108. package/dist/pointcloud/point-cloud-uniforms.d.ts.map +1 -1
  109. package/dist/pointcloud/point-cloud-uniforms.js +8 -0
  110. package/dist/pointcloud/point-cloud-uniforms.js.map +1 -1
  111. package/dist/post-pass-chain.d.ts +2 -0
  112. package/dist/post-pass-chain.d.ts.map +1 -1
  113. package/dist/post-pass-chain.js +1 -1
  114. package/dist/post-pass-chain.js.map +1 -1
  115. package/dist/raycast-point-cloud-query.d.ts.map +1 -1
  116. package/dist/raycast-point-cloud-query.js +11 -1
  117. package/dist/raycast-point-cloud-query.js.map +1 -1
  118. package/dist/renderer-frame-timing.d.ts +21 -0
  119. package/dist/renderer-frame-timing.d.ts.map +1 -0
  120. package/dist/renderer-frame-timing.js +104 -0
  121. package/dist/renderer-frame-timing.js.map +1 -0
  122. package/dist/renderer-viewport.d.ts +13 -7
  123. package/dist/renderer-viewport.d.ts.map +1 -1
  124. package/dist/renderer-viewport.js +15 -7
  125. package/dist/renderer-viewport.js.map +1 -1
  126. package/dist/scene-batch-rebuild.d.ts +7 -0
  127. package/dist/scene-batch-rebuild.d.ts.map +1 -1
  128. package/dist/scene-batch-rebuild.js +12 -0
  129. package/dist/scene-batch-rebuild.js.map +1 -1
  130. package/dist/scene-contents.d.ts +3 -2
  131. package/dist/scene-contents.d.ts.map +1 -1
  132. package/dist/scene-flush.d.ts +43 -0
  133. package/dist/scene-flush.d.ts.map +1 -0
  134. package/dist/scene-flush.js +103 -0
  135. package/dist/scene-flush.js.map +1 -0
  136. package/dist/scene-geometry.d.ts.map +1 -1
  137. package/dist/scene-geometry.js +2 -0
  138. package/dist/scene-geometry.js.map +1 -1
  139. package/dist/scene.d.ts +10 -27
  140. package/dist/scene.d.ts.map +1 -1
  141. package/dist/scene.js +38 -107
  142. package/dist/scene.js.map +1 -1
  143. package/dist/selection-mask-pass.d.ts +2 -27
  144. package/dist/selection-mask-pass.d.ts.map +1 -1
  145. package/dist/selection-mask-pass.js +44 -39
  146. package/dist/selection-mask-pass.js.map +1 -1
  147. package/dist/selection-mask-pipelines.d.ts +3 -3
  148. package/dist/selection-mask-pipelines.d.ts.map +1 -1
  149. package/dist/selection-mask-pipelines.js +11 -8
  150. package/dist/selection-mask-pipelines.js.map +1 -1
  151. package/dist/shaders/depth-nudge.wgsl.d.ts +67 -0
  152. package/dist/shaders/depth-nudge.wgsl.d.ts.map +1 -0
  153. package/dist/shaders/depth-nudge.wgsl.js +130 -0
  154. package/dist/shaders/depth-nudge.wgsl.js.map +1 -0
  155. package/dist/shaders/ghost.wgsl.d.ts +2 -0
  156. package/dist/shaders/ghost.wgsl.d.ts.map +1 -0
  157. package/dist/shaders/ghost.wgsl.js +46 -0
  158. package/dist/shaders/ghost.wgsl.js.map +1 -0
  159. package/dist/shaders/main.wgsl.d.ts +1 -1
  160. package/dist/shaders/main.wgsl.d.ts.map +1 -1
  161. package/dist/shaders/main.wgsl.js +6 -4
  162. package/dist/shaders/main.wgsl.js.map +1 -1
  163. package/dist/shaders/section-2d-overlay.wgsl.d.ts +1 -16
  164. package/dist/shaders/section-2d-overlay.wgsl.d.ts.map +1 -1
  165. package/dist/shaders/section-2d-overlay.wgsl.js +6 -6
  166. package/dist/shaders/section-2d-overlay.wgsl.js.map +1 -1
  167. package/dist/shaders/selection-mask.wgsl.d.ts +2 -10
  168. package/dist/shaders/selection-mask.wgsl.d.ts.map +1 -1
  169. package/dist/shaders/selection-mask.wgsl.js +9 -42
  170. package/dist/shaders/selection-mask.wgsl.js.map +1 -1
  171. package/dist/shaders/symbolic-overlay.wgsl.d.ts +1 -1
  172. package/dist/shaders/symbolic-overlay.wgsl.d.ts.map +1 -1
  173. package/dist/shaders/symbolic-overlay.wgsl.js +5 -4
  174. package/dist/shaders/symbolic-overlay.wgsl.js.map +1 -1
  175. package/dist/shadow-pass.d.ts +0 -19
  176. package/dist/shadow-pass.d.ts.map +1 -1
  177. package/dist/shadow-pass.js +19 -0
  178. package/dist/shadow-pass.js.map +1 -1
  179. package/dist/test/frame-timing-device.d.ts +17 -0
  180. package/dist/test/frame-timing-device.d.ts.map +1 -0
  181. package/dist/test/frame-timing-device.js +70 -0
  182. package/dist/test/frame-timing-device.js.map +1 -0
  183. package/dist/types.d.ts +2 -0
  184. package/dist/types.d.ts.map +1 -1
  185. package/dist/xray-alpha.d.ts +8 -0
  186. package/dist/xray-alpha.d.ts.map +1 -1
  187. package/dist/xray-alpha.js +23 -0
  188. package/dist/xray-alpha.js.map +1 -1
  189. package/package.json +4 -3
package/dist/scene.js CHANGED
@@ -9,6 +9,7 @@ import { createInstancedRteDeltaStream, invalidateInstancedRteDeltas } from './i
9
9
  import { materializeInstances } from './scene-instance-materialization.js';
10
10
  import { InstanceSuppression } from './scene-instance-suppression.js';
11
11
  import { createSceneBatch } from './scene-batch-upload.js';
12
+ import { flushQueuedMeshes } from './scene-flush.js';
12
13
  import { createStaticGpuBuffer } from './gpu-static-upload.js';
13
14
  import { createSceneAppearancePreview, rebindSceneAppearanceAccess } from './scene-appearance-preview.js';
14
15
  import { AppearanceBuckets } from './scene-appearance-buckets.js';
@@ -36,62 +37,15 @@ import { translateSceneModel, rotateSceneModelInstances, releaseInstanceVertices
36
37
  import { ModelTranslations } from './model-translation.js';
37
38
  import { unionInstancedWorldAabb as unionInstanceBounds } from './scene-instance-bounds.js';
38
39
  import { DerivedMeshProvenance } from './scene-derived-mesh-provenance.js';
39
- import { rebuildSceneBatches } from './scene-batch-rebuild.js';
40
+ import { forgetEmptyModelFrames, rebuildSceneBatches } from './scene-batch-rebuild.js';
40
41
  import { regroupStreamedBuckets } from './scene-finalize-regroup.js';
42
+ import { perfCount, perfCounters, perfTally } from '@ifc-lite/load-trace';
41
43
  import { dropAllPartialCaches as dropAllPartialCachesIn, dropPartialCacheForBatch as dropPartialCacheForBatchIn, retireUnusedAlphaSlots as retireUnusedAlphaSlotsIn, } from './partial-batch-cache.js';
42
44
  import { prepareInstancedRender, foldOccurrenceWorldBox, INSTANCE_STRIDE_BYTES, INSTANCE_COLOR_OFFSET, INSTANCE_FLAGS_OFFSET, INSTANCE_FLAG_SELECTED, INSTANCE_FLAG_HIDDEN, INSTANCE_FINISH_FLAGS_MASK, } from './instanced-render.js';
43
45
  import { translateInstanceRecord } from './scene-instance-translation.js';
44
46
  import { discardSceneGpuResourcesForRecovery, prepareSceneDeviceRecovery, repartitionHydratedRecoveryBucket, restoreSceneGpuResourcesAfterRecovery } from './scene-device-recovery.js';
45
47
  const EMPTY_INSTANCED_TEMPLATES = [];
46
- /**
47
- * Pure helper: compute the exclusive end index of the next flushPending()
48
- * append chunk, starting at `readIndex` and bounded by BOTH mesh count
49
- * (`hardEnd`, computed by the caller) and index volume (`maxIndicesPerAppend`).
50
- * Always takes at least one mesh past `readIndex` -- a single oversize mesh is
51
- * split upstream by splitMeshForStreaming, so the volume cap never blocks the
52
- * first mesh of a chunk.
53
- *
54
- * Non-finite-safe by construction: every non-finite `next` (a malformed mesh
55
- * reporting NaN, +Infinity, or -Infinity for `indices.length`) closes the
56
- * chunk explicitly instead of being folded into the running `chunkIndices`
57
- * total. Only NaN would have made a naive cap check `chunkIndices + next >
58
- * maxIndicesPerAppend` silently `false` forever (`NaN > cap` is always
59
- * `false`); +Infinity actually made that same check fire immediately
60
- * (`chunkIndices + Infinity > cap` is `true`), closing the chunk after a
61
- * single oversize mesh instead of growing it unbounded. The current
62
- * `!(chunkIndices + next <= maxIndicesPerAppend)` form below rejects both
63
- * NaN and +Infinity explicitly rather than relying on that asymmetry.
64
- * -Infinity needed a separate, explicit check: `-Infinity <= cap` is always
65
- * `true`, so `!(... <= cap)` lets it straight through, and folding it into
66
- * `chunkIndices` would poison the running total to -Infinity permanently,
67
- * keeping the cap vacuous for every mesh after it, not just the malformed
68
- * one.
69
- */
70
- export function computeFlushChunkEnd(getIndicesLength, readIndex, hardEnd, maxIndicesPerAppend) {
71
- let chunkEnd = readIndex;
72
- let chunkIndices = 0;
73
- while (chunkEnd < hardEnd) {
74
- const next = getIndicesLength(chunkEnd);
75
- if (!Number.isFinite(next)) {
76
- // A malformed mesh reporting a non-finite indices.length (NaN, +/-Infinity)
77
- // must close the chunk here rather than being folded into chunkIndices:
78
- // `chunkIndices += -Infinity` would poison the running total to -Infinity
79
- // permanently, making `!(chunkIndices + next <= maxIndicesPerAppend)`
80
- // false forever and letting the volume cap never fire again for the
81
- // rest of this chunk. Always take at least the first mesh past
82
- // readIndex (same progress guarantee as the NaN case below).
83
- if (chunkEnd === readIndex)
84
- chunkEnd++;
85
- break;
86
- }
87
- if (chunkEnd > readIndex && !(chunkIndices + next <= maxIndicesPerAppend)) {
88
- break;
89
- }
90
- chunkIndices += next;
91
- chunkEnd++;
92
- }
93
- return chunkEnd;
94
- }
48
+ export { computeFlushChunkEnd } from './scene-flush.js';
95
49
  export class Scene {
96
50
  meshes = [];
97
51
  batchedMeshes = []; // flat render array (rebuilt from buckets)
@@ -1199,6 +1153,7 @@ export class Scene {
1199
1153
  }, device, pipeline);
1200
1154
  this.batchedMeshes = [...this.buckets.values()].flatMap(bucket => bucket.batchedMesh ? [bucket.batchedMesh] : []);
1201
1155
  this.pendingBatchKeys.clear();
1156
+ forgetEmptyModelFrames(this.buckets, this.sharedFrameOrigins);
1202
1157
  }
1203
1158
  /**
1204
1159
  * Check if there are pending batch rebuilds
@@ -1782,64 +1737,20 @@ export class Scene {
1782
1737
  * @param budgetMs Time slice per call. The 12 ms default keeps the main
1783
1738
  * thread returning to the worker-message pump while geometry streams; a
1784
1739
  * host with nothing else to serve may drain faster (#6436).
1740
+ * @param trace The streaming load's trace (#6979): a call that uploaded
1741
+ * anything records one `scene.flushPending` span.
1742
+ * @param deterministicUploads Benchmark-only per-mesh fragments with count
1743
+ * and index-volume bounds. These totals describe a different timing workload.
1785
1744
  * @returns true if any meshes were processed (caller should render)
1786
1745
  */
1787
- flushPending(device, pipeline, budgetMs = 12) {
1788
- if (!this.hasQueuedMeshes())
1789
- return false;
1790
- // Drain the queue in chunks bounded by BOTH mesh count AND triangle volume,
1791
- // yielding the frame back at the TOP of the loop. The mesh-count-only chunker
1792
- // could merge a 512-mesh chunk of high-poly meshes (e.g. 899 Velux roof
1793
- // windows at 7624 tris each → ~3.9M tris) in ONE indivisible mergeGeometry +
1794
- // GPU buffer upload copy — hundreds of ms that parked the main thread
1795
- // past the 16s stream watchdog. Capping each appendToBatches by index volume
1796
- // keeps every synchronous slice ≈ one bounded fragment merge (~12-15ms).
1797
- const MAX_MESHES_PER_FLUSH = 4096;
1798
- const MESHES_PER_APPEND = 512;
1799
- const MAX_INDICES_PER_APPEND = Scene.STREAMING_FRAGMENT_MAX_INDICES;
1800
- const start = performance.now();
1801
- let processed = 0;
1802
- while (this.meshQueueReadIndex < this.meshQueue.length && processed < MAX_MESHES_PER_FLUSH) {
1803
- // Yield once the budget is spent (after at least one append) so the main
1804
- // thread returns to the worker-message pump and the watchdog never trips.
1805
- if (processed > 0 && performance.now() - start >= budgetMs) {
1806
- break;
1807
- }
1808
- const hardEnd = Math.min(this.meshQueue.length, this.meshQueueReadIndex + MESHES_PER_APPEND, this.meshQueueReadIndex + (MAX_MESHES_PER_FLUSH - processed));
1809
- const chunkEnd = computeFlushChunkEnd((i) => this.meshQueue[i].indices.length, this.meshQueueReadIndex, hardEnd, MAX_INDICES_PER_APPEND);
1810
- // Defensive, not reachable today: chunkEnd is provably > meshQueueReadIndex
1811
- // here because hardEnd is provably > meshQueueReadIndex whenever this outer
1812
- // loop iterates, via three invariants that hold simultaneously above:
1813
- // (1) this.meshQueue.length > this.meshQueueReadIndex -- the outer while
1814
- // condition that got us into this iteration;
1815
- // (2) this.meshQueueReadIndex + MESHES_PER_APPEND, and MESHES_PER_APPEND
1816
- // (512) is a positive constant;
1817
- // (3) this.meshQueueReadIndex + (MAX_MESHES_PER_FLUSH - processed), and
1818
- // processed < MAX_MESHES_PER_FLUSH -- the other half of the outer
1819
- // while condition -- so that term is >= readIndex + 1 too.
1820
- // hardEnd is the min of all three, so hardEnd >= readIndex + 1, and
1821
- // computeFlushChunkEnd always advances by at least one past readIndex.
1822
- // If a future change breaks any one of those three invariants, hardEnd
1823
- // could collapse to readIndex and the loop would spin the main thread at
1824
- // 100% CPU doing zero allocation -- the exact signature that made #2379
1825
- // expensive to diagnose. This break turns that failure mode into "flush
1826
- // stops early" instead.
1827
- if (chunkEnd === this.meshQueueReadIndex)
1828
- break;
1829
- const chunk = this.meshQueue.slice(this.meshQueueReadIndex, chunkEnd);
1830
- this.meshQueueReadIndex = chunkEnd;
1746
+ flushPending(device, pipeline, budgetMs = 12, trace, deterministicUploads = false) {
1747
+ const result = flushQueuedMeshes(this.meshQueue, this.meshQueueReadIndex, (chunk, nextReadIndex) => {
1748
+ this.meshQueueReadIndex = nextReadIndex;
1831
1749
  this.appendToBatches(chunk, device, pipeline, true);
1832
- processed += chunk.length;
1833
- }
1834
- if (this.meshQueueReadIndex >= this.meshQueue.length) {
1835
- this.meshQueue.length = 0;
1836
- this.meshQueueReadIndex = 0;
1837
- }
1838
- else if (this.meshQueueReadIndex >= 8192 && this.meshQueueReadIndex * 2 >= this.meshQueue.length) {
1839
- this.meshQueue = this.meshQueue.slice(this.meshQueueReadIndex);
1840
- this.meshQueueReadIndex = 0;
1841
- }
1842
- return processed > 0;
1750
+ }, { budgetMs, trace, deterministicUploads, maxIndicesPerAppend: Scene.STREAMING_FRAGMENT_MAX_INDICES });
1751
+ this.meshQueue = result.queue;
1752
+ this.meshQueueReadIndex = result.readIndex;
1753
+ return result.processed > 0;
1843
1754
  }
1844
1755
  /**
1845
1756
  * Create lightweight fragment batches from a single streaming batch.
@@ -1925,6 +1836,7 @@ export class Scene {
1925
1836
  // Save references to old fragments/batches — keep them rendering
1926
1837
  // until the new proper batches are fully built (no visual gap).
1927
1838
  const oldFragments = this.streamingFragments;
1839
+ perfTally('render.finalize', oldFragments.length, 'fragments'); // #6957 fragment rebuilds
1928
1840
  const oldBatches = this.batchedMeshes;
1929
1841
  const fragmentSet = new Set(oldFragments);
1930
1842
  const oldBatchSet = new Set(oldBatches);
@@ -1962,6 +1874,9 @@ export class Scene {
1962
1874
  // already retired by rebuildPendingBatches.
1963
1875
  this.retireFinalizedBatches(oldFragments);
1964
1876
  this.retireFinalizedBatches(regroup?.retired ?? []);
1877
+ // #6957: same batch tally as the time-sliced path (only reached when the rebuild succeeded).
1878
+ if (perfCounters.enabled)
1879
+ perfCount('render.finalize.batches', this.batchedMeshes.filter((b) => !oldBatchSet.has(b)).length);
1965
1880
  }
1966
1881
  /**
1967
1882
  * Time-sliced version of finalizeStreaming.
@@ -1972,15 +1887,21 @@ export class Scene {
1972
1887
  * @param device GPU device
1973
1888
  * @param pipeline Render pipeline
1974
1889
  * @param budgetMs Max milliseconds per chunk (default 8 — half a 60fps frame)
1890
+ * @param trace The load's trace (#6979): `scene.finalize` from this call
1891
+ * until the promise settles, with `scene.finalize.regroup` under it
1975
1892
  * @returns Promise that resolves when all batches are rebuilt
1976
1893
  */
1977
- finalizeStreamingAsync(device, pipeline, budgetMs = 8) {
1894
+ finalizeStreamingAsync(device, pipeline, budgetMs = 8, trace) {
1895
+ const span = trace?.enabled ? trace.begin('scene.finalize', { fragments: this.streamingFragments.length, ephemeral: this.ephemeralStreamingMode }) : -1;
1978
1896
  if (this.ephemeralStreamingMode) {
1979
1897
  this.finishEphemeralStreaming();
1898
+ trace?.end(span);
1980
1899
  return Promise.resolve();
1981
1900
  }
1982
- if (this.streamingFragments.length === 0)
1901
+ if (this.streamingFragments.length === 0) {
1902
+ trace?.end(span);
1983
1903
  return Promise.resolve();
1904
+ }
1984
1905
  // Mark the rebuild as in-flight: the preamble empties streamingFragments
1985
1906
  // synchronously, so settle-sensitive consumers need this flag until the
1986
1907
  // time-sliced rebuild swaps the new batch array in.
@@ -1989,6 +1910,7 @@ export class Scene {
1989
1910
  // eslint-disable-next-line @typescript-eslint/no-this-alias
1990
1911
  const scene = this;
1991
1912
  const oldFragments = this.streamingFragments;
1913
+ perfTally('render.finalize', oldFragments.length, 'fragments'); // #6957 fragment rebuilds
1992
1914
  const oldBatches = this.batchedMeshes;
1993
1915
  const fragmentSet = new Set(oldFragments);
1994
1916
  const oldBatchSet = new Set(oldBatches);
@@ -2010,6 +1932,7 @@ export class Scene {
2010
1932
  let regroup = null;
2011
1933
  let pendingKeys = [];
2012
1934
  let keyIdx = 0;
1935
+ let chunks = 0;
2013
1936
  function rollback() {
2014
1937
  for (const { bucket, previous, previousFrameOrigin, batch } of createdOwned) {
2015
1938
  // Repair the owner BEFORE the free, so no bucket is ever observable
@@ -2031,6 +1954,7 @@ export class Scene {
2031
1954
  function processChunk() {
2032
1955
  try {
2033
1956
  const chunkStart = performance.now();
1957
+ chunks++;
2034
1958
  while (keyIdx < pendingKeys.length) {
2035
1959
  const key = pendingKeys[keyIdx++];
2036
1960
  const bucket = scene.buckets.get(key);
@@ -2070,23 +1994,30 @@ export class Scene {
2070
1994
  ...scene.streamingFragments,
2071
1995
  ];
2072
1996
  scene.retireFinalizedBatches(retired);
1997
+ perfCount('render.finalize.batches', createdOwned.length);
2073
1998
  scene.finalizeInProgress = false;
1999
+ trace?.end(span, { batches: createdOwned.length, chunks });
2074
2000
  resolve();
2075
2001
  }
2076
2002
  catch (err) {
2077
2003
  rollback();
2004
+ trace?.end(span, { error: true });
2078
2005
  reject(err);
2079
2006
  }
2080
2007
  }
2081
2008
  try {
2082
2009
  // --- Synchronous preamble: re-group the streamed meshes only ---
2083
2010
  scene.streamingFragments = [];
2011
+ const regroupStart = performance.now();
2084
2012
  regroup = scene.regroupStreamed();
2013
+ if (trace?.enabled)
2014
+ trace.record('scene.finalize.regroup', regroupStart, performance.now(), undefined, span);
2085
2015
  pendingKeys = Array.from(scene.pendingBatchKeys);
2086
2016
  scene.pendingBatchKeys.clear();
2087
2017
  }
2088
2018
  catch (err) {
2089
2019
  rollback();
2020
+ trace?.end(span, { error: true });
2090
2021
  reject(err);
2091
2022
  return;
2092
2023
  }