@ifc-lite/renderer 3.1.0 → 3.2.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/README.md +22 -0
- package/dist/appearance-preview.d.ts +2 -0
- package/dist/appearance-preview.d.ts.map +1 -1
- package/dist/appearance-preview.js +4 -0
- package/dist/appearance-preview.js.map +1 -1
- package/dist/camera-animation-immediate.d.ts +5 -0
- package/dist/camera-animation-immediate.d.ts.map +1 -0
- package/dist/camera-animation-immediate.js +16 -0
- package/dist/camera-animation-immediate.js.map +1 -0
- package/dist/camera-animation.d.ts +3 -9
- package/dist/camera-animation.d.ts.map +1 -1
- package/dist/camera-animation.js +14 -9
- package/dist/camera-animation.js.map +1 -1
- package/dist/device-recovery.d.ts +60 -0
- package/dist/device-recovery.d.ts.map +1 -0
- package/dist/device-recovery.js +153 -0
- package/dist/device-recovery.js.map +1 -0
- package/dist/device.d.ts +2 -0
- package/dist/device.d.ts.map +1 -1
- package/dist/device.js +12 -0
- package/dist/device.js.map +1 -1
- package/dist/gpu-upload-guard.d.ts +106 -0
- package/dist/gpu-upload-guard.d.ts.map +1 -0
- package/dist/gpu-upload-guard.js +102 -0
- package/dist/gpu-upload-guard.js.map +1 -0
- package/dist/index.d.ts +43 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +155 -139
- package/dist/index.js.map +1 -1
- package/dist/instanced-override-color.d.ts +8 -0
- package/dist/instanced-override-color.d.ts.map +1 -1
- package/dist/instanced-override-color.js +10 -0
- package/dist/instanced-override-color.js.map +1 -1
- package/dist/merged-mesh-extract.d.ts.map +1 -1
- package/dist/merged-mesh-extract.js +5 -0
- package/dist/merged-mesh-extract.js.map +1 -1
- package/dist/model-translation.d.ts +39 -1
- package/dist/model-translation.d.ts.map +1 -1
- package/dist/model-translation.js +103 -16
- package/dist/model-translation.js.map +1 -1
- package/dist/picking-manager.d.ts +2 -1
- package/dist/picking-manager.d.ts.map +1 -1
- package/dist/picking-manager.js +11 -2
- package/dist/picking-manager.js.map +1 -1
- package/dist/reference-images.d.ts +2 -0
- package/dist/reference-images.d.ts.map +1 -1
- package/dist/reference-images.js +2 -0
- package/dist/reference-images.js.map +1 -1
- package/dist/renderer-overlays.d.ts +3 -0
- package/dist/renderer-overlays.d.ts.map +1 -1
- package/dist/renderer-overlays.js +13 -0
- package/dist/renderer-overlays.js.map +1 -1
- package/dist/renderer-symbolic-overlays.d.ts +1 -0
- package/dist/renderer-symbolic-overlays.d.ts.map +1 -1
- package/dist/renderer-symbolic-overlays.js +3 -0
- package/dist/renderer-symbolic-overlays.js.map +1 -1
- package/dist/scene-appearance-preview.d.ts +2 -0
- package/dist/scene-appearance-preview.d.ts.map +1 -1
- package/dist/scene-appearance-preview.js +9 -0
- package/dist/scene-appearance-preview.js.map +1 -1
- package/dist/scene-authored-owner.d.ts +10 -0
- package/dist/scene-authored-owner.d.ts.map +1 -1
- package/dist/scene-authored-owner.js +44 -5
- package/dist/scene-authored-owner.js.map +1 -1
- package/dist/scene-batch-rebuild.d.ts +18 -0
- package/dist/scene-batch-rebuild.d.ts.map +1 -0
- package/dist/scene-batch-rebuild.js +49 -0
- package/dist/scene-batch-rebuild.js.map +1 -0
- package/dist/scene-batch-upload.d.ts +4 -1
- package/dist/scene-batch-upload.d.ts.map +1 -1
- package/dist/scene-batch-upload.js +17 -0
- package/dist/scene-batch-upload.js.map +1 -1
- package/dist/scene-bucket-routing.d.ts +21 -0
- package/dist/scene-bucket-routing.d.ts.map +1 -0
- package/dist/scene-bucket-routing.js +44 -0
- package/dist/scene-bucket-routing.js.map +1 -0
- package/dist/scene-contents.d.ts +8 -0
- package/dist/scene-contents.d.ts.map +1 -1
- package/dist/scene-derived-mesh-provenance.d.ts +13 -0
- package/dist/scene-derived-mesh-provenance.d.ts.map +1 -0
- package/dist/scene-derived-mesh-provenance.js +30 -0
- package/dist/scene-derived-mesh-provenance.js.map +1 -0
- package/dist/scene-device-recovery.d.ts +103 -0
- package/dist/scene-device-recovery.d.ts.map +1 -0
- package/dist/scene-device-recovery.js +299 -0
- package/dist/scene-device-recovery.js.map +1 -0
- package/dist/scene-geometry.d.ts +1 -0
- package/dist/scene-geometry.d.ts.map +1 -1
- package/dist/scene-geometry.js +3 -23
- package/dist/scene-geometry.js.map +1 -1
- package/dist/scene-model-translation.d.ts +11 -1
- package/dist/scene-model-translation.d.ts.map +1 -1
- package/dist/scene-model-translation.js +22 -0
- package/dist/scene-model-translation.js.map +1 -1
- package/dist/scene-precision.d.ts +16 -0
- package/dist/scene-precision.d.ts.map +1 -0
- package/dist/scene-precision.js +73 -0
- package/dist/scene-precision.js.map +1 -0
- package/dist/scene.d.ts +25 -20
- package/dist/scene.d.ts.map +1 -1
- package/dist/scene.js +149 -137
- package/dist/scene.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -106,44 +106,10 @@ import { PartialBatchRequests } from './partial-batch-requests.js';
|
|
|
106
106
|
import { colorSaltByte, packEntityLane } from './scene-geometry.js';
|
|
107
107
|
import { PointCloudRenderer } from './pointcloud/point-cloud-renderer.js';
|
|
108
108
|
import { DeviationComputer } from './deviation/deviation-computer.js';
|
|
109
|
+
import { runGuardedGpuUpload, isDeviceLossThrow } from './gpu-upload-guard.js';
|
|
110
|
+
import { recoverRendererDevice, rendererDeviceLostError } from './device-recovery.js';
|
|
109
111
|
const MAX_ENCODED_ENTITY_ID = 0xFFFFFF;
|
|
110
112
|
let warnedEntityIdRange = false;
|
|
111
|
-
/**
|
|
112
|
-
* Is this throw the GPU device telling us it is gone?
|
|
113
|
-
*
|
|
114
|
-
* The discriminator is the exception TYPE, not its message, because WebGPU
|
|
115
|
-
* draws exactly that line:
|
|
116
|
-
* - a call on a dead / invalid-state device throws a `DOMException`
|
|
117
|
-
* (`InvalidStateError` in Safari 26.5 — the whole of issue #2229);
|
|
118
|
-
* - a buffer allocation the host cannot back throws a plain `RangeError`
|
|
119
|
-
* ("createBuffer failed, size (…) is too large … when mappedAtCreation ==
|
|
120
|
-
* true"), which `gpu-upload-guard` documents happening on a HEALTHY device
|
|
121
|
-
* under memory pressure.
|
|
122
|
-
*
|
|
123
|
-
* Treating the second as a device loss is a false positive that costs the whole
|
|
124
|
-
* session, so only the first latches; everything else degrades one frame.
|
|
125
|
-
*
|
|
126
|
-
* There is deliberately NO consecutive-failure threshold as a middle ground.
|
|
127
|
-
* Not because failures necessarily arrive back-to-back — between two BCF / IDS
|
|
128
|
-
* capture frames the awaited `camera.frameBounds` normally does let an ordinary
|
|
129
|
-
* rAF frame through, which would reset a counter — but because those ordinary
|
|
130
|
-
* frames are not guaranteed to SUCCEED: they allocate too (`ensureMeshResources`
|
|
131
|
-
* creates a buffer per unresourced mesh, and the queued-mesh flush allocates),
|
|
132
|
-
* so under sustained host memory pressure any finite budget is still reachable.
|
|
133
|
-
* A latch whose safety depends on incidental animation timing is the wrong
|
|
134
|
-
* shape of guarantee for "never kill the viewport by mistake".
|
|
135
|
-
*
|
|
136
|
-
* Real losses on browsers that do not throw are still caught by the async
|
|
137
|
-
* `device.lost` promise — which the WebGPU spec makes the sole loss channel
|
|
138
|
-
* anyway (on a conformant engine, calls against a lost device are no-ops, not
|
|
139
|
-
* throws; Safari 26.5's synchronous throw is the deviation being handled here).
|
|
140
|
-
*
|
|
141
|
-
* `typeof` guarded because non-DOM hosts (Node before 17, some workers) have no
|
|
142
|
-
* `DOMException` global; there, no throw can be a WebGPU device signal anyway.
|
|
143
|
-
*/
|
|
144
|
-
function isDeviceLossThrow(error) {
|
|
145
|
-
return typeof DOMException !== 'undefined' && error instanceof DOMException;
|
|
146
|
-
}
|
|
147
113
|
/**
|
|
148
114
|
* The reason `whenReady()` rejects when the renderer is destroyed.
|
|
149
115
|
*
|
|
@@ -157,20 +123,6 @@ function rendererDestroyedError() {
|
|
|
157
123
|
error.name = 'RendererDestroyedError';
|
|
158
124
|
return error;
|
|
159
125
|
}
|
|
160
|
-
/**
|
|
161
|
-
* The reason `whenReady()` rejects once the GPU device has been lost.
|
|
162
|
-
*
|
|
163
|
-
* Deliberately NOT `RendererDestroyedError`: a destroyed renderer is finished,
|
|
164
|
-
* while a lost one is dead only until the host re-initialises it (`init()`
|
|
165
|
-
* clears the latch and readiness is published again). A caller that wants to
|
|
166
|
-
* retry needs to tell those apart, so the loss carries its own `name` — same
|
|
167
|
-
* plain-`Error` shape, for the same reasons.
|
|
168
|
-
*/
|
|
169
|
-
function rendererDeviceLostError() {
|
|
170
|
-
const error = new Error('GPU device was lost before the renderer became ready');
|
|
171
|
-
error.name = 'RendererDeviceLostError';
|
|
172
|
-
return error;
|
|
173
|
-
}
|
|
174
126
|
/**
|
|
175
127
|
* Main renderer class
|
|
176
128
|
*/
|
|
@@ -219,8 +171,9 @@ export class Renderer {
|
|
|
219
171
|
highQuality: true,
|
|
220
172
|
};
|
|
221
173
|
pointCloudRenderer = null;
|
|
222
|
-
|
|
223
|
-
|
|
174
|
+
pointCloudStreamEpoch = 0;
|
|
175
|
+
pointCloudStreamEpochs = new Map(); // handle id → epoch; cleanup rebuilds `{ id }`, cleared per epoch
|
|
176
|
+
/** Set true at the end of the LATEST `init()`; gates `whenReady()` and
|
|
224
177
|
* `isReady()`. Revoked synchronously by `init()` and by `destroy()`, and
|
|
225
178
|
* overridden (not cleared) by a device loss — see `deviceLost`, which the
|
|
226
179
|
* two readiness methods consult alongside this flag because the loss can
|
|
@@ -242,19 +195,18 @@ export class Renderer {
|
|
|
242
195
|
*/
|
|
243
196
|
destroyed = false;
|
|
244
197
|
/**
|
|
245
|
-
* The tail of the `init()`
|
|
246
|
-
* running immediately, so
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
* caller) so one failure does not deadlock every later call.
|
|
198
|
+
* The tail of the GPU lifecycle queue. `init()` and `recoverDevice()` chain
|
|
199
|
+
* onto this rather than running immediately, so overlapping calls cannot
|
|
200
|
+
* both mutate the shared device/pipeline fields while one is awaiting its
|
|
201
|
+
* adapter. Always settled fulfilled — a rejected operation is swallowed
|
|
202
|
+
* HERE (never for its caller) so one failure cannot deadlock later work.
|
|
251
203
|
*/
|
|
252
204
|
initChain = Promise.resolve();
|
|
253
205
|
/**
|
|
254
|
-
* Incremented synchronously by every `init()`
|
|
255
|
-
* `destroy()`. It stamps
|
|
256
|
-
*
|
|
257
|
-
*
|
|
206
|
+
* Incremented synchronously by every `init()` / `recoverDevice()` call and
|
|
207
|
+
* by every public `destroy()`. It stamps the lifecycle event an in-flight
|
|
208
|
+
* operation belongs to: stale work must neither allocate nor publish
|
|
209
|
+
* readiness.
|
|
258
210
|
*
|
|
259
211
|
* Both bumps are load-bearing, for the same reason. Because the queue above
|
|
260
212
|
* defers the body, an init can finish while a later one is still waiting its
|
|
@@ -312,6 +264,10 @@ export class Renderer {
|
|
|
312
264
|
/** Retained so a listener registered AFTER the loss still learns of it. */
|
|
313
265
|
deviceLostInfo = null;
|
|
314
266
|
deviceLostListeners = new Set();
|
|
267
|
+
deviceLossSequence = 0;
|
|
268
|
+
recovery = {
|
|
269
|
+
inFlight: null, lostReferenceImages: false, quantizedBatchesRequested: false, omissions: new Set(), pointCloudOptions: null,
|
|
270
|
+
};
|
|
315
271
|
/** BIM ↔ scan deviation: owns the compute pipeline + its BVH cache. */
|
|
316
272
|
deviationComputer = new DeviationComputer();
|
|
317
273
|
visualEnhancementResolver = new VisualEnhancementResolver();
|
|
@@ -486,7 +442,7 @@ export class Renderer {
|
|
|
486
442
|
this.initChain = run.then(() => undefined, () => undefined);
|
|
487
443
|
return run;
|
|
488
444
|
}
|
|
489
|
-
async initOnce(generation) {
|
|
445
|
+
async initOnce(generation, options = {}) {
|
|
490
446
|
// `pipeline` is the marker for "a previous init() completed": it is
|
|
491
447
|
// assigned unconditionally there and nulled by destroy().
|
|
492
448
|
if (this.pipeline !== null) {
|
|
@@ -504,12 +460,17 @@ export class Renderer {
|
|
|
504
460
|
// the generation and this body running is stamped with the generation
|
|
505
461
|
// that is clearing it. `deviceLostGeneration` deliberately keeps its
|
|
506
462
|
// stale value — it is only ever read alongside this flag.
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
//
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
463
|
+
if (options.clearDeviceLost !== false)
|
|
464
|
+
this.deviceLost = false;
|
|
465
|
+
// Subscribe before init so a loss during the first frames is not missed.
|
|
466
|
+
const initializingDevice = this.device;
|
|
467
|
+
initializingDevice.onDeviceLost((info) => {
|
|
468
|
+
// Ignore a delayed loss from a wrapper recovery already replaced.
|
|
469
|
+
if (this.device !== initializingDevice)
|
|
470
|
+
return;
|
|
471
|
+
this.handleDeviceLost(info, { fromDevicePromise: true });
|
|
472
|
+
});
|
|
473
|
+
await initializingDevice.init(this.canvas);
|
|
513
474
|
// A `destroy()` (or a newer `init()`) landed while we were parked on the
|
|
514
475
|
// device. Everything below allocates a full GPU stack — two pipelines,
|
|
515
476
|
// the picker, the post-processor, the point-cloud and deviation
|
|
@@ -587,7 +548,8 @@ export class Renderer {
|
|
|
587
548
|
// from the GPU click-pick provider above, since raycastScene* is
|
|
588
549
|
// synchronous CPU code while pick() is an async GPU readback.
|
|
589
550
|
this.raycastEngine.setPointCloudProvider(() => this.pointCloudRenderer?.getRayQuerySources() ?? []);
|
|
590
|
-
|
|
551
|
+
if (options.publishReady !== false)
|
|
552
|
+
this.markReady(generation);
|
|
591
553
|
}
|
|
592
554
|
/**
|
|
593
555
|
* Resolves once `init()` has finished and the GPU device + point-cloud
|
|
@@ -719,10 +681,33 @@ export class Renderer {
|
|
|
719
681
|
isDeviceLost() {
|
|
720
682
|
return this.deviceLost;
|
|
721
683
|
}
|
|
722
|
-
|
|
723
|
-
|
|
684
|
+
recoverDevice() {
|
|
685
|
+
return recoverRendererDevice(this);
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* `onLossDetected` for every `runGuardedGpuUpload` call below (#4885):
|
|
689
|
+
* a synchronous Safari-style `DOMException` (issue #2229) caught OUTSIDE
|
|
690
|
+
* `render()` never otherwise reaches `handleDeviceLost` — only the async
|
|
691
|
+
* `device.lost` promise and `render()`'s own catch do — so without this,
|
|
692
|
+
* `isDeviceLost()` would keep answering false and every later upload on
|
|
693
|
+
* this path would keep failing against the same dead device.
|
|
694
|
+
*/
|
|
695
|
+
reportUploadLoss(error) {
|
|
696
|
+
this.handleDeviceLost({
|
|
697
|
+
message: error instanceof Error ? error.message : String(error),
|
|
698
|
+
reason: 'gpu-upload-exception',
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
handleDeviceLost(info, options = {}) {
|
|
702
|
+
// Latched: only the current device's `device.lost` signal is a replacement loss; anything else is a duplicate report.
|
|
703
|
+
if (this.deviceLost) {
|
|
704
|
+
if (options.fromDevicePromise)
|
|
705
|
+
this.deviceLossSequence++;
|
|
724
706
|
return;
|
|
707
|
+
}
|
|
708
|
+
this.deviceLossSequence++;
|
|
725
709
|
this.deviceLost = true;
|
|
710
|
+
this.recovery.lostReferenceImages = this.referenceImages.hasImages();
|
|
726
711
|
this.referenceImages.destroy();
|
|
727
712
|
this.deviceLostGeneration = this.initGeneration;
|
|
728
713
|
this.deviceLostInfo = info;
|
|
@@ -894,24 +879,34 @@ export class Renderer {
|
|
|
894
879
|
* chunks will arrive (currently a no-op but kept for symmetry).
|
|
895
880
|
*/
|
|
896
881
|
beginPointCloudStream(meta) {
|
|
882
|
+
if (this.deviceLost)
|
|
883
|
+
throw rendererDeviceLostError();
|
|
897
884
|
if (!this.pointCloudRenderer) {
|
|
898
885
|
throw new Error('Renderer not initialized. Call init() first.');
|
|
899
886
|
}
|
|
900
|
-
|
|
887
|
+
const handle = this.pointCloudRenderer.beginAsset(meta);
|
|
888
|
+
this.pointCloudStreamEpochs.set(handle.id, this.pointCloudStreamEpoch);
|
|
889
|
+
return handle;
|
|
890
|
+
}
|
|
891
|
+
currentPointCloudStreamRenderer(handle) {
|
|
892
|
+
if (this.deviceLost || !this.pointCloudRenderer
|
|
893
|
+
|| this.pointCloudStreamEpochs.get(handle.id) !== this.pointCloudStreamEpoch)
|
|
894
|
+
throw rendererDeviceLostError();
|
|
895
|
+
return this.pointCloudRenderer;
|
|
901
896
|
}
|
|
902
897
|
appendPointCloudChunk(handle, chunk) {
|
|
903
|
-
|
|
904
|
-
return;
|
|
905
|
-
this.pointCloudRenderer.appendChunk(handle, chunk);
|
|
898
|
+
this.currentPointCloudStreamRenderer(handle).appendChunk(handle, chunk);
|
|
906
899
|
this.modelBoundsTracker.expandForPointClouds();
|
|
907
900
|
this.camera.setSceneBounds(this.modelBounds);
|
|
908
901
|
this.requestRender();
|
|
909
902
|
}
|
|
910
903
|
endPointCloudStream(handle) {
|
|
911
|
-
this.
|
|
904
|
+
this.currentPointCloudStreamRenderer(handle).endAsset(handle);
|
|
912
905
|
this.requestRender();
|
|
913
906
|
}
|
|
914
907
|
removePointCloudAsset(handle) {
|
|
908
|
+
if (!this.pointCloudStreamEpochs.delete(handle.id))
|
|
909
|
+
return;
|
|
915
910
|
this.pointCloudRenderer?.removeAsset(handle);
|
|
916
911
|
// Bounds may have shrunk — recompute from scratch so fit-to-view
|
|
917
912
|
// and section-plane sliders see fresh extents.
|
|
@@ -948,6 +943,15 @@ export class Renderer {
|
|
|
948
943
|
this.clearCaches();
|
|
949
944
|
this.refreshPlacementBounds();
|
|
950
945
|
}
|
|
946
|
+
/** Absolute workspace yaw about the render-frame pivot; GPU-instanced only (#4890). Skips invalidation on a no-op (#4890 review). */
|
|
947
|
+
setModelRotation(modelIndex, angle, pivot) {
|
|
948
|
+
const changed = this.scene.setModelRotation(modelIndex, angle, pivot);
|
|
949
|
+
if (!changed)
|
|
950
|
+
return false;
|
|
951
|
+
this.clearCaches();
|
|
952
|
+
this.refreshPlacementBounds();
|
|
953
|
+
return true;
|
|
954
|
+
}
|
|
951
955
|
/** Streamed clouds are addressed by durable asset handle. */
|
|
952
956
|
setPointCloudTranslation(handle, translation) {
|
|
953
957
|
this.pointCloudRenderer?.setAssetTranslation(handle, translation);
|
|
@@ -1012,22 +1016,26 @@ export class Renderer {
|
|
|
1012
1016
|
* @param geometry - Either a GeometryResult from geometry.process() or an array of MeshData
|
|
1013
1017
|
*/
|
|
1014
1018
|
loadGeometry(geometry) {
|
|
1015
|
-
if (
|
|
1019
|
+
if (this.isDeviceLost())
|
|
1020
|
+
return { ok: false, reason: 'device-lost' }; // #4885: zombie device stays "initialized"
|
|
1021
|
+
if (!this.device.isInitialized() || !this.pipeline)
|
|
1016
1022
|
throw new Error('Renderer not initialized. Call init() first.');
|
|
1017
|
-
}
|
|
1018
1023
|
const meshes = Array.isArray(geometry) ? geometry : geometry.meshes;
|
|
1019
1024
|
if (meshes.length === 0) {
|
|
1020
1025
|
console.warn('[Renderer] loadGeometry called with empty mesh array');
|
|
1021
|
-
return;
|
|
1026
|
+
return { ok: true, value: undefined };
|
|
1022
1027
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1028
|
+
const pipeline = this.pipeline;
|
|
1029
|
+
return runGuardedGpuUpload(() => this.isDeviceLost(), () => {
|
|
1030
|
+
// Use batched rendering for optimal performance
|
|
1031
|
+
const device = this.device.getDevice();
|
|
1032
|
+
this.scene.appendToBatches(meshes, device, pipeline, false);
|
|
1033
|
+
// Calculate and store model bounds for fitToView
|
|
1034
|
+
this.modelBoundsTracker.updateFromMeshes(meshes, (index) => this.scene.getModelTranslation(index));
|
|
1035
|
+
console.log(`[Renderer] Loaded ${meshes.length} meshes`);
|
|
1036
|
+
// Update camera scene bounds for tight orthographic near/far planes
|
|
1037
|
+
this.camera.setSceneBounds(this.modelBounds);
|
|
1038
|
+
}, (error) => this.reportUploadLoss(error));
|
|
1031
1039
|
}
|
|
1032
1040
|
/**
|
|
1033
1041
|
* Add multiple meshes to the scene (convenience method for streaming)
|
|
@@ -1036,17 +1044,20 @@ export class Renderer {
|
|
|
1036
1044
|
* @param isStreaming - If true, throttles batch rebuilding for better streaming performance
|
|
1037
1045
|
*/
|
|
1038
1046
|
addMeshes(meshes, isStreaming = false) {
|
|
1047
|
+
if (this.isDeviceLost())
|
|
1048
|
+
return { ok: false, reason: 'device-lost' };
|
|
1039
1049
|
if (!this.device.isInitialized() || !this.pipeline) {
|
|
1040
1050
|
throw new Error('Renderer not initialized. Call init() first.');
|
|
1041
1051
|
}
|
|
1042
1052
|
if (meshes.length === 0)
|
|
1043
|
-
return;
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1053
|
+
return { ok: true, value: undefined };
|
|
1054
|
+
const pipeline = this.pipeline;
|
|
1055
|
+
return runGuardedGpuUpload(() => this.isDeviceLost(), () => {
|
|
1056
|
+
const device = this.device.getDevice();
|
|
1057
|
+
this.scene.appendToBatches(meshes, device, pipeline, isStreaming);
|
|
1058
|
+
this.modelBoundsTracker.updateFromMeshes(meshes, (index) => this.scene.getModelTranslation(index));
|
|
1059
|
+
this.camera.setSceneBounds(this.modelBounds);
|
|
1060
|
+
}, (error) => this.reportUploadLoss(error));
|
|
1050
1061
|
}
|
|
1051
1062
|
/**
|
|
1052
1063
|
* Fit camera to view all loaded geometry
|
|
@@ -1074,50 +1085,46 @@ export class Renderer {
|
|
|
1074
1085
|
*/
|
|
1075
1086
|
addMesh(mesh) {
|
|
1076
1087
|
if (!this.pipeline)
|
|
1077
|
-
return;
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
resource: { buffer: mesh.uniformBuffer },
|
|
1093
|
-
},
|
|
1094
|
-
],
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1088
|
+
return { ok: true, value: undefined };
|
|
1089
|
+
const pipeline = this.pipeline;
|
|
1090
|
+
const outcome = (!mesh.uniformBuffer && !this.isDeviceLost() && this.device.isInitialized())
|
|
1091
|
+
? runGuardedGpuUpload(() => this.isDeviceLost(), () => {
|
|
1092
|
+
const device = this.device.getDevice();
|
|
1093
|
+
mesh.uniformBuffer = device.createBuffer({
|
|
1094
|
+
size: pipeline.getUniformBufferSize(),
|
|
1095
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1096
|
+
});
|
|
1097
|
+
mesh.bindGroup = device.createBindGroup({
|
|
1098
|
+
layout: pipeline.getBindGroupLayout(),
|
|
1099
|
+
entries: [{ binding: 0, resource: { buffer: mesh.uniformBuffer } }],
|
|
1100
|
+
});
|
|
1101
|
+
}, (error) => this.reportUploadLoss(error))
|
|
1102
|
+
: { ok: true, value: undefined };
|
|
1097
1103
|
this.scene.addMesh(mesh);
|
|
1104
|
+
return outcome;
|
|
1098
1105
|
}
|
|
1099
1106
|
/**
|
|
1100
1107
|
* Ensure all meshes have GPU resources (call after adding meshes if pipeline wasn't ready)
|
|
1101
1108
|
*/
|
|
1102
1109
|
ensureMeshResources() {
|
|
1103
1110
|
if (!this.pipeline || !this.device.isInitialized())
|
|
1104
|
-
return;
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
}
|
|
1111
|
+
return { ok: true, value: undefined };
|
|
1112
|
+
const pipeline = this.pipeline;
|
|
1113
|
+
return runGuardedGpuUpload(() => this.isDeviceLost(), () => {
|
|
1114
|
+
const device = this.device.getDevice();
|
|
1115
|
+
for (const mesh of this.scene.getMeshes()) {
|
|
1116
|
+
if (!mesh.uniformBuffer) {
|
|
1117
|
+
mesh.uniformBuffer = device.createBuffer({
|
|
1118
|
+
size: pipeline.getUniformBufferSize(),
|
|
1119
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1120
|
+
});
|
|
1121
|
+
mesh.bindGroup = device.createBindGroup({
|
|
1122
|
+
layout: pipeline.getBindGroupLayout(),
|
|
1123
|
+
entries: [{ binding: 0, resource: { buffer: mesh.uniformBuffer } }],
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1119
1126
|
}
|
|
1120
|
-
}
|
|
1127
|
+
}, (error) => this.reportUploadLoss(error));
|
|
1121
1128
|
}
|
|
1122
1129
|
/**
|
|
1123
1130
|
* Get model bounds (used for section planes, fitToView, etc.)
|
|
@@ -1210,11 +1217,15 @@ export class Renderer {
|
|
|
1210
1217
|
this.scene.recordBatchDrawn(batch);
|
|
1211
1218
|
}
|
|
1212
1219
|
}
|
|
1220
|
+
/** Guarded entry point (#4885) for the unguarded body below. */
|
|
1221
|
+
createMeshFromData(meshData) {
|
|
1222
|
+
return runGuardedGpuUpload(() => this.isDeviceLost(), () => this.createMeshFromDataUnguarded(meshData), (error) => this.reportUploadLoss(error));
|
|
1223
|
+
}
|
|
1213
1224
|
/**
|
|
1214
1225
|
* Create a GPU Mesh from MeshData (lazy creation for selection highlighting)
|
|
1215
1226
|
* This is called on-demand when a mesh is selected, avoiding 2x buffer creation during streaming
|
|
1216
1227
|
*/
|
|
1217
|
-
|
|
1228
|
+
createMeshFromDataUnguarded(meshData) {
|
|
1218
1229
|
if (!this.device.isInitialized())
|
|
1219
1230
|
return;
|
|
1220
1231
|
const device = this.device.getDevice();
|
|
@@ -1231,7 +1242,7 @@ export class Renderer {
|
|
|
1231
1242
|
// We compute the same `world` here. When there's no shared origin yet
|
|
1232
1243
|
// (legacy / pre-batch), fall back to a plain f64 fold (local + origin).
|
|
1233
1244
|
const o = meshData.origin;
|
|
1234
|
-
const so = this.scene.getSharedFrameOrigin(meshData.modelIndex);
|
|
1245
|
+
const so = this.scene.getSharedFrameOrigin(meshData.modelIndex, meshData);
|
|
1235
1246
|
const ox = o ? o[0] : 0, oy = o ? o[1] : 0, oz = o ? o[2] : 0;
|
|
1236
1247
|
const fr = Math.fround;
|
|
1237
1248
|
const sox = so ? fr(so[0]) : null, soy = so ? fr(so[1]) : 0, soz = so ? fr(so[2]) : 0;
|
|
@@ -1416,6 +1427,7 @@ export class Renderer {
|
|
|
1416
1427
|
* rejecting pipeline creation) batches stay on the f32 path.
|
|
1417
1428
|
*/
|
|
1418
1429
|
async enableQuantizedBatches() {
|
|
1430
|
+
this.recovery.quantizedBatchesRequested = true;
|
|
1419
1431
|
if (!this.pipeline)
|
|
1420
1432
|
return false;
|
|
1421
1433
|
const ok = await this.pipeline.ensureQuantizedPipelines();
|
|
@@ -3044,8 +3056,8 @@ export class Renderer {
|
|
|
3044
3056
|
}
|
|
3045
3057
|
/** Stage one new owner; borrowed mesh buffers must remain immutable until disposal. */
|
|
3046
3058
|
prepareAuthoredOwner(parts) {
|
|
3047
|
-
if (!this.device.isInitialized() || !this.pipeline)
|
|
3048
|
-
throw new Error('Renderer is not initialized.');
|
|
3059
|
+
if (this.deviceLost || !this.device.isInitialized() || !this.pipeline)
|
|
3060
|
+
throw this.deviceLost ? rendererDeviceLostError() : new Error('Renderer is not initialized.');
|
|
3049
3061
|
const prepared = this.scene.prepareAuthoredOwner(parts, this.device.getDevice(), this.pipeline);
|
|
3050
3062
|
return { commit: () => { prepared.commit(); this.refreshPlacementBounds(); this.invalidateBVHCache(); this.requestRender(); }, dispose: prepared.dispose };
|
|
3051
3063
|
}
|
|
@@ -3306,15 +3318,17 @@ export class Renderer {
|
|
|
3306
3318
|
* in-flight init. Callers: the public `destroy()` (which invalidates first)
|
|
3307
3319
|
* and `initOnce()`, tearing down the previous init before building its own.
|
|
3308
3320
|
*/
|
|
3309
|
-
teardown() {
|
|
3321
|
+
teardown(clearScene = true) {
|
|
3310
3322
|
// Nothing below survives this call, so `whenReady()` / `isReady()` must
|
|
3311
3323
|
// go back to waiting. Set first: every release below is synchronous, but
|
|
3312
3324
|
// the flag is what a caller holding a live reference actually reads.
|
|
3313
3325
|
this.ready = false;
|
|
3314
3326
|
// Scene mesh GPU buffers
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3327
|
+
if (clearScene) {
|
|
3328
|
+
this.scene.clear();
|
|
3329
|
+
// Re-arm the section-bounds diagnostic log for the next model.
|
|
3330
|
+
this._loggedSectionBounds = false;
|
|
3331
|
+
}
|
|
3318
3332
|
// Render pipelines (textures + uniform buffers)
|
|
3319
3333
|
this.pipeline?.destroy();
|
|
3320
3334
|
this.pipeline = null;
|
|
@@ -3339,6 +3353,8 @@ export class Renderer {
|
|
|
3339
3353
|
this.overlays.destroy();
|
|
3340
3354
|
this.referenceImages.destroy();
|
|
3341
3355
|
// Point cloud GPU resources
|
|
3356
|
+
this.pointCloudStreamEpoch++;
|
|
3357
|
+
this.pointCloudStreamEpochs.clear();
|
|
3342
3358
|
this.pointCloudRenderer?.clear();
|
|
3343
3359
|
this.pointCloudRenderer = null;
|
|
3344
3360
|
// BIM ↔ scan deviation pipeline + cached BVH GPU buffers.
|