@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/index.js CHANGED
@@ -11,7 +11,7 @@ export { appearanceSourceTriangle, expandAppearanceCorners, equivalentAppearance
11
11
  export { sameCompanionParts } from './appearance-companions.js';
12
12
  export { invertAppearancePartition, validateAppearancePartition } from './appearance-preview.js';
13
13
  import { createReferenceImageManager } from './reference-image-host.js';
14
- import { measureDrawingBuffer, resizeRendererViewport } from './renderer-viewport.js';
14
+ import { MAX_DRAWING_BUFFER_PIXEL_RATIO, measureDrawingBuffer, resizeRendererViewport } from './renderer-viewport.js';
15
15
  export { pickFitPolicy } from './camera-fit-policy.js';
16
16
  // `Scene` is NOT exported: an exported class republishes every method `SceneContents` above just froze.
17
17
  export { Picker } from './picker.js';
@@ -59,11 +59,6 @@ export { isEntityVisible } from './entity-visibility.js';
59
59
  // through its own glTF pipeline rather than this one (#2591).
60
60
  export { DEFAULT_GHOST_ALPHA, OPAQUE_ALPHA_CUTOFF } from './overlay-routing.js';
61
61
  export { VisibilityEpochTracker } from './visibility-epoch.js';
62
- // Frame/pass GPU timing (issue #2670 perf-verdict gate). Opt-in and NOT wired
63
- // into `Renderer` by default — a caller constructs `GpuFrameTimingRecorder`
64
- // itself and attaches its `timestampWrites` to the passes it wants measured;
65
- // see `frame-timing-gpu.ts`'s module doc for the usage pattern, and
66
- // `decideTimingMode` for choosing GPU queries vs. the CPU fallback vs. off.
67
62
  export { decideTimingMode, passDurationsMs, frameTotalMs, aggregateFrameTimings } from './frame-timing.js';
68
63
  export { computeDurationStats, nsToMs, isNegativeDelta } from './frame-timing-stats.js';
69
64
  export { GpuFrameTimingRecorder, hasTimestampQueryFeature } from './frame-timing-gpu.js';
@@ -74,6 +69,7 @@ export { PointPicker, decodePickSample } from './point-picker.js';
74
69
  export { PointCloudRenderer } from './pointcloud/point-cloud-renderer.js';
75
70
  export { PointRenderPipeline } from './pointcloud/point-pipeline.js';
76
71
  import { modelPlacementBounds, sceneMeshBounds } from './model-placement-bounds.js';
72
+ import { configureRendererGpuTiming, readRendererGpuTiming, releaseRendererGpuTiming, beginRendererGpuTiming, renderPassTimestampWrites, submitRendererGpuTiming } from './renderer-frame-timing.js';
77
73
  import { WebGPUDevice } from './device.js';
78
74
  import { RenderPipeline } from './pipeline.js';
79
75
  import { Camera } from './camera.js';
@@ -117,8 +113,9 @@ import { XRayAlpha, XRayEpochTracker } from './xray-alpha.js';
117
113
  import { PartialBatchRequests } from './partial-batch-requests.js';
118
114
  import { uploadIndividualMesh } from './individual-mesh-upload.js';
119
115
  import { PointCloudRenderer } from './pointcloud/point-cloud-renderer.js';
120
- import { appendPointCloudChunk as appendPointCloudChunkImpl, beginPointCloudStream as beginPointCloudStreamImpl, endPointCloudStream as endPointCloudStreamImpl, removePointCloudAsset as removePointCloudAssetImpl, } from './pointcloud/point-cloud-stream-lifecycle.js';
116
+ import { appendPointCloudChunk as appendPointCloudChunkImpl, beginPointCloudStream as beginPointCloudStreamImpl, endPointCloudStream as endPointCloudStreamImpl, removePointCloudAsset as removePointCloudAssetImpl, removePointCloudChunk as removePointCloudChunkImpl, } from './pointcloud/point-cloud-stream-lifecycle.js';
121
117
  import { DeviationComputer } from './deviation/deviation-computer.js';
118
+ export { computeDeviationStatistics, computeDeviationStatisticsAsync, countWithinToleranceAsync, deviationHistogramAsync, summarizeDeviationAssetsAsync } from './deviation/deviation-statistics.js';
122
119
  import { runGuardedGpuUpload, isDeviceLossThrow } from './gpu-upload-guard.js';
123
120
  import { recoverRendererDevice, rendererDeviceLostError } from './device-recovery.js';
124
121
  /**
@@ -172,6 +169,7 @@ export class Renderer {
172
169
  hoverMeshes = new HoverMeshCache();
173
170
  /** Device px per CSS px in the drawing buffer; CSS-px sizes scale by it (#5383). */
174
171
  pixelRatio = 1;
172
+ maxPixelRatio = MAX_DRAWING_BUFFER_PIXEL_RATIO;
175
173
  interactionEffects = new InteractionEffectsGovernor();
176
174
  // Procedural sky background — created lazily on the first frame that
177
175
  // enables it (most sessions never do).
@@ -510,7 +508,7 @@ export class Renderer {
510
508
  // are clamped to the GPU's max 2D texture dimension so the initial
511
509
  // pipeline allocations can't overflow on tall/wide layouts.
512
510
  const maxDim = this.device.getMaxTextureDimension();
513
- const measured = measureDrawingBuffer(this.canvas, maxDim);
511
+ const measured = measureDrawingBuffer(this.canvas, maxDim, this.maxPixelRatio);
514
512
  this.pixelRatio = measured?.pixelRatio ?? 1;
515
513
  const width = Math.max(1, Math.min(measured?.width ?? this.canvas.width, maxDim));
516
514
  const height = Math.max(1, Math.min(measured?.height ?? this.canvas.height, maxDim));
@@ -718,14 +716,14 @@ export class Renderer {
718
716
  this.deviceLossSequence++;
719
717
  this.deviceLost = true;
720
718
  cancelRendererColorFrame(this);
719
+ releaseRendererGpuTiming(this);
721
720
  this.recovery.lostReferenceImages = this.referenceImages.hasImages();
722
721
  this.referenceImages.destroy();
723
722
  this.deviceLostGeneration = this.initGeneration;
724
723
  this.deviceLostInfo = info;
725
724
  console.warn('[Renderer] GPU device lost — halting rendering until re-init:', info.message);
726
- // Readiness describes the GPU objects, and every one of them just died:
727
- // `isReady()` reports it from here on, and anyone parked in
728
- // `whenReady()` is failed rather than left to be resolved by the init
725
+ // Readiness describes dead GPU objects: `isReady()` reports the loss
726
+ // and `whenReady()` waiters fail rather than awaiting the init
729
727
  // this loss may have landed in the middle of. Done BEFORE the listeners
730
728
  // run, so a listener that recovers by calling `init()` synchronously
731
729
  // finds the waiters already settled and re-arms the wait for the next
@@ -894,8 +892,13 @@ export class Renderer {
894
892
  beginPointCloudStream(meta) {
895
893
  return beginPointCloudStreamImpl(this, meta);
896
894
  }
897
- appendPointCloudChunk(handle, chunk) {
898
- appendPointCloudChunkImpl(this, handle, chunk);
895
+ /** `key` makes the chunk removable on its own via `removePointCloudChunk` (one COPC node, #6869). */
896
+ appendPointCloudChunk(handle, chunk, key) {
897
+ appendPointCloudChunkImpl(this, handle, chunk, key);
898
+ }
899
+ /** Free the chunks appended under `key`; returns the points removed (0 for a stale handle). */
900
+ removePointCloudChunk(handle, key) {
901
+ return removePointCloudChunkImpl(this, handle, key);
899
902
  }
900
903
  endPointCloudStream(handle) {
901
904
  endPointCloudStreamImpl(this, handle);
@@ -904,14 +907,13 @@ export class Renderer {
904
907
  removePointCloudAssetImpl(this, handle);
905
908
  }
906
909
  /**
907
- * Reassign a streamed point-cloud's expressId after upload. Use
908
- * this when the federation registry assigns a new model offset and
909
- * the renderer needs to emit the post-offset globalId in picking
910
- * outputs. The change takes effect on the next render — no GPU
911
- * buffer rewrite needed.
910
+ * Bind a streamed point cloud to its federation identity after upload:
911
+ * the post-offset globalId picking and deviation readback report, and,
912
+ * when given, the model index they attribute the asset to (#6887). Takes
913
+ * effect on the next render — no GPU buffer rewrite needed.
912
914
  */
913
- relabelPointCloudAsset(handle, newExpressId) {
914
- this.pointCloudRenderer?.relabelAsset(handle, newExpressId);
915
+ relabelPointCloudAsset(handle, newExpressId, modelIndex) {
916
+ this.pointCloudRenderer?.relabelAsset(handle, newExpressId, modelIndex);
915
917
  this.requestRender();
916
918
  }
917
919
  /** Bounds across flat draw batches. */
@@ -964,6 +966,10 @@ export class Renderer {
964
966
  getPointCloudTransform(handle) {
965
967
  return this.pointCloudRenderer?.getAssetTransform(handle);
966
968
  }
969
+ /** Exact float64 placement copy for CPU math; the f32 transform rounds map-grid origins (#6894). */
970
+ getPointCloudPlacement(handle) {
971
+ return this.pointCloudRenderer?.getAssetPlacement(handle);
972
+ }
967
973
  /**
968
974
  * Set/clear a streamed cloud's column-major model matrix (16 floats) for
969
975
  * IfcMapConversion alignment (#1804). Applied on the next frame's uniform
@@ -981,22 +987,17 @@ export class Renderer {
981
987
  * `deviation/deviation-computer.ts` for the full contract.
982
988
  */
983
989
  async computeDeviations(opts = {}) {
984
- return this.deviationComputer.compute(opts, {
985
- device: this.device,
986
- scene: this.scene,
987
- pointCloudRenderer: this.pointCloudRenderer,
988
- requestRender: () => this.requestRender(),
989
- });
990
+ return this.deviationComputer.compute(opts, this.deviationContext());
990
991
  }
991
992
  /** Read per-scan-asset signed-distance statistics after a completed run. */
992
993
  async readDeviationAssetStats() {
993
- return this.deviationComputer.readAssetStats({
994
- device: this.device,
995
- scene: this.scene,
996
- pointCloudRenderer: this.pointCloudRenderer,
997
- requestRender: () => this.requestRender(),
998
- });
994
+ return this.deviationComputer.readAssetStats(this.deviationContext());
995
+ }
996
+ /** Read every computed point's signed distance (4 B/point) for summary statistics (#6872). */
997
+ async readDeviationDistances() {
998
+ return this.deviationComputer.readDistances(this.deviationContext());
999
999
  }
1000
+ deviationContext = () => ({ device: this.device, scene: this.scene, pointCloudRenderer: this.pointCloudRenderer, requestRender: () => this.requestRender() });
1000
1001
  /**
1001
1002
  * Toggle Eye-Dome Lighting and tune its strength.
1002
1003
  *
@@ -1271,9 +1272,6 @@ export class Renderer {
1271
1272
  }
1272
1273
  });
1273
1274
  }
1274
- /**
1275
- * Render frame
1276
- */
1277
1275
  /** Get diagnostic info for mobile debugging */
1278
1276
  getDiagnostics() {
1279
1277
  const pos = this.camera.getPosition();
@@ -1294,15 +1292,15 @@ export class Renderer {
1294
1292
  bounds: b ? `${b.min.x.toFixed(0)}..${b.max.x.toFixed(0)} ${b.min.y.toFixed(0)}..${b.max.y.toFixed(0)} ${b.min.z.toFixed(0)}..${b.max.z.toFixed(0)}` : 'none',
1295
1293
  };
1296
1294
  }
1295
+ setGpuFrameTiming(enabled) { configureRendererGpuTiming(this, enabled); }
1296
+ getGpuFrameTiming() { return readRendererGpuTiming(this); }
1297
1297
  /**
1298
1298
  * Statistics of the last COMPLETED render() call (draw calls issued,
1299
1299
  * batches drawn / frustum-culled / contribution-culled), or null before
1300
1300
  * the first frame. Pair with `getScene().getResidentGpuBytes()` for the
1301
1301
  * load-complete telemetry snapshot (issue #1682 observability).
1302
1302
  */
1303
- getFrameStats() {
1304
- return this._lastFrameStats;
1305
- }
1303
+ getFrameStats() { return this._lastFrameStats; }
1306
1304
  /**
1307
1305
  * Vendor/architecture identity of the GPU adapter, snapshotted during
1308
1306
  * `init()` (issue #2624 device-loss telemetry), or null when the runtime
@@ -1327,8 +1325,10 @@ export class Renderer {
1327
1325
  this.scene.setQuantizedBatches(true);
1328
1326
  return ok;
1329
1327
  }
1328
+ /** Draw a frame through the shared submission path, preserving the void API. */
1329
+ render(options = {}) { this.renderWithResult(options); }
1330
1330
  /**
1331
- * Draw one frame.
1331
+ * Draw one frame; true only when it submits without a contained synchronous failure (#6709).
1332
1332
  *
1333
1333
  * Never throws, so callers never need to guard this call to keep their
1334
1334
  * animation loop alive.
@@ -1347,26 +1347,22 @@ export class Renderer {
1347
1347
  * carries on. Once enough such frames have degraded without recovering,
1348
1348
  * `onPersistentRenderDegradation` fires once.
1349
1349
  *
1350
- * SCOPE: `renderFrame()` has two try/catch regions — this outer one (canvas
1351
- * resize, context setup, evicted-batch restore) and an inner one opened
1352
- * after the swap-chain texture is acquired, covering encoder work through
1353
- * `submit`. Until #2417 only the outer one discriminated, so a device that
1354
- * died after `getCurrentTexture()` succeeded degraded quietly forever with
1355
- * no latch and no toast. Both now run the same policy; the encode catch
1356
- * additionally balances the frame's validation error scope before doing so.
1357
- */
1358
- render(options = {}) {
1350
+ * Frame setup and encoding through `submit` share `containFrameThrow`
1351
+ * (#2417); the encode catch also balances the validation error scope.
1352
+ * Submission is synchronous, distinct from GPU completion/validation.
1353
+ */
1354
+ renderWithResult(options = {}) {
1359
1355
  this._renderCallCount++;
1360
1356
  // A lost device leaves every pipeline/buffer dead; rendering would only
1361
1357
  // emit a stream of validation errors. Stay quiet until re-init.
1362
1358
  if (this.deviceLost) {
1363
1359
  this._renderSkipCount++;
1364
1360
  cancelRendererColorFrame(this);
1365
- return;
1361
+ return false;
1366
1362
  }
1367
1363
  try {
1368
1364
  this.frameContainedThrow = false;
1369
- this.renderFrame(options);
1365
+ const submitted = this.renderFrame(options);
1370
1366
  // Only a frame that actually got through resets the run. A frame
1371
1367
  // the ENCODE catch contained returns here normally (that catch is
1372
1368
  // inside renderFrame), so "did not throw" is not the same question
@@ -1374,6 +1370,7 @@ export class Renderer {
1374
1370
  if (!this.frameContainedThrow)
1375
1371
  this.consecutiveDegradedFrames = 0;
1376
1372
  retryRendererColorFrame(this, () => this.requestRender());
1373
+ return submitted;
1377
1374
  }
1378
1375
  catch (error) {
1379
1376
  // Safari (26.5) reports device loss SYNCHRONOUSLY: a call against a
@@ -1390,24 +1387,25 @@ export class Renderer {
1390
1387
  this._renderSkipCount++;
1391
1388
  this.containFrameThrow(error, 'frame');
1392
1389
  cancelRendererColorFrame(this);
1390
+ return false;
1393
1391
  }
1394
1392
  }
1395
1393
  /**
1396
- * The frame body. Throws on a synchronously-dead GPU device; `render()`
1397
- * owns the containment. Private for that reason — call `render()`.
1394
+ * The frame body. Throws on a synchronously-dead GPU device;
1395
+ * `renderWithResult()` owns containment for both public render methods.
1398
1396
  */
1399
1397
  renderFrame(options) {
1400
1398
  if (!this.device.isInitialized() || !this.pipeline) {
1401
1399
  this._renderSkipCount++;
1402
- return;
1400
+ return false;
1403
1401
  }
1404
1402
  // Drawing buffer = the element's device-pixel size (capped ratio, clamped
1405
1403
  // to the GPU's max texture dimension); see computeDrawingBufferSize (#5383).
1406
- const measured = measureDrawingBuffer(this.canvas, this.device.getMaxTextureDimension());
1404
+ const measured = measureDrawingBuffer(this.canvas, this.device.getMaxTextureDimension(), this.maxPixelRatio, options.maxPixelRatio);
1407
1405
  // Skip rendering while the canvas is collapsed or too small.
1408
1406
  if (!measured || measured.height < 10) {
1409
1407
  this._renderSkipCount++;
1410
- return;
1408
+ return false;
1411
1409
  }
1412
1410
  const { width, height } = measured;
1413
1411
  this.pixelRatio = measured.pixelRatio;
@@ -1425,12 +1423,12 @@ export class Renderer {
1425
1423
  // Skip rendering if canvas is invalid
1426
1424
  if (this.canvas.width === 0 || this.canvas.height === 0) {
1427
1425
  this._renderSkipCount++;
1428
- return;
1426
+ return false;
1429
1427
  }
1430
1428
  // Ensure context is valid before rendering (handles HMR, focus changes, etc.)
1431
1429
  if (!this.device.ensureContext()) {
1432
1430
  this._renderSkipCount++;
1433
- return; // Skip this frame, context will be ready next frame
1431
+ return false; // Skip this frame, context will be ready next frame
1434
1432
  }
1435
1433
  const device = this.device.getDevice();
1436
1434
  const viewProj = this.camera.getViewProjMatrix().m;
@@ -1619,7 +1617,7 @@ export class Renderer {
1619
1617
  errorScopePushed = false;
1620
1618
  this.drainErrorScope(device);
1621
1619
  }
1622
- return; // Skip this frame, context will be reconfigured next frame
1620
+ return false; // Skip this frame, context will be reconfigured next frame
1623
1621
  }
1624
1622
  let colorCapture = null;
1625
1623
  let colorReadback = null;
@@ -1762,6 +1760,7 @@ export class Renderer {
1762
1760
  }
1763
1761
  // Now record draw commands
1764
1762
  const encoder = device.createCommandEncoder();
1763
+ beginRendererGpuTiming(this, device, encoder);
1765
1764
  // Sun shadow-map pass (#2670, Phase 2). Off unless the caller opts
1766
1765
  // in; when off the hot path pays only this check and (once) an
1767
1766
  // enabled=0 reset on toggle-off. Runs BEFORE the colour pass (its
@@ -1927,6 +1926,7 @@ export class Renderer {
1927
1926
  storeOp: (needsObjectIdPass ? 'store' : 'discard'),
1928
1927
  });
1929
1928
  const pass = encoder.beginRenderPass({
1929
+ timestampWrites: renderPassTimestampWrites(encoder, 'main'),
1930
1930
  colorAttachments,
1931
1931
  depthStencilAttachment: {
1932
1932
  view: this.pipeline.getDepthTextureView(),
@@ -2306,6 +2306,10 @@ export class Renderer {
2306
2306
  // probe (enableQuantizedBatches) verified they exist, so the
2307
2307
  // base fallback here is type-safety, never taken.
2308
2308
  const pipeFor = (batch, kind) => {
2309
+ const ghost = kind === 'transparent' && xray.isGhostBatch(batch)
2310
+ ? this.pipeline.getGhostPipeline(!!batch.quantized, () => this.requestRender()) : null;
2311
+ if (ghost)
2312
+ return ghost;
2309
2313
  const base = kind === 'opaque' ? this.pipeline.getPipeline() : this.pipeline.getTransparentPipeline();
2310
2314
  if (!batch.quantized)
2311
2315
  return base;
@@ -2800,6 +2804,7 @@ export class Renderer {
2800
2804
  height: this.canvas.height,
2801
2805
  // Depth-only: depth24plus-stencil8 cannot be sampled as texture_depth_* with aspect 'all'.
2802
2806
  depthView: this.pipeline.getDepthOnlyTextureView(),
2807
+ maskDepthView: this.pipeline.getDepthTextureView(),
2803
2808
  objectIdView,
2804
2809
  projection: this.camera.getProjMatrix(),
2805
2810
  enhancement: visualEnhancement,
@@ -2810,7 +2815,7 @@ export class Renderer {
2810
2815
  selectionOutline,
2811
2816
  });
2812
2817
  colorReadback = colorCapture && encodeRendererColorFrameCapture(device, encoder, colorCapture);
2813
- device.queue.submit([encoder.finish()]);
2818
+ submitRendererGpuTiming(this, device, encoder);
2814
2819
  if (colorReadback && colorCapture) {
2815
2820
  settleRendererColorFrameCapture(this, colorCapture, colorReadback);
2816
2821
  colorCapture = null;
@@ -2837,8 +2842,10 @@ export class Renderer {
2837
2842
  errorScopePushed = false;
2838
2843
  this.drainErrorScope(device);
2839
2844
  }
2845
+ return true;
2840
2846
  }
2841
2847
  catch (error) {
2848
+ releaseRendererGpuTiming(this);
2842
2849
  discardRendererColorFrameReadback(colorReadback);
2843
2850
  // Balance the validation scope if we threw before popping it above —
2844
2851
  // an unpopped scope would capture every later frame's errors silently.
@@ -2875,6 +2882,7 @@ export class Renderer {
2875
2882
  // the discriminator keys on the TYPE and not on "a frame threw".
2876
2883
  this.containFrameThrow(error, 'encode');
2877
2884
  cancelRendererColorFrame(this);
2885
+ return false;
2878
2886
  }
2879
2887
  }
2880
2888
  /**
@@ -3007,6 +3015,8 @@ export class Renderer {
3007
3015
  resize(width, height) {
3008
3016
  resizeRendererViewport(this.canvas, this.camera, width, height);
3009
3017
  }
3018
+ /** Cap the drawing buffer's device-pixel ratio, trading sharpness for fill on HiDPI screens; see computeDrawingBufferSize. */
3019
+ setMaxPixelRatio(ratio) { this.maxPixelRatio = ratio; this.requestRender(); }
3010
3020
  /** Stage one new owner; borrowed mesh buffers must remain immutable until disposal. */
3011
3021
  prepareAuthoredOwner(parts) {
3012
3022
  if (this.deviceLost || !this.device.isInitialized() || !this.pipeline)
@@ -3286,6 +3296,7 @@ export class Renderer {
3286
3296
  // Re-arm the section-bounds diagnostic log for the next model.
3287
3297
  this._loggedSectionBounds = false;
3288
3298
  }
3299
+ releaseRendererGpuTiming(this);
3289
3300
  // Render pipelines (textures + uniform buffers)
3290
3301
  this.pipeline?.destroy();
3291
3302
  this.pipeline = null;